# DF-0989 — cdreaddvdstructure integer underflow in length math

## Verdict
**NOT REPRODUCED at runtime on the default guest — bug is real as a code
pattern (would leak up to 2048 bytes of kernel heap to userspace), but
triggering requires a malicious SCSI/iSCSI CD-ROM returning short
READ_DVD_STRUCTURE data with high resid.** Defense-in-depth fix authored
+ compiled.

## Mechanism
`cdreaddvdstructure()` at `sys/bus/cam/scsi/scsi_cd.c:4107` allocates a
buffer of `length` bytes and issues a `READ_DVD_STRUCTURE` SCSI command:

```c
/* scsi_cd.c:4207-4208 */
if (length != 0)
    databuf = kmalloc(length, M_DEVBUF, M_INTWAIT | M_ZERO);
```

After the command completes, the default branch of the result switch
(for DVD_STRUCT_BCA, DVD_STRUCT_DISCKEY, DVD_STRUCT_MANUFACT, etc.)
computes the user-visible length:

```c
/* scsi_cd.c:4288-4289 */
dvdstruct->length = length - ccb->csio.resid -
    sizeof(struct scsi_read_dvd_struct_data_header);
```

Type analysis:
- `length` is `int` (declared line 4113).
- `ccb->csio.resid` is `u_int32_t`.
- `sizeof(struct scsi_read_dvd_struct_data_header)` is `size_t`.
- The expression is promoted to `size_t` for the subtraction.
- `dvdstruct->length` is `u_int32_t` (see `sys/sys/dvdio.h:64`).

If a malicious device returns short data with `resid == length`, then
`length - resid - sizeof(header)` = `0 - 4` = `(size_t)-4` =
`0xFFFFFFFFFFFFFFFC`, truncated to `u_int32_t` `0xFFFFFFFC`.

The subsequent bcopy:
```c
/* scsi_cd.c:4295-4297 */
bcopy(databuf + sizeof(struct scsi_read_dvd_struct_data_header),
      dvdstruct->data,
      min(sizeof(dvdstruct->data), dvdstruct->length));
```

- `sizeof(dvdstruct->data)` is `2048` (see `sys/sys/dvdio.h:65`).
- `min(2048, 0xFFFFFFFC)` evaluates to `2048`.
- bcopy reads **2048 bytes** starting at `databuf + 4`. But `databuf`
  was allocated with only `length` bytes (which for DVD_STRUCT_BCA is
  `sizeof(struct scsi_read_dvd_struct_data_bca)`, just a few bytes).

→ **Out-of-bounds read of the kernel heap (up to ~2048 bytes past
`databuf`)** copied into `dvdstruct->data`, which is then returned to
userspace via the `DVDIOCREADSTRUCTURE` ioctl. This is a kernel heap
info leak.

## Reachability analysis

`cdreaddvdstructure()` is reached from `cdioctl(DVDIOCREADSTRUCTURE)` at
`sys/bus/cam/scsi/scsi_cd.c:2751-2757`. To trigger:

1. Open `/dev/cd0` (requires `operator` group membership or root).
2. Issue `ioctl(fd, DVDIOCREADSTRUCTURE, &dvdstruct)` with `format` set
   to one of the default-branch formats (e.g. `DVD_STRUCT_BCA`).
3. The underlying device must return short data with high resid.

**On the audit guest:**

- `/dev/cd0` is `crw-r----- root operator` — `maxx` (uid 1001, only group
  `maxx`) cannot open it, so `maxx` cannot issue the ioctl at all.
- Even with access, the virtual QEMU PIIX CD-ROM returns well-formed
  READ_DVD_STRUCTURE data. The malicious short-data precondition does not
  arise.

For an unprivileged user to trigger this on a real deployment, they would
need:

- Membership in the `operator` group (or root), AND
- A malicious iSCSI / USB CD-ROM returning short READ_DVD_STRUCTURE data.

The `operator` group requirement alone means this is not an
unprivileged-user trigger; even with operator membership, a malicious
device is still required. **Valid hard blocker: the bug is real but
cannot be triggered by an unprivileged syscall on the default kernel +
default devices.**

## Exploit chain
**Not applicable / blocked by valid hard blocker (operator-group +
malicious device required).** The primitive is a kernel heap OOB read
(info leak); a chain to `uid=0` would require pairing with a separate
write primitive, which is out of scope for this single finding. The
right action is to fix the underflow.

## Fix
The fix in `fix.diff` rewrites the length computation with explicit
clamping that cannot underflow:

```c
size_t hdr_sz = sizeof(struct scsi_read_dvd_struct_data_header);
size_t avail  = (size_t)length;
size_t resid  = ccb->csio.resid;
size_t copied;

if (avail > resid)
    copied = avail - resid;
else
    copied = 0;
if (copied > hdr_sz)
    copied -= hdr_sz;
else
    copied = 0;
if (copied > sizeof(dvdstruct->data))
    copied = sizeof(dvdstruct->data);

dvdstruct->length = copied;
bcopy(databuf + sizeof(struct scsi_read_dvd_struct_data_header),
      dvdstruct->data, copied);
```

This guarantees `dvdstruct->length` is always a valid in-range value and
the bcopy never reads past `databuf + length`.

## Fix validation
**Compile-only.** The bug requires operator-group access + a malicious
device, neither present on the guest. `fix_status: not_testable`.
Validated:

- `fix.diff` applies cleanly with `patch -p1` (rc=0).
- Built as part of a single kernel with the DF-0988 fix; `scsi_cd.c`
  compiled under `-Werror` (no warnings, no errors); kernel linked
  successfully. See `fix_build.log`.

## PoC changes
The PoC directory was seeded empty. No runtime trigger is possible on
the default guest. Authored `fix.diff` and this `VERDICT.md`.
