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}