Skip to content

fix(pipeline): sandbox kill now reaps the whole worker tree (BACKLOG #342) - #248

Open
wshallwshall wants to merge 2 commits into
mainfrom
fix-342-sandbox-reap
Open

fix(pipeline): sandbox kill now reaps the whole worker tree (BACKLOG #342)#248
wshallwshall wants to merge 2 commits into
mainfrom
fix-342-sandbox-reap

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Fixes BACKLOG #342. pipeline/sandbox.py.

Killing a sandbox worker did not reap its descendants, so a worker that had spawned children could
leave them running after the sandbox was torn down. The kill path now reaps the whole tree -- a
Windows job object, and killpg on POSIX.

Verified in-lane: plan -> build -> 3-lens adversarial verify -> remediate. Banner gate OK.

…n't outlive it (BACKLOG #342)

`SandboxSession._kill` was a bare `proc.kill()`, which terminates only the
immediate worker. A sandboxed Handler can spawn a grandchild that inherits
fd 1 (the response pipe); before this change that grandchild would outlive the
kill as an orphan still holding the pipe, so the pipe never reached EOF and the
kill was incomplete. This is a beta defect in the shipped code (NOT-DEPLOYED:
no running instance is affected) whose residual is availability / process
hygiene, not misdelivery -- the ADR 0087 codec + per-dispatch id +
unsolicited-frame check already keep any stray grandchild frame harmless.

Fix: `_kill` now reaps the whole worker tree.
- Windows: the worker is assigned to a `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`
  job object before its boot frame (the earliest untrusted code), via stdlib
  ctypes only -- no new dependency; terminating the job kills the tree.
- POSIX: the worker is spawned `start_new_session=True` (its own process-group
  leader) and the group is `killpg(SIGKILL)`'d, guarded on `pgid == proc.pid`
  so only the worker's own group is ever signalled.
A job-assign / API failure degrades to a single-process kill (logged), so a
lingering grandchild is a hygiene residual, never a trust hole.

Regression test (tests/test_sandbox.py): a Handler spawns an fd-1-holding
grandchild; after `_kill` the response pipe must reach EOF (every holder gone)
and the grandchild must be dead. Falsified locally by forcing
`_assign_kill_on_close_job` to return None -- the grandchild survives, the pipe
never EOFs, the test goes red on its primary assertion; restored to green.
The Windows job-object path is exercised locally (this host is Windows); the
POSIX killpg path is guarded to run on the CI ubuntu leg.

Sibling docs synced to the code in the same commit (the reap logic itself stays
inside pipeline/sandbox.py):
- pipeline/_sandbox_codec.py: the child->parent bullet no longer claims a
  grandchild "survives proc.kill() ... at any later moment".
- docs/CONFIGURATION.md: the [sandbox] section no longer says a grandchild
  "outlives the worker's kill"; it now records the tree-reap as best-effort
  hygiene while the codec + request-answer binding remain the trust control.

ADR 0087 / ADR 0147 residual co-design and the vault THREAT-MODEL.md update are
deferred to the owner per the lane brief (reported, not done here).
Flips the BACKLOG #342 status banner from OPEN to BUILT and rewrites its prose
to describe the shipped tree-reap. Only the #342 banner line changed; the ranked
table, the four census distribution lines, and every other item's banner are
untouched.

The census was NOT recomputed -- this commit flips one banner and does not
re-derive the open/closed distribution counts.
@wshallwshall
wshallwshall enabled auto-merge (squash) August 6, 2026 14:16
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.

1 participant