Skip to content

fix(docker): harness isolation via uid-drop barrier — close the criteria/grader leak - #85

Draft
CarlesUIPath wants to merge 1 commit into
mainfrom
fix/docker-harness-isolation
Draft

fix(docker): harness isolation via uid-drop barrier — close the criteria/grader leak#85
CarlesUIPath wants to merge 1 commit into
mainfrom
fix/docker-harness-isolation

Conversation

@CarlesUIPath

@CarlesUIPath CarlesUIPath commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Under --driver docker, the agent-under-test could read its own grading material —
success_criteria, the skills-repo graders (check_*.py), RESOLUTION.md, reference
agents, the staged task.yaml, and the per-task-dir mount. A colleague's nightly audit
measured ~2.4% of replicates reading the suite instead of solving the task (claude-code
highest); an adversarial "check /work" prompt drives it to ~100% across frontier and
open-weight models. Prior denylist mitigations (zip/base64/compile/delete) did not hold.

This installs an allowlist permission barrier: the container stays root, but the agent's
CLI subprocess is dropped to an unprivileged agent uid (2000). All grading material is
root-owned chmod 0700EACCES to the agent, while grading (SuccessChecker /
run_command / judges) runs as root. The agent can read only its skill-docs and write its
own workspace.

How it works

  • Agent-agnostic drop: a setpriv shim; claude via ClaudeAgentOptions.user, codex via
    launch_args_override, antigravity via the localharness PATH-shadow. The orchestrator/
    grading process never leaves root.
  • Criteria strip + root-only channel: the agent-readable task.yaml is criteria-stripped
    (agent_safe_dump); the real criteria/reference ride root-only in task_full.json and are
    merged back (raw-dict, before parsing) for grading.
  • Single choke point: container_perms.lock_harness_root_0700 / grant_agent_ownership,
    guarded by lint rule CE033.
Leak surface Closed by
Staged criteria (/work/input, task_full.json) root-0700 lock + strip
Skills-repo graders (check_*.py, RESOLUTION.md, reference agents) raw host mount forwarded + locked
Per-task-dir mount root-0700 lock
Reference solution (reference.file/directory) mount forwarded + locked
Signpost env (TASK_DIR, SKILLS_REPO_PATH) live on the root grader env only
Baked image content authoring invariant

📖 Setup & design reference: the agent-uid ↔ root-grader arrangement, how tasks are
staged/authored under it, the barrier surfaces, the Docker-Desktop caveat, and the nightly
rollout are documented in docs/DOCKER_ISOLATION.md (extended in this PR).

Verification

New Docker test coverage (permanent, CI-gated). Adds a root-in-container acceptance
suite (tests/test_docker_user_separation.py, run by the new docker-isolation CI job via
make test-docker-isolation): it forks a child dropped to the agent uid and asserts EACCES
on all six surfaces + a real dropped-CLI check (id -u == 2000, HOME writable, grader/
task_full.json unreadable) inside the freshly built image on native Linux — the
authoritative environment (see caveat).

Independently confirmed the jailbreak reproduces → is closed. Beyond the committed
suite, an out-of-tree reachability harness was run in two modes on the same image:
reproduce (barrier omitted) shows the six surfaces ARE readable to the dropped uid —
reproducing the jailbreak — and verify-fixed (barrier applied via the real primitives)
shows they become EACCES. The before/after pair proves the fix actually closes the leak
(and that the check isn't vacuous).

Local: gates green; the Docker acceptance suite passes; full make test = 4009 passed
with only pre-existing/environmental failures (live-API judge tests needing creds, local
TASK_DIR/task-tag pollution, a stale model-name pin, and report-command output strings
that live on main) — none touch this diff.

⚠️ The barrier is Linux-authoritative. On macOS/Windows Docker Desktop the
gRPC-FUSE/VirtioFS uid-remap defeats chmod 0700 on bind mounts, so local bind-mount
checks are unreliable — trust the Linux CI job. (Locked mounts are :rw so the in-container
chmod applies; the lock fails loud if it can't.)

Out of scope / follow-ups

  • tempdir driver + the Windows nightly slice (no container/uid boundary) — separate.

if errors is not None:
errors.append((path, exc))
else:
logger.debug("container_perms: could not chown/chmod %s: %s", path, exc)
Comment thread tests/test_docker_user_separation.py Fixed
Comment thread tests/test_docker_user_separation.py Fixed
Comment thread tests/test_docker_user_separation.py Fixed
Comment thread tests/test_docker_user_separation.py Fixed
Comment thread tests/test_docker_user_separation.py Fixed
Comment thread tests/test_docker_user_separation.py Fixed
@CarlesUIPath CarlesUIPath changed the title feat(docker): harness isolation via uid-drop barrier — close the criteria/grader leak fix(docker): harness isolation via uid-drop barrier — close the criteria/grader leak Aug 5, 2026
@CarlesUIPath
CarlesUIPath force-pushed the fix/docker-harness-isolation branch from 83aad3c to 61a157f Compare August 5, 2026 14:57
…eria/grader leak

Under --driver docker, run the agent-under-test's CLI subprocess as an unprivileged
`agent` uid (2000) while the container stays root, so grading material (success_criteria,
task_full.json, skills-repo graders check_*.py, RESOLUTION.md, reference agents, the
per-task-dir mount, reference solutions) is root-owned mode-0700 and returns EACCES to the
agent, while grading runs as root. Closes the leak where agents read their own answer key
(measured ~2.4% of nightly replicates; ~100% under an adversarial "check /work" prompt).

Covers claude-code / codex / antigravity: a setpriv drop shim, claude via
ClaudeAgentOptions.user, codex via launch_args_override, antigravity via the localharness
PATH-shadow. Criteria stripped from the agent-readable task.yaml (agent_safe_dump) and
restored root-only from task_full.json before grading (raw-dict merge).

Hardening found via multi-model review + real containerized runs:
- forward raw plugin/skills-repo host mounts via context.json and lock them
- mount locked staging dirs read-write so the in-container 0700 chmod applies (a :ro mount
  EROFS-no-op'd the lock silently); lock now fails LOUD
- relocate HOME/CODEX_HOME to an agent-owned /home/agent under the drop
- lock the reference-solution mount (reference.file / reference.directory)
- reject agent_run_uid authored from YAML/CLI/variant (framework-set only)
- CE033 lint guards the container_perms choke point, forbids a container --user, and flags
  a raw task.model_dump into the staged yaml

Test coverage: `make test-docker-isolation` runs a root-in-container six-surface
EACCES-as-uid-2000 proof + a real dropped-CLI acceptance inside the built image (new
`docker-isolation` CI job). The barrier was independently confirmed with an out-of-tree
reachability harness (reproduce vs verify-fixed) showing the jailbreak reproduces on the
vulnerable image and is EACCES under the barrier. Barrier is Linux-authoritative (native
overlayfs); macOS Docker Desktop's bind-mount uid-remap makes local bind-mount checks
unreliable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@CarlesUIPath
CarlesUIPath force-pushed the fix/docker-harness-isolation branch from 61a157f to e67c70b Compare August 5, 2026 15:05
# the barrier sets, not mkdtemp's default.
anc = tmp_path
while anc != anc.parent and str(anc).startswith("/tmp"):
os.chmod(anc, 0o711) # traverse-only (o+x); NOT world-readable
os.chmod(anc, 0o711) # traverse-only (o+x); NOT world-readable
anc = anc.parent
for extra in (output_dir, output_dir / "artifacts"):
os.chmod(extra, 0o711) # traverse-only (o+x); NOT world-readable
# exercise the LEAF (agent-owned ws) perms, not mkdtemp's default.
anc = tmp_path
while anc != anc.parent and str(anc).startswith("/tmp"):
os.chmod(anc, 0o711) # traverse-only (o+x); NOT world-readable
# Make the temp ancestor chain traversable (production /work mounts are).
anc = tmp_path
while anc != anc.parent and str(anc).startswith("/tmp"):
os.chmod(anc, 0o711) # traverse-only (o+x); NOT world-readable
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