Repository navigation
Reconcile orphaned native WSL runtime containers - #125
Conversation
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Coordinator failure can discard lease evidence, and duplicate run identities can be reconciled ambiguously.
Review effort: Balanced
Findings: 3
Open (3)
What changed in this PR
Adds proof-bound recovery for orphaned native-WSL runtime containers while keeping production activation gated.
Changes:
- Adds per-run leases and namespace coordination.
- Adds automatic reconciliation and
cb wsl cleanup. - Updates tests, help text, and WSL documentation.
| File | Description |
|---|---|
| README.md | Documents WSL cleanup and activation status. |
| main.go | Dispatches and documents cleanup commands. |
| internal/wslrun/runner.go | Integrates run leases into execution. |
| internal/wslrun/runner_test.go | Tests lease lifecycle ordering. |
| internal/wslrun/run_linux.go | Wires production reconciliation. |
| internal/wslrun/plan.go | Carries layout into execution. |
| internal/wslreconcile/reconcile.go | Implements reconciliation and guards. |
| internal/wslreconcile/reconcile_test.go | Tests reconciliation behavior. |
| internal/wslreconcile/production_other.go | Adds non-Linux fail-closed wiring. |
| internal/wslreconcile/production_linux.go | Connects Linux Docker operations. |
| internal/wslreconcile/lease_other.go | Adds non-Linux lease stubs. |
| internal/wslreconcile/lease_linux.go | Implements file locks and leases. |
| internal/wslreconcile/lease_linux_test.go | Tests lease path safety. |
| internal/wslreconcile/command.go | Implements the cleanup CLI. |
| internal/wslinstall/install.go | Updates activation status output. |
| internal/wsldocker/reconcile.go | Adds candidate discovery and proof. |
| internal/wsldocker/reconcile_test.go | Tests discovery and ownership proof. |
| internal/hostenv/hostenv.go | Updates WSL gate messaging. |
| docs/wsl.md | Documents reconciliation lifecycle. |
| docs/wsl-process-contract.md | Defines lease and recovery semantics. |
| docs/security-model.md | Updates WSL cleanup guarantees. |
| docs/roadmap-implementation-requirements.md | Records reconciliation completion. |
| docs/roadmap-decisions.md | Updates WSL roadmap status. |
| docs/architecture.md | Describes reconciliation architecture. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
CherylSnowVeil
left a comment
There was a problem hiding this comment.
Reviewed the full diff at b718b68 (current head) against main: internal/wslreconcile (coordinator/lease machinery, reconcile pass, cb wsl cleanup command), internal/wsldocker/reconcile.go discovery+proof, the wslrun create-to-lease wiring, main.go dispatch, and the docs. Also traced the surrounding contracts in wsldocker (create, inspect, remove, signal, wait), wslfs layout validation, and hostenv layout derivation.
Findings
-
[nit] Stale activation-gate text in
cb helpoutput —main.go(usage footer, "Native WSL2" section) still printsactivation awaits orphan reconciliation and real Docker Desktop qualification. Every other gate message was updated to the new wording ("native state commands, integration coverage and real qualification":internal/hostenv/hostenv.go,internal/wslinstall/install.go, README, docs), socb helpnow contradicts the rest of the user-facing surface and implies this PR's feature is still missing. -
[nit]
cb wslusage error omitscleanup—internal/wslinstall/install.gorun()returnsusage: cb wsl prepare (--check | --apply) | cb wsl install (--check | --apply)for any unrecognizedcb wsl <cmd>. Now thatrunWSLroutescleanupbeforewslinstall.Run, the usage hint should list it. -
[nit] Missing symmetric probe-contract guard —
internal/wslreconcile/reconcile.goreconcileLockedvalidatesleaseActive-with-handle andleaseOrphaned-without-handle, but aleaseMissingresult carrying a non-nilleasehandle is accepted and classified as an orphan whose lease then getsRemove()d under--apply. ProductionprobeFileLeasecan't produce that combination, so this is only a gap in the fail-closed seam validation the surrounding checks already enforce — a symmetricstatus == leaseMissing && heldLease != nilrejection would close it.
Verified design/correctness points (no issues)
- Create-to-lease race:
beginRunholds the state-directory flock acrossreconcileLockedand container creation untilAdoptpublishes the locked lease — a coordinator-holding reconciler can never observe a created-but-unleased container. Verified defer ordering inexecuteTool: the cleanup defer is registered after theguard.Closedefer, so container cleanup runs first andcontainerGoneis correctly propagated toClose(containerGone), which removes the lease path only after proof-bound removal succeeds. - Proof-before-mutation: all candidate proofs and lease probes complete before the first signal/remove; each candidate is re-inspected and its full ID, labels, retention mode, and stdio contract re-verified (
requireOwnedContainer+ retention/stdio checks). Ambiguous state fails closed. - Orphan lifecycle: running orphans get SIGKILL → wait → the existing non-force proof-bound removal; 404/409 completion races are tolerated appropriately;
RemoveContainerre-proves and refuses removal of a running container. - Lease file hardening:
O_NOFOLLOWopenat under the 0700 state dir, owner/mode/nlink/device checks, dev/ino revalidation before unlink, and lease retention when container removal is unproven all match the documented contract.--checkperforms no Docker mutation and no filesystem writes (flock on the dir/lease files only). - Dispatch:
cb wsl cleanupcomposes the same fixed-layout validation (wslfs.Check, complete layout required) as install/prepare, fails closed off-Linux and on incomplete deps, and is documented in README/wsl.md/process-contract/security-model consistently. - Cross-frontend isolation: the
cb.wsl_namespacelabel is distro/machine/UID-bound; Windows-side containers never carrycb.kind=run+ the namespace label, so discovery can't collide. - Tests: ordering tests cover begin→create→adopt→remove→close-lease sequencing, read-only
--check, proof-before-mutation, 404/409 completion races, active-lease preservation, and lease symlink/mode rejection. Coverage matches the risk surface.
Overall: a careful, fail-closed implementation consistent with the codebase's proof-bound conventions. Only cosmetic nits above; nothing blocking from this review. CI is green.
|
Addressed all three CherylSnowVeil nits at current head
Validation on
Please re-review the current head when convenient. |
CherylSnowVeil
left a comment
There was a problem hiding this comment.
Re-review at dbcf8a9
Validation of previous findings
All three findings from the earlier review at b718b68 are fixed and verified against the current head:
- [nit] Stale activation-gate text in
cb help— fixed.main.gonow prints "activation awaits native state commands, integration coverage and real Docker Desktop qualification", consistent withhostenv.go, theinstallplan output, README and docs. - [nit]
cb wslusage error omitscleanup— fixed.internal/wslinstall/install.gousage now listscb wsl cleanup (--check | --apply), covered byTestUsageIncludesCleanupCommand. - [nit] Missing symmetric probe-contract guard — fixed.
reconcileLockednow rejectsleaseMissingwith a non-nil handle before any mutation;TestReconcileRejectsMissingLeaseWithHandleproves the handle is closed and no mutation occurs.
Fresh review of the current revision
Re-reviewed the complete current diff: internal/wslreconcile (coordinator/lease machinery, reconcile pass, cb wsl cleanup command), internal/wsldocker/reconcile.go discovery+proof, the wslrun begin→create→adopt→cleanup wiring, main.go dispatch, docs, and new tests — plus the surrounding wsldocker create/inspect/remove/signal/wait and wslfs layout contracts they rely on.
Findings
- [nit] Orphaned lease residue is never reaped once its container is gone —
internal/wslreconcile/reconcile.go. Lease probing is driven by container discovery, soprobeLeaseonly runs for run IDs that still have a matching container. The paths that deliberately retain the lease pathname after the container is gone — coordinator-reacquire failure inRunGuard.Close(reconcile.go:176-188), unproven absence afterreconcileOrphan(reconcile.go:287-289), orfileLease.Remove()refusal on a replaced inode (lease_linux.go:207-209) — leaverun-*.leasefiles that no later pass ever revisits. SinceAdoptis strictly create-then-lease, a lease implies a container once existed, so while the coordinator is held the reconcile pass could safely enumeraterun-*.leasefiles and remove orphaned ones with no matching container. The consequence is only unbounded zero-byte-file accumulation across repeated ambiguous failures — consistent with the documented "recovery evidence" intent, but nothing ever consumes that evidence.
Verified (no issues)
- Create-to-lease race:
beginRunholds the state-dir flock acrossreconcileLocked, container creation andAdoptpublication; a coordinator-holding reconciler cannot observe a created-but-unleased container. Defer ordering inexecuteToolruns container cleanup beforeguard.Close(containerGone), so the lease path is removed only after proof-bound removal succeeds. - Proof-before-mutation: all candidate re-inspections and lease probes complete before the first signal/remove; invalid or inconsistent probe results (unknown status, missing-with-handle, active-with-handle, orphaned-without-handle) fail closed.
- Orphan lifecycle: running orphans get SIGKILL → wait → proof-bound non-force
RemoveContainer, which re-inspects, re-proves labels/retention and refuses removal of a running container; 404/409 completion races tolerated. - Lease hardening:
O_NOFOLLOW+openatunder the 0700 state dir, owner/mode/nlink/device checks, dev/ino revalidation before unlink, refusal on replaced leases. - Guard close ordering:
RunGuard.Close(containerGone=true)reacquires the coordinator and holds it acrossRemove+Close; reacquire failure retains the locked lease pathname as evidence instead of unlinking it. - Ambiguity rejection: discovery fails closed on duplicate container IDs or duplicate run IDs before any proof or mutation; the dedicated discovery transport asserts its own 4 MiB bound above the shared 1 MiB control limit (Linux test).
- Dispatch and gating:
cb wsl cleanupis routed ahead of the host gate alongside prepare/install, requires the complete fixed layout (wslfs.Check+ zero missing directories), fails closed off-Linux via empty production deps pluscurrentLayouterror, and--checkperforms no Docker mutation. - Docs/help consistency: README,
wsl.md,wsl-process-contract.md,security-model.md,architecture.mdand the roadmap docs now consistently describe reconciliation as wired and name state commands/integration/qualification as the remaining gates. - Tests: ordering (begin→create→adopt→remove→close-lease), read-only
--check, proof-before-mutation, 404/409 races, active-lease preservation, unsafe lease rejection, coordinator context handling, duplicate run-ID rejection, and report printing.
Nothing blocking from this review. CI is green.
|
Addressed the fresh re-review nit in The coordinator-held pass now enumerates only strict managed Validation:
The commit is intentionally unsigned because commit signing is currently disabled. Please re-review the current head when convenient. |
CherylSnowVeil
left a comment
There was a problem hiding this comment.
Re-review at 1b0f538
Validation of previous findings
- [nit] Orphaned lease residue is never reaped once its container is gone (flagged at dbcf8a9) — fixed and verified.
reconcileLockednow callsdeps.discoverLeases(discoverFileLeaseson Linux) under the held coordinator, validates each enumerated name by round-trippingleaseName, rejects duplicate run identities, and probes every lease that has no matching retained container before the first mutation.--applyremoves a detached lease only viaitem.lease.Remove()— which still re-proves dev/ino before unlink — and only for unlocked leases; locked lease-only records are classified active and preserved. Lease-missing TOCTOU results are skipped, all invalid status/handle combinations fail closed, and report/CLI output now surfacelease-active/lease-residue/lease-reapedwith totals. Regression coverage includes check/apply reaping, active preservation, proof-before-container-mutation ordering, invalid discovery, invalid probe contracts, and real directory enumeration. - The three earlier findings (stale
cb helpgate text,cb wslusage omission, asymmetric probe-contract guard) remain fixed at this head.
Fresh review of the current revision
Re-reviewed the complete current diff: internal/wslreconcile (coordinator/lease machinery, detached-lease enumeration and reaping, cb wsl cleanup), internal/wsldocker/reconcile.go discovery+proof, the wslrun begin→create→adopt→cleanup wiring, main.go dispatch, docs, and tests — plus the surrounding wsldocker create/inspect/remove/signal/wait and wslfs/wslvolume contracts.
Verified (no issues)
- Detached-lease reaping safety: the pass only runs while the namespace coordinator is held, so no cooperating process can be between container-create and lease publication. A detached unlocked lease therefore always denotes residue; reaping it cannot strand a live run. Locked detached leases (e.g., a run whose container was externally removed, or a
RunGuard.Closestill blocking on coordinator reacquire) are classified active and preserved. - Proof-before-mutation preserved: all container proofs/probes and all detached-lease probes complete before the first signal/remove/unlink; every invalid probe contract (unknown status, missing/active with handle, orphaned without handle) fails closed.
- Lease replacement race:
fileLease.Remove()re-opens the path and refuses on dev/ino mismatch; Remove+Close complete inside the coordinator hold, so a cooperating reconciler cannot observe a replacement pathname on a still-locked inode. - Guard lifecycle:
beginRunholds the coordinator acrossreconcileLocked;Adoptkeeps a failed coordinator-release's locked lease attached for the proven-cleanup path;Close(containerGone)reacquires the coordinator before Remove+Close, and retains the pathname (unlocked, as evidence) on reacquire or removal failure — now correctly consumed by the detached reaper on the next pass. - Create-to-lease race:
executeTooldefers run container cleanup beforeguard.Close(containerGone); the lease path is removed only after proof-bound removal confirms absence, and the deferredAdopt-failure path is covered. - Discovery integrity: duplicate container IDs and duplicate run IDs are rejected before any proof or mutation; the dedicated discovery transport passes the declared 4 MiB bound (asserted above the shared 1 MiB limit by the Linux test).
- Dispatch and gating:
cb wsl cleanupis routed ahead of the host gate, requires the complete exact layout (wslfs.Check, zero missing dirs), fails closed off-Linux via empty production deps plus layout error, and--checkperforms no Docker mutation or filesystem write. - Docs/help consistency: README,
wsl.md,wsl-process-contract.md,security-model.md,architecture.mdand roadmap docs consistently describe the lease evidence lifecycle, detached reaping and the remaining activation gates. - Tests: ordering, read-only check, proof-before-mutation, 404/409 completion races, active-lease preservation, unsafe lease rejection, detached reaping in both modes, invalid discovery/probe contracts, and coordinator context handling match the risk surface.
CI is green across Linux, Windows and Windows ARM64. The current revision was reviewed in full and no actionable issues were found.

Summary
--checkcb wsl cleanup --check|--applyrecoverySafety contract
Validation
go test -count=50 ./internal/wslreconcile ./internal/wslrun ./internal/wsldockergo test -race ./...go vet ./...-trimpath -buildvcs=false -ldflags "-s -w -X main.version=v2.0.0-citest"cb.exe version=>container-bin v2.0.0-citest