frust-hotpatch 0.6.0

Native-only hot-patch runtime for Frust: a jump-table detour ported from subsecond 0.7.10, wire-compatible with dx.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
// Ported from subsecond 0.7.10 (DioxusLabs, MIT OR Apache-2.0),
// https://github.com/DioxusLabs/dioxus/tree/main/packages/subsecond.
//
//! The call-side detour: in a debug build, [`HotFn`] looks its function up in the installed
//! [`JumpTable`](crate::JumpTable) on every call and jumps to the patched version when one is
//! mapped, else runs the original.
//!
//! The lookup key is the address of the function's [`HotFunction::call_it`] monomorphisation, a
//! real function per closure or fn-item type, so any `FnMut` dispatches soundly. A plain function
//! pointer can instead be keyed on its own value, but only through the explicitly typed
//! [`HotFn::from_fn_ptr`]; nothing infers it from a type's size.

use std::cell::Cell;
use std::collections::BTreeSet;
use std::convert::Infallible;
use std::marker::PhantomData;
use std::mem::transmute;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Mutex, PoisonError};

use serde::{Deserialize, Serialize};

use crate::{AddressMap, get_jump_table};

/// Call `f` once through the jump table.
///
/// In a debug build, if the installed table maps `f`'s `call_it` address, the patched version
/// runs; otherwise `f` itself does. There is no stale-call detection and no retry: a panic from
/// `f` unwinds through `call` untouched, and code already running when a patch lands finishes as
/// the old code. Equivalent to `HotFn::current(f).call(())`; a release build calls `f` directly.
pub fn call<O>(f: impl FnMut() -> O) -> O {
    HotFn::current(f).call(())
}

/// The address a hot function currently resolves to.
#[non_exhaustive]
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
pub struct HotFnPtr(pub u64);

impl HotFnPtr {
    /// Wrap a raw address. The safe way to get one is [`HotFn::ptr_address`].
    ///
    /// # Safety
    ///
    /// `index` must be the address of a valid function.
    pub unsafe fn new(index: u64) -> Self {
        Self(index)
    }
}

/// Patched-dispatch misses since the last patch (see [`fall_through_count`]).
static FALL_THROUGHS: AtomicU64 = AtomicU64::new(0);

/// Patched-dispatch hits since the last patch (see [`seam_hits`]).
static SEAM_HITS: AtomicU64 = AtomicU64::new(0);

/// Each distinct missed key since the last patch (see [`missed_keys`]).
static MISSED_KEYS: Mutex<BTreeSet<MissedKey>> = Mutex::new(BTreeSet::new());

/// A jump-table key that missed, located in the image the calling code belongs to.
///
/// The key is the calling image's own [`HotFunction::call_it`] address (or, for
/// [`HotFn::from_fn_ptr`], the pointer's value). `image` is 0 for the base executable and `n` for
/// the n-th patch library this crate loaded, found from the address ranges of those images;
/// `link_address` is the key minus that image's slide, the address the image's symbol table
/// knows. A key in no known image has `image` [`UNKNOWN_IMAGE`](crate::UNKNOWN_IMAGE) and the raw
/// key as `link_address`. This crate only records; the host decides what a miss means.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MissedKey {
    /// 0 for the base executable, `n` for the n-th loaded patch.
    pub image: u32,
    /// The key minus the image's slide.
    pub link_address: u64,
}

thread_local! {
    // Already `const`: the lint misfires on Android, where `thread_local!` expands differently.
    #[allow(clippy::missing_const_for_thread_local)]
    static LAST_FELL_THROUGH: Cell<bool> = const { Cell::new(false) };
}

/// Record one jump-table lookup's outcome: `Some(key)` means a table was installed but held no
/// entry for this call's own key, so the original code ran; `hit` means the table mapped the key.
fn record_lookup(missed_key: Option<u64>, hit: bool) {
    LAST_FELL_THROUGH.with(|c| c.set(missed_key.is_some()));
    if hit {
        SEAM_HITS.fetch_add(1, Ordering::Relaxed);
    }
    if let Some(key) = missed_key {
        FALL_THROUGHS.fetch_add(1, Ordering::Relaxed);
        let (image, link_address) = crate::anchor::classify(key);
        MISSED_KEYS
            .lock()
            .unwrap_or_else(PoisonError::into_inner)
            .insert(MissedKey {
                image,
                link_address,
            });
    }
}

