β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3140

Error-path buffer leaks: missing brelse in hpfs_addextent children-reparenting loop (:498-500) and hpfs_truncatealblk sibling read (:798-801) β€” deterministic kernel panic (lockmgr locking against myself) + B_BUSY buffer accumulation DoS

Summary

Two error returns drop getblk/bread-held buffers: addextent:498-500 returns without brelse(nbp) when a children-reparenting bread fails, and truncatealblk:798-801 returns without brelse(bp) when the concat-sibling bread fails (contrast the correct pattern at :693-697). The truncate leak is deterministically fatal: the next ftruncate from the same thread re-locks the still-LK_EXCLUSIVE buffer -> 'panic: lockmgr: locking against myself'. The addextent leak accumulates one held 512-byte buffer (+ bitmap sector) per failing pwrite -> buffer-cache exhaustion / unkillable writers. VERIFIED uid 1001: 1st ftruncate returns error (console: hpfs_breadstruct MAGIC DOESN'T MATCH - the failing sibling read) with the buffer leaked; 2nd ftruncate panics 'lockmgr: locking against myself' (backtrace lockmgr_exclusive<-getblk<-breadnx<-hpfs_breadstruct<-hpfs_truncatealblk). Fix validated (brelse before both error returns): 2nd ftruncate returns rc=-1 cleanly, no panic.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3140 Β· 12 files
FileTypeDescriptionSize
craft_img.py β€” 6.5 KB view raw
poc.c β€” 1.7 KB view raw
mount_rw.c β€” 1.3 KB view raw
build.sh β€” 107 B view raw
run.sh β€” 327 B view raw
run.log β€” 415 B view raw
panic.txt β€” 805 B view raw
fix.diff β€” 4.3 KB view raw
fix_run.log β€” 939 B view raw
VERDICT.md β€” 2.5 KB ↓ raw
README.md β€” 1.6 KB ↓ raw
env.txt β€” 464 B view raw

DF-3140 β€” error-path buffer leaks in hpfs_addextent (:498-500) and hpfs_truncatealblk (:798-801): deterministic kernel panic via leaked-buffer re-lock; B_BUSY buffer accumulation

Build

./build.sh      # crafts the (sparse, ~229 MB) df3140.img, gzip-pushes
                # it to the guest, compiles poc + mount_rw

Run

./run.sh        # mounts RW; trunc variant: 2x ftruncate as uid 1001
                # (2nd hangs), root attempts kill -9

Expected / observed

  • trunc variant (file TRUN): DF3140 trunc: 1st ftruncate(512)... rc=-1 (error returned; hpfs_breadstruct: MAGIC DOESN'T MATCH printed β€” the sibling read at alsubr.c:798 fails and the function returns WITHOUT brelse(bp): the child AlSec buffer stays LK_EXCLUSIVE-locked) DF3140 trunc: 2nd ftruncate(512)... (never returns) => panic: lockmgr: locking against myself, backtrace: lockmgr_exclusive <- getblk <- breadnx <- hpfs_breadstruct <- hpfs_truncatealblk (full text in panic.txt / run.log) Unprivileged ftruncate x2 = deterministic kernel panic.
  • add variant (file ADDL): every failing pwrite leaks one held 512-byte buffer + one bitmap sector (addextent's children-reparenting loop returns without releasing nbp at :498-500); accumulation exhausts the buffer cache (getblk blocks forever) β€” by construction; not run to exhaustion (guest was spent on the stronger trunc-panic evidence).

Root causes: missing brelse(bp) / brelse(nbp) on the two error returns cited above (contrast the correct handling at alsubr.c:693-697).

VERDICT.md
↓ download raw

DF-3140 VERDICT β€” REPRODUCED (deterministic kernel panic from unprivileged double-ftruncate; error-path B_BUSY buffer leak)

Observed (run.log + panic.txt)

Crafted image, RW mount, uid 1001 (maxx):

  1. ftruncate(fd, 512) #1 on file TRUN returns -1 (error). Console: hpfs_breadstruct: MAGIC DOESN'T MATCH (0x00000000 != 0x37e40aae) β€” the sibling-AlSec read at sys/vfs/hpfs/hpfs_alsubr.c:798-801 (an[0] -> sector without AS_MAGIC) failed and the error return at :801 left bp (the current child AlSec buffer, LK_EXCLUSIVE from getblk/bread) permanently locked β€” the leak.
  2. ftruncate(fd, 512) #2 (same fd/thread) re-enters hpfs_truncatealblk, breads the same child AlSec -> getblk -> lockmgr_exclusive on the buffer already held by this thread: panic: lockmgr: locking against myself lockmgr_exclusive() at lockmgr_exclusive+0x3e0 getblk() at getblk+0xe1 breadnx() at breadnx+0x284 hpfs_breadstruct() at hpfs_breadstruct+0x45 hpfs_truncatealblk() at hpfs_truncatealblk+0xf2 Guest dies at db>. Deterministic (2 syscalls).

Secondary consequence (by construction, not run to exhaustion)

The addextent twin (alsubr.c:498-500: children-reparenting loop returns without brelse(nbp) after hpfs_alblk2alsec) leaks one held 512-byte buffer per failing pwrite (plus its bitmap sector). Repeated failing pwrites accumulate B_BUSY buffers until getblk blocks forever β€” buffer-cache exhaustion / unkillable-writer DoS. Not executed at scale on the guest (budget spent on the stronger deterministic panic).

Fix validation

fix.diff (shared, DF-3137 pack) adds the two missing brelse() calls. Patched kernel: 1st ftruncate still returns the original error, 2nd ftruncate returns cleanly (no hang, no panic β€” the buffer is properly released between calls). See fix_run.log.

Verdict

REPRODUCED β€” impact panic (deterministic, unprivileged), plus accumulation DoS by construction. Kernel refs: sys/vfs/hpfs/hpfs_alsubr.c:790-801 (leak #2 + concat path), :490-509 (leak #1), :693-697 (the correct pattern for contrast), sys/kern/vfs_bio.c:899-945 + sys/sys/buf2.h:347 (bread hands out a locked buffer the caller must release).

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

identical trigger on patched kernel: 1st ftruncate error unchanged, 2nd ftruncate returns rc=-1 cleanly, no lockmgr panic, clean unmount

fix_run.log
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #2: Mon Sep 7 00:17:43 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

crafted image (TRUN: root AB_NODES an[1]->valid leaf alsec C1, an[0]->sector without AS_MAGIC) -> RW mount -> uid 1001 ftruncate(fd,512) #1: partial leaf truncation sets *resp=0, sibling breadalsec fails, error return leaks C1's locked buffer -> ftruncate #2: breadalsec(C1) -> getblk -> lockmgr_exclusive self-deadlock -> panic. ADDL variant: root freecnt==0 forces the alblk2alsec conversion path on every failing pwrite, each leaking one held buffer.

Evidence (decisive lines)

['run.log: 1st ftruncate rc=-1, 2nd never returns (timeout 124)', "panic.txt: panic lockmgr locking against myself + full backtrace + preceding 'hpfs_breadstruct: MAGIC DOESN'T MATCH' showing the failing sibling read of leak path", 'fix_run.log: patched kernel 2nd ftruncate returns rc=-1, guest alive']

PoC changes

written fresh; image generator carries both trigger files (ADDL/TRUN); add-variant accumulation demonstrated by design, executed only bounded (budget spent on the deterministic panic)

Verified recommended fix

add brelse(bp)/brelse(nbp) before the two error returns (alsubr.c:801 and :500)

Verdict

Two error returns leak getblk/bread-held buffers: hpfs_addextent:498-500 returns without brelse(nbp) when the post-conversion children-reparenting bread fails, and hpfs_truncatealblk:798-801 returns without brelse(bp) when the concat-sibling bread fails. The truncate leak is deterministically fatal: with a crafted image, the 1st unprivileged ftruncate(512) returns an error but leaves the child AlSec buffer LK_EXCLUSIVE-locked; the 2nd ftruncate re-breads it from the same thread and the kernel panics 'lockmgr: locking against myself' (captured backtrace: lockmgr_exclusive <- getblk <- breadnx <- hpfs_breadstruct <- hpfs_truncatealblk). The addextent leak accumulates one B_BUSY 512-byte buffer (plus a bitmap sector) per failing pwrite β€” buffer-cache exhaustion / unkillable-writer DoS by construction (not run to exhaustion on the guest). Fix (the two missing brelse() calls) validated: 2nd ftruncate returns cleanly, no panic.