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

Unbounded recursion in hpfs_truncatealblk via crafted AB_NODES AlSec chain: kernel stack exhaustion (guard-page double fault) from unprivileged ftruncate

Field Value
ID DF-3101
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:H
CWE CWE-674 / CWE-121
File sys/vfs/hpfs/hpfs_vnops.c
Lines entry :559-569; recursion hpfs_alsubr.c:742-888 (loop :760, recurse :770-775)
Area vfs/hpfs
Confidence certain
Discovered 2026-09-06
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

hpfs_truncatealblk recurses into child AlSecs with no depth limit and no visited-set. The loop condition 'while (abp->ab_busycnt && (bn <= anp->an_nextoff))' is ALWAYS true when truncating to 0 (bn==0, an_nextoff is an on-disk u32), so a crafted fnode whose AB_NODES root alblk points at a chain of N AB_NODES AlSecs drives N recursion levels. Entry: ftruncate(fd,0) β†’ hpfs_setattr (va_size<fn_size branch) β†’ hpfs_truncate. A 5000-sector chain overflows the kernel stack into its guard page: 'DOUBLE FAULT - KERNEL STACK GUARD HIT! / panic: double fault'. Distinct from DF-0858 (hpfs_hpbmap goto-loop, no stack growth): different function, real C recursion, different failure mode. The cyclic variant instead self-deadlocks on the per-level held buffer lock (unkillable hang). Crafted HPFS image + RW mount (mount(2) directly; stock mount_hpfs forces RO); with the mountpoint uid handed to the user, an unprivileged ftruncate(2) deterministically panics the kernel. VERIFIED: 2.5MB image with 5000-deep AlSec chain; uid 1001 open+ftruncate(0) β†’ guest dies with guard-page double fault (rsp exactly on the guard page). Fix validated (depth limit 32 β†’ EINVAL + console 'alsec tree too deep'): no panic.

Timeline

  • 2026-09-06 Discovered during pass-2 audit of hpfs_vnops.c (GLM 5.3); stack-exhaustion double fault reproduced + fix validated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3101 Β· 15 files
FileTypeDescriptionSize
craft_img.py β€” 5.6 KB view raw
poc.c β€” 906 B view raw
mount_rw.c β€” 1.3 KB view raw
build.sh β€” 92 B view raw
run.sh β€” 116 B view raw
build.log β€” 123 B view raw
run.log β€” 333 B view raw
run.2.log β€” 0 B ↓ download
run.3.log β€” 209 B view raw
panic.txt β€” 1.4 KB view raw
env.txt β€” 202 B view raw
fix.diff β€” 2.0 KB view raw
fix_run.log β€” 439 B view raw
VERDICT.md β€” 3.4 KB ↓ raw
verdict.json β€” 3.6 KB view raw
VERDICT.md
↓ download raw

DF-3101 VERDICT

Verdict: REPRODUCED β€” deterministic kernel stack exhaustion ("DOUBLE FAULT - KERNEL STACK GUARD HIT!", panic) from an unprivileged ftruncate(2) on a crafted HPFS image.

How it was proven

Stock INVARIANTS kernel DragonFly dfbsd 6.5-DEVELOPMENT #0 (guest uname in env.txt). craft_img.py builds a 2.5 MB image whose regular-file fnode carries an AB_NODES root alblk pointing at a chain of 5000 AB_NODES AlSec sectors (each an_nextoff=0xFFFFFFFF, an_lsn β†’ next), terminating in an empty leaf. Root mounts it RW via the mount_rw helper (stock mount_hpfs(8) forces MNT_RDONLY at sbin/mount_hpfs/mount_hpfs.c:106 β€” the write path needs a direct mount(2), DF-0859 precedent) on a mountpoint owned by uid 1001, making /tmp/hpfsmp/FILE owned by the unprivileged user.

Unprivileged trigger (uid 1001):

/tmp/df3101_run /tmp/hpfsmp/FILE    #  open(O_WRONLY); ftruncate(fd, 0)

Result (run.3.log, panic.txt): the syscall never returned (RUN_EXIT=124 via ssh timeout), the guest dropped to DDB:

DOUBLE FAULT - KERNEL STACK GUARD HIT!
Fatal double fault
rip = 0xffffffff80665009
rsp = 0xfffff801182dd000
panic: double fault
Trace: dblfault_handler() at dblfault_handler+0x10c ...

rsp is exactly the kernel-stack guard page: hpfs_truncatealblk recursed ~5000 levels (alsubr.c:764 β†’ :770-775) until the frame push hit unmapped guard memory and the CPU double-faulted. One clean, deterministic panic β€” no heap corruption, nothing speculative.

