# DF-0736 — Verdict

## Verdict

**REPRODUCED** (privileged DoS / hardening gap, not an unprivileged
escalation).

## Mechanism (trigger → primitive → effect)

`sys/netgraph7/ng_ipfw.c:251`:

```c
static int
ng_ipfw_rcvdata(hook_p hook, item_p item)
{
    struct ng_ipfw_tag *ngit;
    struct mbuf *m;

    NGI_GET_M(item, m);
    NG_FREE_ITEM(item);

    if ((ngit = (struct ng_ipfw_tag *)m_tag_locate(m, NGM_IPFW_COOKIE, 0,
        NULL)) == NULL) {
        NG_FREE_M(m);
        return (EINVAL);
    }

    switch (ngit->dir) {
    case NG_IPFW_OUT:                       /* 0 */
        ...
    case NG_IPFW_IN:                        /* 1 */
        ip_input(m);
        return (0);
    default:
        panic("ng_ipfw_rcvdata: bad dir %u", ngit->dir);   /* LINE 251 */
    }
    ...
}
```

`ngit->dir` is a 4-byte `int` carried inside the mbuf's `m_tag` identified
by `(cookie=NGM_IPFW_COOKIE=1105988990, type=0)`.  Any value other than
0/1 falls into the `default:` arm and triggers `panic()`, taking down the
kernel.

### Reachability trace (cited path:line)

| Step | Path |
|---|---|
| 1. ng_ipfw netgraph node must be live | `sys/netgraph7/ng_ipfw.c:106-119` (`ng_ipfw_mod_event` MOD_LOAD → `ng_make_node_common` + `ng_name_node(fw_node, "ipfw")`) |
| 2. An mbuf carrying the forged m_tag arrives on any ipfw hook | `sys/netgraph7/ng_ipfw.c:220` (`ng_ipfw_rcvdata`) |
| 3. The m_tag is located via cookie+type lookup | `sys/netgraph7/ng_ipfw.c:228` (`m_tag_locate(m, NGM_IPFW_COOKIE, 0, NULL)`) |
| 4. `ngit->dir` is read from the m_tag body | `sys/netgraph7/ng_ipfw.c:234` (`switch (ngit->dir)`) |
| 5. Invalid dir → panic | `sys/netgraph7/ng_ipfw.c:251` |

### Who can produce the forged m_tag?

* **`ng_ipfw_input()` at `sys/netgraph7/ng_ipfw.c:259`** is the *only*
  in-tree writer of the m_tag (`m_tag_alloc(NGM_IPFW_COOKIE, ...)` at
  line 286, `ngit->dir = dir` at line 292).  It runs only via the
  `ng_ipfw_input_p` function pointer.
* **`grep -rn ng_ipfw_input_p sys/`** returns *zero* dereferences outside
  `sys/netgraph7/ng_ipfw.{c,h}` itself.  No kernel subsystem ever calls
  it; the writer is dead code.
* The userspace `ng_socket` data-send path
  (`sys/netgraph7/socket/ng_socket.c:412 ngd_send`) explicitly rejects the
  `control` mbuf at line 424 (`if (control != NULL) error = EINVAL`) and
  never attaches an m_tag to the data mbuf, so unprivileged (and even
  root) userspace cannot forge the tag via `sendmsg(2)`.
* `ng_socket` control attach requires `SYSCAP_RESTRICTEDROOT`
  (`sys/netgraph7/socket/ng_socket.c:182`), so unprivileged users cannot
  even reach the netgraph topology.
* The only remaining producer of the forged tag is an in-kernel netgraph
  peer module — which requires `kldload` (root).

### Build / load barriers

* `ng_ipfw` is **not** in `X86_64_GENERIC` (`sys/config/X86_64_GENERIC`
  contains no `options netgraph7_ipfw`).
* `ng_ipfw.ko` is **not** shipped: it's missing from the `SUBDIR=` list
  of `sys/netgraph7/Makefile`.
* Building `ng_ipfw.ko` from source fails as-is because:
  - `#include <netinet/ip_fw.h>` (`sys/netgraph7/ng_ipfw.c:44`) — that
    path does not exist on DragonFly; the header is at
    `sys/net/ipfw/ip_fw.h`.
  - `MODULE_DEPEND(ng_ipfw, ipfw, 2, 2, 2)` (`sys/netgraph7/ng_ipfw.c:83`)
    — requires the `ipfw2` firewall module to be loaded, which on default
    installs blocks all network traffic on load.
  - `ng_ipfw_input_p` is declared `extern` in
    `sys/netgraph7/ng_ipfw.h:36` but **defined nowhere** in the tree
    (`grep -rn 'ng_ipfw_input_t \*ng_ipfw_input_p' sys/` is empty), so
    the module fails to link without an explicit definition added.

These three issues together strongly suggest `ng_ipfw` is broken/dead
code that has not been touched since the FreeBSD import in 2008 (per the
`$DragonFly: src/sys/netgraph7/ng_ipfw.h,v 1.2 2008/06/26 23:05:35 dillon Exp $`
tag).

### Reproduction

