DF-2826 / verdict.json
{ "finding_id": "DF-2826", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "elf_getfiles() calls fp_close(fp) (== fdrop) on the checkpoint file when fdalloc() fails, dropping a reference owned by sys_sys_checkpoint()'s holdfp(). Confirmed twice on the stock kernel: with the checkpoint fd >= 3 the premature fo_close/vrele/ffree kills the file and its vnode mid-restore and elf_loadphdrs() (operating on the freed struct file) panics in vref() via vnode_pager_reference ('panic: vref: bad refcnt 00000000 1'); with the fd < 3 the stolen reference becomes a phantom per-thread fdcache reference, the syscall reports success (error is never set), and the process exit panics with 'fdrop: invalid f_count 0' (fdrop<-closef<-fdfree<-exit1<-sigexit). The same path also leaks tempfp. The underlying primitive is a struct-file refcount underflow (dangling fd table entry once the phantom cache ref is evicted); the in-process reclamation/exploitation stage (implemented in stage2.c) could not execute because the restored program reliably SIGBUSes at its first instruction fetch - an apparently independent defect in the ckpt restore path (documented in VERDICT.md) - so escalation stopped at demonstrated lifetime corruption + deterministic panics. Gate: kern.ckptgroup defaults to wheel; non-wheel user verified to receive EPERM.", "exploit_chain": "crafted ckpt image (valid notes/vminfo/siginfo + one normal ckpt_fileinfo with valid fhandle and cfi_index=0x7fffffff) -> sys_checkpoint(CKPT_THAW, fd, -1, 0) -> elf_getfiles: fhtovp+fp_vpopen succeed, fdalloc fails (want >= lim) -> fp_close(fp) steals holdfp's reference -> [fd>=3] file+vnode die mid-syscall; elf_loadphdrs UAF-reads the freed fp and panics in vref -> [fd<3] dropfp returns a phantom ref to the thread fdcache; CKPT_THAW returns 0 (success-masking); process exits -> fdfree/closef -> fdrop on zero-count file -> panic. Full uid=0 chain (evict phantom -> falloc() reclaims freed M_FILE chunk -> fd aliases foreign file -> premature-free a live victim) is implemented in stage2.c but blocked from executing by the restored-context SIGBUS anomaly.", "evidence": [ "panic.txt: 'panic: vref: bad refcnt 00000000 1' with trace vref<-vnode_pager_reference<-vm_mmap<-fp_mmap<-mmap_phdr (fd>=3 variant)", "panic_fd0.txt: 'panic: fdrop: invalid f_count 0' with trace fdrop<-closef<-fdfree<-exit1<-sigexit (fd<3 variant)", "baseline_fresh_vref_panic.txt: same vref panic reproduced from a fresh with-src snapshot boot", "fix_run.log: patched kernel (#1 Sep 1 20:00:55) - both variants: no kernel panic, guest stays up", "VERDICT.md: full root-cause accounting incl. fdcache modes and the error==0 success mask", "gen.c/stage2.c/trigger_fd3.c/trigger_fd0.c/build.sh/run.sh: reproduction sources" ], "kernel_refs": [ "sys/kern/kern_checkpoint.c:664-668", "sys/kern/kern_checkpoint.c:611-612", "sys/kern/kern_checkpoint.c:756-762", "sys/kern/kern_checkpoint.c:258", "sys/kern/kern_fp.c:559-562", "sys/kern/kern_descrip.c:510-560", "sys/kern/kern_descrip.c:2186-2200" ], "poc_changes": "Seed sketch rebuilt entirely: image generator uses guest headers (_KERNEL_STRUCTURES) for exact kernel struct sizes; fixed note-stream file placement (fseek to reserved header region), page-congruent stack p_offset, correct _start entry offset (0x600000f0) taken from nm; triggers print errno; image must be generated in the same boot as the run (HAMMER2 fsid in the embedded fhandle is boot-dependent).", "attempts": 6, "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": 5400, "guest_dirty": 1, "build_cmd": "sh build.sh (in guest /root/df2826; cc 8.3)", "run_cmd": "./trigger_fd3 | ./gen df2826.ckpt stage2.bin normal 0x600000f0 1 && ./trigger_fd0 | ./trigger_fd3 df2827.ckpt", "code_hash": "97b2db55c59c2d7ec05e4893891f1e219b55d43b3e5a2beda7e045e3ddea1920 (trigger_fd3.c); gen.c 65494a03170e0bd568ab2bb20b7b76b68b4f318e572cc2a42ad01140cde57e5e", "notes": "Panic is proof of the primitive, not the ceiling: the refcount underflow yields a dangling struct file reachable from userland once the phantom fdcache reference is evicted (stage2.c implements eviction+reclaim+alias probe). Execution of that stage was blocked by an independent restore-path defect: a restored program SIGBUSes (vm_fault -> KERN_PROTECTION_FAILURE) at the first instruction fetch of its LOAD mapping; identical mmap+close+exec from an ordinary process works (mt*.c controls). Recommend a separate audit of the post-vmspace_exec mapping/fault path. Also observed: the ckptgroup gate holds for non-wheel users (EPERM verified).", "recommended_fix": "In elf_getfiles()'s fdalloc-failure path do not touch fp: set error = EBADF, dispose the owned tempfp reference; additionally reject e_phnum == 0 in ckpt_thaw_proc (fix.diff, validated).", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Tue Sep 1 20:00: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 /usr/src/sys/kern/kern_checkpoint.c, kernel rebuilt in-guest (make nativekernel, 4299 compile units, RC=0) and installed. On the patched kernel both DF-2826 variants run without any kernel panic (fd>=3: no vref panic; fd<3: no fdrop panic; guest stays up - see fix_run.log) and DF-2827 returns a clean EINVAL instead of the fatal trap.", "fix_evidence": "findings/poc/DF-2826/fix.diff; findings/poc/DF-2826/fix_run.log (baseline vs patched); findings/poc/DF-2826/VERDICT.md" } |