β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4497

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4497 Β· 9 files
FileTypeDescriptionSize
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.md verdict full source-level proof chain
↓ download 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

  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.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-4497

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 for struct ifreq (seed would not compile); added device-absence early-exit in run.sh

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