# DF-0790 — ATTRLIST parsing DoS in `ntfs_ntvattrget`: reclen==0 infinite loop / reclen>len NULL deref

## Verdict: REPRODUCED (live kernel hang on default GENERIC #0 + deterministic userspace harness) → FIX VALIDATED

## The bug (confirmed by source trace + live hang + harness)

**File:** `sys/vfs/ntfs/ntfs_subr.c:196-236` (function `ntfs_ntvattrget`)
**Class:** CWE-835 (infinite loop) + CWE-476 (NULL deref) — both stem from the same missing bounds check on `aalp->reclen`.
**Severity (per finding):** Medium. Confirmed: realistic impact ceiling is **kernel hang / local DoS** (no write primitive, no escalation).

```c
186:    len = lvap->va_datalen;
187:    alpool = kmalloc(len, M_TEMP, M_WAITOK);
188:    error = ntfs_readntvattr_plain(ntmp, ip, lvap, 0, len, alpool, &len, NULL);
...
193:    aalp = (struct attr_attrlist *) alpool;
194:    nextaalp = NULL;
196:    for(; len > 0; aalp = nextaalp) {
202:        if (len > aalp->reclen) {
203:            nextaalp = NTFS_NEXTREC(aalp, struct attr_attrlist *);   // = aalp + aalp->reclen
204:        } else {
205:            nextaalp = NULL;
206:        }
207:        len -= aalp->reclen;        // <-- NO CHECK that reclen != 0, no check reclen <= len
...
236:    }
```

`struct attr_attrlist` (`ntfs.h:144-154`): `al_type(u32) reclen(u16) al_namelen(u8) al_nameoff(u8) al_vcnstart(u64) al_inumber(u32) reserved(u32) al_index(u16) al_name[1](u16)`.

`NTFS_NEXTREC` (`ntfs.h:273`): `#define NTFS_NEXTREC(s, type) ((type)(((caddr_t) s) + (s)->reclen))`.

Two malformed-image shapes, both unguarded:

| Variant | Crafted bytes | Effect in kernel |
|---|---|---|
| **loop** | ATTRLIST entry with `reclen == 0` | `len -= 0` (unchanged); `nextaalp = aalp + 0 == aalp`; the `for()` never advances → **infinite loop** (CWE-835). Kernel spins at 100% CPU; `mount(2)` never returns. |
| **null** | ATTRLIST entry with `reclen > len` | the `else` sets `nextaalp = NULL`; `len -= reclen` **underflows** `size_t` to ~2^64; next iteration: `len > 0` true, `aalp = NULL`; the `len > aalp->reclen` check dereferences NULL → **panic** (CWE-476). |

## Reachability (mount-time — before the sibling lockmgr panic)

`ntfs_ntvattrget`'s ATTRLIST walk is entered when a requested attribute is NOT found inline in the MFT
record but a `$ATTRIBUTE_LIST` (type 0x20) IS present (`ntfs_findvattr` returns -1 with
`lvap` = the ATTRLIST ntvattr, `ntfs_subr.c:133`). The walk then reads the ATTRLIST data into a
heap buffer (`alpool`) and walks it looking for the attribute in other MFT records.

The **mount-time trigger** fires during `ntfs_mountfs`:

```
sys/vfs/ntfs/ntfs_vfsops.c:394-396   ntfs_mountfs() loops over {NTFS_MFTINO, NTFS_ROOTINO, NTFS_BITMAPINO}
sys/vfs/ntfs/ntfs_vfsops.c:396          VFS_VGET(mp, NULL, NTFS_BITMAPINO, ...)
sys/vfs/ntfs/ntfs_vfsops.c:796          .vfs_vget = ntfs_vget -> ntfs_vgetex(NTFS_A_DATA)
sys/vfs/ntfs/ntfs_vfsops.c:745-746      ntfs_vgetex -> ntfs_filesize (for regular-file inos)
sys/vfs/ntfs/ntfs_subr.c:1295           ntfs_filesize -> ntfs_ntvattrget(NTFS_A_DATA)
sys/vfs/ntfs/ntfs_subr.c:175            ntfs_findvattr -> returns -1 (no inline $DATA; lvap=ATTRLIST)
sys/vfs/ntfs/ntfs_subr.c:196-236        BUGGY ATTRLIST walk
```

