Skip to main content

clock_bound/
shm.rs

1//! ClockBound Shared Memory
2//!
3//! This crate implements the low-level IPC functionality to share `ClockErrorBound` data and clock
4//! status over a shared memory segment. This crate is meant to be used by the C and Rust versions
5//! of the ClockBound client library.
6
7// TODO: prevent clippy from checking for dead code. The writer module is only re-exported publicly
8// if the write feature is selected. There may be a better way to do that and re-enable the lint.
9#![expect(dead_code)]
10
11pub mod common;
12mod reader;
13mod shm_header;
14mod tsc;
15mod writer;
16
17// Re-exports reader and writer. The writer is conditionally included under the "writer" feature.
18use common::{CLOCK_MONOTONIC, CLOCK_REALTIME, clock_gettime_safe};
19pub use reader::ShmReader;
20use tsc::read_timestamp_counter_begin;
21pub use writer::{ShmWrite, ShmWriter};
22
23use bon::Builder;
24use errno::Errno;
25use nix::sys::time::{TimeSpec, TimeValLike};
26use std::error::Error;
27use std::fmt;
28
29pub const CLOCKBOUND_SHM_DEFAULT_PATH_V0: &str = "/var/run/clockbound/shm0";
30pub const CLOCKBOUND_SHM_DEFAULT_PATH_V1: &str = "/var/run/clockbound/shm1";
31pub const CLOCKBOUND_SHM_CLIENT_DEFAULT_PATH: &str = CLOCKBOUND_SHM_DEFAULT_PATH_V1;
32
33const FREE_RUNNING_GRACE_PERIOD: TimeSpec = TimeSpec::new(60, 0);
34const NANOS_PER_SECOND: f64 = 1_000_000_000.0;
35
36/// Convenience macro to build a `ShmError::SyscallError` with extra info from errno and custom
37/// origin information.
38#[macro_export]
39macro_rules! syserror {
40    ($msg:expr) => {
41        Err($crate::shm::ShmError::SyscallError($msg, ::errno::errno()))
42    };
43}
44
45pub trait ClockBoundSnapshot {
46    /// The `ClockErrorBound` equivalent of `clock_gettime()`, but with bound on accuracy.
47    ///
48    /// Returns a `ClockBoundNowResult` with contains the (earliest, latest) timespec between which
49    /// current time exists. The interval width is twice the clock error bound (ceb) such that:
50    ///   (earliest, latest) = ((now - ceb), (now + ceb))
51    ///
52    /// The function also returns a clock status to assert that the clock is being synchronized, or
53    /// free-running, or ...
54    #[expect(clippy::missing_errors_doc, reason = "todo")]
55    fn now(&self) -> Result<ClockBoundNowResult, ShmError>;
56}
57
58/// Enum that holds supported layout of the `ClockErrorBound` stored in the ClockBound daemon
59/// shared memory segment.
60#[derive(Debug, Copy, Clone, PartialEq)]
61pub enum ClockErrorBound {
62    V2(ClockErrorBoundV2),
63    V3(ClockErrorBoundV3),
64}
65
66impl ClockErrorBound {
67    pub fn as_of(&self) -> TimeSpec {
68        match self {
69            ClockErrorBound::V2(ceb) => ceb.as_of,
70            ClockErrorBound::V3(ceb) => ceb.as_of,
71        }
72    }
73
74    pub fn void_after(&self) -> TimeSpec {
75        match self {
76            ClockErrorBound::V2(ceb) => ceb.void_after,
77            ClockErrorBound::V3(ceb) => ceb.void_after,
78        }
79    }
80
81    pub fn bound_nsec(&self) -> i64 {
82        match self {
83            ClockErrorBound::V2(ceb) => ceb.bound_nsec,
84            ClockErrorBound::V3(ceb) => ceb.bound_nsec,
85        }
86    }
87
88    pub fn max_drift_ppb(&self) -> u32 {
89        match self {
90            ClockErrorBound::V2(ceb) => ceb.max_drift_ppb,
91            ClockErrorBound::V3(ceb) => ceb.max_drift_ppb,
92        }
93    }
94
95    pub fn clock_status(&self) -> ClockStatus {
96        match self {
97            ClockErrorBound::V2(ceb) => ceb.clock_status,
98            ClockErrorBound::V3(ceb) => ceb.clock_status,
99        }
100    }
101
102    pub fn disruption_marker(&self) -> u64 {
103        match self {
104            ClockErrorBound::V2(ceb) => ceb.disruption_marker,
105            ClockErrorBound::V3(ceb) => ceb.disruption_marker,
106        }
107    }
108
109    pub fn clock_disruption_support_enabled(&self) -> bool {
110        match self {
111            ClockErrorBound::V2(ceb) => ceb.clock_disruption_support_enabled,
112            ClockErrorBound::V3(ceb) => ceb.clock_disruption_support_enabled,
113        }
114    }
115}
116
117impl ClockBoundSnapshot for ClockErrorBound {
118    fn now(&self) -> Result<ClockBoundNowResult, ShmError> {
119        match self {
120            ClockErrorBound::V2(ceb) => ceb.now(),
121            ClockErrorBound::V3(ceb) => ceb.now(),
122        }
123    }
124}
125
126/// Generic `ClockErrorBound` builder.
127#[derive(Builder)]
128// Rename auto-generated build() function into build_internal so we have a custom finishing
129// function to create the enum variants
130#[builder(finish_fn(vis = "", name = build_internal))]
131pub struct ClockErrorBoundGeneric {
132    #[builder(default)]
133    as_of_tsc: u64,
134
135    #[builder(default = TimeSpec::new(0, 0))]
136    as_of: TimeSpec,
137
138    #[builder(default = TimeSpec::new(0, 0))]
139    void_after: TimeSpec,
140
141    #[builder(default)]
142    bound_nsec: i64,
143
144    #[builder(default)]
145    period: f64,
146
147    #[builder(default)]
148    period_err: f64,
149
150    #[builder(default)]
151    disruption_marker: u64,
152
153    #[builder(default)]
154    max_drift_ppb: u32,
155
156    #[builder(default = ClockStatus::Unknown)]
157    clock_status: ClockStatus,
158
159    #[builder(default)]
160    clock_disruption_support_enabled: bool,
161}
162
163impl<S: clock_error_bound_generic_builder::IsComplete> ClockErrorBoundGenericBuilder<S> {
164    /// Custom `build` finishing function on the generated `ClockErrorBoundLayoutBuilder`.
165    ///
166    /// Take the layout version number as a parameter, it is a u16 to ease casting of the earlier
167    /// version of the `SHMHeader`.
168    pub fn build(self, layout_version: ClockErrorBoundLayoutVersion) -> ClockErrorBound {
169        // Build the ClockErrorBoundGeneric object
170        let ceb = self.build_internal();
171
172        // Build the specific version of the ClockErrorBound
173        match layout_version {
174            ClockErrorBoundLayoutVersion::V2 => ClockErrorBound::V2(ClockErrorBoundV2::new(
175                ceb.as_of,
176                ceb.void_after,
177                ceb.bound_nsec,
178                ceb.disruption_marker,
179                ceb.max_drift_ppb,
180                ceb.clock_status,
181                ceb.clock_disruption_support_enabled,
182            )),
183            ClockErrorBoundLayoutVersion::V3 => ClockErrorBound::V3(ClockErrorBoundV3::new(
184                ceb.as_of_tsc,
185                ceb.as_of,
186                ceb.void_after,
187                ceb.period,
188                ceb.period_err,
189                ceb.bound_nsec,
190                ceb.disruption_marker,
191                ceb.max_drift_ppb,
192                ceb.clock_status,
193                ceb.clock_disruption_support_enabled,
194            )),
195        }
196    }
197}
198
199#[derive(Copy, Clone)]
200pub enum ClockErrorBoundLayoutVersion {
201    V2,
202    V3,
203}
204
205impl TryFrom<u8> for ClockErrorBoundLayoutVersion {
206    type Error = ShmError;
207    fn try_from(value: u8) -> Result<Self, ShmError> {
208        match value {
209            2 => Ok(ClockErrorBoundLayoutVersion::V2),
210            3 => Ok(ClockErrorBoundLayoutVersion::V3),
211            _ => Err(ShmError::SegmentVersionNotSupported(format!(
212                "Found version {value}",
213            ))),
214        }
215    }
216}
217
218impl TryFrom<u16> for ClockErrorBoundLayoutVersion {
219    type Error = ShmError;
220    fn try_from(value: u16) -> Result<Self, ShmError> {
221        match value {
222            2 => Ok(ClockErrorBoundLayoutVersion::V2),
223            3 => Ok(ClockErrorBoundLayoutVersion::V3),
224            _ => Err(ShmError::SegmentVersionNotSupported(format!(
225                "Found version {value}",
226            ))),
227        }
228    }
229}
230
231impl From<ClockErrorBoundLayoutVersion> for u16 {
232    fn from(value: ClockErrorBoundLayoutVersion) -> Self {
233        match value {
234            ClockErrorBoundLayoutVersion::V2 => 2,
235            ClockErrorBoundLayoutVersion::V3 => 3,
236        }
237    }
238}
239
240/// Result of the `ClockBoundClient::now()` function.
241#[derive(PartialEq, Clone, Debug)]
242pub struct ClockBoundNowResult {
243    pub earliest: TimeSpec,
244    pub latest: TimeSpec,
245    pub clock_status: ClockStatus,
246}
247
248/// Error condition returned by all low-level ClockBound APIs.
249///
250#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
251pub enum ShmError {
252    /// A system call failed.
253    /// Variant includes the Errno struct with error details, and an indication on the origin of
254    /// the system call that error'ed.
255    SyscallError(String, Errno),
256
257    /// The shared memory segment is not initialized.
258    SegmentNotInitialized(String),
259
260    /// The shared memory segment is initialized but malformed.
261    SegmentMalformed(String),
262
263    /// Failed causality check when comparing timestamps.
264    CausalityBreach(String),
265
266    /// The shared memory segment version is not supported.
267    SegmentVersionNotSupported(String),
268}
269
270impl fmt::Display for ShmError {
271    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272        match self {
273            ShmError::SyscallError(msg, errno) => {
274                write!(f, "Errno: {errno:?} Details: {msg}")
275            }
276            ShmError::SegmentNotInitialized(msg) => {
277                write!(f, "The shared memory segment is not initialized [{msg}].")
278            }
279            ShmError::SegmentMalformed(msg) => {
280                write!(
281                    f,
282                    "The shared memory segment is initialized but malformed [{msg}]."
283                )
284            }
285            ShmError::CausalityBreach(msg) => {
286                write!(
287                    f,
288                    "Failed causality check when comparing timestamps [{msg}]."
289                )
290            }
291            ShmError::SegmentVersionNotSupported(msg) => {
292                write!(
293                    f,
294                    "The shared memory segment version is not supported [{msg}]."
295                )
296            }
297        }
298    }
299}
300
301impl Error for ShmError {}
302
303/// Definition of mutually exclusive clock status exposed to the reader.
304///
305/// Note the data layout is explicitly set to i32. This enum is a field of the ClockBound shared
306/// memory segment, and its representation *may* be different for C code compiled with specific
307/// flags. Making it explicit removes this risk and ambiguity.
308#[repr(i32)]
309#[derive(Debug, Copy, Clone, PartialEq)]
310pub enum ClockStatus {
311    /// The status of the clock is unknown.
312    /// In this clock status, error-bounded timestamps should not be trusted.
313    Unknown = 0,
314
315    /// The clock is kept accurate by the synchronization daemon.
316    /// In this clock status, error-bounded timestamps can be trusted.
317    Synchronized = 1,
318
319    /// The clock is free running and not updated by the synchronization daemon.
320    /// In this clock status, error-bounded timestamps can be trusted.
321    FreeRunning = 2,
322
323    /// The clock has been disrupted and the accuracy of time cannot be bounded.
324    /// In this clock status, error-bounded timestamps should not be trusted.
325    Disrupted = 3,
326}
327
328/// Structure that holds the `ClockErrorBound` data captured at a specific point in time and valid
329/// until a subsequent point in time.
330///
331/// The `ClockErrorBound` structure supports calculating the actual bound on clock error at any time,
332/// using its `now()` method. The internal fields are not meant to be accessed directly.
333///
334/// Note that the timestamps in between which this `ClockErrorBound` data is valid are captured using
335/// a `CLOCK_MONOTONIC_COARSE` clock. The monotonic clock id is required to correctly measure the
336/// duration during which clock drift possibly accrues, and avoid events when the clock is set,
337/// smeared or affected by leap seconds.
338///
339/// The structure is shared across the Shared Memory segment and has a C representation to enforce
340/// this specific layout.
341#[repr(C)]
342#[derive(Debug, Copy, Clone, PartialEq)]
343pub struct ClockErrorBoundV2 {
344    /// The `CLOCK_MONOTONIC_COARSE` timestamp recorded when the bound on clock error was
345    /// calculated. The current implementation relies on Chrony tracking data, which accounts for
346    /// the dispersion between the last clock processing event, and the reading of tracking data.
347    as_of: TimeSpec,
348
349    /// The `CLOCK_MONOTONIC_COARSE` timestamp beyond which the bound on clock error should not be
350    /// trusted. This is a useful signal that the communication with the synchronization daemon is
351    /// has failed, for example.
352    void_after: TimeSpec,
353
354    /// An absolute upper bound on the accuracy of the `CLOCK_REALTIME` clock with regards to true
355    /// time at the instant represented by `as_of`.
356    bound_nsec: i64,
357
358    /// Disruption marker.
359    ///
360    /// This value is incremented (by an unspecified delta) each time the clock has been disrupted.
361    /// This count value is specific to a particular VM/EC2 instance.
362    disruption_marker: u64,
363
364    /// Maximum drift rate of the clock between updates of the synchronization daemon. The value
365    /// stored in `bound_nsec` should increase by the following to account for the clock drift
366    /// since `bound_nsec` was computed:
367    /// `bound_nsec += max_drift_ppb * (now - as_of)`
368    max_drift_ppb: u32,
369
370    /// The synchronization daemon status indicates whether the daemon is synchronized,
371    /// free-running, etc.
372    clock_status: ClockStatus,
373
374    /// Clock disruption support enabled flag.
375    ///
376    /// This indicates whether or not the ClockBound daemon was started with a
377    /// configuration that supports detecting clock disruptions.
378    clock_disruption_support_enabled: bool,
379
380    /// Padding.
381    _padding: [u8; 7],
382}
383
384impl ClockErrorBoundV2 {
385    /// Create a new `ClockErrorBound` struct.
386    pub fn new(
387        as_of: TimeSpec,
388        void_after: TimeSpec,
389        bound_nsec: i64,
390        disruption_marker: u64,
391        max_drift_ppb: u32,
392        clock_status: ClockStatus,
393        clock_disruption_support_enabled: bool,
394    ) -> ClockErrorBoundV2 {
395        ClockErrorBoundV2 {
396            as_of,
397            void_after,
398            bound_nsec,
399            disruption_marker,
400            max_drift_ppb,
401            clock_status,
402            clock_disruption_support_enabled,
403            _padding: [0u8; 7],
404        }
405    }
406
407    /// The `ClockErrorBoundV2` implementation of `now()`, a `clock_gettime()` equivalent but with
408    /// bound on clock accuracy.
409    ///
410    /// Returns a pair of (earliest, latest) timespec between which current time exists. The
411    /// interval width is twice the clock error bound (ceb) such that:
412    ///   (earliest, latest) = ((now - ceb), (now + ceb))
413    /// The function also returns a clock status to assert that the clock is being synchronized, or
414    /// free-running, or ...
415    #[expect(clippy::missing_errors_doc, reason = "todo")]
416    pub fn now(&self) -> Result<ClockBoundNowResult, ShmError> {
417        // Read the clock, start with the REALTIME one to be as close as possible to the event the
418        // caller is interested in. The monotonic clock should be read after. It is correct for the
419        // process be preempted between the two calls: a delayed read of the monotonic clock will
420        // make the bound on clock error more pessimistic, but remains correct.
421        let real = clock_gettime_safe(CLOCK_REALTIME)?;
422        let mono = clock_gettime_safe(CLOCK_MONOTONIC)?;
423
424        self.compute_bound_at(real, mono)
425    }
426
427    /// Compute the bound on clock error at a given point in time.
428    ///
429    /// The time at which the bound is computed is defined by the (real, mono) pair of timestamps
430    /// read from the realtime and monotonic clock respectively, *roughly* at the same time. The
431    /// details to correctly work around the "rough" alignment of the timestamps is not something
432    /// we want to leave to the user of ClockBound, hence this method is private. Although `now()`
433    /// may be it only caller, decoupling the two make writing unit tests a bit easier.
434    #[expect(
435        clippy::cast_precision_loss,
436        clippy::cast_possible_truncation,
437        reason = "todo, come back and evaluate impact"
438    )]
439    fn compute_bound_at(
440        &self,
441        real: TimeSpec,
442        mono: TimeSpec,
443    ) -> Result<ClockBoundNowResult, ShmError> {
444        // Sanity checks:
445        // - `now()` should operate on a consistent snapshot of the shared memory segment, and
446        //   causality between mono and as_of should be enforced.
447        // - a extremely high value of the `max_drift_ppb` is a sign of something going wrong
448        if self.max_drift_ppb >= 1_000_000_000 {
449            return Err(ShmError::SegmentMalformed(format!(
450                "max_drift_ppb too large [{}]",
451                self.max_drift_ppb,
452            )));
453        }
454
455        // If the ClockErrorBound data has not been updated "recently", the status of the clock
456        // cannot be guaranteed. Things are ambiguous, the synchronization daemon may be dead, or
457        // its interaction with the clockbound daemon is broken, or ... In any case, we signal the
458        // caller that guarantees are gone. We could return an Err here, but choosing to leverage
459        // ClockStatus instead, and putting the responsibility on the caller to check the clock
460        // status value being returned.
461        // TODO: this may not be the most ergonomic decision, putting a pin here to revisit this
462        // decision once the client code is fleshed out.
463        let clock_status = match self.clock_status {
464            // If the status in the shared memory segment is Unknown or Disrupted, returns that
465            // status.
466            ClockStatus::Unknown | ClockStatus::Disrupted => self.clock_status,
467
468            // If the status is Synchronized or FreeRunning, the expectation from the client is
469            // that the data is useable. However, if the clockbound daemon died or has not update
470            // the shared memory segment in a while, the status written to the shared memory
471            // segment may not be reliable anymore.
472            ClockStatus::Synchronized | ClockStatus::FreeRunning => {
473                if mono > self.void_after {
474                    // The last update is old and beyond the horizon defined by the daemon, no
475                    // guarantee is provided anymore, hence report Unknown status.
476                    ClockStatus::Unknown
477                } else if mono > self.as_of + FREE_RUNNING_GRACE_PERIOD {
478                    // The last update is too old to be trusted to be synchronized, reports Free
479                    // Running status.
480                    ClockStatus::FreeRunning
481                } else {
482                    // The last update is recent enough, hence report it
483                    self.clock_status
484                }
485            }
486        };
487
488        // Calculate the duration that has elapsed between the instant when the CEB parameters were
489        // snapshot'ed from the SHM segment (approximated by `as_of`), and the instant when the
490        // request to calculate the CEB was actually requested (approximated by `mono`). This
491        // duration is used to compute the growth of the error bound due to local dispersion
492        // between polling chrony and now.
493        //
494        // To avoid miscalculation in case the synchronization daemon is restarted, a
495        // CLOCK_MONOTONIC is used, since it is designed to not jump. Because we want this to be
496        // fast, and the exact accuracy is not critical here, we use CLOCK_MONOTONIC_COARSE on
497        // platforms that support it.
498        //
499        // But ... there is a catch. When validating causality of these events that is, `as_of`
500        // should always be older than `mono`, we observed this test to sometimes fail, with `mono`
501        // being older by a handful of nanoseconds. The root cause is not completely understood,
502        // but points to the clock resolution and/or update strategy and/or propagation of the
503        // updates through the VDSO memory page. See this for details:
504        // https://t.corp.amazon.com/P101954401.
505        //
506        // The following implementation is a mitigation.
507        //   1. if as_of <= mono is younger than as_of, calculate the duration (happy path)
508        //   2. if as_of - epsilon < mono < as_of, set the duration to 0
509        //   3. if mono < as_of - epsilon, return an error
510        //
511        // In short, this relaxes the sanity check a bit to accept some imprecision in the clock
512        // reading routines.
513        //
514        // What is a good value for `epsilon`?
515        // The CLOCK_MONOTONIC_COARSE resolution is a function of the HZ kernel variable defining
516        // the last kernel tick that drives this clock (e.g. HZ=250 leads to a 4 millisecond
517        // resolution). We could use the `clock_getres()` system call to retrieve this value but
518        // this makes diagnosing over different platform / OS configurations more complex. Instead
519        // settling on an arbitrary default value of 1 millisecond.
520        let causality_blur = self.as_of - TimeSpec::new(0, 1000);
521
522        let duration = if mono >= self.as_of {
523            // Happy path, no causality doubt
524            mono - self.as_of
525        } else if mono > causality_blur {
526            // Causality is "almost" broken. We are within a range that could be due to the clock
527            // precision. Let's approximate this to equality between mono and as_of.
528            TimeSpec::new(0, 0)
529        } else {
530            // Causality is breached.
531            return Err(ShmError::CausalityBreach(format!(
532                "as_of ({:?}) more recent than {:?}",
533                self.as_of, mono
534            )));
535        };
536
537        // Inflate the bound on clock error with the maximum drift the clock may be experiencing
538        // between the snapshot being read and ~now.
539        let duration_sec = duration.num_nanoseconds() as f64 / 1_000_000_000_f64;
540        let updated_bound = TimeSpec::nanoseconds(
541            self.bound_nsec + (duration_sec * f64::from(self.max_drift_ppb)) as i64,
542        );
543
544        // Build the (earliest, latest) interval within which true time exists.
545        let earliest = real - updated_bound;
546        let latest = real + updated_bound;
547
548        Ok(ClockBoundNowResult {
549            earliest,
550            latest,
551            clock_status,
552        })
553    }
554}
555
556impl ClockBoundSnapshot for ClockErrorBoundV2 {
557    /// The `ClockErrorBoundV2` implementation of `now()`.
558    ///
559    /// This version relies on the system clock to retrieve the current time as well as grow the
560    /// bound on the clock error at a constant rate.
561    fn now(&self) -> Result<ClockBoundNowResult, ShmError> {
562        // Read the clock, start with the REALTIME one to be as close as possible to the event the
563        // caller is interested in. The monotonic clock should be read after. It is correct for the
564        // process be preempted between the two calls: a delayed read of the monotonic clock will
565        // make the bound on clock error more pessimistic, but remains correct.
566        let real = clock_gettime_safe(CLOCK_REALTIME)?;
567        let mono = clock_gettime_safe(CLOCK_MONOTONIC)?;
568
569        self.compute_bound_at(real, mono)
570    }
571}
572
573/// Structure that holds the `ClockErrorBound` data captured at a specific point in time and valid
574/// until a subsequent point in time.
575///
576/// The `ClockErrorBound` structure supports calculating the actual bound on clock error at any time,
577/// using its `now()` method. The internal fields are not meant to be accessed directly.
578///
579/// Note that this version of the layout allow to not use the OS system clock to retrieve the
580/// current time or grow the clock error bound.
581///
582/// The structure is shared across the Shared Memory segment and has a C representation to enforce
583/// this specific layout.
584#[repr(C)]
585#[derive(Debug, Copy, Clone, PartialEq)]
586pub struct ClockErrorBoundV3 {
587    /// TSC counter value identifying this clock update.
588    ///
589    /// The TSC counter timestamp marking the time the clock and the clock error bound where
590    /// updated last. It represents the same instant as `as_of`.
591    as_of_tsc: u64,
592
593    /// Timestamp of this clock update.
594    ///
595    /// The nanosecond resolution timestamp marking the time the clock and the clock error bound
596    /// where updated last. This timestamp is derived from `as_of_tsc`.
597    as_of: TimeSpec,
598
599    /// Time after which this clock update is void.
600    ///
601    /// The nanosecond timestamp beyond which the bound on clock error should not be trusted. This
602    /// is a useful signal that the communication with the synchronization daemon is has failed,
603    /// for example.
604    void_after: TimeSpec,
605
606    /// Oscillator period estimate.
607    ///
608    /// The period of the oscillator, represented as a fractional part of a second.
609    period_frac: u64,
610
611    /// Oscillator period estimate error.
612    ///
613    /// The error on the estimate of the period of the oscillator, in ppb, represented as a
614    /// fractional part of a second.
615    period_err_frac: u64,
616
617    /// Clock Error Bound
618    ///
619    /// An absolute upper bound on the accuracy of the feed-forward synchronization clock with
620    /// regards to true time at the instant represented by `as_of` and `as_of_tsc`.
621    bound_nsec: i64,
622
623    /// Disruption marker.
624    ///
625    /// This value is incremented (by an unspecified delta) each time the clock has been disrupted.
626    /// This count value is specific to a particular VM/EC2 instance.
627    disruption_marker: u64,
628
629    /// Maximum drift rate in part-per-billion.
630    ///
631    /// Maximum drift rate of the clock between updates of the synchronization daemon. The value
632    /// stored in `bound_nsec` should increase by the following to account for the clock drift
633    /// since `bound_nsec` was computed:
634    /// `bound_nsec += max_drift_ppb * (now - as_of)`
635    max_drift_ppb: u32,
636
637    /// Clock status.
638    ///
639    /// The synchronization daemon status indicates whether the daemon is synchronized,
640    /// free-running, etc.
641    clock_status: ClockStatus,
642
643    /// Clock disruption support enabled flag.
644    ///
645    /// This indicates whether or not the ClockBound daemon was started with a
646    /// configuration that supports detecting clock disruptions.
647    clock_disruption_support_enabled: bool,
648
649    /// Period shift
650    ///
651    /// This is a scaling parameter to convert the `period` into a fractional representation with
652    /// significant digits.
653    period_shift: u8,
654
655    /// Period error shift
656    ///
657    /// This is a scaling parameter to convert the `period_err` into a fractional representation with
658    /// significant digits.
659    period_err_shift: u8,
660
661    /// Padding.
662    _padding: [u8; 5],
663}
664
665impl ClockErrorBoundV3 {
666    /// Create a new `ClockErrorBound` struct.
667    #[allow(clippy::too_many_arguments)]
668    pub fn new(
669        as_of_tsc: u64,
670        as_of: TimeSpec,
671        void_after: TimeSpec,
672        period: f64,
673        period_err: f64,
674        bound_nsec: i64,
675        disruption_marker: u64,
676        max_drift_ppb: u32,
677        clock_status: ClockStatus,
678        clock_disruption_support_enabled: bool,
679    ) -> ClockErrorBoundV3 {
680        // Convert period and period_err into u64 representation
681        let p_frac = PeriodFrac::from(period);
682        let p_err_frac = PeriodFrac::from(period_err);
683
684        ClockErrorBoundV3 {
685            as_of_tsc,
686            as_of,
687            void_after,
688            period_frac: p_frac.frac,
689            period_err_frac: p_err_frac.frac,
690            bound_nsec,
691            disruption_marker,
692            max_drift_ppb,
693            clock_status,
694            clock_disruption_support_enabled,
695            period_shift: p_frac.shift,
696            period_err_shift: p_err_frac.shift,
697            _padding: [0u8; 5],
698        }
699    }
700
701    /// Get the oscillator period as a floating point value in seconds.
702    fn period(&self) -> f64 {
703        f64::from(PeriodFrac {
704            frac: self.period_frac,
705            shift: self.period_shift,
706        })
707    }
708
709    /// Get the oscillator period error as a floating point value.
710    fn period_err(&self) -> f64 {
711        f64::from(PeriodFrac {
712            frac: self.period_err_frac,
713            shift: self.period_err_shift,
714        })
715    }
716
717    #[allow(clippy::cast_precision_loss)]
718    #[allow(clippy::cast_possible_truncation)]
719    fn compute_bound_at_tsc(&self, now_tsc: u64) -> Result<ClockBoundNowResult, ShmError> {
720        // Sanity checks:
721        // - `now()` should operate on a consistent snapshot of the shared memory segment, and
722        //   causality between mono and as_of should be enforced.
723        // - a extremely high value of the `max_drift_ppb` is a sign of something going wrong
724        if self.max_drift_ppb >= 1_000_000_000 {
725            return Err(ShmError::SegmentMalformed(format!(
726                "max_drift_ppb too large: [{}]",
727                self.max_drift_ppb,
728            )));
729        }
730
731        // Compute the number of TSC cycles between now and the instant the ff-sync clock was
732        // updated last. This is computed of a TSC value stored in the snapshot, hence this
733        // duration should never be negative.
734        let duration_tsc = now_tsc.saturating_sub(self.as_of_tsc);
735        let duration = duration_tsc as f64 * self.period();
736        let duration_nsec = duration_tsc as f64 * self.period() * NANOS_PER_SECOND;
737
738        // Convert the TSC timestamp into seconds with a linear projection.
739        let now = TimeSpec::nanoseconds(
740            self.as_of.tv_nsec()
741                + NANOS_PER_SECOND as i64 * self.as_of.tv_sec()
742                + duration_nsec as i64,
743        );
744
745        // Similarly, need to grow the bound on the clock error since the last update.
746        //
747        // First, amount for the underlying oscillator drifts (possibly at a worse
748        // possible rate) in between consecutive clock adjustments.
749        let oscillator_err_nsec = duration * f64::from(self.max_drift_ppb);
750        // And take into account the fact that the ff-sync period is an estimate (polluted by
751        // measurement noise).
752        let p_estimate_err_nsec = duration_nsec * self.period_err();
753
754        let updated_bound = TimeSpec::nanoseconds(
755            (self.bound_nsec as f64 + oscillator_err_nsec + p_estimate_err_nsec) as i64,
756        );
757
758        // Build the (earliest, latest) interval within which true time exists.
759        let earliest = now - updated_bound;
760        let latest = now + updated_bound;
761
762        // If the ClockErrorBound data has not been updated "recently", the status of the clock
763        // cannot be guaranteed. Things are ambiguous, the synchronization daemon may be dead, or
764        // its interaction with the clockbound daemon is broken, or ... In any case, we signal the
765        // caller that guarantees are gone. We could return an Err here, but choosing to leverage
766        // ClockStatus instead, and putting the responsibility on the caller to check the clock
767        // status value being returned.
768        let clock_status = match self.clock_status {
769            // If the status in the shared memory segment is Unknown or Disrupted, returns that
770            // status.
771            ClockStatus::Unknown | ClockStatus::Disrupted => self.clock_status,
772
773            // If the status is Synchronized or FreeRunning, the expectation from the client is
774            // that the data is useable. However, if the clockbound daemon died or has not update
775            // the shared memory segment in a while, the status written to the shared memory
776            // segment may not be reliable anymore.
777            ClockStatus::Synchronized | ClockStatus::FreeRunning => {
778                if now > self.void_after {
779                    // The last update is old and beyond the horizon defined by the daemon, no
780                    // guarantee is provided anymore, hence report Unknown status.
781                    ClockStatus::Unknown
782                } else if now > self.as_of + FREE_RUNNING_GRACE_PERIOD {
783                    // The last update is too old to be trusted to be synchronized, reports Free
784                    // Running status.
785                    ClockStatus::FreeRunning
786                } else {
787                    // The last update is recent enough, hence report it
788                    self.clock_status
789                }
790            }
791        };
792
793        Ok(ClockBoundNowResult {
794            earliest,
795            latest,
796            clock_status,
797        })
798    }
799}
800
801impl ClockBoundSnapshot for ClockErrorBoundV3 {
802    /// The `ClockErrorBoundV3` implementation of `now()`.
803    ///
804    /// This version relies on the system clock to retrieve the current time as well as grow the
805    /// bound on the clock error at a constant rate.
806    fn now(&self) -> Result<ClockBoundNowResult, ShmError> {
807        let now_tsc = read_timestamp_counter_begin();
808        self.compute_bound_at_tsc(now_tsc)
809    }
810}
811
812struct PeriodFrac {
813    frac: u64,
814    shift: u8,
815}
816
817impl PeriodFrac {
818    /// Calculate the multiplication factor to maximize the number of significant digits when
819    /// converting the period from a floating point to an integer representation.
820    ///
821    /// # Panic:
822    /// Panic if the period passed is larger that 1 second.
823    ///
824    #[allow(clippy::cast_precision_loss)]
825    #[allow(clippy::cast_possible_truncation)]
826    #[allow(clippy::cast_sign_loss)]
827    fn calculate_frac_shift(period: f64) -> u8 {
828        // 1HZ and slower should not be seen.
829        assert!(
830            period < 1.0,
831            "Cannot convert period larger than 1 second: {period}"
832        );
833
834        // Protects against the case where a zero period is passed in.
835        if period == 0_f64 {
836            return 0_u8;
837        }
838        let freq: u64 = (1.0 / period) as u64;
839        // Cast: at most 64 zeros in a u64, hence can never go over u8::MAX.
840        (64 - freq.leading_zeros() - 1) as u8
841    }
842}
843
844impl From<f64> for PeriodFrac {
845    #[allow(clippy::cast_precision_loss)]
846    #[allow(clippy::cast_possible_truncation)]
847    #[allow(clippy::cast_sign_loss)]
848    fn from(value: f64) -> Self {
849        let shift = PeriodFrac::calculate_frac_shift(value);
850        // Cast: 64 + 255 unsigned does fit into a i32 without risk of sign error
851        let scale = 64 + i32::from(shift);
852        let frac = (value * 2_f64.powi(scale)) as u64;
853        PeriodFrac { frac, shift }
854    }
855}
856
857impl From<PeriodFrac> for f64 {
858    #[allow(clippy::cast_precision_loss)]
859    #[allow(clippy::cast_possible_truncation)]
860    fn from(value: PeriodFrac) -> Self {
861        let denominator = 2_f64.powi(64 + i32::from(value.shift));
862        (value.frac as f64) / denominator
863    }
864}
865
866#[cfg(test)]
867mod t_lib {
868    use super::*;
869
870    // Convenience macro to build ClockBoundError for unit tests
871    macro_rules! clockbound_v2 {
872        (($asof_tv_sec:literal, $asof_tv_nsec:literal), ($after_tv_sec:literal, $after_tv_nsec:literal)) => {
873            ClockErrorBoundV2::new(
874                TimeSpec::new($asof_tv_sec, $asof_tv_nsec), // as_of
875                TimeSpec::new($after_tv_sec, $after_tv_nsec), // void_after
876                10000,                                      // bound_nsec
877                0,                                          // disruption_marker
878                1000,                                       // max_drift_ppb
879                ClockStatus::Synchronized,                  // clock_status
880                true,                                       // clock_disruption_support_enabled
881            )
882        };
883    }
884
885    /// Assert the bound on clock error is computed correctly
886    #[test]
887    fn compute_bound_ok() {
888        let ceb = clockbound_v2!((0, 0), (10, 0));
889        let real = TimeSpec::new(2, 0);
890        let mono = TimeSpec::new(2, 0);
891
892        let ClockBoundNowResult {
893            earliest,
894            latest,
895            clock_status,
896        } = ceb
897            .compute_bound_at(real, mono)
898            .expect("Failed to compute bound");
899
900        // 2 seconds have passed since the bound was snapshot, hence 2 microsec of drift on top of
901        // the default 10 microsec put in the ClockBoundError data
902        assert_eq!(earliest.tv_sec(), 1);
903        assert_eq!(earliest.tv_nsec(), 1_000_000_000 - 12_000);
904        assert_eq!(latest.tv_sec(), 2);
905        assert_eq!(latest.tv_nsec(), 12_000);
906        assert_eq!(clock_status, ClockStatus::Synchronized);
907    }
908
909    /// Assert the bound on clock error is computed correctly, with realtime and monotonic clocks
910    /// disagreeing on time
911    #[test]
912    fn compute_bound_ok_when_real_ahead() {
913        let ceb = clockbound_v2!((0, 0), (10, 0));
914        let real = TimeSpec::new(20, 0); // realtime clock way ahead
915        let mono = TimeSpec::new(4, 0);
916
917        let ClockBoundNowResult {
918            earliest,
919            latest,
920            clock_status,
921        } = ceb
922            .compute_bound_at(real, mono)
923            .expect("Failed to compute bound");
924
925        // 4 seconds have passed since the bound was snapshot, hence 4 microsec of drift on top of
926        // the default 10 microsec put in the ClockBoundError data
927        assert_eq!(earliest.tv_sec(), 19);
928        assert_eq!(earliest.tv_nsec(), 1_000_000_000 - 14_000);
929        assert_eq!(latest.tv_sec(), 20);
930        assert_eq!(latest.tv_nsec(), 14_000);
931        assert_eq!(clock_status, ClockStatus::Synchronized);
932    }
933
934    /// Assert the clock status is FreeRunning if the ClockErrorBound data is passed the free
935    /// running grace period, simulating behavior of the daemon has died.
936    #[test]
937    fn compute_bound_force_free_running_status() {
938        let ceb = clockbound_v2!((0, 0), (100, 0));
939        let real = TimeSpec::new(61, 0);
940        let mono = TimeSpec::new(61, 0);
941
942        let ClockBoundNowResult {
943            earliest,
944            latest,
945            clock_status,
946        } = ceb
947            .compute_bound_at(real, mono)
948            .expect("Failed to compute bound");
949
950        // 61 seconds have passed since the bound was snapshot, hence 61 microsec of drift have
951        // accumulated at max_drift_ppb on top of the default 10 microsec put in the
952        // ClockBoundError data.
953        assert_eq!(earliest.tv_sec(), 60);
954        assert_eq!(earliest.tv_nsec(), 1_000_000_000 - 71_000);
955        assert_eq!(latest.tv_sec(), 61);
956        assert_eq!(latest.tv_nsec(), 71_000);
957        assert_eq!(clock_status, ClockStatus::FreeRunning);
958    }
959
960    /// Assert the clock status is Unknown if the ClockErrorBound data is passed void_after
961    #[test]
962    fn compute_bound_unknown_status_if_expired() {
963        let ceb = clockbound_v2!((0, 0), (5, 0));
964        let real = TimeSpec::new(10, 0);
965        let mono = TimeSpec::new(10, 0); // Passed void_after
966
967        let ClockBoundNowResult {
968            earliest,
969            latest,
970            clock_status,
971        } = ceb
972            .compute_bound_at(real, mono)
973            .expect("Failed to compute bound");
974
975        // 10 seconds have passed since the bound was snapshot, hence 10 microsec of drift on top of
976        // the default 10 microsec put in the ClockBoundError data
977        assert_eq!(earliest.tv_sec(), 9);
978        assert_eq!(earliest.tv_nsec(), 1_000_000_000 - 20_000);
979        assert_eq!(latest.tv_sec(), 10);
980        assert_eq!(latest.tv_nsec(), 20_000);
981        assert_eq!(clock_status, ClockStatus::Unknown);
982    }
983
984    /// Assert errors are returned if the ClockBoundError data is malformed with bad drift
985    #[test]
986    fn compute_bound_bad_drift() {
987        let mut ceb = clockbound_v2!((0, 0), (10, 0));
988        let real = TimeSpec::new(5, 0);
989        let mono = TimeSpec::new(5, 0);
990        ceb.max_drift_ppb = 2_000_000_000;
991
992        assert!(ceb.compute_bound_at(real, mono).is_err());
993    }
994
995    /// Assert errors are returned if the ClockBoundError data snapshot has been taken after
996    /// reading clocks at 'now'
997    #[test]
998    fn compute_bound_causality_break() {
999        let ceb = clockbound_v2!((5, 0), (10, 0));
1000        let real = TimeSpec::new(1, 0);
1001        let mono = TimeSpec::new(1, 0);
1002
1003        let res = ceb.compute_bound_at(real, mono);
1004
1005        assert!(res.is_err());
1006    }
1007
1008    #[test]
1009    fn test_ceb_v3_new() {
1010        let ceb = ClockErrorBoundV3::new(
1011            1000,                      // as_of_tsc
1012            TimeSpec::new(1, 0),       // as_of
1013            TimeSpec::new(10, 0),      // void_after
1014            1e-9,                      // period
1015            1e-12,                     // period_err
1016            5000,                      // bound_nsec
1017            42,                        // disruption_marker
1018            1000,                      // max_drift_ppb
1019            ClockStatus::Synchronized, // clock_status
1020            true,                      // clock_disruption_support_enabled
1021        );
1022
1023        assert_eq!(ceb.as_of_tsc, 1000);
1024        assert_eq!(ceb.as_of, TimeSpec::new(1, 0));
1025        assert_eq!(ceb.void_after, TimeSpec::new(10, 0));
1026        assert_eq!(ceb.bound_nsec, 5000);
1027        assert_eq!(ceb.disruption_marker, 42);
1028        assert_eq!(ceb.max_drift_ppb, 1000);
1029        assert_eq!(ceb.clock_status, ClockStatus::Synchronized);
1030        assert_eq!(ceb.clock_disruption_support_enabled, true);
1031
1032        // Test period conversion
1033        let period = ceb.period();
1034        assert!((period - 1e-9).abs() < 1e-15);
1035
1036        let period_err = ceb.period_err();
1037        assert!((period_err - 1e-12).abs() < 1e-18);
1038    }
1039
1040    #[test]
1041    fn test_ceb_v3_period_conversion() {
1042        let ceb = ClockErrorBoundV3::new(
1043            0,
1044            TimeSpec::new(0, 0),
1045            TimeSpec::new(10, 0),
1046            2.5e-9, // 400 MHz
1047            1e-11,
1048            1000,
1049            0,
1050            1000,
1051            ClockStatus::Synchronized,
1052            true,
1053        );
1054
1055        let period = ceb.period();
1056        let relative_error = (period - 2.5e-9).abs() / 2.5e-9;
1057        assert!(relative_error < 1e-10);
1058
1059        let period_err = ceb.period_err();
1060        let relative_error = (period_err - 1e-11).abs() / 1e-11;
1061        assert!(relative_error < 1e-10);
1062    }
1063
1064    #[test]
1065    fn test_v3_compute_bound_at_tsc_synchronized_status() {
1066        // Create a V3 CEB with known values
1067        let ceb = ClockErrorBoundV3::new(
1068            1_000_000_000,         // as_of_tsc (1 billion cycles)
1069            TimeSpec::new(1, 0),   // as_of = 1 second
1070            TimeSpec::new(100, 0), // void_after = 100 seconds
1071            1e-9,                  // period = 1 ns (1 GHz clock)
1072            1e-12,                 // period_err = 1ps
1073            10_000,                // bound_nsec = 10 microseconds
1074            0,                     // disruption_marker
1075            1000,                  // max_drift_ppb = 1 ppm
1076            ClockStatus::Synchronized,
1077            true,
1078        );
1079
1080        // Simulate reading TSC 2 seconds later (2 billion more cycles at 1 GHz)
1081        let now_tsc = 3_500_000_000;
1082
1083        let result = ceb.compute_bound_at_tsc(now_tsc).expect("Should succeed");
1084
1085        // Expected time: as_of + 2 seconds = 3 seconds
1086        assert_eq!(result.earliest.tv_sec(), 3); // approximately
1087        assert_eq!(result.latest.tv_sec(), 3); // approximately
1088
1089        // Status should still be Synchronized (within grace period)
1090        assert_eq!(result.clock_status, ClockStatus::Synchronized);
1091    }
1092
1093    // Assert that typical TSC periods (1Hz to 10 GHz range) are converted into scaled integers
1094    // without a loss of precision.
1095    #[test]
1096    fn test_period_frac_conversion_typical_periods() {
1097        let periods = [1e-3, 1e-6, 1e-7, 1e-8, 1e-9, 2e-9, 5e-9, 1e-10];
1098
1099        for &period in &periods {
1100            let frac = PeriodFrac::from(period);
1101            let result: f64 = f64::from(frac);
1102            assert!(result == period);
1103        }
1104    }
1105
1106    // Assert atypical TSC periods are converted into scaled integers
1107    // with a minimum loss of precision.
1108    #[test]
1109    fn test_period_frac_conversion_edge_cases() {
1110        // Very small period (very high frequency)
1111        let small_period = 1e-25;
1112        let frac = PeriodFrac::from(small_period);
1113        let result: f64 = f64::from(frac);
1114        let relative_error = (result - small_period).abs() / small_period;
1115        assert!(relative_error < 1e-10);
1116
1117        // Larger period (lower frequency)
1118        let large_period = 0.1;
1119        let frac = PeriodFrac::from(large_period);
1120        let result: f64 = f64::from(frac);
1121        let relative_error = (result - large_period).abs() / large_period;
1122        assert!(relative_error < 1e-10);
1123    }
1124
1125    // Assert that the conversion panics on non-realistic frequencies.
1126    #[test]
1127    #[should_panic(expected = "Cannot convert period larger than 1 second")]
1128    fn test_period_frac_conversion_panci() {
1129        let large_period = 1.0;
1130        let _ = PeriodFrac::from(large_period);
1131    }
1132
1133    #[test]
1134    fn test_calculate_frac_shift_typical() {
1135        // For a 1 GHz clock (period = 1e-9), frequency = 1e9
1136        // 1e9 in binary is about 30 bits, so shift should be around 29
1137        let period = 1e-9;
1138        let shift = PeriodFrac::calculate_frac_shift(period);
1139        assert!(shift >= 29 && shift <= 30, "shift = {}", shift);
1140
1141        // For a 2.5 GHz clock (period = 4e-10), frequency = 2.5e9
1142        // 2.5e9 in binary is about 31 bits
1143        let period = 4e-10;
1144        let shift = PeriodFrac::calculate_frac_shift(period);
1145        assert!(shift >= 30 && shift <= 32, "shift = {}", shift);
1146    }
1147
1148    #[test]
1149    fn test_calculate_frac_shift_zero() {
1150        // Zero period should return 0 (max of 0 and negative value)
1151        let period = 0.0;
1152        let shift = PeriodFrac::calculate_frac_shift(period);
1153        assert_eq!(shift, 0);
1154    }
1155
1156    #[test]
1157    fn test_zero_period_frac_conversion() {
1158        // Test that zero period doesn't panic and gives reasonable result
1159        let period = 0.0;
1160        let frac = PeriodFrac::from(period);
1161        assert_eq!(frac.shift, 0);
1162        assert_eq!(frac.frac, 0);
1163
1164        let result: f64 = f64::from(frac);
1165        assert_eq!(result, 0.0);
1166    }
1167
1168    #[test]
1169    fn test_precision_maintained() {
1170        // Test that we maintain good precision across conversions
1171        let period = 2.718281828e-9; // Some arbitrary value
1172        let frac = PeriodFrac::from(period);
1173        let result: f64 = f64::from(frac);
1174
1175        // Should maintain at least 10 significant digits
1176        let relative_error = (result - period).abs() / period;
1177        assert!(relative_error < 1e-10);
1178    }
1179
1180    #[test]
1181    fn test_frac_representation_property() {
1182        // Test that the fixed-point representation makes sense
1183        let period = 1e-9;
1184        let frac = PeriodFrac::from(period);
1185
1186        // frac should be non-zero for non-zero period
1187        assert!(frac.frac > 0);
1188
1189        // shift should be reasonable (not 0 or 255)
1190        assert!(frac.shift > 0 && frac.shift < 64);
1191    }
1192}