# DF-0860 — ngc_send missing ng_mesg arglen validation -> heap OOB read

**CVE reference:** FreeBSD CVE-2008-5736 (svn r184036, 2008), never ported to DragonFlyBSD.
**Severity:** Medium (CVSS `AV:L/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:L`).
**CWE:** CWE-125 (OOB Read), CWE-200 (Information Exposure).

## Verdict

**REPRODUCED** — root-triggerable kernel heap out-of-bounds read / information
leak via the netgraph control-socket send path, plus a layout-dependent DoS
panic when `arglen` is large. A targeted single-line validation fix closes it
(VALIDATED by building and booting a single-fix `ng_socket.ko`).

## Privilege gate (why this is root-only, not unpriv→root)

Creating a netgraph **control** socket is gated by:

```c
/* sys/netgraph/socket/ng_socket.c:172 */
if (caps_priv_check(ai->p_ucred,
                    SYSCAP_RESTRICTEDROOT | __SYSCAP_NULLCRED) != 0) {
    error = EPERM;
}
```

`SYSCAP_RESTRICTEDROOT` (`sys/sys/caps.h:132`) requires `cr_uid == 0`
(`sys/kern/kern_caps.c:330`: *"Uid must be 0 unless NOROOTTEST..."*). The
audit's unprivileged user `maxx` (uid 1001, not in wheel) confirmed this
empirically:

```
$ id                            # uid=1001(maxx) gid=1001(maxx) groups=1001(maxx)
$ socket(AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL)
socket(AF_NETGRAPH,SOCK_DGRAM,NG_CONTROL) failed: errno=1 (Operation not permitted)
GATE: EPERM — netgraph control socket requires SYSCAP_RESTRICTEDROOT (euid 0).
```

(Module not loaded → `EPROTONOSUPPORT`; loaded → `EPERM`. Either way maxx is
blocked.) This is a **valid Phase-6 hard blocker**: the primitive is reachable
only from an already-root credential, so there is **no privilege boundary to
cross** — root→kernel is game-over by definition. There is therefore no
unpriv→root escalation chain to develop; the honest impact is a root→kernel
heap info-leak (and a DoS panic).

## Mechanism (trigger → primitive → effect)

1. Root opens a netgraph control socket: `socket(PF_NETGRAPH, SOCK_DGRAM, NG_CONTROL)`.
   This creates a netgraph `socket` node and wires `ngc_usrreqs.pru_send = ngc_send`
   (`ng_socket.c:944,955`).

2. Root sends a short datagram (only the 52-byte `ng_mesg` header, **no** data
   payload) but lies in the header: `header.arglen = N` with `N > 0`. Destination
   address is `"."`.

3. `ngc_send` (`ng_socket.c:201`) linearizes the user mbuf chain:
   ```c
   /* ng_socket.c:248-254 */
   for (len = 0, m0 = m; m0 != NULL; m0 = m0->m_next) len += m0->m_len;  /* len = 52 */
   xmsg = kmalloc(len + 1, M_NETGRAPH, M_WAITOK);                        /* 53-byte alloc */
   m_copydata(m, 0, len, xmsg);                                          /* copies 52 bytes */
   ```
   **No check** that `len >= sizeof(struct ng_mesg)` (52), and **no check**
   that `((struct ng_mesg *)xmsg)->header.arglen <= len - sizeof(struct ng_mesg)`.

4. `ng_send_msg(node, (struct ng_mesg *)xmsg, path=".", &resp)`
   (`ng_socket.c:257`) → `ng_path2node(here, ".", &dest, ...)` resolves `.` to
   the same node (empty segment → `continue`; `ng_base.c:~1130`), so `dest == here`.

5. `CALL_MSG_HANDLER` dispatches to the socket node's `ngs_rcvmsg`
   (`ng_socket.c:788`). Because we set `typecookie` to neither
   `NGM_GENERIC_COOKIE` nor `NGM_SOCKET_COOKIE`, it falls through the
   `if (typecookie == NGM_SOCKET_COOKIE)` branch and reaches:
   ```c
   /* ng_socket.c:836 */
   error = ship_msg(pcbp, msg, addr);     /* msg == our undersized xmsg */
   ```

6. `ship_msg` (`ng_socket.c:729`) trusts `header.arglen`:
   ```c
   /* ng_socket.c:737-738 */
   msglen = sizeof(struct ng_mesg) + msg->header.arglen;   /* 52 + N */
   mdata = m_devget(msg, msglen, 0, NULL);                 /* copies 52+N bytes */
   ```
   `m_devget` reads `52 + N` bytes starting at the 53-byte `xmsg` allocation.
   The `N` bytes past offset 52 are **whatever kernel heap bytes live past the
   allocation** — an out-of-bounds read. The copied bytes are appended to the
   socket's `so_rcv` (`ssb_appendaddr`, `ng_socket.c:751`).

