β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2713

recvmsg(2) copies out uninitialized CMSG_SPACE tail padding of every kernel-built control message β€” unprivileged, groomable kernel heap disclosure

Field Value
ID DF-2713
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
CWE CWE-908 / CWE-200
File sys/kern/uipc_syscalls.c
Lines 1175-1189 (sink); builder uipc_sockbuf.c:595-601
Area kern
Confidence certain
Discovered 2026-08-30
Pass 2 (GLM 5.3 second pass)
Bucket kernleak
Reported pending
Known CVE none
CVE match novel

Summary

sbcreatecontrol() builds each kernel control message in a recycled plain mbuf with m->m_len = CMSG_SPACE(size) but initializes only CMSG_LEN(size) bytes; sys_recvmsg()'s copyout loop copies m->m_len bytes per control mbuf, padding included. On x86_64 the alignment padding is 7 bytes for IP_RECVTTL/IP_RECVTOS (data size 1), 4 for IP_RECVDSTADDR/IPV6_PKTINFO/IPV6_HOPLIMIT/SCM_CREDS, 0-7 for IP_RECVIF β€” stale contents of the recycled mbuf object, disclosed to any unprivileged user (also remotely triggerable: any peer's packets with the victim's RECV* options set). Groom demonstrated: a failing sendmsg(2) allocates a plain MT_CONTROL mbuf, copyins attacker bytes, and frees it to the same per-CPU objcache; the next sbcreatecontrol recycles it and the padding carries the marker. Cross-process disclosure demonstrated (fork()ed child's marker in parent's padding), ~18 stale bytes per received datagram, unlimited repetition.

Proof of concept

VERIFIED (findings/poc/DF-2713/leak_cmsgpad.c): UDP on 127.0.0.1 with IP_RECVTTL+RECVTOS+RECVDSTADDR; groom with EINVAL sendmsg control churn (0xCC marker); 2396-2400 marker bytes per 300-receive run, decisive hexdump cc cc cc cc directly after the 127.0.0.1 data, and 4Γ— 0x5A bytes per run from a separate process. Fix (bzero the whole control message in sbcreatecontrol) validated on kernel #1: 0 marker and 0 nonzero padding bytes in all phases.

--- a/sys/kern/uipc_sockbuf.c
+++ b/sys/kern/uipc_sockbuf.c
@@ -594,6 +594,13 @@
        return (NULL);
    m->m_len = CMSG_SPACE(size);
    cp = mtod(m, struct cmsghdr *);
+   /*
+    * Zero the whole control message (header + data + tail padding)
+    * so the alignment padding between CMSG_LEN(size) and
+    * CMSG_SPACE(size) cannot disclose stale mbuf heap contents via
+    * recvmsg(2) copyout of m_len bytes (sys_recvmsg).
+    */
+   bzero(cp, CMSG_SPACE(size));
    if (p != NULL)
        memcpy(CMSG_DATA(cp), p, size);
    cp->cmsg_len = CMSG_LEN(size);

Timeline

  • 2026-08-30 Discovered during pass-2 audit of uipc_syscalls.c (GLM 5.3); unpriv cross-process leak reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2713 Β· 13 files
FileTypeDescriptionSize
leak_cmsgpad.c β€” 7.8 KB view raw
build.sh β€” 54 B view raw
run.sh β€” 25 B view raw
build.log β€” 67 B view raw
run.log β€” 1.2 KB view raw
run.2.log β€” 1.0 KB view raw
run.3.log β€” 1.1 KB view raw
fix_run.log β€” 919 B view raw
env.txt β€” 250 B view raw
fix.diff β€” 536 B view raw
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 3.5 KB ↓ raw
verdict.json β€” 4.1 KB view raw

DF-2713 β€” Uninitialized cmsg tail-padding kernel heap leak via recvmsg(2)

What

sbcreatecontrol() (sys/kern/uipc_sockbuf.c:585-603) builds every kernel-generated control message (SCM_TIMESTAMP, IP_RECVDSTADDR, IP_RECVTTL, IP_RECVTOS, IP_RECVIF, IPV6_PKTINFO, IPV6_HOPLIMIT, SCM_CREDS, …) in a recycled plain mbuf and sets m->m_len = CMSG_SPACE(size), but it only writes CMSG_LEN(size) bytes (aligned header + data). The alignment padding bytes between CMSG_LEN(size) and CMSG_SPACE(size) are never initialized and retain the stale contents of the recycled mbuf object.