The crafted image places a corrupted `$ATTRIBUTE_LIST` on **ino 6 ($Bitmap)** with **no inline `$DATA`**.
During mount, `ntfs_filesize(ino 6)` calls `ntfs_ntvattrget(NTFS_A_DATA)`, which finds no inline
`$DATA`, enters the ATTRLIST walk, and hits the `reclen==0` infinite loop. This fires **during mount**,
before any directory lookup, so the sibling DF-0786 lockmgr panic (which lives in `ntfs_ntget` on the
directory-lookup path) cannot interfere.

The threat model is the standard filesystem-image model: `vfs.usermount=0` (verified) means the attacker
needs root to issue `mount_ntfs` (e.g. an admin auto-mounting an untrusted USB stick / image, or a
crafted image placed where root will mount it). The post-mount attribute request that reaches
`ntfs_ntvattrget` is unprivileged (any local user with execute on the mountpoint).

## Reproduction — live kernel (default GENERIC `#0`)

Trigger (as root):
```
vnconfig -c vn0 /root/ntfs_0790_mount_loop.img
mount_ntfs -o ro /dev/vn0 /mnt/ntfs
```

**Result:** kernel HANG. `mount(2)` never returns; the ssh command timed out after 30s (rc=124);
the guest became completely unresponsive (subsequent ssh status check also timed out, rc=124) —
the kernel is spinning at 100% CPU in the `ntfs_ntvattrget` ATTRLIST walk. **No panic signature**
(this is CWE-835 infinite loop, not a crash); the serial log shows a clean boot followed by the mount
command hanging forever. Confirmed local DoS.

## Reproduction — userspace harness (deterministic complement)

`harness.c` replicates the exact ATTRLIST walk (`ntfs_subr.c:196-236`) against a buffer placed at the
end of a writable page with a `PROT_NONE` guard page after it. Output (run as unprivileged `maxx`):

```
=== BUGGY walk (kernel behaviour on default GENERIC #0) ===
mode=clean       apply_fix=0    -> rc=0  clean exit (ATTRLIST exhausted)
mode=loop        apply_fix=0    -> rc=1  ITERATION CAP HIT -> would be infinite loop in kernel
mode=null        apply_fix=0    -> rc=2  SIGSEGV -> NULL deref / OOB read in kernel
mode=oob_reclen  apply_fix=0    -> rc=2  SIGSEGV -> NULL deref / OOB read in kernel

=== FIXED walk (proposed fix rejects malformed entries) ===
mode=clean       apply_fix=1    -> rc=0  clean exit (ATTRLIST exhausted)
mode=loop        apply_fix=1    -> rc=-1  FIX REJECTED malformed entry (EINVAL in kernel)
mode=null        apply_fix=1    -> rc=-1  FIX REJECTED malformed entry (EINVAL in kernel)
mode=oob_reclen  apply_fix=1    -> rc=-1  FIX REJECTED malformed entry (EINVAL in kernel)
```

- `loop` confirms the **infinite-loop** branch (reclen==0, caught by iteration cap that the kernel does NOT have).
- `null` confirms the **NULL-deref** branch (reclen>len → size_t underflow → nextaalp==NULL → SIGSEGV).
- `oob_reclen` confirms a bonus **OOB-read** branch (reclen pushes next ptr past buffer end → guard-page fault).

## Escalation assessment (no chain possible — loop/NULL-deref only)

This is a **control-flow** primitive (infinite loop) or a **NULL deref** (panic). There is no write to
attacker-chosen kernel memory: the only effect is hanging the calling thread or crashing the kernel.
Per Phase 6, a pure DoS/panic primitive has no escalation chain to `uid=0`; the correct deliverable is
the characterized impact ceiling: **definite local DoS** (kernel hang requiring a reset, or NULL-deref panic).

## Fix — `fix.diff`

Adds a bounds check at the top of the loop body, BEFORE any field dereference or walk advancement:

- Require the fixed entry header to fit in the remaining `len` (`len >= sizeof(struct attr_attrlist) - sizeof(u_int16_t)`,
  i.e. 26 bytes — the documented minimum ATTRLIST entry size, excluding the flexible `al_name[]` tail).
- Require `aalp->reclen` to be sane: at least the header size, and not greater than `len`.
- On violation: set `error = EINVAL`, `kprintf` for diagnostics, `goto out` (clean return; `alpool` freed).

This rejects both malformed shapes:
- `reclen == 0` → caught by `aalp->reclen < min_header` → EINVAL (no infinite loop).
- `reclen > len` → caught by `aalp->reclen > len` → EINVAL (no size_t underflow, no NULL deref).

The fix is **minimal and targeted at the root cause** (the missing reclen bounds check). It does not
change the on-disk format, the `NTFS_AALPCMP` logic, the `ntfs_vgetex` path, or the happy path for
valid images.

