Skip to main content

underhill_core/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! This module implements the interactive control process and the entry point
5//! for the underhill environment.
6
7#![cfg(target_os = "linux")]
8#![expect(missing_docs)]
9#![forbid(unsafe_code)]
10
11mod diag;
12mod dispatch;
13mod emuplat;
14mod get_tracing;
15mod hibernate;
16mod inspect_internal;
17mod inspect_proc;
18mod livedump;
19mod loader;
20#[cfg(feature = "product_policy")]
21mod measured_product_policy;
22mod nvme_manager;
23mod options;
24mod reference_time;
25mod servicing;
26mod threadpool_vm_task_backend;
27mod vmbus_relay_unit;
28mod vmgs_logger;
29mod vp;
30mod vpci;
31mod worker;
32mod wrapped_partition;
33
34// `pub` so that the missing_docs warning fires for options without
35// documentation.
36pub use options::Options;
37
38use crate::diag::DiagWorker;
39use crate::dispatch::UhVmRpc;
40use crate::worker::UnderhillEnvCfg;
41use crate::worker::UnderhillRemoteConsoleCfg;
42use crate::worker::UnderhillVmWorker;
43use crate::worker::UnderhillWorkerParameters;
44use anyhow::Context;
45use bootloader_fdt_parser::BootTimes;
46use cvm_tracing::CVM_ALLOWED;
47use framebuffer::FRAMEBUFFER_SIZE;
48use framebuffer::FramebufferAccess;
49use futures::StreamExt;
50use futures_concurrency::stream::Merge;
51use get_tracing::init_tracing;
52use get_tracing::init_tracing_backend;
53use inspect::Inspect;
54use inspect::SensitivityLevel;
55use mesh::CancelContext;
56use mesh::CancelReason;
57use mesh::MeshPayload;
58use mesh::error::RemoteError;
59use mesh::rpc::Rpc;
60use mesh::rpc::RpcSend;
61use mesh_process::Mesh;
62use mesh_process::ProcessConfig;
63use mesh_process::try_run_mesh_host;
64use mesh_tracing::RemoteTracer;
65use mesh_tracing::TracingBackend;
66use mesh_worker::RegisteredWorkers;
67use mesh_worker::WorkerEvent;
68use mesh_worker::WorkerHandle;
69use mesh_worker::WorkerHost;
70use mesh_worker::WorkerHostRunner;
71use mesh_worker::launch_local_worker;
72use mesh_worker::register_workers;
73use pal_async::DefaultDriver;
74use pal_async::DefaultPool;
75use pal_async::task::Spawn;
76#[cfg(feature = "profiler")]
77use profiler_worker::ProfilerWorker;
78#[cfg(feature = "profiler")]
79use profiler_worker::ProfilerWorkerParameters;
80use std::time::Duration;
81use vmsocket::VmAddress;
82use vmsocket::VmListener;
83use vnc_worker_defs::VncParameters;
84
85fn new_underhill_remote_console_cfg(
86    framebuffer_gpa_base: Option<u64>,
87) -> anyhow::Result<(
88    UnderhillRemoteConsoleCfg,
89    Option<FramebufferAccess>,
90    Option<mesh::Receiver<Vec<video_core::DirtyRect>>>,
91)> {
92    if let Some(framebuffer_gpa_base) = framebuffer_gpa_base {
93        // Underhill accesses the framebuffer by using /dev/mshv_vtl_low to read
94        // from a second mapping placed after the end of RAM at a static
95        // location specified by the host.
96        //
97        // Open the file directly rather than use the `hcl` crate to avoid
98        // leaking `hcl` stuff into this crate.
99        //
100        // FUTURE: use an approach that doesn't require double mapping the
101        // framebuffer from the host.
102        let gpa_fd = fs_err::OpenOptions::new()
103            .read(true)
104            .write(true)
105            .open("/dev/mshv_vtl_low")
106            .context("failed to open gpa device")?;
107
108        let vram = sparse_mmap::new_mappable_from_file(gpa_fd.file(), true, false)?;
109        let (fb, fba) = framebuffer::framebuffer(vram, FRAMEBUFFER_SIZE, framebuffer_gpa_base)
110            .context("allocating framebuffer")?;
111        tracing::debug!("framebuffer_gpa_base: {:#x}", framebuffer_gpa_base);
112
113        let (dirt_send, dirt_recv) = mesh::channel();
114
115        Ok((
116            UnderhillRemoteConsoleCfg {
117                synth_keyboard: true,
118                synth_mouse: true,
119                synth_video: true,
120                input: mesh::Receiver::new(),
121                framebuffer: Some(fb),
122                dirt_send: Some(dirt_send),
123            },
124            Some(fba),
125            Some(dirt_recv),
126        ))
127    } else {
128        Ok((
129            UnderhillRemoteConsoleCfg {
130                synth_keyboard: false,
131                synth_mouse: false,
132                synth_video: false,
133                input: mesh::Receiver::new(),
134                framebuffer: None,
135                dirt_send: None,
136            },
137            None,
138            None,
139        ))
140    }
141}
142
143pub fn main() -> anyhow::Result<()> {
144    // Install a panic hook to prefix the current async task name before the
145    // standard panic output.
146    install_task_name_panic_hook();
147
148    if let Some(path) = std::env::var_os("OPENVMM_WRITE_SAVED_STATE_PROTO") {
149        if cfg!(debug_assertions) {
150            mesh::payload::protofile::DescriptorWriter::new(
151                vmcore::save_restore::saved_state_roots(),
152            )
153            .write_to_path(path)
154            .context("failed to write protobuf descriptors")?;
155            return Ok(());
156        } else {
157            // The generated code for this is too large for release builds.
158            anyhow::bail!(".proto output only supported in debug builds");
159        }
160    }
161
162    // FUTURE: create and use the affinitized threadpool here.
163    let (_, tracing_driver) = DefaultPool::spawn_on_thread("tracing");
164
165    // Try to run as a worker host, sending a remote tracer that will forward
166    // tracing events back to the initial process for logging to the host. See
167    // [`get_tracing`] doc comments for more details.
168    //
169    // On success the worker runs to completion and then exits the process (does
170    // not return). Any worker host setup errors are return and bubbled up.
171    try_run_mesh_host("underhill", {
172        let tracing_driver = tracing_driver.clone();
173        async |params: MeshHostParams| {
174            if let Some(remote_tracer) = params.tracer {
175                init_tracing(tracing_driver, remote_tracer).context("failed to init tracing")?;
176            }
177            params.runner.run(RegisteredWorkers).await;
178            Ok(())
179        }
180    })?;
181
182    // Initialize the tracing backend used by this and all subprocesses.
183    let mut tracing = init_tracing_backend(tracing_driver.clone())?;
184    // Initialize tracing from the backend.
185    init_tracing(tracing_driver, tracing.tracer()).context("failed to init tracing")?;
186    DefaultPool::run_with(|driver| do_main(driver, tracing))
187}
188
189fn install_task_name_panic_hook() {
190    use std::io::Write;
191
192    let panic_hook = std::panic::take_hook();
193    std::panic::set_hook(Box::new(move |info| {
194        pal_async::task::with_current_task_metadata(|metadata| {
195            if let Some(metadata) = metadata {
196                let _ = write!(std::io::stderr(), "task '{}', ", metadata.name());
197            }
198        });
199        // This will proceed with writing "thread ... panicked at ..."
200        panic_hook(info);
201    }));
202}
203
204async fn do_main(driver: DefaultDriver, mut tracing: TracingBackend) -> anyhow::Result<()> {
205    let opt = Options::parse(Vec::new(), Vec::new())?;
206
207    let crate_name = build_info::get().crate_name();
208    let crate_revision = build_info::get().scm_revision();
209    let openhcl_version = build_info::get().openhcl_version();
210    tracing::info!(
211        CVM_ALLOWED,
212        ?crate_name,
213        ?crate_revision,
214        ?openhcl_version,
215        "VMM process"
216    );
217    log_boot_times().context("failure logging boot times")?;
218
219    // Write the current pid to a file.
220    if let Some(pid_path) = &opt.pid {
221        std::fs::write(pid_path, std::process::id().to_string())
222            .with_context(|| format!("failed to write pid to {}", pid_path.display()))?;
223    }
224
225    let mesh = Mesh::new("underhill".to_string()).context("failed to create mesh")?;
226
227    let r = run_control(driver, &mesh, opt, &mut tracing).await;
228    if let Err(err) = &r {
229        tracing::error!(
230            CVM_ALLOWED,
231            error = err.as_ref() as &dyn std::error::Error,
232            "VM failure"
233        );
234    }
235
236    // Wait a few seconds for child processes to terminate and tracing to finish.
237    CancelContext::new()
238        .with_timeout(Duration::from_secs(10))
239        .until_cancelled(async {
240            mesh.shutdown().await;
241            tracing.shutdown().await;
242        })
243        .await
244        .ok();
245
246    r
247}
248
249fn log_boot_times() -> anyhow::Result<()> {
250    fn diff(start: Option<u64>, end: Option<u64>) -> Option<tracing::field::DebugValue<Duration>> {
251        use reference_time::ReferenceTime;
252        Some(tracing::field::debug(
253            ReferenceTime::new(end?).since(ReferenceTime::new(start?))?,
254        ))
255    }
256
257    // Read boot times provided by the bootloader.
258    let BootTimes {
259        start,
260        end,
261        sidecar_start,
262        sidecar_end,
263    } = BootTimes::new().context("failed to parse boot times")?;
264    tracing::info!(
265        CVM_ALLOWED,
266        start,
267        end,
268        sidecar_start,
269        sidecar_end,
270        elapsed = diff(start, end),
271        sidecar_elapsed = diff(sidecar_start, sidecar_end),
272        "boot loader times"
273    );
274    Ok(())
275}
276
277struct DiagState {
278    _worker: WorkerHandle,
279    request_recv: mesh::Receiver<diag_server::DiagRequest>,
280}
281
282impl DiagState {
283    async fn new() -> anyhow::Result<Self> {
284        // Start the diagnostics worker immediately.
285        let (request_send, request_recv) = mesh::channel();
286        let worker = launch_local_worker::<DiagWorker>(diag::DiagWorkerParameters { request_send })
287            .await
288            .context("failed to launch diagnostics worker")?;
289        Ok(Self {
290            _worker: worker,
291            request_recv,
292        })
293    }
294}
295
296#[derive(Inspect)]
297struct Workers {
298    #[inspect(safe)]
299    vm: WorkerHandle,
300    #[inspect(skip)]
301    vm_rpc: mesh::Sender<UhVmRpc>,
302    vnc: Option<WorkerHandle>,
303    #[cfg(feature = "gdb")]
304    gdb: Option<WorkerHandle>,
305}
306
307#[derive(MeshPayload)]
308struct MeshHostParams {
309    tracer: Option<RemoteTracer>,
310    runner: WorkerHostRunner,
311}
312
313async fn launch_mesh_host(
314    mesh: &Mesh,
315    name: &str,
316    tracer: Option<RemoteTracer>,
317) -> anyhow::Result<WorkerHost> {
318    let (host, runner) = mesh_worker::worker_host();
319    mesh.launch_host(ProcessConfig::new(name), MeshHostParams { tracer, runner })
320        .await?;
321    Ok(host)
322}
323
324async fn launch_workers(
325    mesh: &Mesh,
326    tracing: &mut TracingBackend,
327    control_send: mesh::Sender<ControlRequest>,
328    opt: Options,
329) -> anyhow::Result<Workers> {
330    let env_cfg = UnderhillEnvCfg {
331        vmbus_max_version: opt.vmbus_max_version,
332        vmbus_enable_mnf: opt.vmbus_enable_mnf,
333        vmbus_force_confidential_external_memory: opt.vmbus_force_confidential_external_memory,
334        vmbus_force_gpa_pinning: opt.vmbus_force_gpa_pinning,
335        vmbus_channel_unstick_delay: (opt.vmbus_channel_unstick_delay_ms != 0)
336            .then(|| Duration::from_millis(opt.vmbus_channel_unstick_delay_ms)),
337        cmdline_append: opt.cmdline_append.clone(),
338        reformat_vmgs: opt.reformat_vmgs,
339        vtl0_starts_paused: opt.vtl0_starts_paused,
340        emulated_serial_wait_for_rts: opt.serial_wait_for_rts,
341        force_load_vtl0_image: opt.force_load_vtl0_image,
342        nvme_vfio: opt.nvme_vfio,
343        halt_on_guest_halt: opt.halt_on_guest_halt,
344        no_sidecar_hotplug: opt.no_sidecar_hotplug,
345        gdbstub: opt.gdbstub,
346        hide_isolation: opt.hide_isolation,
347        nvme_keep_alive: opt.nvme_keep_alive,
348        mana_keep_alive: opt.mana_keep_alive,
349        nvme_always_flr: opt.nvme_always_flr,
350        test_configuration: opt.test_configuration,
351        disable_uefi_frontpage: opt.disable_uefi_frontpage,
352        default_boot_always_attempt: opt.default_boot_always_attempt,
353        guest_state_lifetime: opt.guest_state_lifetime,
354        guest_state_encryption_policy: opt.guest_state_encryption_policy,
355        hardware_sealing_policy: opt.hardware_sealing_policy,
356        efi_diagnostics_log_level: opt.efi_diagnostics_log_level,
357        efi_diagnostics_rate_limit: opt.efi_diagnostics_rate_limit,
358        strict_encryption_policy: opt.strict_encryption_policy,
359        attempt_ak_cert_callback: opt.attempt_ak_cert_callback,
360        enable_vpci_relay: opt.enable_vpci_relay,
361        disable_proxy_redirect: opt.disable_proxy_redirect,
362        disable_lower_vtl_timer_virt: opt.disable_lower_vtl_timer_virt,
363        config_timeout_in_seconds: opt.config_timeout_in_seconds,
364        servicing_timeout_dump_collection_in_ms: opt.servicing_timeout_dump_collection_in_ms,
365    };
366
367    let (mut remote_console_cfg, framebuffer_access, dirty_rect_recv) =
368        new_underhill_remote_console_cfg(opt.framebuffer_gpa_base)?;
369
370    let mut vnc_worker = None;
371    if let Some(framebuffer) = framebuffer_access {
372        let listener = VmListener::bind(VmAddress::vsock_any(opt.vnc_port))
373            .context("failed to bind socket")?;
374
375        let input_send = remote_console_cfg.input.sender();
376
377        let vnc_host = launch_mesh_host(mesh, "vnc", Some(tracing.tracer()))
378            .await
379            .context("spawning vnc process failed")?;
380
381        vnc_worker = Some(
382            vnc_host
383                .launch_worker(
384                    vnc_worker_defs::VNC_WORKER_VMSOCKET,
385                    VncParameters {
386                        listener,
387                        framebuffer,
388                        input_send,
389                        dirty_recv: dirty_rect_recv,
390                        max_clients: 16,
391                        evict_oldest: false,
392                    },
393                )
394                .await?,
395        )
396    }
397
398    #[cfg(feature = "gdb")]
399    let mut gdbstub_worker = None;
400    #[cfg_attr(not(feature = "gdb"), expect(unused_mut))]
401    let mut debugger_rpc = None;
402    #[cfg(feature = "gdb")]
403    if opt.gdbstub {
404        let listener = VmListener::bind(VmAddress::vsock_any(opt.gdbstub_port))
405            .context("failed to bind socket")?;
406
407        let gdb_host = launch_mesh_host(mesh, "gdb", Some(tracing.tracer()))
408            .await
409            .context("failed to spawn gdb host process")?;
410
411        // Get the VP count of this machine. It's too early to read it directly
412        // from IGVM parameters, but the kernel already has the IGVM parsed VP
413        // count via the boot loader anyways.
414        let vp_count =
415            pal::unix::affinity::max_present_cpu().context("failed to get max present cpu")? + 1;
416
417        let (send, recv) = mesh::channel();
418        debugger_rpc = Some(recv);
419        gdbstub_worker = Some(
420            gdb_host
421                .launch_worker(
422                    debug_worker_defs::DEBUGGER_VSOCK_WORKER,
423                    debug_worker_defs::DebuggerParameters {
424                        listener,
425                        req_chan: send,
426                        vp_count,
427                        target_arch: if cfg!(guest_arch = "x86_64") {
428                            debug_worker_defs::TargetArch::X86_64
429                        } else {
430                            debug_worker_defs::TargetArch::Aarch64
431                        },
432                    },
433                )
434                .await?,
435        );
436    }
437    let (vm_rpc, vm_rpc_rx) = mesh::channel();
438
439    // Spawn the worker in a separate process in case the diagnostics server (in
440    // this process) is used to run gdbserver against it, or in case it needs to
441    // be restarted.
442    let host = launch_mesh_host(mesh, "vm", Some(tracing.tracer()))
443        .await
444        .context("failed to launch worker process")?;
445
446    let vm_worker = host
447        .start_worker(
448            worker::UNDERHILL_WORKER,
449            UnderhillWorkerParameters {
450                env_cfg,
451                remote_console_cfg,
452                debugger_rpc,
453                vm_rpc: vm_rpc_rx,
454                control_send,
455            },
456        )
457        .context("failed to launch worker")?;
458
459    Ok(Workers {
460        vm: vm_worker,
461        vm_rpc,
462        vnc: vnc_worker,
463        #[cfg(feature = "gdb")]
464        gdb: gdbstub_worker,
465    })
466}
467
468/// State for inspect only.
469#[derive(Inspect)]
470enum ControlState {
471    WaitingForStart,
472    Starting,
473    Started,
474    Restarting,
475}
476
477#[derive(MeshPayload)]
478pub enum ControlRequest {
479    FlushLogs(Rpc<CancelContext, Result<(), CancelReason>>),
480    MakeWorker(Rpc<String, Result<WorkerHost, RemoteError>>),
481}
482
483async fn run_control(
484    driver: DefaultDriver,
485    mesh: &Mesh,
486    opt: Options,
487    mut tracing: &mut TracingBackend,
488) -> anyhow::Result<()> {
489    let (control_send, mut control_recv) = mesh::channel();
490    let mut control_send = Some(control_send);
491
492    if opt.signal_vtl0_started {
493        signal_vtl0_started(&driver)
494            .await
495            .context("failed to signal vtl0 started")?;
496    }
497
498    let mut diag = DiagState::new().await?;
499
500    let (diag_reinspect_send, mut diag_reinspect_recv) = mesh::channel();
501    #[cfg(feature = "profiler")]
502    let mut profiler_host = None;
503    let mut state;
504    let mut workers = if opt.wait_for_start {
505        state = ControlState::WaitingForStart;
506        None
507    } else {
508        state = ControlState::Starting;
509        let workers = launch_workers(mesh, tracing, control_send.take().unwrap(), opt)
510            .await
511            .context("failed to launch workers")?;
512        Some(workers)
513    };
514
515    enum Event {
516        Diag(diag_server::DiagRequest),
517        Worker(WorkerEvent),
518        Control(ControlRequest),
519    }
520
521    let mut restart_rpc = None;
522    #[cfg(feature = "mem-profile-tracing")]
523    let mut profiler = mem_profile_tracing::HeapProfiler::new();
524    loop {
525        let event = {
526            let mut stream = (
527                (&mut diag.request_recv).map(Event::Diag),
528                (&mut diag_reinspect_recv)
529                    .map(|req| Event::Diag(diag_server::DiagRequest::Inspect(req))),
530                (&mut control_recv).map(Event::Control),
531                futures::stream::select_all(workers.as_mut().map(|w| &mut w.vm)).map(Event::Worker),
532            )
533                .merge();
534
535            let Some(event) = stream.next().await else {
536                break;
537            };
538            event
539        };
540
541        match event {
542            Event::Diag(request) => {
543                match request {
544                    diag_server::DiagRequest::Start(rpc) => {
545                        rpc.handle_failable(async |params| {
546                            if workers.is_some() {
547                                Err(anyhow::anyhow!("workers have already been started"))?;
548                            }
549                            let new_opt = Options::parse(params.args, params.env)
550                                .context("failed to parse new options")?;
551
552                            workers = Some(
553                                launch_workers(
554                                    mesh,
555                                    tracing,
556                                    control_send.take().unwrap(),
557                                    new_opt,
558                                )
559                                .await?,
560                            );
561                            state = ControlState::Starting;
562                            anyhow::Ok(())
563                        })
564                        .await
565                    }
566                    diag_server::DiagRequest::Inspect(deferred) => deferred.respond(|resp| {
567                        resp.sensitivity_field("mesh", SensitivityLevel::Safe, mesh)
568                            .sensitivity_field_mut("trace", SensitivityLevel::Safe, &mut tracing)
569                            .sensitivity_field(
570                                "build_info",
571                                SensitivityLevel::Safe,
572                                build_info::get(),
573                            )
574                            .sensitivity_child(
575                                "proc",
576                                SensitivityLevel::Safe,
577                                inspect_proc::inspect_proc,
578                            )
579                            .sensitivity_field("control_state", SensitivityLevel::Safe, &state)
580                            // This node can not be renamed due to stability guarantees.
581                            // See the comment at the top of inspect_internal for more details.
582                            .sensitivity_child("uhdiag", SensitivityLevel::Safe, |req| {
583                                inspect_internal::inspect_internal_diagnostics(
584                                    req,
585                                    &diag_reinspect_send,
586                                    &driver,
587                                )
588                            });
589
590                        resp.merge(&workers);
591                    }),
592                    diag_server::DiagRequest::Crash(pid) => {
593                        mesh.crash(pid);
594                    }
595                    diag_server::DiagRequest::Restart(rpc) => {
596                        let Some(workers) = &mut workers else {
597                            rpc.complete(Err(RemoteError::new(anyhow::anyhow!(
598                                "worker has not been started yet"
599                            ))));
600                            continue;
601                        };
602
603                        let r = async {
604                            if restart_rpc.is_some() {
605                                anyhow::bail!("previous restart still in progress");
606                            }
607
608                            let host = launch_mesh_host(mesh, "vm", Some(tracing.tracer()))
609                                .await
610                                .context("failed to launch worker process")?;
611
612                            workers.vm.restart(&host);
613                            Ok(())
614                        }
615                        .await;
616
617                        if r.is_err() {
618                            rpc.complete(r.map_err(RemoteError::new));
619                        } else {
620                            state = ControlState::Restarting;
621                            restart_rpc = Some(rpc);
622                        }
623                    }
624                    diag_server::DiagRequest::Pause(rpc) => {
625                        let Some(workers) = &mut workers else {
626                            rpc.complete(Err(RemoteError::new(anyhow::anyhow!(
627                                "worker has not been started yet"
628                            ))));
629                            continue;
630                        };
631
632                        // create the req future output the spawn, so that
633                        // we don't need to clone + move vm_rpc.
634                        let req = workers.vm_rpc.call(UhVmRpc::Pause, ());
635
636                        // FUTURE: consider supporting cancellation
637                        driver
638                            .spawn("diag-pause", async move {
639                                let was_paused = req.await.expect("failed to pause VM");
640                                rpc.handle_failable_sync(|_| {
641                                    if !was_paused {
642                                        Err(anyhow::anyhow!("VM is already paused"))
643                                    } else {
644                                        Ok(())
645                                    }
646                                });
647                            })
648                            .detach();
649                    }
650                    diag_server::DiagRequest::PacketCapture(rpc) => {
651                        let Some(workers) = &mut workers else {
652                            rpc.complete(Err(RemoteError::new(anyhow::anyhow!(
653                                "worker has not been started yet"
654                            ))));
655                            continue;
656                        };
657
658                        workers.vm_rpc.send(UhVmRpc::PacketCapture(rpc));
659                    }
660                    #[cfg(feature = "mem-profile-tracing")]
661                    diag_server::DiagRequest::MemoryProfileTrace(rpc) => {
662                        rpc.handle_failable(async |pid| {
663                            if pid == std::process::id() as i32 {
664                                anyhow::Ok(profiler.capture_and_restart())
665                            } else {
666                                let Some(workers) = &mut workers else {
667                                    anyhow::bail!("workers have not been started yet");
668                                };
669
670                                let result = workers
671                                    .vm_rpc
672                                    .call(UhVmRpc::MemoryProfileTrace, pid)
673                                    .await
674                                    .context("failed to get memory profile from worker process")?;
675                                Ok(result?)
676                            }
677                        })
678                        .await
679                    }
680                    diag_server::DiagRequest::Resume(rpc) => {
681                        let Some(workers) = &mut workers else {
682                            rpc.complete(Err(RemoteError::new(anyhow::anyhow!(
683                                "worker has not been started yet"
684                            ))));
685                            continue;
686                        };
687
688                        let was_resumed = workers
689                            .vm_rpc
690                            .call(UhVmRpc::Resume, ())
691                            .await
692                            .context("failed to resumed VM")?;
693
694                        let was_halted = workers
695                            .vm_rpc
696                            .call(UhVmRpc::ClearHalt, ())
697                            .await
698                            .context("failed to clear halt from VPs")?;
699
700                        rpc.handle_sync(|_| {
701                            if was_resumed || was_halted {
702                                Ok(())
703                            } else {
704                                Err(RemoteError::new(anyhow::anyhow!("VM is currently running")))
705                            }
706                        });
707                    }
708                    diag_server::DiagRequest::Save(rpc) => {
709                        let Some(workers) = &mut workers else {
710                            rpc.complete(Err(RemoteError::new(anyhow::anyhow!(
711                                "worker has not been started yet"
712                            ))));
713                            continue;
714                        };
715
716                        workers.vm_rpc.send(UhVmRpc::Save(rpc));
717                    }
718                    #[cfg(feature = "profiler")]
719                    diag_server::DiagRequest::Profile(rpc) => {
720                        let (rpc_params, rpc_sender) = rpc.split();
721                        // Create profiler host if there is none created before
722                        if profiler_host.is_none() {
723                            match launch_mesh_host(mesh, "profiler", Some(tracing.tracer()))
724                                .await
725                                .context("failed to launch profiler host")
726                            {
727                                Ok(host) => {
728                                    profiler_host = Some(host);
729                                }
730                                Err(e) => {
731                                    rpc_sender.complete(Err(RemoteError::new(e)));
732                                    continue;
733                                }
734                            }
735                        }
736
737                        let profiling_duration = rpc_params.duration;
738                        let host = profiler_host.as_ref().unwrap();
739                        let mut profiler_worker;
740                        match host
741                            .launch_worker(
742                                profiler_worker::PROFILER_WORKER,
743                                ProfilerWorkerParameters {
744                                    profiler_request: rpc_params,
745                                },
746                            )
747                            .await
748                        {
749                            Ok(worker) => {
750                                profiler_worker = worker;
751                            }
752                            Err(e) => {
753                                rpc_sender.complete(Err(RemoteError::new(e)));
754                                continue;
755                            }
756                        }
757
758                        driver
759                            .spawn("profiler_worker", async move {
760                                let result = CancelContext::new()
761                                    .with_timeout(Duration::from_secs(profiling_duration + 30))
762                                    .until_cancelled(profiler_worker.join())
763                                    .await
764                                    .context("profiler worker cancelled")
765                                    .and_then(|result| result.context("profiler worker failed"))
766                                    .map_err(RemoteError::new);
767
768                                rpc_sender.complete(result);
769                            })
770                            .detach();
771                    }
772                }
773            }
774            Event::Worker(event) => match event {
775                WorkerEvent::Started => {
776                    if let Some(response) = restart_rpc.take() {
777                        tracing::info!(CVM_ALLOWED, "restart complete");
778                        response.complete(Ok(()));
779                    } else {
780                        tracing::info!(CVM_ALLOWED, "vm worker started");
781                    }
782                    state = ControlState::Started;
783                }
784                WorkerEvent::Stopped => {
785                    anyhow::bail!("worker unexpectedly stopped");
786                }
787                WorkerEvent::Failed(err) => {
788                    return Err(anyhow::Error::from(err)).context("vm worker failed");
789                }
790                WorkerEvent::RestartFailed(err) => {
791                    tracing::error!(
792                        CVM_ALLOWED,
793                        error = &err as &dyn std::error::Error,
794                        "restart failed"
795                    );
796                    restart_rpc.take().unwrap().complete(Err(err));
797                    state = ControlState::Started;
798                }
799            },
800            Event::Control(req) => match req {
801                ControlRequest::FlushLogs(rpc) => {
802                    rpc.handle(async |mut ctx| {
803                        tracing::info!(CVM_ALLOWED, "flushing logs");
804                        ctx.until_cancelled(tracing.flush()).await?;
805                        Ok(())
806                    })
807                    .await
808                }
809                ControlRequest::MakeWorker(rpc) => {
810                    rpc.handle_failable(async |name| {
811                        launch_mesh_host(mesh, &name, Some(tracing.tracer())).await
812                    })
813                    .await
814                }
815            },
816        }
817    }
818
819    Ok(())
820}
821
822async fn signal_vtl0_started(driver: &DefaultDriver) -> anyhow::Result<()> {
823    tracing::info!(CVM_ALLOWED, "signaling vtl0 started early");
824    let (client, task) = guest_emulation_transport::spawn_get_worker(driver.clone())
825        .await
826        .context("failed to spawn GET")?;
827    client.complete_start_vtl0(None).await;
828    // Disconnect the GET so that it can be reused.
829    drop(client);
830    task.await.unwrap();
831    tracing::info!(CVM_ALLOWED, "signaled vtl0 start");
832    Ok(())
833}
834
835// The "base" workers for Underhill. Other workers are defined in the
836// `underhill_resources` crate.
837//
838// FUTURE: split these workers into separate crates and move them to
839// `underhill_resources`, too.
840register_workers! {
841    UnderhillVmWorker,
842    DiagWorker,
843    #[cfg(feature = "profiler")]
844    ProfilerWorker,
845}