Merge pull request #4401 from nia-e/barebones-ptrace
trace: add barebones ptrace setup
This commit is contained in:
commit
e857c655ea
16 changed files with 938 additions and 39 deletions
|
|
@ -80,6 +80,15 @@ dependencies = [
|
|||
"rustc-demangle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.0"
|
||||
|
|
@ -150,6 +159,12 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.40"
|
||||
|
|
@ -224,7 +239,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -243,7 +258,7 @@ dependencies = [
|
|||
"libc",
|
||||
"once_cell",
|
||||
"unicode-width 0.2.0",
|
||||
"windows-sys",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -298,7 +313,7 @@ dependencies = [
|
|||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -314,7 +329,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -333,6 +348,12 @@ version = "2.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
|
|
@ -400,6 +421,25 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipc-channel"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fb8251fb7bcd9ccd3725ed8deae9fe7db8e586495c9eb5b0c52e6233e5e75ea"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"crossbeam-channel",
|
||||
"fnv",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"mio",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"uuid",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
|
|
@ -533,6 +573,18 @@ dependencies = [
|
|||
"adler",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miri"
|
||||
version = "0.1.0"
|
||||
|
|
@ -544,19 +596,34 @@ dependencies = [
|
|||
"colored",
|
||||
"directories",
|
||||
"getrandom 0.3.2",
|
||||
"ipc-channel",
|
||||
"libc",
|
||||
"libffi",
|
||||
"libloading",
|
||||
"measureme",
|
||||
"nix",
|
||||
"rand 0.9.0",
|
||||
"regex",
|
||||
"rustc_version",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"tempfile",
|
||||
"tikv-jemalloc-sys",
|
||||
"ui_test",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
|
|
@ -748,6 +815,8 @@ version = "0.8.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
|
|
@ -757,11 +826,21 @@ version = "0.9.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.3",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
|
|
@ -777,6 +856,9 @@ name = "rand_core"
|
|||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
|
|
@ -879,7 +961,7 @@ dependencies = [
|
|||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -993,7 +1075,7 @@ dependencies = [
|
|||
"getrandom 0.3.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1147,6 +1229,15 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
|
||||
dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
|
|
@ -1241,6 +1332,79 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
|
|
|
|||
|
|
@ -40,6 +40,11 @@ libc = "0.2"
|
|||
libffi = "4.0.0"
|
||||
libloading = "0.8"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
nix = { version = "0.30.1", features = ["mman", "ptrace", "signal"] }
|
||||
ipc-channel = "0.19.0"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
ui_test = "0.29.1"
|
||||
colored = "2"
|
||||
|
|
|
|||
|
|
@ -419,6 +419,11 @@ to Miri failing to detect cases of undefined behavior in a program.
|
|||
Finally, the flag is **unsound** in the sense that Miri stops tracking details such as
|
||||
initialization and provenance on memory shared with native code, so it is easily possible to write
|
||||
code that has UB which is missed by Miri.
|
||||
* `-Zmiri-force-old-native-lib-mode` disables the WIP improved native code access tracking. If for
|
||||
whatever reason enabling native calls leads to odd behaviours or causes Miri to panic, disabling
|
||||
the tracer *might* fix this. This will likely be removed once the tracer has been adequately
|
||||
battle-tested. Note that this flag is only meaningful on Linux systems; other Unixes (currently)
|
||||
exclusively use the old native-lib code.
|
||||
* `-Zmiri-measureme=<name>` enables `measureme` profiling for the interpreted program.
|
||||
This can be used to find which parts of your program are executing slowly under Miri.
|
||||
The profile is written out to a file inside a directory called `<name>`, and can be processed
|
||||
|
|
|
|||
|
|
@ -266,6 +266,18 @@ impl IsolatedAlloc {
|
|||
alloc::dealloc(ptr, layout);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a vector of page addresses managed by the allocator.
|
||||
pub fn pages(&self) -> Vec<usize> {
|
||||
let mut pages: Vec<_> =
|
||||
self.page_ptrs.clone().into_iter().map(|p| p.expose_provenance()).collect();
|
||||
for (ptr, size) in &self.huge_ptrs {
|
||||
for i in 0..size / self.page_size {
|
||||
pages.push(ptr.expose_provenance().strict_add(i * self.page_size));
|
||||
}
|
||||
}
|
||||
pages
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -227,10 +227,11 @@ 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::register_retcode_sv(rustc_driver::EXIT_FAILURE);
|
||||
tcx.dcx().abort_if_errors();
|
||||
rustc_driver::EXIT_FAILURE
|
||||
});
|
||||
|
||||
std::process::exit(return_code);
|
||||
}
|
||||
|
||||
|
|
@ -722,6 +723,8 @@ fn main() {
|
|||
} else {
|
||||
show_error!("-Zmiri-native-lib `{}` does not exist", filename);
|
||||
}
|
||||
} else if arg == "-Zmiri-force-old-native-lib-mode" {
|
||||
miri_config.force_old_native_lib = true;
|
||||
} else if let Some(param) = arg.strip_prefix("-Zmiri-num-cpus=") {
|
||||
let num_cpus = param
|
||||
.parse::<u32>()
|
||||
|
|
@ -792,6 +795,16 @@ 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.force_old_native_lib {
|
||||
// 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::init_sv() };
|
||||
}
|
||||
run_compiler_and_exit(
|
||||
&rustc_args,
|
||||
&mut MiriCompilerCalls::new(miri_config, many_seeds, genmc_config),
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@ pub enum NonHaltingDiagnostic {
|
|||
details: bool,
|
||||
},
|
||||
NativeCallSharedMem,
|
||||
NativeCallNoTrace,
|
||||
WeakMemoryOutdatedLoad {
|
||||
ptr: Pointer,
|
||||
},
|
||||
|
|
@ -629,6 +630,8 @@ impl<'tcx> MiriMachine<'tcx> {
|
|||
Int2Ptr { .. } => ("integer-to-pointer cast".to_string(), DiagLevel::Warning),
|
||||
NativeCallSharedMem =>
|
||||
("sharing memory with a native function".to_string(), DiagLevel::Warning),
|
||||
NativeCallNoTrace =>
|
||||
("unable to trace native code memory accesses".to_string(), DiagLevel::Warning),
|
||||
ExternTypeReborrow =>
|
||||
("reborrow of reference to `extern type`".to_string(), DiagLevel::Warning),
|
||||
CreatedPointerTag(..)
|
||||
|
|
@ -664,6 +667,10 @@ impl<'tcx> MiriMachine<'tcx> {
|
|||
format!("progress report: current operation being executed is here"),
|
||||
Int2Ptr { .. } => format!("integer-to-pointer cast"),
|
||||
NativeCallSharedMem => format!("sharing memory with a native function called via FFI"),
|
||||
NativeCallNoTrace =>
|
||||
format!(
|
||||
"sharing memory with a native function called via FFI, and unable to use ptrace"
|
||||
),
|
||||
WeakMemoryOutdatedLoad { ptr } =>
|
||||
format!("weak memory emulation: outdated value returned from load at {ptr}"),
|
||||
ExternTypeReborrow =>
|
||||
|
|
@ -710,6 +717,22 @@ impl<'tcx> MiriMachine<'tcx> {
|
|||
v
|
||||
}
|
||||
NativeCallSharedMem => {
|
||||
vec![
|
||||
note!(
|
||||
"when memory is shared with a native function call, Miri can only track initialisation and provenance on a best-effort basis"
|
||||
),
|
||||
note!(
|
||||
"in particular, Miri assumes that the native call initializes all memory it has written to"
|
||||
),
|
||||
note!(
|
||||
"Miri also assumes that any part of this memory may be a pointer that is permitted to point to arbitrary exposed memory"
|
||||
),
|
||||
note!(
|
||||
"what this means is that Miri will easily miss Undefined Behavior related to incorrect usage of this shared memory, so you should not take a clean Miri run as a signal that your FFI code is UB-free"
|
||||
),
|
||||
]
|
||||
}
|
||||
NativeCallNoTrace => {
|
||||
vec![
|
||||
note!(
|
||||
"when memory is shared with a native function call, Miri stops tracking initialization and provenance for that memory"
|
||||
|
|
@ -723,6 +746,10 @@ impl<'tcx> MiriMachine<'tcx> {
|
|||
note!(
|
||||
"what this means is that Miri will easily miss Undefined Behavior related to incorrect usage of this shared memory, so you should not take a clean Miri run as a signal that your FFI code is UB-free"
|
||||
),
|
||||
#[cfg(target_os = "linux")]
|
||||
note!(
|
||||
"this is normally partially mitigated, but either -Zmiri-force-old-native-lib-mode was passed or ptrace is disabled on your system"
|
||||
),
|
||||
]
|
||||
}
|
||||
ExternTypeReborrow => {
|
||||
|
|
|
|||
|
|
@ -150,6 +150,8 @@ pub struct MiriConfig {
|
|||
pub retag_fields: RetagFields,
|
||||
/// The location of the shared object files to load when calling external functions
|
||||
pub native_lib: Vec<PathBuf>,
|
||||
/// Whether to force using the old native lib behaviour even if ptrace might be supported.
|
||||
pub force_old_native_lib: bool,
|
||||
/// Run a garbage collector for BorTags every N basic blocks.
|
||||
pub gc_interval: u32,
|
||||
/// The number of CPUs to be reported by miri.
|
||||
|
|
@ -199,6 +201,7 @@ impl Default for MiriConfig {
|
|||
report_progress: None,
|
||||
retag_fields: RetagFields::Yes,
|
||||
native_lib: vec![],
|
||||
force_old_native_lib: false,
|
||||
gc_interval: 10_000,
|
||||
num_cpus: 1,
|
||||
page_size: None,
|
||||
|
|
|
|||
|
|
@ -99,6 +99,9 @@ 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 use crate::shims::trace::{init_sv, register_retcode_sv};
|
||||
|
||||
// Type aliases that set the provenance parameter.
|
||||
pub type Pointer = interpret::Pointer<Option<machine::Provenance>>;
|
||||
pub type StrictPointer = interpret::Pointer<machine::Provenance>;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ pub mod os_str;
|
|||
pub mod panic;
|
||||
pub mod time;
|
||||
pub mod tls;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod trace;
|
||||
|
||||
pub use self::files::FdTable;
|
||||
pub use self::unix::{DirTable, EpollInterestTable};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
//! Implements calling functions from a native library.
|
||||
use std::ops::Deref;
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use libffi::high::call as ffi;
|
||||
use libffi::low::CodePtr;
|
||||
|
|
@ -8,8 +10,16 @@ use rustc_middle::mir::interpret::Pointer;
|
|||
use rustc_middle::ty::{self as ty, IntTy, UintTy};
|
||||
use rustc_span::Symbol;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::alloc::isolated_alloc::IsolatedAlloc;
|
||||
use crate::*;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
type CallResult<'tcx> =
|
||||
InterpResult<'tcx, (ImmTy<'tcx>, Option<shims::trace::messages::MemEvents>)>;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
type CallResult<'tcx> = InterpResult<'tcx, (ImmTy<'tcx>, Option<!>)>;
|
||||
|
||||
impl<'tcx> EvalContextExtPriv<'tcx> for crate::MiriInterpCx<'tcx> {}
|
||||
trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
||||
/// Call native host function and return the output as an immediate.
|
||||
|
|
@ -19,8 +29,13 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
dest: &MPlaceTy<'tcx>,
|
||||
ptr: CodePtr,
|
||||
libffi_args: Vec<libffi::high::Arg<'a>>,
|
||||
) -> InterpResult<'tcx, ImmTy<'tcx>> {
|
||||
) -> CallResult<'tcx> {
|
||||
let this = self.eval_context_mut();
|
||||
#[cfg(target_os = "linux")]
|
||||
let alloc = this.machine.allocator.clone();
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let alloc = ();
|
||||
let maybe_memevents;
|
||||
|
||||
// Call the function (`ptr`) with arguments `libffi_args`, and obtain the return value
|
||||
// as the specified primitive integer type
|
||||
|
|
@ -30,60 +45,71 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
// Unsafe because of the call to native code.
|
||||
// Because this is calling a C function it is not necessarily sound,
|
||||
// but there is no way around this and we've checked as much as we can.
|
||||
let x = unsafe { ffi::call::<i8>(ptr, libffi_args.as_slice()) };
|
||||
Scalar::from_i8(x)
|
||||
let x = unsafe { do_native_call::<i8>(ptr, libffi_args.as_slice(), alloc) };
|
||||
maybe_memevents = x.1;
|
||||
Scalar::from_i8(x.0)
|
||||
}
|
||||
ty::Int(IntTy::I16) => {
|
||||
let x = unsafe { ffi::call::<i16>(ptr, libffi_args.as_slice()) };
|
||||
Scalar::from_i16(x)
|
||||
let x = unsafe { do_native_call::<i16>(ptr, libffi_args.as_slice(), alloc) };
|
||||
maybe_memevents = x.1;
|
||||
Scalar::from_i16(x.0)
|
||||
}
|
||||
ty::Int(IntTy::I32) => {
|
||||
let x = unsafe { ffi::call::<i32>(ptr, libffi_args.as_slice()) };
|
||||
Scalar::from_i32(x)
|
||||
let x = unsafe { do_native_call::<i32>(ptr, libffi_args.as_slice(), alloc) };
|
||||
maybe_memevents = x.1;
|
||||
Scalar::from_i32(x.0)
|
||||
}
|
||||
ty::Int(IntTy::I64) => {
|
||||
let x = unsafe { ffi::call::<i64>(ptr, libffi_args.as_slice()) };
|
||||
Scalar::from_i64(x)
|
||||
let x = unsafe { do_native_call::<i64>(ptr, libffi_args.as_slice(), alloc) };
|
||||
maybe_memevents = x.1;
|
||||
Scalar::from_i64(x.0)
|
||||
}
|
||||
ty::Int(IntTy::Isize) => {
|
||||
let x = unsafe { ffi::call::<isize>(ptr, libffi_args.as_slice()) };
|
||||
Scalar::from_target_isize(x.try_into().unwrap(), this)
|
||||
let x = unsafe { do_native_call::<isize>(ptr, libffi_args.as_slice(), alloc) };
|
||||
maybe_memevents = x.1;
|
||||
Scalar::from_target_isize(x.0.try_into().unwrap(), this)
|
||||
}
|
||||
// uints
|
||||
ty::Uint(UintTy::U8) => {
|
||||
let x = unsafe { ffi::call::<u8>(ptr, libffi_args.as_slice()) };
|
||||
Scalar::from_u8(x)
|
||||
let x = unsafe { do_native_call::<u8>(ptr, libffi_args.as_slice(), alloc) };
|
||||
maybe_memevents = x.1;
|
||||
Scalar::from_u8(x.0)
|
||||
}
|
||||
ty::Uint(UintTy::U16) => {
|
||||
let x = unsafe { ffi::call::<u16>(ptr, libffi_args.as_slice()) };
|
||||
Scalar::from_u16(x)
|
||||
let x = unsafe { do_native_call::<u16>(ptr, libffi_args.as_slice(), alloc) };
|
||||
maybe_memevents = x.1;
|
||||
Scalar::from_u16(x.0)
|
||||
}
|
||||
ty::Uint(UintTy::U32) => {
|
||||
let x = unsafe { ffi::call::<u32>(ptr, libffi_args.as_slice()) };
|
||||
Scalar::from_u32(x)
|
||||
let x = unsafe { do_native_call::<u32>(ptr, libffi_args.as_slice(), alloc) };
|
||||
maybe_memevents = x.1;
|
||||
Scalar::from_u32(x.0)
|
||||
}
|
||||
ty::Uint(UintTy::U64) => {
|
||||
let x = unsafe { ffi::call::<u64>(ptr, libffi_args.as_slice()) };
|
||||
Scalar::from_u64(x)
|
||||
let x = unsafe { do_native_call::<u64>(ptr, libffi_args.as_slice(), alloc) };
|
||||
maybe_memevents = x.1;
|
||||
Scalar::from_u64(x.0)
|
||||
}
|
||||
ty::Uint(UintTy::Usize) => {
|
||||
let x = unsafe { ffi::call::<usize>(ptr, libffi_args.as_slice()) };
|
||||
Scalar::from_target_usize(x.try_into().unwrap(), this)
|
||||
let x = unsafe { do_native_call::<usize>(ptr, libffi_args.as_slice(), alloc) };
|
||||
maybe_memevents = x.1;
|
||||
Scalar::from_target_usize(x.0.try_into().unwrap(), this)
|
||||
}
|
||||
// Functions with no declared return type (i.e., the default return)
|
||||
// have the output_type `Tuple([])`.
|
||||
ty::Tuple(t_list) if (*t_list).deref().is_empty() => {
|
||||
unsafe { ffi::call::<()>(ptr, libffi_args.as_slice()) };
|
||||
return interp_ok(ImmTy::uninit(dest.layout));
|
||||
let (_, mm) = unsafe { do_native_call::<()>(ptr, libffi_args.as_slice(), alloc) };
|
||||
return interp_ok((ImmTy::uninit(dest.layout), mm));
|
||||
}
|
||||
ty::RawPtr(..) => {
|
||||
let x = unsafe { ffi::call::<*const ()>(ptr, libffi_args.as_slice()) };
|
||||
let ptr = Pointer::new(Provenance::Wildcard, Size::from_bytes(x.addr()));
|
||||
let x = unsafe { do_native_call::<*const ()>(ptr, libffi_args.as_slice(), alloc) };
|
||||
maybe_memevents = x.1;
|
||||
let ptr = Pointer::new(Provenance::Wildcard, Size::from_bytes(x.0.addr()));
|
||||
Scalar::from_pointer(ptr, this)
|
||||
}
|
||||
_ => throw_unsup_format!("unsupported return type for native call: {:?}", link_name),
|
||||
};
|
||||
interp_ok(ImmTy::from_scalar(scalar, dest.layout))
|
||||
interp_ok((ImmTy::from_scalar(scalar, dest.layout), maybe_memevents))
|
||||
}
|
||||
|
||||
/// Get the pointer to the function of the specified name in the shared object file,
|
||||
|
|
@ -179,6 +205,13 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
// The first time this happens, print a warning.
|
||||
if !this.machine.native_call_mem_warned.replace(true) {
|
||||
// Newly set, so first time we get here.
|
||||
#[cfg(target_os = "linux")]
|
||||
if shims::trace::Supervisor::poll() {
|
||||
this.emit_diagnostic(NonHaltingDiagnostic::NativeCallSharedMem);
|
||||
} else {
|
||||
this.emit_diagnostic(NonHaltingDiagnostic::NativeCallNoTrace);
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
this.emit_diagnostic(NonHaltingDiagnostic::NativeCallSharedMem);
|
||||
}
|
||||
|
||||
|
|
@ -196,12 +229,55 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
.collect::<Vec<libffi::high::Arg<'_>>>();
|
||||
|
||||
// Call the function and store output, depending on return type in the function signature.
|
||||
let ret = this.call_native_with_args(link_name, dest, code_ptr, libffi_args)?;
|
||||
let (ret, _) = this.call_native_with_args(link_name, dest, code_ptr, libffi_args)?;
|
||||
|
||||
this.write_immediate(*ret, dest)?;
|
||||
interp_ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs the actual native call, returning the result and the events that
|
||||
/// the supervisor detected (if any).
|
||||
///
|
||||
/// SAFETY: See `libffi::fii::call`.
|
||||
#[cfg(target_os = "linux")]
|
||||
unsafe fn do_native_call<T: libffi::high::CType>(
|
||||
ptr: CodePtr,
|
||||
args: &[ffi::Arg<'_>],
|
||||
alloc: Option<Rc<RefCell<IsolatedAlloc>>>,
|
||||
) -> (T, Option<shims::trace::messages::MemEvents>) {
|
||||
use shims::trace::Supervisor;
|
||||
|
||||
unsafe {
|
||||
if let Some(alloc) = alloc {
|
||||
// SAFETY: We don't touch the machine memory past this point
|
||||
let (guard, stack_ptr) = Supervisor::start_ffi(alloc.clone());
|
||||
// SAFETY: Upheld by caller
|
||||
let ret = ffi::call(ptr, args);
|
||||
// SAFETY: We got the guard and stack pointer from start_ffi, and
|
||||
// the allocator is the same
|
||||
(ret, Supervisor::end_ffi(guard, alloc, stack_ptr))
|
||||
} else {
|
||||
// SAFETY: Upheld by caller
|
||||
(ffi::call(ptr, args), None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs the actual native call, returning the result and a `None`.
|
||||
/// Placeholder for platforms that do not support the ptrace supervisor.
|
||||
///
|
||||
/// SAFETY: See `libffi::fii::call`.
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[inline(always)]
|
||||
unsafe fn do_native_call<T: libffi::high::CType>(
|
||||
ptr: CodePtr,
|
||||
args: &[ffi::Arg<'_>],
|
||||
_alloc: (),
|
||||
) -> (T, Option<!>) {
|
||||
(unsafe { ffi::call(ptr, args) }, None)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Enum of supported arguments to external C functions.
|
||||
// We introduce this enum instead of just calling `ffi::arg` and storing a list
|
||||
|
|
|
|||
238
src/tools/miri/src/shims/trace/child.rs
Normal file
238
src/tools/miri/src/shims/trace/child.rs
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use ipc_channel::ipc;
|
||||
use nix::sys::{ptrace, signal};
|
||||
use nix::unistd;
|
||||
|
||||
use super::messages::{Confirmation, MemEvents, TraceRequest};
|
||||
use super::parent::{ChildListener, sv_loop};
|
||||
use super::{FAKE_STACK_SIZE, StartFfiInfo};
|
||||
use crate::alloc::isolated_alloc::IsolatedAlloc;
|
||||
|
||||
static SUPERVISOR: std::sync::Mutex<Option<Supervisor>> = std::sync::Mutex::new(None);
|
||||
|
||||
/// The main means of communication between the child and parent process,
|
||||
/// allowing the former to send requests and get info from the latter.
|
||||
pub struct Supervisor {
|
||||
/// Sender for FFI-mode-related requests.
|
||||
message_tx: ipc::IpcSender<TraceRequest>,
|
||||
/// Used for synchronisation, allowing us to receive confirmation that the
|
||||
/// parent process has handled the request from `message_tx`.
|
||||
confirm_rx: ipc::IpcReceiver<Confirmation>,
|
||||
/// Receiver for memory acceses that ocurred during the FFI call.
|
||||
event_rx: ipc::IpcReceiver<MemEvents>,
|
||||
}
|
||||
|
||||
/// Marker representing that an error occurred during creation of the supervisor.
|
||||
#[derive(Debug)]
|
||||
pub struct SvInitError;
|
||||
|
||||
impl Supervisor {
|
||||
/// Returns `true` if the supervisor process exists, and `false` otherwise.
|
||||
pub fn poll() -> bool {
|
||||
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.
|
||||
///
|
||||
/// 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(
|
||||
alloc: Rc<RefCell<IsolatedAlloc>>,
|
||||
) -> (std::sync::MutexGuard<'static, Option<Supervisor>>, Option<*mut [u8; FAKE_STACK_SIZE]>)
|
||||
{
|
||||
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
|
||||
// happens at a time
|
||||
let Some(sv) = sv_guard.take() else {
|
||||
return (sv_guard, None);
|
||||
};
|
||||
|
||||
// Get pointers to all the pages the supervisor must allow accesses in
|
||||
// and prepare the fake stack
|
||||
let page_ptrs = alloc.borrow().pages();
|
||||
let raw_stack_ptr: *mut [u8; FAKE_STACK_SIZE] =
|
||||
Box::leak(Box::new([0u8; FAKE_STACK_SIZE])).as_mut_ptr().cast();
|
||||
let stack_ptr = raw_stack_ptr.expose_provenance();
|
||||
let start_info = StartFfiInfo { page_ptrs, stack_ptr };
|
||||
|
||||
// Send over the info.
|
||||
// NB: if we do not wait to receive a blank confirmation response, it is
|
||||
// possible that the supervisor is alerted of the SIGSTOP *before* it has
|
||||
// actually received the start_info, thus deadlocking! This way, we can
|
||||
// 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(
|
||||
mut sv_guard: std::sync::MutexGuard<'static, Option<Supervisor>>,
|
||||
_alloc: Rc<RefCell<IsolatedAlloc>>,
|
||||
raw_stack_ptr: Option<*mut [u8; FAKE_STACK_SIZE]>,
|
||||
) -> Option<MemEvents> {
|
||||
// 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
|
||||
// simpler and more robust to simply use the signals which are left for
|
||||
// arbitrary usage. Since this will block until we are continued by the
|
||||
// supervisor, we can assume past this point that everything is back to
|
||||
// normal
|
||||
signal::raise(signal::SIGUSR1).unwrap();
|
||||
|
||||
// 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()));
|
||||
}
|
||||
// On the off-chance something really weird happens, don't block forever
|
||||
let ret = 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!"),
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
// Do *not* leave the supervisor empty, or else we might get another fork...
|
||||
*sv_guard = Some(sv);
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
/// 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`.
|
||||
///
|
||||
/// # Safety
|
||||
/// The invariants for `fork()` must be upheld by the caller.
|
||||
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
|
||||
|
||||
// On Linux, this will check whether ptrace is fully disabled by the Yama module.
|
||||
// If Yama isn't running or we're not on Linux, we'll still error later, but
|
||||
// this saves a very expensive fork call
|
||||
let ptrace_status = std::fs::read_to_string("/proc/sys/kernel/yama/ptrace_scope");
|
||||
if let Ok(stat) = ptrace_status {
|
||||
if let Some(stat) = stat.chars().next() {
|
||||
// Fast-error if ptrace is fully disabled on the system
|
||||
if stat == '3' {
|
||||
return Err(SvInitError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialise the supervisor if it isn't already, placing it into SUPERVISOR
|
||||
let mut lock = SUPERVISOR.lock().unwrap();
|
||||
if lock.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Prepare the IPC channels we need
|
||||
let (message_tx, message_rx) = ipc::channel().unwrap();
|
||||
let (confirm_tx, confirm_rx) = ipc::channel().unwrap();
|
||||
let (event_tx, event_rx) = ipc::channel().unwrap();
|
||||
// SAFETY: Calling sysconf(_SC_PAGESIZE) is always safe and cannot error
|
||||
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }.try_into().unwrap();
|
||||
|
||||
unsafe {
|
||||
// TODO: Maybe use clone3() instead for better signalling of when the child exits?
|
||||
// SAFETY: Caller upholds that only one thread exists.
|
||||
match unistd::fork().unwrap() {
|
||||
unistd::ForkResult::Parent { child } => {
|
||||
// If somehow another thread does exist, prevent it from accessing the lock
|
||||
// and thus breaking our safety invariants
|
||||
std::mem::forget(lock);
|
||||
// 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 };
|
||||
// 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
|
||||
| ptrace::Options::PTRACE_O_TRACEFORK;
|
||||
// Attach to the child process without stopping it
|
||||
match ptrace::seize(child, options) {
|
||||
// Ptrace works :D
|
||||
Ok(_) => {
|
||||
let code = sv_loop(listener, child, event_tx, confirm_tx, page_size)
|
||||
.unwrap_err();
|
||||
// If a return code of 0 is not explicitly given, assume something went
|
||||
// wrong and return 1
|
||||
std::process::exit(code.unwrap_or(1))
|
||||
}
|
||||
// Ptrace does not work and we failed to catch that
|
||||
Err(_) => {
|
||||
// If we can't ptrace, Miri continues being the parent
|
||||
signal::kill(child, signal::SIGKILL).unwrap();
|
||||
SvInitError
|
||||
}
|
||||
}
|
||||
});
|
||||
match init {
|
||||
// The "Ok" case means that we couldn't ptrace
|
||||
Ok(e) => return Err(e),
|
||||
Err(p) => {
|
||||
eprintln!("Supervisor process panicked!\n{p:?}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
unistd::ForkResult::Child => {
|
||||
// Make sure we never get orphaned and stuck in SIGSTOP or similar
|
||||
// SAFETY: prctl PR_SET_PDEATHSIG is always safe to call
|
||||
let ret = libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM);
|
||||
assert_eq!(ret, 0);
|
||||
// First make sure the parent succeeded with ptracing us!
|
||||
signal::raise(signal::SIGSTOP).unwrap();
|
||||
// If we're the child process, save the supervisor info
|
||||
*lock = Some(Supervisor { message_tx, confirm_rx, event_rx });
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn register_retcode_sv(code: i32) {
|
||||
let mut sv_guard = SUPERVISOR.lock().unwrap();
|
||||
if let Some(sv) = sv_guard.take() {
|
||||
sv.message_tx.send(TraceRequest::OverrideRetcode(code)).unwrap();
|
||||
*sv_guard = Some(sv);
|
||||
}
|
||||
}
|
||||
57
src/tools/miri/src/shims/trace/messages.rs
Normal file
57
src/tools/miri/src/shims/trace/messages.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
//! 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:
|
||||
//! ```
|
||||
//! message_tx.send(TraceRequest::StartFfi);
|
||||
//! confirm_rx.recv();
|
||||
//! raise(SIGSTOP);
|
||||
//! /* do ffi call */
|
||||
//! raise(SIGUSR1); // morally equivalent to some kind of "TraceRequest::EndFfi"
|
||||
//! let events = event_rx.recv();
|
||||
//! ```
|
||||
//! `TraceRequest::OverrideRetcode` can be sent at any point in the above, including
|
||||
//! before or after all of them.
|
||||
//!
|
||||
//! 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
|
||||
//! trivially easy to cause a deadlock or crash by messing this up!
|
||||
|
||||
/// An IPC request sent by the child process to the parent.
|
||||
///
|
||||
/// The sender for this channel should live on the child process.
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
pub(super) enum TraceRequest {
|
||||
/// Requests that tracing begins. Following this being sent, the child must
|
||||
/// wait to receive a `Confirmation` on the respective channel and then
|
||||
/// `raise(SIGSTOP)`.
|
||||
///
|
||||
/// To avoid possible issues while allocating memory for IPC channels, ending
|
||||
/// the tracing is instead done via `raise(SIGUSR1)`.
|
||||
StartFfi(super::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.
|
||||
OverrideRetcode(i32),
|
||||
}
|
||||
|
||||
/// A marker type confirming that the supervisor has received the request to begin
|
||||
/// tracing and is now waiting for a `SIGSTOP`.
|
||||
///
|
||||
/// The sender for this channel should live on the parent process.
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
pub(super) 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<super::AccessEvent>,
|
||||
}
|
||||
31
src/tools/miri/src/shims/trace/mod.rs
Normal file
31
src/tools/miri/src/shims/trace/mod.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
mod child;
|
||||
pub mod messages;
|
||||
mod parent;
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
pub use self::child::{Supervisor, init_sv, register_retcode_sv};
|
||||
|
||||
/// The size used for the array into which we can move the stack pointer.
|
||||
const FAKE_STACK_SIZE: usize = 1024;
|
||||
|
||||
/// Information needed to begin tracing.
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
struct StartFfiInfo {
|
||||
/// A vector of page addresses. These should have been automatically obtained
|
||||
/// with `IsolatedAlloc::pages` and prepared with `IsolatedAlloc::prepare_ffi`.
|
||||
page_ptrs: Vec<usize>,
|
||||
/// The address of an allocation that can serve as a temporary stack.
|
||||
/// This should be a leaked `Box<[u8; FAKE_STACK_SIZE]>` cast to an int.
|
||||
stack_ptr: usize,
|
||||
}
|
||||
|
||||
/// 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>),
|
||||
}
|
||||
263
src/tools/miri/src/shims/trace/parent.rs
Normal file
263
src/tools/miri/src/shims/trace/parent.rs
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
use ipc_channel::ipc;
|
||||
use nix::sys::{ptrace, signal, wait};
|
||||
use nix::unistd;
|
||||
|
||||
use super::StartFfiInfo;
|
||||
use super::messages::{Confirmation, MemEvents, TraceRequest};
|
||||
|
||||
/// The flags to use when calling `waitid()`.
|
||||
/// Since bitwise OR on the nix version of these flags is implemented as a trait,
|
||||
/// we can't use them directly so we do it this way
|
||||
const WAIT_FLAGS: wait::WaitPidFlag =
|
||||
wait::WaitPidFlag::from_bits_truncate(libc::WUNTRACED | libc::WEXITED);
|
||||
|
||||
/// A unified event representing something happening on the child process. Wraps
|
||||
/// `nix`'s `WaitStatus` and our custom signals so it can all be done with one
|
||||
/// `match` statement.
|
||||
pub enum ExecEvent {
|
||||
/// Child process requests that we begin monitoring it.
|
||||
Start(StartFfiInfo),
|
||||
/// Child requests that we stop monitoring and pass over the events we
|
||||
/// detected.
|
||||
End,
|
||||
/// The child process with the specified pid was stopped by the given signal.
|
||||
Status(unistd::Pid, signal::Signal),
|
||||
/// The child process with the specified pid entered or exited a syscall.
|
||||
Syscall(unistd::Pid),
|
||||
/// A child process exited or was killed; if we have a return code, it is
|
||||
/// specified.
|
||||
Died(Option<i32>),
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
/// Whether an FFI call is currently ongoing.
|
||||
pub attached: bool,
|
||||
/// If `Some`, overrides the return code with the given value.
|
||||
pub override_retcode: Option<i32>,
|
||||
}
|
||||
|
||||
impl Iterator for ChildListener {
|
||||
type Item = ExecEvent;
|
||||
|
||||
// Allows us to monitor the child process by just iterating over the listener
|
||||
// NB: This should never return None!
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
// Do not block if the child has nothing to report for `waitid`
|
||||
let opts = WAIT_FLAGS | wait::WaitPidFlag::WNOHANG;
|
||||
loop {
|
||||
// Listen to any child, not just the main one. Important if we want
|
||||
// to allow the C code to fork further, along with being a bit of
|
||||
// defensive programming since Linux sometimes assigns threads of
|
||||
// the same process different PIDs with unpredictable rules...
|
||||
match wait::waitid(wait::Id::All, opts) {
|
||||
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)));
|
||||
}
|
||||
// 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::PtraceSyscall(pid) =>
|
||||
if self.attached {
|
||||
return Some(ExecEvent::Syscall(pid));
|
||||
},
|
||||
// Child with the given pid was stopped by the given signal.
|
||||
// It's somewhat dubious when this is returned instead of
|
||||
// WaitStatus::Stopped, but for our purposes they are the
|
||||
// same thing.
|
||||
wait::WaitStatus::PtraceEvent(pid, signal, _) =>
|
||||
if self.attached {
|
||||
// This is our end-of-FFI signal!
|
||||
if signal == signal::SIGUSR1 {
|
||||
self.attached = false;
|
||||
return Some(ExecEvent::End);
|
||||
} else {
|
||||
return Some(ExecEvent::Status(pid, signal));
|
||||
}
|
||||
} else {
|
||||
// Just pass along the signal
|
||||
ptrace::cont(pid, signal).unwrap();
|
||||
},
|
||||
// Child was stopped at the given signal. Same logic as for
|
||||
// WaitStatus::PtraceEvent
|
||||
wait::WaitStatus::Stopped(pid, signal) =>
|
||||
if self.attached {
|
||||
if signal == signal::SIGUSR1 {
|
||||
self.attached = false;
|
||||
return Some(ExecEvent::End);
|
||||
} else {
|
||||
return Some(ExecEvent::Status(pid, signal));
|
||||
}
|
||||
} else {
|
||||
ptrace::cont(pid, signal).unwrap();
|
||||
},
|
||||
_ => (),
|
||||
},
|
||||
// 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)),
|
||||
}
|
||||
|
||||
// Similarly, do a non-blocking poll of the IPC channel
|
||||
if let Ok(req) = self.message_rx.try_recv() {
|
||||
match req {
|
||||
TraceRequest::StartFfi(info) =>
|
||||
// Should never trigger - but better to panic explicitly than deadlock!
|
||||
if self.attached {
|
||||
panic!("Attempting to begin FFI multiple times!");
|
||||
} else {
|
||||
self.attached = true;
|
||||
return Some(ExecEvent::Start(info));
|
||||
},
|
||||
TraceRequest::OverrideRetcode(code) => self.override_retcode = Some(code),
|
||||
}
|
||||
}
|
||||
|
||||
// Not ideal, but doing anything else might sacrifice performance
|
||||
std::thread::yield_now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error came up while waiting on the child process to do something.
|
||||
#[derive(Debug)]
|
||||
enum ExecError {
|
||||
/// The child process died with this return code, if we have one.
|
||||
Died(Option<i32>),
|
||||
}
|
||||
|
||||
/// 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).
|
||||
pub fn sv_loop(
|
||||
listener: ChildListener,
|
||||
init_pid: unistd::Pid,
|
||||
event_tx: ipc::IpcSender<MemEvents>,
|
||||
confirm_tx: ipc::IpcSender<Confirmation>,
|
||||
_page_size: usize,
|
||||
) -> Result<!, Option<i32>> {
|
||||
// Things that we return to the child process
|
||||
let mut acc_events = Vec::new();
|
||||
|
||||
// Memory allocated on the MiriMachine
|
||||
let mut _ch_pages = Vec::new();
|
||||
let mut _ch_stack = None;
|
||||
|
||||
// The pid of the last process we interacted with, used by default if we don't have a
|
||||
// reason to use a different one
|
||||
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).map_err(|e| {
|
||||
match e {
|
||||
ExecError::Died(code) => code,
|
||||
}
|
||||
})?;
|
||||
ptrace::cont(curr_pid, None).unwrap();
|
||||
|
||||
for evt in listener {
|
||||
match evt {
|
||||
// start_ffi was called by the child, so prep memory
|
||||
ExecEvent::Start(ch_info) => {
|
||||
// All the pages that the child process is "allowed to" access
|
||||
_ch_pages = ch_info.page_ptrs;
|
||||
// And the fake stack it allocated for us to use later
|
||||
_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
|
||||
// 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();
|
||||
|
||||
ptrace::syscall(curr_pid, None).unwrap();
|
||||
}
|
||||
// end_ffi was called by the child
|
||||
ExecEvent::End => {
|
||||
// Hand over the access info we traced
|
||||
event_tx.send(MemEvents { acc_events }).unwrap();
|
||||
// And reset our values
|
||||
acc_events = Vec::new();
|
||||
_ch_stack = None;
|
||||
|
||||
// No need to monitor syscalls anymore, they'd just be ignored
|
||||
ptrace::cont(curr_pid, None).unwrap();
|
||||
}
|
||||
// Child process was stopped by a signal
|
||||
ExecEvent::Status(pid, signal) => {
|
||||
eprintln!("Process unexpectedly got {signal}; continuing...");
|
||||
// In case we're not tracing
|
||||
if ptrace::syscall(pid, signal).is_err() {
|
||||
// If *this* fails too, something really weird happened
|
||||
// and it's probably best to just panic
|
||||
signal::kill(pid, signal::SIGCONT).unwrap();
|
||||
}
|
||||
}
|
||||
// Child entered a syscall; we wait for exits inside of this, so it
|
||||
// should never trigger on return from a syscall we care about
|
||||
ExecEvent::Syscall(pid) => {
|
||||
ptrace::syscall(pid, None).unwrap();
|
||||
}
|
||||
ExecEvent::Died(code) => {
|
||||
return Err(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
/// Waits for `wait_signal`. If `init_cont`, it will first do a `ptrace::cont`.
|
||||
/// We want to avoid that in some cases, like at the beginning of FFI.
|
||||
///
|
||||
/// If `pid` is `None`, only one wait will be done and `init_cont` should be false.
|
||||
fn wait_for_signal(
|
||||
pid: Option<unistd::Pid>,
|
||||
wait_signal: signal::Signal,
|
||||
init_cont: bool,
|
||||
) -> Result<unistd::Pid, ExecError> {
|
||||
if init_cont {
|
||||
ptrace::cont(pid.unwrap(), None).unwrap();
|
||||
}
|
||||
// Repeatedly call `waitid` until we get the signal we want, or the process dies
|
||||
loop {
|
||||
let wait_id = match pid {
|
||||
Some(pid) => wait::Id::Pid(pid),
|
||||
None => wait::Id::All,
|
||||
};
|
||||
let stat = wait::waitid(wait_id, WAIT_FLAGS).map_err(|_| ExecError::Died(None))?;
|
||||
let (signal, pid) = match stat {
|
||||
// Report the cause of death, if we know it
|
||||
wait::WaitStatus::Exited(_, code) => {
|
||||
return Err(ExecError::Died(Some(code)));
|
||||
}
|
||||
wait::WaitStatus::Signaled(_, _, _) => return Err(ExecError::Died(None)),
|
||||
wait::WaitStatus::Stopped(pid, signal) => (signal, pid),
|
||||
wait::WaitStatus::PtraceEvent(pid, signal, _) => (signal, pid),
|
||||
// This covers PtraceSyscall and variants that are impossible with
|
||||
// the flags set (e.g. WaitStatus::StillAlive)
|
||||
_ => {
|
||||
ptrace::cont(pid.unwrap(), None).unwrap();
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if signal == wait_signal {
|
||||
return Ok(pid);
|
||||
} else {
|
||||
ptrace::cont(pid, None).map_err(|_| ExecError::Died(None))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@ warning: sharing memory with a native function called via FFI
|
|||
LL | unsafe { print_pointer(&x) };
|
||||
| ^^^^^^^^^^^^^^^^^ sharing memory with a native function
|
||||
|
|
||||
= help: when memory is shared with a native function call, Miri stops tracking initialization and provenance for that memory
|
||||
= help: in particular, Miri assumes that the native call initializes all memory it has access to
|
||||
= help: when memory is shared with a native function call, Miri can only track initialisation and provenance on a best-effort basis
|
||||
= help: in particular, Miri assumes that the native call initializes all memory it has written to
|
||||
= help: Miri also assumes that any part of this memory may be a pointer that is permitted to point to arbitrary exposed memory
|
||||
= help: what this means is that Miri will easily miss Undefined Behavior related to incorrect usage of this shared memory, so you should not take a clean Miri run as a signal that your FFI code is UB-free
|
||||
= note: BACKTRACE:
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ warning: sharing memory with a native function called via FFI
|
|||
LL | unsafe { increment_int(&mut x) };
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ sharing memory with a native function
|
||||
|
|
||||
= help: when memory is shared with a native function call, Miri stops tracking initialization and provenance for that memory
|
||||
= help: in particular, Miri assumes that the native call initializes all memory it has access to
|
||||
= help: when memory is shared with a native function call, Miri can only track initialisation and provenance on a best-effort basis
|
||||
= help: in particular, Miri assumes that the native call initializes all memory it has written to
|
||||
= help: Miri also assumes that any part of this memory may be a pointer that is permitted to point to arbitrary exposed memory
|
||||
= help: what this means is that Miri will easily miss Undefined Behavior related to incorrect usage of this shared memory, so you should not take a clean Miri run as a signal that your FFI code is UB-free
|
||||
= note: BACKTRACE:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue