DF-0613 — VERDICT
==================

**Verdict: REPRODUCED (resource leak / DoS)**

**Impact: dos** (root-only kernel-memory-exhaustion via M_NETGRAPH leak)

**Confidence: certain**

---

## Mechanism

When `NGM_IFACE_POINT2POINT` or `NGM_IFACE_BROADCAST` is sent to an
`ng_iface` node while the interface is UP, `ng_iface_rcvmsg()` at
`sys/netgraph/iface/ng_iface.c:665-666` does:

```c
if ((ifp->if_flags & IFF_UP) != 0)
    return (EBUSY);          /* <-- leaks msg, skips kfree at 732 */
```

This `return (EBUSY)` bypasses the function's cleanup epilogue at
lines 728-733:

```c
if (rptr)
    *rptr = resp;
else if (resp)
    kfree(resp, M_NETGRAPH);
kfree(msg, M_NETGRAPH);      /* <-- skipped by early return */
return (error);
```

Per the netgraph framework contract (`sys/netgraph/netgraph/ng_base.c:1178`:
"It is up to the message handler to free the message"), the `rcvmsg`
handler owns `msg` and must free it on **every** path. The early
return leaks `sizeof(struct ng_mesg) + msg->header.arglen` bytes of
`M_NETGRAPH` per call.

The leaked buffer size is attacker-controlled (sized by
`msg->header.arglen`, set from the user-supplied mbuf chain in
`ngc_send` at `sys/netgraph/socket/ng_socket.c:248-258`).

## Privilege boundary

The `NG_CONTROL` socket is root-only: `ngc_attach` at
`sys/netgraph/socket/ng_socket.c:172-173` gates on
`caps_priv_check(SYSCAP_RESTRICTEDROOT | __SYSCAP_NULLCRED)`.
This is a root→kernel DoS, not an unprivileged escalation.

## Exploitation ceiling

Resource leak → memory exhaustion → DoS. No corruption primitive,
no privilege escalation. Repeated calls exhaust `M_NETGRAPH` and
panic/OOM the system. Severity: Low (root-only, DoS only).

## Runtime reproduction

The leak was confirmed at runtime on the `noinv-installed` snapshot
(INVARIANTS OFF). The default GENERIC kernel (#0, INVARIANTS ON) has
two separate bugs that prevent creating ng_iface nodes at runtime:

1. **kfree(NULL) in ng_iface_get_unit():** `bcopy()` is declared
   `__nonnull(1,2)` at `sys/sys/systm.h:280`. The compiler (gcc 8.3)
   uses this to eliminate the `if (ng_iface_units != NULL)` check
   before `kfree()` at `ng_iface.c:294`, causing a panic on the first
   `ng_iface_get_unit()` call when `ng_iface_units` is still NULL.

2. **ifnet_lock KASSERT in if_attach():** `ifnet_lock()` has
   `KASSERT(td_type != TD_TYPE_NETISR)` at `sys/net/if.c:3787`. The
   ng_socket send path runs in a netisr thread context, so
   `ng_iface_constructor()` calling `if_attach()` triggers the
   KASSERT (INVARIANTS-gated, only on GENERIC).

A test-only constructor fix (changing `if (ng_iface_units != NULL)`
to `if (ng_iface_units_len > 0)`) was applied to both the BEFORE and
AFTER ng_iface.ko modules to work around issue (1). Issue (2) is
bypassed on the noinv kernel (KASSERT is INVARIANTS-gated).

### Before (EBUSY bug present):

```
BEFORE vmstat: netgraph 5 1.11K 0 390M 18
  → 2000 NGM_IFACE_POINT2POINT messages sent
AFTER vmstat:  netgraph 1.96K 112K 0 390M 5.89K
  → M_NETGRAPH grew by ~111K (2000 × ~52 bytes/msg leaked)
```

### After (EBUSY fix applied):

```
BEFORE vmstat: (empty)
  → 2000 NGM_IFACE_POINT2POINT messages sent
AFTER vmstat:  netgraph 5 1.11K 0 390M 5.88K
  → M_NETGRAPH stayed at 1.11K — NO LEAK (messages freed by epilogue)

Run 2 (determinism):
  → 2000 more messages
AFTER vmstat:  netgraph 9 2.20K 0 390M 11.8K
  → grew by only ~1K (new node overhead), NOT 112K — NO LEAK
```

## Fix

`fix.diff` replaces the early `return (EBUSY)` at line 666 with
`error = EBUSY; break;` so the epilogue at lines 728-733 frees `msg`:

```diff
-		if ((ifp->if_flags & IFF_UP) != 0)
-			return (EBUSY);
+		if ((ifp->if_flags & IFF_UP) != 0) {
+			error = EBUSY;
+			break;
+		}
```

This matches the finding markdown's `## Recommended fix` proposal.
The fix was validated: on the noinv kernel with the fix applied,
2000+2000 POINT2POINT messages produced zero M_NETGRAPH growth
(vs ~222K leaked without the fix).

## PoC changes

- Wrote `leak_ng_iface.c` (the finding had only a README; no source
  existed). The PoC creates an ng_iface node via NGM_MKPEER, queries
  its name via NGM_IFACE_GET_IFNAME, brings it UP via SIOCSIFFLAGS,
  then spams NGM_IFACE_POINT2POINT messages. Each call triggers the
  EBUSY early-return leak path.
- Wrote `fix.diff` (standalone git-apply-able diff).
- Wrote `build.sh`, `run.sh`.
