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), adjacentacb->queue.scsi_req_baddr,.sg_real_addr,.carrier_baddr,.sense_baddr(kernel bus addresses), the nextacb'sccb/sg_blocks/dmamapkernel pointers, and an adjacent process's in-flightcdb[12]β yielding KASLR bypass and cross-process SCSI command leakage; - (b) corruption of
csiofields (msg_ptrbecomes a kernel pointer returned to userland;resid,cdb_io,tag_id,init_idcorrupted).
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).
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-1510 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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)
- 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 ofstruct scsi_sense_data). - But
csio->sense_lenitself is never updated β the user-supplied value (up to 255 = u8 max) survives unchanged. - 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. - Both source (
struct acb.sense_data,adwlib.h:425) and destination (ccb->csio.sense_data,cam_ccb.hstruct ccb_scsiio) are 32 bytes. - With
sense_len = 255, the bcopy: - Reads bytes[32..254]ofstruct acbβ that isacb->links.sle_nextandacb->queue.*(kernel pointers, DMA bus addresses) β and copies them into the user-visible ccb. - Writes bytes[32..254]pastccb->csio.sense_dataβ corruptingccb->csio.cdb_len / sglist_cnt / scsi_status / sense_resid / resid / cdb_io / msg_ptr / msg_len / tag_action / tag_id / init_id. csio->sense_lenis fully attacker-controlled viapasssendccb/xpt_merge_ccbbcopy.
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_testablenot_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 ===
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- i
- s
- k
- /
- a
- d
- v
- a
- n
- s
- y
- s
- /
- a
- d
- w
- c
- a
- m
- .
- c
- :
- 3
- 8
- 3
- s
- y
- s
- /
- d
- e
- v
- /
- d
- i
- s
- k
- /
- a
- d
- v
- a
- n
- s
- y
- s
- /
- a
- d
- w
- c
- a
- m
- .
- c
- :
- 3
- 8
- 4
- s
- y
- s
- /
- d
- e
- v
- /
- d
- i
- s
- k
- /
- a
- d
- v
- a
- n
- s
- y
- s
- /
- a
- d
- w
- c
- a
- m
- .
- c
- :
- 1
- 3
- 2
- 1
- s
- y
- s
- /
- d
- e
- v
- /
- d
- i
- s
- k
- /
- a
- d
- v
- a
- n
- s
- y
- s
- /
- a
- d
- w
- c
- a
- m
- .
- c
- :
- 1
- 3
- 2
- 2
- s
- y
- s
- /
- d
- e
- v
- /
- d
- i
- s
- k
- /
- a
- d
- v
- a
- n
- s
- y
- s
- /
- a
- d
- w
- l
- i
- b
- .
- h
- :
- 4
- 2
- 5
- s
- y
- s
- /
- b
- u
- s
- /
- c
- a
- m
- /
- s
- c
- s
- i
- /
- s
- c
- s
- i
- _
- a
- l
- l
- .
- h
- :
- 9
- 1
- 1
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.
No comments yet.