Skip to main content

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}