# DF-1035 — VERDICT

## Verdict

**NOT REPRODUCED (live) — bug CONFIRMED via code trace, fix.diff compiles + boots cleanly.**

The stale-segment-index bug in `bus_dmamap_load_uio` is real and the
cited line numbers are exact. Unlike the other four findings in this
batch, the vulnerable function IS compiled into the audit guest's
running kernel (verified via `nm /boot/kernel/kernel` →
`ffffffff80c1b830 T bus_dmamap_load_uio`). However, the three in-tree
callers (`mpr.c`, `mps.c`, `isp_pci.c`) are SAS/SCSI/fabric HBA
drivers, none of which attach to the QEMU guest's PCI bus. The bug is
therefore *compiled-in but unreachable* on this guest and *reachable*
on any host that loads one of those three HBAs and submits a multi-
iovec uio whose first iov maps more than half of `dmat->nsegments`
DMA segments.

## Mechanism (code trace)

`bus_dmamap_load_uio()` at
`sys/platform/pc64/x86_64/busdma_machdep.c:929-1017` iterates a uio's
iovecs and calls `_bus_dmamap_load_buffer()` for each:

* `busdma_machdep.c:961-962`
  ```c
  segs = segments;
  nsegs_left = dmat->nsegments;
  ```

* `busdma_machdep.c:988-990` (per iovec)
  ```c
  error = _bus_dmamap_load_buffer(dmat, map, addr, minlen,
          segs, nsegs_left,
          pmap, flags, &lastaddr, &nsegs, first);
  ```

* `busdma_machdep.c:994-997`
  ```c
  if (error == 0) {
      nsegs_left -= nsegs;
      segs += nsegs;
  }
  ```
  Between iterations, `segs` is advanced and `nsegs_left` is shrunk.
  But `&nsegs` (passed as `segp`) is NEVER reset — it stays an
  absolute count.

In `_bus_dmamap_load_buffer()` (`busdma_machdep.c:583-760`), the
in/out parameter `*segp` is treated as a **1-based ABSOLUTE index**
into the array passed as the `segments` parameter:

* `busdma_machdep.c:662`
  ```c
  KKASSERT(*segp >= 1 && *segp <= nsegments);
  ```

* `busdma_machdep.c:663-664`
  ```c
  seg = *segp;
  sg = &segments[seg - 1];
  ```

On the 2nd iovec:
* `*segp` (= `&nsegs`) is the absolute count from the first iov (e.g. M).
* The local `segments` parameter is `original_segments + M` (because
  the caller advanced it).
* The local `nsegments` parameter is `nsegs_left = N - M`.
* The `KKASSERT(M <= N - M)` fires when the first iov consumed MORE
  than half the budget (i.e. M > N/2). On default GENERIC (INVARIANTS
  on) this is a kernel panic. On a noinv kernel the KKASSERT is
  stripped, and the next iteration computes `sg =
  &local_segments[M-1]` = `&original_segments[M + M - 1]` — an OOB
  write past the segments array (which is either an 8-slot stack
  cache or a heap alloc sized to `dmat->nsegments`).

The correct reference behaviour is `bus_dmamap_load_mbuf_segment()`
at `busdma_machdep.c:869-923`, which passes CONSTANT
`(segs, maxsegs)` on every call and lets `*segp` be the absolute
end-index.

## Why it does not reproduce on this guest

| Audit-guest fact | Evidence |
|---|---|
| Only 3 in-tree callers, all HBA drivers | `grep -r bus_dmamap_load_uio sys/` → `mpr.c:3533`, `mps.c:2097`, `isp_pci.c:1912` |
| None of the three HBAs present | `pciconf -l` shows no SAS/ISP/LSI devices |
| None of the three modules loaded | `kldstat` shows only `kernel + ehci.ko + xhci.ko` |
| The vulnerable function IS in the running kernel | `nm /boot/kernel/kernel` shows `T bus_dmamap_load_uio` at `0xffffffff80c1b830` |

There is no caller to invoke `bus_dmamap_load_uio` with a multi-iovec
uio on the audit guest.

## Exploit chain

`none` — the bug is unreachable on this guest. On a host with one of
the three HBAs, the primitive is a kernel panic (INVARIANTS) or an
OOB write of `bus_dma_segment_t` (16-byte) records past the segments
array (noinv). The mps/mpr callers build the uio from a SCSI sglist
whose count is attacker-influenced via `/dev/pass*`, so unprivileged
users with pass-device access could in principle shape the iovec
count.

## Fix

`fix.diff` matches `bus_dmamap_load_mbuf_segment()`'s pattern: pass
CONSTANT `(segments, dmat->nsegments)` to every
`_bus_dmamap_load_buffer` call, drop the now-unused `segs` /
`nsegs_left` variables, and treat `nsegs` as the absolute end-index in
the final callback (which `_bus_dmamap_load_buffer` already
guarantees is `>= 1`, satisfying the historical "minimum one segment
even if 0-length buffer" requirement). The inner function returns
EFBIG when segments are exhausted, so termination is preserved.

## Fix validation

This is the only one of the five findings where the fix lands IN the
kernel binary, so we ran a full single-fix kernel build:

* `fix.diff` applies cleanly to
  `/usr/src/sys/platform/pc64/x86_64/busdma_machdep.c` with
  `patch -p1` (3 hunks all succeeded).
* `make -j6 nativekernel KERNCONF=X86_64_GENERIC` rebuilt the kernel
  with the fix; `busdma_machdep.c` recompiled with `-Werror`
  cleanly (`fix_build.log` excerpt shows the cc line and the
  subsequent `linking kernel.debug` step, no errors).
* Installed `kernel.stripped` → `/boot/kernel/kernel` (sha256
  `39ed9f4e937b0bc211e0070ea7643b080c0f3a85fe3bbd4758f901c9e9d8b4e7`).
* Booted the patched kernel cleanly:
  `DragonFly 6.5-DEVELOPMENT #1: Sun Jul 19 15:59:52 UTC 2026`.
* Guest was healthy after boot: `pciconf -l`, `camcontrol devlist`,
  and all 5 PoC harnesses ran cleanly on the patched kernel.
* Not live-tested against the actual bug (no HBA to invoke the path),
  but the regression-test of "kernel boots and is functional with the
  fix" is clean.

`fix_status: not_testable` (no live trigger available; the
patched-kernel boot + compile is the strongest validation possible
without HBA hardware).

## PoC changes

The finding folder was empty; this run authored:
- `poc.c` — documentation harness
- `fix.diff` — the verified fix
- `build.sh`, `run.sh`, `build.log`, `run.log`, `env.txt`,
  `fix_build.log`, `manifest.json`, `VERDICT.md`
