1#![expect(missing_docs)]
5#![cfg(target_os = "linux")]
6
7#![expect(unsafe_code)]
12
13mod ioctl;
14mod nvme;
15pub mod resolver;
16
17use anyhow::Context;
18use blocking::unblock;
19use disk_backend::DiskError;
20use disk_backend::DiskIo;
21use disk_backend::UnmapBehavior;
22use disk_backend::pr::PersistentReservation;
23use disk_backend::pr::ReservationCapabilities;
24use disk_backend::pr::ReservationReport;
25use disk_backend::pr::ReservationType;
26use fs_err::PathExt;
27use guestmem::MemoryRead;
28use guestmem::MemoryWrite;
29use inspect::Inspect;
30use io_uring::opcode;
31use io_uring::types;
32use nvme::check_nvme_status;
33use nvme_spec::nvm;
34use pal::unix::affinity;
35use pal_async::driver::Driver;
36use scsi_buffers::BounceBuffer;
37use scsi_buffers::BounceBufferTracker;
38use scsi_buffers::RequestBuffers;
39use std::fmt::Debug;
40use std::fs;
41use std::os::unix::io::AsRawFd;
42use std::os::unix::prelude::FileTypeExt;
43use std::os::unix::prelude::MetadataExt;
44use std::path::Path;
45use std::path::PathBuf;
46use std::str::FromStr;
47use std::sync::Arc;
48use std::sync::atomic::AtomicU64;
49use std::sync::atomic::Ordering;
50use thiserror::Error;
51use uevent::CallbackHandle;
52use uevent::UeventListener;
53
54pub fn open_file_for_block(
57 path: &Path,
58 read_only: bool,
59 direct: bool,
60) -> std::io::Result<fs::File> {
61 use std::os::unix::prelude::*;
62
63 tracing::debug!(?path, read_only, direct, "open_file_for_block");
64 let mut opts = fs::OpenOptions::new();
65 opts.read(true).write(!read_only);
66 if direct {
67 opts.custom_flags(libc::O_DIRECT);
68 }
69 opts.open(path)
70}
71
72enum MaybeBounceBuffer<'a> {
75 Tracked(scsi_buffers::TrackedBounceBuffer<'a>),
76 Untracked(BounceBuffer),
77}
78
79impl MaybeBounceBuffer<'_> {
80 fn io_vecs(&self) -> &[scsi_buffers::IoBuffer<'_>] {
81 match self {
82 Self::Tracked(t) => t.buffer.io_vecs(),
83 Self::Untracked(b) => b.io_vecs(),
84 }
85 }
86
87 fn as_mut_bytes(&mut self) -> &mut [u8] {
88 match self {
89 Self::Tracked(t) => t.buffer.as_mut_bytes(),
90 Self::Untracked(b) => b.as_mut_bytes(),
91 }
92 }
93}
94
95#[derive(Inspect)]
97#[inspect(extra = "BlockDevice::inspect_extra")]
98pub struct BlockDevice {
99 file: Arc<fs::File>,
100 sector_size: u32,
101 physical_sector_size: u32,
102 sector_shift: u32,
103 sector_count: AtomicU64,
104 optimal_unmap_sectors: u32,
105 read_only: bool,
106 #[inspect(skip)]
107 driver: Box<dyn Driver>,
108 #[inspect(flatten)]
109 device_type: DeviceType,
110 supports_pr: bool,
111 supports_fua: bool,
112 #[inspect(skip)]
113 _uevent_filter: Option<CallbackHandle>,
114 resize_epoch: Arc<ResizeEpoch>,
115 resized_acked: AtomicU64,
116 #[inspect(skip)]
117 bounce_buffer_tracker: Option<Arc<BounceBufferTracker>>,
118 always_bounce: bool,
119}
120
121#[derive(Inspect, Debug, Default)]
122#[inspect(transparent)]
123struct ResizeEpoch {
124 epoch: AtomicU64,
125 #[inspect(skip)]
126 event: event_listener::Event,
127}
128
129#[derive(Debug, Copy, Clone, Inspect)]
130#[inspect(tag = "device_type")]
131enum DeviceType {
132 File {
133 sector_count: u64,
134 },
135 UnknownBlock,
136 NVMe {
137 ns_id: u32,
138 rescap: nvm::ReservationCapabilities,
139 },
140}
141
142impl BlockDevice {
143 fn inspect_extra(&self, resp: &mut inspect::Response<'_>) {
144 match self.device_type {
145 DeviceType::NVMe { .. } => {
146 resp.field_mut_with("interrupt_aggregation", |new_value| {
147 self.inspect_interrupt_coalescing(new_value)
148 });
149 }
150 DeviceType::UnknownBlock => {}
151 DeviceType::File { .. } => {}
152 }
153 }
154
155 fn inspect_interrupt_coalescing(&self, new_value: Option<&str>) -> anyhow::Result<String> {
156 let coalescing = if let Some(new_value) = new_value {
157 let coalescing = (|| {
158 let (threshold, time) = new_value.split_once(' ')?;
159 Some(
160 nvme::InterruptCoalescing::new()
161 .with_aggregation_threshold(threshold.parse().ok()?)
162 .with_aggregation_time(time.parse().ok()?),
163 )
164 })()
165 .context("expected `<aggregation_threshold> <aggregation_time>`")?;
166 nvme::nvme_set_features_interrupt_coalescing(&self.file, coalescing)?;
167 coalescing
168 } else if let Ok(coalescing) = nvme::nvme_get_features_interrupt_coalescing(&self.file) {
169 coalescing
170 } else {
171 return Ok("not supported".into());
172 };
173 Ok(format!(
174 "{} {}",
175 coalescing.aggregation_threshold(),
176 coalescing.aggregation_time()
177 ))
178 }
179}
180
181#[derive(Debug, Error)]
183pub enum NewDeviceError {
184 #[error("block device ioctl error")]
185 IoctlError(#[from] DiskError),
186 #[error("failed to read device metadata")]
187 DeviceMetadata(#[source] anyhow::Error),
188 #[error("invalid file type, not a file or block device")]
189 InvalidFileType,
190 #[error("invalid disk size {0:#x}")]
191 InvalidDiskSize(u64),
192 #[error("driver does not support io-uring")]
193 NoIoUring,
194}
195
196impl BlockDevice {
197 pub async fn new(
205 file: fs::File,
206 read_only: bool,
207 driver: impl Driver,
208 uevent_listener: Option<&UeventListener>,
209 bounce_buffer_tracker: Option<Arc<BounceBufferTracker>>,
210 always_bounce: bool,
211 ) -> Result<BlockDevice, NewDeviceError> {
212 if !driver.io_uring_probe(opcode::Read::CODE) {
213 return Err(NewDeviceError::NoIoUring);
214 }
215 assert!(driver.io_uring_probe(opcode::Write::CODE));
216 assert!(driver.io_uring_probe(opcode::Readv::CODE));
217 assert!(driver.io_uring_probe(opcode::Writev::CODE));
218 assert!(driver.io_uring_probe(opcode::Fsync::CODE));
219
220 let metadata = file.metadata().map_err(DiskError::Io)?;
221
222 let mut uevent_filter = None;
223 let resize_epoch = Arc::new(ResizeEpoch::default());
224
225 let devmeta = if metadata.file_type().is_block_device() {
226 let rdev = metadata.rdev();
227 let (major, minor) = (libc::major(rdev), libc::minor(rdev));
228
229 if let Some(uevent_listener) = uevent_listener {
231 let resize_epoch = resize_epoch.clone();
232 uevent_filter = Some(
233 uevent_listener
234 .add_block_resize_callback(major, minor, {
235 move || {
236 tracing::info!(major, minor, "disk resized");
237 resize_epoch.epoch.fetch_add(1, Ordering::SeqCst);
238 resize_epoch.event.notify(usize::MAX);
239 }
240 })
241 .await,
242 );
243 }
244
245 DeviceMetadata::from_block_device(&file, major, minor)
246 .map_err(NewDeviceError::DeviceMetadata)?
247 } else if metadata.file_type().is_file() {
248 let allow_discard = !read_only && driver.io_uring_probe(opcode::Fallocate::CODE);
252 DeviceMetadata::from_file(&file, &metadata, allow_discard)
253 .map_err(NewDeviceError::DeviceMetadata)?
254 } else {
255 return Err(NewDeviceError::InvalidFileType);
256 };
257
258 let sector_size = devmeta.logical_block_size;
259 let sector_shift = sector_size.trailing_zeros();
260 let physical_sector_size = devmeta.physical_block_size.max(sector_size);
261 let sector_count = devmeta.disk_size >> sector_shift;
262 let unmap_granularity = devmeta.discard_granularity >> sector_shift;
263 let file = Arc::new(file);
264 let device = BlockDevice {
265 file,
266 sector_size,
267 physical_sector_size,
268 sector_shift: sector_size.trailing_zeros(),
269 sector_count: sector_count.into(),
270 optimal_unmap_sectors: unmap_granularity,
271 read_only,
272 driver: Box::new(driver),
273 device_type: devmeta.device_type,
274 supports_pr: devmeta.supports_pr,
275 supports_fua: devmeta.fua,
276 _uevent_filter: uevent_filter,
277 resize_epoch,
278 resized_acked: 0.into(),
279 bounce_buffer_tracker,
280 always_bounce,
281 };
282
283 Ok(device)
284 }
285
286 async fn acquire_bounce_buffer(&self, size: usize) -> Box<MaybeBounceBuffer<'_>> {
289 Box::new(if let Some(tracker) = &self.bounce_buffer_tracker {
290 MaybeBounceBuffer::Tracked(
291 tracker
292 .acquire_bounce_buffers(size, affinity::get_cpu_number() as usize)
293 .await,
294 )
295 } else {
296 MaybeBounceBuffer::Untracked(BounceBuffer::new(size))
297 })
298 }
299
300 fn handle_resize(&self) {
301 if let Err(err) = self.handle_resize_inner() {
302 tracing::error!(
303 error = &err as &dyn std::error::Error,
304 "failed to update disk size"
305 );
306 }
307 }
308
309 fn handle_resize_inner(&self) -> std::io::Result<()> {
310 let mut acked = self.resized_acked.load(Ordering::SeqCst);
311 loop {
312 let epoch = self.resize_epoch.epoch.load(Ordering::SeqCst);
313 if acked == epoch {
314 break Ok(());
315 }
316
317 let size_in_bytes = ioctl::query_block_device_size_in_bytes(&self.file)?;
318
319 let new_sector_count = size_in_bytes / self.sector_size as u64;
320 let original_sector_count = self.sector_count.load(Ordering::SeqCst);
321
322 tracing::debug!(original_sector_count, new_sector_count, "resize");
323 if original_sector_count != new_sector_count {
324 tracing::info!(
325 original_sector_count,
326 new_sector_count,
327 "Disk size updating..."
328 );
329 self.sector_count.store(new_sector_count, Ordering::SeqCst);
330 }
331
332 acked = self
333 .resized_acked
334 .compare_exchange(acked, epoch, Ordering::SeqCst, Ordering::SeqCst)
335 .unwrap_or_else(|x| x);
336 }
337 }
338
339 fn map_io_error(&self, err: std::io::Error) -> DiskError {
340 if !matches!(self.device_type, DeviceType::File { .. }) {
341 match err.raw_os_error() {
342 Some(libc::EBADE) => return DiskError::ReservationConflict,
343 Some(libc::ENOSPC) => return DiskError::IllegalBlock,
344 _ => {}
345 }
346 }
347 DiskError::Io(err)
348 }
349}
350
351fn probe_file_punch_hole(file: &fs::File, file_size: u64) -> bool {
359 let ret = unsafe {
362 libc::fallocate(
363 file.as_raw_fd(),
364 libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE,
365 file_size as libc::off_t,
366 1,
367 )
368 };
369 if ret == 0 {
370 true
371 } else {
372 let err = std::io::Error::last_os_error();
373 tracing::debug!(
374 error = &err as &dyn std::error::Error,
375 "file does not support punch-hole discard; unmap will be a no-op"
376 );
377 false
378 }
379}
380
381struct DeviceMetadata {
382 device_type: DeviceType,
383 disk_size: u64,
384 logical_block_size: u32,
385 physical_block_size: u32,
386 discard_granularity: u32,
387 supports_pr: bool,
388 fua: bool,
389}
390
391fn block_device_queue_path(devpath: &Path) -> anyhow::Result<PathBuf> {
392 let device = if devpath
393 .join("partition")
394 .try_exists()
395 .context("failed to inspect sysfs partition state")?
396 {
397 let partition =
398 fs_err::canonicalize(devpath).context("failed to resolve sysfs partition path")?;
399 partition
400 .parent()
401 .context("sysfs partition path has no parent device")?
402 .to_path_buf()
403 } else {
404 devpath.to_path_buf()
405 };
406 Ok(device.join("queue"))
407}
408
409impl DeviceMetadata {
410 fn from_block_device(file: &fs::File, major: u32, minor: u32) -> anyhow::Result<Self> {
411 let devpath = PathBuf::from(format!("/sys/dev/block/{major}:{minor}"));
413 devpath
414 .fs_err_metadata()
415 .context("could not find sysfs path for block device")?;
416
417 let mut supports_pr = false;
418
419 let device_type = match fs_err::read_to_string(devpath.join("nsid")) {
421 Ok(ns_id) => {
422 let ns_id = ns_id
423 .trim()
424 .parse()
425 .context("failed to parse NVMe namespace ID")?;
426
427 let rescap = nvme::nvme_identify_namespace_data(file, ns_id)?.rescap;
428 let oncs = nvme::nvme_identify_controller_data(file)?.oncs;
429 tracing::debug!(rescap = ?rescap, oncs = ?oncs, "get identify data");
430 supports_pr = oncs.reservations() && u8::from(rescap) != 0;
431 Some(DeviceType::NVMe { ns_id, rescap })
432 }
433 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
434 Err(err) => Err(err).context("failed to read NVMe namespace ID")?,
435 };
436
437 let device_type = device_type.unwrap_or(DeviceType::UnknownBlock);
439
440 fn read_val<T: FromStr>(devpath: &Path, path: &str, msg: &str) -> anyhow::Result<T>
441 where
442 T::Err: 'static + std::error::Error + Send + Sync,
443 {
444 fs_err::read_to_string(devpath.join(path))
445 .with_context(|| format!("failed to read {msg}"))?
446 .trim()
447 .parse()
448 .with_context(|| format!("failed to parse {msg}"))
449 }
450
451 let queuepath = block_device_queue_path(&devpath)?;
452 let logical_block_size = read_val(&queuepath, "logical_block_size", "sector size")?;
453 let physical_block_size =
454 read_val(&queuepath, "physical_block_size", "physical sector size")?;
455
456 let disk_size = read_val::<u64>(&devpath, "size", "disk size")? * 512;
459 let discard_granularity =
460 read_val(&queuepath, "discard_granularity", "discard granularity")?;
461
462 let fua = read_val::<u8>(&queuepath, "fua", "fua")? != 0;
463
464 Self {
465 device_type,
466 disk_size,
467 logical_block_size,
468 physical_block_size,
469 discard_granularity,
470 supports_pr,
471 fua,
472 }
473 .validate()
474 }
475
476 fn from_file(
477 file: &fs::File,
478 metadata: &fs::Metadata,
479 allow_discard: bool,
480 ) -> anyhow::Result<Self> {
481 let logical_block_size = 512;
482 let physical_block_size = metadata.blksize() as u32;
483 let discard_granularity = if allow_discard && probe_file_punch_hole(file, metadata.size()) {
488 physical_block_size
489 } else {
490 0
491 };
492 Self {
493 device_type: DeviceType::File {
494 sector_count: metadata.len() / logical_block_size as u64,
495 },
496 disk_size: metadata.size(),
497 logical_block_size,
498 physical_block_size,
499 discard_granularity,
500 supports_pr: false,
501 fua: false,
502 }
503 .validate()
504 }
505
506 fn validate(self) -> anyhow::Result<Self> {
507 let Self {
508 device_type: _,
509 disk_size,
510 logical_block_size,
511 physical_block_size,
512 discard_granularity,
513 supports_pr: _,
514 fua: _,
515 } = self;
516 if logical_block_size < 512 || !logical_block_size.is_power_of_two() {
517 anyhow::bail!("invalid sector size {logical_block_size}");
518 }
519 if !physical_block_size.is_power_of_two() {
520 anyhow::bail!("invalid physical sector size {physical_block_size}");
521 }
522 if disk_size % logical_block_size as u64 != 0 {
523 anyhow::bail!("invalid disk size {disk_size:#x}");
524 }
525 if discard_granularity % logical_block_size != 0 {
526 anyhow::bail!("invalid discard granularity {discard_granularity}");
527 }
528 Ok(self)
529 }
530}
531
532impl DiskIo for BlockDevice {
533 fn disk_type(&self) -> &str {
534 "block_device"
535 }
536
537 fn sector_count(&self) -> u64 {
538 if self.resize_epoch.epoch.load(Ordering::Relaxed)
539 != self.resized_acked.load(Ordering::Relaxed)
540 {
541 self.handle_resize();
542 }
543 self.sector_count.load(Ordering::Relaxed)
544 }
545
546 fn sector_size(&self) -> u32 {
547 self.sector_size
548 }
549
550 fn disk_id(&self) -> Option<[u8; 16]> {
551 None
552 }
553
554 fn physical_sector_size(&self) -> u32 {
555 self.physical_sector_size
556 }
557
558 fn is_fua_respected(&self) -> bool {
559 self.supports_fua
560 }
561
562 fn is_read_only(&self) -> bool {
563 self.read_only
564 }
565
566 fn pr(&self) -> Option<&dyn PersistentReservation> {
567 if self.supports_pr { Some(self) } else { None }
568 }
569
570 async fn eject(&self) -> Result<(), DiskError> {
571 let file = self.file.clone();
572 unblock(move || {
573 ioctl::lockdoor(&file, false)?;
574 ioctl::eject(&file)
575 })
576 .await
577 .map_err(|err| self.map_io_error(err))?;
578 Ok(())
579 }
580
581 async fn read_vectored(
582 &self,
583 buffers: &RequestBuffers<'_>,
584 sector: u64,
585 ) -> Result<(), DiskError> {
586 let io_size = buffers.len();
587 tracing::trace!(sector, io_size, "read_vectored");
588
589 let mut bounce_buffer = None;
590 let locked;
591 let should_bounce = self.always_bounce
594 || !buffers.is_aligned(self.sector_size() as usize)
595 || !buffers.guest_memory().supports_kernel_lock();
596 let io_vecs = if !should_bounce {
597 locked = buffers.lock(true)?;
598 locked.io_vecs()
599 } else {
600 tracing::trace!("bounce buffering IO");
601
602 bounce_buffer
603 .insert(self.acquire_bounce_buffer(buffers.len()).await)
604 .io_vecs()
605 };
606
607 let bytes_read = unsafe {
612 self.driver.io_uring_submit(
613 opcode::Readv::new(
614 types::Fd(self.file.as_raw_fd()),
615 io_vecs.as_ptr().cast(),
616 io_vecs.len() as u32,
617 )
618 .offset((sector * self.sector_size() as u64) as _)
619 .build(),
620 )
621 }
622 .await
623 .map_err(|err| self.map_io_error(err))?;
624 tracing::trace!(bytes_read, "read_vectored");
625 if bytes_read != io_size as i32 {
626 return Err(DiskError::IllegalBlock);
627 }
628
629 if let Some(mut bounce_buffer) = bounce_buffer {
630 buffers.writer().write(bounce_buffer.as_mut_bytes())?;
631 }
632 Ok(())
633 }
634
635 async fn write_vectored(
636 &self,
637 buffers: &RequestBuffers<'_>,
638 sector: u64,
639 fua: bool,
640 ) -> Result<(), DiskError> {
641 let io_size = buffers.len();
642 tracing::trace!(sector, io_size, "write_vectored");
643
644 if let DeviceType::File { sector_count } = self.device_type {
646 if sector + (io_size as u64 >> self.sector_shift) > sector_count {
647 return Err(DiskError::IllegalBlock);
648 }
649 }
650
651 let mut bounce_buffer;
652 let locked;
653 let should_bounce = self.always_bounce
656 || !buffers.is_aligned(self.sector_size() as usize)
657 || !buffers.guest_memory().supports_kernel_lock();
658 let io_vecs = if !should_bounce {
659 locked = buffers.lock(false)?;
660 locked.io_vecs()
661 } else {
662 tracing::trace!("bounce buffering IO");
663 bounce_buffer = self.acquire_bounce_buffer(buffers.len()).await;
664 buffers.reader().read(bounce_buffer.as_mut_bytes())?;
665 bounce_buffer.io_vecs()
666 };
667
668 let bytes_written = unsafe {
673 self.driver.io_uring_submit(
674 opcode::Writev::new(
675 types::Fd(self.file.as_raw_fd()),
676 io_vecs.as_ptr().cast::<libc::iovec>(),
677 io_vecs.len() as _,
678 )
679 .offset((sector * self.sector_size() as u64) as _)
680 .rw_flags(if fua { libc::RWF_DSYNC } else { 0 })
681 .build(),
682 )
683 }
684 .await
685 .map_err(|err| self.map_io_error(err))?;
686 tracing::trace!(bytes_written, "write_vectored");
687 if bytes_written != io_size as i32 {
688 return Err(DiskError::IllegalBlock);
689 }
690
691 Ok(())
692 }
693
694 async fn sync_cache(&self) -> Result<(), DiskError> {
695 unsafe {
697 self.driver
698 .io_uring_submit(opcode::Fsync::new(types::Fd(self.file.as_raw_fd())).build())
699 }
700 .await
701 .map_err(|err| self.map_io_error(err))?;
702 Ok(())
703 }
704
705 async fn wait_resize(&self, sector_count: u64) -> u64 {
706 loop {
707 let listen = self.resize_epoch.event.listen();
708 let current = self.sector_count();
709 if current != sector_count {
710 break current;
711 }
712 listen.await;
713 }
714 }
715
716 async fn unmap(
717 &self,
718 sector_offset: u64,
719 sector_count: u64,
720 _block_level_only: bool,
721 ) -> Result<(), DiskError> {
722 if sector_offset
726 .checked_add(sector_count)
727 .is_none_or(|end| end > self.sector_count())
728 {
729 return Err(DiskError::IllegalBlock);
730 }
731
732 if self.optimal_unmap_sectors == 0 {
736 return Ok(());
737 }
738
739 let file_offset = sector_offset << self.sector_shift;
740 let length = sector_count << self.sector_shift;
741
742 match self.device_type {
743 DeviceType::File { .. } => {
750 unsafe {
752 self.driver.io_uring_submit(
753 opcode::Fallocate::new(types::Fd(self.file.as_raw_fd()), length)
754 .offset(file_offset)
755 .mode(libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE)
756 .build(),
757 )
758 }
759 .await
760 .map_err(|err| self.map_io_error(err))?;
761 Ok(())
762 }
763 _ => {
765 let file = self.file.clone();
766 match unblock(move || ioctl::discard(&file, file_offset, length)).await {
767 Ok(()) => Ok(()),
768 Err(err)
771 if matches!(
772 err.raw_os_error(),
773 Some(libc::ENOTTY | libc::EOPNOTSUPP | libc::ENOSYS)
774 ) =>
775 {
776 tracing::debug!(
777 error = &err as &dyn std::error::Error,
778 "discard not supported; ignoring"
779 );
780 Ok(())
781 }
782 Err(err) => Err(self.map_io_error(err)),
783 }
784 }
785 }
786 }
787
788 fn unmap_behavior(&self) -> UnmapBehavior {
789 if self.optimal_unmap_sectors == 0 {
790 UnmapBehavior::Ignored
791 } else if matches!(self.device_type, DeviceType::File { .. }) {
792 UnmapBehavior::Zeroes
795 } else {
796 UnmapBehavior::Unspecified
797 }
798 }
799
800 fn optimal_unmap_sectors(&self) -> u32 {
801 self.optimal_unmap_sectors
802 }
803}
804
805#[async_trait::async_trait]
806impl PersistentReservation for BlockDevice {
807 fn capabilities(&self) -> ReservationCapabilities {
808 match &self.device_type {
809 &DeviceType::NVMe { rescap, .. } => {
810 nvme_common::from_nvme_reservation_capabilities(rescap)
811 }
812 DeviceType::File { .. } | DeviceType::UnknownBlock => unreachable!(),
813 }
814 }
815
816 async fn report(&self) -> Result<ReservationReport, DiskError> {
817 assert!(matches!(self.device_type, DeviceType::NVMe { .. }));
818 self.nvme_persistent_reservation_report()
819 .await
820 .map_err(|err| self.map_io_error(err))
821 }
822
823 async fn register(
824 &self,
825 current_key: Option<u64>,
826 new_key: u64,
827 ptpl: Option<bool>,
828 ) -> Result<(), DiskError> {
829 assert!(matches!(self.device_type, DeviceType::NVMe { .. }));
830
831 if ptpl == Some(false) {
835 tracing::warn!("ignoring guest request to disable persist through power loss");
836 }
837
838 let file = self.file.clone();
839 unblock(move || {
840 ioctl::pr_register(
841 &file,
842 current_key.unwrap_or(0),
843 new_key,
844 if current_key.is_none() {
845 ioctl::PR_FL_IGNORE_KEY
846 } else {
847 0
848 },
849 )
850 })
851 .await
852 .and_then(check_nvme_status)
853 .map_err(|err| self.map_io_error(err))?;
854 Ok(())
855 }
856
857 async fn reserve(&self, key: u64, reservation_type: ReservationType) -> Result<(), DiskError> {
858 assert!(matches!(self.device_type, DeviceType::NVMe { .. }));
859 let file = self.file.clone();
860 unblock(move || ioctl::pr_reserve(&file, reservation_type, key))
861 .await
862 .and_then(check_nvme_status)
863 .map_err(|err| self.map_io_error(err))?;
864 Ok(())
865 }
866
867 async fn release(&self, key: u64, reservation_type: ReservationType) -> Result<(), DiskError> {
868 assert!(matches!(self.device_type, DeviceType::NVMe { .. }));
869 let file = self.file.clone();
870 unblock(move || ioctl::pr_release(&file, reservation_type, key))
871 .await
872 .and_then(check_nvme_status)
873 .map_err(|err| self.map_io_error(err))?;
874 Ok(())
875 }
876
877 async fn clear(&self, key: u64) -> Result<(), DiskError> {
878 assert!(matches!(self.device_type, DeviceType::NVMe { .. }));
879 let file = self.file.clone();
880 unblock(move || ioctl::pr_clear(&file, key))
881 .await
882 .and_then(check_nvme_status)
883 .map_err(|err| self.map_io_error(err))?;
884 Ok(())
885 }
886
887 async fn preempt(
888 &self,
889 current_key: u64,
890 preempt_key: u64,
891 reservation_type: ReservationType,
892 abort: bool,
893 ) -> Result<(), DiskError> {
894 assert!(matches!(self.device_type, DeviceType::NVMe { .. }));
895 let file = self.file.clone();
896 unblock(move || {
897 ioctl::pr_preempt(&file, reservation_type, current_key, preempt_key, abort)
898 })
899 .await
900 .and_then(check_nvme_status)
901 .map_err(|err| self.map_io_error(err))?;
902 Ok(())
903 }
904}
905
906#[cfg(test)]
907mod tests {
908 use super::*;
909 use futures::executor::block_on;
910 use guestmem::GuestMemory;
911 use hvdef::HV_PAGE_SIZE;
912 use hvdef::HV_PAGE_SIZE_USIZE;
913 use once_cell::sync::OnceCell;
914 use pal_async::async_test;
915 use pal_uring::IoUringPool;
916 use pal_uring::PoolClient;
917 use scsi_buffers::OwnedRequestBuffers;
918 use test_with_tracing::test;
919
920 #[test]
921 fn whole_device_queue_path_is_unchanged() {
922 let root = tempfile::tempdir().unwrap();
923 let device = root.path().join("nvme0n1");
924 fs::create_dir_all(device.join("queue")).unwrap();
925 fs::write(device.join("queue/logical_block_size"), "512\n").unwrap();
926 let alias = root.path().join("259:0");
927 std::os::unix::fs::symlink(&device, &alias).unwrap();
928
929 let queue = block_device_queue_path(&alias).unwrap();
930 assert_eq!(queue, alias.join("queue"));
931 assert_eq!(
932 fs::read_to_string(queue.join("logical_block_size")).unwrap(),
933 "512\n"
934 );
935 }
936
937 #[test]
938 fn partition_queue_path_uses_parent_device() {
939 let root = tempfile::tempdir().unwrap();
940 let device = root.path().join("nvme0n1");
941 let partition = device.join("nvme0n1p2");
942 fs::create_dir_all(&partition).unwrap();
943 fs::create_dir(device.join("queue")).unwrap();
944 fs::write(partition.join("partition"), "2\n").unwrap();
945 fs::write(device.join("queue/logical_block_size"), "512\n").unwrap();
946 fs::write(device.join("queue/physical_block_size"), "4096\n").unwrap();
947 let alias = root.path().join("259:2");
948 std::os::unix::fs::symlink(&partition, &alias).unwrap();
949
950 let queue = block_device_queue_path(&alias).unwrap();
951 assert_eq!(queue, device.join("queue"));
952 assert_eq!(
953 fs::read_to_string(queue.join("logical_block_size")).unwrap(),
954 "512\n"
955 );
956 assert_eq!(
957 fs::read_to_string(queue.join("physical_block_size")).unwrap(),
958 "4096\n"
959 );
960 assert!(!alias.join("queue").exists());
961 }
962
963 #[test]
964 fn partition_queue_path_does_not_fall_back_to_child_queue() {
965 let root = tempfile::tempdir().unwrap();
966 let device = root.path().join("nvme0n1");
967 let partition = device.join("nvme0n1p2");
968 fs::create_dir_all(partition.join("queue")).unwrap();
969 fs::write(partition.join("partition"), "2\n").unwrap();
970 fs::write(partition.join("queue/logical_block_size"), "4096\n").unwrap();
971
972 let queue = block_device_queue_path(&partition).unwrap();
973 assert_eq!(queue, device.join("queue"));
974 assert!(fs::read_to_string(queue.join("logical_block_size")).is_err());
975 }
976
977 fn is_buggy_kernel() -> bool {
978 let output = String::from_utf8(
980 std::process::Command::new("uname")
981 .arg("-r")
982 .output()
983 .unwrap()
984 .stdout,
985 )
986 .unwrap();
987
988 output.contains("5.13")
989 }
990
991 fn new_block_device_from(file: fs::File) -> Result<BlockDevice, NewDeviceError> {
992 static POOL: OnceCell<PoolClient> = OnceCell::new();
994
995 let client = POOL
996 .get_or_try_init(|| {
997 let pool = IoUringPool::new("test", 16)?;
998 let client = pool.client().clone();
999 std::thread::spawn(|| pool.run());
1000 Ok(client)
1001 })
1002 .map_err(|err| NewDeviceError::IoctlError(DiskError::Io(err)))?;
1003
1004 block_on(BlockDevice::new(
1005 file,
1006 false,
1007 client.initiator().clone(),
1008 None,
1009 None,
1010 false,
1011 ))
1012 }
1013
1014 fn new_block_device() -> Result<BlockDevice, NewDeviceError> {
1015 let test_file = tempfile::tempfile().unwrap();
1016 test_file.set_len(1024 * 64).unwrap();
1017 new_block_device_from(test_file)
1018 }
1019
1020 struct LoopDevice {
1025 path: String,
1026 _backing: tempfile::NamedTempFile,
1027 }
1028
1029 impl LoopDevice {
1030 fn new(size: u64) -> Self {
1031 let backing = tempfile::NamedTempFile::new().unwrap();
1032 backing.as_file().set_len(size).unwrap();
1033 let output = std::process::Command::new("losetup")
1034 .arg("--find")
1035 .arg("--show")
1036 .arg(backing.path())
1037 .output()
1038 .expect("losetup should be installed");
1039 assert!(
1040 output.status.success(),
1041 "losetup failed (needs root): {}",
1042 String::from_utf8_lossy(&output.stderr)
1043 );
1044 Self {
1045 path: String::from_utf8(output.stdout).unwrap().trim().to_owned(),
1046 _backing: backing,
1047 }
1048 }
1049 }
1050
1051 impl Drop for LoopDevice {
1052 fn drop(&mut self) {
1053 let _ = std::process::Command::new("losetup")
1056 .arg("-d")
1057 .arg(&self.path)
1058 .status();
1059 }
1060 }
1061
1062 macro_rules! get_block_device_or_skip {
1063 () => {
1064 match new_block_device() {
1065 Ok(pool) => {
1066 if is_buggy_kernel() {
1067 println!("Test case skipped (buggy kernel version)");
1068 return;
1069 }
1070
1071 pool
1072 }
1073 Err(NewDeviceError::IoctlError(DiskError::Io(err)))
1074 if err.raw_os_error() == Some(libc::ENOSYS) =>
1075 {
1076 println!("Test case skipped (no IO-Uring support)");
1077 return;
1078 }
1079 Err(err) => panic!("{}", err),
1080 }
1081 };
1082 }
1083
1084 #[async_test]
1091 async fn sector_range_conformance() {
1092 let disk = disk_backend::Disk::new(get_block_device_or_skip!()).unwrap();
1093 storage_tests::sector_range::test_disk_sector_range_conformance(&disk).await;
1094 }
1095
1096 #[async_test]
1111 #[ignore = "needs root to attach a loop device"]
1112 async fn sector_range_conformance_loop_device() {
1113 let loop_device = LoopDevice::new(1024 * 1024);
1114 let file = fs::OpenOptions::new()
1115 .read(true)
1116 .write(true)
1117 .open(&loop_device.path)
1118 .unwrap();
1119 let disk = disk_backend::Disk::new(new_block_device_from(file).unwrap()).unwrap();
1120 storage_tests::sector_range::test_disk_sector_range_conformance(&disk).await;
1121 }
1122
1123 async fn run_async_disk_io(fua: bool) {
1124 let disk = get_block_device_or_skip!();
1125
1126 let test_guest_mem = GuestMemory::allocate(0x8000);
1127 test_guest_mem
1128 .write_at(0, &(0..0x8000).map(|x| x as u8).collect::<Vec<_>>())
1129 .unwrap();
1130
1131 let write_buffers = OwnedRequestBuffers::new(&[3, 2, 1, 0]);
1132 disk.write_vectored(&write_buffers.buffer(&test_guest_mem), 0, fua)
1133 .await
1134 .unwrap();
1135
1136 if !fua {
1137 disk.sync_cache().await.unwrap();
1138 }
1139
1140 let read_buffers = OwnedRequestBuffers::new(&[7, 6, 5, 4]);
1141 disk.read_vectored(&read_buffers.buffer(&test_guest_mem), 0)
1142 .await
1143 .unwrap();
1144
1145 let mut source = vec![0u8; 4 * HV_PAGE_SIZE_USIZE];
1146 test_guest_mem.read_at(0, &mut source).unwrap();
1147 let mut target = vec![0u8; 4 * HV_PAGE_SIZE_USIZE];
1148 test_guest_mem
1149 .read_at(4 * HV_PAGE_SIZE, &mut target)
1150 .unwrap();
1151 assert_eq!(source, target);
1152 }
1153
1154 #[async_test]
1155 async fn test_async_disk_io() {
1156 run_async_disk_io(false).await;
1157 }
1158
1159 #[async_test]
1160 async fn test_async_disk_io_fua() {
1161 run_async_disk_io(true).await;
1162 }
1163
1164 async fn run_async_disk_io_unaligned(fua: bool) {
1165 let disk = get_block_device_or_skip!();
1166
1167 let test_guest_mem = GuestMemory::allocate(0x8000);
1168 test_guest_mem
1169 .write_at(0, &(0..0x8000).map(|x| x as u8).collect::<Vec<_>>())
1170 .unwrap();
1171
1172 let write_buffers =
1173 OwnedRequestBuffers::new_unaligned(&[0, 1, 2, 3], 512, 3 * HV_PAGE_SIZE_USIZE);
1174
1175 disk.write_vectored(&write_buffers.buffer(&test_guest_mem), 0, fua)
1176 .await
1177 .unwrap();
1178
1179 if !fua {
1180 disk.sync_cache().await.unwrap();
1181 }
1182
1183 let read_buffers =
1184 OwnedRequestBuffers::new_unaligned(&[4, 5, 6, 7], 512, 3 * HV_PAGE_SIZE_USIZE);
1185 disk.read_vectored(&read_buffers.buffer(&test_guest_mem), 0)
1186 .await
1187 .unwrap();
1188
1189 let mut source = vec![0u8; 3 * HV_PAGE_SIZE_USIZE];
1190 test_guest_mem.read_at(512, &mut source).unwrap();
1191 let mut target = vec![0u8; 3 * HV_PAGE_SIZE_USIZE];
1192 test_guest_mem
1193 .read_at(4 * HV_PAGE_SIZE + 512, &mut target)
1194 .unwrap();
1195 assert_eq!(source, target);
1196 }
1197
1198 #[async_test]
1199 async fn test_async_disk_io_unaligned() {
1200 run_async_disk_io_unaligned(false).await;
1201 }
1202
1203 #[async_test]
1204 async fn test_async_disk_io_unaligned_fua() {
1205 run_async_disk_io_unaligned(true).await;
1206 }
1207
1208 #[async_test]
1209 async fn test_illegal_lba() {
1210 let disk = get_block_device_or_skip!();
1211 let gm = GuestMemory::allocate(512);
1212 match disk
1213 .write_vectored(
1214 &OwnedRequestBuffers::linear(0, 512, true).buffer(&gm),
1215 i64::MAX as u64 / 512,
1216 false,
1217 )
1218 .await
1219 {
1220 Err(DiskError::IllegalBlock) => {}
1221 r => panic!("unexpected result: {:?}", r),
1222 }
1223 }
1224
1225 #[async_test]
1226 async fn test_unmap_file_punch_hole() {
1227 let disk = get_block_device_or_skip!();
1228
1229 if disk.optimal_unmap_sectors() == 0 {
1232 println!("Test case skipped (filesystem does not support punch hole)");
1233 return;
1234 }
1235 assert_eq!(disk.unmap_behavior(), UnmapBehavior::Zeroes);
1236
1237 let gm = GuestMemory::allocate(0x2000);
1238 gm.write_at(0, &vec![0xcdu8; 0x2000]).unwrap();
1239
1240 disk.write_vectored(
1242 &OwnedRequestBuffers::linear(0, 0x2000, false).buffer(&gm),
1243 0,
1244 false,
1245 )
1246 .await
1247 .unwrap();
1248 disk.sync_cache().await.unwrap();
1249
1250 disk.unmap(0, 0x1000 >> disk.sector_shift, false)
1252 .await
1253 .unwrap();
1254
1255 let read = OwnedRequestBuffers::linear(0, 0x2000, true);
1256 disk.read_vectored(&read.buffer(&gm), 0).await.unwrap();
1257
1258 let mut buf = vec![0xffu8; 0x2000];
1259 gm.read_at(0, &mut buf).unwrap();
1260 assert!(
1262 buf[..0x1000].iter().all(|&b| b == 0),
1263 "unmapped range not zeroed"
1264 );
1265 assert!(
1266 buf[0x1000..].iter().all(|&b| b == 0xcd),
1267 "second page corrupted"
1268 );
1269 }
1270}