# DF-2432 — Divide-by-zero kernel panic via CCDIOCSET with ccio_ndisks=0

**Verdict: REPRODUCED (local DoS / kernel panic), then FIXED and VALIDATED.**

## Mechanism (confirmed, path:line)

`CCDIOCSET` (`sys/dev/disk/ccd/ccd.c:1309`, `sys/sys/ccdvar.h:238`) only
validates the component count against its **upper** bound:

```c
if (ccio->ccio_ndisks > CCD_MAXNDISKS) {   /* ccd.c:1319 — no lower bound! */
    ccdunlock(cs);
    return (EINVAL);
}
```

It never rejects `ccio_ndisks == 0`. With zero disks:

1. `kmalloc(0 * sizeof(char *))` and `kmalloc(0 * sizeof(struct vnode *))`
   return the non-NULL `ZERO_LENGTH_PTR`, so the allocation checks pass
   (`ccd.c:1350-1353`); `copyin(..., 0)` is a no-op (`ccd.c:1355`); and the
   component-lookup loop runs zero times (`ccd.c:1372`).
2. `ccdinit()` then runs: `cs->sc_nccdisks = ccd->ccd_ndev = 0`
   (`ccd.c:402`); `kmalloc(0 * sizeof(struct ccdcinfo))` again yields
   `ZERO_LENGTH_PTR` (`ccd.c:405`); and the per-component loop
   (`ccd.c:418`) runs zero times, so **`maxsecsize` stays `0`** (`ccd.c:416`).
3. The pseudo-geometry setup divides by it:
   ```c
   ccg->ccg_secsize = maxsecsize;                     /* = 0 */
   ccg->ccg_nsectors = 1024 * 1024 / ccg->ccg_secsize;/* 1048576 / 0 -> #DE */
   ```
   (`ccd.c:578-580`). If `ccio_ileave > 0` the earlier divide
   `sc_ileave % (maxsecsize / DEV_BSIZE)` == `x % 0` (`ccd.c:515`) faults
   first. Either way the CPU raises a **divide error (#DE, trap 18)** and the
   kernel panics.

## Reproduction (unpatched #0 baseline)

`poc.c` opens `/dev/ccd0` (O_WRONLY, satisfying the `FWRITE` check at
`ccd.c:1313`) and issues `ioctl(fd, CCDIOCSET, &ccio)` with `ccio_ndisks=0`,
`ccio_ileave=0`, `ccio_flags=0`. Run as root after `kldload ccd`.

Observed (`panic.txt`, serial console):
```
Fatal trap 18: integer divide fault while in kernel mode
instruction pointer = 0x8:0xffffffff82600e28
Stopped at      ccdinit+0x578:  divl    %esi,%eax
db>
```
Guest went down (ssh died). 100% reliable.

## Impact ceiling (realistic)

This is a **divide-by-zero DoS**, not memory corruption — no escalation
chain. The trigger requires:
- `kldload ccd` (root), and
- opening `/dev/ccd0` with `O_WRONLY` — `/dev/ccd0` is
  `crw-r----- root:operator`, so the caller needs root or membership in the
  `operator` group. The unprivileged test user (`maxx`, uid 1001) is **not**
  in `operator`, so it is a **root→kernel** DoS. (A real deployment that
  pre-loads ccd and grants an account `operator` would expose it as an
  unprivileged local DoS; that is a deployment choice, not the default.)

## The fix (fix.diff)

Root-cause: reject a zero-component configuration at the CCDIOCSET layer, and
add a defense-in-depth guard against `maxsecsize == 0` in `ccdinit()`:

1. `ccd.c:1330` — `if (ccio->ccio_ndisks == 0 || ccio->ccio_ndisks > CCD_MAXNDISKS)`
   returns `EINVAL`.
2. `ccd.c:515` — `if (maxsecsize == 0) { error = EINVAL; goto fail; }` before
   the divides at `:519`/`:580`.

## Fix validation (rebuilt + reloaded ccd.ko, same #0 kernel base)

`ccd` is a loadable kld module, so the fix was validated **without a kernel
reboot**: rebuilt `ccd.ko` standalone from `/usr/src/sys/dev/disk/ccd`, copied
it to `/boot/kernel/ccd.ko`, and `kldload`-ed it on the running kernel.

| | result |
|---|---|
| **before** (unpatched ccd.ko, #0 kernel) | `Fatal trap 18: integer divide fault … Stopped at ccdinit+0x578: divl %esi,%eax` — guest down |
| **after** (fixed ccd.ko, same #0 kernel) | `poc: CCDIOCSET rejected: Invalid argument (FIXED behavior)` — guest up, **×3/3 runs** |

Fixed `ccd.ko` SHA256: `5189556a9a0aee32ac340f408e3a088d84a6cb1110c9c282ef5f43500047595f`.
The fix closes the bug cleanly (EINVAL replaces the #DE panic).

## Files

- `poc.c` — CCDIOCSET with `ccio_ndisks=0` → divide-by-zero.
- `build.sh` / `run.sh` — `cc` the PoC; `kldload ccd && ./poc /dev/ccd0`.
- `panic.txt` — `Fatal trap 18 … ccdinit+0x578` serial-log signature.
- `run.log` — fixed-module run: `CCDIOCSET rejected: Invalid argument`, guest up.
- `run.2.log`, `run.3.log` — determinism re-runs (×3/3 EINVAL).
- `fix.diff` — `git apply`-able: reject `ndisks==0` + guard `maxsecsize==0`.
- `fix_build.log` — standalone `ccd.ko` build log (compiles clean).
- `fix_env.txt` — `kern.version`, fixed `ccd.ko` sha256, kldstat.
- `env.txt` — guest uname, cc, `/dev/ccd0` perms, `maxx` groups.
