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).
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2859 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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"
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 exactlysg_maxsegsegments, so whenever2*count > sg_maxsegthis reads past the end of the heap object (up to(2*count - sg_maxseg) * 16bytes). 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)readssegs[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
fixedApplied 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)
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.
No comments yet.