# DF-1442 — INQUIRY TRIM status writes past short inquiry buffer (heap overflow)

## Verdict: REPRODUCED (source-trace + deterministic harness); FIX VALIDATED

**Severity (per finding):** High
**Impact:** kernel heap overflow (2-byte write past a `kmalloc(38)` slab object),
fires automatically at boot for any TRIM-capable AHCI SATA disk.

---

## The bug (mechanism, `path:line` at every hop)

`sys/dev/disk/ahci/ahci_cam.c`, function `ahci_xpt_scsi_disk_io()`, the
`INQUIRY` case. After the normal/EVPD inquiry body, the handler writes the
disk's TRIM status into the inquiry buffer **unconditionally** (if the disk
reports DSM/TRIM support):

```c
/* sys/dev/disk/ahci/ahci_cam.c:1134-1139 (UNPATCHED) */
if (at->at_identify.support_dsm) {
    rdata->inquiry_data.vendor_specific1[0] =
        at->at_identify.support_dsm & ATA_SUPPORT_DSM_TRIM;   /* byte 96 */
    rdata->inquiry_data.vendor_specific1[1] =
        at->at_identify.max_dsm_blocks;                       /* byte 97 */
}
```

`rdata` is `csio->data_ptr` (the caller-supplied inquiry buffer) and
`rdata->inquiry_data.vendor_specific1` lives at byte offset **96** of
`struct scsi_inquiry_data` (verified: `sizeof == 256`,
`offsetof(vendor_specific1) == 96`; all fields are 1-byte aligned so there is
no padding to discount). So the two writes land at bytes **96/97** of the
buffer regardless of its actual length.

**Who allocates a short buffer?** CAM Domain Validation. In
`sys/bus/cam/cam_xpt.c::probescsi()`, the `PROBE_INQUIRY_BASIC_DV1`/`DV2`
states allocate a *separate* inquiry buffer sized to the device's reported
`additional_length`:

```c
/* sys/bus/cam/cam_xpt.c:5864-5880 (abridged) */
if (softc->action == PROBE_INQUIRY)
    inquiry_len = SHORT_INQUIRY_LENGTH;          /* 36 */
else
    inquiry_len = SID_ADDITIONAL_LENGTH(inq_buf); /* additional_length + 5 */
inquiry_len = roundup2(inquiry_len, 2);
if (softc->action == PROBE_INQUIRY_BASIC_DV1 || ...DV2) {
    inq_buf = kmalloc(inquiry_len, M_CAMXPT, M_INTWAIT);   /* <-- short buf */
}
scsi_inquiry(csio, ..., inq_buf, inquiry_len, evpd=FALSE, ...);
```

AHCI hardcodes `additional_length = 32` (`ahci_cam.c:1119`), so
`SID_ADDITIONAL_LENGTH = 32 + 5 = 37`, `roundup2(37,2) = 38` → **`kmalloc(38)`**.
The subsequent `vendor_specific1[0]/[1]` writes at byte 96/97 are therefore
**58/59 bytes past the 38-byte allocation** — a heap overflow into the adjacent
slab object.

DV runs for **all** lun-0 devices (the `SID_Sync` gate is commented out at
`cam_xpt.c:6433`), so this fires at every boot for any TRIM-capable AHCI disk.

### Why a harness, not a live in-kernel run

The audit guest has **no AHCI SATA disk** — only `vtblk0` (virtio-blk root fs)
and `acd0` (a DVD-ROM on the legacy `ata(4)` driver, `ata1-master`). The
`ahci_xpt_scsi_disk_io` path is only reachable via an `ahci(4)` HBA with a
disk, which the QEMU config (`dfbsd-qemu/vm.sh` lines 55-62) does not provide.
The driver is compiled in (`device ahci`, `sys/config/X86_64_GENERIC:65`) and
the module exists (`/boot/kernel/ahci.ko`), but there is no hardware to probe.

Because the struct layout is identical in-kernel and the offset arithmetic is
the entire bug, `harness.c` reproduces the exact vulnerable write using the
**verbatim** kernel structs from `sys/bus/cam/scsi/scsi_all.h`, the exact DV1
allocation math, and a canary guard to detect the out-of-bounds write.

### Harness result

