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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4053 Β· 11 files
FileTypeDescriptionSize
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:

  1. Functional (fires on every CHECK CONDITION, any device): autosense is never delivered. CAM gets CAM_AUTOSNS_VALID over 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.

  2. 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 destination cmd_resp->sense is uint8_t sense[96] inside req->vsr_uresp.cmd (struct vtscsi_request, virtio_scsi.h:59), ~12 bytes into the union. Kernel-originated CCBs always use sense_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) CAMIOCOMMAND merges the user-supplied union ccb wholesale (xpt_merge_ccb, scsi_pass.c:546 -> bcopy of everything after ccb_h) with no validation of sense_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, smashing req->vsr_callout (callout func/arg pointers) and req->vsr_link (TAILQ next/prev), and ~80 bytes past the end of the contigmallocd struct vtscsi_request (~268 bytes). Write content = 32 zero bytes then CCB fields (including the 16 fully user-controlled cdb_io.cdb_bytes). A corrupted vsr_link gives a controlled *tqe_prev = tqe_next write 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 inside vsr_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_resid proves the device returned exactly 18 sense bytes (255-237 and 32-14), but sense_data is all 0x00 (Sense Error Code 0x0 from 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).
VERDICT.md
↓ download raw

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)

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Batched 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)
↓ fix.diffper-fix-DF-4053

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'.