diff --git a/Cargo.lock b/Cargo.lock index 6f4bd3eab5..5ecc541725 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" @@ -112,6 +121,26 @@ dependencies = [ "serde", ] +[[package]] +name = "capstone" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "015ef5d5ca1743e3f94af9509ba6bd2886523cfee46e48d15c2ef5216fd4ac9a" +dependencies = [ + "capstone-sys", + "libc", +] + +[[package]] +name = "capstone-sys" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2267cb8d16a1e4197863ec4284ffd1aec26fe7e57c58af46b02590a0235809a0" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "cargo-platform" version = "0.1.9" @@ -150,6 +179,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 +259,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 +278,7 @@ dependencies = [ "libc", "once_cell", "unicode-width 0.2.0", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -298,7 +333,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -314,7 +349,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 +368,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 +441,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,29 +593,57 @@ 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" dependencies = [ "aes", "bitflags", + "capstone", "chrono", "chrono-tz", "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", - "windows-sys", + "windows-sys 0.59.0", +] + +[[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]] @@ -749,6 +837,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", ] @@ -758,11 +848,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" @@ -778,6 +878,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" @@ -880,7 +983,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -994,7 +1097,7 @@ dependencies = [ "getrandom 0.3.2", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -1148,6 +1251,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" @@ -1242,6 +1354,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 e4d7abdb0f..1cdbf73495 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,12 @@ 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"] } +capstone = "0.13" + [target.'cfg(target_family = "windows")'.dependencies] windows-sys = { version = "0.59", features = [ "Win32_Foundation", diff --git a/README.md b/README.md index 126a8dc473..9895bc16d8 100644 --- a/README.md +++ b/README.md @@ -412,6 +412,11 @@ to Miri failing to detect cases of undefined behavior in a program. memory. 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/no_alloc.json b/no_alloc.json new file mode 100644 index 0000000000..f1736d0d60 --- /dev/null +++ b/no_alloc.json @@ -0,0 +1,46 @@ +{ + "backtraces": { + "mean": 2.4533926632, + "stddev": 0.034804963342590305 + }, + "big-allocs": { + "mean": 0.176591686875, + "stddev": 0.006329187267744716 + }, + "mse": { + "mean": 0.7929701672, + "stddev": 0.026615889537335267 + }, + "range-iteration": { + "mean": 4.2779571954, + "stddev": 0.04795180865203916 + }, + "serde1": { + "mean": 2.6164086034, + "stddev": 0.06291730526924717 + }, + "serde2": { + "mean": 5.798478193, + "stddev": 0.08070440346994077 + }, + "slice-chunked": { + "mean": 0.47576527583333333, + "stddev": 0.01340186535694893 + }, + "slice-get-unchecked": { + "mean": 0.784984716, + "stddev": 0.01936144811041201 + }, + "string-replace": { + "mean": 0.5457706088000001, + "stddev": 0.022212161121800092 + }, + "unicode": { + "mean": 3.4395730980000003, + "stddev": 0.08725374514703972 + }, + "zip-equal": { + "mean": 3.023844411, + "stddev": 0.044728068907096435 + } +} \ No newline at end of file diff --git a/src/alloc/isolated_alloc.rs b/src/alloc/isolated_alloc.rs index 7b74d17137..32cbfdeee7 100644 --- a/src/alloc/isolated_alloc.rs +++ b/src/alloc/isolated_alloc.rs @@ -1,5 +1,6 @@ use std::alloc::{self, Layout}; +use nix::sys::mman; use rustc_index::bit_set::DenseBitSet; /// How many bytes of memory each bit in the bitset represents. @@ -269,6 +270,59 @@ 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(); + self.huge_ptrs.iter().for_each(|(ptr, size)| { + for i in 0..size / self.page_size { + pages.push(unsafe { ptr.add(i * self.page_size).expose_provenance() }); + } + }); + pages + } + + /// Protects all owned memory as `PROT_NONE`, preventing accesses. + /// + /// SAFETY: Accessing memory after this point will result in a segfault + /// unless it is first unprotected. + pub unsafe fn prepare_ffi(&mut self) -> Result<(), nix::errno::Errno> { + let prot = mman::ProtFlags::PROT_NONE; + unsafe { self.mprotect(prot) } + } + + /// Deprotects all owned memory by setting it to RW. Erroring here is very + /// likely unrecoverable, so it may panic if applying those permissions + /// fails. + pub fn unprep_ffi(&mut self) { + let prot = mman::ProtFlags::PROT_READ | mman::ProtFlags::PROT_WRITE; + unsafe { + self.mprotect(prot).unwrap(); + } + } + + /// Applies `prot` to every page managed by the allocator. + /// + /// SAFETY: Accessing memory in violation of the protection flags will + /// trigger a segfault. + unsafe fn mprotect(&mut self, prot: mman::ProtFlags) -> Result<(), nix::errno::Errno> { + for &pg in &self.page_ptrs { + unsafe { + // We already know only non-null ptrs are pushed to self.pages + let addr: std::ptr::NonNull = + std::ptr::NonNull::new_unchecked(pg.cast()); + mman::mprotect(addr, self.page_size, prot)?; + } + } + for &(hpg, size) in &self.huge_ptrs { + unsafe { + let addr = std::ptr::NonNull::new_unchecked(hpg.cast()); + mman::mprotect(addr, size.next_multiple_of(self.page_size), prot)?; + } + } + Ok(()) + } } #[cfg(test)] diff --git a/src/alloc_addresses/mod.rs b/src/alloc_addresses/mod.rs index 4a038fe648..4fde493b33 100644 --- a/src/alloc_addresses/mod.rs +++ b/src/alloc_addresses/mod.rs @@ -470,13 +470,43 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { /// This overapproximates the modifications which external code might make to memory: /// We set all reachable allocations as initialized, mark all reachable provenances as exposed /// and overwrite them with `Provenance::WILDCARD`. - fn prepare_exposed_for_native_call(&mut self) -> InterpResult<'tcx> { + fn prepare_exposed_for_native_call(&mut self, _paranoid: bool) -> InterpResult<'tcx> { let this = self.eval_context_mut(); // We need to make a deep copy of this list, but it's fine; it also serves as scratch space // for the search within `prepare_for_native_call`. let exposed: Vec = this.machine.alloc_addresses.get_mut().exposed.iter().copied().collect(); - this.prepare_for_native_call(exposed) + this.prepare_for_native_call(exposed /*, paranoid*/) + } + + /// Makes use of information obtained about memory accesses during FFI to determine which + /// provenances should be exposed. Note that if `prepare_exposed_for_native_call` was not + /// called before the FFI (with `paranoid` set to false) then some of the writes may be + /// lost! + #[cfg(target_os = "linux")] + fn apply_events(&mut self, events: crate::shims::trace::MemEvents) -> InterpResult<'tcx> { + let this = self.eval_context_mut(); + + let mut reads = vec![]; + let mut writes = vec![]; + for acc in events.acc_events { + match acc { + // Ideally, we'd skip reads that occur after certain bytes were + // already written to. However, these are always just conservative + // overestimates - Read(range) means "a read maybe happened + // spanning at most range" - so we can't make use of this for + // now. Maybe we could also skip over reads/writes that hit the + // same bytes, but that's best added together with the stuff above. + shims::trace::AccessEvent::Read(range) => reads.push(range), + shims::trace::AccessEvent::Write(range) => { + writes.push(range); + } + } + } + let _exposed: Vec = + this.machine.alloc_addresses.get_mut().exposed.iter().copied().collect(); + interp_ok(()) + //this.apply_accesses(exposed, events.reads, events.writes) } } diff --git a/src/bin/miri.rs b/src/bin/miri.rs index f8168853d3..94bb8cc812 100644 --- a/src/bin/miri.rs +++ b/src/bin/miri.rs @@ -192,6 +192,13 @@ impl rustc_driver::Callbacks for MiriCompilerCalls { todo!("GenMC mode not yet implemented"); }; + #[cfg(target_os = "linux")] + if !config.native_lib.is_empty() && !config.force_old_native_lib { + // FIXME: This should display a diagnostic / warning on error + // SAFETY: No other threads have spawned yet + let _ = unsafe { miri::init_sv() }; + } + if let Some(many_seeds) = self.many_seeds.take() { assert!(config.seed.is_none()); let exit_code = sync::IntoDynSyncSend(AtomicI32::new(rustc_driver::EXIT_SUCCESS)); @@ -227,10 +234,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); } @@ -707,6 +715,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::() diff --git a/src/diagnostics.rs b/src/diagnostics.rs index 1728a9cfd6..397edf2417 100644 --- a/src/diagnostics.rs +++ b/src/diagnostics.rs @@ -133,6 +133,7 @@ pub enum NonHaltingDiagnostic { details: bool, }, NativeCallSharedMem, + NativeCallNoTrace, WeakMemoryOutdatedLoad { ptr: Pointer, }, @@ -617,6 +618,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(..) @@ -652,6 +655,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 => @@ -695,6 +702,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" @@ -708,6 +731,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 1477f103ca..26385e5a11 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. @@ -197,6 +199,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 51ec19af52..14fee466b9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,6 +98,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 40440bf6da..2b510c7e9c 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,15 @@ 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 +28,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 +44,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.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, @@ -178,6 +203,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); } @@ -185,8 +217,17 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { } } - // Prepare all exposed memory. - this.prepare_exposed_for_native_call()?; + // Prepare all exposed memory, depending on whether we have a supervisor process. + #[cfg(target_os = "linux")] + if super::trace::Supervisor::poll() { + this.prepare_exposed_for_native_call(false)?; + } else { + //this.prepare_exposed_for_native_call(true)?; + //eprintln!("Oh noes!"); + panic!("No ptrace!"); + } + #[cfg(not(target_os = "linux"))] + this.prepare_exposed_for_native_call(true)?; // Convert them to `libffi::high::Arg` type. let libffi_args = libffi_args @@ -195,12 +236,63 @@ 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, maybe_memevents) = + this.call_native_with_args(link_name, dest, code_ptr, libffi_args)?; + + #[cfg(target_os = "linux")] + if let Some(events) = maybe_memevents { + this.apply_events(events)?; + } + #[cfg(not(target_os = "linux"))] + let _ = maybe_memevents; // Suppress the unused warning + 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..d3834efa7f --- /dev/null +++ b/src/shims/trace/child.rs @@ -0,0 +1,245 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use ipc_channel::ipc; +use nix::sys::{ptrace, signal}; +use nix::unistd; + +use crate::alloc::isolated_alloc::IsolatedAlloc; +use crate::shims::trace::parent::{ChildListener, sv_loop}; +use crate::shims::trace::{FAKE_STACK_SIZE, MemEvents, StartFfiInfo, TraceRequest}; + +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. +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 }; + + // SAFETY: We do not access machine memory past this point until the + // supervisor is ready to allow it + unsafe { + if alloc.borrow_mut().prepare_ffi().is_err() { + // Don't mess up unwinding by maybe leaving the memory partly protected + alloc.borrow_mut().unprep_ffi(); + panic!("Cannot protect memory for FFI call!"); + } + } + // 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(); + + // This is safe! It just sets memory to normal expected permissions + alloc.borrow_mut().unprep_ffi(); + // If this is `None`, then `raw_stack_ptr` is None and does not need to + // be deallocated (and there's no need to worry about the guard, since + // it contains nothing) + let sv = sv_guard.take()?; + // SAFETY: Caller upholds that this pointer was allocated as a box with + // this type + unsafe { + drop(Box::from_raw(raw_stack_ptr.unwrap())); + } + // 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 +/// Only a single OS thread must exist in the process when calling this. +pub unsafe fn init_sv() -> Result<(), SvInitError> { + // 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(); + + super::parent::PAGE_SIZE.store(page_size, std::sync::atomic::Ordering::Relaxed); + 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, + pid: child, + 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, 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 => { + // 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 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/mod.rs b/src/shims/trace/mod.rs new file mode 100644 index 0000000000..1dc25b5cb4 --- /dev/null +++ b/src/shims/trace/mod.rs @@ -0,0 +1,44 @@ +mod child; +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, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +enum TraceRequest { + StartFfi(StartFfiInfo), + OverrideRetcode(i32), +} + +/// 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), +} + +/// The final results of an FFI trace, containing every relevant event detected +/// by the tracer. +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct MemEvents { + /// An ordered list of memory accesses that occurred. + pub acc_events: Vec, +} diff --git a/src/shims/trace/parent.rs b/src/shims/trace/parent.rs new file mode 100644 index 0000000000..24a4744405 --- /dev/null +++ b/src/shims/trace/parent.rs @@ -0,0 +1,692 @@ +use std::sync::atomic::{AtomicPtr, AtomicUsize}; + +use ipc_channel::ipc; +use nix::sys::{ptrace, signal, wait}; +use nix::unistd; + +use crate::shims::trace::{AccessEvent, FAKE_STACK_SIZE, MemEvents, StartFfiInfo, TraceRequest}; + +/// The flags to use when calling `waitid()`. +/// Since bitwise or on the nix version of these flags is implemented as a trait, +/// this cannot be const directly so we do it this way +const WAIT_FLAGS: wait::WaitPidFlag = + wait::WaitPidFlag::from_bits_truncate(libc::WUNTRACED | libc::WEXITED); + +/// Arch-specific maximum size a single access might perform. x86 value is set +/// assuming nothing bigger than AVX-512 is available. +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +const ARCH_MAX_ACCESS_SIZE: usize = 64; +/// The largest arm64 simd instruction operates on 16 bytes. +#[cfg(any(target_arch = "arm", target_arch = "aarch64"))] +const ARCH_MAX_ACCESS_SIZE: usize = 16; +/// The max riscv vector instruction can access 8 consecutive 32-bit values. +#[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))] +const ARCH_MAX_ACCESS_SIZE: usize = 32; + +/// The default word size on a given platform, in bytes. +#[cfg(any(target_arch = "x86", target_arch = "arm", target_arch = "riscv32"))] +const ARCH_WORD_SIZE: usize = 4; +#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "riscv64"))] +const ARCH_WORD_SIZE: usize = 8; + +/// The address of the page set to be edited, initialised to a sentinel null +/// pointer. +static PAGE_ADDR: AtomicPtr = AtomicPtr::new(std::ptr::null_mut()); +/// The host pagesize, initialised to a sentinel zero value. +pub static PAGE_SIZE: AtomicUsize = AtomicUsize::new(0); +/// How many consecutive pages to unprotect. 1 by default, unlikely to be set +/// higher than 2. +static PAGE_COUNT: AtomicUsize = AtomicUsize::new(1); + +/// Allows us to get common arguments from the `user_regs_t` across architectures. +/// Normally this would land us ABI hell, but thankfully all of our usecases +/// consist of functions with a small number of register-sized integer arguments. +/// See for sources +trait ArchIndependentRegs { + /// Gets the address of the instruction pointer. + fn ip(&self) -> usize; + /// Set the instruction pointer; remember to also set the stack pointer, or + /// else the stack might get messed up! + fn set_ip(&mut self, ip: usize); + /// Set the stack pointer, ideally to a zeroed-out area. + fn set_sp(&mut self, sp: usize); +} + +// It's fine / desirable behaviour for values to wrap here, we care about just +// preserving the bit pattern +#[cfg(target_arch = "x86_64")] +#[expect(clippy::as_conversions)] +#[rustfmt::skip] +impl ArchIndependentRegs for libc::user_regs_struct { + #[inline] + fn ip(&self) -> usize { self.rip as _ } + #[inline] + fn set_ip(&mut self, ip: usize) { self.rip = ip as _ } + #[inline] + fn set_sp(&mut self, sp: usize) { self.rsp = sp as _ } +} + +#[cfg(target_arch = "x86")] +#[expect(clippy::as_conversions)] +#[rustfmt::skip] +impl ArchIndependentRegs for libc::user_regs_struct { + #[inline] + fn ip(&self) -> usize { self.eip as _ } + #[inline] + fn set_ip(&mut self, ip: usize) { self.eip = ip as _ } + #[inline] + fn set_sp(&mut self, sp: usize) { self.esp = sp as _ } +} + +#[cfg(target_arch = "aarch64")] +#[expect(clippy::as_conversions)] +#[rustfmt::skip] +impl ArchIndependentRegs for libc::user_regs_struct { + #[inline] + fn ip(&self) -> usize { self.pc as _ } + #[inline] + fn set_ip(&mut self, ip: usize) { self.pc = ip as _ } + #[inline] + fn set_sp(&mut self, sp: usize) { self.sp = sp as _ } +} + +#[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))] +#[expect(clippy::as_conversions)] +#[rustfmt::skip] +impl ArchIndependentRegs for libc::user_regs_struct { + #[inline] + fn ip(&self) -> usize { self.pc as _ } + #[inline] + fn set_ip(&mut self, ip: usize) { self.pc = ip as _ } + #[inline] + fn set_sp(&mut self, sp: usize) { self.sp = sp as _ } +} + +/// 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 existed 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, + /// The main child process' pid. + pub pid: unistd::Pid, + /// 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) => { + //eprintln!("Exited main {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 { + // Log that this happened and pass along the signal. + // If we don't do the kill, the child will instead + // act as if it never received this signal! + eprintln!("Ignoring PtraceEvent {signal:?}"); + signal::kill(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 { + eprintln!("Ignoring Stopped {signal:?}"); + signal::kill(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), + /// Something errored, but we should ignore it and proceed. + Shrug, +} + +/// 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, + 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; + + // An instance of the Capstone disassembler, so we don't spawn one on every access + let cs = get_disasm(); + + // The pid of the process that we forked from, used by default if we don't + // have a reason to use another one. + let main_pid = listener.pid; + + // There's an initial sigstop we need to deal with + wait_for_signal(main_pid, signal::SIGSTOP, false).map_err(|e| { + match e { + ExecError::Died(code) => code, + ExecError::Shrug => None, + } + })?; + ptrace::cont(main_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(()).unwrap(); + wait_for_signal(main_pid, signal::SIGSTOP, false).unwrap(); + + ptrace::syscall(main_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(main_pid, None).unwrap(); + } + // Child process was stopped by a signal + ExecEvent::Status(pid, signal) => + match signal { + // If it was a segfault, check if it was an artificial one + // caused by it trying to access the MiriMachine memory + signal::SIGSEGV => + match handle_segfault( + pid, + &ch_pages, + ch_stack.unwrap(), + page_size, + &cs, + &mut acc_events, + ) { + Err(e) => + match e { + ExecError::Died(code) => return Err(code), + ExecError::Shrug => continue, + }, + _ => (), + }, + // Something weird happened + _ => { + eprintln!("Process unexpectedly got {signal}; continuing..."); + // In case we're not tracing + if ptrace::syscall(pid, None).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!() +} + +/// Spawns a Capstone disassembler for the host architecture. +#[rustfmt::skip] +fn get_disasm() -> capstone::Capstone { + use capstone::prelude::*; + let cs_pre = Capstone::new(); + { + #[cfg(target_arch = "x86_64")] + {cs_pre.x86().mode(arch::x86::ArchMode::Mode64)} + #[cfg(target_arch = "x86")] + {cs_pre.x86().mode(arch::x86::ArchMode::Mode32)} + #[cfg(target_arch = "aarch64")] + {cs_pre.arm64().mode(arch::arm64::ArchMode::Arm)} + #[cfg(target_arch = "arm")] + {cs_pre.arm().mode(arch::arm::ArchMode::Arm)} + #[cfg(target_arch = "riscv64")] + {cs_pre.riscv().mode(arch::riscv::ArchMode::RiscV64)} + #[cfg(target_arch = "riscv32")] + {cs_pre.riscv().mode(arch::riscv::ArchMode::RiscV32)} + } + .detail(true) + .build() + .unwrap() +} + +/// 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. +fn wait_for_signal( + pid: unistd::Pid, + wait_signal: signal::Signal, + init_cont: bool, +) -> Result<(), ExecError> { + if init_cont { + ptrace::cont(pid, None).unwrap(); + } + // Repeatedly call `waitid` until we get the signal we want, or the process dies + loop { + let stat = + wait::waitid(wait::Id::Pid(pid), WAIT_FLAGS).map_err(|_| ExecError::Died(None))?; + let signal = match stat { + // Report the cause of death, if we know it + wait::WaitStatus::Exited(_, code) => { + //eprintln!("Exited sig1 {code}"); + return Err(ExecError::Died(Some(code))); + } + wait::WaitStatus::Signaled(_, _, _) => return Err(ExecError::Died(None)), + wait::WaitStatus::Stopped(_, signal) => signal, + wait::WaitStatus::PtraceEvent(_, signal, _) => signal, + // This covers PtraceSyscall and variants that are impossible with + // the flags set (e.g. WaitStatus::StillAlive) + _ => { + ptrace::cont(pid, None).unwrap(); + continue; + } + }; + if signal == wait_signal { + break; + } else { + ptrace::cont(pid, None).map_err(|_| ExecError::Died(None))?; + } + } + Ok(()) +} + +/// Grabs the access that caused a segfault and logs it down if it's to our memory, +/// or kills the child and returns the appropriate error otherwise. +fn handle_segfault( + pid: unistd::Pid, + ch_pages: &[usize], + ch_stack: usize, + page_size: usize, + cs: &capstone::Capstone, + acc_events: &mut Vec, +) -> Result<(), ExecError> { + /// This is just here to not pollute the main namespace with `capstone::prelude::*`. + #[inline] + fn capstone_disassemble( + instr: &[u8], + addr: usize, + cs: &capstone::Capstone, + acc_events: &mut Vec, + ) -> capstone::CsResult<()> { + use capstone::prelude::*; + + // The arch_detail is what we care about, but it relies on these temporaries + // that we can't drop. 0x1000 is the default base address for Captsone, and + // we're expecting 1 instruction + let insns = cs.disasm_count(instr, 0x1000, 1)?; + let ins_detail = cs.insn_detail(&insns[0])?; + let arch_detail = ins_detail.arch_detail(); + + for op in arch_detail.operands() { + match op { + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + arch::ArchOperand::X86Operand(x86_operand) => { + match x86_operand.op_type { + // We only care about memory accesses + arch::x86::X86OperandType::Mem(_) => { + let push = addr..addr.strict_add(usize::from(x86_operand.size)); + // It's called a "RegAccessType" but it also applies to memory + let acc_ty = x86_operand.access.unwrap(); + if acc_ty.is_readable() { + acc_events.push(AccessEvent::Read(push.clone())); + } + if acc_ty.is_writable() { + acc_events.push(AccessEvent::Write(push)); + } + } + _ => (), + } + } + #[cfg(target_arch = "aarch64")] + arch::ArchOperand::Arm64Operand(arm64_operand) => { + // Annoyingly, we don't always get the size here, so just be pessimistic for now + match arm64_operand.op_type { + arch::arm64::Arm64OperandType::Mem(_) => { + // B = 1 byte, H = 2 bytes, S = 4 bytes, D = 8 bytes, Q = 16 bytes + let size = match arm64_operand.vas { + // Not an fp/simd instruction + arch::arm64::Arm64Vas::ARM64_VAS_INVALID => ARCH_WORD_SIZE, + // 1 byte + arch::arm64::Arm64Vas::ARM64_VAS_1B => 1, + // 2 bytes + arch::arm64::Arm64Vas::ARM64_VAS_1H => 2, + // 4 bytes + arch::arm64::Arm64Vas::ARM64_VAS_4B + | arch::arm64::Arm64Vas::ARM64_VAS_2H + | arch::arm64::Arm64Vas::ARM64_VAS_1S => 4, + // 8 bytes + arch::arm64::Arm64Vas::ARM64_VAS_8B + | arch::arm64::Arm64Vas::ARM64_VAS_4H + | arch::arm64::Arm64Vas::ARM64_VAS_2S + | arch::arm64::Arm64Vas::ARM64_VAS_1D => 8, + // 16 bytes + arch::arm64::Arm64Vas::ARM64_VAS_16B + | arch::arm64::Arm64Vas::ARM64_VAS_8H + | arch::arm64::Arm64Vas::ARM64_VAS_4S + | arch::arm64::Arm64Vas::ARM64_VAS_2D + | arch::arm64::Arm64Vas::ARM64_VAS_1Q => 16, + }; + let push = addr..addr.strict_add(size); + // FIXME: This now has access type info in the latest + // git version of capstone because this pissed me off + // and I added it. Change this when it updates + acc_events.push(AccessEvent::Read(push.clone())); + acc_events.push(AccessEvent::Write(push)); + } + _ => (), + } + } + #[cfg(target_arch = "arm")] + arch::ArchOperand::ArmOperand(arm_operand) => + match arm_operand.op_type { + arch::arm::ArmOperandType::Mem(_) => { + // We don't get info on the size of the access, but + // we're at least told if it's a vector inssizetruction + let size = if arm_operand.vector_index.is_some() { + ARCH_MAX_ACCESS_SIZE + } else { + ARCH_WORD_SIZE + }; + let push = addr..addr.strict_add(size); + let acc_ty = arm_operand.access.unwrap(); + if acc_ty.is_readable() { + acc_events.push(AccessEvent::Read(push.clone())); + } + if acc_ty.is_writable() { + acc_events.push(AccessEvent::Write(push)); + } + } + _ => (), + }, + #[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))] + arch::ArchOperand::RiscVOperand(risc_voperand) => { + match risc_voperand { + arch::riscv::RiscVOperand::Mem(_) => { + // We get basically no info here + let push = addr..addr.strict_add(size); + acc_events.push(AccessEvent::Read(push.clone())); + acc_events.push(AccessEvent::Write(push)); + } + _ => (), + } + } + _ => unimplemented!(), + } + } + + Ok(()) + } + + // Get information on what caused the segfault. This contains the address + // that triggered it + let siginfo = ptrace::getsiginfo(pid).map_err(|_| ExecError::Shrug)?; + // All x86, ARM, etc. instructions only have at most one memory operand + // (thankfully!) + // SAFETY: si_addr is safe to call + let addr = unsafe { siginfo.si_addr().addr() }; + let page_addr = addr.strict_sub(addr.strict_rem(page_size)); + + if ch_pages.iter().any(|pg| (*pg..pg.strict_add(page_size)).contains(&addr)) { + // Overall structure: + // - Get the address that caused the segfault + // - Unprotect the memory + // - Step 1 instruction + // - Parse executed code to estimate size & type of access + // - Reprotect the memory + // - Continue + let stack_ptr = ch_stack.strict_add(FAKE_STACK_SIZE / 2); + let regs_bak = ptrace::getregs(pid).unwrap(); + let mut new_regs = regs_bak; + let ip_prestep = regs_bak.ip(); + + // Move the instr ptr into the deprotection code + #[expect(clippy::as_conversions)] + new_regs.set_ip(mempr_off as usize); + // Don't mess up the stack by accident! + new_regs.set_sp(stack_ptr); + + // Modify the PAGE_ADDR global on the child process to point to the page + // that we want unprotected + ptrace::write( + pid, + (&raw const PAGE_ADDR).cast_mut().cast(), + libc::c_long::try_from(page_addr).unwrap(), + ) + .unwrap(); + + // Check if we also own the next page, and if so unprotect it in case + // the access spans the page boundary + if ch_pages.contains(&page_addr.strict_add(page_size)) { + ptrace::write(pid, (&raw const PAGE_COUNT).cast_mut().cast(), 2).unwrap(); + } else { + ptrace::write(pid, (&raw const PAGE_COUNT).cast_mut().cast(), 1).unwrap(); + } + + ptrace::setregs(pid, new_regs).unwrap(); + + // Our mempr_* functions end with a raise(SIGSTOP) + wait_for_signal(pid, signal::SIGSTOP, true)?; + + // Step 1 instruction + ptrace::setregs(pid, regs_bak).unwrap(); + ptrace::step(pid, None).unwrap(); + // Don't use wait_for_signal here since 1 instruction doesn't give room + // for any uncertainty + we don't want it `cont()`ing randomly by accident + // Also, don't let it continue with unprotected memory if something errors! + let _ = wait::waitid(wait::Id::Pid(pid), WAIT_FLAGS).map_err(|_| ExecError::Died(None))?; + + // Save registers and grab the bytes that were executed. This would + // be really nasty if it was a jump or similar but those thankfully + // won't do memory accesses and so can't trigger this! + let regs_bak = ptrace::getregs(pid).unwrap(); + new_regs = regs_bak; + let ip_poststep = regs_bak.ip(); + // We need to do reads/writes in word-sized chunks + let diff = (ip_poststep.strict_sub(ip_prestep)).div_ceil(ARCH_WORD_SIZE); + let instr = (ip_prestep..ip_prestep.strict_add(diff)).fold(vec![], |mut ret, ip| { + // This only needs to be a valid pointer in the child process, not ours + ret.append( + &mut ptrace::read(pid, std::ptr::without_provenance_mut(ip)) + .unwrap() + .to_ne_bytes() + .to_vec(), + ); + ret + }); + + // Now figure out the size + type of access and log it down + // This will mark down e.g. the same area being read multiple times, + // since it's more efficient to compress the accesses at the end + if capstone_disassemble(&instr, addr, cs, acc_events).is_err() { + // Read goes first because we need to be pessimistic + acc_events.push(AccessEvent::Read(addr..addr.strict_add(ARCH_MAX_ACCESS_SIZE))); + acc_events.push(AccessEvent::Write(addr..addr.strict_add(ARCH_MAX_ACCESS_SIZE))); + } + + // Reprotect everything and continue + #[expect(clippy::as_conversions)] + new_regs.set_ip(mempr_on as usize); + new_regs.set_sp(stack_ptr); + ptrace::setregs(pid, new_regs).unwrap(); + wait_for_signal(pid, signal::SIGSTOP, true)?; + + ptrace::setregs(pid, regs_bak).unwrap(); + ptrace::syscall(pid, None).unwrap(); + Ok(()) + } else { + // This was a real segfault, so print some debug info and quit + let regs = ptrace::getregs(pid).unwrap(); + eprintln!("Segfault occurred during FFI at {addr:#018x}"); + eprintln!("Expected access on pages: {ch_pages:#018x?}"); + eprintln!("Register dump: {regs:#x?}"); + ptrace::kill(pid).unwrap(); + Err(ExecError::Died(None)) + } +} + +// We only get dropped into these functions via offsetting the instr pointer +// manually, so we *must not ever* unwind from them + +/// Disables protections on the page whose address is currently in `PAGE_ADDR`. +/// +/// SAFETY: `PAGE_ADDR` should be set to a page-aligned pointer to an owned page, +/// `PAGE_SIZE` should be the host pagesize, and the range from `PAGE_ADDR` to +/// `PAGE_SIZE` * `PAGE_COUNT` must be owned and allocated memory. No other threads +/// should be running. +pub unsafe extern "C" fn mempr_off() { + use std::sync::atomic::Ordering; + + let len = PAGE_SIZE.load(Ordering::Relaxed).wrapping_mul(PAGE_COUNT.load(Ordering::Relaxed)); + // SAFETY: Upheld by caller + unsafe { + // It's up to the caller to make sure this doesn't actually overflow, but + // we mustn't unwind from here, so... + if libc::mprotect( + PAGE_ADDR.load(Ordering::Relaxed).cast(), + len, + libc::PROT_READ | libc::PROT_WRITE, + ) != 0 + { + // Can't return or unwind, but we can do this + std::process::exit(-1); + } + } + // If this fails somehow we're doomed + if signal::raise(signal::SIGSTOP).is_err() { + std::process::exit(-1); + } +} + +/// Reenables protection on the page set by `PAGE_ADDR`. +/// +/// SAFETY: See `mempr_off()`. +pub unsafe extern "C" fn mempr_on() { + use std::sync::atomic::Ordering; + + let len = PAGE_SIZE.load(Ordering::Relaxed).wrapping_mul(PAGE_COUNT.load(Ordering::Relaxed)); + // SAFETY: Upheld by caller + unsafe { + if libc::mprotect(PAGE_ADDR.load(Ordering::Relaxed).cast(), len, libc::PROT_NONE) != 0 { + std::process::exit(-1); + } + } + if signal::raise(signal::SIGSTOP).is_err() { + std::process::exit(-1); + } +} diff --git a/tests/native-lib/pass/ptr_read_access.stderr b/tests/native-lib/pass/ptr_read_access.stderr index ab40811a9d..92d6a1b81b 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 LL | unsafe { print_pointer(&x) }; | ^^^^^^^^^^^^^^^^^ sharing memory with a native function called via FFI | - = 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 a059d7740f..9b74431e41 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 LL | unsafe { increment_int(&mut x) }; | ^^^^^^^^^^^^^^^^^^^^^ sharing memory with a native function called via FFI | - = 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: