elf_getfiles() drops an unowned reference on the checkpoint file when fdalloc() fails: struct file refcount underflow, premature fo_close/vrele/ffree, UAF, success-masking, tempfp leak
| Field | Value |
|---|---|
| ID | DF-2826 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-672 / CWE-416 |
| File | sys/kern/kern_checkpoint.c |
| Lines | 664-668 (the owned ref at :756, dropped again :762) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-31 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
On the fdalloc-failure path elf_getfiles() calls fp_close(fp) β fdrop() β on the checkpoint file whose only reference belongs to sys_sys_checkpoint()'s holdfp(), which dropfp() drops again. One fdrop too many underflows the file refcount with two observed death modes and leaves error==0 (CKPT_THAW reports success for an aborted restore) plus a leaked tempfp. Gated by kern.ckptgroup (default wheel-only; verified EPERM for non-wheel; kern.ckptgroup=-1 exposes it to all local users). fdβ₯3: file+vnode die mid-restore and elf_loadphdrs() UAF-reads the freed struct file (vref panic). fd<3: phantom fdcache reference β dangling fd-table entry once evicted (classic exploitable file-UAF: M_FILE objcache LIFO reclaim aliases a foreign file), or fdrop-on-zero-count panic at exit. The full uid=0 grooming chain was implemented but blocked by an independent restore-path defect (SIGBUS at first instruction fetch β documented in VERDICT.md, recommended for separate audit).
Proof of contest
VERIFIED both modes on the stock INVARIANTS kernel incl. fresh-snapshot
boots (findings/poc/DF-2826/): trigger_fd3 β panic: vref: bad refcnt
00000000 1 (vrefβvnode_pager_referenceβvm_mmapβfp_mmapβmmap_phdr);
trigger_fd0 β CKPT_THAW returns 0, process exits β panic: fdrop:
invalid f_count 0 (fdropβclosefβfdfreeβexit1βsigexit). Fix (do not
touch fp on this path; EBADF + dispose tempfp only) authored.
Recommended fix
See findings/poc/DF-2826/fix.diff.
Timeline
- 2026-08-31 Discovered during pass-2 audit of kern_checkpoint.c (GLM 5.3); both panic modes reproduced same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2826 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.9 KB | β raw | |
| VERDICT.md | β | 6.6 KB | β raw | |
| gen.c | β | 7.4 KB | view raw | |
| stage2.c | β | 3.2 KB | view raw | |
| trigger_fd3.c | β | 654 B | view raw | |
| trigger_fd0.c | β | 795 B | view raw | |
| build.sh | β | 801 B | view raw | |
| run.sh | β | 693 B | view raw | |
| build.log | β | 1.2 KB | view raw | |
| fix_run.log | β | 1.4 KB | view raw | |
| panic.txt | β | 2.4 KB | view raw | |
| panic_fd0.txt | β | 2.1 KB | view raw | |
| baseline_fresh_vref_panic.txt | β | 2.1 KB | view raw | |
| env.txt | β | 669 B | view raw | |
| fix.diff | β | 711 B | view raw | |
| verdict.json | β | 5.7 KB | view raw |
DF-2826 β elf_getfiles() drops an unowned reference on the checkpoint file (refcount underflow β UAF / panics)
- File:
sys/kern/kern_checkpoint.c - Bug lines:
elf_getfiles()fdalloc-failure path, kern_checkpoint.c:664-668 (fp_close(fp)wherefpis the caller's holdfp() reference), plus the missingerror =(syscall reports success) and the leakedtempfp. - Gate:
sys_checkpointis gated bykern.ckptgroup(default 0 = wheel). An unprivileged user not in wheel gets EPERM before any parsing (verified:maxx(uid 1001, no wheel) β EPERM). Root / wheel-group users reach the parser; hosts settingkern.ckptgroup=-1expose it to every user.
Build (inside the guest, as root)
sh build.sh # compiles gen, stage2 (freestanding), triggers,
# generates df2826.ckpt (nfiles=1, fd-steal image)
# and df2827.ckpt (e_phnum=0 image)
The generator must run in the same boot as the trigger: the image embeds a file handle of /etc/passwd whose fsid is boot-dependent on this HAMMER2 root (stale fh β ckpt_fhtovp fails β the bug path is not reached).
Run (expect: kernel panic)
cd /root/df2826 ./trigger_fd3 # checkpoint image on fd 3 (fd >= 3 variant) # panic: vref: bad refcnt 00000000 1 # vref <- vnode_pager_reference <- vm_mmap <- fp_mmap <- mmap_phdr ./gen df2826.ckpt stage2.bin normal 0x600000f0 1 # freshen fh ./trigger_fd0 # checkpoint image on fd 0 (fd < 3 variant) # panic: fdrop: invalid f_count 0 # fdrop <- closef <- fdfree <- exit1 <- sigexit ./trigger_fd3 df2827.ckpt # DF-2827: e_phnum == 0 # Fatal trap 12: page fault while in kernel mode, fault VA = 0x18 # Stopped at sys_sys_checkpoint+0x2f0: movq 0x20(%rax),%rax
Expected output (decisive lines)
fd>=3 variant β the stolen reference makes fdrop() run fo_close() +
vrele() on the still-in-use checkpoint file inside the syscall; by the
time elf_loadphdrs() maps the LOAD segments, the vnode's usecount is 0
and vnode_pager_reference()'s vref() panics:
panic: vref: bad refcnt 00000000 1
vref() at vref+0x36
vnode_pager_reference() at vnode_pager_reference+0x1d
vm_mmap() at vm_mmap+0x3a7
fp_mmap() at fp_mmap+0x14a
mmap_phdr() at mmap_phdr+0x54
Debugger("panic")
fd<3 variant β the stolen reference becomes a phantom per-thread fdcache
reference (dropfp() returns it to the cache without dropping); the syscall
reports success, the process dies on the restored context, and at exit
fdfree()'s closef() fdrops the zero-count file:
panic: fdrop: invalid f_count 0 fdrop() at fdrop+0x10e closef() at closef+0x32 fdfree() at fdfree+0x15e exit1() at exit1+0x13f sigexit() at sigexit+0x4c
Fix
fix.diff (validated in-guest: patched kernel built, both panics gone,
CKPT_THAW returns a clean error instead).
DF-2826 VERDICT β elf_getfiles() unowned fdrop() β file/vnode lifetime corruption
Bottom line
REPRODUCED (certain). elf_getfiles() (sys/kern/kern_checkpoint.c:664-668)
calls fp_close(fp) β i.e. fdrop(fp) (sys/kern/kern_fp.c:559-562) β on the
checkpoint file when fdalloc() cannot allocate the descriptor requested by
a restored ckpt_fileinfo. elf_getfiles() does not own any reference on
fp: the reference belongs to sys_sys_checkpoint()'s holdfp()
(kern_checkpoint.c:756), which is dropped again by dropfp() at
kern_checkpoint.c:762. One fdrop() too many β struct file refcount
underflow with two deterministic death modes, both observed on the stock
guest kernel:
Variant A β checkpoint fd >= 3 (trigger_fd3)
holdfp(td, fd, FREAD): file count = 2 (fd table + holdfp; the holdfp reference is tracked by the per-thread fdcache, mode "lent").elf_getfiles()'s close-loop (kern_checkpoint.c:611-612) runskern_close(fd)for all fds >= 3, which drops the fd-table reference (count = 1) and detaches the fdcache entry (fclearcache detaches a lent entry without a drop β kern_descrip.c:246-263).- The crafted
cfi(valid fhandle fromgetfh("/etc/passwd"),cfi_index = 0x7fffffff) makesckpt_fhtovp()+fp_vpopen()succeed, thenfdalloc()fail (want >= lim, kern_descrip.c:1745) β the buggyfp_close(fp)takes the count 1 β 0 β last-reference path:fo_close()=vn_close()= VOP_CLOSE + vrele(vnode), thenffree(fp)β the checkpoint file and its vnode reference die mid-syscall. elf_getfiles()returnserror == 0(the path never setserror) β CKPT_THAW reports success β and leaks thetempfpit had opened.ckpt_thaw_proc()continues with the freedfp:elf_loadphdrs(fp, ...)(kern_checkpoint.c:258) readsfp->f_type/fp->f_dataout of the freed chunk (UAF read that happens to be intact in the objcache) and callsfp_mmap()βvm_mmap()βvnode_pager_reference()βvref(vp)on the vnode whose usecount the prematurevrele()took to 0:panic: vref: bad refcnt 00000000 1(observed twice, incl. from a freshwith-srcsnapshot boot; see panic.txt, baseline_fresh_vref_panic.txt).
Variant B β checkpoint fd < 3 (trigger_fd0)
- The close-loop does not touch fds 0-2; the fdcache entry for the holdfp reference stays tied to fdnode 0 in "lent" mode.
- The buggy
fp_close(fp)takes count 2 β 1, stealing the cache-owned reference;dropfp()then returns the phantom reference to the cache (mode 2 β 0, no drop; kern_descrip.c:540-552). The file is now at count 1 with two logical owners (fd table + phantom cache ref) β a silent zero-integrity state. - The syscall returns 0 (success-masking). The restored register state (this image restores cs=0x2b/ss=0x33/rflags=0x202, rip into a mapped LOAD segment) starts executing; this particular restored image dies on SIGBUS at the first fetch (see "Anomaly" below), so the process exits.
exit1()βfdfree()βclosef(fd0)βfdrop()on the phantom-owned file:panic: fdrop: invalid f_count 0(fdrop β closef β fdfree β exit1 β sigexit; see panic_fd0.txt).
If the process had instead stayed alive, the phantom reference is dropped
the moment the thread's fdcache entry is evicted (kern_descrip.c:233-234
atomic_add_int(&fp->f_count, -1)), producing a dangling fd-table entry
(freed struct file still installed at fd N) β the textbook exploitable
file-UAF primitive (reclaim via any falloc() on the same per-CPU objcache
magazine β fd aliases a foreign file; close() of either descriptor
prematurely frees a live victim). That stage was not driven to uid=0 in
this run (see Blockers).
Threat model
- Reach:
sys_checkpoint(2 /*CKPT_THAW*/, fd, -1, 0)(syscall 467), gated bykern.ckptgroup(default 0 β wheel). Verified: unprivilegedmaxx(uid 1001, not in wheel) gets EPERM before parsing; root triggers both panics. Anykern.ckptgroup=-1host exposes the bug to all local users. - Attacker input: a crafted checkpoint image β full control of every field;
the only requirement for the fdrop-steal is one
ckpt_fileinfowhosecfi_indexfailsfdalloc()after a successful fhandle open.
PoC artifacts
gen.cβ image generator (uses guest headers via_KERNEL_STRUCTURESso all kernel struct sizes match exactly);build.shdrives it.stage2.cβ freestanding restored-program blob (proves register/vmspace restore works and would carry the post-restore exploitation stage).trigger_fd3.c/trigger_fd0.cβ the two variants.panic.txt(variant A),panic_fd0.txt(variant B),run.logβ serial console captures.
Fix validation
fix.diffremoves the unownedfp_close(fp), setserror = EBADF, and disposes the ownedtempfpreference (also fixing the leak); it additionally rejectse_phnum == 0(DF-2827).- Patched kernel built in-guest (
make nativekernel KERNCONF=X86_64_GENERIC), installed, rebooted; both triggers re-run: no panic β seefix_run.log. The syscall now returns a clean error and the kernel stays up.
Anomaly (unresolved, does not affect this finding)
A fully valid crafted checkpoint (nfiles=0) restores registers and
mappings, but the restored program reliably receives SIGBUS at the first
instruction fetch of the LOAD mapping (fault VA == rip == mapping base;
core shows cs/ss/rflags/rsp perfect and the mapping present). The same
mmap+offset+protection from an ordinary userland program executes fine
(mt.c/mt2.c/mt4.c controls). The mapping is created after
vmspace_exec()/pmap_replacevm() inside the syscall; vm_fault returns
KERN_PROTECTION_FAILURE (trap.c:1015 maps that to SIGBUS). This looks
like an independent defect in the ckpt restore path (fresh-vmspace mmap
not executable/faultable), worth a separate look; it truncated our ability
to run in-process exploitation stages.
Blockers on the uid=0 chain (honest accounting)
The memory-corruption primitive is real and demonstrated (premature
fo_close/vrele/ffree of a live file; UAF reads of the freed chunk inside
the same syscall). Driving it to uid=0 requires the restored program to
survive long enough to (a) evict the phantom fdcache ref on demand, (b)
reclaim the freed struct file, and (c) groom a victim β step (a)/(b) were
implemented in stage2.c but never executed because of the SIGBUS anomaly
above (the restored process dies at its first instruction). With the
anomaly understood/fixed, the chain is standard M_FILE objcache reclamation
(per-CPU LIFO) β no hardening on this guest blocks it (no SMAP/SMEP/KASLR).
Fix verification
fixedfix.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.
findings/poc/DF-2826/fix.diff; findings/poc/DF-2826/fix_run.log (baseline vs patched); findings/poc/DF-2826/VERDICT.md
Confirmed kernel references
Detail
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 (decisive lines)
["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']
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).
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).
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.
No comments yet.