958ff24546
- sbc_hook.rs: trace SBC submission/response flow with request IDs - sbc_request_trace.rs: capture request/response bodies for analysis - sbc_trace.rs: runtime trace buffer with structured logging Work in progress - needs validation against live FIFA 17 client
479 lines
18 KiB
Rust
479 lines
18 KiB
Rust
//! Optional category-request callback tracing through its class-unique vtable.
|
|
//!
|
|
//! Unlike the entry trampolines, these probes atomically replace two aligned
|
|
//! pointer slots. The branchy callback dispatcher at 0x180154830 is never patched.
|
|
|
|
use core::ffi::c_void;
|
|
use core::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
|
|
use windows_sys::Win32::System::LibraryLoader::{
|
|
GetModuleHandleA, GetModuleHandleExA, GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
|
|
GET_MODULE_HANDLE_EX_FLAG_PIN,
|
|
};
|
|
use windows_sys::Win32::System::Memory::{
|
|
VirtualProtect, VirtualQuery, MEMORY_BASIC_INFORMATION, MEM_COMMIT, PAGE_GUARD, PAGE_NOACCESS,
|
|
PAGE_READWRITE,
|
|
};
|
|
use windows_sys::Win32::System::Threading::GetCurrentThreadId;
|
|
|
|
const REQUEST_VTABLE_RVA: usize = 0x22e5c0;
|
|
const SLOT_88: usize = 0x88;
|
|
const SLOT_90: usize = 0x90;
|
|
const ORIGINAL_88_RVA: usize = 0x1631e0;
|
|
const ORIGINAL_90_RVA: usize = 0x154830;
|
|
const ORIGINAL_88_SIGNATURE: [u8; 16] = [
|
|
0x48, 0x89, 0x5c, 0x24, 0x08, 0x48, 0x89, 0x74, 0x24, 0x10, 0x57, 0x48, 0x83, 0xec, 0x20, 0x48,
|
|
];
|
|
const ORIGINAL_90_SIGNATURE: [u8; 16] = [
|
|
0x4c, 0x8b, 0x81, 0x90, 0x00, 0x00, 0x00, 0x4d, 0x85, 0xc0, 0x74, 0x0a, 0x48, 0x81, 0xc1, 0x90,
|
|
];
|
|
|
|
type Callback88 = unsafe extern "system" fn(*mut c_void, *mut c_void);
|
|
type Callback90 = unsafe extern "system" fn(*mut c_void, *mut c_void);
|
|
|
|
static ENABLED: AtomicBool = AtomicBool::new(false);
|
|
static INSTALLED: AtomicBool = AtomicBool::new(false);
|
|
static ORIGINAL_88: AtomicUsize = AtomicUsize::new(0);
|
|
static ORIGINAL_90: AtomicUsize = AtomicUsize::new(0);
|
|
static ENTER_88: AtomicU64 = AtomicU64::new(0);
|
|
static EXIT_88: AtomicU64 = AtomicU64::new(0);
|
|
static ENTER_90: AtomicU64 = AtomicU64::new(0);
|
|
static EXIT_90: AtomicU64 = AtomicU64::new(0);
|
|
static LAST_REQUEST_88: AtomicUsize = AtomicUsize::new(0);
|
|
static LAST_ARGUMENT_88: AtomicUsize = AtomicUsize::new(0);
|
|
static LAST_THREAD_88: AtomicUsize = AtomicUsize::new(0);
|
|
static LAST_REQUEST_90: AtomicUsize = AtomicUsize::new(0);
|
|
static LAST_ARGUMENT_90: AtomicUsize = AtomicUsize::new(0);
|
|
static LAST_THREAD_90: AtomicUsize = AtomicUsize::new(0);
|
|
static CALLBACK_90: AtomicUsize = AtomicUsize::new(0);
|
|
static CALLBACK_98: AtomicUsize = AtomicUsize::new(0);
|
|
static CALLBACK_A0: AtomicUsize = AtomicUsize::new(0);
|
|
static CALLBACK_A8: AtomicUsize = AtomicUsize::new(0);
|
|
static SELECTED_90: AtomicUsize = AtomicUsize::new(0);
|
|
static OWNER_88: AtomicUsize = AtomicUsize::new(0);
|
|
static OWNER_VTABLE_88: AtomicUsize = AtomicUsize::new(0);
|
|
static CONSUMER_88: AtomicUsize = AtomicUsize::new(0);
|
|
static RESPONSE_VTABLE_88: AtomicUsize = AtomicUsize::new(0);
|
|
static OWNER_SLOT_BEFORE_88: AtomicUsize = AtomicUsize::new(0);
|
|
static OWNER_SLOT_AFTER_88: AtomicUsize = AtomicUsize::new(0);
|
|
static OWNER_INNER_88: AtomicUsize = AtomicUsize::new(0);
|
|
static OWNER_STATE_BEFORE_88: AtomicUsize = AtomicUsize::new(usize::MAX);
|
|
static OWNER_STATE_AFTER_88: AtomicUsize = AtomicUsize::new(usize::MAX);
|
|
static OWNER_FLAGS_88: AtomicUsize = AtomicUsize::new(usize::MAX);
|
|
static OWNER_MANAGER_88: AtomicUsize = AtomicUsize::new(0);
|
|
static OWNER_MANAGER_STATE_88: AtomicUsize = AtomicUsize::new(usize::MAX);
|
|
|
|
fn enabled(value: Option<&str>) -> bool {
|
|
matches!(value, Some("1"))
|
|
}
|
|
|
|
fn checked_va(base: usize, rva: usize) -> Option<usize> {
|
|
base.checked_add(rva)
|
|
}
|
|
|
|
fn image_range_covered(size: usize, rva: usize, length: usize) -> bool {
|
|
rva.checked_add(length)
|
|
.map(|end| end <= size)
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
unsafe fn readable_range(address: usize, length: usize) -> bool {
|
|
let Some(end) = address.checked_add(length) else {
|
|
return false;
|
|
};
|
|
let mut mbi: MEMORY_BASIC_INFORMATION = core::mem::zeroed();
|
|
VirtualQuery(
|
|
address as _,
|
|
&mut mbi,
|
|
core::mem::size_of::<MEMORY_BASIC_INFORMATION>(),
|
|
) != 0
|
|
&& mbi.State == MEM_COMMIT
|
|
&& mbi.Protect & (PAGE_GUARD | PAGE_NOACCESS) == 0
|
|
&& end <= mbi.BaseAddress as usize + mbi.RegionSize
|
|
}
|
|
|
|
unsafe fn range_in_image_allocation(base: usize, address: usize, length: usize) -> bool {
|
|
let Some(end) = address.checked_add(length) else {
|
|
return false;
|
|
};
|
|
let mut mbi: MEMORY_BASIC_INFORMATION = core::mem::zeroed();
|
|
VirtualQuery(
|
|
address as _,
|
|
&mut mbi,
|
|
core::mem::size_of::<MEMORY_BASIC_INFORMATION>(),
|
|
) != 0
|
|
&& mbi.AllocationBase as usize == base
|
|
&& mbi.State == MEM_COMMIT
|
|
&& mbi.Protect & (PAGE_GUARD | PAGE_NOACCESS) == 0
|
|
&& end <= mbi.BaseAddress as usize + mbi.RegionSize
|
|
}
|
|
|
|
unsafe fn image_size(base: usize) -> Option<usize> {
|
|
if !readable_range(base, 0x1000) || *(base as *const u16) != 0x5a4d {
|
|
return None;
|
|
}
|
|
let pe_offset = *((base + 0x3c) as *const u32) as usize;
|
|
if pe_offset > 0xf00 {
|
|
return None;
|
|
}
|
|
let pe = base.checked_add(pe_offset)?;
|
|
if *(pe as *const u32) != 0x0000_4550 {
|
|
return None;
|
|
}
|
|
let size_field = pe.checked_add(24 + 0x38)?;
|
|
Some(*(size_field as *const u32) as usize)
|
|
}
|
|
|
|
unsafe fn signature_matches(address: usize, expected: &[u8]) -> bool {
|
|
readable_range(address, expected.len())
|
|
&& core::slice::from_raw_parts(address as *const u8, expected.len()) == expected
|
|
}
|
|
|
|
unsafe fn guarded_ptr(address: usize) -> usize {
|
|
if address & 7 == 0 && readable_range(address, 8) {
|
|
core::ptr::read_volatile(address as *const usize)
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
|
|
unsafe fn guarded_u32(address: usize) -> Option<u32> {
|
|
if readable_range(address, 4) {
|
|
Some(core::ptr::read_volatile(address as *const u32))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
unsafe fn guarded_u8(address: usize) -> Option<u8> {
|
|
if readable_range(address, 1) {
|
|
Some(core::ptr::read_volatile(address as *const u8))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
unsafe fn field_ptr(object: usize, offset: usize) -> usize {
|
|
object
|
|
.checked_add(offset)
|
|
.map(|address| guarded_ptr(address))
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
unsafe extern "system" fn wrapper_88(request: *mut c_void, argument: *mut c_void) {
|
|
ENTER_88.fetch_add(1, Ordering::Relaxed);
|
|
LAST_REQUEST_88.store(request as usize, Ordering::Relaxed);
|
|
LAST_ARGUMENT_88.store(argument as usize, Ordering::Relaxed);
|
|
LAST_THREAD_88.store(GetCurrentThreadId() as usize, Ordering::Relaxed);
|
|
let request_address = request as usize;
|
|
let argument_address = argument as usize;
|
|
let owner = field_ptr(request_address, 8);
|
|
let owner_vtable = guarded_ptr(owner);
|
|
let consumer = field_ptr(owner_vtable, 0x18);
|
|
let owner_slot_before = guarded_ptr(argument_address);
|
|
let response_vtable = guarded_ptr(owner_slot_before);
|
|
let owner_inner = field_ptr(owner, 8);
|
|
let owner_state_before = owner_inner
|
|
.checked_add(8)
|
|
.and_then(|p| guarded_u32(p))
|
|
.map(|v| v as usize)
|
|
.unwrap_or(usize::MAX);
|
|
let owner_flags = owner_inner
|
|
.checked_add(0x0c)
|
|
.and_then(|p| guarded_u8(p))
|
|
.map(|v| v as usize)
|
|
.unwrap_or(usize::MAX);
|
|
let owner_manager = field_ptr(owner_inner, 0x14d0);
|
|
let owner_manager_state = owner_manager
|
|
.checked_add(0x1dc0)
|
|
.and_then(|p| guarded_u32(p))
|
|
.map(|v| v as usize)
|
|
.unwrap_or(usize::MAX);
|
|
OWNER_88.store(owner, Ordering::Relaxed);
|
|
OWNER_VTABLE_88.store(owner_vtable, Ordering::Relaxed);
|
|
CONSUMER_88.store(consumer, Ordering::Relaxed);
|
|
RESPONSE_VTABLE_88.store(response_vtable, Ordering::Relaxed);
|
|
OWNER_SLOT_BEFORE_88.store(owner_slot_before, Ordering::Relaxed);
|
|
OWNER_INNER_88.store(owner_inner, Ordering::Relaxed);
|
|
OWNER_STATE_BEFORE_88.store(owner_state_before, Ordering::Relaxed);
|
|
OWNER_FLAGS_88.store(owner_flags, Ordering::Relaxed);
|
|
OWNER_MANAGER_88.store(owner_manager, Ordering::Relaxed);
|
|
OWNER_MANAGER_STATE_88.store(owner_manager_state, Ordering::Relaxed);
|
|
let original: Callback88 = core::mem::transmute(ORIGINAL_88.load(Ordering::Acquire));
|
|
original(request, argument);
|
|
OWNER_SLOT_AFTER_88.store(guarded_ptr(argument_address), Ordering::Relaxed);
|
|
OWNER_STATE_AFTER_88.store(
|
|
owner_inner
|
|
.checked_add(8)
|
|
.and_then(|p| guarded_u32(p))
|
|
.map(|v| v as usize)
|
|
.unwrap_or(usize::MAX),
|
|
Ordering::Relaxed,
|
|
);
|
|
EXIT_88.fetch_add(1, Ordering::Release);
|
|
}
|
|
|
|
unsafe extern "system" fn wrapper_90(request: *mut c_void, argument: *mut c_void) {
|
|
ENTER_90.fetch_add(1, Ordering::Relaxed);
|
|
LAST_REQUEST_90.store(request as usize, Ordering::Relaxed);
|
|
LAST_ARGUMENT_90.store(argument as usize, Ordering::Relaxed);
|
|
LAST_THREAD_90.store(GetCurrentThreadId() as usize, Ordering::Relaxed);
|
|
let request_address = request as usize;
|
|
let callback_90 = field_ptr(request_address, 0x90);
|
|
let callback_98 = field_ptr(request_address, 0x98);
|
|
let callback_a0 = field_ptr(request_address, 0xa0);
|
|
let callback_a8 = field_ptr(request_address, 0xa8);
|
|
CALLBACK_90.store(callback_90, Ordering::Relaxed);
|
|
CALLBACK_98.store(callback_98, Ordering::Relaxed);
|
|
CALLBACK_A0.store(callback_a0, Ordering::Relaxed);
|
|
CALLBACK_A8.store(callback_a8, Ordering::Relaxed);
|
|
SELECTED_90.store(
|
|
if callback_90 != 0 {
|
|
callback_90
|
|
} else {
|
|
callback_a0
|
|
},
|
|
Ordering::Relaxed,
|
|
);
|
|
let original: Callback90 = core::mem::transmute(ORIGINAL_90.load(Ordering::Acquire));
|
|
original(request, argument);
|
|
EXIT_90.fetch_add(1, Ordering::Release);
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
enum SwapOutcome {
|
|
Installed,
|
|
CleanFailure,
|
|
DegradedFirstSlotActive,
|
|
DegradedProtection,
|
|
}
|
|
|
|
unsafe fn install_slots(base: usize) -> SwapOutcome {
|
|
let Some(vtable) = checked_va(base, REQUEST_VTABLE_RVA) else {
|
|
return SwapOutcome::CleanFailure;
|
|
};
|
|
let Some(original_88) = checked_va(base, ORIGINAL_88_RVA) else {
|
|
return SwapOutcome::CleanFailure;
|
|
};
|
|
let Some(original_90) = checked_va(base, ORIGINAL_90_RVA) else {
|
|
return SwapOutcome::CleanFailure;
|
|
};
|
|
let Some(slot_88) = checked_va(vtable, SLOT_88) else {
|
|
return SwapOutcome::CleanFailure;
|
|
};
|
|
let Some(slot_90) = checked_va(vtable, SLOT_90) else {
|
|
return SwapOutcome::CleanFailure;
|
|
};
|
|
let Some(size) = image_size(base) else {
|
|
return SwapOutcome::CleanFailure;
|
|
};
|
|
if !image_range_covered(size, REQUEST_VTABLE_RVA, SLOT_90 + 8)
|
|
|| !image_range_covered(size, ORIGINAL_88_RVA, ORIGINAL_88_SIGNATURE.len())
|
|
|| !image_range_covered(size, ORIGINAL_90_RVA, ORIGINAL_90_SIGNATURE.len())
|
|
|| slot_88 & 7 != 0
|
|
|| slot_90 & 7 != 0
|
|
|| !crate::sbc_trace::validate_cards_build(base)
|
|
|| !range_in_image_allocation(base, vtable, SLOT_90 + 8)
|
|
|| !range_in_image_allocation(base, original_88, ORIGINAL_88_SIGNATURE.len())
|
|
|| !range_in_image_allocation(base, original_90, ORIGINAL_90_SIGNATURE.len())
|
|
|| !signature_matches(original_88, &ORIGINAL_88_SIGNATURE)
|
|
|| !signature_matches(original_90, &ORIGINAL_90_SIGNATURE)
|
|
|| (slot_88 as *const AtomicUsize)
|
|
.as_ref()
|
|
.unwrap()
|
|
.load(Ordering::Acquire)
|
|
!= original_88
|
|
|| (slot_90 as *const AtomicUsize)
|
|
.as_ref()
|
|
.unwrap()
|
|
.load(Ordering::Acquire)
|
|
!= original_90
|
|
{
|
|
return SwapOutcome::CleanFailure;
|
|
}
|
|
|
|
let mut pinned = core::ptr::null_mut();
|
|
if GetModuleHandleExA(
|
|
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN,
|
|
vtable as *const u8,
|
|
&mut pinned,
|
|
) == 0
|
|
|| pinned as usize != base
|
|
{
|
|
return SwapOutcome::CleanFailure;
|
|
}
|
|
ORIGINAL_88.store(original_88, Ordering::Release);
|
|
ORIGINAL_90.store(original_90, Ordering::Release);
|
|
|
|
// Both slots share the same vtable page. Keep it writable only across the two
|
|
// compare/exchanges and possible rollback.
|
|
let mut old = 0u32;
|
|
if VirtualProtect(slot_88 as _, 16, PAGE_READWRITE, &mut old) == 0 {
|
|
return SwapOutcome::CleanFailure;
|
|
}
|
|
let atom_88 = &*(slot_88 as *const AtomicUsize);
|
|
let atom_90 = &*(slot_90 as *const AtomicUsize);
|
|
let first = atom_88.compare_exchange(
|
|
original_88,
|
|
wrapper_88 as *const () as usize,
|
|
Ordering::AcqRel,
|
|
Ordering::Acquire,
|
|
);
|
|
let outcome = if first.is_err() {
|
|
SwapOutcome::CleanFailure
|
|
} else if atom_90
|
|
.compare_exchange(
|
|
original_90,
|
|
wrapper_90 as *const () as usize,
|
|
Ordering::AcqRel,
|
|
Ordering::Acquire,
|
|
)
|
|
.is_ok()
|
|
{
|
|
SwapOutcome::Installed
|
|
} else if atom_88
|
|
.compare_exchange(
|
|
wrapper_88 as *const () as usize,
|
|
original_88,
|
|
Ordering::AcqRel,
|
|
Ordering::Acquire,
|
|
)
|
|
.is_ok()
|
|
{
|
|
SwapOutcome::CleanFailure
|
|
} else {
|
|
SwapOutcome::DegradedFirstSlotActive
|
|
};
|
|
let mut ignored = 0u32;
|
|
if VirtualProtect(slot_88 as _, 16, old, &mut ignored) == 0 {
|
|
return if outcome == SwapOutcome::DegradedFirstSlotActive {
|
|
outcome
|
|
} else {
|
|
SwapOutcome::DegradedProtection
|
|
};
|
|
}
|
|
outcome
|
|
}
|
|
|
|
unsafe fn worker() {
|
|
for _ in 0..700u32 {
|
|
if crate::sbc_trace::code_patch_installers_ready() {
|
|
break;
|
|
}
|
|
std::thread::sleep(std::time::Duration::from_millis(500));
|
|
}
|
|
if !crate::sbc_trace::code_patch_installers_ready() {
|
|
crate::write_log("SBC_REQUEST_TRACE: code-patch readiness timeout; inactive\n");
|
|
return;
|
|
}
|
|
let mut base = 0usize;
|
|
for _ in 0..600u32 {
|
|
base = GetModuleHandleA(b"CardsDLL_Win64_retail.dll\0".as_ptr()) as usize;
|
|
if base != 0 {
|
|
break;
|
|
}
|
|
std::thread::sleep(std::time::Duration::from_millis(500));
|
|
}
|
|
match if base == 0 { SwapOutcome::CleanFailure } else { install_slots(base) } {
|
|
SwapOutcome::Installed => {
|
|
INSTALLED.store(true, Ordering::Release);
|
|
crate::write_log("SBC_REQUEST_TRACE: category request vtable slots +0x88/+0x90 installed\n");
|
|
let mut seen_88 = 0u64;
|
|
let mut seen_90 = 0u64;
|
|
let mut reports = 0u8;
|
|
while reports < 32 {
|
|
std::thread::sleep(std::time::Duration::from_millis(250));
|
|
let count_88 = ENTER_88.load(Ordering::Acquire);
|
|
let count_90 = ENTER_90.load(Ordering::Acquire);
|
|
if count_88 != seen_88 || count_90 != seen_90 {
|
|
crate::write_log(&format!(
|
|
"SBC_REQUEST_TRACE: +88 entry={} exit={} req={:#x} arg={:#x} tid={} owner={:#x} ovt={:#x} consumer={:#x} rvt={:#x} slot={:#x}->{:#x} inner={:#x} state={}->{} flags={:#x} manager={:#x} manager_state={}; +90 entry={} exit={} req={:#x} arg={:#x} tid={} cb90={:#x} cb98={:#x} cba0={:#x} cba8={:#x} selected={:#x}\n",
|
|
count_88,
|
|
EXIT_88.load(Ordering::Acquire),
|
|
LAST_REQUEST_88.load(Ordering::Relaxed),
|
|
LAST_ARGUMENT_88.load(Ordering::Relaxed),
|
|
LAST_THREAD_88.load(Ordering::Relaxed),
|
|
OWNER_88.load(Ordering::Relaxed),
|
|
OWNER_VTABLE_88.load(Ordering::Relaxed),
|
|
CONSUMER_88.load(Ordering::Relaxed),
|
|
RESPONSE_VTABLE_88.load(Ordering::Relaxed),
|
|
OWNER_SLOT_BEFORE_88.load(Ordering::Relaxed),
|
|
OWNER_SLOT_AFTER_88.load(Ordering::Relaxed),
|
|
OWNER_INNER_88.load(Ordering::Relaxed),
|
|
OWNER_STATE_BEFORE_88.load(Ordering::Relaxed),
|
|
OWNER_STATE_AFTER_88.load(Ordering::Relaxed),
|
|
OWNER_FLAGS_88.load(Ordering::Relaxed),
|
|
OWNER_MANAGER_88.load(Ordering::Relaxed),
|
|
OWNER_MANAGER_STATE_88.load(Ordering::Relaxed),
|
|
count_90,
|
|
EXIT_90.load(Ordering::Acquire),
|
|
LAST_REQUEST_90.load(Ordering::Relaxed),
|
|
LAST_ARGUMENT_90.load(Ordering::Relaxed),
|
|
LAST_THREAD_90.load(Ordering::Relaxed),
|
|
CALLBACK_90.load(Ordering::Relaxed),
|
|
CALLBACK_98.load(Ordering::Relaxed),
|
|
CALLBACK_A0.load(Ordering::Relaxed),
|
|
CALLBACK_A8.load(Ordering::Relaxed),
|
|
SELECTED_90.load(Ordering::Relaxed),
|
|
));
|
|
seen_88 = count_88;
|
|
seen_90 = count_90;
|
|
reports += 1;
|
|
}
|
|
}
|
|
crate::write_log("SBC_REQUEST_TRACE: report cap reached; vtable probes remain passive\n");
|
|
}
|
|
SwapOutcome::CleanFailure => crate::write_log("SBC_REQUEST_TRACE: clean install failure; inactive\n"),
|
|
SwapOutcome::DegradedFirstSlotActive => crate::write_log(
|
|
"SBC_REQUEST_TRACE: DEGRADED slot +0x88 may remain active; terminate game now\n",
|
|
),
|
|
SwapOutcome::DegradedProtection => crate::write_log(
|
|
"SBC_REQUEST_TRACE: DEGRADED vtable page protection restore failed; terminate game now\n",
|
|
),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn install() {
|
|
let armed = enabled(std::env::var("OPENFUT_SBC_REQUEST_TRACE").ok().as_deref());
|
|
ENABLED.store(armed, Ordering::Release);
|
|
if !armed {
|
|
crate::write_log("SBC_REQUEST_TRACE: disabled\n");
|
|
return;
|
|
}
|
|
crate::write_log("SBC_REQUEST_TRACE: requested; deferred install starting\n");
|
|
std::thread::spawn(|| unsafe { worker() });
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn gate_is_exact() {
|
|
assert!(!enabled(None));
|
|
assert!(!enabled(Some("true")));
|
|
assert!(enabled(Some("1")));
|
|
}
|
|
|
|
#[test]
|
|
fn slots_are_aligned_and_class_local() {
|
|
assert_eq!((REQUEST_VTABLE_RVA + SLOT_88) & 7, 0);
|
|
assert_eq!((REQUEST_VTABLE_RVA + SLOT_90) & 7, 0);
|
|
assert_eq!(SLOT_90 - SLOT_88, 8);
|
|
}
|
|
|
|
#[test]
|
|
fn image_coverage_is_checked_and_overflow_safe() {
|
|
assert!(image_range_covered(
|
|
0x230000,
|
|
REQUEST_VTABLE_RVA,
|
|
SLOT_90 + 8
|
|
));
|
|
assert!(!image_range_covered(
|
|
REQUEST_VTABLE_RVA + SLOT_90,
|
|
REQUEST_VTABLE_RVA,
|
|
SLOT_90 + 8
|
|
));
|
|
assert!(!image_range_covered(usize::MAX, usize::MAX, 8));
|
|
}
|
|
}
|