/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ //! Verifies precision, determinism, and cancellation of timer events and clocks. //! //! Syscalls are abused to communicate from the guest to the tool instructions //! necessary to carry out the test, such as setting timers or reading clocks. use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; use std::sync::Mutex; use reverie::syscalls::Getpid; use reverie::syscalls::Gettid; use reverie::syscalls::Syscall; use reverie::syscalls::SyscallInfo; use reverie::syscalls::Sysno; use reverie::syscalls::Tgkill; use reverie::Errno; use reverie::Error; use reverie::GlobalTool; use reverie::Guest; use reverie::Pid; use reverie::Signal; use reverie::Subscription; use reverie::TimerSchedule; use reverie::Tool; use reverie_ptrace::regs::RegAccess; use serde::Deserialize; use serde::Serialize; #[derive(Debug, Default)] struct GlobalState { num_timer_evts: AtomicU64, num_signals: AtomicU64, } #[derive(Debug, Default, Clone)] struct LocalState; #[derive(PartialEq, Debug, Eq, Clone, Copy, Serialize, Deserialize)] enum IncrMsg { Timer, Signal, } #[derive(Debug, Serialize, Deserialize, Default, Clone)] struct Config { sub_syscalls_only: bool, run_basic_tests: bool, timeout_rcbs: u64, timeout_rcbs_alternate: u64, } #[derive(Debug, Serialize, Deserialize, Default, Clone)] struct ThreadClockState { /// baseline for clock comparisons last_tick: u64, /// offset from baseline to assert at a timer event timer_assertion: Option, } #[reverie::global_tool] impl GlobalTool for GlobalState { type Request = IncrMsg; type Response = (); type Config = Config; async fn init_global_state(_: &Self::Config) -> Self { GlobalState { num_timer_evts: AtomicU64::new(0), num_signals: AtomicU64::new(0), } } async fn receive_rpc(&self, _from: Pid, msg: IncrMsg) -> Self::Response { match msg { IncrMsg::Timer => self.num_timer_evts.fetch_add(1, Ordering::SeqCst), IncrMsg::Signal => self.num_signals.fetch_add(1, Ordering::SeqCst), }; } } const BULK_INJECTION_COUNT: u64 = 10; #[reverie::tool] impl Tool for LocalState { type GlobalState = GlobalState; type ThreadState = ThreadClockState; fn subscriptions(cfg: &Config) -> Subscription { if cfg.sub_syscalls_only { Subscription::all_syscalls() } else { Subscription::all() } } async fn handle_thread_start>(&self, guest: &mut T) -> Result<(), Error> { if guest.config().run_basic_tests { assert_eq!(guest.read_clock().unwrap(), 0); assert!(guest.set_timer(TimerSchedule::Rcbs(0)).is_err()); } Ok(()) } async fn handle_syscall_event>( &self, guest: &mut T, syscall: Syscall, ) -> Result { let timeout = TimerSchedule::Rcbs(guest.config().timeout_rcbs); let alt_timeout = TimerSchedule::Rcbs(guest.config().timeout_rcbs_alternate); let (no, args) = syscall.into_parts(); match no { Sysno::clock_getres => { guest.set_timer_precise(timeout).unwrap(); } Sysno::msgrcv => { guest.set_timer_precise(alt_timeout).unwrap(); } Sysno::timer_getoverrun => { guest.set_timer(timeout).unwrap(); } Sysno::fanotify_init => { guest.set_timer_precise(timeout).unwrap(); let kill_call = raise_sigwinch(guest).await; guest.tail_inject(kill_call).await } Sysno::fanotify_mark => { guest.set_timer(timeout).unwrap(); let kill_call = raise_sigwinch(guest).await; guest.tail_inject(kill_call).await } Sysno::msgctl => { guest.set_timer_precise(timeout).unwrap(); for _ in 0..BULK_INJECTION_COUNT { guest.inject(Getpid::new()).await.unwrap(); } guest.tail_inject(Getpid::new()).await } Sysno::msgget => { guest.set_timer(timeout).unwrap(); for _ in 0..BULK_INJECTION_COUNT { guest.inject(Getpid::new()).await.unwrap(); } guest.tail_inject(Getpid::new()).await } Sysno::clock_settime => { let clock_value = guest.read_clock().unwrap(); let ts = guest.thread_state_mut(); ts.last_tick = clock_value; ts.timer_assertion = None; } Sysno::timer_gettime => { let clock_value = guest.read_clock().unwrap(); let ts = guest.thread_state_mut(); ts.last_tick = clock_value; ts.timer_assertion = Some(args.arg0 as u64); } Sysno::clock_adjtime => assert_eq!( guest.read_clock().unwrap(), guest.thread_state_mut().last_tick + args.arg0 as u64 ), _ => guest.tail_inject(syscall).await, }; Ok(0) } async fn handle_timer_event>(&self, guest: &mut T) { guest.send_rpc(IncrMsg::Timer).await; let clock_value = guest.read_clock().unwrap(); let ts = guest.thread_state(); if let Some(val) = ts.timer_assertion { assert_eq!(ts.last_tick + val, clock_value); } } async fn handle_signal_event>( &self, guest: &mut T, signal: Signal, ) -> Result, Errno> { guest.send_rpc(IncrMsg::Signal).await; Ok(Some(signal)) } } async fn raise_sigwinch>(guest: &mut T) -> Tgkill { let pid = guest.inject(Getpid::new()).await.unwrap(); let tid = guest.inject(Gettid::new()).await.unwrap(); Tgkill::new() .with_tgid(pid as _) .with_tid(tid as _) .with_sig(libc::SIGWINCH) } #[derive(Default)] struct InstructionTracingGlobal { timer_events: Mutex>, } #[derive(PartialEq, Default, Debug, Eq, Clone, Copy, Serialize, Deserialize)] struct TimerEvent { rcbs: u64, rip: reverie_ptrace::regs::Reg, } #[derive(Default, Clone, Serialize, Deserialize)] struct InstructionTracingConfig { set_timer_after_syscall: Option<(u64, Option)>, } #[derive(Serialize, Deserialize)] enum InstructionTracingRequest { Timer(TimerEvent), } #[reverie::global_tool] impl GlobalTool for InstructionTracingGlobal { type Request = InstructionTracingRequest; type Response = (); type Config = InstructionTracingConfig; async fn receive_rpc(&self, _from: reverie::Tid, message: Self::Request) -> Self::Response { match message { InstructionTracingRequest::Timer(ev) => self.timer_events.lock().unwrap().push(ev), } } } #[derive(Debug, Default, Clone)] struct InstructionTracing; #[reverie::tool] impl Tool for InstructionTracing { type GlobalState = InstructionTracingGlobal; type ThreadState = ThreadClockState; async fn handle_syscall_event>( &self, guest: &mut T, c: Syscall, ) -> Result { match guest.config().set_timer_after_syscall { Some((rcb, Some(instr))) => { guest.set_timer_precise(reverie::TimerSchedule::RcbsAndInstructions(rcb, instr))?; } Some((rcb, None)) => { guest.set_timer_precise(reverie::TimerSchedule::Rcbs(rcb))?; } _ => {} } guest.tail_inject(c).await } async fn handle_timer_event>(&self, guest: &mut T) { let rcbs = guest.read_clock().expect("clock available"); let rip = guest.regs().await.ip(); guest .send_rpc(InstructionTracingRequest::Timer(TimerEvent { rcbs, rip })) .await; } } #[cfg(all(not(sanitized), test))] mod tests { //! These tests are highly sensitive to the number of branches executed //! in the guest, and this must remain consistent between opt and debug //! mode. If you pass non-constant values into do_branches and need them to //! be exact, be sure to precompute them in the tracer before moving them //! into the tracee, otherwise underflow or overflow checks will break the //! tests. use reverie_ptrace::regs::RegAccess; use reverie_ptrace::ret_without_perf; use reverie_ptrace::testing::check_fn; use reverie_ptrace::testing::check_fn_with_config; use reverie_ptrace::testing::do_branches; use test_case::test_case; use super::*; #[inline(always)] unsafe fn syscall_no_branches(no: Sysno, arg1: libc::c_long) { syscalls::raw_syscall!(no, arg1, 0, 0, 0, 0, 0); } fn sched_precise() { unsafe { syscall_no_branches(Sysno::clock_getres, 0) } } fn sched_precise_alternate_rcb_count() { unsafe { syscall_no_branches(Sysno::msgrcv, 0) } } fn sched_imprecise() { unsafe { syscall_no_branches(Sysno::timer_getoverrun, 0) } } fn mark_clock() { unsafe { syscall_no_branches(Sysno::clock_settime, 0) } } fn assert_clock(delta: u64) { unsafe { syscall_no_branches(Sysno::clock_adjtime, delta as i64) } } fn assert_clock_at_next_timer(value: u64) { unsafe { syscall_no_branches(Sysno::timer_gettime, value as i64) } } fn do_syscall() { unsafe { syscall_no_branches(Sysno::clock_gettime, 0) } } fn immediate_exit() { unsafe { syscall_no_branches(Sysno::exit, 0) } } fn sched_precise_and_raise() { unsafe { syscall_no_branches(Sysno::fanotify_init, 0) } } fn sched_imprecise_and_raise() { unsafe { syscall_no_branches(Sysno::fanotify_mark, 0) } } fn sched_precise_and_inject() { unsafe { syscall_no_branches(Sysno::msgctl, 0) } } fn sched_imprecise_and_inject() { unsafe { syscall_no_branches(Sysno::msgget, 0) } } fn cpuid() { #[cfg(target_arch = "x86_64")] unsafe { core::arch::x86_64::__cpuid(0); } } fn rdtsc() { #[cfg(target_arch = "x86_64")] unsafe { core::arch::x86_64::_rdtsc(); } } fn rdtscp() { #[cfg(target_arch = "x86_64")] unsafe { let mut x = 0u32; core::arch::x86_64::__rdtscp(&mut x as *mut _); } } fn ts_check_fn(rcbs: u64, f: impl FnOnce()) -> GlobalState { use reverie_ptrace::testing::check_fn_with_config; check_fn_with_config::( f, Config { timeout_rcbs: rcbs, ..Default::default() }, true, ) } const MANY_RCBS: u64 = 10000; // Normal perf signaling const LESS_RCBS: u64 = 15; // Low enough to use artificial signaling #[test_case(MANY_RCBS, sched_precise)] #[test_case(MANY_RCBS, sched_imprecise)] #[test_case(LESS_RCBS, sched_precise)] #[test_case(LESS_RCBS, sched_imprecise)] fn timer_delays_timer(rcbs: u64, schedule_timer: fn() -> ()) { ret_without_perf!(); let rcbd2 = rcbs / 2; let rcbx2 = rcbs * 2; let gs = ts_check_fn(rcbs, move || { schedule_timer(); do_branches(rcbd2); schedule_timer(); do_branches(rcbd2); schedule_timer(); do_branches(rcbd2); schedule_timer(); do_branches(rcbx2); }); assert_eq!(gs.num_timer_evts.into_inner(), 1); } #[test_case(MANY_RCBS, sched_precise)] #[test_case(MANY_RCBS, sched_imprecise)] #[test_case(LESS_RCBS, sched_precise)] #[test_case(LESS_RCBS, sched_imprecise)] fn timer_is_single(rcbs: u64, schedule_timer: fn() -> ()) { ret_without_perf!(); let rcbx2 = rcbs * 2; let rcbx10 = rcbs * 10; let rcbx20 = rcbs * 20; let gs = ts_check_fn(rcbs, move || { schedule_timer(); do_branches(rcbx2); schedule_timer(); do_branches(rcbx10); schedule_timer(); do_branches(rcbx20); }); assert_eq!(gs.num_timer_evts.into_inner(), 3); } #[test_case(MANY_RCBS, sched_precise)] #[test_case(MANY_RCBS, sched_precise_and_inject)] #[test_case(LESS_RCBS, sched_precise)] #[test_case(LESS_RCBS, sched_precise_and_inject)] fn precise_delivery_exact(rcbs: u64, schedule_timer: fn() -> ()) { ret_without_perf!(); // `do_branches(n)` does n+1 branches, so `rcbs - 1` will be the // first argument resulting in a timer event. // Precompute to avoid underflow checks in the guest let branch_ct = rcbs - 2; let gs3 = ts_check_fn(rcbs, move || { schedule_timer(); do_branches(branch_ct); immediate_exit(); }); assert_eq!(gs3.num_timer_evts.into_inner(), 0); let branch_ct = rcbs - 1; let gs2 = ts_check_fn(rcbs, move || { schedule_timer(); do_branches(branch_ct); immediate_exit(); }); assert_eq!(gs2.num_timer_evts.into_inner(), 1); } fn early_stop_rcbs(rcbs: u64) -> Vec { // Final value is 2 because do_branches adds 1 [rcbs / 2, 1000, 100, 10, 2] .iter() .map(|x| *x) .filter(|x| *x < rcbs) .collect() } const LET_PASS_STOP_RCBS: u64 = 1; #[test_case(MANY_RCBS, do_syscall, sched_precise, 0)] #[test_case(MANY_RCBS, do_syscall, sched_imprecise, 0)] #[test_case(MANY_RCBS, cpuid, sched_precise, 0)] #[test_case(MANY_RCBS, cpuid, sched_imprecise, 0)] #[test_case(MANY_RCBS, rdtsc, sched_precise, 0)] #[test_case(MANY_RCBS, rdtsc, sched_imprecise, 0)] #[test_case(MANY_RCBS, rdtscp, sched_precise, 0)] #[test_case(MANY_RCBS, rdtscp, sched_imprecise, 0)] #[test_case(MANY_RCBS, sched_precise, sched_precise, 1)] #[test_case(MANY_RCBS, sched_imprecise, sched_precise, 1)] #[test_case(MANY_RCBS, sched_imprecise, sched_imprecise, 1)] #[test_case(MANY_RCBS, sched_precise, sched_imprecise, 1)] #[test_case(LESS_RCBS, do_syscall, sched_precise, 0)] #[test_case(LESS_RCBS, do_syscall, sched_imprecise, 0)] #[test_case(LESS_RCBS, cpuid, sched_precise, 0)] #[test_case(LESS_RCBS, cpuid, sched_imprecise, 0)] #[test_case(LESS_RCBS, rdtsc, sched_precise, 0)] #[test_case(LESS_RCBS, rdtsc, sched_imprecise, 0)] #[test_case(LESS_RCBS, rdtscp, sched_precise, 0)] #[test_case(LESS_RCBS, rdtscp, sched_imprecise, 0)] #[test_case(LESS_RCBS, sched_precise, sched_precise, 1)] #[test_case(LESS_RCBS, sched_imprecise, sched_precise, 1)] #[test_case(LESS_RCBS, sched_imprecise, sched_imprecise, 1)] #[test_case(LESS_RCBS, sched_precise, sched_imprecise, 1)] fn assert_cancels_timers( rcbs: u64, fun: fn() -> (), schedule_timer: fn() -> (), additional_evts: u64, ) { ret_without_perf!(); let rcbx2 = rcbs * 2; for e in early_stop_rcbs(rcbs) { // Precompute to avoid underflow checks in the guest let branch_ct = rcbs - e; let gs = ts_check_fn(rcbs, move || { schedule_timer(); do_branches(branch_ct); fun(); do_branches(rcbx2); }); assert_eq!( gs.num_timer_evts.into_inner(), 0 + additional_evts, "iter: {}", e ); } // Imprecise events can be delayed, in which case nothing fires, so only // test this if precise: if schedule_timer == sched_precise { // Precompute to avoid underflow checks in the guest let branch_ct = rcbs - LET_PASS_STOP_RCBS; let gs = ts_check_fn(rcbs, move || { schedule_timer(); do_branches(branch_ct); fun(); do_branches(rcbx2); }); assert_eq!(gs.num_timer_evts.into_inner(), 1 + additional_evts); } } #[test_case(MANY_RCBS, sched_precise, sched_precise_and_raise)] #[test_case(MANY_RCBS, sched_imprecise, sched_imprecise_and_raise)] #[test_case(LESS_RCBS, sched_precise, sched_precise_and_raise)] #[test_case(LESS_RCBS, sched_imprecise, sched_imprecise_and_raise)] fn signals_cancel_timers( rcbs: u64, schedule_timer: fn() -> (), schedule_timer_and_raise: fn() -> (), ) { ret_without_perf!(); let rcbd2 = rcbs / 2; let rcbx2 = rcbs * 2; // The signal after scheduling should immediately cancel the event let gs = ts_check_fn(rcbs, move || { schedule_timer(); do_branches(rcbd2); schedule_timer_and_raise(); do_branches(rcbx2); schedule_timer_and_raise(); do_branches(rcbx2); }); assert_eq!(gs.num_signals.into_inner(), 2); // defensive assert_eq!(gs.num_timer_evts.into_inner(), 0); // If we don't raise, events delivered as expected: let gs = ts_check_fn(rcbs, move || { schedule_timer(); do_branches(rcbd2); schedule_timer(); do_branches(rcbx2); schedule_timer(); do_branches(rcbx2); }); assert_eq!(gs.num_signals.into_inner(), 0); // defensive assert_eq!(gs.num_timer_evts.into_inner(), 2); } #[test_case(MANY_RCBS, sched_precise)] #[test_case(MANY_RCBS, sched_imprecise)] #[test_case(LESS_RCBS, sched_precise)] #[test_case(LESS_RCBS, sched_imprecise)] fn not_subscribed_doesnt_cancel(rcbs: u64, schedule_timer: fn() -> ()) { ret_without_perf!(); let rcbd2 = rcbs / 2; let rcbx2 = rcbs * 2; let gs = check_fn_with_config::( move || { schedule_timer(); do_branches(rcbd2); cpuid(); rdtsc(); rdtscp(); do_branches(rcbx2); }, Config { timeout_rcbs: rcbs, sub_syscalls_only: true, ..Default::default() }, true, ); assert_eq!(gs.num_timer_evts.into_inner(), 1); } fn loop_with_branch_ct( rcbs: u64, branch_ct: u64, iters: u64, schedule_timer: fn() -> (), ) -> GlobalState { ts_check_fn(rcbs, move || { for _ in 0..iters { schedule_timer(); do_branches(branch_ct); schedule_timer(); // cancel timer before loop branch } immediate_exit(); // RCBs in teardown would trigger the last iter's event }) } #[test_case(MANY_RCBS, sched_precise)] #[test_case(MANY_RCBS, sched_precise_and_inject)] #[test_case(LESS_RCBS, sched_precise)] #[test_case(LESS_RCBS, sched_precise_and_inject)] fn precise_deterministic_loop(rcbs: u64, schedule_timer: fn() -> ()) { ret_without_perf!(); let rcbm2 = rcbs - 2; let rcbm1 = rcbs - 1; const ITERS: u64 = 500; assert_eq!( loop_with_branch_ct(rcbs, rcbm2, ITERS, schedule_timer) .num_timer_evts .into_inner(), 0 ); assert_eq!( loop_with_branch_ct(rcbs, rcbm1, ITERS, schedule_timer) .num_timer_evts .into_inner(), ITERS ); } #[test_case(MANY_RCBS, sched_imprecise)] #[test_case(MANY_RCBS, sched_imprecise_and_inject)] #[test_case(LESS_RCBS, sched_imprecise)] #[test_case(LESS_RCBS, sched_imprecise_and_inject)] fn imprecise_not_early_loop(rcbs: u64, schedule_timer: fn() -> ()) { ret_without_perf!(); const ITERS: u64 = 2000; let rcbm2 = rcbs - 2; assert_eq!( loop_with_branch_ct(rcbs, rcbm2, ITERS, schedule_timer) .num_timer_evts .into_inner(), 0 ); } /// Regression test: Verify that a shorter event isn't cancelled by the /// occurrence of longer one if the two happen to align. #[test] fn long_short_not_cancelled() { ret_without_perf!(); // Doing this test correctly requires correctly predicting the amount of // skid in the underlying RCB signal. For that reason, we run the gamut // of possibilities in increments of 5, several times for each. const ITERS: usize = 50; // <============ MANY_RCBS ==============> // <===== SKID_MARGIN ====> // <= skid ==> // <= overlap => // ---------------------------------------------------- // ^ ^ | ^ ^ // sched timeout | signal delivery // | // `> schedule short event to cause step overlap // schedule time is therefore // MANY_RCBS - (SKID_MARGIN - skid) - (LESS_RCBS / 2) // skid_param = SKID_MARGIN - skid for skid_param in (0u64..200).step_by(5) { let branch_ct = MANY_RCBS - skid_param - (LESS_RCBS / 2); for _ in 0..ITERS { let gs = check_fn_with_config::( move || { sched_precise(); do_branches(branch_ct); sched_precise_alternate_rcb_count(); do_branches(MANY_RCBS * 10); }, Config { timeout_rcbs: MANY_RCBS, timeout_rcbs_alternate: LESS_RCBS, ..Default::default() }, true, ); assert_eq!(gs.num_timer_evts.into_inner(), 1); } } } #[test] fn clock_accuracy() { ret_without_perf!(); for r in [100, 1000, 10000, 100000, 1000000] { let rp1 = r + 1; // precompute check_fn::(move || { mark_clock(); do_branches(r); assert_clock(rp1); }); } } #[test] fn clock_stays_without_branch() { ret_without_perf!(); let r = 2000; let rp1 = r + 1; // precompute check_fn::(move || { mark_clock(); do_branches(r); assert_clock(rp1); assert_clock(rp1); assert_clock(rp1); assert_clock(rp1); assert_clock(rp1); assert_clock(rp1); assert_clock(rp1); assert_clock(rp1); assert_clock(rp1); }); } #[test_case(MANY_RCBS, sched_precise)] #[test_case(MANY_RCBS, sched_imprecise)] #[test_case(LESS_RCBS, sched_precise)] #[test_case(LESS_RCBS, sched_imprecise)] fn clock_with_timer(rcbs: u64, schedule_timer: fn() -> ()) { ret_without_perf!(); let a = rcbs * 2; let b = a + 1; let c = rcbs / 2; let d = c + 1; let gs = ts_check_fn(rcbs, move || { mark_clock(); schedule_timer(); do_branches(a); assert_clock(b); // timer received mark_clock(); schedule_timer(); do_branches(c); assert_clock(d); // timer outstanding }); assert_eq!(gs.num_timer_evts.into_inner(), 1); } #[test_case(MANY_RCBS)] #[test_case(LESS_RCBS)] fn clock_at_timer_delivery(rcbs: u64) { ret_without_perf!(); let rcbx2 = rcbs * 2; let gs = ts_check_fn(rcbs, move || { mark_clock(); assert_clock_at_next_timer(rcbs); sched_precise(); do_branches(rcbx2); }); assert_eq!(gs.num_timer_evts.into_inner(), 1); } #[test] fn basic() { ret_without_perf!(); let _gs = check_fn_with_config::( move || { do_syscall(); }, Config { run_basic_tests: true, ..Default::default() }, true, ); } #[test_case(1000, None, 1000, Some(1))] #[test_case(1000, Some(1), 1000, Some(2))] #[test_case(1000, Some(100), 1000, Some(101))] #[test_case(10, None, 10, Some(1))] #[test_case(10, Some(1), 10, Some(2))] #[test_case(10, Some(100), 10, Some(101))] fn test_set_precice_with_instructions_after_syscall( rcb_1: u64, instr_1: Option, rcb_2: u64, instr_2: Option, ) { ret_without_perf!(); let gs_1 = check_fn_with_config::( move || { do_syscall(); do_branches(MANY_RCBS); }, InstructionTracingConfig { set_timer_after_syscall: Some((rcb_1, instr_1)), ..Default::default() }, true, ); let gs_2 = check_fn_with_config::( move || { do_syscall(); do_branches(MANY_RCBS); }, InstructionTracingConfig { set_timer_after_syscall: Some((rcb_2, instr_2)), ..Default::default() }, true, ); let rips = [&gs_1, &gs_2].map(|s| s.timer_events.lock().unwrap()[0].rip); assert_ne!(rips[0], rips[1]); } }