The included `attack.c` is a tiny in-kernel module that, on `kldload`,
locates the live `ipfw` netgraph node (`ng_name2noderef(NULL, "ipfw")`),
packages an mbuf carrying a minimal IPv4 header + a forged
`NGM_IPFW_COOKIE` m_tag with `dir=2`, and invokes
`node->nd_type->rcvdata(NULL, item)` — driving the exact same code path
netgraph's item dispatcher would take when an mbuf arrives on a hook of
the ipfw node.  On the unfixed kernel this triggers:

```
panic: ng_ipfw_rcvdata: bad dir 2
cpuid = 4
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")
```

Full signature captured in `panic.txt`; full serial console in
`boot_panic.log`.

## Why no escalation chain

The primitive is a direct `panic()` call on attacker-controllable data.
There is no memory-corruption primitive (no OOB write, no UAF, no
type-confusion, no function-pointer overwrite).  The kernel does not
return control to the attacker after the panic — it reboots.  By
definition this is a DoS, not an escalation, and there is no chain to
develop.

Per the audit's threat model this is also a **privileged** DoS:
- `ng_socket` control attach requires `SYSCAP_RESTRICTEDROOT`
- `kldload ng_ipfw.ko` requires root
- The `ng_ipfw.ko` module is not shipped and is non-trivial to build

## Fix

`fix.diff` replaces the `panic()` with the correct "drop + log + return
EINVAL" pattern recommended in the finding summary:

```diff
--- a/sys/netgraph7/ng_ipfw.c
+++ b/sys/netgraph7/ng_ipfw.c
@@ -248,7 +248,10 @@
     case NG_IPFW_IN:
         ip_input(m);
         return (0);
     default:
-        panic("ng_ipfw_rcvdata: bad dir %u", ngit->dir);
+        log(LOG_ERR, "ng_ipfw_rcvdata: bad dir %u, dropping mbuf\n",
+            ngit->dir);
+        m_freem(m);
+        return (EINVAL);
     }
```

`git apply --check` passes on a clean checkout.

## Fix-validation

| Step | Result |
|---|---|
| Apply `fix.diff` to `/usr/src/sys/netgraph7/ng_ipfw.c` | `Hunk #1 succeeded at 248.` |
| Build standalone patched `ng_ipfw.ko` (after the same source-fixing setup as the baseline) | OK, `panic` symbol absent from the resulting `.ko` (`PANIC GONE`) |
| Re-load the same `df736attack.ko` that triggered the baseline panic | `kldload` returned 0; guest stayed up |
| `dmesg` on the patched kernel | `ng_ipfw_rcvdata: bad dir 2, dropping mbuf` + `df736: rcvdata returned 22` (`EINVAL`) — no panic |

Before/after contrast (both runs from the same `df736attack.ko` driving
the same forged mbuf into the same `ipfw` netgraph node):

```
=== BEFORE (unpatched ng_ipfw.ko) ===
panic: ng_ipfw_rcvdata: bad dir 2
ng_ipfw_rcvdata() at ng_ipfw_rcvdata+0xc2 0xffffffff8260b112
df736_modevent() at df736_modevent+0x125 0xffffffff8260c125
Debugger("panic")    <-- kernel halt

=== AFTER (patched ng_ipfw.ko) ===
ng_ipfw_rcvdata: bad dir 2, dropping mbuf
df736: rcvdata returned 22 -- if you see this, the fix is in place
(load_exit=0, guest responsive, no panic)
```

Fix closes the bug.  (`fix_status: fixed`.)

## PoC changes vs. the original PoC

The finding had no PoC on disk (only the DB summary).  This evidence pack
was authored from scratch:

- `attack.c` — minimal in-kernel trigger that locates the `ipfw` node,
  forges the m_tag with `dir=2`, and invokes the rcvdata callback
  directly via `node->nd_type->rcvdata(NULL, item)`.  Avoids the
  static-only netgraph hook-creation APIs.
- `setup.sh` — performs the three pre-existing source fixes
  (`netinet/ip_fw.h` symlink, strips the broken `MODULE_DEPEND(ng_ipfw,
  ipfw, ...)`, defines the missing `ng_ipfw_input_p` symbol) so
  `ng_ipfw.ko` can actually be built and loaded standalone; also
  rebuilds `netgraph.ko` + `ng_socket.ko` from source to match the
  freshly-built modules' ABI.
- `validate_fix.sh` — same setup but applies `fix.diff` first.
- `fix.diff` — replaces `panic()` at line 251 with `log + m_freem +
  return EINVAL`.  Supersedes the finding-markdown proposal (the
  finding's `## Recommended fix` text in the DB summary is one line
  "Fix: m_freem(m) return(EINVAL) + log(LOG_ERR)"; this diff is the
  git-apply-able, line-accurate realization of that proposal and adds
  the missing `m_freem(m)` ordering).

## Final impact

| Field | Value |
|---|---|
| Reproduced | yes (kernel panic) |
| Impact | `panic` (privileged DoS — root-only, non-default config) |
| Confidence | certain |
| Unprivileged privesc | no (the panic is the entire effect; reachability is root-only) |
| Realistic severity | Low / hardening (matches the finding's rating) |
