# DF-0735 — ip_input/ip_output called from netgraph thread context

**Verdict:** REPRODUCED at the source level + harness level (latent / dead-code path).
**Impact:** panic / DoS — but **only reachable via root** (`kldload` of an out-of-tree module) on a configuration that is not built by default. No unprivileged-reachability, no escalation primitive.
**Fix:** VALIDATED via harness — the fix pattern (route via `netisr_queue`) eliminates the panic.

---

## 1. The claim, restated

`sys/netgraph7/ng_ipfw.c` calls the IP layer directly from the netgraph worker
thread:

* `ng_ipfw.c:245` — `return ip_output(m, NULL, NULL, IP_FORWARDING, NULL, NULL);`
* `ng_ipfw.c:248` — `ip_input(m);`

These calls run inside `ng_ipfw_rcvdata()` (the netgraph `rcvdata` callback at
`ng_ipfw.c:220`). Because the hook is set with `NG_HOOK_FORCE_QUEUE(hook)` in
`ng_ipfw_connect` (`ng_ipfw.c:182-187`), data sent to the hook is queued
asynchronously and processed by the **netgraph worker thread** created at
`sys/netgraph7/netgraph/ng_base.c:2787-2789`:

```c
lwkt_create(ngthread, NULL, &td, NULL, 0, i, "netgraph %d", i);
ng_msgport[i] = &td->td_msgport;
```

That thread is a plain `lwkt` thread. **It is never registered on
`netreglist`** — only ports initialized via `netmsg_service_port_init` are
(`sys/net/netisr.c:258-280`, with the `TAILQ_INSERT_TAIL(&netreglist, ...)`
at line 279). `netmsg_service_sync()` (the pfil list lifetime barrier at
`sys/net/pfil.c:238,346`) only walks `netreglist`, so it does **not** cover
the netgraph thread.

The first thing `ip_input()` and `ip_output()` do is:

* `sys/netinet/ip_input.c:460` — `ASSERT_NETISR_NCPUS(mycpuid);`
* `sys/netinet/ip_output.c:185` — `ASSERT_NETISR_NCPUS(mycpuid);`

`ASSERT_NETISR_NCPUS(n)` (defined at `sys/net/netisr2.h:136-139`) is a
`KASSERT(IN_NETISR_NCPUS(n), ...)`, i.e. it is compiled in whenever
`INVARIANTS` is on. The default `X86_64_GENERIC` config ships
`options INVARIANTS` (`sys/config/X86_64_GENERIC:56`), so the assertion is
live on every default DragonFly kernel.

Therefore: the cited call site, executed from the netgraph worker thread,
trips the KASSERT and panics with
`panic: thread <ptr> cpuN is not within netisr_ncpus <N>`.

The sibling node `sys/netgraph7/ng_ip_input.c` shows the correct pattern:

```c
NGI_GET_M(item, m);
NG_FREE_ITEM(item);
m->m_flags &= ~M_HASH;
netisr_queue(NETISR_IP, m);   /* ng_ip_input.c:125 -- routes to netisr */
```

## 2. Reachability assessment (why this is latent)

`ng_ipfw.c` is **dead code on any current DragonFly kernel**:

1. `sys/conf/files` gates it on `optional netgraph7_ipfw`, which is **not** in
   `sys/config/X86_64_GENERIC`, so it is not in the default kernel.
2. It is **not** in the `SUBDIR` list of `sys/netgraph7/Makefile`, so it is
   not built as a loadable `.ko` either. There is no `ng_ipfw.ko` anywhere on
   the running guest (`/boot/kernel/` and the source tree both lack it).
3. The file `#include`s `<netinet/ip_fw.h>` (`ng_ipfw.c:44`), a header that
   **does not exist** under `sys/netinet/` on this tree (the only `ip_fw.h`
   lives at `sys/net/ipfw/ip_fw.h` with a different API). Trying to build the
   file standalone fails immediately with `fatal error: netinet/ip_fw.h: No
   such file or directory`.
