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