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)
PoC verification
Evidence pack
findings/poc/DF-4837 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_PREWRITEfor every bio (reads included); ida_done()gates the post-completion work onqcb->flags & DMA_DATA_TRANSFER(ida.c:506) β which is 0 for every bio β soBUS_DMASYNC_POSTREADnever runs andbus_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_unloadto 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=0for both arms (page returned by the unload).
See run.log, VERDICT.md, fix.diff.
DF-4837 β VERDICT
REPRODUCED (logic/bounce-accounting harness; guest has no Compaq SMART/IDA controller β DF-4559 methodology).
The code path (traced)
ida_construct_qcb()β sys/dev/raid/ida/ida.c:373-417: - :391qcb->flags = 0;β never givenDMA_DATA_IN/DMA_DATA_OUTfor the bio path (contrastida_command():350, which setsflags | IDA_COMMANDfor internal commands). - :396-401 loads the buffer and syncs withop = qcb->flags & DMA_DATA_IN ? BUS_DMASYNC_PREREAD : BUS_DMASYNC_PREWRITEβ always PREWRITE, including forBUF_CMD_READbios.ida_done()β ida.c:499-538: - :506if (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.
- 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'sBUS_SPACE_MAXADDR_32BIT(ida_pci.c:260-265; busdma_machdep.c:272newtag->lowaddr = MIN(parent->lowaddr, newtag->lowaddr)), so on >4 GB machines pages above 4 GB bounce (addr_needs_bounce, busdma_machdep.c:180-189). - 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_pageappends tomap->bpages; only unload/return_bounce_pagesgives 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
fixedValidated 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.
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.
No comments yet.