Fix FIFA17 SBC dispatch response-class check to capture live vtable
This commit is contained in:
@@ -198,6 +198,14 @@ fn decide(input: DecisionInput) -> Result<Decision, Rejection> {
|
||||
Ok(Decision::Repair)
|
||||
}
|
||||
|
||||
/// The parsed response is the FIFA 17 typed SBC-category response only when the
|
||||
/// vtable captured at deserializer exit (object provably live) equals the pinned
|
||||
/// category-response vtable for the running CardsDLL image. A zero capture means
|
||||
/// the object vtable was unreadable and never qualifies.
|
||||
fn response_class_matches(base: usize, response_vtable: usize) -> bool {
|
||||
response_vtable != 0 && base.checked_add(CATEGORY_RESPONSE_VTABLE_RVA) == Some(response_vtable)
|
||||
}
|
||||
|
||||
unsafe fn guarded_u32(address: usize) -> Option<u32> {
|
||||
crate::sbc_trace::readable_range(address, 4)
|
||||
.then(|| core::ptr::read_volatile(address as *const u32))
|
||||
@@ -268,7 +276,6 @@ unsafe extern "system" fn completion_wrapper(
|
||||
);
|
||||
COMPLETION_GENERATION.store(evidence.deserializer_exits, Ordering::Relaxed);
|
||||
|
||||
let response_vtable = crate::sbc_trace::guarded_usize(evidence.deserializer_this);
|
||||
let captured_controller = SBC_CONTROLLER.load(Ordering::Acquire);
|
||||
let live_category_count = evidence
|
||||
.model
|
||||
@@ -295,8 +302,7 @@ unsafe extern "system" fn completion_wrapper(
|
||||
deserializer_reader: evidence.deserializer_reader,
|
||||
deserializer_result: evidence.deserializer_result,
|
||||
deserializer_thread: evidence.deserializer_thread,
|
||||
response_class_matches: response_vtable
|
||||
== evidence.base.checked_add(CATEGORY_RESPONSE_VTABLE_RVA),
|
||||
response_class_matches: response_class_matches(evidence.base, evidence.response_vtable),
|
||||
model: evidence.model,
|
||||
live_category_count,
|
||||
category_count: evidence.category_count,
|
||||
@@ -348,6 +354,39 @@ unsafe extern "system" fn completion_wrapper(
|
||||
original(controller, status)
|
||||
}
|
||||
};
|
||||
crate::write_log(&format!(
|
||||
"SBC_DISPATCH: decide gen={} status={} present={} copyable={} cards={} factory_e={} factory_x={} factory_r={:#x} factory_t={} deser_e={} deser_x={} deser_this={:#x} reader={:#x} deser_ok={} deser_t={} vt_obs={:#x} vt_exp={:#x} class={} model={:#x} live={} count={} notif_e={} notif_x={} ctrl_match={} ctrl_model={} captured_ctrl={:#x} arg_ctrl={:#x} last_gen={} decision={} rejection={}\n",
|
||||
input.deserializer_exits,
|
||||
input.status.map(i64::from).unwrap_or(-1),
|
||||
input.status_present,
|
||||
input.status_copyable,
|
||||
input.cards_build_matches,
|
||||
input.factory_entries,
|
||||
input.factory_exits,
|
||||
input.factory_result,
|
||||
input.factory_thread,
|
||||
input.deserializer_entries,
|
||||
input.deserializer_exits,
|
||||
input.deserializer_this,
|
||||
input.deserializer_reader,
|
||||
input.deserializer_result,
|
||||
input.deserializer_thread,
|
||||
evidence.response_vtable,
|
||||
evidence.base.checked_add(CATEGORY_RESPONSE_VTABLE_RVA).unwrap_or(0),
|
||||
input.response_class_matches,
|
||||
input.model,
|
||||
input.live_category_count,
|
||||
input.category_count,
|
||||
input.notifier_entries,
|
||||
input.notifier_exits,
|
||||
input.controller_matches,
|
||||
input.controller_model_matches,
|
||||
captured_controller,
|
||||
controller as usize,
|
||||
input.last_repaired_generation,
|
||||
COMPLETION_DECISION.load(Ordering::Relaxed),
|
||||
COMPLETION_REJECTION.load(Ordering::Relaxed),
|
||||
));
|
||||
COMPLETION_EXITS.fetch_add(1, Ordering::Release);
|
||||
result
|
||||
}
|
||||
@@ -760,6 +799,18 @@ mod tests {
|
||||
assert_eq!(decide(next), Ok(Decision::Repair));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_class_requires_exact_pinned_vtable() {
|
||||
let base = 0x1_8000_0000usize;
|
||||
let expected = base + CATEGORY_RESPONSE_VTABLE_RVA;
|
||||
assert!(response_class_matches(base, expected));
|
||||
// An unreadable capture (zero) never qualifies.
|
||||
assert!(!response_class_matches(base, 0));
|
||||
// Any other vtable (e.g. a sub-object or a freed/reused slot) is rejected.
|
||||
assert!(!response_class_matches(base, expected + 8));
|
||||
assert!(!response_class_matches(base, base));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relocated_completion_branch_has_proven_layout() {
|
||||
assert_eq!(COMPLETION_COPY_LEN, 14);
|
||||
|
||||
@@ -80,6 +80,7 @@ static TRACE_BASE: AtomicUsize = AtomicUsize::new(0);
|
||||
static DESERIALIZER_EXIT_M: AtomicUsize = AtomicUsize::new(0);
|
||||
static DESERIALIZER_EXIT_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
static DESERIALIZER_EXIT_B_READY: AtomicUsize = AtomicUsize::new(usize::MAX);
|
||||
static DESERIALIZER_EXIT_RESPONSE_VTABLE: AtomicUsize = AtomicUsize::new(0);
|
||||
static NOTIFIER_TRAMPOLINE: AtomicUsize = AtomicUsize::new(0);
|
||||
static CONTROLLER_REGISTER_TRAMPOLINE: AtomicUsize = AtomicUsize::new(0);
|
||||
static NOTIFIER_ENTRIES: AtomicU64 = AtomicU64::new(0);
|
||||
@@ -684,9 +685,11 @@ unsafe extern "system" fn deserializer_wrapper(this: *mut c_void, reader: *mut c
|
||||
.and_then(|slot| guarded_u8(slot))
|
||||
.map(usize::from)
|
||||
.unwrap_or(usize::MAX);
|
||||
let response_vtable = guarded_usize(this as usize).unwrap_or(0);
|
||||
DESERIALIZER_EXIT_M.store(m, Ordering::Relaxed);
|
||||
DESERIALIZER_EXIT_COUNT.store(count, Ordering::Relaxed);
|
||||
DESERIALIZER_EXIT_B_READY.store(ready, Ordering::Relaxed);
|
||||
DESERIALIZER_EXIT_RESPONSE_VTABLE.store(response_vtable, Ordering::Relaxed);
|
||||
DESERIALIZER_EXITS.fetch_add(1, Ordering::Release);
|
||||
result
|
||||
}
|
||||
@@ -703,6 +706,7 @@ pub(crate) struct DispatchEvidence {
|
||||
pub(crate) deserializer_reader: usize,
|
||||
pub(crate) deserializer_result: bool,
|
||||
pub(crate) deserializer_thread: usize,
|
||||
pub(crate) response_vtable: usize,
|
||||
pub(crate) model: usize,
|
||||
pub(crate) category_count: usize,
|
||||
pub(crate) notifier_entries: u64,
|
||||
@@ -722,6 +726,7 @@ pub(crate) fn dispatch_evidence() -> DispatchEvidence {
|
||||
deserializer_reader: DESERIALIZER_LAST_READER.load(Ordering::Relaxed),
|
||||
deserializer_result: DESERIALIZER_LAST_RESULT.load(Ordering::Acquire),
|
||||
deserializer_thread: DESERIALIZER_LAST_THREAD.load(Ordering::Relaxed),
|
||||
response_vtable: DESERIALIZER_EXIT_RESPONSE_VTABLE.load(Ordering::Relaxed),
|
||||
model: DESERIALIZER_EXIT_M.load(Ordering::Relaxed),
|
||||
category_count: DESERIALIZER_EXIT_COUNT.load(Ordering::Relaxed),
|
||||
notifier_entries: NOTIFIER_ENTRIES.load(Ordering::Acquire),
|
||||
|
||||
Reference in New Issue
Block a user