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)
PoC verification
Evidence pack
findings/poc/DF-3101 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 forftruncate(fd,0);an_nextoffis an on-disk u32, so0 <= u32is always true. - hpfs_alsubr.c:764
hpfs_breadalsec()checks onlyAS_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_setattrtruncate 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
fixedBaseline (unpatched #0): double-fault panic. Patched (fix.diff, make nativekernel, reboot): identical unprivileged trigger returns EINVAL, no panic, guest stays up.
fix_run.log
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.
No comments yet.