# DF-0003 -- VERDICT

**Verdict: REPRODUCED** (memory-corruption sink confirmed at runtime on the
audited master-DEV kernel; impact = kernel panic / DoS, root-gated; latent
for any future buggy driver that underflows a unit).

## Summary

`devclass_alloc_unit()` (`sys/kern/subr_bus.c:1064-1125`) only treats `unit
== -1` as a wildcard. Any other negative unit (e.g. `-2`) slips past every
guard and is returned unchanged; `devclass_add_device()` then performs
`dc->devices[dev->unit] = dev` (`subr_bus.c:1144`) -- an 8-byte pointer
write at a negative array index. A tiny kld module calling
`device_add_child(root_bus, "df3neg", -2)` drives this sink directly and
**panics the kernel at exactly `subr_bus.c:1144`** (proven by `addr2line`).
The control (`unit=0`) loads cleanly.

## Mechanism (trigger -> primitive -> effect), cited `path:line`

1. **Trigger**: `device_add_child(root_bus, "df3neg", -2)`
   -> `device_add_child_ordered` (`subr_bus.c:1247`) -> `make_device`
   (`subr_bus.c:1174`). With `name="df3neg"`, `make_device` calls
   `devclass_find_internal(name, NULL, TRUE)` (`subr_bus.c:1183`) which
   **creates** a fresh devclass with `dc->devices = NULL; dc->maxunit = 0`
   (`subr_bus.c:759-760`), then `devclass_add_device(dc, dev)`
   (`subr_bus.c:1211`).

2. **Sink reach**: `devclass_add_device` calls
   `devclass_alloc_unit(dc, &dev->unit)` (`subr_bus.c:1139`).

3. **The missing guard** (`subr_bus.c:1064-1125`):
   * `int unit = *unitp;` (`:1067`) -> `unit = -2`.
   * `if (unit != -1)` (`:1072`) -> TRUE (only `-1` is the wildcard), enter
     the "wired unit" branch.
   * `if (unit >= 0 && unit < dc->maxunit && dc->devices[unit] != NULL)`
     (`:1073`) -> `unit >= 0` is FALSE, the existing-device check is skipped.
   * `if (unit >= dc->maxunit)` (`:1094`) -> `-2 >= 0` is FALSE, the
     table-extension block is skipped -- `dc->devices` is **not** grown and
     the negative unit is **not** caught.
   * `*unitp = unit; return(0);` (`:1123-1124`) -> returns **success** with
     `dev->unit = -2`.

4. **OOB write** (`subr_bus.c:1144`): back in `devclass_add_device`,
   `dc->devices[dev->unit] = dev` -> `dc->devices[-2] = dev`. With
   `dc->devices == NULL` this stores the 8-byte `dev` pointer at address
   `0 + (-2)*sizeof(device_t)` = `0xfffffffffffffff0` (unmapped) -> supervisor
   WRITE page fault.

5. **Effect**: fatal trap 12, kernel panic, guest wedged in DDB. The faulting
   instruction is `devclass_add_device+0xf6: movq %r14,(%rdx,%rax,1)`
   (the indexed 8-byte store), at IP `0xffffffff8068a946`, which `addr2line`
   resolves to **`sys/kern/subr_bus.c:1144`** -- the exact sink line.

`device_set_unit()` (`subr_bus.c:2165-2184`) has a matching OOB **read** at
its bounds check `if (unit < dc->maxunit && dc->devices[unit])` (`:2172`) --
a negative `unit` makes `unit < dc->maxunit` TRUE, so `dc->devices[unit]`
reads out of bounds; if it reads NULL, execution proceeds to
`dev->unit = unit; devclass_add_device(dc, dev)` (`:2177-2178`), hitting the
same write sink. No in-tree caller exercises this.

## Evidence

* `run.log` -- decisive run: control prints
  `DF0003-CTRL: unit=0 -> OK (child=0xfffff80065c20ea0)`, guest stays up;
  trigger panics. Boot.log (serial console) delta shows:
  ```
  fault virtual address = 0xfffffffffffffff0
  fault code            = supervisor write data, page not present
  instruction pointer   = 0x8:0xffffffff8068a946
  Stopped at devclass_add_device+0xf6: movq %r14,(%rdx,%rax,1)
  ```
