Skip to main content

uhyvelib/
vm.rs

1use std::{
2	env,
3	fmt::{self, Debug},
4	fs, io,
5	mem::{drop, take},
6	num::NonZero,
7	ops::Add,
8	path::PathBuf,
9	sync::{Arc, Barrier, Mutex},
10	thread,
11	time::SystemTime,
12};
13
14use align_address::Align;
15use core_affinity::CoreId;
16use gdbstub::{arch::Arch, target::Target};
17use hermit_entry::{
18	Format, HermitVersion, UhyveIfVersion,
19	boot_info::{BootInfo, HardwareInfo, LoadInfo, PlatformInfo, RawBootInfo, SerialPortBase},
20	config, detect_format,
21	elf::{KernelObject, LoadedKernel, ParseKernelError},
22};
23use log::error;
24use nix::sys::pthread::{Pthread, pthread_self};
25use thiserror::Error;
26use uhyve_interface::GuestPhysAddr;
27
28use crate::{
29	HypervisorError, PAGE_SIZE, V1_ADDR_RANGE, V2_ADDR_RANGE,
30	fdt::Fdt,
31	gdb::GdbVcpuManager,
32	isolation::filemap::{UhyveFileMap, UhyveMapLeaf},
33	mem::MmapMemory,
34	net::NetworkBackend,
35	params::{EnvVars, HermitImageMode, NetworkMode, Params},
36	parking::Parker,
37	serial::{Destination, UhyveSerial},
38	stats::{CpuStats, VmStats},
39	vcpu::VirtualCPU,
40};
41#[cfg(target_os = "linux")]
42use crate::{
43	isolation::landlock::initialize,
44	params::{FileSandboxMode, Output},
45};
46
47pub type HypervisorResult<T> = Result<T, HypervisorError>;
48
49#[derive(Error, Debug)]
50pub enum LoadKernelError {
51	#[error(transparent)]
52	Io(#[from] io::Error),
53	#[error("{0}")]
54	ParseKernelError(ParseKernelError),
55	#[error("guest memory size is not large enough")]
56	InsufficientMemory,
57}
58
59type LoadKernelResult<T> = Result<T, LoadKernelError>;
60
61#[cfg(target_os = "linux")]
62pub type DefaultBackend = crate::os::x86_64::kvm_cpu::KvmVm;
63#[cfg(target_os = "macos")]
64pub type DefaultBackend = crate::os::XhyveVm;
65
66/// Trait marking a interface for creating (accelerated) VMs.
67pub(crate) trait VirtualizationBackendInternal: Sized {
68	type VCPU: 'static + VirtualCPU;
69	type VirtioNetImpl: NetworkBackend;
70	const NAME: &str;
71
72	/// Create a new CPU object
73	fn new_cpu(
74		&self,
75		id: usize,
76		kernel_info: Arc<KernelInfo>,
77		enable_stats: bool,
78	) -> HypervisorResult<Self::VCPU>;
79
80	fn new(
81		peripherals: Arc<VmPeripherals<Self::VirtioNetImpl>>,
82		params: &Params,
83	) -> HypervisorResult<Self>;
84
85	fn virtio_net_device(mode: NetworkMode, mmap: Arc<MmapMemory>) -> Self::VirtioNetImpl;
86}
87
88#[derive(Debug, Clone)]
89pub struct VmResult {
90	pub code: i32,
91	pub output: Option<String>,
92	pub stats: Option<VmStats>,
93}
94
95/// mutable devices that a vCPU interacts with
96#[derive(Debug)]
97pub(crate) struct VmPeripherals<VirtioNetImpl: NetworkBackend> {
98	pub file_mapping: Mutex<UhyveFileMap>,
99	pub mem: Arc<MmapMemory>,
100	pub(crate) serial: UhyveSerial,
101	pub virtio_device: Option<Mutex<VirtioNetImpl>>,
102}
103
104// This uses the "private sealed supertrait pattern".
105#[allow(private_bounds)]
106pub trait VirtualizationBackend: Sized + VirtualizationBackendInternal {}
107
108// TODO: Investigate soundness
109// https://github.com/hermitcore/uhyve/issues/229
110unsafe impl<N: NetworkBackend> Send for VmPeripherals<N> {}
111
112unsafe impl<N: NetworkBackend> Sync for VmPeripherals<N> {}
113
114/// static information that does not change during execution
115#[derive(Debug)]
116pub(crate) struct KernelInfo {
117	/// The first instruction after boot
118	pub entry_point: GuestPhysAddr,
119	/// The starting position of the image in physical memory
120	// currently only needed in gdb
121	pub kernel_address: GuestPhysAddr,
122	pub params: Params,
123	pub path: PathBuf,
124	pub stack_address: GuestPhysAddr,
125	/// The location of the whole guest in the physical address space
126	pub guest_address: GuestPhysAddr,
127}
128
129// guest_address + OFFSET
130pub(crate) const BOOT_INFO_OFFSET: u64 = 0x9000;
131const FDT_OFFSET: u64 = 0x5000;
132pub(crate) const KERNEL_OFFSET: u64 = 0x40000;
133const KERNEL_STACK_SIZE: u64 = 0x8000;
134/// The signal for kicking vCPUs out of KVM_RUN.
135///
136/// It is used to stop a vCPU from another thread.
137pub(crate) struct KickSignal;
138
139/// Returns a guest & start address tuple based on the object file.
140///
141/// Generates a tuple containing a potentially random guest address and a derived
142/// start address for Uhyve's virtualized memory. The guest address will not be
143/// random under the following conditions:
144/// - The image is not relocatable / uses uhyve-interface v1.
145/// - ASLR is disabled.
146///
147/// If the image is not relocatable, the start address will be equal to that
148/// present in the unikernel image file's object representation.
149///
150/// - `interface_version`: Version of uhyve-interface.
151/// - `aslr`: `bool` describing whether ASLR is enabled (`true`) or disabled (`false`).
152/// - `object_mem_size`: Memory required to load the object file onto the guest's memory.
153/// - `object_start_addr`: Start address embedded in the unikernel image (if applicable).
154/// - `mem_size`: User-defined memory size that should be available to the VM.
155pub(crate) fn generate_guest_start_address(
156	interface_version: UhyveIfVersion,
157	aslr: bool,
158	object_mem_size: usize,
159	object_start_addr: Option<u64>,
160	mem_size: usize,
161) -> (GuestPhysAddr, GuestPhysAddr) {
162	// Using an interface-specific version's range and by using checked_sub, we
163	// guarantee that the range used during the kernel's execution won't lead
164	// to a boundary violation during the guest's execution.
165	let (guest_address_lb, guest_address_ub): (u64, u64) = {
166		let range = match interface_version.0 {
167			1 => V1_ADDR_RANGE,
168			2 | 3 => V2_ADDR_RANGE,
169			_ => unimplemented!(),
170		};
171		// KERNEL_OFFSET will be added again later for the start address, later.
172		let mem_size = (object_mem_size + mem_size) as u64 + KERNEL_OFFSET;
173		(
174			range.0,
175			range.1.checked_sub(mem_size).unwrap_or_else(|| {
176				let (lb, ub) = range;
177				let hint = match interface_version.0 {
178					1 => " (More than 3GiB memory is not supported for Hermit <= 0.13.2)",
179					2 | 3 => "",
180					_ => unimplemented!(),
181				};
182				panic!("Out of range [{lb:#x}, {ub:#x}) due to memory size {mem_size:#x}.{hint}")
183			}),
184		)
185	};
186
187	match (aslr, object_start_addr) {
188		(true, None) => {
189			let mut rng = rand::rng();
190			let guest_address = GuestPhysAddr::new(
191				rand::RngExt::random_range(&mut rng, guest_address_lb..=guest_address_ub)
192					.align_down(0x20_0000),
193			);
194			(guest_address, guest_address.add(KERNEL_OFFSET))
195		}
196		(false, None) => {
197			let guest_address = GuestPhysAddr::new(guest_address_lb);
198			(guest_address, guest_address.add(KERNEL_OFFSET))
199		}
200		(_, Some(predefined_start_address)) => {
201			assert!(
202				(guest_address_lb..=guest_address_ub).contains(&predefined_start_address),
203				"Predefined address {predefined_start_address:#x} out of range of possible
204				 guest addresses: [{guest_address_lb:#x}, {guest_address_ub:#x}]."
205			);
206			if aslr {
207				warn!("ASLR is enabled but kernel is not relocatable - disabling ASLR");
208			}
209			(
210				GuestPhysAddr::new(guest_address_lb),
211				GuestPhysAddr::new(predefined_start_address),
212			)
213		}
214	}
215}
216
217pub struct UhyveVm<VirtBackend: VirtualizationBackend> {
218	pub(crate) vcpus: Vec<<VirtBackend as VirtualizationBackendInternal>::VCPU>,
219	pub(crate) peripherals: Arc<VmPeripherals<VirtBackend::VirtioNetImpl>>,
220	pub(crate) kernel_info: Arc<KernelInfo>,
221	_virt_backend: VirtBackend,
222}
223#[allow(private_bounds)]
224impl<VirtBackend: VirtualizationBackend<VirtioNetImpl: NetworkBackend>> UhyveVm<VirtBackend> {
225	pub fn new(kernel_path: PathBuf, mut params: Params) -> HypervisorResult<UhyveVm<VirtBackend>> {
226		let memory_size = params.memory_size.get();
227
228		let kernel_data = fs::read(&kernel_path)
229			.map_err(|_e| HypervisorError::InvalidKernelPath(kernel_path.clone()))?;
230
231		// TODO: file_mapping not in kernel_info
232		let mut file_mapping = UhyveFileMap::new(
233			&params.file_mapping,
234			params.tempdir.clone(),
235			#[cfg(target_os = "linux")]
236			params.io_mode,
237		);
238
239		let mut hermit_image = None;
240
241		// `kernel_data` might be an Hermit image
242		let elf = match detect_format(&kernel_data[..]) {
243			None => return Err(HypervisorError::InvalidKernelPath(kernel_path.clone())),
244			Some(Format::Elf) => kernel_data,
245			Some(Format::Gzip) => {
246				let buf_decompressed = {
247					use io::Read;
248
249					// decompress image
250					let mut buf_decompressed = Vec::new();
251					flate2::bufread::GzDecoder::new(&kernel_data[..])
252						.read_to_end(&mut buf_decompressed)?;
253					drop(kernel_data);
254					buf_decompressed
255				};
256
257				let keep_config_data;
258				let keep_kernel_data;
259
260				let handle = match params.hermit_image_mode {
261					HermitImageMode::External => {
262						// insert Hermit image tree into file map
263						file_mapping.add_hermit_image(&buf_decompressed[..])?;
264						drop(buf_decompressed);
265
266						let config_data = if let Some(UhyveMapLeaf::Virtual(data)) = file_mapping
267							.get_host_path(&("/".to_string() + config::Config::DEFAULT_PATH), true)
268						{
269							keep_config_data = data;
270							&keep_config_data[..]
271						} else {
272							return Err(HypervisorError::HermitImageConfigNotFound);
273						};
274
275						let konfig: config::Config<'_> = toml::from_slice(config_data)?;
276
277						// .kernel
278						let inner_kernel_path = match &konfig {
279							config::Config::V1 { kernel, .. } => kernel,
280						};
281						let raw_kernel = if let Some(UhyveMapLeaf::Virtual(data)) =
282							file_mapping.get_host_path(inner_kernel_path, true)
283						{
284							keep_kernel_data = data;
285							&keep_kernel_data[..]
286						} else {
287							error!("Unable to find kernel in Hermit image");
288							return Err(HypervisorError::InvalidKernelPath(kernel_path.clone()));
289						};
290
291						config::ConfigHandle {
292							config: konfig,
293							raw_kernel,
294						}
295					}
296					HermitImageMode::Internal => {
297						match config::parse_tar(hermit_image.insert(buf_decompressed)) {
298							Ok(handle) => handle,
299							Err(e) => {
300								error!("Error during Hermit image parsing: {e}");
301								return Err(HypervisorError::InvalidKernelPath(
302									kernel_path.clone(),
303								));
304							}
305						}
306					}
307				};
308
309				// handle Hermit image configuration
310				match handle.config {
311					config::Config::V1 {
312						mut input,
313						requirements,
314						kernel: _,
315					} => {
316						// .input
317						if params.kernel_args.is_empty() {
318							params.kernel_args.append(
319								&mut take(&mut input.kernel_args)
320									.into_iter()
321									.map(|i| i.into_owned())
322									.collect(),
323							);
324							if !input.app_args.is_empty() {
325								params.kernel_args.push("--".to_string());
326								params.kernel_args.append(
327									&mut take(&mut input.app_args)
328										.into_iter()
329										.map(|i| i.into_owned())
330										.collect(),
331								)
332							}
333						}
334						debug!("Passing kernel arguments: {:?}", params.kernel_args);
335
336						// don't pass privileged env-var commands through
337						input.env_vars.retain(|i| i.contains('='));
338
339						if let EnvVars::Set(env) = &mut params.env {
340							if let Ok(EnvVars::Set(prev_env_vars)) =
341								EnvVars::try_from(&input.env_vars[..])
342							{
343								// env vars from params take precedence
344								let new_env_vars = take(env);
345								*env = prev_env_vars.into_iter().chain(new_env_vars).collect();
346							} else {
347								warn!("Unable to parse env vars from Hermit image configuration");
348							}
349						} else if input.env_vars.is_empty() {
350							info!("Ignoring Hermit image env vars due to `-e host`");
351						}
352
353						// .requirements
354
355						// TODO: what about default memory size?
356						if let Some(required_memory_size) = requirements.memory
357							&& params.memory_size.0 < required_memory_size
358						{
359							return Err(HypervisorError::InsufficientGuestMemorySize {
360								got: params.memory_size.0,
361								wanted: required_memory_size,
362							});
363						}
364
365						if params.cpu_count.get() < requirements.cpus {
366							return Err(HypervisorError::InsufficientGuestCPUs {
367								got: params.cpu_count.get(),
368								wanted: requirements.cpus,
369							});
370						}
371					}
372				}
373
374				handle.raw_kernel.to_vec()
375			}
376		};
377
378		let object: KernelObject<'_> =
379			KernelObject::parse(&elf).map_err(LoadKernelError::ParseKernelError)?;
380
381		let hermit_version = object.hermit_version();
382		if let Some(version) = hermit_version {
383			info!("Loading a Hermit v{version} kernel");
384		} else {
385			info!("Loading a pre Hermit v0.10.0 kernel");
386		}
387
388		let uhyve_interface_version = object
389			.uhyve_interface_version()
390			.unwrap_or(UhyveIfVersion(1));
391
392		// The memory layout of uhyve looks as follows:
393		//
394		//     0x0000_0000 ┌───────────────────┐
395		//                 │ Hypercalls        │
396		//                 ├───────────────────┤
397		//                 │ not present       │
398		//                 │                   │
399		//    guest_address├───────────────────┤ ▲ ▲ ▲ ▲
400		//                 │                   │ │ │ │ │BOOT_INFO_OFFSET
401		//                 ├───────────────────┤ │ │ │ ▼
402		//                 │ Boot Info         │ │ │ │FDT_OFFSET
403		//                 ├───────────────────┤ │ │ ▼
404		//                 │ Device Tree (FDT) │ │ │
405		//                 ├───────────────────┤ │ │
406		//                 │                   │ │ │PAGETABLE_OFFSET
407		//                 ├───────────────────┤ │ ▼
408		//                 │ Pagetables        │ │
409		//    stack_address├───────────────────┤ │
410		//                 │ Stack             │ │KERNEL_OFFSET
411		//   kernel_address├───────────────────┤ ▼
412		//                 │ Kernel            │
413		//   entry_point──►│                   │
414		//                 │                   │
415		//                 ├───────────────────┤
416		//                 │ Kernel Memory     │
417		//                 │                   │
418		//                 └───────────────────┘
419
420		let (guest_address, kernel_address) = generate_guest_start_address(
421			uhyve_interface_version,
422			params.aslr,
423			object.mem_size(),
424			object.start_addr(),
425			memory_size,
426		);
427
428		debug!("Guest starts at {guest_address:#x}");
429		debug!("Kernel gets loaded to {kernel_address:#x}");
430
431		#[cfg(target_os = "linux")]
432		let mut mem = MmapMemory::new(memory_size, guest_address, params.thp, params.ksm);
433
434		#[cfg(not(target_os = "linux"))]
435		let mut mem = MmapMemory::new(memory_size, guest_address, false, false);
436
437		let mounts: Vec<_> = file_mapping.get_all_guest_dirs().collect();
438
439		let serial = UhyveSerial::from_params(&params.output)?;
440
441		// Takes place before the kernel is actually loaded.
442		#[cfg(target_os = "linux")]
443		Self::landlock_init(
444			&params.file_isolation,
445			&file_mapping,
446			&kernel_path,
447			&params.output,
448			#[cfg(feature = "instrument")]
449			&params.trace_dir,
450		);
451
452		assert!(
453			kernel_address.as_u64() > KERNEL_STACK_SIZE,
454			"there should be enough space for the boot stack before the kernel start address",
455		);
456		let stack_address = kernel_address - KERNEL_STACK_SIZE;
457		debug!("Stack starts at {stack_address:#x}");
458
459		let (
460			LoadedKernel {
461				load_info,
462				entry_point,
463			},
464			kernel_end_address,
465		) = load_kernel_to_mem(&object, &mut mem, kernel_address - guest_address)
466			.expect("Unable to load Kernel {kernel_path}");
467
468		// Allocate memory for hermit image
469		let hermit_image = hermit_image.map(|hermit_image| {
470			load_hermit_image_to_mem(
471				&hermit_image[..],
472				&mut mem,
473				(kernel_end_address - guest_address).align_up(PAGE_SIZE as u64),
474			)
475			.expect("Unable to load Hermit image {kernel_path}")
476		});
477
478		let kernel_info = Arc::new(KernelInfo {
479			entry_point: entry_point.into(),
480			kernel_address,
481			guest_address: mem.guest_addr(),
482			path: kernel_path,
483			params,
484			stack_address,
485		});
486
487		// create virtio interface
488		let mem = Arc::new(mem);
489		if let Some(version) = hermit_version
490			&& kernel_info.params.network.is_some()
491			&& (version
492				< HermitVersion {
493					major: 0,
494					minor: 13,
495					patch: 2,
496				}) {
497			return Err(HypervisorError::FeatureMismatch(
498				"Network requires Kernel 0.13.2 or newer",
499			));
500		}
501		let virtio_device = kernel_info
502			.params
503			.network
504			.as_ref()
505			.map(|mode| Mutex::new(VirtBackend::virtio_net_device(mode.clone(), mem.clone())));
506
507		let peripherals = Arc::new(VmPeripherals {
508			mem,
509			// create virtio interface
510			virtio_device,
511			// TODO: file_mapping not in kernel_info
512			file_mapping: Mutex::new(file_mapping),
513			serial,
514		});
515
516		let virt_backend = VirtBackend::new(peripherals.clone(), &kernel_info.params)?;
517
518		let cpu_count = kernel_info.params.cpu_count.get();
519
520		let vcpus: Vec<_> = (0..cpu_count as usize)
521			.map(|cpu_id| {
522				virt_backend
523					.new_cpu(cpu_id, kernel_info.clone(), kernel_info.params.stats)
524					.unwrap()
525			})
526			.collect();
527
528		let freq = vcpus[0].get_cpu_frequency();
529
530		let serial_port = SerialPortBase::new(match uhyve_interface_version.0 {
531			1 => uhyve_interface::v1::HypercallAddress::Uart as _,
532			2 | 3 => uhyve_interface::v2::HypercallAddress::SerialWriteBuffer as _,
533			uhifv => {
534				unimplemented!(
535					"Kernel uses unsupported uhyve-interface version {}. Is Uhyve too old?",
536					uhifv
537				)
538			}
539		});
540
541		write_fdt_into_mem(
542			&peripherals.mem,
543			&kernel_info.params,
544			hermit_image.clone(),
545			freq,
546			mounts,
547		);
548		write_boot_info_to_mem(
549			&peripherals.mem,
550			load_info,
551			cpu_count as u64,
552			freq,
553			serial_port,
554		);
555
556		let legacy_mapping = if let Some(version) = hermit_version {
557			// actually, all versions that have the tag in the elf are valid, but an explicit check doesn't hurt
558			version
559				< HermitVersion {
560					major: 0,
561					minor: 10,
562					patch: 0,
563				}
564		} else {
565			true
566		};
567		init_guest_mem(
568			unsafe { peripherals.mem.as_slice_mut() }, // slice only lives during this fn call
569			guest_address,
570			hermit_image.map(|i| i.end).unwrap_or(kernel_end_address) - guest_address,
571			legacy_mapping,
572		);
573		trace!("VM initialization complete");
574
575		Ok(Self {
576			peripherals,
577			kernel_info,
578			vcpus,
579			_virt_backend: virt_backend,
580		})
581	}
582
583	#[cfg(target_os = "linux")]
584	pub fn landlock_init(
585		file_sandbox_mode: &FileSandboxMode,
586		file_map: &UhyveFileMap,
587		kernel_path: &std::path::Path,
588		output: &Output,
589		#[cfg(feature = "instrument")] trace: &Option<PathBuf>,
590	) {
591		if file_sandbox_mode != &FileSandboxMode::None {
592			debug!("Attempting to initialize Landlock...");
593			let host_paths = file_map.get_all_host_paths();
594			let temp_dir = file_map.get_temp_dir().to_owned();
595			let landlock = initialize(
596				file_sandbox_mode,
597				kernel_path.into(),
598				output,
599				host_paths.map(|i| i.as_os_str()),
600				temp_dir,
601				#[cfg(feature = "instrument")]
602				trace,
603			);
604			landlock.apply_landlock_restrictions();
605		}
606	}
607
608	/// Runs the VM.
609	///
610	/// Blocks until the VM has finished execution.
611	pub fn run(self, cpu_affinity: Option<Vec<CoreId>>) -> VmResult
612	where
613		GdbVcpuManager<VirtBackend>: Target,
614		<GdbVcpuManager<VirtBackend> as Target>::Error: fmt::Debug,
615		<GdbVcpuManager<VirtBackend> as Target>::Arch: Arch<Usize = u64>,
616	{
617		KickSignal::register_handler().unwrap();
618
619		if self.kernel_info.params.gdb_port.is_none() {
620			self.run_no_gdb(cpu_affinity)
621		} else {
622			if cfg!(target_os = "macos") {
623				warn!("This mode is experimental and doesn't yet work correctly.");
624			}
625			self.run_gdb(cpu_affinity)
626		}
627	}
628
629	fn run_no_gdb(self, cpu_affinity: Option<Vec<CoreId>>) -> VmResult {
630		// After spinning up all vCPU threads, the main thread waits for any vCPU to end execution.
631		let main_parker = Parker::current();
632
633		let num_vcpus = self.vcpus.len();
634
635		let pthreads: Mutex<Vec<Pthread>> = Mutex::new(Vec::with_capacity(num_vcpus));
636		let pthreads_published = Barrier::new(num_vcpus + 1);
637
638		let cpu_results = thread::scope(|s| {
639			trace!("Starting vCPUs");
640			let cpu_handles = self
641				.vcpus
642				.into_iter()
643				.enumerate()
644				.map(|(cpu_id, mut cpu)| {
645					let main_parker = main_parker.clone();
646					let local_cpu_affinity = cpu_affinity
647						.as_ref()
648						.and_then(|core_ids| core_ids.get(cpu_id).copied());
649					let pthreads = &pthreads;
650					let pthreads_published = &pthreads_published;
651
652					s.spawn(move || {
653						{
654							pthreads.lock().unwrap().push(pthread_self());
655						}
656						pthreads_published.wait();
657
658						trace!("Create thread for CPU {cpu_id}");
659						match local_cpu_affinity {
660							Some(core_id) => {
661								debug!("Trying to pin thread {} to CPU {}", cpu_id, core_id.id);
662								core_affinity::set_for_current(core_id); // This does not return an error if it fails :(
663							}
664							None => debug!("No affinity specified, not binding thread"),
665						}
666
667						cpu.thread_local_init().expect("Unable to initialize vCPU");
668
669						struct UnparkOnDrop(Parker);
670
671						impl Drop for UnparkOnDrop {
672							fn drop(&mut self) {
673								self.0.unpark();
674							}
675						}
676
677						let _unpark_on_drop = UnparkOnDrop(main_parker);
678
679						// jump into the VM and execute code of the guest
680						match cpu.run() {
681							Ok((code, stats)) => (Ok(code), stats),
682							Err(err) => {
683								error!("CPU {cpu_id} crashed with {err:?}");
684								(Err(err), None)
685							}
686						}
687					})
688				})
689				.collect::<Vec<_>>();
690
691			pthreads_published.wait();
692
693			trace!("Waiting for first CPU to finish");
694			main_parker.park();
695
696			trace!("Killing all threads");
697			for &tid in pthreads.lock().unwrap().iter() {
698				// `pthread_kill` may return ESRCH if the thread already finished;
699				// scoped threads aren't joined until the scope ends, so the id is
700				// still valid, but the kernel may no longer know about it.
701				let _ = KickSignal::pthread_kill(tid);
702			}
703
704			cpu_handles
705				.into_iter()
706				.map(|h| h.join().unwrap())
707				.collect::<Vec<_>>()
708		});
709
710		let code = cpu_results
711			.iter()
712			.find_map(|(ret, _stats)| ret.as_ref().ok().copied().flatten())
713			.unwrap();
714
715		let stats: Vec<CpuStats> = cpu_results
716			.iter()
717			.filter_map(|(_ret, stats)| stats.clone())
718			.collect();
719		let output = if let Destination::Buffer(b) = &self.peripherals.serial.destination {
720			Some(String::from_utf8_lossy(&b.lock().unwrap()).into_owned())
721		} else {
722			None
723		};
724
725		VmResult {
726			code,
727			output,
728			stats: Some(VmStats::new(&stats)),
729		}
730	}
731}
732
733impl<VirtIf: VirtualizationBackend + Debug> fmt::Debug for UhyveVm<VirtIf> {
734	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
735		f.debug_struct(&format!("UhyveVm<{}>", VirtIf::NAME))
736			.field("entry_point", &self.kernel_info.entry_point)
737			.field("stack_address", &self.kernel_info.stack_address)
738			.field("guest_address", &self.kernel_info.guest_address)
739			.field("mem", &self.peripherals.mem)
740			.field("path", &self.kernel_info.path)
741			.field("virtio_device", &self.peripherals.virtio_device)
742			.field("params", &self.kernel_info.params)
743			.field("file_mapping", &self.peripherals.file_mapping)
744			.finish()
745	}
746}
747
748/// Initialize the page tables for the guest
749/// `memory_size` is the length of the memory from the start of the physical
750/// memory till the end of the kernel in bytes.
751fn init_guest_mem(
752	mem: &mut [u8],
753	guest_addr: GuestPhysAddr,
754	memory_size: u64,
755	legacy_mapping: bool,
756) {
757	trace!("Initialize guest memory");
758	crate::arch::init_guest_mem(mem, guest_addr, memory_size, legacy_mapping);
759}
760
761fn write_fdt_into_mem(
762	mem: &MmapMemory,
763	params: &Params,
764	hermit_image: Option<core::ops::Range<GuestPhysAddr>>,
765	cpu_freq: Option<NonZero<u32>>,
766	mounts: Vec<String>,
767) {
768	trace!("Writing FDT in memory");
769
770	let sep = params
771		.kernel_args
772		.iter()
773		.take_while(|arg| *arg != "--")
774		.count();
775
776	let mut fdt = Fdt::new(hermit_image)
777		.unwrap()
778		.memory(mem.address_range())
779		.unwrap()
780		.kernel_args(&params.kernel_args[..sep])
781		.app_args(params.kernel_args.get(sep + 1..).unwrap_or_default());
782
783	fdt = match &params.env {
784		EnvVars::Host => fdt.envs(env::vars()),
785		EnvVars::Set(map) => fdt.envs(map.iter().map(|(a, b)| (a.as_str(), b.as_str()))),
786	};
787
788	if !mounts.is_empty() {
789		fdt = fdt.mounts(mounts).unwrap();
790	}
791
792	#[cfg(target_arch = "aarch64")]
793	{
794		fdt = fdt.gic().unwrap();
795		fdt = fdt.cpus(params.cpu_count).unwrap();
796		fdt = fdt.timer().unwrap();
797	}
798
799	if let Some(tsc_khz) = cpu_freq {
800		fdt = fdt.tsc_khz(tsc_khz.into()).unwrap();
801	}
802	let fdt = fdt.finish().unwrap();
803
804	debug!("fdt.len() = {}", fdt.len());
805	assert!(fdt.len() < (BOOT_INFO_OFFSET - FDT_OFFSET) as usize);
806	unsafe {
807		let fdt_ptr = mem.host_start().add(FDT_OFFSET as usize);
808		fdt_ptr.copy_from_nonoverlapping(fdt.as_ptr(), fdt.len());
809	}
810}
811
812fn write_boot_info_to_mem(
813	mem: &MmapMemory,
814	load_info: LoadInfo,
815	num_cpus: u64,
816	cpu_freq: Option<NonZero<u32>>,
817	#[cfg(target_arch = "x86_64")] serial_port: Option<NonZero<u16>>,
818	#[cfg(target_arch = "aarch64")] serial_port: Option<NonZero<u64>>,
819) {
820	debug!(
821		"Writing BootInfo to {:?}",
822		mem.guest_addr() + BOOT_INFO_OFFSET
823	);
824	let boot_info = BootInfo {
825		hardware_info: HardwareInfo {
826			phys_addr_range: mem.address_range_u64(),
827			serial_port_base: serial_port,
828			device_tree: Some((mem.guest_addr().as_u64() + FDT_OFFSET).try_into().unwrap()),
829		},
830		load_info,
831		platform_info: PlatformInfo::Uhyve {
832			has_pci: cfg!(target_os = "linux"),
833			num_cpus: num_cpus.try_into().unwrap(),
834			cpu_freq,
835			boot_time: SystemTime::now().into(),
836		},
837	};
838	unsafe {
839		let raw_boot_info_ptr = mem.host_start().add(BOOT_INFO_OFFSET as usize) as *mut RawBootInfo;
840		*raw_boot_info_ptr = RawBootInfo::from(boot_info);
841	}
842}
843
844/// loads the kernel `object` into `mem`. `relative_offset` is the start address the kernel relative to `mem`.
845/// Returns the loaded kernel marker and the kernel's end address.
846fn load_kernel_to_mem(
847	object: &KernelObject<'_>,
848	mem: &mut MmapMemory,
849	relative_offset: u64,
850) -> LoadKernelResult<(LoadedKernel, GuestPhysAddr)> {
851	let kernel_end_address = mem.guest_addr() + relative_offset + object.mem_size();
852
853	if kernel_end_address > mem.guest_addr() + mem.size() {
854		return Err(LoadKernelError::InsufficientMemory);
855	}
856
857	Ok((
858		object.load_kernel(
859			// Safety: Slice only lives during this fn call, so no aliasing happens
860			&mut unsafe { mem.as_slice_uninit_mut() }
861				[relative_offset as usize..relative_offset as usize + object.mem_size()],
862			relative_offset + mem.guest_addr().as_u64(),
863		),
864		kernel_end_address,
865	))
866}
867
868/// Loads the `hermit_image` into `mem`. `relative_offset` is the start address the image relative to `mem`.
869/// Returns the memory range of the image in terms of guest addresses.
870fn load_hermit_image_to_mem(
871	hermit_image: &[u8],
872	mem: &mut MmapMemory,
873	relative_offset: u64,
874) -> Option<core::ops::Range<GuestPhysAddr>> {
875	assert!(!hermit_image.is_empty());
876
877	let aligned_image_len = hermit_image.len().align_up(PAGE_SIZE);
878	let image_start_address = mem.guest_addr() + relative_offset;
879	let image_end_address = image_start_address + aligned_image_len as u64;
880
881	if image_end_address > mem.guest_addr() + mem.size() {
882		return None;
883	}
884
885	// TODO: mark that memory in the memory handling as read-only, such that
886	// the hypervisor can more-or-less gracefully shutdown/error in case of
887	// errornous write access to a read-only section.
888
889	// Safety: Slice only lives during the rest of this function invocation, so no aliasing happens
890	let mem_slice =
891		unsafe { &mut mem.as_slice_mut()[relative_offset as usize..][..aligned_image_len] };
892	mem_slice[..hermit_image.len()].copy_from_slice(hermit_image);
893	mem_slice[hermit_image.len()..].fill(0);
894	debug!(
895		"Hermit image: start={relative_offset:#x}, length={:#x}",
896		hermit_image.len()
897	);
898	// Safety: the length supplied matches `mem_slice.len()` and the length is aligned to the page size.
899	if unsafe {
900		libc::mprotect(
901			&mut mem_slice[0] as *mut u8 as *mut libc::c_void,
902			aligned_image_len,
903			libc::PROT_READ | libc::PROT_EXEC,
904		)
905	} == -1
906	{
907		let e = std::io::Error::last_os_error();
908		warn!("Hermit image mprotect(2) failed: {e}");
909	}
910
911	Some(image_start_address..image_end_address)
912}
913
914#[cfg(test)]
915mod tests {
916	use std::ops::Add;
917
918	use hermit_entry::UhyveIfVersion;
919
920	use crate::{
921		RAM_START, V1_MAX_ADDR,
922		vm::{KERNEL_OFFSET, generate_guest_start_address},
923	};
924
925	#[test]
926	fn test_generate_guest_start_address() {
927		let mem_size: usize = 0xBE20_0000; // 3042 MiB
928		let if_v1 = UhyveIfVersion(1);
929		let if_v2 = UhyveIfVersion(2);
930		let object_mem_size: usize = 0x0009_C400;
931		let object_no_start_addr: Option<u64> = None;
932		#[cfg(target_arch = "x86_64")]
933		let object_start_addr: u64 = 0x0002_0000;
934		#[cfg(target_arch = "aarch64")]
935		let object_start_addr: u64 = 0x1002_0000;
936
937		/* v1 */
938
939		// v1: No ASLR, relocatable
940		let (mut guest_address, mut start_address) = generate_guest_start_address(
941			if_v1,
942			false,
943			object_mem_size,
944			object_no_start_addr,
945			mem_size,
946		);
947		assert_eq!(guest_address, RAM_START);
948		assert_eq!(start_address, guest_address.add(KERNEL_OFFSET));
949
950		// v1: ASLR, relocatable
951		(guest_address, start_address) = generate_guest_start_address(
952			if_v1,
953			true,
954			object_mem_size,
955			object_no_start_addr,
956			mem_size,
957		);
958		assert_eq!(start_address, guest_address.add(KERNEL_OFFSET));
959		assert!(start_address.as_u64() <= V1_MAX_ADDR);
960
961		// v1: ASLR, non-relocatable
962		(guest_address, start_address) = generate_guest_start_address(
963			if_v1,
964			true,
965			object_mem_size,
966			object_start_addr.into(),
967			mem_size,
968		);
969		assert_eq!(guest_address, RAM_START);
970		assert_eq!(start_address.as_u64(), object_start_addr);
971		// Note that this is a bit brittle and implicitly relies on RAM_START.
972		assert_eq!(start_address, guest_address.add(0x0002_0000usize));
973		assert!(start_address.as_u64() <= V1_MAX_ADDR);
974
975		/* v2 */
976
977		// v2: No ASLR, relocatable
978		(guest_address, start_address) = generate_guest_start_address(
979			if_v2,
980			false,
981			object_mem_size,
982			object_no_start_addr,
983			mem_size,
984		);
985		assert_eq!(guest_address.as_u64(), 0x0001_0000_0000u64);
986		assert_eq!(start_address, guest_address.add(KERNEL_OFFSET));
987		#[cfg(target_arch = "x86_64")]
988		assert!(start_address.as_u64() >= V1_MAX_ADDR);
989
990		// v2: ASLR, relocatable
991		(guest_address, start_address) = generate_guest_start_address(
992			if_v2,
993			true,
994			object_mem_size,
995			object_no_start_addr,
996			mem_size,
997		);
998		assert_eq!(start_address, guest_address.add(KERNEL_OFFSET));
999		#[cfg(target_arch = "x86_64")]
1000		assert!(start_address.as_u64() >= V1_MAX_ADDR);
1001
1002		// v2: Use entire memory available
1003		//
1004		// (This effectively renders ASLR worthless, yet it is great for testing,
1005		//  underlying arithmetic operations for potential regressions without
1006		//  exclusively relying on randomness!)
1007		(guest_address, start_address) = generate_guest_start_address(
1008			if_v2,
1009			true,
1010			object_mem_size,
1011			object_no_start_addr,
1012			// Highest address, minus everything that is subtracted from it in the function.
1013			0x0010_0000_0000 - object_mem_size - KERNEL_OFFSET as usize - 0x0001_0000_0000,
1014		);
1015		assert_eq!(guest_address.as_u64(), 0x0001_0000_0000);
1016		assert_eq!(start_address, guest_address.add(KERNEL_OFFSET));
1017		#[cfg(target_arch = "x86_64")]
1018		assert!(start_address.as_u64() >= V1_MAX_ADDR);
1019	}
1020}