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}