Skip to content

Commit eaa5f1d

Browse files
committed
vmclock: add support for VMclock device for x86_64
Implement the VMClock device on x86_64 platforms. At the moment, we just allocate the memory region in the guest address space for exposing the device. We don't expose any clock from the host and since we don't support live migration, the device won't do anything at the moment, but we should still be able to see a `/dev/vmclock` inside the guest. We do support the `disruption_marker` field which notifies the guest to adjust clocks due to a time shifting event. Signed-off-by: Babis Chalios <bchalios@amazon.es>
1 parent 6fecb44 commit eaa5f1d

File tree

7 files changed

+267
-3
lines changed

7 files changed

+267
-3
lines changed

src/vmm/src/builder.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ pub fn build_microvm_for_boot(
256256
)?;
257257

258258
device_manager.attach_vmgenid_device(&vm)?;
259+
device_manager.attach_vmclock_device(&vm)?;
259260

260261
#[cfg(target_arch = "aarch64")]
261262
if vcpus[0].kvm_vcpu.supports_pvtime() {
@@ -943,6 +944,10 @@ pub(crate) mod tests {
943944
vmm.device_manager.attach_vmgenid_device(&vmm.vm).unwrap();
944945
}
945946

947+
pub(crate) fn insert_vmclock_device(vmm: &mut Vmm) {
948+
vmm.device_manager.attach_vmclock_device(&vmm.vm).unwrap();
949+
}
950+
946951
pub(crate) fn insert_balloon_device(
947952
vmm: &mut Vmm,
948953
cmdline: &mut Cmdline,

src/vmm/src/device_manager/acpi.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use acpi_tables::{Aml, aml};
55
use vm_memory::GuestMemoryError;
66

77
use crate::Vm;
8+
use crate::devices::acpi::vmclock::VmClock;
89
use crate::devices::acpi::vmgenid::VmGenId;
910
use crate::vstate::resources::ResourceAllocator;
1011

@@ -20,26 +21,37 @@ pub enum ACPIDeviceError {
2021
pub struct ACPIDeviceManager {
2122
/// VMGenID device
2223
pub vmgenid: VmGenId,
24+
/// VMclock device
25+
pub vmclock: VmClock,
2326
}
2427

2528
impl ACPIDeviceManager {
2629
/// Create a new ACPIDeviceManager object
2730
pub fn new(resource_allocator: &mut ResourceAllocator) -> Self {
28-
let vmgenid = VmGenId::new(resource_allocator);
29-
ACPIDeviceManager { vmgenid }
31+
ACPIDeviceManager {
32+
vmgenid: VmGenId::new(resource_allocator),
33+
vmclock: VmClock::new(resource_allocator),
34+
}
3035
}
3136

3237
pub fn attach_vmgenid(&self, vm: &Vm) -> Result<(), ACPIDeviceError> {
3338
vm.register_irq(&self.vmgenid.interrupt_evt, self.vmgenid.gsi)?;
3439
self.vmgenid.activate(vm.guest_memory())?;
3540
Ok(())
3641
}
42+
43+
pub fn attach_vmclock(&self, vm: &Vm) -> Result<(), ACPIDeviceError> {
44+
self.vmclock.activate(vm.guest_memory())?;
45+
Ok(())
46+
}
3747
}
3848

3949
impl Aml for ACPIDeviceManager {
4050
fn append_aml_bytes(&self, v: &mut Vec<u8>) -> Result<(), aml::AmlError> {
4151
// AML for [`VmGenId`] device.
4252
self.vmgenid.append_aml_bytes(v)?;
53+
// AML for [`VmClock`] device.
54+
self.vmclock.append_aml_bytes(v)?;
4355

4456
// Create the AML for the GED interrupt handler
4557
aml::Device::new(

src/vmm/src/device_manager/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,11 @@ impl DeviceManager {
237237
Ok(())
238238
}
239239

240+
pub(crate) fn attach_vmclock_device(&mut self, vm: &Vm) -> Result<(), AttachDeviceError> {
241+
self.acpi_devices.attach_vmclock(vm)?;
242+
Ok(())
243+
}
244+
240245
#[cfg(target_arch = "aarch64")]
241246
pub(crate) fn attach_legacy_devices_aarch64(
242247
&mut self,

src/vmm/src/device_manager/persist.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use super::mmio::*;
1515
#[cfg(target_arch = "aarch64")]
1616
use crate::arch::DeviceType;
1717
use crate::device_manager::acpi::ACPIDeviceError;
18+
use crate::devices::acpi::vmclock::{VmClock, VmClockState};
1819
use crate::devices::acpi::vmgenid::{VMGenIDState, VmGenId};
1920
#[cfg(target_arch = "aarch64")]
2021
use crate::devices::legacy::RTCDevice;
@@ -158,6 +159,7 @@ impl fmt::Debug for MMIODevManagerConstructorArgs<'_> {
158159
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
159160
pub struct ACPIDeviceManagerState {
160161
vmgenid: VMGenIDState,
162+
vmclock: VmClockState,
161163
}
162164

163165
impl<'a> Persist<'a> for ACPIDeviceManager {
@@ -168,13 +170,16 @@ impl<'a> Persist<'a> for ACPIDeviceManager {
168170
fn save(&self) -> Self::State {
169171
ACPIDeviceManagerState {
170172
vmgenid: self.vmgenid.save(),
173+
vmclock: self.vmclock.save(),
171174
}
172175
}
173176

174177
fn restore(vm: Self::ConstructorArgs, state: &Self::State) -> Result<Self, Self::Error> {
175178
let acpi_devices = ACPIDeviceManager {
176179
// This can't fail
177180
vmgenid: VmGenId::restore(vm.guest_memory(), &state.vmgenid).unwrap(),
181+
// This can't fail
182+
vmclock: VmClock::restore(vm.guest_memory(), &state.vmclock).unwrap(),
178183
};
179184

180185
acpi_devices.attach_vmgenid(vm)?;

src/vmm/src/devices/acpi/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
mod generated;
5+
pub mod vmclock;
56
pub mod vmgenid;
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use std::mem::offset_of;
5+
use std::sync::atomic::{Ordering, fence};
6+
7+
use acpi_tables::{Aml, aml};
8+
use log::error;
9+
use serde::{Deserialize, Serialize};
10+
use vm_allocator::AllocPolicy;
11+
use vm_memory::{Address, ByteValued, Bytes, GuestAddress, GuestMemoryError};
12+
13+
use crate::devices::acpi::generated::vmclock_abi::{
14+
VMCLOCK_COUNTER_INVALID, VMCLOCK_MAGIC, VMCLOCK_STATUS_UNKNOWN, vmclock_abi,
15+
};
16+
use crate::snapshot::Persist;
17+
use crate::vstate::memory::GuestMemoryMmap;
18+
use crate::vstate::resources::ResourceAllocator;
19+
20+
// SAFETY: `vmclock_abi` is a POD
21+
unsafe impl ByteValued for vmclock_abi {}
22+
23+
// We are reserving a physical page to expose the [`VmClock`] data
24+
const VMCLOCK_SIZE: u32 = 0x1000;
25+
26+
// Write a value in `vmclock_abi` both in the Firecracker-managed state
27+
// and inside guest memory address that corresponds to it.
28+
macro_rules! write_vmclock_field {
29+
($vmclock:expr, $mem:expr, $field:ident, $value:expr) => {
30+
$vmclock.inner.$field = $value;
31+
$mem.write_obj(
32+
$vmclock.inner.$field,
33+
$vmclock
34+
.guest_address
35+
.unchecked_add(offset_of!(vmclock_abi, $field) as u64),
36+
);
37+
};
38+
}
39+
40+
/// VMclock device
41+
///
42+
/// This device emulates the VMclock device which allows passing information to the guest related
43+
/// to the relation of the host CPU to real-time clock as well as information about disruptive
44+
/// events, such as live-migration.
45+
#[derive(Debug)]
46+
pub struct VmClock {
47+
/// Guest address in which we will write the VMclock struct
48+
pub guest_address: GuestAddress,
49+
/// The [`VmClock`] state we are exposing to the guest
50+
inner: vmclock_abi,
51+
}
52+
53+
impl VmClock {
54+
/// Create a new [`VmClock`] device for a newly booted VM
55+
pub fn new(resource_allocator: &mut ResourceAllocator) -> VmClock {
56+
let addr = resource_allocator
57+
.allocate_system_memory(
58+
VMCLOCK_SIZE as u64,
59+
VMCLOCK_SIZE as u64,
60+
AllocPolicy::LastMatch,
61+
)
62+
.inspect_err(|err| error!("vmclock: could not allocate guest memory for device: {err}"))
63+
.unwrap();
64+
65+
let mut inner = vmclock_abi {
66+
magic: VMCLOCK_MAGIC,
67+
size: VMCLOCK_SIZE,
68+
version: u16::to_le(1),
69+
clock_status: VMCLOCK_STATUS_UNKNOWN,
70+
counter_id: VMCLOCK_COUNTER_INVALID,
71+
..Default::default()
72+
};
73+
74+
VmClock {
75+
guest_address: GuestAddress(addr),
76+
inner,
77+
}
78+
}
79+
80+
/// Activate [`VmClock`] device
81+
pub fn activate(&self, mem: &GuestMemoryMmap) -> Result<(), GuestMemoryError> {
82+
mem.write_slice(self.inner.as_slice(), self.guest_address)?;
83+
Ok(())
84+
}
85+
86+
/// Bump the VM generation counter
87+
pub fn post_load_update(&mut self, mem: &GuestMemoryMmap) {
88+
write_vmclock_field!(self, mem, seq_count, self.inner.seq_count | 1);
89+
90+
// This fence ensures guest sees all previous writes. It is matched to a
91+
// read barrier in the guest.
92+
fence(Ordering::Release);
93+
94+
write_vmclock_field!(
95+
self,
96+
mem,
97+
disruption_marker,
98+
self.inner.disruption_marker.wrapping_add(1)
99+
);
100+
101+
// This fence ensures guest sees the `disruption_marker` update. It is matched to a
102+
// read barrier in the guest.
103+
fence(Ordering::Release);
104+
105+
write_vmclock_field!(self, mem, seq_count, self.inner.seq_count.wrapping_add(1));
106+
}
107+
}
108+
109+
/// (De)serialize-able state of the [`VmClock`]
110+
///
111+
/// We could avoid this and reuse [`VmClock`] itself if `GuestAddress` was `Serialize`/`Deserialize`
112+
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
113+
pub struct VmClockState {
114+
/// Guest address in which we write the [`VmClock`] info
115+
pub guest_address: u64,
116+
/// Data we expose to the guest
117+
pub inner: vmclock_abi,
118+
}
119+
120+
impl<'a> Persist<'a> for VmClock {
121+
type State = VmClockState;
122+
type ConstructorArgs = &'a GuestMemoryMmap;
123+
type Error = GuestMemoryError;
124+
125+
fn save(&self) -> Self::State {
126+
VmClockState {
127+
guest_address: self.guest_address.0,
128+
inner: self.inner,
129+
}
130+
}
131+
132+
fn restore(
133+
constructor_args: Self::ConstructorArgs,
134+
state: &Self::State,
135+
) -> Result<Self, Self::Error> {
136+
let mut vmclock = VmClock {
137+
guest_address: GuestAddress(state.guest_address),
138+
inner: state.inner,
139+
};
140+
vmclock.post_load_update(constructor_args);
141+
Ok(vmclock)
142+
}
143+
}
144+
145+
impl Aml for VmClock {
146+
fn append_aml_bytes(&self, v: &mut Vec<u8>) -> Result<(), aml::AmlError> {
147+
#[allow(clippy::cast_possible_truncation)]
148+
let addr_low = self.guest_address.0 as u32;
149+
let addr_high = (self.guest_address.0 >> 32) as u32;
150+
aml::Device::new(
151+
"_SB_.VCLK".try_into()?,
152+
vec![
153+
&aml::Name::new("_HID".try_into()?, &"AMZNC10C")?,
154+
&aml::Name::new("_CID".try_into()?, &"VMCLOCK")?,
155+
&aml::Name::new("_DDN".try_into()?, &"VMCLOCK")?,
156+
&aml::Method::new(
157+
"_STA".try_into()?,
158+
0,
159+
false,
160+
vec![&aml::Return::new(&0x0fu8)],
161+
),
162+
&aml::Name::new(
163+
"_CRS".try_into()?,
164+
&aml::ResourceTemplate::new(vec![&aml::AddressSpace::new_memory(
165+
aml::AddressSpaceCacheable::Cacheable,
166+
false,
167+
self.guest_address.0,
168+
self.guest_address.0 + VMCLOCK_SIZE as u64 - 1,
169+
)?]),
170+
)?,
171+
],
172+
)
173+
.append_aml_bytes(v)
174+
}
175+
}
176+
177+
#[cfg(test)]
178+
mod tests {
179+
use vm_memory::{Bytes, GuestAddress};
180+
181+
use crate::arch;
182+
use crate::devices::acpi::generated::vmclock_abi::vmclock_abi;
183+
use crate::devices::acpi::vmclock::{VMCLOCK_SIZE, VmClock};
184+
use crate::snapshot::Persist;
185+
use crate::test_utils::single_region_mem;
186+
use crate::utils::u64_to_usize;
187+
use crate::vstate::resources::ResourceAllocator;
188+
189+
// We are allocating memory from the end of the system memory portion
190+
const VMCLOCK_TEST_GUEST_ADDR: GuestAddress =
191+
GuestAddress(arch::SYSTEM_MEM_START + arch::SYSTEM_MEM_SIZE - VMCLOCK_SIZE as u64);
192+
193+
fn default_vmclock() -> VmClock {
194+
let mut resource_allocator = ResourceAllocator::new();
195+
VmClock::new(&mut resource_allocator)
196+
}
197+
198+
#[test]
199+
fn test_new_device() {
200+
let vmclock = default_vmclock();
201+
let mem = single_region_mem(
202+
u64_to_usize(arch::SYSTEM_MEM_START) + u64_to_usize(arch::SYSTEM_MEM_SIZE),
203+
);
204+
205+
let guest_data: vmclock_abi = mem.read_obj(VMCLOCK_TEST_GUEST_ADDR).unwrap();
206+
assert_ne!(guest_data, vmclock.inner);
207+
208+
vmclock.activate(&mem);
209+
210+
let guest_data: vmclock_abi = mem.read_obj(VMCLOCK_TEST_GUEST_ADDR).unwrap();
211+
assert_eq!(guest_data, vmclock.inner);
212+
}
213+
214+
#[test]
215+
fn test_device_save_restore() {
216+
let vmclock = default_vmclock();
217+
let mem = single_region_mem(
218+
u64_to_usize(arch::SYSTEM_MEM_START) + u64_to_usize(arch::SYSTEM_MEM_SIZE),
219+
);
220+
221+
vmclock.activate(&mem).unwrap();
222+
let guest_data: vmclock_abi = mem.read_obj(VMCLOCK_TEST_GUEST_ADDR).unwrap();
223+
224+
let state = vmclock.save();
225+
let vmclock_new = VmClock::restore(&mem, &state).unwrap();
226+
227+
let guest_data_new: vmclock_abi = mem.read_obj(VMCLOCK_TEST_GUEST_ADDR).unwrap();
228+
assert_ne!(guest_data_new, vmclock.inner);
229+
assert_eq!(guest_data_new, vmclock_new.inner);
230+
assert_eq!(
231+
vmclock.inner.disruption_marker + 1,
232+
vmclock_new.inner.disruption_marker
233+
);
234+
}
235+
}

src/vmm/src/persist.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -579,7 +579,7 @@ mod tests {
579579
use crate::builder::tests::insert_vmgenid_device;
580580
use crate::builder::tests::{
581581
CustomBlockConfig, default_kernel_cmdline, default_vmm, insert_balloon_device,
582-
insert_block_devices, insert_net_device, insert_vsock_device,
582+
insert_block_devices, insert_net_device, insert_vmclock_device, insert_vsock_device,
583583
};
584584
#[cfg(target_arch = "aarch64")]
585585
use crate::construct_kvm_mpidrs;
@@ -638,6 +638,7 @@ mod tests {
638638

639639
#[cfg(target_arch = "x86_64")]
640640
insert_vmgenid_device(&mut vmm);
641+
insert_vmclock_device(&mut vmm);
641642

642643
vmm
643644
}

0 commit comments

Comments
 (0)