# DF-4184 — acd_fixate() sends MODE SELECT sized by device-fed page_length out of a 64-byte stack buffer

* File: `sys/dev/disk/nata/atapi-cd.c` (pass 2)
* Class: crafted-device kernel stack information disclosure (CWE-909/CWE-786 read side)
* Reachability: `CDRIOCFIXATE` ioctl on `/dev/acd0` (0640 root:operator, plus the DF
  disk layer's `SYSCAP_RESTRICTEDROOT` gate at `sys/kern/subr_disk.c:1072`), i.e.
  root / RESTRICTEDROOT-capable processes such as burning tools talking to an
  attached ATAPI drive whose MODE SENSE (page 0x05) response is attacker-controlled
  (malicious/firmware-compromised USB ODD, device passthrough).

## Root cause

`acd_fixate()` (`sys/dev/disk/nata/atapi-cd.c:1140-1150`):

```c
struct write_param param;                       /* 64-byte __packed stack object */
if ((error = acd_mode_sense(dev, ATAPI_CDROM_WRITE_PARAMETERS_PAGE,
                            (caddr_t)&param, sizeof(param))))    /* device fills it */
        return error;
param.data_length = 0;
...
if ((error = acd_mode_select(dev, (caddr_t)&param, param.page_length + 10)))
```

`param.page_length` (offset 9 of the MODE SENSE response) is an unvalidated
**device-controlled byte** (0..255). `acd_mode_select()` passes that value +10
straight as the data-out bytecount:

```c
return ata_atapicmd(dev, ccb, pagebuf, pagesize, 0, 30);   /* flags=0 => DATA-OUT */
```

A device answering `page_length = 0xff` makes the kernel push **265 bytes out of
the 64-byte stack object** — 201 bytes of adjacent kernel stack (frame pointers,
kernel text/heap pointers) are transferred to the device. (Unlike
`acd_init_track`/`acd_send_cue`, which hard-set `param.page_length = 0x32`
before the same call, `acd_fixate` keeps the device-fed value.)

## Reproduction (crafted-device emulation on the audit guest)

Stock QEMU ATAPI refuses MODE SENSE page 0x05, so the guest kernel was rebuilt
with a small **device-side emulator hook** in `ata_atapicmd()` (guest-local only;
same methodology as the DF-3690..3696 crafted-disk family) that makes the drive
(1) answer MODE SENSE(0x05) with `page_length=0xff`, (2) accept MODE SELECT /
CLOSE TRACK. `df_audit.mode=1` enables it; nothing in `acd_fixate` is modified.

```
./build.sh    # cc -O -o fixate_stackread fixate_stackread.c   (on the guest)
./run.sh      # /tmp/fixate_stackread /dev/acd0
```

Expected (and observed) decisive kernel console output — see `run.log`:

```
DFAUDIT: hook ccb0=5a ccb2=05 cnt=64 ...   ata_atapicmd(..., sizeof(param))
DFAUDIT: forged MODE_SENSE(0x05) page_length=0xff
DFAUDIT: hook ccb0=55 ccb2=00 cnt=265 ...  MODE SELECT data-out of 265 bytes
DFAUDIT: MODE_SELECT count=265 from 65-byte stack param, over-read bytes:
  ... 201 bytes containing 0xfffff800/0xfffff8xxxxxxxxxx kernel pointers and
      0xffffffff80xxxxxx kernel-text pointers (KASLR-defeating material)
```

The hexdump is produced by the emulation hook reading the same buffer the DMA
engine reads; on real crafted hardware those bytes leave the machine toward the
device.

## Fix validation

`fix.diff` clamps the device-fed page length to the MMC write-parameters-page
maximum (0x32) before any data-out. Validated on the guest with the same
emulated attack:

* `df_audit.pl=255` (same attack): fixate now returns `EINVAL` immediately —
  **no MODE SELECT is issued, no bytes leave the stack object, no panic**.
* `df_audit.pl=50` (valid page) + `df_audit.capfail=1`: MODE SELECT sized 60
  bytes (in-bounds), driver continues normally, ioctl completes — no over-read.

Both runs: `fix_run.log`, `fix.2.log`.

## Contents

* `fixate_stackread.c` — trigger (CDRIOCFIXATE + bounds negative tests)
* `run.log` — decisive baseline run (program output + kernel console)
* `leak_sample.txt` — annotated leaked qwords
* `dfaud_dmesg_raw.txt` — raw dmesg of the F1 boot
* `build.log` — kernel build excerpts (atapi-cd.c compile + RC=0)
* `fix.diff` — the verified fix (against pristine sys/)
* `fix_run.log`, `fix.2.log` — fix-validation runs
* `env.txt`, `userland_runs.txt`
* `manifest.json`, `verdict.json`
