# DF-5773 — VERDICT

**Status: reproduced (mechanism, faithful extraction of init.c:204-230/500-506,
on-guest); in-vkernel trigger traced line-certain but gated on a sibling stock
bug. Fix validated (harness-level + rebuilt kernel).**
**Impact: dos (process-group collateral TERM + permanent vkernel wedge).**
**Confidence: likely (mechanism certain; reachability needs a completed reboot
cycle, which stock currently blocks one bug further up the chain).**

## Root cause (certain, path:line)

`sys/platform/vkernel64/platform/init.c`:

* 204: `while ((childpid = fork()) != 0) {`
* 206-211: SIG_IGN for INT/QUIT/HUP installed **inside the loop, i.e. by the
  monitor AFTER fork()**;
* 217-218: `sa.sa_handler = handle_term; sigaction(SIGTERM, &sa, NULL);` —
  likewise installed only after the fork returned;
* 228-230: `WEXITSTATUS(status) != EX_VKERNEL_REBOOT` else loop → fork again;
* 500-506: `handle_term(int sig) { kill(childpid, sig); }` — in the CHILD of
  any post-reboot iteration, `childpid == 0`, so `kill(0, SIGTERM)` means
  "signal every process in my process group".

Consequences in a reforked child (the normal `reboot` lifecycle:
guest `reboot(2)` → `kern_reboot` → `cpu_reset()` (init.c:1474-1480,
`exit(EX_VKERNEL_REBOOT)`) → monitor refork):

1. **Group splash**: the rc script / supervisor / pipeline that launched the
   vkernel (everything sharing its pgrp) receives SIGTERM.
2. **Self-re-arming storm**: the child's own handler runs with SIGTERM
   self-masked (no SA_NODEFER), `kill(0)` posts a new pending TERM to the
   child itself, and on handler return it is delivered immediately → the
   handler re-runs → `kill(0)` again — a 1:1 self-sustaining loop. The
   vkernel never progresses past the early-boot window (until
   `initshutdown`, shutdown.c:82-83 at SI_BOOT2_PROC0, replaces the
   handler), never answers, and is immune to further TERMs.
   The monitor (same inherited forwarding logic, `childpid > 0`) joins the
   loop by re-forwarding every group TERM it receives.

## Live proof

### Mechanism — `storm_demo.c` (faithful extraction), vulnerable

`run_storm_demo.sh 3` on the guest (bystander `sleep 120` in the same pgrp,
runner traps TERM):

```
demo=28519 bystander=28518 window=3
FIRING TERM at gen2 window
BYSTANDER_DEAD_BY_SPLASH          <- kill(0) hit the whole group
DEMO_ALIVE
DEMO_STILL_ALIVE_AFTER_SECOND_TERM_storm_wedged
# and: 85 x RUNNER_GOT_SIGTERM    <- the storm, re-arming every delivery
```

(baseline_storm.obs; count via `grep -c GOT` = 85.)

### Mechanism — fixed (`storm_demo_fixed.c`, guard `if (childpid > 0)`)

```
demo=40863 bystander=40862 window=3
FIRING TERM at gen2 window
BYSTANDER_ALIVE                   <- no splash
DEMO_ALIVE                        <- TERMs harmlessly ignored in-window,
                                     lifecycle continues (not a wedge)
# and: 0 x RUNNER_GOT_SIGTERM
```

### In-vkernel — rig ready, sibling bug blocks the last step

`run5773.sh` + `reboot2.c`: boots `/vkroot.img` to single-user, invokes
`reboot(2)` directly from process context (bypassing reboot(8), whose
process teardown wedges on this rig). The stock kernel reaches
`boot() called on cpu#0`, stops system threads, syncs disks, then takes

```
Fatal trap 12: page fault while in kernel mode
fault virtual address  = 0x58
instruction pointer    = 0x2b:0x55d143   (naccess, vfs_nlookup.c)
current process        = 1 (sh)
```

and wedges in ddb — the reboot NEVER reaches `cpu_reset()`, so the monitor
never reforks and child #2 (the storm's stage) is unreachable on this
snapshot. With DF-5661's fix applied (`unblock_DF-5561.diff`: shutdown.c
gate + kern_shutdown.c closefrom hardening) the panic moves from the
pre-sync no-op to this same post-sync `naccess` fault — still short of
`cpu_reset`. This is a third, separate defect in the shutdown/reboot
family (out of init.c's scope; logged for the family):
`kern_shutdown.c boot() → shutdown_cleanup_proc → ... → naccess` NULL
deref at offset 0x58 on VKERNEL64.

Every code link in the chain is otherwise verified:
- the sigactions run post-fork (first-boot children have SIG_DFL —
  confirmed live: TERM at 0.15 s into boot #1 = silent child death,
  no splash, no storm);
- `cpu_reset()` exits 32 and the monitor's refork loop re-forks from a
  monitor that by then HAS handle_term installed (init.c:217-218 ran in
  iteration 1) → child #2 inherits it;
- in child #2, `childpid == 0` → `kill(0, SIGTERM)` (init.c:505).

## Why not higher impact

No memory-corruption primitive; the deliverable is the demonstrated
group-wide collateral kill + permanent TERM-proof wedge of the vkernel
process pair. An operator (or rc shutdown, `killall TERM`, a supervision
framework) signaling a booting reforked vkernel kills its own process
group and leaves an unkillable-except-KILL spinning vkernel behind.

## Fix validation

`fix.diff` (init.c, 3 hunks — this finding is hunk 1):
`if (childpid > 0) kill(childpid, sig);`

* Harness: baseline (storm, splash, 85 runner hits, wedge) vs fixed
  (0 hits, bystander alive, normal lifecycle) — above.
* Rebuilt vkernel with the fix (+ the DF-5661 unblock): the TERM burst
  during the (sibling-gated) window produces no splash and no storm
  (`fixed_vk5773.obs`: SLEEPER_ALIVE, no RUNNER_GOT_SIGTERM); behavior
  otherwise unchanged (reboot path still wedges in `naccess`, untouched
  by this fix — as expected, different file).

## Artifacts

* `storm_demo.c` / `storm_demo_fixed.c` / `run_storm_demo.sh` — mechanism
* `baseline_storm.obs` (85 hits, splash, wedge) — decisive baseline
* `run5773.sh`, `reboot2.c`, `baseline_vk5773.obs` — in-vkernel rig
* `unblock_DF-5561.diff` — sibling-bug unblocker applied for the attempt
* `fix.diff` — the verified init.c fix (3 hunks; hunk 1 is this finding)

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
