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