Integer underflow in sysctl_kern_msgbuf causes kernel heap OOB read via copyout
| Field | Value |
|---|---|
| ID | DF-0035 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:L |
| CWE | CWE-191 Integer Underflow; CWE-125 Out-of-bounds Read |
| File | sys/kern/subr_prf.c |
| Lines | 1177-1184 |
| Area | kern |
| Confidence | likely |
| Discovered | 2026-06-29 |
| Reported | pending |
Summary
In the third branch of sysctl_kern_msgbuf(), the copy length passed to
sysctl_handle_opaque is computed as n - rindex_modulo (mixing a byte count
n with a buffer offset rindex_modulo, both u_int) instead of n. This
branch is entered only when xindex_modulo == 0 (i.e. msg_bufx is an exact
multiple of msg_size), in which case n == msg_size - rindex_modulo, so
n - rindex_modulo == msg_size - 2*rindex_modulo, which underflows to ~4 GiB
whenever rindex_modulo > msg_size/2. The copyout (capped at the user's
oldlen) then reads from msg_ptr + rindex_modulo past msg_ptr + msg_size
into adjacent kernel heap, leaking kernel memory to userspace. With
unprivileged_read_msgbuf = 1 (the default), any local user can poll
kern.msgbuf to hit the window.
Root cause
sys/kern/subr_prf.c:1177-1184:
} else if (n <= mbp->msg_size - rindex_modulo) {
/* Can handle in one linear section. */
error = sysctl_handle_opaque(oidp,
mbp->msg_ptr + rindex_modulo,
n - rindex_modulo, /* BUG: should be n */
req);
}
Geometry of branch 3 (rindex_modulo > xindex_modulo and
n <= msg_size - rindex_modulo): the latter, combined with
n = (msg_size - rindex_modulo) + xindex_modulo, holds only when
xindex_modulo == 0, so n == msg_size - rindex_modulo. The valid linear
section from rindex_modulo is exactly n bytes; the code passes
n - rindex_modulo (= msg_size - 2*rindex_modulo), which underflows for
rindex_modulo > msg_size/2. Compare branch 1 (xindex_modulo - rindex_modulo,
a correct offset difference) and branch 4 (msg_size - rindex_modulo then n).
Threat model & preconditions
- Attacker position: any local unprivileged user (
unprivileged_read_msgbufdefault 1;ptr_restrictdefault 0 so kernel pointers in the msgbuf itself are also unmasked). - Privileges gained or impact: information disclosure. The OOB read leaks
kernel heap adjacent to the msgbuf allocation β may include kernel pointers
(KASLR bypass), credential/crypto structures, or other sensitive data. The
window is narrow:
msg_bufx(incremented once per character) crosses amsg_sizeboundary ~once per ~1 MiB of kernel log, while the reader lags past the buffer midpoint; winnable by sustainedsysctlpolling. - Required config or capabilities: none; default kernel.
- Reachability:
sysctl kern.msgbuf(orsysctlbyname) in a polling loop.
Proof of concept
PoC source: findings/poc/DF-0035/msgbuf_oob.c
Build & run (unprivileged, disposable VM)
cc -o msgbuf_oob findings/poc/DF-0035/msgbuf_oob.c ./msgbuf_oob
Expected output
Some reads return non-text / binary (adjacent heap) content beyond the valid
msgbuf. On a fixed kernel the length is always <= msg_size with valid text.
Impact
Unprivileged kernel-heap info leak (race-window-bounded). Medium (AC:H reflects the window; the leak itself can disclose kernel pointers/secret material).
Recommended fix
Pass n (the valid byte count) β matching branch 1/4's correct offset math:
--- a/sys/kern/subr_prf.c
+++ b/sys/kern/subr_prf.c
@@ -1180,7 +1180,7 @@
error = sysctl_handle_opaque(oidp,
mbp->msg_ptr + rindex_modulo,
- n - rindex_modulo,
+ n,
req);
References
sys/kern/subr_prf.c:1177-1184β the underflowingn - rindex_modulo.sys/kern/subr_prf.c:1164-1171,1185-1198β the correct length math in the other branches.- CWE-191 Integer Underflow; CWE-125 Out-of-bounds Read.
Timeline
- 2026-06-29 Discovered during automated file-by-file audit of
sys/kern/subr_prf.c. - pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0035 Β· 23 files| File | Type | Description | Size | |
|---|---|---|---|---|
| msgbuf_oob.c | trigger-source | original unprivileged kern.msgbuf poller (harmless) | 2.7 KB | view raw |
| msgbuf_diag.c | trigger-source | sharper unprivileged diagnostic with detailed reporting | 5.2 KB | view raw |
| dump_msgbuf.c | diagnostic | kvm(3)-based reader of msg_bufx/msg_bufr + branch-3 decision | 3.6 KB | view raw |
| msgbuf_oob_decisive.c | exploit-trigger | DECISIVE root-only trigger: kvm_write bad geometry + sysctl read -> panic on #0 | 6.7 KB | view raw |
| msgbuf_trigger.c | trigger-source | earlier timing-based natural-path trigger (superseded by decisive) | 6.1 KB | view raw |
| msgbuf_brute.c | trigger-source | root-only brute-forcer: 1-byte-step console writes + tight read loop | 2.3 KB | view raw |
| run_brute.sh | repro-script | wrapper: arrange stale msg_bufr via msgbuf_clear, then brute-force | 2.0 KB | view raw |
| build.sh | build-script | builds all five binaries | 1.7 KB | view raw |
| run.sh | run-script | run.sh unprivileged | run.sh decisive | 1.5 KB | view raw |
| panic.txt | panic-signature | tight panic signature from both decisive runs (proof) | 1.6 KB | view raw |
| baseline_panic.txt | panic-signature | fresh baseline panic on #0 unpatched (2026-07-02 this session) | 1.2 KB | view raw |
| leak_sample.txt | leak-sample | explanation of the panic signature and what it proves | 1.6 KB | view raw |
| run.unprivileged.log | run-log | 1.5M-read unprivileged poll on fresh boot: 0 hits | 335 B | view raw |
| env.txt | environment | uname, cc version, sysctls (unpatched #1 from prior session) | 779 B | view raw |
| fix.diff | suggested-fix | git-apply-able: n - rindex_modulo -> n in branch 3 (validated) | 293 B | view raw |
| fix_build.log | build-log | full nativekernel output for single-fix kernel (rc=0, 35602 lines) | 5.6 MB | β download |
| fix_run.log | run-log | decisive PoC on patched #1: returns l=n bytes, no panic | 411 B | view raw |
| fix_run_unpriv.log | run-log | unprivileged msgbuf_diag on patched #1: 0 OOB (sanity) | 745 B | view raw |
| VERDICT.md | verdict | full narrative: reproduced (root-only) + reachability + fix validation | 12.6 KB | β raw |
| README.md | readme | human-facing readme with reproduce instructions | 3.5 KB | β raw |
| manifest.json | manifest | this catalog | 5.0 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-0035 β PoC
sysctl_kern_msgbuf (sys/kern/subr_prf.c) 3rd branch passes the wrong
length to sysctl_handle_opaque. This evidence pack proves the bug is a
real kernel OOB read, AND clarifies that the unprivileged-reachability claim
in the finding is incorrect (the OOB window requires root to open).
The bug
} else if (n <= mbp->msg_size - rindex_modulo) {
error = sysctl_handle_opaque(oidp,
mbp->msg_ptr + rindex_modulo,
n - rindex_modulo, /* BUG: should be n */
req);
}
n is the valid byte count; subtracting rindex_modulo (a buffer offset)
silently wraps to ~4 GiB (both are u_int) whenever rindex_modulo > n.
The resulting copyout then reads past msg_ptr+msg_size into adjacent
kernel memory.
Reproduce
./build.sh
./run.sh unprivileged # as any user; 2M poll of kern.msgbuf. Expected: NO OOB
# (the bug is unreachable in normal operation).
# Exit 2 == "no leak observed" (the honest result).
# DECISIVE (root only; PANICS the kernel; use a disposable guest):
ssh dfbsd
cd /root/poc/DF-0035 && ./run.sh decisive
# -> kernel panic in std_copyout reading past msgbuf (proof of OOB read)
# -> reset the guest with `dfbsd-qemu/vm.sh reset` afterwards.
What you'll see
-
Unprivileged path: no leak. In normal operation
msg_bufrtracksmsg_bufx - msg_size + 2048, which forces branch 3 to fire only atxindex_modulo==0withrindex_modulo=2048. The buggyn - rindex_modulothen equalsmsg_size - 4096β a 2048-byte under-read, no OOB. Confirmed empirically: 2,000,000 sysctl reads as maxx, 0 hits. -
Decisive path (kvm_write-forced geometry): the kernel panics in
std_copyoutbecause the underflowed length (~4 GiB, clipped to oldlen) drives acopyoutthat walks off the end ofmsg_ptr's mapped pages into adjacent unmapped kernel memory. Trap 0xc (page fault), rip insidestd_copyout+0x15a. Reproduced twice; seepanic.txt. Had the adjacent memory been mapped, the same OOB read would have leaked heap residue instead of crashing.
Why unprivileged-only doesn't work
The OOB underflow condition (rindex_modulo > msg_size/2 in branch 3)
requires msg_bufr to be "stale" at a value whose modulo exceeds
msg_size/2. That staleness is produced only by
sysctl_kern_msgbuf_clear (subr_prf.c:1213-1218, root-only β verified:
sysctl kern.msgbuf_clear=1 as maxx β EPERM). Without root, the
steady-state geometry makes the bug a benign under-read. See VERDICT.md
for the full geometry trace.
Fix
fix.diff β one-line change: n - rindex_modulo β n, matching branches 1
and 4. Matches the finding's ## Recommended fix proposal exactly.
Fix validation (Phase 8)
The fix was validated on a single-fix kernel built from with-src + fix.diff:
- Baseline
#0(unpatched):./run.sh decisiveβ kernel panic (obj != NULLinvm_object_hold_shared, trap 0xc instd_copyout+0x15a). Seebaseline_panic.txt. - Single-fix
#1(with fix.diff):./run.sh decisive(identical bad geometry) βsysctl rc=0, returnedl=424272 = nbytes (exactly in-bounds), no panic, guest up. Deterministic across 3 runs. Seefix_run.log. - Normal unprivileged reads unaffected:
msgbuf_diagas maxx β 0 over-long reads (seefix_run_unpriv.log).
kern.version: #0: Thu Jul 2 06:02:54 UTC 2026 β #1: Thu Jul 2 14:31:15 UTC 2026.
Full build log: fix_build.log.
DF-0035 β Verification verdict
Finding: Integer underflow in sysctl_kern_msgbuf 3rd branch causes kernel
heap OOB read via copyout (sys/kern/subr_prf.c:1177-1184).
Verdict: REPRODUCED (with caveats) β the buggy length math is real and
produces a kernel OOB read; but the unprivileged-reachability claim in the
finding is INCORRECT. The OOB only fires after a root-initiated
kern.msgbuf_clear=1 opens a narrow (1-byte-per-msg_size) window. In normal
operation the same bug is a benign 2048-byte under-read with no leak.
1. The bug is real in source
sys/kern/subr_prf.c:1177-1184 (third branch of sysctl_kern_msgbuf):
} else if (n <= mbp->msg_size - rindex_modulo) {
/* Can handle in one linear section. */
error = sysctl_handle_opaque(oidp,
mbp->msg_ptr + rindex_modulo,
n - rindex_modulo, /* BUG: should be n */
req);
}
The valid data length here is n (as branches 1 and 4 correctly use); passing
n - rindex_modulo mixes a byte count with a buffer offset. Because both are
u_int, the subtraction silently wraps to a ~4 GiB value when
rindex_modulo > n. That huge length reaches sysctl_old_user
(sys/kern/kern_sysctl.c:1321), which clips it to req->oldlen and then
copyouts up to oldlen bytes from msg_ptr + rindex_modulo β a read that
runs past msg_ptr + msg_size into whatever kernel memory is adjacent.
2. Decisive empirical proof β kernel panic
msgbuf_oob_decisive.c (root-only) uses kvm_write to place msg_bufx and
msg_bufr in the exact geometry that the natural post-clear path produces
(msg_bufx = msg_size, msg_bufr = msg_size/2 + 100000 β so
xindex_modulo==0, rindex_modulo>msg_size/2), then issues a single
sysctlbyname("kern.msgbuf", buf, big_oldlen). The kernel panics:
panic: assertion "obj != NULL" failed in vm_object_hold_shared vm_fault -> trap_pfault -> trap -> calltrap --- trap 0xc, rip=ffffffff80bca5ba --- std_copyout() at std_copyout+0x15a
trap 0xc is a page fault raised inside the copyout source-side read
(walking off the msgbuf's mapped pages into adjacent unmapped kernel memory).
The panic was reproduced twice with byte-identical signatures (panic.txt).
Had the adjacent memory been mapped, the same OOB read would have leaked
kernel-heap residue to userspace instead of crashing. This is decisive proof
that the buggy branch-3 length math produces an OOB read.
The kvm_write does not change the bug execution β it only shortcuts the
state-setup that the natural path also produces (msgbuf_clear sets
msg_bufr := msg_bufx; subsequent logging then advances msg_bufx to the
next msg_size boundary). When the kernel runs sysctl_kern_msgbuf in that
state, branch 3 executes identically and the underflow happens.
3. Why the finding's threat model is wrong (unreachable from unprivileged)
msg_bufr is only ever modified in two places (sys/kern/subr_prf.c):
msgaddchar(line 1070): bumpsmsg_bufrtoxindex - msg_size + 2048only whenn = xindex - msg_bufr > msg_size - 1024. So in steady statemsg_bufr β msg_bufx - msg_size + 2048exactly, i.e.rindex_modulo = (msg_bufx + 2048) % msg_sizeandn = msg_size - 2048.sysctl_kern_msgbuf_clear(line 1214): setsmsg_bufr := msg_bufx(a write that requires root βkern.msgbuf_clearrejects non-wheel users; verified:sysctl kern.msgbuf_clear=1as maxx β EPERM).
In steady-state geometry, branch 3 fires only when xindex_modulo == 0
(see analysis in VERDICT.md Β§4). At that moment rindex_modulo = 2048 and
n = msg_size - 2048, so the buggy n - rindex_modulo = msg_size - 4096 β a
positive, in-bounds value. The bug becomes a 2048-byte under-read (the
returned msgbuf is 2048 bytes shorter than it should be), not an OOB read.
No leak, no panic.
The OOB underflow condition (rindex_modulo > n, equivalently
rindex_modulo > msg_size/2) requires msg_bufr to be "stale" β i.e. set to
a value whose modulo exceeds msg_size/2. That is only reachable after
root writes kern.msgbuf_clear=1 (which sets msg_bufr := msg_bufx). The
finding's claim that "any local user can poll kern.msgbuf to hit the window"
is false β the window requires root to open.
Empirical confirmation of unreachability
msgbuf_oob(original PoC) run as maxx: 2,000,000 sysctl reads, 0 hits.msgbuf_diag(sharper diagnostic) run as maxx on a fresh boot: 1,500,000 sysctl reads, 0 over-long reads, 0 suspect tails. Maximum returned length = 8573 bytes (= the actual boot-log size). Seerun.unprivileged.log.
4. Geometry trace (why steady state can't underflow)
In sysctl_kern_msgbuf (sys/kern/subr_prf.c:1119-1200) with no shrink
(n <= msg_size - 1024; the steady-state case where msg_bufr tracks
msg_bufx - msg_size + 2048):
rindex_modulo = (msg_bufx + 2048) % msg_sizexindex_modulo = msg_bufx % msg_sizen = msg_size - 2048
Branch decision:
- branch 1 (rindex_modulo < xindex_modulo): holds when
xindex_modulo >= msg_size - 2048 (the +2048 wraps).
- branch 2 (rindex_modulo == xindex_modulo): impossible (would need
2048 β‘ 0).
- branch 3 (rindex_modulo > xindex_modulo AND n <= msg_size - rindex_modulo):
the first clause holds when xindex_modulo < msg_size - 2048; combined
with n <= msg_size - rindex_modulo βΊ msg_size - 2048 <= msg_size - 2048 - xindex_modulo
βΊ xindex_modulo <= 0 βΊ xindex_modulo == 0 (and then
rindex_modulo = 2048).
- branch 4: otherwise (xindex_modulo in [1, msg_size - 2048)).
At the branch-3 moment in steady state:
- buggy length = n - rindex_modulo = (msg_size - 2048) - 2048 = msg_size - 4096
- correct length = n = msg_size - 2048
- difference = 2048 bytes UNDER-read
- copyout reads msg_size - 4096 bytes starting at msg_ptr + 2048, ending
at msg_ptr + msg_size - 2048. In bounds β no leak.
For an OOB read we need rindex_modulo > n. With branch-3's geometry that
requires rindex_modulo > msg_size - rindex_modulo βΊ rindex_modulo > msg_size/2.
But steady state pins rindex_modulo = 2048. Contradiction.
To get rindex_modulo > msg_size/2 in branch 3, msg_bufr must be stale at
a value whose modulo exceeds msg_size/2. That staleness is produced only
by sysctl_kern_msgbuf_clear (subr_prf.c:1213-1218), which is root-only.
5. Realistic severity
The bug is a real code defect (wrong length passed to sysctl_handle_opaque)
and produces a genuine OOB read when the geometry is right. But:
- The geometry requires root to open (
kern.msgbuf_clear=1). - Even after root opens it, the OOB window is exactly one
msg_bufxvalue wide permsg_size(~1 MiB) bytes of new kernel log output. Catching it from a polled sysctl read is a tight race; a 3,000,000-iter brute-force attempt with 1-byte-step console writes did not catch it within the time budget. (The kvm_write trigger above shortcuts this timing deterministically by writingmsg_bufxandmsg_bufrdirectly to the kernel struct.) - An attacker who already has root (to write
kern.msgbuf_clear) does not need an OOB read to escalate. The realistic impact ceiling of THIS bug alone is therefore local kernel info-leak / DoS only after root msgbuf_clear β not the unauthenticated/unprivileged leak claimed.
Suggested severity refinement: Low (rather than Medium) given the root-only window. The code fix is still warranted (it's a real defect that turns an otherwise-correct branch into a latent OOB).
6. The fix
Replace n - rindex_modulo with n in the 3rd branch β matching branches 1
and 4 which already use the correct offset math. One-line change; see
fix.diff (a standalone git apply-able unified diff against
sys/kern/subr_prf.c). This matches the finding's ## Recommended fix
proposal exactly.
7. Fix validation (Phase 8) β single-fix kernel built & booted
The fix was validated end-to-end on a single-fix kernel built from the
with-src baseline + fix.diff only.
Before / after contrast
| Kernel | Decisive PoC result (identical bad geometry) |
|---|---|
#0 unpatched (with-src) |
kernel PANIC: obj != NULL in vm_object_hold_shared, trap 0xc in std_copyout+0x15a (copyout walks off msg_ptr's mapped pages). Guest down. |
#1 + fix.diff |
sysctl rc=0, returned l=424272 bytes (= n, exactly in-bounds), no panic, guest stays up. Deterministic across 3 runs. |
The decisive PoC places msg_bufx = msg_size, msg_bufr = msg_size/2 + 100000
(so xindex_modulo==0, rindex_modulo>msg_size/2), then issues a single
sysctlbyname("kern.msgbuf", buf, 1MiB_oldlen). The geometry is the
underflowing one β the PoC prints bug_len=4294767296 (= n - rindex_modulo
as u_int, the OLD buggy length) to prove the same path is being exercised.
- On
#0that ~4 GiB length reachessysctl_old_userwhich clips it tooldlen(1 MiB) andcopyouts 1 MiB starting atmsg_ptr + rindex_modulo, running pastmsg_ptr + msg_sizeinto unmapped kernel memory β page fault β panic (saved inbaseline_panic.txt). - On
#1the fix passesn(= 424272) tosysctl_handle_opaque; the copyout reads exactlymsg_ptr + rindex_modulo .. msg_ptr + msg_sizeβ in bounds. No fault, no leak (fix_run.log).
Normal unprivileged reads are unaffected: as maxx, 50000 kern.msgbuf reads
returned the natural boot-log length (8735 bytes), 0 over-long, 0 suspect
tails (fix_run_unpriv.log).
Build / boot details
fix.diffapplied withpatch -p1 --forwardcleanly (Hunk #1 at line 1180).- Build:
cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_GENERICβrc=0(full log infix_build.log, 35602 lines). - Install:
cp .../kernel.stripped /boot/kernel/kernel && cp .../kernel.debug /boot/kernel/kernel.debug && sync. Loader boots the barekernelname. kern.version:#0: Thu Jul 2 06:02:54 UTC 2026β#1: Thu Jul 2 14:31:15 UTC 2026.- Patched kernel sha256:
5a7d8e6a57ca3789960978af5d70c65f2828aff7e02e1a6c76cff69748a3a3f6.
Fix status
fixed. The bad-behavior marker (panic / OOB read past msg_size) is present
on the unpatched #0 baseline and absent on the single-fix #1 kernel.
The one-line change (n - rindex_modulo β n) matches the finding's
## Recommended fix proposal and the prior-session fix.diff exactly; no
refinement was needed.
8. Files in this evidence pack
| file | role |
|---|---|
msgbuf_oob.c |
original unprivileged PoC (polls kern.msgbuf; harmless) |
msgbuf_diag.c |
sharper unprivileged diagnostic (reports over-long/suspect reads) |
dump_msgbuf.c |
kvm(3) reader: dumps msg_bufx/bufr and the branch-3 decision |
msgbuf_oob_decisive.c |
DECISIVE root-only trigger: kvm_write bad geometry + sysctl read β panic |
msgbuf_trigger.c |
earlier natural-path trigger attempt (timing-based; superseded) |
msgbuf_brute.c |
root-only natural-path brute-forcer (1-byte-step + read loop) |
run_brute.sh |
wrapper: arrange stale msg_bufr via msgbuf_clear, then brute-force |
build.sh |
builds all five binaries |
run.sh |
runs unprivileged (./run.sh unprivileged) or decisive (./run.sh decisive) |
panic.txt |
tight panic signature from both decisive runs (proof) |
leak_sample.txt |
explanation of the panic signature and what it proves |
run.unprivileged.log |
full unprivileged poll log (1.5M reads, 0 hits β fresh boot) |
env.txt |
guest uname, cc version, relevant sysctls |
fix.diff |
git-apply-able fix: n - rindex_modulo β n in branch 3 |
baseline_panic.txt |
fresh baseline panic on #0 (this session, 2026-07-02) |
fix_build.log |
full nativekernel output for the single-fix kernel (rc=0) |
fix_run.log |
decisive PoC on patched #1: returns n bytes, no panic |
fix_run_unpriv.log |
unprivileged msgbuf_diag on patched #1: 0 OOB (sanity) |
README.md |
(updated) human-facing readme |
manifest.json |
machine-readable catalog |
Fix verification
fixedVALIDATED the fix. fix.diff applied cleanly (patch -p1, Hunk #1 at 1180) and compiled (make -j6 nativekernel, NK_DONE rc=0). The decisive PoC -- identical bad geometry (bug_len=4294767296 underflow) -- PANICS the unpatched #0 with-src baseline ('obj != NULL' in vm_object_hold_shared, trap 0xc in std_copyout+0x15a) but on the single-fix #1 kernel returns exactly l=n=424272 bytes (in-bounds), rc=0, no panic, guest stays up. Deterministic across 3 runs; unprivileged reads unaffected (8735-byte natural length, 0 OOB). The one-line change closes the bug. Guest reset to clean with-src #0 at end.
baseline #0 decisive: 'verify ... n=424272, bug_len=4294767296 (underflow!)' -> PANIC 'panic: assertion "obj != NULL" failed in vm_object_hold_shared' trap 0xc rip=std_copyout+0x15a (guest down). | patched #1 decisive: 'verify ... n=424272, bug_len=4294767296 (underflow!)' -> 'sysctl rc=0, returned length l=424272 (msg_size=1048544)' 'NOTE: read returned 424272 bytes (< msg_size) -- bug is benign here.' (guest up, x3). | nativekernel build: '=== NK_DONE rc=0 === Thu Jul 2 14:34:41 UTC 2026'.
Confirmed kernel references
Detail
Exploit chain
Read primitive only (OOB heap read), not a write/corruption primitive, so no escalation chain. The underflowed length (~4.095 billion as u_int) reaches sysctl_old_user (kern_sysctl.c:1321) which clips it to req->oldlen and copyouts up to oldlen bytes from msg_ptr+rindex_modulo, reading past msg_ptr+msg_size into adjacent kernel heap (potential KASLR/cred-pointer leak). On this guest the adjacent page is unmapped so it panics rather than leaks; had it been mapped the same path would leak heap residue. Window is root-only (msgbuf_clear) and ~1 msg_bufx value wide per msg_size (~1MiB) of new log, so a polled natural trigger is a tight race. No uid0 achievable from this primitive.
Evidence (decisive lines)
BASELINE #0: msgbuf_oob_decisive sets bufx=1048544(Vm=0) bufr=624272(>msg_size/2=524272), verify n=424272 bug_len=4294767296(underflow) -> kernel PANIC, guest down. boot.log: 'panic: assertion "obj != NULL" failed in vm_object_hold_shared' / 'trap 0xc, rip=ffffffff80bcaeaa' / 'std_copyout() at std_copyout+0x15a'. PATCHED #1 (same geometry): verify bufx=1048544 bufr=624272 n=424272 bug_len=4294767296(underflow!) / 'sysctl rc=0, returned length l=424272 (msg_size=1048544)' / 'NOTE: read returned 424272 bytes (< msg_size) -- bug is benign here.' / restored, guest up. Deterministic x3 runs. Unprivileged msgbuf_diag on #1: 50000 reads, max length 8735, 0 over-long, 0 suspect.
PoC changes
No PoC source or fix.diff changes this session -- the prior-session sources and fix.diff were already correct. Added fix-validation artifacts to findings/poc/DF-0035/: baseline_panic.txt (fresh #0 panic), fix_build.log (full nativekernel output, rc=0), fix_run.log (decisive PoC on patched #1), fix_run_unpriv.log (unprivileged sanity on #1), and extended VERDICT.md section 7 + README.md 'Fix validation' + manifest.json with fix_status/contrast.
Verified recommended fix
In sys/kern/subr_prf.c:1183 (3rd branch of sysctl_kern_msgbuf) change the length passed to sysctl_handle_opaque from 'n - rindex_modulo' to 'n' -- n is the valid byte count (as branches 1 and 4 already correctly use); the subtraction mixes a byte count with a buffer offset and silently underflows (u_int) when rindex_modulo > n. Matches the finding's '## Recommended fix' proposal exactly; validated on a built single-fix kernel. Full git-apply-able diff in findings/poc/DF-0035/fix.diff.
Verdict
REPRODUCED (root-only) + FIX VALIDATED. The 3rd branch of sysctl_kern_msgbuf (sys/kern/subr_prf.c:1183) passes n - rindex_modulo (u_int) to sysctl_handle_opaque; when rindex_modulo > n this underflows to ~4 GiB, driving a copyout past msg_ptr+msg_size into adjacent kernel memory. The decisive PoC uses kvm_write to place msg_bufx=msg_size, msg_bufr=msg_size/2+100000 (xindex_modulo==0, rindex_modulo>msg_size/2) then reads kern.msgbuf with a 1MiB oldlen. On the unpatched #0 with-src baseline this PANICS: 'obj != NULL' assertion in vm_object_hold_shared, trap 0xc (page fault) in std_copyout+0x15a (copyout walks off msg_ptr's mapped pages) -- confirmed fresh this session (baseline_panic.txt), identical mechanism to the prior session. The finding's unprivileged-reachability claim is INCORRECT: the OOB geometry requires msg_bufr to be stale, which only root's kern.msgbuf_clear=1 produces (subr_prf.c:1214); 1.5M+ unprivileged sysctl reads produce 0 hits (run.unprivileged.log). Realistic impact ceiling = local kernel heap info-leak/panic after root msgbuf_clear, not unauthenticated leak.
No comments yet.