1#![warn(missing_docs)]
37#![allow(clippy::missing_safety_doc)]
38#![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 #[repr(C)]
52 pub struct Timespec {
53 pub tv_sec: i64,
55 pub tv_nsec: i64,
57 }
58
59 #[repr(C)]
61 pub struct Timeval {
62 pub tv_sec: i64,
64 pub tv_usec: i64,
66 }
67
68 pub type ClockId = c_int;
70 pub const CLOCK_REALTIME: ClockId = 0;
72 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 unsafe {
87 let p = libc_stub::dlsym(libc_stub::RTLD_NEXT, c"clock_gettime".as_ptr());
88 if p.is_null() {
89 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 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 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 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 (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 #[unsafe(no_mangle)]
158 pub unsafe extern "C" fn clock_gettime(clk_id: ClockId, tp: *mut Timespec) -> c_int {
159 let r = unsafe { real_clock_gettime()(clk_id, tp) };
162 if r != 0 || tp.is_null() {
163 return r;
164 }
165 if clk_id == CLOCK_REALTIME {
167 let ts = unsafe { &mut *tp };
169 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 #[unsafe(no_mangle)]
183 pub unsafe extern "C" fn gettimeofday(tv: *mut Timeval, tz: *mut c_void) -> c_int {
184 let r = unsafe { real_gettimeofday()(tv, tz) };
187 if r != 0 || tv.is_null() {
188 return r;
189 }
190 let skew = skew_ns();
192 if skew == 0 {
193 return r;
194 }
195 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 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 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 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 unsafe {
250 std::env::set_var("CHAOS_CLOCK_SKEW_NS", "5000000000"); 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 unsafe { std::env::remove_var("CHAOS_CLOCK_SKEW_NS") };
261 }
262
263 #[test]
264 fn apply_skew_negative_offset_subtracts() {
265 unsafe {
267 std::env::set_var("CHAOS_CLOCK_SKEW_NS", "-2000000000"); 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 unsafe { std::env::remove_var("CHAOS_CLOCK_SKEW_NS") };
278 }
279
280 #[test]
281 fn apply_skew_handles_nsec_carry() {
282 unsafe {
284 std::env::set_var("CHAOS_CLOCK_SKEW_NS", "1500000000"); 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 assert_eq!(ts.tv_sec, 102);
294 assert_eq!(ts.tv_nsec, 100_000_000);
295 unsafe { std::env::remove_var("CHAOS_CLOCK_SKEW_NS") };
297 }
298 }
299}
300
301#[cfg(target_os = "linux")]
302pub use linux::*;
303
304#[cfg(not(target_os = "linux"))]
307mod stub {
308 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::*;