disk_vhdmp/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4#![expect(missing_docs)]
5#![cfg(windows)]
6// UNSAFETY: Calling Win32 VirtualDisk APIs and accessing the unions they return.
7#![expect(unsafe_code)]
8#![expect(clippy::undocumented_unsafe_blocks)]
9
10use disk_backend::DiskError;
11use disk_backend::DiskIo;
12use disk_backend::resolve::ResolveDiskParameters;
13use disk_backend::resolve::ResolvedDisk;
14use disk_file::FileDisk;
15use guid::Guid;
16use inspect::Inspect;
17use mesh::MeshPayload;
18use scsi_buffers::RequestBuffers;
19use std::fs;
20use std::os::windows::prelude::*;
21use std::path::Path;
22use thiserror::Error;
23use vm_resource::ResolveResource;
24use vm_resource::ResourceId;
25use vm_resource::declare_static_resolver;
26use vm_resource::kind::DiskHandleKind;
27
28mod virtdisk {
29    #![expect(non_snake_case, dead_code)]
30
31    use std::os::windows::prelude::*;
32    use winapi::shared::guiddef::GUID;
33    use winapi::shared::minwindef::BOOL;
34    use winapi::um::minwinbase::OVERLAPPED;
35    use winapi::um::winnt::SECURITY_DESCRIPTOR;
36
37    #[repr(C)]
38    #[derive(Copy, Clone)]
39    pub struct VIRTUAL_STORAGE_TYPE {
40        pub DeviceId: u32,
41        pub VendorId: GUID,
42    }
43
44    // Open the backing store without opening any differencing chain parents.
45    // This allows one to fixup broken parent links.
46    pub const OPEN_VIRTUAL_DISK_FLAG_NO_PARENTS: u32 = 0x0000_0001;
47
48    // The backing store being opened is an empty file. Do not perform virtual
49    // disk verification.
50    pub const OPEN_VIRTUAL_DISK_FLAG_BLANK_FILE: u32 = 0x0000_0002;
51
52    // This flag is only specified at boot time to load the system disk
53    // during virtual disk boot.  Must be kernel mode to specify this flag.
54    pub const OPEN_VIRTUAL_DISK_FLAG_BOOT_DRIVE: u32 = 0x0000_0004;
55
56    // This flag causes the backing file to be opened in cached mode.
57    pub const OPEN_VIRTUAL_DISK_FLAG_CACHED_IO: u32 = 0x0000_0008;
58
59    // Open the backing store without opening any differencing chain parents.
60    // This allows one to fixup broken parent links temporarily without updating
61    // the parent locator.
62    pub const OPEN_VIRTUAL_DISK_FLAG_CUSTOM_DIFF_CHAIN: u32 = 0x0000_0010;
63
64    // This flag causes all backing stores except the leaf backing store to
65    // be opened in cached mode.
66    pub const OPEN_VIRTUAL_DISK_FLAG_PARENT_CACHED_IO: u32 = 0x0000_0020;
67
68    // This flag causes a Vhd Set file to be opened without any virtual disk.
69    pub const OPEN_VIRTUAL_DISK_FLAG_VHDSET_FILE_ONLY: u32 = 0x0000_0040;
70
71    // For differencing disks, relative parent locators are not used when
72    // determining the path of a parent VHD.
73    pub const OPEN_VIRTUAL_DISK_FLAG_IGNORE_RELATIVE_PARENT_LOCATOR: u32 = 0x0000_0080;
74
75    // Disable flushing and FUA (both for payload data and for metadata)
76    // for backing files associated with this virtual disk.
77    pub const OPEN_VIRTUAL_DISK_FLAG_NO_WRITE_HARDENING: u32 = 0x0000_0100;
78
79    #[repr(C)]
80    pub struct OPEN_VIRTUAL_DISK_PARAMETERS {
81        pub Version: u32,
82        pub u: OPEN_VIRTUAL_DISK_PARAMETERS_u,
83    }
84
85    #[repr(C)]
86    pub union OPEN_VIRTUAL_DISK_PARAMETERS_u {
87        pub Version1: OPEN_VIRTUAL_DISK_PARAMETERS_1,
88        pub Version2: OPEN_VIRTUAL_DISK_PARAMETERS_2,
89        pub Version3: OPEN_VIRTUAL_DISK_PARAMETERS_3,
90    }
91
92    #[repr(C)]
93    #[derive(Copy, Clone)]
94    pub struct OPEN_VIRTUAL_DISK_PARAMETERS_1 {
95        pub RWDepth: u32,
96    }
97
98    #[repr(C)]
99    #[derive(Copy, Clone)]
100    pub struct OPEN_VIRTUAL_DISK_PARAMETERS_2 {
101        pub GetInfoOnly: BOOL,
102        pub ReadOnly: BOOL,
103        pub ResiliencyGuid: GUID,
104    }
105
106    #[repr(C)]
107    #[derive(Copy, Clone)]
108    pub struct OPEN_VIRTUAL_DISK_PARAMETERS_3 {
109        pub GetInfoOnly: BOOL,
110        pub ReadOnly: BOOL,
111        pub ResiliencyGuid: GUID,
112        pub SnapshotId: GUID,
113    }
114
115    pub const VIRTUAL_DISK_ACCESS_ATTACH_RO: u32 = 0x00010000;
116    pub const VIRTUAL_DISK_ACCESS_ATTACH_RW: u32 = 0x00020000;
117    pub const VIRTUAL_DISK_ACCESS_DETACH: u32 = 0x00040000;
118    pub const VIRTUAL_DISK_ACCESS_GET_INFO: u32 = 0x00080000;
119    pub const VIRTUAL_DISK_ACCESS_CREATE: u32 = 0x00100000;
120    pub const VIRTUAL_DISK_ACCESS_METAOPS: u32 = 0x00200000;
121    pub const VIRTUAL_DISK_ACCESS_READ: u32 = 0x000d0000;
122
123    // Attach the disk as read only
124    pub const ATTACH_VIRTUAL_DISK_FLAG_READ_ONLY: u32 = 0x0000_0001;
125
126    // Will cause all volumes on the disk to be mounted
127    // without drive letters.
128    pub const ATTACH_VIRTUAL_DISK_FLAG_NO_DRIVE_LETTER: u32 = 0x0000_0002;
129
130    // Will decouple the disk lifetime from that of the VirtualDiskHandle.
131    // The disk will be attached until an explicit call is made to
132    // DetachVirtualDisk, even if all handles are closed.
133    pub const ATTACH_VIRTUAL_DISK_FLAG_PERMANENT_LIFETIME: u32 = 0x0000_0004;
134
135    // Indicates that the drive will not be attached to
136    // the local system (but rather to a VM).
137    pub const ATTACH_VIRTUAL_DISK_FLAG_NO_LOCAL_HOST: u32 = 0x0000_0008;
138
139    // Do not assign a custom security descriptor to the disk; use the
140    // system default.
141    pub const ATTACH_VIRTUAL_DISK_FLAG_NO_SECURITY_DESCRIPTOR: u32 = 0x0000_0010;
142
143    // Default volume encryption policies should not be applied to the
144    // disk when attached to the local system.
145    pub const ATTACH_VIRTUAL_DISK_FLAG_BYPASS_DEFAULT_ENCRYPTION_POLICY: u32 = 0x0000_0020;
146
147    pub const GET_VIRTUAL_DISK_INFO_UNSPECIFIED: u32 = 0;
148    pub const GET_VIRTUAL_DISK_INFO_SIZE: u32 = 1;
149    pub const GET_VIRTUAL_DISK_INFO_IDENTIFIER: u32 = 2;
150    pub const GET_VIRTUAL_DISK_INFO_PARENT_LOCATION: u32 = 3;
151    pub const GET_VIRTUAL_DISK_INFO_PARENT_IDENTIFIER: u32 = 4;
152    pub const GET_VIRTUAL_DISK_INFO_PARENT_TIMESTAMP: u32 = 5;
153    pub const GET_VIRTUAL_DISK_INFO_VIRTUAL_STORAGE_TYPE: u32 = 6;
154    pub const GET_VIRTUAL_DISK_INFO_PROVIDER_SUBTYPE: u32 = 7;
155    pub const GET_VIRTUAL_DISK_INFO_IS_4K_ALIGNED: u32 = 8;
156    pub const GET_VIRTUAL_DISK_INFO_PHYSICAL_DISK: u32 = 9;
157    pub const GET_VIRTUAL_DISK_INFO_VHD_PHYSICAL_SECTOR_SIZE: u32 = 10;
158    pub const GET_VIRTUAL_DISK_INFO_SMALLEST_SAFE_VIRTUAL_SIZE: u32 = 11;
159    pub const GET_VIRTUAL_DISK_INFO_FRAGMENTATION: u32 = 12;
160    pub const GET_VIRTUAL_DISK_INFO_IS_LOADED: u32 = 13;
161    pub const GET_VIRTUAL_DISK_INFO_VIRTUAL_DISK_ID: u32 = 14;
162    pub const GET_VIRTUAL_DISK_INFO_CHANGE_TRACKING_STATE: u32 = 15;
163
164    #[repr(C)]
165    #[derive(Copy, Clone)]
166    pub struct GET_VIRTUAL_DISK_INFO {
167        pub Version: u32,
168        pub u: GET_VIRTUAL_DISK_INFO_u,
169    }
170
171    #[repr(C)]
172    #[derive(Copy, Clone)]
173    pub union GET_VIRTUAL_DISK_INFO_u {
174        pub Size: GET_VIRTUAL_DISK_INFO_Size,
175        pub Identifier: GUID,
176        pub ParentIdentifier: GUID,
177        pub ParentTimestamp: u32,
178        pub VirtualStorageType: VIRTUAL_STORAGE_TYPE,
179        pub ProviderSubtype: u32,
180        pub Is4kAligned: BOOL,
181        pub IsLoaded: BOOL,
182        pub VhdPhysicalSectorSize: u32,
183        pub SmallestSafeVirtualSize: u64,
184        pub FragmentationPercentage: u32,
185        pub VirtualDiskId: GUID,
186    }
187
188    #[repr(C)]
189    #[derive(Copy, Clone)]
190    pub struct GET_VIRTUAL_DISK_INFO_Size {
191        pub VirtualSize: u64,
192        pub PhysicalSize: u64,
193        pub BlockSize: u32,
194        pub SectorSize: u32,
195    }
196
197    #[link(name = "virtdisk")]
198    unsafe extern "system" {
199        pub fn OpenVirtualDisk(
200            virtual_storage_type: &mut VIRTUAL_STORAGE_TYPE,
201            path: *const u16,
202            virtual_disk_access_mask: u32,
203            flags: u32,
204            parameters: Option<&mut OPEN_VIRTUAL_DISK_PARAMETERS>,
205            handle: &mut RawHandle,
206        ) -> u32;
207
208        pub fn AttachVirtualDisk(
209            virtual_disk_handle: RawHandle,
210            security_descriptor: Option<&mut SECURITY_DESCRIPTOR>,
211            flags: u32,
212            provider_specific_flags: u32,
213            parameters: usize,
214            overlapped: Option<&mut OVERLAPPED>,
215        ) -> u32;
216
217        pub fn GetVirtualDiskInformation(
218            virtual_disk_handle: RawHandle,
219            virtual_disk_info_size: &mut u32,
220            virtual_disk_info: Option<&mut GET_VIRTUAL_DISK_INFO>,
221            size_use: Option<&mut u32>,
222        ) -> u32;
223
224    }
225}
226
227#[derive(Debug, MeshPayload)]
228pub struct Vhd(fs::File);
229
230fn chk_win32(err: u32) -> std::io::Result<()> {
231    if err == 0 {
232        Ok(())
233    } else {
234        Err(std::io::Error::from_raw_os_error(err as i32))
235    }
236}
237
238impl Vhd {
239    fn open(path: &Path, read_only: bool) -> std::io::Result<Self> {
240        let file = unsafe {
241            let mut storage_type = std::mem::zeroed();
242            // Use a unique ID for each open to avoid virtual disk sharing
243            // within VHDMP. In the future, consider taking this as a parameter
244            // to support failover.
245            let resiliency_guid = Guid::new_random();
246            let mut parameters = virtdisk::OPEN_VIRTUAL_DISK_PARAMETERS {
247                Version: 2,
248                u: virtdisk::OPEN_VIRTUAL_DISK_PARAMETERS_u {
249                    Version2: virtdisk::OPEN_VIRTUAL_DISK_PARAMETERS_2 {
250                        ReadOnly: read_only.into(),
251                        ResiliencyGuid: resiliency_guid.into(),
252                        ..std::mem::zeroed()
253                    },
254                },
255            };
256            let mut path16: Vec<_> = path.as_os_str().encode_wide().collect();
257            path16.push(0);
258            let mut handle = std::mem::zeroed();
259            chk_win32(virtdisk::OpenVirtualDisk(
260                &mut storage_type,
261                path16.as_ptr(),
262                0,
263                0,
264                Some(&mut parameters),
265                &mut handle,
266            ))?;
267            fs::File::from_raw_handle(handle)
268        };
269        Ok(Self(file))
270    }
271
272    fn attach_for_raw_access(&self, read_only: bool) -> std::io::Result<()> {
273        unsafe {
274            let mut flags = virtdisk::ATTACH_VIRTUAL_DISK_FLAG_NO_LOCAL_HOST;
275            if read_only {
276                flags |= virtdisk::ATTACH_VIRTUAL_DISK_FLAG_READ_ONLY;
277            }
278            chk_win32(virtdisk::AttachVirtualDisk(
279                self.0.as_raw_handle(),
280                None,
281                flags,
282                0,
283                0,
284                None,
285            ))?;
286        }
287        Ok(())
288    }
289
290    fn info_static(&self, info_type: u32) -> std::io::Result<virtdisk::GET_VIRTUAL_DISK_INFO> {
291        unsafe {
292            let mut info = virtdisk::GET_VIRTUAL_DISK_INFO {
293                Version: info_type,
294                ..std::mem::zeroed()
295            };
296            let mut size = size_of_val(&info) as u32;
297            chk_win32(virtdisk::GetVirtualDiskInformation(
298                self.0.as_raw_handle(),
299                &mut size,
300                Some(&mut info),
301                None,
302            ))?;
303            Ok(info)
304        }
305    }
306
307    fn get_size(&self) -> std::io::Result<virtdisk::GET_VIRTUAL_DISK_INFO_Size> {
308        unsafe {
309            Ok(self
310                .info_static(virtdisk::GET_VIRTUAL_DISK_INFO_SIZE)?
311                .u
312                .Size)
313        }
314    }
315
316    fn get_physical_sector_size(&self) -> std::io::Result<u32> {
317        unsafe {
318            Ok(self
319                .info_static(virtdisk::GET_VIRTUAL_DISK_INFO_VHD_PHYSICAL_SECTOR_SIZE)?
320                .u
321                .VhdPhysicalSectorSize)
322        }
323    }
324
325    fn get_disk_id(&self) -> std::io::Result<Guid> {
326        unsafe {
327            Ok(self
328                .info_static(virtdisk::GET_VIRTUAL_DISK_INFO_VIRTUAL_DISK_ID)?
329                .u
330                .VirtualDiskId
331                .into())
332        }
333    }
334}
335
336#[derive(MeshPayload)]
337pub struct OpenVhdmpDiskConfig(pub Vhd);
338
339impl ResourceId<DiskHandleKind> for OpenVhdmpDiskConfig {
340    const ID: &'static str = "vhdmp";
341}
342
343pub struct VhdmpDiskResolver;
344declare_static_resolver!(VhdmpDiskResolver, (DiskHandleKind, OpenVhdmpDiskConfig));
345
346#[derive(Debug, Error)]
347pub enum ResolveVhdmpDiskError {
348    #[error("failed to open VHD")]
349    Vhdmp(#[source] Error),
350    #[error("invalid disk")]
351    InvalidDisk(#[source] disk_backend::InvalidDisk),
352}
353
354impl ResolveResource<DiskHandleKind, OpenVhdmpDiskConfig> for VhdmpDiskResolver {
355    type Output = ResolvedDisk;
356    type Error = ResolveVhdmpDiskError;
357
358    fn resolve(
359        &self,
360        rsrc: OpenVhdmpDiskConfig,
361        input: ResolveDiskParameters<'_>,
362    ) -> Result<Self::Output, Self::Error> {
363        ResolvedDisk::new(
364            VhdmpDisk::new(rsrc.0, input.read_only).map_err(ResolveVhdmpDiskError::Vhdmp)?,
365        )
366        .map_err(ResolveVhdmpDiskError::InvalidDisk)
367    }
368}
369
370/// Implementation of [`DiskIo`] for VHD and VHDX files, using the VHDMP driver
371/// as the parser.
372#[derive(Debug, Inspect)]
373pub struct VhdmpDisk {
374    #[inspect(flatten)]
375    vhd: FileDisk,
376    /// Lock uses to serialize IOs, since FileDisk currently cannot handle
377    /// multiple concurrent IOs on files opened with FILE_FLAG_OVERLAPPED on
378    /// Windows (and the VHDMP handle is opened with FILE_FLAG_OVERLAPPED).
379    #[inspect(skip)]
380    io_lock: futures::lock::Mutex<()>,
381    disk_id: Guid,
382}
383
384#[derive(Debug, Error)]
385pub enum Error {
386    #[error("failed to open VHD")]
387    Open(#[source] std::io::Error),
388    #[error("failed to attach VHD")]
389    Attach(#[source] std::io::Error),
390    #[error("failed to query VHD metadata")]
391    Query(#[source] std::io::Error),
392}
393
394impl VhdmpDisk {
395    /// Opens a VHD for use with [`Self::new()`].
396    pub fn open_vhd(path: &Path, read_only: bool) -> Result<Vhd, Error> {
397        let vhd = Vhd::open(path, read_only).map_err(Error::Open)?;
398
399        // N.B. This must be attached here and not later in a worker process
400        //      since this operation may require impersonation, which is
401        //      prohibited from a sandboxed process.
402        vhd.attach_for_raw_access(read_only)
403            .map_err(Error::Attach)?;
404        Ok(vhd)
405    }
406
407    /// Creates a disk from an open VHD handle. `vhd` should have been opened via [`Self::open_vhd()`].
408    pub fn new(vhd: Vhd, read_only: bool) -> Result<Self, Error> {
409        let size = vhd.get_size().map_err(Error::Query)?;
410        let disk_id = vhd.get_disk_id().map_err(Error::Query)?;
411        let metadata = disk_file::Metadata {
412            disk_size: size.VirtualSize,
413            sector_size: size.SectorSize,
414            physical_sector_size: vhd.get_physical_sector_size().map_err(Error::Query)?,
415            read_only,
416        };
417        let vhd = FileDisk::with_metadata(vhd.0, metadata);
418
419        Ok(Self {
420            vhd,
421            io_lock: Default::default(),
422            disk_id,
423        })
424    }
425}
426
427impl DiskIo for VhdmpDisk {
428    fn disk_type(&self) -> &str {
429        "vhdmp"
430    }
431
432    fn sector_count(&self) -> u64 {
433        self.vhd.sector_count()
434    }
435
436    fn sector_size(&self) -> u32 {
437        self.vhd.sector_size()
438    }
439
440    fn is_read_only(&self) -> bool {
441        self.vhd.is_read_only()
442    }
443
444    fn disk_id(&self) -> Option<[u8; 16]> {
445        Some(self.disk_id.into())
446    }
447
448    fn physical_sector_size(&self) -> u32 {
449        self.vhd.physical_sector_size()
450    }
451
452    fn is_fua_respected(&self) -> bool {
453        self.vhd.is_fua_respected()
454    }
455
456    async fn read_vectored(
457        &self,
458        buffers: &RequestBuffers<'_>,
459        sector: u64,
460    ) -> Result<(), DiskError> {
461        let _locked = self.io_lock.lock().await;
462        self.vhd.read(buffers, sector).await
463    }
464
465    async fn write_vectored(
466        &self,
467        buffers: &RequestBuffers<'_>,
468        sector: u64,
469        fua: bool,
470    ) -> Result<(), DiskError> {
471        let _locked = self.io_lock.lock().await;
472        self.vhd.write(buffers, sector, fua).await
473    }
474
475    async fn sync_cache(&self) -> Result<(), DiskError> {
476        let _locked = self.io_lock.lock().await;
477        self.vhd.flush().await
478    }
479
480    async fn unmap(
481        &self,
482        _sector: u64,
483        _count: u64,
484        _block_level_only: bool,
485    ) -> Result<(), DiskError> {
486        Ok(())
487    }
488
489    fn unmap_behavior(&self) -> disk_backend::UnmapBehavior {
490        disk_backend::UnmapBehavior::Ignored
491    }
492}
493
494#[cfg(test)]
495mod tests {
496    use super::VhdmpDisk;
497    use disk_backend::DiskError;
498    use disk_backend::DiskIo;
499    use disk_vhd1::Vhd1Disk;
500    use guestmem::GuestMemory;
501    use pal_async::async_test;
502    use scsi_buffers::OwnedRequestBuffers;
503    use std::io::Write;
504    use tempfile::TempPath;
505
506    fn make_test_vhd() -> TempPath {
507        let mut f = tempfile::Builder::new().suffix(".vhd").tempfile().unwrap();
508        let size = 0x300000;
509        f.write_all(&vec![0u8; size]).unwrap();
510        Vhd1Disk::make_fixed(f.as_file()).unwrap();
511        f.into_temp_path()
512    }
513
514    #[test]
515    fn open_readonly() {
516        let path = make_test_vhd();
517        let _vhd = VhdmpDisk::open_vhd(path.as_ref(), true).unwrap();
518        let _vhd = VhdmpDisk::open_vhd(path.as_ref(), true).unwrap();
519        let _vhd = VhdmpDisk::open_vhd(path.as_ref(), false).unwrap_err();
520    }
521
522    #[async_test]
523    async fn test_invalid_lba() {
524        let path = make_test_vhd();
525        let vhd = VhdmpDisk::open_vhd(path.as_ref(), true).unwrap();
526        let disk = VhdmpDisk::new(vhd, true).unwrap();
527        let gm = GuestMemory::allocate(512);
528        match disk
529            .read_vectored(
530                &OwnedRequestBuffers::linear(0, 512, true).buffer(&gm),
531                0x10000000,
532            )
533            .await
534        {
535            Err(DiskError::IllegalBlock) => {}
536            r => panic!("unexpected result: {:?}", r),
537        }
538    }
539}