mirror of
https://github.com/rust-vmm/vhost-device.git
synced 2025-12-26 22:48:17 +00:00
scsi: Add an file-based target implementation
This implements the previously defined interface by emulating the commands against a file-backed block device. The vast majority of this work was done by Gaelan Steele as part of a GSoC project [1][2]. [1] https://github.com/rust-vmm/vhost-device/pull/4 [2] https://gist.github.com/Gaelan/febec4e4606e1320026a0924c3bf74d0 Co-developed-by: Erik Schilling <erik.schilling@linaro.org> Signed-off-by: Erik Schilling <erik.schilling@linaro.org> Signed-off-by: Gaelan Steele <gbs@canishe.com>
This commit is contained in:
parent
a72a0a74e0
commit
be1eaf3f79
@ -1 +1,3 @@
|
||||
pub mod scsi;
|
||||
// We do not use any of this yet
|
||||
#[allow(dead_code)]
|
||||
mod scsi;
|
||||
|
||||
632
crates/scsi/src/scsi/emulation/block_device.rs
Normal file
632
crates/scsi/src/scsi/emulation/block_device.rs
Normal file
@ -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<u64> for ByteOffset {
|
||||
fn from(value: u64) -> Self {
|
||||
ByteOffset(value)
|
||||
}
|
||||
}
|
||||
impl From<ByteOffset> for u64 {
|
||||
fn from(value: ByteOffset) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
impl Div<BlockSize> 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<BlockSize> for u32 {
|
||||
fn from(value: BlockSize) -> Self {
|
||||
u32::from(value.0)
|
||||
}
|
||||
}
|
||||
impl TryFrom<u32> for BlockSize {
|
||||
type Error = TryFromIntError;
|
||||
|
||||
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
||||
Ok(BlockSize(NonZeroU32::try_from(value)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, PartialOrd)]
|
||||
pub(crate) struct BlockOffset(u64);
|
||||
impl From<BlockOffset> for u64 {
|
||||
fn from(value: BlockOffset) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
impl From<u64> for BlockOffset {
|
||||
fn from(value: u64) -> Self {
|
||||
BlockOffset(value)
|
||||
}
|
||||
}
|
||||
impl Add<BlockOffset> for BlockOffset {
|
||||
type Output = BlockOffset;
|
||||
|
||||
fn add(self, rhs: BlockOffset) -> Self::Output {
|
||||
BlockOffset(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
impl Sub<BlockOffset> for BlockOffset {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, rhs: BlockOffset) -> Self::Output {
|
||||
BlockOffset(self.0 - rhs.0)
|
||||
}
|
||||
}
|
||||
impl Mul<BlockSize> 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<BlockOffset>;
|
||||
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<BlockOffset> {
|
||||
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<T: BlockDeviceBackend> {
|
||||
backend: T,
|
||||
write_protected: bool,
|
||||
rotation_rate: MediumRotationRate,
|
||||
}
|
||||
|
||||
impl<T: BlockDeviceBackend> BlockDevice<T> {
|
||||
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<Vec<u8>> {
|
||||
// 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<T: BlockDeviceBackend> LogicalUnit for BlockDevice<T> {
|
||||
fn execute_command(
|
||||
&mut self,
|
||||
data_in: &mut SilentlyTruncate<&mut dyn Write>,
|
||||
_data_out: &mut dyn Read,
|
||||
req: LunRequest,
|
||||
command: LunSpecificCommand,
|
||||
) -> Result<CmdOutput, CmdError> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
577
crates/scsi/src/scsi/emulation/command.rs
Normal file
577
crates/scsi/src/scsi/emulation/command.rs
Normal file
@ -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<u8> for VpdPage {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(val: u8) -> Result<Self, ()> {
|
||||
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<VpdPage> 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<VpdPage>),
|
||||
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<u16>))] = &[
|
||||
(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<CommandType> {
|
||||
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<Self, ParseError> {
|
||||
// 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<u32>,
|
||||
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<Self, ParseError> {
|
||||
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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
62
crates/scsi/src/scsi/emulation/missing_lun.rs
Normal file
62
crates/scsi/src/scsi/emulation/missing_lun.rs
Normal file
@ -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<CmdOutput, CmdError> {
|
||||
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,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
9
crates/scsi/src/scsi/emulation/mod.rs
Normal file
9
crates/scsi/src/scsi/emulation/mod.rs
Normal file
@ -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;
|
||||
|
||||
48
crates/scsi/src/scsi/emulation/mode_page.rs
Normal file
48
crates/scsi/src/scsi/emulation/mode_page.rs
Normal file
@ -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(())
|
||||
}
|
||||
}
|
||||
107
crates/scsi/src/scsi/emulation/response_data.rs
Normal file
107
crates/scsi/src/scsi/emulation/response_data.rs
Normal file
@ -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: Write>(W, usize);
|
||||
|
||||
impl<W: Write> SilentlyTruncate<W> {
|
||||
pub const fn new(writer: W, len: usize) -> Self {
|
||||
Self(writer, len)
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Write for SilentlyTruncate<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
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<T>(data_in: &mut impl Write, luns: T) -> io::Result<()>
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
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(())
|
||||
}
|
||||
143
crates/scsi/src/scsi/emulation/target.rs
Normal file
143
crates/scsi/src/scsi/emulation/target.rs
Normal file
@ -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<u32>,
|
||||
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<CmdOutput, CmdError>;
|
||||
}
|
||||
|
||||
/// A SCSI target implemented by emulating a device within vhost-user-scsi.
|
||||
pub(crate) struct EmulatedTarget {
|
||||
luns: Vec<Box<dyn LogicalUnit>>,
|
||||
}
|
||||
|
||||
impl EmulatedTarget {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { luns: Vec::new() }
|
||||
}
|
||||
|
||||
pub(crate) fn add_lun(&mut self, logical_unit: Box<dyn LogicalUnit>) {
|
||||
self.luns.push(logical_unit);
|
||||
}
|
||||
|
||||
pub(crate) fn luns(&self) -> impl Iterator<Item = u16> + 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<CmdOutput, CmdError> {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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};
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user