4. Even if those issues were fixed, **no in-kernel caller ever invokes
   `ng_ipfw_input_p`** (the function pointer registered at `ng_ipfw.c:118`
   in `MOD_LOAD`). It is referenced only inside `ng_ipfw.c` and `ng_ipfw.h`
   themselves. Neither `sys/net/ipfw/ip_fw2.c` nor `sys/net/ipfw3/*` mention
   it. So even with the module loaded, no packet path would ever reach
   `ng_ipfw_rcvdata`.

This is therefore a **latent / dead-code** finding. The mechanism is real at
the source level; the cited path is unreachable on any running DragonFly
kernel without substantial additional (root-only) scaffolding.

## 3. Demonstration

Because the cited code cannot be compiled or reached, the proof uses a
**deterministic code-level harness** that mirrors `ng_ipfw.c:248` exactly
(calling `ip_input(m)` directly from a non-netisr thread). This is the
acceptable path called out in the assignment.

Two trigger modules were built (both compile cleanly with the guest gcc 8.3):

| module               | what its trigger does                                              | matches                                       | expected           |
|----------------------|--------------------------------------------------------------------|-----------------------------------------------|--------------------|
| `df735_trigger.ko`   | sysctl handler calls `ip_input(m)` directly from the invoking thread | mirrors `ng_ipfw.c:248` direct call          | **PANIC**          |
| `df735_fixed.ko`     | sysctl handler calls `netisr_queue(NETISR_IP, m)` instead          | mirrors the `fix.diff` pattern (and `ng_ip_input.c:125`) | **NO panic**       |
| `ng_df735_poc.ko`    | real netgraph node whose `rcvdata` calls `ip_input(m)`             | faithful copy of `ng_ipfw_rcvdata`           | builds; netgraph7 ABI mismatch prevents `kldload` |

The sysctl-handler thread, the netgraph worker thread, and a kthread are all
**non-netisr threads**, so the `ASSERT_NETISR_NCPUS` behavior is identical
across them. The trigger module is therefore a faithful demonstration of the
cited mechanism.

### 3a. BASELINE — buggy module panics

Run on the unpatched audit-source kernel
(`DragonFly 6.5-DEVELOPMENT #0`, `with-src` snapshot):

```
# kldload /root/df735/df735_trigger.ko
# sysctl -n debug.df735_trigger=1
0
(kernel panics, guest enters DDB)
```

Serial console (`dfbsd-qemu/boot.log`):

```
panic: thread 0xfffff800905fd480 cpu1 is not within netisr_ncpus 6
cpuid = 1
ip_input() at ip_input+0x3de 0xffffffff807b0dae
ip_input() at ip_input+0x3de 0xffffffff807b0dae
Stopped at      Debugger+0x7c:  movb    $0,0xbdaf09(%rip)
db>
```

The panic message is the exact expansion of
`KASSERT(IN_NETISR_NCPUS(n), ("thread %p cpu%d is not within netisr_ncpus %d",
...))` from `sys/net/netisr2.h:138`, and the backtrace shows the panic
originates inside `ip_input` — confirming the cited `ip_input.c:460`
assertion. (`ip_input+0x3de` is the post-KASSERT-fault `panic` return path.)

### 3b. FIXED module — no panic

Run on the **same** unpatched kernel (the fix pattern is in the module, not
the kernel):

```
# kldload /root/df735/df735_fixed.ko
# for i in 1 2 3 4 5; do sysctl -n debug.df735_fixed=1; done
0 -> 0
0 -> 0
0 -> 0
0 -> 0
0 -> 0
# uptime
 2:55AM  up 4 mins, 0 users, load averages: 0.01, 0.02, 0.00
```

Five triggers, no panic, guest stays up. The mbuf is dispatched to the
`NETISR_IP` thread; `ip_input` runs there, where `ASSERT_NETISR_NCPUS`
holds.

## 4. Exploit chain / escalation

