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)
PoC verification
Evidence pack
findings/poc/DF-4996 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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
if_ral_pci.c:275βerror = (*psc->sc_opns->attach)(dev, pci_get_device(dev));- Child failure exits exist only before
ieee80211_ifattach(): - rt2560:goto fail1..fail5at rt2560.c:235,241,247,253,259;ieee80211_ifattach(ic)only at rt2560.c:295. Fail labels free all rings (335-338) andlockuninit(&sc->sc_mtx)(340). - rt2661: fails at 231,249,256,262;ieee80211_ifattachat 296; fail labels 327-331. - rt2860: fails at 265 (NIC init timeout), 296, 305;ieee80211_ifattachat 343; fail labels 377-386 (incl.lockuninit). Newbus zeroes the softc at allocation, so at this pointic->ic_next = {NULL, NULL}. if_ral_pci.c:277β(void)ral_pci_detach(dev);if_ral_pci.c:310-311βpsc->sc_ihis NULL (bus_setup_intr not yet run) β teardown skipped.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:- rt2560.c:356 / rt2661.c:348 / rt2860.c:398 β
ieee80211_ifdetach(ic); 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);withle_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_ifdetachprecedes 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_testablefix.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
Confirmed kernel references
- sys/dev/netif/ral/if_ral_pci.c:275
- sys/dev/netif/ral/if_ral_pci.c:277
- sys/dev/netif/ral/if_ral_pci.c:312
- sys/dev/netif/ral/rt2560.c:295
- sys/dev/netif/ral/rt2560.c:356
- sys/dev/netif/ral/rt2560.c:582
- sys/dev/netif/ral/rt2860.c:343
- sys/dev/netif/ral/rt2860.c:377
- sys/dev/netif/ral/rt2860.c:398
- sys/netproto/802_11/wlan/ieee80211.c:423
- sys/sys/queue.h:468
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.
No comments yet.