# DF-2555: NTFS $AttrDef heap buffer overflow — VERDICT

## Verdict: REPRODUCED (heap overflow confirmed, fix validated)

## Mechanism

The `$AttrDef` translation loop at `sys/vfs/ntfs/ntfs_vfsops.c:457-461` copies
wchar attribute names from the on-disk `$AttrDef` file into the in-memory
`ntvattrdef` structure using an unbounded `do/while` loop:

```c
j = 0;
do {
    ntmp->ntm_ad[i].ad_name[j] = ad.ad_name[j];  // char ← wchar (truncation)
} while(ad.ad_name[j++]);                          // no bounds check on j
```

The source `ad.ad_name` is `wchar[NTFS_ATTRNAME_MAXLEN]` = `u_int16_t[0x40]` =
**128 bytes** (64 wchars, `sys/vfs/ntfs/ntfs.h:207`).
The destination `ntm_ad[i].ad_name` is `char[0x40]` = **64 bytes**
(`sys/vfs/ntfs/ntfs.h:216`).

When a crafted NTFS image has an `$AttrDef` entry whose name field (64 wchars)
is entirely non-zero AND whose following struct fields (`ad_type`, `reserved1`,
`ad_flag`, `ad_minlen`, `ad_maxlen` — 32 more bytes = 16 wchars) are also
non-zero, the loop continues past the 64-byte destination buffer. After the
source struct ends (160 bytes), the loop reads stack residue until a zero wchar
is encountered.

**Confirmed with instrumented module:**
- Safe image (null-terminated name): `j=21` (within 64-byte limit) ✓
- Evil image (all non-zero): `j=80` → **16-byte heap overflow** past the 64-byte
  `ad_name` buffer, writing 8 bytes past the 72-byte `struct ntvattrdef`
  allocation into adjacent slab memory.

## Primitive characterization

- **Write size:** up to 16+ bytes past the 64-byte destination buffer
- **Allocation:** `kmalloc(num * sizeof(struct ntvattrdef), M_NTFSMNT, M_WAITOK)`
  where `sizeof(struct ntvattrdef)` = 72 bytes (`char[64] + int + uint32_t`)
- **Slab zone:** zoneindex(72) → 8-byte-aligned chunks, zone index 8, chunk
  size exactly 72 bytes. Overflow of 8 bytes goes into the adjacent slab chunk.
- **Content:** attacker-controlled (truncated wchar values from crafted image)
- **Trigger:** root mounts a crafted NTFS image (e.g. from removable media)
- **Impact ceiling:** heap corruption of adjacent slab objects; root-triggered
  at mount time (valid hard blocker for uid0 escalation — the corruption is in
  the mount path, not an unprivileged syscall surface)

## Exploit chain

Not escalated to uid0. The overflow fires in the **mount path** (`ntfs_mountfs`),
which requires root to issue `mount_ntfs`. This is a **root-triggered heap
overflow** — the privilege boundary crossed is root→kernel, not unpriv→root.
Per the Phase 6 bright-line rule, the valid hard blocker applies: the vulnerable
code path is reachable only from a root mount operation. A malicious image on
removable media auto-mounted by an admin is the realistic threat model (Medium
severity, consistent with the finding's classification).

The primitive IS characterized: 8-16 bytes of attacker-controlled heap overflow
into adjacent 72-byte slab chunks, silently corrupting heap metadata on the
default GENERIC kernel (INVARIANTS does not detect intra-chunk overflows with
DragonFlyBSD's bitmap-based slab tracking).

## Fix

Replace the unbounded `do/while` with a bounded `for` loop:

```c
/* Before (vulnerable): */
j = 0;
do {
    ntmp->ntm_ad[i].ad_name[j] = ad.ad_name[j];
} while(ad.ad_name[j++]);
ntmp->ntm_ad[i].ad_namelen = j - 1;

/* After (fixed): */
for (j = 0; j < (int)sizeof(ntmp->ntm_ad[i].ad_name) - 1 &&
     ad.ad_name[j] != 0; j++)
    ntmp->ntm_ad[i].ad_name[j] = ad.ad_name[j];
ntmp->ntm_ad[i].ad_name[j] = '\0';
ntmp->ntm_ad[i].ad_namelen = j;
```

The fix bounds j to `sizeof(ad_name) - 1 = 63`, ensuring the loop never writes
past the 64-byte buffer. Names longer than 63 characters are truncated and
null-terminated.

## PoC changes

Authored `gen_image.py` from scratch (the PoC dir was empty). The Python script
generates a minimal but valid NTFS image with:
- Valid boot sector (OEM ID "NTFS    ", BPB with bps=512, spc=1, mftrecsz=2)
- MFT entries 0-10 with proper fixup arrays
- Entry 4 ($AttrDef) with a crafted resident $DATA containing one entry with all
  160 bytes set to 0x41 (non-zero) + a zero terminator entry
- Entry 5 ($Root) with a properly named ($I30) INDEX_ROOT for an empty directory
- Entry 6 ($Bitmap) with cluster bitmap
- Entry 10 ($UpCase) with non-resident $DATA containing a 128KB toupper table

Also generated `safe.ntfs` as a control image with a normal null-terminated name.

## Fix validation

- **Baseline (#0 kernel, unpatched):** evil image mounts; instrumented module
  shows `j=80` (16-byte overflow) ← **BUG PRESENT**
- **Patched (#1 kernel, with fix.diff):** evil image mounts; instrumented module
  shows `j=63` (bounded, no overflow) ← **BUG FIXED**
- **Safe image on both:** `j=21` (within bounds) ← CONTROL OK

The fix.diff applies cleanly, compiles, and the single-fix kernel boots and
mounts both images correctly without panic.
