1use super::*;
4
5#[cfg(unix)]
6const GUEST_STOP_DELIVERY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(1);
7#[cfg(unix)]
8const GUEST_STOP_FINALIZATION_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
9#[cfg(unix)]
10const GUEST_STOP_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(25);
11
12#[cfg(unix)]
13async fn wait_for_provider_exit(
14 handler: &mut dyn VmHandler,
15 timeout: std::time::Duration,
16) -> Result<bool> {
17 let deadline = tokio::time::Instant::now() + timeout;
18 loop {
19 if handler.try_wait_exit()?.is_some() || handler.has_exited() || !handler.is_running() {
20 return Ok(true);
21 }
22 let now = tokio::time::Instant::now();
23 if now >= deadline {
24 return Ok(false);
25 }
26 tokio::time::sleep(GUEST_STOP_POLL_INTERVAL.min(deadline - now)).await;
27 }
28}
29
30impl VmManager {
31 pub async fn destroy(&mut self) -> Result<()> {
33 self.destroy_with_options(default_stop_signal(), DEFAULT_SHUTDOWN_TIMEOUT_MS)
34 .await
35 }
36
37 pub async fn destroy_with_timeout(&mut self, timeout_ms: u64) -> Result<()> {
39 self.destroy_with_options(default_stop_signal(), timeout_ms)
40 .await
41 }
42
43 #[tracing::instrument(skip(self), fields(box_id = %self.box_id))]
51 pub async fn destroy_with_options(&mut self, signal: i32, timeout_ms: u64) -> Result<()> {
52 let preserve_rootfs = self.config.persistent;
53 self.destroy_with_rootfs_policy(signal, timeout_ms, preserve_rootfs)
54 .await
55 }
56
57 pub(crate) async fn destroy_preserving_rootfs_with_options(
60 &mut self,
61 signal: i32,
62 timeout_ms: u64,
63 ) -> Result<()> {
64 self.destroy_with_rootfs_policy(signal, timeout_ms, true)
65 .await
66 }
67
68 pub(crate) async fn destroy_preserving_rootfs(&mut self) -> Result<()> {
69 self.destroy_with_rootfs_policy(default_stop_signal(), DEFAULT_SHUTDOWN_TIMEOUT_MS, true)
70 .await
71 }
72
73 async fn destroy_with_rootfs_policy(
74 &mut self,
75 signal: i32,
76 timeout_ms: u64,
77 preserve_rootfs: bool,
78 ) -> Result<()> {
79 let mut state = self.state.write().await;
80
81 if *state == BoxState::Stopped {
82 return Ok(());
83 }
84
85 tracing::info!(box_id = %self.box_id, signal, timeout_ms, "Destroying VM");
86
87 *state = BoxState::Stopped;
89
90 let box_dir = self.home_dir.join("boxes").join(&self.box_id);
91
92 let mut stop_error = None;
98 #[cfg(unix)]
99 let requires_guest_rootfs_handoff =
100 if preserve_rootfs && self.boot_mode != VmBootMode::RootfsMaintenance {
101 match crate::rootfs::guest_native_ext4_generation_exists(&box_dir) {
102 Ok(exists) => exists,
103 Err(error) => {
104 stop_error = Some(error);
105 false
106 }
107 }
108 } else {
109 false
110 };
111 if let Some(mut handler) = self.handler.write().await.take() {
112 #[cfg(windows)]
113 let stop_request = match windows_stop::stage(&self.socket_dir(), signal) {
114 Ok(path) => {
115 tracing::debug!(
116 box_id = %self.box_id,
117 signal,
118 path = %path.display(),
119 "Staged Windows guest stop request"
120 );
121 Some(path)
122 }
123 Err(error) => {
124 tracing::warn!(
125 box_id = %self.box_id,
126 signal,
127 error = %error,
128 "Failed to stage Windows guest stop request; force-stop fallback remains active"
129 );
130 None
131 }
132 };
133
134 #[cfg(windows)]
135 let handler_timeout_ms = if timeout_ms == 0 {
136 0
137 } else if let Some(request) = stop_request.as_deref() {
138 let delivery_started = std::time::Instant::now();
139 let delivery_timeout = std::time::Duration::from_millis(
140 timeout_ms.min(WINDOWS_STOP_DELIVERY_TIMEOUT_MS),
141 );
142 let delivered =
143 match windows_stop::wait_until_delivered(request, delivery_timeout).await {
144 Ok(delivered) => delivered,
145 Err(error) => {
146 tracing::warn!(
147 box_id = %self.box_id,
148 error = %error,
149 "Failed while waiting for Windows guest stop request delivery"
150 );
151 false
152 }
153 };
154 let delivery_elapsed_ms =
155 u64::try_from(delivery_started.elapsed().as_millis()).unwrap_or(u64::MAX);
156 let remaining_timeout_ms = timeout_ms.saturating_sub(delivery_elapsed_ms);
157 if delivered {
158 let finalization_timeout_ms = if self.config.persistent {
159 WINDOWS_GUEST_FINALIZATION_TIMEOUT_MS
160 } else {
161 0
162 };
163 let handler_timeout_ms =
164 remaining_timeout_ms.saturating_add(finalization_timeout_ms);
165 tracing::debug!(
166 box_id = %self.box_id,
167 delivery_elapsed_ms,
168 handler_timeout_ms,
169 "Delivered Windows stop request to the guest"
170 );
171 handler_timeout_ms
172 } else {
173 tracing::warn!(
174 box_id = %self.box_id,
175 delivery_elapsed_ms,
176 "Windows guest stop request was not delivered before the forwarding deadline"
177 );
178 remaining_timeout_ms
179 }
180 } else {
181 timeout_ms
182 };
183 #[cfg(unix)]
184 let guest_stop_delivered = if self.boot_mode == VmBootMode::RootfsMaintenance {
185 self.deliver_rootfs_maintenance_shutdown().await
186 } else {
187 self.deliver_guest_stop_signal(signal).await
188 };
189 #[cfg(unix)]
190 let _provider_exited = if guest_stop_delivered {
191 let graceful_wait = if signal == libc::SIGKILL {
192 std::time::Duration::ZERO
193 } else {
194 std::time::Duration::from_millis(timeout_ms)
195 };
196 let exited = match wait_for_provider_exit(handler.as_mut(), graceful_wait).await {
197 Ok(exited) => exited,
198 Err(error) => {
199 tracing::warn!(
200 box_id = %self.box_id,
201 %error,
202 "Failed while waiting for guest-owned shutdown"
203 );
204 if stop_error.is_none() {
205 stop_error = Some(error);
206 }
207 false
208 }
209 };
210 if exited {
211 true
212 } else {
213 let force_delivered = signal == libc::SIGKILL
214 || self.deliver_guest_stop_signal(libc::SIGKILL).await;
215 if force_delivered {
216 match wait_for_provider_exit(
217 handler.as_mut(),
218 GUEST_STOP_FINALIZATION_TIMEOUT,
219 )
220 .await
221 {
222 Ok(exited) => exited,
223 Err(error) => {
224 tracing::warn!(
225 box_id = %self.box_id,
226 %error,
227 "Failed while waiting for forced guest finalization"
228 );
229 if stop_error.is_none() {
230 stop_error = Some(error);
231 }
232 false
233 }
234 }
235 } else {
236 false
237 }
238 }
239 } else {
240 false
241 };
242
243 #[cfg(unix)]
244 let handler_signal = if guest_stop_delivered {
245 libc::SIGKILL
246 } else {
247 signal
248 };
249 #[cfg(windows)]
250 let handler_signal = signal;
251 #[cfg(unix)]
252 let handler_timeout_ms = if guest_stop_delivered { 0 } else { timeout_ms };
253
254 let _handler_stopped = match handler.stop(handler_signal, handler_timeout_ms) {
261 Ok(()) => true,
262 Err(e) => {
263 tracing::error!(box_id = %self.box_id, error = %e, "Failed to stop VM handler; continuing teardown");
264 if stop_error.is_none() {
265 stop_error = Some(e);
266 }
267 false
268 }
269 };
270 #[cfg(not(windows))]
271 {
272 self.shim_exit_code =
273 crate::rootfs::resolve_workload_exit_code(&box_dir, handler.exit_code());
274 }
275 #[cfg(windows)]
276 {
277 self.shim_exit_code = handler.exit_code();
278 }
279
280 #[cfg(unix)]
281 let clean_guest_rootfs_handoff = _handler_stopped
282 && requires_guest_rootfs_handoff
283 && crate::rootfs::guest_rootfs_handoff_complete(&box_dir);
284 #[cfg(unix)]
285 if _handler_stopped && requires_guest_rootfs_handoff && !clean_guest_rootfs_handoff {
286 let error = BoxError::StateError(format!(
287 "Guest-owned persistent rootfs for {} stopped without a verified read-only handoff; the raw disk was retained but must not be treated as clean",
288 self.box_id
289 ));
290 tracing::error!(box_id = %self.box_id, %error);
291 if stop_error.is_none() {
292 stop_error = Some(error);
293 }
294 }
295
296 #[cfg(unix)]
297 if clean_guest_rootfs_handoff && stop_error.is_none() {
298 if let Err(error) = self.rootfs_provider.record_clean_stop(&box_dir) {
299 tracing::error!(
300 box_id = %self.box_id,
301 %error,
302 "Failed to publish the verified rootfs clean-stop transition"
303 );
304 stop_error = Some(error);
305 }
306 }
307
308 #[cfg(windows)]
309 if stop_request.is_some() {
310 if let Err(error) = windows_stop::clear(&self.socket_dir()) {
311 tracing::warn!(
312 box_id = %self.box_id,
313 error = %error,
314 "Failed to clear Windows guest stop request"
315 );
316 }
317 }
318
319 #[cfg(windows)]
320 if _handler_stopped && self.config.persistent {
321 let rootfs = self
322 .home_dir
323 .join("boxes")
324 .join(&self.box_id)
325 .join("rootfs");
326 match a3s_box_core::rootfs_metadata::finalize_terminal_rootfs_metadata(&rootfs) {
327 Ok(true) => tracing::info!(
328 box_id = %self.box_id,
329 path = %rootfs.display(),
330 "Published terminal rootfs metadata after Windows guest exit"
331 ),
332 Ok(false) => tracing::debug!(
333 box_id = %self.box_id,
334 path = %rootfs.display(),
335 "No Windows terminal rootfs metadata required host finalization"
336 ),
337 Err(error) => tracing::warn!(
338 box_id = %self.box_id,
339 path = %rootfs.display(),
340 error = %error,
341 "Refused to publish invalid Windows terminal rootfs metadata"
342 ),
343 }
344 }
345 }
346
347 if let Some(ref mut net) = self.net_manager {
349 net.stop();
350 }
351 self.net_manager = None;
352
353 let mount_aliases_clean = match self.cleanup_sandbox_mount_aliases() {
354 Ok(()) => true,
355 Err(error) => {
356 tracing::error!(
357 box_id = %self.box_id,
358 %error,
359 "Failed to cleanup Sandbox attachment aliases"
360 );
361 if stop_error.is_none() {
362 stop_error = Some(error);
363 }
364 false
365 }
366 };
367
368 let socket_dir = self.socket_dir();
369 #[cfg(target_os = "linux")]
375 crate::network::terminate_passt(&socket_dir);
376
377 if let Err(e) = self.rootfs_provider.cleanup(&box_dir, preserve_rootfs) {
379 tracing::warn!(
380 box_id = %self.box_id,
381 error = %e,
382 "Failed to cleanup rootfs provider"
383 );
384 }
385
386 if let Err(e) = std::fs::remove_dir_all(&socket_dir) {
387 tracing::debug!(
388 box_id = %self.box_id,
389 path = %socket_dir.display(),
390 error = %e,
391 "Failed to cleanup VM socket directory"
392 );
393 }
394
395 if !preserve_rootfs && mount_aliases_clean {
402 match std::fs::remove_dir_all(&box_dir) {
403 Ok(()) => {}
404 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
405 Err(e) => {
406 tracing::warn!(
407 box_id = %self.box_id,
408 path = %box_dir.display(),
409 error = %e,
410 "Failed to remove box directory on destroy"
411 );
412 }
413 }
414 }
415
416 if let Some(ref prom) = self.prom {
418 prom.vm_destroyed_total.inc();
419 prom.vm_count.with_label_values(&["ready"]).dec();
420 prom.remove_vm_resource_metrics(&self.box_id);
421 }
422
423 self.event_emitter.emit(BoxEvent::empty("box.stopped"));
425
426 match stop_error {
429 Some(e) => Err(e),
430 None => Ok(()),
431 }
432 }
433
434 #[cfg(unix)]
435 async fn deliver_guest_stop_signal(&self, signal: i32) -> bool {
436 let socket_path = self
437 .exec_socket_path
438 .as_deref()
439 .or_else(|| self.exec_client.as_ref().map(ExecClient::socket_path));
440 let Some(socket_path) = socket_path else {
441 return false;
442 };
443 let client = ExecClient::for_socket(socket_path);
444 match tokio::time::timeout(GUEST_STOP_DELIVERY_TIMEOUT, client.signal_main(signal)).await {
445 Ok(Ok(true)) => {
446 tracing::debug!(
447 box_id = %self.box_id,
448 signal,
449 socket_path = %socket_path.display(),
450 "Delivered stop signal to the workload through guest control"
451 );
452 true
453 }
454 Ok(Ok(false)) => {
455 tracing::warn!(
456 box_id = %self.box_id,
457 signal,
458 socket_path = %socket_path.display(),
459 "Guest did not acknowledge the workload stop signal"
460 );
461 false
462 }
463 Ok(Err(error)) => {
464 tracing::warn!(
465 box_id = %self.box_id,
466 signal,
467 socket_path = %socket_path.display(),
468 %error,
469 "Failed to deliver the workload stop signal through guest control"
470 );
471 false
472 }
473 Err(_) => {
474 tracing::warn!(
475 box_id = %self.box_id,
476 signal,
477 socket_path = %socket_path.display(),
478 "Timed out delivering the workload stop signal through guest control"
479 );
480 false
481 }
482 }
483 }
484
485 #[cfg(unix)]
486 async fn deliver_rootfs_maintenance_shutdown(&self) -> bool {
487 let socket_path = self
488 .exec_socket_path
489 .as_deref()
490 .or_else(|| self.exec_client.as_ref().map(ExecClient::socket_path));
491 let Some(socket_path) = socket_path else {
492 return false;
493 };
494 let client = ExecClient::for_socket(socket_path);
495 match tokio::time::timeout(
496 GUEST_STOP_DELIVERY_TIMEOUT,
497 client.shutdown_rootfs_maintenance(),
498 )
499 .await
500 {
501 Ok(Ok(true)) => {
502 tracing::debug!(
503 box_id = %self.box_id,
504 socket_path = %socket_path.display(),
505 "Requested clean rootfs maintenance guest shutdown"
506 );
507 true
508 }
509 Ok(Ok(false)) | Ok(Err(_)) | Err(_) => {
510 tracing::warn!(
511 box_id = %self.box_id,
512 socket_path = %socket_path.display(),
513 "Rootfs maintenance guest did not acknowledge shutdown"
514 );
515 false
516 }
517 }
518 }
519
520 pub async fn set_busy(&self) -> Result<()> {
522 let mut state = self.state.write().await;
523
524 if *state != BoxState::Ready {
525 return Err(BoxError::StateError("VM not ready".to_string()));
526 }
527
528 *state = BoxState::Busy;
529 Ok(())
530 }
531
532 pub async fn set_ready(&self) -> Result<()> {
534 let mut state = self.state.write().await;
535
536 if *state != BoxState::Busy && *state != BoxState::Compacting {
537 return Err(BoxError::StateError("Invalid state transition".to_string()));
538 }
539
540 *state = BoxState::Ready;
541 Ok(())
542 }
543
544 pub async fn set_compacting(&self) -> Result<()> {
546 let mut state = self.state.write().await;
547
548 if *state != BoxState::Busy {
549 return Err(BoxError::StateError("VM not busy".to_string()));
550 }
551
552 *state = BoxState::Compacting;
553 Ok(())
554 }
555
556 #[cfg(unix)]
560 pub async fn pause(&self) -> Result<()> {
561 let state = self.state.read().await;
562 match *state {
563 BoxState::Ready | BoxState::Busy | BoxState::Compacting => {}
564 BoxState::Created => {
565 return Err(BoxError::StateError("VM not yet booted".to_string()));
566 }
567 BoxState::Stopped => {
568 return Err(BoxError::StateError("VM is stopped".to_string()));
569 }
570 }
571 drop(state);
572
573 if self
574 .resolved_execution_plan
575 .as_ref()
576 .is_some_and(|plan| plan.backend.is_sandbox())
577 || self.config.isolation.is_sandbox()
578 {
579 return Err(BoxError::StateError(
580 "Pause is not supported by the Sandbox backend yet".to_string(),
581 ));
582 }
583
584 if let Some(pid) = self.pid().await {
585 let ret = unsafe { libc::kill(pid as i32, libc::SIGSTOP) };
587 if ret != 0 {
588 let err = std::io::Error::last_os_error();
589 return Err(BoxError::ExecError(format!(
590 "Failed to send SIGSTOP to pid {}: {}",
591 pid, err
592 )));
593 }
594 tracing::info!(box_id = %self.box_id, pid, "VM paused");
595 Ok(())
596 } else {
597 Err(BoxError::StateError(
598 "VM has no running process".to_string(),
599 ))
600 }
601 }
602
603 #[cfg(unix)]
607 pub async fn resume(&self) -> Result<()> {
608 if self
609 .resolved_execution_plan
610 .as_ref()
611 .is_some_and(|plan| plan.backend.is_sandbox())
612 || self.config.isolation.is_sandbox()
613 {
614 return Err(BoxError::StateError(
615 "Resume is not supported by the Sandbox backend yet".to_string(),
616 ));
617 }
618 if let Some(pid) = self.pid().await {
619 let ret = unsafe { libc::kill(pid as i32, libc::SIGCONT) };
621 if ret != 0 {
622 let err = std::io::Error::last_os_error();
623 return Err(BoxError::ExecError(format!(
624 "Failed to send SIGCONT to pid {}: {}",
625 pid, err
626 )));
627 }
628 tracing::info!(box_id = %self.box_id, pid, "VM resumed");
629 Ok(())
630 } else {
631 Err(BoxError::StateError(
632 "VM has no running process".to_string(),
633 ))
634 }
635 }
636
637 #[cfg(windows)]
639 pub async fn pause(&self) -> Result<()> {
640 Err(BoxError::StateError(
641 "VM pause is not yet supported on Windows".to_string(),
642 ))
643 }
644
645 #[cfg(windows)]
647 pub async fn resume(&self) -> Result<()> {
648 Err(BoxError::StateError(
649 "VM resume is not yet supported on Windows".to_string(),
650 ))
651 }
652
653 pub async fn health_check(&self) -> Result<bool> {
655 let state = self.state.read().await;
656
657 match *state {
658 BoxState::Ready | BoxState::Busy | BoxState::Compacting => {
659 if let Some(ref handler) = *self.handler.read().await {
661 Ok(handler.is_running())
662 } else {
663 Ok(false)
664 }
665 }
666 _ => Ok(false),
667 }
668 }
669
670 pub async fn metrics(&self) -> Option<crate::vmm::VmMetrics> {
672 let vm_metrics = self
673 .handler
674 .read()
675 .await
676 .as_ref()
677 .map(|handler| handler.metrics())?;
678
679 if let Some(ref prom) = self.prom {
681 prom.vm_cpu_percent
682 .with_label_values(&[&self.box_id])
683 .set(vm_metrics.cpu_percent.unwrap_or(0.0) as f64);
684 prom.vm_memory_bytes
685 .with_label_values(&[&self.box_id])
686 .set(vm_metrics.memory_bytes.unwrap_or(0) as f64);
687 }
688
689 Some(vm_metrics)
690 }
691
692 pub async fn pid(&self) -> Option<u32> {
694 self.handler
695 .read()
696 .await
697 .as_ref()
698 .map(|handler| handler.pid())
699 }
700
701 #[cfg(unix)]
703 pub fn tee(&self) -> Option<&dyn TeeExtension> {
704 self.tee.as_deref()
705 }
706
707 #[cfg(unix)]
709 pub fn require_tee(&self) -> Result<&dyn TeeExtension> {
710 self.tee.as_deref().ok_or_else(|| {
711 BoxError::AttestationError("TEE is not configured for this box".to_string())
712 })
713 }
714
715 #[cfg(unix)]
723 pub async fn update_resources(
724 &mut self,
725 update: &crate::resize::ResourceUpdate,
726 ) -> Result<crate::resize::ResizeResult> {
727 crate::resize::validate_update(update)?;
728
729 let mut result = crate::resize::ResizeResult {
730 applied: Vec::new(),
731 rejected: Vec::new(),
732 };
733
734 if !update.has_tier2_changes() {
735 return Ok(result);
736 }
737
738 let sandbox = self
739 .resolved_execution_plan
740 .as_ref()
741 .is_some_and(|plan| plan.backend.is_sandbox())
742 || self.config.isolation.is_sandbox();
743 if sandbox {
744 let mut next_config = self.config.clone();
745 update.apply_to_config(&mut next_config);
746 let runtime_config = next_config.clone();
747 let box_dir = self.home_dir.join("boxes").join(&self.box_id);
748 let box_id = self.box_id.clone();
749 tokio::task::spawn_blocking(move || {
750 crate::sandbox::update_recorded_resources(&box_dir, &box_id, &runtime_config)
751 })
752 .await
753 .map_err(|error| {
754 BoxError::StateError(format!(
755 "A3S OCI resource update worker failed for {}: {error}",
756 self.box_id
757 ))
758 })??;
759 self.config = next_config;
760 result.applied = update
761 .tier2_change_names()
762 .into_iter()
763 .map(str::to_string)
764 .collect();
765 return Ok(result);
766 }
767
768 let commands = update.build_microvm_cgroup_commands();
770 for cmd_str in &commands {
771 let shell_cmd = vec!["sh".to_string(), "-c".to_string(), cmd_str.clone()];
772
773 match self.exec_command(shell_cmd, 5_000_000_000).await {
774 Ok(output) if output.exit_code == 0 => {
775 result.applied.push(cmd_str.clone());
776 }
777 Ok(output) => {
778 let stderr = String::from_utf8_lossy(&output.stderr);
779 let reason = if stderr.trim().is_empty() {
780 format!("exit code {}", output.exit_code)
781 } else {
782 stderr.trim().to_string()
783 };
784 tracing::warn!(
785 box_id = %self.box_id,
786 cmd = %cmd_str,
787 exit_code = output.exit_code,
788 stderr = %stderr,
789 "Cgroup update failed inside guest"
790 );
791 result.rejected.push((cmd_str.clone(), reason));
792 }
793 Err(e) => {
794 tracing::warn!(
795 box_id = %self.box_id,
796 cmd = %cmd_str,
797 error = %e,
798 "Failed to exec cgroup update in guest"
799 );
800 result.rejected.push((cmd_str.clone(), e.to_string()));
801 }
802 }
803 }
804
805 Ok(result)
806 }
807}