diff --git a/crates/scsi/src/main.rs b/crates/scsi/src/main.rs index e48959f..9e7813f 100644 --- a/crates/scsi/src/main.rs +++ b/crates/scsi/src/main.rs @@ -70,7 +70,13 @@ fn create_backend(args: &ScsiArgs) -> Result { } for image in &args.images { - let mut dev = BlockDevice::new(FileBackend::new(File::open(image).expect("Opening image"))); + let mut dev = BlockDevice::new(FileBackend::new( + File::options() + .read(true) + .write(true) + .open(image) + .expect("Opening image"), + )); dev.set_write_protected(args.read_only); dev.set_solid_state(if args.solid_state { MediumRotationRate::NonRotating diff --git a/crates/scsi/src/scsi/emulation/block_device.rs b/crates/scsi/src/scsi/emulation/block_device.rs index 66777e7..b21c833 100644 --- a/crates/scsi/src/scsi/emulation/block_device.rs +++ b/crates/scsi/src/scsi/emulation/block_device.rs @@ -98,6 +98,7 @@ impl Mul for BlockOffset { pub(crate) trait BlockDeviceBackend: Send + Sync { fn read_exact_at(&mut self, buf: &mut [u8], offset: ByteOffset) -> io::Result<()>; + fn write_exact_at(&mut self, buf: &[u8], offset: ByteOffset) -> io::Result<()>; fn size_in_blocks(&mut self) -> io::Result; fn block_size(&self) -> BlockSize; fn sync(&mut self) -> io::Result<()>; @@ -122,6 +123,10 @@ impl BlockDeviceBackend for FileBackend { self.file.read_exact_at(buf, u64::from(offset)) } + fn write_exact_at(&mut self, buf: &[u8], offset: ByteOffset) -> io::Result<()> { + self.file.write_all_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); @@ -168,6 +173,25 @@ impl BlockDevice { Ok(ret) } + fn write_blocks( + &mut self, + lba: BlockOffset, + blocks: BlockOffset, + reader: &mut dyn Read, + ) -> io::Result<()> { + // TODO: Avoid the copies here. + let mut buf = vec![ + 0; + usize::try_from(u64::from(blocks * self.backend.block_size())) + .expect("block length in bytes should fit usize") + ]; + reader.read_exact(&mut buf)?; + self.backend + .write_exact_at(&buf, lba * self.backend.block_size())?; + + Ok(()) + } + pub fn set_write_protected(&mut self, wp: bool) { self.write_protected = wp; } @@ -181,7 +205,7 @@ impl LogicalUnit for BlockDevice { fn execute_command( &mut self, data_in: &mut SilentlyTruncate<&mut dyn Write>, - _data_out: &mut dyn Read, + data_out: &mut dyn Read, req: LunRequest, command: LunSpecificCommand, ) -> Result { @@ -415,6 +439,52 @@ impl LogicalUnit for BlockDevice { } } } + LunSpecificCommand::Write10 { + 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"); + } + + 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::TARGET_FAILURE)); + } + }; + + 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 write_result = self.write_blocks(lba, transfer_length, data_out); + + if fua { + if let Err(e) = self.backend.sync() { + error!("Error syncing file: {}", e); + return Ok(CmdOutput::check_condition(sense::TARGET_FAILURE)); + } + } + + match write_result { + Ok(()) => Ok(CmdOutput::ok()), + Err(e) => { + error!("Error writing to block device: {}", e); + Ok(CmdOutput::check_condition(sense::TARGET_FAILURE)) + } + } + } LunSpecificCommand::Inquiry(page_code) => { // top 3 bits 0: peripheral device code = exists and ready // bottom 5 bits 0: device type = block device diff --git a/crates/scsi/src/scsi/emulation/command.rs b/crates/scsi/src/scsi/emulation/command.rs index 5572174..bdfed33 100644 --- a/crates/scsi/src/scsi/emulation/command.rs +++ b/crates/scsi/src/scsi/emulation/command.rs @@ -174,6 +174,15 @@ pub(crate) enum LunSpecificCommand { lba: u32, transfer_length: u16, }, + Write10 { + /// 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 { @@ -202,6 +211,7 @@ pub(crate) enum CommandType { ReportSupportedOperationCodes, RequestSense, TestUnitReady, + Write10, } pub(crate) const OPCODES: &[(CommandType, (u8, Option))] = &[ @@ -211,6 +221,7 @@ pub(crate) const OPCODES: &[(CommandType, (u8, Option))] = &[ (CommandType::ModeSense6, (0x1a, None)), (CommandType::ReadCapacity10, (0x25, None)), (CommandType::Read10, (0x28, None)), + (CommandType::Write10, (0x2a, None)), (CommandType::ReadCapacity16, (0x9e, Some(0x10))), (CommandType::ReportLuns, (0xa0, None)), ( @@ -374,6 +385,18 @@ impl CommandType { 0b1111_1111, 0b0000_0100, ], + Self::Write10 => &[ + 0x2A, + 0b1111_1100, + 0b1111_1111, + 0b1111_1111, + 0b1111_1111, + 0b1111_1111, + 0b0011_1111, + 0b1111_1111, + 0b1111_1111, + 0b0000_0100, + ], Self::Inquiry => &[ 0x12, 0b0000_0001, @@ -514,6 +537,24 @@ impl Cdb { naca: (cdb[9] & 0b0000_0100) != 0, }) } + CommandType::Write10 => { + if cdb[1] & 0b1110_0000 != 0 { + // Feature (protection) that 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::Write10 { + 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, diff --git a/crates/scsi/src/scsi/emulation/tests/mod.rs b/crates/scsi/src/scsi/emulation/tests/mod.rs index b112ba0..f23e01a 100644 --- a/crates/scsi/src/scsi/emulation/tests/mod.rs +++ b/crates/scsi/src/scsi/emulation/tests/mod.rs @@ -6,12 +6,18 @@ mod bad_lun; mod generic; mod report_supported_operation_codes; -use std::{fs::File, io::Write}; +use std::{ + fs::File, + io::Write, + sync::{Arc, Mutex}, +}; use tempfile::tempfile; use super::{ - block_device::{BlockDevice, FileBackend}, + block_device::{ + BlockDevice, BlockDeviceBackend, BlockOffset, BlockSize, ByteOffset, FileBackend, + }, target::EmulatedTarget, }; use crate::scsi::{ @@ -19,6 +25,51 @@ use crate::scsi::{ CmdOutput, Request, Target, TaskAttr, }; +#[derive(Clone)] +struct TestBackend { + data: Arc>, +} + +impl TestBackend { + fn new() -> Self { + TestBackend { + data: Arc::new(Mutex::new([0; 512 * 16])), + } + } +} + +impl BlockDeviceBackend for TestBackend { + fn read_exact_at(&mut self, buf: &mut [u8], offset: ByteOffset) -> std::io::Result<()> { + let data = self.data.lock().unwrap(); + + let offset = usize::try_from(u64::from(offset)).expect("offset should fit usize"); + buf.copy_from_slice(&data[offset..(offset + buf.len())]); + Ok(()) + } + + fn write_exact_at(&mut self, buf: &[u8], offset: ByteOffset) -> std::io::Result<()> { + let mut data = self.data.lock().unwrap(); + + let offset = usize::try_from(u64::from(offset)).expect("offset should fit usize"); + data[offset..(offset + buf.len())].copy_from_slice(buf); + Ok(()) + } + + fn size_in_blocks(&mut self) -> std::io::Result { + Ok(ByteOffset::from( + u64::try_from(self.data.lock().unwrap().len()).expect("size_in_blocks should fit u64"), + ) / self.block_size()) + } + + fn block_size(&self) -> BlockSize { + BlockSize::try_from(512).expect("512 should be a valid BlockSize") + } + + fn sync(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + fn null_image() -> FileBackend { FileBackend::new(File::open("/dev/null").unwrap()) } @@ -103,6 +154,10 @@ fn do_command_fail(target: &mut EmulatedTarget, cdb: &[u8], expected_error: Sens do_command_fail_lun(target, 0, cdb, expected_error); } +fn block_size_512() -> BlockSize { + BlockSize::try_from(512).expect("512 should be a valid block_size") +} + #[test] fn test_test_unit_ready() { let mut target = EmulatedTarget::new(); @@ -237,6 +292,41 @@ fn test_read_10_cross_out() { ); } +#[test] +fn test_write_10() { + let mut target = EmulatedTarget::new(); + let mut backend = TestBackend::new(); + let dev = BlockDevice::new(backend.clone()); + target.add_lun(Box::new(dev)); + + // TODO: this test relies on the default logical block size of 512. We should + // make that explicit. + + { + let data_out = [b'w'; 512]; + + do_command_in( + &mut target, + &[ + 0x2a, // WRITE (10) + 0, // flags + 0, 0, 0, 5, // LBA: 5 + 0, // reserved, group # + 0, 1, // transfer length: 1 + 0, // control + ], + &data_out, + &[], + ); + + let mut buf = [0_u8; 512]; + backend + .read_exact_at(&mut buf, BlockOffset::from(5) * block_size_512()) + .expect("Reading should work"); + assert_eq!(data_out, buf); + } +} + #[test] fn test_read_capacity_10() { let mut target = EmulatedTarget::new();