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
391impl DeviceMetadata {
392    fn from_block_device(file: &fs::File, major: u32, minor: u32) -> anyhow::Result<Self> {
393        // Ensure the sysfs path exists.
394        let devpath = PathBuf::from(format!("/sys/dev/block/{major}:{minor}"));
395        devpath
396            .fs_err_metadata()
397            .context("could not find sysfs path for block device")?;
398
399        let mut supports_pr = false;
400
401        // Check for NVMe by looking for the namespace ID.
402        let device_type = match fs_err::read_to_string(devpath.join("nsid")) {
403            Ok(ns_id) => {
404                let ns_id = ns_id
405                    .trim()
406                    .parse()
407                    .context("failed to parse NVMe namespace ID")?;
408
409                let rescap = nvme::nvme_identify_namespace_data(file, ns_id)?.rescap;
410                let oncs = nvme::nvme_identify_controller_data(file)?.oncs;
411                tracing::debug!(rescap = ?rescap, oncs = ?oncs, "get identify data");
412                supports_pr = oncs.reservations() && u8::from(rescap) != 0;
413                Some(DeviceType::NVMe { ns_id, rescap })
414            }
415            Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
416            Err(err) => Err(err).context("failed to read NVMe namespace ID")?,
417        };
418
419        // Fall back to unknown.
420        let device_type = device_type.unwrap_or(DeviceType::UnknownBlock);
421
422        fn read_val<T: FromStr>(devpath: &Path, path: &str, msg: &str) -> anyhow::Result<T>
423        where
424            T::Err: 'static + std::error::Error + Send + Sync,
425        {
426            fs_err::read_to_string(devpath.join(path))
427                .with_context(|| format!("failed to read {msg}"))?
428                .trim()
429                .parse()
430                .with_context(|| format!("failed to parse {msg}"))
431        }
432
433        let logical_block_size = read_val(&devpath, "queue/logical_block_size", "sector size")?;
434        let physical_block_size = read_val(
435            &devpath,
436            "queue/physical_block_size",
437            "physical sector size",
438        )?;
439
440        // sys/dev/block/*/*/size shows the size in 512-byte
441        // sectors irrespective of the block device
442        let disk_size = read_val::<u64>(&devpath, "size", "disk size")? * 512;
443        let discard_granularity =
444            read_val(&devpath, "queue/discard_granularity", "discard granularity")?;
445
446        let fua = read_val::<u8>(&devpath, "queue/fua", "fua")? != 0;
447
448        Self {
449            device_type,
450            disk_size,
451            logical_block_size,
452            physical_block_size,
453            discard_granularity,
454            supports_pr,
455            fua,
456        }
457        .validate()
458    }
459
460    fn from_file(
461        file: &fs::File,
462        metadata: &fs::Metadata,
463        allow_discard: bool,
464    ) -> anyhow::Result<Self> {
465        let logical_block_size = 512;
466        let physical_block_size = metadata.blksize() as u32;
467        // Advertise discard support only when the backing filesystem can
468        // actually punch holes. Use the filesystem block size as the optimal
469        // unmap granularity, since `fallocate(PUNCH_HOLE)` only deallocates
470        // whole blocks.
471        let discard_granularity = if allow_discard && probe_file_punch_hole(file, metadata.size()) {
472            physical_block_size
473        } else {
474            0
475        };
476        Self {
477            device_type: DeviceType::File {
478                sector_count: metadata.len() / logical_block_size as u64,
479            },
480            disk_size: metadata.size(),
481            logical_block_size,
482            physical_block_size,
483            discard_granularity,
484            supports_pr: false,
485            fua: false,
486        }
487        .validate()
488    }
489
490    fn validate(self) -> anyhow::Result<Self> {
491        let Self {
492            device_type: _,
493            disk_size,
494            logical_block_size,
495            physical_block_size,
496            discard_granularity,
497            supports_pr: _,
498            fua: _,
499        } = self;
500        if logical_block_size < 512 || !logical_block_size.is_power_of_two() {
501            anyhow::bail!("invalid sector size {logical_block_size}");
502        }
503        if !physical_block_size.is_power_of_two() {
504            anyhow::bail!("invalid physical sector size {physical_block_size}");
505        }
506        if disk_size % logical_block_size as u64 != 0 {
507            anyhow::bail!("invalid disk size {disk_size:#x}");
508        }
509        if discard_granularity % logical_block_size != 0 {
510            anyhow::bail!("invalid discard granularity {discard_granularity}");
511        }
512        Ok(self)
513    }
514}
515
516impl DiskIo for BlockDevice {
517    fn disk_type(&self) -> &str {
518        "block_device"
519    }
520
521    fn sector_count(&self) -> u64 {
522        if self.resize_epoch.epoch.load(Ordering::Relaxed)
523            != self.resized_acked.load(Ordering::Relaxed)
524        {
525            self.handle_resize();
526        }
527        self.sector_count.load(Ordering::Relaxed)
528    }
529
530    fn sector_size(&self) -> u32 {
531        self.sector_size
532    }
533
534    fn disk_id(&self) -> Option<[u8; 16]> {
535        None
536    }
537
538    fn physical_sector_size(&self) -> u32 {
539        self.physical_sector_size
540    }
541
542    fn is_fua_respected(&self) -> bool {
543        self.supports_fua
544    }
545
546    fn is_read_only(&self) -> bool {
547        self.read_only
548    }
549
550    fn pr(&self) -> Option<&dyn PersistentReservation> {
551        if self.supports_pr { Some(self) } else { None }
552    }
553
554    async fn eject(&self) -> Result<(), DiskError> {
555        let file = self.file.clone();
556        unblock(move || {
557            ioctl::lockdoor(&file, false)?;
558            ioctl::eject(&file)
559        })
560        .await
561        .map_err(|err| self.map_io_error(err))?;
562        Ok(())
563    }
564
565    async fn read_vectored(
566        &self,
567        buffers: &RequestBuffers<'_>,
568        sector: u64,
569    ) -> Result<(), DiskError> {
570        let io_size = buffers.len();
571        tracing::trace!(sector, io_size, "read_vectored");
572
573        let mut bounce_buffer = None;
574        let locked;
575        // Memory behind an emulated IOMMU cannot be locked for zero-copy IO, so
576        // fall back to bounce buffering in that case.
577        let should_bounce = self.always_bounce
578            || !buffers.is_aligned(self.sector_size() as usize)
579            || !buffers.guest_memory().supports_locking();
580        let io_vecs = if !should_bounce {
581            locked = buffers.lock(true)?;
582            locked.io_vecs()
583        } else {
584            tracing::trace!("bounce buffering IO");
585
586            bounce_buffer
587                .insert(self.acquire_bounce_buffer(buffers.len()).await)
588                .io_vecs()
589        };
590
591        // SAFETY: `io_vecs` and the underlying locked pages are locals
592        // in this `async fn`--they are part of the same state machine as
593        // the returned future and will not be freed before it completes
594        // or is dropped (which aborts).
595        let bytes_read = unsafe {
596            self.driver.io_uring_submit(
597                opcode::Readv::new(
598                    types::Fd(self.file.as_raw_fd()),
599                    io_vecs.as_ptr().cast(),
600                    io_vecs.len() as u32,
601                )
602                .offset((sector * self.sector_size() as u64) as _)
603                .build(),
604            )
605        }
606        .await
607        .map_err(|err| self.map_io_error(err))?;
608        tracing::trace!(bytes_read, "read_vectored");
609        if bytes_read != io_size as i32 {
610            return Err(DiskError::IllegalBlock);
611        }
612
613        if let Some(mut bounce_buffer) = bounce_buffer {
614            buffers.writer().write(bounce_buffer.as_mut_bytes())?;
615        }
616        Ok(())
617    }
618
619    async fn write_vectored(
620        &self,
621        buffers: &RequestBuffers<'_>,
622        sector: u64,
623        fua: bool,
624    ) -> Result<(), DiskError> {
625        let io_size = buffers.len();
626        tracing::trace!(sector, io_size, "write_vectored");
627
628        // Ensure the write doesn't extend the file.
629        if let DeviceType::File { sector_count } = self.device_type {
630            if sector + (io_size as u64 >> self.sector_shift) > sector_count {
631                return Err(DiskError::IllegalBlock);
632            }
633        }
634
635        let mut bounce_buffer;
636        let locked;
637        // Memory behind an emulated IOMMU cannot be locked for zero-copy IO, so
638        // fall back to bounce buffering in that case.
639        let should_bounce = self.always_bounce
640            || !buffers.is_aligned(self.sector_size() as usize)
641            || !buffers.guest_memory().supports_locking();
642        let io_vecs = if !should_bounce {
643            locked = buffers.lock(false)?;
644            locked.io_vecs()
645        } else {
646            tracing::trace!("bounce buffering IO");
647            bounce_buffer = self.acquire_bounce_buffer(buffers.len()).await;
648            buffers.reader().read(bounce_buffer.as_mut_bytes())?;
649            bounce_buffer.io_vecs()
650        };
651
652        // SAFETY: `io_vecs` and the underlying locked pages are locals
653        // in this `async fn`--they are part of the same state machine as
654        // the returned future and will not be freed before it completes
655        // or is dropped (which aborts).
656        let bytes_written = unsafe {
657            self.driver.io_uring_submit(
658                opcode::Writev::new(
659                    types::Fd(self.file.as_raw_fd()),
660                    io_vecs.as_ptr().cast::<libc::iovec>(),
661                    io_vecs.len() as _,
662                )
663                .offset((sector * self.sector_size() as u64) as _)
664                .rw_flags(if fua { libc::RWF_DSYNC } else { 0 })
665                .build(),
666            )
667        }
668        .await
669        .map_err(|err| self.map_io_error(err))?;
670        tracing::trace!(bytes_written, "write_vectored");
671        if bytes_written != io_size as i32 {
672            return Err(DiskError::IllegalBlock);
673        }
674
675        Ok(())
676    }
677
678    async fn sync_cache(&self) -> Result<(), DiskError> {
679        // SAFETY: No data buffers.
680        unsafe {
681            self.driver
682                .io_uring_submit(opcode::Fsync::new(types::Fd(self.file.as_raw_fd())).build())
683        }
684        .await
685        .map_err(|err| self.map_io_error(err))?;
686        Ok(())
687    }
688
689    async fn wait_resize(&self, sector_count: u64) -> u64 {
690        loop {
691            let listen = self.resize_epoch.event.listen();
692            let current = self.sector_count();
693            if current != sector_count {
694                break current;
695            }
696            listen.await;
697        }
698    }
699
700    async fn unmap(
701        &self,
702        sector_offset: u64,
703        sector_count: u64,
704        _block_level_only: bool,
705    ) -> Result<(), DiskError> {
706        // Reject out-of-range requests up front: `fallocate` silently succeeds
707        // past the end of the file, so the bounds cannot be inferred from its
708        // result (as they can for reads and writes).
709        if sector_offset
710            .checked_add(sector_count)
711            .is_none_or(|end| end > self.sector_count())
712        {
713            return Err(DiskError::IllegalBlock);
714        }
715
716        // When unmap is unsupported (e.g. a filesystem that cannot punch holes,
717        // or a block device that reports no discard granularity), report success
718        // and treat unmap as a no-op.
719        if self.optimal_unmap_sectors == 0 {
720            return Ok(());
721        }
722
723        let file_offset = sector_offset << self.sector_shift;
724        let length = sector_count << self.sector_shift;
725
726        match self.device_type {
727            // Files can't service BLKDISCARD; punch a hole via io-uring instead,
728            // which deallocates the range and makes it read back as zero. Every
729            // error must be propagated: `unmap_behavior` reports
730            // `UnmapBehavior::Zeroes`, so the guest may skip zeroing this range
731            // itself. Swallowing a failure would leave stale, non-zero data
732            // while telling the guest it reads as zero.
733            DeviceType::File { .. } => {
734                // SAFETY: fallocate references no data buffers.
735                unsafe {
736                    self.driver.io_uring_submit(
737                        opcode::Fallocate::new(types::Fd(self.file.as_raw_fd()), length)
738                            .offset(file_offset)
739                            .mode(libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE)
740                            .build(),
741                    )
742                }
743                .await
744                .map_err(|err| self.map_io_error(err))?;
745                Ok(())
746            }
747            // Block devices discard via the BLKDISCARD ioctl on a blocking thread.
748            _ => {
749                let file = self.file.clone();
750                match unblock(move || ioctl::discard(&file, file_offset, length)).await {
751                    Ok(()) => Ok(()),
752                    // The device advertised discard support but can't actually
753                    // service the ioctl; treat unmap as a best-effort no-op.
754                    Err(err)
755                        if matches!(
756                            err.raw_os_error(),
757                            Some(libc::ENOTTY | libc::EOPNOTSUPP | libc::ENOSYS)
758                        ) =>
759                    {
760                        tracing::debug!(
761                            error = &err as &dyn std::error::Error,
762                            "discard not supported; ignoring"
763                        );
764                        Ok(())
765                    }
766                    Err(err) => Err(self.map_io_error(err)),
767                }
768            }
769        }
770    }
771
772    fn unmap_behavior(&self) -> UnmapBehavior {
773        if self.optimal_unmap_sectors == 0 {
774            UnmapBehavior::Ignored
775        } else if matches!(self.device_type, DeviceType::File { .. }) {
776            // `fallocate(PUNCH_HOLE)` deterministically zeroes the range: reads
777            // of a punched hole return zeroes.
778            UnmapBehavior::Zeroes
779        } else {
780            UnmapBehavior::Unspecified
781        }
782    }
783
784    fn optimal_unmap_sectors(&self) -> u32 {
785        self.optimal_unmap_sectors
786    }
787}
788
789#[async_trait::async_trait]
790impl PersistentReservation for BlockDevice {
791    fn capabilities(&self) -> ReservationCapabilities {
792        match &self.device_type {
793            &DeviceType::NVMe { rescap, .. } => {
794                nvme_common::from_nvme_reservation_capabilities(rescap)
795            }
796            DeviceType::File { .. } | DeviceType::UnknownBlock => unreachable!(),
797        }
798    }
799
800    async fn report(&self) -> Result<ReservationReport, DiskError> {
801        assert!(matches!(self.device_type, DeviceType::NVMe { .. }));
802        self.nvme_persistent_reservation_report()
803            .await
804            .map_err(|err| self.map_io_error(err))
805    }
806
807    async fn register(
808        &self,
809        current_key: Option<u64>,
810        new_key: u64,
811        ptpl: Option<bool>,
812    ) -> Result<(), DiskError> {
813        assert!(matches!(self.device_type, DeviceType::NVMe { .. }));
814
815        // The Linux kernel interface to register does not allow ptpl to be
816        // configured. We could manually issue an NVMe command, but this code
817        // path is not really used anyway.
818        if ptpl == Some(false) {
819            tracing::warn!("ignoring guest request to disable persist through power loss");
820        }
821
822        let file = self.file.clone();
823        unblock(move || {
824            ioctl::pr_register(
825                &file,
826                current_key.unwrap_or(0),
827                new_key,
828                if current_key.is_none() {
829                    ioctl::PR_FL_IGNORE_KEY
830                } else {
831                    0
832                },
833            )
834        })
835        .await
836        .and_then(check_nvme_status)
837        .map_err(|err| self.map_io_error(err))?;
838        Ok(())
839    }
840
841    async fn reserve(&self, key: u64, reservation_type: ReservationType) -> Result<(), DiskError> {
842        assert!(matches!(self.device_type, DeviceType::NVMe { .. }));
843        let file = self.file.clone();
844        unblock(move || ioctl::pr_reserve(&file, reservation_type, key))
845            .await
846            .and_then(check_nvme_status)
847            .map_err(|err| self.map_io_error(err))?;
848        Ok(())
849    }
850
851    async fn release(&self, key: u64, reservation_type: ReservationType) -> Result<(), DiskError> {
852        assert!(matches!(self.device_type, DeviceType::NVMe { .. }));
853        let file = self.file.clone();
854        unblock(move || ioctl::pr_release(&file, reservation_type, key))
855            .await
856            .and_then(check_nvme_status)
857            .map_err(|err| self.map_io_error(err))?;
858        Ok(())
859    }
860
861    async fn clear(&self, key: u64) -> Result<(), DiskError> {
862        assert!(matches!(self.device_type, DeviceType::NVMe { .. }));
863        let file = self.file.clone();
864        unblock(move || ioctl::pr_clear(&file, key))
865            .await
866            .and_then(check_nvme_status)
867            .map_err(|err| self.map_io_error(err))?;
868        Ok(())
869    }
870
871    async fn preempt(
872        &self,
873        current_key: u64,
874        preempt_key: u64,
875        reservation_type: ReservationType,
876        abort: bool,
877    ) -> Result<(), DiskError> {
878        assert!(matches!(self.device_type, DeviceType::NVMe { .. }));
879        let file = self.file.clone();
880        unblock(move || {
881            ioctl::pr_preempt(&file, reservation_type, current_key, preempt_key, abort)
882        })
883        .await
884        .and_then(check_nvme_status)
885        .map_err(|err| self.map_io_error(err))?;
886        Ok(())
887    }
888}
889
890#[cfg(test)]
891mod tests {
892    use super::*;
893    use futures::executor::block_on;
894    use guestmem::GuestMemory;
895    use hvdef::HV_PAGE_SIZE;
896    use hvdef::HV_PAGE_SIZE_USIZE;
897    use once_cell::sync::OnceCell;
898    use pal_async::async_test;
899    use pal_uring::IoUringPool;
900    use pal_uring::PoolClient;
901    use scsi_buffers::OwnedRequestBuffers;
902    use test_with_tracing::test;
903
904    fn is_buggy_kernel() -> bool {
905        // 5.13 kernels seem to have a bug with io_uring where tests hang.
906        let output = String::from_utf8(
907            std::process::Command::new("uname")
908                .arg("-r")
909                .output()
910                .unwrap()
911                .stdout,
912        )
913        .unwrap();
914
915        output.contains("5.13")
916    }
917
918    fn new_block_device_from(file: fs::File) -> Result<BlockDevice, NewDeviceError> {
919        // TODO: switch to std::sync::OnceLock once `get_or_try_init` is stable
920        static POOL: OnceCell<PoolClient> = OnceCell::new();
921
922        let client = POOL
923            .get_or_try_init(|| {
924                let pool = IoUringPool::new("test", 16)?;
925                let client = pool.client().clone();
926                std::thread::spawn(|| pool.run());
927                Ok(client)
928            })
929            .map_err(|err| NewDeviceError::IoctlError(DiskError::Io(err)))?;
930
931        block_on(BlockDevice::new(
932            file,
933            false,
934            client.initiator().clone(),
935            None,
936            None,
937            false,
938        ))
939    }
940
941    fn new_block_device() -> Result<BlockDevice, NewDeviceError> {
942        let test_file = tempfile::tempfile().unwrap();
943        test_file.set_len(1024 * 64).unwrap();
944        new_block_device_from(test_file)
945    }
946
947    /// A loop device over a temporary file, detached when dropped.
948    ///
949    /// Attaching one requires `CAP_SYS_ADMIN`, which is why the test using this
950    /// is ignored by default.
951    struct LoopDevice {
952        path: String,
953        _backing: tempfile::NamedTempFile,
954    }
955
956    impl LoopDevice {
957        fn new(size: u64) -> Self {
958            let backing = tempfile::NamedTempFile::new().unwrap();
959            backing.as_file().set_len(size).unwrap();
960            let output = std::process::Command::new("losetup")
961                .arg("--find")
962                .arg("--show")
963                .arg(backing.path())
964                .output()
965                .expect("losetup should be installed");
966            assert!(
967                output.status.success(),
968                "losetup failed (needs root): {}",
969                String::from_utf8_lossy(&output.stderr)
970            );
971            Self {
972                path: String::from_utf8(output.stdout).unwrap().trim().to_owned(),
973                _backing: backing,
974            }
975        }
976    }
977
978    impl Drop for LoopDevice {
979        fn drop(&mut self) {
980            // Detach on the way out even if the test panicked, so that loop
981            // devices are not leaked.
982            let _ = std::process::Command::new("losetup")
983                .arg("-d")
984                .arg(&self.path)
985                .status();
986        }
987    }
988
989    macro_rules! get_block_device_or_skip {
990        () => {
991            match new_block_device() {
992                Ok(pool) => {
993                    if is_buggy_kernel() {
994                        println!("Test case skipped (buggy kernel version)");
995                        return;
996                    }
997
998                    pool
999                }
1000                Err(NewDeviceError::IoctlError(DiskError::Io(err)))
1001                    if err.raw_os_error() == Some(libc::ENOSYS) =>
1002                {
1003                    println!("Test case skipped (no IO-Uring support)");
1004                    return;
1005                }
1006                Err(err) => panic!("{}", err),
1007            }
1008        };
1009    }
1010
1011    /// The shared sector-range conformance suite.
1012    ///
1013    /// This exercises the `DeviceType::File` path, where the kernel does not
1014    /// enforce bounds and the backend's own checks are the only thing stopping
1015    /// a write past the end from extending the file. See
1016    /// [`sector_range_conformance_loop_device`] for the other path.
1017    #[async_test]
1018    async fn sector_range_conformance() {
1019        let disk = disk_backend::Disk::new(get_block_device_or_skip!()).unwrap();
1020        storage_tests::sector_range::test_disk_sector_range_conformance(&disk).await;
1021    }
1022
1023    /// The same suite over a real block device.
1024    ///
1025    /// This is the case the tempfile path cannot cover. For a block device the
1026    /// kernel enforces the bounds and this backend delegates to it, rather than
1027    /// checking the range itself, so it is the delegation that is under test
1028    /// here.
1029    ///
1030    /// Attaching a loop device needs `CAP_SYS_ADMIN`, so this is ignored by
1031    /// default. To run it:
1032    ///
1033    /// ```text
1034    /// sudo -E $(which cargo) nextest run -p disk_blockdevice \
1035    ///     --run-ignored all -E 'test(loop_device)'
1036    /// ```
1037    #[async_test]
1038    #[ignore = "needs root to attach a loop device"]
1039    async fn sector_range_conformance_loop_device() {
1040        let loop_device = LoopDevice::new(1024 * 1024);
1041        let file = fs::OpenOptions::new()
1042            .read(true)
1043            .write(true)
1044            .open(&loop_device.path)
1045            .unwrap();
1046        let disk = disk_backend::Disk::new(new_block_device_from(file).unwrap()).unwrap();
1047        storage_tests::sector_range::test_disk_sector_range_conformance(&disk).await;
1048    }
1049
1050    async fn run_async_disk_io(fua: bool) {
1051        let disk = get_block_device_or_skip!();
1052
1053        let test_guest_mem = GuestMemory::allocate(0x8000);
1054        test_guest_mem
1055            .write_at(0, &(0..0x8000).map(|x| x as u8).collect::<Vec<_>>())
1056            .unwrap();
1057
1058        let write_buffers = OwnedRequestBuffers::new(&[3, 2, 1, 0]);
1059        disk.write_vectored(&write_buffers.buffer(&test_guest_mem), 0, fua)
1060            .await
1061            .unwrap();
1062
1063        if !fua {
1064            disk.sync_cache().await.unwrap();
1065        }
1066
1067        let read_buffers = OwnedRequestBuffers::new(&[7, 6, 5, 4]);
1068        disk.read_vectored(&read_buffers.buffer(&test_guest_mem), 0)
1069            .await
1070            .unwrap();
1071
1072        let mut source = vec![0u8; 4 * HV_PAGE_SIZE_USIZE];
1073        test_guest_mem.read_at(0, &mut source).unwrap();
1074        let mut target = vec![0u8; 4 * HV_PAGE_SIZE_USIZE];
1075        test_guest_mem
1076            .read_at(4 * HV_PAGE_SIZE, &mut target)
1077            .unwrap();
1078        assert_eq!(source, target);
1079    }
1080
1081    #[async_test]
1082    async fn test_async_disk_io() {
1083        run_async_disk_io(false).await;
1084    }
1085
1086    #[async_test]
1087    async fn test_async_disk_io_fua() {
1088        run_async_disk_io(true).await;
1089    }
1090
1091    async fn run_async_disk_io_unaligned(fua: bool) {
1092        let disk = get_block_device_or_skip!();
1093
1094        let test_guest_mem = GuestMemory::allocate(0x8000);
1095        test_guest_mem
1096            .write_at(0, &(0..0x8000).map(|x| x as u8).collect::<Vec<_>>())
1097            .unwrap();
1098
1099        let write_buffers =
1100            OwnedRequestBuffers::new_unaligned(&[0, 1, 2, 3], 512, 3 * HV_PAGE_SIZE_USIZE);
1101
1102        disk.write_vectored(&write_buffers.buffer(&test_guest_mem), 0, fua)
1103            .await
1104            .unwrap();
1105
1106        if !fua {
1107            disk.sync_cache().await.unwrap();
1108        }
1109
1110        let read_buffers =
1111            OwnedRequestBuffers::new_unaligned(&[4, 5, 6, 7], 512, 3 * HV_PAGE_SIZE_USIZE);
1112        disk.read_vectored(&read_buffers.buffer(&test_guest_mem), 0)
1113            .await
1114            .unwrap();
1115
1116        let mut source = vec![0u8; 3 * HV_PAGE_SIZE_USIZE];
1117        test_guest_mem.read_at(512, &mut source).unwrap();
1118        let mut target = vec![0u8; 3 * HV_PAGE_SIZE_USIZE];
1119        test_guest_mem
1120            .read_at(4 * HV_PAGE_SIZE + 512, &mut target)
1121            .unwrap();
1122        assert_eq!(source, target);
1123    }
1124
1125    #[async_test]
1126    async fn test_async_disk_io_unaligned() {
1127        run_async_disk_io_unaligned(false).await;
1128    }
1129
1130    #[async_test]
1131    async fn test_async_disk_io_unaligned_fua() {
1132        run_async_disk_io_unaligned(true).await;
1133    }
1134
1135    #[async_test]
1136    async fn test_illegal_lba() {
1137        let disk = get_block_device_or_skip!();
1138        let gm = GuestMemory::allocate(512);
1139        match disk
1140            .write_vectored(
1141                &OwnedRequestBuffers::linear(0, 512, true).buffer(&gm),
1142                i64::MAX as u64 / 512,
1143                false,
1144            )
1145            .await
1146        {
1147            Err(DiskError::IllegalBlock) => {}
1148            r => panic!("unexpected result: {:?}", r),
1149        }
1150    }
1151
1152    #[async_test]
1153    async fn test_unmap_file_punch_hole() {
1154        let disk = get_block_device_or_skip!();
1155
1156        // A writable file-backed device should advertise discard support via
1157        // punch-hole and report that unmapped ranges read back as zero.
1158        if disk.optimal_unmap_sectors() == 0 {
1159            println!("Test case skipped (filesystem does not support punch hole)");
1160            return;
1161        }
1162        assert_eq!(disk.unmap_behavior(), UnmapBehavior::Zeroes);
1163
1164        let gm = GuestMemory::allocate(0x2000);
1165        gm.write_at(0, &vec![0xcdu8; 0x2000]).unwrap();
1166
1167        // Write two pages of non-zero data.
1168        disk.write_vectored(
1169            &OwnedRequestBuffers::linear(0, 0x2000, false).buffer(&gm),
1170            0,
1171            false,
1172        )
1173        .await
1174        .unwrap();
1175        disk.sync_cache().await.unwrap();
1176
1177        // Unmap the first page and read both pages back.
1178        disk.unmap(0, 0x1000 >> disk.sector_shift, false)
1179            .await
1180            .unwrap();
1181
1182        let read = OwnedRequestBuffers::linear(0, 0x2000, true);
1183        disk.read_vectored(&read.buffer(&gm), 0).await.unwrap();
1184
1185        let mut buf = vec![0xffu8; 0x2000];
1186        gm.read_at(0, &mut buf).unwrap();
1187        // Punched range reads as zero; the untouched page keeps its data.
1188        assert!(
1189            buf[..0x1000].iter().all(|&b| b == 0),
1190            "unmapped range not zeroed"
1191        );
1192        assert!(
1193            buf[0x1000..].iter().all(|&b| b == 0xcd),
1194            "second page corrupted"
1195        );
1196    }
1197}