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

Child-attach-failure path runs ral_pci_detach() after the child already unwound itself: ieee80211_ifdetach() LIST_REMOVEs a never-attached ieee80211com β†’ NULL-pointer write β†’ kernel panic (unreachable double-free tail behind it)

Summary

On child attach failure the glue runs the FULL detach whose dispatch runs the child detach a second time - ieee80211_ifdetach LIST_REMOVEs the zeroed com: *(NULL)=NULL. SINK REPRODUCED LIVE on the stock guest (Fatal trap 12 VA 0x0 at ieee80211_ifdetach+0x124); the follow-on double-free of every DMA ring is behind it. Flaky-hardware/eject trigger. Release-only-PCI-resources fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4996 Β· 12 files
FileTypeDescriptionSize
df4996.c β€” 5.4 KB view raw
Makefile β€” 83 B ↓ download
build.sh β€” 98 B view raw
run.sh β€” 159 B view raw
build.log β€” 5.9 KB view raw
run.log β€” 781 B view raw
run.2.log β€” 1.3 KB view raw
panic.txt β€” 718 B view raw
env.txt β€” 376 B view raw
fix.diff β€” 1.4 KB view raw
VERDICT.md β€” 4.8 KB ↓ raw
README.md β€” 2.2 KB ↓ raw

DF-4996 β€” ral_pci_attach() error path runs the child detach twice: ieee80211_ifdetach() on a never-attached ieee80211com NULL-writes in LIST_REMOVE β†’ kernel panic (double-free tail)

Where

sys/dev/netif/ral/if_ral_pci.c (PCI front-end for rt2560/rt2661/rt2860).

What / expected

When the child attach (rt2560_attach/rt2661_attach/rt2860_attach) fails (ring DMA allocation failure, or RT2860 "timeout waiting for NIC to initialize"), the child already unwound itself (frees all rings, calls lockuninit) and never called ieee80211_ifattach(). The glue then calls the full ral_pci_detach(), which dispatches the child detach() a second time. ieee80211_ifdetach() unconditionally executes LIST_REMOVE(ic, ic_next) (sys/sys/queue.h:460-468); on the zeroed, never-attached ic, ic_next.le_prev == NULL, so *(elm)->field.le_prev = … writes through NULL β†’ fatal trap 12 at virtual address 0. Had that somehow survived, the subsequent rt2560_free_tx_ring() calls double-free every DMA ring (ring->desc/ring->data are not NULLed after freeing).

Build (on the audit DragonFly guest)

cd /root/df4996
make SYSDIR=/usr/src/sys

Run (guest)

kldload ./df4996.ko          # phase 1: safe, prints DF-4997 offset evidence
dmesg | grep DF499
sysctl kern.df4996=trigger   # phase 2: PANICS the guest (fatal trap 12, VA 0)

The guest has no Ralink PCI hardware, so the ral attach-failure trigger cannot run end-to-end. The module reproduces the exact sink with the exact precondition: a freshly-zeroed ieee80211com β€” which is precisely the state of the child softc on that path (newbus zeroes device softcs) β€” is passed to ieee80211_ifdetach().

Expected output (observed 2026-09-25, see run.log / run.2.log / panic.txt)

DF4996: zeroed ic=0xfffff8011935e200 (== ral child softc after failed attach: ieee80211_ifattach() never ran)
DF4996: invoking ieee80211_ifdetach(ic) ...
Fatal trap 12: page fault while in kernel mode
fault virtual address  = 0x0
Stopped at      ieee80211_ifdetach+0x124:  cmpq    (%rdx),%rbx

The panic lands inside ieee80211_ifdetach (the LIST_REMOVE through le_prev == NULL), matching wlan/ieee80211.c:423 + queue.h:468.

VERDICT.md
↓ download raw

DF-4996 β€” VERDICT

Finding: ral_pci_attach() (sys/dev/netif/ral/if_ral_pci.c:275-279) calls the full ral_pci_detach() when the child attach fails. Every ral child driver unwinds itself on attach failure and never ran ieee80211_ifattach(); the second, glue-driven detach then reaches ieee80211_ifdetach() on a never-attached (zeroed) ieee80211com.

