imping_runtime/hook.rs
1//! Function hooks for generated bindings, and their guard.
2//!
3//! Each hookable function has a static [`Slot`] and a generated `thunk` that
4//! calls the slot's closure. The first [`install`] creates the patch, which is
5//! kept for the rest of the process so its trampoline is never freed; guards
6//! only enable and disable it. Replaced closures are leaked, as a thread may
7//! still be running one.
8
9use crate::backend::{self, Patch};
10use crate::error::{Error, Result};
11use crate::function::Function;
12use crate::module::Module;
13use core::fmt;
14use core::marker::PhantomData;
15use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering};
16use std::sync::OnceLock;
17
18/// A hook on a function, returned disabled by a generated function's `hook`.
19///
20/// `F` is the function's generated `Signature` type. While the guard is alive,
21/// this mod can't hook the function again.
22///
23/// # Dropping
24///
25/// Dropping the guard disables the hook as [`disable`](Self::disable) does,
26/// and the function can then be hooked again. If disabling fails, the hook
27/// stays in place and the function stays reserved. A thread still running the
28/// closure is unaffected, as nothing it uses is freed.
29///
30/// # Threads and batches
31///
32/// [`enable`](Self::enable) and [`disable`](Self::disable) suspend every other
33/// thread of the process while patching, so don't call them from inside the
34/// hooked function. Inside an [`imping_hook::batch`] they are queued and
35/// applied together when the batch ends. The `_unsuspended` variants skip the
36/// suspension and take effect at once, even inside a batch.
37pub struct Hook<F: 'static> {
38 installed: &'static Installed<F>,
39 owned: &'static AtomicBool,
40}
41
42impl<F: Copy + 'static> Hook<F> {
43 /// Redirects the function to the hook's closure.
44 ///
45 /// Suspends other threads while patching; see [`Hook`] for threads and batches.
46 ///
47 /// # Errors
48 ///
49 /// [`Error::Hook`] if patching fails.
50 pub fn enable(&self) -> Result<()> {
51 self.installed
52 .patch
53 .enable()
54 .map_err(|source| self.installed.error(source))?;
55 log::debug!("imping: enabled hook at {}", self.installed.location());
56 Ok(())
57 }
58
59 /// Restores the function, so calls skip the closure.
60 ///
61 /// Suspends other threads while patching; see [`Hook`] for threads and batches.
62 ///
63 /// # Errors
64 ///
65 /// [`Error::Hook`] if patching fails.
66 pub fn disable(&self) -> Result<()> {
67 self.installed
68 .patch
69 .disable()
70 .map_err(|source| self.installed.error(source))?;
71 log::debug!("imping: disabled hook at {}", self.installed.location());
72 Ok(())
73 }
74
75 /// Redirects the function to the hook's closure without suspending other
76 /// threads.
77 ///
78 /// Faster than [`enable`](Self::enable); see [`Hook`] for threads and
79 /// batches.
80 ///
81 /// # Errors
82 ///
83 /// [`Error::Hook`] if patching fails.
84 ///
85 /// # Safety
86 ///
87 /// No other thread may execute the function's first instructions while
88 /// they are patched, or it may run a half-written instruction.
89 pub unsafe fn enable_unsuspended(&self) -> Result<()> {
90 // SAFETY: forwarded from the caller.
91 unsafe { self.installed.patch.enable_unsuspended() }
92 .map_err(|source| self.installed.error(source))?;
93 log::debug!("imping: enabled hook at {}", self.installed.location());
94 Ok(())
95 }
96
97 /// Restores the function without suspending other threads.
98 ///
99 /// Faster than [`disable`](Self::disable); see [`Hook`] for threads and
100 /// batches.
101 ///
102 /// # Errors
103 ///
104 /// [`Error::Hook`] if patching fails.
105 ///
106 /// # Safety
107 ///
108 /// No other thread may execute the function's first instructions while
109 /// the original ones are written back.
110 pub unsafe fn disable_unsuspended(&self) -> Result<()> {
111 // SAFETY: forwarded from the caller.
112 unsafe { self.installed.patch.disable_unsuspended() }
113 .map_err(|source| self.installed.error(source))?;
114 log::debug!("imping: disabled hook at {}", self.installed.location());
115 Ok(())
116 }
117
118 /// Returns whether the function is redirected to the closure.
119 pub fn is_enabled(&self) -> bool {
120 self.installed.patch.is_enabled()
121 }
122
123 /// Returns the original, unhooked function.
124 ///
125 /// It stays valid for the rest of the process, even after the guard is
126 /// dropped.
127 pub fn original(&self) -> F {
128 self.installed.original
129 }
130
131 /// Keeps the hook, in its current state, for the rest of the process.
132 ///
133 /// The function stays reserved, and the mod must never be unloaded.
134 pub fn leak(self) {
135 core::mem::forget(self);
136 }
137}
138
139impl<F: 'static> Drop for Hook<F> {
140 fn drop(&mut self) {
141 if self.installed.patch.disable().is_ok() {
142 log::debug!("imping: dropped hook at {}", self.installed.location());
143 self.owned.store(false, Ordering::Release);
144 }
145 }
146}
147
148impl<F: 'static> fmt::Debug for Hook<F> {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 f.debug_struct("Hook")
151 .field("module", &self.installed.module.name())
152 .field("rva", &format_args!("{:#x}", self.installed.rva))
153 .field("enabled", &self.installed.patch.is_enabled())
154 .finish()
155 }
156}
157
158/// A function's patch, installed once and kept for the rest of the process.
159struct Installed<F: 'static> {
160 module: &'static Module,
161 rva: u32,
162 patch: Box<dyn Patch<F>>,
163 original: F,
164}
165
166impl<F> Installed<F> {
167 fn error(&self, source: imping_hook::Error) -> Error {
168 Error::Hook {
169 module: self.module.name(),
170 rva: self.rva,
171 source,
172 }
173 }
174
175 fn location(&self) -> String {
176 format!("`{}`+{:#x}", self.module.name(), self.rva)
177 }
178}
179
180/// The per-function state behind a generated hook: the patch, the current
181/// closure, and whether a [`Hook`] guard owns it.
182///
183/// Generated code declares one `static` per hookable function, with `D` the
184/// closure's `dyn Fn` type and `F` the function's signature.
185pub struct Slot<D: ?Sized + 'static, F: 'static> {
186 installed: OnceLock<Installed<F>>,
187 /// The current closure as a leaked, never freed `Box<D>`; null before the
188 /// first install.
189 detour: AtomicPtr<Box<D>>,
190 /// Whether a live guard owns the slot.
191 owned: AtomicBool,
192 /// Makes the slot `Send`/`Sync` only if the closure is.
193 _closure: PhantomData<Box<D>>,
194}
195
196impl<D: ?Sized + 'static, F: 'static> Slot<D, F> {
197 /// Creates an empty slot.
198 pub const fn new() -> Self {
199 Self {
200 installed: OnceLock::new(),
201 detour: AtomicPtr::new(core::ptr::null_mut()),
202 owned: AtomicBool::new(false),
203 _closure: PhantomData,
204 }
205 }
206}
207
208impl<D: ?Sized + 'static, F: 'static> Default for Slot<D, F> {
209 fn default() -> Self {
210 Self::new()
211 }
212}
213
214impl<D: ?Sized + Send + Sync + 'static, F: Copy + 'static> Slot<D, F> {
215 /// Returns the current closure and the original function, for the thunk.
216 ///
217 /// # Panics
218 ///
219 /// If nothing was installed yet, which is a bug in the generated code. The
220 /// panic aborts the process, since the thunk is an `extern` function.
221 pub fn get(&self) -> (&D, F) {
222 let detour = self.detour.load(Ordering::Acquire);
223 match self.installed.get() {
224 Some(installed) if !detour.is_null() => {
225 // SAFETY: a non-null `detour` is a `Box<D>` leaked by `install`
226 // and never freed.
227 (unsafe { &**detour }, installed.original)
228 }
229 _ => panic!("imping: hook slot used before anything was installed; this is a bug"),
230 }
231 }
232}
233
234/// Hooks `function` with `detour`, returning the guard disabled.
235///
236/// Called by a generated function's `hook`. The first install for a function
237/// finds it before patching: at its RVA if its first bytes match, otherwise
238/// by its pattern.
239///
240/// # Errors
241///
242/// - [`Error::NotInitialized`] if the function's module isn't initialized.
243/// - [`Error::AlreadyHooked`] if a guard for this function is alive.
244/// - [`Error::VerificationFailed`] if the first bytes differ and the pattern
245/// doesn't find the function elsewhere.
246/// - [`Error::NotInBuild`] if the loaded build's IR lacks the function and
247/// nothing else finds it.
248/// - [`Error::Hook`] if patching fails.
249///
250/// # Safety
251///
252/// - The function and `thunk` must both have signature `F`.
253/// - `thunk` must call the closure and original returned by `slot.get()`, and
254/// `slot` must only ever be used with this function.
255pub unsafe fn install<D, F>(
256 function: &'static Function,
257 slot: &'static Slot<D, F>,
258 detour: Box<D>,
259 thunk: F,
260) -> Result<Hook<F>>
261where
262 D: ?Sized + Send + Sync + 'static,
263 F: imping_hook::Function + Copy + Send + Sync + 'static,
264{
265 let module = function.module();
266 let Some(base) = module.base() else {
267 return Err(Error::NotInitialized {
268 module: module.name(),
269 });
270 };
271 if slot.owned.swap(true, Ordering::AcqRel) {
272 return Err(Error::AlreadyHooked {
273 module: module.name(),
274 rva: function.known_rva(),
275 });
276 }
277 // Only the owner gets here, so the first install is never contended.
278 let installed = match slot.installed.get() {
279 Some(installed) => installed,
280 None => {
281 // SAFETY: forwarded from the caller.
282 match unsafe { install_patch(function, base, thunk) } {
283 Ok(installed) => slot.installed.get_or_init(|| installed),
284 Err(error) => {
285 slot.owned.store(false, Ordering::Release);
286 return Err(error);
287 }
288 }
289 }
290 };
291
292 // The previous closure is leaked: a thread may still be running it.
293 let detour = Box::into_raw(Box::new(detour));
294 slot.detour.swap(detour, Ordering::AcqRel);
295
296 Ok(Hook {
297 installed,
298 owned: &slot.owned,
299 })
300}
301
302/// Finds the function and patches it, disabled.
303///
304/// # Safety
305///
306/// The function and `thunk` must both have signature `F`.
307unsafe fn install_patch<F>(
308 function: &'static Function,
309 base: usize,
310 thunk: F,
311) -> Result<Installed<F>>
312where
313 F: imping_hook::Function + Copy + Send + Sync + 'static,
314{
315 let module = function.module();
316 let target = function.locate()?;
317 // Where it was found, which differs from the bindings' RVA if it moved.
318 let rva = target.wrapping_sub(base) as u32;
319 // SAFETY: the caller promises the function and `thunk` share signature `F`.
320 let (patch, original) =
321 unsafe { backend::install(target as *const (), thunk) }.map_err(|source| Error::Hook {
322 module: module.name(),
323 rva,
324 source,
325 })?;
326 log::debug!("imping: hooked `{}`+{rva:#x}", module.name());
327 Ok(Installed {
328 module,
329 rva,
330 patch,
331 original,
332 })
333}