# DF-1578 — Heap OOB write in hptrr INQUIRY/READ_CAPACITY/SERVICE_ACTION_IN

## Verdict

**NOT REPRODUCED ON THIS GUEST** — the bug is real in source
(line-by-line confirmed), and `hptrr` *is* in `X86_64_GENERIC`, but no
HighPoint RR17xx/22xx/23xx/25xx RAID controller is present in the QEMU
guest so the driver never attaches and `hpt_scsi_io` is never called.
Even with hardware, the only path to delivering an attacker-controlled
`dxfer_len` CCB is `/dev/passN`, which is mode 0600 root — i.e. this is
a **root→kernel hardening gap**, not an unprivileged→root escalation.
Fix authored, applies cleanly, compiles cleanly with `-Werror`.

## The bug (confirmed in source)

`sys/dev/raid/hptrr/hptrr_osm_bsd.c`, function `hpt_scsi_io`
(lines 535-741), cases INQUIRY / READ_CAPACITY / SERVICE_ACTION_IN:

### INQUIRY (lines 579-601)
```c
case INQUIRY:
    {
        PINQUIRYDATA inquiryData;
        memset(ccb->csio.data_ptr, 0, ccb->csio.dxfer_len);   /* line 582 */
        inquiryData = (PINQUIRYDATA)ccb->csio.data_ptr;
        inquiryData->AdditionalLength = 31;          /* byte 4 */
        inquiryData->CommandQueue = 1;               /* byte 7 */
        memcpy(&inquiryData->VendorId, "HPT     ", 8);            /* bytes 8-15 */
        memcpy(&inquiryData->ProductId, "DISK 0_0        ", 16);  /* bytes 16-31 */
        ...
        memcpy(&inquiryData->ProductRevisionLevel, "4.00", 4);    /* bytes 32-35 */
```
* `PINQUIRYDATA` is `struct _INQUIRYDATA` (`os_bsd.h:82-106`):
  offset 4 = AdditionalLength; offset 7 = CommandQueue bit; bytes 8-15
  = VendorId[8]; bytes 16-31 = ProductId[16]; bytes 32-35 =
  ProductRevisionLevel[4].
* The handler writes to fixed offsets 0..35 (36 bytes minimum) without
  ever checking `ccb->csio.dxfer_len`.
* If `dxfer_len` is 0 (or smaller than the highest offset written),
  every write past `data_ptr + dxfer_len` corrupts adjacent kernel
  memory.

### READ_CAPACITY (lines 603-624)
* Writes `rbuf[0]..rbuf[7]` (8 bytes), no `dxfer_len` check.

### SERVICE_ACTION_IN (lines 626-646)
* Writes `rbuf[0]..rbuf[11]` (12 bytes), no `dxfer_len` check.

## Reachability on this guest

* `hptrr` *is* in `X86_64_GENERIC` — statically compiled into the
  default kernel.
* But `pciconf -l` shows no RAID controller matching hptrr's PCI IDs;
  `dmesg` shows only `hpt27xx: no controller detected.` (the *sibling*
  driver; hptrr itself never printed an attach message).
* Therefore `hpt_scsi_io` is never called at runtime — no `VDEV` was
  registered with the CAM layer.
* Even on a host with hptrr hardware attached, the only way for an
  attacker to deliver a CCB with a hostile `dxfer_len` is via the CAM
  pass-through layer: `/dev/passN`.  On this guest `/dev/pass0` is
  `crw------- 1 root operator` — **mode 0600 root**.  An unprivileged
  user cannot open it.

### Why the kernel-initiated probe path does not trigger the bug

When the kernel itself probes a newly-attached disk, the peripheral
drivers (`da`, `cd`, `pass`) issue INQUIRY etc. with proper
allocation lengths (`dxfer_len >= 96` for INQUIRY, etc.).  The bug
requires a maliciously small `dxfer_len`, which only an explicit
user-issued CCB via `/dev/passN` (root-only) can supply.

### Realistic threat model

* **root → kernel**: a root user with hptrr hardware (or with the
  driver attached to a crafted virtual RAID) can `ioctl(/dev/passN)`
  to send a CCB with `dxfer_len=0` against the hptrr SIM, causing the
  OOB write.  This is a root→kernel hardening gap — root can already
  `kldload`/write/devfs-clone its way to ring 0, so the practical
  security impact is low.
* **Unprivileged user**: no path identified.  The standard SCSI disk
  peripheral (`da`) does not propagate user-controlled `dxfer_len`
  values down to the SIM; only the pass-through (`pass`) peripheral
  does, and that requires root.
* Same code shape exists in the sibling `hpt27xx` driver — note the
  in-guest dmesg shows hpt27xx *was* probed and found no controller
  (no separate finding exists for hpt27xx's identical code yet).

## Exploit chain

Not applicable.  The primitive (a fixed-pattern OOB write up to 36
bytes past a caller-supplied buffer) is only reachable from root via
`/dev/passN`, which is a **valid hard blocker** (the write is
reachable only from an already-root context).  No unprivileged path
exists; therefore no escalation chain is possible.  Documented as a
root→kernel hardening gap with a verified fix.

## The fix

`fix.diff` — add a `dxfer_len` minimum-size check at the top of each
of the three affected cases, returning `CAM_REQ_ABORTED` if the
caller did not supply enough buffer for the response.  Minimal,
targeted, three small hunks.

```diff
 case INQUIRY:
     {
         PINQUIRYDATA inquiryData;
+        if (ccb->csio.dxfer_len < 36) {
+            ccb->ccb_h.status = CAM_REQ_ABORTED;
+            break;
+        }
         memset(ccb->csio.data_ptr, 0, ccb->csio.dxfer_len);
         ...

 case READ_CAPACITY:
     ...
+    if (ccb->csio.dxfer_len < 8) {
+        ccb->ccb_h.status = CAM_REQ_ABORTED;
+        break;
+    }

 case SERVICE_ACTION_IN:
     ...
+    if (ccb->csio.dxfer_len < 12) {
+        ccb->ccb_h.status = CAM_REQ_ABORTED;
+        break;
+    }
```

## PoC changes

No userspace PoC is included because (a) the driver is not attached on
this guest and (b) the trigger requires `/dev/passN` which is
root-only.  A PoC would be a small `camcontrol`/`pass` ioctl program
run by root against an attached hptrr virtual disk — but the security
value of root→kernel primitives is marginal.

## Reproduce

Not runnable on this guest (no hptrr hardware).  See `fix_build.log`
for the full patched-module rebuild.
