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};
pub fn call<O>(f: impl FnMut() -> O) -> O {
HotFn::current(f).call(())
}
#[non_exhaustive]
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
pub struct HotFnPtr(pub u64);
impl HotFnPtr {
pub unsafe fn new(index: u64) -> Self {
Self(index)
}
}
static FALL_THROUGHS: AtomicU64 = AtomicU64::new(0);
static SEAM_HITS: AtomicU64 = AtomicU64::new(0);
static MISSED_KEYS: Mutex<BTreeSet<MissedKey>> = Mutex::new(BTreeSet::new());
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MissedKey {
pub image: u32,
pub link_address: u64,
}
thread_local! {
#[allow(clippy::missing_const_for_thread_local)]
static LAST_FELL_THROUGH: Cell<bool> = const { Cell::new(false) };
}
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,
});
}
}
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();
}
pub fn seam_hits() -> u64 {
SEAM_HITS.load(Ordering::Relaxed)
}
pub fn missed_keys() -> Vec<MissedKey> {
MISSED_KEYS
.lock()
.unwrap_or_else(PoisonError::into_inner)
.iter()
.copied()
.collect()
}
pub fn last_call_fell_through() -> bool {
LAST_FELL_THROUGH.with(Cell::get)
}
pub fn fall_through_count() -> u64 {
FALL_THROUGHS.load(Ordering::Relaxed)
}
const STRIP_POINTER_TAG: bool = cfg!(all(target_pointer_width = "64", target_os = "android"));
fn split_pointer_tag(addr: u64) -> (u64, u64) {
(addr & 0x00FF_FFFF_FFFF_FFFF, addr & 0xFF00_0000_0000_0000)
}
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)
}
fn call_it_address<A, M, F: HotFunction<A, M>>() -> u64 {
<F as HotFunction<A, M>>::call_it as *const () as usize as u64
}
struct FnPointerOps<A, R, F> {
address: fn(&F) -> u64,
call_at: unsafe fn(u64, A) -> R,
}
pub struct HotFn<A, M, F>
where
F: HotFunction<A, M>,
{
inner: F,
by_value: Option<FnPointerOps<A, F::Return, F>>,
_marker: PhantomData<(A, M)>,
}
impl<A, M, F: HotFunction<A, M>> HotFn<A, M, F> {
pub const fn current(f: F) -> HotFn<A, M, F> {
HotFn {
inner: f,
by_value: None,
_marker: PhantomData,
}
}
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,
}
}
pub fn call(&mut self, args: A) -> F::Return {
match self.try_call(args) {
Ok(ret) => ret,
Err(never) => match never {},
}
}
pub fn ptr_address(&self) -> HotFnPtr {
let key = self.key();
if !cfg!(debug_assertions) {
return HotFnPtr(key);
}
let resolved = unsafe { get_jump_table() }
.and_then(|table| resolve(&table.map, key, STRIP_POINTER_TAG));
HotFnPtr(resolved.unwrap_or(key))
}
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();
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 {
Some(target) => Ok(unsafe { self.call_at(target, args) }),
None => Ok(self.inner.call_it(args)),
}
}
pub unsafe fn try_call_with_ptr(
&mut self,
ptr: HotFnPtr,
args: A,
) -> Result<F::Return, Infallible> {
Ok(unsafe { self.call_at(ptr.0, args) })
}
fn key(&self) -> u64 {
match &self.by_value {
Some(ops) => (ops.address)(&self.inner),
None => call_it_address::<A, M, F>(),
}
}
unsafe fn call_at(&mut self, address: u64, args: A) -> F::Return {
if let Some(ops) = &self.by_value {
return unsafe { (ops.call_at)(address, args) };
}
let call_it = unsafe {
transmute::<*const (), fn(&mut F, A) -> F::Return>(address as usize as *const ())
};
call_it(&mut self.inner, args)
}
}
pub trait HotFunction<Args, Marker> {
type Return;
fn call_it(&mut self, args: Args) -> Self::Return;
}
mod sealed {
pub trait Sealed {}
}
pub trait FnPointer<Args, Marker>: HotFunction<Args, Marker> + Copy + sealed::Sealed {
fn address(&self) -> u64;
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 {
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
}
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);
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();
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);
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));
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());
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));
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;
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();
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);
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);
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);
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");
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();
}
}