# DF-0609 — Verdict: REPRODUCED (panic) → FIX VALIDATED

## Verdict

**REPRODUCED.** The missing `return;` after `m_freem(m); *mp = NULL;` in
`netisr_characterize()` is a real, deterministic UAF-read + NULL-deref bug.
Triggered via a KLD module that calls `netisr_characterize(NETISR_NETGRAPH=30,
&m, 0)` (a slot whose `ni_handler` is NULL on the default GENERIC kernel
because netgraph is not loaded). The fix — adding `return;` — eliminates the
panic and was validated on a single-fix kernel (`#1`).

## Mechanism (trigger → primitive → effect)

The bug is in `sys/net/netisr.c` lines 514–525. Confirmed by source tracing:

1. **Entry:** `netisr_characterize(num=30, mp=&m, hoff=0)` is called. `m` is
   loaded from `*mp` at line 500. `ni = &netisrs[30]`.
2. **Buggy branch (line 515–519):** `ni->ni_handler == NULL` (NETISR_NETGRAPH
   never registered) → `kprintf("Unregistered isr 30")` → `m_freem(m)` (line
   517, **mbuf freed**) → `*mp = NULL` (line 518) → `}` (line 519,
   **NO return — falls through**).
3. **UAF read (line 524):** `if ((m->m_flags & M_HASH) == 0)` — `m` is the
   local variable still pointing at the **freed** mbuf. This is a
   use-after-free read. The freed mbuf's `m_flags` residue was `0x8002`
   (M_PKTHDR|M_EXT), and M_HASH (0x2000) was clear → condition TRUE.
4. **NULL-deref (line 525):** `ni->ni_hashfn(mp, hoff)` — for a never-registered
   ISR, `ni_hashfn` is also NULL (zero-initialized static array). The CPU
   attempts to execute code at address 0x0 → **page fault at NULL**.

The sibling functions `netisr_queue` (line 407: `return (EIO)`) and
`netisr_handle` (line 467: `return EIO`) both return immediately after the same
`m_freem` pattern — confirming the missing `return` in `netisr_characterize`
is the defect.

### Observed panic (unpatched #0 kernel)

```
DF-0609: m=0xfffff80118649000 m_flags=0x8002 — calling netisr_characterize(NETISR_NETGRAPH=30, &mp, 0)
netisr_characterize: Unregistered isr 30
Fatal user address access from kernel mode from sysctl at 0000000000000000
Fatal trap 12: page fault while in kernel mode
fault virtual address    = 0x0
instruction pointer      = 0x8:0x0
```

`instruction pointer = 0x8:0x0` confirms the NULL function-pointer call
through `ni->ni_hashfn` (NULL for an unregistered ISR). This is preceded by
the UAF read of `m->m_flags` from freed memory (confirmed by the freed mbuf's
`0x8002` residue).

## Trigger approach

A KLD module (`trigger_kmod.c`) registers a sysctl `hw.df0609.trigger`.
Writing 1 to it allocates an mbuf via `m_gethdr(M_WAITOK, MT_DATA)` and calls
`netisr_characterize(NETISR_NETGRAPH, &m, 0)`. NETISR_NETGRAPH (30) has
`ni_handler == NULL` on the default GENERIC kernel (netgraph not loaded), so
the buggy branch is entered deterministically.

**Why a KLD trigger?** On the default X86_64_GENERIC kernel, no ether_type
reachable through `ether_characterize()` maps to a NETISR slot with a NULL
handler — IP/ARP/IPv6 all have registered handlers, and MPLS frames fall to
`default: NETISR_MAX` which exits cleanly at line 504–507 (MPLS not compiled
in). The only way to reach the buggy branch is to call
`netisr_characterize()` with a `num` whose handler is NULL, which requires
kernel-context code (KLD module). This mirrors the real-world attack vector
described in the finding: a protocol module's `kldload`/`kldunload` window
where `ni_handler` is transiently NULL while packets of the matching
ether_type are being received.

## Exploit chain

Not a memory-corruption exploitation chain. The bug is a deterministic
panic (DoS) via NULL function-pointer call. The UAF read of `m->m_flags`
(line 524) reads one field from a freed mbuf before the NULL-deref crash; it
drives a branch decision but the value cannot be attacker-controlled in this
single-shot path (the freed mbuf's stale `m_flags` is used immediately, with
no reallocation window between `m_freem` and the read). The realistic impact
ceiling is **local/adjacent DoS (kernel panic)**.

## Fix

Add `return;` after `*mp = NULL;` (line 518), mirroring the early-return
pattern in `netisr_queue:407` and `netisr_handle:467`:

```diff
--- a/sys/net/netisr.c
+++ b/sys/net/netisr.c
@@ -516,6 +516,7 @@
 		kprintf("%s: Unregistered isr %d\n", __func__, num);
 		m_freem(m);
 		*mp = NULL;
+		return;
 	}
```

This is a one-line fix, matches the finding's `## Recommended fix` proposal.

## Fix validation (Phase 8)

### Before (unpatched #0 kernel)

```
kldload df0609_trigger.ko  → OK
sysctl hw.df0609.trigger=1 → kernel PANIC
  netisr_characterize: Unregistered isr 30
  Fatal trap 12: page fault while in kernel mode
  fault virtual address = 0x0
  instruction pointer   = 0x8:0x0
  Guest: DOWN (DDB)
```

### After (single-fix #1 kernel)

```
kldload df0609_trigger.ko  → OK
sysctl hw.df0609.trigger=1 → returned cleanly, NO panic
  netisr_characterize: Unregistered isr 30
  DF-0609: returned OK, mp=0 (NULL == mbuf freed cleanly, no panic — FIX WORKS)
  Guest: UP
```

Ran twice — both clean, no panic. Module unloaded cleanly afterward.

**Patched kernel:** `DragonFly 6.5-DEVELOPMENT #1: Fri Jul  3 03:11:19 UTC 2026`
sha256(`/boot/kernel/kernel`) = `6745b72b035f1a2b9f3602c2886fabfc61d2e657a917b5614335cb9c6785dd5f`

**fix_status: FIXED** — bad behavior (panic) is gone on the patched kernel
and present on the unpatched baseline. Clean before/after.

## PoC changes

- `trigger_kmod.c` — written from scratch (the scaffold referenced a non-
  existent file). A KLD module that registers `hw.df0609.trigger` sysctl;
  writing 1 allocates an mbuf and calls `netisr_characterize(NETISR_NETGRAPH,
  &m, 0)`. Fixed two compile errors during iteration: added `#include
  <sys/malloc.h>` (for `M_WAITOK`) and removed `CTLFLAG_MPSAFE` (not defined
  on DragonFly).
- `Makefile` — DragonFlyBSD KLD Makefile using `bsd.kmod.mk`.
- `build.sh` / `run.sh` — repro scripts.
- `fix.diff` — the one-line `return;` fix, git-apply-able.