/// Zero the per-patch dispatch diagnostics: misses, hits and recorded missed keys.
pub(crate) fn reset_fall_throughs() {
    FALL_THROUGHS.store(0, Ordering::Relaxed);
    SEAM_HITS.store(0, Ordering::Relaxed);
    MISSED_KEYS
        .lock()
        .unwrap_or_else(PoisonError::into_inner)
        .clear();
}

/// How many [`HotFn`] calls, on any thread, found their key in the installed jump table (and so
/// ran the patched version) since the last patch was applied. Reset to zero by every patch.
pub fn seam_hits() -> u64 {
    SEAM_HITS.load(Ordering::Relaxed)
}

/// Every distinct key that missed the jump table since the last patch was applied, each once,
/// ordered by image then link address. Reset by every patch.
///
/// A count cannot tell a stale-code miss from a benign one: a call made from the newest patch's
/// code computes that patch's own `call_it` address, which is never a table key, so it always
/// misses although it runs the newest code. The image of each key tells them apart; the host
/// classifies, this crate only records.
pub fn missed_keys() -> Vec<MissedKey> {
    MISSED_KEYS
        .lock()
        .unwrap_or_else(PoisonError::into_inner)
        .iter()
        .copied()
        .collect()
}

/// Whether this thread's most recent [`HotFn`] call found a jump table installed but no mapping
/// for its own key, and so ran the function compiled into the calling image.
///
/// A purely diagnostic read; dispatch never consults it. The key is the address of `call_it` as
/// seen by the image the call is made from, and the table's keys are the running executable's
/// (base image) addresses. So a miss means one of three things:
///
/// - Stale code: after a patch that changes a generic call's type parameters (e.g. a component's
///   state type), the patched monomorphisation has a new symbol the running binary never calls,
///   so the base caller's old code keeps running.
/// - Nothing to patch: a hot function the patch did not recompile has no entry.
/// - A benign patch-image caller: a call made from patch code (e.g. a patched component's rebuild
///   calling a nested component's hot function) computes the patch image's own `call_it`
///   address, which is never a key. A call from the newest patch's code already runs the newest
///   code; one from an older patch's code still reachable through its vtables runs that older
///   patch's version.
///
/// This flag records only the outcome. The missed key, with the image it lies in, is recorded
/// separately ([`missed_keys`]); that is what tells these apart.
pub fn last_call_fell_through() -> bool {
    LAST_FELL_THROUGH.with(Cell::get)
}

/// How many [`HotFn`] calls, on any thread, missed the jump table since the last patch was
/// applied (see [`last_call_fell_through`]). Reset to zero by every successful `apply_patch`.
///
/// The count includes the benign misses of calls made from patch-image code, so after a patch
/// to an app with nested components it is normally non-zero. It stays a plain count in this
/// crate; telling a stale-code miss from a benign one needs the missed key (whose address range
/// names the calling image), which [`missed_keys`] records.
pub fn fall_through_count() -> u64 {
    FALL_THROUGHS.load(Ordering::Relaxed)
}

/// Whether lookups strip the top-byte pointer tag. Only aarch64 Android tags pointers (MTE / TBI);
/// the jump table is keyed on untagged addresses.
const STRIP_POINTER_TAG: bool = cfg!(all(target_pointer_width = "64", target_os = "android"));

/// Split `addr` into its untagged address and its top-byte tag (the aarch64 Android pointer tag).
fn split_pointer_tag(addr: u64) -> (u64, u64) {
    (addr & 0x00FF_FFFF_FFFF_FFFF, addr & 0xFF00_0000_0000_0000)
}

/// Look `addr` up in `map`. With `strip_tag`, the lookup uses the untagged address and the tag is
/// re-applied to the patched address it returns.
fn resolve(map: &AddressMap, addr: u64, strip_tag: bool) -> Option<u64> {
    let (key, tag) = if strip_tag {
        split_pointer_tag(addr)
    } else {
        (addr, 0)
    };
    map.get(&key).map(|patched| patched | tag)
}

/// The jump-table key of `F`'s [`HotFunction::call_it`] monomorphisation.
fn call_it_address<A, M, F: HotFunction<A, M>>() -> u64 {
    <F as HotFunction<A, M>>::call_it as *const () as usize as u64
}

