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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5272 Β· 10 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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 is len; nothing requires the device to fill it.
  • cam_periph.c:962-963: CAM_REQ_CMP with resid != 0 is 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

fixed
baseline reproduced→ patch + rebuild →patched clean

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

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.