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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4191 Β· 5 files
FileTypeDescriptionSize
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
README.md verdict-summary the claim, preconditions, why untested
↓ download 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).

VERDICT.md verdict full line-by-line source trace
↓ download raw

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

  1. sdhci_start_data() β€” ADMA2 path: sys/dev/disk/sdhci/sdhci.c:1320-1340 c 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 == EINPROGRESS is handled exactly like EFBIG/ENOMEM: fall back to PIO. The request proceeds and completes. Nothing cancels the deferred load.

  2. 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_buffer queues the map with map->buf/map->buflen/map->callback saved and returns EINPROGRESS (sys/platform/pc64/x86_64/busdma_machdep.c:644-657, bus_dmamap_load :793-803 returns it to the driver). The pre-allocation from BUS_DMA_ALLOCNOW|BUS_DMA_ALLOCALL only sizes the initial pool to atop(maxsize) (busdma_machdep.c:405-411); shared-zone exhaustion under system-wide pressure still defers.

  3. Later, when bounce pages free up, busdma re-runs the load with the SAVED map->buf (already-completed request's data->data β€” the bio buffer may be long gone) and calls sdhci_adma2_getaddr (sdhci.c:598-630), which writes slot->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.

  4. Compounding: the PIO fallback skips bus_dmamap_unload() β€” sdhci_finish_data() unloads only under SDHCI_USE_ADMA2 (sdhci.c:1413-1425) β€” and the next ADMA2 request calls bus_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) or hw.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

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_testable
baseline no→ patch + rebuild →patched clean

defensive 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
↓ fix.diffper-fix-DF-4191

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).