Why the recursion is unbounded (path:line)

  • hpfs_alsubr.c:760 while (abp->ab_busycnt && (bn <= anp->an_nextoff)) β€” bn = new length in blocks = 0 for ftruncate(fd,0); an_nextoff is an on-disk u32, so 0 <= u32 is always true.
  • hpfs_alsubr.c:764 hpfs_breadalsec() checks only AS_MAGIC.
  • hpfs_alsubr.c:770-775 recursion into &asp->as_ab β€” no depth argument, no visited set, no budget.
  • Entry: hpfs_vnops.c:559-569 (hpfs_setattr truncate branch) β†’ hpfs_subr.c:774 (hpfs_truncate).

Distinct from DF-0858 (unbounded goto dive in hpfs_hpbmap β€” an infinite loop, no stack growth): this is a different function, a different control-flow defect (recursion), and a different failure mode (guard-page double fault). The cyclic variant of the same defect instead self-deadlocks on the per-level held buffer lock (unkillable hang) β€” noted, not separately verified.

Preconditions (honest)

Root mounts the crafted image RW (custom mounter; stock mount_hpfs cannot produce an RW mount). With the mountpoint uid handed to the user, the trigger itself is fully unprivileged. Crafted-image threat model β€” same gating as DF-0859 (pass 1) for the write path.

Fix validation

fix.diff adds a depth argument (HPFS_MAXALSECDEPTH 32, EINVAL when exceeded) to hpfs_truncatealblk and its two callers. Applied to the guest's /usr/src, make nativekernel (X86_64_GENERIC), rebooted into the patched kernel: the identical unprivileged trigger returns ftruncate rc=-1 errno=22 Invalid argument, no panic, guest stays up (fix_run.log). Baseline (unpatched): panic as above.

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

Baseline (unpatched #0): double-fault panic. Patched (fix.diff, make nativekernel, reboot): identical unprivileged trigger returns EINVAL, no panic, guest stays up.

fix_run.log
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Sun Sep 6 09:09:15 UTC 2026 (fix.diff applied, make nativekernel, X86_64_GENERIC)

Confirmed kernel references

Detail

Exploit chain

attacker supplies crafted HPFS image -> root mounts RW (mount(2) helper; mountpoint uid handed to user) -> unprivileged user opens FILE O_WRONLY (owner) -> ftruncate(fd,0) -> hpfs_setattr (vnops.c:559) -> hpfs_truncate (subr.c:774) -> hpfs_truncatealblk recursion 5000 deep (alsubr.c:770-775) -> kernel stack guard page -> double fault -> panic. No memory corruption escapes the guard page; impact ceiling is denial of service.

Evidence (decisive lines)

["panic.txt: 'DOUBLE FAULT - KERNEL STACK GUARD HIT!' / 'panic: double fault' / rsp=0xfffff801182dd000 on the guard page", 'run.3.log: RW mount of df3101.img, unprivileged ftruncate trigger, RUN_EXIT=124 (syscall never returned), guest down', 'craft_img.py: 5000-sector AB_NODES AlSec chain (an_nextoff=0xFFFFFFFF each) terminating in an empty leaf', 'fix_run.log: patched kernel (fix.diff, depth limit) - same trigger returns EINVAL, no panic, guest stays up']

PoC changes

Image crafter + RW-mount helper built from scratch on the verified DF-0857/DF-0859 pack skeleton; trigger is a 3-line ftruncate program. No seed PoC existed.

Verified recommended fix

Depth-limit hpfs_truncatealblk recursion (depth argument, HPFS_MAXALSECDEPTH=32, EINVAL past it); see fix.diff.

Verdict

REPRODUCED on the stock INVARIANTS kernel #0: an unprivileged ftruncate(2) on a crafted HPFS image (fnode with AB_NODES root alblk pointing at a 5000-sector AlSec chain, each an_nextoff=0xFFFFFFFF) drives hpfs_truncatealblk into unbounded recursion until the kernel stack guard page double-faults: console shows 'DOUBLE FAULT - KERNEL STACK GUARD HIT! / panic: double fault', rsp exactly on the guard. Root cause: hpfs_alsubr.c:760 loop condition (bn <= an_nextoff) is always true for bn=0, :770-775 recursion has no depth limit/visited-set; entry hpfs_vnops.c:559-569 -> hpfs_subr.c:774. Deterministic DoS/panic under the crafted-image threat model (RW mount required - stock mount_hpfs(8) forces MNT_RDONLY, so a direct mount(2) helper is needed, DF-0859 precedent). Fix (depth limit 32) validated on a rebuilt kernel: same trigger returns EINVAL, no panic.