DF-2862 / verdict.json
{ "finding_id": "DF-2862", "status": "reproduced", "reproduced": 1, "impact": "leak", "confidence": "certain", "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.", "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": [ "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" ], "kernel_refs": [ "sys/kern/vfs_conf.c:421", "sys/kern/vfs_conf.c:427", "sys/kern/vfs_conf.c:431", "sys/kern/subr_scanf.c:123", "sys/kern/subr_scanf.c:265", "sys/kern/subr_scanf.c:373", "sys/kern/vfs_conf.c:645", "sys/kern/vfs_conf.c:544", "sys/kern/vfs_conf.c:558" ], "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'", "attempts": 3, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 3000, "guest_dirty": 0, "build_cmd": "cd /root/dfrep && make (KLD; and for fix validation: cd /usr/src && make -j4 quickkernel KERNCONF=X86_64_GENERIC && make installkernel KERNCONF=X86_64_GENERIC)", "run_cmd": "kldload /root/dfrep/dfrep.ko && dmesg | tail -30 (boot-path run: test vfs.root.mountfrom in /boot/loader.conf + reboot)", "code_hash": "cd7d001f8c11665b10f68b6a5fd0a4acf33d2816b2a20ec91adadfaf7a5753ca", "notes": "distinct from known DF-0099 (ksscanf width off-by-one NUL WRITE into vfsname/devname); this is the missing-termination READ overrun of mf. Per orchestrator guidance for mountroot/boot classes, verification = source-proven chain + runtime KLD replica on the kernel's own primitives + one boot-path run with loader.conf + patched-kernel validation; guest reset with-src afterwards. Boot-path run also demonstrated the console sink (candidate 1's contaminated devname printed verbatim).", "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)", "fix_status": "fixed", "fix_kernel_uname": "DragonFly 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", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "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", "fix_evidence": [ "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)" ] } |