/src/crosvm/devices/src/virtio/scsi/device.rs
Line | Count | Source |
1 | | // Copyright 2023 The ChromiumOS Authors |
2 | | // Use of this source code is governed by a BSD-style license that can be |
3 | | // found in the LICENSE file. |
4 | | |
5 | | #![deny(missing_docs)] |
6 | | //! A SCSI controller has SCSI target(s), a SCSI target has logical unit(s). |
7 | | //! crosvm currently supports only one logical unit in a target (LUN0), therefore a SCSI target is |
8 | | //! tied to a logical unit and a disk image belongs to a logical unit in crosvm. |
9 | | |
10 | | use std::cell::RefCell; |
11 | | use std::collections::BTreeMap; |
12 | | use std::collections::BTreeSet; |
13 | | use std::io; |
14 | | use std::io::Read; |
15 | | use std::io::Write; |
16 | | use std::rc::Rc; |
17 | | |
18 | | use anyhow::Context; |
19 | | use base::error; |
20 | | use base::warn; |
21 | | use base::Event; |
22 | | use base::WorkerThread; |
23 | | use cros_async::EventAsync; |
24 | | use cros_async::Executor; |
25 | | use cros_async::ExecutorKind; |
26 | | use disk::AsyncDisk; |
27 | | use disk::DiskFile; |
28 | | use futures::pin_mut; |
29 | | use futures::stream::FuturesUnordered; |
30 | | use futures::FutureExt; |
31 | | use futures::StreamExt; |
32 | | use remain::sorted; |
33 | | use thiserror::Error as ThisError; |
34 | | use virtio_sys::virtio_scsi::virtio_scsi_config; |
35 | | use virtio_sys::virtio_scsi::virtio_scsi_ctrl_an_resp; |
36 | | use virtio_sys::virtio_scsi::virtio_scsi_ctrl_tmf_req; |
37 | | use virtio_sys::virtio_scsi::virtio_scsi_ctrl_tmf_resp; |
38 | | use virtio_sys::virtio_scsi::virtio_scsi_event; |
39 | | use virtio_sys::virtio_scsi::VIRTIO_SCSI_CDB_DEFAULT_SIZE; |
40 | | use virtio_sys::virtio_scsi::VIRTIO_SCSI_SENSE_DEFAULT_SIZE; |
41 | | use virtio_sys::virtio_scsi::VIRTIO_SCSI_S_BAD_TARGET; |
42 | | use virtio_sys::virtio_scsi::VIRTIO_SCSI_S_FUNCTION_REJECTED; |
43 | | use virtio_sys::virtio_scsi::VIRTIO_SCSI_S_FUNCTION_SUCCEEDED; |
44 | | use virtio_sys::virtio_scsi::VIRTIO_SCSI_S_INCORRECT_LUN; |
45 | | use virtio_sys::virtio_scsi::VIRTIO_SCSI_S_OK; |
46 | | use virtio_sys::virtio_scsi::VIRTIO_SCSI_T_AN_QUERY; |
47 | | use virtio_sys::virtio_scsi::VIRTIO_SCSI_T_AN_SUBSCRIBE; |
48 | | use virtio_sys::virtio_scsi::VIRTIO_SCSI_T_TMF; |
49 | | use virtio_sys::virtio_scsi::VIRTIO_SCSI_T_TMF_I_T_NEXUS_RESET; |
50 | | use virtio_sys::virtio_scsi::VIRTIO_SCSI_T_TMF_LOGICAL_UNIT_RESET; |
51 | | use vm_memory::GuestMemory; |
52 | | use zerocopy::FromBytes; |
53 | | use zerocopy::Immutable; |
54 | | use zerocopy::IntoBytes; |
55 | | use zerocopy::KnownLayout; |
56 | | |
57 | | use crate::virtio::async_utils; |
58 | | use crate::virtio::block::sys::get_seg_max; |
59 | | use crate::virtio::copy_config; |
60 | | use crate::virtio::scsi::commands::execute_cdb; |
61 | | use crate::virtio::scsi::constants::CHECK_CONDITION; |
62 | | use crate::virtio::scsi::constants::GOOD; |
63 | | use crate::virtio::scsi::constants::ILLEGAL_REQUEST; |
64 | | use crate::virtio::scsi::constants::MEDIUM_ERROR; |
65 | | use crate::virtio::DescriptorChain; |
66 | | use crate::virtio::DeviceType as VirtioDeviceType; |
67 | | use crate::virtio::Interrupt; |
68 | | use crate::virtio::Queue; |
69 | | use crate::virtio::Reader; |
70 | | use crate::virtio::VirtioDevice; |
71 | | use crate::virtio::Writer; |
72 | | |
73 | | // The following values reflects the virtio v1.2 spec: |
74 | | // <https://docs.oasis-open.org/virtio/virtio/v1.2/csd01/virtio-v1.2-csd01.html#x1-3470004> |
75 | | |
76 | | // Should have one controlq, one eventq, and at least one request queue. |
77 | | const MIN_NUM_QUEUES: usize = 3; |
78 | | // The number of queues exposed by the device. |
79 | | // First crosvm pass this value through `VirtioDevice::read_config`, and then the driver determines |
80 | | // the number of queues which does not exceed the passed value. The determined value eventually |
81 | | // shows as the length of `queues` in `VirtioDevice::activate`. |
82 | | const MAX_NUM_QUEUES: usize = 16; |
83 | | // Max channel should be 0. |
84 | | const DEFAULT_MAX_CHANNEL: u16 = 0; |
85 | | // Max target should be less than or equal to 255. |
86 | | const DEFAULT_MAX_TARGET: u16 = 255; |
87 | | // Max lun should be less than or equal to 16383 |
88 | | const DEFAULT_MAX_LUN: u32 = 16383; |
89 | | |
90 | | const DEFAULT_QUEUE_SIZE: u16 = 1024; |
91 | | |
92 | | // The maximum number of linked commands. |
93 | | const MAX_CMD_PER_LUN: u32 = 1024; |
94 | | // We do not set a limit on the transfer size. |
95 | | const MAX_SECTORS: u32 = u32::MAX; |
96 | | |
97 | | // The length of sense data in fixed format. Details are in SPC-3 t10 revision 23: |
98 | | // <https://www.t10.org/cgi-bin/ac.pl?t=f&f=spc3r23.pdf> |
99 | | const FIXED_FORMAT_SENSE_SIZE: u32 = 18; |
100 | | |
101 | | #[repr(C, packed)] |
102 | | #[derive(Debug, Default, Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)] |
103 | | struct VirtioScsiCmdReqHeader { |
104 | | lun: [u8; 8usize], |
105 | | tag: u64, |
106 | | task_attr: u8, |
107 | | prio: u8, |
108 | | crn: u8, |
109 | | } |
110 | | |
111 | | #[repr(C, packed)] |
112 | | #[derive(Debug, Default, Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)] |
113 | | struct VirtioScsiCmdRespHeader { |
114 | | sense_len: u32, |
115 | | resid: u32, |
116 | | status_qualifier: u16, |
117 | | status: u8, |
118 | | response: u8, |
119 | | } |
120 | | |
121 | | impl VirtioScsiCmdRespHeader { |
122 | 0 | fn ok() -> Self { |
123 | 0 | VirtioScsiCmdRespHeader { |
124 | 0 | sense_len: 0, |
125 | 0 | resid: 0, |
126 | 0 | status_qualifier: 0, |
127 | 0 | status: GOOD, |
128 | 0 | response: VIRTIO_SCSI_S_OK as u8, |
129 | 0 | } |
130 | 0 | } |
131 | | } |
132 | | |
133 | | /// Errors that happen while handling scsi commands. |
134 | | #[sorted] |
135 | | #[derive(ThisError, Debug)] |
136 | | pub enum ExecuteError { |
137 | | #[error("invalid cdb field")] |
138 | | InvalidField, |
139 | | #[error("invalid parameter length")] |
140 | | InvalidParamLen, |
141 | | #[error("{xfer_blocks} blocks from LBA {lba} exceeds end of this device {last_lba}")] |
142 | | LbaOutOfRange { |
143 | | lba: u64, |
144 | | xfer_blocks: usize, |
145 | | last_lba: u64, |
146 | | }, |
147 | | #[error("failed to read message: {0}")] |
148 | | Read(io::Error), |
149 | | #[error("failed to read command from cdb")] |
150 | | ReadCommand, |
151 | | #[error("io error {resid} bytes remained to be read: {desc_error}")] |
152 | | ReadIo { |
153 | | resid: usize, |
154 | | desc_error: disk::Error, |
155 | | }, |
156 | | #[error("writing to a read only device")] |
157 | | ReadOnly, |
158 | | #[error("saving parameters not supported")] |
159 | | SavingParamNotSupported, |
160 | | #[error("synchronization error")] |
161 | | SynchronizationError, |
162 | | #[error("unsupported scsi command: {0}")] |
163 | | Unsupported(u8), |
164 | | #[error("failed to write message: {0}")] |
165 | | Write(io::Error), |
166 | | #[error("io error {resid} bytes remained to be written: {desc_error}")] |
167 | | WriteIo { |
168 | | resid: usize, |
169 | | desc_error: disk::Error, |
170 | | }, |
171 | | } |
172 | | |
173 | | impl ExecuteError { |
174 | | // converts ExecuteError to (VirtioScsiCmdReqHeader, Sense) |
175 | 0 | fn as_resp(&self) -> (VirtioScsiCmdRespHeader, Sense) { |
176 | 0 | let resp = VirtioScsiCmdRespHeader::ok(); |
177 | | // The asc and ascq assignments are taken from the t10 SPC spec. |
178 | | // cf) Table 28 of <https://www.t10.org/cgi-bin/ac.pl?t=f&f=spc3r23.pdf> |
179 | 0 | let sense = match self { |
180 | | Self::Read(_) | Self::ReadCommand => { |
181 | | // UNRECOVERED READ ERROR |
182 | 0 | Sense { |
183 | 0 | key: MEDIUM_ERROR, |
184 | 0 | asc: 0x11, |
185 | 0 | ascq: 0x00, |
186 | 0 | } |
187 | | } |
188 | | Self::Write(_) => { |
189 | | // WRITE ERROR |
190 | 0 | Sense { |
191 | 0 | key: MEDIUM_ERROR, |
192 | 0 | asc: 0x0c, |
193 | 0 | ascq: 0x00, |
194 | 0 | } |
195 | | } |
196 | | Self::InvalidField => { |
197 | | // INVALID FIELD IN CDB |
198 | 0 | Sense { |
199 | 0 | key: ILLEGAL_REQUEST, |
200 | 0 | asc: 0x24, |
201 | 0 | ascq: 0x00, |
202 | 0 | } |
203 | | } |
204 | | Self::InvalidParamLen => { |
205 | | // INVALID PARAMETER LENGTH |
206 | 0 | Sense { |
207 | 0 | key: ILLEGAL_REQUEST, |
208 | 0 | asc: 0x1a, |
209 | 0 | ascq: 0x00, |
210 | 0 | } |
211 | | } |
212 | | Self::Unsupported(_) => { |
213 | | // INVALID COMMAND OPERATION CODE |
214 | 0 | Sense { |
215 | 0 | key: ILLEGAL_REQUEST, |
216 | 0 | asc: 0x20, |
217 | 0 | ascq: 0x00, |
218 | 0 | } |
219 | | } |
220 | | Self::ReadOnly | Self::LbaOutOfRange { .. } => { |
221 | | // LOGICAL BLOCK ADDRESS OUT OF RANGE |
222 | 0 | Sense { |
223 | 0 | key: ILLEGAL_REQUEST, |
224 | 0 | asc: 0x21, |
225 | 0 | ascq: 0x00, |
226 | 0 | } |
227 | | } |
228 | 0 | Self::SavingParamNotSupported => Sense { |
229 | 0 | // SAVING PARAMETERS NOT SUPPORTED |
230 | 0 | key: ILLEGAL_REQUEST, |
231 | 0 | asc: 0x39, |
232 | 0 | ascq: 0x00, |
233 | 0 | }, |
234 | 0 | Self::SynchronizationError => Sense { |
235 | 0 | // SYNCHRONIZATION ERROR |
236 | 0 | key: MEDIUM_ERROR, |
237 | 0 | asc: 0x16, |
238 | 0 | ascq: 0x00, |
239 | 0 | }, |
240 | | // Ignore these errors. |
241 | 0 | Self::ReadIo { resid, desc_error } | Self::WriteIo { resid, desc_error } => { |
242 | 0 | warn!("error while performing I/O {}", desc_error); |
243 | 0 | let hdr = VirtioScsiCmdRespHeader { |
244 | 0 | resid: (*resid).try_into().unwrap_or(u32::MAX).to_be(), |
245 | 0 | ..resp |
246 | 0 | }; |
247 | 0 | return (hdr, Sense::default()); |
248 | | } |
249 | | }; |
250 | 0 | ( |
251 | 0 | VirtioScsiCmdRespHeader { |
252 | 0 | sense_len: FIXED_FORMAT_SENSE_SIZE, |
253 | 0 | status: CHECK_CONDITION, |
254 | 0 | ..resp |
255 | 0 | }, |
256 | 0 | sense, |
257 | 0 | ) |
258 | 0 | } |
259 | | } |
260 | | |
261 | | /// Sense code representation |
262 | | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] |
263 | | pub struct Sense { |
264 | | /// Provides generic information describing an error or exception condition. |
265 | | pub key: u8, |
266 | | /// Additional Sense Code. |
267 | | /// Indicates further information related to the error or exception reported in the key field. |
268 | | pub asc: u8, |
269 | | /// Additional Sense Code Qualifier. |
270 | | /// Indicates further detailed information related to the additional sense code. |
271 | | pub ascq: u8, |
272 | | } |
273 | | |
274 | | impl Sense { |
275 | 0 | fn write_to(&self, writer: &mut Writer, sense_size: u32) -> Result<(), ExecuteError> { |
276 | 0 | let mut sense_data = [0u8; FIXED_FORMAT_SENSE_SIZE as usize]; |
277 | | // Fixed format sense data has response code: |
278 | | // 1) 0x70 for current errors |
279 | | // 2) 0x71 for deferred errors |
280 | 0 | sense_data[0] = 0x70; |
281 | | // sense_data[1]: Obsolete |
282 | | // Sense key |
283 | 0 | sense_data[2] = self.key; |
284 | | // sense_data[3..7]: Information field, which we do not support. |
285 | | // Additional length. The data is 18 bytes, and this byte is 8th. |
286 | 0 | sense_data[7] = 10; |
287 | | // sense_data[8..12]: Command specific information, which we do not support. |
288 | | // Additional sense code |
289 | 0 | sense_data[12] = self.asc; |
290 | | // Additional sense code qualifier |
291 | 0 | sense_data[13] = self.ascq; |
292 | | // sense_data[14]: Field replaceable unit code, which we do not support. |
293 | | // sense_data[15..18]: Field replaceable unit code, which we do not support. |
294 | 0 | writer.write_all(&sense_data).map_err(ExecuteError::Write)?; |
295 | 0 | writer.consume_bytes(sense_size as usize - sense_data.len()); |
296 | 0 | Ok(()) |
297 | 0 | } |
298 | | } |
299 | | |
300 | | /// Describes each SCSI logical unit. |
301 | | struct LogicalUnit { |
302 | | /// The logical block address of the last logical block on the target device. |
303 | | last_lba: u64, |
304 | | /// Block size of the target device. |
305 | | block_size: u32, |
306 | | read_only: bool, |
307 | | // Represents the image on disk. |
308 | | disk_image: Box<dyn DiskFile>, |
309 | | } |
310 | | |
311 | | impl LogicalUnit { |
312 | 0 | fn make_async(self, ex: &Executor) -> anyhow::Result<AsyncLogicalUnit> { |
313 | 0 | let disk_image = self |
314 | 0 | .disk_image |
315 | 0 | .to_async_disk(ex) |
316 | 0 | .context("Failed to create async disk")?; |
317 | 0 | Ok(AsyncLogicalUnit { |
318 | 0 | last_lba: self.last_lba, |
319 | 0 | block_size: self.block_size, |
320 | 0 | read_only: self.read_only, |
321 | 0 | disk_image, |
322 | 0 | }) |
323 | 0 | } |
324 | | } |
325 | | |
326 | | /// A logical unit with an AsyncDisk as the disk. |
327 | | pub struct AsyncLogicalUnit { |
328 | | pub last_lba: u64, |
329 | | pub block_size: u32, |
330 | | pub read_only: bool, |
331 | | // Represents the async image on disk. |
332 | | pub disk_image: Box<dyn AsyncDisk>, |
333 | | } |
334 | | |
335 | | type TargetId = u8; |
336 | | struct Targets(BTreeMap<TargetId, LogicalUnit>); |
337 | | |
338 | | impl Targets { |
339 | 0 | fn try_clone(&self) -> io::Result<Self> { |
340 | 0 | let logical_units = self |
341 | 0 | .0 |
342 | 0 | .iter() |
343 | 0 | .map(|(id, logical_unit)| { |
344 | 0 | let disk_image = logical_unit.disk_image.try_clone()?; |
345 | 0 | Ok(( |
346 | 0 | *id, |
347 | 0 | LogicalUnit { |
348 | 0 | disk_image, |
349 | 0 | last_lba: logical_unit.last_lba, |
350 | 0 | block_size: logical_unit.block_size, |
351 | 0 | read_only: logical_unit.read_only, |
352 | 0 | }, |
353 | 0 | )) |
354 | 0 | }) |
355 | 0 | .collect::<io::Result<_>>()?; |
356 | 0 | Ok(Self(logical_units)) |
357 | 0 | } |
358 | | |
359 | 0 | fn target_ids(&self) -> BTreeSet<TargetId> { |
360 | 0 | self.0.keys().cloned().collect() |
361 | 0 | } |
362 | | } |
363 | | |
364 | | /// Configuration of each SCSI device. |
365 | | pub struct DiskConfig { |
366 | | /// The disk file of the device. |
367 | | pub file: Box<dyn DiskFile>, |
368 | | /// The block size of the SCSI disk. |
369 | | pub block_size: u32, |
370 | | /// Indicates whether the SCSI disk is read only. |
371 | | pub read_only: bool, |
372 | | } |
373 | | |
374 | | /// Vitio device for exposing SCSI command operations on a host file. |
375 | | pub struct Controller { |
376 | | // Bitmap of virtio-scsi feature bits. |
377 | | avail_features: u64, |
378 | | // Sizes for the virtqueue. |
379 | | queue_sizes: Vec<u16>, |
380 | | // The maximum number of segments that can be in a command. |
381 | | seg_max: u32, |
382 | | // The size of the sense data. |
383 | | sense_size: u32, |
384 | | // The byte size of the CDB that the driver will write. |
385 | | cdb_size: u32, |
386 | | executor_kind: ExecutorKind, |
387 | | worker_threads: Vec<WorkerThread<()>>, |
388 | | // Stores target devices by its target id. Currently we only support bus id 0. |
389 | | targets: Option<Targets>, |
390 | | // Whether the devices handles requests in multiple request queues. |
391 | | // If true, each virtqueue will be handled in a separate worker thread. |
392 | | multi_queue: bool, |
393 | | } |
394 | | |
395 | | impl Controller { |
396 | | /// Creates a virtio-scsi device. |
397 | 0 | pub fn new(base_features: u64, disks: Vec<DiskConfig>) -> anyhow::Result<Self> { |
398 | 0 | let multi_queue = disks.iter().all(|disk| disk.file.try_clone().is_ok()); |
399 | 0 | let num_queues = if multi_queue { |
400 | 0 | MAX_NUM_QUEUES |
401 | | } else { |
402 | 0 | MIN_NUM_QUEUES |
403 | | }; |
404 | 0 | let logical_units = disks |
405 | 0 | .into_iter() |
406 | 0 | .enumerate() |
407 | 0 | .map(|(i, disk)| { |
408 | 0 | let num_blocks = disk |
409 | 0 | .file |
410 | 0 | .get_len() |
411 | 0 | .context("Failed to get the length of the disk image")? |
412 | 0 | / disk.block_size as u64; |
413 | 0 | let last_lba = num_blocks |
414 | 0 | .checked_sub(1) |
415 | 0 | .context("Invalid zero-length SCSI LUN")?; |
416 | 0 | let target = LogicalUnit { |
417 | 0 | last_lba, |
418 | 0 | block_size: disk.block_size, |
419 | 0 | read_only: disk.read_only, |
420 | 0 | disk_image: disk.file, |
421 | 0 | }; |
422 | 0 | Ok((i as TargetId, target)) |
423 | 0 | }) |
424 | 0 | .collect::<anyhow::Result<_>>()?; |
425 | | // b/300560198: Support feature bits in virtio-scsi. |
426 | 0 | Ok(Self { |
427 | 0 | avail_features: base_features, |
428 | 0 | queue_sizes: vec![DEFAULT_QUEUE_SIZE; num_queues], |
429 | 0 | seg_max: get_seg_max(DEFAULT_QUEUE_SIZE), |
430 | 0 | sense_size: VIRTIO_SCSI_SENSE_DEFAULT_SIZE, |
431 | 0 | cdb_size: VIRTIO_SCSI_CDB_DEFAULT_SIZE, |
432 | 0 | executor_kind: ExecutorKind::default(), |
433 | 0 | worker_threads: vec![], |
434 | 0 | targets: Some(Targets(logical_units)), |
435 | 0 | multi_queue, |
436 | 0 | }) |
437 | 0 | } |
438 | | |
439 | 0 | fn build_config_space(&self) -> virtio_scsi_config { |
440 | 0 | virtio_scsi_config { |
441 | 0 | // num_queues is the number of request queues only so we subtract 2 for the control |
442 | 0 | // queue and the event queue. |
443 | 0 | num_queues: self.queue_sizes.len() as u32 - 2, |
444 | 0 | seg_max: self.seg_max, |
445 | 0 | max_sectors: MAX_SECTORS, |
446 | 0 | cmd_per_lun: MAX_CMD_PER_LUN, |
447 | 0 | event_info_size: std::mem::size_of::<virtio_scsi_event>() as u32, |
448 | 0 | sense_size: self.sense_size, |
449 | 0 | cdb_size: self.cdb_size, |
450 | 0 | max_channel: DEFAULT_MAX_CHANNEL, |
451 | 0 | max_target: DEFAULT_MAX_TARGET, |
452 | 0 | max_lun: DEFAULT_MAX_LUN, |
453 | 0 | } |
454 | 0 | } |
455 | | |
456 | | // Executes a request in the controlq. |
457 | 0 | fn execute_control( |
458 | 0 | reader: &mut Reader, |
459 | 0 | writer: &mut Writer, |
460 | 0 | target_ids: &BTreeSet<TargetId>, |
461 | 0 | ) -> Result<(), ExecuteError> { |
462 | 0 | let typ = reader.peek_obj::<u32>().map_err(ExecuteError::Read)?; |
463 | 0 | match typ { |
464 | | VIRTIO_SCSI_T_TMF => { |
465 | 0 | let tmf = reader |
466 | 0 | .read_obj::<virtio_scsi_ctrl_tmf_req>() |
467 | 0 | .map_err(ExecuteError::Read)?; |
468 | 0 | let resp = Self::execute_tmf(tmf, target_ids); |
469 | 0 | writer.write_obj(resp).map_err(ExecuteError::Write)?; |
470 | 0 | Ok(()) |
471 | | } |
472 | | VIRTIO_SCSI_T_AN_QUERY | VIRTIO_SCSI_T_AN_SUBSCRIBE => { |
473 | | // We do not support any asynchronous notification queries hence `event_actual` |
474 | | // will be 0. |
475 | 0 | let resp = virtio_scsi_ctrl_an_resp { |
476 | 0 | event_actual: 0, |
477 | 0 | response: VIRTIO_SCSI_S_OK as u8, |
478 | 0 | }; |
479 | 0 | writer.write_obj(resp).map_err(ExecuteError::Write)?; |
480 | 0 | Ok(()) |
481 | | } |
482 | | _ => { |
483 | 0 | error!("invalid type of a control request: {typ}"); |
484 | 0 | Err(ExecuteError::InvalidField) |
485 | | } |
486 | | } |
487 | 0 | } |
488 | | |
489 | | // Executes a TMF (task management function) request. |
490 | 0 | fn execute_tmf( |
491 | 0 | tmf: virtio_scsi_ctrl_tmf_req, |
492 | 0 | target_ids: &BTreeSet<TargetId>, |
493 | 0 | ) -> virtio_scsi_ctrl_tmf_resp { |
494 | 0 | match tmf.subtype { |
495 | | VIRTIO_SCSI_T_TMF_LOGICAL_UNIT_RESET | VIRTIO_SCSI_T_TMF_I_T_NEXUS_RESET => { |
496 | | // We only have LUN0. |
497 | 0 | let lun = tmf.lun; |
498 | 0 | let target_id = lun[1]; |
499 | 0 | let response = if target_ids.contains(&target_id) { |
500 | 0 | let is_lun0 = u16::from_be_bytes([lun[2], lun[3]]) & 0x3fff == 0; |
501 | 0 | if is_lun0 { |
502 | 0 | VIRTIO_SCSI_S_FUNCTION_SUCCEEDED as u8 |
503 | | } else { |
504 | 0 | VIRTIO_SCSI_S_INCORRECT_LUN as u8 |
505 | | } |
506 | | } else { |
507 | 0 | VIRTIO_SCSI_S_BAD_TARGET as u8 |
508 | | }; |
509 | 0 | virtio_scsi_ctrl_tmf_resp { response } |
510 | | } |
511 | 0 | subtype => { |
512 | 0 | error!("TMF request {subtype} is not supported"); |
513 | 0 | virtio_scsi_ctrl_tmf_resp { |
514 | 0 | response: VIRTIO_SCSI_S_FUNCTION_REJECTED as u8, |
515 | 0 | } |
516 | | } |
517 | | } |
518 | 0 | } |
519 | | |
520 | 0 | async fn execute_request( |
521 | 0 | reader: &mut Reader, |
522 | 0 | resp_writer: &mut Writer, |
523 | 0 | data_writer: &mut Writer, |
524 | 0 | targets: &BTreeMap<TargetId, AsyncLogicalUnit>, |
525 | 0 | sense_size: u32, |
526 | 0 | cdb_size: u32, |
527 | 0 | ) -> Result<(), ExecuteError> { |
528 | 0 | let req_header = reader |
529 | 0 | .read_obj::<VirtioScsiCmdReqHeader>() |
530 | 0 | .map_err(ExecuteError::Read)?; |
531 | 0 | match Self::get_logical_unit(req_header.lun, targets) { |
532 | 0 | Some(target) => { |
533 | 0 | let mut cdb = vec![0; cdb_size as usize]; |
534 | 0 | reader.read_exact(&mut cdb).map_err(ExecuteError::Read)?; |
535 | 0 | match execute_cdb(&cdb, reader, data_writer, target).await { |
536 | | Ok(()) => { |
537 | 0 | let hdr = VirtioScsiCmdRespHeader { |
538 | 0 | sense_len: 0, |
539 | 0 | resid: 0, |
540 | 0 | status_qualifier: 0, |
541 | 0 | status: GOOD, |
542 | 0 | response: VIRTIO_SCSI_S_OK as u8, |
543 | 0 | }; |
544 | 0 | resp_writer.write_obj(hdr).map_err(ExecuteError::Write)?; |
545 | 0 | resp_writer.consume_bytes(sense_size as usize); |
546 | 0 | Ok(()) |
547 | | } |
548 | 0 | Err(err) => { |
549 | 0 | error!("error while executing a scsi request: {err}"); |
550 | 0 | let (hdr, sense) = err.as_resp(); |
551 | 0 | resp_writer.write_obj(hdr).map_err(ExecuteError::Write)?; |
552 | 0 | sense.write_to(resp_writer, sense_size) |
553 | | } |
554 | | } |
555 | | } |
556 | | None => { |
557 | 0 | let hdr = VirtioScsiCmdRespHeader { |
558 | 0 | response: VIRTIO_SCSI_S_BAD_TARGET as u8, |
559 | 0 | ..Default::default() |
560 | 0 | }; |
561 | 0 | resp_writer.write_obj(hdr).map_err(ExecuteError::Write)?; |
562 | 0 | resp_writer.consume_bytes(sense_size as usize); |
563 | 0 | Ok(()) |
564 | | } |
565 | | } |
566 | 0 | } |
567 | | |
568 | 0 | fn get_logical_unit( |
569 | 0 | lun: [u8; 8], |
570 | 0 | targets: &BTreeMap<TargetId, AsyncLogicalUnit>, |
571 | 0 | ) -> Option<&AsyncLogicalUnit> { |
572 | | // First byte should be 1. |
573 | 0 | if lun[0] != 1 { |
574 | 0 | return None; |
575 | 0 | } |
576 | | // General search strategy for scsi devices is as follows: |
577 | | // 1) Look for a device which has the same bus id and lun indicated by the given lun. If |
578 | | // there is one, that is the target device. |
579 | | // 2) If we cannot find such device, then we return the first device that has the same bus |
580 | | // id. |
581 | | // Since we only support one LUN per target, we only need to use the target id. |
582 | 0 | let target_id = lun[1]; |
583 | 0 | targets.get(&target_id) |
584 | 0 | } |
585 | | } |
586 | | |
587 | | impl VirtioDevice for Controller { |
588 | 0 | fn keep_rds(&self) -> Vec<base::RawDescriptor> { |
589 | 0 | match &self.targets { |
590 | 0 | Some(targets) => targets |
591 | 0 | .0 |
592 | 0 | .values() |
593 | 0 | .flat_map(|t| t.disk_image.as_raw_descriptors()) |
594 | 0 | .collect(), |
595 | 0 | None => vec![], |
596 | | } |
597 | 0 | } |
598 | | |
599 | 0 | fn features(&self) -> u64 { |
600 | 0 | self.avail_features |
601 | 0 | } |
602 | | |
603 | 0 | fn device_type(&self) -> VirtioDeviceType { |
604 | 0 | VirtioDeviceType::Scsi |
605 | 0 | } |
606 | | |
607 | 0 | fn queue_max_sizes(&self) -> &[u16] { |
608 | 0 | &self.queue_sizes |
609 | 0 | } |
610 | | |
611 | 0 | fn read_config(&self, offset: u64, data: &mut [u8]) { |
612 | 0 | let config_space = self.build_config_space(); |
613 | 0 | copy_config(data, 0, config_space.as_bytes(), offset); |
614 | 0 | } |
615 | | |
616 | 0 | fn write_config(&mut self, offset: u64, data: &[u8]) { |
617 | 0 | let mut config = self.build_config_space(); |
618 | 0 | copy_config(config.as_mut_bytes(), offset, data, 0); |
619 | | // Only `sense_size` and `cdb_size` are modifiable by the driver. |
620 | 0 | self.sense_size = config.sense_size; |
621 | 0 | self.cdb_size = config.cdb_size; |
622 | 0 | } |
623 | | |
624 | 0 | fn activate( |
625 | 0 | &mut self, |
626 | 0 | _mem: GuestMemory, |
627 | 0 | _interrupt: Interrupt, |
628 | 0 | mut queues: BTreeMap<usize, Queue>, |
629 | 0 | ) -> anyhow::Result<()> { |
630 | 0 | let executor_kind = self.executor_kind; |
631 | | // 0th virtqueue is the controlq. |
632 | 0 | let controlq = queues.remove(&0).context("controlq should be present")?; |
633 | | // 1st virtqueue is the eventq. |
634 | | // We do not send any events through eventq. |
635 | 0 | let _eventq = queues.remove(&1).context("eventq should be present")?; |
636 | 0 | let targets = self.targets.take().context("failed to take SCSI targets")?; |
637 | 0 | let target_ids = targets.target_ids(); |
638 | 0 | let sense_size = self.sense_size; |
639 | 0 | let cdb_size = self.cdb_size; |
640 | | // The rest of the queues are request queues. |
641 | 0 | let request_queues = if self.multi_queue { |
642 | 0 | queues |
643 | 0 | .into_values() |
644 | 0 | .map(|queue| { |
645 | 0 | let targets = targets |
646 | 0 | .try_clone() |
647 | 0 | .context("Failed to clone a disk image")?; |
648 | 0 | Ok((queue, targets)) |
649 | 0 | }) |
650 | 0 | .collect::<anyhow::Result<_>>()? |
651 | | } else { |
652 | | // Handle all virtio requests with one thread. |
653 | 0 | vec![( |
654 | 0 | queues |
655 | 0 | .remove(&2) |
656 | 0 | .context("request queue should be present")?, |
657 | 0 | targets, |
658 | | )] |
659 | | }; |
660 | | |
661 | 0 | let worker_thread = WorkerThread::start("v_scsi_ctrlq", move |kill_evt| { |
662 | 0 | let ex = |
663 | 0 | Executor::with_executor_kind(executor_kind).expect("Failed to create an executor"); |
664 | 0 | if let Err(err) = ex |
665 | 0 | .run_until(run_worker( |
666 | 0 | &ex, |
667 | 0 | controlq, |
668 | 0 | kill_evt, |
669 | 0 | QueueType::Control { target_ids }, |
670 | 0 | sense_size, |
671 | 0 | cdb_size, |
672 | 0 | )) |
673 | 0 | .expect("run_until failed") |
674 | | { |
675 | 0 | error!("run_worker failed: {err}"); |
676 | 0 | } |
677 | 0 | }); |
678 | 0 | self.worker_threads.push(worker_thread); |
679 | | |
680 | 0 | for (i, (queue, targets)) in request_queues.into_iter().enumerate() { |
681 | 0 | let worker_thread = |
682 | 0 | WorkerThread::start(format!("v_scsi_req_{}", i + 2), move |kill_evt| { |
683 | 0 | let ex = Executor::with_executor_kind(executor_kind) |
684 | 0 | .expect("Failed to create an executor"); |
685 | 0 | let async_logical_unit = targets |
686 | 0 | .0 |
687 | 0 | .into_iter() |
688 | 0 | .map(|(idx, unit)| match unit.make_async(&ex) { |
689 | 0 | Ok(async_unit) => (idx, async_unit), |
690 | 0 | Err(err) => panic!("{err}"), |
691 | 0 | }) |
692 | 0 | .collect(); |
693 | 0 | if let Err(err) = ex |
694 | 0 | .run_until(run_worker( |
695 | 0 | &ex, |
696 | 0 | queue, |
697 | 0 | kill_evt, |
698 | 0 | QueueType::Request(async_logical_unit), |
699 | 0 | sense_size, |
700 | 0 | cdb_size, |
701 | 0 | )) |
702 | 0 | .expect("run_until failed") |
703 | | { |
704 | 0 | error!("run_worker failed: {err}"); |
705 | 0 | } |
706 | 0 | }); |
707 | 0 | self.worker_threads.push(worker_thread); |
708 | | } |
709 | 0 | Ok(()) |
710 | 0 | } |
711 | | } |
712 | | |
713 | | enum QueueType { |
714 | | Control { target_ids: BTreeSet<TargetId> }, |
715 | | Request(BTreeMap<TargetId, AsyncLogicalUnit>), |
716 | | } |
717 | | |
718 | 0 | async fn run_worker( |
719 | 0 | ex: &Executor, |
720 | 0 | queue: Queue, |
721 | 0 | kill_evt: Event, |
722 | 0 | queue_type: QueueType, |
723 | 0 | sense_size: u32, |
724 | 0 | cdb_size: u32, |
725 | 0 | ) -> anyhow::Result<()> { |
726 | 0 | let kill = async_utils::await_and_exit(ex, kill_evt).fuse(); |
727 | 0 | pin_mut!(kill); |
728 | | |
729 | 0 | let kick_evt = queue |
730 | 0 | .event() |
731 | 0 | .try_clone() |
732 | 0 | .expect("Failed to clone queue event"); |
733 | 0 | let queue_handler = handle_queue( |
734 | 0 | Rc::new(RefCell::new(queue)), |
735 | 0 | EventAsync::new(kick_evt, ex).expect("Failed to create async event for queue"), |
736 | 0 | queue_type, |
737 | 0 | sense_size, |
738 | 0 | cdb_size, |
739 | | ) |
740 | 0 | .fuse(); |
741 | 0 | pin_mut!(queue_handler); |
742 | | |
743 | 0 | futures::select! { |
744 | 0 | _ = queue_handler => anyhow::bail!("queue handler exited unexpectedly"), |
745 | 0 | r = kill => r.context("failed to wait on the kill event"), |
746 | | } |
747 | 0 | } |
748 | | |
749 | 0 | async fn handle_queue( |
750 | 0 | queue: Rc<RefCell<Queue>>, |
751 | 0 | evt: EventAsync, |
752 | 0 | queue_type: QueueType, |
753 | 0 | sense_size: u32, |
754 | 0 | cdb_size: u32, |
755 | 0 | ) { |
756 | 0 | let mut background_tasks = FuturesUnordered::new(); |
757 | 0 | let evt_future = evt.next_val().fuse(); |
758 | 0 | pin_mut!(evt_future); |
759 | | loop { |
760 | 0 | futures::select! { |
761 | 0 | _ = background_tasks.next() => continue, |
762 | 0 | res = evt_future => { |
763 | 0 | evt_future.set(evt.next_val().fuse()); |
764 | 0 | if let Err(e) = res { |
765 | 0 | error!("Failed to read the next queue event: {e}"); |
766 | 0 | continue; |
767 | 0 | } |
768 | | } |
769 | | } |
770 | 0 | while let Some(chain) = queue.borrow_mut().pop() { |
771 | 0 | background_tasks.push(process_one_chain( |
772 | 0 | &queue, |
773 | 0 | chain, |
774 | 0 | &queue_type, |
775 | 0 | sense_size, |
776 | 0 | cdb_size, |
777 | 0 | )); |
778 | 0 | } |
779 | | } |
780 | | } |
781 | | |
782 | 0 | async fn process_one_chain( |
783 | 0 | queue: &RefCell<Queue>, |
784 | 0 | mut avail_desc: DescriptorChain, |
785 | 0 | queue_type: &QueueType, |
786 | 0 | sense_size: u32, |
787 | 0 | cdb_size: u32, |
788 | 0 | ) { |
789 | 0 | let _trace = cros_tracing::trace_event!(VirtioScsi, "process_one_chain"); |
790 | 0 | let len = process_one_request(&mut avail_desc, queue_type, sense_size, cdb_size).await; |
791 | 0 | let mut queue = queue.borrow_mut(); |
792 | 0 | queue.add_used_with_bytes_written(avail_desc, len as u32); |
793 | 0 | queue.trigger_interrupt(); |
794 | 0 | } |
795 | | |
796 | 0 | async fn process_one_request( |
797 | 0 | avail_desc: &mut DescriptorChain, |
798 | 0 | queue_type: &QueueType, |
799 | 0 | sense_size: u32, |
800 | 0 | cdb_size: u32, |
801 | 0 | ) -> usize { |
802 | 0 | let reader = &mut avail_desc.reader; |
803 | 0 | let resp_writer = &mut avail_desc.writer; |
804 | 0 | match queue_type { |
805 | 0 | QueueType::Control { target_ids } => { |
806 | 0 | if let Err(err) = Controller::execute_control(reader, resp_writer, target_ids) { |
807 | 0 | error!("failed to execute control request: {err}"); |
808 | 0 | } |
809 | 0 | resp_writer.bytes_written() |
810 | | } |
811 | 0 | QueueType::Request(async_targets) => { |
812 | 0 | let mut data_writer = resp_writer |
813 | 0 | .split_at(std::mem::size_of::<VirtioScsiCmdRespHeader>() + sense_size as usize); |
814 | 0 | if let Err(err) = Controller::execute_request( |
815 | 0 | reader, |
816 | 0 | resp_writer, |
817 | 0 | &mut data_writer, |
818 | 0 | async_targets, |
819 | 0 | sense_size, |
820 | 0 | cdb_size, |
821 | | ) |
822 | 0 | .await |
823 | | { |
824 | | // If the write of the virtio_scsi_cmd_resp fails, there is nothing we can do to |
825 | | // inform the error to the guest driver (we usually propagate errors with sense |
826 | | // field, which is in the struct virtio_scsi_cmd_resp). The guest driver should |
827 | | // have at least sizeof(virtio_scsi_cmd_resp) bytes of device-writable part |
828 | | // regions. For now we simply emit an error message. |
829 | 0 | let (hdr, sense) = err.as_resp(); |
830 | 0 | if let Err(e) = resp_writer.write_obj(hdr) { |
831 | 0 | error!("failed to write VirtioScsiCmdRespHeader: {e}"); |
832 | 0 | } |
833 | 0 | if let Err(e) = sense.write_to(resp_writer, sense_size) { |
834 | 0 | error!("failed to write sense data: {e}"); |
835 | 0 | } |
836 | 0 | } |
837 | 0 | resp_writer.bytes_written() + data_writer.bytes_written() |
838 | | } |
839 | | } |
840 | 0 | } |
841 | | |
842 | | #[cfg(test)] |
843 | | mod tests { |
844 | | use std::fs::File; |
845 | | use std::mem::size_of; |
846 | | use std::mem::size_of_val; |
847 | | use std::rc::Rc; |
848 | | |
849 | | use cros_async::Executor; |
850 | | use disk::SingleFileDisk; |
851 | | use tempfile::tempfile; |
852 | | use virtio_sys::virtio_scsi::virtio_scsi_cmd_req; |
853 | | use virtio_sys::virtio_scsi::virtio_scsi_cmd_resp; |
854 | | use virtio_sys::virtio_scsi::VIRTIO_SCSI_S_OK; |
855 | | use vm_memory::GuestAddress; |
856 | | use vm_memory::GuestMemory; |
857 | | |
858 | | use super::*; |
859 | | use crate::virtio::create_descriptor_chain; |
860 | | use crate::virtio::scsi::constants::READ_10; |
861 | | use crate::virtio::DescriptorType; |
862 | | |
863 | | fn setup_disk(disk_size: u64) -> (File, Vec<u8>) { |
864 | | let mut file_content = vec![0; disk_size as usize]; |
865 | | for i in 0..disk_size { |
866 | | file_content[i as usize] = (i % 10) as u8; |
867 | | } |
868 | | let mut f = tempfile().unwrap(); |
869 | | f.set_len(disk_size).unwrap(); |
870 | | f.write_all(file_content.as_slice()).unwrap(); |
871 | | (f, file_content) |
872 | | } |
873 | | |
874 | | fn build_read_req_header(target_id: u8, start_lba: u8, xfer_blocks: u8) -> virtio_scsi_cmd_req { |
875 | | let mut cdb = [0; 32]; |
876 | | cdb[0] = READ_10; |
877 | | cdb[5] = start_lba; |
878 | | cdb[8] = xfer_blocks; |
879 | | virtio_scsi_cmd_req { |
880 | | lun: [1, 0, 0, target_id, 0, 0, 0, 0], |
881 | | cdb, |
882 | | ..Default::default() |
883 | | } |
884 | | } |
885 | | |
886 | | fn setup_desciptor_chain( |
887 | | target_id: TargetId, |
888 | | start_lba: u8, |
889 | | xfer_blocks: u8, |
890 | | block_size: u32, |
891 | | mem: &Rc<GuestMemory>, |
892 | | ) -> DescriptorChain { |
893 | | let req_hdr = build_read_req_header(target_id, start_lba, xfer_blocks); |
894 | | let xfer_bytes = xfer_blocks as u32 * block_size; |
895 | | create_descriptor_chain( |
896 | | mem, |
897 | | GuestAddress(0x100), // Place descriptor chain at 0x100. |
898 | | GuestAddress(0x1000), // Describe buffer at 0x1000. |
899 | | vec![ |
900 | | // Request header |
901 | | (DescriptorType::Readable, size_of_val(&req_hdr) as u32), |
902 | | // Response header |
903 | | ( |
904 | | DescriptorType::Writable, |
905 | | size_of::<virtio_scsi_cmd_resp>() as u32, |
906 | | ), |
907 | | (DescriptorType::Writable, xfer_bytes), |
908 | | ], |
909 | | 0, |
910 | | ) |
911 | | .expect("create_descriptor_chain failed") |
912 | | } |
913 | | |
914 | | fn read_blocks( |
915 | | ex: &Executor, |
916 | | file_disks: &[File], |
917 | | target_id: u8, |
918 | | start_lba: u8, |
919 | | xfer_blocks: u8, |
920 | | block_size: u32, |
921 | | ) -> (virtio_scsi_cmd_resp, Vec<u8>) { |
922 | | let xfer_bytes = xfer_blocks as u32 * block_size; |
923 | | let mem = Rc::new( |
924 | | GuestMemory::new(&[(GuestAddress(0u64), 4 * 1024 * 1024)]) |
925 | | .expect("Creating guest memory failed."), |
926 | | ); |
927 | | let req_hdr = build_read_req_header(target_id, start_lba, xfer_blocks); |
928 | | mem.write_obj_at_addr(req_hdr, GuestAddress(0x1000)) |
929 | | .expect("writing req failed"); |
930 | | |
931 | | let mut avail_desc = setup_desciptor_chain(target_id, 0, xfer_blocks, block_size, &mem); |
932 | | |
933 | | let targets = file_disks |
934 | | .iter() |
935 | | .enumerate() |
936 | | .map(|(i, file)| { |
937 | | let file = file.try_clone().unwrap(); |
938 | | let disk_image = Box::new(SingleFileDisk::new(file, ex).unwrap()); |
939 | | let logical_unit = AsyncLogicalUnit { |
940 | | last_lba: 0xFFF, |
941 | | block_size, |
942 | | read_only: false, |
943 | | disk_image, |
944 | | }; |
945 | | (i as TargetId, logical_unit) |
946 | | }) |
947 | | .collect(); |
948 | | ex.run_until(process_one_request( |
949 | | &mut avail_desc, |
950 | | &QueueType::Request(targets), |
951 | | VIRTIO_SCSI_SENSE_DEFAULT_SIZE, |
952 | | VIRTIO_SCSI_CDB_DEFAULT_SIZE, |
953 | | )) |
954 | | .expect("running executor failed"); |
955 | | let resp_offset = GuestAddress((0x1000 + size_of::<virtio_scsi_cmd_resp>()) as u64); |
956 | | let resp = mem |
957 | | .read_obj_from_addr::<virtio_scsi_cmd_resp>(resp_offset) |
958 | | .unwrap(); |
959 | | let dataout_offset = GuestAddress( |
960 | | (0x1000 + size_of::<virtio_scsi_cmd_req>() + size_of::<virtio_scsi_cmd_resp>()) as u64, |
961 | | ); |
962 | | let dataout_slice = mem |
963 | | .get_slice_at_addr(dataout_offset, xfer_bytes as usize) |
964 | | .unwrap(); |
965 | | let mut dataout = vec![0; xfer_bytes as usize]; |
966 | | dataout_slice.copy_to(&mut dataout); |
967 | | (resp, dataout) |
968 | | } |
969 | | |
970 | | fn test_read_blocks( |
971 | | num_targets: usize, |
972 | | blocks: u8, |
973 | | start_lba: u8, |
974 | | xfer_blocks: u8, |
975 | | block_size: u32, |
976 | | ) { |
977 | | let ex = Executor::new().expect("creating an executor failed"); |
978 | | let file_len = blocks as u64 * block_size as u64; |
979 | | let xfer_bytes = xfer_blocks as usize * block_size as usize; |
980 | | let start_off = start_lba as usize * block_size as usize; |
981 | | |
982 | | let (files, file_contents): (Vec<_>, Vec<_>) = |
983 | | (0..num_targets).map(|_| setup_disk(file_len)).unzip(); |
984 | | for (target_id, file_content) in file_contents.iter().enumerate() { |
985 | | let (resp, dataout) = read_blocks( |
986 | | &ex, |
987 | | &files, |
988 | | target_id as TargetId, |
989 | | start_lba, |
990 | | xfer_blocks, |
991 | | block_size, |
992 | | ); |
993 | | |
994 | | let sense_len = resp.sense_len; |
995 | | assert_eq!(sense_len, 0); |
996 | | assert_eq!(resp.status, VIRTIO_SCSI_S_OK as u8); |
997 | | assert_eq!(resp.response, GOOD); |
998 | | |
999 | | assert_eq!(&dataout, &file_content[start_off..(start_off + xfer_bytes)]); |
1000 | | } |
1001 | | } |
1002 | | |
1003 | | #[test] |
1004 | | fn read_first_blocks() { |
1005 | | // Read the first 3 blocks of a 8-block device. |
1006 | | let blocks = 8u8; |
1007 | | let start_lba = 0u8; |
1008 | | let xfer_blocks = 3u8; |
1009 | | |
1010 | | test_read_blocks(1, blocks, start_lba, xfer_blocks, 64u32); |
1011 | | test_read_blocks(1, blocks, start_lba, xfer_blocks, 128u32); |
1012 | | test_read_blocks(1, blocks, start_lba, xfer_blocks, 512u32); |
1013 | | } |
1014 | | |
1015 | | #[test] |
1016 | | fn read_middle_blocks() { |
1017 | | // Read 3 blocks from the 2nd block in the 8-block device. |
1018 | | let blocks = 8u8; |
1019 | | let start_lba = 1u8; |
1020 | | let xfer_blocks = 3u8; |
1021 | | |
1022 | | test_read_blocks(1, blocks, start_lba, xfer_blocks, 64u32); |
1023 | | test_read_blocks(1, blocks, start_lba, xfer_blocks, 128u32); |
1024 | | test_read_blocks(1, blocks, start_lba, xfer_blocks, 512u32); |
1025 | | } |
1026 | | |
1027 | | #[test] |
1028 | | fn read_first_blocks_with_multiple_disks() { |
1029 | | // Read the first 3 blocks of a 8-block device. |
1030 | | let blocks = 8u8; |
1031 | | let start_lba = 0u8; |
1032 | | let xfer_blocks = 3u8; |
1033 | | |
1034 | | test_read_blocks(3, blocks, start_lba, xfer_blocks, 64u32); |
1035 | | test_read_blocks(3, blocks, start_lba, xfer_blocks, 128u32); |
1036 | | test_read_blocks(3, blocks, start_lba, xfer_blocks, 512u32); |
1037 | | } |
1038 | | |
1039 | | #[test] |
1040 | | fn read_middle_blocks_with_multiple_disks() { |
1041 | | // Read 3 blocks from the 2nd block in the 8-block device. |
1042 | | let blocks = 8u8; |
1043 | | let start_lba = 1u8; |
1044 | | let xfer_blocks = 3u8; |
1045 | | |
1046 | | test_read_blocks(3, blocks, start_lba, xfer_blocks, 64u32); |
1047 | | test_read_blocks(3, blocks, start_lba, xfer_blocks, 128u32); |
1048 | | test_read_blocks(3, blocks, start_lba, xfer_blocks, 512u32); |
1049 | | } |
1050 | | } |