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)
PoC verification
Evidence pack
findings/poc/DF-3140 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
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):
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 leftbp(the current child AlSec buffer, LK_EXCLUSIVE from getblk/bread) permanently locked β the leak.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
fixedidentical trigger on patched kernel: 1st ftruncate error unchanged, 2nd ftruncate returns rc=-1 cleanly, no lockmgr panic, clean unmount
fix_run.log
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.
No comments yet.