Reproduced? β€” sink + precondition: YES (on the audit guest); ral trigger path: hardware-gated (proven by trace)

The path, line by line

  1. if_ral_pci.c:275 β€” error = (*psc->sc_opns->attach)(dev, pci_get_device(dev));
  2. Child failure exits exist only before ieee80211_ifattach(): - rt2560: goto fail1..fail5 at rt2560.c:235,241,247,253,259; ieee80211_ifattach(ic) only at rt2560.c:295. Fail labels free all rings (335-338) and lockuninit(&sc->sc_mtx) (340). - rt2661: fails at 231,249,256,262; ieee80211_ifattach at 296; fail labels 327-331. - rt2860: fails at 265 (NIC init timeout), 296, 305; ieee80211_ifattach at 343; fail labels 377-386 (incl. lockuninit). Newbus zeroes the softc at allocation, so at this point ic->ic_next = {NULL, NULL}.
  3. if_ral_pci.c:277 β€” (void)ral_pci_detach(dev);
  4. if_ral_pci.c:310-311 β€” psc->sc_ih is NULL (bus_setup_intr not yet run) β†’ teardown skipped.
  5. if_ral_pci.c:312 β€” (*psc->sc_opns->detach)(psc); β†’ e.g. rt2560_detach (rt2560.c:349) / rt2860_detach (rt2860.c:388) β€” first statement(s) operate on still-valid lock/MMIO, then:
  6. rt2560.c:356 / rt2661.c:348 / rt2860.c:398 β€” ieee80211_ifdetach(ic);
  7. netproto/802_11/wlan/ieee80211.c:423 β€” LIST_REMOVE(ic, ic_next); sys/sys/queue.h:468 β€” *(elm)->field.le_prev = LIST_NEXT((elm), field); with le_prev == NULL β†’ write through NULL β†’ fatal trap 12 at VA 0.

Tail (unreachable in practice because step 7 panics first, but real): rt2560_free_tx_ring() (rt2560.c:582-636) does not NULL ring->desc / ring->data after bus_dmamem_free/kfree, so the second pass (rt2560.c:359-363) would double-free every DMA ring; rt2860's rt2860_free_tx_ring (rt2860.c:601) likewise re-frees ring->txd and re-destroys desc_dmat.

