use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Mutex, PoisonError};
pub const ANCHOR_SYMBOL: &str = "__frust_hotpatch_anchor";
pub const UNKNOWN_IMAGE: u32 = u32::MAX;
static ANCHOR: AtomicUsize = AtomicUsize::new(0);
pub fn set_anchor(address: usize) {
ANCHOR.store(address, Ordering::Release);
}
#[doc(hidden)]
pub fn aslr_reference() -> usize {
ANCHOR.load(Ordering::Acquire)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Image {
pub(crate) index: u32,
pub(crate) start: usize,
pub(crate) end: usize,
pub(crate) slide: usize,
}
static IMAGES: Mutex<Vec<Image>> = Mutex::new(Vec::new());
fn images() -> std::sync::MutexGuard<'static, Vec<Image>> {
IMAGES.lock().unwrap_or_else(PoisonError::into_inner)
}
pub(crate) fn register_base(start: usize, end: usize, slide: usize) {
let mut images = images();
images.retain(|image| image.index != 0);
images.insert(
0,
Image {
index: 0,
start,
end,
slide,
},
);
}
pub(crate) fn register_patch(start: usize, end: usize, slide: usize) -> u32 {
let mut images = images();
let index = images.iter().map(|image| image.index).max().unwrap_or(0) + 1;
images.push(Image {
index,
start,
end,
slide,
});
index
}
pub(crate) fn classify(address: u64) -> (u32, u64) {
let found = images()
.iter()
.copied()
.find(|image| usize::try_from(address).is_ok_and(|a| image.start <= a && a < image.end));
match found {
Some(image) => (image.index, address.wrapping_sub(image.slide as u64)),
None => (UNKNOWN_IMAGE, address),
}
}
pub(crate) fn image_range_containing(address: usize) -> Option<(usize, usize)> {
platform::image_containing(address).map(|(lo, hi, _)| (lo, hi))
}
pub(crate) fn image_slide_containing(address: usize) -> Option<usize> {
platform::image_containing(address).map(|(_, _, slide)| slide)
}
#[cfg(test)]
pub(crate) fn reset_for_test() {
ANCHOR.store(0, Ordering::Release);
images().clear();
}
#[cfg(test)]
pub(crate) fn register_for_test(index: u32, start: usize, end: usize, slide: usize) {
let mut images = images();
images.retain(|image| image.index != index);
images.push(Image {
index,
start,
end,
slide,
});
}
#[cfg(any(target_os = "linux", target_os = "android"))]
mod platform {
use std::ffi::c_void;
struct Probe {
address: usize,
found: Option<(usize, usize, usize)>,
}
unsafe extern "C" fn visit(
info: *mut libc::dl_phdr_info,
_size: libc::size_t,
data: *mut c_void,
) -> libc::c_int {
let (info, probe) = unsafe { (&*info, &mut *data.cast::<Probe>()) };
if info.dlpi_phdr.is_null() {
return 0;
}
let headers =
unsafe { std::slice::from_raw_parts(info.dlpi_phdr, usize::from(info.dlpi_phnum)) };
let base = info.dlpi_addr as usize;
let (mut lo, mut hi) = (usize::MAX, 0usize);
for header in headers.iter().filter(|h| h.p_type == libc::PT_LOAD) {
let start = base.wrapping_add(header.p_vaddr as usize);
lo = lo.min(start);
hi = hi.max(start.wrapping_add(header.p_memsz as usize));
}
if lo <= probe.address && probe.address < hi {
probe.found = Some((lo, hi, base));
return 1;
}
0
}
pub(super) fn image_containing(address: usize) -> Option<(usize, usize, usize)> {
#[cfg(target_pointer_width = "64")]
let address = address & 0x00FF_FFFF_FFFF_FFFF;
let mut probe = Probe {
address,
found: None,
};
unsafe {
libc::dl_iterate_phdr(Some(visit), std::ptr::from_mut(&mut probe).cast::<c_void>());
}
probe.found
}
}
#[cfg(all(target_vendor = "apple", target_pointer_width = "64"))]
mod platform {
unsafe extern "C" {
fn _dyld_image_count() -> u32;
fn _dyld_get_image_header(index: u32) -> *const MachHeader64;
fn _dyld_get_image_vmaddr_slide(index: u32) -> isize;
}
#[repr(C)]
struct MachHeader64 {
magic: u32,
cpu_type: i32,
cpu_subtype: i32,
file_type: u32,
command_count: u32,
commands_size: u32,
flags: u32,
reserved: u32,
}
#[repr(C)]
struct LoadCommand {
cmd: u32,
size: u32,
}
#[repr(C)]
struct Segment64 {
cmd: u32,
size: u32,
name: [u8; 16],
vm_address: u64,
vm_size: u64,
}
const LC_SEGMENT_64: u32 = 0x19;
pub(super) fn image_containing(address: usize) -> Option<(usize, usize, usize)> {
let count = unsafe { _dyld_image_count() };
for index in 0..count {
let (header, slide) = unsafe {
(
_dyld_get_image_header(index),
_dyld_get_image_vmaddr_slide(index) as usize,
)
};
if header.is_null() {
continue;
}
let range = unsafe { segments_extent(header, slide) };
if let Some((lo, hi)) = range
&& lo <= address
&& address < hi
{
return Some((lo, hi, slide));
}
}
None
}
unsafe fn segments_extent(header: *const MachHeader64, slide: usize) -> Option<(usize, usize)> {
let (count, mut cursor) = unsafe { ((*header).command_count, header.add(1).cast::<u8>()) };
let (mut lo, mut hi) = (usize::MAX, 0usize);
for _ in 0..count {
let command = unsafe { &*cursor.cast::<LoadCommand>() };
if command.cmd == LC_SEGMENT_64 {
let segment = unsafe { &*cursor.cast::<Segment64>() };
if !segment.name.starts_with(b"__PAGEZERO") && segment.vm_size != 0 {
let start = (segment.vm_address as usize).wrapping_add(slide);
lo = lo.min(start);
hi = hi.max(start.wrapping_add(segment.vm_size as usize));
}
}
if command.size == 0 {
break;
}
cursor = unsafe { cursor.add(command.size as usize) };
}
(lo < hi).then_some((lo, hi))
}
}
#[cfg(windows)]
mod platform {
use std::ffi::c_void;
unsafe extern "system" {
fn GetModuleHandleExW(flags: u32, address: *const c_void, module: *mut *mut c_void) -> i32;
}
const FROM_ADDRESS: u32 = 0x4;
const UNCHANGED_REFCOUNT: u32 = 0x2;
pub(super) fn image_containing(address: usize) -> Option<(usize, usize, usize)> {
let mut module: *mut c_void = std::ptr::null_mut();
let ok = unsafe {
GetModuleHandleExW(
FROM_ADDRESS | UNCHANGED_REFCOUNT,
address as *const c_void,
&mut module,
)
};
if ok == 0 || module.is_null() {
return None;
}
let base = module as usize;
let size = unsafe {
let e_lfanew = std::ptr::read_unaligned((base + 0x3C) as *const u32) as usize;
std::ptr::read_unaligned((base + e_lfanew + 24 + 56) as *const u32) as usize
};
let preferred = unsafe {
let e_lfanew = std::ptr::read_unaligned((base + 0x3C) as *const u32) as usize;
std::ptr::read_unaligned((base + e_lfanew + 24 + 24) as *const u64) as usize
};
Some((base, base + size, base.wrapping_sub(preferred)))
}
}
#[cfg(not(any(
target_os = "linux",
target_os = "android",
all(target_vendor = "apple", target_pointer_width = "64"),
windows
)))]
mod platform {
pub(super) fn image_containing(_address: usize) -> Option<(usize, usize, usize)> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
fn here() {}
#[test]
fn anchor_reads_zero_until_set_and_is_not_cached_while_unset() {
let _serial = crate::patch::test_support::serial();
reset_for_test();
assert_eq!(aslr_reference(), 0);
assert_eq!(aslr_reference(), 0);
set_anchor(0x1234);
assert_eq!(aslr_reference(), 0x1234);
reset_for_test();
}
#[test]
fn classify_names_the_image_and_subtracts_its_slide() {
let _serial = crate::patch::test_support::serial();
reset_for_test();
register_for_test(0, 0x1000, 0x2000, 0x1000);
register_for_test(1, 0x8000, 0x9000, 0x7000);
assert_eq!(classify(0x1800), (0, 0x800));
assert_eq!(classify(0x8100), (1, 0x1100));
assert_eq!(classify(0x9000), (UNKNOWN_IMAGE, 0x9000));
reset_for_test();
}
#[test]
fn patch_images_are_numbered_in_load_order() {
let _serial = crate::patch::test_support::serial();
reset_for_test();
register_base(0x1000, 0x2000, 0);
assert_eq!(register_patch(0x4000, 0x5000, 0), 1);
assert_eq!(register_patch(0x6000, 0x7000, 0), 2);
assert_eq!(classify(0x6100).0, 2);
reset_for_test();
}
#[test]
#[cfg(any(
target_os = "linux",
target_os = "android",
all(target_vendor = "apple", target_pointer_width = "64"),
windows
))]
fn the_running_executable_has_a_slide_no_larger_than_its_start() {
let address = here as *const () as usize;
let slide = image_slide_containing(address).expect("a slide for this test binary");
let (lo, _) = image_range_containing(address).expect("a range");
assert!(slide <= lo, "slide {slide:#x} above image start {lo:#x}");
}
#[test]
#[cfg(any(
target_os = "linux",
target_os = "android",
all(target_vendor = "apple", target_pointer_width = "64"),
windows
))]
fn the_running_executable_has_a_range_containing_its_own_code() {
let address = here as *const () as usize;
let (lo, hi) = image_range_containing(address).expect("a range for this test binary");
assert!(lo <= address && address < hi);
}
}