fix(hook): harden detour install, unload, and delta reconnection

Initialize every detour before enabling any, so a hook body that calls
another detour's trampoline can never hit an uninitialized detour and abort
the process. Pin the module on load so unloading can no longer leave
kernel32/ntdll patched to jump into freed memory. On a failed FILETIME
conversion, fall through to the real time instead of returning an
uninitialized SYSTEMTIME. Reconnect the shared delta mapping lazily so
setting a delta after injection takes effect instead of being disabled
forever. Add host-agnostic tick-conversion tests.
This commit is contained in:
tiennm99 committed 2026-09-21 16:13:17 +07:00
1 parent a2e34060e0
commit f4bd35ae5b
17 files changed
+385 -70

No files matched your search

-2
View File
@@ -19,5 +19,3 @@ windows-sys = { workspace = true, features = [
"Win32_System_Time",
"Win32_Security",
] }
[target.'cfg(windows)'.dependencies]
+9 -2
View File
@@ -15,12 +15,19 @@
//! then `Local\`, so a controller's elevation state determines the namespace
//! used and the hook just probes both.
#![cfg(windows)]
// `mmf` wraps Win32-only APIs (named file mappings), so it is gated on
// `windows` at the module level. `ticks` and `types` hold pure arithmetic —
// no Win32 dependency at all beyond the FILETIME/SYSTEMTIME conversions
// inside `ticks`, which are individually `#[cfg(windows)]` — so both stay
// buildable and testable on any host (e.g. `cargo test -p time-mocker-core`
// on Linux CI/dev boxes actually exercises the tick-arithmetic tests instead
// of compiling an empty crate).
#[cfg(windows)]
pub mod mmf;
pub mod ticks;
pub mod types;
#[cfg(windows)]
pub use mmf::{CreateOutcome, SharedDeltaReader, SharedDeltaWriter};
pub use types::MockTimeInfo;
+16 -3
View File
@@ -24,7 +24,10 @@ use windows_sys::Win32::System::Memory::{
use crate::types::MockTimeInfo;
fn wide(s: &str) -> Vec<u16> {
OsStr::new(s).encode_wide().chain(std::iter::once(0)).collect()
OsStr::new(s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
/// Shared bookkeeping for an open mapping. Owns the handle + view and frees
@@ -96,6 +99,12 @@ impl SharedDeltaWriter {
let handle = unsafe {
CreateFileMappingW(
INVALID_HANDLE_VALUE,
// NOTE: NULL security descriptor -> default DACL (creator +
// SYSTEM/Administrators). Fine for same-user debug targets;
// a target running as a different user/service identity than
// the controller will fail to open this mapping and silently
// get no hooks installed. Accepted for now: this is a local
// debugging tool, not a service-hardening one.
ptr::null(),
PAGE_READWRITE,
0,
@@ -104,7 +113,9 @@ impl SharedDeltaWriter {
)
};
if handle.is_null() {
return Err(io::Error::from_raw_os_error(unsafe { GetLastError() } as i32));
return Err(io::Error::from_raw_os_error(
unsafe { GetLastError() } as i32
));
}
// Capture ERROR_ALREADY_EXISTS *before* any other syscall that may overwrite it.
let outcome = if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
@@ -130,7 +141,9 @@ impl SharedDeltaReader {
let wname = wide(name);
let handle = unsafe { OpenFileMappingW(FILE_MAP_READ, 0, wname.as_ptr()) };
if handle.is_null() {
return Err(io::Error::from_raw_os_error(unsafe { GetLastError() } as i32));
return Err(io::Error::from_raw_os_error(
unsafe { GetLastError() } as i32
));
}
let view = unsafe { map_view(handle, FILE_MAP_READ)? };
Ok(Self(MappingHandle { handle, view }))
+100 -2
View File
@@ -2,15 +2,50 @@
//!
//! FILETIME = 100-ns units since 1601-01-01 00:00:00 UTC. This crate uses
//! FILETIME ticks throughout to avoid extra arithmetic on the hot path.
//!
//! `apply_delta` and the `*_VALID_TICKS` bounds are plain `i64` arithmetic
//! with no Win32 dependency, so they build and run on any host. The
//! FILETIME/SYSTEMTIME conversions below them call into `windows_sys` and are
//! `#[cfg(windows)]`.
#[cfg(windows)]
use windows_sys::Win32::Foundation::{FILETIME, SYSTEMTIME};
#[cfg(windows)]
use windows_sys::Win32::System::Time::{FileTimeToSystemTime, SystemTimeToFileTime};
/// Lowest FILETIME tick value `FileTimeToSystemTime` accepts. FILETIME is
/// defined as an unsigned 64-bit tick count from the 1601-01-01 epoch, so a
/// negative `i64` (sign bit set) is never a valid FILETIME.
pub const MIN_VALID_TICKS: i64 = 0;
/// Highest FILETIME tick value `FileTimeToSystemTime` accepts: 30827-12-31
/// 23:59:59.9999999 UTC, the last instant representable in a `SYSTEMTIME`
/// (`wYear` is a `u16`, and Win32 defines this as the ceiling). Any FILETIME
/// beyond this fails conversion.
pub const MAX_VALID_TICKS: i64 = 0x7FFF_35F4_F06C_58F0;
/// Add `delta` to `real_ticks` and clamp to the range `FileTimeToSystemTime`
/// can convert. A saturating add alone stops at `i64::MIN`/`i64::MAX`, but
/// those are far outside the valid FILETIME range: the delta comes from
/// shared memory written by a separate, possibly stale controller process,
/// so it must not be trusted to keep the result in range. Clamping here means
/// every caller downstream gets a value that round-trips through
/// `ticks_to_systemtime` instead of failing conversion with an untouched
/// output buffer.
#[inline]
pub fn apply_delta(real_ticks: i64, delta: i64) -> i64 {
real_ticks
.saturating_add(delta)
.clamp(MIN_VALID_TICKS, MAX_VALID_TICKS)
}
#[cfg(windows)]
#[inline]
pub fn filetime_to_i64(ft: FILETIME) -> i64 {
((ft.dwHighDateTime as i64) << 32) | (ft.dwLowDateTime as i64 & 0xFFFF_FFFF)
}
#[cfg(windows)]
#[inline]
pub fn i64_to_filetime(ticks: i64) -> FILETIME {
FILETIME {
@@ -20,6 +55,7 @@ pub fn i64_to_filetime(ticks: i64) -> FILETIME {
}
/// Convert FILETIME ticks to SYSTEMTIME (UTC). Returns None on Win32 failure.
#[cfg(windows)]
pub fn ticks_to_systemtime(ticks: i64) -> Option<SYSTEMTIME> {
let ft = i64_to_filetime(ticks);
let mut st: SYSTEMTIME = unsafe { std::mem::zeroed() };
@@ -32,6 +68,7 @@ pub fn ticks_to_systemtime(ticks: i64) -> Option<SYSTEMTIME> {
}
/// Convert SYSTEMTIME (UTC) to FILETIME ticks. Returns None on Win32 failure.
#[cfg(windows)]
pub fn systemtime_to_ticks(st: &SYSTEMTIME) -> Option<i64> {
let mut ft: FILETIME = unsafe { std::mem::zeroed() };
let ok = unsafe { SystemTimeToFileTime(st, &mut ft) };
@@ -46,10 +83,63 @@ pub fn systemtime_to_ticks(st: &SYSTEMTIME) -> Option<i64> {
mod tests {
use super::*;
#[test]
fn apply_delta_zero_is_identity() {
assert_eq!(apply_delta(1_000, 0), 1_000);
}
#[test]
fn apply_delta_positive_offset() {
assert_eq!(apply_delta(1_000, 500), 1_500);
}
#[test]
fn apply_delta_negative_offset() {
assert_eq!(apply_delta(1_000, -500), 500);
}
#[test]
fn apply_delta_saturates_instead_of_overflowing() {
// i64::MAX + a positive delta would overflow a plain `+`; saturating_add
// (and then the clamp below) must not panic or wrap.
assert_eq!(apply_delta(i64::MAX, i64::MAX), MAX_VALID_TICKS);
assert_eq!(apply_delta(i64::MIN, i64::MIN), MIN_VALID_TICKS);
}
#[test]
fn apply_delta_clamps_below_epoch_to_min_valid() {
// A large negative delta applied to an early real time would produce a
// negative tick count — not representable as FILETIME (unsigned).
assert_eq!(apply_delta(100, -1_000), MIN_VALID_TICKS);
}
#[test]
fn apply_delta_clamps_above_ceiling_to_max_valid() {
assert_eq!(apply_delta(MAX_VALID_TICKS, 1), MAX_VALID_TICKS);
assert_eq!(
apply_delta(MAX_VALID_TICKS - 10, 1_000_000),
MAX_VALID_TICKS
);
}
#[test]
fn apply_delta_respects_exact_clamp_boundaries() {
// One tick inside either boundary must pass through unchanged.
assert_eq!(apply_delta(MIN_VALID_TICKS + 1, 0), MIN_VALID_TICKS + 1);
assert_eq!(apply_delta(MAX_VALID_TICKS - 1, 0), MAX_VALID_TICKS - 1);
assert_eq!(apply_delta(MIN_VALID_TICKS, 0), MIN_VALID_TICKS);
assert_eq!(apply_delta(MAX_VALID_TICKS, 0), MAX_VALID_TICKS);
}
#[cfg(windows)]
#[test]
fn filetime_i64_roundtrip() {
// Covers: zero, small, Unix epoch (1970 in FILETIME ticks), a recent time,
// and the i64 boundary (used as a saturating-add sentinel by the hooks).
// Covers: zero, small, Unix epoch (1970 in FILETIME ticks), a recent
// time, and the i64 boundary. `filetime_to_i64`/`i64_to_filetime` are
// plain bit-packing and round-trip the full i64 range (including
// negative/out-of-FILETIME-range values) even though those values are
// never handed to Win32 conversion APIs — `apply_delta` above is what
// keeps callers in the valid range before that happens.
let cases = [
0_i64,
1,
@@ -66,6 +156,7 @@ mod tests {
}
}
#[cfg(windows)]
#[test]
fn systemtime_roundtrip_utc() {
// 2020-06-15 12:34:56 UTC
@@ -88,4 +179,11 @@ mod tests {
assert_eq!(back.wMinute, st.wMinute);
assert_eq!(back.wSecond, st.wSecond);
}
#[cfg(windows)]
#[test]
fn ticks_to_systemtime_rejects_out_of_range_values() {
assert!(ticks_to_systemtime(MAX_VALID_TICKS + 1).is_none());
assert!(ticks_to_systemtime(MIN_VALID_TICKS - 1).is_none());
}
}
+1
View File
@@ -15,6 +15,7 @@ path = "src/lib.rs"
[dependencies]
time-mocker-core = { path = "../time-mocker-core" }
# Pre-release range: no stable retour release ships static-detour inline x64 hooking yet.
retour = { version = "0.4.0-alpha.4", features = ["static-detour"] }
once_cell = "1.20"
windows-sys = { workspace = true, features = [
+41 -26
View File
@@ -9,15 +9,27 @@
//!
//! We also call `DisableThreadLibraryCalls(hinst)` to suppress all future
//! `DLL_THREAD_ATTACH`/`DETACH` notifications for this DLL — we don't need them.
//!
//! Load-once, never-unload invariant: `DllMain` pins this module (see
//! `pin_module`) and there is no `DLL_PROCESS_DETACH` handler. Once the
//! detours are enabled, kernel32/ntdll contain jumps into this DLL's
//! trampoline pages; unhooking them would require draining every thread that
//! might currently be inside a trampoline, which cannot be done safely from
//! `DLL_PROCESS_DETACH` under the loader lock. Pinning means the loader can
//! never unmap us out from under those jumps, even if something (a future
//! `eject` path, or a stray `FreeLibrary`) tries.
use std::ffi::{c_void, OsStr};
use std::os::windows::ffi::OsStrExt;
use std::ptr;
use time_mocker_core::{local_mmf_name_for_pid, mmf_name_for_pid, SharedDeltaReader};
use time_mocker_core::{local_mmf_name_for_pid, mmf_name_for_pid};
use windows_sys::Win32::Foundation::{CloseHandle, BOOL, HMODULE, TRUE};
use windows_sys::Win32::System::Diagnostics::Debug::OutputDebugStringW;
use windows_sys::Win32::System::LibraryLoader::DisableThreadLibraryCalls;
use windows_sys::Win32::System::LibraryLoader::{
DisableThreadLibraryCalls, GetModuleHandleExW, GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
GET_MODULE_HANDLE_EX_FLAG_PIN,
};
use windows_sys::Win32::System::SystemServices::DLL_PROCESS_ATTACH;
use windows_sys::Win32::System::Threading::{CreateThread, GetCurrentProcessId};
@@ -25,13 +37,10 @@ use crate::hooks::{self, InstallReport};
#[no_mangle]
#[allow(non_snake_case, clippy::missing_safety_doc)]
pub unsafe extern "system" fn DllMain(
hinst: HMODULE,
reason: u32,
_reserved: *mut c_void,
) -> BOOL {
pub unsafe extern "system" fn DllMain(hinst: HMODULE, reason: u32, _reserved: *mut c_void) -> BOOL {
if reason == DLL_PROCESS_ATTACH {
DisableThreadLibraryCalls(hinst);
pin_module();
let thread_handle = CreateThread(
ptr::null(),
0,
@@ -50,6 +59,22 @@ pub unsafe extern "system" fn DllMain(
TRUE
}
/// Bump this module's loader reference count so it can never be unmapped
/// (see the module-doc invariant above). `GET_MODULE_HANDLE_EX_FLAG_PIN`
/// combined with `..._FROM_ADDRESS` resolves the target module from an
/// address inside it — `DllMain`'s own address — so this needs no file path
/// or module name. Best-effort: if it ever fails, we still proceed with
/// install rather than abort the injection, since a pin failure here is far
/// less likely than the process simply never unloading us in practice.
unsafe fn pin_module() {
let mut pinned: HMODULE = ptr::null_mut();
GetModuleHandleExW(
GET_MODULE_HANDLE_EX_FLAG_PIN | GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
DllMain as *const () as usize as *const u16,
&mut pinned,
);
}
unsafe extern "system" fn bootstrap_thread(_param: *mut c_void) -> u32 {
bootstrap();
0
@@ -60,25 +85,15 @@ fn bootstrap() {
let global = mmf_name_for_pid(pid);
let local = local_mmf_name_for_pid(pid);
// Probe Global\ first (matches the elevated controller's preferred namespace);
// fall back to Local\ for the unelevated dev/debug controller path. Either
// name owns an identical 8-byte payload — whichever exists wins.
let shared = match SharedDeltaReader::open(&global) {
Ok(s) => s,
Err(e_global) => match SharedDeltaReader::open(&local) {
Ok(s) => s,
Err(e_local) => {
dbg_log(&format!(
"time-mocker: open MMF '{global}' ({e_global}) and '{local}' ({e_local}) both failed"
));
return;
}
},
};
// Best-effort install. Detours stay armed for the lifetime of the host
// process; the bootstrap thread exits immediately after.
let report = hooks::install(shared);
// Install the hooks unconditionally — do not gate installation on the
// delta MMF already existing. The normal workflow order is "inject, then
// open the delta channel", so at this point in a fresh injection neither
// name may exist yet; `hooks::install` connects to whichever one shows up
// lazily (with retry) on first use inside the hot path. Gating install on
// a successful open here would leave the hooks permanently uninstalled
// for the rest of the process's life whenever the controller sets the
// delta after injection completes.
let report = hooks::install(global, local);
log_install_report(&report);
}
+218 -35
View File
@@ -10,13 +10,33 @@
use once_cell::sync::OnceCell;
use retour::static_detour;
use time_mocker_core::ticks::{filetime_to_i64, i64_to_filetime};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::RwLock;
use time_mocker_core::ticks::{apply_delta, filetime_to_i64, i64_to_filetime};
use time_mocker_core::SharedDeltaReader;
use windows_sys::Win32::Foundation::{FILETIME, SYSTEMTIME};
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
use windows_sys::Win32::System::Time::{FileTimeToSystemTime, SystemTimeToTzSpecificLocalTime};
static SHARED: OnceCell<SharedDeltaReader> = OnceCell::new();
/// Re-attempt opening the delta MMF once every this many `delta()` calls while
/// disconnected. The normal workflow order is inject-then-open-channel, so the
/// first `bootstrap()` probe commonly runs before the controller has created
/// the mapping; without a retry the process would carry no-op hooks for its
/// entire lifetime. A call-count backoff (instead of a wall-clock timer and
/// its own thread) keeps the hot path — the already-connected case — down to
/// one relaxed atomic read, and costs nothing extra to implement.
const RETRY_EVERY_N_CALLS: u32 = 4096;
/// Names to probe plus the lazily-connected reader. `misses_since_retry`
/// drives the backoff in `try_reconnect`.
struct DeltaSource {
global_name: String,
local_name: String,
reader: RwLock<Option<SharedDeltaReader>>,
misses_since_retry: AtomicU32,
}
static SOURCE: OnceCell<DeltaSource> = OnceCell::new();
static_detour! {
static GetSystemTimeDetour: unsafe extern "system" fn(*mut SYSTEMTIME);
@@ -37,52 +57,170 @@ pub struct InstallReport {
pub failed: Vec<(&'static str, String)>,
}
/// Resolve+initialize+enable a single detour. Pushes to `installed` on success,
/// to `failed` (with reason) on any failure — never aborts the wider install.
macro_rules! install_hook {
/// Resolve+initialize a single detour (phase 1 of `install`). Pushes to
/// `failed` (with reason) and yields `false` on any failure instead of
/// aborting the wider install; yields `true` when the detour is ready to be
/// enabled in phase 2.
macro_rules! init_hook {
($report:ident, $detour:ident, $module:literal, $proc:literal, $fn_ty:ty, $callback:ident) => {{
// Safety: `resolve` is unsafe because it dereferences whatever
// GetProcAddress returns as `$fn_ty`; correctness rests on the
// module+proc literal pair matching `$fn_ty`'s extern signature.
match unsafe { resolve::<$fn_ty>($module, $proc) } {
None => $report
.failed
.push(($proc, "GetProcAddress: not found".into())),
None => {
$report
.failed
.push(($proc, "GetProcAddress: not found".into()));
false
}
Some(target) => match unsafe { $detour.initialize(target, $callback) } {
Err(e) => $report.failed.push(($proc, format!("initialize: {e}"))),
Ok(d) => match unsafe { d.enable() } {
Err(e) => $report.failed.push(($proc, format!("enable: {e}"))),
Ok(()) => $report.installed.push($proc),
},
Err(e) => {
$report.failed.push(($proc, format!("initialize: {e}")));
false
}
Ok(_) => true,
},
}
}};
}
pub fn install(shared: SharedDeltaReader) -> InstallReport {
let _ = SHARED.set(shared);
/// Enable a detour that was successfully initialized in phase 1 (phase 2 of
/// `install`). `unmet_dependency`, when `Some`, means a *different* detour
/// this hook body calls through (e.g. `hook_get_system_time` calls
/// `GetSystemTimeAsFileTimeDetour`) failed to initialize — enabling this hook
/// anyway would arm a hook body that panics the first time it runs, so it is
/// recorded as failed instead.
fn try_enable<T: retour::Function>(
report: &mut InstallReport,
detour: &'static retour::StaticDetour<T>,
name: &'static str,
initialized: bool,
unmet_dependency: Option<&'static str>,
) {
if !initialized {
return; // already recorded as failed by init_hook!
}
if let Some(dep) = unmet_dependency {
report
.failed
.push((name, format!("dependency {dep} did not initialize")));
return;
}
match unsafe { detour.enable() } {
Err(e) => report.failed.push((name, format!("enable: {e}"))),
Ok(()) => report.installed.push(name),
}
}
/// Resolve, initialize, and enable every hook, then wire up the (lazily
/// connected) delta source. `global_name`/`local_name` are the two MMF names
/// the bootstrap thread probes — connecting happens on first use inside
/// `delta()`, not here, so hooking still activates even if the controller
/// creates the mapping after injection has already completed.
pub fn install(global_name: String, local_name: String) -> InstallReport {
let _ = SOURCE.set(DeltaSource {
global_name,
local_name,
reader: RwLock::new(None),
misses_since_retry: AtomicU32::new(0),
});
let mut report = InstallReport::default();
install_hook!(
report, GetSystemTimeDetour, "kernel32.dll", "GetSystemTime",
FnSystemTime, hook_get_system_time
// Phase 1: resolve + initialize every detour before enabling any of them.
// `hook_get_system_time` and `hook_get_local_time` call through
// `GetSystemTimeAsFileTimeDetour`'s trampoline, and the trampoline only
// exists once that detour has been initialized — `static_detour!`'s
// generated `call()` panics with `NotInitialized` otherwise, and with
// `panic = "abort"` in the release profile that panic kills the target
// process. Initializing every detour before enabling any of them removes
// the racy window (GetSystemTime enabled while another thread is still
// initializing the FileTime detour); phase 2 below additionally refuses
// to enable a hook whose dependency never initialized at all, which
// removes the permanent version of the same failure.
let system_time_ok = init_hook!(
report,
GetSystemTimeDetour,
"kernel32.dll",
"GetSystemTime",
FnSystemTime,
hook_get_system_time
);
install_hook!(
report, GetLocalTimeDetour, "kernel32.dll", "GetLocalTime",
FnSystemTime, hook_get_local_time
let local_time_ok = init_hook!(
report,
GetLocalTimeDetour,
"kernel32.dll",
"GetLocalTime",
FnSystemTime,
hook_get_local_time
);
install_hook!(
report, GetSystemTimeAsFileTimeDetour, "kernel32.dll", "GetSystemTimeAsFileTime",
FnFileTime, hook_get_system_time_as_filetime
let filetime_ok = init_hook!(
report,
GetSystemTimeAsFileTimeDetour,
"kernel32.dll",
"GetSystemTimeAsFileTime",
FnFileTime,
hook_get_system_time_as_filetime
);
install_hook!(
report, GetSystemTimePreciseAsFileTimeDetour, "kernel32.dll", "GetSystemTimePreciseAsFileTime",
FnFileTime, hook_get_system_time_precise_as_filetime
let precise_filetime_ok = init_hook!(
report,
GetSystemTimePreciseAsFileTimeDetour,
"kernel32.dll",
"GetSystemTimePreciseAsFileTime",
FnFileTime,
hook_get_system_time_precise_as_filetime
);
install_hook!(
report, NtQuerySystemTimeDetour, "ntdll.dll", "NtQuerySystemTime",
FnNtQuerySystemTime, hook_nt_query_system_time
let nt_query_ok = init_hook!(
report,
NtQuerySystemTimeDetour,
"ntdll.dll",
"NtQuerySystemTime",
FnNtQuerySystemTime,
hook_nt_query_system_time
);
// Phase 2: enable. Retour patches the live prologue with no thread
// suspension or i-cache flush of its own (verified in the vendored
// source); a thread executing that exact prologue mid-`enable()` is a
// known, accepted race inherent to this hooking approach, not something
// this crate mitigates.
let unmet_filetime = (!filetime_ok).then_some("GetSystemTimeAsFileTime");
try_enable(
&mut report,
&GetSystemTimeDetour,
"GetSystemTime",
system_time_ok,
unmet_filetime,
);
try_enable(
&mut report,
&GetLocalTimeDetour,
"GetLocalTime",
local_time_ok,
unmet_filetime,
);
try_enable(
&mut report,
&GetSystemTimeAsFileTimeDetour,
"GetSystemTimeAsFileTime",
filetime_ok,
None,
);
try_enable(
&mut report,
&GetSystemTimePreciseAsFileTimeDetour,
"GetSystemTimePreciseAsFileTime",
precise_filetime_ok,
None,
);
try_enable(
&mut report,
&NtQuerySystemTimeDetour,
"NtQuerySystemTime",
nt_query_ok,
None,
);
report
}
@@ -99,16 +237,49 @@ unsafe fn resolve<F: Copy>(module: &str, proc: &str) -> Option<F> {
#[inline]
fn delta() -> i64 {
SHARED.get().map(|s| s.read_delta()).unwrap_or(0)
let Some(source) = SOURCE.get() else {
return 0;
};
if let Ok(guard) = source.reader.read() {
if let Some(reader) = guard.as_ref() {
return reader.read_delta();
}
}
try_reconnect(source)
}
/// Not yet connected to the delta channel. Re-probe both MMF names once every
/// `RETRY_EVERY_N_CALLS` misses (see its doc comment) instead of on every
/// call. A race between threads landing on the same retry slot is harmless —
/// `SharedDeltaReader::open` is idempotent and cheap to call twice.
fn try_reconnect(source: &DeltaSource) -> i64 {
let misses = source.misses_since_retry.fetch_add(1, Ordering::Relaxed);
if !misses.is_multiple_of(RETRY_EVERY_N_CALLS) {
return 0;
}
let opened = SharedDeltaReader::open(&source.global_name)
.or_else(|_| SharedDeltaReader::open(&source.local_name));
match opened {
Ok(reader) => {
let value = reader.read_delta();
if let Ok(mut guard) = source.reader.write() {
*guard = Some(reader);
}
value
}
Err(_) => 0,
}
}
/// Invoke the supplied trampoline to get the real FILETIME, then add the
/// shared delta. Saturating arithmetic — no panic-abort even at i64 bounds.
/// shared delta, clamped to the range `FileTimeToSystemTime` can convert (see
/// `time_mocker_core::ticks::apply_delta`) so callers never see a conversion
/// failure caused by an out-of-range delta from shared memory.
#[inline]
fn fake_filetime(call_real: impl FnOnce(*mut FILETIME)) -> FILETIME {
let mut ft: FILETIME = unsafe { std::mem::zeroed() };
call_real(&mut ft);
let ticks = filetime_to_i64(ft).saturating_add(delta());
let ticks = apply_delta(filetime_to_i64(ft), delta());
i64_to_filetime(ticks)
}
@@ -117,7 +288,15 @@ fn hook_get_system_time(out: *mut SYSTEMTIME) {
return;
}
let ft = fake_filetime(|p| unsafe { GetSystemTimeAsFileTimeDetour.call(p) });
unsafe { FileTimeToSystemTime(&ft, out) };
if unsafe { FileTimeToSystemTime(&ft, out) } == 0 {
// `apply_delta` keeps `ft` in the valid FILETIME range, so this should
// not happen — but the delta is attacker/bug-controlled input from a
// separate process, so never leave `out` uninitialized on the
// off-chance it does. `GetSystemTimeDetour` is exactly the detour
// currently executing this hook body, so its trampoline is guaranteed
// initialized here.
unsafe { GetSystemTimeDetour.call(out) };
}
}
fn hook_get_local_time(out: *mut SYSTEMTIME) {
@@ -132,6 +311,10 @@ fn hook_get_local_time(out: *mut SYSTEMTIME) {
let ft_utc = fake_filetime(|p| unsafe { GetSystemTimeAsFileTimeDetour.call(p) });
let mut st_utc: SYSTEMTIME = unsafe { std::mem::zeroed() };
if unsafe { FileTimeToSystemTime(&ft_utc, &mut st_utc) } == 0 {
// See `hook_get_system_time` — fall back to the real local time rather
// than return with `out` uninitialized. `GetLocalTimeDetour` is this
// hook's own detour, so its trampoline is guaranteed initialized here.
unsafe { GetLocalTimeDetour.call(out) };
return;
}
if unsafe { SystemTimeToTzSpecificLocalTime(std::ptr::null(), &st_utc, out) } == 0 {
@@ -171,6 +354,6 @@ fn hook_nt_query_system_time(out: *mut i64) -> i32 {
// delta+0 with a bogus STATUS_SUCCESS if the real API ever failed.
return status;
}
unsafe { *out = real.saturating_add(delta()) };
unsafe { *out = apply_delta(real, delta()) };
0
}