β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2859

sglist_split() trims `count` entries instead of the surviving remainder β€” stale/duplicated DMA segments and heap OOB read past the segs array (dead code; identical latent bug in FreeBSD HEAD)

Field Value
ID DF-2859
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:L
CWE CWE-682 (primary); CWE-125 (over-copy mode)
File sys/kern/subr_sglist.c
Lines 571-574
Area kern
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

sglist_split()'s front-trim bcopy moves count (entries handed to *head) instead of the surviving original->sg_nseg already computed on the previous line. Deterministically reproduced on the stock guest via a KLD harness: survivors keep stale pre-split segments β€” an earlier segment duplicated, the tail dropped β€” so any DMA consumer would program the wrong physical ranges; in the opposite mode the bcopy reads up to (2Γ—countβˆ’sg_maxseg)Γ—16 bytes past the kmalloc'd segment array. Zero in-tree callers β‡’ Low per the DF-0096..98 dead-code precedent; FreeBSD HEAD still carries the identical bug.

Proof of contest

VERIFIED 3/3 loads (findings/poc/DF-2859/sgsplit_demo.c): orig.after=[P1,P1,P2,P3,P4] (P1 dup, P5 lost); Case C demonstrates the heap OOB read by construction. Fix (bcopy original->sg_nseg entries) validated on a rebuilt kernel: '[P1..P5] / 0x130800/0x800,P2..P5, verdict: PASS'. No uid0 route (dead code).

Two-line diff in findings/poc/DF-2859/fix.diff (validated).

Timeline

  • 2026-09-02 Discovered during pass-2 audit of subr_sglist.c (GLM 5.3); KLD repro + fix validation same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2859 Β· 15 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 5.3 KB ↓ raw
sgsplit_demo.c β€” 5.2 KB view raw
Makefile β€” 64 B ↓ download
build.sh β€” 69 B view raw
run.sh β€” 181 B view raw
build.log β€” 5.5 KB view raw
run.log β€” 2.3 KB view raw
run.2.log β€” 3.3 KB view raw
run.3.log β€” 3.3 KB view raw
fix_run.log β€” 2.4 KB view raw
fix_build.log β€” 696.3 KB ↓ download
fix.diff β€” 528 B view raw
verdict.json β€” 3.8 KB view raw
env.txt β€” 393 B view raw

DF-2859 β€” sglist_split() trims the wrong number of entries from 'original'

Bug (reproduced): sys/kern/subr_sglist.c:571-574

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

The bcopy must move the SURVIVING entries β€” original->sg_nseg, already decremented on the line above β€” not count (the number of entries handed to *head). Two failure modes:

  • under-copy (survivors > count): tail entries never move; the list keeps stale pre-split segments at indexes [count..survivors) β€” duplicated earlier segments, last survivor(s) lost. Any DMA consumer of the list programs the WRONG physical ranges (device reads/writes memory it should not, drops the tail).
  • over-copy (survivors < count): bcopy reads segs[count .. 2count), past the kmalloc'd array (sglist_alloc allocates exactly sg_maxseg segments) whenever 2count > sg_maxseg β€” heap OOB read of up to (2*count - sg_maxseg) * 16 bytes (written only into unused slots).

Reachability: sglist_split() has ZERO callers in the DragonFly tree (only the prototype, sys/sys/sglist.h:105). Dead code β€” severity Low, same framing as DF-0096. The identical bug exists in FreeBSD HEAD (verified 2026-09-02 against upstream subr_sglist.c, "Trim 'count' entries" block).

BUILD push sgsplit_demo.c + Makefile into the guest (e.g. /root/sgdemo) and: cd /root/sgdemo && make (DragonFly KLD, links against the in-kernel sglist_* symbols; build log in build.log)

RUN kldload ./sgsplit_demo.ko # demo runs at MOD_LOAD, prints DF2859 lines dmesg | grep DF2859 kldunload sgsplit_demo

EXPECTED (buggy stock kernel, deterministic): caseA: BUG REPRODUCED β€” orig.after = [P1,P1,P2,P3,P4] (P1 dup, P5 lost) caseB: BUG REPRODUCED β€” orig.after = [tail(P1), tail(P1), P2,P3,P4] caseC: survivors=1, bcopy read segs[3..5] of a 4-seg alloc (OOB read) final: "DF2859: verdict: BUG REPRODUCED"

EXPECTED (kernel with fix.diff): all three cases PASS, caseA orig.after = [P1,P2,P3,P4,P5] caseB orig.after = [0x130800/0x800, P2,P3,P4,P5] final: "DF2859: verdict: PASS"

VERDICT.md
↓ download raw

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():

/* 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):

    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).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Applied fix.diff in guest /usr/src, make -j6 -DNO_CLEAN nativekernel + installkernel (fix_build.log, BUILD_OK), rebooted into kernel #1, re-ran the byte-identical sgsplit_demo.ko: all cases PASS, verdict line PASS, previously-observed duplicated/dropped segments GONE.

fix_run.log (patched PASS output incl. correct [P1..P5] and [0x130800/0x800, P2..P5] survivor lists); fix_build.log (full build log, BUILD_OK)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Wed Sep 2 13:15:59 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

N/A β€” dead code (no in-tree caller of sglist_split). If a consumer is ever added: split a list so survivors != count -> survivor sglist contains stale/duplicated (paddr,len) pairs -> device DMAs wrong physical memory (read of unrelated physical pages into the transfer buffer / write of transfer data to unrelated physical pages) plus heap OOB read in over-copy mode.

Evidence (decisive lines)

["run.log: 'A.orig.after seg[0..1] paddr=0x130000' duplicated, 'verdict: BUG REPRODUCED' (baseline run 1)", 'run.2.log / run.3.log: same corruption on runs 2 and 3 (deterministic)', "fix_run.log: patched kernel -> A.orig.after = P1..P5 distinct, B.orig.after = [0x130800/0x800, P2..P5], 'verdict: PASS'", 'fix_build.log: full nativekernel build log ending BUILD_OK (kernel #1 Wed Sep 2 13:15:59 UTC 2026)', 'VERDICT.md: full narrative incl. dead-code reachability analysis and upstream parity check']

PoC changes

no seed existed; KLD harness authored from scratch (runs at MOD_LOAD via exported sglist_alloc/append_phys/split symbols; three deterministic cases A/B/C)

Verified recommended fix

sglist_split: bcopy the surviving remainder β€” original->sg_nseg * sizeof(struct sglist_seg) β€” instead of count * (see fix.diff)

Verdict

sglist_split()'s trim bcopy (sys/kern/subr_sglist.c:571-574) moves count entries instead of the surviving original->sg_nseg entries, deterministically corrupting the survivor list (duplicated segments, dropped tail β€” wrong DMA ranges for any consumer) and heap-overreading up to (2count - sg_maxseg)16 bytes past the kmalloc'd segs array when survivors < count. Reproduced 3/3 on the stock guest via a KLD library harness (cases A/B); OOB-read mode by construction (case C). sglist_split has zero callers in the DragonFly tree, so the realistic impact ceiling today is none (severity Low, dead code, same framing as DF-0096); the identical bug is still present in FreeBSD HEAD. One-line fix validated by in-guest kernel rebuild: baseline BUG REPRODUCED -> patched PASS.