sys_recvmsg() (sys/kern/uipc_syscalls.c:1172-1192) copies out m->m_len bytes per control mbuf β€” padding included β€” into the user's control buffer.

On x86_64 (8-byte alignment, sizeof(struct cmsghdr)=12, header aligned to 16): IP_RECVTTL/IP_RECVTOS leak 7 stale bytes per packet, IP_RECVDSTADDR/IPV6_PKTINFO/IPV6_HOPLIMIT leak 4, IP_RECVIF leaks 0-7 (sockaddr_dl length dependent), SCM_CREDS 4.

Threat

Any unprivileged local user (no jail, no privileges): open a UDP socket, setsockopt(IP_RECVTTL/IP_RECVTOS/IP_RECVDSTADDR), exchange loopback datagrams, recvmsg(2), read the padding. Because the control mbuf is allocated from the global per-CPU plain-mbuf objcache, the padding discloses whatever any process last freed into that mbuf β€” packet payloads, control buffers, socket data of other users. Grooming with a failing sendmsg(2) (which allocates a plain MT_CONTROL mbuf, copies the attacker's control bytes in, then frees it via sosend's error path, sys/kern/uipc_socket.c:1240) makes the disclosure deterministic.

Reproduce

cc -O2 -Wall -o leak_cmsgpad leak_cmsgpad.c
./leak_cmsgpad

Success criterion: Phase B reports MARKER(0xCC) > 0 and the marker-hit hexdump shows cc cc cc cc after the 7f 00 00 01 (127.0.0.1) data of the IP_RECVDSTADDR cmsg β€” bytes the kernel never wrote, copied to userspace. Phase C demonstrates the same with a fork()ed process's 0x5A marker (cross-process disclosure).

Observed on the guest (3 executions): 2396/2400/2400 (run.log), 8/4/1579 + cross-proc 4/4/4 (run.2.log), 1196/1668/2280 (run.3.log) marker bytes per 300-receive run.

Fix

Zero the tail padding in sbcreatecontrol() (see fix.diff).

VERDICT.md
↓ download raw

DF-2713 β€” VERDICT

Status: REPRODUCED (impact: leak β€” kernel heap disclosure, unprivileged)

How it was reproduced

Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS), 6 CPUs, 4 GB RAM, kern.ipc.nmbclusters=33296.

PoC leak_cmsgpad.c (built with guest cc 8.3, run as unprivileged maxx):

  1. UDP socket on 127.0.0.1 with IP_RECVDSTADDR + IP_RECVTTL + IP_RECVTOS enabled; each loopback datagram produces 3 kernel-built cmsgs in one control mbuf each; per receive the padding amounts are 4+7+7 = 18 bytes (pad_bytes=5400 over 300 iterations confirms the arithmetic: CMSG_SPACE(4)=24 vs CMSG_LEN(4)=20; CMSG_SPACE(1)=24 vs CMSG_LEN(1)=17).
  2. Phase A (baseline): 300-521 non-zero stale bytes per 5400 padding bytes without any grooming β€” the leak exists on a freshly booted system.
  3. Phase B (same-process groom): between receives the PoC issues failing sendmsg(2) calls on an AF_UNIX socketpair with a 40-byte control buffer filled with 0xCC. sys_sendmsg() (sys/kern/uipc_syscalls.c:940-947) allocates a plain m_get(M_WAITOK, MT_CONTROL) mbuf β€” the same objcache the receive-path control mbufs come from β€” copyins the 0xCC bytes, and sosend() frees the mbuf on the EINVAL error path (sys/kern/uipc_socket.c:1240). The next sbcreatecontrol() recycles that mbuf and the un-overwritten padding retains 0xCC. Result: 2396-2400 0xCC bytes leaked per 300-receive run and the decisive hexdump: +00: 14 00 00 00 00 00 00 00 07 00 00 00 cc cc cc cc +10: 7f 00 00 01 cc cc cc cc ... = IP_RECVDSTADDR cmsg (cmsg_len=0x14=20, data 7f 00 00 01 = 127.0.0.1) followed by cc cc cc cc of attacker-supplied stale heap.
  4. Phase C (cross-process): a fork()ed child grooms with 0x5A on its own sockets; the parent's cmsg padding contained 4 x 0x5A bytes per run (run.2.log) β€” disclosure of another process's freed mbuf contents.