7. Root `recvfrom()`s the queued mbuf and reads `52 + N` bytes — the trailing
   `N` being leaked kernel heap. `kfree(msg)` at `ng_socket.c:742` then frees
   the original buffer.

**Effect:** kernel heap information leak. Demonstrated leak of 128 / 64 / 60000
bytes past the header. Leaked bytes contain canonical kernel pointers
(`0xfffff800_4f1643c0` etc.) and residual allocation strings
(`rcng_automountd`, `disabled`, `/usr`). For very large `arglen` the read
crosses into an unmapped page → kernel page fault → DoS panic (layout-dependent;
on this guest the slab neighbourhood happened to be a large mapped run, so a
60000-byte read completed without faulting, but the leak still occurred).

## Exploit chain

Not applicable — this is an **information leak / OOB read**, and the trigger
is **root-only** (control-socket privilege gate, `ng_socket.c:172`). The
primitive is read-only (no attacker-controlled write through this path), and
root→kernel is game-over regardless. Per the Phase-6 hard-blocker rules, there
is no privilege boundary to cross and no escalation chain to develop. The
realistic impact ceiling is: a root process (or a confused-deputy root service
that proxies untrusted netgraph control messages) can disclose kernel heap
memory (defeating KASLR — though KASLR is off on this guest anyway — and
leaking adjacent slab contents) and, depending on slab layout, panic the
kernel.

## PoC

`df0860.c` — a self-contained C harness that opens the control socket, sends a
52-byte header with a lying `arglen`, and `recvfrom`s the leaked bytes. Run as
root with `kldload ng_socket` first:

```
cc -O2 -o df0860 df0860.c        # unprivileged build OK
# as root:
kldload ng_socket
./df0860 128                     # leaks 128 bytes of heap past the header
```

Expected on the **buggy** kernel (root):
```
sendto returned 52
recvfrom returned 180 bytes
LEAK: kernel returned 128 bytes past our 52-byte header (arglen lied as 128):
  leaked heap bytes (128 bytes):
  0000: 00 00 00 00 01 00 00 00 0f 00 00 00 c0 43 16 4f
  0010: 00 f8 ff ff d0 43 16 4f 00 f8 ff ff 72 63 6e 67
  ...
```

Expected on the **fixed** kernel (root):
```
sendto failed: errno=22 (Invalid argument)
```

## Recommended fix (validated)

Add a length/arglen sanity check in `ngc_send` immediately after `m_copydata`
and before `ng_send_msg`. This mirrors FreeBSD svn r184036 (the CVE-2008-5736
fix). See `fix.diff` — it rejects the datagram with `EINVAL` when either the
header does not fit (`len < sizeof(struct ng_mesg)`) or the embedded
`header.arglen` claims more payload than was actually supplied
(`arglen > len - sizeof(struct ng_mesg)`). On the rejection path `xmsg` is
freed locally (normally `ng_send_msg`/its handler frees it, so the early-out
must free it itself).

**Build/validation:** Applied the diff to in-guest `/usr/src`, rebuilt with
`make -j6 nativekernel KERNCONF=X86_64_GENERIC` (rc=0), installed the rebuilt
`ng_socket.ko` to `/boot/kernel/` (the bug lives in the loadable module, not
the base kernel — `nativekernel` rebuilds it but you must copy the new
`.ko`), rebooted, and re-ran the identical PoC. Result on `#1` fixed kernel +
module: `sendto failed: errno=22 (Invalid argument)` — leak **gone**. Honest
messages (`arglen` matching actual data) still succeed (no regression).

## Files

| file | desc |
|------|------|
| `df0860.c` | trigger harness (open control socket, lie arglen, recvfrom leak) |
| `build.sh` / `run.sh` | exact build & run commands |
| `fix.diff` | git-apply-able validation fix (validated) |
| `run.log`, `run.2.log`, `run.3.log` | baseline leak runs (unpatched #0) |
| `baseline_run.log` | post-reset reconfirm of the leak on #0 |
| `run_bigarglen.log` | arglen=60000 leak (60KB OOB read; no panic on this layout) |
| `run_maxx_gate.log` | EPERM as unprivileged maxx (privilege gate proof) |
| `fix_build.log` | single-fix kernel build log (rc=0) |
| `fix_run.log` | patched-kernel re-run: EINVAL, no leak |
| `leak_sample.txt` | leaked-byte summary across runs |
| `env.txt` | guest environment |
