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.
Recommended fix
--- 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)
PoC verification
Evidence pack
findings/poc/DF-2713 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
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):
- 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=5400over 300 iterations confirms the arithmetic: CMSG_SPACE(4)=24 vs CMSG_LEN(4)=20; CMSG_SPACE(1)=24 vs CMSG_LEN(1)=17). - Phase A (baseline): 300-521 non-zero stale bytes per 5400 padding bytes without any grooming β the leak exists on a freshly booted system.
- 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 plainm_get(M_WAITOK, MT_CONTROL)mbuf β the same objcache the receive-path control mbufs come from β copyins the 0xCC bytes, andsosend()frees the mbuf on the EINVAL error path (sys/kern/uipc_socket.c:1240). The nextsbcreatecontrol()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, data7f 00 00 01= 127.0.0.1) followed bycc cc cc ccof attacker-supplied stale heap. - 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)
- sys/kern/uipc_sockbuf.c:595
m->m_len = CMSG_SPACE(size) - sys/kern/uipc_sockbuf.c:597-601 only
CMSG_LEN(size)bytes initialized - sys/kern/uipc_syscalls.c:1175-1189 recvmsg copyout loop copies
m->m_lenbytes per control mbuf, padding included - Builders: sys/netinet/ip_input.c:2200-2272 (SCM_TIMESTAMP 0-pad, IP_RECVDSTADDR, IP_RECVTTL, IP_RECVTOS, IP_RECVIF), sys/netinet6/ip6_input.c ip6_savecontrol (IPV6_PKTINFO etc.), sys/kern/uipc_usrreq.c:695 (SCM_CREDS)
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
fixedApplied 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']
Confirmed kernel references
- sys/kern/uipc_sockbuf.c:585
- sys/kern/uipc_sockbuf.c:595
- sys/kern/uipc_sockbuf.c:597
- sys/kern/uipc_sockbuf.c:599
- sys/kern/uipc_syscalls.c:1175
- sys/kern/uipc_syscalls.c:1182
- sys/kern/uipc_syscalls.c:1189
- sys/netinet/ip_input.c:2206
- sys/netinet/ip_input.c:2212
- sys/netinet/ip_input.c:2218
- sys/netinet6/ip6_input.c:1070
- sys/kern/uipc_usrreq.c:695
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).
No comments yet.