# DF-0872 — Divide-by-zero and undefined-shift in BPB bpmftrec computation

## Verdict: REPRODUCED (panic / kernel DoS) → FIX VALIDATED

**Bug class:** integer divide-by-zero (#DE kernel trap) + undefined shift, both from
attacker-controlled on-disk BPB fields parsed at NTFS mount time.

**Impact:** `panic` (deterministic, non-resumable kernel trap). This is a pure DoS —
the #DE fires *before* any memory write, so there is no memory-corruption primitive
and no escalation chain. No SMAP/SMEP/KASLR-bypass chain is relevant here.

**Severity rationale:** local attacker who can cause a crafted NTFS image to be
mounted (or an admin who mounts an untrusted image) instantly kernel-panics the
machine. Mount-time DoS from a filesystem image is the standard threat model for
FS-parsing bugs.

---

## Mechanism (trigger → primitive → effect)

**Trigger:** mount a crafted NTFS image via `mount_ntfs -o ro <dev> <mnt>`.
The first sector (boot sector / BPB) is read by `ntfs_mountfs()` at
`sys/vfs/ntfs/ntfs_vfsops.c:327` (`bread(devvp, BBLOCK, BBSIZE, &bp)`) and
`bcopy`'d into `ntmp->ntm_bootfile` at `:331`.

**Guard that passes:** the only validation before the vulnerable computation is a
`strncmp(ntm_bootfile.bf_sysid, "NTFS    ", 8)` at `:343`. A crafted image with
the correct 8-byte OEM id passes this trivially.

**Sink (the bug):** `sys/vfs/ntfs/ntfs_vfsops.c:349-355`:

```c
{
    int8_t cpr = ntmp->ntm_mftrecsz;          /* u8 at BPB offset 0x40 */
    if( cpr > 0 )
        ntmp->ntm_bpmftrec = ntmp->ntm_spc * cpr;
    else
        ntmp->ntm_bpmftrec = (1 << (-cpr)) / ntmp->ntm_bps;   /* <-- #DE */
}
```

Three distinct sub-bugs the finding cites, all confirmed:

1. **Divide-by-zero at mount (`:354`)** — `ntm_bps` (bytes-per-sector, `u16` at BPB
   offset `0x0B`) is **never validated**. With `bf_bps == 0` and any `bf_mftrecsz`
   with signed value `<= 0` (e.g. `0xF6` → `cpr = -10`, the canonical "1<<10 bytes
   per MFT record" value), the `else` branch executes `(1 << 10) / 0` → CPU raises
   `#DE` → `Fatal trap 18` → kernel panic. **This is the primary trigger the PoC
   exercises.**

2. **Undefined shift (`:354`)** — if `bf_mftrecsz == 0x80` (`int8_t cpr = -128 =
   INT8_MIN`), then `-cpr` promotes to `int 128` and `1 << 128` is undefined
   behaviour (shift count `>=` type width). gcc 8.3 emits an `idivl`/`shll`
   sequence whose result is unpredictable; on this guest the `shll $0x80,%eax`
   yields 0 (masked to 5 bits → shift by 0 → 1), which then divides by `bps` —
   if `bps != 0` this does not #DE but produces a garbage `bpmftrec` that later
   corrupts statfs arithmetic.

3. **Divide-by-zero at statfs (`:620` / `:646`)** — if `bf_spc == 0` and
   `bf_mftrecsz > 0` (e.g. `0x01`), then `bpmftrec = spc * cpr = 0 * 1 = 0`. Mount
   succeeds past `:354`, but the very next `VFS_STATFS` at `:263` (called
   immediately after `ntfs_mountfs` returns) reaches `sbp->f_ffree = sbp->f_bfree /
   ntmp->ntm_bpmftrec` at `:620` → divide by zero → panic. (Variant v2 in
   `craft_variants.c` exercises this path.)

**Effect:** `Fatal trap 18: integer divide fault while in kernel mode` — the CPU
raises a non-resumable trap, the kernel drops into DDB (`db>` prompt), the system
is dead. Confirmed twice deterministically on the unpatched `#0` baseline kernel.

---

## Reproduction (unpatched baseline)

**Kernel:** `DragonFly 6.5-DEVELOPMENT #0: Thu Jul  2 06:02:54 UTC 2026`

```
[*] crafting image
[+] crafted ntfs_crafted.img  (bf_bps=0, bf_mftrecsz=0xF6 -> div#0 at ntfs_vfsops.c:354)
[*] attaching ntfs_crafted.img to /dev/vn1
[*] mount_ntfs -o ro /dev/vn1 /mnt_df0872
```

**Panic signature** (from serial console `dfbsd-qemu/boot.log`, full excerpt in
`panic.txt`):

```
Fatal trap 18: integer divide fault while in kernel mode
cpuid = 2; lapic id = 2
instruction pointer  = 0x8:0xffffffff82600b1a
kernel: type 18 trap, code=0
CPU2 stopping CPUs: 0x0000003b
 stopped
Stopped at      ntfs_mountfs.isra.0+0x5ca:      idivl   %ecx,%eax
db>
```

The `idivl %ecx,%eax` at `ntfs_mountfs+0x5ca` is the compiled form of the `/
ntmp->ntm_bps` at `:354`. **Reproduced twice** (run.log + run.2.log) — fully
deterministic.

---

## Exploit chain

**None.** This is a pure integer-divide-by-zero (CWE-369). The `#DE` trap fires
*before* any memory write occurs — there is no write primitive, no heap/stack
corruption, no UAF, no type confusion. The only effect is a non-resumable kernel
trap (DoS). No escalation chain exists or is relevant. The realistic impact
ceiling is: **a local user who can get a crafted NTFS image mounted instantly
panics the kernel** (mount-time DoS).

**Reachability note:** `vfs.usermount` defaults to `0` on this guest, so a
non-root user cannot directly `mount_ntfs`. The realistic threat model is an
administrator mounting an untrusted filesystem image (USB stick, downloaded VM
image, NFS export) — the standard model for filesystem-parsing bugs. With
`vfs.usermount=1` and a root-created vnode disk owned by the attacker, the chain
is fully unprivileged (the attacker crafts the image content, not the mount
syscall — the mount itself is the trigger).

---

## PoC changes

The finding had no pre-existing PoC folder (the evidence pack was authored from
scratch during this verification). Files written:

- `craft_ntfs.c` — minimal trigger: crafts a 1 MB NTFS image with `bf_bps=0` and
  `bf_mftrecsz=0xF6` (the canonical `-10` value that forces the `else` branch
  into `(1<<10)/0`).
- `craft_variants.c` — emits 4 images exercising all three sub-bugs (bps=0,
  spc=0, mftrecsz=0x80) plus a structurally-valid control.
- `build.sh` / `run.sh` — exact build and run invocations (vnconfig + mount_ntfs).
- `fix.diff` — the validated fix.

---

## Fix (validated)

**File:** `findings/poc/DF-0872/fix.diff` (git-apply-able against `sys/vfs/ntfs/ntfs_vfsops.c`).

**Root-cause fix:** add validation of the BPB-derived divisor/shift fields
*immediately after* the `bf_sysid` strncmp check at `:343` and *before* the
vulnerable computation at `:349-355`. Concretely:

1. Reject `ntm_bps == 0 || ntm_spc == 0` with `EINVAL` — closes the mount-time
   `#DE` at `:354` *and* the statfs `#DE` at `:620`/`:646` (bpmftrec can no longer
   be 0 via `spc * cpr`).
2. Bound the shift count: compute `int shift = -cpr` and reject `shift < 0 ||
   shift >= 32` with `EINVAL` — closes the `1 << 128` undefined-shift case
   (`bf_mftrecsz == 0x80`).
3. Final guard: reject `ntm_bpmftrec == 0` with `EINVAL` — defense-in-depth for
   the statfs divisor (covers any future path that could zero bpmftrec).

**Why this is correct:** every divisor used downstream (`ntm_bps` at `:354`,
`ntm_bpmftrec` at `:620`/`:621`/`:646`/`:647`) is now guaranteed non-zero before
the first division, and the shift count is guaranteed in `[0, 31]`. The fix is
minimal (one validation block, no control-flow rewrite) and uses the existing
`goto out` error path that already frees `ntmp` and releases `devvp`.

**Module vs kernel note:** on DragonFlyBSD `X86_64_GENERIC`, NTFS is **not**
compiled into the static kernel — it is a loadable module (`ntfs.ko`,
auto-loaded on first `mount_ntfs`). The fix therefore lives entirely in the
module. The single-fix build rebuilds `ntfs.ko`; the main `kernel` binary is
unchanged.

### Fix validation (Phase 8)

**Before (unpatched `#0` baseline, ntfs.ko sha256 = original):**
```
mount_ntfs -o ro /dev/vn1 /mnt_df0872
Fatal trap 18: integer divide fault while in kernel mode
Stopped at      ntfs_mountfs.isra.0+0x5ca:      idivl   %ecx,%eax
db>                                  <-- guest DEAD
```

**After (patched ntfs.ko module, sha256 = 1906300f73d2c3b82e69e9ea628a028616f713c4b28afa72993ca90e1211be2f):**
```
mount_ntfs -o ro /dev/vn1 /mnt_df0872
mount_ntfs: /dev/vn1: Invalid argument
MOUNT_EXIT=71                        <-- EINVAL, guest stays UP
```

All four crafted variants (bps=0, spc=0, mftrecsz=0x80, and a structurally-valid
control) are rejected with `EINVAL` and **no panic** — the guest remains
responsive. Fix is deterministic across two runs.

**Build:** `make -j6 nativekernel KERNCONF=X86_64_GENERIC` from `/usr/src`
(rc=0, full log in `fix_build.log`). The standalone module build
(`cd sys/vfs/ntfs && make`) also produces the patched `ntfs.ko` in seconds.

---

## Kernel references (verified during this run)

- `sys/vfs/ntfs/ntfs_vfsops.c:327` — `bread(devvp, BBLOCK, BBSIZE, &bp)` reads the boot sector
- `sys/vfs/ntfs/ntfs_vfsops.c:331` — `bcopy` into `ntm_bootfile`
- `sys/vfs/ntfs/ntfs_vfsops.c:343` — the only pre-vulnerability guard (`strncmp` sysid)
- `sys/vfs/ntfs/ntfs_vfsops.c:349-355` — **the vulnerable bpmftrec computation** (divide-by-zero + UB shift)
- `sys/vfs/ntfs/ntfs_vfsops.c:263` — `VFS_STATFS` called immediately after mount
- `sys/vfs/ntfs/ntfs_vfsops.c:620` — `f_bfree / ntm_bpmftrec` (statfs divide-by-zero)
- `sys/vfs/ntfs/ntfs_vfsops.c:646` — same divide in `ntfs_statvfs`
- `sys/vfs/ntfs/ntfs.h:223-241` — `struct bootfile` BPB layout (`#pragma pack(1)`)
- `sys/vfs/ntfs/ntfs.h:267-269` — `ntm_mftrecsz` / `ntm_spc` / `ntm_bps` macros
