Generic netmap TX pool mbufs are dispatched with no retained reference: tx_pool slots alias driver-owned and then freed memory (controlled UAF write + double free)
Summary
generic_netmap_txsync takes the preallocated pool mbuf tx_pool[j] and hands its ONLY reference to the driver via generic_xmit_frame -> ifq_dispatch - the refcount retention the file-header design describes is #if 0'd out. tx_pool[j] is never cleared, so after the driver (or the ifq overflow path) frees the mbuf, the slot dangles. Sinks: (a) reuse at the next ring lap writes m->m_len/m_pkthdr.len/hash/rcvif and m_copyback2's attacker-controlled packet bytes into the recycled header, then re-dispatches the freed mbuf (double free at TX completion); (b) generic_set_tx_event m_freem()s an in-flight driver-owned mbuf on dispatch failure or ring exhaustion; (c) the unregister path m_freem()s every pool entry, dangling and in-flight alike. DF-0616 covered only the RX copy overflow; the TX lifetime was missed entirely. Any holder of an open /dev/netmap fd (0660 root:wheel) registering a generic adapter gets kernel heap UAF-write/double-free - the classic mbuf-objcache grooming -> forged mbuf -> root chain - the moment the module is buildable. PLATFORM FACT verified: module unbuildable on this tree (build log in pack) - latent High.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3426 Β· 28 filesDF-3426 β netmap generic TX pool mbuf UAF / double-free (VERIFIED)
What this is
The production netmap KLD (verbatim sys/net/netmap/ sources) rebuilt in a
copied tree with two infrastructure port shims (the stock module does not
compile on this tree), loaded on the stock 6.5-DEVELOPMENT INVARIANTS guest,
and driven through the real /dev/netmap userspace ABI.
The two port shims (no logic under audit is touched):
- wna_shim.[ch] β pointer-sized WNA side table (this tree's struct ifnet
has no if_unused7; netmap_kern.h:747)
- rig_port2.patch β dev_open_args.a_fp β *a_fpp (netmap_freebsd.c:387)
- rig_trace.patch β RIG_TRACE kprintf diagnostics only
Build (guest, as root, files scp'd to /root/df3426)
./build.sh # -> netmap-src/netmap.ko + ./trigger
Run (guest, as root)
./run.sh probe # sanity: register/unregister, counters flat ./run.sh unreg 8 # 8 double frees, counter-exact, guest ALIVE ./run.sh uaf 512 4096 16384 # UAF reuse -> Fatal trap 12 panic ./run.sh evt # ring exhaustion + unregister -> 2^64-767
Prerequisite state (run.sh sets it up): kldload if_tap;
ifconfig tap0 create && ifconfig tap0 up; kldload ./netmap-src/netmap.ko.
tap0 has no reader, so tapifstart()->ifsq_purge() frees every dispatched
mbuf synchronously inside ifq_dispatch() β a deterministic stand-in for
driver TX completion.
Expected output
unreg 8: rc=0;netstat -m"mbuf clusters in use" drops 256 β 248 (exactly β8 = the 8 double frees). Guest stays up.uaf 512 4096 16384: kernel prints bothRIG: txsync enter:laps, thenRIG: ring exhausted,RIG: generic_set_tx_event,RIG: generic_mbuf_destructor CALLED, then "Fatal trap 12: page fault while in kernel mode" and the guest dies in DDB (run.log / panic_uaf_run2_serial.txt).evt: rc=0 butnetstat -mshows18446744073709550849/33552 mbuf clusters in use(counter underflow).
Fix validation (fix.diff)
cd netmap-src && patch < ../fix3426-rig.diff && make # fixed module # then re-run the exact same PoC matrix -> no panic, counters flat, # set_tx_event arms on NULL (mbuf=0) and returns early
See fix_validation.log / fix_validation27.log, VERDICT.md.
Verdict
reproduced (impact: panic β kernel memory corruption gated on root/wheel for /dev/netmap access, as filed). Full narrative: VERDICT.md.
DF-3426 β netmap generic TX pool mbuf UAF / double-free β VERIFIED
status: reproduced | reproduced: 1 | impact: panic (kernel memory corruption) | confidence: certain
The finding was previously closed build_failed (the stock netmap KLD does
not compile on this tree). This run made the production module buildable in
a copied tree with two infrastructure-only port shims (pointer-sized WNA
side table + dev_open_args.a_fpp), loaded it into the stock
6.5-DEVELOPMENT INVARIANTS guest, and drove the real /dev/netmap ABI
(NIOCREGIF/NIOCTXSYNC on a tap(4) port with no reader, so
tapifstart()->ifsq_purge() "completes" TX β i.e. frees the mbuf β
synchronously inside ifq_dispatch()).
What was demonstrated (unfixed production code)
-
Double free, counter-exact, on a live guest (
unreg 8): 8 packets dispatched (each mbuf freed by tap purge insideifq_dispatch), then close() βgeneric_netmap_register(0)m_freem()s every pool entry (netmap_generic.c:275-280) β the 8 dispatched slots freed a second time.netstat -m: mbuf clusters in use 256 β 248 (exactly β8). No legitimate path decrements without an allocation. (unreg_accounting.txt) -
Ring reuse UAF + kernel page fault panic (
uaf 512 4096 16384): lap 1 dispatches 512 pool mbufs (all freed synchronously); a 4096-datagram UDP spray to 127.0.0.1 reallocates the freed combined mbuf+cluster objects as live network mbufs through the objcache magazine fast path; lap 2 rewindsring->cursogeneric_netmap_txsync()walks over the danglingtx_pool[0..510](netmap_generic.c:504) andgeneric_xmit_frame()(netmap_freebsd.c:124-138) writesm_len=0, 64 attacker-supplied bytes viam_copyback2,rcvif/hash, then hands the stolen live mbuf toifq_dispatch()a second time. At ring exhaustiongeneric_set_tx_event(netmap_generic.c:421-445) armsSET_MBUF_DESTRUCTORon a freed pool entry andm_freem()s it. Result: Fatal trap 12, page fault in kernel mode, guest dead in DDB (run.log / panic_uaf_run2_serial.txt; reproduced twice). -
Mass double free at unregister (
evt): full lap + close βnetstat -min-use counter underflows to 18446744073709550849 (= 2^64 β 767; net drift β1023) with 128289406846% of mb_map "in use" β the mbuf accounting system itself testifies to ~1024 double frees. (run_evt.log)
Gating (unchanged from the filing)
/dev/netmap is 0660 root:wheel (netmap.c:2237); the guest's unprivileged
user maxx is not in wheel. The corruption is therefore reachable by
root/wheel (or any service granted the fd), not by an unprivileged user;
severity stays High for kernel memory corruption, consistent with the
filing. From the permitted context the primitive is: (a) a double
objcache_put (two-owner mbuf) and (b) a repeatable attacker-sized write into
recycled mbuf headers/clusters β standard mbuf-forgery escalation material;
a full uid=0 chain from uid!=0 is not applicable through this device node.
Fix validation (fix.diff = clear tx_pool[j] after dispatch)
Baseline (above) vs patched module, identical PoC matrix (fix_validation.log):
| PoC | unfixed | fixed |
|---|---|---|
unreg 8 |
clusters 256β248 (β8) | 256β256 flat |
uaf 512 4096 16384 |
Fatal trap 12, guest dead | rc=0, alive, flat |
evt |
counter = 2^64β767 | flat; event arms on NULL |
Trace under the fix: generic_set_tx_event e=β¦ mbuf=0 β NULL-check early
return (no destructor install, no m_freem of driver-owned memory);
replenish paths (:361, :505-512) refill the NULL slots. fix.diff
applies cleanly to the pristine sys/net/netmap/netmap_generic.c
(patch -p4 --dry-run OK). fix_status: fixed.
Rig faithfulness
The module is the verbatim production source; only two port shims (WNA storage location; cdevpriv arg) and RIG_TRACE kprintfs were added β none touch the TX-pool lifetime logic under audit (see build.sh, rig_trace.patch, rig_port2.patch, wna_shim.[ch]). Guest /usr/src and host sys/ tree md5 match on netmap_generic.c (5c1169dfc687831d5ceb0c973d78d53a).
Environment
DragonFly dfbsd 6.5-DEVELOPMENT #0 X86_64_GENERIC (INVARIANTS), cc 8.3 [DragonFly], see env.txt.
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
fixedfix.diff (clear tx_pool[j] after dispatch) applied to the rig module: the identical PoC matrix that panicked the baseline (uaf 512 4096 16384) and underflowed its counters (unreg 8: -8; evt: 2^64-767) now runs rc=0 on all modes with netstat -m exactly flat (256/33552 before and after), both 4096+16384-datagram sprays complete, generic_set_tx_event arms on a NULL slot (RIG trace: mbuf=0) and returns early, and the guest stays up (fix_validation.log; fix2627 combined-fix rerun in fix_validation27.log). The bug is fixed by this one line because every sink (reuse, set_tx_event m_freem, unregister sweep) operates on tx_pool[] entries that now never alias driver-owned or freed memory.
fix_run.log (= fix_validation.log: full before/after PoC matrix with netstat deltas and RIG traces); fix_validation27.log (combined fix26+fix27 module, all modes rc=0, 0 destructor calls); fix_build.log (fixed-module build); fix.diff (tree patch, applies clean with patch -p4); fix3426-rig.diff/fix2627-rig.diff (rig variants)
Confirmed kernel references
- sys/net/netmap/netmap_generic.c:504
- sys/net/netmap/netmap_generic.c:520
- sys/net/netmap/netmap_generic.c:538
- sys/net/netmap/netmap_generic.c:543
- sys/net/netmap/netmap_generic.c:559
- sys/net/netmap/netmap_generic.c:567
- sys/net/netmap/netmap_generic.c:573
- sys/net/netmap/netmap_generic.c:275
- sys/net/netmap/netmap_generic.c:361
- sys/net/netmap/netmap_freebsd.c:119
- sys/net/netmap/netmap_freebsd.c:124
- sys/net/netmap/netmap_freebsd.c:127
- sys/net/netmap/netmap_freebsd.c:131
- sys/net/netmap/netmap_freebsd.c:138
- sys/net/if.c:3344
- sys/kern/uipc_mbuf.c:1316
- sys/kern/kern_objcache.c:493
Detail
Exploit chain
root/wheel (device gating, netmap.c:2237) -> open /dev/netmap -> NIOCREGIF tap0 (generic adapter, 1024-slot TX ring, tx_pool[] of m_getcl mbufs) -> NIOCTXSYNC lap 1: generic_xmit_frame() (netmap_freebsd.c:119-140) copies attacker bytes and hands each pool mbuf to ifq_dispatch() with the design's refcount retention #if 0'd (netmap_freebsd.c:131-135); the tap purge frees them all synchronously; tx_pool[j] is never cleared (netmap_generic.c:504-548) -> UDP spray reallocates the freed mbuf+cluster objects as live network mbufs via the objcache magazine fast path (no ctor re-run, kern_objcache.c:493-517) -> lap 2 rewinds ring->cur over the dangling slots: m_len=0 write + m_copyback2 of attacker-controlled bytes + rcvif/hash into stolen live mbufs, second ifq_dispatch = double free -> ring exhaustion arms generic_set_tx_event on a freed entry (SET_MBUF_DESTRUCTOR write + m_freem = more double frees; detonates DF-3427's destructor in the recycled mbuf) -> kernel page fault, DDB. Primitive characterization: repeatable attacker-sized write into recycled mbuf headers/clusters + double objcache_put (two-owner mbuf) - classic mbuf-forgery escalation material from the permitted root/wheel context; a full ROP chain was not pursued since unprivileged->uid0 is not applicable through this device node.
Evidence (decisive lines)
run.log + panic_uaf_run2_serial.txt (decisive: RIG traces of both laps, ring-exhaustion arming, destructor entry, then Fatal trap 12 / 'Stopped at generic_mbuf_destructor+0x3f' and DDB); panic_uaf_run1_serial.txt (first reproduction, fault 0x76); unreg_accounting.txt (counter-exact -8 double frees on a live guest); run_evt.log (netstat in-use = 18446744073709550849 = 2^64-767 after evt+close); fix_validation.log (patched module: identical PoC matrix -> rc=0 everywhere, counters exactly flat, set_tx_event arms on mbuf=0 and early-returns); build.log (full untrimmed module build); VERDICT.md (narrative)
PoC changes
Previous attempt ended build_failed (module unbuildable: if_unused7). Repaired rig: (1) rebuilt the full production module from a copied tree with wna_shim.[ch] (pointer-sized WNA side table; netmap_kern.h's own WNA is honored via the #ifndef guard) and rig_port2.patch (a_fp -> *a_fpp); no logic under audit changed; (2) replaced the documentation-stub trigger with a real userspace rig driving NIOCREGIF/NIOCTXSYNC over mmap'd rings (probe/evt/uaf/unreg modes, tunable laps + UDP spray); (3) added RIG_TRACE kprintfs (entry state prints + print_backtrace in the destructor) for mechanism capture.
Verified recommended fix
Clear kring->tx_pool[j] = NULL immediately after generic_xmit_frame() (ownership has been transferred to ifq_dispatch()/the driver on both success and failure); the replenish paths already handle NULL entries. See fix.diff (validated).
Verdict
REPRODUCED at runtime after making the production netmap KLD buildable (verbatim sources; two infrastructure-only port shims: pointer-sized WNA side table because this tree's struct ifnet has no if_unused7, and dev_open_args.a_fp->a_fpp). Driven through the real /dev/netmap ABI on an unopened tap(4) port whose tapifstart()->ifsq_purge() frees every dispatched mbuf synchronously inside ifq_dispatch() (deterministic TX completion). Three independent manifestations on the stock 6.5-DEVELOPMENT INVARIANTS guest: (1) unreg mode: 8 dispatches then close() -> generic_netmap_register(0) m_freem()s every pool entry -> netstat -m mbuf clusters in use drops 256->248 = exactly -8 double frees on a LIVE guest; (2) uaf mode (512-packet lap, 4096-datagram UDP spray, rewound second lap): generic_netmap_txsync reuses the dangling tx_pool[] entries, writes attacker bytes into recycled LIVE mbufs, double-dispatches them, and the run ends in 'Fatal trap 12: page fault while in kernel mode' with the guest dead in DDB (reproduced twice); (3) evt mode: full lap + close underflows the mbuf in-use counter to 2^64-767 (~1024 double frees). Gating unchanged from the filing: /dev/netmap is 0660 root:wheel, so the attacker is root/wheel or a service granted the fd; unpriv->uid0 is not reachable through this node, severity stays High for kernel memory corruption.
No comments yet.