From 890089a541a7ffcf588ad225bb8f537f7699f01e Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Mon, 26 Oct 2026 21:39:52 -0700 Subject: [PATCH 1/5] fix(docker): harden the docker-* task container like repos containers The task container that docker_solver starts for a docker-* run had no --cap-drop, --security-opt no-new-privileges or --pids-limit, unlike the containers repos/container.py starts. It now gets all three, with CHOWN, DAC_OVERRIDE, FOWNER, SETUID and SETGID added back: the harness's own root steps need them (the agent's tree, apt, setpriv). Smoke runs on python:3.12-slim, with the same bind mount and fork load: drop-ALL alone breaks apt, setpriv/runuser, chown and root writes into agent-owned files; the five added capabilities restore them, and each one is needed by at least one check. KILL, NET_BIND_SERVICE and NET_RAW stay dropped. Refs #229 (P2-1a). Changes no numbers. Signed-off-by: Nikolay Petrov --- ...61026_130000_napetrov_docker-solver-hardening.md | 12 ++++++++++++ dokimi/docs/coding-harnesses.md | 7 +++++++ dokimi/docs/contributing-terminal-bench-task.md | 8 ++++++++ dokimi/dokimi/harnesses/container_agent.py | 9 +++++++++ dokimi/dokimi/harnesses/docker_solver.py | 11 +++++------ dokimi/scripts/growth_baseline.json | 2 +- dokimi/tests/test_docker_solver_harness.py | 13 +++++++++++++ 7 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 dokimi/changelog.d/20261026_130000_napetrov_docker-solver-hardening.md diff --git a/dokimi/changelog.d/20261026_130000_napetrov_docker-solver-hardening.md b/dokimi/changelog.d/20261026_130000_napetrov_docker-solver-hardening.md new file mode 100644 index 000000000..553f4af02 --- /dev/null +++ b/dokimi/changelog.d/20261026_130000_napetrov_docker-solver-hardening.md @@ -0,0 +1,12 @@ +### Changed + +- **`docker-*` task containers are hardened like `repos` containers** — the + task container starts with `--cap-drop ALL`, `--security-opt no-new-privileges` + and `--pids-limit 32768`, and gets back only `CHOWN`, `DAC_OVERRIDE`, `FOWNER`, + `SETUID` and `SETGID`, which the harness's own root steps (the agent's tree, + `apt`, `setpriv`) need. A setuid binary in a task image can no longer raise its + privileges. A task whose solve or verifier needs another capability now fails at + that step: killing another user's process (`KILL`), binding a port below 1024 on a + host where `net.ipv4.ip_unprivileged_port_start` is 1024 (`NET_BIND_SERVICE`), or + raw sockets (`NET_RAW`). Before, the container ran with Docker's default + capabilities and no process limit. diff --git a/dokimi/docs/coding-harnesses.md b/dokimi/docs/coding-harnesses.md index 9f5260490..49232d94d 100644 --- a/dokimi/docs/coding-harnesses.md +++ b/dokimi/docs/coding-harnesses.md @@ -290,6 +290,13 @@ The result is in `metrics.isolation_probe` and `isolation_probe.json`: - **Memory.** The agent's CLI and its tmpfs HOME run inside the task's cgroup, so they count against `environment.memory_mb` (about 100 MB for a short session); the host launch ran outside it. +- **Capabilities.** Every `docker-*` task container, `docker-oracle` included, + starts with `--cap-drop ALL`, `no-new-privileges` and `--pids-limit 32768`, + and gets back only `CHOWN`, `DAC_OVERRIDE`, `FOWNER`, `SETUID` and `SETGID` + (`TASK_CONTAINER_HARDENING` in `harnesses/container_agent.py`). A solve or + verifier that needs another capability (`KILL` on another user's process, + `NET_BIND_SERVICE` below port 1024 where the host sets + `ip_unprivileged_port_start` to 1024, `NET_RAW`) fails at that step. - **Network.** The container sits on a per-run `--internal` Docker network, which has no route out. The agent reaches the LLM through an allowlisting CONNECT proxy (`harnesses/egress_proxy.py`, stdlib only). It runs in a diff --git a/dokimi/docs/contributing-terminal-bench-task.md b/dokimi/docs/contributing-terminal-bench-task.md index 8008a4ab9..b0dc90e08 100644 --- a/dokimi/docs/contributing-terminal-bench-task.md +++ b/dokimi/docs/contributing-terminal-bench-task.md @@ -183,6 +183,14 @@ correctness baseline for the *task*, not a tutorial. Every task needs one, because the `docker-oracle` harness is how a task is proven runnable without spending model tokens. +The solve and the verifier run in a container without Linux capabilities +beyond `CHOWN`, `DAC_OVERRIDE`, `FOWNER`, `SETUID` and `SETGID`, so a setuid +binary cannot raise its privileges and the process count is capped at 32768. +A step that signals another user's process, binds a port below 1024 (on a host +whose `ip_unprivileged_port_start` is 1024) or opens a raw socket fails; +`docker-oracle` will show it. See +[What the agent can reach](coding-harnesses.md#what-the-agent-can-reach). + ## 8 — Local verification ```bash diff --git a/dokimi/dokimi/harnesses/container_agent.py b/dokimi/dokimi/harnesses/container_agent.py index da04add5a..f92a76eef 100644 --- a/dokimi/dokimi/harnesses/container_agent.py +++ b/dokimi/dokimi/harnesses/container_agent.py @@ -83,6 +83,15 @@ _FILE_VARS = ("NODE_EXTRA_CA_CERTS", "SSL_CERT_FILE", "REQUESTS_CA_BUNDLE", "AWS_CA_BUNDLE", "GOOGLE_APPLICATION_CREDENTIALS", "AWS_SHARED_CREDENTIALS_FILE", "AWS_CONFIG_FILE") PROXY_PORT = 3128 +# The task container's hardening flags are the ones repos/container.py sets. Unlike there, root +# here gets back the few capabilities its own steps need: CHOWN and FOWNER (chown/chmod of the +# agent's tree), DAC_OVERRIDE (root writing the agent's files), SETUID/SETGID (apt, setpriv). +# Each was needed in the #229 smoke runs; KILL, NET_BIND_SERVICE and the rest stay dropped. +TASK_CONTAINER_HARDENING = ( + "--cap-drop", "ALL", "--security-opt", "no-new-privileges", "--pids-limit", "32768", + "--cap-add", "CHOWN", "--cap-add", "DAC_OVERRIDE", "--cap-add", "FOWNER", + "--cap-add", "SETUID", "--cap-add", "SETGID", +) _PROBE_TIMEOUT_SEC = 120.0 DOCKER_STEP_TIMEOUT = 600.0 # a docker step without a budget of its own (cp, exec, inspect, ...) # The sidecar needs only a Python 3 interpreter. Pinned by digest (a tag moves, and diff --git a/dokimi/dokimi/harnesses/docker_solver.py b/dokimi/dokimi/harnesses/docker_solver.py index bcb5c472a..0903a4114 100644 --- a/dokimi/dokimi/harnesses/docker_solver.py +++ b/dokimi/dokimi/harnesses/docker_solver.py @@ -72,7 +72,8 @@ ) from .claude_json import _last_json_object, _parse_claude_json # noqa: F401 from .command import _parse_reward_text -from .container_agent import AGENT_BIN, DOCKER_STEP_TIMEOUT, FILES_DIR, ContainerSandbox, Docker +from .container_agent import (AGENT_BIN, DOCKER_STEP_TIMEOUT, FILES_DIR, TASK_CONTAINER_HARDENING, + ContainerSandbox, Docker) from .models import HarnessResult, OperationRecord from .network_policy import (DEFAULTS, NetworkPlan, PhasePolicy, build_args, enter_phase, needs_sandbox, resolve_recorded, skill_needs, start_args, redact as _redact) @@ -352,11 +353,9 @@ def _start_container(self, task: TaskSpec, image: str, container: str, log, + _task_verify_timeout(task) + 600)) self._docker(["rm", "-f", container], log, check=False) env_args = [a for k, v in task.container_env.items() for a in ("-e", f"{k}={v}")] - self._docker( - ["run", "-d", "--name", container, *net_args, *env_args, - "--cpus", cpus, "--memory", memory, image, "sleep", str(lifetime)], - log, - ) + run_args = ["run", "-d", "--name", container, *TASK_CONTAINER_HARDENING, *net_args, + *env_args, "--cpus", cpus, "--memory", memory, image, "sleep", str(lifetime)] + self._docker(run_args, log) return enter_phase(plan.solve if inside else DEFAULTS["solve"], container, self._docker, log, bridged="bridge" in net_args) diff --git a/dokimi/scripts/growth_baseline.json b/dokimi/scripts/growth_baseline.json index 72aeb637b..e0dc86497 100644 --- a/dokimi/scripts/growth_baseline.json +++ b/dokimi/scripts/growth_baseline.json @@ -2,7 +2,7 @@ "files": { "dokimi/commands/novelty.py": {"lines": 984, "rationale": "estimate-only projection (expected_usd, arm scaling, extrapolation flag) in novelty CLI, on top of main's W-series and #173 lines; file split tracked separately"}, "dokimi/harnesses/benchflow.py": {"lines": 869}, - "dokimi/harnesses/docker_solver.py": {"lines": 900, "rationale": "pre-existing when the gate was introduced"}, + "dokimi/harnesses/docker_solver.py": {"lines": 899, "rationale": "pre-existing when the gate was introduced"}, "dokimi/llm.py": {"lines": 1118, "rationale": "grew on main in #180 (replay a stored retry) after #93 seeded this baseline; split it in its own PR"}, "dokimi/report/model_compare.py": {"lines": 1209, "rationale": "grew on main in #73 (judge-validity --context stored) before the ratchet landed; split it in its own PR"}, "tests/test_arm_runner.py": {"lines": 1176, "rationale": "pre-existing when the gate was introduced"}, diff --git a/dokimi/tests/test_docker_solver_harness.py b/dokimi/tests/test_docker_solver_harness.py index 13c10553e..100f95a18 100644 --- a/dokimi/tests/test_docker_solver_harness.py +++ b/dokimi/tests/test_docker_solver_harness.py @@ -229,6 +229,19 @@ def test_start_container_malformed_memory_falls_back_instead_of_crashing(tmp_pat assert run_call[run_call.index("--memory") + 1] == "2g" +def test_start_container_drops_capabilities_and_bounds_pids(tmp_path, monkeypatch): + # Regression (#229 P2-1a): the task container had no cap drop, no-new-privileges or + # pids limit, unlike repos/container.py. Only the named capabilities the phase steps + # need (chown, DAC override, file owner, setuid/setgid for apt) are added back. + task = load_task(_fixture_task(tmp_path)) + run_call = next(c for c in _start(task, "container-6") if c[0] == "run") + assert run_call[run_call.index("--cap-drop") + 1] == "ALL" + assert run_call[run_call.index("--security-opt") + 1] == "no-new-privileges" + assert int(run_call[run_call.index("--pids-limit") + 1]) >= 4096 + added = {run_call[i + 1] for i, arg in enumerate(run_call) if arg == "--cap-add"} + assert added == {"CHOWN", "DAC_OVERRIDE", "FOWNER", "SETUID", "SETGID"} + + def test_solve_oracle_copies_the_whole_solution_directory(tmp_path, monkeypatch): # Regression test: a real run against uxlfoundation/skills' # onetbb-histogram-local-aggregation failed (reward 0) because solve.sh From 41355f822385bb0245f4f1aff198064c2e6ab3a0 Mon Sep 17 00:00:00 2001 From: Alexander Rybkin Date: Tue, 27 Oct 2026 02:13:29 -0700 Subject: [PATCH 2/5] docs(docker): list the capability losses that really break a task step A low-port bind still works: Docker sets ip_unprivileged_port_start to 0 in the container's network namespace, so dropping NET_BIND_SERVICE never shows. Name what does fail under the new flags instead (KILL, NET_RAW, MKNOD, SYS_CHROOT) and the silent one: FSETID, a setgid bit dropped with exit 0. Signed-off-by: Alexander Rybkin --- ...0261026_130000_napetrov_docker-solver-hardening.md | 11 ++++++----- dokimi/docs/coding-harnesses.md | 11 +++++++---- dokimi/docs/contributing-terminal-bench-task.md | 7 ++++--- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/dokimi/changelog.d/20261026_130000_napetrov_docker-solver-hardening.md b/dokimi/changelog.d/20261026_130000_napetrov_docker-solver-hardening.md index 553f4af02..544fb2619 100644 --- a/dokimi/changelog.d/20261026_130000_napetrov_docker-solver-hardening.md +++ b/dokimi/changelog.d/20261026_130000_napetrov_docker-solver-hardening.md @@ -5,8 +5,9 @@ and `--pids-limit 32768`, and gets back only `CHOWN`, `DAC_OVERRIDE`, `FOWNER`, `SETUID` and `SETGID`, which the harness's own root steps (the agent's tree, `apt`, `setpriv`) need. A setuid binary in a task image can no longer raise its - privileges. A task whose solve or verifier needs another capability now fails at - that step: killing another user's process (`KILL`), binding a port below 1024 on a - host where `net.ipv4.ip_unprivileged_port_start` is 1024 (`NET_BIND_SERVICE`), or - raw sockets (`NET_RAW`). Before, the container ran with Docker's default - capabilities and no process limit. + privileges. Every other capability Docker grants by default is dropped, so a solve + or verifier step that needs one now fails: killing another user's process (`KILL`), + raw sockets (`NET_RAW`), `mknod` (`MKNOD`), `chroot` (`SYS_CHROOT`). Setting the + setgid bit on a file whose group root is not in (`FSETID`) exits 0 and leaves the + bit off. A port below 1024 still binds. Before, the container ran with Docker's + default capabilities and no process limit. diff --git a/dokimi/docs/coding-harnesses.md b/dokimi/docs/coding-harnesses.md index 49232d94d..56a8b2aa6 100644 --- a/dokimi/docs/coding-harnesses.md +++ b/dokimi/docs/coding-harnesses.md @@ -293,10 +293,13 @@ The result is in `metrics.isolation_probe` and `isolation_probe.json`: - **Capabilities.** Every `docker-*` task container, `docker-oracle` included, starts with `--cap-drop ALL`, `no-new-privileges` and `--pids-limit 32768`, and gets back only `CHOWN`, `DAC_OVERRIDE`, `FOWNER`, `SETUID` and `SETGID` - (`TASK_CONTAINER_HARDENING` in `harnesses/container_agent.py`). A solve or - verifier that needs another capability (`KILL` on another user's process, - `NET_BIND_SERVICE` below port 1024 where the host sets - `ip_unprivileged_port_start` to 1024, `NET_RAW`) fails at that step. + (`TASK_CONTAINER_HARDENING` in `harnesses/container_agent.py`). Every other + capability Docker grants by default is dropped, so a step that needs one + fails: signalling another user's process (`KILL`), a raw socket (`NET_RAW`), + `mknod` (`MKNOD`), `chroot` (`SYS_CHROOT`). Without `FSETID`, `chmod g+s` on + a file whose group root is not in exits 0 and leaves the bit off. A port + below 1024 still binds: Docker sets the container's + `ip_unprivileged_port_start` to 0. - **Network.** The container sits on a per-run `--internal` Docker network, which has no route out. The agent reaches the LLM through an allowlisting CONNECT proxy (`harnesses/egress_proxy.py`, stdlib only). It runs in a diff --git a/dokimi/docs/contributing-terminal-bench-task.md b/dokimi/docs/contributing-terminal-bench-task.md index b0dc90e08..35fbeb04d 100644 --- a/dokimi/docs/contributing-terminal-bench-task.md +++ b/dokimi/docs/contributing-terminal-bench-task.md @@ -186,9 +186,10 @@ spending model tokens. The solve and the verifier run in a container without Linux capabilities beyond `CHOWN`, `DAC_OVERRIDE`, `FOWNER`, `SETUID` and `SETGID`, so a setuid binary cannot raise its privileges and the process count is capped at 32768. -A step that signals another user's process, binds a port below 1024 (on a host -whose `ip_unprivileged_port_start` is 1024) or opens a raw socket fails; -`docker-oracle` will show it. See +A step that signals another user's process, opens a raw socket, or runs `mknod` +or `chroot` fails; `docker-oracle` will show it. One loss is silent: setting the +setgid bit on a file whose group root is not in exits 0 and leaves the bit off. +A port below 1024 still binds. See [What the agent can reach](coding-harnesses.md#what-the-agent-can-reach). ## 8 — Local verification From 0ea71736a2a992a8b13eb1190ac81c435404dbdc Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Tue, 27 Oct 2026 23:08:43 -0700 Subject: [PATCH 3/5] fix(docker): add --init to the task container, pin the flags' position in the test - TASK_CONTAINER_HARDENING gets --init, as repos/container.py has it: without it PID 1 (sleep) never reaps orphans. Probe: 3 orphans left 3 zombies and pids.current 6 without --init, 0 zombies and 4 with it. - --pids-limit reads repos.container.PIDS_LIMIT instead of a second 32768. - SETUID/SETGID are for task scripts that drop privileges (apt, su). The harness's own `exec -u 10001` works without them (probed on debian:trixie-slim with the other flags), so the comment says so. - Test: flags must come before the image, pids pinned to 32768. Moving the flags after `sleep` now fails the test. - Changelog: docker-* results from before may not be comparable. Signed-off-by: Nikolay Petrov --- ...130000_napetrov_docker-solver-hardening.md | 22 ++++++++++--------- dokimi/docs/coding-harnesses.md | 2 +- dokimi/dokimi/harnesses/container_agent.py | 14 +++++++----- dokimi/tests/test_docker_solver_harness.py | 8 +++++-- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/dokimi/changelog.d/20261026_130000_napetrov_docker-solver-hardening.md b/dokimi/changelog.d/20261026_130000_napetrov_docker-solver-hardening.md index 544fb2619..acc93c17f 100644 --- a/dokimi/changelog.d/20261026_130000_napetrov_docker-solver-hardening.md +++ b/dokimi/changelog.d/20261026_130000_napetrov_docker-solver-hardening.md @@ -1,13 +1,15 @@ ### Changed - **`docker-*` task containers are hardened like `repos` containers** — the - task container starts with `--cap-drop ALL`, `--security-opt no-new-privileges` - and `--pids-limit 32768`, and gets back only `CHOWN`, `DAC_OVERRIDE`, `FOWNER`, - `SETUID` and `SETGID`, which the harness's own root steps (the agent's tree, - `apt`, `setpriv`) need. A setuid binary in a task image can no longer raise its - privileges. Every other capability Docker grants by default is dropped, so a solve - or verifier step that needs one now fails: killing another user's process (`KILL`), - raw sockets (`NET_RAW`), `mknod` (`MKNOD`), `chroot` (`SYS_CHROOT`). Setting the - setgid bit on a file whose group root is not in (`FSETID`) exits 0 and leaves the - bit off. A port below 1024 still binds. Before, the container ran with Docker's - default capabilities and no process limit. + task container starts with `--init`, `--cap-drop ALL`, `--security-opt no-new-privileges` + and `--pids-limit 32768`, and gets back only `CHOWN`, `DAC_OVERRIDE` and `FOWNER` + (the harness's own root steps on the agent's tree) and `SETUID` and `SETGID` + (task scripts that drop privileges, such as apt's `_apt` sandbox). A setuid binary + in a task image can no longer raise its privileges. Every other capability Docker + grants by default is dropped, so a solve or verifier step that needs one now fails: + killing another user's process (`KILL`), raw sockets (`NET_RAW`), `mknod` + (`MKNOD`), `chroot` (`SYS_CHROOT`). Setting the setgid bit on a file whose group + root is not in (`FSETID`) exits 0 and leaves the bit off. A port below 1024 still + binds. Before, the container ran with Docker's default capabilities and no process + limit. `docker-*` results from before this change may not be comparable for a task + whose step needs a dropped capability: re-run it to compare. diff --git a/dokimi/docs/coding-harnesses.md b/dokimi/docs/coding-harnesses.md index 56a8b2aa6..bcffe994e 100644 --- a/dokimi/docs/coding-harnesses.md +++ b/dokimi/docs/coding-harnesses.md @@ -291,7 +291,7 @@ The result is in `metrics.isolation_probe` and `isolation_probe.json`: cgroup, so they count against `environment.memory_mb` (about 100 MB for a short session); the host launch ran outside it. - **Capabilities.** Every `docker-*` task container, `docker-oracle` included, - starts with `--cap-drop ALL`, `no-new-privileges` and `--pids-limit 32768`, + starts with `--init`, `--cap-drop ALL`, `no-new-privileges` and `--pids-limit 32768`, and gets back only `CHOWN`, `DAC_OVERRIDE`, `FOWNER`, `SETUID` and `SETGID` (`TASK_CONTAINER_HARDENING` in `harnesses/container_agent.py`). Every other capability Docker grants by default is dropped, so a step that needs one diff --git a/dokimi/dokimi/harnesses/container_agent.py b/dokimi/dokimi/harnesses/container_agent.py index f92a76eef..c0242c50b 100644 --- a/dokimi/dokimi/harnesses/container_agent.py +++ b/dokimi/dokimi/harnesses/container_agent.py @@ -49,6 +49,7 @@ from typing import Callable, Iterable, Optional from dokimi import process +from dokimi.repos.container import PIDS_LIMIT from . import egress_proxy from .agent_sandbox import ( @@ -83,12 +84,15 @@ _FILE_VARS = ("NODE_EXTRA_CA_CERTS", "SSL_CERT_FILE", "REQUESTS_CA_BUNDLE", "AWS_CA_BUNDLE", "GOOGLE_APPLICATION_CREDENTIALS", "AWS_SHARED_CREDENTIALS_FILE", "AWS_CONFIG_FILE") PROXY_PORT = 3128 -# The task container's hardening flags are the ones repos/container.py sets. Unlike there, root -# here gets back the few capabilities its own steps need: CHOWN and FOWNER (chown/chmod of the -# agent's tree), DAC_OVERRIDE (root writing the agent's files), SETUID/SETGID (apt, setpriv). -# Each was needed in the #229 smoke runs; KILL, NET_BIND_SERVICE and the rest stay dropped. +# The task container's hardening flags are the ones repos/container.py sets, --init included +# (PID 1 reaps the orphans a step leaves behind). Unlike there, root here gets back the few +# capabilities its own steps need: CHOWN and FOWNER (chown/chmod of the agent's tree) and +# DAC_OVERRIDE (root writing the agent's files). SETUID/SETGID are for task scripts that drop +# privileges (apt's _apt sandbox, su); the harness's own `exec -u` steps need neither. +# KILL, NET_BIND_SERVICE and the rest stay dropped. TASK_CONTAINER_HARDENING = ( - "--cap-drop", "ALL", "--security-opt", "no-new-privileges", "--pids-limit", "32768", + "--init", "--cap-drop", "ALL", "--security-opt", "no-new-privileges", + "--pids-limit", str(PIDS_LIMIT), "--cap-add", "CHOWN", "--cap-add", "DAC_OVERRIDE", "--cap-add", "FOWNER", "--cap-add", "SETUID", "--cap-add", "SETGID", ) diff --git a/dokimi/tests/test_docker_solver_harness.py b/dokimi/tests/test_docker_solver_harness.py index 100f95a18..fee40e30c 100644 --- a/dokimi/tests/test_docker_solver_harness.py +++ b/dokimi/tests/test_docker_solver_harness.py @@ -232,14 +232,18 @@ def test_start_container_malformed_memory_falls_back_instead_of_crashing(tmp_pat def test_start_container_drops_capabilities_and_bounds_pids(tmp_path, monkeypatch): # Regression (#229 P2-1a): the task container had no cap drop, no-new-privileges or # pids limit, unlike repos/container.py. Only the named capabilities the phase steps - # need (chown, DAC override, file owner, setuid/setgid for apt) are added back. + # need (chown, DAC override, file owner, setuid/setgid for task scripts) are added back. + # The flags must sit before the image: after it, docker hands them to `sleep`. task = load_task(_fixture_task(tmp_path)) run_call = next(c for c in _start(task, "container-6") if c[0] == "run") + image_at = run_call.index("img:latest") assert run_call[run_call.index("--cap-drop") + 1] == "ALL" assert run_call[run_call.index("--security-opt") + 1] == "no-new-privileges" - assert int(run_call[run_call.index("--pids-limit") + 1]) >= 4096 + assert run_call[run_call.index("--pids-limit") + 1] == "32768" added = {run_call[i + 1] for i, arg in enumerate(run_call) if arg == "--cap-add"} assert added == {"CHOWN", "DAC_OVERRIDE", "FOWNER", "SETUID", "SETGID"} + for flag in ("--init", "--cap-drop", "--security-opt", "--pids-limit", "--cap-add"): + assert all(i < image_at for i, arg in enumerate(run_call) if arg == flag) def test_solve_oracle_copies_the_whole_solution_directory(tmp_path, monkeypatch): From c6905636e147d060d108617c8fbb25ef8720e67c Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Tue, 27 Oct 2026 23:10:50 -0700 Subject: [PATCH 4/5] test(docker): the hardening flags must be present, not only ordered before the image The position check passed for a missing flag: all() over no occurrences is True. Deleting --init from TASK_CONTAINER_HARDENING left the test green. Signed-off-by: Nikolay Petrov --- dokimi/tests/test_docker_solver_harness.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dokimi/tests/test_docker_solver_harness.py b/dokimi/tests/test_docker_solver_harness.py index fee40e30c..6f790223a 100644 --- a/dokimi/tests/test_docker_solver_harness.py +++ b/dokimi/tests/test_docker_solver_harness.py @@ -243,6 +243,7 @@ def test_start_container_drops_capabilities_and_bounds_pids(tmp_path, monkeypatc added = {run_call[i + 1] for i, arg in enumerate(run_call) if arg == "--cap-add"} assert added == {"CHOWN", "DAC_OVERRIDE", "FOWNER", "SETUID", "SETGID"} for flag in ("--init", "--cap-drop", "--security-opt", "--pids-limit", "--cap-add"): + assert flag in run_call assert all(i < image_at for i, arg in enumerate(run_call) if arg == flag) From b1340c203a1ca592596679aa0f2f361fc3ee421b Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Wed, 28 Oct 2026 10:54:13 +0000 Subject: [PATCH 5/5] test(docker): reject extra privilege flags in the task container Signed-off-by: Anton Volkov --- dokimi/tests/test_docker_solver_harness.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dokimi/tests/test_docker_solver_harness.py b/dokimi/tests/test_docker_solver_harness.py index 6f790223a..2e922bb0b 100644 --- a/dokimi/tests/test_docker_solver_harness.py +++ b/dokimi/tests/test_docker_solver_harness.py @@ -245,6 +245,8 @@ def test_start_container_drops_capabilities_and_bounds_pids(tmp_path, monkeypatc for flag in ("--init", "--cap-drop", "--security-opt", "--pids-limit", "--cap-add"): assert flag in run_call assert all(i < image_at for i, arg in enumerate(run_call) if arg == flag) + assert "--privileged" not in run_call + assert [run_call[i + 1] for i, arg in enumerate(run_call) if arg == "--security-opt"] == ["no-new-privileges"] def test_solve_oracle_copies_the_whole_solution_directory(tmp_path, monkeypatch):