โฌข DragonFlyBSD Kernel Audit
DF-1035 / poc.c
โ† back to finding โ†“ download raw
/*
 * DF-1035 โ€” bus_dmamap_load_uio uses stale absolute segment index
 * Documentation / analysis harness.
 *
 * Bug location: sys/platform/pc64/x86_64/busdma_machdep.c:961-996
 *
 * bus_dmamap_load_uio() iterates over uio->uio_iov. For each iovec it
 * calls _bus_dmamap_load_buffer(dmat, map, addr, minlen,
 *     segs, nsegs_left, pmap, flags, &lastaddr, &nsegs, first);
 *
 * Between iterations it advances the segment pointer and shrinks the
 * remaining count:
 *     nsegs_left -= nsegs;
 *     segs       += nsegs;       <-- segs now points into the array
 *
 * But _bus_dmamap_load_buffer treats *segp (= &nsegs) as an ABSOLUTE
 * 1-based index into the segments array (lines 662-664):
 *     KKASSERT(*segp >= 1 && *segp <= nsegments);   // nsegments is local param
 *     seg = *segp;
 *     sg = &segments[seg - 1];                      // segments is the LOCAL
 *                                                    // advanced pointer
 *
 * So on the 2nd iovec:
 *   *segp is still the absolute count from the first iov (e.g. 3).
 *   the local 'segments' parameter is original_segments + 3.
 *   'nsegments' parameter is nsegs_left = dmat->nsegments - 3.
 *   KKASSERT(3 >= 1 && 3 <= nsegs_left) โ€” fires when 1st iov consumed
 *     more than half the budget.
 *
 * Even without INVARIANTS, sg = &local_segments[3-1] writes to
 *   original_segments[3 + 2] = original_segments[5] โ€” OOB past the
 *   segments array (kernel stack cache_segments[8] or heap alloc).
 *
 * The correct reference behaviour is bus_dmamap_load_mbuf_segment at
 * line 869, which passes CONSTANT (segs, maxsegs) and lets *segp be
 * absolute end-index in the original array.
 *
 * Reachability on the audit guest:
 *   Only three in-tree callers of bus_dmamap_load_uio on pc64:
 *     sys/dev/raid/mpr/mpr.c:3533
 *     sys/dev/raid/mps/mps.c:2097
 *     sys/dev/disk/isp/isp_pci.c:1912
 *   All three are HBA (SAS/SCSI/fabric) drivers; none are present on
 *   the QEMU guest (only virtio-blk, virtio-net, atapi cd). The symbol
 *   bus_dmamap_load_uio IS in the live kernel (/boot/kernel/kernel),
 *   so the broken code path is compiled-in and active on the default
 *   GENERIC kernel โ€” it just needs an HBA driver instance to invoke it.
 *
 * This harness documents the trigger conditions and the fix.
 */
#include <stdio.h>

int main(void)
{
    printf("=== DF-1035 trigger documentation ===\n");
    printf("\nVulnerable path (bus_dmamap_load_uio, multi-iovec uio):\n");
    printf("  caller (mpr.c/mps.c/isp_pci.c) builds a uio with N>=2 iovecs\n");
    printf("  where the first iov maps to more than dmat->nsegments/2 DMA\n");
    printf("  segments, then invokes bus_dmamap_load_uio()\n");
    printf("    -> loop over uio->uio_iov\n");
    printf("       -> _bus_dmamap_load_buffer(segs=segments+M, nsegments=N-M,\n");
    printf("                                 segp=&nsegs)\n");
    printf("          *segp is the absolute index M from the prior iov\n");
    printf("          KKASSERT(M <= N-M)  -- fires on GENERIC (INVARIANTS on)\n");
    printf("          without INVARIANTS: sg = &local_segments[M-1]\n");
    printf("                              = &original_segments[M+M-1]\n");
    printf("                              -- OOB write into the segments array\n");
    printf("\n");
    printf("Status on this audit guest: not live-reachable โ€” none of the\n");
    printf("three HBA callers (mpr/mps/isp) attach. The broken function IS\n");
    printf("in /boot/kernel/kernel (verified via nm). Fix validated by\n");
    printf("nativekernel rebuild โ€” see VERDICT.md.\n");
    return 0;
}