SIMPLECODER_SANDBOX_AUDIT.md · 9.8 KB

SimpleCoder Sandbox — Audit & Hardening

Auditor: Professor Codephreak (operating mindX as a substrate) Date: 2026-06-07 (archive primitives added 2026-06-10) Scope: agents/simple_coder_agent.py sandbox handling Deliverables: agents/simple_coder_tools.py (new boundary), tests/test_simple_coder_sandbox.py (proof-suite, 41 tests), agent wiring.

Linux philosophy: do one thing and do it well. The sandbox is now one thing —
a boundary — implemented once in simplecoder.tools and proven by tests, rather
than re-derived in every file operation.

Findings (pre-hardening)

The original sandbox was a directory convention, not an enforcement boundary.

#SeverityFinding
1HIGHAllowlist gates the binary name, not its arguments. cat /etc/passwd, rm /home/mindx/.ssh/id_rsa, cp /etc/shadow . all worked — the binaries are allowlisted and run with full filesystem access; cwd=sandbox does not confine the arguments they touch.
2HIGHInterpreters are unrestricted escapes. python/python3/pip are allowlisted and Turing-complete: python3 -c "open('/etc/passwd').read()" / sockets / arbitrary writes. pip install runs arbitrary setup.py.
3HIGHfind -exec / -delete are arbitrary-exec / unconfined-delete primitives, allowlisted via find. git -c injects config → command execution.
4HIGHFull environment leak. env = os.environ.copy() handed every secret (API keys, vault paths, MINDX_) to subprocesses → trivial exfiltration.
5MEDNo resource limits. No setrlimit — fork bombs, memory bombs, and (via communicate()) unbounded output → host memory exhaustion.
6MEDTimeout orphans the process. asyncio.wait_for raised but never killed the child; no process group, so children survived.
7MEDmax_file_size_mb declared but never enforced on read or write.
8LOWAbsolute paths silently re-rooted (/etc/passwdsandbox/etc/passwd) — surprising; hid bugs. Symlink-escape via no-follow not considered.

Net: anything that could run a subprocess could read/write anywhere the process user could, reach the network, and read every secret in the environment.


Hardening (simplecoder.tools.Sandbox)

A single class enforces the boundary. Each guarantee maps to tests in tests/test_simple_coder_sandbox.py.

GuaranteeMechanismCloses
Path containmentresolve() checks lexical + realpath; rejects absolute-outside and .. escapes8
Symlink-escape rejection_has_symlink_escape() walks components; a symlink whose real target leaves root is denied (no-follow default)8
Command allowlistcheck_argv()argv[0] basename must be allowed1
Argument-path containmentpath-like args (absolute / ~ / ..) must resolve inside the sandbox1
Escape-flag denialfind -exec/-delete, git -c; python -c etc. gated by allow_inline_code (strict mode + .strict() drops interpreters entirely)2, 3
Scrubbed envscrubbed_env() — only PATH/LANG/TERM/...; HOME→sandbox; never host secrets4
Resource limitspreexec_fn sets RLIMIT_CPU/AS/FSIZE/NOFILE/NPROC; start_new_session5
Process-group kill on timeoutos.killpg(SIGKILL) + reap6
Bounded output_communicate_capped() caps stdout+stderr, kills runaway producers5
File-size limitsread_text/write_text enforce max_file_bytes7

Archive primitives (added 2026-06-10)

The original hardening flagged extraction safety as missing. Sandbox now owns two archive operations, policy-driven via SandboxPolicy.max_archive_members (2048), max_archive_decompressed_bytes (256 MiB), max_archive_ratio (200):

OperationGuarantees
inspect_zip()Non-extracting. Reports members + flags path_traversal (absolute / .. / drive-rooted names), too_many_members, decompressed_too_large, suspicious_ratio (zip-bomb), nested_archive, corrupt_member, not_a_zip. Only a sandbox-boundary breach raises; hostile archives are reported, not crashed on.
extract_zip()Per-member containment — never ZipFile.extractall; every target is resolve()-proven inside the extraction root before any bytes are written. Running decompressed-byte tally + member-count caps; stream-copy with a hard cap so a lying header can't bomb the disk. First breach aborts and removes the partial file.