/// The fn-pointer operations a [`HotFn::from_fn_ptr`] value dispatches through. Only that
/// constructor builds one, and its `F: FnPointer` bound proves `F` is a plain function pointer.
struct FnPointerOps<A, R, F> {
    address: fn(&F) -> u64,
    call_at: unsafe fn(u64, A) -> R,
}

/// A hot-reloadable function: [`HotFn::call`] runs the newest version the jump table maps.
pub struct HotFn<A, M, F>
where
    F: HotFunction<A, M>,
{
    inner: F,
    /// `Some` only when built by [`HotFn::from_fn_ptr`]: dispatch keys on the pointer's value.
    by_value: Option<FnPointerOps<A, F::Return, F>>,
    _marker: PhantomData<(A, M)>,
}

impl<A, M, F: HotFunction<A, M>> HotFn<A, M, F> {
    /// Wrap `f`, keyed on its `call_it` monomorphisation. Sound for any `FnMut`: a fn item, a
    /// closure of any size, or a function pointer (which then dispatches as one `call_it` shared
    /// by every pointer of that type; use [`HotFn::from_fn_ptr`] to key on the pointer itself).
    pub const fn current(f: F) -> HotFn<A, M, F> {
        HotFn {
            inner: f,
            by_value: None,
            _marker: PhantomData,
        }
    }

    /// Wrap the function pointer `f`, keyed on its own value: the jump table maps the pointed-to
    /// function's address to its patched version. `F` must be a non-higher-ranked
    /// `fn(A, ..) -> R` of up to nine arguments ([`FnPointer`]); wrap anything else with
    /// [`HotFn::current`].
    pub fn from_fn_ptr(f: F) -> HotFn<A, M, F>
    where
        F: FnPointer<A, M>,
    {
        HotFn {
            inner: f,
            by_value: Some(FnPointerOps {
                address: <F as FnPointer<A, M>>::address,
                call_at: <F as FnPointer<A, M>>::call_at,
            }),
            _marker: PhantomData,
        }
    }

    /// Call the function with `args`.
    ///
    /// In a debug build this is one jump-table lookup (see [`HotFn::try_call`]): the patched
    /// version runs when the installed table maps this function's key, else the original. No
    /// stale-call detection, no retry; panics propagate untouched. A release build calls the
    /// function directly and never reads the table.
    pub fn call(&mut self, args: A) -> F::Return {
        match self.try_call(args) {
            Ok(ret) => ret,
            Err(never) => match never {},
        }
    }

    /// The address a call would currently reach: the jump table's mapping for this function's key
    /// when one is installed and maps it, else the key itself. The key is the `call_it` address,
    /// or for [`HotFn::from_fn_ptr`] the pointer's value. A release build never reads the table
    /// and always reports the key.
    ///
    /// A runtime that memoizes state across patches can compare it between patches: an unchanged
    /// address means the state "above" the function is still valid. Tracking it over time is the
    /// caller's job.
    pub fn ptr_address(&self) -> HotFnPtr {
        let key = self.key();
        if !cfg!(debug_assertions) {
            return HotFnPtr(key);
        }
        // SAFETY: the table reference is used only within this call, never held across a patch.
        let resolved = unsafe { get_jump_table() }
            .and_then(|table| resolve(&table.map, key, STRIP_POINTER_TAG));
        HotFnPtr(resolved.unwrap_or(key))
    }

    /// Call the function with `args` through one jump-table lookup.
    ///
    /// Debug builds only: the key (the `call_it` address, or for [`HotFn::from_fn_ptr`] the
    /// pointer's value, with the aarch64 Android pointer tag stripped) is looked up in the
    /// installed table; a hit calls the patched address, a miss or an empty table calls the
    /// original, and the outcome is recorded for [`last_call_fell_through`] /
    /// [`fall_through_count`] / [`missed_keys`]; a hit is counted by [`seam_hits`]. A release build
    /// calls the function directly.
    ///
    /// Never fails: the `Result` keeps subsecond's call shape, and the error type is
    /// [`Infallible`].
    pub fn try_call(&mut self, args: A) -> Result<F::Return, Infallible> {
        if !cfg!(debug_assertions) {
            return Ok(self.inner.call_it(args));
        }

        let key = self.key();
        // SAFETY: the table reference is used only within this block, never held across a patch.
        let target = match unsafe { get_jump_table() } {
            None => {
                record_lookup(None, false);
                None
            }
            Some(table) => {
                let target = resolve(&table.map, key, STRIP_POINTER_TAG);
                let missed = target.is_none().then(|| {
                    if STRIP_POINTER_TAG {
                        split_pointer_tag(key).0
                    } else {
                        key
                    }
                });
                record_lookup(missed, target.is_some());
                target
            }
        };

        match target {
            // SAFETY: the lookup above found `target` as the installed table's entry for this
            // function's own key. Tables are installed only by `apply_patch`, whose safety
            // contract requires two things of every entry: it is the patched version of the keyed
            // function in a table built against this exact executable, and every argument, return
            // and capture type of that function (here `A`, `F::Return` and `F` itself, a
            // closure's captures included) keeps its layout between the running image and the
            // patch. Together they give `target` the signature and the argument layouts
            // `call_at` passes for this `HotFn`. It lives in the loaded patch library, which is
            // never unloaded.
            Some(target) => Ok(unsafe { self.call_at(target, args) }),
            None => Ok(self.inner.call_it(args)),
        }
    }

