1use std::panic::{AssertUnwindSafe, catch_unwind};
8#[cfg(any(unix, windows))]
9use std::path::Path;
10use std::sync::atomic::{AtomicPtr, Ordering};
11use std::sync::{Arc, Mutex, PoisonError};
12
13use crate::JumpTable;
14#[cfg(any(unix, windows))]
15use crate::anchor::{
16 ANCHOR_SYMBOL, aslr_reference, image_range_containing, image_slide_containing, register_base,
17 register_patch,
18};
19use crate::hot_fn::reset_fall_throughs;
20
21static APP_JUMP_TABLE: AtomicPtr<JumpTable> = AtomicPtr::new(std::ptr::null_mut());
25
26type Handler = Arc<dyn Fn() + Send + Sync + 'static>;
27static HOTRELOAD_HANDLERS: Mutex<Vec<Handler>> = Mutex::new(Vec::new());
28
29#[cfg(any(unix, windows))]
33static APPLY_LOCK: Mutex<()> = Mutex::new(());
34
35#[derive(Debug, PartialEq, thiserror::Error)]
37pub enum PatchError {
38 #[error("Failed to load library: {0}")]
41 Dlopen(String),
42
43 #[error("Failed to load library on Android: {0}")]
45 AndroidMemfd(String),
46
47 #[error("no ASLR anchor is set; the jump table cannot be rebased")]
51 AnchorUnresolved,
52
53 #[error("hot patching is available only in builds with debug assertions")]
55 ReleaseBuild,
56
57 #[error("cannot determine the address range of a loaded image")]
60 ImageRangeUnresolved,
61
62 #[error("{} unreported layout mismatch(es); patch refused", .0.len())]
66 LayoutMismatchPending(Vec<LayoutMismatch>),
67
68 #[error("{image} anchor mismatch: table implies offset {got:#x}, image slide is {expected:#x}")]
76 AnchorMismatch {
77 image: &'static str,
79 expected: u64,
81 got: u64,
83 },
84}
85
86pub fn register_handler(handler: Arc<dyn Fn() + Send + Sync + 'static>) {
92 HOTRELOAD_HANDLERS
93 .lock()
94 .unwrap_or_else(PoisonError::into_inner)
95 .push(handler);
96}
97
98pub unsafe fn get_jump_table() -> Option<&'static JumpTable> {
105 let ptr = APP_JUMP_TABLE.load(Ordering::Acquire);
106 unsafe { ptr.as_ref() }
111}
112
113fn commit_patch(table: JumpTable) {
115 APP_JUMP_TABLE.store(Box::into_raw(Box::new(table)), Ordering::Release);
117 reset_fall_throughs();
118 run_handlers();
119}
120
121fn run_handlers() -> usize {
123 let handlers = HOTRELOAD_HANDLERS
125 .lock()
126 .unwrap_or_else(PoisonError::into_inner)
127 .clone();
128 let mut panicked = 0;
129 for handler in &handlers {
130 if catch_unwind(AssertUnwindSafe(|| handler())).is_err() {
131 panicked += 1;
132 eprintln!(
133 "frust-hotpatch: a patch handler panicked; the patch stays installed and the \
134 remaining handlers still run"
135 );
136 }
137 }
138 panicked
139}
140
141#[cfg(any(unix, windows))]
151pub unsafe fn load_patch_library(path: &Path) -> Result<libloading::Library, PatchError> {
152 #[cfg(target_os = "android")]
153 {
154 unsafe { crate::android::memfd_dlopen(path) }
156 }
157
158 #[cfg(not(target_os = "android"))]
159 {
160 unsafe { libloading::Library::new(path) }.map_err(|err| PatchError::Dlopen(err.to_string()))
162 }
163}
164
165#[cfg(any(unix, windows))]
214pub unsafe fn apply_patch(table: JumpTable) -> Result<(), PatchError> {
215 if !cfg!(debug_assertions) {
216 return Err(PatchError::ReleaseBuild);
217 }
218 unsafe { apply_patch_with_anchor(table, aslr_reference) }.map(|_| ())
220}
221
222#[cfg(any(unix, windows))]
224#[derive(Debug, PartialEq)]
225pub struct ApplyReport {
226 pub applied: bool,
228 pub layout_mismatches: Vec<LayoutMismatch>,
232 pub error: Option<PatchError>,
234 pub load: std::time::Duration,
238 pub install: std::time::Duration,
241}
242
243#[cfg(any(unix, windows))]
257pub fn apply_from_devtools(bytes_path: &Path, mut table: JumpTable) -> ApplyReport {
258 let refused = |layout_mismatches: Vec<LayoutMismatch>, error: Option<PatchError>| ApplyReport {
259 applied: false,
260 layout_mismatches,
261 error,
262 load: std::time::Duration::ZERO,
263 install: std::time::Duration::ZERO,
264 };
265 if !cfg!(debug_assertions) {
266 return refused(Vec::new(), Some(PatchError::ReleaseBuild));
267 }
268 table.lib = bytes_path.to_path_buf();
269 match unsafe { apply_patch_with_anchor(table, aslr_reference) } {
275 Ok(timings) => ApplyReport {
276 applied: true,
277 layout_mismatches: Vec::new(),
278 error: None,
279 load: timings.load,
280 install: timings.install,
281 },
282 Err(PatchError::LayoutMismatchPending(records)) => refused(records, None),
283 Err(error) => refused(Vec::new(), Some(error)),
284 }
285}
286
287#[cfg(any(unix, windows))]
289#[derive(Debug, PartialEq)]
290struct ApplyTimings {
291 load: std::time::Duration,
292 install: std::time::Duration,
293}
294
295#[cfg(any(unix, windows))]
301unsafe fn apply_patch_with_anchor(
302 mut table: JumpTable,
303 anchor: fn() -> usize,
304) -> Result<ApplyTimings, PatchError> {
305 let _serial = APPLY_LOCK.lock().unwrap_or_else(PoisonError::into_inner);
306
307 let pending = pending_layout_mismatches();
310 if !pending.is_empty() {
311 return Err(PatchError::LayoutMismatchPending(pending));
312 }
313
314 let anchor = anchor();
319 if anchor == 0 {
320 return Err(PatchError::AnchorUnresolved);
321 }
322 let old_offset = anchor.wrapping_sub(table.aslr_reference as usize);
323 let (base_start, base_end) =
324 image_range_containing(anchor).ok_or(PatchError::ImageRangeUnresolved)?;
325 let base_slide = image_slide_containing(anchor).ok_or(PatchError::ImageRangeUnresolved)?;
328 if old_offset != base_slide {
329 return Err(PatchError::AnchorMismatch {
330 image: "base",
331 expected: base_slide as u64,
332 got: old_offset as u64,
333 });
334 }
335
336 let loading = std::time::Instant::now();
337 let lib = unsafe { load_patch_library(&table.lib)? };
339 let load = loading.elapsed();
340 let installing = std::time::Instant::now();
341 let lib: &'static libloading::Library = Box::leak(Box::new(lib));
344
345 let symbol = unsafe { lib.get::<*const ()>(ANCHOR_SYMBOL.as_bytes()) }.map_err(|err| {
347 PatchError::Dlopen(format!(
348 "patch library has no `{ANCHOR_SYMBOL}` anchor: {err}"
349 ))
350 })?;
351 let patch_anchor = unsafe { symbol.try_as_raw_ptr() }.ok_or_else(|| {
354 PatchError::Dlopen(format!("patch library's `{ANCHOR_SYMBOL}` anchor is null"))
355 })?;
356 let new_offset = patch_anchor.wrapping_byte_sub(table.new_base_address as usize) as usize;
357 let (patch_start, patch_end) =
358 image_range_containing(patch_anchor as usize).ok_or(PatchError::ImageRangeUnresolved)?;
359 let patch_slide =
361 image_slide_containing(patch_anchor as usize).ok_or(PatchError::ImageRangeUnresolved)?;
362 if new_offset != patch_slide {
363 return Err(PatchError::AnchorMismatch {
364 image: "patch",
365 expected: patch_slide as u64,
366 got: new_offset as u64,
367 });
368 }
369
370 table.map = table
371 .map
372 .iter()
373 .map(|(old, new)| {
374 (
375 (*old as usize).wrapping_add(old_offset) as u64,
376 (*new as usize).wrapping_add(new_offset) as u64,
377 )
378 })
379 .collect();
380
381 register_base(base_start, base_end, old_offset);
383 register_patch(patch_start, patch_end, new_offset);
384 commit_patch(table);
385 Ok(ApplyTimings {
386 load,
387 install: installing.elapsed(),
388 })
389}
390
391#[derive(Clone, Debug, PartialEq, Eq, Hash)]
394pub struct LayoutMismatch {
395 pub type_name: String,
397 pub stored: String,
399 pub own: String,
401}
402
403impl std::fmt::Display for LayoutMismatch {
404 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405 write!(
406 f,
407 "{}: stored {}, own {}",
408 self.type_name, self.stored, self.own
409 )
410 }
411}
412
413static LAYOUT_MISMATCHES: Mutex<Vec<LayoutMismatch>> = Mutex::new(Vec::new());
414
415pub fn report_layout_mismatch(
423 type_name: &str,
424 stored: impl std::fmt::Display,
425 own: impl std::fmt::Display,
426) {
427 let record = LayoutMismatch {
428 type_name: type_name.to_owned(),
429 stored: stored.to_string(),
430 own: own.to_string(),
431 };
432 let mut records = LAYOUT_MISMATCHES
433 .lock()
434 .unwrap_or_else(PoisonError::into_inner);
435 if !records.contains(&record) {
436 records.push(record);
437 }
438}
439
440pub fn pending_layout_mismatches() -> Vec<LayoutMismatch> {
442 LAYOUT_MISMATCHES
443 .lock()
444 .unwrap_or_else(PoisonError::into_inner)
445 .clone()
446}
447
448pub fn mark_layout_mismatches_reported(reported: &[LayoutMismatch]) {
451 LAYOUT_MISMATCHES
452 .lock()
453 .unwrap_or_else(PoisonError::into_inner)
454 .retain(|record| !reported.contains(record));
455}
456
457#[cfg(test)]
459pub(crate) mod test_support {
460 use std::sync::{Mutex, MutexGuard, PoisonError};
461
462 use super::{APP_JUMP_TABLE, HOTRELOAD_HANDLERS, LAYOUT_MISMATCHES, Ordering, commit_patch};
463 use crate::JumpTable;
464 use crate::hot_fn::reset_fall_throughs;
465
466 static SERIAL: Mutex<()> = Mutex::new(());
467
468 pub(crate) fn serial() -> MutexGuard<'static, ()> {
470 SERIAL.lock().unwrap_or_else(PoisonError::into_inner)
471 }
472
473 pub(crate) fn reset() {
475 APP_JUMP_TABLE.store(std::ptr::null_mut(), Ordering::Release);
476 HOTRELOAD_HANDLERS
477 .lock()
478 .unwrap_or_else(PoisonError::into_inner)
479 .clear();
480 reset_fall_throughs();
481 crate::anchor::reset_for_test();
482 LAYOUT_MISMATCHES
483 .lock()
484 .unwrap_or_else(PoisonError::into_inner)
485 .clear();
486 }
487
488 pub(crate) fn install(table: JumpTable) {
490 commit_patch(table);
491 }
492}
493
494#[cfg(test)]
495mod tests {
496 use std::sync::atomic::AtomicU32;
497
498 use crate::anchor::UNKNOWN_IMAGE;
499
500 use super::*;
501
502 fn table(entries: &[(u64, u64)]) -> JumpTable {
503 JumpTable {
504 lib: "/nonexistent/frust-hotpatch-test.so".into(),
505 map: entries.iter().copied().collect(),
506 aslr_reference: (test_anchor() - slide_of_test_anchor()) as u64,
508 new_base_address: 0x2000,
509 ifunc_count: 0,
510 }
511 }
512
513 fn test_anchor_fn() {}
515
516 fn test_anchor() -> usize {
517 test_anchor_fn as *const () as usize
518 }
519
520 fn slide_of_test_anchor() -> usize {
521 crate::anchor::image_slide_containing(test_anchor()).unwrap_or(0)
522 }
523
524 fn counting_handler() -> (Arc<AtomicU32>, Handler) {
525 let count = Arc::new(AtomicU32::new(0));
526 let seen = Arc::clone(&count);
527 (
528 count,
529 Arc::new(move || {
530 seen.fetch_add(1, Ordering::SeqCst);
531 }),
532 )
533 }
534
535 #[test]
536 fn missing_library_is_a_load_error() {
537 let err = unsafe { load_patch_library(Path::new("/nonexistent/frust-hotpatch-test.so")) };
539 assert!(matches!(
540 err,
541 Err(PatchError::Dlopen(_) | PatchError::AndroidMemfd(_))
542 ));
543 }
544
545 #[test]
546 fn handler_runs_once_per_patch() {
547 let _serial = test_support::serial();
548 test_support::reset();
549 let (count, handler) = counting_handler();
550 register_handler(handler);
551
552 test_support::install(table(&[]));
553 assert_eq!(count.load(Ordering::SeqCst), 1);
554 test_support::install(table(&[]));
555 assert_eq!(count.load(Ordering::SeqCst), 2);
556
557 test_support::reset();
558 }
559
560 #[test]
561 fn panicking_handler_does_not_abort_the_patch() {
562 let _serial = test_support::serial();
563 test_support::reset();
564 register_handler(Arc::new(|| panic!("handler failure under test")));
565 let (count, handler) = counting_handler();
566 register_handler(handler);
567
568 commit_patch(table(&[(1, 2)]));
569 assert_eq!(count.load(Ordering::SeqCst), 1, "later handlers still run");
570 let installed = unsafe { get_jump_table() }.map(|t| t.map.get(&1).copied());
572 assert_eq!(installed, Some(Some(2)), "the patch stays installed");
573
574 assert_eq!(run_handlers(), 1);
576 test_support::reset();
577 }
578
579 #[test]
580 fn poisoned_handler_lock_is_recovered() {
581 let _serial = test_support::serial();
582 test_support::reset();
583 let _ = std::thread::spawn(|| {
584 let _held = HOTRELOAD_HANDLERS.lock();
585 panic!("poisoning the handler lock under test");
586 })
587 .join();
588 assert!(HOTRELOAD_HANDLERS.is_poisoned());
589
590 let (count, handler) = counting_handler();
591 register_handler(handler);
592 test_support::install(table(&[]));
593 assert_eq!(count.load(Ordering::SeqCst), 1);
594
595 HOTRELOAD_HANDLERS.clear_poison();
596 test_support::reset();
597 }
598
599 #[cfg(any(unix, windows))]
600 #[test]
601 fn poisoned_apply_lock_is_recovered() {
602 let _serial = test_support::serial();
603 test_support::reset();
604 let _ = std::thread::spawn(|| {
605 let _held = APPLY_LOCK.lock();
606 panic!("poisoning the apply lock under test");
607 })
608 .join();
609 assert!(APPLY_LOCK.is_poisoned());
610
611 let err = unsafe { apply_patch_with_anchor(table(&[(1, 2)]), test_anchor) };
614 assert!(matches!(
615 err,
616 Err(PatchError::Dlopen(_) | PatchError::AndroidMemfd(_))
617 ));
618 assert!(unsafe { get_jump_table() }.is_none());
620
621 APPLY_LOCK.clear_poison();
622 test_support::reset();
623 }
624
625 #[cfg(any(unix, windows))]
626 #[test]
627 fn unresolved_anchor_fails_closed() {
628 let _serial = test_support::serial();
629 test_support::reset();
630 let (count, handler) = counting_handler();
631 register_handler(handler);
632
633 let err = unsafe { apply_patch_with_anchor(table(&[(0x1100, 0x2100)]), || 0) };
637 assert_eq!(err, Err(PatchError::AnchorUnresolved));
638 assert!(unsafe { get_jump_table() }.is_none(), "nothing installed");
640 assert_eq!(count.load(Ordering::SeqCst), 0, "no handler fired");
641
642 test_support::reset();
643 }
644
645 #[cfg(any(unix, windows))]
646 #[test]
647 #[cfg_attr(
648 not(debug_assertions),
649 ignore = "the release profile refuses before the anchor; see release_profile_refuses"
650 )]
651 fn apply_refuses_until_an_anchor_is_set_then_proceeds() {
652 let _serial = test_support::serial();
653 test_support::reset();
654
655 let err = unsafe { apply_patch(table(&[(0x1100, 0x2100)])) };
657 assert_eq!(err, Err(PatchError::AnchorUnresolved), "no anchor yet");
658
659 crate::set_anchor(test_anchor());
660 let err = unsafe { apply_patch(table(&[(0x1100, 0x2100)])) };
662 assert!(
663 matches!(
664 err,
665 Err(PatchError::Dlopen(_) | PatchError::AndroidMemfd(_))
666 ),
667 "with an anchor the call reaches the loader: {err:?}"
668 );
669 test_support::reset();
670 }
671
672 #[cfg(any(unix, windows))]
673 #[test]
674 #[cfg_attr(
675 debug_assertions,
676 ignore = "covers the release profile; run `cargo test --release -p frust-hotpatch`"
677 )]
678 fn release_profile_refuses() {
679 let _serial = test_support::serial();
680 test_support::reset();
681 crate::set_anchor(test_anchor());
682 let err = unsafe { apply_patch(table(&[])) };
684 assert_eq!(err, Err(PatchError::ReleaseBuild));
685 let report = apply_from_devtools(Path::new("/nonexistent/x.so"), table(&[]));
686 assert!(!report.applied);
687 assert_eq!(report.error, Some(PatchError::ReleaseBuild));
688 test_support::reset();
689 }
690
691 #[cfg(any(unix, windows))]
692 #[test]
693 fn debug_profile_does_not_refuse_with_release_build() {
694 if !cfg!(debug_assertions) {
695 return;
696 }
697 let _serial = test_support::serial();
698 test_support::reset();
699 crate::set_anchor(test_anchor());
700 let err = unsafe { apply_patch(table(&[])) };
702 assert_ne!(err, Err(PatchError::ReleaseBuild));
703 test_support::reset();
704 }
705
706 #[cfg(any(unix, windows))]
708 fn anchorless_library() -> &'static str {
709 if cfg!(target_vendor = "apple") {
710 "/usr/lib/libSystem.B.dylib"
711 } else if cfg!(windows) {
712 "kernel32.dll"
713 } else {
714 "libc.so.6"
715 }
716 }
717
718 #[cfg(any(unix, windows))]
719 #[test]
720 #[cfg_attr(not(debug_assertions), ignore = "apply_patch refuses in release")]
721 fn library_without_the_anchor_symbol_is_refused() {
722 let _serial = test_support::serial();
723 test_support::reset();
724 crate::set_anchor(test_anchor());
725 let mut t = table(&[(0x1100, 0x2100)]);
726 t.lib = anchorless_library().into();
727 let err = unsafe { apply_patch(t) };
729 match err {
730 Err(PatchError::Dlopen(msg)) => assert!(msg.contains(ANCHOR_SYMBOL), "{msg}"),
731 other => panic!("expected Dlopen naming the anchor, got {other:?}"),
732 }
733 assert!(unsafe { get_jump_table() }.is_none());
735 test_support::reset();
736 }
737
738 #[cfg(any(unix, windows))]
741 #[test]
742 #[cfg_attr(not(debug_assertions), ignore = "apply_patch refuses in release")]
743 fn real_patch_library_applies_through_the_anchor_symbol() {
744 let _serial = test_support::serial();
745 test_support::reset();
746
747 let dir = std::env::temp_dir().join(format!("frust-hotpatch-test-{}", std::process::id()));
748 std::fs::create_dir_all(&dir).expect("create scratch dir");
749 let src = dir.join("patch.rs");
750 std::fs::write(
751 &src,
752 "#[unsafe(no_mangle)] pub extern \"C\" fn __frust_hotpatch_anchor() {}\n\
753 #[unsafe(no_mangle)] pub extern \"C\" fn patched() -> u32 { 7 }\n",
754 )
755 .expect("write patch source");
756 let ext = if cfg!(windows) {
757 "dll"
758 } else if cfg!(target_vendor = "apple") {
759 "dylib"
760 } else {
761 "so"
762 };
763 let lib = dir.join(format!("patch.{ext}"));
764 let status = std::process::Command::new("rustc")
765 .args([
766 "--edition=2024",
767 "--crate-type=cdylib",
768 "--crate-name=patch",
769 "-o",
770 ])
771 .arg(&lib)
772 .arg(&src)
773 .status();
774 let Ok(status) = status else {
775 eprintln!("rustc is not spawnable; real-library apply test skipped");
776 return;
777 };
778 assert!(status.success(), "building the patch library failed");
779
780 crate::set_anchor(test_anchor());
781 let base_slide = crate::anchor::image_slide_containing(test_anchor()).expect("base slide");
784 let good_old = test_anchor().wrapping_sub(base_slide) as u64;
785 let probe: &'static libloading::Library = Box::leak(Box::new(
788 unsafe { load_patch_library(&lib) }.expect("map patch"),
789 ));
790 let symbol = unsafe { probe.get::<*const ()>(ANCHOR_SYMBOL.as_bytes()) }.expect("anchor");
792 let patch_anchor = unsafe { symbol.try_as_raw_ptr() }.expect("non-null anchor") as usize;
794 let patch_slide = crate::anchor::image_slide_containing(patch_anchor).expect("patch slide");
795 let good_new = patch_anchor.wrapping_sub(patch_slide) as u64;
796 let build = |old: u64, new: u64| {
797 let mut t = table(&[(0x1100, 0x2100)]);
798 t.lib = lib.clone();
799 t.aslr_reference = old;
800 t.new_base_address = new;
801 t
802 };
803
804 let wrong_base = unsafe { apply_patch(build(good_old + 0x10, good_new)) };
807 assert_eq!(
808 wrong_base,
809 Err(PatchError::AnchorMismatch {
810 image: "base",
811 expected: base_slide as u64,
812 got: base_slide as u64 - 0x10,
813 })
814 );
815 let wrong_patch = unsafe { apply_patch(build(good_old, good_new + 0x10)) };
817 assert_eq!(
818 wrong_patch,
819 Err(PatchError::AnchorMismatch {
820 image: "patch",
821 expected: patch_slide as u64,
822 got: patch_slide as u64 - 0x10,
823 })
824 );
825 assert!(unsafe { get_jump_table() }.is_none(), "nothing installed");
827 assert_eq!(
828 crate::anchor::classify(test_anchor() as u64).0,
829 UNKNOWN_IMAGE
830 );
831
832 unsafe { apply_patch(build(good_old, good_new)) }.expect("apply through the anchor symbol");
834
835 let old_offset = base_slide;
836 let installed = unsafe { get_jump_table() }.expect("table installed");
838 assert_eq!(installed.map.len(), 1);
839 let (key, value) = installed.map.iter().next().expect("one entry");
840 assert_eq!(*key, (0x1100usize.wrapping_add(old_offset)) as u64);
841 assert_ne!(
842 *value, 0x2100,
843 "the target was rebased onto the loaded image"
844 );
845 assert_eq!(crate::anchor::classify(test_anchor() as u64).0, 0);
847 assert_eq!(crate::anchor::classify(*value).0, 1);
848
849 test_support::reset();
850 let _ = std::fs::remove_dir_all(&dir);
851 }
852
853 #[cfg(any(unix, windows))]
854 #[test]
855 #[cfg_attr(not(debug_assertions), ignore = "apply_patch refuses in release")]
856 fn raw_apply_patch_refuses_while_a_mismatch_is_unreported_then_proceeds() {
857 let _serial = test_support::serial();
858 test_support::reset();
859 crate::set_anchor(test_anchor());
860
861 report_layout_mismatch("RawState", 4, 8);
862 let expected = pending_layout_mismatches();
863 let refused = unsafe { apply_patch(table(&[(1, 2)])) };
865 assert_eq!(
866 refused,
867 Err(PatchError::LayoutMismatchPending(expected.clone()))
868 );
869 assert!(unsafe { get_jump_table() }.is_none());
871
872 mark_layout_mismatches_reported(&expected);
873 let proceeded = unsafe { apply_patch(table(&[(1, 2)])) };
875 assert!(
876 matches!(
877 proceeded,
878 Err(PatchError::Dlopen(_) | PatchError::AndroidMemfd(_))
879 ),
880 "once reported the raw apply reaches the loader: {proceeded:?}"
881 );
882 test_support::reset();
883 }
884
885 #[cfg(any(unix, windows))]
886 #[test]
887 #[cfg_attr(not(debug_assertions), ignore = "apply_patch refuses in release")]
888 fn devtools_apply_refuses_while_a_mismatch_is_unreported() {
889 let _serial = test_support::serial();
890 test_support::reset();
891 crate::set_anchor(test_anchor());
892
893 report_layout_mismatch("HomeState", 4, 8);
894 report_layout_mismatch("HomeState", 4, 8);
895 let expected = LayoutMismatch {
896 type_name: "HomeState".into(),
897 stored: "4".into(),
898 own: "8".into(),
899 };
900 assert_eq!(
901 pending_layout_mismatches(),
902 vec![expected.clone()],
903 "deduplicated"
904 );
905
906 let report = apply_from_devtools(Path::new("/nonexistent/x.so"), table(&[(1, 2)]));
908 assert!(!report.applied);
909 assert_eq!(report.layout_mismatches, vec![expected.clone()]);
910 assert_eq!(report.error, None, "refused for the record, not an error");
911 assert_eq!(
912 (report.load, report.install),
913 (std::time::Duration::ZERO, std::time::Duration::ZERO),
914 "nothing was loaded or installed, so nothing was timed"
915 );
916 assert!(unsafe { get_jump_table() }.is_none());
918
919 report_layout_mismatch("Other", 1, 2);
921 mark_layout_mismatches_reported(&report.layout_mismatches);
922 assert_eq!(pending_layout_mismatches().len(), 1);
923 mark_layout_mismatches_reported(&pending_layout_mismatches());
924 assert!(pending_layout_mismatches().is_empty());
925
926 let report = apply_from_devtools(Path::new("/nonexistent/x.so"), table(&[(1, 2)]));
927 assert!(report.layout_mismatches.is_empty());
928 assert!(
929 matches!(
930 report.error,
931 Some(PatchError::Dlopen(_) | PatchError::AndroidMemfd(_))
932 ),
933 "once reported the apply proceeds to the loader: {report:?}"
934 );
935 assert_eq!(
936 LayoutMismatch {
937 type_name: "T".into(),
938 stored: "1".into(),
939 own: "2".into()
940 }
941 .to_string(),
942 "T: stored 1, own 2"
943 );
944 test_support::reset();
945 }
946}