# VERDICT — DF-2534 (REPRODUCED: use-after-free READ window in real xa_size; NULL-write panic in xa_strategy)

## Bottom line

Both cited defects are real and were mechanically demonstrated against the
REAL functions (verbatim sha256-gated xdisk.c compiled into the guest
kernel, fabricated cdev/softc, interposed disk/dmsg externals):

1. **`xa_size` TOCTOU/UAF read (production-reachable window).**
   `xa_size` reads `sc = a_dev->si_drv1` at :833, NULL-checks it, then
   dereferences `sc->info.d_media_blocks` at :835 — with NO lock.
   `xa_terminate_check` NULLs `si_drv1` at :665 and `kfree(sc)`s at :680
   under `xdisk_lk`; a reader that loaded the pointer before the NULL-store
   dereferences freed memory after it. Hammer proof: cpu0 thread in the real
   `xa_size` vs cpu1 thread driving the real `xa_terminate_check`:

       reader xa_size iterations:      157,024,256  (151M on the safe ENXIO path)
       terminator frees:                51,550,501  (100% same-address LIFO reuse)
       UAF READS CAPTURED:                    664

   Each capture is airtight: the reader observed
   `0xC0FFEE00DF2534` — a value that was only ever written into the slot
   AFTER `kfree(sc)` and re-allocation, so the :835 load provably landed
   after the free (a stale-pointer dereference of freed kernel heap through
   shipping code). First capture arrived within milliseconds of start.

2. **`xa_strategy` missing NULL check (write-fault panic).**
   :801 loads `sc = a_dev->si_drv1` with no check and :805 passes
   `&sc->stats` to `devstat_start_transaction` → with `sc == NULL`:
   `Fatal trap 12 ... fault virtual address = 0x90, supervisor write data`
   at `devstat_start_transaction+0xa: lock xaddl %eax,0x58(%rdi)` —
   `0x90 == offsetof(xa_softc, stats) + 0x58`, i.e. a write through the
   NULL softc exactly as coded. Demonstrated with fabricated state.

## Honest narrowness quantification

- **`xa_size` is the live production window.** psize is not drained by
  destruction: `diskpsize` (sys/kern/subr_disk.c:1259-1276) re-dispatches
  `dev_doperate` to the raw device when `dssize()` returns -1 and the disk
  sets `DSO_RAWPSIZE` — which xdisk sets (xdisk.c:491). `dev_doperate`
  (kern_device.c:544) has no closing/refs gate; it dispatches straight
  through `si_ops`. So a psize racing span-loss teardown reads freed heap.
  What the primitive yields: an 8-byte read at a fixed offset of a freed
  M_XDISK slab slot, returned to the caller as the device size (via
  dssize/DIOCGPART paths) — an attacker-groomable kernel heap info-leak
  channel (spray an object into the freed slot, read its content at that
  offset). It is a READ: no control-flow or write primitive; uid=0 is not
  reachable from this bug alone.
- **`xa_strategy` is practically narrowed by destruction ordering**:
  `xa_terminate_check` calls `disk_destroy` at :663 BEFORE NULLing
  `si_drv1` at :665, and disk_destroy→destroy_dev drains device
  references; `xa_terminate_check` also refuses to free while
  `bioq`/`tag_pendq` are non-empty (:646-647). So a NULL `sc` at strategy
  entry requires a caller that slipped past the drain — the NULL-write
  panic demonstration is fabricated-state (post-drain) and labeled as
  such; its value is proving the code has no defense at all if the drain
  assumption ever breaks.

## Threat model (per team directive)

xdisk devices are created by root via dmsg; afterwards the `/dev/xa%d`
nodes are ordinary disks. Whoever can open the node (permissions
permitting) can race `lseek`/size-query paths against span teardown by an
unprivileged-triggerable disconnect. Ceiling: 8-byte heap disclosure per
hit (Low), repeatable at will; not an escalation primitive by itself.

## Fix validation (fixed)

`fix.diff` takes `xdisk_lk` and re-checks `si_drv1` under it in both
functions, matching the file's own idiom (the same lock
`xa_terminate_check` uses to NULL+free). In `xa_strategy` the bio is
queued before the lock is dropped so `xa_terminate_check`'s empty-queue
precondition protects the softc; lock order `xdisk_lk -> sc->lk` matches
`xaio_rcvdmsg`. Applied in guest /usr/src together with DF-2533's fix
(combined tree sha256
`15d189799d499d96ee6bbb5796ef3b3c5e316fce06d15507a47776119ae43385`),
kernel rebuilt (#3 Mon Oct 19 21:13:06), EXACT same triggers re-run:

- race: 82,313,216 `xa_size` iterations vs 30,146,560 frees (100%
  same-address reuse) → **0 UAF reads** (baseline: 664)
- strategy NULL: clean return 0, bio completed with `b_error=6 (ENXIO)`,
  `b_flags=0x800 (B_ERROR)` via biodone — no fault

## PoC notes

- The magic-value method cannot false-positive: `si_drv1` never points at
  the re-allocated block while set, and the planted value exists only
  after the free.
- The hammer threads periodically `tsleep` 1 tick (a spinning kernel
  thread starves its CPU under the cooperative LWKT scheduler — harness
  artifact, not part of 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.
