Skip to content

#2 Contain the extraction child - #33

Merged
gregoryfoster merged 21 commits into
mainfrom
2-contain-child
Oct 6, 2026
Merged

gregoryfoster merged 21 commits into
mainfrom
2-contain-child

Conversation

@gregoryfoster

@gregoryfoster gregoryfoster commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Closes #2. Plan: docs/plans/2026-10-06-2-contain-the-child.md. Gate 2 approved 2026-10-06.

What

Layer 1: the child contains itself. src/processor/_contain.py uses stdlib ctypes only, so no new dependency.

  • Landlock, on a read/execute allowlist derived from the interpreter:
    • each sys.path entry, the stdlib, the directories of the shared libraries already loaded, ld.so.cache, and the mime-types files co-core reads at import;
    • /, /etc, /home, a home directory, and any directory holding a .env are refused;
    • no TCP bind or connect;
    • abstract unix sockets and signals are scoped.
  • seccomp: socket, socketpair and io_uring_* fail with EPERM, and another arch's syscalls kill the child. Landlock on 6.12 lets a connect to a pathname unix socket through (tailscaled's socket is 0666), and a test pins that gap.
  • Ordering: _child contains itself before it reads its request, and exits 70 if it can't, which counts as a strike.

Layer 2: the parent is undumpable. processor run calls PR_SET_DUMPABLE 0 first. It refuses to start under CO_PROCESSOR_CHILD_CONTAINMENT=required (the default) wherever the child can't be contained. starting now logs build, child_containment and landlock_abi.

Layer 3: a dedicated user and releases. This follows the cohort standard, broker#22 as shipped in status#9 (R1–R13).

  • The unit runs as processor from /srv/processor/live → releases/<build>. It takes the GCS key through LoadCredential=, and adds ProtectSystem=strict and ProtectHome=yes.
  • scripts/deploy.sh:
    • builds from a commit on origin/main: git archive plus the wheelhouse, then uv sync --locked --no-editable on /usr/bin/python3.12, with REVISION written last and the release made a-w,go+rX;
    • switches live atomically and installs the unit from the release;
    • verifies the new MainPID's starting (this build, required) and then consuming, followed by the smoke run as processor through systemd-run;
    • switches back on failure: exit 1 when the old build answers, 4 when nothing does.

Evidence

  • Local: 363 passed, 1 skipped (a worktree has no repo .env). Header: child containment: required (Landlock ABI 6).
  • CI: green, required (Landlock ABI 7). The first run caught a real bug, fixed in 4f7a6ec: the library directories had come from sysconfig's LIBDIR, which on setup-python is not the system's, so libgcc_s was refused.
  • Every denial is paired with an uncontained control:
    • reading a file outside the allowlist, the real /etc/processor/.env and GCS key, /proc/<parent>/environ, and a home file;
    • writing;
    • TCP to a live listener and to 127.0.0.1:6379;
    • a unix socket, /run/docker.sock and tailscaled.sock;
    • signalling the parent, and exec /bin/sh.
  • The whole corpus (cases plus real/, 22 inputs) is identical through the contained child. It was also run from a real release built with the deploy's own steps.
  • scripts/deploy.sh has 23 end-to-end tests with stubs, mutation-checked.

Rollout (gate 3; the plan's § Rollout)

  • Stage A, after the FF merge: the old path (git pull && uv sync --frozen --no-dev && sudo systemctl restart processor). Layers 1 and 2 go live while the service still runs as exedev. Rollback: CO_PROCESSOR_CHILD_CONTAINMENT=off.
  • Stage B, the operator's install: DEPLOYMENT.md § Install, ending with scripts/deploy.sh. Rollback: DEPLOYMENT.md § Rollback.

Follow-ups to file: processor's CI gate and drift check (status#11 and #12). The feedback for the cohort skill is in the plan's last section and is linked from Provisioner#23.

🤖 Generated with Claude Code

gregoryfoster and others added 7 commits October 6, 2026 00:08
…mpable parent, processor user, /opt tree)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…R1–R13, broker#22); feedback for the cohort skill

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t, via ctypes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s closed without containment, reports its build

- _child: contain(mode) before reading the request; exit 70 when it cannot
- run_in_child(containment=…) required; Deps.containment; CO_PROCESSOR_CHILD_CONTAINMENT (default required)
- __main__._run: make_undumpable first; refuse to start under required where unavailable; starting logs build, child_containment, landlock_abi
- build_id(): the release's REVISION, else dev
- tests: every denial paired with an off control; the whole corpus (cases + real/) through the contained child; report header names the mode

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…under /srv/processor/live

Follows the cohort release standard (broker#22; status#9 R1–R13): git archive of an
origin/main commit, wheelhouse, uv sync --locked --no-editable on /usr/bin/python3.12,
REVISION last, a-w,go+rX; atomic switch; unit installed from the release; verify by the
new MainPID's starting (build, child_containment required) + consuming, then the smoke
as the unit's user via systemd-run; switch back on failure (exit 1, or 4).

Unit: User=processor, LoadCredential for the GCS key, ProtectSystem=strict, ProtectHome=yes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… processor user, releases and scripts/deploy.sh

Spec §3's 'Known limitation' becomes 'Containment'. DEPLOYMENT: /etc/processor/ (root,
LoadCredential, processor_cli), releases, deploy.sh, containment checks, Stage B install
and rollback; the 2026-10-02 install kept as history. AGENTS: no longer 'not a sandbox';
shipping is scripts/deploy.sh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…jects, not sysconfig's LIBDIR

CI (setup-python, Landlock ABI 7) ran the suite under required: LIBDIR named the
toolcache, so libgcc_s.so.1 beside libc was refused and the extractors failed to import.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gregoryfoster
gregoryfoster marked this pull request as ready for review October 6, 2026 04:02
gregoryfoster and others added 14 commits October 6, 2026 13:47
…ed child before any command

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the loader included

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… it restored

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h would escape both layers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…om this boot since the restart

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…idth longs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s tools

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the service uid, denied to the contained child

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
uv's Linux default hardlinks from ~/.cache/uv; on one filesystem every release file
was the cache's and each dev venv's inode, so the release chmod reached them and an
edit to any reached production. A real build with UV_LINK_MODE=copy: no file with
more than one link; the live venv's modes untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t by a glob the operator's shell cannot expand

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pped ruff)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…han exiting silently under set -e

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ser reaches a release under any umask

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

Contain the extraction child: it runs as the service user and can reach the service's secrets

1 participant