1use super::*;
4
5impl VmManager {
6 pub async fn boot(&mut self) -> Result<()> {
8 let boot_span = tracing::info_span!("vm_boot", box_id = %self.box_id);
9 {
11 let state = self.state.read().await;
12 if *state != BoxState::Created {
13 return Err(BoxError::StateError("VM already booted".to_string()));
14 }
15 }
16 super::validate_snapshot_launch(&self.config)?;
17
18 let box_dir = self.home_dir.join("boxes").join(&self.box_id);
19 self.preserve_rootfs_on_boot_failure =
20 self.config.persistent && layout::persistent_rootfs_generation_exists(&box_dir)?;
21
22 let execution_plan = a3s_box_core::resolve_execution(&self.config)?;
23 self.resolved_execution_plan = Some(execution_plan.clone());
24 if execution_plan.backend.is_sandbox() {
25 let boot_start = std::time::Instant::now();
26 return self
27 .boot_sandbox(execution_plan, &boot_span, boot_start)
28 .await;
29 }
30
31 let boot_start = std::time::Instant::now();
32
33 tracing::info!(parent: &boot_span, box_id = %self.box_id, "Booting VM");
34
35 let layout = match self
37 .prepare_layout()
38 .instrument(tracing::info_span!(parent: &boot_span, "prepare_layout"))
39 .await
40 {
41 Ok(layout) => layout,
42 Err(error) => {
43 self.cleanup_boot_failure().await;
44 return Err(error);
45 }
46 };
47 self.image_config = layout.oci_config.clone();
48
49 if !self.rootfs_provider.guest_owns_terminal_fencing() {
53 if let Err(error) =
54 a3s_box_core::rootfs_metadata::stage_terminal_rootfs_metadata_for_boot(
55 &layout.rootfs_path,
56 )
57 {
58 self.cleanup_boot_failure().await;
59 return Err(BoxError::IoError(error));
60 }
61 }
62
63 let mut spec = match self.build_microvm_instance_spec(&layout) {
65 Ok(s) => s,
66 Err(e) => {
67 self.cleanup_boot_failure().await;
68 return Err(e);
69 }
70 };
71
72 let bridge_network = match &self.config.network {
74 a3s_box_core::NetworkMode::Bridge { network } => Some(network.clone()),
75 _ => None,
76 };
77 if let Some(network_name) = bridge_network.as_deref() {
78 let net_config = match self.setup_bridge_network(network_name) {
79 Ok(n) => n,
80 Err(e) => {
81 self.cleanup_boot_failure().await;
82 return Err(e);
83 }
84 };
85
86 let ip_cidr = format!("{}/{}", net_config.ip_address, net_config.prefix_len);
89 spec.entrypoint
90 .env
91 .push(("A3S_NET_IP".to_string(), ip_cidr));
92 spec.entrypoint.env.push((
93 "A3S_NET_GATEWAY".to_string(),
94 net_config.gateway.to_string(),
95 ));
96 spec.entrypoint.env.push((
97 "A3S_NET_DNS".to_string(),
98 net_config
99 .dns_servers
100 .iter()
101 .map(|s| s.to_string())
102 .collect::<Vec<_>>()
103 .join(","),
104 ));
105
106 spec.network = Some(net_config);
107 }
108
109 #[cfg(target_os = "macos")]
110 if spec.network.is_none()
111 && matches!(self.config.network, a3s_box_core::NetworkMode::Tsi)
112 && !self.config.port_map.is_empty()
113 {
114 let net_config = match self.setup_published_default_network() {
115 Ok(network) => network,
116 Err(error) => {
117 self.cleanup_boot_failure().await;
118 return Err(error);
119 }
120 };
121 let ip_cidr = format!("{}/{}", net_config.ip_address, net_config.prefix_len);
122 spec.entrypoint
123 .env
124 .push(("A3S_NET_IP".to_string(), ip_cidr));
125 spec.entrypoint.env.push((
126 "A3S_NET_GATEWAY".to_string(),
127 net_config.gateway.to_string(),
128 ));
129 spec.entrypoint.env.push((
130 "A3S_NET_DNS".to_string(),
131 net_config
132 .dns_servers
133 .iter()
134 .map(ToString::to_string)
135 .collect::<Vec<_>>()
136 .join(","),
137 ));
138 spec.network = Some(net_config);
139 }
140
141 let host_config = match self.guest_host_config(
145 bridge_network.as_deref(),
146 spec.network
147 .as_ref()
148 .map(|network| network.dns_servers.as_slice()),
149 ) {
150 Ok(config) => config,
151 Err(error) => {
152 self.cleanup_boot_failure().await;
153 return Err(error);
154 }
155 };
156 let uses_guest_boot_config =
157 match Self::finalize_microvm_guest_boot_config(&spec, host_config) {
158 Ok(value) => value,
159 Err(error) => {
160 self.cleanup_boot_failure().await;
161 return Err(error);
162 }
163 };
164 if layout.resumed_rootfs.is_some() && !uses_guest_boot_config {
165 self.cleanup_boot_failure().await;
166 return Err(BoxError::BoxBootError {
167 message: "guest-owned rootfs did not select the private guest boot transport"
168 .to_string(),
169 hint: None,
170 });
171 }
172 if !uses_guest_boot_config {
173 let resolv_content = a3s_box_core::dns::generate_resolv_conf(&self.config.dns);
174 if let Err(error) = crate::oci::rootfs::write_guest_file(
175 &layout.rootfs_path,
176 "etc/resolv.conf",
177 &resolv_content,
178 ) {
179 self.cleanup_boot_failure().await;
180 return Err(error);
181 }
182 if let Err(error) = self.write_hostname_file(&layout) {
183 self.cleanup_boot_failure().await;
184 return Err(error);
185 }
186 let hosts_result = match bridge_network.as_deref() {
187 Some(network_name) => self.write_hosts_file(&layout, network_name),
188 None => self.write_standalone_hosts_file(&layout),
189 };
190 if let Err(error) = hosts_result {
191 self.cleanup_boot_failure().await;
192 return Err(error);
193 }
194 }
195
196 if !self.rootfs_provider.guest_owns_diff_baseline() {
200 self.create_diff_baseline(&layout);
201 }
202
203 #[cfg(target_os = "macos")]
204 let artifact_cache = match self.rootfs_artifact_cache_options(
205 &layout,
206 &spec.entrypoint.executable,
207 uses_guest_boot_config,
208 ) {
209 Ok(cache) => cache,
210 Err(error) => {
211 self.cleanup_boot_failure().await;
212 return Err(error);
213 }
214 };
215 #[cfg(not(target_os = "macos"))]
216 let artifact_cache = None;
217
218 if let Some(resumed) = layout.resumed_rootfs.as_ref() {
223 spec.rootfs = resumed.source.clone();
224 } else {
225 spec.rootfs = match self.rootfs_provider.finalize_for_boot(
226 &box_dir,
227 &layout.rootfs_path,
228 crate::rootfs::RootfsFinalizeOptions {
229 disk_mib: self.config.resources.disk_mb,
230 persistent: self.config.persistent,
231 snapshot: super::rootfs_snapshot_requested(&self.config),
232 artifact_cache,
233 },
234 ) {
235 Ok(rootfs) => rootfs,
236 Err(error) => {
237 self.cleanup_boot_failure().await;
238 return Err(error);
239 }
240 };
241 }
242
243 if self.provider.is_none() {
245 let shim_path = match VmController::find_shim() {
246 Ok(p) => p,
247 Err(e) => {
248 self.cleanup_boot_failure().await;
249 return Err(e);
250 }
251 };
252 let controller = match VmController::new(shim_path) {
253 Ok(c) => c,
254 Err(e) => {
255 self.cleanup_boot_failure().await;
256 return Err(e);
257 }
258 };
259 self.provider = Some(Box::new(controller));
260 }
261
262 let handler = {
264 let provider = self
265 .provider
266 .as_ref()
267 .ok_or_else(|| BoxError::BoxBootError {
268 message: "VMM provider not initialized".to_string(),
269 hint: Some("Ensure VmManager has a provider set before boot".to_string()),
270 })?;
271 let vm_start_span = tracing::info_span!(parent: &boot_span, "vm_start");
272 match async { provider.start(&spec).await }
273 .instrument(vm_start_span)
274 .await
275 {
276 Ok(h) => h,
277 Err(e) => {
278 self.cleanup_boot_failure().await;
279 return Err(e);
280 }
281 }
282 };
283
284 *self.handler.write().await = Some(handler);
286
287 {
289 let wait_span = tracing::info_span!(parent: &boot_span, "wait_for_ready");
290 if let Err(e) = async {
291 self.wait_for_vm_running().await?;
292
293 #[cfg(unix)]
298 if is_restore_mode(&self.config) {
299 self.probe_exec_ready_once(&layout.exec_socket_path).await;
300 } else {
301 self.wait_for_exec_ready(&layout.exec_socket_path).await?;
302 }
303 #[cfg(windows)]
304 self.wait_for_exec_ready(&layout.exec_socket_path).await?;
305 Ok::<(), BoxError>(())
306 }
307 .instrument(wait_span)
308 .await
309 {
310 self.cleanup_boot_failure().await;
311 return Err(e);
312 }
313 }
314
315 if self.rootfs_provider.guest_owns_diff_baseline() {
316 if let Err(error) = crate::rootfs::publish_guest_diff_baseline(&box_dir) {
317 self.cleanup_boot_failure().await;
318 return Err(error);
319 }
320 }
321
322 if let Err(error) = Self::clear_microvm_guest_boot_config(&spec) {
326 self.cleanup_boot_failure().await;
327 return Err(error);
328 }
329
330 #[cfg(unix)]
341 if !is_restore_mode(&self.config)
342 && std::env::var("BOX_DEFERRED_MAIN")
343 .map(|v| v == "1")
344 .unwrap_or(false)
345 {
346 if let Some(client) = self.exec_client.as_ref() {
347 match client.spawn_main(None).await {
348 Ok(true) => tracing::info!("deferred container main spawned"),
349 Ok(false) => tracing::warn!("deferred spawn-main not acknowledged"),
350 Err(e) => tracing::warn!(error = %e, "deferred spawn-main failed"),
351 }
352 }
353 }
354
355 self.exec_socket_path = Some(layout.exec_socket_path.clone());
357 self.pty_socket_path = Some(layout.pty_socket_path.clone());
358 self.port_forward_socket_path = Some(layout.port_forward_socket_path.clone());
359
360 #[cfg(unix)]
362 if !matches!(self.config.tee, TeeConfig::None) {
363 self.tee = Some(Box::new(crate::tee::SnpTeeExtension::new(
364 self.box_id.clone(),
365 layout.attest_socket_path.clone(),
366 )));
367 }
368
369 *self.state.write().await = BoxState::Ready;
371
372 if let Some(ref prom) = self.prom {
374 let boot_duration = boot_start.elapsed().as_secs_f64();
375 prom.vm_boot_duration.observe(boot_duration);
376 prom.vm_created_total.inc();
377 prom.vm_count.with_label_values(&["ready"]).inc();
378 }
379
380 self.event_emitter.emit(BoxEvent::empty("box.ready"));
382
383 tracing::info!(parent: &boot_span, box_id = %self.box_id, "VM ready");
384
385 Ok(())
386 }
387
388 #[cfg(target_os = "macos")]
389 fn rootfs_artifact_cache_options(
390 &self,
391 layout: &BoxLayout,
392 guest_executable: &str,
393 uses_guest_boot_config: bool,
394 ) -> Result<Option<crate::rootfs::RootfsArtifactCacheOptions>> {
395 if !self.rootfs_provider.supports_artifact_cache()
396 || !self.config.cache.enabled
397 || !uses_guest_boot_config
398 || layout.resumed_rootfs.is_some()
399 {
400 return Ok(None);
401 }
402 let Some(oci_manifest_digest) = layout.oci_manifest_digest.as_ref() else {
403 return Ok(None);
404 };
405 let relative = guest_executable.strip_prefix('/').ok_or_else(|| {
406 BoxError::BuildError(format!(
407 "guest-init executable is not absolute: {guest_executable}"
408 ))
409 })?;
410 let guest_init =
411 crate::oci::rootfs::resolve_guest_file_path(&layout.rootfs_path, relative)?;
412 let guest_init_sha256 = Self::guest_init_sha256(&guest_init)?;
413
414 let architecture = a3s_box_core::platform::Platform::host().architecture;
415 Ok(Some(crate::rootfs::RootfsArtifactCacheOptions {
416 directory: self.resolve_cache_dir().join("rootfs-ext4-v1"),
417 oci_manifest_digest: oci_manifest_digest.clone(),
418 platform: format!("linux/{architecture}"),
419 guest_init_sha256,
420 max_entries: self.config.cache.max_rootfs_entries,
421 max_allocated_bytes: self.config.cache.max_cache_bytes,
422 }))
423 }
424
425 pub(crate) fn guest_init_sha256(path: &std::path::Path) -> Result<String> {
426 use sha2::{Digest, Sha256};
427 use std::io::Read;
428
429 let path_metadata = std::fs::symlink_metadata(path).map_err(BoxError::IoError)?;
430 let mut open_options = std::fs::OpenOptions::new();
431 open_options.read(true);
432 #[cfg(unix)]
433 {
434 use std::os::unix::fs::OpenOptionsExt;
435 open_options.custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW);
436 }
437 let mut file = open_options.open(path).map_err(BoxError::IoError)?;
438 let metadata = file.metadata().map_err(BoxError::IoError)?;
439 const MAX_GUEST_INIT_BYTES: u64 = 256 * 1024 * 1024;
440 if !path_metadata.is_file()
441 || path_metadata.file_type().is_symlink()
442 || !metadata.is_file()
443 || path_metadata.len() != metadata.len()
444 || metadata.len() > MAX_GUEST_INIT_BYTES
445 {
446 return Err(BoxError::BuildError(format!(
447 "guest-init cache identity source is not a bounded plain file: {}",
448 path.display()
449 )));
450 }
451 #[cfg(unix)]
452 {
453 use std::os::unix::fs::MetadataExt;
454 if path_metadata.dev() != metadata.dev() || path_metadata.ino() != metadata.ino() {
455 return Err(BoxError::BuildError(format!(
456 "guest-init changed while opening cache identity source: {}",
457 path.display()
458 )));
459 }
460 }
461 let expected_length = metadata.len();
462 let mut read_length = 0u64;
463 let mut hasher = Sha256::new();
464 let mut buffer = vec![0u8; 1024 * 1024];
465 {
466 let mut bounded = file.by_ref().take(MAX_GUEST_INIT_BYTES + 1);
467 loop {
468 let read = bounded.read(&mut buffer).map_err(BoxError::IoError)?;
469 if read == 0 {
470 break;
471 }
472 read_length = read_length.checked_add(read as u64).ok_or_else(|| {
473 BoxError::BuildError("guest-init length overflow".to_string())
474 })?;
475 if read_length > expected_length {
476 return Err(BoxError::BuildError(format!(
477 "guest-init changed while computing cache identity: {}",
478 path.display()
479 )));
480 }
481 hasher.update(&buffer[..read]);
482 }
483 }
484 if read_length != expected_length
485 || file.metadata().map_err(BoxError::IoError)?.len() != expected_length
486 {
487 return Err(BoxError::BuildError(format!(
488 "guest-init changed while computing cache identity: {}",
489 path.display()
490 )));
491 }
492 Ok(format!("sha256:{}", hex::encode(hasher.finalize())))
493 }
494
495 pub(super) fn create_diff_baseline(&self, layout: &BoxLayout) {
496 let box_dir = self.home_dir.join("boxes").join(&self.box_id);
497 if let Err(error) =
498 crate::rootfs::create_diff_baseline_if_absent(&box_dir, &layout.rootfs_path)
499 {
500 tracing::warn!(
501 box_id = %self.box_id,
502 %error,
503 "Failed to create rootfs diff baseline before workload launch"
504 );
505 }
506 }
507}