| /* |
| * 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; |
| } |