Skip to main content

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}