# DF-2859 — VERDICT

**Status: reproduced (deterministic, 3/3 baseline runs) · fixed (kernel rebuilt with fix.diff → PASS)**
**Impact ceiling today: none (dead code) · Primitive: sglist contents corruption (wrong DMA ranges) + heap OOB read**

## What the bug is

`sys/kern/subr_sglist.c:571-574`, `sglist_split()`:

```c
/* Trim 'count' entries from the front of 'original'. */
original->sg_nseg -= count;
bcopy(original->sg_segs + count, original->sg_segs, count *
    sizeof(struct sglist_seg));
```

After handing the first `length` bytes to `*head` (count = full entries moved,
decremented once more at :564 when a segment had to be split), `original` must
relocate its SURVIVING entries — `original->sg_nseg` of them, already computed
on the line above — from index `count` down to index 0. The bcopy instead moves
`count` entries. It is only correct by accident when survivors == count.

* **under-copy (survivors > count)** — tail entries are never moved; slots
  [count..survivors) keep stale pre-split content. The resulting list
  duplicates an earlier segment and drops the last survivor(s). A DMA consumer
  (the only kind of sglist consumer) programs the wrong physical ranges: the
  device reads/writes memory belonging to a different part of the transfer and
  the true tail is never touched — silent memory corruption / info exposure at
  the consumer.
* **over-copy (survivors < count)** — the bcopy reads
  `segs[count .. 2*count)`. `sglist_alloc()` (subr_sglist.c:206-217) sizes the
  allocation at exactly `sg_maxseg` segments, so whenever `2*count >
  sg_maxseg` this reads past the end of the heap object (up to
  `(2*count - sg_maxseg) * 16` bytes). The OOB bytes are written only into
  unused slots (no disclosure), but the read itself is an OOB heap access
  (page/slab-edge fault possible).

## How it was reproduced (KLD library harness)

`sgsplit_demo.c` — DragonFly KLD, runs at MOD_LOAD, no device needed. Builds
lists with 6 (resp. 4) physically discontiguous segments via the exported
`sglist_alloc`/`sglist_append_phys`, then calls the exported `sglist_split()`
and prints the survivor lists.

* **Case A** (split==0, N=6, length=0x1000 → count=1, survivors=5):
  stock kernel `A.orig.after = [P1, P1, P2, P3, P4]` — P1 duplicated, P5 lost.
  Correct: `[P1, P2, P3, P4, P5]`.
* **Case B** (split!=0, N=6, length=0x1800 → count'=1, survivors=5):
  stock kernel `B.orig.after = [0x130800/0x800, 0x130800/0x800, P2, P3, P4]`
  — the split tail duplicated, P5 lost. Correct:
  `[0x130800/0x800, P2, P3, P4, P5]`.
* **Case C** (maxseg=N=4, length=0x3000 → count=3, survivors=1):
  `bcopy(segs+3, segs, 3*16)` reads `segs[4]`, `segs[5]` — 32 bytes past the
  4-segment allocation. OOB read by construction (values land in unused
  slots); noted in the demo output.

Baseline (stock kernel #0, 2026-09-02): **"DF2859: verdict: BUG REPRODUCED"**
in 3/3 loads (run.log, run.2.log, run.3.log).

## Fix validation

`fix.diff` (authored against the read-only host `sys/` tree, verified
`git apply --check` clean, applied inside the guest's /usr/src copy):

```diff
 	original->sg_nseg -= count;
-	bcopy(original->sg_segs + count, original->sg_segs, count *
-	    sizeof(struct sglist_seg));
+	bcopy(original->sg_segs + count, original->sg_segs,
+	    original->sg_nseg * sizeof(struct sglist_seg));
```

Guest rebuild: `make -j6 -DNO_CLEAN nativekernel && make installkernel`
(full log fix_build.log, 20021 lines, BUILD_OK; only subr_sglist.c recompiled
+ relink), reboot into kernel `#1 Wed Sep 2 13:15:59 UTC 2026`, re-ran the
byte-identical `sgsplit_demo.ko`:

* Case A: `A.orig.after = [P1, P2, P3, P4, P5]` → **PASS**
* Case B: `B.orig.after = [0x130800/0x800, P2, P3, P4, P5]` → **PASS**
* `DF2859: verdict: PASS` (twice; the single "BUG REPRODUCED" line in
  fix_run.log is stale msgbuf content from the pre-reboot run — the new
  cycle's lines all say PASS).

Baseline reproduced = yes; patched reproduced = no. **fix_status: fixed.**

## Reachability and severity (honest)

`sglist_split()` has **zero callers** in the DragonFly tree (only the
prototype at sys/sys/sglist.h:105). The live surface of subr_sglist.c is the
append/alloc/free family used by virtio (virtio_blk.c:315,862-887,
virtio_scsi.c:306,1042-1058, if_vtnet.c:1422-1441/2003-2017,
virtio_balloon.c:562,710, virtio_random.c:200) — this guest's own root disk
(vtblk) exercises it. None of those call split/join/slice/uio APIs. Severity
is therefore **Low (dead code)**, consistent with the DF-0096..0098 precedent.

Upstream note: FreeBSD HEAD (checked 2026-09-02, raw.githubusercontent.com/
freebsd/freebsd-src/main/sys/kern/subr_sglist.c) still contains the identical
`count *` trim block — inherited upstream bug; worth an upstream report where
sglist_split may have live callers.

## Why the other named functions from the brief do not appear

This DragonFly file is the 2009 FreeBSD v1.3 import (715 lines). There is no
sglist zone allocator (plain kmalloc/M_SGLIST, so no zone teardown), and
sglist_append_bio / sglist_apply / sglist_count_uio / sglist_count_vmpages /
uiomove glue are later upstream additions not present here.

## Environment

Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0/#1 X86_64_GENERIC x86_64,
securelevel -1, /usr/src + /usr/obj prebuilt (see env.txt). Guest reset to the
clean `with-src` snapshot after validation (stock kernel #0, stock source
md5 9d876a7c2006cd1c4e6f967c7820c7c1 confirmed).
