Sense-data memcpy has dst/src swapped vs FreeBSD: autosense never delivered (all-zero sense + CAM_AUTOSNS_VALID) and device-controlled copy length overflows struct vtscsi_request when a pass CCB carries sense_len > 96
Summary
DragonFly's port swapped the autosense memcpy arguments: it copies FROM the just-bzero()ed 32-byte CCB sense buffer INTO the 96-byte device-response buffer (FreeBSD is the opposite). Consequence 1 (every check condition, any virtio-scsi device): CAM gets CAM_AUTOSNS_VALID over an all-zero sense buffer - silently breaks all sense-driven CAM error handling. Consequence 2: kernel periphs use SSD_FULL_SIZE=32 but pass(4) merges the user CCB wholesale with sense_len up to 255 unvalidated, so a malicious backend returning sense_len >= 97 writes up to 255 bytes into vsr_uresp.cmd.sense[96] - ~159 bytes past the field, smashing req->vsr_callout and req->vsr_link (shaped TAILQ_REMOVE write) and ~80 bytes past the request allocation; also OOB-reads the CCB. LIVE-VERIFIED on the audit guest's real vtscsi0 (temp launcher -device virtio-scsi-pci + scsi-hd): baseline returns all-zero sense with resid proving 18 real bytes were delivered; patched kernel returns the correct ILLEGAL REQUEST/ASC 0x21 sense - fix kernel-validated (kernel #1 batched build). Overflow arm needs a malicious backend (stock QEMU clamps to 96) - documented source-certain.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4053 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.8 KB | β raw | |
| VERDICT.md | β | 5.0 KB | β raw | |
| sense_test.c | β | 3.8 KB | view raw | |
| build.sh | β | 110 B | view raw | |
| run.sh | β | 167 B | view raw | |
| run.log | β | 1.0 KB | view raw | |
| run.patched.log | β | 1.2 KB | view raw | |
| env.txt | β | 681 B | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| fix_build.log | β | 1.9 KB | view raw | |
| verdict.json | β | 4.3 KB | view raw |
DF-4053 β virtio-scsi sense memcpy dst/src swapped (DFly-only port bug)
File: sys/dev/virtual/virtio/scsi/virtio_scsi.c:1392-1394 β pass 2 (GLM 5.3).
Device-driven threat model (QEMU host process / malicious virtio-scsi backend).
THE BUG
DragonFly (imported from FreeBSD r311305 but hand-adjusted):
bzero(&csio->sense_data, sizeof(csio->sense_data));
memcpy(cmd_resp->sense, &csio->sense_data, /* dst/src SWAPPED */
csio->sense_len - csio->sense_resid);
FreeBSD (all versions since import, verified against release/11.4.0):
memcpy(&csio->sense_data, cmd_resp->sense,
csio->sense_len - csio->sense_resid);
The copy direction is inverted in the DragonFly port: the driver copies FROM the just-bzero()ed CCB sense buffer INTO the device response buffer instead of delivering the device's sense data to CAM.
Two consequences:
-
Functional (fires on every CHECK CONDITION, any device): autosense is never delivered. CAM gets
CAM_AUTOSNS_VALIDover an all-zero sense buffer (Sense Error Code 0x0), breaking all sense-driven error recovery in da/cd/sa/pass (media-change detection, retry policy, error reporting). Proven live on the audit guest β see below. -
Memory-safety (device + pass CCB with sense_len > 96): the copy length is
csio->sense_len - csio->sense_resid: * device sense_len >= ccb sense_len -> length = csio->sense_len * device sense_len < ccb sense_len -> length = device sense_len i.e.min(device_sense_len, ccb_sense_len), fully device-influenced. The destinationcmd_resp->senseisuint8_t sense[96]insidereq->vsr_uresp.cmd(struct vtscsi_request, virtio_scsi.h:59), ~12 bytes into the union. Kernel-originated CCBs always usesense_len = SSD_FULL_SIZE = 32(sys/bus/cam/scsi/scsi_all.h:953; struct scsi_sense_data is 32 bytes on DragonFly), so the normal path stays in bounds. But pass(4)CAMIOCOMMANDmerges the user-supplied union ccb wholesale (xpt_merge_ccb, scsi_pass.c:546 -> bcopy of everything after ccb_h) with no validation ofsense_len(u_int8_t, up to 255). A CCB with sense_len=255 from a pass(4) user plus a device returning sense_len >= 97 yields a copy of up to 255 bytes into the 96-byte field: ~159 bytes past it, smashingreq->vsr_callout(callout func/arg pointers) andreq->vsr_link(TAILQ next/prev), and ~80 bytes past the end of thecontigmallocdstruct vtscsi_request(~268 bytes). Write content = 32 zero bytes then CCB fields (including the 16 fully user-controlledcdb_io.cdb_bytes). A corruptedvsr_linkgives a controlled*tqe_prev = tqe_nextwrite when the request is next dequeued (TAILQ_REMOVE in vtscsi_dequeue_request). The memcpy also OOB-READS the CCB (up to 255 bytes read from the 32-byte sense_data) β kernel heap bytes land insidevsr_uresp, though that region is bzero'd on re-enqueue (virtio_scsi.c:2213).
Stock QEMU clamps device sense_len to the negotiated sense_size=96, so reproducing the overflow needs a modified/malicious backend β exactly the threat model (QEMU host compromise or malicious passthrough device) but not possible with an unmodified QEMU 7.2.
MAKING THE GUEST ATTACKABLE
The audit guest normally boots with virtio-blk only. vm.sh was NOT
modified; a temporary launcher (dfbsd-qemu/vm-vtscsi.sh, deleted after
the run) booted the with-src snapshot with:
-device virtio-scsi-pci,id=scsi0 \ -drive file=vtscsi-disk.img,if=none,format=raw,id=vtscsi0 \ -device scsi-hd,drive=vtscsi0
(The root disk needs an explicit bootindex=0 on a
-device virtio-blk-pci, otherwise SeaBIOS hangs on the empty scsi disk.)
virtio_scsi is compiled into X86_64_GENERIC, so vtscsi0+da0+pass0 appear with no kldload.
BUILD (on the guest)
cc -O2 -Wall -o /tmp/sense_test /tmp/sense_test.c -lcam
RUN (root; /dev/pass0 is root-only)
/tmp/sense_test 255 # unvalidated pass CCB sense_len=255 /tmp/sense_test 32 # what every kernel periph uses (SSD_FULL_SIZE) camcontrol cmd da0 -v -i 512 - -c "28 00 ff ff ff f0 00 00 01 00"
The command is an out-of-range READ(10) (LBA 0xfffffff0 on a 64 MB disk) so QEMU's scsi-hd returns CHECK CONDITION with a known sense (ILLEGAL REQUEST, ASC 0x21 "LOGICAL BLOCK ADDRESS OUT OF RANGE").
EXPECTED
- Baseline (buggy) kernel β
CAM_AUTOSNS_VALID+scsi_status=0x02+sense_residproves the device returned exactly 18 sense bytes (255-237 and 32-14), but sense_data is all 0x00 (Sense Error Code 0x0from camcontrol). Reproduced 100% of runs. - Patched kernel (fix.diff: copy direction restored + MIN() clamp):
sense_data becomes
70 00 05 ... 21 00 ...β real error code 0x70, sense key 0x5 ILLEGAL REQUEST, ASC 0x21. Fix validated by rebuild + reboot on this guest (fix_build.log / run.patched.log).
DF-4053 β VERDICT
REPRODUCED on live hardware path (functional manifestation) + source-certain memory-corruption primitive that stock QEMU cannot fire (device-gated).
What was reproduced (baseline stock kernel #0, real vtscsi0)
Guest booted from the with-src snapshot with a temporary QEMU launcher adding
-device virtio-scsi-pci,id=scsi0 + -drive vtscsi-disk.img (raw) -device
scsi-hd (vm.sh untouched; the root disk needs an explicit bootindex=0,
otherwise SeaBIOS hangs β see README). virtio_scsi is in X86_64_GENERIC, so
vtscsi0 attaches with da0/pass0 (env.txt).
An out-of-range READ(10) (LBA 0xfffffff0 on the 64 MB disk) makes QEMU's scsi-hd return CHECK CONDITION with an 18-byte sense (ILLEGAL REQUEST, ASC 0x21). On the stock kernel:
ccb_h.status = 0xcc CAM_AUTOSNS_VALID CAM_SCSI_STATUS_ERROR csio.scsi_status = 0x02 (CHECK CONDITION) csio.sense_resid = 237 <- 255-237 = device returned 18 sense bytes csio.sense_data[0..31] = all 0x00 camcontrol: "Sense Error Code 0x0"
The resid arithmetic proves cmd_resp->sense_len == 18 (also 32-14=18 in the
sense_len=32 run), so virtio_scsi.c:1383 executed the autosense branch β and
sense_data stayed all-zero anyway. The only way that happens with this code is
the memcpy at :1393 copying FROM the just-bzero()ed csio->sense_data INTO
cmd_resp->sense (dst/src swapped vs FreeBSD, verified against
freebsd-src release/11.4.0 memcpy(&csio->sense_data, cmd_resp->sense, ...)).
100% of runs.
What is source-certain but device-gated (the heap-overflow half)
Copy length = csio->sense_len - csio->sense_resid = min(device sense_len,
ccb sense_len). Kernel periphs always use SSD_FULL_SIZE=32 (bounded, safe β
which is why the bug survived as "autosense mysteriously broken"). pass(4)
CAMIOCOMMAND merges the user ccb wholesale (xpt_merge_ccb) with sense_len
unvalidated up to 255 (the sense_len=255 run above shows the value flows
straight into the driver's math). A malicious virtio-scsi backend announcing
sense_len >= 97 then makes the driver write up to 255 bytes into
vsr_uresp.cmd.sense[96] (offset 12 of the union in struct vtscsi_request):
~159 bytes past the field β vsr_callout (function/arg pointers) and
vsr_link (TAILQ next/prev) are overwritten, and ~80 bytes land past the end
of the contigmalloc'd (~268-byte) request; the next dequeue's TAILQ_REMOVE
performs *tqe_prev = tqe_next through the corrupted link (write-what/where
shaped by 16 user-controlled CDB bytes at a known offset). Stock QEMU clamps
device sense_len to the negotiated sense_size=96, so the write half cannot be
fired by an unmodified QEMU 7.2 β it needs exactly the malicious-backend
threat model this driver is audited under (plus root access to /dev/pass0,
which caps severity at Medium, same tier as the device-only DF-1352).
Fix validation (mandatory for memory-corruption class)
fix.diff restores the FreeBSD copy direction and bounds the length
(MIN(len, sizeof(csio->sense_data))), closing BOTH directions (the reversed
copy also OOB-reads the CCB when len > 32). Batched with DF-4054's fix, built
(make nativekernel RC=0), installed, rebooted into kernel #1 (fix_build.log).
Same trigger on the patched kernel:
csio.sense_resid = 237 csio.sense_data = 70 00 05 00 00 00 00 0a 00 00 00 00 21 00 00 00 VERDICT: sense_data is REAL β sense key=0x05 ASC=0x21 ASCQ=0x00 camcontrol: "ILLEGAL REQUEST asc:21,0 / Logical block address out of range"
Behavior delta is exactly the fix; resid math unchanged (device still returns 18 bytes). The zero-sense bug is GONE.
Kernel references (re-verified line-by-line during verification)
- sys/dev/virtual/virtio/scsi/virtio_scsi.c:1392-1394 (swapped memcpy + bzero)
- sys/dev/virtual/virtio/scsi/virtio_scsi.h:53-60 (sense[96], packed resp)
- sys/dev/virtual/virtio/scsi/virtio_scsivar.h:113-137 (vsr_uresp/vsr_callout/vsr_link)
- sys/bus/cam/cam_ccb.h:602,606 (sense_data[32], sense_len u_int8_t)
- sys/bus/cam/scsi/scsi_all.h:911-953 (32-byte scsi_sense_data, SSD_FULL_SIZE)
- sys/bus/cam/scsi/scsi_pass.c:462-505 + xpt_merge_ccb (cam_xpt.c:3888-3898 β wholesale user ccb merge, no sense_len validation)
- FreeBSD release/11.4.0 sys/dev/virtio/scsi/virtio_scsi.c:1333-1335 (correct order)
Classification
status=reproduced (the defect executes live on a real vtscsi0; the memory-corruption direction is bounded away only by stock-QEMU's 96-byte sense clamp + the root-only pass path), impact=none at runtime (no panic / leak / escalation fired on stock QEMU β and none is possible without a modified backend), confidence=certain. Severity filed as Medium (device-driven heap OOB write, privileged-trigger preconditions; see finding markdown).
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.
Fix verification
fixedBatched build+install+reboot validated: baseline kernel #0 shows zeroed sense (bug live); patched kernel #1 delivers 70 00 05 ... 21 00 and camcontrol decodes ILLEGAL REQUEST asc:21,0 β reversed-copy behavior GONE with both fixes compiled in.
run.log vs run.patched.log; fix_build.log (RC=0, install completed, uname #1)
Confirmed kernel references
Detail
Exploit chain
unmodified-QEMU ceiling: broken autosense only. Malicious-device chain (not exercisable on stock QEMU): root opens /dev/pass0 -> CAMIOCOMMAND XPT_SCSI_IO with csio.sense_len=255 (merged wholesale by xpt_merge_ccb, no validation) -> device returns cmd_resp.sense_len>=97 -> vtscsi_complete_scsi_cmd_response memcpy writes up to 255 bytes into vsr_uresp.cmd.sense[96] -> vsr_callout func/arg and vsr_link next/prev corrupted (16 user-controlled CDB bytes at known offset) -> vtscsi_dequeue_request TAILQ_REMOVE performs *tqe_prev=tqe_next (shaped arbitrary write) -> kernel code execution in guest context. Device+root preconditions cap severity at Medium.
Evidence (decisive lines)
run.log (baseline: all-zero sense + resid 237/14 => device returned 18 bytes), run.patched.log (fix: 70 00 05 ... 21 00 real sense), env.txt (vtscsi0/da0/pass0, guest uname, camcontrol devlist), fix_build.log (nativekernel RC=0 + installkernel + uname #1), VERDICT.md, sense_test.c
PoC changes
PoC authored fresh (no seed): sense_test.c uses bus/cam headers (guest has no cam/ userland path) + libcam; required explicit bootindex=0 on the root virtio-blk device or SeaBIOS hangs when the scsi-hd is attached.
Verified recommended fix
Restore FreeBSD copy direction and clamp: memcpy(&csio->sense_data, cmd_resp->sense, MIN(csio->sense_len - csio->sense_resid, sizeof(csio->sense_data)));
Verdict
REPRODUCED on a live vtscsi0 (guest booted with a temporary -device virtio-scsi-pci launcher): every CHECK CONDITION returns CAM_AUTOSNS_VALID over an all-zero sense buffer (sense_resid proves the device delivered 18 real sense bytes; camcontrol prints 'Sense Error Code 0x0') because virtio_scsi.c:1393 copies FROM the just-bzero()ed csio->sense_data INTO cmd_resp->sense β dst/src swapped vs FreeBSD release/11.4.0. The same length math is device-influenced (len = min(device sense_len, ccb sense_len)); with an unvalidated pass(4) CAMIOCOMMAND sense_len up to 255 and a malicious backend announcing >=97, the 96-byte vsr_uresp.cmd.sense destination overflows ~159 bytes over vsr_callout/vsr_link and ~80 bytes past the contigmalloc'd struct vtscsi_request (TAILQ_REMOVE then yields a shaped write) β source-certain, but stock QEMU clamps device sense_len to 96, so the write half cannot fire on an unmodified QEMU; only the reversed-copy functional break was live (impact=none at runtime). Fix (restore direction + MIN(len, sizeof(sense_data))) validated: rebuilt kernel #1 delivers the real sense 70 00 05 ... 21 00 and camcontrol decodes 'ILLEGAL REQUEST asc:21,0 Logical block address out of range'.
No comments yet.