# DF-4341 VERDICT

**Status: inconclusive on this guest (finding stands on static proof)**
**reproduced: 0 · impact on this guest: none · confidence in the finding: certain**

## What the finding claims

Three unsynchronized execution contexts mutate the same per-channel state in
`sys/dev/netif/mn/if_mn.c`:

| Context | Entry | Evidence of no synchronization |
|---|---|---|
| HW interrupt | `bus_setup_intr(self, sc->irq, INTR_MPSAFE, mn_intr, sc, &sc->intrhand, NULL)` — `if_mn.c:1329-1330` | 7th arg (serializer) is `NULL`; `mn_rx_intr`/`mn_tx_intr` take no lock, no crit section (`if_mn.c:1061-1092, 1097-1177`) |
| Data sender thread | `ng_send_data()` calls `rcvdata` synchronously in the caller's thread — `sys/netgraph/netgraph/ng_base.c:1678-1697` | `ngmn_rcvdata` (`if_mn.c:573-645`) mutates `xl`/`dp->vnext`/`dp->m` lock-free |
| Hook-teardown thread | `ng_disconnect_hook()` → `type->disconnect(hook)` lock-free — `ng_base.c:839-858` | `ngmn_disconnect` (`if_mn.c:743-797`) frees both rings + mbufs, then the core frees the hook itself (`ng_base.c:854-858`) |

The only guard is the TOCTOU `sch->state != UP` read (`if_mn.c:1070`,
`:1109`) against `sch->state = DOWN` at `if_mn.c:758` — unordered, unlocked.

Consequences (each traced to a sink):
- **UAF write**: `m->m_pkthdr.len = m->m_len = (dp->status >> 16) & 0x1fff`
  (`if_mn.c:1122`) writes through `dp->m` read from a freed-and-reused
  descriptor (the global freelist `mn_desc_free`, `if_mn.c:483-503`, recycles
  them across channels inside the same ISR).
- **UAF write on freed hook**: `ng_queue_data(sch->hook, m, NULL)`
  (`if_mn.c:1125`) after the core freed the hook.
- **Double free / freelist alias**: `mn_tx_intr` (`if_mn.c:1089-1090`) and
  `ngmn_disconnect` (`if_mn.c:787-794`) both push the same `trxd` onto
  `mn_desc_free`; `ngmn_rcvdata` (`if_mn.c:620-626`) links/writes descriptors
  the ISR may already have freed.
- **HW write-after-free without any race**: `ngmn_disconnect` only *warns*
  when the 30 µs channel-zap did not complete (`if_mn.c:773-776`) and then
  frees the RX mbufs anyway (`:779-784`) — an still-armed MUNICH32X keeps
  DMA-ing E1 frames into freed clusters.
- **NULL-deref amplification**: once the ring pointers are corrupted,
  `dp = sc->ch[chan]->x1` can be NULL in the drain loop and
  `vtophys(dp)==ctxd` fails, so `m = dp->m` (`if_mn.c:1078`) faults on NULL.

## Verification attempt on the guest

Guest: DragonFly 6.5-DEVELOPMENT `#0 ... X86_64_GENERIC` (see env_guest.txt).

1. `pciconf -l | grep 110a` → **no Munich32X device present**.
2. `/boot/kernel/if_mn.ko` → **not built**; `kldload if_mn` → ENOENT.
3. `grep dev/netif/mn /usr/src/sys/conf/files` → `dev/netif/mn/if_mn.c
   optional mn netgraph` — the driver is not in GENERIC (`device mn` absent),
   so the audit kernel cannot instantiate it.
4. Even with a hand-built module, `mn_probe` (`if_mn.c:1252-1275`) matches
   only PCI ID `0x2101110a` and returns ENXIO otherwise, so no softc, no
   `ng_newtype` (`if_mn.c:1287-1291`), and no `mn0` node can ever exist on
   this guest.

Therefore the trigger (`trigger_race.sh`) cannot be executed: the target
device/feature is absent. The honest classification for this run is
**inconclusive** (not `not_reproduced` — nothing was refuted; the code-level
absence of synchronization is a static fact, and every sink above is cited).

## Why the finding is still real

- The netgraph core provably calls driver methods with no lock
  (`ng_base.c:1678-1697`, `:839-858`).
- The driver provably registers its ISR with no serializer (`if_mn.c:1329`).
- The driver provably frees the exact objects the ISR walks
  (`if_mn.c:779-794` vs `:1075-1090`, `:1117-1150`).
- Sibling findings DF-1539/1540/1541 already established this node type's
  surface is reachable by a `SYSCAP_RESTRICTEDROOT` actor when the hardware
  exists.

## Fix validation

Not performable on this guest (no hardware, driver not in the build). The
`fix.diff` in this pack is the reviewed, line-accurate patch: serializer
wiring + polled zap with refusal path. It applies cleanly to the read-only
`sys/` tree (`git apply --check` verified on the host clone).

## Files

- `README.md` — full finding write-up
- `trigger_race.sh` — the userland trigger (needs real hardware + E1 traffic)
- `run.sh` — what would be run, and what was actually run on the guest
- `env_guest.txt` / `run.log` — guest environment + verification-session log
- `fix.diff` — git-apply-able fix (not applied to `sys/`)
- `verdict.json` / `manifest.json`

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