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