No parser validates array width β RAID0/5 strategy indexes disks[] OOB via user-chosen LBA (wild device_t deref); RAID1/01 paths OOB-write disks[width].last_lba and composite->request[]
Summary
Every parser copies an attacker 'array width' with no MAX_DISKS check (10 parser sites), while total_disks (which gates READY) is an independent field - so a checksum-valid Promise/FreeBSD RAID0 array with total_disks=1 and width=64 assembles READY, and strategy computes drv=(blkno/interleave)%width, reads disks[drv].flags/.dev OOB and derefs the OOB pointer. RAID1/01 additionally read disks[drv+width], take drv+=width leading to request->dev=disks[width].dev (wild) and the OOB WRITE disks[width].last_lba=blkno+chunk (user-chosen 64-bit value), plus composite->request[] pointer writes past request[32]; config_changed reads disks[disk+width] OOB at attach; done() derefs disks[this+-width]. REPRODUCED on the guest: width=64 disk panicked at boot in device_get_softc (fault VA 0x80, dev=NULL read OOB) with zero interaction. FIX VALIDATED on rebuilt kernel: 'ar0: illegal array geometry (width 64)', I/O fails cleanly, boot completes. Crafted disk metadata -> assembled array -> any I/O (including the kernel's own partition scan at boot) yields OOB reads with wild device_t deref; write primitives are heap-groom dependent (chains documented, not completed).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3691 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.3 KB | β raw | |
| VERDICT.md | β | 3.6 KB | β raw | |
| mkmeta.py | β | 4.0 KB | view raw | |
| build.sh | β | 133 B | view raw | |
| run.sh | β | 153 B | view raw | |
| run.log | β | 787 B | view raw | |
| panic.txt | β | 11.1 KB | view raw | |
| run.patched.log | β | 13.1 KB | view raw | |
| fix.diff | β | 3.8 KB | view raw | |
| env.txt | β | 505 B | view raw | |
| verdict.json | β | 3.6 KB | view raw | |
| manifest.json | β | 4.4 KB | view raw |
DF-3691 β No parser validates array width; the I/O strategy turns it into OOB disks[] access (incl. wild dev deref and OOB last_lba writes)
Root cause (source)
Every vendor parser copies an attacker-controlled "array width" into
raid->width with no check against MAX_DISKS (disks[] is 16 entries):
- Adaptec
:1685width = be16toh(configs[0].total_disks)β 16-bit (0..65535) - HPTv2
:1868width = meta->array_width(u8) - HPTv3
:2039/:2045/:2051/:2057width = configs[0].total_disks(u8, or /2) - LSI v2
:2828/:2835/:2843array_width(u8) - LSI v3
:2971/:2976total_disks/array_width(u8) - nVidia
:3108array_width(u8) - Promise
:3248array_width(u8) - SII
:3563raid0_disks(u8)
total_disks (which gates READY) is a separate metadata field in most
formats, so an array can be READY with a single online disk while width is
arbitrary. The volumetric I/O path then indexes disks[] with width-derived
values:
- RAID0/RAID5
ata_raid_strategy:360/:366βdrv = (blkno / interleave) % width(RAID0) β user LBA choosesdrvin [0, width);:400-409readsrdp->disks[drv].flagsOOB and setsrequest->dev = rdp->disks[drv].devfrom OOB heap βata_raid_send_requestβdevice_get_softc(wild). - RAID1/01
:421-426,:441-463β readsdisks[drv + width]OOB;:581drv += widthβ:585-587request->dev = disks[width].dev(wild) andrdp->disks[width].last_lba = blkno + chunkβ OOB write of a user-chosen 64-bit value atdisks + width*48 + 32. - RAID1/01 rebuild composites
:504-505,:562-563βcomposite->request[drv]/composite->request[this]withthis = drv Β± widthup to 2*width-1:request[]is 32 entries (MAX_COMPOSITES), width β₯ 16 writes a kernel pointer OOB past the objcache'data_composite. ata_raid_config_changed:941-949β RAID1/01 READY gating readsdisks[disk + width]OOB at attach time.ata_raid_done:681-684/:743βmirror = this Β± widthOOB deref.
Reproduce (this pack used the RAID0 path)
f2_width.img = FreeBSD/Promise-native metadata (vendor-independent
fallback parser β any controller): RAID0, interleave=1, width=64,
total_disks=1, disks[0] ONLINE, checksum valid. Boot with the disk
attached:
ar0: 48MB <FreeBSD PseudoRAID RAID0 (stripe 0 KB)> status: READY ar0: disk0 READY using ad0 at ata0-master ar0: MBR magic not found; assume a COMPATIBILITY_SLICE (s0) Fatal trap 12: page fault while in kernel mode fault virtual address = 0x80 instruction pointer = 0x8:0xffffffff8068cc00 current process = Idle Stopped at device_get_softc: movq 0x80(%rdi),%rax
The kernel's own MBR slice-scan reads /dev/ar0; for blkno % 64 >= 16 the
strategy computes drv >= 16, reads disks[drv].dev OOB (zeroed heap β
NULL) and dereferences it: fault VA 0x80 = offsetof(struct device, softc)
with dev == NULL. No user process needed β presenting the disk is enough.
Expected
Baseline: panic shown above (captured in panic.txt).
Patched (fix.diff "illegal array geometry" guard in ata_raid_strategy):
array still assembles but every bio fails EIO; boot completes, no fault
(run.patched.log).
Files
mkmeta.py (shared builder), run.sh, panic.txt, run.patched.log,
fix.diff, verdict.json.
DF-3691 VERDICT
Status: reproduced (panic; deterministic at boot from one crafted disk).
Reproduction
Crafted FreeBSD/Promise RAID0 metadata (checksum-valid, single online disk,
total_disks=1, interleave=1, width=64) on ata0-master:
the array assembled READY at boot (ar0: 48MB <FreeBSD PseudoRAID RAID0> status:
READY), then the kernel's own slice-scan I/O drove
drv = (blkno / interleave) % width = blkno % 64 past disks[15]:
Fatal trap 12: page fault while in kernel mode fault virtual address = 0x80 current process = Idle Stopped at device_get_softc: movq 0x80(%rdi),%rax
0x80 = offsetof(struct device, softc); %rdi = request->dev =
rdp->disks[drv].dev read OOB from zeroed heap β NULL. The exact sink is
sys/dev/disk/nata/ata-raid.c:400-410 (flags OOB read passes because zeros
don't look PRESENT|ONLINE) β :409 request->dev = rdp->disks[request->this].dev
β :410 ata_raid_send_request β device_get_softc(NULL).
Primitive characterization
- RAID0/RAID5 (demonstrated): user-selected LBA picks
drvin [0,width); OOB read of adevice_tatar_softc + disks_off + drv*48used as a pointer β wild/fake-device deref. With heap grooming this is a type-confusedstruct ata_device *(max_iosize/mode/unit consumed, request queued throughdevice_get_parent(dev)chain) β an exploitation path that was not pursued to uid0 in this run (baseline heap behind ar_softc was zeros β clean NULL-deref panic). - RAID1/RAID01 (source-proven, same root cause): OOB writes β
rdp->disks[drv+width].last_lba = blkno + chunk(:567/:587, user-chosen 64-bit value atdisks + width*48 + 32) andcomposite->request[this] = <ata_request *>(:504-505/:562-563, kernel pointer written up torequest[2*width-1], array is 32). These require the OOB flags word atdisks[width].flagsto have AR_DF_ONLINE (heap-groom dependent), which is why the RAID0 read path was used for the deterministic demonstration. - Width sources include a 16-bit field (Adaptec
be16toh(total_disks), up to 65535 βdrvup to 65534, ~3.1 MB away fromdisks[]).
Why READY with width=64 passes sanity checks
ata_raid_config_changed loops disk < rdp->total_disks (=1, online) for
RAID0; nothing cross-checks width vs total_disks/MAX_DISKS anywhere.
ata_raid_attach:157 rounds total_sectors down by interleave*width but
never validates width.
Fix validation
fix.diff adds a geometry guard at the top of ata_raid_strategy
(width <1 or >MAX_DISKS; RAID1/01 2*width >MAX_DISKS; RAID5 width<2 β
biofinish EIO) and clamps the RAID1/01 loop in ata_raid_config_changed.
Patched kernel: same disk boots, array still announced READY at attach
(status logic for total_disks=1 RAID0 is unchanged), but all I/O returns EIO
("illegal array geometry (width 64)" on first request), boot completes to
login, no fault. run.patched.log. fix_status = fixed.
Distinction from known findings
DF-1174/DF-2496 cover divide-by-zero from zero width/interleave.
DF-1173/DF-2493/DF-2497 cover disk_number/total_disks indexing and the
SPAN/JBOD walk. No prior finding covers width-driven indexing in the
RAID0/1/01/5 strategy, config_changed, done, or the composite
request[] writes.
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
fixedPatched kernel, same disk: array still announced at attach but first I/O hits 'ar0: illegal array geometry (width 64)', partition table read fails cleanly with EIO, boot reaches login, no fault. Baseline boot panic gone.
run.patched.log: 'ar0: illegal array geometry (width 64)' + 'error accessing offset 0 ... for 512' + vm.sh up succeeded (ssh alive)
Confirmed kernel references
- sys/dev/disk/nata/ata-raid.c:1685
- sys/dev/disk/nata/ata-raid.c:1868
- sys/dev/disk/nata/ata-raid.c:2039
- sys/dev/disk/nata/ata-raid.c:3108
- sys/dev/disk/nata/ata-raid.c:3248
- sys/dev/disk/nata/ata-raid.c:3563
- sys/dev/disk/nata/ata-raid.c:360
- sys/dev/disk/nata/ata-raid.c:400
- sys/dev/disk/nata/ata-raid.c:409
- sys/dev/disk/nata/ata-raid.c:421
- sys/dev/disk/nata/ata-raid.c:504
- sys/dev/disk/nata/ata-raid.c:567
- sys/dev/disk/nata/ata-raid.c:587
- sys/dev/disk/nata/ata-raid.c:943
Detail
Exploit chain
crafted disk (Promise-native RAID0, width=64) -> array READY at boot -> kernel slice-scan bio -> drv = blkno%64 >= 16 -> rdp->disks[drv].dev read OOB -> wild/NULL device_t deref in ata_raid_send_request -> fatal trap. Exploitation beyond panic (heap-groom the OOB dev into a fake ata_device, or use RAID1 last_lba OOB write) not pursued this run; primitive documented in VERDICT.md.
Evidence (decisive lines)
panic.txt + run.log (fault VA 0x80 = offsetof(device,softc) with dev=NULL), VERDICT.md primitive characterization, run.patched.log ('illegal array geometry (width 64)', I/O error, boot completes)
PoC changes
built metadata from scratch (mkmeta.py); promise struct offsets verified against real header via guest-compiled layout probe (raid@512, array_width@546, disk[8]@564, checksum@2044); first try worked
Verified recommended fix
geometry guard at top of ata_raid_strategy (width 1..MAX_DISKS; RAID1/01 2*width<=MAX_DISKS; RAID5 width>=2 -> EIO) plus clamped RAID1/01 loop in ata_raid_config_changed (fix.diff hunks 3+5)
Verdict
No parser validates raid->width against MAX_DISKS; total_disks and width are independent metadata fields, so a checksum-valid Promise/FreeBSD RAID0 array with total_disks=1 (READY with one online disk) and width=64 assembles at boot, and the first kernel I/O (MBR slice scan) computes drv=(blkno/interleave)%width=blkno%64, indexes rdp->disks[32+].dev OOB and dereferences the OOB-read pointer (NULL from zeroed heap) in device_get_softc - fatal page fault at boot, no user interaction. RAID1/01 variants additionally OOB-WRITE disks[width].last_lba (user-chosen 64-bit value) and composite->request[this] (kernel pointer) per source analysis; RAID0 path used for the deterministic demo. Width sources include a 16-bit Adaptec field (up to 65535).
No comments yet.