Repository navigation
fix(metrics): the docker backend of example execution drops every capability - #289
Conversation
…ability The docker backend started the snippet's container with docker's default capabilities and without no-new-privileges. The default image, python:3.11-slim, runs as root, so the snippet kept CAP_SETUID, CAP_CHOWN and the rest, and a setuid binary in the image (su) still worked. The other docker call sites set both: repos/container.py and the task container's TASK_CONTAINER_HARDENING. The ContainerPolicy here now sets no_new_privileges and cap_drop_all. Without CAP_DAC_OVERRIDE that root cannot enter the host user's 0700 temp directory, so the directory is chmod 0755 and the snippet 0644; otherwise every snippet fails with "Permission denied". Refs intel#229 Signed-off-by: Vladislav Perevezentsev <vladislav.perevezentsev@intel.com>
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #289 +/- ##
==========================================
+ Coverage 93.25% 93.52% +0.26%
==========================================
Files 272 274 +2
Lines 35117 35627 +510
Branches 5964 6052 +88
==========================================
+ Hits 32750 33320 +570
+ Misses 1548 1490 -58
+ Partials 819 817 -2
🚀 New features to boost your workflow:
|
|
✅ OpenCodeReview: Review complete: 0 finding(s) across 5 selected item(s). Review details
|
os.chmod there toggles only the read-only bit, so the directory and the snippet never read 0o755 and 0o644. Signed-off-by: Vladislav Perevezentsev <vladislav.perevezentsev@intel.com>
|
✅ OpenCodeReview: Review complete: 0 finding(s) across 5 selected item(s). Review details
|
antonwolfy
left a comment
There was a problem hiding this comment.
LGTM. The fix is right: hardening_argv() is spliced right after --rm, the same place as in repos/container.py, and the 0755 directory / 0644 snippet is set before the runner call. The docs, the module docstring and the changelog fragment match the new argv.
What I ran, on a Linux review host at 0489255:
make ci-local: 25 passed, 3 failed, 7 skipped. 10182 tests passed, changed-line coverage gate passes, ruff, mypy, bandit, schema_compat, mkdocs and the package build pass.- The 3 FAILs are not the PR's.
pip-auditreports thefilelock3.32.7 advisories in the lock files, which this PR does not touch. The gitleaks install step could not download its binary; I rangitleaks gitover the PR's two commits by hand: no leaks. - Mutations of the new code against both test files (56 tests): directory mode 0o757, 0o750 or 0o711; script mode 0o600, 0o666 or 0o755; either chmod removed; chmod moved after the runner call;
cap_drop_all,no_new_privilegesorhardening_argv()removed. All fail the tests.
Checked, no issues
- Order: the chmod calls run after the snippet is written and before
runner(...); the new test reads the modes inside the fake runner, underumask 077. - Tests: the new test is offline, writes only to the temp directory, restores the umask and skips on Windows.
- Commits: both are signed off.
- Repo rules: fragment format and SPDX header are right, no generated artifacts, no file on the growth baseline.
|
I approved this at
|
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
antonwolfy
left a comment
There was a problem hiding this comment.
LGTM. I pushed 903eb64, which shortens the mount comment in example_runner.py to two lines: the 4-line block fails the comment-block check now on main, as the bot pointed out.
Summary
The
dockerbackend of example execution (example_pass_rate,metrics/example_runner.py) started the snippet's container with docker's default capabilities and withoutno-new-privileges. The default image,python:3.11-slim, runs as root, so a fenced example from an untrusted document keptCAP_SETUID,CAP_CHOWN,CAP_DAC_OVERRIDEand the rest, and a setuid binary in the image (su) still worked. This is the open part of P2-1 in #229 (napetrov's comment of 2026-10-29): since #249 this site builds its flags withContainerPolicy, but it does not setcap_drop_allorno_new_privileges. The other call sites already set both:repos/container.pyand the task container (TASK_CONTAINER_HARDENING).docker run --rm + --security-opt no-new-privileges --cap-drop ALL --network none --cpus 1.0 --memory 512m --pids-limit 64 --read-only -v <tmp>:/work:ro python:3.11-slim timeout 5 <interpreter> /work/snippet.<ext>The flags alone break every snippet:
bash: /work/snippet.sh: Permission denied.TemporaryDirectorycreates the mounted directory 0700 for the host user, and a root withoutCAP_DAC_OVERRIDEcannot enter a directory it does not own. So the directory is now 0755 and the snippet 0644. The snippet is the document's own text, and the mount stays read-only (:ro).Evidence
Real docker on this machine (Docker 29.1.3,
python:3.11-slim), snippets throughrun_example(..., ExecutionPolicy(backend="docker")):b0605dcCapEffin/proc/self/status00000000a80425fb(docker's default set)0000000000000000NoNewPrivs01su -s /bin/sh -c id nobodyuid=65534(nobody)su: cannot set groups: Operation not permittedecho ok(bash, sh)python3 -c 'print(1)',sha256sumfrom bashPermission denied(exit 126 / 2)Tests:
tests/test_container_argv_characterization.py: the two full-argv pins of this site gain the two flags, in the same position as inrepos/container.py(right after--rm). The test that pinned their absence ("Recorded gap ... a separate follow-up") is replaced by one that requires--cap-drop ALLandno-new-privilegesfor each language.tests/test_example_runner.py(new test): while docker runs, the mounted directory is 0755 and the snippet 0644, also underumask 077.example_runner.py:6 failed, 50 passedfor these two files. On this branch:56 passed.no_new_privileges, droppingcap_drop_all, not splicinghardening_argv(), dropping either chmod, or a directory mode of 0750.make ci-local: pytest 10129 passed, 57 skipped. One FAIL,pip-audit: the filelock 3.32.7 advisories in the lock files, also on main (ci(deps): document and ignore ten more filelock 3.32.7 advisories #288).banditB103 onchmod 0o755is marked# nosec B103with the reason, as the repo does for B104/B108/B310.Merge Danger
Door: two-way. A revert restores the default capabilities.
Blast Radius: only
example_pass_ratewithexecution.backend: docker(the default backend isnone). A snippet that needs root's capabilities in the container now fails:chown,su/sudo, binding a port below 1024, or reading a root-only file. With--read-onlyand--network nonesuch a snippet could already do little. A customdocker_imagewhose interpreter or files are readable only by root now fails as well. The extrachmodmakes the snippet's temp directory readable by other host users for the run; it holds only the snippet.Overlap: no open PR touches
metrics/example_runner.py,container_policy.py,tests/test_container_argv_characterization.pyortests/test_example_runner.py.Not in this PR:
--user <host uid:gid>, asrepos/container.pypasses, would avoid the chmod, but it changes who the snippet runs as: an image whose interpreter sits under/rootwould break. Here the snippet keeps running as the image's user.--init:repos/container.pypasses it to reap a build server's orphans; a snippet is one short process undertimeout.harnesses/container_agent.py), listed as not re-checked in the same Consolidate duplicated subsystems (stats, LLM routing, judge loop, containers, digests, writers) #229 comment.EXECUTORS["python"]is the host'ssys.executable(e.g./home/.../.venv/bin/python), which does not exist in the container (Exit code 127: timeout: failed to run command ...). A separate fix.Refs #229
Checklist
pytestpasses (10129 passed, 57 skipped)ruff,mypy, and schema checks pass (make ci-local; onlypip-auditfails, as on main)docs/example-execution.md(backend table, security notes); changelog fragment underSecurityAGENTS.md: mutations, the other docker call sites, real docker runs before and after