    /// Call the function at `ptr` directly, bypassing the jump table.
    ///
    /// `ptr` is consulted on every call, in every build profile (release included): the table is
    /// never read and no fall-through is recorded.
    ///
    /// # Safety
    ///
    /// For a [`HotFn::current`] value, `ptr` must be this monomorphisation's `call_it` (signature
    /// `fn(&mut F, A) -> F::Return`) or its patched equivalent. For a [`HotFn::from_fn_ptr`]
    /// value, `ptr` must be a function of type `F`: the pointer's own target or its patched
    /// equivalent. Either way the argument and return layouts (for [`HotFn::current`], `F`'s own
    /// layout too: a closure's captures) must not have changed since `ptr` was taken.
    /// [`HotFn::ptr_address`] on a `HotFn` built the same way returns such an address.
    pub unsafe fn try_call_with_ptr(
        &mut self,
        ptr: HotFnPtr,
        args: A,
    ) -> Result<F::Return, Infallible> {
        // SAFETY: this function's contract is exactly `call_at`'s.
        Ok(unsafe { self.call_at(ptr.0, args) })
    }

    /// The jump-table key: the pointer's value for a [`HotFn::from_fn_ptr`] value, else the
    /// `call_it` address.
    fn key(&self) -> u64 {
        match &self.by_value {
            Some(ops) => (ops.address)(&self.inner),
            None => call_it_address::<A, M, F>(),
        }
    }

    /// Call the function at `address`.
    ///
    /// # Safety
    ///
    /// For a `by_value` (fn-pointer) `HotFn`, `address` must be a function of type `F`; otherwise
    /// it must be a function with `call_it`'s signature for this monomorphisation.
    unsafe fn call_at(&mut self, address: u64, args: A) -> F::Return {
        if let Some(ops) = &self.by_value {
            // SAFETY: `ops` exists only for a `from_fn_ptr` value, so `F` is a plain function
            // pointer and `ops.call_at` is its `FnPointer::call_at`, whose contract (`address`
            // is a function of type `F`) is this function's.
            return unsafe { (ops.call_at)(address, args) };
        }
        // SAFETY: the caller guarantees `address` has `call_it`'s exact signature for this
        // monomorphisation. On 32-bit targets the host still writes 64-bit addresses, so
        // truncating to `usize` is lossless; function pointers need no alignment beyond 1.
        let call_it = unsafe {
            transmute::<*const (), fn(&mut F, A) -> F::Return>(address as usize as *const ())
        };
        call_it(&mut self.inner, args)
    }
}

/// A function that can be detoured through the jump table: implemented for every `FnMut` of up to
/// nine arguments (taken as a tuple). `FnOnce` is excluded because a patch may re-run the function.
pub trait HotFunction<Args, Marker> {
    /// The function's return type.
    type Return;

    /// Call the function with its arguments as a tuple.
    ///
    /// The `rust-call` ABI is unstable, so this wrapper gives every implementing type a stable,
    /// real function per monomorphisation: the address the jump table is keyed on.
    fn call_it(&mut self, args: Args) -> Self::Return;
}

mod sealed {
    /// Restricts [`FnPointer`](super::FnPointer) to the plain function-pointer types below.
    pub trait Sealed {}
}

/// A plain, non-higher-ranked function-pointer type `fn(A, ..) -> R` of up to nine arguments: the
/// only types [`HotFn::from_fn_ptr`] accepts. Sealed; implemented by this crate only.
pub trait FnPointer<Args, Marker>: HotFunction<Args, Marker> + Copy + sealed::Sealed {
    /// The function's address, the jump-table key.
    fn address(&self) -> u64;

