# DF-2455 — FD_STYPE accepts unvalidated fd_type → kernel divide-by-zero / shift-UB (defense-in-depth fix)

## Verdict
**NOT REPRODUCED** on this audit guest — valid hard blocker: the floppy path is
**HW-gated (no ISA FDC)** AND **config-gated (no `device fd` in X86_64_GENERIC)**,
so `/dev/fd0` does not exist and the `fd` driver is not even compiled in.  The
bug is **real in source** (confirmed by code trace) and the `fix.diff` is a
defense-in-depth guard that **compiles cleanly in-tree** (verified by a full
`nativekernel` build with `device fd` added).

## Mechanism (code trace, `sys/dev/disk/fd/fd.c`)

The `FD_STYPE` ioctl stores a fully caller-supplied `struct fd_type` into
`fd->ft` with **no field validation** (`fd.c:2325-2330`):

```c
case FD_STYPE:                  /* set drive type */
    if (caps_priv_check(ap->a_cred, SYSCAP_RESTRICTEDROOT))
        return EPERM;            /* <-- root-only */
    fd->ft = *(struct fd_type *)ap->a_data;   /* no bounds check */
    break;
```

Those fields are then used as **divisors** and **shift counts** on the next
open/I/O:

- `fd.c:1069-1071` and `fd.c:1435-1437`:
  `d_secpercyl = ft->sectrac * ft->heads;`
  `d_ncylinders = ft->size / info.d_secpercyl;`  → **divide by zero** if
  `sectrac==0` or `heads==0`.
- `fdstate` does `blknum/(sectrac*heads)` and `sec/sectrac` (same divisor
  family).
- `fd.c:1484`: `fdblk = 128 << (fd->ft.secsize);` → **signed-shift overflow /
  UB** for `secsize >= 25`, and shift-count >= width for `secsize >= 32`.

So a planted type with `sectrac=0` (or `heads=0`) arms a kernel **divide-by-zero
trap**; `secsize>=25` is **signed-shift UB**.  FD_STYPE is root-gated
(`caps_priv_check(SYSCAP_RESTRICTEDROOT)`), so the unprivileged angle is the
"root arms a trap, operator-group user trips it on open()" scenario from the
finding summary — a real but narrow, root-assisted threat.

## Why not reproduced (valid hard blocker)

The "dead/unreachable at runtime on this guest AND no harness can exercise it"
blocker applies — **double**:

1. **HW gate**: this QEMU audit guest has no ISA floppy controller (no `-fda`,
   no FDC).  `dmesg` has zero `fdc`/`floppy` lines; `/dev/fd0` does not exist.
2. **Config gate**: `sys/conf/files` marks `dev/disk/fd/fd.c` as `optional fd`,
   and `X86_64_GENERIC` has no `device fd` (only `natapifd`, the ATAPI floppy).
   The floppy driver is therefore **not compiled into the default kernel** at
   all.

There is no way to reach the ioctl handler without both a compiled-in driver
and a floppy device node.  `./fd_stype /dev/fd0` fails at `open()` with
`ENOENT` — the gate holds; no divide-by-zero.

## Fix (`fix.diff`) — defense-in-depth

Adds validation to `FD_STYPE` before installing the type: require
`sectrac > 0`, `heads > 0`, and `0 <= secsize <= 7` (FDC sector-size codes are
0-6; this also forbids the shift-UB range), returning `EINVAL` otherwise.  This
closes the div-by-zero and shift-UB paths regardless of HW presence.  **Matches
the finding's intent** (validate fd_type) as a defense-in-depth guard.

## Fix validation

`not_testable` for runtime (no FDC), but the fix was verified to **apply +
compile in-tree**: a full `make -j6 nativekernel KERNCONF=X86_64_GENERIC` with
`device fd` added to the config (to actually compile `fd.c`) completed with
**`rc=0`, 0 compiler errors**, producing `fd.o` and `kernel.stripped` containing
the patched driver.  By trace, the added `EINVAL` guard sits directly on the
root-cause store path (`fd->ft = *nft` only after the bounds pass), so the
div-by-zero/shift-UB can no longer be armed via FD_STYPE.
