Three problems in the shutdown and reaping paths, all of them found in review. Reaping and waiting cannot be two mechanisms. `waitpid(-1, ...)` collects any child, so the reaper and a caller waiting on its own child race for the same status, and whichever loses gets nothing — losing the workload's exit, which is the one thing this component exists to report. The reaper is now the only waiter and hands each exit to whoever asked for that pid. Registering interest holds the same lock the delivery takes, so a child that exits before its caller is registered is still delivered rather than dropped; there is a test that fails without that. Killing the workload killed everything. `kill(-1, SIGKILL)` is every process init may signal, so a workload that overstayed its grace period took the guest's services with it, before the ordered stop the shutdown promises them had even started. It signals the one pid now. The shutdown had no workload to stop. It built a fresh handle with no pid, so the graceful stop was a no-op and the workload only died in the sweep that follows — which is exactly the order this was written to avoid. The handle the session used is now the handle the shutdown uses, and waiting for the workload waits for that pid rather than for any child to leave.
nesinit
PID 1 inside a box.
A microVM has no init unless something is it. Three of the jobs are nobody else's, and this is all of them:
- Reaping. A process whose parent dies is reparented to PID 1. Without a reaper, every orphan the workload leaves behind holds a pid and a slot in the process table until the guest is gone.
- Ordered shutdown. The workload stops first and alone, then everything else, then the disks are flushed and the machine is powered off. An init that returns leaves a guest running with nothing in it.
- The guest end of the control channel. One vsock connection out, carrying what to run in and what happened back.
It does not know what it is running. It is handed a command line, a set of shares and what an exit means; there is no code path here that branches on which workload it started, and there is not meant to be.
The channel
The guest dials out on a fixed vsock port and speaks first:
guest → { "type": "ready", "protocol_version": 2 }
guest ← { "type": "boot", "exec": {...}, "mounts": [...], "geometry": {...}, "on_exit": {...} }
guest → { "type": "workload_exited", "exit_code": 0 }
Newline-delimited JSON. Dialling out rather than being connected to is worth keeping for two reasons: the listener is up before the VM starts, so nothing races a booting kernel and nothing has to retry, and the connection establishing is itself the liveness signal — without it the far end needs a timeout to tell a slow boot from a dead one.
The version goes out before anything is read, so a peer that cannot talk to this build refuses it before handing over a descriptor rather than failing later on a field that turned out to be missing.
The types are in nesprotocol::lifecycle,
behind the lifecycle feature, so both ends of the channel read one definition
and neither can drift from it silently.
It reports; it does not supervise
When the workload ends, the exit goes up the channel and the session is over.
on_exit says what that exit means — whether it ends the session — and
nothing here restarts anything. Starting something again is a decision for the
end that can see whether restarting is repair or a loop.
A signalled workload is reported as signalled, with no exit code. Reporting
0 for a killed process would make a kill look like a clean run.
What is not here yet
Mounting shares. The descriptor's mounts are refused rather than ignored — a
workload started without the shares it was promised fails later, somewhere
else, for a reason nobody can see from the guest.
Testing
cargo test -p nesinit
No VM required, and that is the point of the two seams. Reaping is tested
against real forked children — PR_SET_CHILD_SUBREAPER makes a test process
inherit orphans the same way PID 1 does — and the channel is tested over an
in-memory pipe, because the transport contributes nothing to the protocol
beyond ordering and framing.