DragonFlyBSD Kernel Audit
DF-2826 / verdict.json
← back to finding ↓ download raw
{
  "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"
}