Skip to main content

disk_blockdevice/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4#![expect(missing_docs)]
5#![cfg(target_os = "linux")]
6
7//! Implements the [`DiskIo`] trait for virtual disks backed by a raw block
8//! device.
9
10// UNSAFETY: Issuing IOs and calling ioctls.
11#![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
54/// Opens a file for use with [`BlockDevice`] or
55/// [`disk_backend_resources::BlockDeviceDiskHandle`].
56pub 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
72/// A bounce buffer that may or may not be tracked by a
73/// [`BounceBufferTracker`].
74enum 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/// A storvsp disk backed by a raw block device.
96#[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/// New device error
182#[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    /// Constructs a new `BlockDevice` backed by the specified file.
198    ///
199    /// # Arguments
200    /// * `file` - The backing device opened for raw access.
201    /// * `read_only` - Indicates whether the device is opened for read-only access.
202    /// * `driver` - The async driver to use for issuing IOs (must support io-uring).
203    /// * `always_bounce` - Whether to always use bounce buffers for IOs, even for those that are aligned.
204    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            // Register for resize events.
230            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            // Discard on a file is serviced via `fallocate(PUNCH_HOLE)`, issued
249            // asynchronously through io-uring. Only offer it when the file is
250            // writable and the ring supports the FALLOCATE opcode (Linux 5.6+).
251            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    /// Use a box to avoid embedding a large `TrackedBounceBuffer` directly in
287    /// the calling future.
288    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
351/// Probes whether the filesystem backing `file` supports hole punching via
352/// `fallocate(FALLOC_FL_PUNCH_HOLE)`, used to service guest discard/unmap.
353///
354/// This is non-destructive: it punches a one-byte hole at the end of the file
355/// with `FALLOC_FL_KEEP_SIZE`, so the file size and existing data are
356/// unchanged. Filesystems that support the operation return success;
357/// unsupported ones return `EOPNOTSUPP` (or `ENOSYS` on ancient kernels).
358fn probe_file_punch_hole(file: &fs::File, file_size: u64) -> bool {
359    // SAFETY: FFI call with a valid fd owned by `file`. Punching past the end
360    // of the file with `FALLOC_FL_KEEP_SIZE` touches no existing data.
361    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        // Ensure the sysfs path exists.
412        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        // Check for NVMe by looking for the namespace ID.
420        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        // Fall back to unknown.
438        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        // sys/dev/block/*/*/size shows the size in 512-byte
457        // sectors irrespective of the block device
458        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        // Advertise discard support only when the backing filesystem can
484        // actually punch holes. Use the filesystem block size as the optimal
485        // unmap granularity, since `fallocate(PUNCH_HOLE)` only deallocates
486        // whole blocks.
487        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        // Memory behind an emulated IOMMU cannot be locked for zero-copy IO, so
592        // fall back to bounce buffering in that case.
593        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        // SAFETY: `io_vecs` and the underlying locked pages are locals
608        // in this `async fn`--they are part of the same state machine as
609        // the returned future and will not be freed before it completes
610        // or is dropped (which aborts).
611        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        // Ensure the write doesn't extend the file.
645        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        // Memory behind an emulated IOMMU cannot be locked for zero-copy IO, so
654        // fall back to bounce buffering in that case.
655        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        // SAFETY: `io_vecs` and the underlying locked pages are locals
669        // in this `async fn`--they are part of the same state machine as
670        // the returned future and will not be freed before it completes
671        // or is dropped (which aborts).
672        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        // SAFETY: No data buffers.
696        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        // Reject out-of-range requests up front: `fallocate` silently succeeds
723        // past the end of the file, so the bounds cannot be inferred from its
724        // result (as they can for reads and writes).
725        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        // When unmap is unsupported (e.g. a filesystem that cannot punch holes,
733        // or a block device that reports no discard granularity), report success
734        // and treat unmap as a no-op.
735        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            // Files can't service BLKDISCARD; punch a hole via io-uring instead,
744            // which deallocates the range and makes it read back as zero. Every
745            // error must be propagated: `unmap_behavior` reports
746            // `UnmapBehavior::Zeroes`, so the guest may skip zeroing this range
747            // itself. Swallowing a failure would leave stale, non-zero data
748            // while telling the guest it reads as zero.
749            DeviceType::File { .. } => {
750                // SAFETY: fallocate references no data buffers.
751                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            // Block devices discard via the BLKDISCARD ioctl on a blocking thread.
764            _ => {
765                let file = self.file.clone();
766                match unblock(move || ioctl::discard(&file, file_offset, length)).await {
767                    Ok(()) => Ok(()),
768                    // The device advertised discard support but can't actually
769                    // service the ioctl; treat unmap as a best-effort no-op.
770                    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            // `fallocate(PUNCH_HOLE)` deterministically zeroes the range: reads
793            // of a punched hole return zeroes.
794            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        // The Linux kernel interface to register does not allow ptpl to be
832        // configured. We could manually issue an NVMe command, but this code
833        // path is not really used anyway.
834        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        // 5.13 kernels seem to have a bug with io_uring where tests hang.
979        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        // TODO: switch to std::sync::OnceLock once `get_or_try_init` is stable
993        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    /// A loop device over a temporary file, detached when dropped.
1021    ///
1022    /// Attaching one requires `CAP_SYS_ADMIN`, which is why the test using this
1023    /// is ignored by default.
1024    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            // Detach on the way out even if the test panicked, so that loop
1054            // devices are not leaked.
1055            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    /// The shared sector-range conformance suite.
1085    ///
1086    /// This exercises the `DeviceType::File` path, where the kernel does not
1087    /// enforce bounds and the backend's own checks are the only thing stopping
1088    /// a write past the end from extending the file. See
1089    /// [`sector_range_conformance_loop_device`] for the other path.
1090    #[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    /// The same suite over a real block device.
1097    ///
1098    /// This is the case the tempfile path cannot cover. For a block device the
1099    /// kernel enforces the bounds and this backend delegates to it, rather than
1100    /// checking the range itself, so it is the delegation that is under test
1101    /// here.
1102    ///
1103    /// Attaching a loop device needs `CAP_SYS_ADMIN`, so this is ignored by
1104    /// default. To run it:
1105    ///
1106    /// ```text
1107    /// sudo -E $(which cargo) nextest run -p disk_blockdevice \
1108    ///     --run-ignored all -E 'test(loop_device)'
1109    /// ```
1110    #[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        // A writable file-backed device should advertise discard support via
1230        // punch-hole and report that unmapped ranges read back as zero.
1231        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        // Write two pages of non-zero data.
1241        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        // Unmap the first page and read both pages back.
1251        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        // Punched range reads as zero; the untouched page keeps its data.
1261        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}