# DF-0736 — Trigger / Reproduction

**Claim:** `panic("ng_ipfw_rcvdata: bad dir %u", ngit->dir)` at
`sys/netgraph7/ng_ipfw.c:251` is reachable when a netgraph peer delivers an
mbuf carrying a forged `m_tag` (cookie=`NGM_IPFW_COOKIE`, type=0) with an
invalid `dir` value (anything other than `NG_IPFW_OUT=0` / `NG_IPFW_IN=1`).

## Reachability model — privileged DoS only

| Path | Status |
|---|---|
| `ng_ipfw` compiled into `X86_64_GENERIC` | ❌ not in default config (`options netgraph7_ipfw` is required) |
| `ng_ipfw.ko` shipped in `/boot/kernel` | ❌ not built (missing from `sys/netgraph7/Makefile` SUBDIR; build also fails due to broken `<netinet/ip_fw.h>` include and undefined `ng_ipfw_input_p` symbol) |
| In-kernel writer of the m_tag (`ng_ipfw_input_p`) | ❌ zero callers in the tree |
| Userspace `ng_socket` forging the m_tag | ❌ `ng_socket` data-send path rejects the `control` mbuf and never adds an m_tag |
| `ng_socket` control attach from unprivileged user | ❌ requires `SYSCAP_RESTRICTEDROOT` (root) |
| Forged-m_tag delivery from a custom kernel module | ✅ requires root (`kldload`) |

**Realistic impact:** privileged DoS (root → kernel panic) on a non-default
configuration that requires manual source patching to even build.  Not an
unprivileged privesc.

## Reproduction on the unpatched kernel

The bug lives in a non-default module that cannot be built as-is.  The
`setup.sh` + `attack.c` pair performs the minimal patching needed to build
`ng_ipfw.ko` from source and triggers the panic.

```sh
# Host -> guest:
scp -F dfbsd-qemu/config findings/poc/DF-0736/setup.sh    dfbsd:/root/setup.sh
scp -F dfbsd-qemu/config findings/poc/DF-0736/attack.c    dfbsd:/root/df736attack/attack.c
scp -F dfbsd-qemu/config findings/poc/DF-0736/fix.diff    dfbsd:/root/fix.diff   # for the fix-validation pass

# Guest:
sh /root/setup.sh                              # rebuild + load netgraph, ng_socket, ng_ipfw
(cd /root/df736attack && make)                 # build attack.ko
kldload /root/df736attack/df736attack.ko       # PANIC at sys/netgraph7/ng_ipfw.c:251
```

Expected output (serial console / `dfbsd-qemu/boot.log`):

```
df736: invoking ng_ipfw_rcvdata on node 0xfffff8004f1e8b50 with dir=2 -> expect panic at sys/netgraph7/ng_ipfw.c:251
panic: ng_ipfw_rcvdata: bad dir 2
cpuid = 4
Trace beginning at frame 0xfffff801189bd7a8
ng_ipfw_rcvdata() at ng_ipfw_rcvdata+0xc2 0xffffffff8260b112 
df736_modevent() at df736_modevent+0x125 0xffffffff8260c125 
module_register_init() at module_register_init+0x49 0xffffffff806232e9
linker_load_file.part.3() at linker_load_file.part.3+0x1b9 0xffffffff80624619
linker_load_module() at linker_load_module+0x116 0xffffffff80625e16
Debugger("panic")
Stopped at      Debugger+0x7c:  movb    $0,0xbdaf09(%rip)
db>
```

The `panic.txt` file contains the full signature captured from
`dfbsd-qemu/boot.log`.

## Fix-validation

Apply `fix.diff` to `/usr/src/sys/netgraph7/ng_ipfw.c`, rebuild the
standalone `ng_ipfw.ko` (same setup.sh pattern), and re-load the attack
module.  On the fixed kernel:

```
df736: invoking ng_ipfw_rcvdata on node 0xfffff8004f1e8b50 with dir=2 -> expect panic at sys/netgraph7/ng_ipfw.c:251
ng_ipfw_rcvdata: bad dir 2, dropping mbuf
df736: rcvdata returned 22 -- if you see this, the fix is in place
```

`rcvdata returned 22` is `EINVAL` — the mbuf is freed via `m_freem(m)` and
the call returns cleanly.  No panic.

## Files

| File | Purpose |
|---|---|
| `attack.c` | In-kernel attack module: locates the `ipfw` netgraph node, builds an mbuf with a forged `NGM_IPFW_COOKIE` m_tag (`dir=2`), and invokes `node->nd_type->rcvdata(NULL, item)` to drive the panic path. |
| `setup.sh` | Rebuilds `netgraph.ko` + `ng_socket.ko` from source (so the ABI matches freshly-built modules), patches `ng_ipfw.c` (broken `<netinet/ip_fw.h>` include via symlink, strips the dangling `MODULE_DEPEND(ng_ipfw, ipfw, ...)`, defines the missing `ng_ipfw_input_p` symbol), builds `ng_ipfw.ko`, and loads everything. |
| `validate_fix.sh` | Same as `setup.sh` but additionally applies `fix.diff` first; the attack then returns cleanly instead of panicking. |
| `fix.diff` | Standalone `git apply`-able unified diff: replaces `panic()` at line 251 with `log(LOG_ERR,...) + m_freem(m) + return (EINVAL)`. |
| `panic.txt` / `panic_sig.txt` | Full panic signature from `dfbsd-qemu/boot.log`. |
| `fix_run_dmesg.txt` | `dmesg` excerpt from the patched-kernel re-run showing `bad dir 2, dropping mbuf` + `rcvdata returned 22`. |
| `boot_panic.log` | Full untrimmed serial console capture of the panic run. |
| `env.txt` | Guest `uname -a`, `kern.version`, `cc --version`. |

## Why no `uid=0` chain

There is no memory-corruption primitive here — the bug is a direct
`panic()` call on attacker-controllable data.  Panic is the *only* effect;
no slab grooming, no function-pointer overwrite, no `ucred` forge.  The
chain terminates at "kernel panics, host reboots" — a DoS, by definition
not an escalation.

Per the audit's threat model, the bug is also gated behind root-only
operations (kldload + netgraph access) and requires a non-default kernel
configuration.  Reported impact: privileged DoS (root → kernel panic) /
hardening opportunity.
