mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #300] [MERGED] test(groom): prove name resolution is dead in the sandbox, non-vacuously #302
Labels
No labels
cursor-review-catalog-drift
groom
groom
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/github-workflows#302
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/Comfy-Org/github-workflows/pull/300
Author: @mattmillerai
Created: 9/16/2026
Status: ✅ Merged
Merged: 9/17/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-4304-groom-sandbox-net-bridge📝 Commits (4)
c6f9049test(groom): prove name resolution is dead in the sandbox, non-vacuously977b7cctest(groom): key the 7d netns proof on the jail's own /proc, not a curl exit codec5a8cebtest(groom): key 7d part (ii) on netns IDENTITY, and close the round-2 gapsae3c6fdMerge branch 'main' into matt/be-4304-groom-sandbox-net-bridge📊 Changes
2 files changed (+188 additions, -10 deletions)
View changed files
📝
.github/groom/README.md(+40 -8)📝
.github/groom/tests/sandbox-tests.sh(+148 -2)📄 Description
ELI-5
The groom agent runs in a locked box with no way onto the network — the only thing it can reach is a unix socket that proxies to the API. We already had tests proving it can't dial any IP address. But every one of those tests dials a raw IP on purpose, so none of them ever asked the box to look up a name. This adds that test. The interesting part is that the obvious version of it is a lie: it passes even when the box is fully connected to the network, so it would never catch a regression. This adds the version that actually goes red.
What this is
The confinement work (isolated netns, unix-socket broker, in-jail TCP→UDS bridge, all three groom agent steps wired to it) already landed. Reading the tree against the acceptance criteria, everything was in place except one leg of the proof, which is what this PR adds.
Criterion-by-criterion, against the current default branch:
broker.mjstakes a socket path asargv[2]--share-netfor a full isolated netns, broker socket bind-mounted in, TCP→UDS bridge soANTHROPIC_BASE_URLkeeps workingagent-sandbox.sh --uds+--unshare-all, bridged byjail-shim.mjsThe finding: the obvious DNS check is vacuous
My first draft was the one-liner — resolve a name in the jail, assert it fails. I ran it against a shared-netns control before trusting it, and it passed there too.
The reason is that
/etc/resolv.confis a symlink into/run, and the sandbox mounts/etcread-only but deliberately does not mount/run(mounting it would be a confinement regression in its own right). So the resolver has no nameserver configured no matter what the network namespace looks like. A bare "DNS fails" assertion is therefore green whether the jail is isolated or wide open — it could never catch someone putting the sandbox back on a shared network, which is precisely the regression it would exist to catch.So §7d asserts both halves:
Resolution fails, on resolver-specific exit codes rather than a bare non-zero:
getent2 is "key not found" (a missing binary is 127) andcurl6 isCURLE_COULDNT_RESOLVE_HOST(a connect failure is 7, a timeout 28, a missing binary 127). This is the same false-pass class the existing §7a/7b/7c tool-presence guard was written for, closed here by construction instead. Part (i) is only evidence about the sandbox if the name is live off-jail, so a host-side control (bounded bytimeout 5) gates it: when this host cannot resolve the name either, the assertions still run and still have to hold, but part (i) is reportedSKIPrather than counted as proof.The netns is empty — read out of the jail's own
/proc. The wrapper passes--proc /proc, a fresh procfs mount inside the new netns, so/proc/net/devand/proc/net/routedescribe the jail's network: the first must listloand nothing else, the second must carry no default route. This is the half only an isolated netns can produce, and it is what makes the section a real proof.Review's first pass at this keyed part 2 on an exact
curl7 to a hardcoded nameserver address instead, and that was wrong — see the thread on §7d.curl7 isCURLE_COULDNT_CONNECT, which equally covers ECONNREFUSED, ENETUNREACH and a firewall REJECT, so a fully shared netns on a filtered or offline host returns 7 too and the assertion went green on exactly the regression it exists to catch; it was TCP-only besides. The/procfacts cannot be faked by host network state, need no egress of any kind, and cover a UDP/53 path as well as TCP — no route is no route, for any protocol. The 1.1.1.1:53 probe is kept as behavioral confirmation but relaxed to "did not succeed"; its exact code is no longer load-bearing.Verification
Run on a Linux host with unprivileged user namespaces, against the real
agent-sandbox.sh:shellcheck -x .github/groom/*.sh .github/groom/tests/*.sh— clean.python3 -m unittest discover -s .github/groom/tests -p 'test_*.py'— 398 tests, OK.python3 .github/workflow-pins/check_workflow_pins.py— 11 workflows, 0 defaults, all ref checkouts guarded.python3 .github/agents-md-integrity/check_agents_md.py --root .— passed (2 pre-existing warnings: AGENTS.md is 155 lines vs the 150 aspirational target, and no CODEOWNERS; both predate this branch).sandbox-tests.sh§1–§4, §7a–§7e — all PASS, including §7d.Empirical falsification of the new negative assertion. This diff asserts a capability is absent, which is exactly the claim a self-authored test cannot establish on its own, so I went and attempted the capability rather than assuming it:
getentpath,curl's own resolver, and a direct connect to a nameserver address. All three fail; none of them is reachable by any route. The jail's own/proc/net/devlistsloalone and its/proc/net/routeis empty, so there is no route for any protocol to take.--share-net. It correctly goes red:jail netns has non-loopback interface(s): ens4 docker0 tailscale0 — the netns is SHARED with the host, so nothing here proves isolation.curl-7 assertion was separately shown false-passable, which is why part 2 was rekeyed: in a fully shared, fully routable netns, a connect to any closed port returns 7 (ECONNREFUSED). Exit 7 therefore does not imply "no route", and an assertion demanding it cannot distinguish an isolated netns from a shared one.--unshare-allexits 0.Judgment calls
socatfor the in-jail bridge and--net-bridge//run/groom/broker.sockfor the flag and mount path. What shipped usesjail-shim.mjs(node, already present — noapt-get install), the flag--uds, and/run/broker.sock. Those choices are not mine and are not changed here; I mention them only so the plan's wording isn't read as unimplemented.ci-groom.yml. That reference is stale — thesandbox-testsjob lives intest-groom-scripts.yml, path-filtered on.github/groom/**, so the new section is picked up with no workflow edit.Skip-caller-bump: truetrailer. This PR is a test file plus a README, neither of which a consumer ever loads at run time, so the caller bump it will trigger is a no-op SHA churn. But the trailer is documented for comment/docs-only edits and a test file is not literally that, and the guidance is to leave it off when in doubt. Left off deliberately; a reviewer who reads it as docs-only can add it.--max-timeon the in-jail probes is a backstop, not margin. The original comment oversold it; corrected in review. It is not sized to outlast a retrying resolver — glibc's defaults (timeout:5, attempts:2) burn exactly 10s on one blackholing nameserver — but every failure here is immediate in practice, and a clipped lookup exits 28 and fails loud with the code printed rather than passing silently.Residual
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1— the suppression the criterion names as the expected mitigation — and that the netns has been isolated on the default branch since the phase that shipped it, so any such stall would already be observable in existing groom run history rather than being introduced by this PR. Someone with access to a groom run should still time one dry-run against a recent pre-isolation run and confirm per-turn latency is unchanged; if it is not, the mitigation to investigate is residual non-API traffic from the CLI timing out with no route, and the documented fallback is to stop-ship the isolation and keep the earlier phases as the baseline.sandbox-tests.sh§5 and §6 were not run on my host and are unchanged by this diff. They neednoderesolvable under/usr(the jail'sPATHis/usr/local/bin:/usr/bin:/bin); on my hostnodelives outside/usr, so both fail there. I confirmed this is pre-existing and not caused by this branch by running the unmodified suite from the base commit and getting the identical failure at the identical point. CI runners havenodeunder/usr/local/bin, which is inside the read-only/usrbind, so both sections exercise normally there — but I have not observed them pass on this branch, only on CI's behalf.Part 2 of §7d can itself become vacuous on a host with no egress at all.Resolved in review. This was real and worse than stated — it applied to any filtered host, not just a fully offline one. Part 2 no longer depends on host reachability at all: it reads the jail's interface list and route table out of/proc, which no host network state can fake.Provenance
--share-netjail, exit 1, naming ens4/docker0/tailscale0); the superseded exact-curl-7 form was separately demonstrated false-passable in a shared, routable netns (exit 7 on any closed port), which is the measurement behind the review rekey.agent-sandbox.sh)🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.
paths:positive #320