diff --git a/cloud-hypervisor/src/main.rs b/cloud-hypervisor/src/main.rs index 8365bab08..8fb9a8318 100644 --- a/cloud-hypervisor/src/main.rs +++ b/cloud-hypervisor/src/main.rs @@ -34,8 +34,8 @@ use vmm::vm_config::IvshmemConfig; use vmm::vm_config::{ BalloonConfig, ConsoleConfig, DeviceConfig, DiskConfig, FsConfig, GenericVhostUserConfig, LandlockConfig, NetConfig, NumaConfig, PciSegmentConfig, PlatformConfig, PmemConfig, - RateLimiterGroupConfig, RngConfig, SerialConfig, TpmConfig, UserDeviceConfig, VdpaConfig, - VmConfig, VsockConfig, + RateLimiterGroupConfig, RngConfig, RtcConfig, SerialConfig, TpmConfig, UserDeviceConfig, + VdpaConfig, VmConfig, VsockConfig, }; use vmm_sys_util::eventfd::EventFd; use vmm_sys_util::signal::block_signal; @@ -404,6 +404,12 @@ fn get_cli_options_sorted( .help(RngConfig::SYNTAX) .default_value(default_rng) .group("vm-config"), + Arg::new("rtc") + .long("rtc") + .help(RtcConfig::SYNTAX) + .num_args(0..=1) + .default_missing_value("") + .group("vm-config"), Arg::new("seccomp") .long("seccomp") .num_args(1) @@ -1043,6 +1049,7 @@ mod unit_tests { iommu: false, numa: None, watchdog: false, + rtc: None, #[cfg(feature = "guest_debug")] gdb: false, pci_segments: None, diff --git a/fuzz/fuzz_targets/http_api.rs b/fuzz/fuzz_targets/http_api.rs index 07a1effa4..a4687f490 100644 --- a/fuzz/fuzz_targets/http_api.rs +++ b/fuzz/fuzz_targets/http_api.rs @@ -198,6 +198,7 @@ impl RequestHandler for StubApiRequestHandler { iommu: false, numa: None, watchdog: false, + rtc: None, gdb: false, pci_segments: None, platform: None, diff --git a/virtio-devices/src/lib.rs b/virtio-devices/src/lib.rs index 6ac397798..7c6ebeecd 100644 --- a/virtio-devices/src/lib.rs +++ b/virtio-devices/src/lib.rs @@ -27,6 +27,7 @@ pub mod mem; pub mod net; mod pmem; mod rng; +mod rtc; pub mod seccomp_filters; mod thread_helper; pub mod transport; @@ -54,6 +55,7 @@ pub use self::mem::{BlocksState, Mem, VIRTIO_MEM_ALIGN_SIZE, VirtioMemMappingSou pub use self::net::{Net, NetCtrlEpollHandler}; pub use self::pmem::Pmem; pub use self::rng::Rng; +pub use self::rtc::Rtc; pub use self::vdpa::{Vdpa, VdpaDmaMapping}; pub use self::vsock::Vsock; pub use self::watchdog::Watchdog; diff --git a/virtio-devices/src/rtc.rs b/virtio-devices/src/rtc.rs new file mode 100644 index 000000000..9a3b7bb05 --- /dev/null +++ b/virtio-devices/src/rtc.rs @@ -0,0 +1,707 @@ +// Copyright © 2026, Microsoft Corporation +// +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// + +use std::mem::size_of; +use std::os::unix::io::AsRawFd; +use std::sync::atomic::AtomicBool; +use std::sync::{Arc, Barrier}; +use std::{io, result}; + +use anyhow::anyhow; +use event_monitor::event; +use log::{error, info, warn}; +use seccompiler::SeccompAction; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use virtio_queue::{Queue, QueueT}; +use vm_memory::{Address, ByteValued, Bytes, GuestAddressSpace, GuestMemoryAtomic}; +use vm_migration::{Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable}; +use vm_virtio::{AccessPlatform, Translatable}; +use vmm_sys_util::eventfd::EventFd; + +use super::{ + ActivateResult, EPOLL_HELPER_EVENT_LAST, EpollHelper, EpollHelperError, EpollHelperHandler, + Error as DeviceError, VIRTIO_F_ACCESS_PLATFORM, VIRTIO_F_VERSION_1, VirtioCommon, VirtioDevice, + VirtioDeviceType, +}; +use crate::device::ActivationContext; +use crate::seccomp_filters::Thread; +use crate::thread_helper::spawn_virtio_thread; +use crate::{GuestMemoryMmap, VirtioInterrupt, VirtioInterruptType}; + +const QUEUE_SIZE: u16 = 256; +const QUEUE_SIZES: &[u16] = &[QUEUE_SIZE]; + +// New descriptors are pending on the virtio queue. +const QUEUE_AVAIL_EVENT: u16 = EPOLL_HELPER_EVENT_LAST + 1; + +// Virtio RTC request message types +const VIRTIO_RTC_REQ_READ: u16 = 0x0001; +const VIRTIO_RTC_REQ_CFG: u16 = 0x1000; +const VIRTIO_RTC_REQ_CLOCK_CAP: u16 = 0x1001; +const VIRTIO_RTC_REQ_CROSS_CAP: u16 = 0x1002; + +// Virtio RTC status codes +const VIRTIO_RTC_S_OK: u8 = 0; +const VIRTIO_RTC_S_EOPNOTSUPP: u8 = 2; +const VIRTIO_RTC_S_ENODEV: u8 = 3; +const VIRTIO_RTC_S_EINVAL: u8 = 4; +const VIRTIO_RTC_S_EIO: u8 = 5; + +// Clock types +const VIRTIO_RTC_CLOCK_UTC_MAYBE_SMEARED: u8 = 4; +const VIRTIO_RTC_SMEAR_UNSPECIFIED: u8 = 0; + +// Number of clocks exposed by this device +const NUM_CLOCKS: u16 = 1; + +/// Request header: 8 bytes +#[derive(Copy, Clone, Debug, Default)] +#[repr(C)] +struct VirtioRtcReqHead { + msg_type: u16, + reserved: [u8; 6], +} + +// SAFETY: it only has data and has no implicit padding. +unsafe impl ByteValued for VirtioRtcReqHead {} + +/// Response header: 8 bytes +#[derive(Copy, Clone, Debug, Default)] +#[repr(C)] +struct VirtioRtcRespHead { + status: u8, + reserved: [u8; 7], +} + +// SAFETY: it only has data and has no implicit padding. +unsafe impl ByteValued for VirtioRtcRespHead {} + +/// Request body for READ and CLOCK_CAP (after head): 8 bytes +#[derive(Copy, Clone, Debug, Default)] +#[repr(C)] +struct VirtioRtcReqClockBody { + clock_id: u16, + reserved: [u8; 6], +} + +// SAFETY: it only has data and has no implicit padding. +unsafe impl ByteValued for VirtioRtcReqClockBody {} + +/// Request body for CROSS_CAP (after head): 8 bytes +#[derive(Copy, Clone, Debug, Default)] +#[repr(C)] +struct VirtioRtcReqCrossCapBody { + clock_id: u16, + hw_counter: u8, + reserved: [u8; 5], +} + +// SAFETY: it only has data and has no implicit padding. +unsafe impl ByteValued for VirtioRtcReqCrossCapBody {} + +/// CFG response: head (8) + num_clocks (2) + reserved (6) = 16 bytes +#[derive(Copy, Clone, Debug, Default)] +#[repr(C)] +struct VirtioRtcRespCfg { + head: VirtioRtcRespHead, + num_clocks: u16, + reserved: [u8; 6], +} + +// SAFETY: it only has data and has no implicit padding. +unsafe impl ByteValued for VirtioRtcRespCfg {} + +/// CLOCK_CAP response: head (8) + type (1) + leap_second_smearing (1) + flags (1) + reserved (5) = 16 bytes +#[derive(Copy, Clone, Debug, Default)] +#[repr(C)] +struct VirtioRtcRespClockCap { + head: VirtioRtcRespHead, + type_: u8, + leap_second_smearing: u8, + flags: u8, + reserved: [u8; 5], +} + +// SAFETY: it only has data and has no implicit padding. +unsafe impl ByteValued for VirtioRtcRespClockCap {} + +/// READ response: head (8) + clock_reading (8) = 16 bytes +#[derive(Copy, Clone, Debug, Default)] +#[repr(C)] +struct VirtioRtcRespRead { + head: VirtioRtcRespHead, + clock_reading: u64, +} + +// SAFETY: it only has data and has no implicit padding. +unsafe impl ByteValued for VirtioRtcRespRead {} + +/// CROSS_CAP response: head (8) + flags (1) + reserved (7) = 16 bytes +#[derive(Copy, Clone, Debug, Default)] +#[repr(C)] +struct VirtioRtcRespCrossCap { + head: VirtioRtcRespHead, + flags: u8, + reserved: [u8; 7], +} + +// SAFETY: it only has data and has no implicit padding. +unsafe impl ByteValued for VirtioRtcRespCrossCap {} + +/// Parsed request from guest. +enum VirtioRtcRequest { + Cfg, + ClockCap { clock_id: u16 }, + Read { clock_id: u16 }, + CrossCap { clock_id: u16, _hw_counter: u8 }, + Invalid, + Unknown, +} + +/// Response to be written back to the guest. +enum VirtioRtcResponse { + Cfg(VirtioRtcRespCfg), + ClockCap(VirtioRtcRespClockCap), + Read(VirtioRtcRespRead), + CrossCap(VirtioRtcRespCrossCap), + Error(VirtioRtcRespHead), +} + +#[derive(Error, Debug)] +enum Error { + #[error("Descriptor chain too short")] + DescriptorChainTooShort, + #[error("Invalid descriptor")] + InvalidDescriptor, + #[error("Failed to translate guest address")] + AddressTranslation(#[source] io::Error), + #[error("Failed to read request from guest memory")] + GuestMemoryRead(#[source] vm_memory::guest_memory::Error), + #[error("Failed to write to guest memory")] + GuestMemoryWrite(#[source] vm_memory::guest_memory::Error), + #[error("Failed adding used index")] + QueueAddUsed(#[source] virtio_queue::Error), +} + +struct RtcEpollHandler { + mem: GuestMemoryAtomic, + queue: Queue, + interrupt_cb: Arc, + queue_evt: EventFd, + kill_evt: EventFd, + pause_evt: EventFd, + access_platform: Option>, +} + +impl RtcEpollHandler { + fn process_queue(&mut self) -> Result { + let mut used_descs = false; + + while let Some(mut desc_chain) = self.queue.pop_descriptor_chain(self.mem.memory()) { + let access_platform = self.access_platform.as_deref(); + + // Process the descriptor chain and prepare the response. + // If processing fails, we still need to add a used descriptor with a response indicating the error. + // And continue the loop to process the full queue, instead of breaking on the first error. + let resp_len = match Self::process_descriptor(access_platform, &mut desc_chain) { + Ok(len) => len, + Err(e) => { + error!("Failed to process virtio-rtc descriptor: {e}"); + 0 + } + }; + + self.queue + .add_used(desc_chain.memory(), desc_chain.head_index(), resp_len) + .map_err(Error::QueueAddUsed)?; + + used_descs = true; + } + + Ok(used_descs) + } + + fn process_descriptor( + access_platform: Option<&dyn AccessPlatform>, + desc_chain: &mut virtio_queue::DescriptorChain, + ) -> Result + where + M: std::ops::Deref, + M::Target: vm_memory::GuestMemory, + { + let req_desc = desc_chain.next().ok_or(Error::DescriptorChainTooShort)?; + let req_len = req_desc.len(); + + // The request descriptor must be readable by the device. + if req_desc.is_write_only() || req_len < size_of::() as u32 { + return Err(Error::InvalidDescriptor); + } + + let req_addr = req_desc + .addr() + .translate_gva(access_platform, req_desc.len() as usize) + .map_err(Error::AddressTranslation)?; + + // Read the request header + let req_head: VirtioRtcReqHead = desc_chain + .memory() + .read_obj(req_addr) + .map_err(Error::GuestMemoryRead)?; + + let body_addr = req_addr + .checked_add(size_of::() as u64) + .ok_or(Error::InvalidDescriptor)?; + + // Parse the full request based on msg_type + let request = match req_head.msg_type { + VIRTIO_RTC_REQ_CFG => VirtioRtcRequest::Cfg, + VIRTIO_RTC_REQ_CLOCK_CAP => { + if req_len + < (size_of::() + size_of::()) as u32 + { + VirtioRtcRequest::Invalid + } else { + let body: VirtioRtcReqClockBody = desc_chain + .memory() + .read_obj(body_addr) + .map_err(Error::GuestMemoryRead)?; + VirtioRtcRequest::ClockCap { + clock_id: body.clock_id, + } + } + } + VIRTIO_RTC_REQ_READ => { + if req_len + < (size_of::() + size_of::()) as u32 + { + VirtioRtcRequest::Invalid + } else { + let body: VirtioRtcReqClockBody = desc_chain + .memory() + .read_obj(body_addr) + .map_err(Error::GuestMemoryRead)?; + VirtioRtcRequest::Read { + clock_id: body.clock_id, + } + } + } + VIRTIO_RTC_REQ_CROSS_CAP => { + if req_len + < (size_of::() + size_of::()) as u32 + { + VirtioRtcRequest::Invalid + } else { + let body: VirtioRtcReqCrossCapBody = desc_chain + .memory() + .read_obj(body_addr) + .map_err(Error::GuestMemoryRead)?; + VirtioRtcRequest::CrossCap { + clock_id: body.clock_id, + _hw_counter: body.hw_counter, + } + } + } + _ => VirtioRtcRequest::Unknown, + }; + + let response = Self::handle_request(&request); + + let resp_desc = desc_chain.next().ok_or(Error::DescriptorChainTooShort)?; + + // The response descriptor must be writable by the device. + if !resp_desc.is_write_only() { + return Err(Error::InvalidDescriptor); + } + + let resp_addr = resp_desc + .addr() + .translate_gva(access_platform, resp_desc.len() as usize) + .map_err(Error::AddressTranslation)?; + + let resp_len = match &response { + VirtioRtcResponse::Cfg(_) => size_of::() as u32, + VirtioRtcResponse::ClockCap(_) => size_of::() as u32, + VirtioRtcResponse::Read(_) => size_of::() as u32, + VirtioRtcResponse::CrossCap(_) => size_of::() as u32, + VirtioRtcResponse::Error(_) => size_of::() as u32, + }; + + if resp_desc.len() < resp_len { + // If the write-only buffer can at least hold the response head, + // write an EINVAL status per the spec. + if resp_desc.len() >= size_of::() as u32 { + let einval = VirtioRtcRespHead { + status: VIRTIO_RTC_S_EINVAL, + ..Default::default() + }; + desc_chain + .memory() + .write_obj(einval, resp_addr) + .map_err(Error::GuestMemoryWrite)?; + return Ok(size_of::() as u32); + } + return Err(Error::InvalidDescriptor); + } + + match &response { + VirtioRtcResponse::Cfg(resp) => { + desc_chain + .memory() + .write_obj(*resp, resp_addr) + .map_err(Error::GuestMemoryWrite)?; + } + VirtioRtcResponse::ClockCap(resp) => { + desc_chain + .memory() + .write_obj(*resp, resp_addr) + .map_err(Error::GuestMemoryWrite)?; + } + VirtioRtcResponse::Read(resp) => { + desc_chain + .memory() + .write_obj(*resp, resp_addr) + .map_err(Error::GuestMemoryWrite)?; + } + VirtioRtcResponse::CrossCap(resp) => { + desc_chain + .memory() + .write_obj(*resp, resp_addr) + .map_err(Error::GuestMemoryWrite)?; + } + VirtioRtcResponse::Error(resp) => { + warn!("virtio-rtc: responding with error status {}", resp.status); + desc_chain + .memory() + .write_obj(*resp, resp_addr) + .map_err(Error::GuestMemoryWrite)?; + } + } + + Ok(resp_len) + } + + fn handle_request(req: &VirtioRtcRequest) -> VirtioRtcResponse { + match req { + VirtioRtcRequest::Cfg => VirtioRtcResponse::Cfg(VirtioRtcRespCfg { + head: VirtioRtcRespHead { + status: VIRTIO_RTC_S_OK, + ..Default::default() + }, + num_clocks: NUM_CLOCKS, + ..Default::default() + }), + VirtioRtcRequest::ClockCap { clock_id } => match clock_id { + 0 => VirtioRtcResponse::ClockCap(VirtioRtcRespClockCap { + head: VirtioRtcRespHead { + status: VIRTIO_RTC_S_OK, + ..Default::default() + }, + type_: VIRTIO_RTC_CLOCK_UTC_MAYBE_SMEARED, + leap_second_smearing: VIRTIO_RTC_SMEAR_UNSPECIFIED, + flags: 0, // alarm not supported + ..Default::default() + }), + _ => VirtioRtcResponse::ClockCap(VirtioRtcRespClockCap { + head: VirtioRtcRespHead { + status: VIRTIO_RTC_S_ENODEV, + ..Default::default() + }, + ..Default::default() + }), + }, + VirtioRtcRequest::Read { clock_id } => match clock_id { + // Use CLOCK_REALTIME (SystemTime) intentionally: this is the PTP reference + // clock for chronyd in MSHV L2 guests, so readings must track the L1 host's + // NTP-disciplined wall clock. CLOCK_MONOTONIC would drift from L1 wall time + // as NTP adjusts, causing L2-L1 divergence. + 0 => match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) { + Ok(now) => { + let nanos = now.as_nanos(); + if nanos > u64::MAX as u128 { + return VirtioRtcResponse::Read(VirtioRtcRespRead { + head: VirtioRtcRespHead { + status: VIRTIO_RTC_S_EIO, + ..Default::default() + }, + ..Default::default() + }); + } + VirtioRtcResponse::Read(VirtioRtcRespRead { + head: VirtioRtcRespHead { + status: VIRTIO_RTC_S_OK, + ..Default::default() + }, + clock_reading: nanos as u64, + }) + } + Err(_) => VirtioRtcResponse::Read(VirtioRtcRespRead { + head: VirtioRtcRespHead { + status: VIRTIO_RTC_S_EIO, + ..Default::default() + }, + ..Default::default() + }), + }, + _ => VirtioRtcResponse::Read(VirtioRtcRespRead { + head: VirtioRtcRespHead { + status: VIRTIO_RTC_S_ENODEV, + ..Default::default() + }, + ..Default::default() + }), + }, + VirtioRtcRequest::CrossCap { clock_id, .. } => match clock_id { + 0 => VirtioRtcResponse::CrossCap(VirtioRtcRespCrossCap { + head: VirtioRtcRespHead { + status: VIRTIO_RTC_S_OK, + ..Default::default() + }, + flags: 0, // no cross-timestamping support + ..Default::default() + }), + _ => VirtioRtcResponse::CrossCap(VirtioRtcRespCrossCap { + head: VirtioRtcRespHead { + status: VIRTIO_RTC_S_ENODEV, + ..Default::default() + }, + ..Default::default() + }), + }, + VirtioRtcRequest::Invalid => VirtioRtcResponse::Error(VirtioRtcRespHead { + status: VIRTIO_RTC_S_EINVAL, + ..Default::default() + }), + VirtioRtcRequest::Unknown => VirtioRtcResponse::Error(VirtioRtcRespHead { + status: VIRTIO_RTC_S_EOPNOTSUPP, + ..Default::default() + }), + } + } + + fn signal_used_queue(&self) -> result::Result<(), DeviceError> { + self.interrupt_cb + .trigger(VirtioInterruptType::Queue(0)) + .map_err(|e| { + error!("Failed to signal used queue: {e:?}"); + DeviceError::FailedSignalingUsedQueue(e) + }) + } + + fn run( + &mut self, + paused: &AtomicBool, + paused_sync: &Barrier, + ) -> result::Result<(), EpollHelperError> { + let mut helper = EpollHelper::new(&self.kill_evt, &self.pause_evt)?; + helper.add_event(self.queue_evt.as_raw_fd(), QUEUE_AVAIL_EVENT)?; + helper.run(paused, paused_sync, self)?; + + Ok(()) + } +} + +impl EpollHelperHandler for RtcEpollHandler { + fn handle_event( + &mut self, + _helper: &mut EpollHelper, + event: &epoll::Event, + ) -> result::Result<(), EpollHelperError> { + let ev_type = event.data as u16; + match ev_type { + QUEUE_AVAIL_EVENT => { + self.queue_evt.read().map_err(|e| { + EpollHelperError::HandleEvent(anyhow!("Failed to get queue event: {e:?}")) + })?; + let needs_notification = self.process_queue().map_err(|e| { + EpollHelperError::HandleEvent(anyhow!("Failed to process queue : {e:?}")) + })?; + if needs_notification { + self.signal_used_queue().map_err(|e| { + EpollHelperError::HandleEvent(anyhow!("Failed to signal used queue: {e:?}")) + })?; + } + } + _ => { + return Err(EpollHelperError::HandleEvent(anyhow!( + "Unexpected event: {ev_type}" + ))); + } + } + Ok(()) + } +} + +/// Virtio RTC device exposing high-resolution host clocks to the guest. +pub struct Rtc { + common: VirtioCommon, + id: String, + seccomp_action: SeccompAction, + exit_evt: EventFd, +} + +#[derive(Deserialize, Serialize)] +pub struct RtcState { + pub avail_features: u64, + pub acked_features: u64, +} + +impl Rtc { + /// Create a new virtio RTC device. + pub fn new( + id: String, + access_platform_enabled: bool, + seccomp_action: SeccompAction, + exit_evt: EventFd, + state: Option, + ) -> io::Result { + let (avail_features, acked_features, paused) = if let Some(state) = state { + info!("Restoring virtio-rtc {id}"); + (state.avail_features, state.acked_features, true) + } else { + let mut avail_features = 1u64 << VIRTIO_F_VERSION_1; + + if access_platform_enabled { + avail_features |= 1u64 << VIRTIO_F_ACCESS_PLATFORM; + } + + (avail_features, 0, false) + }; + + Ok(Rtc { + common: VirtioCommon { + device_type: VirtioDeviceType::Rtc as u32, + queue_sizes: QUEUE_SIZES.to_vec(), + paused_sync: Some(Arc::new(Barrier::new(2))), + avail_features, + acked_features, + min_queues: 1, + paused: Arc::new(AtomicBool::new(paused)), + ..Default::default() + }, + id, + seccomp_action, + exit_evt, + }) + } + + fn state(&self) -> RtcState { + RtcState { + avail_features: self.common.avail_features, + acked_features: self.common.acked_features, + } + } + + #[cfg(fuzzing)] + pub fn wait_for_epoll_threads(&mut self) { + self.common.wait_for_epoll_threads(); + } +} + +impl Drop for Rtc { + fn drop(&mut self) { + if let Some(kill_evt) = self.common.kill_evt.take() { + // Ignore the result because there is nothing we can do about it. + let _ = kill_evt.write(1); + } + self.common.wait_for_epoll_threads(); + } +} + +impl VirtioDevice for Rtc { + fn device_type(&self) -> u32 { + self.common.device_type + } + + fn queue_max_sizes(&self) -> &[u16] { + &self.common.queue_sizes + } + + fn features(&self) -> u64 { + self.common.avail_features + } + + fn ack_features(&mut self, value: u64) { + self.common.ack_features(value); + } + + fn activate(&mut self, context: ActivationContext) -> ActivateResult { + let ActivationContext { + mem, + interrupt_cb, + mut queues, + device_status, + } = context; + self.common.activate(&queues, interrupt_cb.clone())?; + let (kill_evt, pause_evt) = self.common.dup_eventfds(); + + let (_, queue, queue_evt) = queues.remove(0); + + let mut handler = RtcEpollHandler { + mem, + queue, + interrupt_cb: interrupt_cb.clone(), + queue_evt, + kill_evt, + pause_evt, + access_platform: self.common.access_platform.clone(), + }; + + let paused = self.common.paused.clone(); + let paused_sync = self.common.paused_sync.clone(); + let mut epoll_threads = Vec::new(); + spawn_virtio_thread( + &self.id, + &self.seccomp_action, + Thread::VirtioRtc, + &mut epoll_threads, + &self.exit_evt, + device_status.clone(), + interrupt_cb.clone(), + move || handler.run(&paused, paused_sync.as_ref().unwrap()), + )?; + + self.common.epoll_threads = Some(epoll_threads); + + event!("virtio-device", "activated", "id", &self.id); + Ok(()) + } + + fn reset(&mut self) { + self.common.reset(); + event!("virtio-device", "reset", "id", &self.id); + } + + fn set_access_platform(&mut self, access_platform: Arc) { + self.common.set_access_platform(access_platform); + } + + fn access_platform(&self) -> Option> { + self.common.access_platform() + } +} + +impl Pausable for Rtc { + fn pause(&mut self) -> result::Result<(), MigratableError> { + self.common.pause() + } + + fn resume(&mut self) -> result::Result<(), MigratableError> { + self.common.resume() + } +} + +impl Snapshottable for Rtc { + fn id(&self) -> String { + self.id.clone() + } + + fn snapshot(&mut self) -> std::result::Result { + Snapshot::new_from_state(&self.state()) + } +} + +impl Transportable for Rtc {} +impl Migratable for Rtc {} diff --git a/virtio-devices/src/seccomp_filters.rs b/virtio-devices/src/seccomp_filters.rs index 184122f1e..2cb11fff5 100644 --- a/virtio-devices/src/seccomp_filters.rs +++ b/virtio-devices/src/seccomp_filters.rs @@ -23,6 +23,7 @@ pub enum Thread { VirtioNetCtl, VirtioPmem, VirtioRng, + VirtioRtc, VirtioVhostBlock, VirtioVhostFs, VirtioGenericVhostUser, @@ -196,6 +197,15 @@ fn virtio_rng_thread_rules() -> Vec<(i64, Vec)> { ] } +fn virtio_rtc_thread_rules() -> Vec<(i64, Vec)> { + vec![ + (libc::SYS_sched_getaffinity, vec![]), + (libc::SYS_set_robust_list, vec![]), + #[cfg(feature = "sev_snp")] + (libc::SYS_ioctl, create_mshv_sev_snp_ioctl_seccomp_rule()), + ] +} + fn virtio_vhost_fs_thread_rules() -> Vec<(i64, Vec)> { vec![ (libc::SYS_clock_nanosleep, vec![]), @@ -306,6 +316,7 @@ fn get_seccomp_rules(thread_type: Thread) -> Vec<(i64, Vec)> { Thread::VirtioNetCtl => virtio_net_ctl_thread_rules(), Thread::VirtioPmem => virtio_pmem_thread_rules(), Thread::VirtioRng => virtio_rng_thread_rules(), + Thread::VirtioRtc => virtio_rtc_thread_rules(), Thread::VirtioVhostBlock => virtio_vhost_block_thread_rules(), Thread::VirtioVhostFs => virtio_vhost_fs_thread_rules(), Thread::VirtioGenericVhostUser => virtio_generic_vhost_user_thread_rules(), diff --git a/vm-virtio/src/lib.rs b/vm-virtio/src/lib.rs index fbd94b2b7..c64e93ec3 100644 --- a/vm-virtio/src/lib.rs +++ b/vm-virtio/src/lib.rs @@ -33,6 +33,7 @@ pub enum VirtioDeviceType { Balloon = 5, Fs9P = 9, Gpu = 16, + Rtc = 17, Input = 18, Vsock = 19, Iommu = 23, @@ -53,6 +54,7 @@ impl From for VirtioDeviceType { 5 => VirtioDeviceType::Balloon, 9 => VirtioDeviceType::Fs9P, 16 => VirtioDeviceType::Gpu, + 17 => VirtioDeviceType::Rtc, 18 => VirtioDeviceType::Input, 19 => VirtioDeviceType::Vsock, 23 => VirtioDeviceType::Iommu, @@ -76,8 +78,9 @@ impl fmt::Display for VirtioDeviceType { VirtioDeviceType::Console => "console", VirtioDeviceType::Rng => "rng", VirtioDeviceType::Balloon => "balloon", - VirtioDeviceType::Gpu => "gpu", VirtioDeviceType::Fs9P => "9p", + VirtioDeviceType::Gpu => "gpu", + VirtioDeviceType::Rtc => "rtc", VirtioDeviceType::Input => "input", VirtioDeviceType::Vsock => "vsock", VirtioDeviceType::Iommu => "iommu", diff --git a/vmm/src/api/openapi/cloud-hypervisor.yaml b/vmm/src/api/openapi/cloud-hypervisor.yaml index 5ed7e9d22..6714517c7 100644 --- a/vmm/src/api/openapi/cloud-hypervisor.yaml +++ b/vmm/src/api/openapi/cloud-hypervisor.yaml @@ -667,6 +667,8 @@ components: watchdog: type: boolean default: false + rtc: + $ref: "#/components/schemas/RtcConfig" pvpanic: type: boolean default: false @@ -1087,6 +1089,21 @@ components: src: type: string + RtcConfig: + type: object + properties: + id: + type: string + pci_segment: + type: integer + format: int16 + pci_device_id: + type: integer + format: uint8 + iommu: + type: boolean + default: false + BalloonConfig: required: - size diff --git a/vmm/src/config.rs b/vmm/src/config.rs index 3f507b830..40f043a51 100644 --- a/vmm/src/config.rs +++ b/vmm/src/config.rs @@ -116,6 +116,9 @@ pub enum Error { /// Error parsing RNG options #[error("Error parsing --rng")] ParseRng(#[source] OptionParserError), + /// Error parsing RTC options + #[error("Error parsing --rtc")] + ParseRtc(#[source] OptionParserError), /// Error parsing balloon options #[error("Error parsing --balloon")] ParseBalloon(#[source] OptionParserError), @@ -474,6 +477,7 @@ pub struct VmParams<'a> { pub pvpanic: bool, pub numa: Option>, pub watchdog: bool, + pub rtc: Option<&'a str>, #[cfg(feature = "guest_debug")] pub gdb: bool, pub pci_segments: Option>, @@ -544,6 +548,7 @@ impl<'a> VmParams<'a> { .get_many::("numa") .map(|x| x.map(|y| y as &str).collect()); let watchdog = args.get_flag("watchdog"); + let rtc: Option<&str> = args.get_one::("rtc").map(|x| x as &str); let pci_segments: Option> = args .get_many::("pci-segment") .map(|x| x.map(|y| y as &str).collect()); @@ -593,6 +598,7 @@ impl<'a> VmParams<'a> { pvpanic, numa, watchdog, + rtc, #[cfg(feature = "guest_debug")] gdb, pci_segments, @@ -1780,6 +1786,28 @@ impl RngConfig { } } +impl RtcConfig { + pub const SYNTAX: &'static str = "Virtio RTC parameters \"\ + iommu=on|off,id=,\ + pci_segment=,pci_device_id=\". \ + Passing --rtc with no arguments enables the device with default \ + settings."; + + pub fn parse(rtc: &str) -> Result { + let mut parser = OptionParser::new(); + parser.add_all(PciDeviceCommonConfig::OPTIONS_IOMMU); + parser.parse(rtc).map_err(Error::ParseRtc)?; + + let pci_common = PciDeviceCommonConfig::parse(rtc)?; + + Ok(RtcConfig { pci_common }) + } + + pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> { + self.pci_common.validate(vm_config) + } +} + impl BalloonConfig { pub const SYNTAX: &'static str = "Balloon parameters \"size=,deflate_on_oom=on|off,\ free_page_reporting=on|off\""; @@ -3054,6 +3082,12 @@ impl VmConfig { Self::validate_identifier(&mut id_list, &self.rng.pci_common.id)?; self.iommu |= self.rng.pci_common.iommu; + if let Some(rtc) = &self.rtc { + rtc.validate(self)?; + Self::validate_identifier(&mut id_list, &rtc.pci_common.id)?; + self.iommu |= rtc.pci_common.iommu; + } + self.console.validate(self)?; Self::validate_identifier(&mut id_list, &self.console.pci_common.id)?; self.iommu |= self.console.pci_common.iommu; @@ -3290,6 +3324,11 @@ impl VmConfig { let rng = RngConfig::parse(vm_params.rng)?; + let mut rtc: Option = None; + if let Some(rtc_params) = &vm_params.rtc { + rtc = Some(RtcConfig::parse(rtc_params)?); + } + let mut balloon: Option = None; if let Some(balloon_params) = &vm_params.balloon { balloon = Some(BalloonConfig::parse(balloon_params)?); @@ -3471,6 +3510,7 @@ impl VmConfig { iommu: false, // updated in VmConfig::validate() numa, watchdog: vm_params.watchdog, + rtc, #[cfg(feature = "guest_debug")] gdb, pci_segments, @@ -3591,6 +3631,7 @@ impl Clone for VmConfig { disks: self.disks.clone(), net: self.net.clone(), rng: self.rng.clone(), + rtc: self.rtc.clone(), balloon: self.balloon.clone(), #[cfg(feature = "pvmemcontrol")] pvmemcontrol: self.pvmemcontrol.clone(), @@ -4825,6 +4866,7 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}" iommu: false, numa: None, watchdog: false, + rtc: None, #[cfg(feature = "guest_debug")] gdb: false, pci_segments: None, @@ -5072,6 +5114,7 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}" iommu: false, numa: None, watchdog: false, + rtc: None, #[cfg(feature = "guest_debug")] gdb: false, pci_segments: None, diff --git a/vmm/src/device_manager.rs b/vmm/src/device_manager.rs index 6843b9151..ce4f962c6 100644 --- a/vmm/src/device_manager.rs +++ b/vmm/src/device_manager.rs @@ -139,6 +139,7 @@ const DEBUGCON_DEVICE_NAME: &str = "__debug_console"; #[cfg(target_arch = "aarch64")] const GPIO_DEVICE_NAME: &str = "__gpio"; const RNG_DEVICE_NAME: &str = "__rng"; +const RTC_DEVICE_NAME: &str = "__rtc"; const IOMMU_DEVICE_NAME: &str = "__iommu"; #[cfg(feature = "pvmemcontrol")] const PVMEMCONTROL_DEVICE_NAME: &str = "__pvmemcontrol"; @@ -193,6 +194,10 @@ pub enum DeviceManagerError { #[error("Cannot create virtio-rng device")] CreateVirtioRng(#[source] io::Error), + /// Cannot create virtio-rtc device + #[error("Cannot create virtio-rtc device")] + CreateVirtioRtc(#[source] io::Error), + /// Cannot create generic vhost-user device #[error("Cannot create generic vhost-user device")] CreateGenericVhostUser(#[source] virtio_devices::vhost_user::Error), @@ -2654,6 +2659,9 @@ impl DeviceManager { // Add vDPA devices if required self.make_vdpa_devices(snapshot)?; + // Add virtio-rtc device + self.make_virtio_rtc_devices(snapshot)?; + Ok(()) } /// Creates a [`MetaVirtioDevice`] from the provided [`DiskConfig`]. @@ -3116,6 +3124,53 @@ impl DeviceManager { Ok(()) } + fn make_virtio_rtc_devices(&mut self, snapshot: Option<&Snapshot>) -> DeviceManagerResult<()> { + let Some(mut rtc_config) = self.config.lock().unwrap().rtc.clone() else { + return Ok(()); + }; + + info!("Creating virtio-rtc device: {rtc_config:?}"); + + let id = match rtc_config.pci_common.id.as_ref() { + Some(id) => id.clone(), + None => rtc_config + .pci_common + .id + .insert(RTC_DEVICE_NAME.to_string()) + .clone(), + }; + + let virtio_rtc_device = Arc::new(Mutex::new( + virtio_devices::Rtc::new( + id.clone(), + self.force_access_platform | rtc_config.pci_common.iommu, + self.seccomp_action.clone(), + self.exit_evt + .try_clone() + .map_err(DeviceManagerError::EventFd)?, + state_from_id(snapshot, id.as_str()) + .map_err(DeviceManagerError::RestoreGetState)?, + ) + .map_err(DeviceManagerError::CreateVirtioRtc)?, + )); + self.virtio_devices.push(MetaVirtioDevice { + virtio_device: Arc::clone(&virtio_rtc_device) + as Arc>, + pci_common: rtc_config.pci_common.clone(), + dma_handler: None, + }); + + // Fill the device tree with a new node. In case of restore, we + // know there is nothing to do, so we can simply override the + // existing entry. + self.device_tree + .lock() + .unwrap() + .insert(id.clone(), device_node!(id, virtio_rtc_device)); + + Ok(()) + } + fn make_generic_vhost_user_device( &mut self, generic_vhost_user_cfg: &mut GenericVhostUserConfig, diff --git a/vmm/src/lib.rs b/vmm/src/lib.rs index ed26eebc4..58abd061f 100644 --- a/vmm/src/lib.rs +++ b/vmm/src/lib.rs @@ -2755,6 +2755,7 @@ mod unit_tests { iommu: false, numa: None, watchdog: false, + rtc: None, #[cfg(feature = "guest_debug")] gdb: false, pci_segments: None, diff --git a/vmm/src/vm_config.rs b/vmm/src/vm_config.rs index b85ae0e13..b6f0eb17f 100644 --- a/vmm/src/vm_config.rs +++ b/vmm/src/vm_config.rs @@ -451,6 +451,12 @@ impl Default for RngConfig { } } +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] +pub struct RtcConfig { + #[serde(flatten)] + pub pci_common: PciDeviceCommonConfig, +} + impl ApplyLandlock for RngConfig { fn apply_landlock(&self, landlock: &mut Landlock) -> LandlockResult<()> { // Rng Path only need read access @@ -1051,6 +1057,8 @@ pub struct VmConfig { pub numa: Option>, #[serde(default)] pub watchdog: bool, + #[serde(default)] + pub rtc: Option, #[cfg(feature = "guest_debug")] #[serde(default)] pub gdb: bool,