underhill_core/emuplat/
local_clock.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use 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/// Implementation of [`LocalClock`], backed a real time source on the host.
19///
20/// The linux kernel in VTL2 doesn't (currently) have any native way to track
21/// "real time" outside of VTL2, and as such, Underhill is forced to query the
22/// host whenever it needs to check the real time.
23///
24/// DEVNOTE: If VTL2 gains some kind of "notification on resume" functionality,
25/// it should be possible to avoid querying the host on each `get_time` call,
26/// and instead use VTL2-local time keeping facilities to track deltas from a
27/// single host time query.
28#[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    /// Create a new [`UnderhillLocalClock`]. Resolves immediately if provided
53    /// with `saved_state`.
54    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                        // If there is no existing host time offset, default to using
74                        // the host's provided timezone offset.
75                        //
76                        // Hosts _could_ choose to pass time as UTC, but in Hyper-V,
77                        // this is set to the host's _local_ time, as this allows
78                        // Windows guests to report the correct time on first boot.
79                        // Windows assumes the time stored in the RTC is the machine's
80                        // _local_ time, whereas Linux assume the time stored in the RTC
81                        // stores UTC.
82                        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        // prevent guests from persisting an RTC time in the distant past,
96        // which could be used to circumvent time-based licensing checks.
97        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        // persist the skew in units of 100ns
137        let raw_skew: i64 = (time::Duration::from(new_skew).whole_nanoseconds() / 100)
138            .try_into()
139            .unwrap();
140
141        // TODO: swap this out for a non-blocking version that guarantees the skew is written out _eventually_
142        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    /// Encapsulates all the nitty-gritty details of how real time gets fetched
163    /// from the Host.
164    #[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        /// Return the host's current time
197        pub fn now(&self) -> OffsetDateTime {
198            // The RTC only has 1s time granularity, so there's no reason to
199            // spam the GET with time requests if the previous request was less
200            // than a second ago.
201            //
202            // TODO: if the GET was updated to include a "on VTL2 resume"
203            // packet, we could hook into that notification to avoid having to
204            // constantly query the host over the GET to get current time (using
205            // VTL2 local time-keeping to maintain a delta since last host
206            // query).
207            //
208            // ...but this is fine for now.
209            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                    // TODO: this block_on really ain't great, but since we're
220                    // not hammering the GET on _each_ access, it's okay for now...
221                    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        // the timezone reported by the host is negative minutes from utc
242        // i.e. Localtime = UTC - TimeZone
243        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    /// Creates a new clock that is backed by the same time source.
256    ///
257    /// It is appropriate to use this method if the system is expected to have
258    /// one time source / RTC device, like a normal physical machine. It would
259    /// not be appropriate to use this method if there are multiple independent
260    /// time sources in the system. The VMGS file can only store the state for
261    /// one time source, so the time sources would trample each other without
262    /// extending the VMGS file or saving a second one.
263    pub fn new_linked_clock(&self) -> Self {
264        ArcMutexUnderhillLocalClock(self.0.clone())
265    }
266}
267
268// required for emuplat servicing optimization
269impl 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}