These feed the SimpleCoder audit_package operation (inspect → extract → ast-only static risk scan) used by the external-package adoption pipeline — see PACKAGE_ADOPTION.md.

Proof of absolute — the honest part

In-process checks cannot make a general-purpose interpreter absolutely contained: an interpreter you are permitted to run can open sockets and read any file the process user can read. The guarantees that are absolute in-process: path containment, symlink-escape rejection, env scrubbing, resource limits, output bounding, file-size limits.

For absolute containment of execution, the kernel must enforce it. Sandbox auto-detects and probes bwrap (bubblewrap) / nsjail; when a working backend exists, every command is wrapped so namespaces — not a Python check — draw the boundary. When the backend is missing or cannot create namespaces (e.g. already inside a sandbox → EAGAIN), Sandbox gracefully falls back to in-process defence-in-depth and reports this via info()["isolation_backend"]. The honest operational posture:

disabled with SandboxPolicy.strict() for untrusted input, or the agent run as an unprivileged user in a container.


What changed in the agent

SimpleCoderAgent now routes; the boundary enforces*:

No public method signatures changed; existing callers are unaffected.

Verification

.mindx_env/bin/python -m pytest tests/test_simple_coder_sandbox.py -q   # 41 passed

Covered: relative/absolute/.. containment, symlink escape (in & out), allowlist, argument-path escape (absolute/../~), find -exec/-delete, git -c, inline-code default vs strict, env scrubbing, read/write size limits, write-outside, run success/failure/timeout-kill/output-cap, and info() introspection.

Archive coverage (10 tests, added 2026-06-10): member listing, traversal flagging and extraction blocking (nothing escapes the sandbox), nested-archive flagging, non-zip rejection, outside-sandbox denial for both the source zip and the extraction destination, member-count cap, decompressed-size cap, happy path. Reusable per-package test templates live in simple_coder_sandbox/tests/ for future external imports.

Production deployment — bubblewrap (kernel-enforced isolation) ✅

Bubblewrap is installed and active on the prod VPS (mindx.pythai.net, 168.231.126.58). Sandbox auto-detects and probes it, so the live SimpleCoder runs every command inside a bwrap namespace.

Install / enable (run as root, once)

apt-get install -y bubblewrap          # bwrap 0.9.0 (already present on the VPS)

Ubuntu 24.04 locks down unprivileged user namespaces via AppArmor, which makes

bwrap fail for a non-root service user (mindx) at uid-map / loopback setup.

Re-enable it (runtime + persisted) so bwrap can build its namespace:

sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 cat > /etc/sysctl.d/99-mindx-bwrap.conf <<'CONF' kernel.apparmor_restrict_unprivileged_userns = 0 CONF

Verified posture on prod (as the mindx service user)

CheckResult
Sandbox.isolation_backendbwrap
allowlisted python3 -c "print(42)"SUCCESS, runs under bwrap
network egress socket.create_connection(("8.8.8.8",53))Network is unreachable--unshare-all severs the net namespace
cat /etc/passwddenied (arg-path containment; /etc not bound into the mount)
proof-suite tests/test_simple_coder_sandbox.py31 passed

This is the "proof of absolute" for execution: an allowlisted interpreter can no longer reach the network or read the host filesystem, because the kernel — not a Python check — draws the boundary.

Trade-off (documented)

Relaxing kernel.apparmor_restrict_unprivileged_userns re-opens unprivileged user namespaces host-wide (the very thing Ubuntu 24.04 locked down as a CVE mitigation). On a dedicated single-tenant mindX VPS this is an acceptable, deliberate trade to gain kernel-enforced sandboxing for the autonomous coder. On a shared host, prefer a per-binary AppArmor profile that permits userns for /usr/bin/bwrap only, instead of the global sysctl.

Residual / recommended next steps

  1. ~~Ship bwrap on the prod VPS~~ DONE — installed, AppArmor relaxed, verified isolation_backend=bwrap with network severed.
  2. For untrusted directives, default the agent policy to .strict() (no interpreters).
  3. Consider a seccomp profile for the bwrap wrap (network is already unshared via --unshare-all).
  4. On shared hosts, replace the global sysctl with a per-binary AppArmor profile for bwrap.

Referenced in this document
PACKAGE_ADOPTION

All DocumentsDocument IndexThe Book of mindXImprovement JournalAPI Reference