Skip to content

feat(py): the macOS seatbelt sandbox the worker engages on itself - #355

Draft
jat255 wants to merge 1 commit into
jat255/t7d4-sandbox-gate-rlimitsfrom
jat255/tpw5-macos-seatbelt-sandbox
Draft

feat(py): the macOS seatbelt sandbox the worker engages on itself#355
jat255 wants to merge 1 commit into
jat255/t7d4-sandbox-gate-rlimitsfrom
jat255/tpw5-macos-seatbelt-sandbox

Conversation

@jat255

@jat255 jat255 commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

This PR adds the macOS seatbelt sandbox the execution worker engages on itself, reached through ctypes so it needs no compiled extension, and it fills in the seatbelt field of the capability probe from #352.

Stacked on #352, so review that one first. Roots are arguments rather than computed here: the worker entry point is where the scratch directory and the parent's tmpdir are known, so it assembles the root set when it lands. Tracked as kata tpw5.

Agent-written detail

The profile follows the rules and the order of the Darwin branch of pkg-r/src/sandbox.c: a permissive default, then each filesystem deny immediately followed by its allowlist, because later rules win in SBPL. Write roots are granted for reading too, /dev/null stays a writable literal, and file-read-metadata stays allowed so path traversal keeps working.

Two things differ from the R implementation. A root that is not absolute is rejected, which R does not check, because realpath would otherwise resolve it against the worker's cwd and grant some unrelated directory. And the read allow is left out entirely when there is nothing to grant, rather than emitted with no path predicate; an unscoped allow sitting after the deny would hand back every readable file on the host. The local review caught that as a real hole, and there is now a live test that fails without the fix.

protection_mode() starts returning "sandbox" on macOS instead of raising. That is the intended direction, and nothing calls it from Commons.__init__ yet (task 12, kata 2q9b), so no construction path changes here. The probe looks the symbol up rather than trusting the platform name, so a macOS that finally drops these entry points (deprecated since 10.8) reports no seatbelt instead of promising one the worker cannot engage.

No tests/shared/ fixture for this: R builds its profile inside sandbox.c with nothing reachable from R's test suite, so a shared fixture could not be executed by both suites. The observable behaviour is pinned per language against the same C source of truth.

The two tests for the platform check call _seatbelt_present() rather than sandbox_capabilities(). Claiming to be Linux means patching the shared platform module, so going through the aggregate would send the seccomp and Landlock probes down their Linux paths on a host that has neither interface.

Testing. Profile generation is pure, so its rules run on any host. The live path is exercised for real on macOS and skipped elsewhere: a subprocess engages the seatbelt and then proves a write inside its root succeeds, a write outside raises, a read outside the read roots is denied, /dev/null stays writable, and the network rule blocks a connection to a listener in the test process, which keeps the test off the internet. The subprocess imports the module by directory rather than through commons, which is how the worker reaches it and proves it needs nothing from the package. ruff check, pyrefly check src tests, and the full pytest run are clean; pyrefly's six remaining errors are missing optional OpenTelemetry imports that predate this branch.

@jat255 jat255 added py Affects the Python implementation needs-manual-review Agent-created work that needs a human review labels Sep 10, 2026
@jat255 jat255 added this to the py-M6: code execution milestone Sep 11, 2026
Seatbelt's entry points are plain libSystem symbols, so ctypes reaches
them without a compiled extension. The profile follows the same rules and
the same order as the Darwin branch of pkg-r/src/sandbox.c: a permissive
default, then each filesystem deny immediately followed by its allowlist,
since later rules win in SBPL.

Roots are arguments rather than computed here. The worker entry point is
where the scratch directory and the parent's tmpdir are known, so the root
set is assembled there when it lands.

Both the original and the symlink-free form of every root are granted,
because the sandbox matches resolved paths and macOS /tmp and /var live
under /private. A root that is not absolute is rejected, which R does not
check: realpath would otherwise resolve it against the worker's cwd and
grant some other directory.

sandbox_capabilities() now reports a real seatbelt value, looked up as a
symbol rather than inferred from the platform. protection_mode() therefore
returns "sandbox" on macOS instead of raising; nothing calls it from
Commons.__init__ yet, so no construction path changes.
@jat255
jat255 force-pushed the jat255/tpw5-macos-seatbelt-sandbox branch from 6f7945c to 456c329 Compare September 11, 2026 19:47
@jat255
jat255 added this pull request to stack #364 September 11, 2026 19:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-manual-review Agent-created work that needs a human review py Affects the Python implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant