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

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_msgbuf default 1; ptr_restrict default 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 a msg_size boundary ~once per ~1 MiB of kernel log, while the reader lags past the buffer midpoint; winnable by sustained sysctl polling.
  • Required config or capabilities: none; default kernel.
  • Reachability: sysctl kern.msgbuf (or sysctlbyname) 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).

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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0035 Β· 23 files
FileTypeDescriptionSize
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
README.md readme human-facing readme with reproduce instructions
↓ download 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

At sys/kern/subr_prf.c:1183:

} 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_bufr tracks msg_bufx - msg_size + 2048, which forces branch 3 to fire only at xindex_modulo==0 with rindex_modulo=2048. The buggy n - rindex_modulo then equals msg_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_copyout because the underflowed length (~4 GiB, clipped to oldlen) drives a copyout that walks off the end of msg_ptr's mapped pages into adjacent unmapped kernel memory. Trap 0xc (page fault), rip inside std_copyout+0x15a. Reproduced twice; see panic.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 != NULL in vm_object_hold_shared, trap 0xc in std_copyout+0x15a). See baseline_panic.txt.
  • Single-fix #1 (with fix.diff): ./run.sh decisive (identical bad geometry) β†’ sysctl rc=0, returned l=424272 = n bytes (exactly in-bounds), no panic, guest up. Deterministic across 3 runs. See fix_run.log.
  • Normal unprivileged reads unaffected: msgbuf_diag as maxx β†’ 0 over-long reads (see fix_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.

VERDICT.md verdict full narrative: reproduced (root-only) + reachability + fix validation
↓ download raw

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): bumps msg_bufr to xindex - msg_size + 2048 only when n = xindex - msg_bufr > msg_size - 1024. So in steady state msg_bufr β‰ˆ msg_bufx - msg_size + 2048 exactly, i.e. rindex_modulo = (msg_bufx + 2048) % msg_size and n = msg_size - 2048.
  • sysctl_kern_msgbuf_clear (line 1214): sets msg_bufr := msg_bufx (a write that requires root β€” kern.msgbuf_clear rejects non-wheel users; verified: sysctl kern.msgbuf_clear=1 as 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). See run.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_size
  • xindex_modulo = msg_bufx % msg_size
  • n = 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_bufx value wide per msg_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 writing msg_bufx and msg_bufr directly 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 #0 that ~4 GiB length reaches sysctl_old_user which clips it to oldlen (1 MiB) and copyouts 1 MiB starting at msg_ptr + rindex_modulo, running past msg_ptr + msg_size into unmapped kernel memory β†’ page fault β†’ panic (saved in baseline_panic.txt).
  • On #1 the fix passes n (= 424272) to sysctl_handle_opaque; the copyout reads exactly msg_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.diff applied with patch -p1 --forward cleanly (Hunk #1 at line 1180).
  • Build: cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_GENERIC β†’ rc=0 (full log in fix_build.log, 35602 lines).
  • Install: cp .../kernel.stripped /boot/kernel/kernel && cp .../kernel.debug /boot/kernel/kernel.debug && sync. Loader boots the bare kernel name.
  • 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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED 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'.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Thu Jul 2 14:31:15 UTC 2026 (sha256 5a7d8e6a57ca3789960978af5d70c65f2828aff7e02e1a6c76cff69748a3a3f6)

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.