    /// Call the function at `address` as `Self`.
    ///
    /// # Safety
    ///
    /// `address` must be a function of exactly type `Self`.
    unsafe fn call_at(address: u64, args: Args) -> Self::Return;
}

macro_rules! impl_hot_function {
    (
        $(
            ($marker:ident, $($arg:ident),*)
        ),*
    ) => {
        $(
            /// Arity marker; hidden to keep [`HotFunction`] effectively sealed.
            #[doc(hidden)]
            pub struct $marker;

            impl<T, $($arg,)* R> HotFunction<($($arg,)*), $marker> for T
            where
                T: FnMut($($arg),*) -> R,
            {
                type Return = R;

                fn call_it(&mut self, args: ($($arg,)*)) -> Self::Return {
                    #[allow(non_snake_case)]
                    let ( $($arg,)* ) = args;
                    self($($arg),*)
                }
            }

            impl<$($arg,)* R> sealed::Sealed for fn($($arg),*) -> R {}

            impl<$($arg,)* R> FnPointer<($($arg,)*), $marker> for fn($($arg),*) -> R {
                fn address(&self) -> u64 {
                    *self as usize as u64
                }

                unsafe fn call_at(address: u64, args: ($($arg,)*)) -> R {
                    // SAFETY: the caller guarantees `address` is a function of exactly this type.
                    // On 32-bit targets the host still writes 64-bit addresses, so truncating to
                    // `usize` is lossless; function pointers need no alignment beyond 1.
                    let f = unsafe {
                        transmute::<*const (), fn($($arg),*) -> R>(address as usize as *const ())
                    };
                    #[allow(non_snake_case)]
                    let ( $($arg,)* ) = args;
                    f($($arg),*)
                }
            }
        )*
    };
}

impl_hot_function!(
    (Fn0Marker,),
    (Fn1Marker, A),
    (Fn2Marker, A, B),
    (Fn3Marker, A, B, C),
    (Fn4Marker, A, B, C, D),
    (Fn5Marker, A, B, C, D, E),
    (Fn6Marker, A, B, C, D, E, F),
    (Fn7Marker, A, B, C, D, E, F, G),
    (Fn8Marker, A, B, C, D, E, F, G, H),
    (Fn9Marker, A, B, C, D, E, F, G, H, I)
);

#[cfg(test)]
mod tests {
    use super::*;
    use crate::JumpTable;
    use crate::patch::test_support;

    fn add(a: u32, b: u32) -> u32 {
        a + b
    }

    fn mul(a: u32, b: u32) -> u32 {
        a * b
    }

    /// A stand-in for the patched `call_it` of `F`: the same signature, different behaviour.
    fn patched_call_it<F>(_f: &mut F, args: (u32, u32)) -> u32 {
        args.0 * args.1
    }

    fn call_it_key_of<A, M, F: HotFunction<A, M>>(_f: &F) -> u64 {
        call_it_address::<A, M, F>()
    }

    fn patched_call_it_of<F>(_f: &F) -> u64 {
        patched_call_it::<F> as *const () as usize as u64
    }

    fn table(entries: &[(u64, u64)]) -> JumpTable {
        JumpTable {
            lib: "/nonexistent/frust-hotpatch-test.so".into(),
            map: entries.iter().copied().collect(),
            aslr_reference: 0,
            new_base_address: 0,
            ifunc_count: 0,
        }
    }

    #[test]
    fn unpatched_calls_run_the_original() {
        let _serial = test_support::serial();
        test_support::reset();
        assert_eq!(HotFn::current(add).call((2, 3)), 5);
        let fp: fn(u32, u32) -> u32 = add;
        assert_eq!(HotFn::current(fp).call((4, 5)), 9);
        assert_eq!(HotFn::from_fn_ptr(fp).call((4, 5)), 9);
        let offset = 10;
        assert_eq!(HotFn::current(|x: u32| x + offset).call((1,)), 11);
        assert_eq!(call(|| 7), 7);
    }

    #[test]
    fn no_table_is_not_a_fall_through() {
        let _serial = test_support::serial();
        test_support::reset();
        HotFn::current(add).call((1, 1));
        assert!(!last_call_fell_through());
    }

