frust_hotpatch/hot_fn.rs
1// Ported from subsecond 0.7.10 (DioxusLabs, MIT OR Apache-2.0),
2// https://github.com/DioxusLabs/dioxus/tree/main/packages/subsecond.
3//
4//! The call-side detour: in a debug build, [`HotFn`] looks its function up in the installed
5//! [`JumpTable`](crate::JumpTable) on every call and jumps to the patched version when one is
6//! mapped, else runs the original.
7//!
8//! The lookup key is the address of the function's [`HotFunction::call_it`] monomorphisation, a
9//! real function per closure or fn-item type, so any `FnMut` dispatches soundly. A plain function
10//! pointer can instead be keyed on its own value, but only through the explicitly typed
11//! [`HotFn::from_fn_ptr`]; nothing infers it from a type's size.
12
13use std::cell::Cell;
14use std::collections::BTreeSet;
15use std::convert::Infallible;
16use std::marker::PhantomData;
17use std::mem::transmute;
18use std::sync::atomic::{AtomicU64, Ordering};
19use std::sync::{Mutex, PoisonError};
20
21use serde::{Deserialize, Serialize};
22
23use crate::{AddressMap, get_jump_table};
24
25/// Call `f` once through the jump table.
26///
27/// In a debug build, if the installed table maps `f`'s `call_it` address, the patched version
28/// runs; otherwise `f` itself does. There is no stale-call detection and no retry: a panic from
29/// `f` unwinds through `call` untouched, and code already running when a patch lands finishes as
30/// the old code. Equivalent to `HotFn::current(f).call(())`; a release build calls `f` directly.
31pub fn call<O>(f: impl FnMut() -> O) -> O {
32 HotFn::current(f).call(())
33}
34
35/// The address a hot function currently resolves to.
36#[non_exhaustive]
37#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
38pub struct HotFnPtr(pub u64);
39
40impl HotFnPtr {
41 /// Wrap a raw address. The safe way to get one is [`HotFn::ptr_address`].
42 ///
43 /// # Safety
44 ///
45 /// `index` must be the address of a valid function.
46 pub unsafe fn new(index: u64) -> Self {
47 Self(index)
48 }
49}
50
51/// Patched-dispatch misses since the last patch (see [`fall_through_count`]).
52static FALL_THROUGHS: AtomicU64 = AtomicU64::new(0);
53
54/// Patched-dispatch hits since the last patch (see [`seam_hits`]).
55static SEAM_HITS: AtomicU64 = AtomicU64::new(0);
56
57/// Each distinct missed key since the last patch (see [`missed_keys`]).
58static MISSED_KEYS: Mutex<BTreeSet<MissedKey>> = Mutex::new(BTreeSet::new());
59
60/// A jump-table key that missed, located in the image the calling code belongs to.
61///
62/// The key is the calling image's own [`HotFunction::call_it`] address (or, for
63/// [`HotFn::from_fn_ptr`], the pointer's value). `image` is 0 for the base executable and `n` for
64/// the n-th patch library this crate loaded, found from the address ranges of those images;
65/// `link_address` is the key minus that image's slide, the address the image's symbol table
66/// knows. A key in no known image has `image` [`UNKNOWN_IMAGE`](crate::UNKNOWN_IMAGE) and the raw
67/// key as `link_address`. This crate only records; the host decides what a miss means.
68#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
69pub struct MissedKey {
70 /// 0 for the base executable, `n` for the n-th loaded patch.
71 pub image: u32,
72 /// The key minus the image's slide.
73 pub link_address: u64,
74}
75
76thread_local! {
77 // Already `const`: the lint misfires on Android, where `thread_local!` expands differently.
78 #[allow(clippy::missing_const_for_thread_local)]
79 static LAST_FELL_THROUGH: Cell<bool> = const { Cell::new(false) };
80}
81
82/// Record one jump-table lookup's outcome: `Some(key)` means a table was installed but held no
83/// entry for this call's own key, so the original code ran; `hit` means the table mapped the key.
84fn record_lookup(missed_key: Option<u64>, hit: bool) {
85 LAST_FELL_THROUGH.with(|c| c.set(missed_key.is_some()));
86 if hit {
87 SEAM_HITS.fetch_add(1, Ordering::Relaxed);
88 }
89 if let Some(key) = missed_key {
90 FALL_THROUGHS.fetch_add(1, Ordering::Relaxed);
91 let (image, link_address) = crate::anchor::classify(key);
92 MISSED_KEYS
93 .lock()
94 .unwrap_or_else(PoisonError::into_inner)
95 .insert(MissedKey {
96 image,
97 link_address,
98 });
99 }
100}
101
102/// Zero the per-patch dispatch diagnostics: misses, hits and recorded missed keys.
103pub(crate) fn reset_fall_throughs() {
104 FALL_THROUGHS.store(0, Ordering::Relaxed);
105 SEAM_HITS.store(0, Ordering::Relaxed);
106 MISSED_KEYS
107 .lock()
108 .unwrap_or_else(PoisonError::into_inner)
109 .clear();
110}
111
112/// How many [`HotFn`] calls, on any thread, found their key in the installed jump table (and so
113/// ran the patched version) since the last patch was applied. Reset to zero by every patch.
114pub fn seam_hits() -> u64 {
115 SEAM_HITS.load(Ordering::Relaxed)
116}
117
118/// Every distinct key that missed the jump table since the last patch was applied, each once,
119/// ordered by image then link address. Reset by every patch.
120///
121/// A count cannot tell a stale-code miss from a benign one: a call made from the newest patch's
122/// code computes that patch's own `call_it` address, which is never a table key, so it always
123/// misses although it runs the newest code. The image of each key tells them apart; the host
124/// classifies, this crate only records.
125pub fn missed_keys() -> Vec<MissedKey> {
126 MISSED_KEYS
127 .lock()
128 .unwrap_or_else(PoisonError::into_inner)
129 .iter()
130 .copied()
131 .collect()
132}
133
134/// Whether this thread's most recent [`HotFn`] call found a jump table installed but no mapping
135/// for its own key, and so ran the function compiled into the calling image.
136///
137/// A purely diagnostic read; dispatch never consults it. The key is the address of `call_it` as
138/// seen by the image the call is made from, and the table's keys are the running executable's
139/// (base image) addresses. So a miss means one of three things:
140///
141/// - Stale code: after a patch that changes a generic call's type parameters (e.g. a component's
142/// state type), the patched monomorphisation has a new symbol the running binary never calls,
143/// so the base caller's old code keeps running.
144/// - Nothing to patch: a hot function the patch did not recompile has no entry.
145/// - A benign patch-image caller: a call made from patch code (e.g. a patched component's rebuild
146/// calling a nested component's hot function) computes the patch image's own `call_it`
147/// address, which is never a key. A call from the newest patch's code already runs the newest
148/// code; one from an older patch's code still reachable through its vtables runs that older
149/// patch's version.
150///
151/// This flag records only the outcome. The missed key, with the image it lies in, is recorded
152/// separately ([`missed_keys`]); that is what tells these apart.
153pub fn last_call_fell_through() -> bool {
154 LAST_FELL_THROUGH.with(Cell::get)
155}
156
157/// How many [`HotFn`] calls, on any thread, missed the jump table since the last patch was
158/// applied (see [`last_call_fell_through`]). Reset to zero by every successful `apply_patch`.
159///
160/// The count includes the benign misses of calls made from patch-image code, so after a patch
161/// to an app with nested components it is normally non-zero. It stays a plain count in this
162/// crate; telling a stale-code miss from a benign one needs the missed key (whose address range
163/// names the calling image), which [`missed_keys`] records.
164pub fn fall_through_count() -> u64 {
165 FALL_THROUGHS.load(Ordering::Relaxed)
166}
167
168/// Whether lookups strip the top-byte pointer tag. Only aarch64 Android tags pointers (MTE / TBI);
169/// the jump table is keyed on untagged addresses.
170const STRIP_POINTER_TAG: bool = cfg!(all(target_pointer_width = "64", target_os = "android"));
171
172/// Split `addr` into its untagged address and its top-byte tag (the aarch64 Android pointer tag).
173fn split_pointer_tag(addr: u64) -> (u64, u64) {
174 (addr & 0x00FF_FFFF_FFFF_FFFF, addr & 0xFF00_0000_0000_0000)
175}
176
177/// Look `addr` up in `map`. With `strip_tag`, the lookup uses the untagged address and the tag is
178/// re-applied to the patched address it returns.
179fn resolve(map: &AddressMap, addr: u64, strip_tag: bool) -> Option<u64> {
180 let (key, tag) = if strip_tag {
181 split_pointer_tag(addr)
182 } else {
183 (addr, 0)
184 };
185 map.get(&key).map(|patched| patched | tag)
186}
187
188/// The jump-table key of `F`'s [`HotFunction::call_it`] monomorphisation.
189fn call_it_address<A, M, F: HotFunction<A, M>>() -> u64 {
190 <F as HotFunction<A, M>>::call_it as *const () as usize as u64
191}
192
193/// The fn-pointer operations a [`HotFn::from_fn_ptr`] value dispatches through. Only that
194/// constructor builds one, and its `F: FnPointer` bound proves `F` is a plain function pointer.
195struct FnPointerOps<A, R, F> {
196 address: fn(&F) -> u64,
197 call_at: unsafe fn(u64, A) -> R,
198}
199
200/// A hot-reloadable function: [`HotFn::call`] runs the newest version the jump table maps.
201pub struct HotFn<A, M, F>
202where
203 F: HotFunction<A, M>,
204{
205 inner: F,
206 /// `Some` only when built by [`HotFn::from_fn_ptr`]: dispatch keys on the pointer's value.
207 by_value: Option<FnPointerOps<A, F::Return, F>>,
208 _marker: PhantomData<(A, M)>,
209}
210
211impl<A, M, F: HotFunction<A, M>> HotFn<A, M, F> {
212 /// Wrap `f`, keyed on its `call_it` monomorphisation. Sound for any `FnMut`: a fn item, a
213 /// closure of any size, or a function pointer (which then dispatches as one `call_it` shared
214 /// by every pointer of that type; use [`HotFn::from_fn_ptr`] to key on the pointer itself).
215 pub const fn current(f: F) -> HotFn<A, M, F> {
216 HotFn {
217 inner: f,
218 by_value: None,
219 _marker: PhantomData,
220 }
221 }
222
223 /// Wrap the function pointer `f`, keyed on its own value: the jump table maps the pointed-to
224 /// function's address to its patched version. `F` must be a non-higher-ranked
225 /// `fn(A, ..) -> R` of up to nine arguments ([`FnPointer`]); wrap anything else with
226 /// [`HotFn::current`].
227 pub fn from_fn_ptr(f: F) -> HotFn<A, M, F>
228 where
229 F: FnPointer<A, M>,
230 {
231 HotFn {
232 inner: f,
233 by_value: Some(FnPointerOps {
234 address: <F as FnPointer<A, M>>::address,
235 call_at: <F as FnPointer<A, M>>::call_at,
236 }),
237 _marker: PhantomData,
238 }
239 }
240
241 /// Call the function with `args`.
242 ///
243 /// In a debug build this is one jump-table lookup (see [`HotFn::try_call`]): the patched
244 /// version runs when the installed table maps this function's key, else the original. No
245 /// stale-call detection, no retry; panics propagate untouched. A release build calls the
246 /// function directly and never reads the table.
247 pub fn call(&mut self, args: A) -> F::Return {
248 match self.try_call(args) {
249 Ok(ret) => ret,
250 Err(never) => match never {},
251 }
252 }
253
254 /// The address a call would currently reach: the jump table's mapping for this function's key
255 /// when one is installed and maps it, else the key itself. The key is the `call_it` address,
256 /// or for [`HotFn::from_fn_ptr`] the pointer's value. A release build never reads the table
257 /// and always reports the key.
258 ///
259 /// A runtime that memoizes state across patches can compare it between patches: an unchanged
260 /// address means the state "above" the function is still valid. Tracking it over time is the
261 /// caller's job.
262 pub fn ptr_address(&self) -> HotFnPtr {
263 let key = self.key();
264 if !cfg!(debug_assertions) {
265 return HotFnPtr(key);
266 }
267 // SAFETY: the table reference is used only within this call, never held across a patch.
268 let resolved = unsafe { get_jump_table() }
269 .and_then(|table| resolve(&table.map, key, STRIP_POINTER_TAG));
270 HotFnPtr(resolved.unwrap_or(key))
271 }
272
273 /// Call the function with `args` through one jump-table lookup.
274 ///
275 /// Debug builds only: the key (the `call_it` address, or for [`HotFn::from_fn_ptr`] the
276 /// pointer's value, with the aarch64 Android pointer tag stripped) is looked up in the
277 /// installed table; a hit calls the patched address, a miss or an empty table calls the
278 /// original, and the outcome is recorded for [`last_call_fell_through`] /
279 /// [`fall_through_count`] / [`missed_keys`]; a hit is counted by [`seam_hits`]. A release build
280 /// calls the function directly.
281 ///
282 /// Never fails: the `Result` keeps subsecond's call shape, and the error type is
283 /// [`Infallible`].
284 pub fn try_call(&mut self, args: A) -> Result<F::Return, Infallible> {
285 if !cfg!(debug_assertions) {
286 return Ok(self.inner.call_it(args));
287 }
288
289 let key = self.key();
290 // SAFETY: the table reference is used only within this block, never held across a patch.
291 let target = match unsafe { get_jump_table() } {
292 None => {
293 record_lookup(None, false);
294 None
295 }
296 Some(table) => {
297 let target = resolve(&table.map, key, STRIP_POINTER_TAG);
298 let missed = target.is_none().then(|| {
299 if STRIP_POINTER_TAG {
300 split_pointer_tag(key).0
301 } else {
302 key
303 }
304 });
305 record_lookup(missed, target.is_some());
306 target
307 }
308 };
309
310 match target {
311 // SAFETY: the lookup above found `target` as the installed table's entry for this
312 // function's own key. Tables are installed only by `apply_patch`, whose safety
313 // contract requires two things of every entry: it is the patched version of the keyed
314 // function in a table built against this exact executable, and every argument, return
315 // and capture type of that function (here `A`, `F::Return` and `F` itself, a
316 // closure's captures included) keeps its layout between the running image and the
317 // patch. Together they give `target` the signature and the argument layouts
318 // `call_at` passes for this `HotFn`. It lives in the loaded patch library, which is
319 // never unloaded.
320 Some(target) => Ok(unsafe { self.call_at(target, args) }),
321 None => Ok(self.inner.call_it(args)),
322 }
323 }
324
325 /// Call the function at `ptr` directly, bypassing the jump table.
326 ///
327 /// `ptr` is consulted on every call, in every build profile (release included): the table is
328 /// never read and no fall-through is recorded.
329 ///
330 /// # Safety
331 ///
332 /// For a [`HotFn::current`] value, `ptr` must be this monomorphisation's `call_it` (signature
333 /// `fn(&mut F, A) -> F::Return`) or its patched equivalent. For a [`HotFn::from_fn_ptr`]
334 /// value, `ptr` must be a function of type `F`: the pointer's own target or its patched
335 /// equivalent. Either way the argument and return layouts (for [`HotFn::current`], `F`'s own
336 /// layout too: a closure's captures) must not have changed since `ptr` was taken.
337 /// [`HotFn::ptr_address`] on a `HotFn` built the same way returns such an address.
338 pub unsafe fn try_call_with_ptr(
339 &mut self,
340 ptr: HotFnPtr,
341 args: A,
342 ) -> Result<F::Return, Infallible> {
343 // SAFETY: this function's contract is exactly `call_at`'s.
344 Ok(unsafe { self.call_at(ptr.0, args) })
345 }
346
347 /// The jump-table key: the pointer's value for a [`HotFn::from_fn_ptr`] value, else the
348 /// `call_it` address.
349 fn key(&self) -> u64 {
350 match &self.by_value {
351 Some(ops) => (ops.address)(&self.inner),
352 None => call_it_address::<A, M, F>(),
353 }
354 }
355
356 /// Call the function at `address`.
357 ///
358 /// # Safety
359 ///
360 /// For a `by_value` (fn-pointer) `HotFn`, `address` must be a function of type `F`; otherwise
361 /// it must be a function with `call_it`'s signature for this monomorphisation.
362 unsafe fn call_at(&mut self, address: u64, args: A) -> F::Return {
363 if let Some(ops) = &self.by_value {
364 // SAFETY: `ops` exists only for a `from_fn_ptr` value, so `F` is a plain function
365 // pointer and `ops.call_at` is its `FnPointer::call_at`, whose contract (`address`
366 // is a function of type `F`) is this function's.
367 return unsafe { (ops.call_at)(address, args) };
368 }
369 // SAFETY: the caller guarantees `address` has `call_it`'s exact signature for this
370 // monomorphisation. On 32-bit targets the host still writes 64-bit addresses, so
371 // truncating to `usize` is lossless; function pointers need no alignment beyond 1.
372 let call_it = unsafe {
373 transmute::<*const (), fn(&mut F, A) -> F::Return>(address as usize as *const ())
374 };
375 call_it(&mut self.inner, args)
376 }
377}
378
379/// A function that can be detoured through the jump table: implemented for every `FnMut` of up to
380/// nine arguments (taken as a tuple). `FnOnce` is excluded because a patch may re-run the function.
381pub trait HotFunction<Args, Marker> {
382 /// The function's return type.
383 type Return;
384
385 /// Call the function with its arguments as a tuple.
386 ///
387 /// The `rust-call` ABI is unstable, so this wrapper gives every implementing type a stable,
388 /// real function per monomorphisation: the address the jump table is keyed on.
389 fn call_it(&mut self, args: Args) -> Self::Return;
390}
391
392mod sealed {
393 /// Restricts [`FnPointer`](super::FnPointer) to the plain function-pointer types below.
394 pub trait Sealed {}
395}
396
397/// A plain, non-higher-ranked function-pointer type `fn(A, ..) -> R` of up to nine arguments: the
398/// only types [`HotFn::from_fn_ptr`] accepts. Sealed; implemented by this crate only.
399pub trait FnPointer<Args, Marker>: HotFunction<Args, Marker> + Copy + sealed::Sealed {
400 /// The function's address, the jump-table key.
401 fn address(&self) -> u64;
402
403 /// Call the function at `address` as `Self`.
404 ///
405 /// # Safety
406 ///
407 /// `address` must be a function of exactly type `Self`.
408 unsafe fn call_at(address: u64, args: Args) -> Self::Return;
409}
410
411macro_rules! impl_hot_function {
412 (
413 $(
414 ($marker:ident, $($arg:ident),*)
415 ),*
416 ) => {
417 $(
418 /// Arity marker; hidden to keep [`HotFunction`] effectively sealed.
419 #[doc(hidden)]
420 pub struct $marker;
421
422 impl<T, $($arg,)* R> HotFunction<($($arg,)*), $marker> for T
423 where
424 T: FnMut($($arg),*) -> R,
425 {
426 type Return = R;
427
428 fn call_it(&mut self, args: ($($arg,)*)) -> Self::Return {
429 #[allow(non_snake_case)]
430 let ( $($arg,)* ) = args;
431 self($($arg),*)
432 }
433 }
434
435 impl<$($arg,)* R> sealed::Sealed for fn($($arg),*) -> R {}
436
437 impl<$($arg,)* R> FnPointer<($($arg,)*), $marker> for fn($($arg),*) -> R {
438 fn address(&self) -> u64 {
439 *self as usize as u64
440 }
441
442 unsafe fn call_at(address: u64, args: ($($arg,)*)) -> R {
443 // SAFETY: the caller guarantees `address` is a function of exactly this type.
444 // On 32-bit targets the host still writes 64-bit addresses, so truncating to
445 // `usize` is lossless; function pointers need no alignment beyond 1.
446 let f = unsafe {
447 transmute::<*const (), fn($($arg),*) -> R>(address as usize as *const ())
448 };
449 #[allow(non_snake_case)]
450 let ( $($arg,)* ) = args;
451 f($($arg),*)
452 }
453 }
454 )*
455 };
456}
457
458impl_hot_function!(
459 (Fn0Marker,),
460 (Fn1Marker, A),
461 (Fn2Marker, A, B),
462 (Fn3Marker, A, B, C),
463 (Fn4Marker, A, B, C, D),
464 (Fn5Marker, A, B, C, D, E),
465 (Fn6Marker, A, B, C, D, E, F),
466 (Fn7Marker, A, B, C, D, E, F, G),
467 (Fn8Marker, A, B, C, D, E, F, G, H),
468 (Fn9Marker, A, B, C, D, E, F, G, H, I)
469);
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474 use crate::JumpTable;
475 use crate::patch::test_support;
476
477 fn add(a: u32, b: u32) -> u32 {
478 a + b
479 }
480
481 fn mul(a: u32, b: u32) -> u32 {
482 a * b
483 }
484
485 /// A stand-in for the patched `call_it` of `F`: the same signature, different behaviour.
486 fn patched_call_it<F>(_f: &mut F, args: (u32, u32)) -> u32 {
487 args.0 * args.1
488 }
489
490 fn call_it_key_of<A, M, F: HotFunction<A, M>>(_f: &F) -> u64 {
491 call_it_address::<A, M, F>()
492 }
493
494 fn patched_call_it_of<F>(_f: &F) -> u64 {
495 patched_call_it::<F> as *const () as usize as u64
496 }
497
498 fn table(entries: &[(u64, u64)]) -> JumpTable {
499 JumpTable {
500 lib: "/nonexistent/frust-hotpatch-test.so".into(),
501 map: entries.iter().copied().collect(),
502 aslr_reference: 0,
503 new_base_address: 0,
504 ifunc_count: 0,
505 }
506 }
507
508 #[test]
509 fn unpatched_calls_run_the_original() {
510 let _serial = test_support::serial();
511 test_support::reset();
512 assert_eq!(HotFn::current(add).call((2, 3)), 5);
513 let fp: fn(u32, u32) -> u32 = add;
514 assert_eq!(HotFn::current(fp).call((4, 5)), 9);
515 assert_eq!(HotFn::from_fn_ptr(fp).call((4, 5)), 9);
516 let offset = 10;
517 assert_eq!(HotFn::current(|x: u32| x + offset).call((1,)), 11);
518 assert_eq!(call(|| 7), 7);
519 }
520
521 #[test]
522 fn no_table_is_not_a_fall_through() {
523 let _serial = test_support::serial();
524 test_support::reset();
525 HotFn::current(add).call((1, 1));
526 assert!(!last_call_fell_through());
527 }
528
529 #[test]
530 fn ptr_address_of_a_fn_pointer_is_its_value() {
531 let _serial = test_support::serial();
532 test_support::reset();
533 let fp: fn(u32, u32) -> u32 = add;
534 assert_eq!(HotFn::from_fn_ptr(fp).ptr_address().0, fp as usize as u64);
535 // Through `current`, a fn pointer is keyed on its `call_it` like any other `FnMut`.
536 assert_eq!(HotFn::current(fp).ptr_address().0, call_it_key_of(&fp));
537 }
538
539 #[test]
540 #[cfg_attr(
541 not(debug_assertions),
542 ignore = "dispatch reads the table only in debug builds; run `cargo test -p frust-hotpatch`"
543 )]
544 fn patched_dispatch_hits_misses_and_resets() {
545 let _serial = test_support::serial();
546 test_support::reset();
547
548 // Hit: the table maps `add`'s `call_it` to a stand-in with the same signature.
549 let key = call_it_key_of(&add);
550 let target = patched_call_it_of(&add);
551 test_support::install(table(&[(key, target)]));
552 assert_eq!(HotFn::current(add).call((3, 4)), 12);
553 assert!(!last_call_fell_through());
554 assert_eq!(fall_through_count(), 0);
555 assert_eq!(HotFn::current(add).ptr_address().0, target);
556
557 // Miss: `mul`'s `call_it` is a different monomorphisation, absent from the table.
558 assert_eq!(HotFn::current(mul).call((3, 4)), 12);
559 assert!(last_call_fell_through());
560 assert_eq!(fall_through_count(), 1);
561 assert_eq!(HotFn::current(mul).ptr_address().0, call_it_key_of(&mul));
562
563 // A newer patch resets the miss count.
564 test_support::install(table(&[(key, target)]));
565 assert_eq!(fall_through_count(), 0);
566 assert_eq!(HotFn::current(add).call((2, 5)), 10);
567 assert!(!last_call_fell_through());
568
569 test_support::reset();
570 }
571
572 #[test]
573 #[cfg_attr(
574 not(debug_assertions),
575 ignore = "dispatch reads the table only in debug builds; run `cargo test -p frust-hotpatch`"
576 )]
577 fn fn_pointer_dispatch_keys_on_the_pointer_value() {
578 let _serial = test_support::serial();
579 test_support::reset();
580
581 let fp: fn(u32, u32) -> u32 = add;
582 let patched: fn(u32, u32) -> u32 = mul;
583 test_support::install(table(&[(fp as usize as u64, patched as usize as u64)]));
584 assert_eq!(HotFn::from_fn_ptr(fp).call((3, 4)), 12);
585 assert!(!last_call_fell_through());
586 // `current` keys on the shared `call_it`, which this table does not map.
587 assert_eq!(HotFn::current(fp).call((3, 4)), 7);
588 assert!(last_call_fell_through());
589
590 test_support::reset();
591 }
592
593 #[test]
594 fn try_call_with_ptr_calls_the_given_address() {
595 let _serial = test_support::serial();
596 test_support::reset();
597 let ptr = HotFnPtr(patched_call_it_of(&add));
598 // SAFETY: `patched_call_it::<F>` has exactly `call_it`'s signature for `add`'s fn item.
599 let out = unsafe { HotFn::current(add).try_call_with_ptr(ptr, (3, 4)) };
600 assert_eq!(out, Ok(12));
601
602 let fp: fn(u32, u32) -> u32 = add;
603 let patched: fn(u32, u32) -> u32 = mul;
604 // SAFETY: `mul` is a function of exactly type `fn(u32, u32) -> u32`.
605 let out = unsafe {
606 HotFn::from_fn_ptr(fp).try_call_with_ptr(HotFnPtr(patched as usize as u64), (3, 4))
607 };
608 assert_eq!(out, Ok(12));
609 }
610
611 fn decoy() -> usize {
612 1
613 }
614
615 fn wrong_target() -> usize {
616 999
617 }
618
619 #[test]
620 #[cfg_attr(
621 not(debug_assertions),
622 ignore = "dispatch reads the table only in debug builds; run `cargo test -p frust-hotpatch`"
623 )]
624 fn pointer_sized_closure_is_not_a_fn_pointer() {
625 let _serial = test_support::serial();
626 test_support::reset();
627
628 // A capturing closure exactly as large as a fn pointer, whose captured value is itself a
629 // mapped function address: treating its bytes as a fn pointer would jump to `wrong_target`.
630 let n: usize = decoy as *const () as usize;
631 assert_eq!(
632 std::mem::size_of_val(&move || n),
633 std::mem::size_of::<fn()>()
634 );
635
636 assert_eq!(call(move || n), n);
637 assert!(!last_call_fell_through());
638
639 let wrong: fn() -> usize = wrong_target;
640 test_support::install(table(&[(n as u64, wrong as usize as u64)]));
641 assert_eq!(call(move || n), n);
642 assert!(last_call_fell_through());
643
644 test_support::reset();
645 }
646
647 #[test]
648 fn panics_propagate_through_call() {
649 let _serial = test_support::serial();
650 let res = std::panic::catch_unwind(|| call(|| -> u32 { panic!("not ours") }));
651 assert!(res.is_err());
652 }
653
654 #[test]
655 fn split_pointer_tag_masks_the_top_byte() {
656 let tagged = 0xb400_7fff_1234_5678;
657 let (untagged, tag) = split_pointer_tag(tagged);
658 assert_eq!(untagged, 0x0000_7fff_1234_5678);
659 assert_eq!(tag, 0xb400_0000_0000_0000);
660 assert_eq!(untagged | tag, tagged);
661 }
662
663 #[test]
664 fn tagged_lookup_strips_and_reapplies_the_tag() {
665 let map: AddressMap = [(0x0000_7fff_1234_5678, 0x0000_7fff_9999_0000)]
666 .into_iter()
667 .collect();
668 let tagged = 0xb400_7fff_1234_5678;
669 assert_eq!(
670 resolve(&map, tagged, true),
671 Some(0xb400_7fff_9999_0000),
672 "the aarch64 Android path looks up untagged and re-tags the target"
673 );
674 assert_eq!(
675 resolve(&map, tagged, false),
676 None,
677 "untagged path keys verbatim"
678 );
679 assert_eq!(
680 resolve(&map, 0x0000_7fff_1234_5678, true),
681 Some(0x0000_7fff_9999_0000)
682 );
683 }
684
685 #[test]
686 #[cfg_attr(
687 not(debug_assertions),
688 ignore = "dispatch reads the table only in debug builds; run `cargo test -p frust-hotpatch`"
689 )]
690 fn seam_hits_count_and_reset_per_patch() {
691 let _serial = test_support::serial();
692 test_support::reset();
693
694 let key = call_it_key_of(&add);
695 let target = patched_call_it_of(&add);
696 test_support::install(table(&[(key, target)]));
697 assert_eq!(seam_hits(), 0);
698 HotFn::current(add).call((1, 2));
699 HotFn::current(add).call((1, 2));
700 assert_eq!(seam_hits(), 2);
701 // A miss is not a hit.
702 HotFn::current(mul).call((1, 2));
703 assert_eq!(seam_hits(), 2);
704
705 test_support::install(table(&[(key, target)]));
706 assert_eq!(seam_hits(), 0, "a patch resets the hit count");
707 test_support::reset();
708 }
709
710 #[test]
711 #[cfg_attr(
712 not(debug_assertions),
713 ignore = "dispatch reads the table only in debug builds; run `cargo test -p frust-hotpatch`"
714 )]
715 fn missed_keys_are_recorded_once_with_image_and_link_address() {
716 let _serial = test_support::serial();
717 test_support::reset();
718
719 let add_key = call_it_key_of(&add);
720 let mul_key = call_it_key_of(&mul);
721 // Synthetic ranges: `add`'s key lies in image 0 (slide 0x100), `mul`'s in patch image 2.
722 let base = |key: u64| (key as usize - 0x10, key as usize + 0x10);
723 let (a_lo, a_hi) = base(add_key);
724 let (m_lo, m_hi) = base(mul_key);
725 crate::anchor::register_for_test(0, a_lo, a_hi, 0x100);
726 crate::anchor::register_for_test(2, m_lo, m_hi, 0x40);
727
728 // A table that maps neither, so both miss.
729 test_support::install(table(&[(1, 2)]));
730 for _ in 0..3 {
731 HotFn::current(add).call((1, 2));
732 HotFn::current(mul).call((1, 2));
733 }
734 assert_eq!(
735 fall_through_count(),
736 6,
737 "the count keeps counting every miss"
738 );
739 let mut expected = vec![
740 MissedKey {
741 image: 0,
742 link_address: add_key - 0x100,
743 },
744 MissedKey {
745 image: 2,
746 link_address: mul_key - 0x40,
747 },
748 ];
749 expected.sort();
750 assert_eq!(missed_keys(), expected, "each distinct key once");
751
752 test_support::install(table(&[(1, 2)]));
753 assert!(missed_keys().is_empty(), "a patch resets the recorded keys");
754
755 // A key in no known image is reported as such, with the raw key.
756 crate::anchor::reset_for_test();
757 HotFn::current(add).call((1, 2));
758 assert_eq!(
759 missed_keys(),
760 vec![MissedKey {
761 image: crate::UNKNOWN_IMAGE,
762 link_address: add_key
763 }]
764 );
765 test_support::reset();
766 }
767}