# VERDICT DF-4497 — sbsh stale xq/rq pointers after deactivate() (double-free / UAF)

## Bottom line

**INCONCLUSIVE at runtime (hardware-gated), defect CONFIRMED at source level.**
The QEMU audit guest has no Granch SBNI12/16 PCI modem (vendor 0x55, device
0x9d — see `env.txt`, `pciconf -l`), so the driver never attaches and the race
cannot be executed. The code defect itself is provable by inspection with no
assumptions beyond "the device raises two status bits in the same SR read or a
latched bit survives until a deferred ISR" — both are normal MMIO latching
semantics of this 2003-era driver's own register protocol.

## Source-level proof chain

1. `sbsh_intr()` latches `status = sc->regs->SR` once, then handles bits in a
   fixed order: **EXT first** (`if_sbsh.c:571-574`), then UFL (`:576-581`),
   RXS (`:583-587`), TXS (`:589-592`). SR bits are write-1-to-clear and are
   NOT cleared by `deactivate()` or `sbsh_stop()` — a TXS/RXS/UFL bit that
   latched before or during step 2 is still present in the local `status`
   variable / in SR for a deferred ISR.

2. EXT handling reaches `cx28975_interrupt()` (`:865-892`), which calls
   `deactivate()` on carrier loss while ACTIVE (`:878-881`).
   `deactivate()` (`:849-860`):
   - forces `CTDR = LTDR` and `CRDR = LRDR` (`:855-856`),
   - calls `drop_queues()` (`:859`) — `m_freem()` of every mbuf in
     `sc->rq[]`/`sc->xq[]` and resets head/tail indices (`:811-823`),
     **leaving the freed pointers in the arrays**, and
   - does *not* touch `head_tdesc`/`head_rdesc`, does *not* clear SR.

3. Because `CTDR`/`CRDR` were just forced to the posted ends, the subsequent
   TXS/UFL/RXS branches see `head_tdesc != CTDR` / `head_rdesc != CRDR`
   whenever frames were in flight:
   - `free_sent_buffers()` (`:726-748`) walks descriptors and executes
     `m_freem(sc->xq[sc->head_xq++])` (`:739`) on slots whose mbufs were
     freed a few instructions earlier → **double free**.
   - `indicate_frames()` (`:789-809`) takes `m = sc->rq[sc->head_rq++]`
     (`:796`), writes `m->m_pkthdr.len = m->m_len = <11-bit device value>`
     (`:799-800`) into freed memory, and passes the freed mbuf to
     `if_input()` (`:803`) → **UAF + second free** in the ether_input path.
   - `resume_tx()` (`:611-621`) additionally re-enables `TXEN` on the
     deactivated chip and its walk indexes `sc->tbd[cur_tbd]` with an
     unmasked device-supplied CTDR.

4. Deferred-ISR variant (no same-ISR coincidence needed): `sbsh_stop()` runs
   under `ifp->if_serializer` from SIOCSIFFLAGS down (`:491-496`); a TXS whose
   hardware interrupt was raised before the serializer was taken is delivered
   right after the ioctl releases it, with `state == DOWN` and the same stale
   arrays. Neither `free_sent_buffers()` nor `indicate_frames()` checks
   `sc->state` (contrast `sbsh_start()`, which does gate on ACTIVE,
   `:319-320`).

## Why not `reproduced`

No trigger can create `sbsh0` on this guest: probe requires PCI
0x55:0x9d:0x…:3 (`if_sbsh.c:206-215`), absent per `env.txt`. The BPF injector
compiled and ran; it fails at `BIOCSETIF: Device not configured` (`run.log`)
— the interface does not exist.

## Exploit chain (for the day suitable hardware/emulation exists)

Peer drops carrier while frames complete → double-free of an mbuf cluster →
classic mbuf/cluster freelist poisoning → overlapping allocation → forge
`m_ext` / refcount → arbitrary free → uid=0. This is a race with a
device-timing window, so reliability would be moderate; the RX variant writes
a device-controlled 11-bit value at two fixed offsets in freed memory, which
is the stronger grooming primitive.

## Fix

`fix.diff` — three layers: (1) `state == ACTIVE` guards in
`free_sent_buffers`/`indicate_frames`/`resume_tx`; (2) `deactivate()` drains
latched SR bits and re-syncs `head_tdesc`/`head_rdesc` to the forced cursors;
(3) `drop_queues()` NULLs the slots it frees. Authored after verification,
against the read-only `sys/` tree; not runtime-validated (baseline itself
cannot run on this guest).

## 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.
