AHCI command-table stride ATA_AHCI_CT_SIZE=256 != sizeof(struct ata_ahci_cmd_tab)=1152: latent work-area OOB the moment per-tag (NCQ) dispatch is enabled
Summary
The struct is 1152 bytes packed but every placement site (soft ctp and HW cmd_table_phys) strides by 256 - safe ONLY because tag is hardcoded 0 inside the 8KB work area. tag>=1 aliases live command tables; tag>=28 writes prd_tab past the work allocation (kernel-heap OOB with attacker-influenced FIS/PRD + OOB physical address programmed into the engine). Companion: KASSERT(nsegs<=64) sits AFTER the PRD write loop, bound held only by max_iosize=63*PAGE_SIZE. Self-consistent-constant + static-assert + hoisted-KASSERT fix to defuse before NCQ ever lands.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4810 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | file | 3.5 KB | β raw | |
| VERDICT.md | file | 2.1 KB | β raw | |
| fix.diff | file | 1.7 KB | view raw | |
| verdict.json | file | 2.8 KB | view raw |
DF-4810 β AHCI command-table stride (ATA_AHCI_CT_SIZE=256) β sizeof(struct ata_ahci_cmd_tab)=1152 β latent OOB of the DMA work area
File: sys/dev/disk/nata/chipsets/ata-ahci.c + sys/dev/disk/nata/ata-all.h (pass 2)
Severity: Info (hardening / defense-in-depth β not exploitable in the tree as-is) Β·
CWE-131 (incorrect calculation of buffer size) Β· confidence: certain
Build / run
Not applicable β this is a latent-hazard finding, proven by arithmetic on the
type/constant definitions. No PoC can exercise it today because tag is
hardcoded to 0 on every code path.
The mismatch
struct ata_ahci_cmd_tab (sys/dev/disk/nata/ata-all.h:249-255), __packed:
cfis[64] + acmd[32] + reserved[32] = 128 (== ATA_AHCI_CT_SG_OFFSET, ata-all.h:238 β) prd_tab[64] Γ sizeof(struct ata_ahci_dma_prd = 8+4+4 = 16) = 1024 total = 1152 bytes
but the stride used to place command tables inside the channel's DMA work area
is ATA_AHCI_CT_SIZE = 256 (sys/dev/disk/nata/ata-all.h:239), and every
indexing site uses that stride:
ata-ahci.c:254-255(ctp = work + ATA_AHCI_CT_OFFSET + ATA_AHCI_CT_SIZE*tag)ata-ahci.c:277(cmd-list placement uses CL_SIZE β consistent)ata-ahci.c:286-287(cmd_table_phys = work_bus + CT_OFFSET + CT_SIZE*tagβ this is what the hardware is told)
Why it is safe today (and why it is a landmine)
tagisint tag = 0at both sites (ata-ahci.c:250,ata-ahci.c:343); this driver never issues more than one command per port (no NCQ).- The work area is
MAXWSPCSZ = PAGE_SIZE*2 = 8192bytes (ata-dma.c:55-56, allocated atata-dma.c:130-145), so the single real table at[1280, 1280+1152) = [1280, 2432)fits. - If any future change derives
tagfrom the request (the natural NCQ evolution β FreeBSD's latersys/dev/ahcidoes exactly that): - tag β₯ 1: tables overlap/alias (stride 256 < struct 1152) β silent corruption of a live command's FIS/PRD by the next one;
- tag β₯ 28:
1280 + 28Γ256 = 8448 > 8192βctpandctp->prd_tabpoint past the end of the 8192-byte bus_dmamem_alloc'd work area (heap/DMA-memory OOB write with fully attacker-influenced content: FIS bytes and PRD addresses of the I/O), andcmd_table_physhands that OOB address to the AHCI engine for real DMA.
Secondary hardening items in the same function family:
ata_ahci_dmasetprd()(ata-ahci.c:531-546):KASSERT(nsegs <= ATA_AHCI_DMA_ENTRIES)sits after thefor (i = 0; i < nsegs; i++) prd[i] = ...write loop, and KASSERT is compiled out without INVARIANTS. Today the bound holds only structurally:max_iosize = 63ΓPAGE_SIZE(ata-ahci.c:558) caps a transfer at 64 pages-spanned == exactly 64 PRD entries ==prd_tab[64](the dma tag itself would permit up toATA_DMA_ENTRIES = 256segments,ata-dma.c:99,112). One careless bump ofmax_iosizeormax_iosize/PAGE_SIZEarithmetic change turns this into a live OOB write with zero margin.
Fix
Make the constant self-consistent and assert it (see fix.diff):
-#define ATA_AHCI_CT_SIZE 256
+/* must equal sizeof(struct ata_ahci_cmd_tab) = 128 + 64*16 = 1152 */
+#define ATA_AHCI_CT_SIZE 1152
plus _Static_assert(ATA_AHCI_CT_SIZE == sizeof(struct ata_ahci_cmd_tab), ...)
in ata-ahci.c, and move the KASSERT(nsegs <= ATA_AHCI_DMA_ENTRIES) before
the write loop in ata_ahci_dmasetprd. With tag still 0 the address
arithmetic is unchanged (CT_OFFSET + 0*CT_SIZE), so the constant fix is a
no-op at runtime today and only removes the trap.
DF-4810 β VERDICT
Status: untested (latent hardening finding; Phase V not applicable β the
hazard is provably unreachable in the tree as-is, tag hardcoded 0).
Reproduced?
No β by design of the finding: there is no runtime path today that can place a
command table at tag != 0. The defect is an arithmetic inconsistency between
ATA_AHCI_CT_SIZE (256, ata-all.h:239) and the real size of
struct ata_ahci_cmd_tab (64+32+32+64Γ16 = 1152 bytes, ata-all.h:249-255,
__packed, ATA_AHCI_CT_SG_OFFSET=128 confirming the 128-byte header). All
indexing and the hardware-visible cmd_table_phys (ata-ahci.c:254-255,
286-287) use the wrong stride, held safe only by int tag = 0
(ata-ahci.c:250, 343) inside the 8192-byte work allocation (MAXWSPCSZ,
ata-dma.c:55-56,130-145).
Impact ceiling if the constraint is ever lifted (why filed at all)
- tag β₯ 1 β adjacent command tables overlap (stride < struct): silent cross-command FIS/PRD corruption.
- tag β₯ 28 β
1280 + 28Γ256 = 8448 > 8192:ctp->prd_tabwrites past the end of the bus_dmamem_alloc'd work area (kernel-heap OOB write, content = attacker-influenced DMA addresses), and the same OOB physical address is programmed into the AHCI command list for the engine to DMA through.
This is exactly the change a NCQ enablement patch would make, which is why the constant deserves to be correct before that day. Filed as Info (defense-in-depth, no demonstrated impact today), confidence certain (the mismatch is a compile-time fact).
Recommended fix
ATA_AHCI_CT_SIZE 256 β 1152 + _Static_assert + move the
KASSERT(nsegs <= ATA_AHCI_DMA_ENTRIES) in ata_ahci_dmasetprd
(ata-ahci.c:544) above the write loop. See fix.diff (authored against the
read-only sys/ tree, never applied). Runtime-neutral today because only
tag 0 is used.
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_testablefix.diff corrects ATA_AHCI_CT_SIZE to 1152, adds a _Static_assert tying the constant to the struct, and moves the dmasetprd KASSERT before the write loop; runtime-neutral today (only tag 0 used, CT_OFFSET+0*CT_SIZE unchanged); not build-validated on the guest because no code path can exercise tag!=0
findings/poc/DF-4810/fix.diff
Confirmed kernel references
- sys/dev/disk/nata/ata-all.h:239
- sys/dev/disk/nata/ata-all.h:249
- sys/dev/disk/nata/ata-all.h:253
- sys/dev/disk/nata/chipsets/ata-ahci.c:250
- sys/dev/disk/nata/chipsets/ata-ahci.c:254
- sys/dev/disk/nata/chipsets/ata-ahci.c:286
- sys/dev/disk/nata/chipsets/ata-ahci.c:343
- sys/dev/disk/nata/chipsets/ata-ahci.c:538
- sys/dev/disk/nata/chipsets/ata-ahci.c:544
- sys/dev/disk/nata/chipsets/ata-ahci.c:558
- sys/dev/disk/nata/ata-dma.c:55
- sys/dev/disk/nata/ata-dma.c:130
Detail
Evidence (decisive lines)
findings/poc/DF-4810/VERDICT.md (arithmetic proof); findings/poc/DF-4810/fix.diff (constant fix + _Static_assert + KASSERT reorder)
PoC changes
not applicable (no seed PoC; finding produced by pass-2 static analysis)
Verified recommended fix
Set ATA_AHCI_CT_SIZE=1152 with _Static_assert(ATA_AHCI_CT_SIZE == sizeof(struct ata_ahci_cmd_tab)) and hoist the nsegs KASSERT above the PRD write loop
Verdict
Latent-hazard (Info) finding, not guest-verified because it is unreachable as-is: the AHCI command-table stride ATA_AHCI_CT_SIZE=256 (ata-all.h:239) disagrees with sizeof(struct ata_ahci_cmd_tab)=1152 (ata-all.h:249-255, __packed: 128-byte header + 64 PRDs x 16). All table placements and the hardware-visible cmd_table_phys (ata-ahci.c:254-255, 286-287) use the wrong stride and are held in-bounds only by the hardcoded tag=0 (ata-ahci.c:250, 343) within the 8192-byte work area (MAXWSPCSZ, ata-dma.c:55-56, 130-145). tag>=1 aliases live tables; tag>=28 writes prd_tab past the end of the bus_dmamem_alloc'd work area and programs the OOB physical address into the AHCI command list. Would become memory corruption the moment NCQ/per-tag dispatch is added. Also: ata_ahci_dmasetprd KASSERTs nsegs<=64 only AFTER the write loop (ata-ahci.c:538-544) and the true bound rests solely on max_iosize=63*PAGE_SIZE == exactly 64 page-spans == prd_tab[64] (ata-ahci.c:558) with the dma tag itself allowing 256 segments (ata-dma.c:99,112).
No comments yet.