There is **no escalation chain** — this is not a memory-corruption primitive.
The bug produces a deterministic kernel panic / DoS, gated behind root-only
setup (`kldload` of an out-of-tree module to even reach the cited path). The
production (INVARIANTS-OFF) concern raised in the finding — that
`pfil_run_hooks` in `ip_input`/`ip_output` iterates a list whose lifetime is
synced via `netmsg_service_sync` (which does **not** cover the netgraph
thread), so concurrent `pfil_add_hook`/`pfil_remove_hook` could free the list
out from under the netgraph thread — is real **in theory** but only reachable
on a configuration that:
  (a) compiles and loads `ng_ipfw` (impossible today without fixing the
      `netinet/ip_fw.h` include),
  (b) wires `ng_ipfw_input_p` into ipfw2/ipfw3 (no such caller exists),
  (c) runs an attacker-controlled concurrent pfil hooks add/remove storm.

None of those hold on any shipping DragonFly kernel. There is no
unprivileged attack surface.

## 5. fix.diff

`fix.diff` patches `sys/netgraph7/ng_ipfw.c` to route through netisr instead
of calling ip_input/ip_output directly:

* adds `#include <net/netisr.h>` and `<net/netmsg2.h>`,
* adds `ng_ipfw_ip_output_dispatch()` netmsg handler (mirrors dummynet's
  `ip_dn_ip_output` at `sys/net/dummynet/ip_dummynet_glue.c:270`),
* in `ng_ipfw_rcvdata()`, the `NG_IPFW_OUT` case dispatches the mbuf to a
  netisr port via `lwkt_sendmsg(netisr_cpuport(mycpuid), ...)` and calls
  `ip_output` from there (mirrors dummynet's `ip_dn_queue` pattern at
  `sys/net/dummynet/ip_dummynet_glue.c:82-99`),
* the `NG_IPFW_IN` case calls `netisr_queue(NETISR_IP, m)`, matching the
  sibling `sys/netgraph7/ng_ip_input.c:125` pattern exactly.

The diff applies cleanly with `patch -p1 --forward` (all 4 hunks succeed).
It is minimal and targeted: the only behavior change is *where* ip_input /
ip_output are called (which thread), not what they do.

### Fix validation (Phase 8)

The default kernel does not link `ng_ipfw.c`, so the standard "single-fix
kernel + reboot" Phase 8 path does not apply (no kernel objects change). The
fix is validated at the harness level instead, by demonstrating that the
**fix pattern** (route via netisr) eliminates the panic the **buggy pattern**
(direct call) produces, on the same kernel:

| Phase                          | Module           | Outcome                                                  |
|--------------------------------|------------------|----------------------------------------------------------|
| baseline (buggy mechanism)     | `df735_trigger.ko`  | **PANIC** `panic: thread ... is not within netisr_ncpus 6` at `ip_input+0x3de` |
| patched (fix mechanism)        | `df735_fixed.ko`    | **NO panic**, 5x triggers, guest stays up                |

This is a clean before/after contrast on the same `6.5-DEVELOPMENT #0`
audit-source kernel. `fix_status = fixed`.

## 6. Honest impact summary

| dimension           | value                                                                  |
|---------------------|------------------------------------------------------------------------|
| bug class           | wrong-thread call → assertion panic (INVARIANTS) / lock-context race (production) |
| reachability        | **latent**: cited code is dead on this tree (won't compile, no caller, not in default kernel or module SUBDIR) |
| preconditions       | root must `kldload` an out-of-tree module (and even then, no caller exists to feed packets in) |
| unpriv→root chain?  | **No** — no memory-corruption primitive, no unprivileged reachability. |
| realistic impact    | source-level latent bug; would become a real root-triggerable DoS *if* the netgraph7_ipfw path were ever wired up |
| confidence          | certain (source-level mechanism verified + harness panic confirmed)    |

The finding is real as a code-quality / latent-bug issue. It is **not** an
exploitable vulnerability on any shipping DragonFly configuration.
