1#![cfg(target_os = "linux")]
7#![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#[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
107unsafe impl Send for Mapping {}
109unsafe impl Sync for Mapping {}
111
112#[derive(Debug, Error)]
114pub enum NewSidecarClientError {
115 #[error("{operation} failed in sidecar driver")]
117 Io {
118 operation: &'static str,
120 #[source]
122 err: std::io::Error,
123 },
124 #[error("driver error")]
126 Driver(#[source] std::io::Error),
127}
128
129impl SidecarClient {
130 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 return Ok(None);
146 }
147 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 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 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 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 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 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 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 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
355pub 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#[derive(Debug, Error)]
379pub enum SidecarError {
380 #[error("driver error")]
382 Io(#[source] std::io::Error),
383 #[error("sidecar error: {0}")]
385 Sidecar(String),
386 #[error("hypervisor error")]
388 Hypervisor(#[source] HvError),
389}
390
391impl<'a> SidecarVp<'a> {
392 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 pub fn cpu_context(&self) -> *mut CpuContextX64 {
407 unsafe { addr_of_mut!((*self.shmem.as_ptr()).command_page.cpu_context) }
410 }
411
412 pub fn intercept_message(&self) -> *const HvMessage {
416 unsafe { addr_of!((*self.shmem.as_ptr()).command_page.intercept_message) }
419 }
420
421 pub fn register_page(&self) -> *mut hvdef::HvX64RegisterPage {
428 unsafe { addr_of_mut!((*self.shmem.as_ptr()).register_page) }
431 }
432
433 pub fn test(&mut self) -> Result<(), SidecarError> {
435 tracing::trace!("test");
436 let () = self.dispatch_sync(SidecarCommand::NONE, ())?;
437 Ok(())
438 }
439
440 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 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 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 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 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 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 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 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
642pub struct SidecarRun<'a, 'b> {
646 vp: &'a mut SidecarVp<'b>,
647 waited: bool,
648}
649
650impl SidecarRun<'_, '_> {
651 pub fn cancel(&mut self) {
655 if !self.waited {
656 self.vp.stop_async();
657 }
658 }
659
660 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}