1use anyhow::Context;
10use guid::Guid;
11use mshv_host_boot::HYPERVISOR_CMDLINE_FILE;
12use mshv_host_boot::KERNEL_CMDLINE_FILE;
13use std::collections::BTreeMap;
14use std::io::Seek;
15use std::io::Write;
16use std::path::Path;
17use std::path::PathBuf;
18
19const SECTOR_SIZE: u64 = 512;
20const MIB: u64 = 1024 * 1024;
21const ESP_GUID: Guid = guid::guid!("C12A7328-F81F-11D2-BA4B-00A0C93EC93B");
22
23#[derive(Debug)]
24struct ImageTimeProvider;
25
26impl fatfs::TimeProvider for ImageTimeProvider {
27 fn get_current_date(&self) -> fatfs::Date {
28 fatfs::Date {
29 year: 1980,
30 month: 1,
31 day: 1,
32 }
33 }
34
35 fn get_current_date_time(&self) -> fatfs::DateTime {
36 fatfs::DateTime {
37 date: self.get_current_date(),
38 time: fatfs::Time {
39 hour: 0,
40 min: 0,
41 sec: 0,
42 millis: 0,
43 },
44 }
45 }
46}
47
48pub struct MshvImage<'a> {
51 pub host_boot: &'a Path,
53 pub payload: &'a Path,
56 pub kernel: &'a Path,
58 pub initrd: &'a Path,
60 pub kernel_cmdline: &'a str,
62 pub hypervisor_cmdline: &'a str,
64}
65
66impl MshvImage<'_> {
67 pub fn build(&self, output: &Path) -> anyhow::Result<()> {
72 let mut files = BTreeMap::new();
73 for (destination, source) in [
74 ("EFI/BOOT/BOOTX64.EFI", self.host_boot.to_path_buf()),
75 ("HvLoader.efi", self.payload.join("HvLoader.efi")),
76 ("lxhvloader.dll", self.payload.join("lxhvloader.dll")),
77 ("bzImage", self.kernel.to_path_buf()),
78 ("initramfs.cpio", self.initrd.to_path_buf()),
79 ] {
80 insert_file(&mut files, destination.to_owned(), source)?;
81 }
82 collect_files(&mut files, self.payload, Path::new("Windows"))?;
83 for required in ["windows/system32/hvix64.exe", "windows/system32/hvax64.exe"] {
84 anyhow::ensure!(
85 files.keys().any(|path| path.eq_ignore_ascii_case(required)),
86 "missing hypervisor payload file {required}"
87 );
88 }
89 let kernel_cmdline =
90 mshv_host_boot::command_line(self.kernel_cmdline).map_err(anyhow::Error::msg)?;
91 let hypervisor_cmdline =
92 mshv_host_boot::command_line(self.hypervisor_cmdline).map_err(anyhow::Error::msg)?;
93 let configs = [
94 (
95 KERNEL_CMDLINE_FILE,
96 format!("initrd=\\initramfs.cpio {kernel_cmdline}\n"),
97 ),
98 (
99 HYPERVISOR_CMDLINE_FILE,
100 format!("\\lxhvloader.dll {hypervisor_cmdline}\n"),
101 ),
102 ];
103 for (_, content) in &configs {
104 mshv_host_boot::command_line(content).map_err(anyhow::Error::msg)?;
105 }
106 let config_size: u64 = configs.iter().map(|(_, value)| value.len() as u64).sum();
107 let payload_size = files.values().try_fold(config_size, |size, path| {
108 size.checked_add(fs_err::metadata(path)?.len())
109 .context("image size overflow")
110 })?;
111 let size = payload_size
114 .checked_add(payload_size / 8)
115 .and_then(|size| size.checked_add(64 * MIB))
116 .context("image size overflow")?;
117 let sectors = size.div_ceil(SECTOR_SIZE);
118 anyhow::ensure!(
119 sectors <= u32::MAX as u64,
120 "image exceeds the FAT32 sector-count limit"
121 );
122 let parent = output
123 .parent()
124 .filter(|path| !path.as_os_str().is_empty())
125 .unwrap_or(Path::new("."));
126 let mut image = tempfile::NamedTempFile::new_in(parent)?;
127 image.as_file().set_len(sectors * SECTOR_SIZE)?;
128
129 let mut gpt = gptman::GPT::new_from(&mut image, SECTOR_SIZE, Guid::new_random().into())?;
130 gptman::GPT::write_protective_mbr_into(&mut image, SECTOR_SIZE)?;
131 gpt[1] = gptman::GPTPartitionEntry {
132 partition_type_guid: ESP_GUID.into(),
133 unique_partition_guid: Guid::new_random().into(),
134 starting_lba: 2048,
135 ending_lba: gpt.header.last_usable_lba,
136 attribute_bits: 0,
137 partition_name: "MSHV".into(),
138 };
139 gpt.write_into(&mut image)?;
140 let mut partition = fscommon::StreamSlice::new(
141 &mut image,
142 gpt[1].starting_lba * SECTOR_SIZE,
143 (gpt[1].ending_lba + 1) * SECTOR_SIZE,
144 )?;
145 fatfs::format_volume(
146 &mut partition,
147 fatfs::FormatVolumeOptions::new()
148 .fat_type(fatfs::FatType::Fat32)
149 .volume_label(*b"MSHV_BOOT "),
150 )?;
151 partition.rewind()?;
152 let fs = fatfs::FileSystem::new(
155 partition,
156 fatfs::FsOptions::new().time_provider(&ImageTimeProvider),
157 )?;
158 {
159 let root = fs.root_dir();
160 for (destination, source) in &files {
161 let mut dest = create_file(&root, destination)?;
162 std::io::copy(&mut fs_err::File::open(source)?, &mut dest)
163 .with_context(|| format!("copying {} to {destination}", source.display()))?;
164 dest.flush()?;
165 }
166 for (path, content) in &configs {
167 let mut dest = create_file(&root, path)?;
168 dest.write_all(content.as_bytes())?;
169 dest.flush()?;
170 }
171 }
172 fs.unmount()?;
173 image.as_file().sync_all()?;
174 image
175 .persist_noclobber(output)
176 .with_context(|| format!("publishing {}", output.display()))?;
177 Ok(())
178 }
179}
180
181fn insert_file(
182 files: &mut BTreeMap<String, PathBuf>,
183 destination: String,
184 source: PathBuf,
185) -> anyhow::Result<()> {
186 let metadata = fs_err::symlink_metadata(&source)?;
187 anyhow::ensure!(
188 metadata.file_type().is_file(),
189 "expected a regular file, not a symlink or special file: {}",
190 source.display()
191 );
192 anyhow::ensure!(
193 metadata.len() <= u32::MAX as u64,
194 "file exceeds the FAT32 file-size limit: {}",
195 source.display()
196 );
197 anyhow::ensure!(
198 !files
199 .keys()
200 .any(|path| path.eq_ignore_ascii_case(&destination)),
201 "duplicate FAT path: {destination}"
202 );
203 files.insert(destination, source);
204 Ok(())
205}
206
207fn collect_files(
208 files: &mut BTreeMap<String, PathBuf>,
209 root: &Path,
210 relative: &Path,
211) -> anyhow::Result<()> {
212 let path = root.join(relative);
213 let metadata = fs_err::symlink_metadata(&path)?;
214 anyhow::ensure!(
215 metadata.is_dir(),
216 "expected payload directory: {}",
217 path.display()
218 );
219 for entry in fs_err::read_dir(&path)? {
220 let entry = entry?;
221 let name = entry.file_name();
222 let name = name.to_str().context("non-UTF-8 payload filename")?;
223 anyhow::ensure!(
224 !name.is_empty()
225 && name.is_ascii()
226 && !name
227 .chars()
228 .any(|c| c.is_control() || "\"*/:<>?\\|".contains(c))
229 && !name.ends_with([' ', '.']),
230 "unsupported FAT filename: {name:?}"
231 );
232 let relative = relative.join(name);
233 let file_type = entry.file_type()?;
234 if file_type.is_dir() {
235 collect_files(files, root, &relative)?;
236 } else {
237 let destination = relative
238 .components()
239 .map(|component| component.as_os_str().to_str().context("non-UTF-8 path"))
240 .collect::<anyhow::Result<Vec<_>>>()?
241 .join("/");
242 insert_file(files, destination, entry.path())?;
243 }
244 }
245 Ok(())
246}
247
248fn create_file<'a, T: fatfs::ReadWriteSeek>(
249 root: &fatfs::Dir<'a, T>,
250 path: &str,
251) -> anyhow::Result<fatfs::File<'a, T>> {
252 if let Some((parents, _)) = path.rsplit_once('/') {
253 let mut directory = root.clone();
254 for component in parents.split('/') {
255 directory = directory
256 .create_dir(component)
257 .with_context(|| format!("creating directory for {path}"))?;
258 }
259 }
260 root.create_file(path)
261 .with_context(|| format!("creating {path}"))
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use std::io::Read;
268 use test_with_tracing::test;
269
270 #[test]
271 fn image_contents_and_no_clobber() -> anyhow::Result<()> {
272 let dir = tempfile::tempdir()?;
273 let payload = dir.path().join("payload");
274 fs_err::create_dir_all(payload.join("Windows/System32"))?;
275 for name in [
276 "HvLoader.efi",
277 "lxhvloader.dll",
278 "Windows/System32/hvix64.exe",
279 "Windows/System32/hvax64.exe",
280 "Windows/System32/kdstub.dll",
281 ] {
282 fs_err::write(payload.join(name), name)?;
283 }
284 let artifact = dir.path().join("artifact");
285 fs_err::write(&artifact, b"artifact")?;
286 let inputs = MshvImage {
287 host_boot: &artifact,
288 payload: &payload,
289 kernel: &artifact,
290 initrd: &artifact,
291 kernel_cmdline: "rdinit=/init console=ttyS0 key=\"two words\" x=$root;halt",
292 hypervisor_cmdline: "MSHV_ENABLE=TRUE MSHV_SCHEDULER_TYPE=CORE",
293 };
294 let output = dir.path().join("boot.img");
295 inputs.build(&output)?;
296 let mut image = fs_err::File::open(&output)?;
297 let gpt = gptman::GPT::read_from(&mut image, SECTOR_SIZE)?;
298 assert_eq!(gpt[1].partition_type_guid, <[u8; 16]>::from(ESP_GUID));
299 assert_eq!(gpt[1].starting_lba, 2048);
300 let partition = fscommon::StreamSlice::new(
301 image,
302 gpt[1].starting_lba * SECTOR_SIZE,
303 (gpt[1].ending_lba + 1) * SECTOR_SIZE,
304 )?;
305 let fs = fatfs::FileSystem::new(partition, fatfs::FsOptions::new())?;
306 assert_eq!(fs.fat_type(), fatfs::FatType::Fat32);
307 assert_valid_timestamps(&fs.root_dir())?;
308 for path in ["EFI/BOOT/BOOTX64.EFI", "bzImage", "initramfs.cpio"] {
309 let mut bytes = Vec::new();
310 fs.root_dir().open_file(path)?.read_to_end(&mut bytes)?;
311 assert_eq!(bytes, b"artifact");
312 }
313 for (path, expected) in [
314 (
315 KERNEL_CMDLINE_FILE,
316 format!("initrd=\\initramfs.cpio {}\n", inputs.kernel_cmdline),
317 ),
318 (
319 HYPERVISOR_CMDLINE_FILE,
320 format!("\\lxhvloader.dll {}\n", inputs.hypervisor_cmdline),
321 ),
322 ] {
323 let mut content = String::new();
324 fs.root_dir()
325 .open_file(path)?
326 .read_to_string(&mut content)?;
327 assert_eq!(content, expected);
328 }
329 assert!(fs.root_dir().open_file("EFI/BOOT/grubx64.efi").is_err());
330 assert!(fs.root_dir().open_dir("boot/grub2").is_err());
331 let mut extra = String::new();
332 fs.root_dir()
333 .open_file("Windows/System32/kdstub.dll")?
334 .read_to_string(&mut extra)?;
335 assert_eq!(extra, "Windows/System32/kdstub.dll");
336 fs.unmount()?;
337 for (kernel_cmdline, hypervisor_cmdline) in [
338 ("x\nhalt", inputs.hypervisor_cmdline),
339 (inputs.kernel_cmdline, "MSHV_ENABLE=TRUE\0"),
340 ] {
341 let invalid = MshvImage {
342 kernel_cmdline,
343 hypervisor_cmdline,
344 ..inputs
345 };
346 assert!(invalid.build(&dir.path().join("invalid.img")).is_err());
347 assert!(!dir.path().join("invalid.img").exists());
348 }
349 fs_err::write(&output, b"do not overwrite")?;
350 assert!(inputs.build(&output).is_err());
351 assert_eq!(fs_err::read(&output)?, b"do not overwrite");
352 fs_err::remove_file(payload.join("Windows/System32/hvix64.exe"))?;
353 assert!(inputs.build(&dir.path().join("missing.img")).is_err());
354 assert!(!dir.path().join("missing.img").exists());
355 Ok(())
356 }
357
358 fn assert_valid_timestamps<T: fatfs::ReadWriteSeek>(
359 directory: &fatfs::Dir<'_, T>,
360 ) -> anyhow::Result<()> {
361 use fatfs::TimeProvider;
362
363 for entry in directory.iter() {
364 let entry = entry?;
365 let expected = ImageTimeProvider.get_current_date_time();
366 assert_eq!(entry.created(), expected, "{}", entry.file_name());
367 assert_eq!(entry.modified(), expected, "{}", entry.file_name());
368 assert_eq!(entry.accessed(), expected.date, "{}", entry.file_name());
369 if entry.is_dir() && !matches!(entry.file_name().as_str(), "." | "..") {
370 assert_valid_timestamps(&entry.to_dir())?;
371 }
372 }
373 Ok(())
374 }
375
376 #[test]
377 fn rejects_case_collisions_and_oversized_files() -> anyhow::Result<()> {
378 let dir = tempfile::tempdir()?;
379 let source = dir.path().join("source");
380 let file = fs_err::File::create(&source)?;
381 let mut files = BTreeMap::new();
382 insert_file(&mut files, "Windows/a.dll".into(), source.clone())?;
383 assert!(insert_file(&mut files, "windows/A.DLL".into(), source.clone()).is_err());
384 file.set_len(u32::MAX as u64 + 1)?;
385 assert!(insert_file(&mut files, "Windows/b.dll".into(), source).is_err());
386 Ok(())
387 }
388
389 #[cfg(unix)]
390 #[test]
391 fn rejects_payload_symlinks() -> anyhow::Result<()> {
392 let dir = tempfile::tempdir()?;
393 let windows = dir.path().join("Windows");
394 fs_err::create_dir(&windows)?;
395 std::os::unix::fs::symlink(dir.path(), windows.join("loop"))?;
396 assert!(collect_files(&mut BTreeMap::new(), dir.path(), Path::new("Windows")).is_err());
397 Ok(())
398 }
399}