# DF-3558 VERDICT

**Status: reproduced** (deterministic, 256/256 per run, 3 stress runs)
**Impact: heap OOB read (CWE-125) exposed as a heap-residue match oracle;
root-gated; module buildable+loadable but not shipped by default.**
**Fix: validated (fixed).**

## What was found

`ngh_rcvmsg()` — sys/netgraph7/hole/ng_hole.c:163-168 — checks only
`msg->header.arglen == NG_HOOKSIZ` (32) before passing `(char *)msg->data`
to `ng_findhook()` (sys/netgraph7/netgraph/ng_base.c:1095-1107, a plain
`strcmp` against each hook name). The length of what the user ACTUALLY sent
is never validated anywhere on the path:

* `ngc_send()` (sys/netgraph7/socket/ng_socket.c:254-262) kmallocs
  `len+1` bytes where `len` is the real mbuf-chain byte count and copies
  exactly that many bytes — the user's `header.arglen` claim is not
  checked against it;
* `ng_package_msg()` (sys/netgraph7/netgraph/ng_base.c:3007-3027) performs
  no validation either.

So a control message that DECLARES `arglen=32` but SENDS zero data bytes
gives `msg->data[]` = 100% stale heap, and the strcmp reads it (and
everything up to the first NUL beyond it) out of bounds of the 57-byte
kmalloc. This is the consumer-side instance of the class already filed as
DF-0610 (ng_nat), DF-2600 (ng_source), DF-3496 (ng_fec), on top of the
framework enabler DF-0534.

## Why the honest-length unterminated case is NOT the bug

I initially suspected the classic "32 bytes, no NUL" case. It is provably
safe here: stored hook names are NUL-terminated within 31 characters, so
`strcmp(hookname, data)` stops at the first difference — at worst index
31, the last in-bounds byte. There is no way to read `data[32]` with a
full-length message. The PoC carries this as an explicit negative control
(full-length unterminated "abcde1xxxx..." → ENOENT).

## Reproduction (run.log, run.2.log, run.3.log)

1. hole node, hook "abcde1", one 5-byte packet (frames=1, octets=5).
2. GROOM: getstats, arglen=32, 7 real bytes "abcde1\0" — kmalloc(64),
   same 64-byte slab zone as the attack; when freed the chunk retains
   the name at data offset.
3. ATTACK: getstats, arglen=32, ZERO real bytes — kmalloc(57→64 zone).
   The allocator returns the groom chunk; strcmp completes the name from
   bytes past the sent payload; stats come back.

```
RESULT oob_matches=256/256 enoent=1 rejected=0 other=0
RESULT OOB-READ PROVEN: stats returned for a request that supplied ZERO
hook-name bytes (kernel strcmp completed the name from stale heap past
the sent payload)
```

Deterministic because DF's slab allocator zones are keyed by size class
(8-byte granularity below 128: kern_slaballoc.c `zoneindex()`), and the
per-CPU objcache hands the just-freed 64-class chunk straight back.
ng7 also returns the node handler's error synchronously through
`sendto()` (ENOENT when no hook matches), which makes the match/mismatch
oracle trivially observable.

## Why this is not worse than Low

* Root-gated: AF_NETGRAPH control sockets require privilege
  (ngc_attach caps_priv_check); uid 1001 gets EPERM at socket()
  (run.log unpriv section, mirroring DF-3557's unpriv_check).
* The OOB read's observable is a match oracle (did a stale-heap-completed
  name match a hook), not a direct byte disclosure; response content is
  the hook's own stats.
* Not shipped: no `NETGRAPH7_HOLE` in the default kernel, not in
  netgraph7/Makefile SUBDIR — but hole/ has a working module Makefile
  (build.log) so it is reachable for anyone building/loading it.

## Fix validation (fix_run.log)

`fix.diff` = socket-side length validation (effective) + hole-side NUL
check (defense-in-depth):

* socket/ng_socket.c: after the version check, reject when
  `len < sizeof(struct ng_mesg) || len < sizeof(struct ng_mesg) +
  header.arglen` (EINVAL).
* hole/ng_hole.c: additionally require `msg->data[NG_HOOKSIZ-1]=='\0'`.

Fresh `vm.sh reset with-src`, patch applied to the guest's /usr/src,
ng_socket.ko + ng_hole.ko rebuilt (-Werror clean), exact PoC re-run:

```
STEP sanity getstats: token=22531 frames=1 octets=5     <- no regression
STEP negative control: unterminated full-length -> sendto errno=22
RESULT oob_matches=0/256 enoent=1 rejected=256 other=0
RESULT fixed behavior: under-sent requests rejected with EINVAL at the
socket (no OOB possible)
```

`echo_neg` (ng_echo smoke) still passes on the patched kernel. The
previously-observed bad behavior is gone; well-formed messages still
work.

## The other three files in this batch (negative results, live where noted)

* **ng_split.c** — pass 2 found nothing beyond known DF-0759 (out-hook
  double NG_FREE_ITEM, already verified reproduced/panic). Hook-private
  pointers into `priv` are safe because ng_rmnode destroys all hooks
  (ng_base.c:715-716) before the type shutdown frees priv; in/mixed
  fallthrough paths free the item exactly once; newhook dedups
  (EISCONN); no rcvmsg surface.
* **ng_ip_input.c** — the DF-0735 lead is REFUTED by code: ng_ip_input
  hands the mbuf to `netisr_queue(NETISR_IP, m)` (ng_ip_input.c:125),
  which only characterizes the hash and `lwkt_sendmsg`s it to the proper
  per-CPU netisr port (net/netisr.c:394-442) — ip_input runs in correct
  NETISR context, unlike ng_ipfw's direct `ip_input(m)` (ng_ipfw.c:248),
  which anyway is unbuildable dead code (DF-3556). M_PKTHDR is enforced
  on every data hop by CHECK_DATA_MBUF (ng_base.c:1942/361). File is also
  unwired (no module Makefile, not in SUBDIR, no default-kernel option).
* **ng_echo.c** — clean, with LIVE negative evidence (echo_neg.log):
  the closed echo<->echo ring is inert. Data sent into any hook of a
  ring member reflects straight back to the sender (ng_echo.c:106
  NG_FWD_ITEM_HOOK on the arrival hook only), and control messages are
  responded to the ORIGINAL sender via NG_RESPOND_MSG →
  ng_address_ID(NGI_RETADDR) (netgraph.h:998-1013) — the exact relay
  channel that made ng_UI exploitable (DF-3557) does not exist in
  ng_echo. Echo-containing rings that ARE injectable (e.g. via ng_tee's
  left2right hook) are instances of the already-filed DF-3540/DF-3553
  class whose root cause (unbounded synchronous forwarding, no cycle
  detection) lives outside ng_echo.c.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
