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

ida bio path never sets DMA direction flags: bounced READ data never copied back (silent disk corruption) and dmamap_unload/bounce pages leak on >4GB hosts

Summary

qcb->flags=0 on the bio path so ida_done's DMA_DATA_TRANSFER gate is false for ALL disk I/O: POSTREAD sync (the only bounce copyback) and dmamap_unload are skipped. On >4GB hosts the 32-bit lowaddr forces bouncing - every bounced READ completes B_OK with STALE buffer contents (silent integrity corruption) and one bounce page leaks per I/O (unbounded growth). Benign on <=4GB (which is why it appears to work). Harness-verified with verbatim flag logic (baseline STALE + leaked=N; fixed delivers data + leaked=0). Per-direction flags + assign-before-sync fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4837 Β· 11 files
FileTypeDescriptionSize
README.md β€” 2.1 KB ↓ raw
VERDICT.md β€” 3.4 KB ↓ raw
df4837_harness.c β€” 5.3 KB view raw
build.sh β€” 94 B view raw
run.sh β€” 89 B view raw
build.log β€” 99 B view raw
run.log β€” 15.6 KB view raw
env.txt β€” 242 B view raw
fix.diff β€” 1.1 KB view raw
manifest.json β€” 812 B view raw
verdict.json β€” 4.1 KB view raw

DF-4837 β€” ida bio path leaves qcb->flags == 0: bounced READ data never copied back + bounce pages leak

  • File: sys/dev/raid/ida/ida.c:391 (with :399-401 and :506-513)
  • Severity: Medium (silent disk-data corruption + per-I/O bounce-page leak on

    4 GB machines with this 32-bit PCI card; benign on ≀4 GB boxes)

  • Reproduced: logic/bounce-accounting harness on the DragonFly guest (no Compaq SMART controller exists in the guest).

Root cause

ida_construct_qcb() clears qcb->flags = 0 (ida.c:391) and never sets the DMA direction for disk I/O, so:

  • ida.c:399-401 computes the pre-sync as BUS_DMASYNC_PREWRITE for every bio (reads included);
  • ida_done() gates the post-completion work on qcb->flags & DMA_DATA_TRANSFER (ida.c:506) β€” which is 0 for every bio β€” so BUS_DMASYNC_POSTREAD never runs and bus_dmamap_unload() is never called for disk I/O.

On machines with >4 GB RAM this is not cosmetic: the buffer tag inherits the parent's 32-bit limit (ida_pci.c:260-265; DFly clamps child lowaddr, busdma_machdep.c:272 MIN(parent->lowaddr, ...)), so buffer pages above 4 GB bounce. DFly busdma then requires

  • bus_dmamap_sync(PREWRITE) origβ†’bounce (busdma_machdep.c:1044-1053) β€” done,
  • bus_dmamap_sync(POSTREAD) bounceβ†’orig (busdma_machdep.c:1054-1063) β€” skipped: the device's read data stays in the bounce page and the caller's buffer keeps stale contents (silent data corruption on every bounced READ),
  • bus_dmamap_unload to return bounce pages to the zone free list (map->bpages, add_bounce_page/return_bounce_pages) β€” skipped: each load appends bounce pages that are never returned β†’ unbounded kernel memory leak that grows with every disk I/O.

Build / run (on the DragonFly guest)

cc -O2 -Wall -o df4837 df4837_harness.c
./df4837

Expected

  • BASELINE single READ: caller buffer STALE (device data LOST), leaked=1.
  • BASELINE 64 READs: leaked=64 (one bounce page per I/O, none returned).
  • FIXED (flags set per direction, per fix.diff): buffer has device data, leaked=0 for both arms (page returned by the unload).

See run.log, VERDICT.md, fix.diff.

VERDICT.md
↓ download raw

DF-4837 β€” VERDICT

REPRODUCED (logic/bounce-accounting harness; guest has no Compaq SMART/IDA controller β€” DF-4559 methodology).

The code path (traced)

  1. ida_construct_qcb() β€” sys/dev/raid/ida/ida.c:373-417: - :391 qcb->flags = 0; β€” never given DMA_DATA_IN/DMA_DATA_OUT for the bio path (contrast ida_command() :350, which sets flags | IDA_COMMAND for internal commands). - :396-401 loads the buffer and syncs with op = qcb->flags & DMA_DATA_IN ? BUS_DMASYNC_PREREAD : BUS_DMASYNC_PREWRITE β€” always PREWRITE, including for BUF_CMD_READ bios.
  2. ida_done() β€” ida.c:499-538: - :506 if (qcb->flags & DMA_DATA_TRANSFER) β€” 0 for every bio β†’ the whole block is skipped:
    • :509-511 POSTREAD sync never executes,
    • :512 bus_dmamap_unload() never called.
  3. Why it matters only above 4 GB: the buffer tag is created with lowaddr = BUS_SPACE_MAXADDR (ida.c:209-213) but DFly clamps the child to the parent's BUS_SPACE_MAXADDR_32BIT (ida_pci.c:260-265; busdma_machdep.c:272 newtag->lowaddr = MIN(parent->lowaddr, newtag->lowaddr)), so on >4 GB machines pages above 4 GB bounce (addr_needs_bounce, busdma_machdep.c:180-189).
  4. Bounce data path (busdma_machdep.c:1034-1067): the device DMAs into the bounce page; only bus_dmamap_sync(BUS_DMASYNC_POSTREAD) copies bounce→orig (:1054-1063). Skipped ⇒ every bounced READ completes "successfully" with the buffer's previous contents — silent disk data corruption for the FS layer. add_bounce_page appends to map->bpages; only unload/return_bounce_pages gives them back — skipped ⇒ one leaked bounce page per I/O, unbounded.

On ≀4 GB machines the tag never bounces (lowaddr β‰₯ ptoa(Maxmem) β‡’ no BUS_DMA_BOUNCE_LOWADDR), sync/unload are bookkeeping-only and the bug is latent β€” which is why this ancient driver "works" on small boxes.

What the harness proves (run.log)

Faithful model of the map/zone bookkeeping (map_load reserves a bounce page per load; PREWRITE copies orig→bounce; POSTREAD copies bounce→orig; unload returns pages) driven by the verbatim flag logic of ida.c:

  • BASELINE, 1 READ: buffer stays STALE (device data lost), 1 bounce page held.
  • BASELINE, 64 READs: still stale, 64 bounce pages held (none returned).
  • FIXED (per-direction flags per fix.diff): buffer receives the device data, 0 pages held after either arm (page returned by unload).

Impact

  • Integrity: silently wrong data returned for filesystem reads whose pages live above 4 GB (no error surfaced β€” bio completes B_OK).
  • Availability: unbounded kernel-memory leak growing with every bounced I/O; eventual memory pressure / stall.
  • Precondition: >4 GB RAM + a Compaq SMART/IDA (32-bit PCI) controller β€” old card in a big box (data-recovery setups are exactly this). No attacker control needed beyond ordinary disk I/O once the config exists.

Fix

Set the DMA direction flags on the bio path (and, in ida_command(), assign qcb->flags before the pre-sync instead of after) β€” fix.diff.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Validated with the harness fixed-model arms: with per-direction flags, ida_done performs the POSTREAD copyback and the unload β€” buffer receives device data and 0 bounce pages remain held after 1 and 64 READs; baseline loses data and leaks one page per I/O. fix.diff not applied to the read-only sys/ tree; kernel-build validation not meaningful without an IDA controller in the guest.

run.log FIXED lines: 'FIXED : after 1 READ bios: caller buffer has device data ... (leaked=0)' and same for 64; fix.diff in pack.
↓ fix.diffper-fix-DF-4837

Confirmed kernel references

Detail

Exploit chain

no privilege chain β€” integrity/availability bug: >4GB host + 32-bit Compaq SMART/IDA card -> ordinary disk I/O -> bounced READs silently return stale page contents (filesystem-level data corruption) + one leaked bounce page per I/O (unbounded kernel memory leak, eventual stall)

Evidence (decisive lines)

run.log: 'BASELINE: after 1 READ bios: caller buffer STALE (device data LOST); bounce pages allocated=1 still held=1 (leaked=1)'; 'BASELINE: after 64 READ bios: ... allocated=64 still held=64 (leaked=64)'; 'FIXED : after 64 READ bios: caller buffer has device data; bounce pages allocated=1 still held=0 (leaked=0)'. VERDICT.md has the full path:line trace incl. the busdma clamp and POSTREAD copyback.

PoC changes

No seed PoC (pass-2 new finding). Harness models DFly busdma bounce bookkeeping (per-load page reservation, PREWRITE orig->bounce, POSTREAD bounce->orig, unload returns pages) and drives it with the exact flag logic of ida_construct_qcb()/ida_done(), baseline vs fixed arms.

Verified recommended fix

Set qcb->flags = DMA_DATA_TRANSFER | (read ? DMA_DATA_IN : DMA_DATA_OUT) in ida_construct_qcb() and assign qcb->flags in ida_command() before the pre-sync (fix.diff in pack)

Verdict

REPRODUCED (logic/bounce-accounting harness; guest has no IDA controller β€” DF-4559 methodology). ida_construct_qcb() clears qcb->flags=0 (ida.c:391) and never sets DMA direction for bios, so the :399-401 pre-sync is always BUS_DMASYNC_PREWRITE and ida_done()'s :506 gate (flags & DMA_DATA_TRANSFER == 0) skips BOTH the POSTREAD sync and bus_dmamap_unload for every disk I/O. On >4GB machines the buffer tag bounces (parent 32-bit clamp, busdma_machdep.c:272; ida_pci.c:260-265): POSTREAD is where bounce->orig copyback happens (busdma_machdep.c:1054-1063) so every bounced READ completes B_OK with the buffer's STALE contents (silent disk data corruption for the FS layer), and every load leaks its bounce page (add_bounce_page appends; only unload returns them) β€” unbounded kernel memory growth with I/O. On <=4GB boxes the tag never bounces and the bug is latent. Harness with faithful map/zone bookkeeping driven by the verbatim flag logic: baseline 1 READ -> STALE buffer + 1 page held; 64 READs -> 64 pages held; fixed model (per-direction flags) delivers device data and holds 0 pages.