# 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:

1. The armed pool mbuf's *own* `m_freem()` (in `generic_set_tx_event`,
   `netmap_generic.c:443`) does **not** invoke `ext_free`: pool mbufs are
   `m_getcl()` combined-cache objects (`M_CLCACHE|M_EXT|M_EXT_CLUSTER`,
   sharecount 1), and that branch of `m_free()`
   (`sys/kern/uipc_mbuf.c:1371-1407`) returns the mbuf+cluster pair to the
   cache *without* calling `ext_free`.  So the write
   `m->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_put` runs no destructor,
   and `objcache_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 restores
   `ext_free = m_mclfree`.

2. Whoever allocates that recycled mbuf+cluster inherits the netmap
   destructor.  In the decisive run it was the trigger's UDP spray
   (`uaf` mode): a live UDP packet mbuf was allocated from the booby-trapped
   cache entry, and when `udp_input()` finished with it —
   `m_freem → m_free` taking an `ext_free`-invoking branch
   (`uipc_mbuf.c:1412/1433`) — `generic_mbuf_destructor` was called with
   `m = 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.

3. Arming in isolation (`evt` mode): ring exhaustion
   (`nr_hwavail == 0`, `netmap_generic.c:567-574`) armed the event on
   `e=511` — a slot whose mbuf had already been dispatched and freed in the
   *same* lap — so `SET_MBUF_DESTRUCTOR` is a UAF write into freed memory
   and the following `m_freem` a 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 from `mbuf(pkthdr+)cluster` caches).

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.