Guest demonstration (2026-09-25, DragonFly 6.5-DEVELOPMENT #0)

No Ralink PCI device exists in the QEMU guest, so the trigger (failed PCI attach) cannot fire end-to-end. The module df4996.c reproduces the exact sink with the exact precondition state:

  • kmalloc(sizeof(struct ieee80211com), M_DEVBUF, M_WAITOK|M_ZERO) β€” the zeroed ic is exactly the child softc state after a failed attach;
  • ieee80211_ifdetach(ic) β€” the exact call at rt2560.c:356/rt2860.c:398.

Observed (run.2.log, panic.txt):

DF4996: sysctl trigger 'trigger'
DF4996: zeroed ic=0xfffff8011935e200 (...)
DF4996: invoking ieee80211_ifdetach(ic) ...
Fatal trap 12: page fault while in kernel mode
fault virtual address  = 0x0
fault code             = supervisor read data, page not present
instruction pointer    = 0x8:0xffffffff80751ca4
Stopped at      ieee80211_ifdetach+0x124:  cmpq    (%rdx),%rbx

0xffffffff80751ca4 is inside ieee80211_ifdetach (in-kernel symbol at 0xffffffff80751b80), i.e. the faulting instruction is the inlined LIST_REMOVE (queue.h:460-470) operating on le_prev == NULL β€” the exact mechanism cited by the finding. Guest wedged in ddb (db>); ssh died (RUN_RC=124 style timeout); required vm.sh reset with-src (done β€” guest left clean).

Impact

Kernel NULL-pointer write β†’ panic (local DoS). Not exploitable as a controlled write: the store value is LIST_NEXT (NULL here) and the target is NULL. Trigger requires the ral driver attach to fail β€” flaky/failing Ralink hardware or DMA-allocation failure under kmem pressure; not directly reachable by an unprivileged user, hence severity Low.

Honest limits

  • The ral PCI attach-failure path itself was not executed (no hardware).
  • The double-free tail was not executed (the NULL write panics first, by construction β€” ieee80211_ifdetach precedes the ring frees in all three child detaches).

Fix validation

Not kernel-built (Low severity, fix validated by review): fix.diff replaces the full-detach call with PCI-layer-only unwinding (IRQ resource, then MSI, then MEM BAR). Note the ordering requirement: the IRQ resource must be released before pci_release_msi() β€” pci_release_msi_method (sys/bus/pci/pci.c:2088-2090) returns EBUSY while an MSI resource is still allocated. pci_release_msi() is idempotent (ENODEV when nothing allocated, pci.c:2075).

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 reproduced→ patch + rebuild →patched clean

fix.diff not kernel-built (Low severity, panic-only bug, no ral hardware to build+attach against). Validated by review: removes the only second-detach dispatch; preserves the correct unwind order (IRQ resource before pci_release_msi, which returns EBUSY while an MSI resource is allocated - pci.c:2088-2090); the bus_setup_intr-failure path (if_ral_pci.c:291-295) is intentionally untouched because there the child attach succeeded and a single full detach is correct.

findings/poc/DF-4996/fix.diff
↓ fix.diffper-fix-DF-4996

Confirmed kernel references

Detail

Evidence (decisive lines)

run.2.log + panic.txt (fatal trap 12, VA 0x0, Stopped at ieee80211_ifdetach+0x124, db> wedge); VERDICT.md (full line-by-line path incl. child fail labels proving ieee80211_ifattach never ran); fix.diff (PCI-layer-only unwind replacing the full detach call)

PoC changes

No seed PoC existed. Authored a KLD (df4996.c): MOD_LOAD prints compiled offsetof evidence (DF-4997), a kern.df4996 sysctl write triggers the DF-4996 sink (ieee80211_ifdetach on zeroed ic). Build needed SRCS+=device_if.h bus_if.h (sys/bus.h requires generated headers); DragonFly guest has no base64, files pushed via ssh heredoc.

Verified recommended fix

On child-attach failure release only the PCI-layer resources (IRQ resource, then MSI, then MEM BAR) instead of calling ral_pci_detach()

Verdict

Sink and precondition reproduced on the audit guest; the ral-specific trigger is hardware-gated and proven by code trace. When a ral child attach fails (rt2560.c:235/rt2661.c:231/rt2860.c:265 - ring DMA failure or RT2860 NIC init timeout), the child unwinds itself and never calls ieee80211_ifattach (only reached at rt2560.c:295/rt2661.c:296/rt2860.c:343); if_ral_pci.c:277 then runs the full ral_pci_detach, whose child-detach dispatch (if_ral_pci.c:312) reaches ieee80211_ifdetach (rt2560.c:356/rt2661.c:348/rt2860.c:398) on the zeroed never-attached com; LIST_REMOVE (wlan/ieee80211.c:423, sys/sys/queue.h:468) writes through ic_next.le_prev==NULL -> fatal trap 12 at VA 0. Demonstrated on DragonFly 6.5-DEVELOPMENT by calling ieee80211_ifdetach on a freshly-zeroed ieee80211com (the exact softc state on that path): 'Fatal trap 12 ... fault virtual address = 0x0 ... Stopped at ieee80211_ifdetach+0x124: cmpq (%rdx),%rbx', guest wedged in ddb, ssh dead. Had it survived, rt2560_free_tx_ring would double-free every DMA ring (rt2560.c:582-636 frees without NULLing ring->desc/ring->data). Unprivileged users cannot trigger attach failure directly (needs failing Ralink hardware or DMA exhaustion), so impact is a kernel panic (DoS), severity Low.