disk_vhd1/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! A VHD1 disk implementation. Currently only supports fixed VHD1.
5
6#![expect(missing_docs)]
7#![forbid(unsafe_code)]
8
9use disk_backend::DiskError;
10use disk_backend::DiskIo;
11use disk_backend::resolve::ResolveDiskParameters;
12use disk_backend::resolve::ResolvedDisk;
13use disk_backend_resources::FixedVhd1DiskHandle;
14use disk_file::FileDisk;
15use guid::Guid;
16use inspect::Inspect;
17use scsi_buffers::RequestBuffers;
18use std::fs::File;
19use std::io;
20use std::io::Read;
21use std::io::Seek;
22use std::io::Write;
23use thiserror::Error;
24use vhd1_defs::VhdFooter;
25use vm_resource::ResolveResource;
26use vm_resource::declare_static_resolver;
27use vm_resource::kind::DiskHandleKind;
28use zerocopy::FromZeros;
29use zerocopy::IntoBytes;
30
31pub struct Vhd1Resolver;
32declare_static_resolver!(Vhd1Resolver, (DiskHandleKind, FixedVhd1DiskHandle));
33
34#[derive(Debug, Error)]
35pub enum ResolveVhd1DiskError {
36    #[error("failed to open VHD")]
37    Open(#[source] OpenError),
38    #[error("invalid disk")]
39    InvalidDisk(#[source] disk_backend::InvalidDisk),
40}
41
42impl ResolveResource<DiskHandleKind, FixedVhd1DiskHandle> for Vhd1Resolver {
43    type Output = ResolvedDisk;
44    type Error = ResolveVhd1DiskError;
45
46    fn resolve(
47        &self,
48        rsrc: FixedVhd1DiskHandle,
49        params: ResolveDiskParameters<'_>,
50    ) -> Result<Self::Output, Self::Error> {
51        let disk =
52            Vhd1Disk::open_fixed(rsrc.0, params.read_only).map_err(ResolveVhd1DiskError::Open)?;
53        ResolvedDisk::new(disk).map_err(ResolveVhd1DiskError::InvalidDisk)
54    }
55}
56
57/// An open VHD1 disk.
58#[derive(Debug, Inspect)]
59pub struct Vhd1Disk {
60    #[inspect(flatten)]
61    file: FileDisk,
62    unique_id: Guid,
63}
64
65const DEFAULT_SECTOR_SIZE: u32 = 512;
66const DEFAULT_PHYSICAL_SECTOR_SIZE: u32 = 512;
67
68#[derive(Debug)]
69struct Metadata {
70    disk_size: u64,
71    sector_size: u32,
72    unique_id: Guid,
73}
74
75impl Metadata {
76    /// Parses the essential metadata out of the footer.
77    fn from_footer(footer: VhdFooter, file_size: u64) -> Result<Metadata, OpenError> {
78        if footer.cookie != VhdFooter::COOKIE_MAGIC {
79            return Err(OpenError::InvalidFooterCookie);
80        }
81        if footer.checksum != footer.compute_checksum().to_be_bytes() {
82            return Err(OpenError::InvalidFooterChecksum);
83        }
84        if footer.file_format_version != VhdFooter::FILE_FORMAT_VERSION_MAGIC.to_be_bytes() {
85            return Err(OpenError::UnsupportedVersion(
86                footer.file_format_version.into(),
87            ));
88        }
89        // FUTURE: support parsing non-fixed VHDs.
90        if footer.disk_type != VhdFooter::DISK_TYPE_FIXED.to_be_bytes() {
91            return Err(OpenError::NotFixed);
92        }
93        let disk_size = footer.current_size.into();
94        let sector_size = DEFAULT_SECTOR_SIZE;
95        if disk_size > file_size - VhdFooter::LEN || disk_size % (sector_size as u64) != 0 {
96            return Err(OpenError::InvalidDiskSize(disk_size));
97        }
98
99        let unique_id = footer.unique_id;
100        Ok(Metadata {
101            disk_size,
102            sector_size,
103            unique_id,
104        })
105    }
106}
107
108/// An error encountered while opening a VHD.
109#[derive(Debug, thiserror::Error)]
110#[non_exhaustive]
111pub enum OpenError {
112    #[error("invalid VHD file size: {0}")]
113    InvalidFileSize(u64),
114    #[error("invalid VHD disk size: {0}")]
115    InvalidDiskSize(u64),
116    #[error("io error")]
117    Io(#[from] io::Error),
118    #[error("VHD file footer is missing")]
119    InvalidFooterCookie,
120    #[error("invalid VHD footer checksum")]
121    InvalidFooterChecksum,
122    #[error("unsupported VHD version: {0:#x}")]
123    UnsupportedVersion(u32),
124    #[error("not a fixed VHD")]
125    NotFixed,
126}
127
128impl Vhd1Disk {
129    /// Turns a raw image into a fixed VHD.
130    pub fn make_fixed(mut file: &File) -> Result<(), OpenError> {
131        let meta = file.metadata()?;
132        let len = meta.len();
133        if len % VhdFooter::ALIGNMENT != 0 {
134            return Err(OpenError::InvalidDiskSize(len));
135        }
136        file.seek(io::SeekFrom::End(0))?;
137        file.write_all(VhdFooter::new_fixed(len, Guid::new_random()).as_bytes())?;
138        Ok(())
139    }
140
141    /// Opens a fixed VHD.
142    pub fn open_fixed(mut file: File, read_only: bool) -> Result<Self, OpenError> {
143        let meta = file.metadata()?;
144        let len = meta.len();
145        if len < VhdFooter::LEN || len % VhdFooter::ALIGNMENT != 0 {
146            return Err(OpenError::InvalidFileSize(len));
147        }
148        file.seek(io::SeekFrom::End(-512))?;
149        let mut footer: VhdFooter = FromZeros::new_zeroed();
150        file.read_exact(footer.as_mut_bytes())?;
151        let metadata = Metadata::from_footer(footer, len)?;
152
153        // Just wrap FileDisk for handling actual IO.
154        let file = FileDisk::with_metadata(
155            file,
156            disk_file::Metadata {
157                disk_size: metadata.disk_size,
158                sector_size: metadata.sector_size,
159                physical_sector_size: DEFAULT_PHYSICAL_SECTOR_SIZE,
160                read_only,
161            },
162        );
163
164        Ok(Self {
165            file,
166            unique_id: metadata.unique_id,
167        })
168    }
169
170    /// Drops the parsing state, returning the file handle.
171    pub fn into_inner(self) -> File {
172        self.file.into_inner()
173    }
174}
175
176impl DiskIo for Vhd1Disk {
177    fn disk_type(&self) -> &str {
178        "vhd1"
179    }
180
181    fn sector_count(&self) -> u64 {
182        self.file.sector_count()
183    }
184
185    fn sector_size(&self) -> u32 {
186        self.file.sector_size()
187    }
188
189    fn is_read_only(&self) -> bool {
190        self.file.is_read_only()
191    }
192
193    fn disk_id(&self) -> Option<[u8; 16]> {
194        Some(self.unique_id.into())
195    }
196
197    fn physical_sector_size(&self) -> u32 {
198        self.file.physical_sector_size()
199    }
200
201    fn is_fua_respected(&self) -> bool {
202        self.file.is_fua_respected()
203    }
204
205    async fn read_vectored(
206        &self,
207        buffers: &RequestBuffers<'_>,
208        sector: u64,
209    ) -> Result<(), DiskError> {
210        self.file.read_vectored(buffers, sector).await
211    }
212
213    async fn write_vectored(
214        &self,
215        buffers: &RequestBuffers<'_>,
216        sector: u64,
217        fua: bool,
218    ) -> Result<(), DiskError> {
219        self.file.write_vectored(buffers, sector, fua).await
220    }
221
222    async fn sync_cache(&self) -> Result<(), DiskError> {
223        self.file.sync_cache().await
224    }
225
226    async fn unmap(
227        &self,
228        _sector: u64,
229        _count: u64,
230        _block_level_only: bool,
231    ) -> Result<(), DiskError> {
232        Ok(())
233    }
234
235    fn unmap_behavior(&self) -> disk_backend::UnmapBehavior {
236        disk_backend::UnmapBehavior::Ignored
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::Vhd1Disk;
243    use disk_backend::Disk;
244    use guestmem::GuestMemory;
245    use pal_async::async_test;
246    use scsi_buffers::OwnedRequestBuffers;
247    use std::io::Write;
248    use zerocopy::IntoBytes;
249
250    #[async_test]
251    async fn open_fixed() {
252        let mut file = tempfile::tempfile().unwrap();
253        let data = (0..0x100000_u32).collect::<Vec<_>>();
254        file.write_all(data.as_bytes()).unwrap();
255        Vhd1Disk::make_fixed(&file).unwrap();
256        let vhd = Disk::new(Vhd1Disk::open_fixed(file, false).unwrap()).unwrap();
257
258        let mem = GuestMemory::allocate(0x1000);
259
260        let mut buf = [0_u32; 128];
261        vhd.read_vectored(
262            &OwnedRequestBuffers::linear(0, 512, true).buffer(&mem),
263            1000,
264        )
265        .await
266        .unwrap();
267        mem.read_at(0, buf.as_mut_bytes()).unwrap();
268        assert!(buf.iter().copied().eq(1000_u32 * 128..1001 * 128));
269    }
270}