Merge pull request #4435 from nia-e/trace-fixups
native_lib/trace: fix and reenable
This commit is contained in:
commit
a09cf9cc5a
10 changed files with 312 additions and 338 deletions
|
|
@ -302,23 +302,20 @@ impl IsolatedAlloc {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns a vector of page addresses managed by the allocator.
|
||||
pub fn pages(&self) -> Vec<usize> {
|
||||
let mut pages: Vec<usize> =
|
||||
self.page_ptrs.iter().map(|p| p.expose_provenance().get()).collect();
|
||||
for (ptr, size) in self.huge_ptrs.iter() {
|
||||
for i in 0..size / self.page_size {
|
||||
pages.push(ptr.expose_provenance().get().strict_add(i * self.page_size));
|
||||
}
|
||||
}
|
||||
pages
|
||||
/// Returns a list of page addresses managed by the allocator.
|
||||
pub fn pages(&self) -> impl Iterator<Item = usize> {
|
||||
let pages = self.page_ptrs.iter().map(|p| p.expose_provenance().get());
|
||||
pages.chain(self.huge_ptrs.iter().flat_map(|(ptr, size)| {
|
||||
(0..size / self.page_size)
|
||||
.map(|i| ptr.expose_provenance().get().strict_add(i * self.page_size))
|
||||
}))
|
||||
}
|
||||
|
||||
/// Protects all owned memory as `PROT_NONE`, preventing accesses.
|
||||
///
|
||||
/// SAFETY: Accessing memory after this point will result in a segfault
|
||||
/// unless it is first unprotected.
|
||||
pub unsafe fn prepare_ffi(&mut self) -> Result<(), nix::errno::Errno> {
|
||||
pub unsafe fn start_ffi(&mut self) -> Result<(), nix::errno::Errno> {
|
||||
let prot = mman::ProtFlags::PROT_NONE;
|
||||
unsafe { self.mprotect(prot) }
|
||||
}
|
||||
|
|
@ -326,7 +323,7 @@ impl IsolatedAlloc {
|
|||
/// Deprotects all owned memory by setting it to RW. Erroring here is very
|
||||
/// likely unrecoverable, so it may panic if applying those permissions
|
||||
/// fails.
|
||||
pub fn unprep_ffi(&mut self) {
|
||||
pub fn end_ffi(&mut self) {
|
||||
let prot = mman::ProtFlags::PROT_READ | mman::ProtFlags::PROT_WRITE;
|
||||
unsafe {
|
||||
self.mprotect(prot).unwrap();
|
||||
|
|
|
|||
|
|
@ -233,8 +233,6 @@ impl rustc_driver::Callbacks for MiriCompilerCalls {
|
|||
} else {
|
||||
let return_code = miri::eval_entry(tcx, entry_def_id, entry_type, &config, None)
|
||||
.unwrap_or_else(|| {
|
||||
//#[cfg(target_os = "linux")]
|
||||
//miri::native_lib::register_retcode_sv(rustc_driver::EXIT_FAILURE);
|
||||
tcx.dcx().abort_if_errors();
|
||||
rustc_driver::EXIT_FAILURE
|
||||
});
|
||||
|
|
@ -337,6 +335,9 @@ impl rustc_driver::Callbacks for MiriBeRustCompilerCalls {
|
|||
fn exit(exit_code: i32) -> ! {
|
||||
// Drop the tracing guard before exiting, so tracing calls are flushed correctly.
|
||||
deinit_loggers();
|
||||
// Make sure the supervisor knows about the code code.
|
||||
#[cfg(target_os = "linux")]
|
||||
miri::native_lib::register_retcode_sv(exit_code);
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
|
||||
|
|
@ -355,6 +356,11 @@ fn run_compiler_and_exit(
|
|||
args: &[String],
|
||||
callbacks: &mut (dyn rustc_driver::Callbacks + Send),
|
||||
) -> ! {
|
||||
// Install the ctrlc handler that sets `rustc_const_eval::CTRL_C_RECEIVED`, even if
|
||||
// MIRI_BE_RUSTC is set. We do this late so that when `native_lib::init_sv` is called,
|
||||
// there are no other threads.
|
||||
rustc_driver::install_ctrlc_handler();
|
||||
|
||||
// Invoke compiler, catch any unwinding panics and handle return code.
|
||||
let exit_code =
|
||||
rustc_driver::catch_with_exit_code(move || rustc_driver::run_compiler(args, callbacks));
|
||||
|
|
@ -439,10 +445,6 @@ fn main() {
|
|||
let args = rustc_driver::catch_fatal_errors(|| rustc_driver::args::raw_args(&early_dcx))
|
||||
.unwrap_or_else(|_| std::process::exit(rustc_driver::EXIT_FAILURE));
|
||||
|
||||
// Install the ctrlc handler that sets `rustc_const_eval::CTRL_C_RECEIVED`, even if
|
||||
// MIRI_BE_RUSTC is set.
|
||||
rustc_driver::install_ctrlc_handler();
|
||||
|
||||
// If the environment asks us to actually be rustc, then do that.
|
||||
if let Some(crate_kind) = env::var_os("MIRI_BE_RUSTC") {
|
||||
// Earliest rustc setup.
|
||||
|
|
@ -750,15 +752,15 @@ fn main() {
|
|||
|
||||
debug!("rustc arguments: {:?}", rustc_args);
|
||||
debug!("crate arguments: {:?}", miri_config.args);
|
||||
#[cfg(target_os = "linux")]
|
||||
if !miri_config.native_lib.is_empty() && miri_config.native_lib_enable_tracing {
|
||||
// FIXME: This should display a diagnostic / warning on error
|
||||
// SAFETY: If any other threads exist at this point (namely for the ctrlc
|
||||
// handler), they will not interact with anything on the main rustc/Miri
|
||||
// thread in an async-signal-unsafe way such as by accessing shared
|
||||
// semaphores, etc.; the handler only calls `sleep()` and `exit()`, which
|
||||
// are async-signal-safe, as is accessing atomics
|
||||
//let _ = unsafe { miri::native_lib::init_sv() };
|
||||
// SAFETY: No other threads are running
|
||||
#[cfg(target_os = "linux")]
|
||||
if unsafe { miri::native_lib::init_sv() }.is_err() {
|
||||
eprintln!(
|
||||
"warning: The native-lib tracer could not be started. Is this an x86 Linux system, and does Miri have permissions to ptrace?\n\
|
||||
Falling back to non-tracing native-lib mode."
|
||||
);
|
||||
}
|
||||
}
|
||||
run_compiler_and_exit(
|
||||
&rustc_args,
|
||||
|
|
|
|||
|
|
@ -96,10 +96,10 @@ pub use rustc_const_eval::interpret::{self, AllocMap, Provenance as _};
|
|||
use rustc_middle::{bug, span_bug};
|
||||
use tracing::{info, trace};
|
||||
|
||||
//#[cfg(target_os = "linux")]
|
||||
//pub mod native_lib {
|
||||
// pub use crate::shims::{init_sv, register_retcode_sv};
|
||||
//}
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod native_lib {
|
||||
pub use crate::shims::{init_sv, register_retcode_sv};
|
||||
}
|
||||
|
||||
// Type aliases that set the provenance parameter.
|
||||
pub type Pointer = interpret::Pointer<Option<machine::Provenance>>;
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ pub mod tls;
|
|||
pub mod unwind;
|
||||
|
||||
pub use self::files::FdTable;
|
||||
//#[cfg(target_os = "linux")]
|
||||
//pub use self::native_lib::trace::{init_sv, register_retcode_sv};
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use self::native_lib::trace::{init_sv, register_retcode_sv};
|
||||
pub use self::unix::{DirTable, EpollInterestTable};
|
||||
|
||||
/// What needs to be done after emulating an item (a shim or an intrinsic) is done.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
//! Implements calling functions from a native library.
|
||||
|
||||
// FIXME: disabled since it fails to build on many targets.
|
||||
//#[cfg(target_os = "linux")]
|
||||
//pub mod trace;
|
||||
|
||||
use std::ops::Deref;
|
||||
|
||||
use libffi::high::call as ffi;
|
||||
|
|
@ -13,14 +9,55 @@ use rustc_middle::mir::interpret::Pointer;
|
|||
use rustc_middle::ty::{self as ty, IntTy, UintTy};
|
||||
use rustc_span::Symbol;
|
||||
|
||||
//#[cfg(target_os = "linux")]
|
||||
//use self::trace::Supervisor;
|
||||
#[cfg_attr(
|
||||
not(all(
|
||||
target_os = "linux",
|
||||
target_env = "gnu",
|
||||
any(target_arch = "x86", target_arch = "x86_64")
|
||||
)),
|
||||
path = "trace/stub.rs"
|
||||
)]
|
||||
pub mod trace;
|
||||
|
||||
use crate::*;
|
||||
|
||||
//#[cfg(target_os = "linux")]
|
||||
//type CallResult<'tcx> = InterpResult<'tcx, (ImmTy<'tcx>, Option<self::trace::messages::MemEvents>)>;
|
||||
//#[cfg(not(target_os = "linux"))]
|
||||
type CallResult<'tcx> = InterpResult<'tcx, (ImmTy<'tcx>, Option<!>)>;
|
||||
/// The final results of an FFI trace, containing every relevant event detected
|
||||
/// by the tracer.
|
||||
#[allow(dead_code)]
|
||||
#[cfg_attr(target_os = "linux", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug)]
|
||||
pub struct MemEvents {
|
||||
/// An list of memory accesses that occurred, in the order they occurred in.
|
||||
pub acc_events: Vec<AccessEvent>,
|
||||
}
|
||||
|
||||
/// A single memory access.
|
||||
#[allow(dead_code)]
|
||||
#[cfg_attr(target_os = "linux", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug)]
|
||||
pub enum AccessEvent {
|
||||
/// A read may have occurred on this memory range.
|
||||
/// Some instructions *may* read memory without *always* doing that,
|
||||
/// so this can be an over-approximation.
|
||||
/// The range info, however, is reliable if the access did happen.
|
||||
Read(AccessRange),
|
||||
/// A read may have occurred on this memory range.
|
||||
/// Some instructions *may* write memory without *always* doing that,
|
||||
/// so this can be an over-approximation.
|
||||
/// The range info, however, is reliable if the access did happen.
|
||||
Write(AccessRange),
|
||||
}
|
||||
|
||||
/// The memory touched by a given access.
|
||||
#[allow(dead_code)]
|
||||
#[cfg_attr(target_os = "linux", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AccessRange {
|
||||
/// The base address in memory where an access occurred.
|
||||
pub addr: usize,
|
||||
/// The number of bytes affected from the base.
|
||||
pub size: usize,
|
||||
}
|
||||
|
||||
impl<'tcx> EvalContextExtPriv<'tcx> for crate::MiriInterpCx<'tcx> {}
|
||||
trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
||||
|
|
@ -31,18 +68,17 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
dest: &MPlaceTy<'tcx>,
|
||||
ptr: CodePtr,
|
||||
libffi_args: Vec<libffi::high::Arg<'a>>,
|
||||
) -> CallResult<'tcx> {
|
||||
) -> InterpResult<'tcx, (crate::ImmTy<'tcx>, Option<MemEvents>)> {
|
||||
let this = self.eval_context_mut();
|
||||
//#[cfg(target_os = "linux")]
|
||||
//let alloc = this.machine.allocator.as_ref().unwrap();
|
||||
#[cfg(target_os = "linux")]
|
||||
let alloc = this.machine.allocator.as_ref().unwrap();
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
// Placeholder value.
|
||||
let alloc = ();
|
||||
|
||||
// SAFETY: We don't touch the machine memory past this point.
|
||||
//#[cfg(target_os = "linux")]
|
||||
//let (guard, stack_ptr) = unsafe { Supervisor::start_ffi(alloc) };
|
||||
|
||||
// Call the function (`ptr`) with arguments `libffi_args`, and obtain the return value
|
||||
// as the specified primitive integer type
|
||||
let res = 'res: {
|
||||
trace::Supervisor::do_ffi(alloc, || {
|
||||
// Call the function (`ptr`) with arguments `libffi_args`, and obtain the return value
|
||||
// as the specified primitive integer type
|
||||
let scalar = match dest.layout.ty.kind() {
|
||||
// ints
|
||||
ty::Int(IntTy::I8) => {
|
||||
|
|
@ -93,7 +129,7 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
// have the output_type `Tuple([])`.
|
||||
ty::Tuple(t_list) if (*t_list).deref().is_empty() => {
|
||||
unsafe { ffi::call::<()>(ptr, libffi_args.as_slice()) };
|
||||
break 'res interp_ok(ImmTy::uninit(dest.layout));
|
||||
return interp_ok(ImmTy::uninit(dest.layout));
|
||||
}
|
||||
ty::RawPtr(..) => {
|
||||
let x = unsafe { ffi::call::<*const ()>(ptr, libffi_args.as_slice()) };
|
||||
|
|
@ -101,23 +137,14 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
Scalar::from_pointer(ptr, this)
|
||||
}
|
||||
_ =>
|
||||
break 'res Err(err_unsup_format!(
|
||||
return Err(err_unsup_format!(
|
||||
"unsupported return type for native call: {:?}",
|
||||
link_name
|
||||
))
|
||||
.into(),
|
||||
};
|
||||
interp_ok(ImmTy::from_scalar(scalar, dest.layout))
|
||||
};
|
||||
|
||||
// SAFETY: We got the guard and stack pointer from start_ffi, and
|
||||
// the allocator is the same
|
||||
//#[cfg(target_os = "linux")]
|
||||
//let events = unsafe { Supervisor::end_ffi(alloc, guard, stack_ptr) };
|
||||
//#[cfg(not(target_os = "linux"))]
|
||||
let events = None;
|
||||
|
||||
interp_ok((res?, events))
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the pointer to the function of the specified name in the shared object file,
|
||||
|
|
@ -214,10 +241,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
if !this.machine.native_call_mem_warned.replace(true) {
|
||||
// Newly set, so first time we get here.
|
||||
this.emit_diagnostic(NonHaltingDiagnostic::NativeCallSharedMem {
|
||||
//#[cfg(target_os = "linux")]
|
||||
//tracing: self::trace::Supervisor::is_enabled(),
|
||||
//#[cfg(not(target_os = "linux"))]
|
||||
tracing: false,
|
||||
tracing: self::trace::Supervisor::is_enabled(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,22 @@ use std::rc::Rc;
|
|||
use ipc_channel::ipc;
|
||||
use nix::sys::{ptrace, signal};
|
||||
use nix::unistd;
|
||||
use rustc_const_eval::interpret::InterpResult;
|
||||
|
||||
use super::CALLBACK_STACK_SIZE;
|
||||
use super::messages::{Confirmation, MemEvents, StartFfiInfo, TraceRequest};
|
||||
use super::messages::{Confirmation, StartFfiInfo, TraceRequest};
|
||||
use super::parent::{ChildListener, sv_loop};
|
||||
use crate::alloc::isolated_alloc::IsolatedAlloc;
|
||||
use crate::shims::native_lib::MemEvents;
|
||||
|
||||
/// A handle to the single, shared supervisor process across all `MiriMachine`s.
|
||||
/// Since it would be very difficult to trace multiple FFI calls in parallel, we
|
||||
/// need to ensure that either (a) only one `MiriMachine` is performing an FFI call
|
||||
/// at any given time, or (b) there are distinct supervisor and child processes for
|
||||
/// each machine. The former was chosen here.
|
||||
///
|
||||
/// This should only contain a `None` if the supervisor has not (yet) been initialised;
|
||||
/// otherwise, if `init_sv` was called and did not error, this will always be nonempty.
|
||||
static SUPERVISOR: std::sync::Mutex<Option<Supervisor>> = std::sync::Mutex::new(None);
|
||||
|
||||
/// The main means of communication between the child and parent process,
|
||||
|
|
@ -34,32 +44,23 @@ impl Supervisor {
|
|||
SUPERVISOR.lock().unwrap().is_some()
|
||||
}
|
||||
|
||||
/// Begins preparations for doing an FFI call. This should be called at
|
||||
/// the last possible moment before entering said call. `alloc` points to
|
||||
/// the allocator which handed out the memory used for this machine.
|
||||
///
|
||||
/// Performs an arbitrary FFI call, enabling tracing from the supervisor.
|
||||
/// As this locks the supervisor via a mutex, no other threads may enter FFI
|
||||
/// until this one returns and its guard is dropped via `end_ffi`. The
|
||||
/// pointer returned should be passed to `end_ffi` to avoid a memory leak.
|
||||
///
|
||||
/// SAFETY: The resulting guard must be dropped *via `end_ffi`* immediately
|
||||
/// after the desired call has concluded.
|
||||
pub unsafe fn start_ffi(
|
||||
/// until this function returns.
|
||||
pub fn do_ffi<'tcx>(
|
||||
alloc: &Rc<RefCell<IsolatedAlloc>>,
|
||||
) -> (std::sync::MutexGuard<'static, Option<Supervisor>>, Option<*mut [u8; CALLBACK_STACK_SIZE]>)
|
||||
{
|
||||
f: impl FnOnce() -> InterpResult<'tcx, crate::ImmTy<'tcx>>,
|
||||
) -> InterpResult<'tcx, (crate::ImmTy<'tcx>, Option<MemEvents>)> {
|
||||
let mut sv_guard = SUPERVISOR.lock().unwrap();
|
||||
// If the supervisor is not initialised for whatever reason, fast-fail.
|
||||
// This might be desired behaviour, as even on platforms where ptracing
|
||||
// is not implemented it enables us to enforce that only one FFI call
|
||||
// If the supervisor is not initialised for whatever reason, fast-return.
|
||||
// As a side-effect, even on platforms where ptracing
|
||||
// is not implemented, we enforce that only one FFI call
|
||||
// happens at a time.
|
||||
let Some(sv) = sv_guard.take() else {
|
||||
return (sv_guard, None);
|
||||
};
|
||||
let Some(sv) = sv_guard.as_mut() else { return f().map(|v| (v, None)) };
|
||||
|
||||
// Get pointers to all the pages the supervisor must allow accesses in
|
||||
// and prepare the callback stack.
|
||||
let page_ptrs = alloc.borrow().pages();
|
||||
let page_ptrs = alloc.borrow().pages().collect();
|
||||
let raw_stack_ptr: *mut [u8; CALLBACK_STACK_SIZE] =
|
||||
Box::leak(Box::new([0u8; CALLBACK_STACK_SIZE])).as_mut_ptr().cast();
|
||||
let stack_ptr = raw_stack_ptr.expose_provenance();
|
||||
|
|
@ -68,9 +69,9 @@ impl Supervisor {
|
|||
// SAFETY: We do not access machine memory past this point until the
|
||||
// supervisor is ready to allow it.
|
||||
unsafe {
|
||||
if alloc.borrow_mut().prepare_ffi().is_err() {
|
||||
if alloc.borrow_mut().start_ffi().is_err() {
|
||||
// Don't mess up unwinding by maybe leaving the memory partly protected
|
||||
alloc.borrow_mut().unprep_ffi();
|
||||
alloc.borrow_mut().end_ffi();
|
||||
panic!("Cannot protect memory for FFI call!");
|
||||
}
|
||||
}
|
||||
|
|
@ -82,27 +83,13 @@ impl Supervisor {
|
|||
// enforce an ordering for these events.
|
||||
sv.message_tx.send(TraceRequest::StartFfi(start_info)).unwrap();
|
||||
sv.confirm_rx.recv().unwrap();
|
||||
*sv_guard = Some(sv);
|
||||
// We need to be stopped for the supervisor to be able to make certain
|
||||
// modifications to our memory - simply waiting on the recv() doesn't
|
||||
// count.
|
||||
signal::raise(signal::SIGSTOP).unwrap();
|
||||
(sv_guard, Some(raw_stack_ptr))
|
||||
}
|
||||
|
||||
/// Undoes FFI-related preparations, allowing Miri to continue as normal, then
|
||||
/// gets the memory accesses and changes performed during the FFI call. Note
|
||||
/// that this may include some spurious accesses done by `libffi` itself in
|
||||
/// the process of executing the function call.
|
||||
///
|
||||
/// SAFETY: The `sv_guard` and `raw_stack_ptr` passed must be the same ones
|
||||
/// received by a prior call to `start_ffi`, and the allocator must be the
|
||||
/// one passed to it also.
|
||||
pub unsafe fn end_ffi(
|
||||
alloc: &Rc<RefCell<IsolatedAlloc>>,
|
||||
mut sv_guard: std::sync::MutexGuard<'static, Option<Supervisor>>,
|
||||
raw_stack_ptr: Option<*mut [u8; CALLBACK_STACK_SIZE]>,
|
||||
) -> Option<MemEvents> {
|
||||
let res = f();
|
||||
|
||||
// We can't use IPC channels here to signal that FFI mode has ended,
|
||||
// since they might allocate memory which could get us stuck in a SIGTRAP
|
||||
// with no easy way out! While this could be worked around, it is much
|
||||
|
|
@ -113,42 +100,40 @@ impl Supervisor {
|
|||
signal::raise(signal::SIGUSR1).unwrap();
|
||||
|
||||
// This is safe! It just sets memory to normal expected permissions.
|
||||
alloc.borrow_mut().unprep_ffi();
|
||||
alloc.borrow_mut().end_ffi();
|
||||
|
||||
// If this is `None`, then `raw_stack_ptr` is None and does not need to
|
||||
// be deallocated (and there's no need to worry about the guard, since
|
||||
// it contains nothing).
|
||||
let sv = sv_guard.take()?;
|
||||
// SAFETY: Caller upholds that this pointer was allocated as a box with
|
||||
// this type.
|
||||
unsafe {
|
||||
drop(Box::from_raw(raw_stack_ptr.unwrap()));
|
||||
drop(Box::from_raw(raw_stack_ptr));
|
||||
}
|
||||
// On the off-chance something really weird happens, don't block forever.
|
||||
let ret = sv
|
||||
let events = sv
|
||||
.event_rx
|
||||
.try_recv_timeout(std::time::Duration::from_secs(5))
|
||||
.map_err(|e| {
|
||||
match e {
|
||||
ipc::TryRecvError::IpcError(_) => (),
|
||||
ipc::TryRecvError::Empty =>
|
||||
eprintln!("Waiting for accesses from supervisor timed out!"),
|
||||
panic!("Waiting for accesses from supervisor timed out!"),
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
// Do *not* leave the supervisor empty, or else we might get another fork...
|
||||
*sv_guard = Some(sv);
|
||||
ret
|
||||
|
||||
res.map(|v| (v, events))
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialises the supervisor process. If this function errors, then the
|
||||
/// supervisor process could not be created successfully; else, the caller
|
||||
/// is now the child process and can communicate via `start_ffi`/`end_ffi`,
|
||||
/// receiving back events through `get_events`.
|
||||
/// is now the child process and can communicate via `do_ffi`, receiving back
|
||||
/// events at the end.
|
||||
///
|
||||
/// # Safety
|
||||
/// The invariants for `fork()` must be upheld by the caller.
|
||||
/// The invariants for `fork()` must be upheld by the caller, namely either:
|
||||
/// - Other threads do not exist, or;
|
||||
/// - If they do exist, either those threads or the resulting child process
|
||||
/// only ever act in [async-signal-safe](https://www.man7.org/linux/man-pages/man7/signal-safety.7.html) ways.
|
||||
pub unsafe fn init_sv() -> Result<(), SvInitError> {
|
||||
// FIXME: Much of this could be reimplemented via the mitosis crate if we upstream the
|
||||
// relevant missing bits.
|
||||
|
|
@ -191,8 +176,7 @@ pub unsafe fn init_sv() -> Result<(), SvInitError> {
|
|||
// The child process is free to unwind, so we won't to avoid doubly freeing
|
||||
// system resources.
|
||||
let init = std::panic::catch_unwind(|| {
|
||||
let listener =
|
||||
ChildListener { message_rx, attached: false, override_retcode: None };
|
||||
let listener = ChildListener::new(message_rx, confirm_tx.clone());
|
||||
// Trace as many things as possible, to be able to handle them as needed.
|
||||
let options = ptrace::Options::PTRACE_O_TRACESYSGOOD
|
||||
| ptrace::Options::PTRACE_O_TRACECLONE
|
||||
|
|
@ -218,7 +202,9 @@ pub unsafe fn init_sv() -> Result<(), SvInitError> {
|
|||
// The "Ok" case means that we couldn't ptrace.
|
||||
Ok(e) => return Err(e),
|
||||
Err(p) => {
|
||||
eprintln!("Supervisor process panicked!\n{p:?}");
|
||||
eprintln!(
|
||||
"Supervisor process panicked!\n{p:?}\n\nTry running again without using the native-lib tracer."
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
|
@ -239,13 +225,11 @@ pub unsafe fn init_sv() -> Result<(), SvInitError> {
|
|||
}
|
||||
|
||||
/// Instruct the supervisor process to return a particular code. Useful if for
|
||||
/// whatever reason this code fails to be intercepted normally. In the case of
|
||||
/// `abort_if_errors()` used in `bin/miri.rs`, the return code is erroneously
|
||||
/// given as a 0 if this is not used.
|
||||
/// whatever reason this code fails to be intercepted normally.
|
||||
pub fn register_retcode_sv(code: i32) {
|
||||
let mut sv_guard = SUPERVISOR.lock().unwrap();
|
||||
if let Some(sv) = sv_guard.take() {
|
||||
if let Some(sv) = sv_guard.as_mut() {
|
||||
sv.message_tx.send(TraceRequest::OverrideRetcode(code)).unwrap();
|
||||
*sv_guard = Some(sv);
|
||||
sv.confirm_rx.recv().unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,28 @@
|
|||
//! Houses the types that are directly sent across the IPC channels.
|
||||
//!
|
||||
//! The overall structure of a traced FFI call, from the child process's POV, is
|
||||
//! as follows:
|
||||
//! When forking to initialise the supervisor during `init_sv`, the child raises
|
||||
//! a `SIGSTOP`; if the parent successfully ptraces the child, it will allow it
|
||||
//! to resume. Else, the child will be killed by the parent.
|
||||
//!
|
||||
//! After initialisation is done, the overall structure of a traced FFI call from
|
||||
//! the child process's POV is as follows:
|
||||
//! ```
|
||||
//! message_tx.send(TraceRequest::StartFfi);
|
||||
//! confirm_rx.recv();
|
||||
//! confirm_rx.recv(); // receives a `Confirmation`
|
||||
//! raise(SIGSTOP);
|
||||
//! /* do ffi call */
|
||||
//! raise(SIGUSR1); // morally equivalent to some kind of "TraceRequest::EndFfi"
|
||||
//! let events = event_rx.recv();
|
||||
//! let events = event_rx.recv(); // receives a `MemEvents`
|
||||
//! ```
|
||||
//! `TraceRequest::OverrideRetcode` can be sent at any point in the above, including
|
||||
//! before or after all of them.
|
||||
//! before or after all of them. `confirm_rx.recv()` is to be called after, to ensure
|
||||
//! that the child does not exit before the supervisor has registered the return code.
|
||||
//!
|
||||
//! NB: sending these events out of order, skipping steps, etc. will result in
|
||||
//! unspecified behaviour from the supervisor process, so use the abstractions
|
||||
//! in `super::child` (namely `start_ffi()` and `end_ffi()`) to handle this. It is
|
||||
//! in `super::child` (namely `do_ffi()`) to handle this. It is
|
||||
//! trivially easy to cause a deadlock or crash by messing this up!
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
/// An IPC request sent by the child process to the parent.
|
||||
///
|
||||
/// The sender for this channel should live on the child process.
|
||||
|
|
@ -34,6 +37,8 @@ pub enum TraceRequest {
|
|||
StartFfi(StartFfiInfo),
|
||||
/// Manually overrides the code that the supervisor will return upon exiting.
|
||||
/// Once set, it is permanent. This can be called again to change the value.
|
||||
///
|
||||
/// After sending this, the child must wait to receive a `Confirmation`.
|
||||
OverrideRetcode(i32),
|
||||
}
|
||||
|
||||
|
|
@ -41,7 +46,7 @@ pub enum TraceRequest {
|
|||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
pub struct StartFfiInfo {
|
||||
/// A vector of page addresses. These should have been automatically obtained
|
||||
/// with `IsolatedAlloc::pages` and prepared with `IsolatedAlloc::prepare_ffi`.
|
||||
/// with `IsolatedAlloc::pages` and prepared with `IsolatedAlloc::start_ffi`.
|
||||
pub page_ptrs: Vec<usize>,
|
||||
/// The address of an allocation that can serve as a temporary stack.
|
||||
/// This should be a leaked `Box<[u8; CALLBACK_STACK_SIZE]>` cast to an int.
|
||||
|
|
@ -54,27 +59,3 @@ pub struct StartFfiInfo {
|
|||
/// The sender for this channel should live on the parent process.
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
pub struct Confirmation;
|
||||
|
||||
/// The final results of an FFI trace, containing every relevant event detected
|
||||
/// by the tracer. Sent by the supervisor after receiving a `SIGUSR1` signal.
|
||||
///
|
||||
/// The sender for this channel should live on the parent process.
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
pub struct MemEvents {
|
||||
/// An ordered list of memory accesses that occurred. These should be assumed
|
||||
/// to be overcautious; that is, if the size of an access is uncertain it is
|
||||
/// pessimistically rounded up, and if the type (read/write/both) is uncertain
|
||||
/// it is reported as whatever would be safest to assume; i.e. a read + maybe-write
|
||||
/// becomes a read + write, etc.
|
||||
pub acc_events: Vec<AccessEvent>,
|
||||
}
|
||||
|
||||
/// A single memory access, conservatively overestimated
|
||||
/// in case of ambiguity.
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
pub enum AccessEvent {
|
||||
/// A read may have occurred on no more than the specified address range.
|
||||
Read(Range<usize>),
|
||||
/// A write may have occurred on no more than the specified address range.
|
||||
Write(Range<usize>),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,4 +5,6 @@ mod parent;
|
|||
pub use self::child::{Supervisor, init_sv, register_retcode_sv};
|
||||
|
||||
/// The size of the temporary stack we use for callbacks that the server executes in the client.
|
||||
/// This should be big enough that `mempr_on` and `mempr_off` can safely be jumped into with the
|
||||
/// stack pointer pointing to a "stack" of this size without overflowing it.
|
||||
const CALLBACK_STACK_SIZE: usize = 1024;
|
||||
|
|
|
|||
|
|
@ -5,26 +5,17 @@ use nix::sys::{ptrace, signal, wait};
|
|||
use nix::unistd;
|
||||
|
||||
use super::CALLBACK_STACK_SIZE;
|
||||
use super::messages::{AccessEvent, Confirmation, MemEvents, StartFfiInfo, TraceRequest};
|
||||
use super::messages::{Confirmation, StartFfiInfo, TraceRequest};
|
||||
use crate::shims::native_lib::{AccessEvent, AccessRange, MemEvents};
|
||||
|
||||
/// The flags to use when calling `waitid()`.
|
||||
/// Since bitwise or on the nix version of these flags is implemented as a trait,
|
||||
/// this cannot be const directly so we do it this way.
|
||||
const WAIT_FLAGS: wait::WaitPidFlag =
|
||||
wait::WaitPidFlag::from_bits_truncate(libc::WUNTRACED | libc::WEXITED);
|
||||
|
||||
/// Arch-specific maximum size a single access might perform. x86 value is set
|
||||
/// assuming nothing bigger than AVX-512 is available.
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
const ARCH_MAX_ACCESS_SIZE: usize = 64;
|
||||
/// The largest arm64 simd instruction operates on 16 bytes.
|
||||
#[cfg(any(target_arch = "arm", target_arch = "aarch64"))]
|
||||
const ARCH_MAX_ACCESS_SIZE: usize = 16;
|
||||
wait::WaitPidFlag::WUNTRACED.union(wait::WaitPidFlag::WEXITED);
|
||||
|
||||
/// The default word size on a given platform, in bytes.
|
||||
#[cfg(any(target_arch = "x86", target_arch = "arm"))]
|
||||
#[cfg(target_arch = "x86")]
|
||||
const ARCH_WORD_SIZE: usize = 4;
|
||||
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
const ARCH_WORD_SIZE: usize = 8;
|
||||
|
||||
/// The address of the page set to be edited, initialised to a sentinel null
|
||||
|
|
@ -53,39 +44,25 @@ trait ArchIndependentRegs {
|
|||
// It's fine / desirable behaviour for values to wrap here, we care about just
|
||||
// preserving the bit pattern.
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
#[expect(clippy::as_conversions)]
|
||||
#[rustfmt::skip]
|
||||
impl ArchIndependentRegs for libc::user_regs_struct {
|
||||
#[inline]
|
||||
fn ip(&self) -> usize { self.rip as _ }
|
||||
fn ip(&self) -> usize { self.rip.try_into().unwrap() }
|
||||
#[inline]
|
||||
fn set_ip(&mut self, ip: usize) { self.rip = ip as _ }
|
||||
fn set_ip(&mut self, ip: usize) { self.rip = ip.try_into().unwrap() }
|
||||
#[inline]
|
||||
fn set_sp(&mut self, sp: usize) { self.rsp = sp as _ }
|
||||
fn set_sp(&mut self, sp: usize) { self.rsp = sp.try_into().unwrap() }
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "x86")]
|
||||
#[expect(clippy::as_conversions)]
|
||||
#[rustfmt::skip]
|
||||
impl ArchIndependentRegs for libc::user_regs_struct {
|
||||
#[inline]
|
||||
fn ip(&self) -> usize { self.eip as _ }
|
||||
fn ip(&self) -> usize { self.eip.try_into().unwrap() }
|
||||
#[inline]
|
||||
fn set_ip(&mut self, ip: usize) { self.eip = ip as _ }
|
||||
fn set_ip(&mut self, ip: usize) { self.eip = ip.try_into().unwrap() }
|
||||
#[inline]
|
||||
fn set_sp(&mut self, sp: usize) { self.esp = sp as _ }
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
#[expect(clippy::as_conversions)]
|
||||
#[rustfmt::skip]
|
||||
impl ArchIndependentRegs for libc::user_regs_struct {
|
||||
#[inline]
|
||||
fn ip(&self) -> usize { self.pc as _ }
|
||||
#[inline]
|
||||
fn set_ip(&mut self, ip: usize) { self.pc = ip as _ }
|
||||
#[inline]
|
||||
fn set_sp(&mut self, sp: usize) { self.sp = sp as _ }
|
||||
fn set_sp(&mut self, sp: usize) { self.esp = sp.try_into().unwrap() }
|
||||
}
|
||||
|
||||
/// A unified event representing something happening on the child process. Wraps
|
||||
|
|
@ -109,11 +86,24 @@ pub enum ExecEvent {
|
|||
/// A listener for the FFI start info channel along with relevant state.
|
||||
pub struct ChildListener {
|
||||
/// The matching channel for the child's `Supervisor` struct.
|
||||
pub message_rx: ipc::IpcReceiver<TraceRequest>,
|
||||
message_rx: ipc::IpcReceiver<TraceRequest>,
|
||||
/// ...
|
||||
confirm_tx: ipc::IpcSender<Confirmation>,
|
||||
/// Whether an FFI call is currently ongoing.
|
||||
pub attached: bool,
|
||||
attached: bool,
|
||||
/// If `Some`, overrides the return code with the given value.
|
||||
pub override_retcode: Option<i32>,
|
||||
override_retcode: Option<i32>,
|
||||
/// Last code obtained from a child exiting.
|
||||
last_code: Option<i32>,
|
||||
}
|
||||
|
||||
impl ChildListener {
|
||||
pub fn new(
|
||||
message_rx: ipc::IpcReceiver<TraceRequest>,
|
||||
confirm_tx: ipc::IpcSender<Confirmation>,
|
||||
) -> Self {
|
||||
Self { message_rx, confirm_tx, attached: false, override_retcode: None, last_code: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for ChildListener {
|
||||
|
|
@ -133,16 +123,10 @@ impl Iterator for ChildListener {
|
|||
Ok(stat) =>
|
||||
match stat {
|
||||
// Child exited normally with a specific code set.
|
||||
wait::WaitStatus::Exited(_, code) => {
|
||||
let code = self.override_retcode.unwrap_or(code);
|
||||
return Some(ExecEvent::Died(Some(code)));
|
||||
}
|
||||
wait::WaitStatus::Exited(_, code) => self.last_code = Some(code),
|
||||
// Child was killed by a signal, without giving a code.
|
||||
wait::WaitStatus::Signaled(_, _, _) =>
|
||||
return Some(ExecEvent::Died(self.override_retcode)),
|
||||
// Child entered a syscall. Since we're always technically
|
||||
// tracing, only pass this along if we're actively
|
||||
// monitoring the child.
|
||||
wait::WaitStatus::Signaled(_, _, _) => self.last_code = None,
|
||||
// Child entered or exited a syscall.
|
||||
wait::WaitStatus::PtraceSyscall(pid) =>
|
||||
if self.attached {
|
||||
return Some(ExecEvent::Syscall(pid));
|
||||
|
|
@ -179,10 +163,8 @@ impl Iterator for ChildListener {
|
|||
},
|
||||
_ => (),
|
||||
},
|
||||
// This case should only trigger if all children died and we
|
||||
// somehow missed that, but it's best we not allow any room
|
||||
// for deadlocks.
|
||||
Err(_) => return Some(ExecEvent::Died(None)),
|
||||
// This case should only trigger when all children died.
|
||||
Err(_) => return Some(ExecEvent::Died(self.override_retcode.or(self.last_code))),
|
||||
}
|
||||
|
||||
// Similarly, do a non-blocking poll of the IPC channel.
|
||||
|
|
@ -196,7 +178,10 @@ impl Iterator for ChildListener {
|
|||
self.attached = true;
|
||||
return Some(ExecEvent::Start(info));
|
||||
},
|
||||
TraceRequest::OverrideRetcode(code) => self.override_retcode = Some(code),
|
||||
TraceRequest::OverrideRetcode(code) => {
|
||||
self.override_retcode = Some(code);
|
||||
self.confirm_tx.send(Confirmation).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -211,6 +196,12 @@ impl Iterator for ChildListener {
|
|||
#[derive(Debug)]
|
||||
pub struct ExecEnd(pub Option<i32>);
|
||||
|
||||
/// Whether to call `ptrace::cont()` immediately. Used exclusively by `wait_for_signal`.
|
||||
enum InitialCont {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
/// This is the main loop of the supervisor process. It runs in a separate
|
||||
/// process from the rest of Miri (but because we fork, addresses for anything
|
||||
/// created before the fork - like statics - are the same).
|
||||
|
|
@ -239,12 +230,12 @@ pub fn sv_loop(
|
|||
let mut curr_pid = init_pid;
|
||||
|
||||
// There's an initial sigstop we need to deal with.
|
||||
wait_for_signal(Some(curr_pid), signal::SIGSTOP, false)?;
|
||||
wait_for_signal(Some(curr_pid), signal::SIGSTOP, InitialCont::No)?;
|
||||
ptrace::cont(curr_pid, None).unwrap();
|
||||
|
||||
for evt in listener {
|
||||
match evt {
|
||||
// start_ffi was called by the child, so prep memory.
|
||||
// Child started ffi, so prep memory.
|
||||
ExecEvent::Start(ch_info) => {
|
||||
// All the pages that the child process is "allowed to" access.
|
||||
ch_pages = ch_info.page_ptrs;
|
||||
|
|
@ -252,17 +243,17 @@ pub fn sv_loop(
|
|||
ch_stack = Some(ch_info.stack_ptr);
|
||||
|
||||
// We received the signal and are no longer in the main listener loop,
|
||||
// so we can let the child move on to the end of start_ffi where it will
|
||||
// so we can let the child move on to the end of the ffi prep where it will
|
||||
// raise a SIGSTOP. We need it to be signal-stopped *and waited for* in
|
||||
// order to do most ptrace operations!
|
||||
confirm_tx.send(Confirmation).unwrap();
|
||||
// We can't trust simply calling `Pid::this()` in the child process to give the right
|
||||
// PID for us, so we get it this way.
|
||||
curr_pid = wait_for_signal(None, signal::SIGSTOP, false).unwrap();
|
||||
curr_pid = wait_for_signal(None, signal::SIGSTOP, InitialCont::No).unwrap();
|
||||
|
||||
ptrace::syscall(curr_pid, None).unwrap();
|
||||
}
|
||||
// end_ffi was called by the child.
|
||||
// Child wants to end tracing.
|
||||
ExecEvent::End => {
|
||||
// Hand over the access info we traced.
|
||||
event_tx.send(MemEvents { acc_events }).unwrap();
|
||||
|
|
@ -322,10 +313,6 @@ fn get_disasm() -> capstone::Capstone {
|
|||
{cs_pre.x86().mode(arch::x86::ArchMode::Mode64)}
|
||||
#[cfg(target_arch = "x86")]
|
||||
{cs_pre.x86().mode(arch::x86::ArchMode::Mode32)}
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
{cs_pre.arm64().mode(arch::arm64::ArchMode::Arm)}
|
||||
#[cfg(target_arch = "arm")]
|
||||
{cs_pre.arm().mode(arch::arm::ArchMode::Arm)}
|
||||
}
|
||||
.detail(true)
|
||||
.build()
|
||||
|
|
@ -339,9 +326,9 @@ fn get_disasm() -> capstone::Capstone {
|
|||
fn wait_for_signal(
|
||||
pid: Option<unistd::Pid>,
|
||||
wait_signal: signal::Signal,
|
||||
init_cont: bool,
|
||||
init_cont: InitialCont,
|
||||
) -> Result<unistd::Pid, ExecEnd> {
|
||||
if init_cont {
|
||||
if matches!(init_cont, InitialCont::Yes) {
|
||||
ptrace::cont(pid.unwrap(), None).unwrap();
|
||||
}
|
||||
// Repeatedly call `waitid` until we get the signal we want, or the process dies.
|
||||
|
|
@ -374,6 +361,74 @@ fn wait_for_signal(
|
|||
}
|
||||
}
|
||||
|
||||
/// Add the memory events from `op` being executed while there is a memory access at `addr` to
|
||||
/// `acc_events`. Return whether this was a memory operand.
|
||||
fn capstone_find_events(
|
||||
addr: usize,
|
||||
op: &capstone::arch::ArchOperand,
|
||||
acc_events: &mut Vec<AccessEvent>,
|
||||
) -> bool {
|
||||
use capstone::prelude::*;
|
||||
match op {
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
arch::ArchOperand::X86Operand(x86_operand) => {
|
||||
match x86_operand.op_type {
|
||||
// We only care about memory accesses
|
||||
arch::x86::X86OperandType::Mem(_) => {
|
||||
let push = AccessRange { addr, size: x86_operand.size.into() };
|
||||
// It's called a "RegAccessType" but it also applies to memory
|
||||
let acc_ty = x86_operand.access.unwrap();
|
||||
// The same instruction might do both reads and writes, so potentially add both.
|
||||
// We do not know the order in which they happened, but writing and then reading
|
||||
// makes little sense so we put the read first. That is also the more
|
||||
// conservative choice.
|
||||
if acc_ty.is_readable() {
|
||||
acc_events.push(AccessEvent::Read(push.clone()));
|
||||
}
|
||||
if acc_ty.is_writable() {
|
||||
acc_events.push(AccessEvent::Write(push));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
// FIXME: arm64
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Extract the events from the given instruction.
|
||||
fn capstone_disassemble(
|
||||
instr: &[u8],
|
||||
addr: usize,
|
||||
cs: &capstone::Capstone,
|
||||
acc_events: &mut Vec<AccessEvent>,
|
||||
) -> capstone::CsResult<()> {
|
||||
// The arch_detail is what we care about, but it relies on these temporaries
|
||||
// that we can't drop. 0x1000 is the default base address for Captsone, and
|
||||
// we're expecting 1 instruction.
|
||||
let insns = cs.disasm_count(instr, 0x1000, 1)?;
|
||||
let ins_detail = cs.insn_detail(&insns[0])?;
|
||||
let arch_detail = ins_detail.arch_detail();
|
||||
|
||||
let mut found_mem_op = false;
|
||||
|
||||
for op in arch_detail.operands() {
|
||||
if capstone_find_events(addr, &op, acc_events) {
|
||||
if found_mem_op {
|
||||
panic!("more than one memory operand found; we don't know which one accessed what");
|
||||
}
|
||||
found_mem_op = true;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Grabs the access that caused a segfault and logs it down if it's to our memory,
|
||||
/// or kills the child and returns the appropriate error otherwise.
|
||||
fn handle_segfault(
|
||||
|
|
@ -384,111 +439,6 @@ fn handle_segfault(
|
|||
cs: &capstone::Capstone,
|
||||
acc_events: &mut Vec<AccessEvent>,
|
||||
) -> Result<(), ExecEnd> {
|
||||
/// This is just here to not pollute the main namespace with `capstone::prelude::*`.
|
||||
#[inline]
|
||||
fn capstone_disassemble(
|
||||
instr: &[u8],
|
||||
addr: usize,
|
||||
cs: &capstone::Capstone,
|
||||
acc_events: &mut Vec<AccessEvent>,
|
||||
) -> capstone::CsResult<()> {
|
||||
use capstone::prelude::*;
|
||||
|
||||
// The arch_detail is what we care about, but it relies on these temporaries
|
||||
// that we can't drop. 0x1000 is the default base address for Captsone, and
|
||||
// we're expecting 1 instruction.
|
||||
let insns = cs.disasm_count(instr, 0x1000, 1)?;
|
||||
let ins_detail = cs.insn_detail(&insns[0])?;
|
||||
let arch_detail = ins_detail.arch_detail();
|
||||
|
||||
for op in arch_detail.operands() {
|
||||
match op {
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
arch::ArchOperand::X86Operand(x86_operand) => {
|
||||
match x86_operand.op_type {
|
||||
// We only care about memory accesses
|
||||
arch::x86::X86OperandType::Mem(_) => {
|
||||
let push = addr..addr.strict_add(usize::from(x86_operand.size));
|
||||
// It's called a "RegAccessType" but it also applies to memory
|
||||
let acc_ty = x86_operand.access.unwrap();
|
||||
if acc_ty.is_readable() {
|
||||
acc_events.push(AccessEvent::Read(push.clone()));
|
||||
}
|
||||
if acc_ty.is_writable() {
|
||||
acc_events.push(AccessEvent::Write(push));
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
arch::ArchOperand::Arm64Operand(arm64_operand) => {
|
||||
// Annoyingly, we don't always get the size here, so just be pessimistic for now.
|
||||
match arm64_operand.op_type {
|
||||
arch::arm64::Arm64OperandType::Mem(_) => {
|
||||
// B = 1 byte, H = 2 bytes, S = 4 bytes, D = 8 bytes, Q = 16 bytes.
|
||||
let size = match arm64_operand.vas {
|
||||
// Not an fp/simd instruction.
|
||||
arch::arm64::Arm64Vas::ARM64_VAS_INVALID => ARCH_WORD_SIZE,
|
||||
// 1 byte.
|
||||
arch::arm64::Arm64Vas::ARM64_VAS_1B => 1,
|
||||
// 2 bytes.
|
||||
arch::arm64::Arm64Vas::ARM64_VAS_1H => 2,
|
||||
// 4 bytes.
|
||||
arch::arm64::Arm64Vas::ARM64_VAS_4B
|
||||
| arch::arm64::Arm64Vas::ARM64_VAS_2H
|
||||
| arch::arm64::Arm64Vas::ARM64_VAS_1S => 4,
|
||||
// 8 bytes.
|
||||
arch::arm64::Arm64Vas::ARM64_VAS_8B
|
||||
| arch::arm64::Arm64Vas::ARM64_VAS_4H
|
||||
| arch::arm64::Arm64Vas::ARM64_VAS_2S
|
||||
| arch::arm64::Arm64Vas::ARM64_VAS_1D => 8,
|
||||
// 16 bytes.
|
||||
arch::arm64::Arm64Vas::ARM64_VAS_16B
|
||||
| arch::arm64::Arm64Vas::ARM64_VAS_8H
|
||||
| arch::arm64::Arm64Vas::ARM64_VAS_4S
|
||||
| arch::arm64::Arm64Vas::ARM64_VAS_2D
|
||||
| arch::arm64::Arm64Vas::ARM64_VAS_1Q => 16,
|
||||
};
|
||||
let push = addr..addr.strict_add(size);
|
||||
// FIXME: This now has access type info in the latest
|
||||
// git version of capstone because this pissed me off
|
||||
// and I added it. Change this when it updates.
|
||||
acc_events.push(AccessEvent::Read(push.clone()));
|
||||
acc_events.push(AccessEvent::Write(push));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "arm")]
|
||||
arch::ArchOperand::ArmOperand(arm_operand) =>
|
||||
match arm_operand.op_type {
|
||||
arch::arm::ArmOperandType::Mem(_) => {
|
||||
// We don't get info on the size of the access, but
|
||||
// we're at least told if it's a vector instruction.
|
||||
let size = if arm_operand.vector_index.is_some() {
|
||||
ARCH_MAX_ACCESS_SIZE
|
||||
} else {
|
||||
ARCH_WORD_SIZE
|
||||
};
|
||||
let push = addr..addr.strict_add(size);
|
||||
let acc_ty = arm_operand.access.unwrap();
|
||||
if acc_ty.is_readable() {
|
||||
acc_events.push(AccessEvent::Read(push.clone()));
|
||||
}
|
||||
if acc_ty.is_writable() {
|
||||
acc_events.push(AccessEvent::Write(push));
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Get information on what caused the segfault. This contains the address
|
||||
// that triggered it.
|
||||
let siginfo = ptrace::getsiginfo(pid).unwrap();
|
||||
|
|
@ -515,7 +465,7 @@ fn handle_segfault(
|
|||
// global atomic variables. This is what we use the temporary callback stack for.
|
||||
// - Step 1 instruction
|
||||
// - Parse executed code to estimate size & type of access
|
||||
// - Reprotect the memory by executing `mempr_on` in the child.
|
||||
// - Reprotect the memory by executing `mempr_on` in the child, using the callback stack again.
|
||||
// - Continue
|
||||
|
||||
// Ensure the stack is properly zeroed out!
|
||||
|
|
@ -552,7 +502,7 @@ fn handle_segfault(
|
|||
ptrace::setregs(pid, new_regs).unwrap();
|
||||
|
||||
// Our mempr_* functions end with a raise(SIGSTOP).
|
||||
wait_for_signal(Some(pid), signal::SIGSTOP, true)?;
|
||||
wait_for_signal(Some(pid), signal::SIGSTOP, InitialCont::Yes)?;
|
||||
|
||||
// Step 1 instruction.
|
||||
ptrace::setregs(pid, regs_bak).unwrap();
|
||||
|
|
@ -573,6 +523,12 @@ fn handle_segfault(
|
|||
let regs_bak = ptrace::getregs(pid).unwrap();
|
||||
new_regs = regs_bak;
|
||||
let ip_poststep = regs_bak.ip();
|
||||
|
||||
// Ensure that we've actually gone forwards.
|
||||
assert!(ip_poststep > ip_prestep);
|
||||
// But not by too much. 64 bytes should be "big enough" on ~any architecture.
|
||||
assert!(ip_prestep.strict_add(64) > ip_poststep);
|
||||
|
||||
// We need to do reads/writes in word-sized chunks.
|
||||
let diff = (ip_poststep.strict_sub(ip_prestep)).div_ceil(ARCH_WORD_SIZE);
|
||||
let instr = (ip_prestep..ip_prestep.strict_add(diff)).fold(vec![], |mut ret, ip| {
|
||||
|
|
@ -587,20 +543,14 @@ fn handle_segfault(
|
|||
});
|
||||
|
||||
// Now figure out the size + type of access and log it down.
|
||||
// This will mark down e.g. the same area being read multiple times,
|
||||
// since it's more efficient to compress the accesses at the end.
|
||||
if capstone_disassemble(&instr, addr, cs, acc_events).is_err() {
|
||||
// Read goes first because we need to be pessimistic.
|
||||
acc_events.push(AccessEvent::Read(addr..addr.strict_add(ARCH_MAX_ACCESS_SIZE)));
|
||||
acc_events.push(AccessEvent::Write(addr..addr.strict_add(ARCH_MAX_ACCESS_SIZE)));
|
||||
}
|
||||
capstone_disassemble(&instr, addr, cs, acc_events).expect("Failed to disassemble instruction");
|
||||
|
||||
// Reprotect everything and continue.
|
||||
#[expect(clippy::as_conversions)]
|
||||
new_regs.set_ip(mempr_on as usize);
|
||||
new_regs.set_sp(stack_ptr);
|
||||
ptrace::setregs(pid, new_regs).unwrap();
|
||||
wait_for_signal(Some(pid), signal::SIGSTOP, true)?;
|
||||
wait_for_signal(Some(pid), signal::SIGSTOP, InitialCont::Yes)?;
|
||||
|
||||
ptrace::setregs(pid, regs_bak).unwrap();
|
||||
ptrace::syscall(pid, None).unwrap();
|
||||
|
|
|
|||
34
src/tools/miri/src/shims/native_lib/trace/stub.rs
Normal file
34
src/tools/miri/src/shims/native_lib/trace/stub.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
use rustc_const_eval::interpret::InterpResult;
|
||||
|
||||
static SUPERVISOR: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
pub struct Supervisor;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SvInitError;
|
||||
|
||||
impl Supervisor {
|
||||
#[inline(always)]
|
||||
pub fn is_enabled() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn do_ffi<'tcx, T>(
|
||||
_: T,
|
||||
f: impl FnOnce() -> InterpResult<'tcx, crate::ImmTy<'tcx>>,
|
||||
) -> InterpResult<'tcx, (crate::ImmTy<'tcx>, Option<super::MemEvents>)> {
|
||||
// We acquire the lock to ensure that no two FFI calls run concurrently.
|
||||
let _g = SUPERVISOR.lock().unwrap();
|
||||
f().map(|v| (v, None))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[allow(dead_code, clippy::missing_safety_doc)]
|
||||
pub unsafe fn init_sv() -> Result<!, SvInitError> {
|
||||
Err(SvInitError)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[allow(dead_code)]
|
||||
pub fn register_retcode_sv<T>(_: T) {}
|
||||
Loading…
Add table
Add a link
Reference in a new issue