Skip to content

Add a real process-boundary backend (backend="process")#267

Merged
seanwevans merged 1 commit into
mainfrom
claude/process-backend-boundary
Jul 20, 2026
Merged

Add a real process-boundary backend (backend="process")#267
seanwevans merged 1 commit into
mainfrom
claude/process-backend-boundary

Conversation

@seanwevans

Copy link
Copy Markdown
Owner

Why

The sub-interpreter backend is not a security boundary against hostile Python. A textbook object-graph walk recovers an unrestricted __import__ and reaches the host, bypassing the trimmed builtins and the import allow-list:

for cls in ().__class__.__base__.__subclasses__():
    if cls.__name__ == "catch_warnings":
        real_import = cls()._module.__builtins__["__import__"]
        real_import("os").listdir("/")   # runs, no PolicyError

backend="process" was declared in the API but failed closed as unimplemented. This PR makes it real: guest code runs in a separate OS process, so that escape — and every other in-process trick — is confined to the guest process and can no longer touch the supervisor's address space. This is the first of several layers toward a genuine boundary (kernel confinement follows).

What

  • pyisolate/runtime/child.py — a fresh-interpreter guest runtime (python -m pyisolate.runtime.child <fd>). It reuses the existing import allow-list / safe-builtins guards from runtime.thread and speaks a length-framed JSON protocol over an inherited AF_UNIX socket. A fresh interpreter (not fork) means no supervisor memory or secrets are inherited.
  • pyisolate/runtime/process_backend.pyProcessSandbox, a handle that duck-types the subset of the SandboxThread surface the Sandbox wrapper delegates to (exec/call/recv/stop/kill/cancel/reap/is_alive/name/stats). Guest→host values are JSON only — the supervisor must never pickle.loads bytes produced by untrusted guest code. Not-yet-supported handle methods (checkpoint/reset/tracing) raise NotImplementedError rather than AttributeError.
  • supervisor.py"process" moves into IMPLEMENTED_BACKENDS; it routes to a parallel registry so the thread-specific watchdog / warm-pool / cgroup machinery is left untouched. Tenant-quota accounting is shared via the existing reservation helpers.
  • docs/execution-model.md — documents the implemented status and the JSON-serialization constraint. Threat-model reconciliation is intentionally deferred until the kernel-confinement layers land.

Boundary property (tested)

test_object_graph_escape_is_confined_to_the_child_process runs the escape above and asserts it reaches os only inside the child process (a different PID), never the supervisor. Other tests cover round-trip, import-allowlist denial, exception surfacing, the JSON-only constraint, call, child termination on close, list_active, and that microvm still fails closed.

Scope / follow-ups

This PR establishes the process boundary and transport only. Kernel-level confinement of the guest process is deliberately out of scope here and comes next:

  • no-new-privs + seccomp syscall filter + rlimits (verified installable in CI),
  • Landlock FS enforcement from policy (live-gated where the kernel supports it),
  • wiring the eBPF policy maps end-to-end,
  • reconciling the threat model once the boundary is enforced.

Testing

  • New tests/test_process_backend.py: 13 tests, all passing.
  • Full suite: 421 passed, 2 environment-gated skips; all pre-commit hooks (isort, black, pylint, flake8, mypy, pytest) pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DDSofWX5HwawsrwbN2nSXR


Generated by Claude Code

The sub-interpreter backend is not a boundary against hostile Python: a
textbook object-graph walk (recovering an unrestricted __import__ via
().__class__.__base__.__subclasses__()) bypasses the trimmed builtins and
the import allow-list and reaches the host. This lands the first layer of
a real boundary by running guest code in a separate OS process.

- pyisolate/runtime/child.py: fresh-interpreter guest runtime, launched as
  python -m pyisolate.runtime.child <fd>. Reuses the existing import
  allow-list / safe-builtins guards and speaks a length-framed JSON
  protocol over an inherited AF_UNIX socket.
- pyisolate/runtime/process_backend.py: ProcessSandbox handle that
  duck-types the SandboxThread surface the Sandbox wrapper delegates to.
  Guest->host values are JSON only -- the supervisor must never
  pickle.loads bytes produced by untrusted guest code. Not-yet-supported
  handle methods (checkpoint/reset/tracing) raise NotImplementedError
  rather than AttributeError.
- supervisor.py: move "process" into IMPLEMENTED_BACKENDS and route it to
  a parallel registry so the thread-specific watchdog/warm-pool/cgroup
  machinery is untouched; tenant-quota accounting is shared.

The escape that fully compromises the sub-interpreter backend still runs
in the child, but it is confined to that process and cannot touch
supervisor memory -- covered by a dedicated test. Kernel confinement of
the guest process (no-new-privs, seccomp, rlimits, Landlock, cgroups)
follows in later changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDSofWX5HwawsrwbN2nSXR
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants