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

CIS-controlled PCMCIA I/O window size drives out-of-allocation port I/O in ata_pccard_attach()

Summary

The pccard bus returns the CIS-declared window verbatim - lengths 9..14 put ATA_CONTROL at fixed offset 14 outside the allocation, lengths 1..7 leave offsets 0..7 outside; the ALTSTAT alias makes both the control write and the status-poll read paths poke adjacent host device ports. Physical-card trigger, no kernel memory access. Validate-window-before-wiring fix (git-apply verified).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5175 Β· 6 files
FileTypeDescriptionSize
README.md β€” 4.6 KB ↓ raw
VERDICT.md β€” 4.9 KB ↓ raw
cis_window_trace.md β€” 2.0 KB ↓ raw
fix.diff β€” 1.2 KB view raw
manifest.json β€” 1.4 KB view raw
verdict.json β€” 3.5 KB view raw

DF-5175 β€” CIS-controlled PCMCIA I/O window size drives out-of-allocation port I/O in ata_pccard_attach()

What

sys/dev/disk/nata/ata-card.c wires ATA register access offsets onto the I/O resource returned by the pccard bus without validating that the resource actually covers those offsets. On 16-bit PCMCIA, the size of that resource is the I/O window length declared in the card's CIS (CFE tuple), i.e. fully card-controlled. Two windows of sizes make the wiring go out of bounds:

  • size 9..14 β€” rman_get_size(io) > ATA_IOSIZE (ata-card.c:103) is true, so ATA_CONTROL is mapped at fixed offset 14 into the window (ata-card.c:104-106). A 1-byte access at window_start+14 lies outside the allocation for every size ≀ 14.
  • size 1..7 β€” the > ATA_IOSIZE test is false, the driver takes the separate-ctlio branch, and the primary registers are wired at offsets 0..7 (ata-card.c:93-96) against a window shorter than 8 ports.

Every subsequent ATA control-path access then performs out-of-allocation I/O:

  • writes: ATA_IDX_OUTB(ch, ATA_CONTROL, …) from ata_generic_reset() / command setup (sys/dev/disk/nata/ata-lowlevel.c) β€” 1-byte writes of the ATA control byte (values from the small set 0x00/0x02/0x04/0x08/0x0a/0x0e…);
  • reads: ATA_ALTSTAT is aliased onto the same resource+offset by ata_default_registers() (sys/dev/disk/nata/ata-all.c:734-735) and polled for status.

For the size<8 case the OOB offsets 1..7 receive command/LBA/feature bytes (semi-attacker-influenced values once the "disk" answers IDENTIFY).

Precondition / threat model

  • Attacker must physically insert a malicious 16-bit PCMCIA card whose CIS declares PCCARD_FUNCTION_DISK (probe matches unconditionally, ata-card.c:63-67) and a first CFE I/O window of length 9..14 (or 1..7).
  • CardBus (32-bit) cards do not go through this file β€” they attach via the PCI path (ata-pci.c); this file registers on the pccard bus only (ata-card.c:163).
  • Impact ceiling: 1-byte port pokes at fixed offsets just past (or within a truncated) PCMCIA window. The bridge window is programmed with the exact CIS length (byte granularity), so the stray accesses stay on the host bus and can land in a neighboring device's port range (serial / FDC / PIT class registers live in the same allocator region). No kernel memory is touched; worst case is perturbation of adjacent hardware (device malfunction, potential system instability).

Severity: Low (AV:P β€” physical insertion required). This is a robustness/defense-in-depth bound-validation bug, not a memory-corruption bug.

Why untested here

The verification guest (QEMU/KVM, virtio hardware) has no PCMCIA/cbb controller and no slot, so the trigger cannot be instantiated. The proof below is a complete code-level trace from card-controlled CIS bytes to the bus_space_* call. Reproducing on real hardware requires a programmable PCMCIA card or a bridge-level emulator declaring the crafted CFE.

Reference trigger (for hardware testers)

CIS contents needed (attribute memory):

  • CISTPL_DEVICE / CISTPL_VERS_1: minimal valid card header;
  • CISTPL_FUNCID: function = 4 (disk);
  • CISTPL_CONFIG + CISTPL_CFTABLE_ENTRY: IO space descriptor with num_iospace = 1, window length 10 (0x09 stored, +1 by the parser), addr size 2 bytes, start = 0 (any);
  • one IRQ line.

Expected observable: ata_pccard_attach succeeds; during ata_attach β†’ ATA_RESET, an outb of the control byte lands on window_base + 14, i.e. 4 ports past the 10-port window owned by the card. A logic analyzer on the host bus, or an I/O-port trap on a hypervisor that models a pccard bridge, shows the access outside the programmed window.

Reproduce (code-level)

Read VERDICT.md β€” it traces every hop with path:line:

CIS CFE bytes (card-controlled)
  β†’ pccard_cis.c:1076-1092   length = tuple bytes + 1   (no 8/16 constraint)
  β†’ pccard.c:429-449         window pre-allocated with exactly that length
  β†’ pccard.c:1121-1149       child bus_alloc_resource returns the window
                             verbatim; requested count ATA_IOSIZE ignored
  β†’ ata-card.c:103-106       size>8 β‡’ ATA_CONTROL at offset 14 (OOB for ≀14)
  β†’ ata-card.c:93-96         offsets 0..7 wired unconditionally (OOB for <8)
  β†’ ata-all.c:734-735        ATA_ALTSTAT aliased to same res+offset
  β†’ ata-lowlevel.c           ATA_IDX_OUTB/INB β‡’ bus_space_write/read_1 at
                             window_start+14 (or +1..+7) β€” outside allocation

No build is required or possible for this pack (no target hardware in the audit environment). fix.diff is the verified-by-inspection fix.

VERDICT.md
↓ download raw

DF-5175 β€” VERDICT

Status: untested (hardware-gated; Low severity β†’ Phase V skipped per audit contract). Not a false positive: the defect is proven by complete code-level trace below; only runtime demonstration is impossible in the audit environment (QEMU guest has no PCMCIA controller / slot).

Reproduced?

No. Neither a physical PCMCIA slot nor a pccard-bridge emulator exists in the audit guest. Impact therefore recorded as none (not demonstrated), confidence likely (every link in the chain is source-provable; the only empirical unknown is real-hardware window programming, which is dictated by the ExCA/cbb window registers taking the exact CIS length).

The chain, hop by hop

  1. CIS parse β€” window length is raw card bytes. sys/bus/pccard/pccard_cis.c:1074-1092: for PCCARD_TPCE_IO_RANGE_LENGTHSIZE_{ONE,TWO,FOUR}, cfe->iospace[i].length = pccard_tuple_read_{1,2,4}(tuple, idx) then length++. Any length 1..2^32 is accepted; there is no 8/16-only constraint anywhere in the parser.

  2. Bus pre-allocates the window with exactly the CIS length. sys/bus/pccard/pccard.c:429-449 (pccard_function_init): for each CFE IO space, len = cfe->iospace[i].length; bus_alloc_resource(bus, SYS_RES_IOPORT, &rid=i, start, end, len, rman_make_alignment_flags(len)). Alignment is pow2-rounded placement; the resource size stays len. resource_list_add records the same length. Note rid = i β€” the first CIS window becomes the child's ATA_IOADDR_RID (0) resource.

  3. Child allocation returns the CIS-sized window, requested count ignored. sys/dev/disk/nata/ata-card.c:87-90 asks for ATA_IOSIZE (8) ports, but pccard_alloc_resource (sys/bus/pccard/pccard.c:1121-1149) finds the pre-existing rle for rid 0 (set in step 2) and returns rle->res verbatim β€” the requested count is never compared against the window. Hence rman_get_size(io) at ata-card.c:103 is the card-chosen CIS length, not 8.

  4. Unvalidated offset wiring. * ata-card.c:93-96: offsets 0..7 wired for ATA_DATA..ATA_COMMAND (OOB whenever CIS length < 8). * ata-card.c:103-106: rman_get_size(io) > ATA_IOSIZE (β‰₯9) β‡’ ATA_CONTROL at offset 14 (ATA_PCCARD_CTLOFFSET, sys/dev/disk/nata/ata-all.h:95). A 1-byte access at +14 is inside the allocation only for length β‰₯ 15. CIS lengths 9..14 make every control access out-of-allocation. * ata-card.c:119 β†’ ata_default_registers() (sys/dev/disk/nata/ata-all.c:723-735) aliases ATA_ALTSTAT onto the same {res, offset} β€” so the status-poll read path is OOB too.

  5. Sinks. ATA_IDX_OUTB/INB expand to bus_space_write_1/read_1(rman_get_bustag(res), rman_get_bushandle(res), offset, …) (sys/dev/disk/nata/ata-all.h:679-712) β€” x86 outb/inb at window_base+offset, no bounds checks. Control writes happen in ata_generic_reset()/ata_generic_command() (ata-lowlevel.c) during ata_attach β†’ ATA_RESET (sys/dev/disk/nata/ata-all.c:128-131), i.e. on the very first attach of the malicious card; status reads poll ATA_ALTSTAT.

  6. Reachability gate. ata_pccard_probe returns 0 for any PCCARD_FUNCTION_DISK card (ata-card.c:63-67) β€” no product-table match needed. The driver registers on the pccard bus only (ata-card.c:163); CardBus cards use the PCI path (ata-pci.c) and are unaffected by this file's wiring.

Why this is NOT DF-2173 (known, not re-reported)

DF-2173 = resource leak (io/ctlio orphaned when ata_probe/ata_attach fail, ata-card.c:125-128, because newbus never calls detach after failed attach). DF-5175 = out-of-allocation port I/O on the success path caused by trusting the CIS window size when wiring register offsets. Different sink (r_io offsets vs. resource lifetime), different trigger (CIS geometry vs. attach failure), different code lines (93-106 vs 125-128).

Impact ceiling (honest)

1-byte port writes with near-fixed values (control byte bits IDS/RESET/4BIT/ HOB) at window+14, or semi-controlled command/LBA bytes at window+1..7 for short windows, hitting whatever host device owns the adjacent ports. No kernel memory is read or written; no privilege boundary is crossed by software. Physical access required. β†’ Low, hardening/robustness.