Three executions (run.log, run.2.log, run.3.log) all show marker bytes in the padding β€” not a one-shot artifact.

Root cause chain (path:line)

Not a duplicate

DF-2702 is the SCM_RIGHTS MSG_PEEK raw-pointer leak at the soreceive sink (uipc_socket.c). DF-0010/DF-2557 are about uninitialized cmsgcred content on the SO_PASSCRED path. DF-0389 is rtsock sockaddr padding. This finding is the generic CMSG_SPACE tail-padding of every sbcreatecontrol()-built cmsg, surfaced by the recvmsg copyout in uipc_syscalls.c.

Exploitability ceiling

kernleak: repeatable, deterministic-content (groomable) disclosure of stale kernel mbuf heap to any unprivileged local user; up to ~18 stale bytes per received datagram (more with IP_RECVIF / IPv6 options / SCM_CREDS stacked, and repeatable without limit). Cross-process content disclosure demonstrated (Phase C).

Fix validation

fix.diff bzeros the full CMSG_SPACE in sbcreatecontrol(). Applied to the guest's /usr/src copy, make nativekernel + make installkernel + reboot; the same PoC then reported 0 marker bytes and 0 non-zero padding bytes in all phases (fix_run.log). Baseline (unpatched) vs patched output included.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Applied fix.diff to the guest's /usr/src, make nativekernel + installkernel (kernel #1), rebooted, re-ran the identical PoC: 0 marker bytes and 0 nonzero padding bytes in all phases (fix_run.log) vs 2396-2400 marker bytes on baseline. Leak eliminated; no regressions observed during the run.

['fix_run.log', 'fix.diff']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Mon Aug 31 04:46:23 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

socket(AF_INET,SOCK_DGRAM) + setsockopt(IP_RECVTTL/IP_RECVTOS/IP_RECVDSTADDR); groom the per-CPU plain-mbuf objcache with failing sendmsg(2) calls whose 40-byte control buffer carries the marker (sys_sendmsg allocates a plain MT_CONTROL mbuf, copyins the bytes, sosend frees it via uipc_socket.c:1240); sendto/recvmsg on 127.0.0.1; read 18 stale heap bytes per receive from [CMSG_LEN(size), CMSG_SPACE(size)) of each cmsg. Repeatable without limit; discloses whatever any process last freed into the mbuf cache (packet payloads, control buffers of other users' sockets).

Evidence (decisive lines)

["run.log: Phase B MARKER(0xCC)=2396/2400/2400 per 300-iter run + marker-hit hexdump '+00: 14 00 00 00 00 00 00 00 07 00 00 00 cc cc cc cc / +10: 7f 00 00 01 cc cc cc cc' (cc bytes in the IP_RECVDSTADDR padding right after the 127.0.0.1 data)", "run.2.log: Phase C XPROC-MARKER(0x5A)=4 in all 3 runs (fork()ed child's bytes in parent's padding)", 'run.3.log: Phase B 1196/1668/2280 marker bytes (3rd execution, stability)', 'fix_run.log: kernel #1 with fix.diff -> 0 marker and 0 nonzero padding bytes in all phases']

PoC changes

Rewrote the seed concept entirely: final PoC grooms the plain-mbuf objcache via FAILING sendmsg(2) control marshalling (EINVAL from unp_internalize frees the marker mbuf through sosend's error path) because AF_UNIX dgram data mbufs come from the pkthdr cache and never met the control-mbuf cache; UDP payload sized 1200B so the trigger packet itself uses clusters and does not consume plain mbufs.

Verified recommended fix

bzero the full CMSG_SPACE(size) in sbcreatecontrol() before filling header/data (uipc_sockbuf.c:596).

Verdict

Uninitialized CMSG_SPACE tail padding of every sbcreatecontrol()-built control message is copied to userspace by sys_recvmsg()'s per-mbuf copyout of m->m_len bytes; reproduced as unprivileged user maxx on the stock INVARIANTS kernel with deterministic attacker-planted content (2396-2400 marker bytes per 300-receive run in the same process, 4 marker bytes per run from a fork()ed process), and eliminated entirely by zeroing the padding (kernel #1 with fix.diff: 0 marker, 0 nonzero bytes in all phases).