SET_MBUF_DESTRUCTOR/generic_mbuf_destructor is ABI-incompatible with DragonFly mbufs: ext_free receives the 16-byte mbcluster, not the mbuf (OOB read -> wild ifp deref + permanent cluster leak)
Summary
SET_MBUF_DESTRUCTOR casts generic_mbuf_destructor(struct mbuf *) into m->m_ext.ext_free, but DragonFly invokes ext_free(ext_arg) where ext_arg is the 16-byte struct mbcluster. The destructor's MBUF_IFP(m) reads m_pkthdr.rcvif at offset 40 of the 16-byte object - a 32-byte OOB read into adjacent objcache memory - and feeds the bogus pointer to netmap_generic_irq -> netmap_common_irq (ifp->if_capenable, then NA(ifp)/nm_notify): wild-pointer deref/call chain. It also writes m->m_ext.ext_free=NULL through the confused pointer, and by shadowing m_mclfree never drops mcl_refs: every event mbuf leaks its 2KB cluster permanently and mclmeta_dtor's KKASSERT(mcl_refs==0) panics INVARIANTS kernels on objcache reclaim. Reached from generic_set_tx_event on dispatch failure or ring exhaustion. Same /dev/netmap precondition as DF-3426 (stacked on it). Latent until buildable. Fix: drop the destructor interception (rely on tx_clean polling).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3427 Β· 24 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.8 KB | β raw | |
| VERDICT.md | β | 4.8 KB | β raw | |
| run.log | β | 3.0 KB | view raw | |
| panic_uaf_run2_serial.txt | β | 16.2 KB | view raw | |
| run_evt.log | β | 3.3 KB | view raw | |
| trigger_txpool_uaf.c | β | 6.4 KB | view raw | |
| build.sh | β | 93 B | view raw | |
| run.sh | β | 161 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.6 KB | view raw | |
| fix3426-rig.diff | β | 365 B | view raw | |
| fix2627-rig.diff | β | 788 B | view raw | |
| fix_run.log | β | 1.5 KB | view raw | |
| fix_build.log | β | 10.8 KB | view raw | |
| fix_validation27.log | β | 1.5 KB | view raw | |
| verdict.json | β | 6.5 KB | view raw | |
| netmap_build.previous_attempt.log | β | 8.8 KB | view raw | |
| trigger_destructor_abi.c.previous_attempt | β | 2.7 KB | β download |
DF-3427 β SET_MBUF_DESTRUCTOR / generic_mbuf_destructor ABI incoherence (VERIFIED)
The rig, build and run instructions are shared with DF-3426 (this pack
carries its own copies of the sources; see ../DF-3426/README.md).
Build (guest, root)
./build.sh # verbatim-production netmap.ko + ./trigger
Run (guest, root)
./run.sh evt # arming in isolation (dormant, no panic)
./run.sh uaf 512 4096 16384 # detonation: destructor fires inside a
# recycled LIVE UDP mbuf -> Fatal trap 12
Expected output
evt: RIG tracegeneric_set_tx_event e=511(a slot already freed in the same lap) then nothing; guest alive butnetstat -munderflows at close (the DF-3426 double frees).uaf:RIG: generic_mbuf_destructor CALLED m=<mbcluster>followed by them_free β m_freem β udp_input β ip_input β netmsg_service_loopbacktrace andFatal trap 12: page fault while in kernel mode,Stopped at generic_mbuf_destructor+0x3f: testb $0x80,0x75(%rdi), fault address 0x76 / 0xffffffff00000076 β the OOB-readrcvifgarbage dereferenced asifp->if_capenable. Guest dead in DDB.
Fix validation
cd netmap-src && patch -p4 < ../fix.diff (macro no-op + __unused) # combined with the DF-3426 fix; all PoC modes rc=0, flat counters, # ZERO destructor callbacks (fix_run.log / fix_build.log)
Verdict
reproduced β the ABI incoherence detonated at runtime with the exact
predicted crash signature; mechanism refined (see VERDICT.md): the armed
mbuf's own free never calls ext_free (CLCACHE branch); the stale
ext_free=generic_mbuf_destructor survives objcache magazine recycling
(no ctor re-run) and detonates in whichever subsystem next recycles the
mbuf β demonstrated in udp_input. Full narrative: VERDICT.md.
DF-3427 β SET_MBUF_DESTRUCTOR ABI incoherence β VERIFIED (mechanism refined)
status: reproduced | reproduced: 1 | impact: panic | confidence: certain
Same rig as DF-3426 (the production netmap module, verbatim sources + two
infrastructure port shims, driven through the real /dev/netmap ABI; see
../DF-3426/ for build/run). This file records what the runtime showed
about the destructor ABI specifically.
What the finding claimed vs. what the runtime shows
Claim: generic_mbuf_destructor receives ext_arg (the 16-byte
struct mbcluster) instead of the mbuf, reads m_pkthdr.rcvif at offset 40
(32+ bytes OOB) and derefs the garbage through
netmap_generic_irq()->ifp->if_capenable.
Runtime: exactly that happened β with one mechanistic refinement that makes it worse than filed:
-
The armed pool mbuf's own
m_freem()(ingeneric_set_tx_event,netmap_generic.c:443) does not invokeext_free: pool mbufs arem_getcl()combined-cache objects (M_CLCACHE|M_EXT|M_EXT_CLUSTER, sharecount 1), and that branch ofm_free()(sys/kern/uipc_mbuf.c:1371-1407) returns the mbuf+cluster pair to the cache without callingext_free. So the writem->m_ext.ext_free = generic_mbuf_destructor(netmap_generic.c:101-104) is not a leak on this path β it is a landmine left in a freed object:objcache_putruns no destructor, andobjcache_get's per-CPU magazine fast path (sys/kern/kern_objcache.c:493-517) hands the object to the next allocator without re-running the cache ctor, so nothing restoresext_free = m_mclfree. -
Whoever allocates that recycled mbuf+cluster inherits the netmap destructor. In the decisive run it was the trigger's UDP spray (
uafmode): a live UDP packet mbuf was allocated from the booby-trapped cache entry, and whenudp_input()finished with it βm_freem β m_freetaking anext_free-invoking branch (uipc_mbuf.c:1412/1433) βgeneric_mbuf_destructorwas called withm = ext_arg = 0xfffff8008d402cc0, a 16-byte mbcluster:
RIG: generic_set_tx_event e=1023 mbuf=0xfffff8012d4a0600 (...)
RIG: generic_mbuf_destructor CALLED m=0xfffff8008d402cc0 (...)
Trace: m_free() at m_free+0x123
m_free() at m_free+0x123
m_freem() at m_freem+0x15
udp_input() at udp_input+0x2f8
ip_input() ... netmsg_service_loop()
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xffffffff00000076 (run 1: 0x76)
Stopped at generic_mbuf_destructor+0x3f: testb $0x80,0x75(%rdi)
The destructor read m_pkthdr.rcvif (offset 40) out of the 16-byte
mbcluster β garbage ifp (0x1 / 0xffffffff00000001) β
ifp->if_capenable at offset 0x75 β page fault. Guest dead in DDB.
- Arming in isolation (
evtmode): ring exhaustion (nr_hwavail == 0,netmap_generic.c:567-574) armed the event one=511β a slot whose mbuf had already been dispatched and freed in the same lap β soSET_MBUF_DESTRUCTORis a UAF write into freed memory and the followingm_freema double free (both counted under DF-3426). Without traffic to recycle the header the destructor stayed dormant and the guest survived; the detonation is deferred and cross-subsystem (any allocation frommbuf(pkthdr+)clustercaches).
Not observed at runtime: the finding's secondary "permanent 2KB cluster
leak / mclmeta_dtor INVARIANTS panic" (uipc_mbuf.c:632) β that would
require m_mclfree to actually be shadowed on an ext_free-invoking path,
which the pool mbufs' CLCACHE free branch never takes. The OOB read / wild
deref is the real, demonstrated impact.
Fix (fix.diff, validated)
SET_MBUF_DESTRUCTOR becomes a no-op on DragonFly (the function is kept,
marked __unused, for documentation): the ext_free ABI on this OS cannot
express the Linux skb-destructor concept the 2013 port assumed, and
TX-completion notification is already provided by
generic_netmap_tx_clean() polling. Built as the combined module
(fix26+fix27), the full DF-3426/DF-3427 PoC matrix runs rc=0 with flat
counters and zero destructor callbacks (fix_run.log =
fix_validation27.log; fix_build.log). The fix applies cleanly to the
pristine tree (patch -p4 --dry-run, sys/net/netmap/netmap_generic.c).
Note DF-3426's slot-clearing fix alone also prevents arming (the event slot
is always NULL), so fix.diff here is defense-in-depth against any future
caller of the macro.
Gating
Identical to DF-3426: /dev/netmap 0660 root:wheel.
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 (SET_MBUF_DESTRUCTOR no-op + generic_mbuf_destructor marked __unused; validated as the combined fix26+fix27 module) applied and rebuilt: the identical PoC matrix (uaf 512 4096 16384, evt, unreg 8) runs rc=0 with netstat -m exactly flat and dmesg shows ZERO generic_mbuf_destructor callbacks (fix_run.log = fix_validation27.log, fix_build.log). The macro can no longer plant ext_free landmines in recycled mbufs. fix.diff applies cleanly to the pristine sys tree (patch -p4 --dry-run). Note: DF-3426's slot-clearing fix alone also prevents this arming (event slot always NULL); this fix is the defense-in-depth that kills the ABI-incoherent primitive itself.
fix_run.log (= fix_validation27.log: combined-fix PoC matrix, 0 destructor calls, flat counters, uptime OK); fix_build.log (fixed-module build); fix.diff (tree patch, macro no-op + __unused); fix2627-rig.diff (rig variant)
Confirmed kernel references
- sys/net/netmap/netmap_generic.c:101
- sys/net/netmap/netmap_generic.c:322
- sys/net/netmap/netmap_generic.c:327
- sys/net/netmap/netmap_generic.c:331
- sys/net/netmap/netmap_generic.c:431
- sys/net/netmap/netmap_generic.c:439
- sys/net/netmap/netmap_generic.c:443
- sys/net/netmap/netmap_generic.c:567
- sys/kern/uipc_mbuf.c:1371
- sys/kern/uipc_mbuf.c:1412
- sys/kern/uipc_mbuf.c:1433
- sys/kern/uipc_mbuf.c:652
- sys/kern/kern_objcache.c:493
Detail
Exploit chain
root/wheel -> open /dev/netmap -> NIOCREGIF tap0 -> NIOCTXSYNC until nr_hwavail==0 (ring exhaustion) -> generic_set_tx_event writes ext_free=generic_mbuf_destructor into a pool mbuf (already freed by driver TX completion in the same lap) and m_freem()s it (double free, DF-3426 family) -> the freed combined mbuf+cluster object carries the stale destructor through objcache_put/get (magazine fast path, no ctor re-run) -> any subsystem's allocation inherits it (demonstrated: UDP spray) -> that mbuf's normal free calls ext_free(ext_arg=mbcluster) -> generic_mbuf_destructor reads m_pkthdr.rcvif 32+ bytes past the 16-byte mbcluster -> garbage ifp -> netmap_generic_irq derefs ifp->if_capenable -> kernel page fault / panic. Cross-subsystem deferred detonation; attacker-controlled heap grooming determines the OOB-read garbage (here 0x1 / 0xffffffff00000001 -> non-canonical deref).
Evidence (decisive lines)
run.log + panic_uaf_run2_serial.txt (decisive: 'RIG: generic_mbuf_destructor CALLED m=0xfffff8008d402cc0' with the m_free<-m_free<-m_freem<-udp_input<-ip_input<-netmsg_service_loop backtrace, then Fatal trap 12 at generic_mbuf_destructor+0x3f testb $0x80,0x75(%rdi), fault 0xffffffff00000076); panic_uaf_run1_serial.txt in ../DF-3426 (first reproduction, fault 0x76); run_evt.log (arming in isolation: generic_set_tx_event e=511 on an already-freed slot, destructor dormant, guest survives); fix_run.log = fix_validation27.log (combined fix: 0 destructor callbacks, all PoC modes rc=0); VERDICT.md (full mechanism)
PoC changes
Shared rig with DF-3426 (verbatim production module + WNA side-table shim + a_fpp port shim + RIG_TRACE; see that pack). Trigger rewritten from the documentation stub into the real userspace rig; RIG_TRACE adds a print_backtrace(-6) at generic_mbuf_destructor entry which captured the udp_input detonation path.
Verified recommended fix
Make SET_MBUF_DESTRUCTOR a no-op on DragonFly (ext_free receives ext_arg = mbcluster, ABI-incompatible with the mbuf-typed destructor) and keep generic_mbuf_destructor __unused for documentation; TX completion is already reported by generic_netmap_tx_clean() polling. See fix.diff (validated).
Verdict
REPRODUCED at runtime with the exact predicted crash signature, plus one mechanism refinement that widens the blast radius. generic_set_tx_event (armed on ring exhaustion, netmap_generic.c:567-574, and on dispatch failure :538) installs SET_MBUF_DESTRUCTOR (ext_free = generic_mbuf_destructor) and m_freem()s the pool entry. Runtime shows the armed mbuf's own free does NOT invoke ext_free (m_free's combined-cache branch, sys/kern/uipc_mbuf.c:1371-1407, caches mbuf+cluster without calling ext_free), so the stale ext_free is a landmine in a freed object: objcache_put runs no destructor and objcache_get's magazine fast path (kern_objcache.c:493-517) does not re-run the cache ctor, so the booby-trapped mbuf is handed to the next allocator as-is. In the decisive run a live UDP spray mbuf inherited the destructor; when udp_input()'s m_freem/m_free took an ext_free-invoking branch (uipc_mbuf.c:1412/1433), generic_mbuf_destructor was called with m = ext_arg = a 16-byte struct mbcluster, read m_pkthdr.rcvif at offset 40 (OOB), and dereferenced the garbage pointer as ifp->if_capenable: 'Fatal trap 12: page fault', fault address 0x76 / 0xffffffff00000076, 'Stopped at generic_mbuf_destructor+0x3f: testb $0x80,0x75(%rdi)', guest dead in DDB. The finding's secondary claims (permanent cluster leak, mclmeta_dtor INVARIANTS panic at uipc_mbuf.c:632) were NOT observed: m_mclfree shadowing never happens because the pool mbufs' free path never calls ext_free. Gating identical to DF-3426 (/dev/netmap 0660 root:wheel).
No comments yet.