Fix validation

Not runnable (same hardware gate). fix.diff fails the attach closed for windows < 8 and only takes the shared-control-at-14 path for windows β‰₯ 15 (ATA_PCCARD_CTLOFFSET + ATA_CTLIOSIZE), both expressed with the existing named constants. Authored against the read-only sys/ tree; never applied there.

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.

cis_window_trace.md
↓ download raw

DF-5175 β€” code-level proof chain (the "trigger" is card attribute memory, not a program)

GATE sys/dev/disk/nata/ata-card.c:63-67 fcn == PCCARD_FUNCTION_DISK => probe matches ANY disk card ALLOC sys/dev/disk/nata/ata-card.c:87-90 bus_alloc_resource(..., ATA_IOADDR_RID=0, 0, ~0, ATA_IOSIZE=8, RF_ACTIVE) WINDOW sys/bus/pccard/pccard.c:429-449 window pre-allocated by pccard with len = CIS iospace[0].length RETURN sys/bus/pccard/pccard.c:1121-1149 pccard_alloc_resource returns rle->res verbatim; count(8) NEVER checked vs window CIS-LEN sys/bus/pccard/pccard_cis.c:1074-1092 length = raw tuple byte(s) + 1 ; any value accepted WIRE-A sys/dev/disk/nata/ata-card.c:93-96 r_io[0..7].offset = i => OOB if length < 8 WIRE-B sys/dev/disk/nata/ata-card.c:103-106 size > 8 => r_io[ATA_CONTROL].offset = 14 => OOB if length <= 14 ALIAS sys/dev/disk/nata/ata-all.c:734-735 ATA_ALTSTAT <= r_io[ATA_CONTROL] (same res+offset, read path OOB too) RESET sys/dev/disk/nata/ata-all.c:128-131 ata_attach -> ATA_RESET on first attach SINK sys/dev/disk/nata/ata-all.h:679-712 ATA_IDX_OUTB/INB => bus_space_write_1/read_1(bustag, bushandle, offset) HOST x86: outb/inb at window_base+offset, outside the rman allocation and outside the bridge window (ExCA window programmed with the exact CIS length, byte granularity)

Reference CIS geometry for hardware testers (16-bit PCMCIA attribute memory): CISTPL_DEVICE, CISTPL_VERS_1 minimal card header CISTPL_FUNCID function = 4 (disk) CISTPL_CONFIG + CISTPL_CFTABLE_ENTRY TPCE_IO with HASRANGE, num_iospace=1, LENGTHSIZE_ONE, length byte = 0x09 (=> 10 ports) => 10 > ATA_IOSIZE(8) but 10 <= 14 => control at +14 is 4 ports OOB variant: length byte = 0x00 (=> 1 port) => primary regs at offsets 0..7 on a 1-port window

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored against read-only sys/ and git-apply-verified on a pristine copy; runtime fix validation impossible for the same hardware reason as the trigger (no PCMCIA controller in any available environment); changes fail the attach closed for undersized windows and make the shared-control branch require a window that actually covers offset 14

findings/poc/DF-5175/fix.diff; verified 'APPLIES_CLEAN' + 'MATCHES_INTENDED' against a pristine copy of sys/dev/disk/nata/ata-card.c
↓ fix.diffper-fix-DF-5175

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-5175/VERDICT.md (hop-by-hop path:line trace); cis_window_trace.md (chain + reference CIS geometry); fix.diff (applies clean, verified with git apply --check on a pristine copy)

PoC changes

n/a β€” no seeded PoC existed; the pack documents a code-level proof chain plus reference CIS geometry for hardware testing instead of an executable

Verified recommended fix

Validate rman_get_size(io) in ata_pccard_attach: fail closed for windows < ATA_IOSIZE, and only use the shared control-at-offset-14 layout when size >= ATA_PCCARD_CTLOFFSET + ATA_CTLIOSIZE (see fix.diff)

Verdict

Untested at runtime because the trigger requires a malicious 16-bit PCMCIA card in a physical slot and the QEMU verification guest has no pccard/cbb controller; the defect itself is fully proven by source trace. The pccard bus returns the CIS-declared I/O window verbatim to ata_pccard_attach (pccard.c:1121-1149, window sized at pccard.c:429-449 from raw CIS bytes at pccard_cis.c:1074-1092), and ata-card.c wires register offsets without validating coverage: window lengths 9..14 put ATA_CONTROL at fixed offset 14 (ata-card.c:103-106) outside the allocation, lengths 1..7 leave primary register offsets 0..7 (ata-card.c:93-96) outside it; reads are aliased through ATA_ALTSTAT (ata-all.c:734-735). Result is 1-byte out-of-allocation port I/O (near-fixed control-byte writes, status reads) at fixed offsets near the window β€” adjacent-host-device perturbation at worst, no kernel memory access, physical access required. Low severity; Phase V PoC/fix-build skipped per audit contract for Low hardening findings; fix.diff authored and git-apply-verified against the pristine tree.