Skip to main content

openvmm_helpers/
disk.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Guest disk helpers.
5
6use anyhow::Context;
7use std::path::Path;
8use vm_resource::Resource;
9use vm_resource::kind::DiskHandleKind;
10
11fn disk_open_error(path: &Path, verb: &str) -> String {
12    let mut msg = format!("{verb} '{}'", path.display());
13
14    // On windows, attempt to detect we ran under wsl by reading the WSLENV and
15    // bail out with a helpful hint that it needs to be a windows path.
16    if cfg!(windows) && std::env::var_os("WSLENV").is_some() {
17        msg += ". Linux paths are not supported when running Windows executables \
18                under WSL, make sure the path is a valid Windows path \
19                (use `wslpath -w` to convert)";
20    }
21
22    msg
23}
24
25/// Options for opening a disk file.
26#[derive(Clone, Copy)]
27pub struct OpenDiskOptions {
28    /// Open the disk as read-only.
29    pub read_only: bool,
30    /// Bypass the OS page cache for direct disk I/O.
31    pub direct: bool,
32}
33
34/// Opens the resources needed for using a disk from a file at `path`.
35///
36/// If the file ends with .vhd and is a fixed VHD1, it will be opened using
37/// the user-mode VHD parser. Otherwise, if the file ends with .vhd, the
38/// file will be opened using the kernel-mode VHD parser (Windows only).
39///
40/// If the file ends with .vhdx, the kernel-mode VHD parser is used on
41/// Windows. On Linux, the pure-Rust VHDX parser is used, with automatic
42/// parent-locator walking for differencing chains.
43///
44/// If the file ends with .vmdk, the VMDK backend is used for all platforms.
45pub async fn open_disk_type(
46    path: &Path,
47    options: OpenDiskOptions,
48) -> anyhow::Result<Resource<DiskHandleKind>> {
49    let read_only = options.read_only;
50    let ensure_no_direct = |ext| {
51        if options.direct {
52            anyhow::bail!("direct I/O is not supported for {ext} files");
53        };
54        Ok(())
55    };
56    Ok(match path.extension().and_then(|s| s.to_str()) {
57        Some("vmdk") => {
58            ensure_no_direct(".vmdk")?;
59            disk_vmdk::open_vmdk(path, read_only)
60                .with_context(|| disk_open_error(path, "failed to open"))?
61        }
62        Some("vhd") => {
63            let file = std::fs::OpenOptions::new()
64                .read(true)
65                .write(!read_only)
66                .open(path)
67                .with_context(|| disk_open_error(path, "failed to open"))?;
68
69            match disk_vhd1::Vhd1Disk::open_fixed(file, read_only) {
70                Ok(vhd) => {
71                    ensure_no_direct("fixed .vhd")?;
72                    Resource::new(disk_backend_resources::FixedVhd1DiskHandle(
73                        vhd.into_inner(),
74                    ))
75                }
76                Err(disk_vhd1::OpenError::NotFixed) => {
77                    #[cfg(windows)]
78                    {
79                        Resource::new(disk_vhdmp::OpenVhdmpDiskConfig(
80                            disk_vhdmp::VhdmpDisk::options()
81                                .read_only(read_only)
82                                .cached_io(!options.direct)
83                                .open(path)
84                                .with_context(|| disk_open_error(path, "failed to open"))?,
85                        ))
86                    }
87                    #[cfg(not(windows))]
88                    anyhow::bail!("non-fixed VHD not supported on Linux");
89                }
90                Err(err) => return Err(err.into()),
91            }
92        }
93        Some("vhdx") => {
94            #[cfg(windows)]
95            {
96                Resource::new(disk_vhdmp::OpenVhdmpDiskConfig(
97                    disk_vhdmp::VhdmpDisk::options()
98                        .read_only(read_only)
99                        .cached_io(!options.direct)
100                        .open(path)
101                        .with_context(|| disk_open_error(path, "failed to open"))?,
102                ))
103            }
104            #[cfg(not(windows))]
105            {
106                ensure_no_direct(".vhdx")?;
107                disklayer_vhdx::chain::open_vhdx_chain(path, read_only).await?
108            }
109        }
110        Some("iso") if !read_only => {
111            anyhow::bail!("iso file cannot be opened as read/write")
112        }
113        Some("vmgs") => {
114            ensure_no_direct(".vmgs")?;
115            // VMGS files are fixed VHD1s. Don't bother to validate the footer
116            // here; let the resource resolver do that later.
117            let file = std::fs::OpenOptions::new()
118                .read(true)
119                .write(!read_only)
120                .open(path)
121                .with_context(|| disk_open_error(path, "failed to open"))?;
122
123            Resource::new(disk_backend_resources::FixedVhd1DiskHandle(file))
124        }
125        _ => open_raw_disk(path, options, None)?,
126    })
127}
128
129/// Create and open the resources needed for using a disk from a file at `path`.
130pub fn create_disk_type(
131    path: &Path,
132    size: u64,
133    options: OpenDiskOptions,
134) -> anyhow::Result<Resource<DiskHandleKind>> {
135    Ok(match path.extension().and_then(|s| s.to_str()) {
136        Some("vhd") | Some("vmgs") => {
137            if options.direct {
138                anyhow::bail!("direct I/O is not supported for VHD files");
139            }
140            let file = std::fs::OpenOptions::new()
141                .create(true)
142                .truncate(true)
143                .read(true)
144                .write(true)
145                .open(path)
146                .with_context(|| disk_open_error(path, "failed to create"))?;
147
148            file.set_len(size)?;
149            disk_vhd1::Vhd1Disk::make_fixed(&file)?;
150            Resource::new(disk_backend_resources::FixedVhd1DiskHandle(file))
151        }
152        Some("vhdx") => {
153            anyhow::bail!("creating vhdx not supported")
154        }
155        Some("vmdk") => {
156            anyhow::bail!("creating vmdk not supported")
157        }
158        Some("iso") => {
159            anyhow::bail!("creating iso not supported")
160        }
161        _ => open_raw_disk(path, options, Some(size))?,
162    })
163}
164
165/// Open or create a raw file or block device, returning the appropriate
166/// disk resource for the current platform.
167fn open_raw_disk(
168    path: &Path,
169    options: OpenDiskOptions,
170    size: Option<u64>,
171) -> anyhow::Result<Resource<DiskHandleKind>> {
172    if options.direct && !cfg!(target_os = "linux") {
173        anyhow::bail!("direct I/O is only supported on Linux");
174    }
175
176    let create = size.is_some();
177    let mut opts = std::fs::OpenOptions::new();
178    opts.read(true).write(!options.read_only);
179    if create {
180        opts.create(true).truncate(true);
181    }
182
183    #[cfg(target_os = "linux")]
184    if options.direct {
185        use std::os::unix::fs::OpenOptionsExt;
186        opts.custom_flags(libc::O_DIRECT);
187    }
188
189    let verb = if create {
190        "failed to create"
191    } else {
192        "failed to open"
193    };
194    let file = opts
195        .open(path)
196        .with_context(|| disk_open_error(path, verb))?;
197
198    if let Some(size) = size {
199        file.set_len(size)?;
200    }
201
202    #[cfg(target_os = "linux")]
203    {
204        Ok(Resource::new(
205            disk_backend_resources::BlockDeviceDiskHandle { file },
206        ))
207    }
208    #[cfg(not(target_os = "linux"))]
209    {
210        Ok(Resource::new(disk_backend_resources::FileDiskHandle(file)))
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use pal_async::async_test;
218
219    // Creates a minimal flat VMDK descriptor and source file for routing tests.
220    fn create_test_vmdk() -> (tempfile::TempDir, std::path::PathBuf) {
221        let directory = tempfile::tempdir().unwrap();
222        std::fs::write(directory.path().join("source.bin"), [0; 512]).unwrap();
223        let descriptor = directory.path().join("disk.vmdk");
224        std::fs::write(
225            &descriptor,
226            "# Disk DescriptorFile\n\
227             version=1\n\
228             parentCID=ffffffff\n\
229             createType=\"monolithicFlat\"\n\
230             # Extent description\n\
231             RW 1 FLAT \"source.bin\" 0\n",
232        )
233        .unwrap();
234        (directory, descriptor)
235    }
236
237    #[async_test]
238    async fn opens_flat_vmdk_read_only() {
239        let (_directory, descriptor) = create_test_vmdk();
240        open_disk_type(
241            &descriptor,
242            OpenDiskOptions {
243                read_only: true,
244                direct: false,
245            },
246        )
247        .await
248        .unwrap();
249    }
250
251    #[async_test]
252    async fn rejects_vmdk_write_direct_io_and_creation() {
253        let (_directory, descriptor) = create_test_vmdk();
254        // Writable VMDK opens are not supported.
255        assert!(
256            open_disk_type(
257                &descriptor,
258                OpenDiskOptions {
259                    read_only: false,
260                    direct: false,
261                },
262            )
263            .await
264            .is_err()
265        );
266        // VMDK sources cannot use direct I/O.
267        assert!(
268            open_disk_type(
269                &descriptor,
270                OpenDiskOptions {
271                    read_only: true,
272                    direct: true,
273                },
274            )
275            .await
276            .is_err()
277        );
278        // The generic disk creator must not treat VMDK as a raw file.
279        assert!(
280            create_disk_type(
281                &descriptor,
282                512,
283                OpenDiskOptions {
284                    read_only: false,
285                    direct: false,
286                },
287            )
288            .is_err()
289        );
290    }
291}