From 58f67ac4b270992542c9408a3659f395dffd3c7c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Aug 2026 20:55:54 +0000 Subject: [PATCH] Describe the subinterpreter backend as what it actually is README, SECURITY.md and docs/threat-model.md all stated that guest code under backend="subinterpreter" "runs in a sub-interpreter in the supervisor's own process". It does not. pyisolate/runtime/thread.py runs each guest in a threading.Thread and execs guest source against a restricted __builtins__ mapping -- its own module docstring called itself "a greatly simplified placeholder that executes code in a dedicated thread using the standard interpreter". No security claim changes: that backend is documented throughout as an execution cell and not a boundary against hostile Python, which is equally true of a thread and of a real sub-interpreter. What changes is the mechanism a reader should assume. The difference is observable -- a thread shares sys.modules and the GIL with the supervisor, a sub-interpreter does not -- so anyone reasoning about isolation, or about the no-GIL parallel- cell axis, was working from the wrong model. Add a "Sub-interpreter status" section to the README stating the current mechanism, comparing it with the intended one, and pointing untrusted workloads at backend="process". Reference it from the three documents that made the claim and from docs/execution-model.md. Record landing the real implementation (concurrent.interpreters on 3.14, _interpreters on 3.12+), or renaming the backend, as a Now/next roadmap item. Also fix thread.py's docstring, which pointed readers at an AGENTS.md that does not exist anywhere in the repository. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012ebvMQ3vLxdK3joymz6Feg --- README.md | 41 +++++++++++++++++++++++++++++++------ ROADMAP.md | 10 ++++++++- SECURITY.md | 5 ++++- docs/execution-model.md | 6 ++++++ docs/threat-model.md | 16 +++++++++------ pyisolate/runtime/thread.py | 19 ++++++++++++----- 6 files changed, 78 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 20474b0..43e4dfa 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ PyIsolate `0.0.x` is a prototype for API, policy, broker, observability, and tes ## Features and roadmap -* **Sub-interpreter sandbox API** — available for prototype development and conformance testing. +* **Sub-interpreter sandbox API** — the API surface is available for prototype development and conformance testing. The backend currently executes guests in a dedicated thread, not a CPython sub-interpreter; see [Sub-interpreter status](#sub-interpreter-status). * **Import allow-listing and user-space quotas** — available as prototype guardrails; not a complete adversarial security boundary. * **No-GIL/free-threaded CPython support** — experimental roadmap target for CPython 3.13+ `--disable-gil` builds. * **Kernel enforcement** — experimental roadmap target; eBPF-LSM, cgroup, and verifier-backed policy enforcement are not guaranteed by the current release. @@ -210,6 +210,32 @@ Use `pyisolate.policy.refresh("policy/.yml", token="secret")` to hot‑loa --- +## Sub-interpreter status + +`backend="subinterpreter"` does **not** currently use a CPython sub-interpreter. +`pyisolate/runtime/thread.py` runs each guest in a `threading.Thread` and +`exec`s guest source against a restricted `__builtins__` mapping. The backend +carries the name of its intended implementation. + +This does not change any security claim in this repository — that backend is +documented throughout as an execution cell and *not* a boundary against hostile +Python, which is equally true of a thread and of a real sub-interpreter. What it +changes is the mechanism you should assume when reasoning about it: + +| | thread (today) | sub-interpreter (intended) | +| --- | --- | --- | +| Address space | shared with supervisor | shared with supervisor | +| `sys.modules` | shared with supervisor | per-interpreter | +| Boundary vs hostile Python | none | none | +| GIL | shared | per-interpreter on free-threaded builds | + +Landing the real implementation (`concurrent.interpreters` on 3.14, `_interpreters` +on 3.12+) is roadmap work. Until then, treat "sub-interpreter" as the name of an +API mode, not a description of the runtime, and use `backend="process"` for any +guest you do not trust. + +--- + ## Canonical execution model A cell is intentionally limited to seven operations: `exec`, `call`, `post`, `recv`, `log`, `metric`, and `request`. @@ -225,11 +251,14 @@ See [docs/execution-model.md](docs/execution-model.md). We keep this model small **The boundary is the backend.** Pick the backend to match your trust level: * **`backend="subinterpreter"`** (default) - an **execution cell**, not a - boundary against hostile Python. The guest runs in a sub-interpreter in the - supervisor's own process; restricted builtins and the import allow-list are - bypassable guardrails (adversarial Python can walk `object.__subclasses__()` - to reach the real `os`/`open`). Use it for **trusted** code, or for scheduling - and organization. + boundary against hostile Python. Today the guest runs in a dedicated + *thread* of the supervisor's own process, with guest code `exec`'d against a + restricted `__builtins__` mapping — **not** in a CPython sub-interpreter; the + backend is named for its intended implementation, which is roadmap work (see + [Sub-interpreter status](#sub-interpreter-status)). Restricted builtins and + the import allow-list are bypassable guardrails (adversarial Python can walk + `object.__subclasses__()` to reach the real `os`/`open`). Use it for + **trusted** code, or for scheduling and organization. * **`backend="process"`** - the **boundary mode**. The guest runs in a separate OS process, confined in depth by the kernel before any guest code runs: * `PR_SET_NO_NEW_PRIVS` + a seccomp deny-list that kills the process on diff --git a/ROADMAP.md b/ROADMAP.md index ac8aa3a..0797d9e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -7,7 +7,8 @@ normative statement. ## Delivered -- **Backends** — `backend="subinterpreter"` (execution cell) and +- **Backends** — `backend="subinterpreter"` (execution cell; currently a + dedicated thread rather than a CPython sub-interpreter — see below) and `backend="process"` (the boundary mode): a real separate-process boundary with `no_new_privs` + a seccomp deny-list, Landlock filesystem rules, Landlock TCP-egress rules (ABI ≥ 4), a coarse per-cgroup eBPF/LSM deny-mask, and @@ -30,6 +31,13 @@ normative statement. ## Now / next +- **Real sub-interpreters for `backend="subinterpreter"`** — the backend is + named for its intended implementation but runs guests in a `threading.Thread` + today, so guests share `sys.modules` and the GIL with the supervisor. Build it + on `concurrent.interpreters` (3.14) / `_interpreters` (3.12+), or rename the + backend to `thread` and let this item own the real thing. Either way the + boundary claim is unchanged: it is an execution cell, not a boundary against + hostile Python. - **Broker request execution** — the `request` op currently surfaces a `BrokerRequest` to the host but nothing executes it or returns a result. Add a request/response round-trip and a pluggable, capability-scoped handler so the diff --git a/SECURITY.md b/SECURITY.md index 4131a87..32c487c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,7 +6,10 @@ most important thing to understand before deploying PyIsolate: - `backend="subinterpreter"` (the default) is an **execution cell**, not a - boundary against hostile Python. Run only trusted code in it. + boundary against hostile Python. Run only trusted code in it. It currently + runs guests in a dedicated thread of the supervisor process, not a CPython + sub-interpreter — see "Sub-interpreter status" in the README. Neither is a + boundary, so nothing below changes. - `backend="process"` is the **boundary mode**: the guest runs in a separate OS process confined in depth by the kernel. - `backend="microvm"` is reserved and not yet implemented. diff --git a/docs/execution-model.md b/docs/execution-model.md index 38b7bd2..4fbbcaa 100644 --- a/docs/execution-model.md +++ b/docs/execution-model.md @@ -12,6 +12,12 @@ The public API names the isolation backend explicitly: `backend="subinterpreter" `subinterpreter` and `process` are implemented; `microvm` is reserved and fails closed until a launcher is available. +The `subinterpreter` backend currently executes guests in a dedicated **thread** +of the supervisor process rather than a CPython sub-interpreter — the mode is +named for its intended implementation. The cell ABI below is identical either +way, and so is the boundary claim (neither is one). See "Sub-interpreter status" +in the README. + The `process` backend runs guest code in a separate OS process, so in-process Python escapes (for example recovering an unrestricted `__import__` by walking `object.__subclasses__()`) can no longer reach the supervisor's address space — diff --git a/docs/threat-model.md b/docs/threat-model.md index cfac943..0a687f9 100644 --- a/docs/threat-model.md +++ b/docs/threat-model.md @@ -15,12 +15,16 @@ PyIsolate's security posture is **not uniform** across backends. Read every answer below as conditional on the backend you select. - **`backend="subinterpreter"`** (the default) is an *execution cell*, **not** a - security boundary against hostile Python. Guest code runs in a sub-interpreter - in the supervisor's own process; the restricted builtins and import allow-list - are ergonomic guardrails that adversarial Python can bypass (for example by - walking `object.__subclasses__()` to recover an unrestricted `__import__` and - reaching the real `os`/`open`). Use it for trusted code, or for scheduling and - organization — not to contain code you do not trust. + security boundary against hostile Python. Guest code runs in a dedicated + **thread** of the supervisor's own process — not, despite the backend's name, + in a CPython sub-interpreter; see "Sub-interpreter status" in the README. The + restricted builtins and import allow-list are ergonomic guardrails that + adversarial Python can bypass (for example by walking + `object.__subclasses__()` to recover an unrestricted `__import__` and reaching + the real `os`/`open`). This document's answers are unchanged by that + distinction: neither a thread nor a sub-interpreter is a boundary against + hostile Python. Use it for trusted code, or for scheduling and organization — + not to contain code you do not trust. - **`backend="process"`** is the boundary mode. The guest runs in a **separate OS process** (so those in-process escapes can no longer touch supervisor diff --git a/pyisolate/runtime/thread.py b/pyisolate/runtime/thread.py index 74ee409..551c59f 100644 --- a/pyisolate/runtime/thread.py +++ b/pyisolate/runtime/thread.py @@ -1,8 +1,17 @@ -"""Sandbox thread implementation. - -This is a greatly simplified placeholder that executes code in a dedicated -thread using the standard interpreter. Real isolation would leverage -sub‑interpreters and eBPF enforcement as outlined in AGENTS.md. +"""Sandbox thread implementation -- the ``backend="subinterpreter"`` runtime. + +Despite that backend name, this executes guest code in a dedicated +:class:`threading.Thread` of the supervisor's own process, using the standard +interpreter: guest source is ``exec``'d against a restricted ``__builtins__`` +mapping with thread-local policy state. It is **not** a CPython sub-interpreter, +and it is not a security boundary against hostile Python -- adversarial code can +walk ``object.__subclasses__()`` to recover the real ``__import__`` and reach +``os``/``open``. See "Sub-interpreter status" in the README, and +``docs/threat-model.md`` for the normative boundary statement. + +Use :mod:`pyisolate.runtime.process_backend` (``backend="process"``) for code +you do not trust. Running guests in real sub-interpreters, and kernel eBPF +enforcement for this backend, are roadmap items tracked in ROADMAP.md. """ from __future__ import annotations