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.
PoC verification
Evidence pack
findings/poc/DF-0011 ยท 11 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 narrative: NULL-check missing -> deref at 0x10, with path:line | 6.7 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) | 318 B | view raw |
| build.log | build-log | final successful build, full output | 13 B | view raw |
| run.log | run-log | decisive run record (build+launch+serial panic excerpt) | 2.1 KB | view raw |
| panic.txt | panic-signature | Fatal trap 12, fault vaddr 0x10, Stopped at unp_internalize+0x11 (reproduced twice from fresh resets) | 2.8 KB | view raw |
| env.txt | environment | uname, cc version, kern.ipc sysctls (nmbclusters/nmbufs/maxsockbuf) | 307 B | view raw |
| fix.diff | suggested-fix | git-apply-able: NULL-check sbcreatecontrol at uipc_usrreq.c:694, ENOBUFS + unp_free + break | 409 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.
Confirmed kernel references
Detail
Exploit chain
none -- clean NULL deref (read of mh_data from a NULL mbuf), not memory corruption. Ceiling impact = reliable unprivileged local DoS (panic). Triggered by exhausting the plain 'mbuf' objcache (nmbufs~72904) via pinned AF_UNIX SOCK_DGRAM datagrams -- each costs 2 plain mbufs (MT_CONTROL + MT_SONAME) but only 1 pkthdr (MT_DATA), so the 2:1 plain:pkthdr ratio exhausts plain while leaving the pkthdr cache ~half full, letting the trigger's data mbuf allocate (M_WAITOK in sosend) so execution reaches sbcreatecontrol->NULL->panic. The PoC runs pinner+trigger concurrently to land on the exhaustion crossover (a naive pin-then-fire overshoots into a wedge).
Evidence (decisive lines)
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 trigger, run as maxx) kernel: type 12 trap, code=0 Stopped at unp_internalize.isra.12+0x11: movq 0x10(%rdi),%rbx db> (reproduced identically twice from fresh vm.sh resets; full excerpt in findings/poc/DF-0011/panic.txt)
PoC changes
The seeded nopasscred_panic.c had (a) a compile bug (#define N SOCK 4096 -> NSOCK undeclared) and (b) a flawed strategy that exhausted the pkthdr cache (datagram data) instead of the plain 'mbuf' cache sbcreatecontrol uses, so it only wedged the guest. Rewrote it to pin plain mbufs via SCM_CREDS-bearing datagrams at the 2:1 plain:pkthdr ratio and fire the no-control SO_PASSCRED trigger CONCURRENTLY during the ramp so it lands on the plain-cache-exhaustion crossover instead of overshooting into a wedge. Kept flood_trigger.c as an auxiliary hold-open variant. Added build.sh/run.sh, captured build.log/run.log/panic.txt/env.txt, wrote VERDICT.md + manifest.json, authored fix.diff (NULL-check sbcreatecontrol).
Verified recommended fix
fix.diff (git-apply-able, git apply --check verified) adds the missing NULL check at sys/kern/uipc_usrreq.c:694-699: after ncon = sbcreatecontrol(...), if (ncon == NULL) { error = ENOBUFS; unp_free(unp2); break; }. unp_free(unp2) is explicit because the break skips the normal SOCK_DGRAM path's unp_free(unp2) at :717; the function's release: epilogue already frees m and control and disposes control when error!=0, so no leak. This SUPERSEDES the finding markdown's ## Recommended fix (which used a bare break that would leak the unp2 reference) while preserving its intent. Full diff in findings/poc/DF-0011/fix.diff.
Verdict
REPRODUCED. The bug is real: in uipc_send (sys/kern/uipc_usrreq.c:694-699) the SOCK_DGRAM SO_PASSCRED synthesis path uses sbcreatecontrol()'s return value WITHOUT a NULL check; sbcreatecontrol (uipc_sockbuf.c:585-594) returns NULL when its m_getl(M_NOWAIT, MT_CONTROL) fails (plain 'mbuf' objcache exhausted), and unp_internalize(NULL) at uipc_usrreq.c:1706 does cm = mtod(control,...) = load of mh_data from a NULL mbuf, faulting at virtual address 0x10 (offsetof(struct m_hdr, mh_data) = mh_next[8]+mh_nextpkt[8] = 0x10; sys/sys/mbuf.h:79-90). Confirmed by two kernel panics from independent fresh vm.sh resets with the identical, mechanism-matching serial signature: 'objcache(mbuf) exhausted', 'Fatal trap 12: page fault while in kernel mode', 'fault virtual address = 0x10', 'Stopped at unp_internalize.isra.12+0x11: movq 0x10(%rdi),%rbx' (%rdi==0==NULL control), current process = the unprivileged trigger (nopasscred_panic run as maxx). The finding markdown guessed fault address 0x0; the real address is 0x10 (mh_data offset) -- same bug, sharper.