    #[test]
    fn ptr_address_of_a_fn_pointer_is_its_value() {
        let _serial = test_support::serial();
        test_support::reset();
        let fp: fn(u32, u32) -> u32 = add;
        assert_eq!(HotFn::from_fn_ptr(fp).ptr_address().0, fp as usize as u64);
        // Through `current`, a fn pointer is keyed on its `call_it` like any other `FnMut`.
        assert_eq!(HotFn::current(fp).ptr_address().0, call_it_key_of(&fp));
    }

    #[test]
    #[cfg_attr(
        not(debug_assertions),
        ignore = "dispatch reads the table only in debug builds; run `cargo test -p frust-hotpatch`"
    )]
    fn patched_dispatch_hits_misses_and_resets() {
        let _serial = test_support::serial();
        test_support::reset();

        // Hit: the table maps `add`'s `call_it` to a stand-in with the same signature.
        let key = call_it_key_of(&add);
        let target = patched_call_it_of(&add);
        test_support::install(table(&[(key, target)]));
        assert_eq!(HotFn::current(add).call((3, 4)), 12);
        assert!(!last_call_fell_through());
        assert_eq!(fall_through_count(), 0);
        assert_eq!(HotFn::current(add).ptr_address().0, target);

        // Miss: `mul`'s `call_it` is a different monomorphisation, absent from the table.
        assert_eq!(HotFn::current(mul).call((3, 4)), 12);
        assert!(last_call_fell_through());
        assert_eq!(fall_through_count(), 1);
        assert_eq!(HotFn::current(mul).ptr_address().0, call_it_key_of(&mul));

        // A newer patch resets the miss count.
        test_support::install(table(&[(key, target)]));
        assert_eq!(fall_through_count(), 0);
        assert_eq!(HotFn::current(add).call((2, 5)), 10);
        assert!(!last_call_fell_through());

        test_support::reset();
    }

    #[test]
    #[cfg_attr(
        not(debug_assertions),
        ignore = "dispatch reads the table only in debug builds; run `cargo test -p frust-hotpatch`"
    )]
    fn fn_pointer_dispatch_keys_on_the_pointer_value() {
        let _serial = test_support::serial();
        test_support::reset();

        let fp: fn(u32, u32) -> u32 = add;
        let patched: fn(u32, u32) -> u32 = mul;
        test_support::install(table(&[(fp as usize as u64, patched as usize as u64)]));
        assert_eq!(HotFn::from_fn_ptr(fp).call((3, 4)), 12);
        assert!(!last_call_fell_through());
        // `current` keys on the shared `call_it`, which this table does not map.
        assert_eq!(HotFn::current(fp).call((3, 4)), 7);
        assert!(last_call_fell_through());

        test_support::reset();
    }

    #[test]
    fn try_call_with_ptr_calls_the_given_address() {
        let _serial = test_support::serial();
        test_support::reset();
        let ptr = HotFnPtr(patched_call_it_of(&add));
        // SAFETY: `patched_call_it::<F>` has exactly `call_it`'s signature for `add`'s fn item.
        let out = unsafe { HotFn::current(add).try_call_with_ptr(ptr, (3, 4)) };
        assert_eq!(out, Ok(12));

        let fp: fn(u32, u32) -> u32 = add;
        let patched: fn(u32, u32) -> u32 = mul;
        // SAFETY: `mul` is a function of exactly type `fn(u32, u32) -> u32`.
        let out = unsafe {
            HotFn::from_fn_ptr(fp).try_call_with_ptr(HotFnPtr(patched as usize as u64), (3, 4))
        };
        assert_eq!(out, Ok(12));
    }

    fn decoy() -> usize {
        1
    }

    fn wrong_target() -> usize {
        999
    }

    #[test]
    #[cfg_attr(
        not(debug_assertions),
        ignore = "dispatch reads the table only in debug builds; run `cargo test -p frust-hotpatch`"
    )]
    fn pointer_sized_closure_is_not_a_fn_pointer() {
        let _serial = test_support::serial();
        test_support::reset();

        // A capturing closure exactly as large as a fn pointer, whose captured value is itself a
        // mapped function address: treating its bytes as a fn pointer would jump to `wrong_target`.
        let n: usize = decoy as *const () as usize;
        assert_eq!(
            std::mem::size_of_val(&move || n),
            std::mem::size_of::<fn()>()
        );

        assert_eq!(call(move || n), n);
        assert!(!last_call_fell_through());

        let wrong: fn() -> usize = wrong_target;
        test_support::install(table(&[(n as u64, wrong as usize as u64)]));
        assert_eq!(call(move || n), n);
        assert!(last_call_fell_through());

        test_support::reset();
    }

    #[test]
    fn panics_propagate_through_call() {
        let _serial = test_support::serial();
        let res = std::panic::catch_unwind(|| call(|| -> u32 { panic!("not ours") }));
        assert!(res.is_err());
    }

    #[test]
    fn split_pointer_tag_masks_the_top_byte() {
        let tagged = 0xb400_7fff_1234_5678;
        let (untagged, tag) = split_pointer_tag(tagged);
        assert_eq!(untagged, 0x0000_7fff_1234_5678);
        assert_eq!(tag, 0xb400_0000_0000_0000);
        assert_eq!(untagged | tag, tagged);
    }

    #[test]
    fn tagged_lookup_strips_and_reapplies_the_tag() {
        let map: AddressMap = [(0x0000_7fff_1234_5678, 0x0000_7fff_9999_0000)]
            .into_iter()
            .collect();
        let tagged = 0xb400_7fff_1234_5678;
        assert_eq!(
            resolve(&map, tagged, true),
            Some(0xb400_7fff_9999_0000),
            "the aarch64 Android path looks up untagged and re-tags the target"
        );
        assert_eq!(
            resolve(&map, tagged, false),
            None,
            "untagged path keys verbatim"
        );
        assert_eq!(
            resolve(&map, 0x0000_7fff_1234_5678, true),
            Some(0x0000_7fff_9999_0000)
        );
    }

    #[test]
    #[cfg_attr(
        not(debug_assertions),
        ignore = "dispatch reads the table only in debug builds; run `cargo test -p frust-hotpatch`"
    )]
    fn seam_hits_count_and_reset_per_patch() {
        let _serial = test_support::serial();
        test_support::reset();

        let key = call_it_key_of(&add);
        let target = patched_call_it_of(&add);
        test_support::install(table(&[(key, target)]));
        assert_eq!(seam_hits(), 0);
        HotFn::current(add).call((1, 2));
        HotFn::current(add).call((1, 2));
        assert_eq!(seam_hits(), 2);
        // A miss is not a hit.
        HotFn::current(mul).call((1, 2));
        assert_eq!(seam_hits(), 2);

        test_support::install(table(&[(key, target)]));
        assert_eq!(seam_hits(), 0, "a patch resets the hit count");
        test_support::reset();
    }

    #[test]
    #[cfg_attr(
        not(debug_assertions),
        ignore = "dispatch reads the table only in debug builds; run `cargo test -p frust-hotpatch`"
    )]
    fn missed_keys_are_recorded_once_with_image_and_link_address() {
        let _serial = test_support::serial();
        test_support::reset();

        let add_key = call_it_key_of(&add);
        let mul_key = call_it_key_of(&mul);
        // Synthetic ranges: `add`'s key lies in image 0 (slide 0x100), `mul`'s in patch image 2.
        let base = |key: u64| (key as usize - 0x10, key as usize + 0x10);
        let (a_lo, a_hi) = base(add_key);
        let (m_lo, m_hi) = base(mul_key);
        crate::anchor::register_for_test(0, a_lo, a_hi, 0x100);
        crate::anchor::register_for_test(2, m_lo, m_hi, 0x40);

        // A table that maps neither, so both miss.
        test_support::install(table(&[(1, 2)]));
        for _ in 0..3 {
            HotFn::current(add).call((1, 2));
            HotFn::current(mul).call((1, 2));
        }
        assert_eq!(
            fall_through_count(),
            6,
            "the count keeps counting every miss"
        );
        let mut expected = vec![
            MissedKey {
                image: 0,
                link_address: add_key - 0x100,
            },
            MissedKey {
                image: 2,
                link_address: mul_key - 0x40,
            },
        ];
        expected.sort();
        assert_eq!(missed_keys(), expected, "each distinct key once");

        test_support::install(table(&[(1, 2)]));
        assert!(missed_keys().is_empty(), "a patch resets the recorded keys");

        // A key in no known image is reported as such, with the raw key.
        crate::anchor::reset_for_test();
        HotFn::current(add).call((1, 2));
        assert_eq!(
            missed_keys(),
            vec![MissedKey {
                image: crate::UNKNOWN_IMAGE,
                link_address: add_key
            }]
        );
        test_support::reset();
    }
}