Skip to main content

sidecar_client/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! The client interface to the sidecar kernel driver.
5
6#![cfg(target_os = "linux")]
7// UNSAFETY: Manually mapping memory for the sidecar kernel and calling ioctls.
8#![expect(unsafe_code)]
9
10use fs_err::os::unix::fs::OpenOptionsExt;
11use hvdef::HvError;
12use hvdef::HvMessage;
13use hvdef::HvRegisterName;
14use hvdef::HvRegisterValue;
15use hvdef::HvStatus;
16use hvdef::hypercall::HvInputVtl;
17use hvdef::hypercall::HvRegisterAssoc;
18use hvdef::hypercall::TranslateVirtualAddressExOutputX64;
19use pal_async::driver::PollImpl;
20use pal_async::driver::SpawnDriver;
21use pal_async::fd::PollFdReady;
22use pal_async::interest::InterestSlot;
23use pal_async::interest::PollEvents;
24use pal_async::task::Task;
25use parking_lot::Mutex;
26use sidecar_defs::CommandPage;
27use sidecar_defs::CpuContextX64;
28use sidecar_defs::GetSetVpRegisterRequest;
29use sidecar_defs::PAGE_SIZE;
30use sidecar_defs::RunVpResponse;
31use sidecar_defs::SidecarCommand;
32use sidecar_defs::TranslateGvaRequest;
33use sidecar_defs::TranslateGvaResponse;
34use std::fs::File;
35use std::future::poll_fn;
36use std::io::Read;
37use std::mem::MaybeUninit;
38use std::ops::Range;
39use std::os::fd::AsRawFd;
40use std::os::raw::c_void;
41use std::ptr::NonNull;
42use std::ptr::addr_of;
43use std::ptr::addr_of_mut;
44use std::sync::Arc;
45use std::sync::atomic::AtomicBool;
46use std::sync::atomic::Ordering::Acquire;
47use std::sync::atomic::Ordering::Release;
48use std::task::Poll;
49use std::task::Waker;
50use thiserror::Error;
51use zerocopy::FromBytes;
52use zerocopy::FromZeros;
53use zerocopy::Immutable;
54use zerocopy::IntoBytes;
55use zerocopy::KnownLayout;
56
57mod ioctl {
58    const BASE: u8 = 0xb8;
59    nix::ioctl_write_int_bad!(mshv_vtl_sidecar_start, nix::request_code_none!(BASE, 0xf0));
60    nix::ioctl_write_int_bad!(mshv_vtl_sidecar_stop, nix::request_code_none!(BASE, 0xf1));
61    nix::ioctl_write_int_bad!(mshv_vtl_sidecar_run, nix::request_code_none!(BASE, 0xf2));
62    nix::ioctl_read!(mshv_vtl_sidecar_info, BASE, 0xf3, SidecarInfo);
63
64    #[repr(C)]
65    pub(crate) struct SidecarInfo {
66        pub base_cpu: u32,
67        pub cpu_count: u32,
68        pub per_cpu_shmem: u32,
69    }
70}
71
72/// A sidecar client.
73///
74/// This is actually a client to multiple sidecar devices, since there is one
75/// per node. This is abstracted away for the caller.
76#[derive(Debug)]
77pub struct SidecarClient {
78    nodes: Vec<SidecarNode>,
79}
80
81#[derive(Debug)]
82struct SidecarNode {
83    mapping: Mapping,
84    per_cpu_shmem_size: usize,
85    cpus: Range<u32>,
86    _task: Task<()>,
87    state: Arc<SidecarClientState>,
88    in_use: Vec<AtomicBool>,
89}
90
91#[derive(Debug)]
92struct SidecarClientState {
93    file: File,
94    vps: Vec<Mutex<VpState>>,
95}
96
97#[derive(Debug)]
98enum VpState {
99    Stopped,
100    Running(Option<Waker>),
101    Finished,
102}
103
104#[derive(Debug)]
105struct Mapping(NonNull<c_void>, usize);
106
107// SAFETY: the underlying mapping can be accessed from any CPU.
108unsafe impl Send for Mapping {}
109// SAFETY: the underlying mapping can be accessed from any CPU.
110unsafe impl Sync for Mapping {}
111
112/// An error returned by [`SidecarClient::new`].
113#[derive(Debug, Error)]
114pub enum NewSidecarClientError {
115    /// IO failure interacting with the sidecar driver.
116    #[error("{operation} failed in sidecar driver")]
117    Io {
118        /// The IO operation.
119        operation: &'static str,
120        /// The error.
121        #[source]
122        err: std::io::Error,
123    },
124    /// An error from an IO driver.
125    #[error("driver error")]
126    Driver(#[source] std::io::Error),
127}
128
129impl SidecarClient {
130    /// Create a new sidecar client. Returns `None` if no sidecar devices are found.
131    ///
132    /// `driver(cpu)` returns the driver to use for polling the sidecar device
133    /// whose base CPU is `cpu`.
134    pub fn new<T: SpawnDriver>(
135        mut driver: impl FnMut(u32) -> T,
136    ) -> Result<Option<Self>, NewSidecarClientError> {
137        let mut nodes = Vec::new();
138        let mut expected_base = 0;
139        loop {
140            let node = match SidecarNode::new(&mut driver, nodes.len()) {
141                Ok(Some(node)) => node,
142                Ok(None) => {
143                    if nodes.is_empty() {
144                        // No sidecar devices could be found at all.
145                        return Ok(None);
146                    }
147                    // No more nodes.
148                    break;
149                }
150                Err(err) => return Err(err),
151            };
152            if node.cpus.start > expected_base {
153                tracing::info!(
154                    node = nodes.len(),
155                    gap_start = expected_base,
156                    gap_end = node.cpus.start,
157                    "sidecar node follows a gap; earlier node(s) skipped (no sidecar-started APs)"
158                );
159            }
160            expected_base = node.cpus.end;
161            nodes.push(node);
162        }
163        let layout: Vec<Range<u32>> = nodes.iter().map(|node| node.cpus.clone()).collect();
164        tracing::info!(
165            ?layout,
166            "sidecar client initialized with {} node(s)",
167            nodes.len()
168        );
169        Ok(Some(Self { nodes }))
170    }
171
172    /// Returns a sidecar VP accessor for the given CPU.
173    pub fn vp(&self, cpu: u32) -> SidecarVp<'_> {
174        self.nodes
175            .iter()
176            .find_map(|node| node.vp(cpu))
177            .expect("invalid cpu")
178    }
179
180    /// Returns the CPU index that manages the given VP.
181    pub fn base_cpu(&self, cpu: u32) -> u32 {
182        self.nodes
183            .iter()
184            .find_map(|node| node.cpus.contains(&cpu).then_some(node.cpus.start))
185            .expect("invalid cpu")
186    }
187}
188
189impl SidecarNode {
190    fn new<T: SpawnDriver>(
191        driver: &mut impl FnMut(u32) -> T,
192        node: usize,
193    ) -> Result<Option<Self>, NewSidecarClientError> {
194        let file = match fs_err::OpenOptions::new()
195            .read(true)
196            .write(true)
197            .custom_flags(libc::O_NONBLOCK)
198            .open(format!("/dev/mshv_vtl_sidecar{node}"))
199        {
200            Ok(file) => file,
201            Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
202            Err(err) => {
203                return Err(NewSidecarClientError::Io {
204                    operation: "open",
205                    err,
206                });
207            }
208        };
209
210        // SAFETY: calling the ioctl with a valid output pointer. The ioctl is
211        // guaranteed to initialize the output on success (but pre-zero it just to be safe).
212        let info = unsafe {
213            let mut info = MaybeUninit::zeroed();
214            ioctl::mshv_vtl_sidecar_info(file.as_raw_fd(), info.as_mut_ptr()).map_err(|err| {
215                NewSidecarClientError::Io {
216                    operation: "query info",
217                    err: err.into(),
218                }
219            })?;
220            info.assume_init()
221        };
222
223        let cpus = info.base_cpu..info.base_cpu + info.cpu_count;
224        let per_cpu_shmem_size = info.per_cpu_shmem as usize;
225        assert!(
226            per_cpu_shmem_size >= size_of::<VpSharedPages>(),
227            "invalid state size"
228        );
229
230        let mapping = {
231            let mapping_len = cpus.len() * per_cpu_shmem_size;
232            // SAFETY: creating a new mapping, which has no safety requirements.
233            let mapping = unsafe {
234                libc::mmap(
235                    std::ptr::null_mut(),
236                    mapping_len,
237                    libc::PROT_READ | libc::PROT_WRITE,
238                    libc::MAP_SHARED,
239                    file.as_raw_fd(),
240                    0,
241                )
242            };
243            if mapping == libc::MAP_FAILED {
244                return Err(NewSidecarClientError::Io {
245                    operation: "mmap",
246                    err: std::io::Error::last_os_error(),
247                });
248            }
249            Mapping(NonNull::new(mapping).unwrap(), mapping_len)
250        };
251
252        // Start the driver on the first CPU in the node.
253        let driver = driver(cpus.start);
254
255        let fd_ready = driver
256            .new_dyn_fd_ready(file.as_raw_fd())
257            .map_err(NewSidecarClientError::Driver)?;
258
259        let state = Arc::new(SidecarClientState {
260            file: file.into(),
261            vps: cpus.clone().map(|_| Mutex::new(VpState::Stopped)).collect(),
262        });
263
264        let task = driver.spawn(
265            "sidecar-wait",
266            sidecar_wait_loop(fd_ready, state.clone(), cpus.start),
267        );
268
269        tracing::debug!(
270            "sidecar node {node} started, cpus {}..={}",
271            cpus.start,
272            cpus.end - 1
273        );
274
275        Ok(Some(Self {
276            state,
277            per_cpu_shmem_size,
278            mapping,
279            in_use: cpus.clone().map(|_| AtomicBool::new(false)).collect(),
280            cpus,
281            _task: task,
282        }))
283    }
284
285    fn vp(&self, cpu: u32) -> Option<SidecarVp<'_>> {
286        if !self.cpus.contains(&cpu) {
287            return None;
288        }
289        let index = cpu - self.cpus.start;
290        assert!(
291            !self.in_use[index as usize].swap(true, Acquire),
292            "vp in use"
293        );
294        // SAFETY: the mapping is valid and the index is within the range of CPUs.
295        let shmem = unsafe {
296            self.mapping
297                .0
298                .as_ptr()
299                .byte_add(index as usize * self.per_cpu_shmem_size)
300        }
301        .cast();
302        Some(SidecarVp {
303            cpu: cpu as i32,
304            index: index as usize,
305            shmem: NonNull::new(shmem).unwrap(),
306            node: self,
307        })
308    }
309}
310
311async fn sidecar_wait_loop(
312    mut fd_ready: PollImpl<dyn PollFdReady>,
313    state: Arc<SidecarClientState>,
314    base_cpu: u32,
315) {
316    let err = loop {
317        poll_fn(|cx| fd_ready.poll_fd_ready(cx, InterestSlot::Read, PollEvents::IN)).await;
318        let mut cpu = 0u32;
319        let n = match (&state.file).read(cpu.as_mut_bytes()) {
320            Ok(n) => n,
321            Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
322                fd_ready.clear_fd_ready(InterestSlot::Read);
323                continue;
324            }
325            Err(err) => break err,
326        };
327        assert_eq!(n, 4, "unexpected read size");
328        tracing::trace!(cpu, "sidecar stop");
329        let index = cpu - base_cpu;
330        let VpState::Running(waker) =
331            std::mem::replace(&mut *state.vps[index as usize].lock(), VpState::Finished)
332        else {
333            panic!("cpu {cpu} stopped without start");
334        };
335        if let Some(waker) = waker {
336            waker.wake();
337        }
338    };
339    tracing::error!(
340        error = &err as &dyn std::error::Error,
341        "sidecar wait failed"
342    );
343}
344
345impl Drop for Mapping {
346    fn drop(&mut self) {
347        // SAFETY: the mapping is valid and the length is correct.
348        let r = unsafe { libc::munmap(self.0.as_ptr(), self.1) };
349        if r != 0 {
350            panic!("munmap failed: {}", std::io::Error::last_os_error());
351        }
352    }
353}
354
355/// An accessor for a sidecar VP.
356pub struct SidecarVp<'a> {
357    cpu: i32,
358    index: usize,
359    shmem: NonNull<VpSharedPages>,
360    node: &'a SidecarNode,
361}
362
363#[repr(C)]
364struct VpSharedPages {
365    command_page: CommandPage,
366    register_page: hvdef::HvX64RegisterPage,
367}
368
369const _: () = assert!(size_of::<VpSharedPages>().is_multiple_of(PAGE_SIZE));
370
371impl Drop for SidecarVp<'_> {
372    fn drop(&mut self) {
373        assert!(self.node.in_use[self.index].swap(false, Release));
374    }
375}
376
377/// An error from a sidecar operation.
378#[derive(Debug, Error)]
379pub enum SidecarError {
380    /// An IO error interacting with the sidecar driver.
381    #[error("driver error")]
382    Io(#[source] std::io::Error),
383    /// An error from the sidecar kernel.
384    #[error("sidecar error: {0}")]
385    Sidecar(String),
386    /// An error from the hypervisor.
387    #[error("hypervisor error")]
388    Hypervisor(#[source] HvError),
389}
390
391impl<'a> SidecarVp<'a> {
392    /// Runs the VP.
393    pub fn run(&mut self) -> Result<SidecarRun<'_, 'a>, SidecarError> {
394        tracing::trace!("run vp");
395        self.set_command::<_, u8>(SidecarCommand::RUN_VP, (), 0);
396        self.start_async()?;
397        Ok(SidecarRun {
398            vp: self,
399            waited: false,
400        })
401    }
402
403    /// Returns a pointer to the CPU context.
404    ///
405    /// This pointer is only valid for access while the VP is stopped.
406    pub fn cpu_context(&self) -> *mut CpuContextX64 {
407        // SAFETY: the command page pointer is valid so these pointer computations
408        // are also valid.
409        unsafe { addr_of_mut!((*self.shmem.as_ptr()).command_page.cpu_context) }
410    }
411
412    /// Returns a pointer to the intercept message from the hypervisor.
413    ///
414    /// This pointer is only valid for access while the VP is stopped.
415    pub fn intercept_message(&self) -> *const HvMessage {
416        // SAFETY: the command page pointer is valid so these pointer computations
417        // are also valid.
418        unsafe { addr_of!((*self.shmem.as_ptr()).command_page.intercept_message) }
419    }
420
421    /// Returns a pointer to the register page, mapped with the hypervisor.
422    ///
423    /// If the hypervisor does not support register pages, then the `is_valid`
424    /// field will be 0.
425    ///
426    /// This pointer is only valid for access while the VP is stopped.
427    pub fn register_page(&self) -> *mut hvdef::HvX64RegisterPage {
428        // SAFETY: the command page pointer is valid so these pointer computations
429        // are also valid.
430        unsafe { addr_of_mut!((*self.shmem.as_ptr()).register_page) }
431    }
432
433    /// Tests that the VP is running in the sidecar kernel.
434    pub fn test(&mut self) -> Result<(), SidecarError> {
435        tracing::trace!("test");
436        let () = self.dispatch_sync(SidecarCommand::NONE, ())?;
437        Ok(())
438    }
439
440    /// Gets a VP register by name.
441    pub fn get_vp_registers(
442        &mut self,
443        target_vtl: HvInputVtl,
444        names: &[HvRegisterName],
445        values: &mut [HvRegisterValue],
446    ) -> Result<(), SidecarError> {
447        tracing::trace!(count = names.len(), "get vp register");
448        for (names, values) in names
449            .chunks(sidecar_defs::MAX_GET_SET_VP_REGISTERS)
450            .zip(values.chunks_mut(sidecar_defs::MAX_GET_SET_VP_REGISTERS))
451        {
452            let buf = self.set_command(
453                SidecarCommand::GET_VP_REGISTERS,
454                GetSetVpRegisterRequest {
455                    count: names.len() as u16,
456                    target_vtl,
457                    rsvd: 0,
458                    status: HvStatus::SUCCESS,
459                    rsvd2: [0; 10],
460                    regs: [],
461                },
462                names.len(),
463            );
464            for (i, name) in names.iter().enumerate() {
465                buf[i] = HvRegisterAssoc {
466                    name: *name,
467                    pad: Default::default(),
468                    value: FromZeros::new_zeroed(),
469                };
470            }
471            self.run_sync()?;
472            let (&GetSetVpRegisterRequest { status, .. }, buf) =
473                self.command_result::<_, HvRegisterAssoc>(names.len())?;
474            status.result().map_err(SidecarError::Hypervisor)?;
475            for (i, value) in values.iter_mut().enumerate() {
476                *value = buf[i].value;
477            }
478        }
479        Ok(())
480    }
481
482    /// Sets a VP register by name.
483    pub fn set_vp_registers(
484        &mut self,
485        target_vtl: HvInputVtl,
486        regs: &[HvRegisterAssoc],
487    ) -> Result<(), SidecarError> {
488        tracing::trace!(count = regs.len(), "set vp register");
489        for regs in regs.chunks(sidecar_defs::MAX_GET_SET_VP_REGISTERS) {
490            let buf = self.set_command(
491                SidecarCommand::SET_VP_REGISTERS,
492                GetSetVpRegisterRequest {
493                    count: regs.len() as u16,
494                    target_vtl,
495                    rsvd: 0,
496                    status: HvStatus::SUCCESS,
497                    rsvd2: [0; 10],
498                    regs: [],
499                },
500                regs.len(),
501            );
502            buf.copy_from_slice(regs);
503            self.run_sync()?;
504            let &GetSetVpRegisterRequest { status, .. } = self.command_result::<_, u8>(0)?.0;
505            status.result().map_err(SidecarError::Hypervisor)?;
506        }
507        Ok(())
508    }
509
510    /// Issues a hypercall to translate a guest virtual address to a guest
511    /// physical address.
512    pub fn translate_gva(
513        &mut self,
514        gvn: u64,
515        control_flags: hvdef::hypercall::TranslateGvaControlFlagsX64,
516    ) -> Result<TranslateVirtualAddressExOutputX64, SidecarError> {
517        tracing::trace!("translate gva");
518        let &TranslateGvaResponse {
519            status,
520            rsvd: _,
521            output,
522        } = self.dispatch_sync(
523            SidecarCommand::TRANSLATE_GVA,
524            TranslateGvaRequest { gvn, control_flags },
525        )?;
526        status.result().map_err(SidecarError::Hypervisor)?;
527        Ok(output)
528    }
529
530    fn set_command<
531        T: IntoBytes + Immutable + KnownLayout,
532        S: IntoBytes + FromBytes + Immutable + KnownLayout,
533    >(
534        &mut self,
535        command: SidecarCommand,
536        input: T,
537        n: usize,
538    ) -> &mut [S] {
539        // SAFETY: no command is running, so the sidecar kernel will not
540        // concurrently modify the state page.
541        let shmem = unsafe { self.shmem.as_mut() };
542        shmem.command_page.command = command;
543        input
544            .write_to_prefix(shmem.command_page.request_data.as_mut_bytes())
545            .unwrap();
546        <[S]>::mut_from_prefix_with_elems(
547            &mut shmem.command_page.request_data.as_mut_bytes()[input.as_bytes().len()..],
548            n,
549        )
550        .unwrap()
551        .0
552    }
553
554    fn dispatch_sync<O: FromBytes + Immutable + KnownLayout>(
555        &mut self,
556        command: SidecarCommand,
557        input: impl IntoBytes + Immutable + KnownLayout,
558    ) -> Result<&O, SidecarError> {
559        self.set_command::<_, u8>(command, input, 0);
560        self.run_sync()?;
561        Ok(self.command_result::<_, u8>(0)?.0)
562    }
563
564    fn run_sync(&mut self) -> Result<(), SidecarError> {
565        // SAFETY: no safety requirements on this ioctl.
566        unsafe {
567            ioctl::mshv_vtl_sidecar_run(self.node.state.file.as_raw_fd(), self.cpu)
568                .map_err(|err| SidecarError::Io(err.into()))?;
569        }
570        Ok(())
571    }
572
573    fn start_async(&mut self) -> Result<(), SidecarError> {
574        let old = std::mem::replace(
575            &mut *self.node.state.vps[self.index].lock(),
576            VpState::Running(None),
577        );
578        assert!(matches!(old, VpState::Stopped));
579        // SAFETY: no safety requirements on this ioctl.
580        unsafe {
581            ioctl::mshv_vtl_sidecar_start(self.node.state.file.as_raw_fd(), self.cpu)
582                .map_err(|err| SidecarError::Io(err.into()))?;
583        }
584        Ok(())
585    }
586
587    fn stop_async(&mut self) {
588        // SAFETY: no safety requirements on this ioctl.
589        unsafe {
590            ioctl::mshv_vtl_sidecar_stop(self.node.state.file.as_raw_fd(), self.cpu)
591                .expect("failed to stop vp");
592        }
593    }
594
595    async fn wait_async(&mut self) {
596        poll_fn(|cx| {
597            let mut vp = self.node.state.vps[self.index].lock();
598            match &mut *vp {
599                VpState::Stopped => unreachable!(),
600                VpState::Running(waker) => {
601                    if waker.as_ref().is_none_or(|w| !cx.waker().will_wake(w)) {
602                        *waker = Some(cx.waker().clone());
603                    }
604                    Poll::Pending
605                }
606                VpState::Finished => {
607                    *vp = VpState::Stopped;
608                    Poll::Ready(())
609                }
610            }
611        })
612        .await
613    }
614
615    fn command_result<
616        O: FromBytes + Immutable + KnownLayout,
617        S: FromBytes + Immutable + KnownLayout,
618    >(
619        &mut self,
620        n: usize,
621    ) -> Result<(&O, &[S]), SidecarError> {
622        // SAFETY: the sidecar kernel will not concurrently modify the state
623        // page after the command has completed.
624        let shmem = unsafe { self.shmem.as_ref() };
625        if shmem.command_page.has_error != 0 {
626            let s = String::from_utf8_lossy(
627                &shmem.command_page.error.buf[..shmem.command_page.error.len as usize],
628            );
629            return Err(SidecarError::Sidecar(s.into_owned()));
630        }
631        let (output, slice) = shmem
632            .command_page
633            .request_data
634            .as_bytes()
635            .split_at(size_of::<O>());
636        let output = O::ref_from_bytes(output).unwrap();
637        let (slice, _) = <[S]>::ref_from_prefix_with_elems(slice, n).unwrap();
638        Ok((output, slice))
639    }
640}
641
642/// An object representing a running VP.
643///
644/// Panics if dropped without waiting for the VP to stop.
645pub struct SidecarRun<'a, 'b> {
646    vp: &'a mut SidecarVp<'b>,
647    waited: bool,
648}
649
650impl SidecarRun<'_, '_> {
651    /// Requests that the sidecar kernel stop the VP.
652    ///
653    /// You must still call `wait` after this to ensure the VP has stopped.
654    pub fn cancel(&mut self) {
655        if !self.waited {
656            self.vp.stop_async();
657        }
658    }
659
660    /// Waits for the VP to stop.
661    ///
662    /// Returns `true` if the VP hit an intercept.
663    pub async fn wait(&mut self) -> Result<bool, SidecarError> {
664        if !self.waited {
665            self.vp.wait_async().await;
666            self.waited = true;
667        }
668        let &RunVpResponse { intercept } = self.vp.command_result::<_, u8>(0)?.0;
669        Ok(intercept != 0)
670    }
671}
672
673impl Drop for SidecarRun<'_, '_> {
674    fn drop(&mut self) {
675        assert!(self.waited, "failed to stop vp");
676    }
677}