Skip to content

feat(vm): signal guest CRNG reseed after clone/restore#77

Merged
CMGS merged 7 commits into
masterfrom
feat/clone-reseed-signal
Jul 3, 2026
Merged

feat(vm): signal guest CRNG reseed after clone/restore#77
CMGS merged 7 commits into
masterfrom
feat/clone-reseed-signal

Conversation

@CMGS

@CMGS CMGS commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

What

Closes the host side of M1 reseed. After clone/restore, N guests share byte-identical snapshot memory → byte-identical CRNG state (colliding TLS keys/UUIDs). cocoon now sends 32 bytes of crypto/rand entropy + a reseed order to cocoon-agent (v0.1.6, client.Reseed) over the existing hybrid-vsock channel.

  • cocoon vm reseed VM [--machine-id] — manual verb (fails loudly)
  • Auto-signal wired into every clone success path (regen_machine_id=true) and restore (false — same VM identity, only CRNG rotates), as a non-fatal signalReseed: skips Windows (agent verb is Linux-only) and legacy no-vsock VMs; on agent version skew logs exactly one WARN pointing at the manual verb, never fails the command
  • refreshVM: the *types.VM that Clone/Restore return is built in-process and never passes through ToVM, so VsockSocket was always zero → signalReseed would silently skip on every real clone. Re-Inspect before signaling. (Bug found during e2e.)
  • resolveRunningVM extracted — exec and reseed shared the resolve+inspect+running-check ceremony verbatim
  • go.mod: cocoon-agent → v0.1.6

FC guests also carry VMGenID (kernel auto-reseed on resume) so this is belt-and-suspenders there; on CH it's the only reseed.

Verification (e2e on bare-metal 79, isolated store, audit-logged)

  • Clone (CNI): reseed fired silently, clone healthy, exec OK
  • Restore (in-place): no signal-induced change — /etc/machine-id byte+mtime identical (regen=false correctly skips machine-id)
  • Degradation: guest downgraded to agent v0.1.5 → exactly one WARN (expected first frame type "exec", got "reseed"), clone otherwise healthy
  • Manual verb: exit 0 both ways; mtime confirms --machine-id threads through
  • make lint 0×2, 25 pkgs green, make fmt-check clean

Known follow-up (not this PR)

The CRNG entropy/reseed ioctls (the security-critical mechanism) work with agent v0.1.6. But --machine-id regen does not yet change the id end-to-end: agent v0.1.6 truncates /etc/machine-id but not the baked /var/lib/dbus/machine-id, so systemd-machine-id-setup re-adopts the stale copy. Fixed separately in cocoon-agent (dbus copy removal) → needs a v0.1.7 bump. This PR's host plumbing is correct and version-independent; machine-id regen lights up once the agent is bumped.

CMGS added 4 commits July 3, 2026 23:26
Cloned/restored guests share byte-identical CRNG state; push fresh
entropy and a reseed order over vsock to cocoon-agent (>= v0.1.6) so
random output diverges again. Clone also requests /etc/machine-id
regeneration; restore does not, since the VM identity is unchanged.

Non-fatal by design: agent version skew or a legacy VM without vsock
only logs a warning and points at the new manual 'cocoon vm reseed'
verb, never fails the clone/restore command itself.
Clone/DirectClone/Restore/DirectRestore return an in-process *types.VM
that never passes through ToVM, so VsockSocket (and PID/SocketPath)
stay zero-valued; signalReseed would silently no-op on every call.
Re-Inspect via the new refreshVM helper before signaling, threading
the Hypervisor handle through the direct-clone/restore call chains.

Also fire the clone signal before the JSON-output early return so
automation (JSON) callers get it too, not just human-CLI output.
Both exec and reseed resolved the hypervisor, inspected the VM, and
required running state with identical wrapped-error ceremony; hoist it
into one Handler helper (op prefixes the error).
The v0.1.6 tag was re-cut to include the dbus machine-id fix; the release
binaries changed, so refresh the pinned SHA256s (version unchanged).
@CMGS

