DF-5779 / verdict.json
{ "finding_id": "DF-5779", "status": "reproduced", "reproduced": true, "impact": "panic", "confidence": "certain", "verdict": "The seq_puts() shim (sys/dev/drm/include/linux/seq_file.h:44) places its DATA argument in ksnprintfs format parameter. Proven three ways on the live guest: (1) shipped /boot/kernel/drm.ko disassembly of __drm_puts_seq_file shows mov %rsi,%rdx putting str into ksnprintfs fmt register; (2) a kmod replicating the exact macro shows %d inside data interpreted against a never-passed vararg (slot value leaked into the seq buffer) while the Linux contract requires verbatim output; (3) data containing %s walks unset vararg slots and strlen() faults on a wild pointer -> Fatal trap 12, guest down (panic.txt). The kernel printf cores %n is a live write-through-pointer conversion (subr_prf.c:688-701), demonstrated controlled (wrote 2). LATENT in-tree today: CONFIG_DEBUG_FS is undefined, drm_debugfs.c is not built, no struct seq_file is ever instantiated, and all current call sites pass literals \u2014 so the finding is filed Low (hardening/contract divergence), but any ported caller passing a dynamic string (EDID/connector-derived bytes, as upstream drivers do) silently acquires a kernel format-string primitive with %n write and %s wild-read. DF-2224 (seq_printf va_list forwarding) known and not re-reported.", "exploit_chain": "Simulated-caller chain (proven): dynamic string reaches seq_puts(m, s) -> ksnprintf(buf, size, s-as-fmt) -> kvcprintf interprets s: %d/%x/%p leak unset vararg slots into the seq buffer (rig B: link=0); %n writes retval through a wild pointer (conversion live, rig A: wrote 2 to &written); %s strlen()s a wild pointer -> kernel page fault + panic (rig C, 0x1a640). In-tree activation requires a future port passing attacker-influenced data (e.g. EDID strings) to seq_puts/drm_puts via drm_seq_file_printer; today that dispatch (debugfs) does not exist on DragonFly, so no unprivileged-user-to-kernel path is currently wired.", "evidence": "disasm.txt (mov %rsi,%rdx -> ksnprintf fmt slot in shipped drm.ko); run.log (rig A %n wrote 2; rig B link=0); run_c.log + panic.txt (Fatal trap 12, Stopped at strlen, guest down); fix_run.log (fixed shim: same inputs verbatim, guest alive); env.txt (uname, md5 match of guest /usr/src vs audited tree, no CONFIG_DEBUG_FS)", "kernel_refs": [ "sys/dev/drm/include/linux/seq_file.h:44", "sys/dev/drm/linux_printf.c:32", "sys/dev/drm/drm_print.c:128", "sys/kern/subr_prf.c:688", "sys/kern/subr_prf.c:750", "sys/dev/drm/drm_internal.h:139" ], "poc_changes": "New pass-2 finding (no seed sketch). Built the rig set from scratch; first transfer attempt produced empty object files (vm.sh run_root consumes ssh stdin for the command itself) \u2014 re-transferred via base64-in-command; widened rig C to nine %s so the va_arg walk leaves the register-save area and reaches stack slots, making the wild read deterministic instead of a lucky (null).", "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": 1560, "guest_dirty": 0, "build_cmd": "in-guest: cd /tmp/rig && make SYSDIR=/usr/src/sys WERROR= && make -f Makefile.c SYSDIR=/usr/src/sys WERROR= (host wrapper: findings/poc/DF-5779/build.sh)", "run_cmd": "kldload /tmp/rig/seqputs_rig.ko; dmesg | tail (A+B) ; kldload /tmp/rig/seqputs_rigc.ko (panic) ; fix: kldload /tmp/rigfix/seqputs_rigfix.ko", "code_hash": "sha256:a821025698929a8c62a9efafc69cea1ccd114cc97c92de9d8b8cba7957d4d518 (seqputs_rigc.c, panic trigger); rig A/B: 624cb57bc3fba6ee56a6cbb4457d30d2a6e7aa97d74f9920c0503ecb412d03b3; fix rig: 0fdbaf7cb62562a63b3d5d4eec623bc3f33ce914532044c5e5621d95f58b434f", "notes": "Severity filed Low (latent): no in-tree reachable caller \u2014 CONFIG_DEBUG_FS undefined, drm_debugfs.c absent from sys/conf/files, struct seq_file never instantiated, all current seq_puts/seq_printf args literal. Class: kernel format-string trap for ported code (CWE-134); %n write primitive live in subr_prf.c:688. Negative results recorded in the audit JSON: ksnprintf truncation is safe (snprintf_func remain>=2, subr_prf.c:493-502); m->buf==NULL/size==0 path writes nothing; %pV prints as pointer+literal V (no corruption); %*ph arg counts align; seq_printfs overwrite-not-append (no m->count) is correctness-only and unreachable. DF-2224 not re-reported.", "recommended_fix": "Replace the seq_puts macro with an inline function that passes the data string through \"%s\": static inline void seq_puts(struct seq_file *m, const char *str) { ksnprintf(m->buf, m->size, \"%s\", str); } (fix.diff in this pack)", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Fixed shim compiled into seqputs_rigfix.ko and replayed on the live guest: the exact rig-C string that panicked the baseline kernel now prints verbatim, %d/%n/%p inside data remain literal, guest stays up, module unloads cleanly (fix_run.log). The fix unit is a header macro; a consumer compiled against the exact fixed macro is semantically equivalent to rebuilding every drm kmod. Baseline bad behavior (interpretation, slot leak, wild-read panic) is GONE with the fix.", "fix_evidence": "findings/poc/DF-5779/fix_run.log (rigfix B/C/N all verbatim, DONE verbatim, guest alive, UNLOADED_CLEAN); baseline in run.log/run_c.log/panic.txt; diff in fix.diff" } |