# 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
