blob: 82aa0f50e624f33ed4cf4fd1d341b50778434cad [file] [log] [blame]
/*
* Copyright (c) 2023 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/kernel.h>
#include <zephyr/storage/disk_access.h>
#include <zephyr/sys/byteorder.h>
#include "usbd_msc_scsi.h"
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(usbd_msc, CONFIG_USBD_MSC_LOG_LEVEL);
#define INQUIRY_VERSION_SPC_2 0x04
#define INQUIRY_VERSION_SPC_3 0x05
#define INQUIRY_VERSION_SPC_4 0x06
#define INQUIRY_VERSION_SPC_5 0x07
/* Claim conformance to SPC-2 because this allows us to implement less commands
* and do not care about multiple reserved bits that became actual options
* later on. DO NOT change unless you make sure that all mandatory commands are
* implemented and all options (e.g. vpd pages) that are mandatory at given
* version are also implemented.
*/
#define CLAIMED_CONFORMANCE_VERSION INQUIRY_VERSION_SPC_2
#define T10_VENDOR_LENGTH 8
#define T10_PRODUCT_LENGTH 16
#define T10_REVISION_LENGTH 4
/* Optional, however Windows insists on reading Unit Serial Number.
* There doesn't seem to be requirement on minimum product serial number length,
* however when the number is not available the device shall return ASCII spaces
* in the field.
*/
#define UNIT_SERIAL_NUMBER " "
/* Every SCSI command has to abide to the general handling rules. Use macros
* to allow generating boilerplate handling code.
*/
#define SCSI_CMD_STRUCT(opcode) struct scsi_##opcode##_cmd
#define SCSI_CMD_HANDLER(opcode) \
static int scsi_##opcode(struct scsi_ctx *ctx, \
struct scsi_##opcode##_cmd *cmd, \
uint8_t data_in_buf[static CONFIG_USBD_MSC_SCSI_BUFFER_SIZE])
/* SAM-6 5.2 Command descriptor block (CDB)
* Table 43 – CONTROL byte
*/
#define GET_CONTROL_NACA(cmd) (cmd->control & BIT(2))
/* SPC-5 4.3.3 Variable type data field requirements
* Table 25 — Code set enumeration
*/
enum code_set {
CODE_SET_BINARY = 0x1,
CODE_SET_ASCII = 0x2,
CODE_SET_UTF8 = 0x3,
};
/* SPC-5 F.3.1 Operation codes Table F.2 — Operation codes */
enum scsi_opcode {
TEST_UNIT_READY = 0x00,
REQUEST_SENSE = 0x03,
INQUIRY = 0x12,
MODE_SENSE_6 = 0x1A,
START_STOP_UNIT = 0x1B,
PREVENT_ALLOW_MEDIUM_REMOVAL = 0x1E,
READ_FORMAT_CAPACITIES = 0x23,
READ_CAPACITY_10 = 0x25,
READ_10 = 0x28,
WRITE_10 = 0x2A,
MODE_SENSE_10 = 0x5A,
};
SCSI_CMD_STRUCT(TEST_UNIT_READY) {
uint8_t opcode;
uint32_t reserved;
uint8_t control;
} __packed;
/* DESC bit was reserved in SPC-2 and is optional since SPC-3 */
#define GET_REQUEST_SENSE_DESC(cmd) (cmd->desc & BIT(0))
SCSI_CMD_STRUCT(REQUEST_SENSE) {
uint8_t opcode;
uint8_t desc;
uint8_t reserved2;
uint8_t reserved3;
uint8_t allocation_length;
uint8_t control;
} __packed;
#define SENSE_VALID BIT(7)
#define SENSE_CODE_CURRENT_ERRORS 0x70
#define SENSE_CODE_DEFERRED_ERRORS 0x71
#define SENSE_FILEMARK BIT(7)
#define SENSE_EOM BIT(6)
#define SENSE_ILI BIT(5)
#define SENSE_KEY_MASK BIT_MASK(4)
#define SENSE_SKSV BIT(7)
struct scsi_request_sense_response {
uint8_t valid_code;
uint8_t obsolete;
uint8_t filemark_eom_ili_sense_key;
uint32_t information;
uint8_t additional_sense_length;
uint32_t command_specific_information;
uint16_t additional_sense_with_qualifier;
uint8_t field_replaceable_unit_code;
uint8_t sksv;
uint16_t sense_key_specific;
} __packed;
#define INQUIRY_EVPD BIT(0)
/* CMDDT in SPC-2, but obsolete since SPC-3 */
#define INQUIRY_CMDDT_OBSOLETE BIT(1)
enum vpd_page_code {
VPD_SUPPORTED_VPD_PAGES = 0x00,
VPD_UNIT_SERIAL_NUMBER = 0x80,
VPD_DEVICE_IDENTIFICATION = 0x83,
};
/* SPC-5 Table 517 — DESIGNATOR TYPE field */
enum designator_type {
DESIGNATOR_VENDOR = 0x0,
DESIGNATOR_T10_VENDOR_ID_BASED = 0x1,
DESIGNATOR_EUI_64_BASED = 0x2,
DESIGNATOR_NAA = 0x3,
DESIGNATOR_RELATIVE_TARGET_PORT_IDENTIFIER = 0x4,
DESIGNATOR_TARGET_PORT_GROUP = 0x5,
DESIGNATOR_MD5_LOGICAL_UNIT_IDENTIFIER = 0x6,
DESIGNATOR_SCSI_NAME_STRING = 0x8,
DESIGNATOR_PROTOCOL_SPECIFIC_PORT_IDENTIFIER = 0x9,
DESIGNATOR_UUID_IDENTIFIER = 0xA,
};
SCSI_CMD_STRUCT(INQUIRY) {
uint8_t opcode;
uint8_t cmddt_evpd;
uint8_t page_code;
/* Allocation length was 8-bit (LSB only) in SPC-2. MSB was reserved
* and hence SPC-2 compliant initiators should set it to 0.
*/
uint16_t allocation_length;
uint8_t control;
} __packed;
struct scsi_inquiry_response {
uint8_t peripheral;
uint8_t rmb;
uint8_t version;
uint8_t format;
uint8_t additional_length;
uint8_t sccs;
uint8_t encserv;
uint8_t cmdque;
char scsi_vendor[T10_VENDOR_LENGTH];
char product[T10_PRODUCT_LENGTH];
char revision[T10_REVISION_LENGTH];
/* SPC-5 states the standard INQUIRY should contain at least 36 bytes.
* We do the minimum required (and thus end the inquiry response here),
* as there is no real use in Zephyr for vendor specific data and/or
* parameters and we don't claim conformance to specific versions.
*/
} __packed;
#define MODE_SENSE_PAGE_CODE_ALL_PAGES 0x3F
SCSI_CMD_STRUCT(MODE_SENSE_6) {
uint8_t opcode;
uint8_t dbd;
uint8_t page;
uint8_t subpage;
uint8_t allocation_length;
uint8_t control;
} __packed;
/* SPC-5 7.5.6 Mode parameter header formats
* Table 443 — Mode parameter header(6)
*/
struct scsi_mode_sense_6_response {
uint8_t mode_data_length;
uint8_t medium_type;
uint8_t device_specific_parameter;
uint8_t block_descriptor_length;
} __packed;
#define GET_IMMED(cmd) (cmd->immed & BIT(0))
#define GET_POWER_CONDITION_MODIFIER(cmd) (cmd->condition & BIT_MASK(4))
#define GET_POWER_CONDITION(cmd) ((cmd->start & 0xF0) >> 4)
#define GET_NO_FLUSH(cmd) (cmd->start & BIT(2))
#define GET_LOEJ(cmd) (cmd->start & BIT(1))
#define GET_START(cmd) (cmd->start & BIT(0))
/* SBC-4 Table 114 — POWER CONDITION and POWER CONDITION MODIFIER field */
enum power_condition {
POWER_COND_START_VALID = 0x0,
POWER_COND_ACTIVE = 0x1,
POWER_COND_IDLE = 0x2,
POWER_COND_STANDBY = 0x3,
POWER_COND_LU_CONTROL = 0x7,
POWER_COND_FORCE_IDLE_0 = 0xA,
POWER_COND_FORCE_STANDBY_0 = 0xB,
};
SCSI_CMD_STRUCT(START_STOP_UNIT) {
uint8_t opcode;
uint8_t immed;
uint8_t reserved;
uint8_t condition;
uint8_t start;
uint8_t control;
} __packed;
#define GET_PREVENT(cmd) (cmd->prevent & BIT_MASK(2))
/* SBC-4 Table 77 — PREVENT field */
enum prevent_field {
MEDIUM_REMOVAL_ALLOWED = 0,
MEDIUM_REMOVAL_SHALL_BE_PREVENTED = 1,
PREVENT_OBSOLETE_2 = 2,
PREVENT_OBSOLETE_3 = 3,
};
SCSI_CMD_STRUCT(PREVENT_ALLOW_MEDIUM_REMOVAL) {
uint8_t opcode;
uint8_t reserved1;
uint8_t reserved2;
uint8_t reserved3;
uint8_t prevent;
uint8_t control;
} __packed;
SCSI_CMD_STRUCT(READ_FORMAT_CAPACITIES) {
uint8_t opcode;
uint8_t reserved1;
uint8_t reserved2;
uint8_t reserved3;
uint8_t reserved4;
uint8_t reserved5;
uint8_t reserved6;
uint16_t allocation_length;
uint8_t control;
} __packed;
struct capacity_list_header {
uint8_t reserved1;
uint8_t reserved2;
uint8_t reserved3;
uint8_t capacity_list_length;
} __packed;
enum descriptor_types {
UNFORMATTED_OR_BLANK_MEDIA = 1,
FORMATTED_MEDIA = 2,
NO_MEDIA_PRESENT_OR_UNKNOWN_CAPACITY = 3,
};
struct current_maximum_capacity_descriptor {
uint32_t number_of_blocks;
uint8_t type;
uint32_t block_length : 24;
} __packed;
struct scsi_read_format_capacities_response {
struct capacity_list_header header;
struct current_maximum_capacity_descriptor desc;
} __packed;
SCSI_CMD_STRUCT(READ_CAPACITY_10) {
uint8_t opcode;
uint8_t reserved_or_obsolete[8];
uint8_t control;
} __packed;
struct scsi_read_capacity_10_response {
uint32_t last_lba;
uint32_t block_length;
} __packed;
SCSI_CMD_STRUCT(READ_10) {
uint8_t opcode;
uint8_t rdprotect;
uint32_t lba;
uint8_t group_number;
uint16_t transfer_length;
uint8_t control;
} __packed;
SCSI_CMD_STRUCT(WRITE_10) {
uint8_t opcode;
uint8_t wrprotect;
uint32_t lba;
uint8_t group_number;
uint16_t transfer_length;
uint8_t control;
} __packed;
SCSI_CMD_STRUCT(MODE_SENSE_10) {
uint8_t opcode;
uint8_t llbaa_dbd;
uint8_t page;
uint8_t subpage;
uint8_t reserved4;
uint8_t reserved5;
uint8_t reserved6;
uint16_t allocation_length;
uint8_t control;
} __packed;
/* SPC-5 7.5.6 Mode parameter header formats
* Table 444 — Mode parameter header(10)
*/
struct scsi_mode_sense_10_response {
uint16_t mode_data_length;
uint8_t medium_type;
uint8_t device_specific_parameter;
uint8_t longlba;
uint8_t reserved5;
uint16_t block_descriptor_length;
} __packed;
static int update_disk_info(struct scsi_ctx *const ctx)
{
int status = disk_access_status(ctx->disk);
if (disk_access_ioctl(ctx->disk, DISK_IOCTL_GET_SECTOR_COUNT, &ctx->sector_count) != 0) {
ctx->sector_count = 0;
status = -EIO;
}
if (disk_access_ioctl(ctx->disk, DISK_IOCTL_GET_SECTOR_SIZE, &ctx->sector_size) != 0) {
ctx->sector_size = 0;
status = -EIO;
}
if (ctx->sector_size > CONFIG_USBD_MSC_SCSI_BUFFER_SIZE) {
status = -ENOMEM;
}
return status;
}
static size_t good(struct scsi_ctx *ctx, size_t data_in_bytes)
{
ctx->status = GOOD;
ctx->sense_key = NO_SENSE;
ctx->asc = NO_ADDITIONAL_SENSE_INFORMATION;
return data_in_bytes;
}
static size_t illegal_request(struct scsi_ctx *ctx, enum scsi_additional_sense_code asc)
{
ctx->status = CHECK_CONDITION;
ctx->sense_key = ILLEGAL_REQUEST;
ctx->asc = asc;
return 0;
}
static size_t not_ready(struct scsi_ctx *ctx, enum scsi_additional_sense_code asc)
{
ctx->status = CHECK_CONDITION;
ctx->sense_key = NOT_READY;
ctx->asc = asc;
return 0;
}
static size_t medium_error(struct scsi_ctx *ctx, enum scsi_additional_sense_code asc)
{
ctx->status = CHECK_CONDITION;
ctx->sense_key = MEDIUM_ERROR;
ctx->asc = asc;
return 0;
}
void scsi_init(struct scsi_ctx *ctx, const char *disk, const char *vendor,
const char *product, const char *revision)
{
memset(ctx, 0, sizeof(struct scsi_ctx));
ctx->disk = disk;
ctx->vendor = vendor;
ctx->product = product;
ctx->revision = revision;
scsi_reset(ctx);
}
void scsi_reset(struct scsi_ctx *ctx)
{
ctx->prevent_removal = false;
ctx->medium_loaded = true;
}
/* SPC-5 TEST UNIT READY command */
SCSI_CMD_HANDLER(TEST_UNIT_READY)
{
if (!ctx->medium_loaded || update_disk_info(ctx) != DISK_STATUS_OK) {
return not_ready(ctx, MEDIUM_NOT_PRESENT);
} else {
return good(ctx, 0);
}
}
/* SPC-5 REQUEST SENSE command */
SCSI_CMD_HANDLER(REQUEST_SENSE)
{
struct scsi_request_sense_response r;
int length;
ctx->cmd_is_data_read = true;
/* SPC-2 should ignore DESC (it was reserved)
* SPC-3 can ignore DESC if not supported
* SPC-4 and later shall error out if DESC is not supported
*/
if ((CLAIMED_CONFORMANCE_VERSION >= INQUIRY_VERSION_SPC_4) &&
(GET_REQUEST_SENSE_DESC(cmd))) {
return illegal_request(ctx, INVALID_FIELD_IN_CDB);
}
r.valid_code = SENSE_CODE_CURRENT_ERRORS;
r.obsolete = 0;
r.filemark_eom_ili_sense_key = ctx->sense_key & SENSE_KEY_MASK;
r.information = sys_cpu_to_be32(0);
r.additional_sense_length = sizeof(struct scsi_request_sense_response) - 1 -
offsetof(struct scsi_request_sense_response, additional_sense_length);
r.command_specific_information = sys_cpu_to_be32(0);
r.additional_sense_with_qualifier = sys_cpu_to_be16(ctx->asc);
r.field_replaceable_unit_code = 0;
r.sksv = 0;
r.sense_key_specific = sys_cpu_to_be16(0);
BUILD_ASSERT(sizeof(r) <= CONFIG_USBD_MSC_SCSI_BUFFER_SIZE);
length = MIN(cmd->allocation_length, sizeof(r));
memcpy(data_in_buf, &r, length);
/* REQUEST SENSE completed successfully, old sense information is
* cleared according to SPC-5.
*/
return good(ctx, length);
}
static int fill_inquiry(struct scsi_ctx *ctx,
uint8_t buf[static CONFIG_USBD_MSC_SCSI_BUFFER_SIZE])
{
/* For simplicity prepare whole response on stack and then copy
* requested length.
*/
struct scsi_inquiry_response r;
memset(&r, 0, sizeof(struct scsi_inquiry_response));
/* Accessible; Direct access block device (SBC) */
r.peripheral = 0x00;
/* Removable; not a part of conglomerate. Note that when device is
* accessible via USB Mass Storage, it should always be marked as
* removable to allow Safely Remove Hardware.
*/
r.rmb = 0x80;
r.version = CLAIMED_CONFORMANCE_VERSION;
/* ACA not supported; No SAM-5 LUNs; Complies to SPC */
r.format = 0x02;
r.additional_length = sizeof(struct scsi_inquiry_response) - 1 -
offsetof(struct scsi_inquiry_response, additional_length);
/* No embedded storage array controller available */
r.sccs = 0x00;
/* No embedded enclosure services */
r.encserv = 0x00;
/* Does not support SAM-5 command management model */
r.cmdque = 0x00;
strncpy(r.scsi_vendor, ctx->vendor, sizeof(r.scsi_vendor));
strncpy(r.product, ctx->product, sizeof(r.product));
strncpy(r.revision, ctx->revision, sizeof(r.revision));
BUILD_ASSERT(sizeof(r) <= CONFIG_USBD_MSC_SCSI_BUFFER_SIZE);
memcpy(buf, &r, sizeof(r));
return sizeof(r);
}
static int fill_vpd_page(struct scsi_ctx *ctx, enum vpd_page_code page,
uint8_t buf[static CONFIG_USBD_MSC_SCSI_BUFFER_SIZE])
{
uint16_t offset = 0;
uint8_t *page_start = &buf[4];
switch (page) {
case VPD_SUPPORTED_VPD_PAGES:
/* Page Codes must appear in ascending order */
page_start[offset++] = VPD_SUPPORTED_VPD_PAGES;
page_start[offset++] = VPD_UNIT_SERIAL_NUMBER;
page_start[offset++] = VPD_DEVICE_IDENTIFICATION;
break;
case VPD_DEVICE_IDENTIFICATION:
/* Absolute minimum is one vendor based descriptor formed by
* concatenating Vendor ID and Unit Serial Number
*
* Other descriptors (EUI-64 or NAA) should be there but should
* is equivalent to "it is strongly recommended" and adding them
* is pretty much problematic because these descriptors involve
* (additional) unique identifiers.
*/
page_start[offset++] = CODE_SET_ASCII;
page_start[offset++] = DESIGNATOR_T10_VENDOR_ID_BASED;
page_start[offset++] = 0x00;
page_start[offset++] = T10_VENDOR_LENGTH + sizeof(UNIT_SERIAL_NUMBER) - 1;
strncpy(&page_start[offset], ctx->vendor, T10_VENDOR_LENGTH);
offset += T10_VENDOR_LENGTH;
memcpy(&page_start[offset], UNIT_SERIAL_NUMBER, sizeof(UNIT_SERIAL_NUMBER) - 1);
offset += sizeof(UNIT_SERIAL_NUMBER) - 1;
break;
case VPD_UNIT_SERIAL_NUMBER:
memcpy(page_start, UNIT_SERIAL_NUMBER, sizeof(UNIT_SERIAL_NUMBER) - 1);
offset += sizeof(UNIT_SERIAL_NUMBER) - 1;
break;
default:
return -ENOTSUP;
}
/* Accessible; Direct access block device (SBC) */
buf[0] = 0x00;
buf[1] = page;
sys_put_be16(offset, &buf[2]);
return offset + 4;
}
/* SPC-5 6.7 INQUIRY command */
SCSI_CMD_HANDLER(INQUIRY)
{
int ret;
ctx->cmd_is_data_read = true;
if (cmd->cmddt_evpd & INQUIRY_CMDDT_OBSOLETE) {
/* Optional in SPC-2 and later obsoleted, do not support it */
ret = -EINVAL;
} else if (cmd->cmddt_evpd & INQUIRY_EVPD) {
/* Linux won't ask for VPD unless enabled with
* echo "Zephyr:Disk:0x10000000" > /proc/scsi/device_info
*/
ret = MIN(sys_be16_to_cpu(cmd->allocation_length),
fill_vpd_page(ctx, cmd->page_code, data_in_buf));
} else if (cmd->page_code != 0) {
LOG_WRN("Page Code is %d but EVPD set", cmd->page_code);
ret = -EINVAL;
} else {
/* Standard inquiry */
ret = MIN(sys_be16_to_cpu(cmd->allocation_length),
fill_inquiry(ctx, data_in_buf));
}
if (ret < 0) {
return illegal_request(ctx, INVALID_FIELD_IN_CDB);
}
return good(ctx, ret);
}
/* SPC-5 6.14 MODE SENSE(6) command */
SCSI_CMD_HANDLER(MODE_SENSE_6)
{
struct scsi_mode_sense_6_response r;
int length;
ctx->cmd_is_data_read = true;
if (cmd->page != MODE_SENSE_PAGE_CODE_ALL_PAGES || cmd->subpage != 0) {
return illegal_request(ctx, INVALID_FIELD_IN_CDB);
}
r.mode_data_length = 3;
r.medium_type = 0x00;
r.device_specific_parameter = 0x00;
r.block_descriptor_length = 0x00;
BUILD_ASSERT(sizeof(r) <= CONFIG_USBD_MSC_SCSI_BUFFER_SIZE);
length = MIN(cmd->allocation_length, sizeof(r));
memcpy(data_in_buf, &r, length);
return good(ctx, length);
}
/* SBC-4 5.31 START STOP UNIT command */
SCSI_CMD_HANDLER(START_STOP_UNIT)
{
bool medium_loaded = ctx->medium_loaded;
/* Safe Hardware Removal is essentially START STOP UNIT command that
* asks to eject the media. Disk is shown as safely removed when
* device (SCSI target) responds with NOT READY/MEDIUM NOT PRESENT to
* TEST UNIT READY command
*/
if (GET_POWER_CONDITION(cmd) == POWER_COND_START_VALID) {
if (GET_LOEJ(cmd)) {
if (GET_START(cmd)) {
medium_loaded = true;
} else {
medium_loaded = false;
}
}
}
if (!medium_loaded && ctx->medium_loaded && ctx->prevent_removal) {
return illegal_request(ctx, MEDIUM_REMOVAL_PREVENTED);
}
ctx->medium_loaded = medium_loaded;
return good(ctx, 0);
}
/* SBC-4 5.15 PREVENT ALLOW MEDIUM REMOVAL command */
SCSI_CMD_HANDLER(PREVENT_ALLOW_MEDIUM_REMOVAL)
{
switch (GET_PREVENT(cmd)) {
case MEDIUM_REMOVAL_ALLOWED:
ctx->prevent_removal = false;
break;
case MEDIUM_REMOVAL_SHALL_BE_PREVENTED:
ctx->prevent_removal = true;
break;
case PREVENT_OBSOLETE_2:
case PREVENT_OBSOLETE_3:
break;
}
return good(ctx, 0);
}
/* MMC-6 6.23 READ FORMAT CAPACITIES command
* Microsoft Windows issues this command for all USB drives (no idea why)
*/
SCSI_CMD_HANDLER(READ_FORMAT_CAPACITIES)
{
struct scsi_read_format_capacities_response r;
int length;
ctx->cmd_is_data_read = true;
memset(&r, 0, sizeof(r));
r.header.capacity_list_length = sizeof(r) - sizeof(r.header);
if (update_disk_info(ctx) < 0) {
r.desc.number_of_blocks = sys_cpu_to_be32(UINT32_MAX);
r.desc.type = NO_MEDIA_PRESENT_OR_UNKNOWN_CAPACITY;
} else {
r.desc.number_of_blocks = sys_cpu_to_be32(ctx->sector_count);
r.desc.type = FORMATTED_MEDIA;
}
r.desc.block_length = sys_cpu_to_be32(ctx->sector_size);
ctx->cmd_is_data_read = true;
BUILD_ASSERT(sizeof(r) <= CONFIG_USBD_MSC_SCSI_BUFFER_SIZE);
length = MIN(sys_be16_to_cpu(cmd->allocation_length), sizeof(r));
memcpy(data_in_buf, &r, length);
return good(ctx, length);
}
/* SBC-4 5.20 READ CAPACITY (10) command */
SCSI_CMD_HANDLER(READ_CAPACITY_10)
{
struct scsi_read_capacity_10_response r;
ctx->cmd_is_data_read = true;
if (!ctx->medium_loaded || update_disk_info(ctx) != DISK_STATUS_OK) {
return not_ready(ctx, MEDIUM_NOT_PRESENT);
}
r.last_lba = sys_cpu_to_be32(ctx->sector_count ? ctx->sector_count - 1 : 0);
r.block_length = sys_cpu_to_be32(ctx->sector_size);
ctx->cmd_is_data_read = true;
BUILD_ASSERT(sizeof(r) <= CONFIG_USBD_MSC_SCSI_BUFFER_SIZE);
memcpy(data_in_buf, &r, sizeof(r));
return good(ctx, sizeof(r));
}
static int
validate_transfer_length(struct scsi_ctx *ctx, uint32_t lba, uint16_t length)
{
uint32_t last_lba = lba + length - 1;
if (lba >= ctx->sector_count) {
LOG_WRN("LBA %d is out of range", lba);
return -EINVAL;
}
/* SBC-4 explicitly mentions that transfer length 0 is OK */
if (length == 0) {
return 0;
}
if ((last_lba >= ctx->sector_count) || (last_lba < lba)) {
LOG_WRN("%d blocks starting at %d go out of bounds", length, lba);
return -EINVAL;
}
return 0;
}
static size_t fill_read_10(struct scsi_ctx *ctx,
uint8_t buf[static CONFIG_USBD_MSC_SCSI_BUFFER_SIZE])
{
uint32_t sectors;
sectors = MIN(CONFIG_USBD_MSC_SCSI_BUFFER_SIZE, ctx->remaining_data) / ctx->sector_size;
if (disk_access_read(ctx->disk, buf, ctx->lba, sectors) != 0) {
/* Terminate transfer */
sectors = 0;
}
ctx->lba += sectors;
return sectors * ctx->sector_size;
}
SCSI_CMD_HANDLER(READ_10)
{
uint32_t lba = sys_be32_to_cpu(cmd->lba);
uint16_t transfer_length = sys_be16_to_cpu(cmd->transfer_length);
ctx->cmd_is_data_read = true;
if (!ctx->medium_loaded || update_disk_info(ctx) != DISK_STATUS_OK) {
return not_ready(ctx, MEDIUM_NOT_PRESENT);
}
if (validate_transfer_length(ctx, lba, transfer_length)) {
return illegal_request(ctx, LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE);
}
ctx->read_cb = fill_read_10;
ctx->lba = lba;
ctx->remaining_data = ctx->sector_size * transfer_length;
return good(ctx, 0);
}
static size_t store_write_10(struct scsi_ctx *ctx, const uint8_t *buf, size_t length)
{
uint32_t remaining_sectors;
uint32_t sectors;
bool error = false;
remaining_sectors = ctx->remaining_data / ctx->sector_size;
sectors = MIN(length, ctx->remaining_data) / ctx->sector_size;
if (disk_access_write(ctx->disk, buf, ctx->lba, sectors) != 0) {
/* Flush cache and terminate transfer */
sectors = 0;
remaining_sectors = 0;
error = true;
}
/* Flush cache if this is the last sector in transfer */
if (remaining_sectors - sectors == 0) {
if (disk_access_ioctl(ctx->disk, DISK_IOCTL_CTRL_SYNC, NULL)) {
LOG_ERR("Disk cache sync failed");
error = true;
}
}
ctx->lba += sectors;
if (error) {
return medium_error(ctx, WRITE_ERROR);
} else {
return sectors * ctx->sector_size;
}
}
SCSI_CMD_HANDLER(WRITE_10)
{
uint32_t lba = sys_be32_to_cpu(cmd->lba);
uint16_t transfer_length = sys_be16_to_cpu(cmd->transfer_length);
ctx->cmd_is_data_write = true;
if (!ctx->medium_loaded || update_disk_info(ctx) != DISK_STATUS_OK) {
return not_ready(ctx, MEDIUM_NOT_PRESENT);
}
if (validate_transfer_length(ctx, lba, transfer_length)) {
return illegal_request(ctx, LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE);
}
ctx->write_cb = store_write_10;
ctx->lba = lba;
ctx->remaining_data = ctx->sector_size * transfer_length;
return good(ctx, 0);
}
/* SPC-5 6.15 MODE SENSE(10) command */
SCSI_CMD_HANDLER(MODE_SENSE_10)
{
struct scsi_mode_sense_10_response r;
int length;
ctx->cmd_is_data_read = true;
if (cmd->page != MODE_SENSE_PAGE_CODE_ALL_PAGES || cmd->subpage != 0) {
return illegal_request(ctx, INVALID_FIELD_IN_CDB);
}
r.mode_data_length = sys_cpu_to_be16(6);
r.medium_type = 0x00;
r.device_specific_parameter = 0x00;
r.longlba = 0x00;
r.reserved5 = 0x00;
r.block_descriptor_length = sys_cpu_to_be16(0);
BUILD_ASSERT(sizeof(r) <= CONFIG_USBD_MSC_SCSI_BUFFER_SIZE);
length = MIN(sys_be16_to_cpu(cmd->allocation_length), sizeof(r));
memcpy(data_in_buf, &r, length);
return good(ctx, length);
}
int scsi_usb_boot_cmd_len(const uint8_t *cb, int len)
{
/* Universal Serial Bus Mass Storage Specification For Bootability
* requires device to accept CBW padded to 12 bytes for commands
* documented in Bootability specification. Windows 11 uses padding
* for REQUEST SENSE command.
*/
if (len != 12) {
return len;
}
switch (cb[0]) {
case TEST_UNIT_READY: return sizeof(SCSI_CMD_STRUCT(TEST_UNIT_READY));
case REQUEST_SENSE: return sizeof(SCSI_CMD_STRUCT(REQUEST_SENSE));
case INQUIRY: return sizeof(SCSI_CMD_STRUCT(INQUIRY));
case READ_CAPACITY_10: return sizeof(SCSI_CMD_STRUCT(READ_CAPACITY_10));
case READ_10: return sizeof(SCSI_CMD_STRUCT(READ_10));
case WRITE_10: return sizeof(SCSI_CMD_STRUCT(WRITE_10));
case MODE_SENSE_10: return sizeof(SCSI_CMD_STRUCT(MODE_SENSE_10));
default: return len;
}
}
size_t scsi_cmd(struct scsi_ctx *ctx, const uint8_t *cb, int len,
uint8_t data_in_buf[static CONFIG_USBD_MSC_SCSI_BUFFER_SIZE])
{
ctx->cmd_is_data_read = false;
ctx->cmd_is_data_write = false;
ctx->remaining_data = 0;
ctx->read_cb = NULL;
ctx->write_cb = NULL;
#define SCSI_CMD(opcode) do { \
if (len == sizeof(SCSI_CMD_STRUCT(opcode)) && cb[0] == opcode) { \
LOG_DBG("SCSI " #opcode); \
if (GET_CONTROL_NACA(((SCSI_CMD_STRUCT(opcode)*)cb))) { \
return illegal_request(ctx, INVALID_FIELD_IN_CDB); \
} \
return scsi_##opcode(ctx, (SCSI_CMD_STRUCT(opcode)*)cb, \
data_in_buf); \
} \
} while (0)
SCSI_CMD(TEST_UNIT_READY);
SCSI_CMD(REQUEST_SENSE);
SCSI_CMD(INQUIRY);
SCSI_CMD(MODE_SENSE_6);
SCSI_CMD(START_STOP_UNIT);
SCSI_CMD(PREVENT_ALLOW_MEDIUM_REMOVAL);
SCSI_CMD(READ_FORMAT_CAPACITIES);
SCSI_CMD(READ_CAPACITY_10);
SCSI_CMD(READ_10);
SCSI_CMD(WRITE_10);
SCSI_CMD(MODE_SENSE_10);
LOG_ERR("Unknown SCSI opcode 0x%02x", cb[0]);
return illegal_request(ctx, INVALID_FIELD_IN_CDB);
}
bool scsi_cmd_is_data_read(struct scsi_ctx *ctx)
{
return ctx->cmd_is_data_read;
}
bool scsi_cmd_is_data_write(struct scsi_ctx *ctx)
{
return ctx->cmd_is_data_write;
}
size_t scsi_cmd_remaining_data_len(struct scsi_ctx *ctx)
{
return ctx->remaining_data;
}
size_t scsi_read_data(struct scsi_ctx *ctx,
uint8_t buf[static CONFIG_USBD_MSC_SCSI_BUFFER_SIZE])
{
size_t retrieved = 0;
__ASSERT_NO_MSG(ctx->cmd_is_data_read);
if ((ctx->remaining_data > 0) && ctx->read_cb) {
retrieved = ctx->read_cb(ctx, buf);
}
ctx->remaining_data -= retrieved;
if (retrieved == 0) {
/* Terminate transfer. Host will notice data residue. */
ctx->remaining_data = 0;
}
return retrieved;
}
size_t scsi_write_data(struct scsi_ctx *ctx, const uint8_t *buf, size_t length)
{
size_t processed = 0;
__ASSERT_NO_MSG(ctx->cmd_is_data_write);
length = MIN(length, ctx->remaining_data);
if ((length > 0) && ctx->write_cb) {
processed = ctx->write_cb(ctx, buf, length);
}
ctx->remaining_data -= processed;
if (processed == 0) {
/* Terminate transfer. Host will notice data residue. */
ctx->remaining_data = 0;
}
return processed;
}
enum scsi_status_code scsi_cmd_get_status(struct scsi_ctx *ctx)
{
return ctx->status;
}