DragonFlyBSD Kernel Audit
← triage · dashboard
DF-0860

ngc_send does not validate ng_mesg length or arglen causing heap OOB read (FreeBSD CVE-2008-5736 never ported)

Summary

ng_socket.c:248-254 ngc_send linearizes user mbuf into xmsg=kmalloc(len+1) casts to struct ng_mesg* with NO check len>=sizeof(ng_mesg)(52) and NO check arglen<=len-sizeof(ng_mesg). :257 ng_send_msg passes to node. When path empty ng_path2node treats as here loops back. :737 ship_msg msglen=sizeof(ng_mesg)+msg->header.arglen trusts arglen. :738 m_devget(msg,msglen,...) reads msglen bytes from small buffer. arglen=20428 with 52-byte send = 20KB heap OOB read leaked to userspace via recvfrom. arglen=65535 crosses page = panic. FreeBSD fixed in svn r184036 (2008) as CVE-2008-5736 NEVER PORTED to DragonFly. Requires root control socket (SYSCAP_RESTRICTEDROOT). Impact: kernel heap info leak + DoS panic.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0860 · 19 files
FileTypeDescriptionSize
df0860.c trigger-source open ng control socket, lie header.arglen, recvfrom the OOB heap read 6.6 KB view raw
build.sh build-script cc -O2 -o df0860 df0860.c 175 B view raw
run.sh run-script kldload ng_socket + ./df0860 <leak_bytes> (root) 612 B view raw
README.md readme human-facing build/run/expected + impact 1.9 KB ↓ raw
VERDICT.md verdict full mechanism walkthrough + fix validation 7.9 KB ↓ raw
fix.diff suggested-fix validate header.arglen <= len-sizeof(ng_mesg) in ngc_send (CVE-2008-5736 port) 1.1 KB view raw
run.log run-log baseline leak #1 (arglen=128): 128-byte heap leak 774 B view raw
run.2.log run-log baseline leak #2 (arglen=128) 774 B view raw
run.3.log run-log baseline leak #3 (arglen=64) 544 B view raw
baseline_run.log run-log post-reset reconfirm of leak on unpatched #0 839 B view raw
run_bigarglen.log run-log arglen=60000 -> 60KB OOB read (no panic on this slab layout) 413 B view raw
run_maxx_gate.log run-log EPERM as unprivileged maxx (privilege gate proof) 206 B view raw
fix_build.log build-log single-fix nativekernel build (rc=0) 282 B view raw
fix_run.log run-log patched kernel+module re-run: sendto EINVAL, no leak 418 B view raw
leak_sample.txt leak-sample leaked-byte summary across runs 697 B view raw
env.txt environment uname, cc version, module/sysctl state 256 B view raw
env_uname.txt environment kern.version of baseline 59 B view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human-facing build/run/expected + impact
↓ download raw

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

Root-triggerable kernel heap out-of-bounds read / information leak in the netgraph control-socket send path (sys/netgraph/socket/ng_socket.c). This is the DragonFlyBSD instance of FreeBSD CVE-2008-5736 (svn r184036, 2008), which was never ported.

Build

cc -O2 -o df0860 df0860.c        # compiles unprivileged

Run (ROOT only)

The trigger requires a netgraph control socket, which is gated by caps_priv_check(SYSCAP_RESTRICTEDROOT) (ng_socket.c:172) — i.e. euid 0. The netgraph socket module must also be loaded.

# as root:
kldload ng_socket
./df0860 128                     # leaks 128 bytes of heap past the 52-byte header
./df0860 64                      # leaks 64 bytes
LEAK_BYTES=2048 ./df0860         # or via env

As an unprivileged user the socket(2) call returns EPERM (or EPROTONOSUPPORT if the module is not loaded) — see run_maxx_gate.log.

Expected

