cdgetpage/find_mode_page_* trust device-controlled mode-header blk_desc_len: heap OOB read+write in all CD audio ioctls
Summary
cdgetmode validates only data_length, never blk_desc_len; find_mode_page_10 computes the audio page pointer as mode_buf+8+blk_desc_len with a fully device-controlled 16-bit value - CDIOCSETVOL/SETPATCH write 4 user bytes each at page+8..15, GETVOL returns 4 OOB heap bytes, plus RMW flags on 8 more ioctls. 6-byte variant reaches +259; the 10-byte path itself is device-forced (failed MODE_SENSE(6) permanently bumps minimum_command_size). Malicious USB/SAN CD target picks the offset per command; privileged open (mount/automount). REPRODUCED end-to-end on guest with evilcd.ko fake CD SIM: GETVOL returned foreign heap bytes; SETVOL+SETPATCH wrote exact user bytes {a3 68 a4 69} into a stamped slab-adjacent victim chunk (delta 0x40), byte-for-byte attributed, read back; patched kernel (bound hdr_size+bd_len+pages) returns ENOSPC, 0 corrupted victims. Bound-the-bd_len fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5271 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| evilcd.c | β | 26.7 KB | view raw | |
| Makefile | β | 62 B | β download | |
| poc_write.c | β | 2.6 KB | view raw | |
| build.sh | β | 85 B | view raw | |
| run.sh | β | 173 B | view raw | |
| build.log | β | 5.7 KB | view raw | |
| run.log | β | 2.1 KB | view raw | |
| run.baseline.log | β | 2.1 KB | view raw | |
| dmesg.baseline.txt | β | 7.6 KB | view raw | |
| VERDICT.md | β | 4.7 KB | β raw | |
| fix.diff | β | 1.6 KB | view raw | |
| verdict.json | β | 6.2 KB | view raw | |
| fix_build.log | β | 5.6 MB | β download | |
| fix_run.log | β | 1.1 KB | view raw | |
| run.patched.log | β | 1.1 KB | view raw |
DF-5271 VERDICT β mode-page blk_desc_len OOB read/write: REPRODUCED
Bottom line
Reproduced. A fake CD-ROM CAM SIM (evilcd.ko) driving the stock
guest kernel's cd(4) driver demonstrates, in one run:
- device-forced 10-byte mode switch β the device fails MODE_SENSE(6)
with CAM_REQ_INVALID;
cd6byteworkaround()(scsi_cd.c:3127-3131) permanently setssoftc->minimum_command_size = 10(dmesg:MODE_SENSE(6) failed, increasing minimum CDB size to 10 bytes); - evil MODE SENSE(10) header β
data_length = 0(passes the only sanity check, scsi_cd.c:3437-3466) andblk_desc_len = <computed>(16-bit, device-chosen); - OOB READ to userland β CDIOCGETVOL returns bytes read at
mode_buf + 8 + blk_desc_len + {9,11,13,15}; the first run returned00 00 00 ab, i.e. bytes of a neighboring heap chunk and of the probe victim's stamp (0xAB from 0xABCD0003); - OOB WRITE from userland data β CDIOCSETVOL/CDIOCSETPATCH write user-supplied volume/patch bytes through the same pointer;
- byte-exact victim attribution β the SIM allocates a stamped 32-byte
probe victim from inside the MODE SENSE(10) handler (same zone as the
32-byte
mode_buf, delta 0x40 in the decisive run) and aimsblk_desc_lenat it;check_victimsfinds it corrupted with exactly the four user bytes (a3 68 a4 69= patch[2], vol[2], patch[3], vol[3]); a second foreign chunk received66 67(vol[0], vol[1]) and the final GETVOL read those back (vol = 66 67 68 69), proving the write persisted in memory the driver does not own.
Primitive characterization
- write: 4 fully user-chosen bytes per ioctl (CDIOCSETVOL
vol[0..3]at page+9/11/13/15, CDIOCSETPATCHpatch[0..3]at page+8/10/12/14), plus fixed bytes CHANNEL_0/1 (0x01/0x02); target offsetmode_buf + 8 + blk_desc_len + {8..15}withblk_desc_lenβ [0,0xFFFF] chosen by the device per command, i.e. any offset up to ~64 KB past a 32-byte heap chunk; unlimited repetitions, no crash in the verified range (silent corruption of neighboring objects). - read: CDIOCGETVOL returns 4 OOB bytes per ioctl at the same device-chosen offset (heap info leak / KASLR-relevant data disclosure).
- The 6-byte variant (
blk_desc_len1 byte) reaches +259 from the same 32-byte chunk and is also unguarded;cd6byteworkaround()'s MODE_SELECT conversion readsfind_mode_page_6()OOB (scsi_cd.c:3173-3183) feedingpage_size.
uid=0 escalation was not pursued to completion: the entry point is
gated by caps_priv_check_self(SYSCAP_RESTRICTEDROOT) (scsi_cd.c:1006), so
the realistic threat is malicious-device-driven kernel corruption (the same
model as the known TOC findings DF-0987/DF-2268), not unprivβroot. The
primitive (repeatable 4-controlled-byte write at a 64 KB device-chosen
window, plus an OOB read for aiming) is however straightforwardly
weaponizable against adjacent heap objects.
Why the kernel is wrong (path:line)
- scsi_cd.c:1970 (and every audio ioctl):
alloc_len = sizeof(union cd_mode_data_6_10)= 32 bytes. - scsi_cd.c:3400-3411
scsi_mode_sense_len()transfers at most 32 bytes β the transfer is bounded, the header fields are not validated. - scsi_cd.c:3437-3466: the only post-transfer check compares
data_lengthagainstalloc_len;blk_desc_lenis never checked. - scsi_all.h:1418-1438
find_mode_page_6/10()returnbuf + hdrsize + blk_desc_lenunconditionally. - scsi_cd.c:1880-1893
cdgetpage()hands that pointer to the ioctls. - Write sinks: scsi_cd.c:1986-1987 (PLAYTRACKS flags RMW), 2081-2082, 2122-2123, 2443-2448 (SETPATCH, 4 user bytes), 2508-2515 (SETVOL, 4 user bytes), 2542-2547, 2574-2579, 2606-2609, 2636-2639, 2666-2669; read sink: scsi_cd.c:2476-2481 (GETVOL β userland).
- Device-controlled path switch: scsi_cd.c:3127-3128 / 3391-3395.
Fix validation
fix.diff makes cdgetmode() reject mode headers whose
hdr_size + blk_desc_len + sizeof(union cd_pages) exceeds alloc_len
(the existing ENOSPC path). Built as a single-fix kernel
(make nativekernel KERNCONF=X86_64_GENERIC, log: fix_build.log),
installed, rebooted, exact same PoC re-run:
- CDIOCGETVOL now fails with ENOSPC (driver prints
allocated mode page ... < returned length ...),
- no probe-victim corruption (check_victims: probe victim untouched,
0 corrupted chunks),
- no readback of previously written bytes.
See run.patched.log, fix_run.log.
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 applied to guest /usr/src, make nativekernel K86_64_GENERIC + installkernel + reboot): identical PoC run returns ENOSPC from the audio ioctls (driver prints 'allocated mode page ... < returned length'), probe victim untouched (0 corrupted chunks), no readback of previously written bytes. See fix_build.log, run.patched.log.
fix_run.log / run.patched.log: CDIOCGETVOL fails ENOSPC, check_victims: probe victim untouched, 0 corrupted; guest stays up.
Confirmed kernel references
- sys/bus/cam/scsi/scsi_cd.c:3398
- sys/bus/cam/scsi/scsi_cd.c:3437-3466
- sys/bus/cam/scsi/scsi_cd.c:1880-1893
- sys/bus/cam/scsi/scsi_all.h:1418-1438
- sys/bus/cam/scsi/scsi_cd.c:2443-2448
- sys/bus/cam/scsi/scsi_cd.c:2508-2515
- sys/bus/cam/scsi/scsi_cd.c:2476-2481
- sys/bus/cam/scsi/scsi_cd.c:1986-1987
- sys/bus/cam/scsi/scsi_cd.c:3127-3131
- sys/bus/cam/scsi/scsi_cd.c:3173-3183
Detail
Exploit chain
evil USB/SCSI CD-ROM + (root|automount|mount) open -> device fails MODE_SENSE(6) -> cd6byteworkaround switches periph to 10-byte mode commands permanently -> device returns mode header {data_length=0, blk_desc_len=N} -> cdgetmode ENOSPC check passes (checks data_length only) -> cdgetpage/find_mode_page_10 = mode_buf+8+N -> CDIOCSETVOL writes vol[0..3] at page+9/11/13/15 and CDIOCSETPATCH patch[0..3] at page+8/10/12/14 (4 arbitrary user bytes per ioctl, repeatable) / CDIOCGETVOL discloses 4 OOB heap bytes per ioctl -> corruption of any object within +64KB of the 32-byte M_SCSICD chunk -> panic now, or object-pointer corruption for code execution with heap grooming.
Evidence (decisive lines)
run.log lines 'CDIOCGETVOL ... vol = 00 00 00 ab' (OOB read), 'CDIOCSETVOL wrote {66 67 68 69}', 'readback: vol = 66 67 68 69' (persistent OOB write); dmesg.baseline.txt '[EVILCD] probe victim 0xfffff8004f102620 ... CORRUPTED at offset 0' + 'bytes: a368a46904000d605c5c5c...' (user bytes landed at the exact computed offsets in a live foreign heap chunk, stamp tail 04000d60 intact); VERDICT.md has the byte-for-byte attribution table.
PoC changes
Seed sketch was never runnable: built a full fake-CD-ROM CAM SIM (evilcd.c) per the DF-5240 evilsim pattern (T_CDROM removable inquiry, READ CAPACITY, READ TOC, MODE SENSE 6/10, READ SUBCHANNEL, PREVENT/START_STOP handlers; XPT_PATH_INQ must NOT bzero the union ccb or timeout_ch is wiped -> xpt_free_ccb KKASSERT panic). Key rig additions: (1) reject MODE_SENSE(6) with CAM_REQ_INVALID to make the driver itself switch to 10-byte mode commands; (2) allocate a stamped 32-byte probe victim from INSIDE the MODE SENSE(10) handler (same thread/CPU/slab zone as the just-allocated mode_buf, delta 0x40 in the decisive run) and compute blk_desc_len = victim-mode_buf-20 so the ioctl writes land on it; (3) sysctl check_victims scans and hexdumps; (4) usched :1 pinning for LIFO chunk stability; poc_write flags added for pre-armed runs (no fork between ioctls).
Verified recommended fix
In cdgetmode(), reject mode headers whose hdr_size + blk_desc_len + sizeof(union cd_pages) exceeds alloc_len (extend the existing data_length ENOSPC check); fix.diff validated.
Verdict
REPRODUCED on the stock guest kernel with an evil CD-ROM CAM SIM (evilcd.ko): the device forces the 6->10-byte mode-command switch (cd6byteworkaround sets minimum_command_size=10 permanently after a rejected MODE_SENSE(6)), then returns a MODE SENSE(10) header with data_length=0 (passes the only check in cdgetmode, which validates data_length but never blk_desc_len) and a computed 16-bit blk_desc_len. cdgetpage -> find_mode_page_10 returns mode_buf+8+blk_desc_len and the audio ioctls read and write through it: CDIOCGETVOL returned foreign-chunk/stamp bytes (00 00 00 ab), CDIOCSETVOL/CDIOCSETPATCH wrote the exact user bytes into a surgically-aimed stamped probe victim (a368a469 at the computed offsets, byte-for-byte accounted: patch[2], vol[2], patch[3], vol[3]), a second neighboring chunk received 66/67, and a final CDIOCGETVOL read the written bytes back (66 67 68 69), proving persistent corruption of memory the driver does not own. No panic (silent corruption), guest stayed up. uid=0 chain not pursued past primitive characterization because the trigger requires a privileged open (root-only device node per SYSCAP_RESTRICTEDROOT); the primitive itself (repeatable 4-controlled-byte device-aimed write in a 64KB window + OOB read for aiming) is weaponizable against adjacent heap objects.
No comments yet.