Skip to main content

disk_backend/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! The shared disk backend abstraction for OpenVMM storage.
5//!
6//! This crate defines [`Disk`] and the [`DiskIo`] trait, the central
7//! interface between storage frontends (NVMe, SCSI/StorVSP, IDE) and disk
8//! backends (host files, block devices, remote blobs, and more).
9//!
10//! # Architecture
11//!
12//! Every disk backend implements [`DiskIo`]. Frontends don't interact with
13//! backends directly — they hold a [`Disk`], which wraps a type-erased
14//! backend (`DynDisk`, an adapter around [`DiskIo`] that normalizes return
15//! futures) behind an `Arc` for cheap, concurrent cloning. The `Disk`
16//! wrapper caches immutable metadata (sector size, physical sector size,
17//! disk ID, FUA support) at construction time and validates that sector
18//! sizes are powers of two and at least 512 bytes.
19//!
20//! # I/O model
21//!
22//! All I/O is **async** and uses **scatter-gather** buffers via
23//! [`RequestBuffers`]. Callers must pass
24//! buffers that are an integral number of sectors.
25//!
26//! The key operations are:
27//!
28//! - [`DiskIo::read_vectored`] / [`DiskIo::write_vectored`] — async
29//!   scatter-gather read and write. The `fua` parameter on writes requests
30//!   Force Unit Access (write-through to stable storage). Whether FUA is
31//!   actually respected depends on the backend — check
32//!   [`DiskIo::is_fua_respected`].
33//! - [`DiskIo::sync_cache`] — flush (equivalent to SCSI SYNCHRONIZE CACHE
34//!   or NVMe FLUSH).
35//! - [`DiskIo::unmap`] — trim / deallocate sectors. The
36//!   [`DiskIo::unmap_behavior`] method reports whether unmapped sectors
37//!   become zero, become indeterminate, or whether unmap is ignored
38//!   entirely.
39//! - [`DiskIo::eject`] — eject media (optical drives only). The default
40//!   returns [`DiskError::UnsupportedEject`]. Eject is a media state change
41//!   managed by the SCSI DVD layer, not by the backend.
42//! - [`DiskIo::wait_resize`] — block until the disk's sector count changes.
43//!   The default returns [`std::future::pending()`], meaning the backend
44//!   never signals a resize. Only backends that can detect runtime capacity
45//!   changes (e.g., `BlockDeviceDisk` via Linux uevent, `NvmeDisk` via AEN)
46//!   should override this. Decorators and layered disks delegate to the
47//!   inner backend.
48//!
49//! # Error model
50//!
51//! All I/O methods return [`DiskError`], which frontends translate into
52//! protocol-specific errors (NVMe status codes, SCSI sense keys). The
53//! variants cover out-of-range LBAs, I/O errors, medium errors with
54//! sub-classification, guest memory access failures, read-only violations,
55//! persistent reservation conflicts, and unsupported eject.
56//!
57//! # Available backends
58//!
59//! | Backend | Crate | Description |
60//! |---------|-------|-------------|
61//! | `FileDisk` | `disk_file` | Host file, cross-platform |
62//! | `Vhd1Disk` | `disk_vhd1` | VHD1 fixed format |
63//! | `VhdmpDisk` | `disk_vhdmp` | Windows vhdmp driver |
64//! | `BlobDisk` | `disk_blob` | Read-only HTTP / Azure Blob |
65//! | `BlockDeviceDisk` | `disk_blockdevice` | Linux block device (io_uring) |
66//! | `NvmeDisk` | `disk_nvme` | Physical NVMe (user-mode driver) |
67//! | `StripedDisk` | `disk_striped` | Striped across multiple disks |
68//! | `CryptDisk` | `disk_crypt` | XTS-AES-256 encryption wrapper |
69//! | `DelayDisk` | `disk_delay` | Injected I/O latency wrapper |
70//! | `DiskWithReservations` | `disk_prwrap` | In-memory PR emulation wrapper |
71//! | `LayeredDisk` | `disk_layered` | Layered disk with per-sector presence |
72
73#![forbid(unsafe_code)]
74
75pub mod pr;
76pub mod resolve;
77pub mod sync_wrapper;
78
79use guestmem::AccessError;
80use inspect::Inspect;
81use scsi_buffers::RequestBuffers;
82use stackfuture::StackFuture;
83use std::fmt::Debug;
84use std::future::Future;
85use std::future::ready;
86use std::pin::Pin;
87use std::sync::Arc;
88use thiserror::Error;
89
90/// A disk operation error.
91#[derive(Debug, Error)]
92pub enum DiskError {
93    /// The request failed due to a preempt and abort status.
94    #[error("aborted command")]
95    AbortDueToPreemptAndAbort,
96    /// The LBA was out of range.
97    #[error("illegal request")]
98    IllegalBlock,
99    /// The request failed due to invalid input.
100    #[error("invalid input")]
101    InvalidInput,
102    /// The request failed due to an unrecovered IO error.
103    #[error("io error")]
104    Io(#[source] std::io::Error),
105    /// The request failed due to a reportable medium error.
106    #[error("medium error")]
107    MediumError(#[source] std::io::Error, MediumErrorDetails),
108    /// The request failed due to a failure to access the specified buffers.
109    #[error("failed to access guest memory")]
110    MemoryAccess(#[from] AccessError),
111    /// The request failed because the disk is read-only.
112    #[error("attempt to write to read-only disk/range")]
113    ReadOnly,
114    /// The request failed due to a persistent reservation conflict.
115    #[error("reservation conflict")]
116    ReservationConflict,
117    /// The request failed because eject is not supported.
118    #[error("unsupported eject")]
119    UnsupportedEject,
120}
121
122/// Failure details for [`DiskError::MediumError`].
123#[derive(Debug)]
124pub enum MediumErrorDetails {
125    /// The medium had an application tag check failure.
126    ApplicationTagCheckFailed,
127    /// The medium had a guard check failure.
128    GuardCheckFailed,
129    /// The medium had a reference tag check failure.
130    ReferenceTagCheckFailed,
131    /// The medium had an unrecovered read error.
132    UnrecoveredReadError,
133    /// The medium had a write fault.
134    WriteFault,
135}
136
137/// Disk metadata and IO operations.
138///
139/// # Sector range validation
140///
141/// Sector numbers reaching a backend originate with the guest, so
142/// implementations **must not panic** for any sector value, and must return
143/// [`DiskError::IllegalBlock`] for requests that fall outside the disk.
144/// Callers are *not* required to validate the range beforehand — they cannot
145/// do so meaningfully, since [`DiskIo::sector_count`] may change at runtime,
146/// so a range checked by a caller can be invalidated before the request is
147/// issued. Only the backend can validate against its own state.
148///
149/// An implementation may delegate this to whatever it is layered on top of,
150/// but only if the backing object's bounds coincide exactly with the disk's,
151/// out-of-range operations fail rather than silently succeeding, and the
152/// resulting error is mapped to [`DiskError::IllegalBlock`].
153///
154/// In exchange, implementations may rely on one guarantee from [`Disk`]: the
155/// end byte offset of any request — that is, `(sector + count) * sector_size`
156/// — is representable and no greater than [`i64::MAX`]. Sector arithmetic
157/// therefore cannot overflow, and a backend that transforms the offset (adding
158/// a header size, a chunk base, and so on) has 2^63 bytes of headroom in which
159/// to do so. Note that this says nothing about how large the disk is, so it is
160/// not a substitute for the range check above.
161pub trait DiskIo: 'static + Send + Sync + Inspect {
162    /// Returns the disk type name as a string.
163    ///
164    /// This is used for diagnostic purposes.
165    fn disk_type(&self) -> &str;
166
167    /// Returns the current sector count.
168    ///
169    /// For some backing stores, this may change at runtime. If it does, then
170    /// the backing store must also implement [`DiskIo::wait_resize`].
171    fn sector_count(&self) -> u64;
172
173    /// Returns the logical sector size of the backing store.
174    ///
175    /// This must not change at runtime.
176    fn sector_size(&self) -> u32;
177
178    /// Optionally returns a 16-byte identifier for the disk, if there is a
179    /// natural one for this backing store.
180    ///
181    /// This may be exposed to the guest as a unique disk identifier.
182    /// This must not change at runtime.
183    fn disk_id(&self) -> Option<[u8; 16]>;
184
185    /// Returns the physical sector size of the backing store.
186    ///
187    /// This must not change at runtime.
188    fn physical_sector_size(&self) -> u32;
189
190    /// Returns true if the `fua` parameter to [`DiskIo::write_vectored`] is
191    /// respected by the backing store by ensuring that the IO is immediately
192    /// committed to disk.
193    fn is_fua_respected(&self) -> bool;
194
195    /// Returns true if the disk is read only.
196    fn is_read_only(&self) -> bool;
197
198    /// Unmap sectors from the layer.
199    ///
200    /// See the [trait documentation](DiskIo#sector-range-validation) for the
201    /// requirements on out-of-range requests.
202    fn unmap(
203        &self,
204        sector: u64,
205        count: u64,
206        block_level_only: bool,
207    ) -> impl Future<Output = Result<(), DiskError>> + Send;
208
209    /// Returns the behavior of the unmap operation.
210    ///
211    /// This tells callers what happens to the content of unmapped sectors:
212    ///
213    /// - [`UnmapBehavior::Zeroes`] — unmapped sectors read back as zero.
214    /// - [`UnmapBehavior::Unspecified`] — content may or may not change, and
215    ///   not necessarily to zero.
216    /// - [`UnmapBehavior::Ignored`] — unmap is a no-op; content is unchanged.
217    fn unmap_behavior(&self) -> UnmapBehavior;
218
219    /// Returns the optimal granularity for unmaps, in sectors.
220    fn optimal_unmap_sectors(&self) -> u32 {
221        1
222    }
223
224    /// Optionally returns a trait object to issue persistent reservation
225    /// requests.
226    fn pr(&self) -> Option<&dyn pr::PersistentReservation> {
227        None
228    }
229
230    /// Issues an asynchronous eject media operation to the disk.
231    ///
232    /// The default implementation returns [`DiskError::UnsupportedEject`].
233    /// Eject is primarily a media state change managed by the SCSI DVD layer
234    /// (`SimpleScsiDvd`), not by disk backends. Backends generally do not
235    /// need to override this.
236    fn eject(&self) -> impl Future<Output = Result<(), DiskError>> + Send {
237        ready(Err(DiskError::UnsupportedEject))
238    }
239
240    /// Issues an asynchronous read-scatter operation to the disk.
241    ///
242    /// # Arguments
243    /// * `buffers` - An object representing the data buffers into which the disk data will be transferred.
244    /// * `sector` - The logical sector at which the read operation starts.
245    fn read_vectored(
246        &self,
247        buffers: &RequestBuffers<'_>,
248        sector: u64,
249    ) -> impl Future<Output = Result<(), DiskError>> + Send;
250
251    /// Issues an asynchronous write-gather operation to the disk.
252    /// # Arguments
253    /// * `buffers` - An object representing the data buffers containing the data to transfer to the disk.
254    /// * `sector` - The logical sector at which the write operation starts.
255    /// * `fua` - A flag indicates if FUA (force unit access) is requested.
256    fn write_vectored(
257        &self,
258        buffers: &RequestBuffers<'_>,
259        sector: u64,
260        fua: bool,
261    ) -> impl Future<Output = Result<(), DiskError>> + Send;
262
263    /// Issues an asynchronous flush operation to the disk.
264    fn sync_cache(&self) -> impl Future<Output = Result<(), DiskError>> + Send;
265
266    /// Waits for the disk sector count to change from the specified value.
267    ///
268    /// Returns the new sector count once [`DiskIo::sector_count`] would return
269    /// a value different from `sector_count`. Frontends use this to detect
270    /// runtime capacity changes and notify the guest (NVMe via AEN, SCSI via
271    /// UNIT_ATTENTION).
272    ///
273    /// The default implementation returns [`std::future::pending()`], meaning
274    /// the disk never signals a resize. Only backends that can detect runtime
275    /// capacity changes should override this — for example, `BlockDeviceDisk`
276    /// (via Linux uevent) and `NvmeDisk` (via NVMe AEN). Decorator wrappers
277    /// and `LayeredDisk` should delegate to the inner disk.
278    fn wait_resize(&self, sector_count: u64) -> impl Future<Output = u64> + Send {
279        let _ = sector_count;
280        std::future::pending()
281    }
282}
283
284/// An asynchronous block device.
285///
286/// This type is cheap to clone, for sharing the disk among multiple concurrent
287/// users.
288#[derive(Inspect, Clone)]
289#[inspect(extra = "Self::inspect_extra")]
290pub struct Disk(#[inspect(flatten)] Arc<DiskInner>);
291
292impl Disk {
293    fn inspect_extra(&self, resp: &mut inspect::Response<'_>) {
294        resp.field("disk_type", self.0.disk.disk_type())
295            .field("sector_count", self.0.disk.sector_count())
296            .field("supports_pr", self.0.disk.pr().is_some());
297    }
298}
299
300impl Debug for Disk {
301    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302        f.debug_tuple("Disk").finish()
303    }
304}
305
306#[derive(Inspect)]
307#[inspect(bound = "T: DynDisk")]
308struct DiskInner<T: ?Sized = dyn DynDisk> {
309    sector_size: u32,
310    sector_shift: u32,
311    physical_sector_size: u32,
312    disk_id: Option<[u8; 16]>,
313    is_fua_respected: bool,
314    is_read_only: bool,
315    unmap_behavior: UnmapBehavior,
316    optimal_unmap_sectors: u32,
317    disk: T,
318}
319
320/// Errors that can occur when creating a `Disk`.
321#[derive(Debug, Error)]
322pub enum InvalidDisk {
323    /// The sector size is invalid.
324    #[error("invalid sector size: {0}")]
325    InvalidSectorSize(u32),
326    /// The physical sector size is invalid.
327    #[error("invalid physical sector size: {0}")]
328    InvalidPhysicalSectorSize(u32),
329}
330
331impl Disk {
332    /// Returns a new disk wrapping the given backing object.
333    pub fn new(disk: impl 'static + DiskIo) -> Result<Self, InvalidDisk> {
334        // Cache the metadata locally to validate it and so that it can be
335        // accessed without needing to go through the trait object. This is more
336        // efficient and ensures the backing disk does not change these values
337        // during the lifetime of the disk.
338        let sector_size = disk.sector_size();
339        if !sector_size.is_power_of_two() || sector_size < 512 {
340            return Err(InvalidDisk::InvalidSectorSize(sector_size));
341        }
342        let physical_sector_size = disk.physical_sector_size();
343        if !physical_sector_size.is_power_of_two() || physical_sector_size < sector_size {
344            return Err(InvalidDisk::InvalidPhysicalSectorSize(physical_sector_size));
345        }
346        Ok(Self(Arc::new(DiskInner {
347            sector_size,
348            sector_shift: sector_size.trailing_zeros(),
349            physical_sector_size,
350            disk_id: disk.disk_id(),
351            is_fua_respected: disk.is_fua_respected(),
352            is_read_only: disk.is_read_only(),
353            optimal_unmap_sectors: disk.optimal_unmap_sectors(),
354            unmap_behavior: disk.unmap_behavior(),
355            disk,
356        })))
357    }
358
359    /// Returns the current sector count.
360    ///
361    /// For some backing stores, this may change at runtime. Use
362    /// [`wait_resize`](Self::wait_resize) to detect this change.
363    pub fn sector_count(&self) -> u64 {
364        self.0.disk.sector_count()
365    }
366
367    /// Returns the logical sector size of the backing store.
368    pub fn sector_size(&self) -> u32 {
369        self.0.sector_size
370    }
371
372    /// Returns log2 of the logical sector size of the backing store.
373    pub fn sector_shift(&self) -> u32 {
374        self.0.sector_shift
375    }
376
377    /// Optionally returns a 16-byte identifier for the disk, if there is a
378    /// natural one for this backing store.
379    ///
380    /// This may be exposed to the guest as a unique disk identifier.
381    pub fn disk_id(&self) -> Option<[u8; 16]> {
382        self.0.disk_id
383    }
384
385    /// Returns the physical sector size of the backing store.
386    pub fn physical_sector_size(&self) -> u32 {
387        self.0.physical_sector_size
388    }
389
390    /// Returns true if the `fua` parameter to
391    /// [`write_vectored`](Self::write_vectored) is respected by the backing
392    /// store by ensuring that the IO is immediately committed to disk.
393    pub fn is_fua_respected(&self) -> bool {
394        self.0.is_fua_respected
395    }
396
397    /// Returns true if the disk is read only.
398    pub fn is_read_only(&self) -> bool {
399        self.0.is_read_only
400    }
401
402    /// Returns the largest sector number that may appear as the end of a
403    /// request while keeping the end byte offset representable.
404    fn max_sector(&self) -> u64 {
405        (i64::MAX as u64) >> self.0.sector_shift
406    }
407
408    /// Checks that a request's end byte offset is representable.
409    ///
410    /// This is deliberately *not* a range check: it never consults
411    /// [`sector_count`](Self::sector_count), so it cannot mask a bug in a
412    /// backend that fails to validate the range itself, and it cannot be
413    /// invalidated by the disk being resized. Range validation belongs to the
414    /// backend, which is the only component that can perform it atomically
415    /// with the I/O.
416    ///
417    /// What it does guarantee is that `(sector + count) * sector_size` does not
418    /// overflow and is at most [`i64::MAX`], which is the real limit imposed by
419    /// `pread64`/`pwrite64` and the Windows file APIs. Backends may rely on
420    /// this to do sector and offset arithmetic without worrying about
421    /// wraparound.
422    fn check_representable(&self, sector: u64, count: u64) -> Result<(), DiskError> {
423        match sector.checked_add(count) {
424            Some(end) if end <= self.max_sector() => Ok(()),
425            // No disk can be 2^63 bytes, so such a sector is out of range for
426            // any disk.
427            _ => Err(DiskError::IllegalBlock),
428        }
429    }
430
431    /// Returns the number of sectors spanned by `buffers`.
432    ///
433    /// Callers must pass a whole number of sectors, so this is normally exact.
434    /// Rounding down is nonetheless the right choice for a caller that does
435    /// not: `max_sector` is `i64::MAX` rounded *down* to a sector, which for a
436    /// power-of-two sector size leaves exactly `sector_size - 1` bytes of slack
437    /// below `i64::MAX` — enough to cover a partial trailing sector. So the end
438    /// byte offset stays representable either way.
439    fn buffer_sectors(&self, buffers: &RequestBuffers<'_>) -> u64 {
440        (buffers.len() as u64) >> self.0.sector_shift
441    }
442
443    /// Unmap sectors from the disk.
444    ///
445    /// If the disk reports [`UnmapBehavior::Ignored`], the request is not
446    /// passed to the backing object at all, since by definition it would do
447    /// nothing. The range is still validated first — a no-op is still not a
448    /// legal response to a request naming sectors the disk does not have.
449    pub async fn unmap(
450        &self,
451        sector: u64,
452        count: u64,
453        block_level_only: bool,
454    ) -> Result<(), DiskError> {
455        self.check_representable(sector, count)?;
456        if self.unmap_behavior() == UnmapBehavior::Ignored {
457            // This is the one place where `Disk` range checks a request, and it
458            // is sound precisely because it is the one place where `Disk` does
459            // not delegate: there is no backend check for it to be redundant
460            // with, and none for it to mask. The check being momentarily stale
461            // if the disk is resized is harmless here, because the operation
462            // does nothing either way — only the status code is observable.
463            //
464            // The addition cannot overflow because of `check_representable`.
465            if sector + count > self.sector_count() {
466                return Err(DiskError::IllegalBlock);
467            }
468            return Ok(());
469        }
470        self.0.disk.unmap(sector, count, block_level_only).await
471    }
472
473    /// Returns the behavior of the unmap operation.
474    pub fn unmap_behavior(&self) -> UnmapBehavior {
475        self.0.unmap_behavior
476    }
477
478    /// Returns the optimal granularity for unmaps, in sectors.
479    pub fn optimal_unmap_sectors(&self) -> u32 {
480        self.0.optimal_unmap_sectors
481    }
482
483    /// Optionally returns a trait object to issue persistent reservation
484    /// requests.
485    pub fn pr(&self) -> Option<&dyn pr::PersistentReservation> {
486        self.0.disk.pr()
487    }
488
489    /// Issues an asynchronous eject media operation to the disk.
490    pub fn eject(&self) -> impl use<'_> + Future<Output = Result<(), DiskError>> + Send {
491        self.0.disk.eject()
492    }
493
494    /// Issues an asynchronous read-scatter operation to the disk.
495    ///
496    /// # Arguments
497    ///
498    /// * `buffers` - An object representing the data buffers into which the disk data will be transferred.
499    /// * `sector` - The logical sector at which the read operation starts.
500    pub async fn read_vectored(
501        &self,
502        buffers: &RequestBuffers<'_>,
503        sector: u64,
504    ) -> Result<(), DiskError> {
505        self.check_representable(sector, self.buffer_sectors(buffers))?;
506        self.0.disk.read_vectored(buffers, sector).await
507    }
508
509    /// Issues an asynchronous write-gather operation to the disk.
510    ///
511    /// # Arguments
512    ///
513    /// * `buffers` - An object representing the data buffers containing the data to transfer to the disk.
514    /// * `sector` - The logical sector at which the write operation starts.
515    /// * `fua` - A flag indicates if FUA (force unit access) is requested.
516    ///
517    /// # Panics
518    ///
519    /// The caller must pass a buffer with an integer number of sectors.
520    pub async fn write_vectored(
521        &self,
522        buffers: &RequestBuffers<'_>,
523        sector: u64,
524        fua: bool,
525    ) -> Result<(), DiskError> {
526        self.check_representable(sector, self.buffer_sectors(buffers))?;
527        self.0.disk.write_vectored(buffers, sector, fua).await
528    }
529
530    /// Issues an asynchronous flush operation to the disk.
531    pub fn sync_cache(&self) -> impl use<'_> + Future<Output = Result<(), DiskError>> + Send {
532        self.0.disk.sync_cache()
533    }
534
535    /// Waits for the disk sector count to change from the specified value.
536    pub fn wait_resize(&self, sector_count: u64) -> impl use<'_> + Future<Output = u64> {
537        self.0.disk.wait_resize(sector_count)
538    }
539}
540
541/// The behavior of the [`DiskIo::unmap`] operation.
542///
543/// This describes what happens to the content of unmapped sectors. Frontends
544/// use this to report the correct behavior to the guest (e.g., SCSI
545/// `LBPRZ` bit or NVMe DLFEAT field).
546#[derive(Clone, Copy, Debug, PartialEq, Eq, Inspect)]
547pub enum UnmapBehavior {
548    /// Unmap may or may not change the content, and not necessarily to zero.
549    /// The guest cannot assume anything about the content of unmapped sectors.
550    Unspecified,
551    /// Unmaps are guaranteed to be ignored — the content is unchanged.
552    /// The disk reports that unmap is not supported.
553    Ignored,
554    /// Unmap will deterministically zero the content. The guest can rely on
555    /// reading back zeroes from unmapped sectors.
556    Zeroes,
557}
558
559/// The amount of space reserved for a DiskIo future
560///
561/// This was chosen by running `cargo test -p storvsp -- --no-capture` and looking at the required
562/// size that was given in the failure message
563const ASYNC_DISK_STACK_SIZE: usize = 1256;
564
565type IoFuture<'a> = StackFuture<'a, Result<(), DiskError>, { ASYNC_DISK_STACK_SIZE }>;
566
567trait DynDisk: Send + Sync + Inspect {
568    fn disk_type(&self) -> &str;
569    fn sector_count(&self) -> u64;
570
571    fn unmap(&self, sector_offset: u64, sector_count: u64, block_level_only: bool) -> IoFuture<'_>;
572
573    fn pr(&self) -> Option<&dyn pr::PersistentReservation>;
574    fn eject(&self) -> IoFuture<'_>;
575
576    fn read_vectored<'a>(&'a self, buffers: &'a RequestBuffers<'_>, sector: u64) -> IoFuture<'a>;
577
578    fn write_vectored<'a>(
579        &'a self,
580        buffers: &'a RequestBuffers<'_>,
581        sector: u64,
582        fua: bool,
583    ) -> IoFuture<'a>;
584
585    fn sync_cache(&self) -> IoFuture<'_>;
586
587    fn wait_resize<'a>(
588        &'a self,
589        sector_count: u64,
590    ) -> Pin<Box<dyn 'a + Send + Future<Output = u64>>> {
591        let _ = sector_count;
592        Box::pin(std::future::pending())
593    }
594}
595
596impl<T: DiskIo> DynDisk for T {
597    fn disk_type(&self) -> &str {
598        self.disk_type()
599    }
600
601    fn sector_count(&self) -> u64 {
602        self.sector_count()
603    }
604
605    fn unmap(
606        &self,
607        sector_offset: u64,
608        sector_count: u64,
609        block_level_only: bool,
610    ) -> StackFuture<'_, Result<(), DiskError>, { ASYNC_DISK_STACK_SIZE }> {
611        StackFuture::from_or_box(self.unmap(sector_offset, sector_count, block_level_only))
612    }
613
614    fn pr(&self) -> Option<&dyn pr::PersistentReservation> {
615        self.pr()
616    }
617
618    fn eject(&self) -> IoFuture<'_> {
619        StackFuture::from_or_box(self.eject())
620    }
621
622    fn read_vectored<'a>(&'a self, buffers: &'a RequestBuffers<'_>, sector: u64) -> IoFuture<'a> {
623        StackFuture::from_or_box(self.read_vectored(buffers, sector))
624    }
625
626    fn write_vectored<'a>(
627        &'a self,
628        buffers: &'a RequestBuffers<'a>,
629        sector: u64,
630        fua: bool,
631    ) -> IoFuture<'a> {
632        StackFuture::from_or_box(self.write_vectored(buffers, sector, fua))
633    }
634
635    fn sync_cache(&self) -> IoFuture<'_> {
636        StackFuture::from_or_box(self.sync_cache())
637    }
638}