CDIOCREADSUBCHANNEL: non-zeroed kmalloc + short device transfer + device-inflated data_len clamp = uninitialized kernel heap disclosure
Summary
24-byte buffer kmalloc'd without M_ZERO; CAM_REQ_CMP with resid!=0 accepted and unchecked; device DMAing only a 4-byte header while data_len claims 0x14 defeats the min() clamp - 20 uninit kernel heap bytes copyout'd per call, unlimited. Guest-verified with evilcd (armed=2): 64/64x3 rounds stable leak; marker phase leaked the kernel's own sysctl OID name tail ('...ilcd spray_leak'). Patched (M_ZERO) returns all-zero 64/64. Recon aid for DF-5271 on the same device. M_ZERO fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5272 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| evilcd.c | β | 26.7 KB | view raw | |
| poc_leak.c | β | 2.7 KB | view raw | |
| build.sh | β | 85 B | view raw | |
| run.sh | β | 173 B | view raw | |
| build.log | β | 5.7 KB | view raw | |
| run.baseline.log | β | 2.1 KB | view raw | |
| dmesg.baseline.txt | β | 7.6 KB | view raw | |
| VERDICT.md | β | 2.2 KB | β raw | |
| fix.diff | β | 539 B | view raw | |
| verdict.json | β | 4.2 KB | view raw |
DF-5272 VERDICT β CDIOCREADSUBCHANNEL heap leak: REPRODUCED
Bottom line
Reproduced. With the evil device completing READ SUBCHANNEL as a
4-byte short transfer while its header claims data_len = 0x0014, the
driver copyouts 24 bytes of a non-zeroed 24-byte kmalloc buffer:
64/64 ioctls per round, three rounds, leaked the same stable kernel-heap
remnant (ffffffff 00000000 00000000 01000000), and the in-process
marker phase leaked the kernel's own sysctl OID name string fragment
"β¦ilcd\0spray_leak\0" (bytes[0..23] = 00110014|ffffffff|696c6364β¦),
proving uninitialized kernel heap crosses the copyout boundary. The
0xEE userland canary confirms the leaked bytes are kernel-supplied, and
the device only ever wrote the first 4.
Mechanism (path:line)
- scsi_cd.c:2141
kmalloc(sizeof(struct cd_sub_channel_info), M_SCSICD, M_WAITOK)β no M_ZERO. - scsi_cd.c:2148-2158: user length bounds-checked to [sizeof(header)=4, sizeof(info)=24] β fine.
- scsi_cd.c:2163-2164
cdreadsubchannel(..., len)β the CDB allocation length islen; nothing requires the device to fill it. - cam_periph.c:962-963:
CAM_REQ_CMPwithresid != 0is success; no caller checks resid on this path. - scsi_cd.c:2174-2176:
len = min(len, data_len[0]<<8 + data_len[1] + 4)β the clamp uses the device-supplied length field, so the device keeps len at the full user value while having transferred 4 bytes. - scsi_cd.c:2178
copyout(data, args->data, len)β 20 uninitialized heap bytes disclosed.
Fix validation
fix.diff adds M_ZERO to the allocation. Validated in the same
single-fix kernel build as DF-5271 (fix_build.log, fix_run.log,
run.patched.log): after the patch the identical PoC returns
bytes[0..23] = 00110014 0000...000 β the 20 tail bytes are zeroes on
every run (64/64), no heap remnants, no markers, guest stays up.
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 (M_ZERO added, built+installed+rebooted together with DF-5271's fix): identical PoC returns bytes[4..23] all zero on every run (64/64), no heap remnants, no marker hits; guest stays up.
run.patched.log / fix_run.log: zero tail bytes 64/64 on the patched kernel vs. leaked remnants 64/64 on baseline.
Confirmed kernel references
Detail
Exploit chain
evil CD-ROM + privileged open -> CDIOCREADSUBCHANNEL(data_len=24) -> kmalloc(24, M_SCSICD, M_WAITOK) without M_ZERO -> device DMAs only the 4-byte header with data_len=0x0014 -> success with resid=20 (unchecked) -> len=min(24, 20+4)=24 -> copyout of 20 uninitialized heap bytes -> repeatable heap-state sampling adjacent to the DF-5271 write primitive (reconnaissance).
Evidence (decisive lines)
run.log phase-1 lines '[poc] 64/64 runs leaked non-zero heap bytes' x3; phase-2 lines 'bytes[0..23] = 00110014ffffffff696c63640073707261795f6c65616b00' + 'uninit[4..23] = ....ilcd.spray_leak.' (00 11 00 14 is the device header; the rest is leaked kernel heap containing the sysctl OID name string). Patched run: tail all-zero 64/64.
PoC changes
Built the short-transfer response into the shared evilcd SIM (armed>=2: 4-byte DMA + data_len field 0x0014 + resid=dxfer-4). Marker phase needed allocator-aware rigs: freed-chunk markers must avoid bytes [0..7] (SLChunk.c_Next linkage - overwriting them caused a BADFREE2 panic, preserved in notes), and the marker spray must run in-process (sysctlbyname from the PoC between ioctls, no fork) because fork/exec noise reallocates the recycled chunk.
Verified recommended fix
kmalloc the subchannel buffer with M_ZERO (one-word fix; fix.diff validated together with DF-5271's fix in a single rebuilt kernel).
Verdict
REPRODUCED on the stock guest kernel: the evil device completes READ SUBCHANNEL with a 4-byte short transfer while its returned header claims data_len=0x0014; cam_periph_runccb treats CAM_REQ_CMP with resid!=0 as success, cdioctl clamps the copyout length with the device-supplied data_len field (min(24, 0x14+4)=24) and copyouts 24 bytes of a kmalloc'd-without-M_ZERO buffer, so 20 uninitialized kernel-heap bytes reach userspace on every call. Plain rounds leaked the same stable kernel remnant 64/64 times; an in-process marker phase (alloc+mark+kfree of 24-byte chunks via sysctl between ioctls, no fork) leaked the kernel's own sysctl OID name string 'spray_leak' tail, proving live kernel heap data crosses the boundary. Patched kernel (M_ZERO) returns all-zero tail bytes on every run.
No comments yet.