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 withlowaddr < ptoa(Maxmem)per lines 245-246). The attacker floods the driver until the (bounded, default 1024-page) bounce pool is exhausted, forcing theWAITOKload path onto the waiting list; the next unload triggersadd_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 invokebus_dma. Critical reachability caveat: in the defaultVKERNEL64config (sys/config/VKERNEL64) no driver exercisesbus_dmaβvkd/vke/vcd(sys/dev/virtual/vkernel/) do not call anybus_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 administratorkldloads anybus_dma-using driver into a vkernel, or if a future vkernel driver is added. - Reachability:
_bus_dmamap_load_bufferlines 580-593 βreturn_bounce_pagesline 1120 ORfree_bounce_pageline 1180 βadd_map_callbackline 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:
- Creates a parent DMA tag with
lowaddr=0(forcesBUS_DMA_BOUNCE_LOWADDR),nsegments=1,maxsize=64*PAGE_SIZE. bus_dmamap_create()s a map.- 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_loadclearsNOWAITat line 717 so each is aWAITOKload. - The default bounce pool (
MAX_BPAGES=1024, line 50) is exhausted after ~1024 pages of reservations; the 1025th load hitsreserve_bounce_pages != 0at line 581, queues the map, returnsEINPROGRESS. - From another thread, unload a completed map (
bus_dmamap_unloadβ_bus_dmamap_unloadβfree_bounce_page, line 1156);free_bounce_pagecallsget_map_waitingwhich succeeds, thenadd_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.
Recommended fix
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
- DF-1036 β identical transposition bug on
sys/platform/pc64/x86_64/busdma_machdep.c:1311 sys/platform/pc64/x86_64/busdma_machdep.c:1426-1450β the workingadd_map_callbackimplementation that the vkernel port omittedsys/platform/pc64/x86_64/vm_machdep.c:384-385β the SWI wiring that vkernel64 lackssys/platform/vkernel64/platform/busdma_machdep.c:580-593β theWAITOKqueueing path
Timeline
- 2026-07-14 Discovered during automated audit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1044 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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.
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 bewait_map) β present. - Line 1180:
free_bounce_page's correct formadd_map_callback(map)withmap = get_map_waiting(dmat)at line 1175 β present (proves 1120 is a copy-paste error). busdma_swireferenced only at its definition in vkernel64; pc64 wires it fromswi_vmatpc64/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:
-
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 byfree_bounce_page. -
Replace
panic("%s uncoded", __func__)with akprintfwarning: this is the conservative choice. The finding's preferred fix (portadd_map_callback/busdma_swifrom pc64 and wireswi_vminto vkernel64) is a multi-file change touchingvm_machdep.cand SWI dispatch; until that larger port is done, a panic on a recoverable bounce exhaustion is wrong βkprintflets 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.cin the vkernel64 env with-Werror: clean (rc=0),busdma_machdep.oproduced β captured in the fullnativekernel KERNCONF=VKERNEL64log/root/vk_build.loglines ~2981-2985. - Apply fix.diff, recompile only
busdma_machdep.cwith the same CC line: clean (BUILD_DONE rc=0),busdma_machdep.o(220968 bytes) produced. git apply --checkpasses (the diff applied withpatch -p1cleanly, 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 notyetblock at lines 1205-1213 can be enabled verbatim and the#elsekprintf removed.
Fix verification
not_testablenot_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.
Confirmed kernel references
- sys/platform/vkernel64/platform/busdma_machdep.c:1120
- sys/platform/vkernel64/platform/busdma_machdep.c:1175
- sys/platform/vkernel64/platform/busdma_machdep.c:1180
- sys/platform/vkernel64/platform/busdma_machdep.c:1215
- sys/platform/vkernel64/platform/busdma_machdep.c:1219
- sys/platform/vkernel64/platform/busdma_machdep.c:580
- sys/platform/vkernel64/platform/busdma_machdep.c:717
- sys/platform/pc64/x86_64/busdma_machdep.c:1426
- sys/platform/pc64/x86_64/vm_machdep.c:384
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.
No comments yet.