fwe_stop() frees RX bulkxfer chunks and their mbufs while fwohci_rbuf_update()/fwe_as_input() still manipulate or walk them β teardown race causes UAF write/read and mbuf double-free
Summary
fwe_stop (under if_serializer) sleeps up to 1s inside irx_disable with FWXFERQ_OPEN/HANDLER still set, then m_freem's every chunk mbuf and kfrees bulkxfer; fwohci_rbuf_update runs in irq context with crit_enter only, moves the same chunks and calls hand=fwe_as_input which takes the serializer only inside itself - no common lock covers the transition (serializers non-recursive). Losing interleavings: STAILQ ops on freed bulkxfer (UAF write), mtod on freed mbuf, m_freem double-free of clusters, freed-mbuf delivery into if_input. Remote FW peer times the traffic; root teardown races it. FireWire-hw-gated (QEMU cannot emulate OHCI-1394). Flag-reorder + slz serializer + hand revalidation fix authored.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4762 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.6 KB | β raw | |
| VERDICT.md | β | 6.7 KB | β raw | |
| fwe_trigger.c | β | 2.7 KB | view raw | |
| build.sh | β | 89 B | view raw | |
| run.sh | β | 211 B | view raw | |
| build.log | β | 139 B | view raw | |
| run.log | β | 847 B | view raw | |
| env.txt | β | 904 B | view raw | |
| fix.diff | β | 3.5 KB | view raw | |
| verdict.json | β | 4.0 KB | view raw | |
| manifest.json | β | 1.0 KB | view raw |
DF-4762 β fwe_stop() RX teardown races fwohci_rbuf_update()/fwe_as_input(): kfree(bulkxfer) + m_freem() while chunks are still being queued/walked β UAF write/read + mbuf double-free
File: sys/dev/netif/fwe/if_fwe.c (pass 2). Related: sys/bus/firewire/fwohci.c.
Build / Run / Expected
build.sh # cc -O -o fwe_trigger fwe_trigger.c (on the audit guest) run.sh # as root: probe fwe0 + document the ifconfig-down race driver
On hardware with an OHCI-1394 controller (fwohci0 β fwe0), the full
reproducer is:
- Peer node streams isochronous packets on the fwe stream channel
(default
hw.firewire.fwe.stream_ch=1), e.g. another machine doing IP-over-FireWire traffic to us. - Root loop:
while :; do ifconfig fwe0 up; ifconfig fwe0 down; done. - Expected on a vulnerable kernel: panic (page fault in
fwe_as_inputwalking a freedbulkxferchunk, orm_freem()double-free of an mbuf cluster freed byfwe_stop) within seconds-to-minutes of racing.
On the audit QEMU guest there is no FireWire controller (QEMU has no
OHCI-1394 device model at all), so fwe0 cannot exist β see run.log
(exit 2, "Device not configured") and env.txt. Runtime verification is
therefore blocked by absent hardware; the pack carries the full
source-level proof instead (VERDICT.md).
Verdict (see VERDICT.md, verdict.json)
Not runtime-testable on this guest (hardware absent). Source-level analysis: certain race window, no common lock across the freed state; severity Medium (admin-triggered teardown racing remote peer traffic; kernel memory corruption when hit).
DF-4762 VERDICT β fwe_stop() vs RX-completion race β UAF/double-free
Status: NOT RUNTIME-TESTABLE on the audit guest (hardware absent)
The audit guest is QEMU/virtio-only (env.txt: pciconf lists no class
0x0c0010 OHCI-1394 device; ifconfig -a has no fwe0). QEMU provides no
IEEE-1394 host controller model, so fwe(4) β which only attaches as a
child of fwohci(4) (DRIVER_MODULE(fwe, firewire, ...),
sys/dev/netif/fwe/if_fwe.c:619) β can never be instantiated here. The
PoC compiles and runs on the guest and exits 2 with "fwe0 not present"
(run.log), i.e. the trigger itself is sound; only the target device is
missing. Per the run contract this is not_testable; persisted into the
DB enum as inconclusive.
The bug (source-level proof)
Object lifetime: fwe_init() (if_fwe.c:315-357) allocates
xferq->bulkxfer = kmalloc(sizeof(struct fw_bulkxfer) * bnchunk) (line
340) β an array whose entries themselves are linked onto the xferq
chunk queues: STAILQ_INSERT_TAIL(&xferq->stfree, &xferq->bulkxfer[i],
link) (line 352), each carrying an mbuf (bulkxfer[i].mbuf, line 349).
fwohci_irx_enable() (fwohci.c:1311) moves them stfreeβstdma under
crit_enter() only; fwohci_rbuf_update() (fwohci.c:2132, called from
the fwohci interrupt/poll context at fwohci.c:1818) moves them
stdmaβstvalid (fwohci.c:2167-2168) and then invokes the registered
handler ir->hand(ir) (fwohci.c:2184) = fwe_as_input(), which only
inside itself does lwkt_serialize_enter(ifp->if_serializer)
(if_fwe.c:543) and then walks STAILQ_FIRST(&xferq->stvalid),
dereferences sxfer->mbuf, writes sxfer->mbuf = m_getcl(...)
(if_fwe.c:552), m_freem(m) (562), and hands m to if_input (586).
The teardown fwe_stop() (if_fwe.c:244-282) runs under the same
serializer (entered by fwe_detach at 289-291, or by the SIOCSIFFLAGS
ioctl path) and:
- line 262:
fc->irx_disable(fc, fwe->dma_ch)β fwohci_irx_disable (fwohci.c:1281) stops the DMA andtsleeps up to 1 s β but theFWXFERQ_OPEN/FWXFERQ_HANDLERguards checked by the interrupt path (fwohci.c:1813, 2183) are cleared only after it returns (if_fwe.c:263-266), so during that whole second the door is open; - line 269:
m_freem(xferq->bulkxfer[i].mbuf)β frees every chunk mbuf; - line 270:
kfree(xferq->bulkxfer, M_FWE)β frees the array itself, whilestvalid/stdma(queue heads in the still-live xferq) point into it and an in-flightfwohci_rbuf_update()/blockedfwe_as_input()still references those entries.
No lock is held in common across step 2/3 and the RX-completion path:
fwohci_rbuf_update() runs with only crit_enter() (a critical
section, not a serializer), and fwe_as_input() acquires the serializer
only after fwohci_rbuf_update() has already been manipulating the
freed chunks. lwkt serializers are non-recursive
(sys/sys/serialize.h:42), which is why fwe_as_input cannot be called
with the serializer already held by rbuf_update β the code structure
itself proves there is no common exclusion.
Concrete losing interleavings (either suffices):
- (a)
fwohci_rbuf_updatepasses theFWXFERQ_OPENcheck (fwohci.c:1813) during irx_disable's 1 s sleep, executes STAILQ_REMOVE_HEAD/INSERT_TAIL onchunk->link(fwohci.c:2167-2168) concurrently with fwe_stop's kfree at if_fwe.c:270 β UAF write into freed bulkxfer memory; then callshand(2184) which blocks on the serializer held by fwe_stop, resumes after the kfree and walksstvalid(if_fwe.c:544-586):mtod(sxfer->mbuf)on a freed mbuf,sxfer->mbuf = m_getcl(...)UAF write (552),m->m_data += ...UAF write into the freed mbuf (567),m_freem(m)double-free (562 β the same mbuf was already freed by fwe_stop:269), andif_input(ifp, m)feeding a freed mbuf chain into the entire network stack. - (b)
fwe_as_inputis already blocked atlwkt_serialize_enter(543) while fwe_stop frees everything (269-270); on resume it walks the freed chunks β same UAF/double-free as (a).
Impact ceiling: double-free of mbuf clusters (system-wide allocator
metadata corruption) plus controlled-offset UAF writes
(sxfer->mbuf/m->m_data into freed zone memory). Triggering requires
an admin action (ifconfig fwe0 down, SIOCSIFFLAGS β if.c gates it on
root β or driver detach) racing inbound isochronous traffic from a peer
node; the peer (attacker) fully controls the traffic timing, so the
race win probability is a function of how often teardown happens under
load. Not unprivileged-only β severity Medium (mirrors the DF-4497
sbsh deactivate-race family).
Why not false-positive
The lock model is verifiable statically: the only serialization
fwe_as_input has with fwe_stop is ifp->if_serializer, acquired at
if_fwe.c:543 β after fwohci_rbuf_update has mutated the shared chunk
queues (fwohci.c:2148-2168); fwe_stop's frees (if_fwe.c:269-270) are
guarded by that serializer alone. Nothing prevents the interleavings
above. Related-but-distinct from DF-1849 (unbounded read of
stream.len at if_fwe.c:560-569): this finding is about freeing
chunks/mbufs that completion processing still touches.
Fix validation
Not possible on this guest (no hardware): fix_status = not_testable.
fix.diff is authored against the read-only tree (never applied):
fwe_stop(): clearFWXFERQ_OPEN|FWXFERQ_HANDLER|FWXFERQ_STREAMand NULLhandbefore calling irx_disable (closes the fwohci.c:1813/2183 check windows during the disable sleep).firewirereg.h/fwohci.c: givefw_xferqan optional serializer (slz);fwohci_rbuf_update()moves chunks and snapshotshandunderslz;fwe_init()setsxferq->slz = ifp->if_serializer.fwe_as_input(): revalidatexferq->hand == fwe_as_inputafter acquiring the serializer and bail if torn down (protects callers that were already blocked at 543).
On hardware the validation procedure is: stream from a peer while looping ifconfig down/up; baseline kernel must panic/UAF within minutes; patched kernel must survive indefinitely (see run.sh).
Related minor issue (not filed separately)
At detach with in-flight TX xfers, fwe_stop frees only the idle
xfers on fwe->xferlist (if_fwe.c:272-277); up to 127 in-flight xfers
are later re-inserted into the (reinitialized or orphaned) list and
leaked (bus_generic_detach does not free the softc β
sys/kern/subr_bus.c:2867-2880 β so this is a leak, not a UAF).
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 against read-only sys/ after source verification; cannot build/validate on the guest (no OHCI-1394 hardware, fwe driver cannot attach).
fix.diff in this pack
Confirmed kernel references
- sys/dev/netif/fwe/if_fwe.c:258
- sys/dev/netif/fwe/if_fwe.c:269
- sys/dev/netif/fwe/if_fwe.c:270
- sys/dev/netif/fwe/if_fwe.c:334
- sys/dev/netif/fwe/if_fwe.c:340
- sys/dev/netif/fwe/if_fwe.c:543
- sys/dev/netif/fwe/if_fwe.c:552
- sys/dev/netif/fwe/if_fwe.c:562
- sys/bus/firewire/fwohci.c:1813
- sys/bus/firewire/fwohci.c:2148
- sys/bus/firewire/fwohci.c:2167
- sys/bus/firewire/fwohci.c:2184
- sys/bus/firewire/fwohci.c:1281
Detail
Exploit chain
peer streams on stream_ch while root ifconfig down: rbuf_update inserts freed bulkxfer chunk into stvalid (UAF write) / blocked fwe_as_input resumes after kfree -> m_freem() double-free of mbuf cluster (global allocator metadata corruption) plus controlled-offset UAF writes into freed zone memory; full uid0 chain not assessed - no hardware to develop it on.
Evidence (decisive lines)
VERDICT.md (interleavings + path:line proof); run.log (PoC builds BUILD_OK on guest, exits 2 'fwe0 not present'); env.txt (guest PCI/ifconfig inventory); fix.diff (3-file serializer fix)
PoC changes
Authored the trigger from scratch (no seed): if_nametoindex('fwe0') probe + SO_BROADCAST UDP flood for the TX half; the root half (ifconfig down loop) documented in run.sh. No code errors found in own draft; build succeeded first try on guest.
Verified recommended fix
Serialize RX chunk processing against teardown: clear FWXFERQ_OPEN/HANDLER and hand=NULL in fwe_stop() BEFORE irx_disable(); add fw_xferq.slz serializer taken by fwohci_rbuf_update() around chunk-queue mutation and hand snapshot; fwe_as_input() revalidates hand after acquiring if_serializer.
Verdict
NOT runtime-testable on the audit guest: fwe(4) only attaches under an OHCI-1394 FireWire controller and the QEMU guest has none (env.txt: no class 0x0c0010 PCI device; ifconfig -l has no fwe0; QEMU has no 1394 device model at all) - contract status not_testable, mapped to 'inconclusive' for the poc_results enum. Source-level proof stands: fwe_stop() (if_fwe.c:258-281) frees bulkxfer chunks and their mbufs under if_serializer only, while fwohci_rbuf_update() (fwohci.c:2148-2184, interrupt context, crit_enter() only) manipulates the same stdma/stvalid queues and then calls fwe_as_input(), which takes that serializer only after the mutation; FWXFERQ_OPEN/HANDLER guards are cleared AFTER fwohci_irx_disable()'s 1s sleep, so a completion racing ifconfig-down/detach walks freed chunks -> UAF write (sxfer->mbuf, m->m_data), m_freem double-free, and freed-mbuf delivery into if_input. Trigger needs admin teardown racing peer-controlled isochronous traffic -> Medium, memcorrupt bucket.
No comments yet.