* `panic.txt` -- crash signature + `addr2line` proof that IP
  `0xffffffff8068a946` = `subr_bus.c:1144`.
* `build.log` -- full successful `bsd.kmod.mk` build.

## Reachability -- why this is "latent" but real

`device_add_child` / `devclass_add_device` / `devclass_alloc_unit` are
**internal newbus kernel APIs**; no syscall or ioctl invokes them. The unit
originates from bus-driver code (`device_add_child*`) or loader hints
(root-controlled). Auditing the entire audited `sys/` tree:

* 114 in-tree `device_add_child(_ordered)` callers and the single
  `device_set_unit` caller (`sys/dev/serial/sio/sio.c:407`).
* 84 callers pass the literal `-1` wildcard (the handled case).
* The rest pass provably **non-negative** units:
  - `sys/bus/pci/pci_pci.c:344` -- `sc->secbus`, a `uint8_t` PCI secondary
    bus number (0-255, `pcib_private.h:53`).
  - `sys/dev/disk/nata/ata-pci.c:233/242` -- `for(unit=0;...)` loop counter;
    `freeunit = 2; ... freeunit++` (monotonic).
  - `sys/dev/acpica/acpi_pcib.c:161`, `sys/bus/pci/x86_64/pci_bus.c:474` --
    `busno` / `bus` (PCI bus numbers).
  - `sys/dev/misc/puc/puc.c:327` -- `puc_find_free_unit()` (>= 0 by definition).
  - `sio.c` `sio_pci_kludge_unit()` -- `unit` starts at 0 and only `++`s.

=> **No in-tree path produces a unit < -1.** The bug is therefore a
real-but-latent memory-corruption defect reachable today only by root
(`kldload`, demonstrated) or by any future/buggy driver that underflows a
unit (signed subtraction underflow, signed parse of a device-reported field,
etc.). The fix is a one-line guard.

## Exploit chain (memory-corruption class -- analysis)

* **Primitive**: `dc->devices[N] = dev` with attacker-selectable negative
  index `N` (the unit), writing the 8-byte kernel-heap pointer `dev` at
  offset `N*8` before `dc->devices`.
* **Triggering requirement**: root (`kldload`) or a buggy driver. **Not**
  reachable from unprivileged userspace.
* **This PoC's effect**: deterministic panic (the easy fresh-devclass case
  has `dc->devices == NULL`, so the store faults immediately -- a clean DoS,
  not controllable corruption).
* **Controllable-corruption variant (theoretical)**: target an *existing*
  devclass whose `dc->devices` is a real heap pointer (e.g. reuse a known
  driver's devclass), choose `unit` so `dc->devices[N]` lands on an adjacent
  slab object (function pointer / `ucred *` / refcount), and groom the heap.
  This would need root (kldload) and a kernel-ROP/ucred-forgery conversion,
  which is beyond what an unprivileged attacker can reach. Given the
  root-only trigger, the realistic impact ceiling is **local DoS by root** +
  latent corruption for a future driver bug. **No uid0 chain was pursued** --
  there is no unprivileged trigger to escalate from, and the root case is
  already game-over for the attacker.

## PoC changes (vs. the filed PoC)

* Original `poc_negunit.c` included `<sys/bus.h>` (drags in platform/APIC
  headers) and used `cc -DKERNEL` (wrong macro -- the guard is `_KERNEL`).
  Forward-declared the newbus symbols and built via the standard
  `bsd.kmod.mk` (correct kernel CFLAGS, machine forwarders). The
  hand-build (`build.sh`) is kept as a legacy path but the Makefile build is
  authoritative.
* Added `poc_ctrl.c` + `Makefile.ctrl` -- a `unit=0` control that loads
  cleanly, so the trigger panic is provably caused by the negative unit and
  not by module plumbing.
* Added `setup_env.sh` (install kernel headers + forwarders on a guest that
  lacks `/usr/src`), `run.sh`, `env.txt`, `panic.txt`, `build.log`,
  `run.log`, `manifest.json`, and `fix.diff`.

## Recommended fix

Reject negative units at the entry of `devclass_alloc_unit` (`< -1`) and
`device_set_unit` (`< 0`). See `fix.diff` (git-apply-able, supersedes the
finding markdown's draft -- adds an explicit `EINVAL` return and comments).
