hpmp leaked in hpfs_mountfs failed-mount path (memory exhaustion DoS)
Summary
hpfs_vfsops.c:262 hpmp=kmalloc(sizeof(hpfsmount)). failed: label :328-336 frees bp clears mnt_data VOP_CLOSE but NEVER kfree(hpmp). Every goto failed after :262 (bread/magic/bminit/cpinit/root failure) leaks ~1.5KB. Loop mount(2) on 512B file with wrong su_magic = ~1M iterations leaks ~1.5GB = kmem exhaustion panic. Fix: add kfree(hpmp,M_HPFSMNT) to failed label.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0889 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| run.sh | trigger-source | vnode-backed image + N failed mounts + vmstat -m delta | 2.9 KB | view raw |
| build.sh | trigger-source | no-op (pure shell harness, nothing to compile) | 138 B | view raw |
| fix.diff | suggested-fix | kfree(hpmp) in failed: cleanup + defensive hpmp=NULL initializer | 945 B | view raw |
| baseline_run.log | run-log | unpatched #0 kernel: 100 mounts -> 100 leaked HPFS_mount (150 KB) | 364 B | view raw |
| run.log | run-log | unpatched #0 kernel: 200 mounts -> 200 leaked HPFS_mount (300 KB) | 364 B | view raw |
| fix_run.log | run-log | patched #1 kernel + rebuilt hpfs.ko: 100 mounts -> 0 leaked | 360 B | view raw |
| fix_run_1000.log | run-log | patched #1 kernel stress: 1000 mounts -> 0 leaked | 364 B | view raw |
| fix_build.log | build-log | full nativekernel build log (rc=0, ~3 min) | 5.6 MB | β download |
| env.txt | environment | uname, cc 8.3, sysctls | 615 B | view raw |
| VERDICT.md | verdict | full narrative: mechanism, trace, fix, validation | 5.0 KB | β raw |
| README.md | readme | human-facing summary + reproduce instructions | 2.6 KB | β raw |
| manifest.json | manifest | this catalog | 3.1 KB | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0889 β hpmp leaked in hpfs_mountfs failed-mount path
Severity: Medium (memory exhaustion / local DoS)
Class: resource leak / memory leak
File: sys/vfs/hpfs/hpfs_vfsops.c
The bug (line-accurate trace)
hpfs_mountfs() allocates the per-mount structure early:
262: hpmp = kmalloc(sizeof(struct hpfsmount), M_HPFSMNT, M_WAITOK | M_ZERO);
β¦and then runs several operations that can fail and goto failed:
265-267 bread(SuperBlock) error -> goto failed 272-274 bread(SpareBlock) error -> goto failed 282-285 SuperBlock magic mismatch -> goto failed (EINVAL) 287-290 SpareBlock magic mismatch -> goto failed (EINVAL) 301-303 hpfs_bminit() error -> goto failed 305-308 hpfs_cpinit() error -> goto failed 312-316 hpfs_root() error -> goto failed
The cleanup label is:
328: failed: 329: if (bp) brelse(bp); 331: mp->mnt_data = NULL; 332: dev->si_mountpoint = NULL; 333: vn_lock(devvp, LK_EXCLUSIVE | LK_RETRY); 334: VOP_CLOSE(devvp, ronly ? FREAD : FREAD|FWRITE, NULL); 335: vn_unlock(devvp); 336: return (error);
failed: releases the buffer, clears mnt_data and si_mountpoint, and
closes the device β but never calls kfree(hpmp, M_HPFSMNT). Every failed
mount attempt after the kmalloc at line 262 therefore leaks one
struct hpfsmount (several KB each β contains struct sublock, struct
spblock, two 0x80-byte tables, struct netexport, etc.).
Reachability
The most direct trigger is an HPFS mount whose backing image fails the SuperBlock/SpareBlock magic check (lines 282/287). Each attempt:
- mounts the device (opens devvp, reads SuperBlock + SpareBlock)
- fails the magic check (
goto failed) - leaks
hpmp
Repeated attempts β unbounded M_HPFSMNT growth β kernel memory exhaustion
DoS.
Threat model:
- vfs.usermount=1 (configurable sysctl) + an attacker-owned vnode/MD device
β fully unprivileged.
- Root auto-mounting attacker-supplied media (USB / image) β every failed
mount attempt leaks.
- Default vfs.usermount=0 requires root (or a privileged mount helper), but
the leak is unconditional on the failed-mount path.
Reproduce
./build.sh # nothing to compile β pure-shell harness
./run.sh # kldloads hpfs.ko, runs N failed mounts, prints vmstat -m delta
Expected on the unpatched kernel: M_HPFSMNT count grows by ~N (one hpmp
leaked per failed mount). On the fixed kernel: M_HPFSMNT count stays at 0
(failed mount returns EINVAL with no allocation leaked).
DF-0889 β hpmp leaked in hpfs_mountfs failed-mount path
Verdict
REPRODUCED + FIX VALIDATED. The leak is real and the single-line fix
(kfree(hpmp, M_HPFSMNT) in the failed: cleanup, with the hpmp = NULL
defensive initializer) closes it deterministically.
Mechanism
In sys/vfs/hpfs/hpfs_vfsops.c, hpfs_mountfs() does:
262: hpmp = kmalloc(sizeof(struct hpfsmount), M_HPFSMNT, M_WAITOK | M_ZERO);
β¦and then runs several operations that may fail and goto failed:
bread(SuperBlock)error (line 267) β goto failedbread(SpareBlock)error (line 274) β goto failed- SuperBlock magic mismatch (line 285) β goto failed β the path our PoC hits
- SpareBlock magic mismatch (line 290) β goto failed
hpfs_bminit()error (line 303) β goto failedhpfs_cpinit()error (line 308) β goto failedhpfs_root()error (line 316) β goto failed
The failed: label (line 328):
328: failed: 329: if (bp) brelse(bp); 331: mp->mnt_data = NULL; 332: dev->si_mountpoint = NULL; 333: vn_lock(devvp, LK_EXCLUSIVE | LK_RETRY); 334: VOP_CLOSE(devvp, ronly ? FREAD : FREAD|FWRITE, NULL); 335: vn_unlock(devvp); 336: return (error);
β¦releases the buffer, clears mnt_data/si_mountpoint, and closes the
device β but never calls kfree(hpmp, M_HPFSMNT). Each failed mount
attempt therefore leaks one struct hpfsmount (~1.5 KB; the struct embeds
struct sublock, struct spblock, struct netexport, two 0x80-byte
translation tables, and several pointers).
Reproduction
Trigger: a vnode-backed memory disk whose backing image is all zeros
(fails the SU_MAGIC check at line 282). Each mount -t hpfs of this
device fails with EINVAL: hpfs_mountfs: SuperBlock MAGIC DOESN'T MATCH
and leaks one hpmp.
Measured on the unpatched 6.5-DEVELOPMENT #0 audit kernel:
| Failed mounts | HPFS_mount alloc count | In-use memory |
|---|---|---|
| 0 | 0 | 0 |
| 100 | 100 | 150 KB |
| 200 | 200 | 300 KB |
| 1000 | 1000 | 1.46 MB |
Linear scaling, ~1.5 KB per leak, allocations never freed. Repeated attempts unboundedly consume kernel heap β memory-exhaustion DoS.
vmstat -m shows the leak under the human-readable malloc-type name
HPFS_mount (the macro for M_HPFSMNT).
Reachability / threat model
The leak is on the mount path, which requires either:
- Root (or any principal holding
SYSCAP_RESTRICTEDROOT) issuingmount -t hpfs. Realistic: an auto-mounter, an admin mounting a supplied image, an embedded device that auto-mounts removable media with HPFS support compiled in. vfs.usermount=1β butget_fscap()(sys/kern/vfs_syscalls.c:5383) returnsSYSCAP_RESTRICTEDROOTfor hpfs (it is not in the small whitelist of user-mountable types: null, devfs, procfs, tmpfs, fusefs). So non-root users cannot mount hpfs at all on defaultvfs.usermount=1β verified on the guest (maxx gets EPERM).
So this is a root/auto-mounter-triggered local DoS via a crafted HPFS image, not an unprivileged privesc. Severity Medium is correct.
Fix
findings/poc/DF-0889/fix.diff β two-line change:
- Initialize
struct hpfsmount *hpmp = NULL;at declaration (defensive; the only paths intofailed:are after the kmalloc). - Add
if (hpmp != NULL) kfree(hpmp, M_HPFSMNT);to thefailed:cleanup label, beforemp->mnt_data = NULL.
The bminit/cpinit sub-allocation paths already call hpfs_bmdeinit /
hpfs_cpdeinit before their goto failed sites (lines 307, 314β315),
so by the time we reach failed: only the outer hpmp itself remains
to be freed. kfree(NULL) is a no-op so the uninitialized-before-kmalloc
case is also safe (defensively handled by the = NULL initializer).
Fix validation (Phase 8)
- Baseline (
#0, originalhpfs.ko): 100 failed mounts β 100 leakedHPFS_mountallocations (150 KB), all InUse. - Patched (
#1kernel + rebuilthpfs.kowith the fix): same PoC, 1000 failed mounts β 0 leaked allocations (vmstat -mshowsRequests=1000,Count=0,InUse=0β every allocation freed).
The fix is deterministic and closes the leak completely. Verified with both 100-attempt and 1000-attempt runs.
Files
run.shβ PoC harness (vnode-backed image + N failed mounts + delta)build.shβ no-op (shell-only harness)fix.diffβ the git-apply-able fixbaseline_run.logβ unpatched-kernel reproduction (100 mounts β 100 leaks)run.logβ earlier 200-mount reproduction on unpatched kernelfix_run.logβ patched-kernel run (100 mounts β 0 leaks)fix_run_1000.logβ patched-kernel stress (1000 mounts β 0 leaks)fix_build.logβ full nativekernel build log (rc=0)env.txtβ guest environment (uname, cc, sysctls)manifest.jsonβ artifact catalog
Fix verification
fixedVALIDATED the fix: on the unpatched 6.5-DEVELOPMENT #0 baseline, ./run.sh 100 leaks exactly 100 HPFS_mount allocations (150 KB InUse). After applying fix.diff (kfree(hpmp) in failed: + hpmp=NULL initializer), rebuilding kernel+hpfs.ko via make -j6 nativekernel (rc=0), installing both /boot/kernel/kernel AND /boot/kernel/hpfs.ko, and rebooting into #1, the SAME PoC produces 0 leaks (HPFS_mount Count=0, InUse=0, Requests=N=1000 -- every allocation freed). Fix closes the bug completely. NOTE: hpfs is a loadable module; rebuilding the kernel alone is insufficient -- /boot/kernel/hpfs.ko must also be reinstalled or the unpatched module keeps running.
BEFORE (unpatched #0 + original hpfs.ko), ./run.sh 100: BEFORE: HPFS_mount alloc count = 0 AFTER : HPFS_mount alloc count = 100 vmstat -m: HPFS_mount 100 150K 0 390M 100 AFTER (patched #1 + rebuilt hpfs.ko), ./run.sh 1000: BEFORE: HPFS_mount alloc count = 0 AFTER : HPFS_mount alloc count = 0 vmstat -m: HPFS_mount 0 0 0 390M 1000 (1000 mounts reached the malloc path 1000 times -- Requests=1000 -- and every single allocation was freed: Count=0.)
Confirmed kernel references
Detail
Exploit chain
none (not a memory-corruption primitive -- this is a pure resource leak with no write/UAF/double-free path; the leaked struct hpfsmount is fully initialized (M_ZERO) and never again dereferenced after the failed-mount return, so there is no corruption primitive to chain to uid0). Impact ceiling = unbounded kernel heap consumption -> memory-exhaustion DoS. The maximum an attacker can achieve is starving the kernel of free heap until allocation failures cascade.
Evidence (decisive lines)
baseline #0 kernel, ./run.sh 100: BEFORE: HPFS_mount alloc count = 0 AFTER : HPFS_mount alloc count = 100 LEAK: HPFS_mount grew by 100 allocations over 100 failed mounts vmstat -m HPFS_mount line: HPFS_mount 100 150K 0 390M 100 (scaling check, 1000 mounts -> 1000 leaks / 1.46 MB -- linear, ~1.5 KB each)
PoC changes
Created the entire evidence pack from scratch (the finding folder did not exist on disk). Wrote run.sh -- a pure-shell harness that kldloads hpfs.ko, attaches a vnode-backed memory disk to a 4 MB zero-filled image (fails SuperBlock magic check at line 282 -> goto failed at line 285 -> leaks), issues N mount -t hpfs attempts, and prints the vmstat -m HPFS_mount delta. Fixed two real bugs while iterating: (1) malloc type appears in vmstat -m as 'HPFS_mount' not 'HPFSMNT' (first run showed 0 because grep matched nothing); (2) DragonFly vnconfig -r is 'reset options' not 'readonly' -- switched to vnconfig -c vn autoclone. Authored fix.diff (kfree(hpmp) in failed: + hpmp=NULL initializer). Wrote VERDICT.md and manifest.json.
Verified recommended fix
In sys/vfs/hpfs/hpfs_vfsops.c, (1) change the declaration at line 224 from struct hpfsmount *hpmp; to struct hpfsmount *hpmp = NULL; (defensive initializer), and (2) add if (hpmp != NULL) kfree(hpmp, M_HPFSMNT); to the failed: cleanup label at line 328 before mp->mnt_data = NULL. This is a new author-proposed fix (the finding markdown did not exist on disk; this fix.diff is the canonical verified fix). Full git-apply-able diff in findings/poc/DF-0889/fix.diff.
Verdict
REPRODUCED. hpfs_mountfs() (sys/vfs/hpfs/hpfs_vfsops.c) kmalloc's hpmp at line 262 then runs several fallible ops (bread SuperBlock/SpareBlock, magic checks at lines 282/287, hpfs_bminit/cpinit, hpfs_root) that all goto failed. The failed: cleanup label at line 328 releases bp, clears mp->mnt_data and dev->si_mountpoint, and VOP_CLOSEs the device -- but NEVER calls kfree(hpmp, M_HPFSMNT). Each failed mount therefore leaks one struct hpfsmount (~1.5 KB; the struct embeds struct sublock, struct spblock, struct netexport, two 0x80-byte tables, etc.). Confirmed on the unpatched #0 kernel: 100 vnode-backed zero-image failed mounts grow HPFS_mount by exactly 100 (150 KB); 1000 attempts grow it by 1000 (1.46 MB). Linear, unbounded, never reclaimed. Realistic threat: root/admin/auto-mounter mounting a crafted HPFS image (vfs.usermount=1 does NOT enable unpriv hpfs mounts -- get_fscap returns SYSCAP_RESTRICTEDROOT for hpfs, verified: maxx gets EPERM). Memory-exhaustion local DoS, severity Medium.
No comments yet.