# DF-0003 - devclass_alloc_unit() negative-unit heap out-of-bounds write

`poc_negunit.c` / `poc_ctrl.c` -- kld modules that drive the negative-unit
heap OOB-write sink in `devclass_alloc_unit()` / `devclass_add_device()`
(`sys/kern/subr_bus.c`).

## The bug (memory-safety, CERTAIN -- reproduced on the audited kernel)

`devclass_alloc_unit()` only treats `unit == -1` as a wildcard. Any other
negative unit (e.g. `-2`) enters the "wired unit" branch but skips the
existing-device check (`unit >= 0`, `subr_bus.c:1073`) and the table-extension
check (`unit >= dc->maxunit`, `subr_bus.c:1094`), so it returns success with
the negative unit. `devclass_add_device()` then executes

    dc->devices[dev->unit] = dev;     // sys/kern/subr_bus.c:1144

an 8-byte pointer write at a NEGATIVE index into the kmalloc'd `dc->devices`
array. `device_set_unit()` has a matching OOB read at `subr_bus.c:2172`.

## Reproduction (VERIFIED)

A kld module that calls `device_add_child(root_bus, "df3neg", -2)`:

* **Control** (`poc_ctrl.ko`, unit=0) loads cleanly and prints
  `DF0003-CTRL: unit=0 -> OK (child=0xfffff800...)`. Guest stays up.
* **Trigger** (`poc_negunit.ko`, unit=-2) panics the kernel immediately:

  ```
  Fatal trap 12: page fault while in kernel mode
  fault virtual address = 0xfffffffffffffff0
  fault code            = supervisor write data, page not present
  Stopped at devclass_add_device+0xf6: movq %r14,(%rdx,%rax,1)
  ```

  `addr2line -e /boot/kernel/kernel 0xffffffff8068a946` ->
  **`sys/kern/subr_bus.c:1144`** (the exact sink line).

  The fault address `0xfffffffffffffff0` = `(device_t*)NULL + (-2)` =
  `0 + (-2)*8`, i.e. the negative-index write target. For the freshly-created
  devclass `dc->devices == NULL`, so the store hits an unmapped address and
  the kernel page-faults on the WRITE -- at the sink line.

The ONLY difference between the control and the trigger is the literal unit
(`0` vs `-2`), so the panic is caused specifically by the negative unit.

## Reachability (the crux)

There is **no unprivileged-userspace path** to this sink in the default
kernel. `device_add_child` / `devclass_add_device` / `devclass_alloc_unit`
are internal newbus APIs; no syscall/ioctl invokes them. Auditing all 114
in-tree `device_add_child*` callers and the single `device_set_unit` caller
(`sio.c`):

* 84 pass the `-1` wildcard (the legitimate, handled case);
* the rest pass provably non-negative units -- PCI bus numbers
  (`uint8_t secbus` 0-255 in `pci_pci.c:344`, `busno`, `bus`), `for(unit=0;;unit++)`
  loop counters (`ata-all.c`, `ata-pci.c`), a monotonically-increasing
  `freeunit`/`puc_find_free_unit()` (starts >= 0 and only grows), and
  `sio_pci_kludge_unit()`'s `unit` that starts at 0 and only `++`s.

So no in-tree driver computes a negative unit. The bug is therefore a
**real-but-latent memory-corruption defect**: reachable today only by root
(`kldload`, demonstrated here) or by any future/buggy driver that underflows a
unit (signed subtraction, signed parse of a device-reported field, etc.). The
fix is a one-line guard that converts the latent footgun into a hard EINVAL.

## Build & run (on the DragonFly guest)

Building a kld requires the kernel source tree's headers; the guest ships
without `/usr/src`, so `setup_env.sh` installs a headers-only subset first.

```
# one-time (root): install kernel headers + machine forwarders
sh setup_env.sh

# build both modules (as any user)
make SYSDIR=/usr/src/sys                      # -> poc_negunit.ko (trigger, -2)
make -f Makefile.ctrl SYSDIR=/usr/src/sys     # -> poc_ctrl.ko    (control, 0)

# run (root): control loads clean, trigger panics
sh run.sh
```

## Files

| file | purpose |
|------|---------|
| `poc_negunit.c` | trigger source -- `device_add_child(root_bus,"df3neg",-2)` |
| `poc_ctrl.c`    | control source -- `device_add_child(root_bus,"df3ctrl", 0)` |
| `Makefile` / `Makefile.ctrl` | build via `bsd.kmod.mk` (correct kernel CFLAGS) |
| `setup_env.sh`  | install kernel headers + machine forwarders on the guest |
| `build.sh`      | (legacy) hand-build path; superseded by the Makefiles |
| `run.sh`        | load control then trigger |
| `build.log`     | full successful build output |
| `run.log`       | decisive run: control marker + trigger panic (from boot.log) |
| `panic.txt`     | crash signature with addr2line proof |
| `env.txt`       | guest uname/cc/config/kldstat |
| `VERDICT.md`    | full narrative verdict |
| `fix.diff`      | git-apply-able fix (reject `< -1` in devclass_alloc_unit, `< 0` in device_set_unit) |
| `manifest.json` | artifact catalog |
