diff --git a/crates/scsi/src/lib.rs b/crates/scsi/src/lib.rs index b967efa..25dcad9 100644 --- a/crates/scsi/src/lib.rs +++ b/crates/scsi/src/lib.rs @@ -1 +1,3 @@ -pub mod scsi; +// We do not use any of this yet +#[allow(dead_code)] +mod scsi; diff --git a/crates/scsi/src/scsi/emulation/block_device.rs b/crates/scsi/src/scsi/emulation/block_device.rs new file mode 100644 index 0000000..66777e7 --- /dev/null +++ b/crates/scsi/src/scsi/emulation/block_device.rs @@ -0,0 +1,632 @@ +// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause + +use std::{ + convert::{TryFrom, TryInto}, + fs::File, + io::{self, Read, Write}, + num::{NonZeroU32, NonZeroU64, TryFromIntError}, + ops::{Add, Div, Mul, Sub}, + os::unix::prelude::*, +}; + +use log::{debug, error, warn}; + +use super::{ + command::{ + parse_opcode, CommandType, LunSpecificCommand, ModePageSelection, ModeSensePageControl, + ParseOpcodeResult, ReportSupportedOpCodesMode, SenseFormat, VpdPage, OPCODES, + }, + mode_page::ModePage, + response_data::{respond_standard_inquiry_data, SilentlyTruncate}, + target::{LogicalUnit, LunRequest}, +}; +use crate::scsi::{sense, CmdError, CmdOutput, TaskAttr}; + +pub(crate) enum MediumRotationRate { + Unreported, + NonRotating, +} + +#[derive(Clone, Copy, PartialEq, PartialOrd)] +pub(crate) struct ByteOffset(u64); +impl From for ByteOffset { + fn from(value: u64) -> Self { + ByteOffset(value) + } +} +impl From for u64 { + fn from(value: ByteOffset) -> Self { + value.0 + } +} +impl Div for ByteOffset { + type Output = BlockOffset; + + fn div(self, rhs: BlockSize) -> Self::Output { + BlockOffset(self.0 / NonZeroU64::from(rhs.0)) + } +} + +#[derive(Clone, Copy, PartialEq, PartialOrd)] +pub(crate) struct BlockSize(NonZeroU32); +impl From for u32 { + fn from(value: BlockSize) -> Self { + u32::from(value.0) + } +} +impl TryFrom for BlockSize { + type Error = TryFromIntError; + + fn try_from(value: u32) -> Result { + Ok(BlockSize(NonZeroU32::try_from(value)?)) + } +} + +#[derive(Clone, Copy, PartialEq, PartialOrd)] +pub(crate) struct BlockOffset(u64); +impl From for u64 { + fn from(value: BlockOffset) -> Self { + value.0 + } +} +impl From for BlockOffset { + fn from(value: u64) -> Self { + BlockOffset(value) + } +} +impl Add for BlockOffset { + type Output = BlockOffset; + + fn add(self, rhs: BlockOffset) -> Self::Output { + BlockOffset(self.0 + rhs.0) + } +} +impl Sub for BlockOffset { + type Output = Self; + + fn sub(self, rhs: BlockOffset) -> Self::Output { + BlockOffset(self.0 - rhs.0) + } +} +impl Mul for BlockOffset { + type Output = ByteOffset; + + fn mul(self, rhs: BlockSize) -> Self::Output { + ByteOffset(self.0 * u64::from(NonZeroU64::from(rhs.0))) + } +} + +pub(crate) trait BlockDeviceBackend: Send + Sync { + fn read_exact_at(&mut self, buf: &mut [u8], offset: ByteOffset) -> io::Result<()>; + fn size_in_blocks(&mut self) -> io::Result; + fn block_size(&self) -> BlockSize; + fn sync(&mut self) -> io::Result<()>; +} + +pub(crate) struct FileBackend { + file: File, + block_size: BlockSize, +} + +impl FileBackend { + pub fn new(file: File) -> Self { + Self { + file, + block_size: BlockSize::try_from(512).expect("512 is valid BlockSize"), + } + } +} + +impl BlockDeviceBackend for FileBackend { + fn read_exact_at(&mut self, buf: &mut [u8], offset: ByteOffset) -> io::Result<()> { + self.file.read_exact_at(buf, u64::from(offset)) + } + + fn size_in_blocks(&mut self) -> io::Result { + let len = ByteOffset::from(self.file.metadata()?.len()); + assert!(u64::from(len) % NonZeroU64::from(self.block_size.0) == 0); + Ok(len / self.block_size) + } + + fn block_size(&self) -> BlockSize { + self.block_size + } + + fn sync(&mut self) -> io::Result<()> { + self.file.sync_data() + } +} + +pub(crate) struct BlockDevice { + backend: T, + write_protected: bool, + rotation_rate: MediumRotationRate, +} + +impl BlockDevice { + pub(crate) const fn new(backend: T) -> Self { + Self { + backend, + write_protected: false, + rotation_rate: MediumRotationRate::Unreported, + } + } + + fn read_blocks(&mut self, lba: BlockOffset, blocks: BlockOffset) -> io::Result> { + // TODO: Ideally, this would be a read_vectored directly into guest + // address space. Instead, we have an allocation and several copies. + + let mut ret = vec![ + 0; + usize::try_from(u64::from(blocks * self.backend.block_size())) + .expect("block length in bytes should fit usize") + ]; + + self.backend + .read_exact_at(&mut ret[..], lba * self.backend.block_size())?; + + Ok(ret) + } + + pub fn set_write_protected(&mut self, wp: bool) { + self.write_protected = wp; + } + + pub fn set_solid_state(&mut self, rotation_rate: MediumRotationRate) { + self.rotation_rate = rotation_rate; + } +} + +impl LogicalUnit for BlockDevice { + fn execute_command( + &mut self, + data_in: &mut SilentlyTruncate<&mut dyn Write>, + _data_out: &mut dyn Read, + req: LunRequest, + command: LunSpecificCommand, + ) -> Result { + if req.crn != 0 { + // CRN is a weird bit of the protocol we wouldn't ever expect to be used over + // virtio-scsi; but it's allowed to set it non-zero + warn!("Received non-zero CRN: {}", req.crn); + } + + if req.task_attr != TaskAttr::Simple { + // virtio-scsi spec allows us to treat all task attrs as SIMPLE. + warn!("Ignoring non-simple task attr of {:?}", req.task_attr); + } + + if req.prio != 0 { + // My reading of SAM-6 is that priority is purely advisory, so it's fine to + // ignore it. + warn!("Ignoring non-zero priority of {}.", req.prio); + } + + if req.naca { + // We don't support NACA, and say as much in our INQUIRY data, so if + // we get it that's an error. + warn!("Driver set NACA bit, which is unsupported."); + return Ok(CmdOutput::check_condition(sense::INVALID_FIELD_IN_CDB)); + } + + debug!("Incoming command: {:?}", command); + + match command { + LunSpecificCommand::TestUnitReady => Ok(CmdOutput::ok()), + LunSpecificCommand::ReadCapacity10 => { + match self.backend.size_in_blocks() { + Ok(size) => { + // READ CAPACITY (10) returns a 32-bit LBA, which may not be enough. If it + // isn't, we're supposed to return 0xffff_ffff and hope the driver gets the + // memo and uses the newer READ CAPACITY (16). + + // n.b. this is the last block, ie (length-1), not length + let final_block: u32 = u64::from(size - BlockOffset(1)) + .try_into() + .unwrap_or(0xffff_ffff); + let block_size: u32 = u32::from(self.backend.block_size()); + + data_in + .write_all(&u32::to_be_bytes(final_block)) + .map_err(CmdError::DataIn)?; + data_in + .write_all(&u32::to_be_bytes(block_size)) + .map_err(CmdError::DataIn)?; + + Ok(CmdOutput::ok()) + } + Err(e) => { + error!("Error getting image size: {}", e); + // TODO: Is this a reasonable sense code to send? + Ok(CmdOutput::check_condition(sense::UNRECOVERED_READ_ERROR)) + } + } + } + LunSpecificCommand::ReadCapacity16 => { + match self.backend.size_in_blocks() { + Ok(size) => { + // n.b. this is the last block, ie (length-1), not length + let final_block = u64::from(size - BlockOffset(1)); + let block_size = u32::from(self.backend.block_size()); + + data_in + .write_all(&u64::to_be_bytes(final_block)) + .map_err(CmdError::DataIn)?; + data_in + .write_all(&u32::to_be_bytes(block_size)) + .map_err(CmdError::DataIn)?; + + // no protection stuff; 1-to-1 logical/physical blocks + data_in.write_all(&[0, 0]).map_err(CmdError::DataIn)?; + + // top 2 bits: thin provisioning stuff; other 14 bits are lowest + // aligned LBA, which is zero + data_in + .write_all(&[0b1100_0000, 0]) + .map_err(CmdError::DataIn)?; + + // reserved + data_in.write_all(&[0; 16]).map_err(CmdError::DataIn)?; + + Ok(CmdOutput::ok()) + } + Err(e) => { + error!("Error getting image size: {}", e); + // TODO: Is this a reasonable sense code to send? + Ok(CmdOutput::check_condition(sense::UNRECOVERED_READ_ERROR)) + } + } + } + LunSpecificCommand::ModeSense6 { mode_page, pc, dbd } => { + // we use this for the pages array if we only need a single element; lifetime + // rules mean it has to be declared here + let single_page_array: [ModePage; 1]; + + let pages = match mode_page { + ModePageSelection::Single(x) => { + single_page_array = [x]; + &single_page_array + } + ModePageSelection::AllPageZeros => ModePage::ALL_ZERO, + }; + + let pages_len: u32 = pages.iter().map(|x| u32::from(x.page_length() + 2)).sum(); + // SPC-6r05, 7.5.6: "Logical units that support more than 256 bytes of block + // descriptors and mode pages should implement ten-byte mode commands. The MODE + // DATA LENGTH field in the six-byte CDB header limits the transferred data to + // 256 bytes." + // Unclear what exactly we're supposed to do if we have more than 256 bytes of + // mode pages and get sent a MODE SENSE (6). In any case, we don't at the + // moment; if we ever get that much, this unwrap() will start + // crashing us and we can figure out what to do. + let pages_len = u8::try_from(pages_len).unwrap(); + + // mode parameter header + data_in + .write_all(&[ + pages_len + 3, // size in bytes after this one + 0, // medium type - 0 for SBC + if self.write_protected { + 0b1001_0000 // WP, support DPOFUA + } else { + 0b0001_0000 // support DPOFUA + }, + 0, // block desc length + ]) + .map_err(CmdError::DataIn)?; + + if !dbd { + // TODO: Block descriptors are optional, so we currently + // don't provide them. Does any driver + // actually use them? + } + + for page in pages { + match pc { + ModeSensePageControl::Current | ModeSensePageControl::Default => { + page.write(data_in).map_err(CmdError::DataIn)?; + } + ModeSensePageControl::Changeable => { + // SPC-6 6.14.3: "If the logical unit does not + // implement changeable parameters mode pages and + // the device server receives a MODE SENSE command + // with 01b in the PC field, then the device server + // shall terminate the command with CHECK CONDITION + // status, with the sense key set to ILLEGAL + // REQUEST, and the additional sense code set to + // INVALID FIELD IN CDB." + return Ok(CmdOutput::check_condition(sense::INVALID_FIELD_IN_CDB)); + } + ModeSensePageControl::Saved => { + return Ok(CmdOutput::check_condition( + sense::SAVING_PARAMETERS_NOT_SUPPORTED, + )) + } + } + } + + Ok(CmdOutput::ok()) + } + LunSpecificCommand::Read10 { + dpo, + fua, + lba, + transfer_length, + } => { + if dpo { + // DPO is just a hint that the guest probably won't access + // this any time soon, so we can ignore it + debug!("Silently ignoring DPO flag"); + } + + if fua { + // Somewhat weirdly, SCSI supports FUA on reads. Here's the + // key bit: "A force unit access (FUA) bit set to one + // specifies that the device server shall read the logical + // blocks from… the medium. If the FUA bit is set to one + // and a volatile cache contains a more recent version of a + // logical block than… the medium, then, before reading the + // logical block, the device server shall write the logical + // block to… the medium." + + // I guess the idea is that you can read something back, and + // be absolutely sure what you just read will persist. + + // So for our purposes, we need to make sure whatever we + // return has been saved to disk. fsync()ing the whole image + // is a bit blunt, but does the trick. + + if let Err(e) = self.backend.sync() { + error!("Error syncing file: {}", e); + return Ok(CmdOutput::check_condition(sense::TARGET_FAILURE)); + } + } + + // Ignore group number: AFAICT, it's for separating reads from different + // workloads in performance metrics, and we don't report anything like that + + let size = match self.backend.size_in_blocks() { + Ok(size) => size, + Err(e) => { + error!("Error getting image size for read: {}", e); + return Ok(CmdOutput::check_condition(sense::UNRECOVERED_READ_ERROR)); + } + }; + + let lba = BlockOffset(lba.into()); + let transfer_length = BlockOffset(transfer_length.into()); + + if lba + transfer_length > size { + return Ok(CmdOutput::check_condition( + sense::LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE, + )); + } + + let read_result = self.read_blocks(lba, transfer_length); + + match read_result { + Ok(bytes) => { + data_in.write_all(&bytes[..]).map_err(CmdError::DataIn)?; + Ok(CmdOutput::ok()) + } + Err(e) => { + error!("Error reading image: {}", e); + Ok(CmdOutput::check_condition(sense::UNRECOVERED_READ_ERROR)) + } + } + } + LunSpecificCommand::Inquiry(page_code) => { + // top 3 bits 0: peripheral device code = exists and ready + // bottom 5 bits 0: device type = block device + data_in.write_all(&[0]).map_err(CmdError::DataIn)?; + + if let Some(code) = page_code { + let mut out = vec![]; + match code { + VpdPage::SupportedVpdPages => { + out.push(VpdPage::SupportedVpdPages.into()); + out.push(VpdPage::BlockDeviceCharacteristics.into()); + out.push(VpdPage::LogicalBlockProvisioning.into()); + } + VpdPage::BlockDeviceCharacteristics => { + let rotation_rate: u16 = match self.rotation_rate { + MediumRotationRate::Unreported => 0, + MediumRotationRate::NonRotating => 1, + }; + out.extend_from_slice(&rotation_rate.to_be_bytes()); + // nothing worth setting in the rest + out.extend_from_slice(&[0; 58]); + } + VpdPage::LogicalBlockProvisioning => { + out.push(0); // don't support threshold sets + out.push(0b1110_0100); // support unmapping w/ UNMAP + // and WRITE SAME (10 & 16), + // don't support anchored + // LBAs or group descriptors + out.push(0b0000_0010); // thin provisioned + out.push(0); // no threshold % support + } + _ => return Ok(CmdOutput::check_condition(sense::INVALID_FIELD_IN_CDB)), + } + + data_in + .write_all(&[code.into()]) + .map_err(CmdError::DataIn)?; + data_in + .write_all( + &u16::try_from(out.len()) + .expect("VPD page < 2^16 bits") + .to_be_bytes(), + ) + .map_err(CmdError::DataIn)?; + data_in.write_all(&out).map_err(CmdError::DataIn)?; + } else { + respond_standard_inquiry_data(data_in).map_err(CmdError::DataIn)?; + } + + Ok(CmdOutput::ok()) + } + LunSpecificCommand::ReportSupportedOperationCodes { rctd, mode } => { + // helpers for output data format + fn one_command_supported( + data_in: &mut impl Write, + ty: CommandType, + ) -> io::Result<()> { + data_in.write_all(&[0])?; // unused flags + data_in.write_all(&[0b0000_0011])?; // supported, don't set a bunch of flags + let tpl = ty.cdb_template(); + data_in.write_all( + &u16::try_from(tpl.len()) + .expect("length of TPL to be same as CDB") + .to_be_bytes(), + )?; + data_in.write_all(tpl)?; + Ok(()) + } + + fn one_command_not_supported(data_in: &mut impl Write) -> io::Result<()> { + data_in.write_all(&[0])?; // unused flags + data_in.write_all(&[0b0000_0001])?; // not supported + data_in.write_all(&[0; 2])?; // cdb len + Ok(()) + } + + fn timeout_descriptor(data_in: &mut impl Write) -> io::Result<()> { + // timeout descriptor + data_in.write_all(&0xa_u16.to_be_bytes())?; // len + data_in.write_all(&[0, 0])?; // reserved, cmd specific + data_in.write_all(&0_u32.to_be_bytes())?; + data_in.write_all(&0_u32.to_be_bytes())?; + Ok(()) + } + + match mode { + ReportSupportedOpCodesMode::All => { + let cmd_len = if rctd { 20 } else { 8 }; + let len = u32::try_from(OPCODES.len() * cmd_len) + .expect("less than (2^32 / 20) ~= 2^27 opcodes"); + data_in + .write_all(&len.to_be_bytes()) + .map_err(CmdError::DataIn)?; + + for &(ty, (opcode, sa)) in OPCODES { + data_in.write_all(&[opcode]).map_err(CmdError::DataIn)?; + data_in.write_all(&[0]).map_err(CmdError::DataIn)?; // reserved + data_in + .write_all(&sa.unwrap_or(0).to_be_bytes()) + .map_err(CmdError::DataIn)?; + data_in.write_all(&[0]).map_err(CmdError::DataIn)?; // reserved + + let ctdp: u8 = if rctd { 0b10 } else { 0b00 }; + let servactv = u8::from(sa.is_some()); + data_in + .write_all(&[ctdp | servactv]) + .map_err(CmdError::DataIn)?; + + data_in + .write_all( + &u16::try_from(ty.cdb_template().len()) + .expect("length of TPL to be same as CDB") + .to_be_bytes(), + ) + .map_err(CmdError::DataIn)?; + + if rctd { + timeout_descriptor(data_in).map_err(CmdError::DataIn)?; + } + } + } + ReportSupportedOpCodesMode::OneCommand(opcode) => match parse_opcode(opcode) { + ParseOpcodeResult::Command(ty) => { + one_command_supported(data_in, ty).map_err(CmdError::DataIn)?; + + if rctd { + timeout_descriptor(data_in).map_err(CmdError::DataIn)?; + } + } + ParseOpcodeResult::ServiceAction(_) => { + return Ok(CmdOutput::check_condition(sense::INVALID_FIELD_IN_CDB)); + } + ParseOpcodeResult::Invalid => { + warn!("Reporting that we don't support command {:#2x}. It might be worth adding.", opcode); + one_command_not_supported(data_in).map_err(CmdError::DataIn)?; + } + }, + ReportSupportedOpCodesMode::OneServiceAction(opcode, sa) => { + match parse_opcode(opcode) { + ParseOpcodeResult::Command(_) => { + return Ok(CmdOutput::check_condition(sense::INVALID_FIELD_IN_CDB)) + } + ParseOpcodeResult::ServiceAction(unparsed_sa) => { + if let Some(ty) = unparsed_sa.parse(sa) { + one_command_supported(data_in, ty).map_err(CmdError::DataIn)?; + + if rctd { + timeout_descriptor(data_in).map_err(CmdError::DataIn)?; + } + } else { + warn!("Reporting that we don't support command {:#2x}/{:#2x}. It might be worth adding.", opcode, sa); + one_command_not_supported(data_in).map_err(CmdError::DataIn)?; + } + } + ParseOpcodeResult::Invalid => { + // the spec isn't super clear what we're supposed to do here, but I + // think an invalid opcode is one for which our implementation + // "does not implement service actions", so we say invalid field in + // CDB + warn!("Reporting that we don't support command {:#2x}/{:#2x}. It might be worth adding.", opcode, sa); + return Ok(CmdOutput::check_condition(sense::INVALID_FIELD_IN_CDB)); + } + } + } + ReportSupportedOpCodesMode::OneCommandOrServiceAction(opcode, sa) => { + match parse_opcode(opcode) { + ParseOpcodeResult::Command(ty) => { + if sa == 0 { + one_command_supported(data_in, ty).map_err(CmdError::DataIn)?; + + if rctd { + timeout_descriptor(data_in).map_err(CmdError::DataIn)?; + } + } else { + one_command_not_supported(data_in).map_err(CmdError::DataIn)?; + } + } + ParseOpcodeResult::ServiceAction(unparsed_sa) => { + if let Some(ty) = unparsed_sa.parse(sa) { + one_command_supported(data_in, ty).map_err(CmdError::DataIn)?; + + if rctd { + timeout_descriptor(data_in).map_err(CmdError::DataIn)?; + } + } else { + warn!("Reporting that we don't support command {:#2x}/{:#2x}. It might be worth adding.", opcode, sa); + one_command_not_supported(data_in).map_err(CmdError::DataIn)?; + } + } + ParseOpcodeResult::Invalid => { + warn!("Reporting that we don't support command {:#2x}[/{:#2x}]. It might be worth adding.", opcode, sa); + one_command_not_supported(data_in).map_err(CmdError::DataIn)?; + } + } + } + } + Ok(CmdOutput::ok()) + } + LunSpecificCommand::RequestSense(format) => { + match format { + SenseFormat::Fixed => { + data_in + .write_all(&sense::NO_ADDITIONAL_SENSE_INFORMATION.to_fixed_sense()) + .map_err(CmdError::DataIn)?; + Ok(CmdOutput::ok()) + } + SenseFormat::Descriptor => { + // Don't support desciptor format. + Ok(CmdOutput::check_condition(sense::INVALID_FIELD_IN_CDB)) + } + } + } + } + } +} diff --git a/crates/scsi/src/scsi/emulation/command.rs b/crates/scsi/src/scsi/emulation/command.rs new file mode 100644 index 0000000..5572174 --- /dev/null +++ b/crates/scsi/src/scsi/emulation/command.rs @@ -0,0 +1,577 @@ +// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause + +//! Data structures and parsing code for SCSI commands. A rough overview: +//! We need to deal with opcodes in two places: in parsing commands themselves, +//! and in implementing REPORT SUPPORTED OPERATION CODES. Therefore, we parse +//! commands in two steps. First, we parse the opcode (and sometimes service +//! action) into a `CommandType` (a C-style enum containing just the commands, +//! not their parameters), then using that, we parse the rest of the CDB and +//! obtain a `Cdb`, which consists of a `Command`, an enum representing a +//! command and its parameters, along with some fields shared across many or all +//! commands. + +use std::convert::{TryFrom, TryInto}; + +use log::warn; +use num_enum::TryFromPrimitive; + +use crate::scsi::emulation::mode_page::ModePage; + +/// One of the modes supported by SCSI's REPORT LUNS command. +#[derive(PartialEq, Eq, TryFromPrimitive, Debug, Copy, Clone)] +#[repr(u8)] +pub(crate) enum ReportLunsSelectReport { + NoWellKnown = 0x0, + WellKnownOnly = 0x1, + All = 0x2, + Administrative = 0x10, + TopLevel = 0x11, + SameConglomerate = 0x12, +} + +/// A type of "vital product data" page returned by SCSI's INQUIRY command. +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +pub(crate) enum VpdPage { + Ascii(u8), + Ata, // * + BlockDeviceCharacteristics, // * + BlockDeviceCharacteristicsExt, + BlockLimits, // * + BlockLimitsExt, + CfaProfile, + DeviceConstituents, + DeviceIdentification, // * + ExtendedInquiry, + FormatPresets, + LogicalBlockProvisioning, // * + ManagementNetworkAddresses, + ModePagePolicy, + PowerCondition, + PowerConsumption, + PortocolSpecificLogicalUnit, + ProtocolSpecificPort, + Referrals, + ScsiFeatureSets, + ScsiPorts, + SoftwareInterfaceIdentification, + SupportedVpdPages, // * + ThirdPartyCopy, + UnitSerialNumber, // * + ZonedBlockDeviceCharacteristics, // * +} +// starred ones are ones Linux will use if available + +#[derive(PartialEq, Eq, TryFromPrimitive, Debug, Copy, Clone)] +#[repr(u8)] +pub(crate) enum ModeSensePageControl { + Current = 0b00, + Changeable = 0b01, + Default = 0b10, + Saved = 0b11, +} + +impl TryFrom for VpdPage { + type Error = (); + + fn try_from(val: u8) -> Result { + match val { + 0x00 => Ok(Self::SupportedVpdPages), + 0x1..=0x7f => Ok(Self::Ascii(val)), + 0x80 => Ok(Self::UnitSerialNumber), + 0x83 => Ok(Self::DeviceIdentification), + 0x84 => Ok(Self::SoftwareInterfaceIdentification), + 0x85 => Ok(Self::ManagementNetworkAddresses), + 0x86 => Ok(Self::ExtendedInquiry), + 0x87 => Ok(Self::ModePagePolicy), + 0x88 => Ok(Self::ScsiPorts), + 0x89 => Ok(Self::Ata), + 0x8a => Ok(Self::PowerCondition), + 0x8b => Ok(Self::DeviceConstituents), + 0x8c => Ok(Self::CfaProfile), + 0x8d => Ok(Self::PowerConsumption), + 0x8f => Ok(Self::ThirdPartyCopy), + 0x90 => Ok(Self::PortocolSpecificLogicalUnit), + 0x91 => Ok(Self::ProtocolSpecificPort), + 0x92 => Ok(Self::ScsiFeatureSets), + 0xb0 => Ok(Self::BlockLimits), + 0xb1 => Ok(Self::BlockDeviceCharacteristics), + 0xb2 => Ok(Self::LogicalBlockProvisioning), + 0xb3 => Ok(Self::Referrals), + 0xb5 => Ok(Self::BlockDeviceCharacteristicsExt), + 0xb6 => Ok(Self::ZonedBlockDeviceCharacteristics), + 0xb7 => Ok(Self::BlockLimitsExt), + 0xb8 => Ok(Self::FormatPresets), + _ => Err(()), + } + } +} + +impl From for u8 { + fn from(pc: VpdPage) -> Self { + match pc { + VpdPage::Ascii(val) => val, + VpdPage::Ata => 0x89, + VpdPage::BlockDeviceCharacteristics => 0xb1, + VpdPage::BlockDeviceCharacteristicsExt => 0xb5, + VpdPage::BlockLimits => 0xb0, + VpdPage::BlockLimitsExt => 0xb7, + VpdPage::CfaProfile => 0x8c, + VpdPage::DeviceConstituents => 0x8b, + VpdPage::DeviceIdentification => 0x83, + VpdPage::ExtendedInquiry => 0x86, + VpdPage::FormatPresets => 0xb8, + VpdPage::LogicalBlockProvisioning => 0xb2, + VpdPage::ManagementNetworkAddresses => 0x85, + VpdPage::ModePagePolicy => 0x87, + VpdPage::PowerCondition => 0x8a, + VpdPage::PowerConsumption => 0x8d, + VpdPage::PortocolSpecificLogicalUnit => 0x90, + VpdPage::ProtocolSpecificPort => 0x91, + VpdPage::Referrals => 0xb3, + VpdPage::ScsiFeatureSets => 0x92, + VpdPage::ScsiPorts => 0x88, + VpdPage::SoftwareInterfaceIdentification => 0x84, + VpdPage::SupportedVpdPages => 0x00, + VpdPage::ThirdPartyCopy => 0x8f, + VpdPage::UnitSerialNumber => 0x80, + VpdPage::ZonedBlockDeviceCharacteristics => 0xb6, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub(crate) enum SenseFormat { + Fixed, + Descriptor, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub(crate) enum ModePageSelection { + AllPageZeros, + Single(ModePage), +} + +#[derive(Debug)] +pub(crate) enum LunIndependentCommand { + ReportLuns(ReportLunsSelectReport), +} + +#[derive(Debug)] +pub(crate) enum LunSpecificCommand { + Inquiry(Option), + ModeSense6 { + pc: ModeSensePageControl, + mode_page: ModePageSelection, + /// Disable block descriptors + dbd: bool, + }, + Read10 { + /// Disable page out (i.e. hint that this page won't be accessed again + /// soon, so we shouldn't bother caching it) + dpo: bool, + /// Force unit access (i.e. bypass cache) + fua: bool, + lba: u32, + transfer_length: u16, + }, + ReadCapacity10, + ReadCapacity16, + ReportSupportedOperationCodes { + /// SCSI RCTD bit: whether we should include timeout descriptors. + rctd: bool, + mode: ReportSupportedOpCodesMode, + }, + RequestSense(SenseFormat), + TestUnitReady, +} + +#[derive(Debug)] +pub(crate) enum Command { + LunIndependentCommand(LunIndependentCommand), + LunSpecificCommand(LunSpecificCommand), +} + +#[derive(Clone, Copy, Debug)] +pub(crate) enum CommandType { + Inquiry, + ModeSense6, + Read10, + ReadCapacity10, + ReadCapacity16, + ReportLuns, + ReportSupportedOperationCodes, + RequestSense, + TestUnitReady, +} + +pub(crate) const OPCODES: &[(CommandType, (u8, Option))] = &[ + (CommandType::TestUnitReady, (0x0, None)), + (CommandType::RequestSense, (0x3, None)), + (CommandType::Inquiry, (0x12, None)), + (CommandType::ModeSense6, (0x1a, None)), + (CommandType::ReadCapacity10, (0x25, None)), + (CommandType::Read10, (0x28, None)), + (CommandType::ReadCapacity16, (0x9e, Some(0x10))), + (CommandType::ReportLuns, (0xa0, None)), + ( + CommandType::ReportSupportedOperationCodes, + (0xa3, Some(0xc)), + ), +]; + +#[derive(Debug, Clone, Copy)] +pub(crate) struct UnparsedServiceAction(u8); +impl UnparsedServiceAction { + pub fn parse(self, service_action: u16) -> Option { + OPCODES + .iter() + .find(|(_, opcode)| *opcode == (self.0, Some(service_action))) + .map(|&(ty, _)| ty) + } +} + +/// See `parse_opcode` +#[derive(Debug, Clone, Copy)] +pub(crate) enum ParseOpcodeResult { + /// The opcode represents a single command. + Command(CommandType), + /// The opcode requires a service action. + ServiceAction(UnparsedServiceAction), + /// The opcode is invalid. + Invalid, +} + +/// Determine the command that corresponds to a SCSI opcode. +/// +/// This is a little weird. Most SCSI commands are just identified by the +/// opcode - the first byte of the CDB - but some opcodes require a second +/// byte, called the service action. Generally, each distinct service action +/// value is treated as a first-class command. But there's some weirdness +/// around parsing, especially with invalid commands: sometimes, we're +/// expected to behave differently for a valid opcode with an invalid +/// service action vs an invalid opcode. +/// +/// To allow for this, we have a two-step parsing API. First, a caller +/// calls `parse_opcode` with the first byte of the CDB. This could return +/// three things: +/// - `Command`: the opcode corresponded to a single-byte command; we're done. +/// - `Invalid`: the opcode isn't recognized at all; we're done. +/// - `ServiceAction`: the opcode is the first byte of a service action; the +/// caller needs to call .parse() on the `UnparsedServiceAction` we returned +/// with the service action byte. +pub(crate) fn parse_opcode(opcode: u8) -> ParseOpcodeResult { + let found = OPCODES.iter().find(|(_, (x, _))| *x == opcode); + match found { + Some(&(ty, (_, None))) => ParseOpcodeResult::Command(ty), + Some((_, (_, Some(_)))) => { + // we found some service action that uses this opcode; so this is a + // service action opcode, and we need the service action + ParseOpcodeResult::ServiceAction(UnparsedServiceAction(opcode)) + } + None => ParseOpcodeResult::Invalid, + } +} + +impl CommandType { + fn from_cdb(cdb: &[u8]) -> Result { + // TODO: Variable-length CDBs put the service action in a different + // place. This'll need to change if we ever support those. IIRC, Linux + // doesn't ever use them, so it may never be relevant. + match parse_opcode(cdb[0]) { + ParseOpcodeResult::Command(ty) => Ok(ty), + ParseOpcodeResult::ServiceAction(sa) => sa + .parse(u16::from(cdb[1] & 0b0001_1111)) + .ok_or(ParseError::InvalidField), + ParseOpcodeResult::Invalid => Err(ParseError::InvalidCommand), + } + } + + /// Return the SCSI "CDB usage data" (see SPC-6 6.34.3) for this command + /// type. + /// + /// Basically, this consists of a structure the size of the CDB for the + /// command, starting with the opcode and service action (if any), then + /// proceeding to a bitmap of fields we recognize. + pub const fn cdb_template(self) -> &'static [u8] { + match self { + Self::TestUnitReady => &[ + 0x0, + 0b0000_0000, + 0b0000_0000, + 0b0000_0000, + 0b0000_0000, + 0b0000_0100, + ], + Self::RequestSense => &[ + 0x3, + 0b0000_0001, + 0b0000_0000, + 0b0000_0000, + 0b1111_1111, + 0b0000_0100, + ], + Self::ReportLuns => &[ + 0xa0, + 0b0000_0000, + 0b1111_1111, + 0b0000_0000, + 0b0000_0000, + 0b0000_0000, + 0b1111_1111, + 0b1111_1111, + 0b1111_1111, + 0b1111_1111, + 0b0000_0000, + 0b0000_0100, + ], + Self::ReadCapacity10 => &[ + 0x25, + 0b0000_0000, + 0b0000_0000, + 0b0000_0000, + 0b0000_0000, + 0b0000_0000, + 0b0000_0000, + 0b0000_0000, + 0b0000_0000, + 0b0000_0100, + ], + Self::ReadCapacity16 => &[ + 0x9e, + 0x10, + 0b0000_0000, + 0b0000_0000, + 0b0000_0000, + 0b0000_0000, + 0b0000_0000, + 0b0000_0000, + 0b0000_0000, + 0b0000_0000, + 0b1111_1111, + 0b1111_1111, + 0b1111_1111, + 0b1111_1111, + 0b0000_0000, + 0b0000_0100, + ], + Self::ModeSense6 => &[ + 0x1a, + 0b0000_1000, + 0b1111_1111, + 0b1111_1111, + 0b1111_1111, + 0b0000_0100, + ], + Self::Read10 => &[ + 0x28, + 0b1111_1100, + 0b1111_1111, + 0b1111_1111, + 0b1111_1111, + 0b1111_1111, + 0b0011_1111, + 0b1111_1111, + 0b1111_1111, + 0b0000_0100, + ], + Self::Inquiry => &[ + 0x12, + 0b0000_0001, + 0b1111_1111, + 0b1111_1111, + 0b1111_1111, + 0b0000_0100, + ], + Self::ReportSupportedOperationCodes => &[ + 0xa3, + 0xc, + 0b1000_0111, + 0b1111_1111, + 0b1111_1111, + 0b1111_1111, + 0b1111_1111, + 0b1111_1111, + 0b1111_1111, + 0b1111_1111, + 0b0000_0000, + 0b0000_0100, + ], + } + } +} + +#[derive(Debug)] +pub(crate) struct Cdb { + pub command: Command, + pub allocation_length: Option, + pub naca: bool, +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub(crate) enum ParseError { + /// The opcode (specifically the first byte of the CDB) is unknown, i.e. we + /// should respond with INVALID COMMAND OPERATION CODE + InvalidCommand, + /// Another field of the CDB (including the service action, if any) is + /// invalid, i.e. we should respond with INVALID FIELD IN CDB. + InvalidField, + /// The CDB has fewer bytes than necessary for its opcode. + TooSmall, +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub(crate) enum ReportSupportedOpCodesMode { + All, + OneCommand(u8), + OneServiceAction(u8, u16), + OneCommandOrServiceAction(u8, u16), +} + +impl Cdb { + // TODO: do we want to ensure reserved fields are 0? SCSI allows, but + // doesn't require, us to do so. + pub(crate) fn parse(cdb: &[u8]) -> Result { + let ct = CommandType::from_cdb(cdb)?; + if cdb.len() < ct.cdb_template().len() { + return Err(ParseError::TooSmall); + } + // Shrink the cdb down to its size, so accidentally accessing fields past the + // length panics + let cdb = &cdb[..ct.cdb_template().len()]; + + // unwraps below are safe: they're just calling TryFrom to convert from slices + // to fixed-size arrays; in each case, we're using constant indexes and we + // verified above that they're in bounds, so none of them can panic at runtime + + match ct { + CommandType::Inquiry => { + // INQUIRY + let evpd = match cdb[1] { + 0 => false, + 1 => true, + // obselete or reserved bits set + _ => return Err(ParseError::InvalidField), + }; + let page_code_raw = cdb[2]; + let page_code = match (evpd, page_code_raw) { + (false, 0) => None, + (true, pc) => Some(pc.try_into().map_err(|_| ParseError::InvalidField)?), + (false, _) => return Err(ParseError::InvalidField), + }; + Ok(Self { + command: Command::LunSpecificCommand(LunSpecificCommand::Inquiry(page_code)), + allocation_length: Some(u32::from(u16::from_be_bytes( + cdb[3..5].try_into().unwrap(), + ))), + naca: (cdb[5] & 0b0000_0100) != 0, + }) + } + CommandType::ModeSense6 => { + let dbd = match cdb[1] { + 0b0000_1000 => true, + 0b0000_0000 => false, + _ => return Err(ParseError::InvalidField), + }; + let pc = (cdb[2] & 0b1100_0000) >> 6; + let page_code = cdb[2] & 0b0011_1111; + let subpage_code = cdb[3]; + let mode: ModePageSelection = match (page_code, subpage_code) { + (0x8, 0x0) => ModePageSelection::Single(ModePage::Caching), + (0x3f, 0x0) => ModePageSelection::AllPageZeros, + _ => { + warn!( + "Rejecting request for unknown mode page {:#2x}/{:#2x}.", + page_code, subpage_code + ); + return Err(ParseError::InvalidField); + } + }; + Ok(Self { + command: Command::LunSpecificCommand(LunSpecificCommand::ModeSense6 { + pc: pc.try_into().map_err(|_| ParseError::InvalidField)?, + mode_page: mode, + dbd, + }), + allocation_length: Some(u32::from(cdb[4])), + naca: (cdb[5] & 0b0000_0100) != 0, + }) + } + CommandType::Read10 => { + if cdb[1] & 0b1110_0100 != 0 { + // Features (protection and rebuild assist) we don't + // support; the standard says to respond with INVALID + // FIELD IN CDB for these if unsupported + return Err(ParseError::InvalidField); + } + Ok(Self { + command: Command::LunSpecificCommand(LunSpecificCommand::Read10 { + dpo: cdb[1] & 0b0001_0000 != 0, + fua: cdb[1] & 0b0000_1000 != 0, + lba: u32::from_be_bytes(cdb[2..6].try_into().unwrap()), + transfer_length: u16::from_be_bytes(cdb[7..9].try_into().unwrap()), + }), + allocation_length: None, + naca: (cdb[9] & 0b0000_0100) != 0, + }) + } + CommandType::ReadCapacity10 => Ok(Self { + command: Command::LunSpecificCommand(LunSpecificCommand::ReadCapacity10), + allocation_length: None, + naca: (cdb[9] & 0b0000_0100) != 0, + }), + CommandType::ReadCapacity16 => Ok(Self { + command: Command::LunSpecificCommand(LunSpecificCommand::ReadCapacity16), + allocation_length: Some(u32::from_be_bytes(cdb[10..14].try_into().unwrap())), + naca: (cdb[15] & 0b0000_0100) != 0, + }), + CommandType::ReportLuns => Ok(Self { + command: Command::LunIndependentCommand(LunIndependentCommand::ReportLuns( + cdb[2].try_into().map_err(|_| ParseError::InvalidField)?, + )), + allocation_length: Some(u32::from_be_bytes(cdb[6..10].try_into().unwrap())), + naca: (cdb[9] & 0b0000_0100) != 0, + }), + CommandType::ReportSupportedOperationCodes => { + let rctd = cdb[2] & 0b1000_0000 != 0; + let mode = match cdb[2] & 0b0000_0111 { + 0b000 => ReportSupportedOpCodesMode::All, + 0b001 => ReportSupportedOpCodesMode::OneCommand(cdb[3]), + 0b010 => ReportSupportedOpCodesMode::OneServiceAction( + cdb[3], + u16::from_be_bytes(cdb[4..6].try_into().unwrap()), + ), + 0b011 => ReportSupportedOpCodesMode::OneCommandOrServiceAction( + cdb[3], + u16::from_be_bytes(cdb[4..6].try_into().unwrap()), + ), + _ => return Err(ParseError::InvalidField), + }; + + Ok(Self { + command: Command::LunSpecificCommand( + LunSpecificCommand::ReportSupportedOperationCodes { rctd, mode }, + ), + allocation_length: Some(u32::from_be_bytes(cdb[6..10].try_into().unwrap())), + naca: (cdb[11] & 0b0000_0100) != 0, + }) + } + CommandType::RequestSense => { + let format = if cdb[1] & 0b0000_0001 == 1 { + SenseFormat::Descriptor + } else { + SenseFormat::Fixed + }; + Ok(Self { + command: Command::LunSpecificCommand(LunSpecificCommand::RequestSense(format)), + allocation_length: Some(u32::from(cdb[4])), + naca: (cdb[5] & 0b0000_0100) != 0, + }) + } + CommandType::TestUnitReady => Ok(Self { + command: Command::LunSpecificCommand(LunSpecificCommand::TestUnitReady), + allocation_length: None, + naca: (cdb[5] & 0b0000_0100) != 0, + }), + } + } +} diff --git a/crates/scsi/src/scsi/emulation/missing_lun.rs b/crates/scsi/src/scsi/emulation/missing_lun.rs new file mode 100644 index 0000000..cb94baa --- /dev/null +++ b/crates/scsi/src/scsi/emulation/missing_lun.rs @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause + +use std::io::{Read, Write}; + +use super::{ + command::{LunSpecificCommand, SenseFormat}, + response_data::{respond_standard_inquiry_data, SilentlyTruncate}, + target::{LogicalUnit, LunRequest}, +}; +use crate::scsi::{sense, CmdError, CmdError::DataIn, CmdOutput}; + +pub(crate) struct MissingLun; + +impl LogicalUnit for MissingLun { + fn execute_command( + &mut self, + data_in: &mut SilentlyTruncate<&mut dyn Write>, + _data_out: &mut dyn Read, + _req: LunRequest, + cmd: LunSpecificCommand, + ) -> Result { + match cmd { + LunSpecificCommand::Inquiry(page_code) => { + // peripheral qualifier 0b011: logical unit not accessible + // device type 0x1f: unknown/no device type + data_in.write_all(&[0b0110_0000 | 0x1f]).map_err(DataIn)?; + match page_code { + Some(_) => { + // SPC-6 7.7.2: "If the PERIPHERAL QUALIFIER field is + // not set to 000b, the contents of the PAGE LENGTH + // field and the VPD parameters are outside the + // scope of this standard." + // + // Returning a 0 length and no data seems sensible enough. + data_in.write_all(&[0]).map_err(DataIn)?; + } + None => { + respond_standard_inquiry_data(data_in).map_err(DataIn)?; + } + } + Ok(CmdOutput::ok()) + } + LunSpecificCommand::RequestSense(format) => { + match format { + SenseFormat::Fixed => { + data_in + .write_all(&sense::LOGICAL_UNIT_NOT_SUPPORTED.to_fixed_sense()) + .map_err(DataIn)?; + Ok(CmdOutput::ok()) + } + SenseFormat::Descriptor => { + // Don't support desciptor format. + Ok(CmdOutput::check_condition(sense::INVALID_FIELD_IN_CDB)) + } + } + } + _ => Ok(CmdOutput::check_condition( + sense::LOGICAL_UNIT_NOT_SUPPORTED, + )), + } + } +} diff --git a/crates/scsi/src/scsi/emulation/mod.rs b/crates/scsi/src/scsi/emulation/mod.rs new file mode 100644 index 0000000..377b9a4 --- /dev/null +++ b/crates/scsi/src/scsi/emulation/mod.rs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause + +pub(crate) mod block_device; +mod command; +pub(crate) mod missing_lun; +pub(crate) mod mode_page; +mod response_data; +pub(crate) mod target; + diff --git a/crates/scsi/src/scsi/emulation/mode_page.rs b/crates/scsi/src/scsi/emulation/mode_page.rs new file mode 100644 index 0000000..e0c30e7 --- /dev/null +++ b/crates/scsi/src/scsi/emulation/mode_page.rs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause + +use std::io::{self, Write}; + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub(crate) enum ModePage { + Caching, +} + +impl ModePage { + pub(crate) const ALL_ZERO: &'static [Self] = &[Self::Caching]; + + pub(crate) const fn page_code(self) -> (u8, u8) { + match self { + Self::Caching => (0x8, 0), + } + } + + pub(crate) const fn page_length(self) -> u8 { + match self { + Self::Caching => 0x12, + } + } + + pub(crate) fn write(self, data_in: &mut impl Write) -> io::Result<()> { + assert_eq!(self.page_code().1, 0, "Subpages aren't supported yet."); + + data_in.write_all(&[ + self.page_code().0, // top 2 bits: no subpage, saving not supported + self.page_length(), // page length + ])?; + + match self { + Self::Caching => { + data_in.write_all(&[ + // Writeback Cache Enable, lots of bits zero + // n.b. kernel logs will show WCE off; it always says + // that for read-only devices, which we are rn + 0b0000_0100, + ])?; + // various cache fine-tuning stuff we can't really control + data_in.write_all(&[0; 0x11])?; + } + } + + Ok(()) + } +} diff --git a/crates/scsi/src/scsi/emulation/response_data.rs b/crates/scsi/src/scsi/emulation/response_data.rs new file mode 100644 index 0000000..352d069 --- /dev/null +++ b/crates/scsi/src/scsi/emulation/response_data.rs @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause + +//! Some helpers for writing response data, shared between `BlockDevice` and +//! `MissingLun` + +use std::{cmp::min, convert::TryFrom, io, io::Write}; + +/// A wrapper around a `Write` that silently truncates its input after a given +/// number of bytes. This matches the semantics of SCSI's ALLOCATION LENGTH +/// field; anything beyond the allocation length is silently omitted. +pub struct SilentlyTruncate(W, usize); + +impl SilentlyTruncate { + pub const fn new(writer: W, len: usize) -> Self { + Self(writer, len) + } +} + +impl Write for SilentlyTruncate { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + if self.1 == 0 { + // our goal is to silently fail, so once we've stopped actually + // writing, just pretend all writes work + return Ok(buf.len()); + } + let len = min(buf.len(), self.1); + let buf = &buf[..len]; + let written = self.0.write(buf)?; + self.1 -= written; + Ok(written) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.0.flush() + } +} + +fn encode_lun(lun: u16) -> [u8; 8] { + let lun = u8::try_from(lun).expect("more than 255 LUNs are currently unsupported"); + [0, lun, 0, 0, 0, 0, 0, 0] +} + +/// Write the response data for a REPORT LUNS command. +pub fn respond_report_luns(data_in: &mut impl Write, luns: T) -> io::Result<()> +where + T: IntoIterator, + T::IntoIter: ExactSizeIterator, +{ + let iter = luns.into_iter(); + data_in.write_all( + &(u32::try_from(iter.len() * 8)) + .expect("less than 256 LUNS") + .to_be_bytes(), + )?; + data_in.write_all(&[0; 4])?; // reserved + for lun in iter { + data_in.write_all(&encode_lun(lun))?; + } + Ok(()) +} + +/// Write the response data for a standard (i.e. not VPD) inquiry, excluding the +/// first byte (the peripheal qualifier and device type). +pub fn respond_standard_inquiry_data(data_in: &mut impl Write) -> io::Result<()> { + // TODO: Feature bits here we might want to support: + // - NormACA + // - command queueing + data_in.write_all(&[ + // various bits: not removable, not part of a + // conglomerate, no info on hotpluggability + 0, + 0x7, // version: SPC-6 + // bits: don't support NormACA, support modern LUN format + // INQUIRY data version 2 + 0b0001_0000 | 0x2, + 91, // additional INQURIY data length + // bunch of feature bits we don't support: + 0, + 0, + 0, + ])?; + + // TODO: register this or another name with T10 + data_in.write_all(b"rust-vmm")?; + data_in.write_all(b"vhost-user-scsi ")?; + data_in.write_all(b"v0 ")?; + + // The Linux kernel doesn't request any more than this, so any data we return + // after this point is mostly academic. + + data_in.write_all(&[0; 22])?; + + let product_descs: &[u16; 8] = &[ + 0x00c0, // SAM-6 (no version claimed) + 0x05c0, // SPC-5 (no version claimed) + 0x0600, // SBC-4 (no version claimed) + 0x0, 0x0, 0x0, 0x0, 0x0, + ]; + + for desc in product_descs { + data_in.write_all(&desc.to_be_bytes())?; + } + + data_in.write_all(&[0; 22])?; + + Ok(()) +} diff --git a/crates/scsi/src/scsi/emulation/target.rs b/crates/scsi/src/scsi/emulation/target.rs new file mode 100644 index 0000000..82e660c --- /dev/null +++ b/crates/scsi/src/scsi/emulation/target.rs @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause + +use std::convert::TryFrom; +use std::io::{Read, Write}; + +use log::error; + +use super::{ + command::{ + Cdb, Command, LunIndependentCommand, LunSpecificCommand, ParseError, ReportLunsSelectReport, + }, + missing_lun::MissingLun, + response_data::{respond_report_luns, SilentlyTruncate}, +}; +use crate::scsi::{sense, CmdError, CmdOutput, Request, Target, TaskAttr}; + +pub(crate) struct LunRequest { + pub _id: u64, + pub task_attr: TaskAttr, + pub crn: u8, + pub prio: u8, + pub _allocation_length: Option, + pub naca: bool, +} + +/// A single logical unit of an emulated SCSI device. +pub(crate) trait LogicalUnit: Send + Sync { + /// Process a SCSI command sent to this logical unit. + /// + /// # Return value + /// This function returns a Result, but it should return Err only in limited + /// circumstances: when something goes wrong at the transport level, such + /// as writes to `req.data_in` failing or `req.cdb` being too short. + /// Any other errors, such as invalid SCSI commands or I/O errors + /// accessing an underlying file, should result in an Ok return value + /// with a `CmdOutput` representing a SCSI-level error (i.e. CHECK + /// CONDITION status, and appropriate sense data). + fn execute_command( + &mut self, + data_in: &mut SilentlyTruncate<&mut dyn Write>, + data_out: &mut dyn Read, + parameters: LunRequest, + command: LunSpecificCommand, + ) -> Result; +} + +/// A SCSI target implemented by emulating a device within vhost-user-scsi. +pub(crate) struct EmulatedTarget { + luns: Vec>, +} + +impl EmulatedTarget { + pub(crate) fn new() -> Self { + Self { luns: Vec::new() } + } + + pub(crate) fn add_lun(&mut self, logical_unit: Box) { + self.luns.push(logical_unit); + } + + pub(crate) fn luns(&self) -> impl Iterator + ExactSizeIterator + '_ { + // unwrap is safe: we limit LUNs at 256 + self.luns + .iter() + .enumerate() + .map(|(idx, _logical_unit)| u16::try_from(idx).unwrap()) + } +} + +impl Default for EmulatedTarget { + fn default() -> Self { + Self::new() + } +} + +impl Target for EmulatedTarget { + fn execute_command( + &mut self, + lun: u16, + data_out: &mut dyn Read, + data_in: &mut dyn Write, + req: Request, + ) -> Result { + match Cdb::parse(req.cdb) { + Ok(cdb) => { + let mut data_in = SilentlyTruncate::new( + data_in, + cdb.allocation_length.map_or(usize::MAX, |x| x as usize), + ); + + match cdb.command { + Command::LunIndependentCommand(cmd) => match cmd { + LunIndependentCommand::ReportLuns(select_report) => { + match select_report { + ReportLunsSelectReport::NoWellKnown + | ReportLunsSelectReport::All => { + respond_report_luns(&mut data_in, self.luns()) + .map_err(CmdError::DataIn)?; + } + ReportLunsSelectReport::WellKnownOnly + | ReportLunsSelectReport::Administrative + | ReportLunsSelectReport::TopLevel + | ReportLunsSelectReport::SameConglomerate => { + respond_report_luns(&mut data_in, vec![].into_iter()) + .map_err(CmdError::DataIn)?; + } + } + Ok(CmdOutput::ok()) + } + }, + Command::LunSpecificCommand(cmd) => { + let req = LunRequest { + _id: req.id, + task_attr: req.task_attr, + crn: req.crn, + prio: req.prio, + _allocation_length: cdb.allocation_length, + naca: cdb.naca, + }; + match self.luns.get_mut(lun as usize) { + Some(lun) => lun.execute_command(&mut data_in, data_out, req, cmd), + None => MissingLun.execute_command(&mut data_in, data_out, req, cmd), + } + } + } + } + Err(ParseError::InvalidCommand) => { + error!("Rejecting CDB for unknown command: {:?}", req.cdb); + Ok(CmdOutput::check_condition( + sense::INVALID_COMMAND_OPERATION_CODE, + )) + } + // TODO: SCSI has a provision for INVALID FIELD IN CDB to include the + // index of the invalid field, but it's not clear if that's mandatory. + // In any case, QEMU omits it. + Err(ParseError::InvalidField) => { + error!("Rejecting CDB with invalid field: {:?}", req.cdb); + Ok(CmdOutput::check_condition(sense::INVALID_FIELD_IN_CDB)) + } + Err(ParseError::TooSmall) => Err(CmdError::CdbTooShort), + } + } +} diff --git a/crates/scsi/src/scsi/mod.rs b/crates/scsi/src/scsi/mod.rs index 0c05d58..9c1f158 100644 --- a/crates/scsi/src/scsi/mod.rs +++ b/crates/scsi/src/scsi/mod.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause + +pub mod emulation; pub mod sense; use std::io::{self, Read, Write}; diff --git a/crates/scsi/src/scsi/sense.rs b/crates/scsi/src/scsi/sense.rs index 7ced47e..ad7da96 100644 --- a/crates/scsi/src/scsi/sense.rs +++ b/crates/scsi/src/scsi/sense.rs @@ -22,6 +22,7 @@ impl SenseTriple { const NO_SENSE: u8 = 0; const MEDIUM_ERROR: u8 = 0x3; +const HARDWARE_ERROR: u8 = 0x4; const ILLEGAL_REQUEST: u8 = 0x5; pub const NO_ADDITIONAL_SENSE_INFORMATION: SenseTriple = SenseTriple(NO_SENSE, 0, 0); @@ -33,3 +34,4 @@ pub const LOGICAL_UNIT_NOT_SUPPORTED: SenseTriple = SenseTriple(ILLEGAL_REQUEST, pub const SAVING_PARAMETERS_NOT_SUPPORTED: SenseTriple = SenseTriple(ILLEGAL_REQUEST, 0x39, 0x0); pub const UNRECOVERED_READ_ERROR: SenseTriple = SenseTriple(MEDIUM_ERROR, 0x11, 0x0); +pub const TARGET_FAILURE: SenseTriple = SenseTriple(HARDWARE_ERROR, 0x44, 0x0);