Alioth Code Coverage

boot.rs56.12%

1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15mod config;
16
17use std::collections::HashMap;
18use std::ffi::CString;
19use std::mem;
20use std::path::{Path, PathBuf};
21
22use alioth::board::{BoardConfig, CpuConfig};
23#[cfg(target_arch = "x86_64")]
24use alioth::device::fw_cfg::FwCfgItemParam;
25use alioth::errors::{DebugTrace, trace_error};
26#[cfg(target_os = "macos")]
27use alioth::hv::Hvf;
28#[cfg(target_os = "linux")]
29use alioth::hv::Kvm;
30use alioth::hv::{Coco, HvConfig, Hypervisor};
31use alioth::loader::{Executable, Payload};
32use alioth::mem::{MemBackend, MemConfig};
33#[cfg(target_os = "linux")]
34use alioth::vfio::{CdevParam, ContainerParam, GroupParam, IoasParam};
35#[cfg(target_os = "linux")]
36use alioth::virtio::DeviceId;
37use alioth::virtio::dev::balloon::BalloonParam;
38use alioth::virtio::dev::blk::BlkFileParam;
39use alioth::virtio::dev::entropy::EntropyParam;
40#[cfg(target_os = "linux")]
41use alioth::virtio::vu::frontend::VuFrontendParam;
42use alioth::virtio::worker::WorkerApi;
43use alioth::vm::Machine;
44use clap::Args;
45use serde_aco::help_text;
46use snafu::{ResultExt, Snafu};
47
48use crate::objects::{DOC_OBJECTS, parse_objects};
49
50use self::config::{BlkParam, Config, FsParam, NetParam, VsockParam};
51
52#[trace_error]
53#[derive(Snafu, DebugTrace)]
54#[snafu(module, context(suffix(false)))]
55pub enum Error {
56 #[snafu(display("Failed to parse {arg}"))]
57 ParseArg {
58 arg: String,
59 error: serde_aco::Error,
60 },
61 #[snafu(display("Failed to parse objects"), context(false))]
62 ParseObjects { source: crate::objects::Error },
63 #[cfg(target_os = "linux")]
64 #[snafu(display("Failed to access system hypervisor"))]
65 Hypervisor { source: alioth::hv::Error },
66 #[snafu(display("Failed to create a VM"))]
67 CreateVm { source: alioth::vm::Error },
68 #[snafu(display("Failed to boot a VM"))]
69 BootVm { source: alioth::vm::Error },
70 #[snafu(display("VM did not shutdown peacefully"))]
71 WaitVm { source: alioth::vm::Error },
72}
73
74#[derive(Args, Debug, Clone, Default)]
75#[command(arg_required_else_help = true, alias("run"))]
76pub struct BootArgs {
77 #[arg(long, help(
78 help_text::<HvConfig>("Specify the Hypervisor to run on.")
79 ), value_name = "HV")]
80 hypervisor: Option<String>,
81
82 /// Path to a Linux kernel image.
83 #[arg(short, long, value_name = "PATH")]
84 kernel: Option<Box<Path>>,
85
86 /// Path to an ELF kernel with PVH note.
87 #[cfg(target_arch = "x86_64")]
88 #[arg(long, value_name = "PATH")]
89 pvh: Option<Box<Path>>,
90
91 /// Path to a firmware image.
92 #[arg(long, short, value_name = "PATH")]
93 firmware: Option<Box<Path>>,
94
95 /// Command line to pass to the kernel, e.g. `console=ttyS0`.
96 #[arg(short, long, alias = "cmd-line", value_name = "ARGS")]
97 cmdline: Option<CString>,
98
99 /// Path to an initramfs image.
100 #[arg(short, long, value_name = "PATH")]
101 initramfs: Option<Box<Path>>,
102
103 /// DEPRECATED: Use --cpu instead.
104 #[arg(long, default_value_t = 1)]
105 num_cpu: u16,
106
107 #[arg(short('p'), long, help(
108 help_text::<CpuConfig>("Configure the VCPUs of the guest.")
109 ))]
110 cpu: Option<Box<str>>,
111
112 /// DEPRECATED: Use --memory instead.
113 #[arg(long, default_value = "1G")]
114 mem_size: String,
115
116 #[arg(short, long, help(
117 help_text::<MemConfig>("Specify the memory of the guest.")
118 ))]
119 memory: Option<String>,
120
121 /// Add a pvpanic device.
122 #[arg(long)]
123 pvpanic: bool,
124
125 #[cfg(target_arch = "x86_64")]
126 #[arg(long, help(
127 help_text::<FwCfgItemParam>("Add an extra item to the fw_cfg device.")
128 ), value_name = "ITEM")]
129 fw_cfg: Vec<String>,
130
131 /// Add a VirtIO entropy device.
132 #[arg(long)]
133 entropy: bool,
134
135 #[arg(long, help(
136 help_text::<NetParam>("Add a VirtIO net device.")
137 ))]
138 net: Vec<String>,
139
140 #[arg(long, help(
141 help_text::<BlkParam>("Add a VirtIO block device.")
142 ))]
143 blk: Vec<String>,
144
145 #[arg(long, help(
146 help_text::<Coco>("Enable confidential compute supported by host platform.")
147 ))]
148 coco: Option<String>,
149
150 #[arg(long, help(
151 help_text::<FsParam>("Add a VirtIO filesystem device.")
152 ))]
153 fs: Vec<String>,
154
155 #[arg(long, help(
156 help_text::<VsockParam>("Add a VirtIO vsock device.")
157 ))]
158 vsock: Option<String>,
159
160 #[cfg(target_os = "linux")]
161 #[arg(long, help(help_text::<CdevParam>(
162 "Assign a host PCI device to the guest using IOMMUFD API."
163 ) ))]
164 vfio_cdev: Vec<String>,
165
166 #[cfg(target_os = "linux")]
167 #[arg(long, help(help_text::<IoasParam>("Create a new IO address space.")))]
168 vfio_ioas: Vec<String>,
169
170 #[cfg(target_os = "linux")]
171 #[arg(long, help(help_text::<GroupParam>(
172 "Assign a host PCI device to the guest using legacy VFIO API."
173 )))]
174 vfio_group: Vec<String>,
175
176 #[cfg(target_os = "linux")]
177 #[arg(long, help(help_text::<ContainerParam>("Add a new VFIO container.")))]
178 vfio_container: Vec<String>,
179
180 #[arg(long)]
181 #[arg(long, help(help_text::<BalloonParam>("Add a VirtIO balloon device.")))]
182 balloon: Option<String>,
183
184 #[arg(short, long("object"), help = DOC_OBJECTS, value_name = "OBJECT")]
185 objects: Vec<String>,
186}
187
188fn parse_net_arg(arg: &str, objects: &HashMap<&str, &str>) -> serde_aco::Result<NetParam> {8x
189 #[cfg(target_os = "linux")]
190 if let Ok(param) = serde_aco::from_args(arg, objects) {6x
191 Ok(param)2x
192 } else {
193 let param = serde_aco::from_args(arg, objects)?;4x
194 Ok(NetParam::Tap(param))4x
195 }
196
197 #[cfg(target_os = "macos")]
198 serde_aco::from_args(arg, objects)2x
199}8x
200
201fn parse_blk_arg(arg: &str, objects: &HashMap<&str, &str>) -> BlkParam {17x
202 if let Ok(param) = serde_aco::from_args(arg, objects) {17x
203 param11x
204 } else if let Ok(param) = serde_aco::from_args(arg, objects) {6x
205 BlkParam::File(param)3x
206 } else {
207 eprintln!("Please update the cmd line to --blk file,path={arg}");3x
208 BlkParam::File(BlkFileParam {3x
209 path: PathBuf::from(arg).into(),3x
210 readonly: false,3x
211 api: WorkerApi::Mio,3x
212 })3x
213 }
214}17x
215
216fn parse_mem_arg(8x
217 arg: Option<String>,8x
218 mem_size: String,8x
219 objects: &HashMap<&str, &str>,8x
220) -> Result<MemConfig, Error> {8x
221 let config = if let Some(arg) = arg {8x
222 serde_aco::from_args(&arg, objects).context(error::ParseArg { arg })?5x
223 } else {
224 #[cfg(target_os = "linux")]
225 eprintln!("Please update the cmd line to --memory size={mem_size},backend=memfd");2x
226 MemConfig {
227 size: serde_aco::from_args(&mem_size, objects)3x
228 .context(error::ParseArg { arg: mem_size })?,3x
229 #[cfg(target_os = "linux")]
230 backend: MemBackend::Memfd,2x
231 #[cfg(not(target_os = "linux"))]
232 backend: MemBackend::Anonymous,1x
233 ..Default::default()3x
234 }
235 };
236 Ok(config)8x
237}8x
238
239fn parse_cpu_arg(9x
240 arg: Option<Box<str>>,9x
241 num_cpu: u16,9x
242 objects: &HashMap<&str, &str>,9x
243) -> Result<CpuConfig, Error> {9x
244 let config = if let Some(arg) = arg {9x
245 serde_aco::from_args(&arg, objects).context(error::ParseArg { arg })?6x
246 } else {
247 eprintln!("Please update the cmd line to --cpu count={num_cpu}");3x
248 CpuConfig {3x
249 count: num_cpu,3x
250 ..Default::default()3x
251 }3x
252 };
253 Ok(config)9x
254}9x
255
256fn parse_payload_arg(args: &mut BootArgs) -> Payload {7x
257 let mut payload = Payload {7x
258 firmware: args.firmware.take(),7x
259 initramfs: args.initramfs.take(),7x
260 cmdline: args.cmdline.take(),7x
261 ..Default::default()7x
262 };7x
263 payload.executable = args.kernel.take().map(Executable::Linux);7x
264 #[cfg(target_arch = "x86_64")]
265 if payload.executable.is_none() {3x
266 payload.executable = args.pvh.take().map(Executable::Pvh);1x
267 }2x
268 payload7x
269}7x
270
271fn parse_args(mut args: BootArgs, objects: HashMap<&str, &str>) -> Result<Config, Error> {3x
272 let payload = parse_payload_arg(&mut args);3x
273
274 let mut board_config = BoardConfig::default();3x
275 if let Some(arg) = args.coco {3x
276 let param = serde_aco::from_args(&arg, &objects).context(error::ParseArg { arg })?;
277 board_config.coco = Some(param);
278 };3x
279 board_config.mem = parse_mem_arg(args.memory, args.mem_size, &objects)?;3x
280 board_config.cpu = parse_cpu_arg(args.cpu, args.num_cpu, &objects)?;3x
281
282 let mut config = Config {3x
283 board: board_config,3x
284 pvpanic: args.pvpanic,3x
285 payload,3x
286 ..Default::default()3x
287 };3x
288
289 #[cfg(target_arch = "x86_64")]
290 for arg in args.fw_cfg {2x
291 let param = serde_aco::from_args(&arg, &objects).context(error::ParseArg { arg })?;2x
292 config.fw_cfg.push(param);2x
293 }
294
295 if args.entropy {3x
296 config.entropy = Some(EntropyParam::default());3x
297 }3x
298
299 for arg in args.net {5x
300 let param = parse_net_arg(&arg, &objects).context(error::ParseArg { arg })?;5x
301 config.net.push(param);5x
302 }
303
304 for arg in args.blk {6x
305 let param = parse_blk_arg(&arg, &objects);6x
306 config.blk.push(param);6x
307 }6x
308
309 for arg in args.fs {5x
310 let param = serde_aco::from_args(&arg, &objects).context(error::ParseArg { arg })?;5x
311 config.fs.push(param);5x
312 }
313
314 if let Some(arg) = args.vsock {3x
315 let param = serde_aco::from_args(&arg, &objects).context(error::ParseArg { arg })?;3x
316 config.vsock = Some(param);3x
317 }
318
319 if let Some(arg) = args.balloon {3x
320 let param = serde_aco::from_args(&arg, &objects).context(error::ParseArg { arg })?;3x
321 config.balloon = Some(param);3x
322 }
323
324 #[cfg(target_os = "linux")]
325 for arg in args.vfio_ioas {2x
326 let param = serde_aco::from_args(&arg, &objects).context(error::ParseArg { arg })?;2x
327 config.vfio_ioas.push(param);2x
328 }
329 #[cfg(target_os = "linux")]
330 for arg in args.vfio_cdev {2x
331 let param = serde_aco::from_args(&arg, &objects).context(error::ParseArg { arg })?;2x
332 config.vfio_cdev.push(param);2x
333 }
334 #[cfg(target_os = "linux")]
335 for arg in args.vfio_container {2x
336 let param = serde_aco::from_args(&arg, &objects).context(error::ParseArg { arg })?;2x
337 config.vfio_container.push(param);2x
338 }
339 #[cfg(target_os = "linux")]
340 for arg in args.vfio_group {2x
341 let param = serde_aco::from_args(&arg, &objects).context(error::ParseArg { arg })?;2x
342 config.vfio_group.push(param);2x
343 }
344
345 Ok(config)3x
346}3x
347
348fn create<H: Hypervisor>(hypervisor: &H, config: Config) -> Result<Machine<H>, alioth::vm::Error> {
349 let vm = Machine::new(hypervisor, config.board)?;
350
351 #[cfg(target_arch = "x86_64")]
352 vm.add_com1()?;
353 #[cfg(target_arch = "aarch64")]
354 vm.add_pl011()?;
355 #[cfg(target_arch = "aarch64")]
356 vm.add_pl031();
357
358 if config.pvpanic {
359 vm.add_pvpanic()?;
360 }
361
362 #[cfg(target_arch = "x86_64")]
363 if config.payload.firmware.is_some() || !config.fw_cfg.is_empty() {
364 vm.add_fw_cfg(config.fw_cfg.into_iter())?;
365 };
366
367 if let Some(param) = config.entropy {
368 vm.add_virtio_dev("virtio-entropy", param)?;
369 }
370
371 for (index, param) in config.net.into_iter().enumerate() {
372 match param {
373 #[cfg(target_os = "linux")]
374 NetParam::Tap(tap_param) => vm.add_virtio_dev(format!("virtio-net-{index}"), tap_param),
375 #[cfg(target_os = "linux")]
376 NetParam::Vu(sock) => {
377 let param = VuFrontendParam {
378 id: DeviceId::NET,
379 socket: sock.socket,
380 };
381 vm.add_virtio_dev(format!("vu-net-{index}"), param)
382 }
383 #[cfg(target_os = "macos")]
384 NetParam::Vmnet(p) => vm.add_virtio_dev(format!("virtio-net-{index}"), p),
385 }?;
386 }
387
388 for (index, param) in config.blk.into_iter().enumerate() {
389 match param {
390 BlkParam::File(p) => vm.add_virtio_dev(format!("virtio-blk-{index}"), p),
391 #[cfg(target_os = "linux")]
392 BlkParam::Vu(s) => {
393 let p = VuFrontendParam {
394 id: DeviceId::BLOCK,
395 socket: s.socket,
396 };
397 vm.add_virtio_dev(format!("vu-net-{index}"), p)
398 }
399 }?;
400 }
401
402 for (index, param) in config.fs.into_iter().enumerate() {
403 match param {
404 FsParam::Dir(p) => vm.add_virtio_dev(format!("virtio-fs-{index}"), p),
405 #[cfg(target_os = "linux")]
406 FsParam::Vu(p) => vm.add_virtio_dev(format!("vu-fs-{index}"), p),
407 }?;
408 }
409
410 if let Some(param) = config.vsock {
411 match param {
412 #[cfg(target_os = "linux")]
413 VsockParam::Vhost(p) => vm.add_virtio_dev("vhost-vsock", p),
414 VsockParam::Uds(p) => vm.add_virtio_dev("uds-vsock", p),
415 #[cfg(target_os = "linux")]
416 VsockParam::Vu(s) => {
417 let p = VuFrontendParam {
418 id: DeviceId::SOCKET,
419 socket: s.socket,
420 };
421 vm.add_virtio_dev("vu-vsock", p)
422 }
423 }?;
424 }
425
426 if let Some(param) = config.balloon {
427 vm.add_virtio_dev("virtio-balloon", param)?;
428 }
429
430 #[cfg(target_os = "linux")]
431 for param in config.vfio_ioas.into_iter() {
432 vm.add_vfio_ioas(param)?;
433 }
434 #[cfg(target_os = "linux")]
435 for (index, param) in config.vfio_cdev.into_iter().enumerate() {
436 vm.add_vfio_cdev(format!("vfio-{index}").into(), param)?;
437 }
438
439 #[cfg(target_os = "linux")]
440 for param in config.vfio_container.into_iter() {
441 vm.add_vfio_container(param)?;
442 }
443 #[cfg(target_os = "linux")]
444 for (index, param) in config.vfio_group.into_iter().enumerate() {
445 vm.add_vfio_devs_in_group(&index.to_string(), param)?;
446 }
447
448 vm.add_payload(config.payload);
449
450 Ok(vm)
451}
452
453pub fn boot(mut args: BootArgs) -> Result<(), Error> {
454 let object_args = mem::take(&mut args.objects);
455 let objects = parse_objects(&object_args)?;
456
457 let hv_config = if let Some(arg) = args.hypervisor.take() {
458 serde_aco::from_args(&arg, &objects).context(error::ParseArg { arg })?
459 } else {
460 HvConfig::default()
461 };
462 let hypervisor = match hv_config {
463 #[cfg(target_os = "linux")]
464 HvConfig::Kvm(kvm_config) => Kvm::new(kvm_config).context(error::Hypervisor)?,
465 #[cfg(target_os = "macos")]
466 HvConfig::Hvf => Hvf {},
467 };
468
469 let config = parse_args(args, objects)?;
470
471 let vm = create(&hypervisor, config).context(error::CreateVm)?;
472
473 vm.boot().context(error::BootVm)?;
474 vm.wait().context(error::WaitVm)?;
475 Ok(())
476}
477
478#[cfg(test)]
479#[path = "boot_test.rs"]
480mod tests;
481