underhill_core/emuplat/
local_clock.rs1use self::host_time::HostSystemTimeAccess;
5use cvm_tracing::CVM_ALLOWED;
6use inspect::Inspect;
7use local_clock::LocalClock;
8use local_clock::LocalClockDelta;
9use local_clock::LocalClockTime;
10use vmcore::non_volatile_store::NonVolatileStore;
11use vmcore::non_volatile_store::NonVolatileStoreError;
12use vmcore::save_restore::SaveRestore;
13
14const NANOS_IN_SECOND: i64 = 1_000_000_000;
15const NANOS_100_IN_SECOND: i64 = NANOS_IN_SECOND / 100;
16const MILLIS_IN_TWO_DAYS: i64 = 100 * 60 * 60 * 24 * 2;
17
18#[derive(Inspect)]
29pub struct UnderhillLocalClock {
30 #[inspect(skip)]
31 store: Box<dyn NonVolatileStore>,
32 host_time: HostSystemTimeAccess,
33 offset_from_host_time: LocalClockDelta,
34}
35
36impl std::fmt::Debug for UnderhillLocalClock {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 let Self {
39 store: _,
40 host_time,
41 offset_from_host_time,
42 } = self;
43
44 f.debug_struct("UnderhillLocalClock")
45 .field("host_time", host_time)
46 .field("offset_from_host_time", offset_from_host_time)
47 .finish()
48 }
49}
50
51impl UnderhillLocalClock {
52 pub async fn new(
55 get: guest_emulation_transport::GuestEmulationTransportClient,
56 store: Box<dyn NonVolatileStore>,
57 saved_state: Option<<Self as SaveRestore>::SavedState>,
58 ) -> anyhow::Result<Self> {
59 let host_time = HostSystemTimeAccess::new(get);
60
61 let mut this = Self {
62 store,
63 host_time,
64 offset_from_host_time: LocalClockDelta::default(),
65 };
66
67 match saved_state {
68 Some(state) => this.restore(state)?,
69 None => {
70 this.offset_from_host_time = match fetch_skew_from_store(&mut this.store).await? {
71 Some(skew) => skew,
72 None => {
73 let skew = this.host_time.now().offset();
83 let skew = time::Duration::seconds(skew.whole_seconds().into());
84 tracing::info!(
85 CVM_ALLOWED,
86 ?skew,
87 "no saved skew found: defaulting to host local time"
88 );
89 skew.into()
90 }
91 }
92 }
93 };
94
95 let neg_two_days = LocalClockDelta::from_millis(-MILLIS_IN_TWO_DAYS);
98 if this.offset_from_host_time < neg_two_days {
99 this.offset_from_host_time = neg_two_days;
100 tracing::warn!(
101 CVM_ALLOWED,
102 "Guest time was more than two days in the past."
103 );
104 }
105
106 Ok(this)
107 }
108}
109
110async fn fetch_skew_from_store(
111 store: &mut dyn NonVolatileStore,
112) -> Result<Option<LocalClockDelta>, NonVolatileStoreError> {
113 let raw_skew = match store.restore().await? {
114 Some(x) => x,
115 None => return Ok(None),
116 };
117
118 let raw_skew_100ns = i64::from_le_bytes(raw_skew.try_into().expect("invalid stored RTC skew"));
119 let skew = time::Duration::new(
120 raw_skew_100ns / NANOS_100_IN_SECOND,
121 (raw_skew_100ns % NANOS_100_IN_SECOND) as i32,
122 );
123 tracing::info!(CVM_ALLOWED, ?skew, "restored existing RTC skew");
124 Ok(Some(skew.into()))
125}
126
127impl LocalClock for UnderhillLocalClock {
128 fn get_time(&mut self) -> LocalClockTime {
129 LocalClockTime::from(self.host_time.now()) + self.offset_from_host_time
130 }
131
132 fn set_time(&mut self, new_time: LocalClockTime) {
133 let new_skew = new_time - LocalClockTime::from(self.host_time.now());
134 self.offset_from_host_time = new_skew;
135
136 let raw_skew: i64 = (time::Duration::from(new_skew).whole_nanoseconds() / 100)
138 .try_into()
139 .unwrap();
140
141 let res = pal_async::local::block_on(self.store.persist(raw_skew.to_le_bytes().into()));
143 if let Err(err) = res {
144 tracing::error!(
145 CVM_ALLOWED,
146 err = &err as &dyn std::error::Error,
147 "failed to persist RTC skew"
148 );
149 }
150 }
151}
152
153mod host_time {
154 use super::NANOS_100_IN_SECOND;
155 use inspect::Inspect;
156 use parking_lot::Mutex;
157 use std::time::Duration;
158 use std::time::Instant;
159 use time::OffsetDateTime;
160 use time::UtcOffset;
161
162 #[derive(Debug)]
165 pub struct HostSystemTimeAccess {
166 get: guest_emulation_transport::GuestEmulationTransportClient,
167 cached_host_time: Mutex<Option<(Instant, OffsetDateTime)>>,
168 }
169
170 impl Inspect for HostSystemTimeAccess {
171 fn inspect(&self, req: inspect::Request<'_>) {
172 let HostSystemTimeAccess {
173 get: _,
174 cached_host_time,
175 } = self;
176
177 let mut res = req.respond();
178
179 if let Some((last_query, cached_time)) = *cached_host_time.lock() {
180 res.display_debug("since_last_query", &(Instant::now() - last_query))
181 .display("cached_time", &cached_time);
182 }
183 }
184 }
185
186 impl HostSystemTimeAccess {
187 pub fn new(
188 get: guest_emulation_transport::GuestEmulationTransportClient,
189 ) -> HostSystemTimeAccess {
190 HostSystemTimeAccess {
191 get,
192 cached_host_time: Mutex::new(None),
193 }
194 }
195
196 pub fn now(&self) -> OffsetDateTime {
198 let now = Instant::now();
210 let mut cached_host_time = self.cached_host_time.lock();
211
212 match *cached_host_time {
213 Some((last_query, cached_time))
214 if now.duration_since(last_query) < Duration::from_secs(1) =>
215 {
216 cached_time
217 }
218 _ => {
219 let new_time = get_time_to_date_time(pal_async::local::block_with_io(|_| {
222 self.get.host_time()
223 }));
224 *cached_host_time = Some((now, new_time));
225 new_time
226 }
227 }
228 }
229 }
230
231 fn get_time_to_date_time(time: guest_emulation_transport::api::Time) -> OffsetDateTime {
232 const WINDOWS_EPOCH: OffsetDateTime = time::macros::datetime!(1601-01-01 0:00 UTC);
233
234 let host_time_since_windows_epoch = time::Duration::new(
235 time.utc / NANOS_100_IN_SECOND,
236 (time.utc % NANOS_100_IN_SECOND) as i32,
237 );
238
239 let host_time_utc = WINDOWS_EPOCH + host_time_since_windows_epoch;
240
241 host_time_utc.to_offset(
244 UtcOffset::from_whole_seconds(-time.time_zone as i32 * 60)
245 .expect("unexpectedly large timezone offset"),
246 )
247 }
248}
249
250#[derive(Debug, Inspect)]
251#[inspect(transparent)]
252pub struct ArcMutexUnderhillLocalClock(pub std::sync::Arc<parking_lot::Mutex<UnderhillLocalClock>>);
253
254impl ArcMutexUnderhillLocalClock {
255 pub fn new_linked_clock(&self) -> Self {
264 ArcMutexUnderhillLocalClock(self.0.clone())
265 }
266}
267
268impl LocalClock for ArcMutexUnderhillLocalClock {
270 fn get_time(&mut self) -> LocalClockTime {
271 self.0.lock().get_time()
272 }
273
274 fn set_time(&mut self, new_time: LocalClockTime) {
275 self.0.lock().set_time(new_time)
276 }
277}
278
279mod save_restore {
280 use super::*;
281 use vmcore::save_restore::RestoreError;
282 use vmcore::save_restore::SaveError;
283 use vmcore::save_restore::SaveRestore;
284
285 mod state {
286 use mesh::payload::Protobuf;
287 use vmcore::save_restore::SavedStateRoot;
288
289 #[derive(Protobuf, SavedStateRoot)]
290 #[mesh(package = "underhill.emuplat.local_clock")]
291 pub struct SavedState {
292 #[mesh(1)]
293 pub offset_from_host_time_millis: i64,
294 }
295 }
296
297 impl SaveRestore for UnderhillLocalClock {
298 type SavedState = state::SavedState;
299
300 fn save(&mut self) -> Result<Self::SavedState, SaveError> {
301 Ok(state::SavedState {
302 offset_from_host_time_millis: self.offset_from_host_time.as_millis(),
303 })
304 }
305
306 fn restore(&mut self, state: Self::SavedState) -> Result<(), RestoreError> {
307 let state::SavedState {
308 offset_from_host_time_millis,
309 } = state;
310
311 self.offset_from_host_time = LocalClockDelta::from_millis(offset_from_host_time_millis);
312
313 Ok(())
314 }
315 }
316}