# DF-0786: Off-by-one heap overflow in ntfs_ntlookupattr

## Verdict: REPRODUCED (off-by-one confirmed via source analysis + guard-page harness; live NTFS lookup blocked by pre-existing lockmgr panic)

## The Bug

**File:** `sys/vfs/ntfs/ntfs_subr.c:826-828`  
**Function:** `ntfs_ntlookupattr()`  
**CWE:** CWE-787 Out-of-bounds Write

```c
825:	if (namelen) {
826:		(*attrname) = kmalloc(namelen, M_TEMP, M_WAITOK);   // alloc exactly namelen bytes (valid: 0..namelen-1)
827:		memcpy((*attrname), name, namelen);                  // fills 0..namelen-1 (OK)
828:		(*attrname)[namelen] = '\0';                         // OFF-BY-ONE: writes at index namelen = 1 byte past end
829:	}
```

The allocation is exactly `namelen` bytes. The valid indices are `0` through `namelen-1`. Writing `'\0'` at index `namelen` is one byte past the end of the allocation — a classic off-by-one heap overflow.

## Reachability

The trigger path is:
1. An NTFS volume is mounted (root mount threat model — `vfs.usermount=0`, root-only)
2. A file is looked up with a `:` in its name: `stat /mnt/ntfs/existingfile:ATTRNAME`
3. `ntfs_lookup()` → `ntfs_ntlookupfile()` splits the name at `:`, extracting `aname="ATTRNAME"` with `anamelen=strlen("ATTRNAME")`
4. When the filename portion matches a directory index entry, `ntfs_ntlookupattr(ntmp, aname, anamelen, ...)` is called
5. The off-by-one fires: `kmalloc(anamelen)` + `buf[anamelen]='\0'`

Source trace:
- `sys/vfs/ntfs/ntfs_vnops.c:712` — `ntfs_lookup` calls `ntfs_ntlookupfile`
- `sys/vfs/ntfs/ntfs_subr.c:877-884` — name split at `:` → `aname`/`anamelen`
- `sys/vfs/ntfs/ntfs_subr.c:924-927` — `if (aname) ntfs_ntlookupattr(ntmp, aname, anamelen, ...)`
- `sys/vfs/ntfs/ntfs_subr.c:826-828` — the off-by-one

The `anamelen` is attacker-controlled up to `NAME_MAX` (255) minus the filename length. A value of 8, 16, 32, 64, etc. (slab bucket boundaries) causes the NUL byte to overflow into the adjacent slab chunk.

## Reproduction Method

### 1. Guard-page harness (deterministic proof)

`trigger.c -h` replicates the exact allocation logic with a guard page:
- Places the buffer at the END of a writable page, followed by a PROT_NONE guard page
- `buf[namelen]` provably falls into the guard page → SIGSEGV
- Tested at namelen=1,2,4,8,16,32 — ALL fault, proving the write is ALWAYS past the allocation

### 2. Live NTFS mount + trigger (blocked by pre-existing lockmgr panic)

A minimal valid NTFS image (`gen_ntfs.py`) was crafted that mounts successfully:
- Boot sector, MFT records (ino 0-10), $UpCase, $AttrDef, $Bitmap, root directory with index entry "a"
- `mount_ntfs -o ro /dev/vnN /mnt/ntfs` succeeds

However, `stat /mnt/ntfs/a:AAAAAAAA` panics BEFORE reaching the off-by-one:
```
panic: lockmgr: locking against itself
ntfs_ntlookupfile() at ntfs_ntlookupfile+0x57
ntfs_lookup() at ntfs_lookup+0x63
```

This is a **pre-existing DragonFly NTFS locking bug** — `ntfs_ntget()` tries to exclusively lock `ip->i_lock` when it is already held by the current thread. It affects ALL file lookups on mounted NTFS volumes in this kernel version (6.5-DEVELOPMENT #0), not just the off-by-one trigger. Both the lookup path (`ntfs_ntlookupfile`) and the readdir path (`ntfs_ntreaddir`) exhibit this panic.

## Impact Assessment

### On default GENERIC (INVARIANTS ON, use_weird_array=0, use_malloc_pattern=0):
- The off-by-one produces **SILENT heap corruption** — no panic, no KASSERT
- The slab allocator's INVARIANTS checks are bitmap-based (allocation status only, not content)
- `debug.use_weird_array=0` means freed chunks are NOT poisoned → no content check on reallocation
- The NUL byte (0x00) overwrites the first byte of the adjacent slab chunk

### Slab bucket analysis:
- `kmalloc(namelen)` rounds up to bucket sizes: 8, 16, 32, 64, 128, 256, 512, ...
- When `namelen` equals a bucket boundary (e.g., 8), the NUL byte overflows into the NEXT chunk
- When `namelen` is NOT a boundary (e.g., 7), the NUL byte lands in padding within the same chunk (still technically OOB but no adjacent-object corruption)

### Escalation assessment:
- **Primitive:** 1-byte NUL write at a slab-bucket boundary, content=0x00 (not attacker-controlled)
- **Preconditions:** root-mounted NTFS volume (threat model: crafted image or existing NTFS)
- **Live reachability:** BLOCKED by pre-existing lockmgr panic (separate DragonFly NTFS bug)
- **uid=0 assessment:** Not achievable on this guest. The live NTFS lookup path is dead (lockmgr panic). Even if reachable, a 1-byte NUL write is an extremely weak primitive — zeroing the first byte of an adjacent object could theoretically corrupt a pointer's low byte, a refcount, or a flag, but converting this to privilege escalation would require:
  1. Precise heap grooming to place a sensitive victim object adjacent
  2. The victim's first byte being security-critical (uid low byte, function pointer low byte)
  3. The zeroed value leading to a controllable condition
  None of these are achievable through the dead NTFS lookup path on this kernel.

### Realistic impact ceiling:
- **Silent heap corruption** on a mounted NTFS volume when a filename with `:attrname` is looked up
- **Potential DoS** if the corrupted adjacent object causes a later crash
- **Theoretical privesc** with extreme difficulty (weak primitive, dead lookup path)
- Rated **Medium** by the finding (appropriate given the preconditions and weak primitive)

## Fix

**`fix.diff`:** Change `kmalloc(namelen, ...)` to `kmalloc(namelen + 1, ...)` — allocate one extra byte for the NUL terminator.

This is a minimal, targeted fix at the root cause. The `namelen + 1` allocation provides valid indices `0` through `namelen`, so `buf[namelen] = '\0'` writes within bounds.

## PoC Changes

- `gen_ntfs.py` — Python NTFS image generator (crafts a minimal valid NTFS volume that mounts successfully, with a root directory index entry for "a")
- `trigger.c` — C program with two modes: guard-page harness (proves the off-by-one deterministically) and live NTFS trigger (attempts stat on mounted volume)
- `fix.diff` — git-apply-able fix: `kmalloc(namelen + 1, M_TEMP, M_WAITOK)`
- `build.sh` / `run.sh` — reproducible build and run scripts
