1#[path = "vm_sandbox.rs"]
4mod sandbox;
5
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8#[cfg(unix)]
9use std::time::Duration;
10
11use a3s_box_core::{
12 BoxError, EventEmitter, ExecutionBackend, ExecutionId, ExecutionManagerError,
13 ExecutionManagerResult, ExecutionState, KillOutcome, DEFAULT_SHUTDOWN_TIMEOUT_MS,
14};
15use async_trait::async_trait;
16use dashmap::mapref::entry::Entry;
17use dashmap::DashMap;
18use tokio::sync::Mutex;
19
20use super::resources::ExecutionResourceGuard;
21use super::vm_process::{locate_microvm_process, LocatedProcess};
22#[cfg(target_os = "linux")]
23use super::TransientRegistryAuthBroker;
24use super::{
25 LocalExecutionBackend, LocalExecutionHandle, LocalExecutionObservation,
26 LocalExecutionResourcePlan, LocalExecutionTermination,
27};
28use crate::vm::{TERMINAL_EXIT_POLL_INTERVAL, TERMINAL_EXIT_POLL_TIMEOUT};
29use crate::{
30 BoxRecord, ManagedExecutionMetadata, ManagedExecutionOperation, ManagedExecutionState,
31 VmManager,
32};
33
34type SharedVm = Arc<Mutex<VmManager>>;
35
36#[derive(Clone)]
39pub struct VmLocalExecutionBackend {
40 home_dir: PathBuf,
41 managers: Arc<DashMap<String, SharedVm>>,
42 pull_progress_fn: Option<crate::PullProgressFn>,
43 #[cfg(target_os = "linux")]
44 transient_registry_auth: Option<TransientRegistryAuthBroker>,
45}
46
47impl VmLocalExecutionBackend {
48 pub fn new(home_dir: impl Into<PathBuf>) -> Self {
49 Self {
50 home_dir: home_dir.into(),
51 managers: Arc::new(DashMap::new()),
52 pull_progress_fn: None,
53 #[cfg(target_os = "linux")]
54 transient_registry_auth: None,
55 }
56 }
57
58 pub fn with_pull_progress_fn(mut self, pull_progress_fn: crate::PullProgressFn) -> Self {
59 self.pull_progress_fn = Some(pull_progress_fn);
60 self
61 }
62
63 #[cfg(target_os = "linux")]
64 pub(crate) fn with_transient_registry_auth(
65 mut self,
66 broker: TransientRegistryAuthBroker,
67 ) -> Self {
68 self.transient_registry_auth = Some(broker);
69 self
70 }
71
72 pub fn home_dir(&self) -> &Path {
73 &self.home_dir
74 }
75
76 fn metadata<'a>(
77 &self,
78 record: &'a BoxRecord,
79 ) -> ExecutionManagerResult<&'a ManagedExecutionMetadata> {
80 self.metadata_for_route(record, crate::ManagedRuntimeRoute::BoxVm)
81 }
82
83 fn metadata_for_route<'a>(
84 &self,
85 record: &'a BoxRecord,
86 expected_route: crate::ManagedRuntimeRoute,
87 ) -> ExecutionManagerResult<&'a ManagedExecutionMetadata> {
88 uuid::Uuid::parse_str(&record.id).map_err(|error| {
89 ExecutionManagerError::Internal(format!(
90 "managed execution has an invalid internal ID {}: {error}",
91 record.id
92 ))
93 })?;
94 let expected_box_dir = self.home_dir.join("boxes").join(&record.id);
95 if record.box_dir != expected_box_dir {
96 return Err(ExecutionManagerError::Internal(format!(
97 "managed execution {} has an unexpected host directory {}",
98 record.id,
99 record.box_dir.display()
100 )));
101 }
102 let metadata = record.managed_execution.as_ref().ok_or_else(|| {
103 ExecutionManagerError::Internal(format!(
104 "execution {} lost managed lifecycle metadata",
105 record.id
106 ))
107 })?;
108 metadata
109 .validate()
110 .map_err(|error| ExecutionManagerError::Internal(error.to_string()))?;
111 let resolved_route = super::router::resolved_runtime_route(record)?;
112 if resolved_route != expected_route {
113 return Err(ExecutionManagerError::Internal(format!(
114 "managed execution {} is pinned to {:?}, not {:?}",
115 record.id, resolved_route, expected_route
116 )));
117 }
118 if record.isolation != metadata.request.config.isolation {
119 return Err(ExecutionManagerError::Internal(format!(
120 "managed execution {} has inconsistent isolation metadata",
121 record.id
122 )));
123 }
124 Ok(metadata)
125 }
126
127 fn new_manager(&self, record: &BoxRecord) -> ExecutionManagerResult<VmManager> {
128 let metadata = self.metadata(record)?;
129 self.new_manager_from_metadata(record, metadata)
130 }
131
132 pub(super) fn new_oci_preparation_manager(
133 &self,
134 record: &BoxRecord,
135 ) -> ExecutionManagerResult<VmManager> {
136 let metadata = self.metadata_for_route(record, crate::ManagedRuntimeRoute::OciSdk)?;
137 self.new_manager_from_metadata(record, metadata)
138 }
139
140 fn new_manager_from_metadata(
141 &self,
142 record: &BoxRecord,
143 metadata: &ManagedExecutionMetadata,
144 ) -> ExecutionManagerResult<VmManager> {
145 let mut config = metadata.request.config.clone();
146 config.network = record.network_mode.clone();
151 if let Some(shm_size) = metadata.request.policy.shm_size {
152 let has_shared_memory_mount = config
153 .tmpfs
154 .iter()
155 .any(|entry| entry.split(':').next() == Some("/dev/shm"));
156 if !has_shared_memory_mount {
157 config.tmpfs.push(format!("/dev/shm:size={shm_size}"));
158 }
159 }
160 let mut manager = VmManager::with_box_id(config, EventEmitter::new(256), record.id.clone());
161 manager.home_dir = self.home_dir.clone();
162 manager.set_healthcheck_disabled(metadata.request.policy.healthcheck_disabled);
163 if let Some(pull_progress_fn) = self.pull_progress_fn.clone() {
164 manager.set_pull_progress_fn(pull_progress_fn);
165 }
166 manager.anonymous_volumes = record.anonymous_volumes.clone();
167 manager.set_log_config(record.log_config.clone());
168 manager.resolved_execution_plan = Some(metadata.plan.clone());
169 manager.managed_secret_root = metadata.request.policy.managed_secret_root.clone();
170 Ok(manager)
171 }
172
173 #[cfg(target_os = "linux")]
174 fn claim_transient_registry_auth_for_boot(&self, manager: &mut VmManager) {
175 manager.transient_registry_auth = self
176 .transient_registry_auth
177 .as_ref()
178 .and_then(|broker| broker.take(manager.box_id()));
179 }
180
181 fn manager(&self, execution_id: &str) -> Option<SharedVm> {
182 self.managers
183 .get(execution_id)
184 .map(|entry| Arc::clone(entry.value()))
185 }
186
187 fn remove_manager(&self, execution_id: &str, expected: &SharedVm) {
188 if let Entry::Occupied(entry) = self.managers.entry(execution_id.to_string()) {
189 if Arc::ptr_eq(entry.get(), expected) {
190 entry.remove();
191 }
192 }
193 }
194
195 async fn handle_from_manager(
196 &self,
197 record: &BoxRecord,
198 manager: &VmManager,
199 ) -> ExecutionManagerResult<LocalExecutionHandle> {
200 let execution_id = execution_id(record)?;
201 let pid = manager.pid().await.ok_or_else(|| {
202 ExecutionManagerError::Internal(format!(
203 "runtime returned no host PID for {execution_id}"
204 ))
205 })?;
206 let pid_start_time = crate::process::pid_start_time(pid);
207 #[cfg(target_os = "linux")]
208 if pid_start_time.is_none() {
209 return Err(ExecutionManagerError::NotFound(execution_id));
210 }
211 if !crate::process::is_process_alive_with_identity(pid, pid_start_time) {
212 return Err(ExecutionManagerError::NotFound(execution_id));
213 }
214 let exec_socket_path = manager
215 .exec_socket_path()
216 .map(Path::to_path_buf)
217 .ok_or_else(|| {
218 ExecutionManagerError::Internal(format!(
219 "runtime returned no exec socket for {}",
220 record.id
221 ))
222 })?;
223 let anonymous_volumes = if manager.anonymous_volumes().is_empty() {
224 self.anonymous_volumes_for_record(record).await
225 } else {
226 manager.anonymous_volumes().to_vec()
227 };
228 Ok(LocalExecutionHandle {
229 started_at: record.started_at.unwrap_or_else(chrono::Utc::now),
230 pid: Some(pid),
231 pid_start_time,
232 exec_socket_path,
233 console_log: record.box_dir.join("logs/console.log"),
234 anonymous_volumes,
235 oci_runtime: None,
236 })
237 }
238
239 async fn inspect_registered(
240 &self,
241 record: &BoxRecord,
242 shared: SharedVm,
243 ) -> ExecutionManagerResult<LocalExecutionObservation> {
244 let mut manager = shared.lock().await;
245 let preserve_rootfs = should_force_rootfs_preservation(record)?;
246 let exit_code = manager
247 .try_wait_exit()
248 .await
249 .map_err(|error| runtime_error("inspect", record, error))?;
250 let mut state = manager.state().await;
251 let terminal = exit_code.is_some() || state == crate::BoxState::Stopped;
252 if terminal {
253 return self
254 .finish_registered_terminal(record, &shared, manager, preserve_rootfs, exit_code)
255 .await;
256 }
257
258 if state == crate::BoxState::Created {
259 if manager.has_exited().await {
260 return self
261 .finish_registered_terminal(record, &shared, manager, preserve_rootfs, None)
262 .await;
263 }
264 if !self.promote_if_ready(record, &mut manager).await {
265 return Ok(LocalExecutionObservation {
266 state: ExecutionState::Creating,
267 handle: None,
268 exit_code: None,
269 });
270 }
271 state = manager.state().await;
272 }
273
274 if !manager
275 .health_check()
276 .await
277 .map_err(|error| runtime_error("inspect", record, error))?
278 {
279 return self
280 .finish_registered_terminal(record, &shared, manager, preserve_rootfs, None)
281 .await;
282 }
283
284 if state != crate::BoxState::Ready
285 && state != crate::BoxState::Busy
286 && state != crate::BoxState::Compacting
287 {
288 return Err(ExecutionManagerError::Internal(format!(
289 "runtime manager for {} is in unexpected state {state:?}",
290 record.id
291 )));
292 }
293 if matches!(
294 managed_state(record)?,
295 ManagedExecutionState::Starting | ManagedExecutionState::RestartStarting
296 ) && !exec_endpoint_ready(manager.exec_socket_path()).await
297 {
298 return Ok(LocalExecutionObservation {
299 state: ExecutionState::Creating,
300 handle: None,
301 exit_code: None,
302 });
303 }
304 let visible_state = visible_active_state(record)?;
305 let handle = match self.handle_from_manager(record, &manager).await {
306 Ok(handle) => handle,
307 Err(ExecutionManagerError::NotFound(_)) => {
308 return self
315 .finish_registered_terminal(record, &shared, manager, preserve_rootfs, None)
316 .await;
317 }
318 Err(error) => return Err(error),
319 };
320 Ok(LocalExecutionObservation {
321 state: visible_state,
322 handle: Some(handle),
323 exit_code: None,
324 })
325 }
326
327 async fn finish_registered_terminal(
328 &self,
329 record: &BoxRecord,
330 shared: &SharedVm,
331 mut manager: tokio::sync::MutexGuard<'_, VmManager>,
332 preserve_rootfs: bool,
333 exit_code: Option<i32>,
334 ) -> ExecutionManagerResult<LocalExecutionObservation> {
335 let deadline = tokio::time::Instant::now() + TERMINAL_EXIT_POLL_TIMEOUT;
340 let mut exit_code = manager.exit_code().or(exit_code);
341 while exit_code.is_none() {
342 exit_code = manager
343 .try_wait_exit()
344 .await
345 .map_err(|error| runtime_error("collect exit status", record, error))?;
346 if exit_code.is_some() || tokio::time::Instant::now() >= deadline {
347 break;
348 }
349 tokio::time::sleep(TERMINAL_EXIT_POLL_INTERVAL).await;
350 }
351 let exit_code = exit_code.ok_or_else(|| {
352 ExecutionManagerError::Unavailable(format!(
353 "runtime reported execution {} as terminal before its exact exit status became available",
354 record.id
355 ))
356 })?;
357 let cleanup = destroy_after_observation(&mut manager, preserve_rootfs).await;
358 drop(manager);
359 self.remove_manager(&record.id, shared);
360 cleanup.map_err(|error| runtime_error("clean up", record, error))?;
361 Ok(LocalExecutionObservation {
362 state: ExecutionState::Stopped,
363 handle: None,
364 exit_code: Some(exit_code),
365 })
366 }
367
368 async fn promote_if_ready(&self, record: &BoxRecord, manager: &mut VmManager) -> bool {
369 let socket_dir = crate::vm::runtime_socket_dir(&self.home_dir, &record.id);
370 let exec_socket = socket_dir.join("exec.sock");
371 if !exec_endpoint_ready(Some(&exec_socket)).await {
372 return false;
373 }
374 manager.exec_socket_path = Some(exec_socket);
375 manager.pty_socket_path = Some(socket_dir.join("pty.sock"));
376 manager.port_forward_socket_path = Some(socket_dir.join("portfwd.sock"));
377 *manager.state.write().await = crate::BoxState::Ready;
378 true
379 }
380
381 async fn recover_microvm(&self, record: &BoxRecord) -> ExecutionManagerResult<SharedVm> {
382 self.metadata(record)?;
383 let execution_id = execution_id(record)?;
384 let execution_id_label = record.id.clone();
385 let recorded = record.pid.map(|pid| (pid, record.pid_start_time));
386 let located = tokio::task::spawn_blocking(move || {
387 locate_microvm_process(&execution_id_label, recorded)
388 })
389 .await
390 .map_err(|error| {
391 ExecutionManagerError::Internal(format!(
392 "MicroVM process discovery task failed for {}: {error}",
393 record.id
394 ))
395 })?
396 .map_err(ExecutionManagerError::Internal)?
397 .ok_or(ExecutionManagerError::NotFound(execution_id))?;
398 self.attach_microvm(record, located).await
399 }
400
401 async fn attach_microvm(
402 &self,
403 record: &BoxRecord,
404 located: LocatedProcess,
405 ) -> ExecutionManagerResult<SharedVm> {
406 let mut manager = self.new_manager(record)?;
407 let socket_dir = crate::vm::runtime_socket_dir(&self.home_dir, &record.id);
408 manager
409 .attach_running_process(
410 located.pid,
411 socket_dir.join("exec.sock"),
412 Some(socket_dir.join("pty.sock")),
413 )
414 .await
415 .map_err(|error| runtime_error("recover", record, error))?;
416 if located.start_time.is_some()
417 && crate::process::pid_start_time(located.pid) != located.start_time
418 {
419 return Err(ExecutionManagerError::NotFound(execution_id(record)?));
420 }
421 let recovered = Arc::new(Mutex::new(manager));
422 match self.managers.entry(record.id.clone()) {
423 Entry::Occupied(entry) => Ok(Arc::clone(entry.get())),
424 Entry::Vacant(entry) => {
425 entry.insert(Arc::clone(&recovered));
426 Ok(recovered)
427 }
428 }
429 }
430
431 #[cfg(not(windows))]
432 async fn require_microvm(&self, record: &BoxRecord) -> ExecutionManagerResult<SharedVm> {
433 match self.manager(&record.id) {
434 Some(manager) => Ok(manager),
435 None => self.recover_microvm(record).await,
436 }
437 }
438
439 async fn destroy_registered(
440 &self,
441 record: &BoxRecord,
442 shared: SharedVm,
443 remove_anonymous_volumes: bool,
444 force_preserve_rootfs: bool,
445 timeout_secs: Option<u64>,
446 ) -> ExecutionManagerResult<LocalExecutionTermination> {
447 let mut manager = shared.lock().await;
448 let mut anonymous_volumes = if manager.anonymous_volumes().is_empty() {
449 record.anonymous_volumes.clone()
450 } else {
451 manager.anonymous_volumes().to_vec()
452 };
453 let result = match (
454 graceful_stop_options(record, timeout_secs)?,
455 force_preserve_rootfs,
456 ) {
457 (Some((signal, timeout_ms)), true) => {
458 manager
459 .destroy_preserving_rootfs_with_options(signal, timeout_ms)
460 .await
461 }
462 (Some((signal, timeout_ms)), false) => {
463 manager.destroy_with_options(signal, timeout_ms).await
464 }
465 (None, true) => manager.destroy_preserving_rootfs().await,
466 (None, false) => manager.destroy().await,
467 };
468 let exit_code = manager.exit_code();
469 drop(manager);
470 self.remove_manager(&record.id, &shared);
471 result.map_err(|error| runtime_error("kill", record, error))?;
472 if remove_anonymous_volumes {
473 if anonymous_volumes.is_empty() {
474 anonymous_volumes = self.anonymous_volumes_for_record(record).await;
475 }
476 self.cleanup_anonymous_volumes(&record.id, anonymous_volumes)
477 .await;
478 }
479 Ok(LocalExecutionTermination {
480 outcome: KillOutcome::Killed,
481 exit_code,
482 })
483 }
484
485 async fn anonymous_volumes_for_record(&self, record: &BoxRecord) -> Vec<String> {
486 if !record.anonymous_volumes.is_empty() {
487 return record.anonymous_volumes.clone();
488 }
489 let home_dir = self.home_dir.clone();
490 let execution_id = record.id.clone();
491 let short_id = record.id.chars().take(8).collect::<String>();
492 let result = tokio::task::spawn_blocking(move || -> a3s_box_core::Result<Vec<String>> {
493 let store =
494 crate::VolumeStore::new(home_dir.join("volumes.json"), home_dir.join("volumes"));
495 let prefix = format!("anon_{short_id}_");
496 let mut names = store
497 .load()?
498 .into_values()
499 .filter(|volume| {
500 volume
501 .labels
502 .get("anonymous")
503 .is_some_and(|value| value == "true")
504 && (volume.in_use_by.iter().any(|id| id == &execution_id)
505 || volume.name.starts_with(&prefix))
506 })
507 .map(|volume| volume.name)
508 .collect::<Vec<_>>();
509 names.sort();
510 Ok(names)
511 })
512 .await;
513 match result {
514 Ok(Ok(names)) => names,
515 Ok(Err(error)) => {
516 tracing::warn!(
517 execution_id = %record.id,
518 %error,
519 "Failed to load anonymous volumes during managed cleanup"
520 );
521 Vec::new()
522 }
523 Err(error) => {
524 tracing::warn!(
525 execution_id = %record.id,
526 %error,
527 "Anonymous volume recovery task failed"
528 );
529 Vec::new()
530 }
531 }
532 }
533
534 async fn cleanup_anonymous_volumes(&self, owner: &str, names: Vec<String>) {
535 if names.is_empty() {
536 return;
537 }
538 let home_dir = self.home_dir.clone();
539 let owner = owner.to_string();
540 let task = tokio::task::spawn_blocking(move || {
541 let store = crate::VolumeStore::new(
542 home_dir.join("volumes.json"),
543 home_dir.join("volumes"),
544 );
545 for name in names {
546 if let Err(error) = store.remove_anonymous(&name, &owner) {
547 tracing::warn!(volume = %name, %error, "Failed to remove managed anonymous volume");
548 }
549 }
550 })
551 .await;
552 if let Err(error) = task {
553 tracing::warn!(%error, "Anonymous volume cleanup task failed");
554 }
555 }
556
557 async fn terminate_execution(
558 &self,
559 record: &BoxRecord,
560 ) -> ExecutionManagerResult<LocalExecutionTermination> {
561 let metadata = self.metadata(record)?;
562 let remove_anonymous_volumes = record.auto_remove;
563 let timeout_secs = record.stop_timeout;
564 if let Some(manager) = self.manager(&record.id) {
565 return self
566 .destroy_registered(
567 record,
568 manager,
569 remove_anonymous_volumes,
570 false,
571 timeout_secs,
572 )
573 .await;
574 }
575 if !metadata.paused_with_memory {
579 let manager = Arc::new(Mutex::new(self.new_manager(record)?));
580 return self
581 .destroy_registered(
582 record,
583 manager,
584 remove_anonymous_volumes,
585 false,
586 timeout_secs,
587 )
588 .await;
589 }
590 match metadata.plan.backend {
591 ExecutionBackend::A3sOci => {
592 self.destroy_detached_sandbox(record, remove_anonymous_volumes, false, timeout_secs)
593 .await
594 }
595 ExecutionBackend::Krun => {
596 let manager = self.recover_microvm(record).await?;
597 self.destroy_registered(
598 record,
599 manager,
600 remove_anonymous_volumes,
601 false,
602 timeout_secs,
603 )
604 .await
605 }
606 }
607 }
608}
609
610async fn destroy_after_observation(
611 manager: &mut VmManager,
612 preserve_rootfs: bool,
613) -> a3s_box_core::Result<()> {
614 if preserve_rootfs {
615 manager.destroy_preserving_rootfs().await
616 } else {
617 manager.destroy().await
618 }
619}
620
621#[async_trait]
622impl LocalExecutionBackend for VmLocalExecutionBackend {
623 fn route_for_create(
624 &self,
625 _record: &BoxRecord,
626 ) -> ExecutionManagerResult<crate::ManagedRuntimeRoute> {
627 Ok(crate::ManagedRuntimeRoute::BoxVm)
628 }
629
630 async fn plan_create_resources(
631 &self,
632 record: &BoxRecord,
633 ) -> ExecutionManagerResult<LocalExecutionResourcePlan> {
634 let metadata = self.metadata(record)?;
635 if !metadata.plan.backend.is_sandbox() {
636 return Ok(LocalExecutionResourcePlan::default());
637 }
638
639 let mut manager = self.new_manager(record)?;
643 #[cfg(target_os = "linux")]
644 {
645 if let Some(broker) = &self.transient_registry_auth {
651 manager.transient_registry_auth = broker.clone_auth(metadata.operation_id.as_str());
652 }
653 }
654 let anonymous_volumes = manager
655 .plan_image_anonymous_volumes()
656 .await
657 .map_err(|error| match error {
658 BoxError::ConfigError(message) => ExecutionManagerError::InvalidRequest(message),
659 error => ExecutionManagerError::Unavailable(format!(
660 "Box image resource planning failed for {}: {error}",
661 record.id
662 )),
663 })?;
664 Ok(LocalExecutionResourcePlan { anonymous_volumes })
665 }
666
667 async fn start(&self, record: &BoxRecord) -> ExecutionManagerResult<LocalExecutionHandle> {
668 super::record::validate_record_health(record)?;
669 self.metadata(record)?;
670 let box_dir = record.box_dir.clone();
671 let execution_id = record.id.clone();
672 tokio::task::spawn_blocking(move || {
673 crate::rootfs::stage_box_terminal_rootfs_metadata(&box_dir)
674 })
675 .await
676 .map_err(|error| {
677 ExecutionManagerError::Internal(format!(
678 "rootfs metadata staging task failed for {execution_id}: {error}"
679 ))
680 })?
681 .map_err(|error| {
682 ExecutionManagerError::Internal(format!(
683 "failed to stage rootfs metadata for {execution_id}: {error}"
684 ))
685 })?;
686 let mut manager = self.new_manager(record)?;
687 let requested_persistence = manager.config.persistent;
688 if should_reuse_preserved_rootfs(record)?
689 && crate::vm::persistent_rootfs_generation_exists(&record.box_dir)
690 .map_err(|error| runtime_error("inspect retained rootfs", record, error))?
691 {
692 manager.config.persistent = true;
693 }
694 let manager = Arc::new(Mutex::new(manager));
695 match self.managers.entry(record.id.clone()) {
696 Entry::Occupied(_) => {
697 return Err(ExecutionManagerError::Unavailable(format!(
698 "execution {} already has an in-process runtime owner",
699 record.id
700 )))
701 }
702 Entry::Vacant(entry) => {
703 entry.insert(Arc::clone(&manager));
704 }
705 }
706
707 let mut guard = manager.lock().await;
708 let resource_home = self.home_dir.clone();
709 let resource_record = record.clone();
710 let resources = match tokio::task::spawn_blocking(move || {
711 ExecutionResourceGuard::prepare(&resource_home, &resource_record)
712 })
713 .await
714 {
715 Ok(Ok(resources)) => resources,
716 Ok(Err(error)) => {
717 drop(guard);
718 self.remove_manager(&record.id, &manager);
719 return Err(error);
720 }
721 Err(error) => {
722 drop(guard);
723 self.remove_manager(&record.id, &manager);
724 return Err(ExecutionManagerError::Internal(format!(
725 "managed resource preparation task failed for {}: {error}",
726 record.id
727 )));
728 }
729 };
730 #[cfg(target_os = "linux")]
731 self.claim_transient_registry_auth_for_boot(&mut guard);
732 if let Err(error) = guard.boot().await {
733 guard.config.persistent = requested_persistence;
734 if guard.exit_code().is_some() {
741 tracing::debug!(
742 execution_id = %record.id,
743 %error,
744 "Runtime completed while startup was establishing readiness"
745 );
746 resources.disarm();
747 return Err(ExecutionManagerError::Unavailable(format!(
748 "execution {} completed during startup",
749 record.id
750 )));
751 }
752 drop(guard);
753 self.remove_manager(&record.id, &manager);
754 let rollback = tokio::task::spawn_blocking(move || resources.rollback()).await;
755 if let Err(rollback_error) = rollback {
756 tracing::warn!(
757 execution_id = %record.id,
758 %rollback_error,
759 "Managed resource rollback task failed"
760 );
761 }
762 return Err(runtime_error("start", record, error));
763 }
764 guard.config.persistent = requested_persistence;
765 resources.disarm();
766 let exited_during_start = guard
767 .try_wait_exit()
768 .await
769 .map_err(|error| runtime_error("collect startup exit status", record, error))?
770 .is_some()
771 || guard.has_exited().await;
772 if exited_during_start {
773 return Err(ExecutionManagerError::Unavailable(format!(
774 "execution {} completed during startup",
775 record.id
776 )));
777 }
778 self.handle_from_manager(record, &guard).await
779 }
780
781 async fn inspect(
782 &self,
783 record: &BoxRecord,
784 ) -> ExecutionManagerResult<LocalExecutionObservation> {
785 let metadata = self.metadata(record)?;
786 if metadata.plan.backend.is_sandbox() {
787 return self.inspect_sandbox(record).await;
788 }
789 if let Some(manager) = self.manager(&record.id) {
790 return self.inspect_registered(record, manager).await;
791 }
792 let manager = self.recover_microvm(record).await?;
793 self.inspect_registered(record, manager).await
794 }
795
796 async fn pause(
797 &self,
798 record: &BoxRecord,
799 keep_memory: bool,
800 ) -> ExecutionManagerResult<LocalExecutionHandle> {
801 let metadata = self.metadata(record)?;
802 if metadata.plan.backend.is_sandbox() {
803 if !keep_memory {
804 return Err(unsupported(
805 record,
806 "pause without memory retention",
807 "the Sandbox backend",
808 ));
809 }
810 return self.pause_sandbox(record).await;
811 }
812 if !keep_memory {
813 return Err(unsupported(
814 record,
815 "pause without memory retention",
816 "the local MicroVM backend",
817 ));
818 }
819 #[cfg(windows)]
820 {
821 Err(unsupported(
822 record,
823 "pause",
824 "the local MicroVM backend on Windows",
825 ))
826 }
827 #[cfg(not(windows))]
828 {
829 let shared = self.require_microvm(record).await?;
830 let manager = shared.lock().await;
831 require_recorded_pid(record, &manager).await?;
832 manager
833 .pause()
834 .await
835 .map_err(|error| runtime_error("pause", record, error))?;
836 self.handle_from_manager(record, &manager).await
837 }
838 }
839
840 async fn resume(&self, record: &BoxRecord) -> ExecutionManagerResult<LocalExecutionHandle> {
841 let metadata = self.metadata(record)?;
842 if metadata.plan.backend.is_sandbox() {
843 return self.resume_sandbox(record).await;
844 }
845 #[cfg(windows)]
846 {
847 Err(unsupported(
848 record,
849 "resume",
850 "the local MicroVM backend on Windows",
851 ))
852 }
853 #[cfg(not(windows))]
854 {
855 let shared = self.require_microvm(record).await?;
856 let manager = shared.lock().await;
857 require_recorded_pid(record, &manager).await?;
858 manager
859 .resume()
860 .await
861 .map_err(|error| runtime_error("resume", record, error))?;
862 self.handle_from_manager(record, &manager).await
863 }
864 }
865
866 async fn prepare_quiescent_rootfs(&self, record: &BoxRecord) -> ExecutionManagerResult<()> {
867 self.new_manager(record)?
868 .prepare_preserved_rootfs()
869 .map(|_| ())
870 .map_err(|error| runtime_error("prepare quiescent rootfs", record, error))
871 }
872
873 async fn cleanup_quiescent_rootfs(&self, record: &BoxRecord) -> ExecutionManagerResult<()> {
874 self.new_manager(record)?
875 .cleanup_preserved_rootfs()
876 .map_err(|error| runtime_error("clean up quiescent rootfs", record, error))
877 }
878
879 async fn kill(&self, record: &BoxRecord) -> ExecutionManagerResult<KillOutcome> {
880 Ok(self.terminate_execution(record).await?.outcome)
881 }
882
883 async fn kill_with_status(
884 &self,
885 record: &BoxRecord,
886 ) -> ExecutionManagerResult<LocalExecutionTermination> {
887 self.terminate_execution(record).await
888 }
889
890 async fn stop_for_restart(
891 &self,
892 record: &BoxRecord,
893 timeout_secs: Option<u64>,
894 ) -> ExecutionManagerResult<KillOutcome> {
895 let metadata = self.metadata(record)?;
896 #[cfg(target_os = "linux")]
897 if metadata.plan.backend.is_sandbox() {
898 super::snapshot::persist_sandbox_snapshot_mappings(record)?;
899 }
900 let timeout_secs = timeout_secs.or(record.stop_timeout);
901 if let Some(manager) = self.manager(&record.id) {
902 return Ok(self
903 .destroy_registered(record, manager, false, true, timeout_secs)
904 .await?
905 .outcome);
906 }
907 match metadata.plan.backend {
908 ExecutionBackend::A3sOci => Ok(self
909 .destroy_detached_sandbox(record, false, true, timeout_secs)
910 .await?
911 .outcome),
912 ExecutionBackend::Krun => {
913 let manager = self.recover_microvm(record).await?;
914 Ok(self
915 .destroy_registered(record, manager, false, true, timeout_secs)
916 .await?
917 .outcome)
918 }
919 }
920 }
921}
922
923pub(super) fn should_force_rootfs_preservation(record: &BoxRecord) -> ExecutionManagerResult<bool> {
924 let state = super::support::managed_state(record)?;
925 let metadata = record.managed_execution.as_ref().ok_or_else(|| {
926 ExecutionManagerError::Internal(format!(
927 "execution {} has no managed lifecycle metadata",
928 record.id
929 ))
930 })?;
931 Ok(match state {
932 ManagedExecutionState::Starting => true,
938 ManagedExecutionState::Pausing => matches!(
939 metadata.pending_operation.as_ref(),
940 Some(ManagedExecutionOperation::Pause {
941 keep_memory: false,
942 ..
943 })
944 ),
945 ManagedExecutionState::Resuming => !metadata.paused_with_memory,
946 ManagedExecutionState::RestartStopping | ManagedExecutionState::RestartStarting => true,
947 _ => false,
948 })
949}
950
951fn should_reuse_preserved_rootfs(record: &BoxRecord) -> ExecutionManagerResult<bool> {
952 Ok(matches!(
953 super::support::managed_state(record)?,
954 ManagedExecutionState::Resuming | ManagedExecutionState::RestartStarting
955 ) && should_force_rootfs_preservation(record)?)
956}
957
958fn graceful_stop_options(
959 record: &BoxRecord,
960 timeout_secs: Option<u64>,
961) -> ExecutionManagerResult<Option<(i32, u64)>> {
962 if timeout_secs.is_none() && record.stop_signal.is_none() {
963 return Ok(None);
964 }
965 let timeout_ms = timeout_secs
966 .unwrap_or(DEFAULT_SHUTDOWN_TIMEOUT_MS / 1_000)
967 .checked_mul(1_000)
968 .ok_or_else(|| {
969 ExecutionManagerError::InvalidRequest(format!(
970 "stop timeout is too large for execution {}",
971 record.id
972 ))
973 })?;
974 let signal = record
975 .stop_signal
976 .as_deref()
977 .map(a3s_box_core::vmm::parse_signal_name)
978 .unwrap_or(libc::SIGTERM);
979 Ok(Some((signal, timeout_ms)))
980}
981
982#[cfg(not(windows))]
983async fn require_recorded_pid(
984 record: &BoxRecord,
985 manager: &VmManager,
986) -> ExecutionManagerResult<()> {
987 let execution_id = execution_id(record)?;
988 let pid = manager
989 .pid()
990 .await
991 .ok_or_else(|| ExecutionManagerError::NotFound(execution_id.clone()))?;
992 if record.pid != Some(pid)
993 || !crate::process::is_process_alive_with_identity(pid, record.pid_start_time)
994 {
995 return Err(ExecutionManagerError::NotFound(execution_id));
996 }
997 Ok(())
998}
999
1000fn visible_active_state(record: &BoxRecord) -> ExecutionManagerResult<ExecutionState> {
1001 match managed_state(record)? {
1002 ManagedExecutionState::Paused => Ok(ExecutionState::Paused),
1003 ManagedExecutionState::Resuming => {
1004 let metadata = record.managed_execution.as_ref().ok_or_else(|| {
1005 ExecutionManagerError::Internal(format!(
1006 "execution {} has no managed lifecycle metadata",
1007 record.id
1008 ))
1009 })?;
1010 if metadata.paused_with_memory {
1011 Ok(ExecutionState::Paused)
1012 } else {
1013 Ok(ExecutionState::Running)
1017 }
1018 }
1019 ManagedExecutionState::Starting
1020 | ManagedExecutionState::RestartStarting
1021 | ManagedExecutionState::Running
1022 | ManagedExecutionState::Pausing
1023 | ManagedExecutionState::Killing => Ok(ExecutionState::Running),
1024 ManagedExecutionState::Snapshotting => match record
1025 .managed_execution
1026 .as_ref()
1027 .and_then(|metadata| metadata.pending_operation.as_ref())
1028 {
1029 Some(ManagedExecutionOperation::Snapshot {
1030 source_state: ManagedExecutionState::Running,
1031 ..
1032 }) => Ok(ExecutionState::Running),
1033 Some(ManagedExecutionOperation::Snapshot {
1034 source_state: ManagedExecutionState::Paused,
1035 ..
1036 }) => Ok(ExecutionState::Paused),
1037 _ => Err(ExecutionManagerError::Internal(format!(
1038 "execution {} has invalid snapshot metadata",
1039 record.id
1040 ))),
1041 },
1042 ManagedExecutionState::RestartStopping => match record
1043 .managed_execution
1044 .as_ref()
1045 .and_then(|metadata| metadata.pending_operation.as_ref())
1046 {
1047 Some(ManagedExecutionOperation::Restart {
1048 source_state: ManagedExecutionState::Paused,
1049 ..
1050 }) => Ok(ExecutionState::Paused),
1051 Some(ManagedExecutionOperation::Restart {
1052 source_state: ManagedExecutionState::Running,
1053 ..
1054 }) => Ok(ExecutionState::Running),
1055 _ => Err(ExecutionManagerError::Internal(format!(
1056 "execution {} has invalid restart teardown metadata",
1057 record.id
1058 ))),
1059 },
1060 state => Err(ExecutionManagerError::Internal(format!(
1061 "execution {} has no active runtime in managed state {state}",
1062 record.id
1063 ))),
1064 }
1065}
1066
1067fn managed_state(record: &BoxRecord) -> ExecutionManagerResult<ManagedExecutionState> {
1068 record
1069 .managed_state()
1070 .map_err(|error| ExecutionManagerError::Internal(error.to_string()))?
1071 .ok_or_else(|| {
1072 ExecutionManagerError::Internal(format!("execution {} is not managed", record.id))
1073 })
1074}
1075
1076fn execution_id(record: &BoxRecord) -> ExecutionManagerResult<ExecutionId> {
1077 ExecutionId::new(record.id.clone())
1078 .map_err(|error| ExecutionManagerError::Internal(error.to_string()))
1079}
1080
1081fn runtime_error(
1082 action: &str,
1083 record: &BoxRecord,
1084 error: impl std::fmt::Display,
1085) -> ExecutionManagerError {
1086 ExecutionManagerError::Internal(format!(
1087 "failed to {action} execution {}: {error}",
1088 record.id
1089 ))
1090}
1091
1092fn unsupported(record: &BoxRecord, operation: &str, backend: &str) -> ExecutionManagerError {
1093 ExecutionManagerError::Unavailable(format!(
1094 "{operation} is not supported by {backend} for execution {}",
1095 record.id
1096 ))
1097}
1098
1099#[cfg(unix)]
1100async fn exec_endpoint_ready(path: Option<&Path>) -> bool {
1101 let Some(path) = path else {
1102 return false;
1103 };
1104 let attempt = async {
1105 let client = crate::ExecClient::connect(path).await.ok()?;
1106 client.heartbeat().await.ok().filter(|ready| *ready)
1107 };
1108 tokio::time::timeout(Duration::from_millis(500), attempt)
1109 .await
1110 .ok()
1111 .flatten()
1112 .is_some()
1113}
1114
1115#[cfg(not(unix))]
1116async fn exec_endpoint_ready(path: Option<&Path>) -> bool {
1117 path.is_some()
1118}
1119
1120#[cfg(test)]
1121#[path = "vm_backend_tests.rs"]
1122mod tests;