# DF-2270 — VERDICT

## Status: REPRODUCED (dos, certain)

The missing bounds check is real, is the outlier in its own file, and the
full consequence chain was reproduced mechanically on a stock INVARIANTS
DragonFly 6.5-DEVELOPMENT guest with the byte-identical driver code
compiled into the kernel (CAM verbatim-interposition rig, `rig/`).

## Root cause (line-accurate)

`chgetelemstatus()` (sys/bus/cam/scsi/scsi_ch.c:1053):

- **:1066** `int chet = cesr->cesr_element_type;` — u_int16_t from the
  copied-in ioctl struct; user-controlled 0..65535. NO bounds check follows.
- **:1077-1079** the only range gate:
  ```c
  if ((softc->sc_counts[chet] - cesr->cesr_element_base) <= 0
      || (cesr->cesr_element_base + cesr->cesr_element_count)
	      > softc->sc_counts[chet])
      return (EINVAL);
  ```
  `sc_counts` is `int sc_counts[CHET_MAX + 1]` = `int[4]`
  (scsi_ch.c:158-159). For chet 4..65535 the gate is evaluated against
  out-of-bounds heap words (twice — the array is read in both clauses).
- **:1100** and **:1137-1138** `softc->sc_firsts[chet]` — two more OOB
  reads whose values are written into the READ ELEMENT STATUS CDB sent to
  the device (element address fully heap-derived).
- **:1120-1122** `size = 8 + 8 + desclen * cesr_element_count` with the
  count that the OOB word just authorized.
- **:1129** `kmalloc(size, M_DEVBUF, M_INTWAIT)`.

Every sibling handler checks: chmove :805, chexchange :859, chposition
:924, chsetvoltag :1261 (`if (xxx_type > CHET_DT) return (EINVAL/ENODEV)`).
chgetelemstatus is the only one that does not.

## Mechanical evidence

Rig: fabricated `ch_softc` zeroed at offset 256KB of a 512KB M_DEVBUF
region filled with `0x5a`; periph + `make_dev(&ch_ops, ..., UID_ROOT,
GID_OPERATOR, 0600, "ch%d", 0)` exactly as chregister does (:364-366);
fake CAM transport answers MODE SENSE (element address page 0x1d:
firsts 0x1000/0x2000/0x3000/0x4000, counts 2/16/2/4; cap page 0x1f) and
READ ELEMENT STATUS. Tests drove the REAL code through real userland
`ioctl(2)` on `/dev/ch0`.

1. **OOB map** (`sysctl debug.dfch.map=1`, run_dmesg.log): chet 0..3
   in-range; chet 4-5 read `sc_movemask`/`sc_exchangemask` bytes as
   `0xffffffff` (-1, gate rejects); chet 6 reads settledelay+pad (0);
   **chet >= 7 reads the canary `0x5a5a5a5a` (1515870810) — the gate
   ACCEPTS any base/count for a nonexistent element type**.
2. **Validation bypass through the real device** (run_all.log):
   - `ges 1 0 16` -> SUCCESS (control, 16 statuses returned)
   - `ges 1 0 17` -> EINVAL (control: in-range validation works)
   - `ges 8 0 300` -> **SUCCESS: 300 element statuses returned for
     element type 8**, authorized solely by the OOB canary word
   - `ges 1000 0 1000` -> **SUCCESS** (OOB read 4000 bytes past the array)
   - `ges 4 0 1` -> EINVAL (heap-dependent: movemask bytes as -1)
3. **M_INTWAIT consequence** (hang_serial.log / hang_pagedaemon.log,
   captured on the serial console): the same call with count=65535 and a
   target desclen of 0xffff computes `size = 0xffff*0xffff+16 ≈ 4GiB`;
   `kmalloc(..., M_DEVBUF, M_INTWAIT)` never completes:
   ```
   DFH2270HANG: *** THREAD DID NOT RETURN within 8s ... (watchdog fired)
   pagedaemon: WARNING! Continuous paging for 1 minutes
   pagedaemon: WARNING! Continuous paging for 1 minutes
   ```
   The guest then died outright (ssh banner exchange timeout; the VM had
   to be force-killed and reset). sysctl run_rc=124. This is a
   guest-killing local DoS from a single ioctl.
   (Iteration 1 of this test panicked on a harness calling-convention
   artifact — kthread omitted `cam_periph_lock` — documented in
   hang_iteration1.txt and fixed; iteration 2 is the valid evidence.)
4. **OOB-derived CDB content**: with chet=8 the fake transport received
   READ ELEMENT STATUS with sea=0xffff (`sc_firsts[8]` OOB read) — the
   element address handed to the device is heap garbage (visible in
   rig transport counters).

## Privesc assessment (team directive)

CLOSED — no unprivileged escalation:
- `/dev/ch0` is `crw------- root operator` (make_dev at scsi_ch.c:364-366;
  observed live on the guest, run_all.log). Only uid 0 can open it.
- `chopen` independently requires `SYSCAP_RESTRICTEDROOT`
  (scsi_ch.c:396) — verified present in the verbatim code compiled here.
- The OOB reads are reads; no write primitive exists in this function
  (the kmalloc target is fresh memory; `user_data`/`data` writes are
  driver-internal). The impact is authorization-from-garbage + DoS.
Medium severity (privileged-user DoS + heap-dependent OOB read in a
default-config-reachable driver) is the correct ceiling.

## Fix validation (A/B)

`fix.diff` (one hunk, the chmove idiom at :805 applied to
chgetelemstatus, placed right after `/* perform argument checking */`):

- vulnerable: `ges 8 0 300` SUCCESS, `ges 1000 0 1000` SUCCESS, hang test
  kills the guest
- fixed (run_fixed_all.log): `ges 8 0 300` EINVAL, `ges 1000 0 1000`
  EINVAL, `ges 1 0 16` SUCCESS, `ges 1 0 17` EINVAL (no regression), and
  the identical direct-call hang trigger returns instantly:
  `DFH2270HANG: chgetelemstatus RETURNED (no hang)`, `RES ccbs=0`.

fix_status: fixed.

## Honest notes

- The `debug.dfch.hang` trigger calls the real chgetelemstatus with a
  kernel-side cesr after taking the periph lock exactly as chioctl does;
  the userspace `ges` path (real ioctl) demonstrates the same gate bypass
  without any harness shortcut.
- The first baseline rig iteration had a shim bug (MODE SENSE guard
  `len >= 64` vs actual param_len 30) that zeroed the real counts; fixed
  and the full suite re-run — the bypass results were identical across
  both iterations (they never depended on the in-range values).

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
