sbsh: deactivate() drops xq/rq mbufs but leaves stale pointers - TXS/RXS/UFL processing after carrier loss (or after ifconfig down) double-frees / uses freed mbufs
Summary
deactivate forces CTDR=LTDR/CRDR=LRDR and drop_queues m_frees every queued mbuf while leaving the freed pointers; the EXT branch runs before UFL/RXS/TXS in the same ISR and a latched TXS/RXS survives into the deferred ISR after ifconfig down. free_sent_buffers/indicate_frames/resume_tx have no state==ACTIVE guard: they pop stale xq[]/rq[] entries - TX side is a direct double free; RX side writes a device-controlled 11-bit value into freed memory and passes the freed mbuf to if_input. Heap corruption with a partially-controlled write - potential uid=0 chain when the race fires on real hardware (no 0x55:0x9d card on guest). State-guard + SR-drain + head-resync + NULL-slots fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4497 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | trigger-source | BPF TX-saturation injector (needs the card + root) | 1.7 KB | view raw |
| build.sh | build-script | compile trigger | 65 B | view raw |
| run.sh | run-script | device-absence aware runner | 89 B | view raw |
| build.log | build-log | guest compile BUILD_OK | 124 B | view raw |
| run.log | run-log | BIOCSETIF: Device not configured (sbsh0 cannot exist) | 51 B | view raw |
| env.txt | env | uname + pciconf absence proof | 222 B | view raw |
| VERDICT.md | verdict | full source-level proof chain | 4.3 KB | β raw |
| fix.diff | verified-fix | state guards + SR drain + slot NULLing | 2.0 KB | view raw |
| verdict.json | verdict-json | machine verdict (strict status set) | 2.6 KB | view raw |
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
-
sbsh_intr()latchesstatus = sc->regs->SRonce, 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 bydeactivate()orsbsh_stop()β a TXS/RXS/UFL bit that latched before or during step 2 is still present in the localstatusvariable / in SR for a deferred ISR. -
EXT handling reaches
cx28975_interrupt()(:865-892), which callsdeactivate()on carrier loss while ACTIVE (:878-881).deactivate()(:849-860): - forcesCTDR = LTDRandCRDR = LRDR(:855-856), - callsdrop_queues()(:859) βm_freem()of every mbuf insc->rq[]/sc->xq[]and resets head/tail indices (:811-823), leaving the freed pointers in the arrays, and - does not touchhead_tdesc/head_rdesc, does not clear SR. -
Because
CTDR/CRDRwere just forced to the posted ends, the subsequent TXS/UFL/RXS branches seehead_tdesc != CTDR/head_rdesc != CRDRwhenever frames were in flight: -free_sent_buffers()(:726-748) walks descriptors and executesm_freem(sc->xq[sc->head_xq++])(:739) on slots whose mbufs were freed a few instructions earlier β double free. -indicate_frames()(:789-809) takesm = sc->rq[sc->head_rq++](:796), writesm->m_pkthdr.len = m->m_len = <11-bit device value>(:799-800) into freed memory, and passes the freed mbuf toif_input()(:803) β UAF + second free in the ether_input path. -resume_tx()(:611-621) additionally re-enablesTXENon the deactivated chip and its walk indexessc->tbd[cur_tbd]with an unmasked device-supplied CTDR. -
Deferred-ISR variant (no same-ISR coincidence needed):
sbsh_stop()runs underifp->if_serializerfrom SIOCSIFFLAGS down (:491-496); a TXS whose hardware interrupt was raised before the serializer was taken is delivered right after the ioctl releases it, withstate == DOWNand the same stale arrays. Neitherfree_sent_buffers()norindicate_frames()checkssc->state(contrastsbsh_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.
Fix verification
not_testablefix.diff authored post-verification against read-only sys/; baseline itself cannot run on this guest (no device), so patched-kernel validation was not possible
fix.diff
Confirmed kernel references
Detail
Exploit chain
peer carrier drop coincident with frame completion (or root ifconfig-down under load) -> deactivate() drop_queues frees mbufs -> same/deferred ISR TXS branch double-frees xq[] mbuf, RXS branch writes 11-bit device value into freed mbuf and feeds it to if_input -> mbuf freelist poisoning -> uid=0 (requires real hardware for the race window)
Evidence (decisive lines)
VERDICT.md (source proof chain); run.log (BIOCSETIF: Device not configured - interface cannot exist on guest); env.txt (pciconf: no 0x55:0x9d); fix.diff (state guards + SR drain + slot NULLing)
PoC changes
added missing #include
Verified recommended fix
guard free_sent_buffers/indicate_frames/resume_tx on sc->state == ACTIVE; drain latched SR bits and resync head_tdesc/head_rdesc in deactivate(); NULL freed xq/rq slots in drop_queues()
Verdict
Defect confirmed at source level (stale xq/rq mbuf pointers walked by free_sent_buffers/indicate_frames after deactivate() frees them via drop_queues, because sbsh_intr handles EXT before TXS/RXS/UFL and no state guard exists); runtime reproduction impossible on the audit QEMU guest because the Granch SBNI12/16 PCI device (0x55:0x9d) is absent, so the driver never attaches. BPF trigger compiled and ran on the guest, failing only at BIOCSETIF (no sbsh0).
No comments yet.