On the buggy kernel (6.5-DEVELOPMENT #0, root, module loaded):

sendto returned 52
recvfrom returned 180 bytes
LEAK: kernel returned 128 bytes past our 52-byte header (arglen lied as 128):
  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
  ...                                       ^ kernel pointers + heap strings

On the fixed kernel (root): sendto failed: errno=22 (Invalid argument) — the lying arglen is rejected before reaching ship_msg. Honest messages (arglen matching actual data) still succeed.

Impact

Kernel heap information leak (canonical kernel pointers + residual allocation contents disclosed to a root process) and, for large arglen, a layout-dependent DoS panic when the OOB read crosses an unmapped page. Not an unprivileged escalation — the trigger is root-only.

See VERDICT.md for the full mechanism walkthrough and fix.diff for the validated one-line validation fix.

VERDICT.md verdict full mechanism walkthrough + fix validation
↓ download raw

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:

/* 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 recvfroms 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)

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

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED the fix. The PoC leaked 128 bytes of heap on the unpatched 6.5-DEVELOPMENT #0 baseline (recvfrom returned 180 bytes, trailing 128 = leaked heap incl kernel pointers). On the single-fix kernel #1 (nativekernel rc=0) with the rebuilt ng_socket.ko installed, the SAME ./df0860 128 invocation is now rejected: sendto failed: errno=22 (Invalid argument) -- no recvfrom, no leak. Honest messages (arglen matching actual data, e.g. arglen=16/len=68 and arglen=0/len=52) still succeed -> no regression. NOTE: ng_socket.c compiles into the loadable module ng_socket.ko, NOT the base kernel, so nativekernel rebuilds it but you MUST also copy /usr/obj/.../ng_socket.ko over /boot/kernel/ng_socket.ko and reboot (copying only kernel.stripped leaves the old vulnerable module in place -- caught and corrected during validation).

BEFORE (unpatched #0, baseline_run.log):
  sending 52-byte datagram with header.arglen=128 -> path "."
  sendto returned 52
  recvfrom returned 180 bytes
  LEAK: kernel returned 128 bytes past our 52-byte header
AFTER (patched #1 + rebuilt ng_socket.ko, fix_run.log):
  sending 52-byte datagram with header.arglen=128 -> path "."
  sendto failed: errno=22 (Invalid argument)   <- leak GONE
  (honest arglen=16/len=68 still accepted: sendto=68; no regression)
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sat Jul 11 15:29:24 UTC 2026

Confirmed kernel references

Detail

Exploit chain

Not applicable / blocked by a VALID Phase-6 hard blocker. The bug is an OOB READ (no attacker-controlled write through this path), and -- critically -- the trigger is reachable ONLY from an already-root credential: opening a netgraph CONTROL socket requires caps_priv_check(p_ucred, SYSCAP_RESTRICTEDROOT) at ng_socket.c:172, which (sys/kern/kern_caps.c:330) requires cr_uid==0. The audit's unprivileged user maxx (uid 1001) confirmed the gate empirically: socket(AF_NETGRAPH,SOCK_DGRAM,NG_CONTROL) returns EPERM (run_maxx_gate.log). There is therefore no privilege boundary to cross (root->kernel is game-over by definition) and no escalation chain to develop. Realistic impact ceiling: a root process (or a confused-deputy root service proxying netgraph control messages) can disclose kernel heap memory and, depending on slab layout, panic the kernel (arglen=60000 completed a 60KB OOB read without faulting on this guest's layout; arglen=65535 would panic on a tighter layout). No exploit-chain file written because the primitive is read-only and root-gated; df0860.c is both the trigger and the impact demonstration.

Evidence (decisive lines)

BASELINE (unpatched #0, root, ng_socket loaded), ./df0860 128:
  sending 52-byte datagram with header.arglen=128 -> path "."
  sendto returned 52
  recvfrom returned 180 bytes
  LEAK: kernel returned 128 bytes past our 52-byte header (arglen lied as 128):
  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
  ...  (non-zero bytes in leak: 84 / 128; ASCII rcng_automountd/disabled//usr)

PRIVILEGE GATE (unprivileged maxx, module loaded):
  socket(AF_NETGRAPH,SOCK_DGRAM,NG_CONTROL) failed: errno=1 (Operation not permitted)
  GATE: EPERM - netgraph control socket requires SYSCAP_RESTRICTEDROOT (euid 0).

PoC changes

No PoC scaffolding existed on disk (only the DB row), so I authored the full evidence pack from scratch: df0860.c (control-socket harness that sends a 52-byte ng_mesg header with a lying arglen and recvfroms the leaked heap bytes), build.sh, run.sh (kldload ng_socket + run), VERDICT.md (full mechanism walkthrough), README.md, fix.diff (validated one-line arglen check), manifest.json, plus the captured run/fix logs and env.txt.

Verified recommended fix

In ngc_send, immediately after m_copydata(m,0,len,xmsg) and before ng_send_msg, reject the datagram with EINVAL when len < sizeof(struct ng_mesg) OR ((struct ng_mesg*)xmsg)->header.arglen > len - sizeof(struct ng_mesg); free xmsg on that error path (ng_send_msg normally frees it). This is the DragonFly port of FreeBSD svn r184036 (CVE-2008-5736). Supersedes the finding proposal (the finding described the bug accurately but no concrete diff existed; this is the authoritatively-validated fix). Full git-apply-able diff in findings/poc/DF-0860/fix.diff.

Verdict

REPRODUCED. The bug is real: ngc_send (sys/netgraph/socket/ng_socket.c:201) linearizes the user datagram into xmsg=kmalloc(len+1) (line 253) and casts it to (struct ng_mesg*) WITHOUT checking len>=sizeof(ng_mesg) (52) or header.arglen<=len-sizeof(ng_mesg). When the destination path is '.' ng_path2node loops to the same node (ng_base.c:~1130), and ngs_rcvmsg (ng_socket.c:788) -- for a typecookie that is neither NGM_GENERIC_COOKIE nor NGM_SOCKET_COOKIE -- calls ship_msg(pcbp, msg=xmsg, addr) (ng_socket.c:836). ship_msg trusts header.arglen: msglen=sizeof(ng_mesg)+arglen (line 737), m_devget(msg,msglen,...) (line 738) reads 52+arglen bytes from the undersized buffer -> heap OOB read queued on so_rcv and returned to userspace via recvfrom. Confirmed as root: sendto(52-byte header with arglen=128) -> recvfrom returns 180 bytes, the trailing 128 are leaked kernel heap containing canonical pointers (0xfffff800_4f1643c0) and residual strings (rcng_automountd, disabled, /usr). This is the never-ported FreeBSD CVE-2008-5736 (svn r184036).