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

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2862 Β· 13 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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)

  1. sys/kern/vfs_conf.c:421 β€” mf = kmalloc(MFSNAMELEN+MNAMELEN, ...) = 96 bytes (MFSNAMELEN 16, MNAMELEN 80, sys/sys/mount.h:92-93).
  2. sys/kern/vfs_conf.c:427 β€” strncpy(mf, cp, MFSNAMELEN+MNAMELEN) with n == sizeof(mf): a candidate remainder >= 96 bytes fills the buffer completely and leaves it without a NUL terminator (the preceding bzero at :426 is fully overwritten).
  3. sys/kern/vfs_conf.c:431 β€” ksscanf(mf, patt, ...) β†’ sys/kern/subr_scanf.c:123 inr = strlen(inp) β€” strlen runs past the end of the 96-byte heap allocation.
  4. 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 in devname[80].
  5. Disclosure sink: sys/kern/vfs_conf.c:645 kprintf("no disk named '%s'\n", name) via setrootbyname (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.c zoneindex() rounds <128 to 8-byte granularity) makes the byte at mf[96] deterministic. Result: first96_nonzero=1 devname[0]=51 devname='QQQQ…' β€” bytes from beyond the 96-byte allocation copied into devname. 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 at mf[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) β€” same kprintf that would print OOB-sourced devname bytes 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

fixed
baseline reproduced→ patch + rebuild →patched clean

kernel #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)']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Wed Sep 2 13:46:57 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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.