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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3691 Β· 12 files
FileTypeDescriptionSize
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 :1685 width = be16toh(configs[0].total_disks) β€” 16-bit (0..65535)
  • HPTv2 :1868 width = meta->array_width (u8)
  • HPTv3 :2039/:2045/:2051/:2057 width = configs[0].total_disks (u8, or /2)
  • LSI v2 :2828/:2835/:2843 array_width (u8)
  • LSI v3 :2971/:2976 total_disks/array_width (u8)
  • nVidia :3108 array_width (u8)
  • Promise :3248 array_width (u8)
  • SII :3563 raid0_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 chooses drv in [0, width); :400-409 reads rdp->disks[drv].flags OOB and sets request->dev = rdp->disks[drv].dev from OOB heap β†’ ata_raid_send_request β†’ device_get_softc(wild).
  • RAID1/01 :421-426, :441-463 β€” reads disks[drv + width] OOB; :581 drv += width β†’ :585-587 request->dev = disks[width].dev (wild) and rdp->disks[width].last_lba = blkno + chunk β€” OOB write of a user-chosen 64-bit value at disks + width*48 + 32.
  • RAID1/01 rebuild composites :504-505, :562-563 β€” composite->request[drv] / composite->request[this] with this = drv Β± width up to 2*width-1: request[] is 32 entries (MAX_COMPOSITES), width β‰₯ 16 writes a kernel pointer OOB past the objcache'd ata_composite.
  • ata_raid_config_changed:941-949 β€” RAID1/01 READY gating reads disks[disk + width] OOB at attach time.
  • ata_raid_done:681-684/:743 β€” mirror = this Β± width OOB 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.

VERDICT.md
↓ download raw

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 drv in [0,width); OOB read of a device_t at ar_softc + disks_off + drv*48 used as a pointer β†’ wild/fake-device deref. With heap grooming this is a type-confused struct ata_device * (max_iosize/mode/unit consumed, request queued through device_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 at disks + width*48 + 32) and composite->request[this] = <ata_request *> (:504-505/:562-563, kernel pointer written up to request[2*width-1], array is 32). These require the OOB flags word at disks[width].flags to 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 β†’ drv up to 65534, ~3.1 MB away from disks[]).

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched 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)
↓ fix.diffper-fix-DF-3691

Confirmed kernel references

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).