# DF-5128 — VERDICT

**Status: reproduced** (impact: panic — OOB sg-entry processing in the real
`sg_pcopy_to_buffer()`; deterministically repeatable; fix validated)
**Confidence: certain** for the code defect and the OOB-processing primitive.

## The bug

`sys/dev/drm/include/linux/scatterlist.h` (the inline API surface of the
audited `sys/dev/drm/linux_scatterlist.c`):

`_sg_iter_next()` (scatterlist.h:252-270), the engine behind
`for_each_sg_page()` (scatterlist.h:338-340), decrements the unsigned
`iter->maxents` once per sg entry advanced and breaks when it *becomes* 0 —
**leaving `iter->sg` pointing at the out-of-budget entry**, which the macro's
`iter->sg` loop condition then executes the body on. On the next call the
0-budget counter **underflows to UINT_MAX**, permanently disarming the only
secondary stop: iteration then runs to the physical `SG_END` of the table —
**across `sg_chain()` pointers into other heap chunks** — no matter what
`nents` the caller declared. Linux semantics (`__sg_page_iter_next`, which DF
even carries as *dead code* in the same header at lines 279-298, unused by
any caller) hard-stop at `nents`.

Every page-granular consumer of the shim inherits this:
`sg_pcopy_from_buffer`/`sg_pcopy_to_buffer` (linux_scatterlist.c:165,201),
`drm_clflush_sg` (drm_cache.c:128), `radeon_ttm_tt_unpin_userptr`
(radeon_ttm.c:624).

## How it was proven (audit QEMU guest, DragonFly 6.5-DEVELOPMENT #0
Thu Jul 2 06:02:54 UTC 2026, X86_64_GENERIC, drm.ko 5.x-suite loaded via
`kldload i915`)

### Phase 1 — semantics (safe)

`sysctl debug.sgoverrun=1` builds tables with the *real* `__sg_alloc_table()`
(resolved from drm.ko) and walks them with `for_each_sg_page` under a caller
budget of `nents=2`:

* flat 6-entry table: body executed on entries **0,1,2,3,4,5**
  (`visited=6 budget=2 -> OVERRUN`); the printed maxents trace shows the
  underflow exactly: `2 -> 1 -> 0 (entry 2 processed!) -> 4294967295 -> …`
* chained 250-entry table (3 heap chunks): `visited=250`, entry indices
  relative to the first chunk reach 2,043,903 units (~78 MB of heap away) —
  the walk crosses chunk boundaries via chain pointers.
  Linux semantics: exactly 2.

### Phase 2 — impact (fatal, deterministic)

`sysctl debug.sgoverrun=2` calls the **real `sg_pcopy_to_buffer()` from
`sys/dev/drm/linux_scatterlist.c`** with `nents=2` on a 6-entry table whose
entries ≥2 are out-of-budget (page=NULL, length=PAGE_SIZE — the shape a
coalescing `dma_map_sg()` leaves behind under Linux semantics):

```
Fatal user address access from kernel mode from sysctl
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x68          <- (NULL vm_page)->phys_addr @ +0x68
Stopped at sg_pcopy_to_buffer+0xfc: addq 0x68(%rax),%rcx
```

Guest dead (DDB). The consumer function dereferenced an entry the caller
never declared. With a freed/foreign `struct page` in the out-of-budget entry
(instead of NULL) the same path is an **OOB `memcpy` through the DMAP**
(read in `to_buffer`, write in `from_buffer`) — the memory-corruption ceiling,
composable with the known multi-page length confusion (DF-2128).

## Reachability today (honest)

* Requires drm.ko/i915.ko/radeon.ko loaded (root `kldload` — standard on any
  DragonFly desktop with GPU drivers in loader.conf; the shim is the base of
  all LinuxKPI dma/bo operations).
* No current in-tree producer creates tables with `SG_END` beyond the
  caller-declared `nents`: DF's `dma_map_sg()` (dma-mapping.h:73-97) never
  coalesces, and i915 marks `SG_END` on the last filled entry
  (i915_gem.c:2988). The trap is armed for any code written to *Linux*
  semantics — where `dma_map_sg()` coalescing makes `nents < orig_nents` the
  norm (drm_prime/amdgpu/i915 upstream all rely on it) — i.e. every future
  port or any fix that makes `dma_map_sg()` Linux-faithful turns this into a
  live memory-corruption primitive for unprivileged GPU users.

## Fix validation (mandatory — memory-corruption class)

`fix.diff` hard-stops the walk at the caller's entry budget
(`maxents <= 1 → iter->sg = NULL`) and removes the underflowing test:

* Applied inside the guest's `/usr/src` (byte-identical to the audited tree,
  md5 verified), incremental `make nativekernel` (35,726-line build log,
  0 errors), rebuilt `drm.ko` (the iterator is inlined into the real
  `sg_pcopy_to_buffer`) + PoC module.
* Phase 1 post-fix: `visited=2` flat **and** chained, final `maxents=1` —
  no overrun, no underflow.
* Phase 2 post-fix: `SGOVER P2: returned 8192 - no out-of-budget entry
  touched`; guest stays up (`vm.sh status` = up); repeated twice.

Baseline bad behavior: **gone** on the patched kernel.

## Files (decisive evidence)

* `run.phase1.dmesg.log` — overrun + underflow trace (baseline)
* `panic.txt` / `run.phase2.log` — fatal fault in `sg_pcopy_to_buffer+0xfc`
* `run.fix.phase1.log`, `run.fix.phase2.log`, `run.fix.stability.log` — post-fix
* `buildkernel.fix.log` — patched kernel build
* `sgoverrun.c` — PoC module (build: `make`; run: `sysctl debug.sgoverrun=1|2`)

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
