Repository navigation
feat: probe nsjail with a real jail and add a gVisor compat mode - #11865
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…den the probe dir Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deploying windmill with
|
| Latest commit: |
cf220c2
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://ff3cc6e0.windmill.pages.dev |
| Branch Preview URL: | https://nsjail-gvisor-compat.windmill.pages.dev |
…imits from the worker Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/review |
|
Claude finished @rubenfiszel's task in 1m 17s —— View job Good to merge Review: probe nsjail with a real jail, plus a gVisor compat mode
I found no P0, P1 or P2 issues. These are the points I checked:
Optional, non-blocking: AGENTS.md complianceComments state constraints rather than narrating the change, and the new module has a Test coverage
|
Codex Reviewcc @rubenfiszel Should address issues before merging: DuckDB fails open after a failed nsjail probe
Test coverageNo committed Rust unit or worker integration tests cover the new probe/config rewriting. Add a regression covering DuckDB’s response to a failed probe when sandboxing is required. I built the Dockerfile’s nsjail stage successfully, ran the probe successfully under privileged runc, confirmed it fails in a restricted container, and passed two isolated Rust checks covering config rewriting, idempotence, relative paths, and missing files. I did not run full worker integration. The PR author reports the broader gVisor job, capability, and limit checks. No automated tests are expected for the Docker/config wiring itself. Full final-image builds remain unverified. Exercise dbt’s captured phases and DuckDB jobs under gVisor with compatibility enabled; both should complete using their jail profiles. With compatibility disabled and the probe failing, a worker explicitly configured for nsjail must reject DuckDB execution. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/review |
|
Claude finished @rubenfiszel's task in 1m 11s —— View job Good to merge Re-review after
|
Codex ReviewGood to merge The prior DuckDB fail-open finding is resolved: Test coverageThe added Rust unit test covers config-rewrite idempotence and duplicate-field removal. I compiled and ran it independently with No automated tests are expected for the Docker/config wiring itself. Remaining manual verification: under gVisor with compatibility enabled, run DuckDB and dbt jobs, including dbt’s captured phases, and confirm they complete inside their jails. With compatibility disabled and the probe failing, confirm a worker configured for nsjail rejects DuckDB execution. Build a full affected image to verify the final packaging and startup probe. |
Pi ReviewGood to merge. I reviewed the cumulative diff ( Verified points:
Considered and dismissed
Test coverage
|
|
I'll analyze this and get back to you. |

Summary
Two related changes to how workers use nsjail.
A real startup probe. The worker used to decide that nsjail was available by running
nsjail --help. That succeeds in any container that ships the binary, even when nsjail cannot build a jail there: an unprivileged container, a host that restricts unprivileged user namespaces, or a gVisor (runsc) sandbox. The worker then reportedjob_isolation=nsjailwhile every jailed job failed. The probe now runs/bin/truein a jail with the same namespaces, rlimit handling and user-namespace switch as the job profiles (nsjail/probe.config.proto).NSJAIL_AVAILABLEisNone. The worker reports the isolation it really has (unshareornone) and logs nsjail's own error. That is the same fallback as when nsjail is missing: jobs that require nsjail still fail closed.infowhenDISABLE_NSJAILis set (the default), since most such workers never jail anything, and aterrorotherwise.NSJAIL_GVISOR_COMPAT=true, opt-in, for workers under gVisor such as GKE Sandbox (RuntimeClass gvisor). Stock nsjail cannot create a jail on released gVisor (release-20261012.0), for four reasons:clone(CLONE_NEWCGROUP)returns EINVAL until a cgroup2 fs has been mounted.prlimit64(RLIMIT_MSGQUEUE)returns EPERM for any value: the limit is not in gVisor'ssetableLimits(setrlimit(RLIMIT_MSGQUEUE) fails with EPERM, even to its current value google/gvisor#15944).prctl(PR_SET_SECUREBITS, KEEP_CAPS|NO_SETUID_FIXUP)returns EPERM. Fixed in gVisor by kernel: implement SECBIT_NO_SETUID_FIXUP google/gvisor#15130, not yet released.clone_newnet(no_networkjobs),rtnl_link_alloc_cache()fails withMessage sequence number mismatch. gVisor acks anNLM_F_ACKdump beforeNLMSG_DONE, unlike Linux, and libnl rejects that (netlink: dump requests with NLM_F_ACK are acked before NLMSG_DONE, which breaks libnl caches google/gvisor#15945).nsjail patch (
docker/nsjail-patches/gvisor-compat.patch, applied withgit applyin the nsjail stage ofDockerfile,docker/DockerfileSlimanddocker/DockerfileSlimEe). It adds one config field and command-line flag,gvisor_compat(default false). When it is false, nsjail behaves exactly as upstream. When it is true:iface_own, no MACVLAN). The cache is only used for that.setrlimit(RLIMIT_MSGQUEUE)EPERM is a warning. Every other rlimit stays fatal.PR_SET_SECUREBITSEPERM is tolerated only when the real, effective and saved UIDs already equal the jail's UID. Thensetresuid()changes nothing, so the bits would have had no effect. A jail that changes UID still fails.Windmill side. In compat mode the worker rewrites each nsjail config right before nsjail starts, adding
clone_newcgroup: falseandgvisor_compat: true. The rewrite happens at spawn time rather than in the 24 profiles, because those are rendered by about 36 separate.replacechains with no shared renderer. The spawn is the one place they all pass through:start_child_process, plus the two spawns that bypass it, the DuckDB runner and dbt'srun_captured. The probe takes the same path. A spawn that missed the rewrite would fail under gVisor rather than run with less isolation.Changes
nsjail/probe.config.proto,probe_nsjailandNSJAIL_AVAILABLEinworker.rs: the startup probe.nsjail_compat.rs, plusapply_nsjail_gvisor_compatincommon.rs, called fromstart_child_process,duckdb_runner::spawn_runneranddbt_executor::run_captured.NSJAIL_GVISOR_COMPATis a standaloneLazyLock, becauseworker.rs'slazy_static!block is at its macro recursion limit.docker/nsjail-patches/gvisor-compat.patch, applied in the three Dockerfiles that build nsjail.Test plan
Verified live on a k3s 1.35 VM: server under runc, worker pod under runc or under
RuntimeClass gvisorwith runscrelease-20261012.0, the current release. The worker binary was built from this branch. nsjail was built from the pinned commit with the patch applied, in adebian:trixie-slimstage identical to the Dockerfiles'. Jobs were bash, python3, bun, python with a pip dependency (lock + download profiles), andno_networkbash.nsjail;no_networkleaves onlyloand egress fails.DISABLE_NUSER=true+SYS_ADMIN/SETPCAP: bash, python3 andno_networkpass.CapInh/CapPrm/CapEff/CapBnd/CapAmbare all0000000000000000andNoNewPrivs: 1in every case. Verbose nsjail on released runsc shows all three tolerances firing, with the full bounding set still dropped.--user 1000and no user namespace fails onPR_SET_SECUREBITSunder compat./proc/self/limitsmatch runc (AS 4 GiB, CPU 1000, FSIZE 1000 MiB, NOFILE 10000, CORE 0, msgqueue 819200). MEMLOCK follows the pod's own soft limit, which nsjail pins, and gVisor's pod default is lower.clone(...CLONE_NEWCGROUP...)EINVAL. The worker reportsnone(unsharewithENABLE_UNSHARE_PID=true), and jailed jobs fail closed.kernel.apparmor_restrict_unprivileged_userns=1, non-root runc worker: the probe reportsnoneinstead ofnsjail. This is the existing silent failure the probe now surfaces.SELECT 42, and a DuckDB query globbing/tmp/windmill/*,/proc/[0-9]*and/home/*from inside the jail, which returns 0 each, as on runc. Also checked dbt (dbt-core 1.12 + dbt-duckdb, installed at runtime)buildwith two models (PASS=2), andshow, which goes throughrun_captured. The same set passes on runc with compat off.none,DISABLE_NSJAIL=false), every DuckDB and dbt job fails with nsjail's launch error and none returns rows. DuckDB is no longer run outside the jail.Docs: windmill-labs/windmilldocs PR to follow (a gVisor section on the security/isolation page, plus the env var).
Upstream: the gVisor gaps are filed as google/gvisor#15944 and google/gvisor#15945. The Linux-neutral form of the netlink and msgqueue changes is proposed upstream in google/nsjail#365; the patch can be dropped once that lands and gVisor ships #15130.
🤖 Generated with Claude Code