Skip to content

review: whole-repo audit after #447 — eleven bugs fixed with hardware negative controls - #455

Merged
CMGS merged 23 commits into
mainfrom
review/post-447
Nov 5, 2026
Merged

CMGS merged 23 commits into
mainfrom
review/post-447

Conversation

@CMGS

@CMGS CMGS commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Verdict

Whole-repo audit of main at 88e6aa5 (live move #445 and interrupted-capture settle #447 included). Eleven bugs found and fixed, each in its own commit with a regression test that fails on the old code. Every fixed path ran on hardware against the old binary as a negative control, and the old binary showed each bad outcome; the final head passed the whole hardware regression. Codex: four rounds, one P1 and one P2 (from a separate review) found and fixed, then no findings for 88e6aa5d..c4b161bb (963b2fc after it only renames a variable for govet). Gates are green on both platforms. The hygiene ledger is closed: 0 files due in every lens.

Bugs fixed

commit bug hardware (old binary → new)
8d0d678 Without archive_failover, a release during a live move answered 204 while the move still committed: the released sandbox ran and billed on the target with its old token M1c: old 204 + claim live on target; new 503
a5dd3fb A settle that failed the guest returned success: a wake handed out the dead guest's socket, a hibernate captured the failed claim R2: old 502 / vm hibernate on a failed claim; new 409, no hibernate
8989efb A background settle that another caller already settled cleared ReaderSafe, so reads woke a reader-safe claim R4: old warning + vm restore; new served by the reader VM
dcb197a When snapshot list failed at start, reconcile adopted a hibernate intent whose snapshot never landed: every wake failed R1: old 500 "snapshot not found" forever; new settles and liveness restarts
be5e432 Target admission skipped the cocoon migrate floor: a move to an older cocoon streamed every round and paused the guest before failing M3: old 3 rounds streamed; new 409 in 0.04 s, no rounds
2b6dd05 A target that started draining during the rounds still took the commit, after evacuation had listed its claims M2: old claim lands on the draining node; new 409, claim stays on source
97437c9 An archive whose claim woke just before the export snapshotted the running guest (a pause the client saw, a full upload, then a discarded checkpoint) R3: old save + export 3/3; new none 3/3
b1b7fd8 A failed vm migrate abort was reported as 503 "move registry down" M4: old 503; new 502 and the abort retries through
7db8477 A target restarted after a host crash between commit and start treated cocoon's cold boot as the moved guest: no restart recorded, guest env not rewritten M5: old restarts 0, env missing; new restarts 1, vmm_restart, env back
58204dc + 8b580a9 A source restarted mid-move aborted without dropping the target's reservation (a following move answered 429 for ~2 min); the fix's first form could reach an uninstalled move transport during reconcile (Codex P1), so main now installs it before reconcile M7: old 429 for 121 s until the idle drop; new 200 on the first try, the restart does not crash
c4b161b The cold-boot fix above still started the received guest warm when the restart's vm inspect failed M5i: inspect fails once, new retries 2 s later and hands the dead VM to liveness; old cold-boots through the move path

Also fixed: e2e/go.mod left untidy by the dependency bump turned main's CI red (#454, merged first). Test-only: a background capture settle outliving a test failed 3 of 10 full runs with "directory not empty" (d64aab2); a data race in a test (f063d74); assertions that could not fail, missing wake-verb and tenant-read coverage, evacuation of a moving claim (a009823).

What ran

  • Per-file readers: 10 readers, every file in full: 57 due files (all sandboxd: move a running claim to a named peer with cocoon live migration #445/pool: settle a capture interrupted by sandboxd's death #447 code and their tests) plus the infra lens on boot/Dockerfile. Flow readers: 8; five new flows declared and read in full (settle-capture, hibernate-wake, live-move, cocoon-cli, restart-reconcile), six existing flows re-read for the windows sandboxd: move a running claim to a named peer with cocoon live migration #445/pool: settle a capture interrupted by sandboxd's death #447 changed. Docs reader: 127 sentences checked. A fresh style reader (36/36 touched files) and flow reader (21 windows, 0 bugs) on the final diff.
  • /simplify and /loc-justify: applied: one dead error type, three single-caller wrappers, an unreachable guard, a redundant error branch; kept items are recorded in the hygiene ledger with reasons.
  • Codex: 4 rounds: no findings → one P1 (fixed in 8b580a9) → P1 closed, no findings → a separate review's P2 (fixed in c4b161b) closed, no findings for 88e6aa5d..c4b161bb. Rounds 1–3 ran on one resumed thread; round 4's resume opened a new thread.
  • Gates: make go-lint 10/10 0 issues (5 modules × linux/darwin) at 963b2fc; make go-test (race) 29 ok, 0 fail at c4b161b; asl 0 findings on all modules both GOOS; Python ruff format/check clean, pytest 278 passed; Rust silkd and boot/init fmt/clippy clean, tests pass on mac (161/15) and in a linux/arm64 container (162/17) — Rust unchanged in this round.
  • Hardware (cocoon v0.7.7, Cloud Hypervisor 35b098c65260): kit 1 on one node (e2e + R1–R4), kit 2 across two nodes (live move M0–M6), each row on the new binary and on 88e6aa5 as the negative control.
row result
e2e (smoke, volumes, live volumes, reap, restart) pass, 23 s
M0 move both ways 200 both ways, 180 / 211 ms downtime, disk, memory and env markers intact
M6a–e kill source or target at each move step each ends with one owner, one VMM, no journal left; memory survives where the guest never stopped

Final-head regression (4aef153; M5i on c4b161b), new binary: e2e pass; R1, R2 (both orders), R3 (3/3 ticks), R4 (100 attempts, 1 reached the race order, served by the reader); M0 (332 / 217 ms), M1c, M2, M3, M4, M5, M6a–e, M7, M5i all pass; #447 interrupted capture: TERM 1 s in, TERM mid-dump, SIGKILL 1 s in (guest resumed) and SIGKILL mid-dump (snapshot adopted) all settle with the memory marker intact and nothing left after release.

Numbers

  • 23 commits; production Go +130 / −112 (net +18): the eleven fixes add +45 (8d0d678 +4, a5dd3fb +3, 8989efb +5, dcb197a +3, be5e432 +3, 97437c9 +8, b1b7fd8 +3, 7db8477 +10, 8b580a9 +2, 58204dc 0, c4b161b +4), cuts and simplification −27; tests net +240. Comment lines +10 / −14.
  • Hot paths: the claim path gains one map lookup under a lock it already holds and loses a guard; the move's downtime window gains one bool read; restart-only paths gain one vm inspect. No timeout, budget or default changed.

Coverage

flow / window coverage
release vs live move, drain vs commit, target migrate floor, abort failure, cold received VM injected on hardware, both binaries
move crash windows (source/target killed at round, final capture, commit, start) injected on hardware
reconcile with snapshot list failing after a host-crash-style kill injected on hardware, both binaries
settle gives up then guest silent (30-min budget) injected on hardware, both binaries
archive vs wake, background settle vs reader-safe hibernate injected on hardware, both binaries (the latter a scheduling race: 2/87 and 4/57 attempts reached it)
interrupted capture (#447) injected on hardware at the final head (TERM and SIGKILL, fast and mid-dump)
claim-lifecycle, warm-pool, failover, capture-store, live-volumes, api-relay windows changed by #445/#447 read (changed windows); no window changed behaviour outside the rows above
real power loss of a node not covered: the testbed has no power control; stand-in is killing sandboxd and the VMM together (R1, M5), which leaves the same on-disk state

Known issues kept without code

Each is bounded, loud and converges; all are in the hygiene ledger with the reason:

  • [contrived] a peer holding the fleet token could send a non-regular tar entry in a move round; it is written as a plain file.
  • TestRetryResolvesDanglingIntent takes 5 s of real time (synctest hangs on the settle mutex).
  • boot/Dockerfile: a possibly stale cdn comment with a duplicate mirror entry, and rustfmt/clippy downloaded on every uncached boot build — touching it rebuilds the boot image (decision below).

Decisions for CMGS

  1. boot/Dockerfile cleanup (comment, duplicate mirror, toolchain components): costs a boot image rebuild. Recommendation: fold it into the next change that rebuilds the image anyway.
  2. Release and rollout: this branch plus cocoon v0.7.7 is the tested pair. Recommendation: release it as the next sandboxd version paired with cocoon v0.7.7.

CMGS added 21 commits November 5, 2026 11:20
Without archives Release went straight to releaseResolved, which never checked
the move: the source dropped the claim and answered 204 while the move still
committed, so the released sandbox ran and billed on the target.
settleCapture marked the claim failed when the guest did not resume but
returned nil, so a wake handed out the dead guest's socket and a hibernate
captured the failed claim; a later restartFailed then left a running VM
recorded as hibernated.
The goroutine a failed capture leaves queues on the claim's transition; when a
wake or hibernate settled first, it still logged an interrupted capture and
cleared ReaderSafe on the new hibernate, so reads woke the VM.
…at start

Reconcile adopted the intent as the wake image without knowing the snapshot
existed, so a crash before cocoon persisted it left a claim every wake failed
to restore; the background settle now reads the list with retry first.
Only the source checked its cocoon version, so a move to a node still on an
older cocoon streamed every round and paused the guest before the target's
vm migrate receive failed.
Admission refused a draining target, but a drain or evacuation that began
during the rounds still took the claim after evacuation had listed the node's
claims, so taking the node down lost it.
…irst, tighten comments

Two reconcile sweeps logged as pool.Reconcile; godocs that restated code or
contradicted it (atomic hibernate, a received-VM-only Start, the v0 package
summary, m.mu-only runningShaped, the move-concurrency fallback, AtCapacity)
now match the code; a stale cocoon v0.5.8 note is gone.
MigrateUnsupported had no errors.As consumer outside a test; operator wake and
replicaHeld forwarded once; finalPause guarded a zero it never receives; the
tenant cap skipped an empty-map range that costs nothing.
archive checked for a wake image before it took the claim's transition, so a
wake that landed first made the export snapshot the running guest: a pause
the client saw and a full upload, then a discarded checkpoint. The archive now
passes the wake image it saw and the export refuses any other source.
failMoveOut wrapped every settle error in ErrMoveStoreDown and logged that the
abort waited for the registry, so a vm migrate abort failure with PG healthy
answered 503 "move registry down" and sent the operator to the wrong place.
A target that restarted between the commit and the start ran vm start on a
VMM the host crash had killed, so cocoon cold-booted the guest and the claim
went live as if moved: no restart recorded, no guest env rewritten, a claim
probe budget for a cold boot. Restart now inspects the VM and leaves a dead
one to liveness, which restarts it like any other. The test engine now models
a received VM as cocoon does: a live VMM with the guest paused.
Column names, downtime_ms, what the 200 promises, the 403/503 answers, target
admission and the draining commit, the move registry's table and pool, the
live-move capture and receive budgets, the v0.7.7 qualifier on an adopted
hibernate, and sentences that described cocoon internals instead of its CLI.
…laim; drop assertions that cannot fail

Tests that could not fail or did not test their name: a config fixture that set
the defaults it checked, a compile-time interface assertion, a liveness check
read before maintenance quiesced, a nil dereference on the failure path, an
unreachable dialer branch, a dead token column and a dead env var.
errors.Join already classifies a store-down abort as ErrMoveStoreDown, a settle
with no intent returns before touching ReaderSafe, and the move-in restart
inspects the VM only on the branch that starts it.
A failed capture leaves a settle goroutine that commits claims.json moments
later; tests that ended first failed their TempDir cleanup with "directory not
empty" (3 of 10 full runs). The cold move-in test now also waits for its
liveness pass to finish its usage write.
…tion

The move journal kept only the epoch and series, so a source that restarted
mid-move aborted without telling the target, whose reservation then held a
move_concurrency slot until the 2-minute idle drop (hardware: a following move
to that target answered 429 for about 2 minutes).
Reconcile resumes a journaled move, whose abort now drops the target's
reservation through the move transport, but main installed that transport only
after the mesh joined; a restart whose abort finished first called a nil
transport and crashed.
…ntent wake test

The background settle a failed hibernate leaves reads them concurrently; the
race detector flagged it once in a full run.
@CMGS CMGS changed the title review: whole-repo audit after #447 — ten bugs fixed with hardware negative controls review: ten bugs fixed with hardware negative controls Nov 5, 2026
CMGS added 2 commits November 5, 2026 13:09
…ting it warm

An inspect error read as a live VMM, so a target restarted after a host crash
could still cold-boot the received guest through the move path, skipping the
restart record and guest env rewrite. The restart now retries like a registry
outage until inspect answers.
@CMGS CMGS changed the title review: ten bugs fixed with hardware negative controls review: whole-repo audit after #447 — eleven bugs fixed with hardware negative controls Nov 5, 2026
@CMGS
CMGS merged commit 14b4692 into main Nov 5, 2026
1 check passed
@CMGS
CMGS deleted the review/post-447 branch November 5, 2026 05:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant