DF-2739 / verdict.json
{ "finding_id": "DF-2739", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "REPRODUCED end-to-end on the stock INVARIANTS guest with pure syscalls (kldload(2)/kldunload(2)/kldstat(2)) and two purpose-built KLDs. linker_file_unload()'s module-veto path (sys/kern/kern_linker.c:517-518) drops the caller's reference even though the unload failed, leaving a live linker_file with refs==0 (directly observable via kldstat: 'vetoa.ko refs=0' while loaded). A subsequent MODULE_DEPEND load bumps refs 0->1 (kern_linker.c:1636); the original user's kldunload then takes the full-teardown path and frees the file while the dependent's deps[] still references it. Unloading the dependent recurses into linker_file_unload(freed) (kern_linker.c:544-545) -> use-after-free: read of freed->refs, TAILQ_REMOVE on freed memory, indirect call via freed->ops. Guest panicked with 'Bad link elm ... next->prev != elm' and a backtrace showing the nested linker_file_unload frames. Primitive: deterministic UAF read+write+indirect-call on an M_LINKER chunk (~200B, groomable via the linker's own allocations). uid=0 escalation is NOT reachable: both kldload(2) and kldunload(2) are gated by caps_priv_check_self(SYSCAP_NOKLD) (kern_linker.c:794,:841) โ the hard blocker is privilege, verified by tracing every linker_file_unload caller. Fix validated on a rebuilt kernel (make nativekernel): with the erroneous refs-- removed, refs never underflows (1->2->1), vetoa survives the dependent, dependent unload is clean, no panic, guest stays up.", "exploit_chain": "root: kldload vetoa.ko (refs=1,userrefs=1) -> kern.vetoa_veto=1 -> kldunload (module vetoes; kernel refs-- -> refs=0, file live) -> kldload depb.ko (MODULE_DEPEND vetoa; refs 0->1) -> veto off -> kldunload vetoa (refs==1 -> FULL TEARDOWN under live dependent; depb->deps[0] dangles) -> kldunload depb -> linker_file_unload(freed vetoa) -> TAILQ_REMOVE on freed chunk -> panic (INVARIANTS) / silent heap corruption + controllable ops->unload indirect call on production builds. Privilege gate: SYSCAP_NOKLD (root-only trigger).", "evidence": [ "run.log โ baseline: refs=0 after vetoed unload, premature teardown at step 4, hang/panic at step 5 (ssh timed out as guest died)", "panic.txt โ 'panic: Bad link elm 0xfffff8008bb64280 next->prev != elm' + nested linker_file_unload frames from sys_kldunload", "run.fixed.log โ patched kernel: refs stays 1 after veto, refs=2 after dep load, vetoa survives step-4 unload (refs=1), clean depb unload, guest up", "build.log / build2 kernel install in VERDICT.md", "fix.diff โ one-line removal of the veto-path refs--" ], "kernel_refs": [ "sys/kern/kern_linker.c:514", "sys/kern/kern_linker.c:517", "sys/kern/kern_linker.c:518", "sys/kern/kern_linker.c:494", "sys/kern/kern_linker.c:540", "sys/kern/kern_linker.c:544", "sys/kern/kern_linker.c:556", "sys/kern/kern_linker.c:1636", "sys/kern/kern_linker.c:841", "sys/kern/kern_linker.c:853" ], "poc_changes": "Seed had no runnable PoC. Authored: vetoa.ko (module whose MOD_UNLOAD returns EBUSY while sysctl kern.vetoa_veto=1), depb.ko (MODULE_DEPEND on vetoa), poc.c (syscall orchestration + kldstat refs observability). Fixed the bug-marker heuristic after fix validation (key on kldfind ENOENT, not on kldunload success).", "attempts": 2, "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": 130, "guest_dirty": 0, "build_cmd": "cd /tmp/kldtest && csh build.sh (vetoa_mod+depb_mod via bsd.kmod.mk, cc -O -o poc poc.c)", "run_cmd": "cd /tmp/kldtest && ./poc", "code_hash": "sha256(poc.c)=e537385cc78bcf71646172bbf8196f0b2de7af861f90afba04218cfd5450692b vetoa.c=4d0ceecb162513a85c995db160833f733e740dcc6c05678a68df756535905f45 depb.c=d7302dd02d3a362a3e118d009d7032e86180afacec03a8d7d4883a907160d55 fix.diff=9b02abc76b0b7208534072a9a01f0b433a95b04a5047f538b4b813bd59d07054", "notes": "Root-only trigger (SYSCAP_NOKLD) โ kernel-robustness bug, not an unprivileged escalation. On non-INVARIANTS builds the UAF is silent corruption; the freed chunk is M_LINKER (same zone as link_elf elf_file/symtab allocations) so grooming is trivial for the privileged attacker. runtime_sec=130 is the wall time of the decisive baseline run (ssh killed at 120s while the guest panicked in step 5); the panic itself occurred ~25s into the PoC.", "recommended_fix": "Remove the file->refs-- on the module-veto path of linker_file_unload(): the caller's reference must survive a failed unload (fix.diff, validated).", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Mon Aug 31 12:46:55 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff applied to guest /usr/src, kernel rebuilt (make -j6 nativekernel KERNCONF=X86_64_GENERIC) and installed, guest rebooted into #1. Exact same PoC re-run: step-2 EBUSY leaves refs=1 (no underflow), step-3 refs=2, step-4 kldunload takes the refs>1 fast path and vetoa STAYS loaded (kldstat refs=1), step-5 dependent unload is clean. No panic, no corruption, guest remains up. Bad behavior eliminated.", "fix_evidence": [ "run.fixed.log (vetoa.ko refs sequence 1 -> 1 -> 2 -> 1, 'poc exit: 0', kldstat tail shows both modules loaded and unloadable)", "VERDICT.md 'Fix validation' section" ] } |