Skip to main content

chaos_clock_skew/
lib.rs

1//! LD_PRELOAD-Shim fuer Clock-Skew-Chaos (WP 5.F.2 Phase-B).
2//!
3//! Crate `chaos-clock-skew`. Safety classification: **STANDARD**
4//! (FFI-Boundary, dlsym-basiert).
5//!
6//! Setzt `clock_gettime()` und `gettimeofday()` so um, dass eine
7//! konfigurierbare Drift addiert wird. Die Drift kommt aus Env-
8//! Variablen die der Test-Harness setzt:
9//!
10//! * `CHAOS_CLOCK_SKEW_NS` — fixe Offset in Nanosekunden (signed,
11//!   negative Werte = Clock laeuft hinten).
12//! * `CHAOS_CLOCK_DRIFT_PPM` — kontinuierliche Drift in
13//!   parts-per-million (z.B. `100` = 100 µs/s langsamer).
14//!
15//! Aktivierung:
16//!
17//! ```bash
18//! cargo build -p chaos-clock-skew --release
19//! LD_PRELOAD=$(pwd)/target/release/libchaos_clock_skew.so \
20//!   CHAOS_CLOCK_SKEW_NS=100000000 \
21//!   ./my-process
22//! ```
23//!
24//! Auf macOS/Windows kompiliert die Crate nicht (kein dlsym-Pendant
25//! fuer system clock). Fuer den CI laeuft sie nur auf Linux.
26//!
27//! # Was wird NICHT abgefangen?
28//!
29//! * `CLOCK_MONOTONIC_RAW` — bleibt unveraendert (nur fuer Bench-Tools
30//!   wichtig).
31//! * `CLOCK_BOOTTIME` — bleibt unveraendert.
32//! * Direct-syscall `clock_gettime` ueber `vDSO`-Bypass —
33//!   `LD_PRELOAD` greift bei vDSO-Calls nur wenn das Programm explizit
34//!   ueber libc callt; das ist bei stdlib-Code der Fall.
35
36#![warn(missing_docs)]
37#![allow(clippy::missing_safety_doc)]
38// Init-time panics akzeptieren: ohne dlsym(RTLD_NEXT) ist die
39// LD_PRELOAD-Lib funktional tot, also ist Process-Termination das
40// einzig sinnvolle Fail-Mode. Das ist Comfort-Tool-Code, kein
41// Runtime-Pfad.
42#![allow(clippy::panic)]
43
44#[cfg(target_os = "linux")]
45mod linux {
46    use core::ffi::{c_int, c_void};
47    use core::ptr;
48    use std::sync::OnceLock;
49
50    /// Linux-`timespec`. Layout glibc-kompatibel.
51    #[repr(C)]
52    pub struct Timespec {
53        /// Sekunden seit Epoch.
54        pub tv_sec: i64,
55        /// Nanosekunden, 0..1_000_000_000.
56        pub tv_nsec: i64,
57    }
58
59    /// Linux-`timeval`. Layout glibc-kompatibel.
60    #[repr(C)]
61    pub struct Timeval {
62        /// Sekunden.
63        pub tv_sec: i64,
64        /// Microsekunden.
65        pub tv_usec: i64,
66    }
67
68    /// `clockid_t`-Enum, glibc-kompatibel.
69    pub type ClockId = c_int;
70    /// `CLOCK_REALTIME`.
71    pub const CLOCK_REALTIME: ClockId = 0;
72    /// `CLOCK_MONOTONIC`.
73    pub const CLOCK_MONOTONIC: ClockId = 1;
74
75    type ClockGettimeFn = unsafe extern "C" fn(ClockId, *mut Timespec) -> c_int;
76    type GetTimeOfDayFn = unsafe extern "C" fn(*mut Timeval, *mut c_void) -> c_int;
77
78    static REAL_CLOCK_GETTIME: OnceLock<ClockGettimeFn> = OnceLock::new();
79    static REAL_GETTIMEOFDAY: OnceLock<GetTimeOfDayFn> = OnceLock::new();
80
81    fn real_clock_gettime() -> ClockGettimeFn {
82        *REAL_CLOCK_GETTIME.get_or_init(|| {
83            // SAFETY: dlsym mit RTLD_NEXT liefert die echte glibc-
84            // Implementation. Pointer-Cast auf Funktionstyp gleicher
85            // Signatur ist kontraktgemaess.
86            unsafe {
87                let p = libc_stub::dlsym(libc_stub::RTLD_NEXT, c"clock_gettime".as_ptr());
88                if p.is_null() {
89                    // Fallback: gar nichts auflösen, Crashes lassen
90                    // wir kontrolliert bei Aufruf.
91                    panic!("dlsym(clock_gettime) failed");
92                }
93                core::mem::transmute::<*mut c_void, ClockGettimeFn>(p)
94            }
95        })
96    }
97
98    fn real_gettimeofday() -> GetTimeOfDayFn {
99        *REAL_GETTIMEOFDAY.get_or_init(|| {
100            // SAFETY: gleiche Begruendung wie real_clock_gettime.
101            unsafe {
102                let p = libc_stub::dlsym(libc_stub::RTLD_NEXT, c"gettimeofday".as_ptr());
103                if p.is_null() {
104                    panic!("dlsym(gettimeofday) failed");
105                }
106                core::mem::transmute::<*mut c_void, GetTimeOfDayFn>(p)
107            }
108        })
109    }
110
111    /// Gibt den fixen Skew-Offset (ns) zurueck. Wird bei jedem Call
112    /// gelesen, damit Tests den Wert zur Laufzeit aendern koennen.
113    fn skew_ns() -> i64 {
114        std::env::var("CHAOS_CLOCK_SKEW_NS")
115            .ok()
116            .and_then(|s| s.parse::<i64>().ok())
117            .unwrap_or(0)
118    }
119
120    fn drift_ppm() -> i64 {
121        std::env::var("CHAOS_CLOCK_DRIFT_PPM")
122            .ok()
123            .and_then(|s| s.parse::<i64>().ok())
124            .unwrap_or(0)
125    }
126
127    /// Wendet Skew + Drift auf einen Timespec an.
128    fn apply_skew(ts: &mut Timespec, monotonic_ns: i64) {
129        let skew = skew_ns();
130        let drift = drift_ppm();
131        let drift_offset_ns = if drift != 0 {
132            // PPM = ns/sec/1e6; absolute drift = elapsed_ns * ppm / 1e6.
133            (monotonic_ns / 1_000_000) * drift / 1_000
134        } else {
135            0
136        };
137        let total = skew.saturating_add(drift_offset_ns);
138        let total_sec = total / 1_000_000_000;
139        let total_nsec = total % 1_000_000_000;
140        ts.tv_sec = ts.tv_sec.saturating_add(total_sec);
141        let mut new_nsec = ts.tv_nsec.saturating_add(total_nsec);
142        if new_nsec >= 1_000_000_000 {
143            ts.tv_sec = ts.tv_sec.saturating_add(1);
144            new_nsec -= 1_000_000_000;
145        }
146        if new_nsec < 0 {
147            ts.tv_sec = ts.tv_sec.saturating_sub(1);
148            new_nsec += 1_000_000_000;
149        }
150        ts.tv_nsec = new_nsec;
151    }
152
153    /// `clock_gettime`-Replacement.
154    ///
155    /// # Safety
156    /// `tp` muss valid sein.
157    #[unsafe(no_mangle)]
158    pub unsafe extern "C" fn clock_gettime(clk_id: ClockId, tp: *mut Timespec) -> c_int {
159        // SAFETY: real_clock_gettime() liefert die echte glibc-
160        // Implementation; die Aufruf-Semantik ist signaturgleich.
161        let r = unsafe { real_clock_gettime()(clk_id, tp) };
162        if r != 0 || tp.is_null() {
163            return r;
164        }
165        // Skew nur fuer CLOCK_REALTIME — MONOTONIC bleibt unveraendert.
166        if clk_id == CLOCK_REALTIME {
167            // SAFETY: r==0, tp wurde von der echten Funktion befuellt.
168            let ts = unsafe { &mut *tp };
169            // Drift: monotonic_ns ist die Wallclock seit Boot.
170            // Naeherung: ts.tv_sec * 1e9 + tv_nsec (ist seit-Epoch
171            // statt seit-Boot, aber die PPM-Drift ist gleichmaessig).
172            let monotonic_ns = ts.tv_sec.saturating_mul(1_000_000_000) + ts.tv_nsec;
173            apply_skew(ts, monotonic_ns);
174        }
175        r
176    }
177
178    /// `gettimeofday`-Replacement.
179    ///
180    /// # Safety
181    /// `tv` muss valid sein.
182    #[unsafe(no_mangle)]
183    pub unsafe extern "C" fn gettimeofday(tv: *mut Timeval, tz: *mut c_void) -> c_int {
184        // SAFETY: real_gettimeofday() liefert die echte glibc-
185        // Implementation; signaturkompatibler Pass-Through.
186        let r = unsafe { real_gettimeofday()(tv, tz) };
187        if r != 0 || tv.is_null() {
188            return r;
189        }
190        // Skew anwenden.
191        let skew = skew_ns();
192        if skew == 0 {
193            return r;
194        }
195        // SAFETY: r==0 → tv wurde befuellt.
196        let v = unsafe { &mut *tv };
197        let total_us = skew / 1_000;
198        let total_sec = total_us / 1_000_000;
199        let total_us_rem = total_us % 1_000_000;
200        v.tv_sec = v.tv_sec.saturating_add(total_sec);
201        let mut new_us = v.tv_usec.saturating_add(total_us_rem);
202        if new_us >= 1_000_000 {
203            v.tv_sec = v.tv_sec.saturating_add(1);
204            new_us -= 1_000_000;
205        }
206        if new_us < 0 {
207            v.tv_sec = v.tv_sec.saturating_sub(1);
208            new_us += 1_000_000;
209        }
210        v.tv_usec = new_us;
211        // Schlucke unused tz.
212        let _ = tz;
213        let _ = ptr::eq::<()>(ptr::null(), ptr::null());
214        r
215    }
216
217    mod libc_stub {
218        use core::ffi::{c_char, c_void};
219        unsafe extern "C" {
220            pub fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
221        }
222        // RTLD_NEXT auf Linux glibc = -1 als void*.
223        pub const RTLD_NEXT: *mut c_void = -1isize as *mut c_void;
224    }
225
226    #[cfg(test)]
227    mod tests {
228        use super::*;
229
230        #[test]
231        fn apply_skew_zero_is_noop() {
232            // SAFETY: env-mutation in single-threaded test scope.
233            unsafe {
234                std::env::remove_var("CHAOS_CLOCK_SKEW_NS");
235                std::env::remove_var("CHAOS_CLOCK_DRIFT_PPM");
236            }
237            let mut ts = Timespec {
238                tv_sec: 100,
239                tv_nsec: 500,
240            };
241            apply_skew(&mut ts, 100_000_000_000);
242            assert_eq!(ts.tv_sec, 100);
243            assert_eq!(ts.tv_nsec, 500);
244        }
245
246        #[test]
247        fn apply_skew_positive_offset_adds_seconds() {
248            // SAFETY: env-mutation in single-threaded test scope.
249            unsafe {
250                std::env::set_var("CHAOS_CLOCK_SKEW_NS", "5000000000"); // +5s
251                std::env::remove_var("CHAOS_CLOCK_DRIFT_PPM");
252            }
253            let mut ts = Timespec {
254                tv_sec: 100,
255                tv_nsec: 0,
256            };
257            apply_skew(&mut ts, 0);
258            assert_eq!(ts.tv_sec, 105);
259            // SAFETY: env-cleanup im single-threaded scope.
260            unsafe { std::env::remove_var("CHAOS_CLOCK_SKEW_NS") };
261        }
262
263        #[test]
264        fn apply_skew_negative_offset_subtracts() {
265            // SAFETY: env-mutation in single-threaded test scope.
266            unsafe {
267                std::env::set_var("CHAOS_CLOCK_SKEW_NS", "-2000000000"); // -2s
268                std::env::remove_var("CHAOS_CLOCK_DRIFT_PPM");
269            }
270            let mut ts = Timespec {
271                tv_sec: 100,
272                tv_nsec: 0,
273            };
274            apply_skew(&mut ts, 0);
275            assert_eq!(ts.tv_sec, 98);
276            // SAFETY: env-cleanup im single-threaded scope.
277            unsafe { std::env::remove_var("CHAOS_CLOCK_SKEW_NS") };
278        }
279
280        #[test]
281        fn apply_skew_handles_nsec_carry() {
282            // SAFETY: env-mutation in single-threaded test scope.
283            unsafe {
284                std::env::set_var("CHAOS_CLOCK_SKEW_NS", "1500000000"); // +1.5s
285                std::env::remove_var("CHAOS_CLOCK_DRIFT_PPM");
286            }
287            let mut ts = Timespec {
288                tv_sec: 100,
289                tv_nsec: 600_000_000,
290            };
291            apply_skew(&mut ts, 0);
292            // 100s + 0.6s + 1.5s = 102s + 0.1s
293            assert_eq!(ts.tv_sec, 102);
294            assert_eq!(ts.tv_nsec, 100_000_000);
295            // SAFETY: env-cleanup im single-threaded scope.
296            unsafe { std::env::remove_var("CHAOS_CLOCK_SKEW_NS") };
297        }
298    }
299}
300
301#[cfg(target_os = "linux")]
302pub use linux::*;
303
304// Stub fuer Nicht-Linux: lib kompiliert, hat aber keine no_mangle-
305// Symbole.
306#[cfg(not(target_os = "linux"))]
307mod stub {
308    /// Auf Nicht-Linux ist die Crate ein No-Op-Stub.
309    pub fn unsupported() -> &'static str {
310        "chaos-clock-skew is Linux-only"
311    }
312
313    #[cfg(test)]
314    mod tests {
315        use super::*;
316
317        #[test]
318        fn stub_advertises_unsupported() {
319            assert!(unsupported().contains("Linux-only"));
320        }
321    }
322}
323
324#[cfg(not(target_os = "linux"))]
325pub use stub::*;