DF-2269 / verdict.json
{ "finding_id": "DF-2269", "status": "reproduced", "reproduced": 1, "impact": "leak", "confidence": "certain", "verdict": "Reproduced on the stock INVARIANTS guest through the real cdioctl to cdreaddvdstructure path with a verbatim sha256-gated scsi_cd.c compiled into the kernel and only four CAM externals interposed (pass-through outside the tested call so the real ATAPI acd0 keeps working). A malicious transport answering READ DVD STRUCTURE with CAM_REQ_CMP and a short transfer drives the mixed int u_int32_t subtraction at scsi_cd.c:4288-4289 to wrap, so min(2048, ~4G) copies a full 2048 bytes out of the small format databuf: for MEDIUM_STAT (8-byte kmalloc) 2044 bytes are read past the end, for BCA 1860, SPARE_AREA 2036, PROTDISCID 2032, UNIQUEID 2032, PRERECORDED 1984; formats with length >= 2052 stay in bounds but still report the wrapped ~0xFFFFFFFE length to userspace. The boundary matrix pins the wrap exactly: transferred 8 5 4 are clean (length 4 1 0), transferred 3..0 and resid 9 12 100 wrap to 0xffffffxx with 2048-byte copies, and resid 0xFFFFFFFF wraps the other way to length 5 (a 1-byte overread, proving a signed-cast-only fix is insufficient). The leaked dvd_struct.data contains byte-exact heap-neighbor content: the first canary chunk begins at databuf+8 (data[4..7] of the dump) and kernel heap pointers (M_DEVBUF zone free-list linkage, fffff8008d3622xx every 16 bytes) appear throughout; content varies run to run and boot to boot (nonzero 2044 vs 2006 vs 0; markers 5-160; ptrlikes 5-37), which is the heap-state-dependent disclosure a KASLR or heap-grooming attacker wants. The ioctl layer copies the whole 2064-byte struct back to userspace after the handler returns 0 (sys_generic.c:668,725). Honest ceiling per team directive: leak-only, no write primitive (the destination field is exactly 2048 bytes), and reach needs a caller that passed the SYSCAP_RESTRICTEDROOT open gate (scsi_cd.c:1006) plus an attacker-controlled device; privesc closed, no uid=0 route from this bug alone.", "exploit_chain": "", "evidence": "run_dmesg.log: unfixed battery, the *** OOB READ lines, marker attribution (marker XX: data[N..M] => N bytes PAST the 8-byte databuf) and ptr-like fffff8008d3622xx listings; run_fixed_currentboot.txt: identical battery on the fix.diff kernel, all underflow rows ret=5 EIO length=0 copy=0 nonzero=0 markers=0 while transferred=8 5 4 rows keep the exact unfixed fnv1a hashes 7ac988210d38cfe1 5137c7c66c9c4391 28c31cf8df2ec325; leak_sample_boot1.txt / leak_sample_boot2.txt / leak_boot2_summary.txt: full 2048-byte dumps across boots showing heap-state variance; VERDICT.md: boundary matrix table, formats sweep table, fix A/B and rig iteration notes", "kernel_refs": [ "sys/bus/cam/scsi/scsi_cd.c:4288", "sys/bus/cam/scsi/scsi_cd.c:4289", "sys/bus/cam/scsi/scsi_cd.c:4295", "sys/bus/cam/scsi/scsi_cd.c:4297", "sys/bus/cam/scsi/scsi_cd.c:4207", "sys/bus/cam/scsi/scsi_cd.c:4208", "sys/bus/cam/scsi/scsi_cd.c:4122", "sys/bus/cam/scsi/scsi_cd.c:4226", "sys/bus/cam/scsi/scsi_cd.c:1006", "sys/bus/cam/scsi/scsi_cd.c:2751", "sys/bus/cam/cam_ccb.h:607", "sys/sys/dvdio.h:58", "sys/kern/sys_generic.c:668", "sys/kern/sys_generic.c:725", "sys/vfs/devfs/devfs_vnops.c:1569", "sys/kern/kern_device.c:237" ], "poc_changes": "No evidence pack existed; built the DF-2263-style CAM verbatim-interposition rig from scratch for scsi_cd.c: df_pre.c interposes xpt_path_periph cam_periph_getccb cam_periph_runccb xpt_release_ccb, df_post.c implements the malicious 0xad transport (GOOD status plus chosen transferred/resid), a same-size-class M_DEVBUF canary groom (0xC0-0xFF markers every 2nd freed plus 0x5A runway) around the kmalloc(length) databuf, marker and kernel-pointer scans with byte offsets past the allocation end, a full 2048-byte hexdump, and sysctls debug.df2269.matrix formats leak repeat. Two iterations were needed: shims must pass through to real CAM outside the armed window because the guest has a real ATAPI acd0 whose boot-time CCBs otherwise hit a NULL periph panic, and READ DVD STRUCTURE alloc_len lives at CDB bytes 8-9, not 9-10", "attempts": 4, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #3: Sat Oct 17 22:03:22 UTC 2026 (unfixed repro) and #4: Sat Oct 17 22:24:06 UTC 2026 (fix A/B) root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 7800, "guest_dirty": 0, "build_cmd": "./build.sh (nativekernel KERNCONF=X86_64_GENERIC with harness TU, installkernel, reboot); fix A/B via ./run_fixed.sh (patch guest /usr/src, quickkernel, installkernel, reboot)", "run_cmd": "vm.sh run_root sysctl debug.df2269.matrix=1 ; sysctl debug.df2269.formats=1 ; sysctl debug.df2269.leak=1 ; sysctl debug.df2269.repeat=10 ; dmesg | grep DFH2269", "code_hash": "01cbe4f20b0539e41f631840a2dfd5be8f4244bd76d11bc06307046ea06afb0c", "notes": "No panic occurred at any point (pure OOB read); no panic.txt. Privesc per team directive: closed, leak-only; reach requires the SYSCAP_RESTRICTEDROOT open gate plus device-controlled responses, so the realistic value of the primitive is kernel heap and pointer disclosure (KASLR and heap-grooming enabler) against a privileged opener using a malicious or buggy optical device. Secondary effect worth noting upstream: even the in-bounds formats and DVD_STRUCT_LIST report the wrapped length in dvd_struct.length, so userland callers that trust it can overread their own buffers. The fixed kernel was validated on the guest then the guest was reset to the with-src snapshot; the audit-side sys tree was never modified", "recommended_fix": "In the default case compute transferred with checked unsigned math and fail with EIO when resid exceeds the allocation or the transfer never covered the 4-byte header (see fix.diff)", "fix_status": "fixed", "fix_evidence": "A/B on identical battery: unfixed kernel #3 returns 0 with length 0xffffffxx and 2048-byte OOB copies on every underflow row; fix.diff kernel #4 returns EIO with length 0 copy 0 nonzero 0 markers 0 ptrlikes 0 on the same rows and on all 11 formats with a 2-byte short transfer, while transferred 8 5 4 rows are bit-identical (fnv1a 7ac988210d38cfe1 5137c7c66c9c4391 28c31cf8df2ec325 match the unfixed hashes); the resid 0xFFFFFFFF row that defeats a naive signed-cast fix (unfixed length 5 copy 5) is also rejected" } |