DF-2894 / verdict.json
{ "finding_id": "DF-2894", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "kenv_init() copies bootloader-supplied env strings of unbounded length into the dynamic kenv table (only the entry count is bounded), while kgetenv() strcpy()s the looked-up value into a fixed 258-byte kernel-stack buffer; an unprivileged kenv(2) KENV_GET on a boot-planted >=258-byte value smashed the kernel stack (Fatal trap 9, saved RBP=0x4141414141414141, fault at kgetenv's ret) and took the guest down. Planting requires loader.conf/boot-prompt control, so the demonstrated unpriv impact is panic; the smash itself is byte-controlled with no canary/SMEP/SMAP/KASLR, giving a credible boot-time-plant -> unpriv-detonate code-exec ceiling. Ingest clamp validated by in-guest kernel rebuild: entry dropped at boot, PoC returns ENOENT, guest healthy.", "exploit_chain": "1) privileged/console plant: audit.smash=\"<byte-controlled >=258-byte value>\" in /boot/loader.conf (data-only persistence); 2) kenv_init (SI_BOOT1_POST) ingests it unvalidated; 3) any unprivileged user: kenv(2) KENV_GET audit.smash -> kgetenv -> strcpy past buf[258] -> saved frame/return address overwritten with planter-chosen bytes (strcpy payload, NUL-free); 4) demonstrated: GPF panic at kgetenv ret; on this no-SMEP/SMAP/KASLR/no-canary guest, steps 3-4 are RIP control -> ROP -> uid=0 (chain start still requires the privileged plant, hence impact=panic).", "evidence": [ "run.log โ control GET returns (exit 0); trigger GET never returns (ssh hung)", "panic.txt โ 'Fatal trap 9 ... frame pointer = 0x4141414141414141' / 'Stopped at kgetenv.part.4+0xea: ret' / current process = unpriv trigger", "env.txt โ plant lines + stock-kernel identity (kernel #0, X86_64_GENERIC)", "fix_run.log โ fixed kernel #1: boot warning 'oversize entry (713 bytes)', table entry absent, trigger -> ENOENT exit 1, guest up", "fix_build.log โ make nativekernel KBUILD_DONE_OK; kernel CFLAGS show -fno-stack-protector", "fix.diff โ ingest clamp in kenv_init + strlen guard in kgetenv" ], "kernel_refs": [ "sys/kern/kern_environment.c:580-588", "sys/kern/kern_environment.c:231", "sys/kern/kern_environment.c:239", "sys/kern/kern_environment.c:135-136", "sys/kern/kern_environment.c:263-266", "sys/platform/pc64/x86_64/machdep.c:2669" ], "poc_changes": "seed sketch was never runnable: rebuilt as a plain kenv(3) userland trigger (libc exports kenv; implicit-decl warning is cosmetic); plant done via /boot/loader.conf + in-guest awk for the 700-byte value; /tmp is tmpfs so the binary was rebuilt after each reboot.", "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": 1150, "guest_dirty": 1, "build_cmd": "cc -O2 -o /tmp/df2894_trigger /tmp/df2894_trigger.c (in guest, as uid=1001)", "run_cmd": "kenv | grep -c '^audit.smash=' ; /tmp/df2894_trigger audit.hello ; /tmp/df2894_trigger audit.smash (in guest, as uid=1001, after loader.conf plant + reboot)", "code_hash": "ae62c631b68d0bb960dfa4257035d5663e6c7ea481ae582c6f54aa6a6599e7e6", "notes": "DUMP path clamp, ksetenv exact-fit math, kunsetenv compaction, alloc-before-lock/free-after-unlock, kfreeenv static->dynamic window (SYSINIT boot is single-threaded), and kgetenv_quad suffix parsing were all checked and killed. Guest left mid-validation on patched kernel; reset to with-src snapshot afterwards.", "recommended_fix": "kenv_init(): drop boot-env entries longer than KENV_MNAMELEN+KENV_MVALLEN (mirror ksetenv limits) with a boot warning; defense-in-depth strlen(cp)>=sizeof(buf) guard in kgetenv().", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Wed Sep 2 22:58:18 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Identical loader.conf plant + identical unpriv PoC on baseline kernel #0 -> GPF panic (RBP=0x41..), guest down; on fixed kernel #1 -> boot warning drops the 713-byte entry, KENV_GET returns ENOENT, control var unaffected, guest stays up.", "fix_evidence": [ "fix_run.log", "fix_build.log" ] } |