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 }
421
422 self.event_emitter.emit(BoxEvent::empty("box.stopped"));
424
425 match stop_error {
428 Some(e) => Err(e),
429 None => Ok(()),
430 }
431 }
432
433 #[cfg(unix)]
434 async fn deliver_guest_stop_signal(&self, signal: i32) -> bool {
435 let socket_path = self
436 .exec_socket_path
437 .as_deref()
438 .or_else(|| self.exec_client.as_ref().map(ExecClient::socket_path));
439 let Some(socket_path) = socket_path else {
440 return false;
441 };
442 let client = ExecClient::for_socket(socket_path);
443 match tokio::time::timeout(GUEST_STOP_DELIVERY_TIMEOUT, client.signal_main(signal)).await {
444 Ok(Ok(true)) => {
445 tracing::debug!(
446 box_id = %self.box_id,
447 signal,
448 socket_path = %socket_path.display(),
449 "Delivered stop signal to the workload through guest control"
450 );
451 true
452 }
453 Ok(Ok(false)) => {
454 tracing::warn!(
455 box_id = %self.box_id,
456 signal,
457 socket_path = %socket_path.display(),
458 "Guest did not acknowledge the workload stop signal"
459 );
460 false
461 }
462 Ok(Err(error)) => {
463 tracing::warn!(
464 box_id = %self.box_id,
465 signal,
466 socket_path = %socket_path.display(),
467 %error,
468 "Failed to deliver the workload stop signal through guest control"
469 );
470 false
471 }
472 Err(_) => {
473 tracing::warn!(
474 box_id = %self.box_id,
475 signal,
476 socket_path = %socket_path.display(),
477 "Timed out delivering the workload stop signal through guest control"
478 );
479 false
480 }
481 }
482 }
483
484 #[cfg(unix)]
485 async fn deliver_rootfs_maintenance_shutdown(&self) -> bool {
486 let socket_path = self
487 .exec_socket_path
488 .as_deref()
489 .or_else(|| self.exec_client.as_ref().map(ExecClient::socket_path));
490 let Some(socket_path) = socket_path else {
491 return false;
492 };
493 let client = ExecClient::for_socket(socket_path);
494 match tokio::time::timeout(
495 GUEST_STOP_DELIVERY_TIMEOUT,
496 client.shutdown_rootfs_maintenance(),
497 )
498 .await
499 {
500 Ok(Ok(true)) => {
501 tracing::debug!(
502 box_id = %self.box_id,
503 socket_path = %socket_path.display(),
504 "Requested clean rootfs maintenance guest shutdown"
505 );
506 true
507 }
508 Ok(Ok(false)) | Ok(Err(_)) | Err(_) => {
509 tracing::warn!(
510 box_id = %self.box_id,
511 socket_path = %socket_path.display(),
512 "Rootfs maintenance guest did not acknowledge shutdown"
513 );
514 false
515 }
516 }
517 }
518
519 pub async fn set_busy(&self) -> Result<()> {
521 let mut state = self.state.write().await;
522
523 if *state != BoxState::Ready {
524 return Err(BoxError::StateError("VM not ready".to_string()));
525 }
526
527 *state = BoxState::Busy;
528 Ok(())
529 }
530
531 pub async fn set_ready(&self) -> Result<()> {
533 let mut state = self.state.write().await;
534
535 if *state != BoxState::Busy && *state != BoxState::Compacting {
536 return Err(BoxError::StateError("Invalid state transition".to_string()));
537 }
538
539 *state = BoxState::Ready;
540 Ok(())
541 }
542
543 pub async fn set_compacting(&self) -> Result<()> {
545 let mut state = self.state.write().await;
546
547 if *state != BoxState::Busy {
548 return Err(BoxError::StateError("VM not busy".to_string()));
549 }
550
551 *state = BoxState::Compacting;
552 Ok(())
553 }
554
555 #[cfg(unix)]
559 pub async fn pause(&self) -> Result<()> {
560 let state = self.state.read().await;
561 match *state {
562 BoxState::Ready | BoxState::Busy | BoxState::Compacting => {}
563 BoxState::Created => {
564 return Err(BoxError::StateError("VM not yet booted".to_string()));
565 }
566 BoxState::Stopped => {
567 return Err(BoxError::StateError("VM is stopped".to_string()));
568 }
569 }
570 drop(state);
571
572 if self
573 .resolved_execution_plan
574 .as_ref()
575 .is_some_and(|plan| plan.backend.is_sandbox())
576 || self.config.isolation.is_sandbox()
577 {
578 return Err(BoxError::StateError(
579 "Pause is not supported by the Sandbox backend yet".to_string(),
580 ));
581 }
582
583 if let Some(pid) = self.pid().await {
584 let ret = unsafe { libc::kill(pid as i32, libc::SIGSTOP) };
586 if ret != 0 {
587 let err = std::io::Error::last_os_error();
588 return Err(BoxError::ExecError(format!(
589 "Failed to send SIGSTOP to pid {}: {}",
590 pid, err
591 )));
592 }
593 tracing::info!(box_id = %self.box_id, pid, "VM paused");
594 Ok(())
595 } else {
596 Err(BoxError::StateError(
597 "VM has no running process".to_string(),
598 ))
599 }
600 }
601
602 #[cfg(unix)]
606 pub async fn resume(&self) -> Result<()> {
607 if self
608 .resolved_execution_plan
609 .as_ref()
610 .is_some_and(|plan| plan.backend.is_sandbox())
611 || self.config.isolation.is_sandbox()
612 {
613 return Err(BoxError::StateError(
614 "Resume is not supported by the Sandbox backend yet".to_string(),
615 ));
616 }
617 if let Some(pid) = self.pid().await {
618 let ret = unsafe { libc::kill(pid as i32, libc::SIGCONT) };
620 if ret != 0 {
621 let err = std::io::Error::last_os_error();
622 return Err(BoxError::ExecError(format!(
623 "Failed to send SIGCONT to pid {}: {}",
624 pid, err
625 )));
626 }
627 tracing::info!(box_id = %self.box_id, pid, "VM resumed");
628 Ok(())
629 } else {
630 Err(BoxError::StateError(
631 "VM has no running process".to_string(),
632 ))
633 }
634 }
635
636 #[cfg(windows)]
638 pub async fn pause(&self) -> Result<()> {
639 Err(BoxError::StateError(
640 "VM pause is not yet supported on Windows".to_string(),
641 ))
642 }
643
644 #[cfg(windows)]
646 pub async fn resume(&self) -> Result<()> {
647 Err(BoxError::StateError(
648 "VM resume is not yet supported on Windows".to_string(),
649 ))
650 }
651
652 pub async fn health_check(&self) -> Result<bool> {
654 let state = self.state.read().await;
655
656 match *state {
657 BoxState::Ready | BoxState::Busy | BoxState::Compacting => {
658 if let Some(ref handler) = *self.handler.read().await {
660 Ok(handler.is_running())
661 } else {
662 Ok(false)
663 }
664 }
665 _ => Ok(false),
666 }
667 }
668
669 pub async fn metrics(&self) -> Option<crate::vmm::VmMetrics> {
671 let vm_metrics = self
672 .handler
673 .read()
674 .await
675 .as_ref()
676 .map(|handler| handler.metrics())?;
677
678 if let Some(ref prom) = self.prom {
680 prom.vm_cpu_percent
681 .with_label_values(&[&self.box_id])
682 .set(vm_metrics.cpu_percent.unwrap_or(0.0) as f64);
683 prom.vm_memory_bytes
684 .with_label_values(&[&self.box_id])
685 .set(vm_metrics.memory_bytes.unwrap_or(0) as f64);
686 }
687
688 Some(vm_metrics)
689 }
690
691 pub async fn pid(&self) -> Option<u32> {
693 self.handler
694 .read()
695 .await
696 .as_ref()
697 .map(|handler| handler.pid())
698 }
699
700 #[cfg(unix)]
702 pub fn tee(&self) -> Option<&dyn TeeExtension> {
703 self.tee.as_deref()
704 }
705
706 #[cfg(unix)]
708 pub fn require_tee(&self) -> Result<&dyn TeeExtension> {
709 self.tee.as_deref().ok_or_else(|| {
710 BoxError::AttestationError("TEE is not configured for this box".to_string())
711 })
712 }
713
714 #[cfg(unix)]
722 pub async fn update_resources(
723 &mut self,
724 update: &crate::resize::ResourceUpdate,
725 ) -> Result<crate::resize::ResizeResult> {
726 crate::resize::validate_update(update)?;
727
728 let mut result = crate::resize::ResizeResult {
729 applied: Vec::new(),
730 rejected: Vec::new(),
731 };
732
733 if !update.has_tier2_changes() {
734 return Ok(result);
735 }
736
737 let sandbox = self
738 .resolved_execution_plan
739 .as_ref()
740 .is_some_and(|plan| plan.backend.is_sandbox())
741 || self.config.isolation.is_sandbox();
742 if sandbox {
743 let mut next_config = self.config.clone();
744 update.apply_to_config(&mut next_config);
745 let runtime_config = next_config.clone();
746 let box_dir = self.home_dir.join("boxes").join(&self.box_id);
747 let box_id = self.box_id.clone();
748 tokio::task::spawn_blocking(move || {
749 crate::sandbox::update_recorded_resources(&box_dir, &box_id, &runtime_config)
750 })
751 .await
752 .map_err(|error| {
753 BoxError::StateError(format!(
754 "A3S OCI resource update worker failed for {}: {error}",
755 self.box_id
756 ))
757 })??;
758 self.config = next_config;
759 result.applied = update
760 .tier2_change_names()
761 .into_iter()
762 .map(str::to_string)
763 .collect();
764 return Ok(result);
765 }
766
767 let commands = update.build_microvm_cgroup_commands();
769 for cmd_str in &commands {
770 let shell_cmd = vec!["sh".to_string(), "-c".to_string(), cmd_str.clone()];
771
772 match self.exec_command(shell_cmd, 5_000_000_000).await {
773 Ok(output) if output.exit_code == 0 => {
774 result.applied.push(cmd_str.clone());
775 }
776 Ok(output) => {
777 let stderr = String::from_utf8_lossy(&output.stderr);
778 let reason = if stderr.trim().is_empty() {
779 format!("exit code {}", output.exit_code)
780 } else {
781 stderr.trim().to_string()
782 };
783 tracing::warn!(
784 box_id = %self.box_id,
785 cmd = %cmd_str,
786 exit_code = output.exit_code,
787 stderr = %stderr,
788 "Cgroup update failed inside guest"
789 );
790 result.rejected.push((cmd_str.clone(), reason));
791 }
792 Err(e) => {
793 tracing::warn!(
794 box_id = %self.box_id,
795 cmd = %cmd_str,
796 error = %e,
797 "Failed to exec cgroup update in guest"
798 );
799 result.rejected.push((cmd_str.clone(), e.to_string()));
800 }
801 }
802 }
803
804 Ok(result)
805 }
806}