1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
//! A narrowly checked workaround for TF2's Windows `host_timescale` cheat gate.
//!
//! Linux has no corresponding gate. This does not change `sv_cheats`, console
//! flags, or clients' replicated variables. It only bypasses the Windows
//! engine's check immediately before applying a positive `host_timescale`.
use crate::interfaces::Cvar;
#[cfg(target_os = "windows")]
use sdk_raw::tf2::host_timescale::Gate;
use sdk_raw::tf2::host_timescale::GateError;
use sdk_raw::util::patch::{self, PatchOperation};
use std::marker::PhantomData;
use std::rc::Rc;
/// Owns the single-byte Windows workaround until it is explicitly restored.
///
/// The Windows engine module remains loaded while this guard exists. Unknown
/// instruction sequences are refused. `Drop` attempts restoration, but callers
/// should call [`Self::restore`] to observe and handle errors before unloading.
/// On Linux this is a no-op guard.
#[doc(alias("Host_AccumulateTime"))]
#[must_use = "keep the patch guard until the plugin restores normal time scaling"]
pub struct HostTimescalePatch {
#[cfg(target_os = "windows")]
gate: Gate,
_not_thread_safe: PhantomData<Rc<()>>,
}
impl HostTimescalePatch {
/// Locates the workaround's target in the currently loaded Windows engine.
/// This validates and retains the image without changing instructions.
///
/// The signature was verified in TF2's 64-bit Windows `Host_AccumulateTime`.
/// Both references to `host_timescale` and the reference to `sv_cheats` must
/// resolve to the registered variables' actual `m_pParent` fields. The two
/// rejection branches must lead to the same verified continuation, and the
/// replacement branch must lead to the float load. Exactly one match is
/// required across the engine's readable executable sections.
///
/// # Safety
///
/// The caller must be on the server's main thread in a TF2 dedicated-server
/// callback, outside execution of `Host_AccumulateTime`. No other thread may
/// execute or patch that routine during installation or restoration. The
/// guard must be restored and dropped under the same conditions, before
/// engine shutdown. `cvar` must belong to that server's engine.
pub unsafe fn locate(cvar: Cvar<'_>) -> Result<Self, PatchError> {
#[cfg(target_os = "windows")]
let gate = {
let timescale = cvar
.find_var(c"host_timescale")
.ok_or(PatchError::MissingVariable("host_timescale"))?;
let cheats = cvar
.find_var(c"sv_cheats")
.ok_or(PatchError::MissingVariable("sv_cheats"))?;
// SAFETY: The caller upholds the engine-thread and timing contract
// for locating, enabling, restoring and dropping, which the guard
// forwards to its gate.
unsafe { Gate::locate(timescale.as_ptr(), cheats.as_ptr()) }?
};
#[cfg(not(target_os = "windows"))]
let _ = cvar;
Ok(Self {
#[cfg(target_os = "windows")]
gate,
_not_thread_safe: PhantomData,
})
}
/// Enables the workaround, retaining restoration ownership on any error.
///
/// If a Windows operation fails after writing the byte, this guard still
/// owns the patch and any pending page-protection or cache cleanup. Call
/// [`Self::restore`] before releasing it. Calling this again after successful
/// enabling is idempotent while its installed instruction remains unchanged.
///
/// # Safety
///
/// As for [`Self::locate`]: run on the server's main thread while nobody
/// executes or patches `Host_AccumulateTime`.
pub unsafe fn enable(&mut self) -> Result<(), PatchError> {
#[cfg(target_os = "windows")]
{
// SAFETY: The caller upholds the engine-thread and timing contract.
Ok(unsafe { self.gate.enable() }?)
}
#[cfg(not(target_os = "windows"))]
Ok(())
}
/// Whether this guard has an outstanding installed Windows patch.
///
/// This is always `false` on Linux.
pub fn is_active(&self) -> bool {
#[cfg(target_os = "windows")]
return self.gate.is_active();
#[cfg(not(target_os = "windows"))]
false
}
/// Restores the original instruction, page protection, and instruction cache.
///
/// This is idempotent after successful restoration. If another component
/// changed the byte, it is left alone and an error is returned. A failed
/// Windows operation can be retried while the guard remains alive.
///
/// # Safety
///
/// As for [`Self::locate`]: run on the server's main thread while nobody
/// executes or patches `Host_AccumulateTime`.
pub unsafe fn restore(&mut self) -> Result<(), PatchError> {
#[cfg(target_os = "windows")]
{
// SAFETY: The caller upholds the engine-thread and timing contract.
Ok(unsafe { self.gate.restore() }?)
}
#[cfg(not(target_os = "windows"))]
Ok(())
}
}
/// Why the engine's time-scale workaround could not be installed or restored.
#[derive(Debug, thiserror::Error)]
pub enum PatchError {
/// The engine has no console variable with this name, so the gate's
/// references to it cannot be verified.
#[error("the engine does not register `{0}`")]
MissingVariable(&'static str),
/// `engine.dll`'s headers or mapped pages are not the committed, readable
/// 64-bit PE image the scan expects.
#[error("engine.dll has an unsupported or inaccessible PE image")]
InvalidImage,
/// `host_timescale` or `sv_cheats` is not in `engine.dll`'s data sections,
/// or no candidate passed every signature and reference check, as with
/// another engine build or an already patched gate.
#[error("engine.dll does not contain the verified TF2 time-scale gate")]
UnsupportedEngine,
/// More than one candidate passed every check, so none is patched.
#[error("engine.dll contains more than one verified TF2 time-scale gate")]
AmbiguousGate,
/// The gate's instructions no longer match what the guard verified or
/// wrote. They are left unchanged.
#[error("another component changed the time-scale gate's instruction")]
InstructionChanged,
/// A Windows API call failed. The OS error is both in the message and the
/// error's [`source`](std::error::Error::source).
#[error("{operation} failed: {source}")]
Windows {
/// The failed Windows function and what it was applied to.
operation: &'static str,
/// The error Windows reported for the call.
#[source]
source: std::io::Error,
},
}
impl From<GateError> for PatchError {
fn from(error: GateError) -> Self {
match error {
GateError::AmbiguousGate => Self::AmbiguousGate,
GateError::InvalidImage => Self::InvalidImage,
GateError::ModuleNotLoaded(source) => Self::Windows {
operation: "GetModuleHandleExW(engine.dll)",
source,
},
GateError::UnsupportedEngine => Self::UnsupportedEngine,
}
}
}
impl From<patch::PatchError> for PatchError {
fn from(error: patch::PatchError) -> Self {
match error {
patch::PatchError::InstructionChanged => Self::InstructionChanged,
patch::PatchError::Os { operation, source } => Self::Windows {
operation: match operation {
PatchOperation::FlushInstructionCache => {
"FlushInstructionCache(time-scale gate)"
}
PatchOperation::Reprotect => {
"VirtualProtect(time-scale gate, original protection)"
}
PatchOperation::Unprotect => "VirtualProtect(time-scale gate, writable)",
},
source,
},
}
}
}