# VERDICT — DF-2857

## Status: REPRODUCED (panic, default config, unprivileged) — fix VALIDATED (fixed)

## Baseline (stock INVARIANTS kernel #0, Thu Jul 2 06:02:54 UTC 2026)

* Binary built in-guest: `cc -O2 -Wall -o shm_teardown_race shm_teardown_race.c`
* Run as `nobody`: `su -m nobody -c /tmp/shm_teardown_race rmid 400`
* Defaults verified before the run: `kern.ipc.shm_use_phys=1`,
  `kern.ipc.shm_allow_removed=1` (no tunables touched, unlike DF-2677 which
  needed `use_phys>=2` to widen *shmat's* window — this bug's window is on the
  *teardown* side and is wide on stock defaults).
* Result: kernel panicked within the first run (< ~60 s):

```
panic: vm_object_deallocate: object deallocated too many times: 5
cpuid = 2
vm_object_deallocate_locked() at vm_object_deallocate_locked+0x272
vm_object_deallocate_locked() at vm_object_deallocate_locked+0x272
vm_object_deallocate() at vm_object_deallocate+0x85
shm_deallocate_segment() at shm_deallocate_segment+0x17
sys_shmctl() at sys_shmctl+0xd6
syscall2() at syscall2+0x11e
Debugger("panic")
```

The two stacked `vm_object_deallocate_locked` frames are direct proof of the
mechanism: the second `IPC_RMID` re-entered `shm_deallocate_segment()` while
the first teardown was still on the stack, and dropped a reference on an
object whose `ref_count` was already 0 → unconditional panic at
sys/vm/vm_object.c:690.

Mechanism (all under default sysctls):
1. `nobody` creates a 192 MB `IPC_PRIVATE` segment, attaches, faults every
   page in, detaches → segment alive with `shm_nattch == 0`.
2. Racer processes spam `shmctl(shmid, IPC_RMID)`. First success enters
   `sys_shmctl` → `shm_deallocate_segment` (sys/kern/sysv_shm.c:445).
3. Inside, `vm_object_deallocate()` → `vm_object_terminate()` frees ~48k
   pages, calling `lwkt_user_yield()` every 64 pages (vm_object.c:932);
   the LWKT switch **forces a release** of `shm_token`.
4. Until line sysv_shm.c:193 (`shm_perm.mode = SHMSEG_FREE`),
   `shm_find_segment_by_shmid()` still returns the segment
   (`SHMSEG_ALLOCATED` set; `SHMSEG_REMOVED` ignored because
   `shm_allow_removed=1`).
5. The next racing `IPC_RMID` passes `ipcperm(IPC_M)` (owner), sees
   `shm_nattch <= 0`, calls `shm_deallocate_segment()` again →
   `vm_object_deallocate()` on the ref-0 object → panic.

Corruption ceiling beyond the panic (not needed for the verdict): in
interleavings where a racing `shmat` re-raises `ref_count` (OBJ_DEAD already
set), the second `vm_object_deallocate` does *not* re-terminate, returns, and
the second pass then `kfree()`s the same `shm_handle` chunk again and
double-decrements `shm_committed`/`shm_nused` — a classic double-free/
accounting-corruption door, pre-empted in the observed interleaving by the
ref-0 panic. The `shmat` racer mode of the PoC exercises the sibling door
(reference on the dying object → `vm_object_terminate(2): object with
references`), i.e. the DF-2677 sink reachable through this *separate,
unfixed* find-during-teardown window.

## Fix validation (kernel #1, Wed Sep  2 12:38:41 UTC 2026)

* `fix.diff` applied to the guest's `/usr/src` (hunk at sysv_shm.c:184):
  clear `shm_perm.mode = SHMSEG_FREE` + `shm_internal = NULL` and do the
  committed/nused accounting **before** the blocking
  `vm_object_deallocate()/kfree`, so the segment is unfindable before the
  token can drop.
* Kernel rebuilt (`make -j6 nativekernel`, rc=0, full log:
  `fix_build.log.gz`, includes the patched `sysv_shm.c` compile), installed,
  rebooted into `#1`.
* Exact same PoC re-run as `nobody`, same defaults: **400/400 iterations
  completed, exit 0, guest stayed up** (`run.fixed.log`). Second racing RMIDs
  now get `EINVAL` (segment hidden before teardown) instead of entering the
  teardown a second time.

## Artifacts

| file | what |
|---|---|
| `shm_teardown_race.c` | PoC source (rmid + shmat racer modes) |
| `build.sh` / `run.sh` | exact build/run |
| `serial_full.log` | full serial console (boot → panic) |
| `panic.txt` | panic + backtrace (baseline) |
| `run.fixed.log` | patched-kernel run: 400/400 clean |
| `fix_build.log.gz` | full untrimmed patched-kernel build log (rc=0) |
| `fix.diff` | verified one-hunk reorder fix |
| `env.txt` | guest/kernel/sysctl environment |

Note: the first full build log was written to guest tmpfs and lost at
reboot; it was re-captured by rebuilding (including a forced
`sysv_shm.o` rebuild) to a persistent path — `fix_build.log.gz`.
