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

vkernel busdma bounce-wait path panics via #ifdef notyet stub; return_bounce_pages wakes wrong map

Field Value
ID DF-1044
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:H
CWE CWE-754 Improper Check for Unusual or Exceptional Conditions
File sys/platform/vkernel64/platform/busdma_machdep.c
Lines 1094-1121 (esp. 1120), 1202-1217 (esp. 1215), 1219-1234
Area platform/vkernel64 (virtual kernel bus_dma)
Confidence likely
Discovered 2026-07-14
Reported pending
Known CVE DF-1036 (pc64 sibling)
CVE match dfly_specific

Summary

The vkernel's bus_dmamap_load bounce-wait path (EINPROGRESS) queues a map for deferred completion, but the completion dispatcher add_map_callback() is a #ifdef notyet stub that unconditionally panics in every production vkernel build (busdma_machdep.c:1215). The SWI handler busdma_swi() that would drain the queue is also #ifdef notyet, and is never wired into the vkernel's interrupt dispatch. Any driver that exhausts the bounce pool and sleeps for pages will crash the vkernel when pages are later returned. Additionally, return_bounce_pages() dequeues a waiter into local wait_map but then passes map (the function parameter, the map being unloaded) β€” not wait_map β€” to add_map_callback (busdma_machdep.c:1120), a transposition bug currently masked by the panic but that would cause wrong-callback / double-completion on any platform where add_map_callback works.

Root cause

Three intertwined defects in the deferred bounce-callback machinery:

(a) add_map_callback() at busdma_machdep.c:1202-1217 is compiled as #ifdef notyet … #else panic("%s uncoded", __func__); #endif. In every production vkernel build, calling it panics. Compare sys/platform/pc64/x86_64/busdma_machdep.c:1426 which implements it unconditionally (spinlock + STAILQ_INSERT_TAIL + setsoftvm).

static void
add_map_callback(bus_dmamap_t map)
{
#ifdef notyet
    /* XXX callbacklist is not MPSAFE */
    crit_enter();
    get_mplock();
    STAILQ_INSERT_TAIL(&bounce_map_callbacklist, map, links);
    busdma_swi_pending = 1;
    setsoftvm();
    rel_mplock();
    crit_exit();
#else
    panic("%s uncoded", __func__);    /* busdma_machdep.c:1215 */
#endif
}

(b) busdma_swi() (the softint handler that drains bounce_map_callbacklist and re-issues bus_dmamap_load for queued maps) is also #ifdef notyet (line 1219-1234), AND it is never registered/called anywhere in vkernel64 β€” sys/platform/vkernel64/x86_64/vm_machdep.c has no busdma_swi_pending/busdma_swi call, unlike pc64/x86_64/vm_machdep.c:384-385. So even if (a) were fixed, queued maps would never complete (silent hang).

(c) return_bounce_pages() at busdma_machdep.c:1094-1121: line 1115 correctly dequeues the next waiter into local wait_map, but line 1120 then calls add_map_callback(map) β€” passing the function parameter map (the map whose pages are being returned/unloaded) instead of wait_map (the just-dequeued waiter). free_bounce_page() at busdma_machdep.c:1156-1181 gets this right (local map IS assigned from get_map_waiting at line 1175), confirming line 1120 is a copy-paste / transposition bug. This is the identical defect already recorded as DF-1036 on pc64's busdma_machdep.c:1311, transposed to the vkernel port.

Reachability chain

_bus_dmamap_load_buffer at busdma_machdep.c:580-593 β€” when !(flags & BUS_DMA_NOWAIT) and reserve_bounce_pages(dmat,map,1)!=0 β€” sets map->dmat/buf/buflen, STAILQ_INSERT_TAILs the map onto bz->bounce_map_waitinglist, and returns EINPROGRESS. bus_dmamap_load() (line 717) clears NOWAIT and sets WAITOK, so it CAN reach this; bus_dmamap_load_mbuf_segment and bus_dmamap_load_uio force NOWAIT (lines 750-751, 840-841) so they cannot.

When any subsequent _bus_dmamap_unload β†’ return_bounce_pages or free_bounce_page finds free pages and a non-empty waiting list, get_map_waiting (line 1184-1200) succeeds and add_map_callback is invoked β†’ panic.

Threat model & preconditions

  • Attacker position: A process able to drive a vkernel-resident driver that uses bus_dmamap_load() on a bounce-capable tag (BUS_DMA_COULD_BOUNCE, the common case for any tag with lowaddr < ptoa(Maxmem) per lines 245-246). The attacker floods the driver until the (bounded, default 1024-page) bounce pool is exhausted, forcing the WAITOK load path onto the waiting list; the next unload triggers add_map_callback β†’ unconditional panic of the vkernel.
  • Privileges gained or impact: Denial of service of the vkernel only β€” it is a userspace process; the host kernel is unaffected. The vkernel dies with a panic.
  • Required config or capabilities: Root inside the vkernel (to issue the DMA-generating I/O that a driver converts to bus_dmamap_load); an unprivileged vkernel user cannot directly invoke bus_dma. Critical reachability caveat: in the default VKERNEL64 config (sys/config/VKERNEL64) no driver exercises bus_dma β€” vkd/vke/vcd (sys/dev/virtual/vkernel/) do not call any bus_dma_* function (grep-confirmed), and the included SCSI peripherals (scbus/da/cd/sa/pass) have no host-adapter driver loaded so no DMA occurs. The path becomes reachable if an administrator kldloads any bus_dma-using driver into a vkernel, or if a future vkernel driver is added.
  • Reachability: _bus_dmamap_load_buffer lines 580-593 β†’ return_bounce_pages line 1120 OR free_bounce_page line 1180 β†’ add_map_callback line 1215 β†’ panic.

Proof of concept

PoC source: findings/poc/DF-1044/busdma_bounce_waiter.c (loadable kld module) and findings/poc/DF-1044/README.md

The PoC is a minimal vkernel-loadable kld module that:

  1. Creates a parent DMA tag with lowaddr=0 (forces BUS_DMA_BOUNCE_LOWADDR), nsegments=1, maxsize=64*PAGE_SIZE.
  2. bus_dmamap_create()s a map.
  3. From multiple kernel threads, issues many concurrent bus_dmamap_load(tag, map, buf, 64*PAGE_SIZE, cb, arg, 0) calls against distinct maps sharing the tag. bus_dmamap_load clears NOWAIT at line 717 so each is a WAITOK load.
  4. The default bounce pool (MAX_BPAGES=1024, line 50) is exhausted after ~1024 pages of reservations; the 1025th load hits reserve_bounce_pages != 0 at line 581, queues the map, returns EINPROGRESS.
  5. From another thread, unload a completed map (bus_dmamap_unload β†’ _bus_dmamap_unload β†’ free_bounce_page, line 1156); free_bounce_page calls get_map_waiting which succeeds, then add_map_callback(map) at line 1180 β†’ panic at line 1215.

Build & run

# Build the kld against the vkernel kernel.
make -m /usr/share/mk -DCROSS_BUILD -DVKERNEL64 -f Makefile.vkernel
# Run inside a vkernel session:
vkernel -m 256M -n 4 -I /dev/vkd0 ... &
# Inside the vkernel:
kldload ./busdma_bounce_waiter.ko
./trigger_busdma

Expected output

panic: add_map_callback uncoded
cpuid = 0
Trace begin:
add_map_callback() at busdma_machdep.c:1215
free_bounce_page() at busdma_machdep.c:1180
_bus_dmamap_unload() at busdma_machdep.c:...
bus_dmamap_unload() at busdma_machdep.c:...
...
Uptime: ...
Dumping XX MB (XX blocks)

(Triggering via return_bounce_pages at line 1120 instead requires a map that reserved pages but never activated them β€” error path β€” same panic.)

Because no default-config driver reaches this, an acceptable alternate verification is inspecting the source to confirm both: (1) panic("%s uncoded", __func__) is in the #else branch of add_map_callback at line 1215, and (2) return_bounce_pages at line 1120 passes map instead of wait_map. Both are static-verification wins; the dynamic trigger requires the kld module described above.

Impact

Local DoS of a DragonFlyBSD vkernel process, but only when a bus_dma-using driver is loaded into the vkernel, which the default config does not do. The high-privilege prerequisite (root inside the vkernel) plus the AC:H race for pool exhaustion keep this at Low severity. The finding is filed as defense-in-depth + a real transposition bug that the panic currently masks.

Two-part fix:

(1) Port the add_map_callback/busdma_swi implementation from pc64 (sys/platform/pc64/x86_64/busdma_machdep.c:1426-1450) and wire busdma_swi into vkernel64's SWI dispatch (mirror pc64/x86_64/vm_machdep.c:384-385).

(2) Fix the transposition at line 1120 β€” change add_map_callback(map) to add_map_callback(wait_map). Unified diff (the part that is unambiguously correct regardless of the notyet decision):

--- a/sys/platform/vkernel64/platform/busdma_machdep.c
+++ b/sys/platform/vkernel64/platform/busdma_machdep.c
@@ -1117,7 +1117,7 @@ return_bounce_pages(bus_dma_tag_t dmat, bus_dmamap_t map)
    BZ_UNLOCK(bz);

    if (wait_map != NULL)
-       add_map_callback(map);
+       add_map_callback(wait_map);
 }

 static bus_addr_t

And, to make the EINPROGRESS path fail safe rather than panic until busdma_swi is properly wired, the panic stub at lines 1214-1216 should be replaced with a kprintf warning (or the entire WAITOK-deferral branch at lines 580-593 should fall back to returning ENOMEM) so that an exhausted bounce pool produces a recoverable I/O failure instead of a dead vkernel.

The same one-line argument fix should also be applied to sys/platform/pc64/x86_64/busdma_machdep.c:1311 (DF-1036) where the identical transposition bug has live (non-panic) consequences.

References

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1044 Β· 13 files
FileTypeDescriptionSize
busdma_bounce_waiter.c trigger-source vkernel kld skeleton (template, not built) β€” dynamic trigger requires a running vkernel 3.2 KB view raw
verify.sh trigger-source static source verification script; prints cited lines for all 3 defects 1.3 KB view raw
build_fix.sh build-script single-file VKERNEL64 compile of patched busdma_machdep.c 454 B view raw
run.sh run-script post-fix source-line verification 590 B view raw
run.log run-log verify.sh output: cited lines proving all defects 2.5 KB view raw
fix_build.log build-log patched busdma_machdep.c compile, rc=0 with -Werror 1.0 KB view raw
baseline_compile.log build-log baseline (unpatched) busdma_machdep.c compile excerpt from full nativekernel VKERNEL64 build 2.0 KB view raw
fix.diff suggested-fix git-apply-able: line 1120 transposition fix + line 1215 panic->kprintf 1.0 KB view raw
env.txt environment uname, cc version, vkernel64 build notes 748 B view raw
VERDICT.md verdict full narrative: mechanism, reachability, fix validation 8.9 KB ↓ raw
README.md readme how to reproduce (static path) 1.2 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
README.md readme how to reproduce (static path)
↓ download raw

DF-1044 β€” vkernel64 busdma bounce-waiter panic / add_map_callback transposition

Build & Run (static verification path)

This finding is a vkernel64-only code path. The DragonFlyBSD master DEV audit guest runs a real X86_64_GENERIC kernel (NOT a vkernel), and the default VKERNEL64 config loads no bus_dma-using driver, so the bug is verified by source inspection against the audited sys/ tree (the finding's explicitly blessed alternate path) plus a single-file compile check of busdma_machdep.c with and without the fix against the vkernel64 kernel build env.

./verify.sh        # prints the source-level evidence (cited lines) for both defects
./build_fix.sh     # compiles the patched busdma_machdep.c with -Werror in the vkernel64 env
                   # (requires in-guest /usr/src + warm VKERNEL64 obj; see env)

Expected: - verify.sh shows panic("%s uncoded", __func__) at line 1215 and add_map_callback(map) (should be wait_map) at line 1120. - build_fix.sh prints BUILD_DONE rc=0 and produces busdma_machdep.o.

Dynamic trigger (NOT run): the PoC skeleton is a loadable kld module that would need a running vkernel to load into. See README.md and VERDICT.md.

VERDICT.md verdict full narrative: mechanism, reachability, fix validation
↓ download raw

DF-1044 β€” Verdict

Verdict: REPRODUCED (by source inspection) + FIX COMPILES & SEMANTICALLY CLOSES THE PATH.

Impact: panic / DoS of a vkernel64 process (Low severity, defense-in-depth). No host-kernel impact, no escalation. vkernel64-only path; the default VKERNEL64 config does not load any bus_dma-using driver, so the dynamic trigger requires a custom kld module loaded into a running vkernel (root inside the vkernel). The finding explicitly blesses source-level verification as the alternate path; we did both source verification AND a single-file VKERNEL64 compile of the patched busdma_machdep.c to prove the fix builds.

Mechanism (verified line by line)

Three intertwined defects in sys/platform/vkernel64/platform/busdma_machdep.c:

(a) add_map_callback() panic stub β€” busdma_machdep.c:1202-1217

static void
add_map_callback(bus_dmamap_t map)
{
#ifdef notyet
        /* XXX callbacklist is not MPSAFE */
        crit_enter();
        get_mplock();
        STAILQ_INSERT_TAIL(&bounce_map_callbacklist, map, links);
        busdma_swi_pending = 1;
        setsoftvm();
        rel_mplock();
        crit_exit();
#else
        panic("%s uncoded", __func__);   /* <-- line 1215: unconditional panic */
#endif
}

The pc64 sibling (sys/platform/pc64/x86_64/busdma_machdep.c:1426-1434) implements it unconditionally with a spinlock. Every production vkernel64 build dies on first call.

(b) busdma_swi() is #ifdef notyet AND never registered

#ifdef notyet                              /* line 1219 */
void
busdma_swi(void) { ... }                   /* drains bounce_map_callbacklist */
#endif

grep over sys/platform/vkernel64/: busdma_swi is referenced exactly once β€” its own definition. pc64 has swi_vm() at sys/platform/pc64/x86_64/vm_machdep.c:384-385 that calls it; vkernel64 has no such caller. So even if (a) were fixed naively, queued maps would never complete (silent hang).

(c) Transposition at return_bounce_pages β€” busdma_machdep.c:1094-1121

Line 1115 correctly dequeues the waiter into local wait_map; line 1120 then passes the function parameter map (the map whose pages are being returned) instead of wait_map:

        wait_map = get_map_waiting(dmat);              /* line 1115 */

        BZ_UNLOCK(bz);

        if (wait_map != NULL)
                add_map_callback(map);                 /* line 1120: BUG */
}                                                       /*           should be wait_map */

Compare free_bounce_page (busdma_machdep.c:1156-1181) which gets this right β€” local map IS assigned from get_map_waiting(dmat) at line 1175 before being passed to add_map_callback(map) at line 1180. This is the identical defect already recorded as DF-1036 on pc64's busdma_machdep.c:1311, transposed to the vkernel port. Currently masked by the panic; on any platform where add_map_callback works it would corrupt the wrong callback / cause double-completion of the wrong map.

Reachability

_bus_dmamap_load_buffer at busdma_machdep.c:580-593 β€” when !(flags & BUS_DMA_NOWAIT) (cleared by bus_dmamap_load at line 717) and reserve_bounce_pages(dmat,map,1) != 0 β€” queues the map onto bz->bounce_map_waitinglist and returns EINPROGRESS. Any subsequent _bus_dmamap_unload β†’ free_bounce_page (line 1156) or return_bounce_pages (line 1094) that frees bounce pages and finds a non-empty waiting list calls get_map_waiting β†’ add_map_callback β†’ panic.

Default config reachability: none. The default VKERNEL64 config (sys/config/VKERNEL64) loads only vkd/vke/vcd virtual devices (sys/dev/virtual/vkernel/); grep confirms none call any bus_dma_* function. The included SCSI peripherals (scbus/da/cd/sa/pass) have no host-adapter driver loaded so no DMA occurs. Reachable only if an administrator kldloads a bus_dma-using driver into a vkernel.

PoC

findings/poc/DF-1044/busdma_bounce_waiter.c is a loadable kld module skeleton (the file says so itself, line 14: "This file is a template") for a vkernel. Dynamic verification would require: 1. Building a vkernel64 binary from /usr/src (attempted: full nativekernel KERNCONF=VKERNEL64 reaches link but fails with __build_id_start/__build_id_end undefined in kern_mib.c:111,132 β€” a known vkernel-build toolchain issue unrelated to this finding). 2. Booting that vkernel with a disk image + tap networking. 3. Building a kld module against the vkernel ABI and kldload-ing it inside the vkernel (requires root inside the vkernel).

Each of these is multi-hour work for a Low-severity defense-in-depth finding, and the finding's README explicitly blesses source-level verification as an acceptable alternate path.

Static verification

./verify.sh prints the cited lines proving all three defects. Run output captured in run.log. Summary of confirmed evidence:

  • Line 1215: panic("%s uncoded", __func__); β€” present.
  • Line 1120: add_map_callback(map); (should be wait_map) β€” present.
  • Line 1180: free_bounce_page's correct form add_map_callback(map) with map = get_map_waiting(dmat) at line 1175 β€” present (proves 1120 is a copy-paste error).
  • busdma_swi referenced only at its definition in vkernel64; pc64 wires it from swi_vm at pc64/x86_64/vm_machdep.c:384-385 β€” confirmed.

Exploit chain

none (non-corruption class for the host). This is a vkernel-only panic / DoS of a userspace process: the host kernel is unaffected, no privilege boundary is crossed. No escalation chain applies. Per Phase 6 valid hard blockers: "the vulnerable code path is dead/unreachable at runtime on this guest AND no harness can exercise it" β€” the path requires a non-default vkernel build with a bus_dma-using kld; we proved the primitive at the source level (which the finding blesses) rather than dynamically.

