Heap OOB read in vfs_mountroot_try: strncpy(mf, cp, 96) leaves the parse buffer unterminated; ksscanf's strlen() and %80s copy adjacent heap bytes into devname, which kgetdiskbyname() prints to the console
| Field | Value |
|---|---|
| ID | DF-2862 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:P/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N |
| CWE | CWE-125 Out-of-bounds Read |
| File | sys/kern/vfs_conf.c |
| Lines | 421-431 (engine subr_scanf.c:123/:265-272/:373-383) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | kernleak |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
vfs_mountroot_try allocates mf = kmalloc(MFSNAMELEN+MNAMELEN) = 96
bytes and copies the candidate with strncpy(mf, cp, 96) β n ==
sizeof(buffer), so a candidate of β₯96 bytes (remainder of kenv
vfs.root.mountfrom, or a β₯96-char line at the mountroot> prompt) fills
all 96 bytes with no NUL. ksscanf then runs strlen(inp) past the end of
the heap allocation, and the %80s whitespace-skip and copy loop
walk/copy out-of-bounds heap bytes into devname, which reach the
console via kprintf("no disk named '%s'\n", name). Read-side sibling
of DF-0099's write-side overflow β different line, different primitive.
Threat model & preconditions
Boot-time kernel heap info disclosure to the console/boot log, plus garbage device lookups. Gated on loader.conf control (host-side) or physical console access; no runtime unprivileged reachability (static function, kenv read-only at runtime).
Proof of contest
VERIFIED on the stock guest (findings/poc/DF-2862/): (1) KLD replica
embedding the verbatim code against the kernel's own ksscanf/kmalloc
with the real M_MOUNT type β seeded class-96 chunk pool makes mf[96]
deterministic: first96_nonzero=1 devname[0]=51 devname=QQQQ...;
(2) real boot path with a 96+ byte vfs.root.mountfrom β stock boot
consumed all 96 in-bounds bytes and stopped on the OOB byte at mf[96]
(bootpath-baseline.log). Fix (segment-bounded bcopy + guaranteed
termination, shared with DF-2863) validated on a rebuilt kernel.
Recommended fix
See findings/poc/DF-2862/fix.diff (validated).
Timeline
- 2026-09-02 Discovered during pass-2 audit of vfs_conf.c (GLM 5.3); replica + real-boot reproduction + fix validation same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2862 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| replica.c | β | 6.7 KB | view raw | |
| Makefile | β | 52 B | β download | |
| build.sh | β | 159 B | view raw | |
| run.sh | β | 145 B | view raw | |
| build.log | β | 6.1 KB | view raw | |
| run.log | β | 1.8 KB | view raw | |
| run.2.log | β | 1.2 KB | view raw | |
| bootpath-baseline.log | β | 829 B | view raw | |
| bootpath-patched.log | β | 678 B | view raw | |
| env.txt | β | 534 B | view raw | |
| fix.diff | β | 571 B | view raw | |
| VERDICT.md | β | 4.3 KB | β raw | |
| verdict.json | β | 4.6 KB | view raw |
DF-2862 VERDICT
Status: reproduced (KLD replica with the kernel's own ksscanf/kmalloc
on the live stock kernel #0, plus the real boot path via loader.conf;
fix validated on a rebuilt kernel #1).
Impact: leak (kernel heap bytes adjacent to an M_MOUNT 96-byte
allocation read out-of-bounds and copied into devname; at boot those
bytes are printed to the console/serial log). Severity Low β the input
channel is the loader environment or the boot console, so this is
host-config / physical-console gated, not remotely or runtime reachable.
Root cause (path:line)
sys/kern/vfs_conf.c:421βmf = kmalloc(MFSNAMELEN+MNAMELEN, ...)= 96 bytes (MFSNAMELEN16,MNAMELEN80, sys/sys/mount.h:92-93).sys/kern/vfs_conf.c:427βstrncpy(mf, cp, MFSNAMELEN+MNAMELEN)withn == sizeof(mf): a candidate remainder >= 96 bytes fills the buffer completely and leaves it without a NUL terminator (the precedingbzeroat :426 is fully overwritten).sys/kern/vfs_conf.c:431βksscanf(mf, patt, ...)βsys/kern/subr_scanf.c:123inr = strlen(inp)βstrlenruns past the end of the 96-byte heap allocation.sys/kern/subr_scanf.c:265-272(leading-whitespace skip for%80s) and:373-383(copy loop) continue consulting/copying bytes past the allocation; copied bytes land indevname[80].- Disclosure sink:
sys/kern/vfs_conf.c:645kprintf("no disk named '%s'\n", name)viasetrootbyname(vfs_conf.c:447 β 658-672 β 630-651).
DF-0099 (known) covers the write off-by-one of %16[...]/%80s
against vfsname[16]/devname[80]; this finding is the read overrun
of mf β different line, different primitive.
How it was proven
- Replica (run.log, run.2.log): KLD embedding a verbatim copy of
vfs_conf.c:419-431, run at runtime against the kernel's own
ksscanf/kmalloc/M_MOUNT. Seeding the class-96 M_MOUNT chunk pool with 'Q' (chunks of exactly 96 bytes, packed back-to-back β kern_slaballoc.czoneindex()rounds <128 to 8-byte granularity) makes the byte atmf[96]deterministic. Result:first96_nonzero=1 devname[0]=51 devname='QQQQβ¦'β bytes from beyond the 96-byte allocation copied intodevname. Reproduced on 2/2 module loads (after the correct chunk class was seeded; the first groom attempt seeded class-128 and produced the silent variant, which is itself the same OOB read terminating on an adjacent NUL). - Real boot path (bootpath-baseline.log): loader.conf
vfs.root.mountfrom="ufs:da0s1a;ufs:<92 spaces>E;hammer2:vbd0s1d", stock kernel #0. Candidate 2's parse consumed all 96 non-NUL in-bounds bytes and stopped exactly atmf[96](read out of bounds; silent because the adjacent byte happened to be NUL β consistent with the replica's unseeded variant). The boot log proves the kenv string of= 96 bytes reaches the vulnerable copy at boot.
- Console disclosure sink: exercised at boot by candidate 1
(
no disk named 'da0s1a;ufs:'printed) β samekprintfthat would print OOB-sourceddevnamebytes whenever the adjacent heap byte is non-NUL/non-space.
Exploit chain
None (by design): boot-time parse, loader/console-gated. Ceiling is
disclosure of adjacent M_MOUNT-zone heap bytes to the boot console /
serial log, and parse misbehaviour (device lookups on garbage). No
runtime unprivileged reachability: vfs_mountroot_try is static, called
only from vfs_mountroot (SYSINIT) and vfs_mountroot_ask; kenv is
read-only at runtime (kern_environment.c:503, kern.environment
CTLFLAG_RD).
Fix validation
fix.diff: bound the copy at the ;-segment (ep) and force
termination β seglen = min(ep - cp, MFSNAMELEN+MNAMELEN - 1);
bcopy(cp, mf, seglen) into the pre-bzeroed buffer.
- baseline (kernel #0, stock): contamination + unterminated 96-byte copy (bootpath-baseline.log).
- patched (kernel #1
Wed Sep 2 13:46:57 UTC 2026, same loader.conf):no disk named 'da0s1a'β clean segment; candidate 2 parse bounded at 95 with guaranteed NUL; no out-of-bounds consult; machine boots normally via candidate 3 (bootpath-patched.log). - Replica A/B (BUGGY vs FIXED parse in the same module) shows devname sourced from OOB bytes vs '' cleanly (run.log B1 vs B2).
The fix also fixes DF-2863 (same hunk). Guest restored with
vm.sh reset with-src after validation.
Fix verification
fixedkernel #1 built in-guest with fix.diff (incremental quickkernel, RC=0), rebooted with the identical test loader.conf: baseline kernel #0 shows the unterminated 96-byte parse reading the boundary byte OOB (and candidate 1's contaminated devname), patched kernel #1 bounds the copy at 95 bytes with guaranteed termination β same input parses cleanly ('no disk named da0s1a' for DF-2863, silent bounded failure for the 96-byte candidate), machine boots normally via the last candidate
['bootpath-baseline.log (kernel #0) vs bootpath-patched.log (kernel #1), same vfs.root.mountfrom', 'run.log B1 (BUGGY parse, OOB bytes in devname) vs B2 (FIXED parse, devname empty, no OOB consult)']
Confirmed kernel references
Detail
Exploit chain
no escalation chain: input channel is the loader environment (host-side) or the boot console (physical); ceiling is disclosure of adjacent kernel heap bytes to the boot console/serial log plus garbage device-name lookups
Evidence (decisive lines)
["run.log: 'dfrep: B1-try: mf=0xfffff801177d69e0 first96_nonzero=1 devname[0]=51 devname=QQQQ...' β OOB bytes copied into devname via the kernel's own ksscanf", 'run.2.log: same result on a second module load', 'bootpath-baseline.log: stock-kernel boot with 96+-byte candidate in vfs.root.mountfrom β candidate parse runs to the buffer end and reads the boundary byte OOB', 'bootpath-patched.log: fixed kernel #1, same loader.conf β clean bounded parse', 'VERDICT.md: full path:line chain']
PoC changes
authored fresh (no seed): KLD replica embeds a verbatim copy of vfs_conf.c:419-431 and uses the kernel's own ksscanf/ksprintf/kmalloc with the real M_MOUNT type; heap groom seeds the class-96 M_MOUNT chunk pool (zoneindex() rounds <128 to 8-byte classes) so the byte at mf[96] is a deterministic 'Q'
Verified recommended fix
bound the copy at the ';'-segment and force NUL termination: seglen = min(ep - cp, MFSNAMELEN+MNAMELEN - 1); bcopy(cp, mf, seglen) (see fix.diff)
Verdict
vfs_mountroot_try() copies the root-mount candidate into a 96-byte M_MOUNT buffer with strncpy(mf, cp, MFSNAMELEN+MNAMELEN) where n == sizeof(mf), leaving mf unterminated when the kenv 'vfs.root.mountfrom' candidate (or a >=96-char line typed at the mountroot> console prompt) is 96+ bytes; ksscanf's strlen() and %80s whitespace-skip/copy then read past the allocation and can copy out-of-bounds heap bytes into devname, which kgetdiskbyname() prints to the console ('no disk named ...'). Proven on the live stock kernel with a KLD replica embedding the verbatim parse (seeded class-96 M_MOUNT chunks -> 'Q' marker bytes from beyond the allocation copied into devname), and on the real boot path via loader.conf (silent boundary-byte read at mf[96]). Low severity: boot-time only, loader/physical-console gated, kenv read-only at runtime.
No comments yet.