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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1035 Β· 11 files
FileTypeDescriptionSize
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
VERDICT.md verdict narrative analysis
↓ download 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-962 c 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-997 c if (error == 0) { nsegs_left -= nsegs; segs += nsegs; } Between iterations, segs is advanced and nsegs_left is shrunk. But &nsegs (passed as segp) 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:662 c KKASSERT(*segp >= 1 && *segp <= nsegments);

  • busdma_machdep.c:663-664 c 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.diff applies cleanly to /usr/src/sys/platform/pc64/x86_64/busdma_machdep.c with patch -p1 (3 hunks all succeeded).
  • make -j6 nativekernel KERNCONF=X86_64_GENERIC rebuilt the kernel with the fix; busdma_machdep.c recompiled with -Werror cleanly (fix_build.log excerpt shows the cc line and the subsequent linking kernel.debug step, no errors).
  • Installed kernel.stripped β†’ /boot/kernel/kernel (sha256 39ed9f4e937b0bc211e0070ea7643b080c0f3a85fe3bbd4758f901c9e9d8b4e7).
  • 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_testable

compile+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.