## Fix validation (Phase 8) — VALIDATED

`ntfs.ko` was rebuilt standalone (`KERNCONF=X86_64_GENERIC make` in `sys/vfs/ntfs/`, warm obj) and
installed to `/boot/kernel/ntfs.ko`. NTFS is `optional ntfs` (a loadable module, not compiled into
GENERIC), so only the module needed rebuilding — no kernel rebuild or reboot required.

### Before (unpatched `ntfs.ko`, kernel `#0`)
```
mount_ntfs -o ro /dev/vn0 /mnt/ntfs   # ntfs_0790_mount_loop.img
-> mount(2) NEVER RETURNS. ssh timed out after 30s (rc=124).
   Guest completely wedged; kernel spinning in ntfs_ntvattrget.
```

### After (patched `ntfs.ko`, same kernel `#0`)
```
mount_ntfs -o ro /dev/vn0 /mnt/ntfs   # ntfs_0790_mount_loop.img
mount_ntfs: /dev/vn0: Invalid argument   MOUNT_RC=71   (<1s, guest UP)
dmesg: ntfs_ntvattrget: malformed attrlist entry (reclen 0, len 26)

mount_ntfs -o ro /dev/vn2 /mnt/ntfs2  # ntfs_0790_mount_null.img
mount_ntfs: /dev/vn2: Invalid argument   MOUNT_RC=71   (<1s, guest UP)
dmesg: ntfs_ntvattrget: malformed attrlist entry (reclen 32, len 26)
```

### Regression check (clean NTFS image still mounts)
```
mount_ntfs -o ro /dev/vn1 /mnt/ntfs   # clean DF-0786 ntfs.img
-> MOUNT_RC=0   /dev/vn1 on /mnt/ntfs (ntfs, read-only, local)
```

The fix closes the bug on the default GENERIC kernel without regressing valid images.

## PoC changes

- `harness.c` — userspace replication of `ntfs_ntvattrget:196-236` against a guard-page-backed ATTRLIST
  buffer. Accepts `clean|loop|null|oob_reclen` mode and an optional `apply_fix` flag that runs the
  proposed fixed walker for direct before/after comparison.
- `gen_ntfs_0790.py` — crafted-image generator building on the proven DF-0786 NTFS scaffolding.
  Two trigger variants:
  - ino-5 (root dir) variant: `loop`/`null` modes corrupt the root dir's `$ATTRIBUTE_LIST`.
  - **mount-time** variant: `mount_loop`/`mount_null` place the corrupted `$ATTRIBUTE_LIST` on ino 6
    ($Bitmap) with no inline `$DATA`, so the bug fires DURING mount via `ntfs_filesize` — before the
    sibling DF-0786 lockmgr panic can interfere.
- `build.sh` / `run.sh` — exact reproducible build & run. `run.sh` runs the harness (as maxx);
  the live mount trigger requires root and is documented in `hang_evidence.txt`.
- `fix.diff` — standalone, `git apply`-able unified diff fixing the bug at the root cause.

## How to reproduce (for a teammate)

```sh
# 1. userspace harness (deterministic, no root)
scp -F dfbsd-qemu/config -q findings/poc/DF-0790/{harness.c,build.sh,run.sh} dfbsd-maxx:poc/DF-0790/
ssh -F dfbsd-qemu/config dfbsd-maxx 'cd poc/DF-0790 && sh build.sh && sh run.sh'

# 2. live kernel reproduction (root, EXPECTS HANG — guest dies, must reset after)
python3 findings/poc/DF-0790/gen_ntfs_0790.py mount_loop /tmp/ntfs_0790_mount_loop.img
scp -F dfbsd-qemu/config -q /tmp/ntfs_0790_mount_loop.img dfbsd:/root/
./dfbsd-qemu/vm.sh run_root 'vnconfig -c vn0 /root/ntfs_0790_mount_loop.img && mount_ntfs -o ro /dev/vn0 /mnt/ntfs'
# ^HANGS FOREVER — guest wedged
./dfbsd-qemu/vm.sh reset with-src

# 3. fix validation (rebuild ntfs.ko, install, re-run same PoC)
scp -F dfbsd-qemu/config -q findings/poc/DF-0790/fix.diff dfbsd:/root/
./dfbsd-qemu/vm.sh run_root 'cd /usr/src && patch -p1 < /root/fix.diff && cd sys/vfs/ntfs && KERNCONF=X86_64_GENERIC make && cp ntfs.ko /boot/kernel/ntfs.ko'
# re-run step 2; mount now fails cleanly with EINVAL, guest stays UP
```
