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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3426 Β· 28 files
FileTypeDescriptionSize
README.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 4.7 KB ↓ raw
run.log β€” 3.0 KB view raw
panic_uaf_run1_serial.txt β€” 2.9 KB view raw
panic_uaf_run2_serial.txt β€” 16.2 KB view raw
unreg_accounting.txt β€” 1.6 KB view raw
run_evt.log β€” 3.3 KB view raw
trigger_txpool_uaf.c β€” 6.4 KB view raw
build.sh β€” 161 B view raw
run.sh β€” 202 B view raw
build.log β€” 10.8 KB view raw
env.txt β€” 593 B view raw
wna_shim.h β€” 712 B view raw
wna_shim.c β€” 630 B view raw
Makefile β€” 693 B ↓ download
rig_trace.patch β€” 2.2 KB view raw
rig_port2.patch β€” 357 B view raw
fix.diff β€” 1.2 KB view raw
fix3426-rig.diff β€” 365 B view raw
fix2627-rig.diff β€” 788 B view raw
fix_validation.log β€” 3.0 KB view raw
fix_validation27.log β€” 1.5 KB view raw
fix_run.log β€” 3.0 KB view raw
fix_build.log β€” 10.8 KB view raw
build_fix26.log β€” 10.8 KB view raw
build_fix2627.log β€” 10.8 KB view raw
verdict.json β€” 7.4 KB view raw
netmap_build.previous_attempt.log β€” 8.8 KB view raw

DF-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 both RIG: txsync enter: laps, then RIG: 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 but netstat -m shows 18446744073709550849/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.

VERDICT.md
↓ download raw

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)

  1. Double free, counter-exact, on a live guest (unreg 8): 8 packets dispatched (each mbuf freed by tap purge inside ifq_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)

  2. 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 rewinds ring->cur so generic_netmap_txsync() walks over the dangling tx_pool[0..510] (netmap_generic.c:504) and generic_xmit_frame() (netmap_freebsd.c:124-138) writes m_len=0, 64 attacker-supplied bytes via m_copyback2, rcvif/hash, then hands the stolen live mbuf to ifq_dispatch() a second time. At ring exhaustion generic_set_tx_event (netmap_generic.c:421-445) arms SET_MBUF_DESTRUCTOR on a freed pool entry and m_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).

  3. Mass double free at unregister (evt): full lap + close β†’ netstat -m in-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

fixed
baseline reproduced→ patch + rebuild →patched clean

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

Confirmed kernel references

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.