Skip to main content

imping_hook/
batch.rs

1//! Committing many enables and disables in one transaction.
2//!
3//! Each transaction suspends every other thread, and finding them can take
4//! tens of milliseconds on a busy system, so one transaction per patch adds up.
5
6use crate::error::{Error, Result};
7use std::cell::Cell;
8use std::sync::{Mutex, MutexGuard};
9use std::thread::{self, ThreadId};
10
11/// A queued change of one patch.
12struct Pending {
13    /// The batching thread that queued it.
14    thread: ThreadId,
15    /// The patch's detour. Its owner keeps it boxed, so the address is stable,
16    /// and removes its entries with [`forget`] before freeing it.
17    detour: *const (dyn detour::Detour + 'static),
18    target: usize,
19    enable: bool,
20}
21
22// SAFETY: the pointer is only dereferenced under the `QUEUE` lock, and owners
23// remove their entries under that lock before freeing the (Sync) detour.
24unsafe impl Send for Pending {}
25
26/// Changes queued by every thread in a [`batch`].
27///
28/// Shared so a patch dropped on any thread can remove its entries, and locked
29/// during a commit so no detour is freed meanwhile.
30static QUEUE: Mutex<Vec<Pending>> = Mutex::new(Vec::new());
31
32thread_local! {
33    /// Whether this thread is inside a [`batch`].
34    static BATCHING: Cell<bool> = const { Cell::new(false) };
35}
36
37fn lock() -> MutexGuard<'static, Vec<Pending>> {
38    // Poisoning is harmless: entries are only pushed or removed whole.
39    QUEUE
40        .lock()
41        .unwrap_or_else(|poisoned| poisoned.into_inner())
42}
43
44/// Runs `f`, then applies the enables and disables it made on this thread in
45/// one transaction, so other threads are suspended once rather than per patch.
46///
47/// Inside `f`, `enable` and `disable` on a [`Hook`](crate::Hook) or
48/// [`Injection`](crate::Injection) are queued and return `Ok` at once, and
49/// `is_enabled` still reports the old state. The last change queued for a
50/// patch wins, a nested `batch` joins the outer one, and other threads aren't
51/// affected. If `f` panics, the queue is still applied before the panic goes on.
52///
53/// Returns an error, naming the address, for each change that couldn't be
54/// applied. If applying them together fails, each is retried alone, so one bad
55/// patch doesn't hold back the rest.
56///
57/// # Examples
58///
59/// ```no_run
60/// use imping_hook::Injection;
61///
62/// # fn targets() -> Vec<*const ()> { Vec::new() }
63/// let mut injections = Vec::new();
64/// for target in targets() {
65///     // SAFETY: each target is an instruction start of a function this
66///     // program owns, and a `nop` changes nothing.
67///     injections.push(unsafe { Injection::at(target).code(&[0x90]).install() }.unwrap());
68/// }
69/// let failed = imping_hook::batch(|| {
70///     for injection in &injections {
71///         injection.enable().expect("queued");
72///     }
73/// });
74/// assert!(failed.is_empty());
75/// ```
76pub fn batch(f: impl FnOnce()) -> Vec<Error> {
77    if BATCHING.with(|b| b.replace(true)) {
78        // Nested: the outermost batch commits.
79        f();
80        return Vec::new();
81    }
82    let guard = Commit;
83    f();
84    guard.finish()
85}
86
87/// Applies this thread's queue when the outermost `batch` ends, even when
88/// unwinding.
89struct Commit;
90
91impl Commit {
92    fn finish(self) -> Vec<Error> {
93        core::mem::forget(self);
94        commit_current_thread()
95    }
96}
97
98impl Drop for Commit {
99    fn drop(&mut self) {
100        // Only reached when unwinding; the errors can't be returned.
101        let _ = commit_current_thread();
102    }
103}
104
105/// Queues a change if this thread is in a batch, or returns `false` so the
106/// caller applies it itself.
107pub(crate) fn queue(detour: &(dyn detour::Detour + 'static), target: usize, enable: bool) -> bool {
108    if !BATCHING.with(Cell::get) {
109        return false;
110    }
111    let thread = thread::current().id();
112    let mut queue = lock();
113    queue.retain(|p| p.target != target);
114    queue.push(Pending {
115        thread,
116        detour,
117        target,
118        enable,
119    });
120    true
121}
122
123/// Drops every change any thread queued for `target`.
124///
125/// Called before its detour is freed or a change is applied directly. Waits for
126/// a commit in progress.
127pub(crate) fn forget(target: usize) {
128    lock().retain(|p| p.target != target);
129}
130
131/// Ends this thread's batch and applies its changes in one transaction, or
132/// one by one if that fails.
133fn commit_current_thread() -> Vec<Error> {
134    BATCHING.with(|b| b.set(false));
135    let thread = thread::current().id();
136    let mut queue = lock();
137    let (mine, others): (Vec<_>, Vec<_>) = queue.drain(..).partition(|p| p.thread == thread);
138    *queue = others;
139    if mine.is_empty() {
140        return Vec::new();
141    }
142    // Hold the lock until everything is applied so no owner frees a detour.
143    let mut transaction = detour::Transaction::new();
144    for p in &mine {
145        // SAFETY: owners remove their entries under this lock before freeing
146        // their boxed detour, so every queued pointer is live.
147        let detour = unsafe { &*p.detour };
148        if p.enable {
149            transaction.enable(detour);
150        } else {
151            transaction.disable(detour);
152        }
153    }
154    // SAFETY: `Threads::All` suspends every other thread, and the caller isn't
155    // running the patched bytes.
156    if unsafe { transaction.commit(detour::Threads::All) }.is_ok() {
157        return Vec::new();
158    }
159    mine.iter().filter_map(|p| commit_one(p).err()).collect()
160}
161
162fn commit_one(p: &Pending) -> Result<()> {
163    // SAFETY: see `commit_current_thread`; the queue is still locked.
164    let detour = unsafe { &*p.detour };
165    let mut transaction = detour::Transaction::new();
166    if p.enable {
167        transaction.enable(detour);
168    } else {
169        transaction.disable(detour);
170    }
171    // SAFETY: see `commit_current_thread`.
172    unsafe { transaction.commit(detour::Threads::All) }
173        .map_err(|error| Error::from_detour(error, p.target))
174}