# DF-0785 — ntfs_ntlookupfile heap buffer overflow — PoC

Reproduction package for DragonFlyBSD `ntfs_ntlookupfile` heap OOB write
(CWE-787). Verified on `6.5-DEVELOPMENT #0` (X86_64_GENERIC, INVARIANTS ON).

## Bug

`sys/vfs/ntfs/ntfs_subr.c` `ntfs_ntlookupfile`:

```c
blsize = vap->va_a_iroot->ir_size;                 /* :867  allocation size  */
rdsize = vap->va_datalen;                          /* :868  copy size        */
...
rdbuf = kmalloc(blsize, M_TEMP, M_WAITOK);         /* :888                   */
error = ntfs_readattr(ntmp, ip, NTFS_A_INDXROOT, "$I30",
                      0, rdsize, rdbuf, NULL);      /* :890-891 copies rdsize */
```

`ir_size` (an on-disk `u_int32_t` inside the resident `$INDEX_ROOT` header)
sizes the buffer; `va_datalen` (a *separate* on-disk field, the resident
attribute data length) sizes the copy. **No check `rdsize <= blsize` exists.**
A crafted NTFS image sets `ir_size < va_datalen`, so `ntfs_readattr`'s
`memcpy` writes `rdsize` attacker-controlled bytes into a `blsize`-byte slab
object → heap overflow.

The sibling reader `ntfs_ntreaddir` sizes correctly:
```c
fp->f_dirblbuf = kmalloc(max(vap->va_datalen, fp->f_dirblsz), M_NTFSDIR, M_WAITOK); /* :1105 */
```

Note `ntfs_readattr`'s own guard (`ntfs_subr.c:1671-1676`,
`roff + rsize > va_datalen`) does **not** stop this: the buggy call passes
`rsize = rdsize = va_datalen`, so `rdsize > va_datalen` is always false. The
guard never sees the *buffer* size.

## Trigger

Mount a crafted NTFS image (root `$INDEX_ROOT` with `ir_size < va_datalen`),
then issue any non-`.` / non-`..` name lookup into the volume — `stat /mnt/x`,
`ls /mnt/realfile`, etc. — which reaches `ntfs_lookup → ntfs_ntlookupfile`.

Mount is privileged (`SYSCAP_RESTRICTEDROOT`); the post-mount name lookup is
unprivileged — same threat model as the ext2/hammer image findings
(`vfs.usermount=1` + a root-created attacker-owned image is a realistic
precondition).

## Reproduce

```sh
./build.sh                       # craft image + compile harnesses (guest cc)
./run.sh                         # A) harness before/after, B) live kernel overflow
```

`run.sh` does two things:

**A. Deterministic harness** (`harness.c` / `harness_fixed.c`) — transcribes the
exact `kmalloc(blsize)` + `ntfs_readattr(rdsize)` copy with a guard-paged
allocator and faults byte-exactly at the overflow. No slab luck required.
- unfixed: `Segmentation fault` (exit **139**) — overflow into PROT_NONE page.
- fixed:   clean completion (exit **0**) — `kmalloc(max(blsize,rdsize))` fits.

**B. Live kernel overflow** (unfixed `ntfs.ko`) — mount the crafted image then
~200 unprivileged name lookups; the heap overflow corrupts the M_TEMP slab
free-list and the periodic `slab_cleanup` timer trips the INVARIANTS
zone-alignment assertion:

```
panic: assertion "(((intptr_t)chunk ^ (intptr_t)z) & ZoneMask) == 0"
       failed in chunk_mark_free at kern_slaballoc.c:1675
chunk_mark_free() -> slab_cleanup() -> slotimer_callback() -> softclock_handler()
```

## Expected

| kernel / module           | harness          | live (mount + 200 lookups)            |
|---------------------------|------------------|---------------------------------------|
| unfixed `#0` GENERIC      | SIGSEGV (139)    | **panic** in `chunk_mark_free`        |
| fixed `ntfs.ko`           | clean (0)        | clean ENOENT, guest UP, 0 panics      |

## Impact

Heap OOB write, **fully attacker-controlled content** (the crafted resident
`$INDEX_ROOT` data), attacker-controlled size (`ir_size` picks the slab bucket,
`va_datalen` picks the overflow extent). On GENERIC INVARIANTS a single lookup
corrupts silently (DragonFly slab tracks allocation in a zone bitmap and has no
content canary); under slab churn it panics. Either way it is a groomable
arbitrary-write primitive in a heavily-used zone (`M_TEMP`) — the classic
material for `uid=0` given the audit guest's SMAP/SMEP/KASLR-off posture.

## Fix

`findings/poc/DF-0785/fix.diff` — one line, matching the sibling `ntfs_ntreaddir`:

```diff
-	rdbuf = kmalloc(blsize, M_TEMP, M_WAITOK);
+	rdbuf = kmalloc(max(blsize, rdsize), M_TEMP, M_WAITOK);
```

Validated by rebuilding `ntfs.ko` with the fix, hot-swapping it, and re-running
the identical workload: no panic, guest stays up (`fix_run.log`).