```
sizeof(struct scsi_inquiry_data) = 256
offsetof(vendor_specific1)       = 96
CAM DV1 inquiry_len = 38 (additional_length=32)

[UNPATCHED] mode=0, buffer=38 bytes, write at offset 96/97
canary clobbered at 96/97: YES / YES (0x01/0x02 vs canary 0xcd)
*** HEAP OVERFLOW CONFIRMED (mode 0) ***
wrote 0x01 at arena[96] and 0x02 at arena[97]
(canary was 0xcd; these bytes are 58/59 past the 38-byte allocation)

[PATCHED] mode=1, buffer=38 bytes, write at offset 96/97
canary clobbered at 96/97: no / no (0xcd/0xcd vs canary 0xcd)
*** NO OVERFLOW (mode 1): write correctly skipped/guarded ***

=== SUMMARY ===
UNPATCHED logic: OVERFLOW (bug present)
PATCHED   logic: clean (fix holds)
```

## Threat model & realistic impact ceiling

This is **not** an unprivileged-local-user-to-root escalation. The two written
bytes come from the disk's own IDENTIFY data at **boot-time** DV probe:

- `vendor_specific1[0]` = `support_dsm & ATA_SUPPORT_DSM_TRIM` = fixed **0x01**
  (TRIM bit), not attacker-shaped.
- `vendor_specific1[1]` = `max_dsm_blocks` (IDENTIFY word 105) — controlled by a
  **malicious disk**, not by a user process.

An unprivileged local user **cannot** present a fake IDENTIFY, cannot trigger
the boot-time DV probe on demand (`camcontrol rescan` is root-only), and cannot
shape the slab layout at boot (no user processes exist yet). The realistic
attacker is therefore a **malicious AHCI SATA / USB-SATA device** (or a crafted
disk image) that corrupts adjacent kernel heap during probe.

- **Default GENERIC kernel (INVARIANTS ON):** the slab allocator poisons free
  chunks (`WEIRD_ADDR` 0xdeadc0de) and tracks chunk state in `z_Bitmap`
  (`sys/kern/kern_slaballoc.c`). Corrupting an adjacent chunk's bytes trips the
  INVARIANTS checks on its next alloc/free → **panic (KASSERT) / DoS**.
- **Non-INVARIANTS kernel:** silent corruption of the adjacent slab object.

So the realistic impact ceiling on a stock system is a **boot-time kernel panic
/ DoS** whenever a TRIM-capable AHCI SSD is attached; a malicious device could
in principle aim the corruption at a chosen slab neighbor. There is no
unprivileged-user escalation chain — the valid blocker is that the write is
reachable only from the disk's boot-time IDENTIFY, a context no local user can
drive.

## The fix (`fix.diff`)

Guard the TRIM-status write with a size check so it only fires when the inquiry
buffer actually extends to `vendor_specific1[1]`:

```c
/* sys/dev/disk/ahci/ahci_cam.c (PATCHED) */
if (at->at_identify.support_dsm &&
    rdata_len >= offsetof(struct scsi_inquiry_data, vendor_specific1) + 2) {
    rdata->inquiry_data.vendor_specific1[0] = ...;
    rdata->inquiry_data.vendor_specific1[1] = ...;
}
```

`offsetof(... vendor_specific1) + 2 == 98`. For the DV1/DV2 38-byte buffer,
`38 >= 98` is false → write correctly skipped. For a full 256-byte inquiry
buffer, `256 >= 98` → write proceeds as before. Minimal, targeted at the root
cause; matches the finding's proposed fix.

## Fix validation (Phase 8)

- `fix.diff` applies cleanly (`patch -p1`, Hunk #1 succeeded at 1129).
- Single-fix kernel built from the patched source: `make -j6 nativekernel
  KERNCONF=X86_64_GENERIC` → **rc=0, no errors** (`fix_build.log`).
- Patched kernel installed (`/boot/kernel/kernel`, sha256
  `9f885e22...`, BuildID `dd34bd78...`) and **boots cleanly** as
  `DragonFly 6.5-DEVELOPMENT #1: Fri Jul 17 15:23:44 UTC 2026`.
- The harness PATCHED-mode (the same guard logic compiled into the kernel)
  shows **no overflow** on the patched kernel, while UNPATCHED-mode still
  overflows (`fix_run.log`).

Because the in-kernel trigger needs AHCI SATA hardware absent from the audit
guest, the before/after is demonstrated via the harness (which replicates the
exact kernel code path) plus the patched-kernel build+boot; the guard is
source-verified to close the path.

## How to reproduce

```
./build.sh && ./run.sh
```
Builds `harness` and runs both UNPATCHED and PATCHED logic modes. UNPATCHED
prints `*** HEAP OVERFLOW CONFIRMED ***`; PATCHED prints `*** NO OVERFLOW ***`.
