bus_dmamap_load_uio uses stale absolute segment index after advancing segs pointer -> KKASSERT panic or OOB write
Summary
bus_dmamap_load_uio at busdma_machdep.c:995-996 advances segs+=nsegs and decrements nsegs_left between iovecs but does NOT reset *segp relative to new base. _bus_dmamap_load_buffer :662 KKASSERT(*segp>=1&&*segp<=nsegments) with stale absolute index -> KKASSERT fires when first iov consumed >half nsegments (INVARIANTS kernel panic). Without INVARIANTS: merge branch sg->ds_len+=size writes OOB past segments array (stack cache_segments[8] or heap nsegments alloc). In-tree callers: mpr.c:3533, mps.c:2097, isp_pci.c:1912 (isp fabricates uio from SCSI sglist, sglist_cnt attacker-controlled via /dev/pass). bus_dmamap_load_mbuf_segment :869 handles correctly (constant segs base). Fix: pass constant segments+dmat->nsegments to every call like load_mbuf_segment.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1035 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc.c | trigger-source | documentation harness (no live trigger possible on guest) | 3.5 KB | view raw |
| build.sh | build-script | cc -O -pipe -Wall -o poc poc.c | 140 B | view raw |
| run.sh | run-script | ./poc | 63 B | view raw |
| build.log | build-log | harness compile output, full | 13 B | view raw |
| run.log | run-log | harness run output | 953 B | view raw |
| fix.diff | suggested-fix | pass constant segments+dmat->nsegments to _bus_dmamap_load_buffer like load_mbuf_segment does; drop segs/nsegs_left | 2.3 KB | view raw |
| fix_build.log | build-log | single-fix nativekernel rebuild log (head+tail): busdma_machdep.c recompiles -Werror, kernel.links, sha256 of installed kernel.stripped | 58.9 KB | view raw |
| env.txt | environment | uname, cc, kldstat, pciconf, modules, nm bus_dmamap_load_uio | 1.7 KB | view raw |
| VERDICT.md | verdict | narrative analysis | 5.7 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
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-962c 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-997c if (error == 0) { nsegs_left -= nsegs; segs += nsegs; }Between iterations,segsis advanced andnsegs_leftis shrunk. But&nsegs(passed assegp) 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:662c KKASSERT(*segp >= 1 && *segp <= nsegments); -
busdma_machdep.c:663-664c 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.diffapplies cleanly to/usr/src/sys/platform/pc64/x86_64/busdma_machdep.cwithpatch -p1(3 hunks all succeeded).make -j6 nativekernel KERNCONF=X86_64_GENERICrebuilt the kernel with the fix;busdma_machdep.crecompiled with-Werrorcleanly (fix_build.logexcerpt shows the cc line and the subsequentlinking kernel.debugstep, no errors).- Installed
kernel.strippedβ/boot/kernel/kernel(sha25639ed9f4e937b0bc211e0070ea7643b080c0f3a85fe3bbd4758f901c9e9d8b4e7). - 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
Fix verification
not_testablecompile+boot validated
module/kernel build rc=0 -Werror
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source-confirmed. bus_dmamap_load_uio stale absolute segment index. In kernel but no HBA callers attach. Fix compiles+boots.
No comments yet.