CMGS commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Update — machine-id regen now works end-to-end with v0.1.6. The agent dbus fix (cocoon-agent PR #7) landed and the v0.1.6 tag was re-cut to include it (the earlier release binaries didn't have it). This PR now carries a chore(os-image) commit re-pinning the v0.1.6 SHA256s to the re-cut release (version string unchanged, only the checksums — same pattern as the prior re-pin v0.1.5 SHA after tag re-cut).

So the earlier 'known follow-up needs v0.1.7' note is superseded: the security-critical CRNG reseed and --machine-id regen both work with the re-cut v0.1.6 that the next os-image build will bake. New pins: x86_64 7b4f80c6…, arm64 27aaa08b….

Review follow-ups on the reseed signal:
- reseedVM retried on ANY error, so a version-skew old agent (dial OK,
  reseed frame rejected) burned the full retry budget (~4s) on every clone
  during the rollout window. Retry only failed dials (the re-listen-after-
  resume case); once a live agent answers, its reply is final. Old-agent
  reseed now returns in ~2ms instead of ~4s, verified on bare metal.
- fold refresh+signal into Handler.reseedAfterResume so a caller can't
  signal on a stale (pre-ToVM) VM; move refreshVM beside it and log the
  swallowed inspect error at Debug (was mislabeled 'vsock not configured').
- signalReseed reports whether a reseed was attempted, making the
  Windows/no-vsock skip guards assertable.
@CMGS

CMGS commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Full review pass run (was incomplete before — thanks for pushing)

Ran senior /code walk + 3-lens /simplify (reuse/quality/efficiency) + tighten. Findings triaged and applied:

Real defect fixed (efficiency lens + my own walk): reseedVM retried on any error. A version-skew old agent answers the reseed frame with an instant rejection (dial succeeds → expected first frame type exec, got reseed), which the loop treated as retryable → ~4s of pure sleep on every clone/restore for the entire rollout window (pre-upgrade snapshots carry the old agent). Fix: retry only failed dials (the genuine 'agent re-listens after resume' case); a live agent's reply is final.

Verified on bare metal (isolated store, agent version confirmed, cleaned up):

case result
reseed on v0.1.6 guest (happy) exit 0, 3ms
guest downgraded, version asserted cocoon-agent version v0.1.5
reseed on settled v0.1.5 old agent exit 1, 2ms, expected first frame type "exec", got "reseed" — was ~4s before

Refactors from the review:

  • Handler.reseedAfterResume folds the refresh+signal pair (4 verbatim sites) so a caller can't signal on a stale pre-ToVM VM by mistake; refreshVM moved beside it and now logs the swallowed inspect error at Debug (previously mislabeled as 'vsock not configured').
  • signalReseed returns whether a reseed was attempted → the Windows / no-vsock skip guards are now asserted (were assertion-free).
  • resolveRunningVM (earlier) dedups exec+reseed; confirmed no third caller.

Verdict from the lenses: retry loop was the one real defect; refreshVM extra Inspect (~100µs) is acceptable, sync-not-async is correct (a detached reseed would be killed on CLI exit, leaving duplicate CRNG state), threading hyper is free. lint 0×2, 25 pkgs green.

CMGS added 2 commits July 4, 2026 01:44
reseedAfterResume (a method) was interleaved between reseedVM and
signalReseed; move it up beside Reseed so methods are grouped and the
standalone helpers follow, per the file-org convention.
Re-cutting the v0.1.6 tag changed the module content, but the Go proxy +
checksum DB treat a version as immutable, so the recorded v0.1.6 hash no
longer matches — a fresh checkout fails 'verifying ...@v0.1.6: checksum
mismatch'. v0.1.7 is a clean version never before ingested. go.mod/go.sum
and all os-image pins (ubuntu + android 14/15) move to v0.1.7.
@CMGS

CMGS commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Fixed — moved to cocoon-agent v0.1.7 (the go.sum poisoning was real)

The reviewer is correct and this was a must-fix. Re-cutting the v0.1.6 tag changed the module content, but Go's proxy + checksum DB treat a version as immutable, so go.sum's recorded v0.1.6 hash no longer matched the re-cut content. GitHub checks passed only on a warm GOMODCACHE; a fresh checkout would fail verifying ...@v0.1.6: checksum mismatch. Updating go.sum's h1 alone wouldn't fix it either — sum.golang.org already locked the original v0.1.6 hash, so a clean fetch with GOSUMDB on would still mismatch.

Fix: cocoon-agent v0.1.7 (fresh version, same dbus-fix content as the re-cut v0.1.6, never before ingested by the Go proxy/sumdb):

  • go.mod/go.sum → v0.1.7 (h1 lPoN/IBEJUdWph030qkyRUc9MY64DhuJ5XrYNjdoUKk=)
  • os-image pins (ubuntu x86_64+arm64, android 14.0/15.0) → v0.1.7 with the v0.1.7 release SHAs, no v0.1.6 residue

Verified in the reviewer's exact clean-cache scenario (fresh empty GOMODCACHE, sumdb on):

env GOWORK=off GOPROXY=https://proxy.golang.org,direct GOSUMDB=sum.golang.org GOMODCACHE=<fresh> go test ./cmd/vm
→ ok  github.com/cocoonstack/cocoon/cmd/vm  0.359s

Also folded in a style commit grouping the Handler methods above the standalone reseed helpers (reseedAfterResume was interleaved). Lesson logged on my end: never re-cut a released tag that Go tooling has ingested — cut forward.

@CMGS CMGS merged commit bfe361e into master Jul 3, 2026
4 checks passed
@CMGS CMGS deleted the feat/clone-reseed-signal branch July 3, 2026 17:56
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