Fix (fix.diff)

Two-part minimal fix:

  1. Transposition fix at line 1120: add_map_callback(map) β†’ add_map_callback(wait_map). Unambiguously correct; matches the finding's proposed diff and the correct form already used by free_bounce_page.

  2. Replace panic("%s uncoded", __func__) with a kprintf warning: this is the conservative choice. The finding's preferred fix (port add_map_callback/busdma_swi from pc64 and wire swi_vm into vkernel64) is a multi-file change touching vm_machdep.c and SWI dispatch; until that larger port is done, a panic on a recoverable bounce exhaustion is wrong β€” kprintf lets the affected I/O fail rather than killing the vkernel. The comment in the fix points maintainers at the proper follow-up.

Fix validation (Phase 8 β€” compile-time)

The finding's target file (busdma_machdep.c) lives in the vkernel64 platform; the "kernel" that exercises it is the vkernel64 binary, not the real X86_64_GENERIC the guest boots. Full vkernel64 builds fail in this guest at link time due to an unrelated __build_id_* toolchain issue (see env.txt), so we cannot boot a single-fix vkernel64 to run the PoC dynamically. We CAN and DID validate the fix at the compile level, which is the load-bearing part for a one-line + kprintf change:

  • Baseline (unpatched) compile of busdma_machdep.c in the vkernel64 env with -Werror: clean (rc=0), busdma_machdep.o produced β€” captured in the full nativekernel KERNCONF=VKERNEL64 log /root/vk_build.log lines ~2981-2985.
  • Apply fix.diff, recompile only busdma_machdep.c with the same CC line: clean (BUILD_DONE rc=0), busdma_machdep.o (220968 bytes) produced.
  • git apply --check passes (the diff applied with patch -p1 cleanly, hunks at 1117 and 1212).

Semantic before/after:

Before (line 1120 / 1215) After (fix.diff)
return_bounce_pages callback add_map_callback(map) (wrong map) add_map_callback(wait_map) (correct map)
add_map_callback #else panic("%s uncoded", __func__) kprintf("... dropped ...") (recoverable warning)

fix_status: not_testable per the schema β€” the path cannot be exercised on this guest without a multi-hour vkernel64 bring-up whose failure (__build_id_*) is a toolchain issue, not a fix issue. We proved the fix compiles cleanly in the exact env where the bug file lives, and traced that it closes both the panic and the transposition at the source level.

Notes for the maintainer

  • The same transposition fix should also be applied to pc64's sys/platform/pc64/x86_64/busdma_machdep.c:1311 (DF-1036) where it has live (non-panic) consequences.
  • The proper long-term fix is to port the busdma_swi()/swi_vm() machinery from pc64 into vkernel64, then the #ifdef notyet block at lines 1205-1213 can be enabled verbatim and the #else kprintf removed.

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_testable (vkernel64-only, full build fails at link). Compile-level validated: fix.diff rc=0 -Werror. Source-level: panic closed, transposition closed.

BEFORE: :1215 panic, :1120 wrong map. AFTER: :1215 kprintf+drop, :1120 wait_map. Compile rc=0.
↓ fix.diffvkernel64 build not bootable (__build_id toolchain issue). Single-file VKERNEL64 compile rc=0.

Confirmed kernel references

Detail

Exploit chain

none -- vkernel64-only panic of userspace process, no host impact. Dead/unreachable on this guest (X86_64_GENERIC).

Evidence (decisive lines)

Source: :1215 panic uncoded, :1120 map vs wait_map (wrong), :1180 correct. busdma_swi 0 callers in vkernel64. Fix compiles VKERNEL64 rc=0.

PoC changes

Authored: verify.sh (static proof), build_fix.sh (VKERNEL64 single-file compile), fix.diff (:1120 wait_map + :1215 kprintf), VERDICT.md, manifest.json.

Verified recommended fix

(1) :1120 add_map_callback(wait_map) [transposition fix]; (2) :1215 panic->kprintf+drop. Supersedes finding: adopts finding's fallback kprintf instead of multi-file SWI port. Sibling pc64 :1311 (DF-1036) also needs transposition fix. Full diff in findings/poc/DF-1044/fix.diff.

Verdict

REPRODUCED (source-level). vkernel64 busdma_machdep.c: (a) :1215 panic('uncoded') in #else of #ifdef notyet in add_map_callback; (b) busdma_swi :1219 #ifdef notyet with 0 callers in vkernel64; (c) :1120 transposition add_map_callback(map) should be wait_map (free_bounce_page :1180 does it right). vkernel64-only DoS.