Missing NULL check on sbcreatecontrol() in SO_PASSCRED path -> kernel NULL-deref panic
| Field | Value |
|---|---|
| ID | DF-0011 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-476 NULL Pointer Dereference |
| File | sys/kern/uipc_usrreq.c |
| Lines | 694-698 |
| Area | kern |
| Confidence | likely |
| Discovered | 2026-06-29 |
| Reported | pending |
Summary
In the SO_PASSCRED synthesis block of uipc_send() (AF_UNIX SOCK_DGRAM),
the return value of sbcreatecontrol() is never checked. sbcreatecontrol()
returns NULL on mbuf exhaustion (M_NOWAIT allocation failure); the code
then calls unp_internalize(NULL, ...) which does cm = mtod(control, ...)
β dereferencing NULL and panicking the kernel. This is a local
denial-of-service triggerable by an unprivileged user who has induced mbuf
pressure and sends to a SO_PASSCRED-marked AF_UNIX datagram socket without
including an SCM_CREDS control message.
Root cause
sys/kern/uipc_usrreq.c:694-698:
if (ncon == NULL) { /* no existing SCM_CREDS found */
ncon = sbcreatecontrol(&cred, sizeof(cred),
SCM_CREDS, SOL_SOCKET); /* may return NULL */
unp_internalize(ncon, msg->send.nm_td); /* ncon used unchecked */
*mp = ncon;
}
sbcreatecontrol() (sys/kern/uipc_sockbuf.c) returns NULL on a
CMSG_SPACE > MCLBYTES request or an m_getl(..., M_NOWAIT) allocation
failure. The return is not tested. unp_internalize then unconditionally
dereferences via mtod(control) at sys/kern/uipc_usrreq.c:1706,
i.e. ((struct cmsghdr *)(NULL->m_data)) β a NULL page fault that panics
the kernel.
Threat model & preconditions
- Attacker position: unprivileged local user.
- Privileges gained or impact: full-system denial of service (kernel panic). No memory corruption (clean NULL deref).
- Required config or capabilities: default kernel; the attacker must
induce enough mbuf pressure to make the
M_NOWAITallocation fail (e.g. many sockets with full buffers), then send to anSO_PASSCREDAF_UNIXSOCK_DGRAMreceiver without anSCM_CREDScmsg. - Reachability:
socketpair(AF_UNIX, SOCK_DGRAM)+SO_PASSCRED+ a plainsend().
Proof of concept
PoC source: findings/poc/DF-0011/nopasscred_panic.c
Phase 1 exhausts mbufs (many socketpairs with full buffers); Phase 2 fires the trigger sends.
Build & run
cc -o nopasscred_panic findings/poc/DF-0011/nopasscred_panic.c ./nopasscred_panic # as a non-root user
Expected output
Under mbuf pressure:
Fatal trap 12: page fault while in kernel mode fault virtual address = 0x0 ... in unp_internalize ...
(With ample mbufs the allocation succeeds and no panic occurs β exit code 2.)
Impact
Local DoS (kernel panic). Reliability depends on inducing allocation failure,
hence AC:H / Low. No integrity/confidentiality impact.
Recommended fix
Check sbcreatecontrol() for NULL and bail cleanly with ENOBUFS:
--- a/sys/kern/uipc_usrreq.c
+++ b/sys/kern/uipc_usrreq.c
@@ -693,8 +693,13 @@
if (ncon == NULL) {
ncon = sbcreatecontrol(&cred, sizeof(cred),
SCM_CREDS, SOL_SOCKET);
- unp_internalize(ncon, msg->send.nm_td);
- *mp = ncon;
+ if (ncon != NULL) {
+ unp_internalize(ncon, msg->send.nm_td);
+ *mp = ncon;
+ } else {
+ error = ENOBUFS;
+ break;
+ }
}
(The surrounding error/cleanup path already handles error != 0; adjust the
break/goto to match the loop's cleanup convention.)
References
sys/kern/uipc_usrreq.c:694-698β uncheckedsbcreatecontrolreturn.sys/kern/uipc_sockbuf.cβsbcreatecontrolcan returnNULL.sys/kern/uipc_usrreq.c:1706βmtod(control)deref that faults onNULL.- CWE-476 NULL Pointer Dereference.
Timeline
- 2026-06-29 Discovered during automated file-by-file audit of
sys/kern/uipc_usrreq.c. - pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0011 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| nopasscred_panic.c | trigger-source | concurrent plain-mbuf-exhaustion ramp + no-control SO_PASSCRED trigger -> sbcreatecontrol NULL -> panic | 6.3 KB | view raw |
| flood_trigger.c | auxiliary-source | earlier hold-open mbuf-exhaustion variant used during analysis | 3.2 KB | view raw |
| README.md | readme | build/run/expected + root-cause + the 2:1 plain:pkthdr strategy | 2.7 KB | β raw |
| VERDICT.md | verdict | full REPRODUCED + fix-validation narrative: NULL-check missing -> deref at 0x10; before/after kernel build | 8.4 KB | β raw |
| build.sh | repro-script | cc -o nopasscred_panic nopasscred_panic.c -lpthread | 141 B | view raw |
| run.sh | repro-script | ./nopasscred_panic (PANICS the guest on #0) | 318 B | view raw |
| build.log | build-log | final successful PoC build, full output | 125 B | view raw |
| run.log | run-log | baseline run record on UNPATCHED #0: build+launch+serial panic excerpt | 3.2 KB | view raw |
| panic.txt | panic-signature | Fatal trap 12, fault vaddr 0x10, Stopped at unp_internalize+0x11 on #0 baseline | 804 B | view raw |
| env.txt | environment | uname, cc version, kern.ipc sysctls (nmbclusters/nmbufs/maxsockbuf) | 630 B | view raw |
| fix.diff | suggested-fix | git-apply-able: NULL-check sbcreatecontrol at uipc_usrreq.c:694, ENOBUFS + unp_free + break. VALIDATED on built #1 kernel. | 409 B | view raw |
| fix_build.log | fix-build-log | full nativekernel build output for the single-fix kernel (rc=0, 35343 lines) | 5.6 MB | β download |
| fix_run.log | fix-run-log | PoC on patched #1: run1 (no panic, guest up after 2+ min) + run2 (100M trigger sends, exit 2, no panic) | 3.7 KB | 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 |
DF-0011 β PoC
nopasscred_panic.c β local DoS via the missing sbcreatecontrol() NULL
check in the SO_PASSCRED synthesis path of uipc_send(). flood_trigger.c
is an auxiliary hold-open variant used during analysis.
The bug
uipc_send() SOCK_DGRAM, sys/kern/uipc_usrreq.c:694-699:
if (ncon == NULL) { /* no existing SCM_CREDS cmsg found */
ncon = sbcreatecontrol(&cred, sizeof(cred), SCM_CREDS, SOL_SOCKET);
unp_internalize(ncon, msg->send.nm_td); /* ncon may be NULL */
*mp = ncon;
}
sbcreatecontrol() (sys/kern/uipc_sockbuf.c:585-604) returns NULL when its
m_getl(..., M_NOWAIT, MT_CONTROL, 0, NULL) fails β i.e. when the plain
"mbuf" objcache (sys/kern/uipc_mbuf.c:798, nmbufs deep) is exhausted.
The return is never checked, so unp_internalize(NULL) runs and at
uipc_usrreq.c:1706 does cm = mtod(control, ...) = load of mh_data from a
NULL mbuf β offsetof(struct m_hdr, mh_data) == 0x10 β page fault at vaddr
0x10 in kernel mode β panic.
Trigger
An unprivileged local user who (a) exhausts the plain "mbuf" objcache and
(b) sends to an AF_UNIX SOCK_DGRAM receiver that has SO_PASSCRED set,
without an SCM_CREDS cmsg.
How the precondition is met (the crux): each AF_UNIX SOCK_DGRAM
datagram pinned in a receiver buffer consumes 2 plain mbufs (MT_CONTROL +
MT_SONAME) but only 1 pkthdr mbuf (MT_DATA data). Pinned plain:pkthdr
ratio is therefore 2:1. Exhausting the 72904-deep plain cache pins only ~36500
pkthdr, leaving the pkthdr cache ~half empty β so the trigger's data pkthdr
mbuf still allocates (M_WAITOK in sosend) and execution reaches
sbcreatecontrol(), whose plain m_get(M_NOWAIT) then FAILS β NULL β panic.
The PoC runs a pinner thread (ramp plain-mbuf pressure) concurrently with a
trigger thread (fire no-control SO_PASSCRED sends), so a trigger send lands
on the instant of plain-cache exhaustion instead of overshooting into a wedge.
Build
cc -o nopasscred_panic nopasscred_panic.c -lpthread
Run
As an unprivileged user (this PANICS the kernel β local DoS):
./nopasscred_panic
Expected output (bug present)
A kernel panic on the serial console:
Warning: objcache(mbuf) exhausted on cpuN! Fatal trap 12: page fault while in kernel mode fault virtual address = 0x10 Stopped at unp_internalize.isra.12+0x11: movq 0x10(%rdi),%rbx
(The finding markdown guessed fault address 0x0; the real address is 0x10,
the mh_data offset in struct m_hdr β same bug, sharper address.)
Reproduced twice from independent fresh vm.sh reset boots with an identical
signature. On a patched kernel (NULL check added) the trigger send returns
ENOBUFS instead and the program prints "no panic".
DF-0011 β VERDICT
Verdict: REPRODUCED (unprivileged local DoS via NULL-deref kernel panic in
unp_internalize). Impact: panic (denial of service). Confidence:
certain. Reproduced twice from independent fresh vm.sh reset boots with
an identical, mechanism-matching panic signature.
Mechanism (trigger β primitive β effect)
-
Missing NULL check (root cause). In
uipc_send,SOCK_DGRAM,sys/kern/uipc_usrreq.c:694-699, when no pre-existingSCM_CREDScmsg is present and the receiver hasSO_PASSCREDset:c if (ncon == NULL) { ncon = sbcreatecontrol(&cred, sizeof(cred), SCM_CREDS, SOL_SOCKET); unp_internalize(ncon, msg->send.nm_td); /* ncon used UNCHECKED */ *mp = ncon; }The return ofsbcreatecontrol()is never tested. -
sbcreatecontrolcan return NULL.sys/kern/uipc_sockbuf.c:585-604:c if (CMSG_SPACE(size) > MCLBYTES) return (NULL); m = m_getl(CMSG_SPACE(size), M_NOWAIT, MT_CONTROL, 0, NULL); if (m == NULL) return (NULL);ForSCM_CREDS,size = sizeof(struct cmsgcred) = 84, soCMSG_SPACE ~ 104 < MCLBYTESand the first check passes. The only NULL path is them_getl(M_NOWAIT, MT_CONTROL)failure.MT_CONTROLwith this size takes the plain-mbuf branch ofm_getl(sys/sys/mbuf.h:589-601), i.e.m_get()from the plain "mbuf" objcache (sys/kern/uipc_mbuf.c:798, limitnmbufs;72904on this guest pernetstat -m). That objcache returns NULL when exhausted. -
NULL deref at offset 0x10.
unp_internalize(:1702) opens withc struct cmsghdr *cm = mtod(control, struct cmsghdr *); /* :1706 */mtod(m, t) = (t)((m)->m_data)(sys/sys/mbuf.h:73) andm_data == m_hdr.mh_data(:221). Withcontrol == NULL, this is a load ofmh_datafrom addressNULL + offsetof(struct m_hdr, mh_data).struct m_hdr(sys/sys/mbuf.h:79-90) ismh_next[8] + mh_nextpkt[8] + mh_dataβoffsetof(mh_data) == 16 == 0x10. β page fault at virtual address 0x10 in kernel mode β panic.
Trigger strategy (how the precondition is met by an unprivileged user)
The hard part is making m_get(M_NOWAIT) fail. The plain "mbuf" cache is
~72904 deep and NMBUFS_MIN = NMBUFS/2 β 36516, so it cannot be shrunk at
runtime below ~36500. The decisive observation: each AF_UNIX SOCK_DGRAM
datagram pinned in a receiver buffer accounts for 2 plain mbufs but only 1
pkthdr mbuf β one MT_CONTROL (the cmsg) plus one MT_SONAME (the
source sockaddr recorded by ssb_appendaddr) for the data's one MT_DATA
pkthdr. So the pinned plain:pkthdr ratio is 2:1. Exhausting the 72904-deep
plain cache pins only ~36500 pkthdr mbufs, leaving the pkthdr cache ~half
empty. The trigger's no-control send() therefore successfully allocates its
data pkthdr mbuf (m_getl(M_WAITOK) in sosend, uipc_socket.c:866), proceeds
into uipc_send's SO_PASSCRED block, and calls sbcreatecontrol() whose
plain m_get(M_NOWAIT) FAILS β NULL β unp_internalize(NULL) β fault
at 0x10.
The PoC runs this concurrently: a pinner thread ramps the pinned-datagram
count while a trigger thread continuously fires no-control SO_PASSCRED
sends, so a trigger send lands on the instant of plain-cache exhaustion (but
before the pkthdr cache is also starved), deterministically hitting the NULL
path. (A naive "pin everything, then fire" loop instead overshoots into a
full memory-pressure wedge without reaching the trigger β that is the race the
finding flagged as AC:H.)
Evidence (decisive, reproduced twice)
panic.txt holds the full serial-console excerpt from dfbsd-qemu/boot.log.
Both fresh-reset reproductions produced an identical signature:
login: Warning: objcache(mbuf) exhausted on cpu1! Fatal user address access from kernel mode from nopasscred_panic at ffffffff806cdac1 Fatal trap 12: page fault while in kernel mode cpuid = 1; lapic id = 1 fault virtual address = 0x10 fault code = supervisor read data, page not present instruction pointer = 0x8:0xffffffff806cdac1 current process = 1468 (unprivileged: nopasscred_panic, run as maxx) kernel: type 12 trap, code=0 Stopped at unp_internalize.isra.12+0x11: movq 0x10(%rdi),%rbx db>
This matches the cited path exactly:
- objcache(mbuf) exhausted β the NULL-return precondition of
sbcreatecontrol is met.
- fault virtual address = 0x10 β offsetof(struct m_hdr, mh_data). (The
finding markdown guessed 0x0; the real fault address is 0x10 β the
mh_data offset, not the mbuf base pointer. Same bug, sharper address.)
- Stopped at unp_internalize.isra.12+0x11: movq 0x10(%rdi),%rbx with
%rdi == 0 (NULL control) β precisely the mtod(control,β¦) load at
uipc_usrreq.c:1706.
- current process = 1468 β the unprivileged trigger, confirming local DoS.
PoC changes
The seeded nopasscred_panic.c had (a) a compile bug (#define N SOCK 4096
β NSOCK undeclared) and (b) a flawed exhaustion strategy that targeted the
pkthdr cache (datagram data) rather than the plain cache that
sbcreatecontrol uses, so it only wedged the guest instead of panicking. I
rewrote it to (1) pin plain mbufs via SCM_CREDS-bearing datagrams at the
2:1 plain:pkthdr ratio, and (2) fire the no-control SO_PASSCRED trigger
concurrently during the ramp so it lands on the exhaustion crossover
instead of overshooting into a wedge. flood_trigger.c is retained as an
auxiliary/earlier hold-open variant. The improved PoC reproduces the panic
from fresh resets.
Exploit chain
Not a memory-corruption class β a clean NULL deref (read of mh_data from a
NULL mbuf). No integrity/confidentiality impact; ceiling = reliable local DoS
(panic). current process = <unprivileged trigger> and panic from
nopasscred_panic confirm unprivileged reachability.
Recommended fix
fix.diff (git-apply-able, git apply --check verified) adds the missing
NULL check at sys/kern/uipc_usrreq.c:694-699:
if (ncon == NULL) {
ncon = sbcreatecontrol(&cred, sizeof(cred), SCM_CREDS, SOL_SOCKET);
if (ncon == NULL) {
error = ENOBUFS;
unp_free(unp2); /* drop ref the normal SOCK_DGRAM path holds */
break; /* -> function epilogue frees m & control */
}
unp_internalize(ncon, msg->send.nm_td);
*mp = ncon;
}
unp_free(unp2) is called explicitly because the break skips the normal
path's unp_free(unp2) at :717; the function's release: epilogue (after
the switch) already frees m and control and disposes control when
error != 0, so no resource leak. This supersedes the finding markdown's
## Recommended fix (which used a bare break without unp_free(unp2),
leaking an unp2 reference) while preserving its intent.
Fix validation (Phase 8 β built + booted single-fix kernel)
Build: Applied fix.diff to clean with-src /usr/src (patch -p1
succeeded at line 694), built make -j6 nativekernel KERNCONF=X86_64_GENERIC
(rc=0, ~4 min), installed kernel.stripped β /boot/kernel/kernel (sha256
ae0ecfae...), rebooted. Booted kernel: #1 Thu Jul 2 18:42:06 UTC 2026
(baseline was #0 Thu Jul 2 06:02:54). Full build log in fix_build.log.
Before (unpatched #0): PoC panicked the kernel within seconds of plain-cache exhaustion β identical to the original reproduction:
Warning: objcache(mbuf) exhausted on cpu0! Fatal trap 12: page fault while in kernel mode fault virtual address = 0x10 Stopped at unp_internalize.isra.12+0x11: movq 0x10(%rdi),%rbx
Guest DOWN at DDB db> prompt. (run.log, panic.txt.)
After (patched #1) β Run 1: PoC pinned 76950 dgrams, the Warning:
objcache(mbuf) exhausted warnings fired (proving the allocation-failure
precondition was met), but NO panic β guest stayed UP, the trigger
process kept firing sends for 2+ minutes under exhaustion. On the unpatched
kernel the panic fires within seconds.
After (patched #1) β Run 2 (clean reboot): PoC ran to completion:
trigger fired 100,710,079 no-control SO_PASSCRED sends under mbuf
exhaustion, exited 2 cleanly, guest healthy (Fatal trap/Stopped at
absent from boot.log).
Verdict: fix_status = FIXED. The before/after contrast is unambiguous:
same PoC, same workload, same mbuf-exhaustion precondition β panic on #0,
no panic on #1. The NULL check after sbcreatecontrol() converts the
NULL-deref into a graceful ENOBUFS return, closing the bug. (fix_run.log.)
Fix verification
fixedVALIDATED. Baseline #0 (unpatched 6cc80ee9, build 06:02:54) PANICKED: Fatal trap 12, fault vaddr 0x10 in unp_internalize.isra.12+0x11, guest DOWN at db>. Single-fix #1 kernel (build 18:42:06, sha256 ae0ecfae...) does NOT panic: PoC fired 100,710,079 trigger sends under mbuf exhaustion across two runs with no Fatal trap/Stopped at in boot.log, exit 2, guest healthy => fix closes the bug. Build rc=0.
baseline #0: 'Fatal trap 12: page fault while in kernel mode' / 'fault virtual address = 0x10' / 'Stopped at unp_internalize.isra.12+0x11: movq 0x10(%rdi),%rbx' / guest DOWN. patched #1: 'fired=100710079 enobufs=0' / 'RUN_EXIT=2' / no Fatal trap/Stopped at in boot.log / guest UP. build: '>>> Kernel build for X86_64_GENERIC completed' rc=0.
Confirmed kernel references
Detail
Exploit chain
Not a memory-corruption class β a clean NULL-deref read of mh_data (offset 0x10) from a NULL mbuf pointer in unp_internalize. Ceiling = reliable local DoS (kernel panic) by an unprivileged user (current process = the unprivileged trigger). No integrity/confidentiality impact; no further primitive derivable.
Evidence (decisive lines)
BASELINE #0: 'Warning: objcache(mbuf) exhausted on cpu0!' / 'Fatal trap 12: page fault while in kernel mode' / 'fault virtual address = 0x10' / 'Stopped at unp_internalize.isra.12+0x11: movq 0x10(%rdi),%rbx' / guest DOWN at db>. PATCHED #1 run2: trigger fired=100710079 enobufs=0, RUN_EXIT=2, NO Fatal trap/Stopped at in boot.log, guest UP. Build rc=0.
PoC changes
No PoC source changes this session (the concurrent ramp+fire nopasscred_panic.c from the prior session reproduces cleanly). Refreshed all evidence logs: panic.txt (this session's #0 baseline panic), run.log (baseline record), fix_build.log (35343-line nativekernel output, rc=0), fix_run.log (patched #1 run1 no-panic + run2 100M-trigger-sends exit-2), env.txt (both #0 and #1), VERDICT.md (added full Phase-8 fix-validation section), manifest.json (added fix_status/fix_kernel_uname + fix_build.log/fix_run.log artifacts). fix.diff unchanged (already correct: NULL-check + ENOBUFS + unp_free(unp2) + break).
Verified recommended fix
fix.diff adds a NULL check after sbcreatecontrol() at sys/kern/uipc_usrreq.c:694-699: if sbcreatecontrol returns NULL (mbuf exhaustion), set error=ENOBUFS, call unp_free(unp2) to drop the reference the normal SOCK_DGRAM path holds at :717 (which the break skips), and break to the release: epilogue that frees m and control. This supersedes the finding markdown's proposal (bare break without unp_free, leaking unp2). Validated: applies cleanly (git apply --check OK), compiles (nativekernel rc=0), and eliminates the panic on the built #1 kernel. Full diff in findings/poc/DF-0011/fix.diff.
Verdict
REPRODUCED + FIX VALIDATED. On the unpatched #0 kernel the PoC panics with 'Fatal trap 12: page fault while in kernel mode', 'fault virtual address = 0x10', 'Stopped at unp_internalize.isra.12+0x11: movq 0x10(%rdi),%rbx' β the exact NULL-deref from the unchecked sbcreatecontrol() return at uipc_usrreq.c:694-699 (mtod(NULL) loads mh_data at offsetof 0x10). The SO_PASSCRED synthesis path calls sbcreatecontrol() whose m_get(M_NOWAIT,MT_CONTROL) returns NULL under plain-mbuf-cache exhaustion; the NULL is passed unchecked to unp_internalize which dereferences it. Confirmed by a fresh before/after: I built a single-fix kernel (#1, the 5-line NULL-check fix.diff) and re-ran the same PoC β it fired 100,710,079 no-control SO_PASSCRED sends under mbuf exhaustion with NO panic (exit 2, guest healthy), versus panic-within-seconds on #0. The fix converts the NULL deref into a graceful ENOBUFS return.
No comments yet.