Kernel stack/heap OOB write via untrusted total_disks in ata_raid_create and ata_raid_status ioctls
Summary
Two ioctl handlers iterate up to user/corpus-controlled disk count and index fixed 16-element arrays without bounds check. ata_raid_create IOCATARAIDCREATE loops disk<config->total_disks over config->disks[16] and rdp->disks[16]. ata_raid_status IOCATARAIDSTATUS loops i<rdp->total_disks over status->disks[16]. Both total_disks accepted verbatim. config->total_disks raw int from user; rdp->total_disks attacker-inflatable via metadata. IOCATARAIDCREATE with total_disks=200 overwrites ~184 bytes kernel stack past 84-byte struct. Reachable by operator-group /dev/ata. Stack-smashing privilege escalation.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2495 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.8 KB | β raw | |
| VERDICT.md | β | 5.3 KB | β raw | |
| dfrig_ata.c | β | 23.7 KB | view raw | |
| df2495_status.c | β | 1.2 KB | view raw | |
| Makefile | β | 442 B | β download | |
| build.sh | β | 176 B | view raw | |
| run.sh | β | 426 B | view raw | |
| build.log | β | 955 B | view raw | |
| run.log | β | 3.9 KB | view raw | |
| env.txt | β | 701 B | view raw | |
| fix.diff | β | 1.5 KB | view raw | |
| fix_build.log | β | 513 B | view raw | |
| fix_run.log | β | 1.4 KB | view raw | |
| fix_tests_123.log | β | 1.3 KB | view raw | |
| fix_tests_67.log | β | 1.1 KB | view raw | |
| manifest.json | β | 1.7 KB | view raw | |
| verdict.json | β | 6.9 KB | view raw |
DF-2495 β inflated rdp->total_disks (LSI v2 disk_count) drives a 1912-byte kernel-heap OOB write out of ata_raid_status() (IOCATARAIDSTATUS); ata_raid_create() likewise loops config->total_disks unchecked
File: sys/dev/disk/nata/ata-raid.c:1013-1025, 1051-1151, 2856 Β· Severity: High (filed) Β· Bucket: memcorrupt Β· Confidence: certain
Status: reproduced on the QEMU guest (production parse + production
ata_raid_status() through the rig; the real-kernel /dev/ata ioctl arm also
executed β see run.log and VERDICT.md for the honest impact ceiling: /dev/ata
is 0600 root-only on this tree, so the ioctl trigger is root-gated).
Build (guest, root)
cd /root/dfrig && ./build.sh # dfrig_ata.ko (production ata-raid.c compiled in) cc -O2 -Wall -o df2495_status df2495_status.c
Run
sh run.sh # rig test 3 + real-kernel arm (test 7 + ioctl)
Expected (see run.log)
- rig test 3: production parse sets
rdp->total_disks=255from craftedconfigs[0].raid.disk_count; productionata_raid_status()then writesstatus->disks[0..254]β 2064 bytes into/through a 152-byte buffer (the exact size the generic ioctl layer kmallocs for IOCATARAIDSTATUS): 1912 bytes past the allocation, smashing 12/24 marker guard chunks with the production{state=0, lun=-1}pattern. - real-kernel arm: rig test 7 installs the inflated array into the REAL
ata_raid_arrays[0];./df2495_status 0(root, real syscall) returnstotal_disks=255β the real kernel wrote the same 1912 OOB bytes.
Fix
fix.diff: reject disk_count > MAX_DISKS at the LSI v2 parse, bound
rdp->total_disks in ata_raid_status(), and reject out-of-range
config->total_disks in ata_raid_create().
Validated on a rebuilt kernel (fix_build.log / fix_run.log).
DF-2495 VERDICT β reproduced (1912-byte kernel-heap OOB write out of production ata_raid_status()), with an honest impact ceiling
Bottom line
The pass-1 claim is real at the code level and reproduced through production code twice; its practical trigger surface on this tree is narrower than filed (details below).
The write. ata_raid_lsiv2_read_meta() sets
raid->total_disks = meta->configs[raid_entry].raid.disk_count
(ata-raid.c:2856) β a raw u_int8_t from the crafted $XIDE$ superblock,
no bound against MAX_DISKS=16. Every later loop trusts it. The one that
writes is ata_raid_status() (ata-raid.c:1013-1025):
for (i = 0; i < rdp->total_disks; i++ ) {
status->disks[i].state = 0; /* unconditional OOB store */
... status->disks[i].lun = -1; ...
}
status is the generic ioctl layer's copyin buffer: 152 bytes, and because
152 > STK_PARAMS (128) kern_ioctl kmallocs it (sys/kern/sys_generic.c:664-680).
With total_disks=255 the loop writes 2064 bytes β 1912 bytes past the
allocation β with the production pattern {state=0, lun=-1} per entry.
Reproduced:
- rig test 3 (guard-buffer proof): production parse β rdp->total_disks=255
installed in the module's real ata_raid_arrays[0]; production
ata_raid_status() called with a 152-byte kmalloc'd buffer surrounded by
0xAA guard chunks β 12/24 guards smashed, victim tail overwritten with
ffffffff00000000 words (run.log).
- real-kernel arm: rig test 7 ran the same production parse with
raidp = the REAL kernel ata_raid_arrays (0xffffffff8114dec0), then the
REAL /dev/ata IOCATARAIDSTATUS ioctl (df2495_status, ordinary
ioctl(2) through ata-all.c:443 β ata_raid_ioctl:249 β ata_raid_status)
returned 0 with total_disks=255 β i.e. the real kernel executed all 255
iterations and wrote the 1912 OOB bytes into its own heap.
No panic observed: across the single-shot run plus a 3x700-iteration hammer with concurrent heap churn, the smashed zone neighbors were free chunks every time. The write primitive is proven; crash/escalation was not achieved on this guest.
Honest impact ceiling (differs from the filing)
/dev/ataiscrw------- root operator(mode 0600,ata-all.c:1025make_dev(&ata_ops, 0, UID_ROOT, GID_OPERATOR, 0600, ...)) β the filing's "reachable by operator-group" does not hold on this tree: the ioctl arms (STATUS and CREATE) are root-only. As a privilege escalation this is therefore inert here; as a memory-corruption defect it remains real (root actions on attacker-inflated state, plus the fsck-style "admin runs atacontrol status" confused-deputy pattern).- The unprivileged half of the chain is the crafted-disk metadata:
disk_count=255inflatestotal_disksat boot without any ioctl. The boot-time writers over the inflated count (ata_raid_config_changedata-raid.c:924-926clearsAR_DF_ONLINEbits up to 255 entries past thedisks[16]table β an OOB heap read-modify-write at attach; the attach print loop at 194-232 OOB-reads the same range) could not be armed on the standard guest for the same infrastructure reason as DF-2494: any IDE hard disk hangs SeaBIOS on the vm.sh invocation (verified with a benign image too). - The CREATE arm (
ata-raid.c:1051-1151) does readconfig->disks[disk]past the 88-byte struct oncedisk >= 16and can write-1atconfig->disks[disk](1148) β but the loop only advances past disks whosedevclass_get_device()lookup succeeds, so with β€16 subdisk units in the system the OOB iterations hit stale-stack garbage, fail the lookup, and the single-1store lands inside the 128-byteSTK_PARAMSstkbuf. Missing-bound defect confirmed at code level; not weaponized further.
Fix validation
fix.diff (1) rejects disk_count > MAX_DISKS at the LSI v2 parse before
the array is accepted, (2) bounds rdp->total_disks in ata_raid_status()
(defense in depth for arrays inflated by any other parser), and (3) rejects
config->total_disks outside [1, MAX_DISKS] in ata_raid_create().
Validated on a single-fix rebuilt kernel (fix_build.log / fix_run.log):
the parse now rejects the crafted disk_count=255 (rc=0), no array is
installed in the real ata_raid_arrays, and the STATUS ioctl returns ENXIO.
Kernel references
sys/dev/disk/nata/ata-raid.c:2856βraid->total_disks = disk_count(unvalidated u8)sys/dev/disk/nata/ata-raid.c:1013-1025βata_raid_statusOOB write loopsys/dev/disk/nata/ata-raid.c:1051-1053, 1148βata_raid_createuncheckedconfig->total_diskssys/dev/disk/nata/ata-raid.c:924-926, 194β boot-time inflated-total_disksloops (config_changed RMW / attach prints)sys/kern/sys_generic.c:664-680β 152-byte ioctl payload kmalloc (STK_PARAMS=128)sys/dev/disk/nata/ata-all.c:1025β/dev/atacreated 0600 root:operatorsys/sys/nata.h:440-483βata_ioc_raid_config.disks[16]/ata_ioc_raid_status.disks[16]
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
fixedSingle-fix kernel (fix.diff: disk_count>MAX_DISKS rejected at the LSI v2 parse; ata_raid_status bounds rdp->total_disks; ata_raid_create validates config->total_disks) built with nativekernel, installed, rebooted; rig + df2495_status rebuilt on the fixed tree. Baseline behaviors are gone: rig test3's parse now rejects the crafted disk_count=255 (rc=0, 'parse failed, cannot run status') so the production status OOB write never executes; the real-kernel test7 installs nothing (total_disks=0) and the REAL /dev/ata IOCATARAIDSTATUS returns ENXIO (errno=6) instead of 0-with-1912-OOB-bytes.
fix_run.log: 'parse rc=0: raidp[0]=0 rdp->total_disks=0 ... parse failed, cannot run status' and 'ioctl returned -1 (errno=6) total_disks=0' vs run.log baseline 'guard chunks hit by the overflow: 12/24' and 'ioctl returned 0 (errno=0) total_disks=255'.
Confirmed kernel references
Detail
Exploit chain
crafted LSI v2 superblock with configs[0].raid.disk_count=255 on an ATA disk behind nata -> subdisk attach -> ata_raid_lsiv2_read_meta -> raid->total_disks=255 (ata-raid.c:2856, no bound vs MAX_DISKS=16) -> [boot-time, unprivileged] ata_raid_config_changed (924-926) read-modify-writes disks[16..254] flags (heap OOB bit-clears past the ar_softc) and the attach print loop OOB-reads the same range; [root-gated ioctl half] IOCATARAIDSTATUS lun=0 through /dev/ata (0600 root-only) -> kern_ioctl kmallocs 152 bytes -> production ata_raid_status writes status->disks[0..254] = 2064 bytes = 1912 bytes past the buffer with {0,-1} pairs -> kernel heap corruption of adjacent kmem-pool chunks (silent on this guest; panic potential on denser heaps). CREATE arm: config->total_disks up to INT_MAX loops config->disks[disk] past the 88-byte struct (OOB reads from the 128-byte stkbuf; -1 stores land within stkbuf) -- root-gated, not weaponized.
Evidence (decisive lines)
run.log: 'parse rc=1 ... rdp->total_disks=255 (MAX_DISKS=16, ata-raid.c:2856 takes disk_count verbatim)' / 'writing status->disks[0..254] -> last byte at +2064 (1912 bytes PAST the buffer)' / 'guard chunks hit by the overflow: 12/24 (0xffffffff = lun -1 from production status->disks[i].lun = -1)' / real-kernel arm: 'ioctl returned 0 (errno=0) total_disks=255' with /dev/ata shown crw------- root operator; fix_run.log: 'parse rc=0 ... cannot run status' + 'ioctl returned -1 (errno=6)'.
PoC changes
No pass-1 pack existed (empty dirs); authored fresh: the DF-2494 rig module drives the production parse (disk_count=255, raid_number=0, disk_number=2 to avoid the configs[0] raid/disk union overlay) and calls the production ata_raid_status() with a 152-byte kmalloc'd victim surrounded by 0xAA guard chunks, replicating the exact kern_ioctl buffer; df2495_status.c is a plain ioctl(2) program for the real /dev/ata arm (pass-1 sketch had wrong struct source -- the real copyin buffer is kernel-side, sys/kern/sys_generic.c:664-680, which is why the smash is kernel heap, not user memory).
Verified recommended fix
Reject disk_count > MAX_DISKS in ata_raid_lsiv2_read_meta(); defensively bound rdp->total_disks in ata_raid_status(); reject config->total_disks outside [1,MAX_DISKS] in ata_raid_create().
Verdict
CONFIRMED at the code level with the OOB write executed by production code twice; impact ceiling on this tree is narrower than filed. The LSI v2 parse takes configs[raid_entry].raid.disk_count (raw u_int8_t) verbatim into rdp->total_disks (ata-raid.c:2856, MAX_DISKS=16). Production ata_raid_status() (ata-raid.c:1013-1025) then runs for (i=0; i
No comments yet.