sdhci_start_data treats bus_dmamap_load()==EINPROGRESS as a plain failure: the deferred sdhci_adma2_getaddr callback stays armed against the shared ADMA2 descriptor table (and the map against a freed buffer) β potential mid-transfer descriptor rewrite -> controller DMA to stale addresses
Summary
On DF/amd64 bus_dmamap_load returns EINPROGRESS when the 32-bit adma2_tag needs bounce pages and the shared pool is exhausted: the map is queued with the callback saved. sdhci_start_data handles EINPROGRESS exactly like EFBIG - falls back to PIO - so the deferred load survives the request; when pages free up, busdma re-runs the load against the (now freed) buffer and fires sdhci_adma2_getaddr, rewriting the shared adma2_descs while an unrelated later ADMA2 request may be streaming from that table; the next request also re-loads the still-queued map (double-enqueue). finish_data skips unload on the fallback. Requires whitelisted-ADMA2 controller + >4GB host + transient bounce exhaustion. Hardware-gated; defensive fail-the-command fix compile-proven (sdhci.ko -Werror).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4191 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | verdict-summary | the claim, preconditions, why untested | 2.9 KB | β raw |
| VERDICT.md | verdict | full line-by-line source trace | 4.1 KB | β raw |
| fix.diff | suggested-fix | defensive: EINPROGRESS fails the command instead of PIO fallback with the load armed | 900 B | view raw |
| sdhci_fix_apply.log | apply-log | patch(1) application on guest /usr/src | 287 B | view raw |
| sdhci_module_build.log | fix-build-log | sdhci.ko module build with fix, -Werror, MAKE_RC=0 | 23.5 KB | view raw |
DF-4191 β sdhci_start_data mishandles bus_dmamap_load() == EINPROGRESS: deferred sdhci_adma2_getaddr stays armed against the live ADMA2 descriptor table
File: sys/dev/disk/sdhci/sdhci.c (pass 2, GLM 5.3). Speculative β
hardware-gated (no SDHCI controller emulation in stock QEMU), verified by
source-level trace; the defensive fix is compile-proven (module build).
The claim (source trace)
sdhci_start_data() loads the ADMA2 map with BUS_DMA_WAITOK
(sys/dev/disk/sdhci/sdhci.c:1326-1328). On DragonFly/amd64,
bus_dmamap_load() can return EINPROGRESS when the map needs bounce pages
(data buffer above BUS_SPACE_MAXADDR_32BIT) and the bounce pool is
temporarily exhausted (sys/platform/pc64/x86_64/busdma_machdep.c:644-657:
map queued on bounce_map_waitinglist, map->callback/map->buf saved,
error returned to caller).
sdhci_start_data() treats any non-zero error identically
(sdhci.c:1329-1340): prints "Dma load for ADMA2 fail: %d" and falls back
to PIO for this request β but the deferred load still owns
slot->adma2_map and will later re-run _bus_dmamap_load_buffer with the
saved map->buf (the request's data->data, freed after the bio completes)
and fire sdhci_adma2_getaddr(slot) (sdhci.c:598-630), which rewrites the
shared slot->adma2_descs descriptor table β while an unrelated later
request may be mid-ADMA2-transfer using that same table (next request reloads
the same map, which is still on the waiting list β map double-enqueue as
well). Additionally sdhci_finish_data() skips bus_dmamap_unload() for the
PIO fallback (sdhci.c:1413-1425 gated on SDHCI_USE_ADMA2), leaving the map
attached to a stale buffer.
Impact if realized: (a) ADMA2 descriptor table rewritten mid-DMA β controller fetches descriptors pointing at the previous request's (freed) buffer addresses β device DMA into/out of stale kernel memory (memory corruption / info leak); (b) busdma waiting-list corruption from re-loading a queued map.
Preconditions: ADMA2 enabled (whitelisted controllers β sdhci_pci.c:100-128,
sdhci_acpi.c:147,170-171, or hw.sdhci.adma2_test=1), host RAM > 4 GB so
mmcsd bounce is needed for the 32-bit-limited tag, and transient bounce-page
exhaustion at that instant. Narrow but real window; card I/O load influences
the timing.
Why not reproduced
The audit guest has no SDHCI controller (QEMU does not emulate one), and the
bug requires physical bounce-page pressure interacting with live hardware
DMA β not reproducible in the harness meaningfully better than the trace.
Classified untested / speculative accordingly.
Fix
fix.diff (defensive): treat EINPROGRESS as a hard failure of the command
(set slot->curcmd->error = MMC_ERR_INVALID, drop SDHCI_USE_ADMA2, return)
instead of continuing in PIO with the deferred load armed.
Compile-validated: sdhci.ko module builds with the fix under -Werror on
the guest's /usr/src copy (sdhci_module_build.log, MAKE_RC=0).
DF-4191 β VERDICT
Status: UNTETED / SPECULATIVE (hardware-gated; source-verified mechanism) Impact (if realized): device-DMA to stale addresses (kernel memory corruption) / busdma map-state corruption Confidence: speculative
Mechanism, line by line
-
sdhci_start_data()β ADMA2 path:sys/dev/disk/sdhci/sdhci.c:1320-1340c err = bus_dmamap_load(slot->adma2_tag, slot->adma2_map, data->data, data->len, sdhci_adma2_getaddr, slot, dumping ? BUS_DMA_NOWAIT : BUS_DMA_WAITOK); /* :1326-1328 */ if (err != 0) { /* :1329 */ device_printf(slot->bus, "Dma load for ADMA2 fail: %d\n", err); } else if (descs[0].address == 0) { ... } if (err != 0 || descs[0].address == 0) { slot->flags &= ~SDHCI_USE_ADMA2; /* :1338 */ goto pio_fallback; /* :1339 */ }err == EINPROGRESSis handled exactly likeEFBIG/ENOMEM: fall back to PIO. The request proceeds and completes. Nothing cancels the deferred load. -
Why EINPROGRESS is possible: the adma2 tag is created 32-bit-limited (
sdhci.c:549-554,BUS_SPACE_MAXADDR_32BIT). Buffers at physical addresses above 4 GB need bounce pages; when none are free,_bus_dmamap_load_bufferqueues the map withmap->buf/map->buflen/map->callbacksaved and returns EINPROGRESS (sys/platform/pc64/x86_64/busdma_machdep.c:644-657,bus_dmamap_load:793-803returns it to the driver). The pre-allocation fromBUS_DMA_ALLOCNOW|BUS_DMA_ALLOCALLonly sizes the initial pool toatop(maxsize)(busdma_machdep.c:405-411); shared-zone exhaustion under system-wide pressure still defers. -
Later, when bounce pages free up, busdma re-runs the load with the SAVED
map->buf(already-completed request'sdata->dataβ the bio buffer may be long gone) and callssdhci_adma2_getaddr(sdhci.c:598-630), which writesslot->adma2_descsβ the descriptor table a concurrently running ADMA2 request may be streaming from. Descriptor rewrite mid-transfer = controller consumes addresses of the previous request's buffer β DMA to stale/freed kernel pages. -
Compounding: the PIO fallback skips
bus_dmamap_unload()βsdhci_finish_data()unloads only underSDHCI_USE_ADMA2(sdhci.c:1413-1425) β and the next ADMA2 request callsbus_dmamap_load()on the still-queued map (map->pagesneeded != 0), re-reserving and re-enqueueing (busdma_machdep.c:613-660) β waiting-list double-enqueue of the same map.
Attack surface / preconditions
- ADMA2 active: whitelisted Intel/AMD controllers (
sdhci_pci.c:100-128,sdhci_acpi.c:147,170-171) orhw.sdhci.adma2_test=1. - Host with > 4 GB RAM (mmcsd bounce for the 32-bit tag).
- Transient bounce-pool exhaustion exactly at an ADMA2 load; sustained disk I/O (attacker-controllable as any local user reading/writing the card) widens the window.
Why untested
No SDHCI controller exists on the audit guest (stock QEMU), and the failure
mode needs live hardware DMA plus allocator pressure; a userspace replica
would only restate the trace. Filed as speculative per pass-2 rules; the
defensive fix.diff is compile-proven (sdhci.ko under -Werror,
MAKE_RC=0 β sdhci_module_build.log).
Kernel references
- sys/dev/disk/sdhci/sdhci.c:1326-1340 (EINPROGRESS treated as plain failure)
- sys/dev/disk/sdhci/sdhci.c:598-630 (deferred callback target, shared descs)
- sys/dev/disk/sdhci/sdhci.c:1413-1425 (unload skipped on PIO fallback)
- sys/dev/disk/sdhci/sdhci.c:549-554 (32-bit-limited adma2 tag)
- sys/platform/pc64/x86_64/busdma_machdep.c:644-657, :793-803 (EINPROGRESS contract: callback deferred, map->buf saved)
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testabledefensive fix compiles clean (sdhci.ko, -Werror, MAKE_RC=0); behavioral validation impossible without SDHCI hardware
sdhci_module_build.log (MAKE_RC=0, sdhci.ko 43512 bytes linked); sdhci_fix_apply.log
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md line-by-line trace; sdhci_fix_apply.log; sdhci_module_build.log (MAKE_RC=0, sdhci.ko linked)
PoC changes
no seed; no harness (hardware+allocator-pressure dependent); source trace + compile-proven defensive fix only
Verified recommended fix
treat EINPROGRESS from bus_dmamap_load as a hard command failure (MMC_ERR_INVALID) in the ADMA2 path instead of falling back to PIO with the deferred load armed; long-term cancel/unload deferred loads on request teardown
Verdict
Speculative, source-verified mechanism, hardware-gated on the audit guest (no SDHCI controller in stock QEMU). sdhci_start_data() (sdhci.c:1326-1340) treats bus_dmamap_load()==EINPROGRESS (deferred bounce load; map->buf/map->callback saved by busdma_machdep.c:644-657/793-803) exactly like a hard failure and falls back to PIO, leaving the deferred sdhci_adma2_getaddr() callback armed against the shared slot->adma2_descs table and the map attached to the request's soon-freed buffer (unload is skipped on the fallback, sdhci.c:1413-1425). When bounce pages free up, the deferred callback rewrites descriptors that a later in-flight ADMA2 request may be streaming from (device DMA to stale addresses) and re-loading the queued map double-enqueues it. Preconditions: whitelisted-ADMA2 controller + >4GB RAM bounce + transient pool exhaustion β narrow, hence speculative. Defensive fix.diff compile-proven: sdhci.ko builds under -Werror with it (MAKE_RC=0).
No comments yet.