# 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.
