diff --git a/Cargo.lock b/Cargo.lock index 192d4f444c..95124ac6fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index a314488bb2..76018efc4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md index 5554c7975f..dead4931f2 100644 --- a/README.md +++ b/README.md @@ -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=` 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 ``, and can be processed diff --git a/src/alloc/isolated_alloc.rs b/src/alloc/isolated_alloc.rs index 3a7879f372..123597ed88 100644 --- a/src/alloc/isolated_alloc.rs +++ b/src/alloc/isolated_alloc.rs @@ -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 { + 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)] diff --git a/src/bin/miri.rs b/src/bin/miri.rs index d4ba7fbd6a..d410d7bcc8 100644 --- a/src/bin/miri.rs +++ b/src/bin/miri.rs @@ -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::() @@ -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), diff --git a/src/diagnostics.rs b/src/diagnostics.rs index 54a7c1407e..095f4ba8c8 100644 --- a/src/diagnostics.rs +++ b/src/diagnostics.rs @@ -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 => { diff --git a/src/eval.rs b/src/eval.rs index 7a5f96ec17..0ad20cefb9 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -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, + /// 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, diff --git a/src/lib.rs b/src/lib.rs index e96c81d5b1..b1c15b006f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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>; pub type StrictPointer = interpret::Pointer; diff --git a/src/shims/mod.rs b/src/shims/mod.rs index b498551ace..e7b0a784c2 100644 --- a/src/shims/mod.rs +++ b/src/shims/mod.rs @@ -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}; diff --git a/src/shims/native_lib.rs b/src/shims/native_lib.rs index 030c2e3672..53d1de060f 100644 --- a/src/shims/native_lib.rs +++ b/src/shims/native_lib.rs @@ -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)>; +#[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>, - ) -> 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::(ptr, libffi_args.as_slice()) }; - Scalar::from_i8(x) + let x = unsafe { do_native_call::(ptr, libffi_args.as_slice(), alloc) }; + maybe_memevents = x.1; + Scalar::from_i8(x.0) } ty::Int(IntTy::I16) => { - let x = unsafe { ffi::call::(ptr, libffi_args.as_slice()) }; - Scalar::from_i16(x) + let x = unsafe { do_native_call::(ptr, libffi_args.as_slice(), alloc) }; + maybe_memevents = x.1; + Scalar::from_i16(x.0) } ty::Int(IntTy::I32) => { - let x = unsafe { ffi::call::(ptr, libffi_args.as_slice()) }; - Scalar::from_i32(x) + let x = unsafe { do_native_call::(ptr, libffi_args.as_slice(), alloc) }; + maybe_memevents = x.1; + Scalar::from_i32(x.0) } ty::Int(IntTy::I64) => { - let x = unsafe { ffi::call::(ptr, libffi_args.as_slice()) }; - Scalar::from_i64(x) + let x = unsafe { do_native_call::(ptr, libffi_args.as_slice(), alloc) }; + maybe_memevents = x.1; + Scalar::from_i64(x.0) } ty::Int(IntTy::Isize) => { - let x = unsafe { ffi::call::(ptr, libffi_args.as_slice()) }; - Scalar::from_target_isize(x.try_into().unwrap(), this) + let x = unsafe { do_native_call::(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::(ptr, libffi_args.as_slice()) }; - Scalar::from_u8(x) + let x = unsafe { do_native_call::(ptr, libffi_args.as_slice(), alloc) }; + maybe_memevents = x.1; + Scalar::from_u8(x.0) } ty::Uint(UintTy::U16) => { - let x = unsafe { ffi::call::(ptr, libffi_args.as_slice()) }; - Scalar::from_u16(x) + let x = unsafe { do_native_call::(ptr, libffi_args.as_slice(), alloc) }; + maybe_memevents = x.1; + Scalar::from_u16(x.0) } ty::Uint(UintTy::U32) => { - let x = unsafe { ffi::call::(ptr, libffi_args.as_slice()) }; - Scalar::from_u32(x) + let x = unsafe { do_native_call::(ptr, libffi_args.as_slice(), alloc) }; + maybe_memevents = x.1; + Scalar::from_u32(x.0) } ty::Uint(UintTy::U64) => { - let x = unsafe { ffi::call::(ptr, libffi_args.as_slice()) }; - Scalar::from_u64(x) + let x = unsafe { do_native_call::(ptr, libffi_args.as_slice(), alloc) }; + maybe_memevents = x.1; + Scalar::from_u64(x.0) } ty::Uint(UintTy::Usize) => { - let x = unsafe { ffi::call::(ptr, libffi_args.as_slice()) }; - Scalar::from_target_usize(x.try_into().unwrap(), this) + let x = unsafe { do_native_call::(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::>>(); // 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( + ptr: CodePtr, + args: &[ffi::Arg<'_>], + alloc: Option>>, +) -> (T, Option) { + 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( + 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 diff --git a/src/shims/trace/child.rs b/src/shims/trace/child.rs new file mode 100644 index 0000000000..dcfdaad748 --- /dev/null +++ b/src/shims/trace/child.rs @@ -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> = 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, + /// Used for synchronisation, allowing us to receive confirmation that the + /// parent process has handled the request from `message_tx`. + confirm_rx: ipc::IpcReceiver, + /// Receiver for memory acceses that ocurred during the FFI call. + event_rx: ipc::IpcReceiver, +} + +/// 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>, + ) -> (std::sync::MutexGuard<'static, Option>, 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>, + _alloc: Rc>, + raw_stack_ptr: Option<*mut [u8; FAKE_STACK_SIZE]>, + ) -> Option { + // 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); + } +} diff --git a/src/shims/trace/messages.rs b/src/shims/trace/messages.rs new file mode 100644 index 0000000000..1014ca750c --- /dev/null +++ b/src/shims/trace/messages.rs @@ -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, +} diff --git a/src/shims/trace/mod.rs b/src/shims/trace/mod.rs new file mode 100644 index 0000000000..a073482044 --- /dev/null +++ b/src/shims/trace/mod.rs @@ -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, + /// 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), + /// A write may have occurred on no more than the specified address range. + Write(Range), +} diff --git a/src/shims/trace/parent.rs b/src/shims/trace/parent.rs new file mode 100644 index 0000000000..5eb8aa8465 --- /dev/null +++ b/src/shims/trace/parent.rs @@ -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), +} + +/// 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, + /// Whether an FFI call is currently ongoing. + pub attached: bool, + /// If `Some`, overrides the return code with the given value. + pub override_retcode: Option, +} + +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 { + // 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), +} + +/// 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, + confirm_tx: ipc::IpcSender, + _page_size: usize, +) -> Result> { + // 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, + wait_signal: signal::Signal, + init_cont: bool, +) -> Result { + 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))?; + } + } +} diff --git a/tests/native-lib/pass/ptr_read_access.stderr b/tests/native-lib/pass/ptr_read_access.stderr index 04a3025bae..e256796e7a 100644 --- a/tests/native-lib/pass/ptr_read_access.stderr +++ b/tests/native-lib/pass/ptr_read_access.stderr @@ -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: diff --git a/tests/native-lib/pass/ptr_write_access.stderr b/tests/native-lib/pass/ptr_write_access.stderr index c893b0d9f6..53cbb05a32 100644 --- a/tests/native-lib/pass/ptr_write_access.stderr +++ b/tests/native-lib/pass/ptr_write_access.stderr @@ -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: