# VERDICT — DF-2533 (REPRODUCED: kernel panic + permanent lock leak)

## Bottom line

Both defensive early-return paths in `xa_close` are defective exactly as
claimed, and both defects were mechanically demonstrated by driving the REAL
`xa_close` (verbatim, sha256-gated, compiled into the guest kernel) into each
path with fabricated raced-destruction state:

1. **Path 1 — `si_drv1 == NULL` (xdisk.c:769-770): returns holding the
   exclusively-acquired `xdisk_lk` (leak).**
   Observed: `xa_close returned 6` (ENXIO) and
   `xdisk_lk lockstatus=2 after return (LK_EXCLUSIVE=2 => EXCLUSIVE LOCK LEAKED)`.
   A probe thread running the REAL `xa_open` on another CPU wedged forever on
   the leaked lock (`started=1 finished=0 => STUCK FOREVER`) — the leak
   deadlocks every later xdisk subsystem operation: `xdisk_open`,
   `xdisk_close`, `xdisk_ioctl` (XDISKIOCATTACH), `xa_open`, `xa_close`,
   `xaio_rcvdmsg` (every dmsg rx dispatch), `xa_terminate_check` callers.
   The harness released the lock afterwards only to keep the guest alive; a
   production caller has no owner to release it: the leak is permanent.

2. **Path 2 — `sc->terminating` (xdisk.c:771-773): releases `sc->lk`, a lock
   that was NEVER acquired, while still holding `xdisk_lk`.**
   Observed: unconditional kernel panic
   `panic: lockmgr: LK_RELEASE: no lock held`
   backtrace `lockmgr_release() <- xa_close() at xa_close+0x183` —
   kern_lock.c:768 panics on any release of a lock with no holders; this is
   NOT INVARIANTS-gated, so production kernels panic too. Pre-call state
   printed: `sc->lk.lk_count=0x0` (lockinit leaves count 0). Had a kernel not
   panicked, `xdisk_lk` would additionally be leaked on this path as well.

The correct pattern exists 60 lines above in `xa_open` (:703-715): both of
its defensive paths `lockmgr(&xdisk_lk, LK_RELEASE)` before returning.

## Production reachability (honest quantification)

- The device ops carry `D_TRACKCLOSE` (xdisk.c:188), so devfs dispatches
  `d_close` for EVERY close of a `/dev/xa%d` node, not just the last
  (sys/vfs/devfs/devfs_vnops.c:1104/1170). Two processes holding the node
  open: peer drops the span AND the first closer drives `opencnt` to 0 →
  `xa_terminate_check` frees the softc and NULLs `si_drv1` (:665-680) under
  `xdisk_lk` → the second closer enters `xa_close`, acquires `xdisk_lk`,
  reads `si_drv1 == NULL` and takes the leak path. A variant: `xa_open`'s
  BLK_OPEN error unwind (:749-753) runs `xa_terminate_check` while devfs
  still owes the error-path `d_close`.
- The `terminating` branch requires observing `si_drv1 != NULL` together
  with `sc->terminating == 1`; in the current code both transitions happen
  under `xdisk_lk` inside `xa_terminate_check`, so this branch is defensive
  (dead today) — but it is a booby trap: any future reordering (e.g. moving
  the `terminating = 1` store, or splitting the lock) turns it into the
  panic above. The panic was verified mechanically with fabricated state.
- Threat model (per team directive): xdisk devices are created by root via
  dmsg attach, but the resulting `/dev/xa%d` nodes are ordinary block
  devices; ANY holder of an open node can trigger `xa_close` at the moment
  the raced destruction state arises. Impact ceiling: permanent subsystem
  wedge of all xd* devices plus the dmsg rx machinery (local DoS), and — on
  the terminating path — an unconditional kernel panic. No memory-corruption
  primitive: no uid=0 chain exists for this bug class; the escalation
  ceiling is denial of service.

## Exploit chain

Not memory corruption. Chain is: (unprivileged holder of an xa node)
close(2) concurrent with span-loss teardown → leaked exclusive `xdisk_lk` →
every subsequent xdisk/dmsg operation sleeps forever (`xaopen`/`xawait`
stuck threads pile up) → subsystem-wide local DoS; or the terminating
variant → instant node panic.

## Fix validation (fixed)

`fix.diff` mirrors the `xa_open` pattern: both defensive paths release
`xdisk_lk`; the terminating branch no longer touches `sc->lk`. Applied in
guest /usr/src (both DF-2533+DF-2534 diffs, combined tree sha256
`15d189799d499d96ee6bbb5796ef3b3c5e316fce06d15507a47776119ae43385`),
kernel rebuilt (#3 Mon Oct 19 21:13:06) and the EXACT same triggers re-run:

- baseline: lockstatus=2 after `xa_close` return, probe thread stuck;
  patched: lockstatus=0, probe completes (run.patched.log)
- baseline: `panic: lockmgr: LK_RELEASE: no lock held`;
  patched: clean ENXIO return, no panic, `sc->lk.lk_count=0x0` untouched

## PoC notes

- The harness kernels were built from the verbatim guest source (sha256
  gate in build.sh, hash recorded in env.txt/sha256_gate.log); only the
  external registration calls were interposed via pre-include `#define`
  renames — `xa_close`/`xa_open` machine code is the shipping code.
- The probe thread calls the real `xa_open`, not a stubbed lock take.
- Harness iterations: first race-hammer version starved a CPU (LWKT
  cooperative scheduler) and wedged the guest mid-run; fixed with periodic
  1-tick `tsleep` in the hammer loops (harness-only artifact, unrelated to
  the 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.
