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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0889 Β· 14 files
FileTypeDescriptionSize
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
README.md readme human-facing summary + reproduce instructions
↓ download 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:

  1. mounts the device (opens devvp, reads SuperBlock + SpareBlock)
  2. fails the magic check (goto failed)
  3. 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).

VERDICT.md verdict full narrative: mechanism, trace, fix, validation
↓ download raw

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 failed
  • bread(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 failed
  • hpfs_cpinit() error (line 308) β†’ goto failed
  • hpfs_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:

  1. Root (or any principal holding SYSCAP_RESTRICTEDROOT) issuing mount -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.
  2. vfs.usermount=1 β€” but get_fscap() (sys/kern/vfs_syscalls.c:5383) returns SYSCAP_RESTRICTEDROOT for 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 default vfs.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:

  1. Initialize struct hpfsmount *hpmp = NULL; at declaration (defensive; the only paths into failed: are after the kmalloc).
  2. Add if (hpmp != NULL) kfree(hpmp, M_HPFSMNT); to the failed: cleanup label, before mp->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, original hpfs.ko): 100 failed mounts β†’ 100 leaked HPFS_mount allocations (150 KB), all InUse.
  • Patched (#1 kernel + rebuilt hpfs.ko with the fix): same PoC, 1000 failed mounts β†’ 0 leaked allocations (vmstat -m shows Requests=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 fix
  • baseline_run.log β€” unpatched-kernel reproduction (100 mounts β†’ 100 leaks)
  • run.log β€” earlier 200-mount reproduction on unpatched kernel
  • fix_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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED 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.)
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 12 00:30:37 UTC 2026 (sha256 kernel=e83a62042675931956c5accce97e057a3aa4faed7bc076490d9cf84a97287ab0, hpfs.ko=32e6d66310e977d683716d0f88cf90841f746a135974f2a37f05df82ec10472c)

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.