# DF-2269 — Kernel heap OOB read in cdreaddvdstructure() default case

**Verdict: REPRODUCED (impact: kernel heap info leak) — fix validated A/B.**

`sys/bus/cam/scsi/scsi_cd.c:4288-4297`: in the `default:` case of
`cdreaddvdstructure()`, `dvdstruct->length` is computed as
`length - ccb->csio.resid - sizeof(header)` in mixed `int` / `u_int32_t`
arithmetic. A device that completes READ DVD STRUCTURE (opcode 0xad) with
CAM_REQ_CMP and a transfer short of the 4-byte header (or any
`resid > length - 4`) wraps the unsigned subtraction, `min(2048, ~4G)`
selects 2048, and the `bcopy` reads up to 2044 bytes past an 8-byte
`kmalloc(M_DEVBUF)` databuf into the caller's `dvd_struct.data` — which
the ioctl layer then copies out to userspace (`sys/kern/sys_generic.c:725`).

## Rig

Same CAM verbatim-interposition pattern as the DF-2263/2264/2265 scsi_sa
rig: a byte-for-byte `scsi_cd.c` (sha256-gated to the audit tree:
`34ae0dc0ed9a601b91f1849f70fc876bfc96232fc9e74306d8bda47bf5a5d0b8`)
is concatenated between `df_pre.c` (interposes only `xpt_path_periph`,
`cam_periph_getccb`, `cam_periph_runccb`, `xpt_release_ccb`) and
`df_post.c` (fake malicious transport + heap-canary groom + sysctl
drivers) and compiled into the kernel as `bus/cam/scsi/df2269_harness.c`
("device cd" is commented out of the guest config so the harness TU is
the only copy).  The shims PASS THROUGH to the real CAM functions except
during the synchronous window around the tested `cdioctl()` call —
this guest really has an ATAPI DVD-ROM (acd0) that must keep working.

Everything between entry and the buggy line is REAL kernel code:
`cdioctl` → `cam_periph_hold` → `cdcheckmedia`-skip (VALID_MEDIA) →
`cdreaddvdstructure` → `cdgetccb` → `scsi_read_dvd_structure` →
`cam_fill_csio` → `cdrunccb` → (fake transport sets resid + CAM_REQ_CMP)
→ the default-case arithmetic and `bcopy`.

## Build / run (audit host, from this directory)

    ./build.sh            # push + full nativekernel + install + reboot (~12 min)
    ./run.sh              # battery: matrix, formats, leak dump, repeat

For the fixed (A/B) run:

    ./run_fixed.sh        # applies fix.diff in the guest, quickkernel, reboot,
                          # re-runs the identical battery, un-patches

Key sysctl triggers (run as root on the guest):

    sysctl debug.df2269.matrix=1    # resid boundary matrix (MEDIUM_STAT, 8B buf)
    sysctl debug.df2269.formats=1   # 2-byte short transfer on all 11 formats
    sysctl debug.df2269.leak=1      # full 2048-byte dump of dvd_struct.data
    sysctl debug.df2269.repeat=N    # N leak runs (heap-state variance)

## Expected (unpatched)

* transferred=8/5/4 (resid=0/3/4): clean — length 4/1/0, no OOB.
* transferred<=3 (resid>=5), resid>length, resid=0xFFFFFFFF:
  `length=0xffffffxx`, `*** OOB READ: kernel copied 2048 bytes from a
  8-byte heap buffer (2044 bytes past its end)`, leaked data[]
  contains pre-placed canary chunks AND kernel heap pointers
  (M_DEVBUF zone free-list linkage, e.g. fffff8008d3622xx every 16 bytes).
* BCA (192B buf): 1860 bytes past end, 190 ptr-like values.
* Formats with len>=2052 (DISCKEY/DDS/MANUFACT/DISCKEYBLOCK/LIST):
  wrapped length reported to userspace but copy stays in bounds.

## Expected (patched, fix.diff)

* Sane transfers bit-identical (same return, same length, same fnv1a).
* Every underflow/impossible-resid row: `ret=5 (EIO)`, length=0,
  copy=0, zero leaked bytes, zero markers, zero kernel pointers.

## Files

See manifest.json.  Highlights: `run_dmesg.log` (unfixed battery),
`run_fixed_currentboot.txt` (fixed battery), `leak_sample_boot1.txt` /
`leak_sample_boot2.txt` / `leak_boot2_summary.txt` (leak dumps and
variance across boots), `VERDICT.md` (full narrative), `fix.diff`
(git-apply-able against sys/).
