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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5271 Β· 15 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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:

  1. device-forced 10-byte mode switch β€” the device fails MODE_SENSE(6) with CAM_REQ_INVALID; cd6byteworkaround() (scsi_cd.c:3127-3131) permanently sets softc->minimum_command_size = 10 (dmesg: MODE_SENSE(6) failed, increasing minimum CDB size to 10 bytes);
  2. evil MODE SENSE(10) header β€” data_length = 0 (passes the only sanity check, scsi_cd.c:3437-3466) and blk_desc_len = <computed> (16-bit, device-chosen);
  3. OOB READ to userland β€” CDIOCGETVOL returns bytes read at mode_buf + 8 + blk_desc_len + {9,11,13,15}; the first run returned 00 00 00 ab, i.e. bytes of a neighboring heap chunk and of the probe victim's stamp (0xAB from 0xABCD0003);
  4. OOB WRITE from userland data β€” CDIOCSETVOL/CDIOCSETPATCH write user-supplied volume/patch bytes through the same pointer;
  5. 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 aims blk_desc_len at it; check_victims finds it corrupted with exactly the four user bytes (a3 68 a4 69 = patch[2], vol[2], patch[3], vol[3]); a second foreign chunk received 66 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, CDIOCSETPATCH patch[0..3] at page+8/10/12/14), plus fixed bytes CHANNEL_0/1 (0x01/0x02); target offset mode_buf + 8 + blk_desc_len + {8..15} with blk_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_len 1 byte) reaches +259 from the same 32-byte chunk and is also unguarded; cd6byteworkaround()'s MODE_SELECT conversion reads find_mode_page_6() OOB (scsi_cd.c:3173-3183) feeding page_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_length against alloc_len; blk_desc_len is never checked.
  • scsi_all.h:1418-1438 find_mode_page_6/10() return buf + hdrsize + blk_desc_len unconditionally.
  • 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Single-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.
↓ fix.diffper-fix-DF-5271

Confirmed kernel references

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.