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..627b57639e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,10 @@ features = ['unprefixed_malloc_on_supported_platforms'] libc = "0.2" libffi = "4.0.0" libloading = "0.8" +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 = [ diff --git a/src/alloc_bytes.rs b/src/alloc/alloc_bytes.rs similarity index 68% rename from src/alloc_bytes.rs rename to src/alloc/alloc_bytes.rs index 2bac2659ec..a28a8c0aa7 100644 --- a/src/alloc_bytes.rs +++ b/src/alloc/alloc_bytes.rs @@ -1,12 +1,23 @@ use std::alloc::Layout; use std::borrow::Cow; +use std::cell::RefCell; +use std::rc::Rc; use std::{alloc, slice}; use rustc_abi::{Align, Size}; use rustc_middle::mir::interpret::AllocBytes; +#[cfg(target_os = "linux")] +use crate::alloc::isolated_alloc::IsolatedAlloc; use crate::helpers::ToU64 as _; +#[derive(Clone, Debug)] +pub enum MiriAllocParams { + Global, + #[cfg(target_os = "linux")] + Isolated(Rc>), +} + /// Allocation bytes that explicitly handle the layout of the data they're storing. /// This is necessary to interface with native code that accesses the program store in Miri. #[derive(Debug)] @@ -18,13 +29,16 @@ pub struct MiriAllocBytes { /// * If `self.layout.size() == 0`, then `self.ptr` was allocated with the equivalent layout with size 1. /// * Otherwise, `self.ptr` points to memory allocated with `self.layout`. ptr: *mut u8, + /// Whether this instance of `MiriAllocBytes` had its allocation created by calling `alloc::alloc()` + /// (`Global`) or the discrete allocator (`Isolated`) + params: MiriAllocParams, } impl Clone for MiriAllocBytes { fn clone(&self) -> Self { let bytes: Cow<'_, [u8]> = Cow::Borrowed(self); let align = Align::from_bytes(self.layout.align().to_u64()).unwrap(); - MiriAllocBytes::from_bytes(bytes, align, ()) + MiriAllocBytes::from_bytes(bytes, align, self.params.clone()) } } @@ -37,8 +51,16 @@ impl Drop for MiriAllocBytes { } else { self.layout }; + // SAFETY: Invariant, `self.ptr` points to memory allocated with `self.layout`. - unsafe { alloc::dealloc(self.ptr, alloc_layout) } + unsafe { + match self.params.clone() { + MiriAllocParams::Global => alloc::dealloc(self.ptr, alloc_layout), + #[cfg(target_os = "linux")] + MiriAllocParams::Isolated(alloc) => + alloc.borrow_mut().dealloc(self.ptr, alloc_layout), + } + } } } @@ -67,6 +89,7 @@ impl MiriAllocBytes { fn alloc_with( size: u64, align: u64, + params: MiriAllocParams, alloc_fn: impl FnOnce(Layout) -> *mut u8, ) -> Result { let size = usize::try_from(size).map_err(|_| ())?; @@ -80,22 +103,32 @@ impl MiriAllocBytes { Err(()) } else { // SAFETY: All `MiriAllocBytes` invariants are fulfilled. - Ok(Self { ptr, layout }) + Ok(Self { ptr, layout, params }) } } } impl AllocBytes for MiriAllocBytes { - /// Placeholder! - type AllocParams = (); + type AllocParams = MiriAllocParams; - fn from_bytes<'a>(slice: impl Into>, align: Align, _params: ()) -> Self { + fn from_bytes<'a>( + slice: impl Into>, + align: Align, + params: MiriAllocParams, + ) -> Self { let slice = slice.into(); let size = slice.len(); let align = align.bytes(); + let p_clone = params.clone(); // SAFETY: `alloc_fn` will only be used with `size != 0`. - let alloc_fn = |layout| unsafe { alloc::alloc(layout) }; - let alloc_bytes = MiriAllocBytes::alloc_with(size.to_u64(), align, alloc_fn) + let alloc_fn = |layout| unsafe { + match p_clone { + MiriAllocParams::Global => alloc::alloc(layout), + #[cfg(target_os = "linux")] + MiriAllocParams::Isolated(alloc) => alloc.borrow_mut().alloc(layout), + } + }; + let alloc_bytes = MiriAllocBytes::alloc_with(size.to_u64(), align, params, alloc_fn) .unwrap_or_else(|()| { panic!("Miri ran out of memory: cannot create allocation of {size} bytes") }); @@ -105,12 +138,19 @@ impl AllocBytes for MiriAllocBytes { alloc_bytes } - fn zeroed(size: Size, align: Align, _params: ()) -> Option { + fn zeroed(size: Size, align: Align, params: MiriAllocParams) -> Option { let size = size.bytes(); let align = align.bytes(); + let p_clone = params.clone(); // SAFETY: `alloc_fn` will only be used with `size != 0`. - let alloc_fn = |layout| unsafe { alloc::alloc_zeroed(layout) }; - MiriAllocBytes::alloc_with(size, align, alloc_fn).ok() + let alloc_fn = |layout| unsafe { + match p_clone { + MiriAllocParams::Global => alloc::alloc_zeroed(layout), + #[cfg(target_os = "linux")] + MiriAllocParams::Isolated(alloc) => alloc.borrow_mut().alloc_zeroed(layout), + } + }; + MiriAllocBytes::alloc_with(size, align, params, alloc_fn).ok() } fn as_mut_ptr(&mut self) -> *mut u8 { diff --git a/src/alloc/isolated_alloc.rs b/src/alloc/isolated_alloc.rs new file mode 100644 index 0000000000..5af80d8796 --- /dev/null +++ b/src/alloc/isolated_alloc.rs @@ -0,0 +1,363 @@ +use std::alloc::{self, Layout}; + +use nix::sys::mman; +use rustc_index::bit_set::DenseBitSet; + +use crate::helpers::ToU64; + +/// A dedicated allocator for interpreter memory contents, ensuring they are stored on dedicated +/// pages (not mixed with Miri's own memory). This is very useful for native-lib mode. +#[derive(Debug)] +pub struct IsolatedAlloc { + /// Pointers to page-aligned memory that has been claimed by the allocator. + /// Every pointer here must point to a page-sized allocation claimed via + /// the global allocator. + page_ptrs: Vec<*mut u8>, + /// Pointers to multi-page-sized allocations. These must also be page-aligned, + /// with their size stored as the second element of the vector. + huge_ptrs: Vec<(*mut u8, usize)>, + /// Metadata about which bytes have been allocated on each page. The length + /// of this vector must be the same as that of `page_ptrs`, and the domain + /// size of the bitset must be exactly `page_size / 8`. + /// + /// Conceptually, each bit of the bitset represents the allocation status of + /// one 8-byte chunk on the corresponding element of `page_ptrs`. Thus, + /// indexing into it should be done with a value one-eighth of the corresponding + /// offset on the matching `page_ptrs` element. + page_infos: Vec>, + /// The host (not emulated) page size, or 0 if it has not yet been set. + page_size: usize, +} + +impl IsolatedAlloc { + /// Creates an empty allocator. + pub fn new() -> Self { + Self { + page_ptrs: Vec::new(), + huge_ptrs: Vec::new(), + page_infos: Vec::new(), + page_size: unsafe { libc::sysconf(libc::_SC_PAGESIZE).try_into().unwrap() }, + } + } + + /// Expands the available memory pool by adding one page. + fn add_page(&mut self, page_size: usize) -> (*mut u8, &mut DenseBitSet) { + assert_ne!(page_size, 0); + + let page_layout = unsafe { Layout::from_size_align_unchecked(page_size, page_size) }; + // We don't overwrite the bytes we hand out so make sure they're zeroed by default! + let page_ptr = unsafe { alloc::alloc(page_layout) }; + // `page_infos` has to have one-eighth as many bits as a page has bytes + // (or one-64th as many bytes) + self.page_infos.push(DenseBitSet::new_empty(page_size / 8)); + self.page_ptrs.push(page_ptr); + (page_ptr, self.page_infos.last_mut().unwrap()) + } + + /// For simplicity, we allocate in multiples of 8 bytes with at least that + /// alignment. + #[inline] + fn normalized_layout(layout: Layout) -> (usize, usize) { + let align = if layout.align() < 8 { 8 } else { layout.align() }; + let size = layout.size().next_multiple_of(8); + (size, align) + } + + /// Allocates memory as described in `Layout`. This memory should be deallocated + /// by calling `dealloc` on this same allocator. + /// + /// SAFETY: See `alloc::alloc()` + pub fn alloc(&mut self, layout: Layout) -> *mut u8 { + unsafe { self.allocate(layout, false) } + } + + /// Same as `alloc`, but zeroes out the memory. + /// + /// SAFETY: See `alloc::alloc_zeroed()` + pub fn alloc_zeroed(&mut self, layout: Layout) -> *mut u8 { + unsafe { self.allocate(layout, true) } + } + + /// Abstracts over the logic of `alloc_zeroed` vs `alloc`, as determined by + /// the `zeroed` argument. + /// + /// SAFETY: See `alloc::alloc()`, with the added restriction that `page_size` + /// corresponds to the host pagesize. + unsafe fn allocate(&mut self, layout: Layout, zeroed: bool) -> *mut u8 { + if layout.align() > self.page_size || layout.size() > self.page_size { + unsafe { self.alloc_multi_page(layout, zeroed) } + } else { + for (&mut page, pinfo) in std::iter::zip(&mut self.page_ptrs, &mut self.page_infos) { + if let Some(ptr) = + unsafe { Self::alloc_from_page(self.page_size, layout, page, pinfo, zeroed) } + { + return ptr; + } + } + + // We get here only if there's no space in our existing pages + let page_size = self.page_size; + let (page, pinfo) = self.add_page(page_size); + unsafe { Self::alloc_from_page(page_size, layout, page, pinfo, zeroed).unwrap() } + } + } + + /// Used internally by `allocate` to abstract over some logic. + /// + /// SAFETY: `page` must be a page-aligned pointer to an allocated page, + /// where the allocation is (at least) `page_size` bytes. + unsafe fn alloc_from_page( + page_size: usize, + layout: Layout, + page: *mut u8, + pinfo: &mut DenseBitSet, + zeroed: bool, + ) -> Option<*mut u8> { + let (size, align) = IsolatedAlloc::normalized_layout(layout); + + // Check every alignment-sized block and see if there exists a `size` + // chunk of empty space i.e. forall idx . !pinfo.contains(idx / 8) + for idx in (0..page_size).step_by(align) { + let idx_pinfo = idx / 8; + let size_pinfo = size / 8; + // DenseBitSet::contains() panics if the index is out of bounds + if pinfo.domain_size() < idx_pinfo + size_pinfo { + break; + } + let pred = !(idx_pinfo..idx_pinfo + size_pinfo).any(|idx| pinfo.contains(idx)); + if pred { + pinfo.insert_range(idx_pinfo..idx_pinfo + size_pinfo); + unsafe { + let ptr = page.add(idx); + if zeroed { + // Only write the bytes we were specifically asked to + // zero out, even if we allocated more + ptr.write_bytes(0, layout.size()); + } + return Some(ptr); + } + } + } + None + } + + /// Allocates in multiples of one page on the host system. + /// + /// SAFETY: Same as `alloc_inner()`. + unsafe fn alloc_multi_page(&mut self, layout: Layout, zeroed: bool) -> *mut u8 { + let ret = + unsafe { if zeroed { alloc::alloc_zeroed(layout) } else { alloc::alloc(layout) } }; + self.huge_ptrs.push((ret, layout.size())); + ret + } + + /// Deallocates a pointer from this allocator. + /// + /// SAFETY: This pointer must have been allocated by calling `alloc()` (or + /// `alloc_zeroed()`) with the same layout as the one passed on this same + /// `IsolatedAlloc`. + pub unsafe fn dealloc(&mut self, ptr: *mut u8, layout: Layout) { + let (size, align) = IsolatedAlloc::normalized_layout(layout); + + let ptr_idx = ptr.addr() % self.page_size; + let page_addr = ptr.addr() - ptr_idx; + + if align > self.page_size || size > self.page_size { + unsafe { + self.dealloc_multi_page(ptr, layout); + } + } else { + let pinfo = std::iter::zip(&mut self.page_ptrs, &mut self.page_infos) + .find(|(page, _)| page.addr() == page_addr); + let Some((_, pinfo)) = pinfo else { + panic!( + "Freeing in an unallocated page: {ptr:?}\nHolding pages {:?}", + self.page_ptrs + ) + }; + let ptr_idx_pinfo = ptr_idx / 8; + let size_pinfo = size / 8; + for idx in ptr_idx_pinfo..ptr_idx_pinfo + size_pinfo { + pinfo.remove(idx); + } + } + + let mut free = vec![]; + let page_layout = + unsafe { Layout::from_size_align_unchecked(self.page_size, self.page_size) }; + for (idx, pinfo) in self.page_infos.iter().enumerate() { + if pinfo.is_empty() { + free.push(idx); + } + } + free.reverse(); + for idx in free { + let _ = self.page_infos.remove(idx); + unsafe { + alloc::dealloc(self.page_ptrs.remove(idx), page_layout); + } + } + } + + /// SAFETY: Same as `dealloc()` with the added requirement that `layout` + /// must ask for a size larger than the host pagesize. + unsafe fn dealloc_multi_page(&mut self, ptr: *mut u8, layout: Layout) { + let idx = self + .huge_ptrs + .iter() + .position(|pg| ptr.addr() == pg.0.addr()) + .expect("Freeing unallocated pages"); + let ptr = self.huge_ptrs.remove(idx).0; + unsafe { + 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.addr().to_u64()).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().to_u64() }); + } + }); + pages + } + + /// Protects all owned memory, 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)] +mod tests { + use super::*; + + fn assert_zeroes(ptr: *mut u8, layout: Layout) { + unsafe { + for ofs in 0..layout.size() { + assert_eq!(0, ptr.add(ofs).read()); + } + } + } + + #[test] + fn small_zeroes() { + let layout = Layout::from_size_align(256, 32).unwrap(); + // allocate_zeroed + let ptr = unsafe { IsolatedAlloc::alloc_zeroed(layout, 0) }; + assert_zeroes(ptr, layout); + unsafe { + IsolatedAlloc::dealloc(ptr, layout, 0); + } + } + + #[test] + fn big_zeroes() { + let layout = Layout::from_size_align(16 * 1024, 128).unwrap(); + let ptr = unsafe { IsolatedAlloc::alloc_zeroed(layout, 1) }; + assert_zeroes(ptr, layout); + unsafe { + IsolatedAlloc::dealloc(ptr, layout, 1); + } + } + + #[test] + fn repeated_allocs() { + for sz in (1..=(16 * 1024)).step_by(128) { + let layout = Layout::from_size_align(sz, 1).unwrap(); + let ptr = unsafe { IsolatedAlloc::alloc_zeroed(layout, 2) }; + assert_zeroes(ptr, layout); + unsafe { + ptr.write_bytes(255, sz); + IsolatedAlloc::dealloc(ptr, layout, 2); + } + } + } + + #[test] + fn no_overlaps() { + no_overlaps_inner(3); + } + + fn no_overlaps_inner(id: u64) { + // Some random sizes and aligns + let mut sizes = vec![32; 10]; + sizes.append(&mut vec![15; 4]); + sizes.append(&mut vec![256; 12]); + // Give it some multi-page ones too + sizes.append(&mut vec![32 * 1024; 4]); + + let mut aligns = vec![16; 12]; + aligns.append(&mut vec![256; 2]); + aligns.append(&mut vec![64; 12]); + aligns.append(&mut vec![4096; 4]); + + assert_eq!(sizes.len(), aligns.len()); + let layouts: Vec<_> = std::iter::zip(sizes, aligns) + .map(|(sz, al)| Layout::from_size_align(sz, al).unwrap()) + .collect(); + let ptrs: Vec<_> = layouts + .iter() + .map(|layout| unsafe { IsolatedAlloc::alloc_zeroed(*layout, id) }) + .collect(); + + for (&ptr, &layout) in std::iter::zip(&ptrs, &layouts) { + // Make sure we don't allocate overlapping ranges + unsafe { + assert_zeroes(ptr, layout); + ptr.write_bytes(255, layout.size()); + IsolatedAlloc::dealloc(ptr, layout, id); + } + } + } + + #[test] + fn check_leaks() { + // Generate some noise first + no_overlaps_inner(4); + let alloc = ALLOCATOR.lock().unwrap(); + + // Should get auto-deleted if the allocations are empty + assert!(!alloc.allocators.contains_key(&4)); + } +} +*/ diff --git a/src/alloc/mod.rs b/src/alloc/mod.rs new file mode 100644 index 0000000000..3be885920d --- /dev/null +++ b/src/alloc/mod.rs @@ -0,0 +1,5 @@ +mod alloc_bytes; +#[cfg(target_os = "linux")] +pub mod isolated_alloc; + +pub use self::alloc_bytes::{MiriAllocBytes, MiriAllocParams}; diff --git a/src/alloc_addresses/mod.rs b/src/alloc_addresses/mod.rs index d2977a55e4..0fa84fa326 100644 --- a/src/alloc_addresses/mod.rs +++ b/src/alloc_addresses/mod.rs @@ -135,11 +135,12 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> { if this.machine.native_lib.is_some() { // In native lib mode, we use the "real" address of the bytes for this allocation. // This ensures the interpreted program and native code have the same view of memory. + let params = this.machine.get_default_alloc_params(); let base_ptr = match info.kind { AllocKind::LiveData => { if memory_kind == MiriMemoryKind::Global.into() { // For new global allocations, we always pre-allocate the memory to be able use the machine address directly. - let prepared_bytes = MiriAllocBytes::zeroed(info.size, info.align, ()) + let prepared_bytes = MiriAllocBytes::zeroed(info.size, info.align, params) .unwrap_or_else(|| { panic!("Miri ran out of memory: cannot create allocation of {size:?} bytes", size = info.size) }); @@ -158,8 +159,11 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> { } AllocKind::Function | AllocKind::VTable => { // Allocate some dummy memory to get a unique address for this function/vtable. - let alloc_bytes = - MiriAllocBytes::from_bytes(&[0u8; 1], Align::from_bytes(1).unwrap(), ()); + let alloc_bytes = MiriAllocBytes::from_bytes( + &[0u8; 1], + Align::from_bytes(1).unwrap(), + params, + ); let ptr = alloc_bytes.as_ptr(); // Leak the underlying memory to ensure it remains unique. std::mem::forget(alloc_bytes); @@ -429,7 +433,8 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { prepared_alloc_bytes.copy_from_slice(bytes); interp_ok(prepared_alloc_bytes) } else { - interp_ok(MiriAllocBytes::from_bytes(std::borrow::Cow::Borrowed(bytes), align, ())) + let params = this.machine.get_default_alloc_params(); + interp_ok(MiriAllocBytes::from_bytes(std::borrow::Cow::Borrowed(bytes), align, params)) } } @@ -465,13 +470,26 @@ 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 _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/concurrency/thread.rs b/src/concurrency/thread.rs index 5014bbeedb..14769ee10c 100644 --- a/src/concurrency/thread.rs +++ b/src/concurrency/thread.rs @@ -897,6 +897,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { if tcx.is_foreign_item(def_id) { throw_unsup_format!("foreign thread-local statics are not supported"); } + let params = this.machine.get_default_alloc_params(); let alloc = this.ctfe_query(|tcx| tcx.eval_static_initializer(def_id))?; // We make a full copy of this allocation. let mut alloc = alloc.inner().adjust_from_tcx( @@ -905,7 +906,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { interp_ok(MiriAllocBytes::from_bytes( std::borrow::Cow::Borrowed(bytes), align, - (), + params.clone(), )) }, |ptr| this.global_root_pointer(ptr), diff --git a/src/helpers.rs b/src/helpers.rs index dcc74b099d..11e8ea9d8f 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1435,3 +1435,17 @@ impl ToU64 for usize { self.try_into().unwrap() } } + +/// And add a similar method for `isize`s. Currently only used on Linux for +/// the `trace` module. +#[cfg(target_os = "linux")] +pub trait ToI64 { + fn to_i64(self) -> i64; +} + +#[cfg(target_os = "linux")] +impl ToI64 for isize { + fn to_i64(self) -> i64 { + self.try_into().unwrap() + } +} diff --git a/src/lib.rs b/src/lib.rs index 9d663ca9ed..8802216448 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ #![feature(nonzero_ops)] #![feature(strict_overflow_ops)] #![feature(pointer_is_aligned_to)] +#![feature(ptr_metadata)] #![feature(unqualified_local_imports)] #![feature(derive_coerce_pointee)] #![feature(arbitrary_self_types)] @@ -69,8 +70,8 @@ extern crate rustc_target; #[allow(unused_extern_crates)] extern crate rustc_driver; +mod alloc; mod alloc_addresses; -mod alloc_bytes; mod borrow_tracker; mod clock; mod concurrency; @@ -105,8 +106,8 @@ pub type OpTy<'tcx> = interpret::OpTy<'tcx, machine::Provenance>; pub type PlaceTy<'tcx> = interpret::PlaceTy<'tcx, machine::Provenance>; pub type MPlaceTy<'tcx> = interpret::MPlaceTy<'tcx, machine::Provenance>; +pub use crate::alloc::MiriAllocBytes; pub use crate::alloc_addresses::{EvalContextExt as _, ProvenanceMode}; -pub use crate::alloc_bytes::MiriAllocBytes; pub use crate::borrow_tracker::stacked_borrows::{ EvalContextExt as _, Item, Permission, Stack, Stacks, }; diff --git a/src/machine.rs b/src/machine.rs index 0c5127d802..7def7f1fe6 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -532,6 +532,10 @@ pub struct MiriMachine<'tcx> { /// Needs to be queried by ptr_to_int, hence needs interior mutability. pub(crate) rng: RefCell, + /// The allocator used for the machine's `AllocBytes`. + #[cfg(target_os = "linux")] + pub(crate) allocator: Rc>, + /// The allocation IDs to report when they are being allocated /// (helps for debugging memory leaks and use after free bugs). tracked_alloc_ids: FxHashSet, @@ -638,7 +642,8 @@ impl<'tcx> MiriMachine<'tcx> { let path = Path::new(out).join(filename); measureme::Profiler::new(path).expect("Couldn't create `measureme` profiler") }); - let rng = StdRng::seed_from_u64(config.seed.unwrap_or(0)); + let seed = config.seed.unwrap_or(0); + let rng = StdRng::seed_from_u64(seed); let borrow_tracker = config.borrow_tracker.map(|bt| bt.instantiate_global_state(config)); let data_race = if config.genmc_mode { // `genmc_ctx` persists across executions, so we don't create a new one here. @@ -715,6 +720,8 @@ impl<'tcx> MiriMachine<'tcx> { local_crates, extern_statics: FxHashMap::default(), rng: RefCell::new(rng), + #[cfg(target_os = "linux")] + allocator: Rc::new(RefCell::new(crate::alloc::isolated_alloc::IsolatedAlloc::new())), tracked_alloc_ids: config.tracked_alloc_ids.clone(), track_alloc_accesses: config.track_alloc_accesses, check_alignment: config.check_alignment, @@ -917,6 +924,8 @@ impl VisitProvenance for MiriMachine<'_> { backtrace_style: _, local_crates: _, rng: _, + #[cfg(target_os = "linux")] + allocator: _, tracked_alloc_ids: _, track_alloc_accesses: _, check_alignment: _, @@ -1804,8 +1813,18 @@ impl<'tcx> Machine<'tcx> for MiriMachine<'tcx> { Cow::Borrowed(ecx.machine.union_data_ranges.entry(ty).or_insert_with(compute_range)) } - /// Placeholder! - fn get_default_alloc_params(&self) -> ::AllocParams {} + fn get_default_alloc_params(&self) -> ::AllocParams { + use crate::alloc::MiriAllocParams; + + #[cfg(target_os = "linux")] + if self.native_lib.is_some() { + MiriAllocParams::Isolated(self.allocator.clone()) + } else { + MiriAllocParams::Global + } + #[cfg(not(target_os = "linux"))] + MiriAllocParams::Global + } } /// Trait for callbacks handling asynchronous machine operations. 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 1e6c93333c..1a8244a199 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::cell::RefCell; use std::ops::Deref; +use std::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,69 +44,81 @@ 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, /// if it exists. The function must be in the shared object file specified: we do *not* - /// return pointers to functions in dependencies of the library. + /// return pointers to functions in dependencies of the library. fn get_func_ptr_explicitly_from_lib(&mut self, link_name: Symbol) -> Option { let this = self.eval_context_mut(); // Try getting the function from the shared library. - let (lib, lib_path) = this.machine.native_lib.as_ref().unwrap(); + // On windows `_lib_path` will be unused, hence the name starting with `_`. + let (lib, _lib_path) = this.machine.native_lib.as_ref().unwrap(); let func: libloading::Symbol<'_, unsafe extern "C" fn()> = unsafe { lib.get(link_name.as_str().as_bytes()).ok()? }; #[expect(clippy::as_conversions)] // fn-ptr to raw-ptr cast needs `as`. @@ -109,7 +135,7 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> { // This code is a reimplementation of the mechanism for getting `dli_fname` in `libloading`, // from: https://docs.rs/libloading/0.7.3/src/libloading/os/unix/mod.rs.html#411 // using the `libc` crate where this interface is public. - let mut info = std::mem::MaybeUninit::::zeroed(); + let mut info = std::mem::MaybeUninit::::uninit(); unsafe { if libc::dladdr(fn_ptr, info.as_mut_ptr()) != 0 { let info = info.assume_init(); @@ -117,9 +143,8 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> { let fname_ptr = info.dli_fname.as_ptr(); #[cfg(not(target_os = "cygwin"))] let fname_ptr = info.dli_fname; - assert!(!fname_ptr.is_null()); if std::ffi::CStr::from_ptr(fname_ptr).to_str().unwrap() - != lib_path.to_str().unwrap() + != _lib_path.to_str().unwrap() { return None; } @@ -180,8 +205,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::init().is_ok() { + 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 @@ -190,12 +224,45 @@ 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 { + //eprintln!("events: {events:#018x?}"); + this.apply_events(events)?; + } + #[cfg(not(target_os = "linux"))] + let _ = maybe_memevents; // Suppress the unused warning + it's useless this.write_immediate(*ret, dest)?; interp_ok(true) } } +#[cfg(target_os = "linux")] +unsafe fn do_native_call( + ptr: CodePtr, + args: &[ffi::Arg<'_>], + alloc: Rc>, +) -> (T, Option) { + use shims::trace::Supervisor; + + unsafe { + let guard = Supervisor::start_ffi(alloc.clone()); + let ret = ffi::call(ptr, args); + (ret, Supervisor::end_ffi(guard, alloc)) + } +} + +#[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.rs b/src/shims/trace.rs new file mode 100644 index 0000000000..004f055a61 --- /dev/null +++ b/src/shims/trace.rs @@ -0,0 +1,1130 @@ +use std::cell::RefCell; +use std::ops::Range; +use std::rc::Rc; + +use ipc_channel::ipc; +use nix::sys::{ptrace, signal, wait}; +use nix::unistd; + +use crate::alloc::isolated_alloc::IsolatedAlloc; +//use crate::alloc::isolated_alloc::IsolatedAlloc; +use crate::helpers::{ToI64, ToU64}; + +/// Opcode for an instruction to raise SIGTRAP, to be written in the child process. +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +const BREAKPT_INSTR: isize = 0xCC; +#[cfg(any(target_arch = "arm", target_arch = "aarch64"))] +const BREAKPT_INSTR: isize = 0xD420; + +/// 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: u64 = 64; +#[cfg(any(target_arch = "arm", target_arch = "aarch64"))] +const ARCH_MAX_ACCESS_SIZE: u64 = 16; + +/// The size used for the array into which we can move the stack pointer. +const FAKE_STACK_SIZE: usize = 1024; + +static SUPERVISOR: std::sync::Mutex> = std::sync::Mutex::new(None); +// TODO: move these into a per-MiriMachine struct +static mut PAGE_ADDR: *mut libc::c_void = std::ptr::null_mut(); +static mut PAGE_SIZE: u64 = 4096; + +unsafe impl Send for Supervisor {} + +/// 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 { + /// The return value of a function call, if one just happened. + fn retval(&self) -> usize; + /// The first ptr-sized argument. + fn arg1(&self) -> usize; + /// The second ptr-sized argument. + fn arg2(&self) -> usize; + /// If entering a syscall, this is the syscall number. + fn syscall_nr(&self) -> isize; + /// The instruction pointer. + fn ip(&self) -> usize; + /// The stack pointer. + fn sp(&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 { + fn retval(&self) -> usize { self.rax as _ } + fn arg1(&self) -> usize { self.rdi as _ } + fn arg2(&self) -> usize { self.rsi as _ } + fn syscall_nr(&self) -> isize { self.orig_rax as _ } + fn ip(&self) -> usize { self.rip as _ } + fn sp(&self) -> usize { self.rsp as _ } + fn set_ip(&mut self, ip: usize) { self.rip = ip as _ } + 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 { + fn retval(&self) -> usize { self.eax as _ } + fn arg1(&self) -> usize { self.edi as _ } + fn arg2(&self) -> usize { self.esi as _ } + fn syscall_nr(&self) -> isize { self.orig_eax as _ } + fn ip(&self) -> usize { self.eip as _ } + fn sp(&self) -> usize { self.esp as _ } + fn set_ip(&mut self, ip: usize) { self.eip = ip as _ } + 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 { + fn retval(&self) -> usize { self.regs[0] as _ } + fn arg1(&self) -> usize { self.regs[0] as _ } + fn arg2(&self) -> usize { self.regs[1] as _ } + fn syscall_nr(&self) -> isize { self.regs[8] as _ } + fn ip(&self) -> usize { self.pc as _ } + fn sp(&self) -> usize { self.sp as _ } + fn set_ip(&mut self, ip: usize) { self.pc = ip as _ } + fn set_sp(&mut self, sp: usize) { self.sp = sp as _ } +} + +/// 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 requests (mainly `BeginFfi` and `EndFfi`). + t_message: ipc::IpcSender, + /// Receiver for memory acceses that ocurred at the end of the FFI call. + /// Also gives us the pointer needed to free the "fake stack" used during + /// the tracing, separate from the main `MemEvents` so as to not propagate + /// it further. + r_event: ipc::IpcReceiver<(MemEvents, usize)>, +} + +impl Supervisor { + /// 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`. + pub fn init() -> Result<(), ()> { + 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 disabled on the system + if stat != '0' && stat != '1' { + return Err(()); + } + } + } + + // We don't need to get a lock, just asserting it's being held means + // it's being initialised and set up + let maybe_lock = SUPERVISOR.try_lock(); + let mut lock = match maybe_lock { + Ok(lock) => lock, + Err(e) => { + match e { + std::sync::TryLockError::Poisoned(_) => return Err(()), + std::sync::TryLockError::WouldBlock => return Ok(()), + }; + } + }; + // If we have it, then init if needed + if lock.is_none() { + let (t_message, r_message) = ipc::channel().unwrap(); + let (t_event, r_event) = ipc::channel().unwrap(); + let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }.try_into().unwrap(); + + unsafe { + PAGE_SIZE = page_size; + + match unistd::fork().unwrap() { + unistd::ForkResult::Parent { child } => { + drop(lock); + let p = std::panic::catch_unwind(|| { + let listener = + ChildListener { rx: r_message, pid: child, attached: false }; + sv_loop(listener, t_event, page_size) + }); + eprintln!("{p:?}"); + std::process::exit(-1); + } + unistd::ForkResult::Child => { + *lock = Some(Supervisor { t_message, r_event }); + } + } + } + } + Ok(()) + } + + /// Begins preparations for doing an FFI call. This should be called at + /// the last possible moment before entering said call. `id` is the value + /// with which the machine's memory has been allocated in the `IsolatedAlloc`, + /// aka `machine.seed`. + /// + /// 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`. + /// + /// 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> { + //eprintln!("Seed {id} wants the SV lock"); + let mut sv_guard = SUPERVISOR.lock().unwrap(); + //eprintln!("Seed {id} got the SV lock!"); + if let Some(sv) = sv_guard.take() { + 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 pid = unistd::Pid::this().as_raw(); + let begin_info = BeginFfiInfo { page_ptrs, stack_ptr, pid }; + sv.t_message.send(TraceRequest::BeginFfi(begin_info)).unwrap(); + *sv_guard = Some(sv); + 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!"); + } + } + signal::raise(signal::SIGSTOP).unwrap(); + } + sv_guard + } + + /// 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. + pub fn end_ffi( + mut sv_guard: std::sync::MutexGuard<'static, Option>, + alloc: Rc>, + ) -> Option { + if let Some(sv) = sv_guard.take() { + sv.t_message.send(TraceRequest::EndFfi).unwrap(); + alloc.borrow_mut().unprep_ffi(); + + // On the off-chance something really weird happens, don't block forever + let (ret, ptr) = sv + .r_event + .try_recv_timeout(std::time::Duration::from_secs(1)) + .map_err(|e| { + match e { + ipc::TryRecvError::IpcError(e) => ipc::TryRecvError::IpcError(e), + ipc::TryRecvError::Empty => { + // timed out! + eprintln!("Waiting for accesses from supervisor timed out!"); + ipc::TryRecvError::Empty + } + } + }) + .ok()?; + if ptr != 0 { + let ptr: *mut [u8; FAKE_STACK_SIZE] = std::ptr::with_exposed_provenance_mut(ptr); + unsafe { + let stack_box = Box::from_raw(ptr); + drop(stack_box); + } + } + *sv_guard = Some(sv); + Some(ret) + } else { + None + } + } +} + +/// A message from the child process to the parent. +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +enum TraceRequest { + BeginFfi(BeginFfiInfo), + EndFfi, +} + +/// Information needed to begin tracing. +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +struct BeginFfiInfo { + /// 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, + pid: i32, +} + +/// A single memory access. +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub enum AccessEvent { + Read(Range), + Write(Range), +} + +/// The result(s) of a call to a `libc` allocation-related function. Note that +/// some function e.g. `realloc` will generate multiple events (an allocation +/// and a deallocation). +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub enum LibcEvent { + /// An allocation was created spanning the `Range`. + Malloc(Range), + /// A pointer with the inner address was `free`d. + Free(u64), +} + +/// A singular page mapping or unmapping. The inner field is always a page-aligned +/// address, representing a single system page being mapped/unmapped. +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub enum MmapEvent { + Mmap(u64), + Munmap(u64), +} + +/// 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, + /// An ordered list of libc events that occurred. `malloc`s which were `free`d + /// before the end of the FFI call will have been removed, but nothing else. + pub libc_events: Vec, + /// An ordered list of page mappings and unmappings that occurred. Same as + /// for `libc_events`, mappings that were unmapped will have been removed, + /// but nothing else. + pub mmap_events: Vec, +} + +/// A listener for the request channel along with relevant state. This lives +/// on the parent process. +struct ChildListener { + /// The matching channel for the child's `Supervisor` struct. + rx: ipc::IpcReceiver, + /// The child process' pid. + pid: unistd::Pid, + /// Whether an FFI call is currently ongoing. + attached: bool, +} + +/// A unified event representing something happening on the child process. Wraps +/// `nix`'s `WaitStatus` and our `TraceRequest` so it can all be done with one +/// `match` statement. +enum ExecEvent { + Status(wait::WaitStatus), + Request(TraceRequest), + Died(i32), +} + +impl Iterator for ChildListener { + type Item = ExecEvent; + + // Allows us to iterate over events in the child process + fn next(&mut self) -> Option { + // Do not block if the child has nothing to report for `waitpid`, but + // also report its status if it stopped and we're not tracing it anymore + // NB: Getting rid of WUNTRACED *will* cause it to hang randomly in CI! + let opts = wait::WaitPidFlag::WNOHANG | wait::WaitPidFlag::WUNTRACED; + loop { + match wait::waitpid(self.pid, Some(opts)) { + Ok(stat) => + match stat { + wait::WaitStatus::Exited(_pid, retcode) => + return Some(ExecEvent::Died(retcode)), + wait::WaitStatus::StillAlive => (), + _ => + if self.attached { + // If we're not attached, this can give weird + // data we're not expecting in `sv_loop` + return Some(ExecEvent::Status(stat)); + }, + }, + Err(errno) => { + // Not really any other way to do this cast + #[expect(clippy::as_conversions)] + return Some(ExecEvent::Died(errno as i32)); + } + } + + // Same, non-blocking poll of the IPC channel + if let Ok(msg) = self.rx.try_recv() { + match &msg { + TraceRequest::BeginFfi(info) => + if self.attached { + panic!("Attempting to begin FFI multiple times!"); + } else { + self.pid = unistd::Pid::from_raw(info.pid); + self.attached = true; + }, + TraceRequest::EndFfi => self.attached = false, + } + return Some(ExecEvent::Request(msg)); + } + // We ideally need to know quickly if an FFI call is entered... + std::thread::yield_now(); + } + } +} + +/// Values needed for us to intercept calls to certain libc functions, such as +/// their addresses and original content (before we overwrote them). +#[derive(Clone, Copy)] +struct MagicLibcValues { + malloc_addr: usize, + realloc_addr: usize, + free_addr: usize, + malloc_bytes: i64, + free_bytes: i64, + realloc_bytes: i64, +} + +impl MagicLibcValues { + /// Gets the needed values. Note that while safe to do after, this should + /// be done *before* anything is overwritten with `raise(SIGTRAP)`s. + fn read() -> Self { + // No other real way to do this + #[allow(clippy::as_conversions)] + let malloc_addr = libc::malloc as usize; + #[allow(clippy::as_conversions)] + let realloc_addr = libc::realloc as usize; + #[allow(clippy::as_conversions)] + let free_addr = libc::free as usize; + Self { + malloc_addr, + realloc_addr, + free_addr, + // i'm sorry + malloc_bytes: unsafe { + std::ptr::with_exposed_provenance::(malloc_addr).read_volatile() + }, + realloc_bytes: unsafe { + std::ptr::with_exposed_provenance::(realloc_addr).read_volatile() + }, + free_bytes: unsafe { + std::ptr::with_exposed_provenance::(free_addr).read_volatile() + }, + } + } + + /// Restores the data at the start of the stored functions to its original + /// contents on the child process. + fn restore(&self, pid: unistd::Pid) -> Result<(), nix::errno::Errno> { + ptrace::write( + pid, + std::ptr::with_exposed_provenance_mut(self.malloc_addr), + self.malloc_bytes, + )?; + ptrace::write( + pid, + std::ptr::with_exposed_provenance_mut(self.realloc_addr), + self.realloc_bytes, + )?; + ptrace::write(pid, std::ptr::with_exposed_provenance_mut(self.free_addr), self.free_bytes) + } + + /// Overwrites the first 8 bytes of the `_addr` fields with the specified + /// data, in the child process. `data` should probably be some kind of + /// breakpoint/SIGTRAP instruction. + fn overwrite_all(&self, pid: unistd::Pid, data: i64) -> Result<(), nix::errno::Errno> { + ptrace::write(pid, std::ptr::with_exposed_provenance_mut(self.malloc_addr), data)?; + ptrace::write(pid, std::ptr::with_exposed_provenance_mut(self.realloc_addr), data)?; + ptrace::write(pid, std::ptr::with_exposed_provenance_mut(self.free_addr), data) + } +} + +/// 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 are the same). +fn sv_loop( + listener: ChildListener, + t_event: ipc::IpcSender<(MemEvents, usize)>, + page_size: u64, +) -> ! { + // Things that we return to the child process + let mut acc_events = Vec::new(); + let mut mmap_events = Vec::new(); + let mut libc_events = Vec::new(); + + // Memory allocated on the MiriMachine + let mut ch_pages = Vec::new(); + let mut ch_stack = None; + + // Bits needed to intercept libc calls that we care about + let libc_vals = MagicLibcValues::read(); + + // An instance of the Capstone disassembler, so we don't spawn one on every access + let cs = get_disasm(); + + let mut main_pid = listener.pid; + //let main_pid = nix::unistd::Pid::from_raw(-1); + let mut retcode = 0; + + 'listen: for evt in listener { + match evt { + ExecEvent::Status(wait_status) => + match wait_status { + // Process killed by signal + wait::WaitStatus::Signaled(_pid, signal, _) => { + eprintln!("Process killed by {signal:?}"); + retcode = 1; + break 'listen; + } + // Stopped, probably by a segfault or SIGTRAP + wait::WaitStatus::Stopped(pid, signal) => { + match signal { + signal::SIGSEGV => { + if let Err(ret) = handle_segfault( + pid, + &ch_pages, + ch_stack.unwrap(), + page_size, + &cs, + &mut acc_events, + ) { + retcode = ret; + break 'listen; + } + } + signal::SIGTRAP => { + // should only trigger on malloc-related calls + if let Err(ret) = handle_sigtrap(pid, &mut libc_events, libc_vals) { + retcode = ret; + break 'listen; + } + } + _ => { + eprintln!( + "Process unexpectedly stopped at {signal}; continuing..." + ); + // In case we're not tracing + if ptrace::syscall(pid, None).is_err() { + signal::kill(pid, signal::SIGCONT).unwrap(); + } + } + } + } + // Should never trigger, but it's here just in case + wait::WaitStatus::PtraceEvent(pid, signal, evt) => { + eprintln!("Got unexpected event {evt} with {signal}; continuing..."); + ptrace::syscall(pid, None).unwrap(); + } + // Entering or exiting a syscall + wait::WaitStatus::PtraceSyscall(pid) => { + let regs = ptrace::getregs(pid).unwrap(); + match regs.syscall_nr().to_i64() { + libc::SYS_mmap => { + // No need for a discrete fn here, it's very tiny. + // The length needs to be a multiple of the pagesize anyways so + // we can just assume it and the syscall will error if it's not + let pg_count = regs.arg2().to_u64().strict_div(page_size); + ptrace::syscall(pid, None).unwrap(); + match wait_for_syscall(pid, libc::SYS_mmap) { + Ok(regs) => { + // We *want* this to wrap + #[expect(clippy::as_conversions)] + if regs.retval() as isize > 0 { + let addr = regs.retval().to_u64(); + // Only push if the mmap was successful + for i in 0..pg_count { + mmap_events.push(MmapEvent::Mmap( + addr.strict_add(i.strict_mul(page_size)), + )); + } + } + } + Err(ret) => { + retcode = ret; + break 'listen; + } + } + } + libc::SYS_munmap => { + if let Err(ret) = + handle_munmap(pid, regs, &mut mmap_events, page_size) + { + retcode = ret; + break 'listen; + } + } + // TODO: handle brk/sbrk + // or not, using sbrk in 2025 means you deserve UB + // Also maybe intercept and prevent fork()? + _ => (), + } + + ptrace::syscall(pid, None).unwrap(); + } + _ => (), + }, + ExecEvent::Request(trace_request) => + match trace_request { + TraceRequest::BeginFfi(ch_info) => { + ch_pages = ch_info.page_ptrs; + ch_stack = Some(ch_info.stack_ptr); + main_pid = unistd::Pid::from_raw(ch_info.pid); + + // Trace everything possibly dangerous just to be safe + let options = ptrace::Options::PTRACE_O_TRACESYSGOOD + | ptrace::Options::PTRACE_O_TRACECLONE + | ptrace::Options::PTRACE_O_TRACEFORK; + // `seize` won't issue another SIGSTOP so we wait for the one that's + // baked into start_ffi() + ptrace::seize(main_pid, options).unwrap(); + if let Err(ret) = wait_for_signal(main_pid, signal::SIGSTOP, false) { + retcode = ret; + break 'listen; + } + + // Now overwrite the libc bits we care about + libc_vals.overwrite_all(main_pid, BREAKPT_INSTR.to_i64()).unwrap(); + + ptrace::syscall(main_pid, None).unwrap(); + } + + TraceRequest::EndFfi => { + // We can't do most ptrace things (including detaching) + // if the tracee isn't stopped, so do that + signal::kill(main_pid, signal::SIGSTOP).unwrap(); + + // Hand over the access info we got + t_event + .send(( + MemEvents { acc_events, mmap_events, libc_events }, + ch_stack.unwrap(), + )) + .unwrap(); + acc_events = Vec::new(); + mmap_events = Vec::new(); + libc_events = Vec::new(); + ch_stack = None; + + if let Err(ret) = wait_for_signal(main_pid, signal::SIGSTOP, false) { + retcode = ret; + break 'listen; + } + + libc_vals.restore(main_pid).unwrap(); + ptrace::detach(main_pid, None).unwrap(); + signal::kill(main_pid, signal::SIGCONT).unwrap(); + } + }, + ExecEvent::Died(child_code) => { + if child_code != 0 { + eprintln!("Process exited with code {child_code}"); + retcode = child_code; + } + break 'listen; + } + } + } + + std::process::exit(retcode); +} + +/// Spawns a Capstone disassembler for the host architecture. +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() + } + #[cfg(target_arch = "arm")] + { + cs_pre.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 and end of FFI. +fn wait_for_signal( + pid: unistd::Pid, + wait_signal: signal::Signal, + init_cont: bool, +) -> Result<(), i32> { + if init_cont { + ptrace::cont(pid, None).unwrap(); + } + // Repeatedly call `waitpid` until we get the signal we want, or the process dies + loop { + let stat = wait::waitpid(pid, None).unwrap(); + let signal = match stat { + wait::WaitStatus::Exited(_, status) => return Err(status), + wait::WaitStatus::Signaled(_, signal, _) => signal, + wait::WaitStatus::Stopped(_, signal) => signal, + wait::WaitStatus::PtraceEvent(_, signal, _) => signal, + _ => { + ptrace::cont(pid, None).unwrap(); + continue; + } + }; + if signal == wait_signal { + break; + } else { + match ptrace::cont(pid, None) { + Ok(_) => (), + Err(e) => + match e { + // waitpid sometimes lies about death status... + nix::errno::Errno::ESRCH => return Err(0), + _ => panic!("Cannot continue process: signal {signal}, err {e}"), + }, + } + } + } + Ok(()) +} + +/// Waits for the child to return from its current syscall, grabbing its registers. +fn wait_for_syscall(pid: unistd::Pid, syscall: i64) -> Result { + loop { + ptrace::syscall(pid, None).unwrap(); + let stat = wait::waitpid(pid, None).unwrap(); + match stat { + wait::WaitStatus::Exited(_, status) => return Err(status), + wait::WaitStatus::PtraceSyscall(pid) => { + let regs = ptrace::getregs(pid).unwrap(); + if regs.syscall_nr().to_i64() == syscall { + return Ok(regs); + } else { + panic!("Missed syscall while waiting for it to return: id {syscall}"); + } + } + _ => (), + } + } +} + +fn handle_munmap( + pid: unistd::Pid, + regs: libc::user_regs_struct, + mmap_events: &mut Vec, + page_size: u64, +) -> Result<(), i32> { + // The unmap call might hit multiple mappings we've saved, + // or overlap with them partially (or both) + let um_addr = regs.arg1().to_u64(); + let um_count = regs.arg2().to_u64().strict_div(page_size); + //let um_end = um_start.strict_add(um_len); + let mut idxes = vec![]; + let mut to_append = vec![]; + for (idx, &mp) in mmap_events + .iter() + .filter_map(|mp| { + match mp { + MmapEvent::Mmap(addr) => Some(addr), + MmapEvent::Munmap(_) => None, + } + }) + .enumerate() + { + for i in 0..um_count { + // Either we're unmapping a page we know about, + // or this is some new page we should report back + if mp == um_addr.strict_add(i.strict_mul(page_size)) { + idxes.push(idx); + } else { + to_append.push(MmapEvent::Munmap(um_addr.strict_add(i.strict_mul(page_size)))); + } + } + } + mmap_events.append(&mut to_append); + + // We iterate thru this while removing elements so if + // it's not reversed we will mess up the mappings badly! + idxes.reverse(); + + ptrace::syscall(pid, None).unwrap(); + let regs = wait_for_syscall(pid, libc::SYS_munmap)?; + //let regs = ptrace::getregs(pid).unwrap(); + + // Again, this *should* wrap + #[expect(clippy::as_conversions)] + if regs.retval() as isize > 0 { + // Unmap succeeded, so take out the page(s) from our list. No need to + // worry about partial unmaps because we only store individual pages + for idx in idxes { + mmap_events.remove(idx); + } + } + + Ok(()) +} + +/// Grabs the access that caused a segfault and logs it down if it's to our memory, +/// or returns the an exit code if the process died (-1 if we kill it due to it +/// actually segfaulting). +fn handle_segfault( + pid: unistd::Pid, + ch_pages: &[u64], + ch_stack: usize, + page_size: u64, + cs: &capstone::Capstone, + acc_events: &mut Vec, +) -> Result<(), i32> { + /// This is just here to not pollute the main namespace with capstone::prelude::* + /// and so that we can get a Result instead of just unwrapping on error + #[inline] + fn capstone_disassemble( + instr: &[u8], + addr: u64, + cs: &capstone::Capstone, + acc_events: &mut Vec, + ) -> capstone::CsResult<()> { + use capstone::prelude::*; + + 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 { + arch::ArchOperand::X86Operand(x86_operand) => { + let size: u64 = x86_operand.size.into(); + match x86_operand.op_type { + arch::x86::X86OperandType::Mem(_) => { + // 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(addr..addr.strict_add(size))); + } + if acc_ty.is_writable() { + acc_events.push(AccessEvent::Write(addr..addr.strict_add(size))); + } + } + _ => (), + } + } + arch::ArchOperand::Arm64Operand(arm64_operand) => { + // Annoyingly, we don't get the size here, so just be pessimistic for now + match arm64_operand.op_type { + arch::arm64::Arm64OperandType::Mem(_) => { + // 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 + + // Also FIXME: We do get some info on whether this + // is a vector instruction, maybe we can limit the + // max size based on that? + 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), + )); + } + _ => (), + } + } + arch::ArchOperand::ArmOperand(arm_operand) => + match arm_operand.op_type { + arch::arm::ArmOperandType::Mem(_) => { + let acc_ty = arm_operand.access.unwrap(); + if acc_ty.is_readable() { + acc_events.push(AccessEvent::Read( + addr..addr.strict_add(ARCH_MAX_ACCESS_SIZE), + )); + } + if acc_ty.is_writable() { + acc_events.push(AccessEvent::Write( + addr..addr.strict_add(ARCH_MAX_ACCESS_SIZE), + )); + } + } + _ => (), + }, + arch::ArchOperand::RiscVOperand(_risc_voperand) => todo!(), + _ => unimplemented!(), + } + } + + Ok(()) + } + + let siginfo = ptrace::getsiginfo(pid).unwrap(); + let addr = unsafe { siginfo.si_addr().addr().to_u64() }; + 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); + + ptrace::write(pid, (&raw mut PAGE_ADDR).cast(), libc::c_long::try_from(page_addr).unwrap()) + .unwrap(); + ptrace::setregs(pid, new_regs).unwrap(); + + 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 + let _ = wait::waitpid(pid, None).unwrap(); + + // 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(); + let diff = (ip_poststep.strict_sub(ip_prestep)).div_ceil(8); + let instr = (ip_prestep..ip_prestep.strict_add(diff)).fold(vec![], |mut ret, ip| { + 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 + // For now this will mark down e.g. the same area being read multiple + // times, but that's still correct even if a bit inefficient + if capstone_disassemble(&instr, addr, cs, acc_events).is_err() { + // Read goes first bc 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 { + 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(-1) + } +} + +/// Intercept the allocation/deallocation that happened upon calling `malloc` +/// or similar, logging them down. If the child dies, its return code is returned +/// as an error. +fn handle_sigtrap( + pid: unistd::Pid, + libc_events: &mut Vec, + libc_vals: MagicLibcValues, +) -> Result<(), i32> { + let regs = ptrace::getregs(pid).unwrap(); + match regs.ip().strict_sub(1) { + // malloc + a if a == libc_vals.malloc_addr => { + // Grab the size from registers and save it if the call is successful + let size = regs.arg1().to_u64(); + if let Ok(ptr) = + intercept_retptr(pid, regs, libc_vals.malloc_addr, libc_vals.malloc_bytes)? + .try_into() + { + libc_events.push(LibcEvent::Malloc(ptr..ptr.strict_add(size))); + } + } + // realloc + a if a == libc_vals.realloc_addr => { + let old_ptr = regs.arg1().to_u64(); + let size = regs.arg2().to_u64(); + let pos = libc_events.iter().position(|rg| { + match rg { + LibcEvent::Malloc(rg) => rg.start <= old_ptr && old_ptr < rg.end, + LibcEvent::Free(_) => false, + } + }); + if let Ok(ptr) = + intercept_retptr(pid, regs, libc_vals.realloc_addr, libc_vals.realloc_bytes)? + .try_into() + { + if let Some(pos) = pos { + // Freeing something we spotted during this run, so just pretend + // it never happened + libc_events.remove(pos); + } else { + // Or it's removing a preexisting pointer, so we need to do both + libc_events.push(LibcEvent::Free(old_ptr)); + } + // Make sure it's ordered right! This goes at the end + libc_events.push(LibcEvent::Malloc(ptr..ptr.strict_add(size))); + } + } + // free + a if a == libc_vals.free_addr => { + let old_ptr = regs.arg1().to_u64(); + let pos = libc_events.iter().position(|rg| { + match rg { + LibcEvent::Malloc(rg) => rg.start <= old_ptr && old_ptr < rg.end, + LibcEvent::Free(_) => false, + } + }); + // This can lead to double-frees, but that's on the C code... + // No real way for us to catch it here + if let Some(pos) = pos { + // Same as for realloc + libc_events.remove(pos); + } else { + libc_events.push(LibcEvent::Free(old_ptr)); + } + // Return value here doesn't matter + intercept_retptr(pid, regs, libc_vals.free_addr, libc_vals.free_bytes)?; + } + // This should almost definitely never happen, but better safe than sorry + a => { + eprintln!("Process got an unexpected SIGTRAP at addr {a:#018x?}; continuing..."); + ptrace::syscall(pid, None).unwrap(); + } + } + + Ok(()) +} + +/// Gets the pointer or error value returned by the `libc` allocation functions +/// upon their being called. `fn_addr` should be the address of the respective +/// function, with `fn_bytes` being the original bytes it held before being +/// overwritten. +fn intercept_retptr( + pid: unistd::Pid, + mut regs: libc::user_regs_struct, + fn_addr: usize, + fn_bytes: i64, +) -> Result { + // Outline: + // - Move instr ptr back before the sigtrap happened + // - Restore the function to what it's supposed to be + // - Change the function we're returning to so it gives us a sigtrap + // - Catch it there + // - Get the register-sized return value + // - Patch the function back so it traps as before + regs.set_ip(regs.ip().strict_sub(1)); + // Again, just need to keep the same bit pattern + #[expect(clippy::as_conversions)] + let ret_addr: usize = + ptrace::read(pid, std::ptr::without_provenance_mut(regs.sp())).unwrap() as _; + let ret_bytes = ptrace::read(pid, std::ptr::without_provenance_mut(ret_addr)).unwrap(); + + ptrace::write( + pid, + std::ptr::without_provenance_mut(ret_addr), + BREAKPT_INSTR.try_into().unwrap(), + ) + .unwrap(); + // This one we did technically expose provenance for but it's in a different process anyways, so... + ptrace::write(pid, std::ptr::without_provenance_mut(fn_addr), fn_bytes).unwrap(); + ptrace::setregs(pid, regs).unwrap(); + wait_for_signal(pid, signal::SIGTRAP, true)?; + + // now we're getting the return hopefully + let mut regs = ptrace::getregs(pid).unwrap(); + #[expect(clippy::as_conversions)] + let ptr = regs.retval() as isize; // ! + regs.set_ip(regs.ip().strict_sub(1)); + ptrace::write( + pid, + std::ptr::without_provenance_mut(fn_addr), + BREAKPT_INSTR.try_into().unwrap(), + ) + .unwrap(); + ptrace::write(pid, std::ptr::without_provenance_mut(ret_addr), ret_bytes).unwrap(); + ptrace::setregs(pid, regs).unwrap(); + + ptrace::syscall(pid, None).unwrap(); + Ok(ptr) +} + +// We only get dropped into these functions via offsetting the instr pointer +// manually, so we *must not ever* unwind from it + +/// 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, +/// and `PAGE_SIZE` should be the host pagesize. +pub unsafe extern "C" fn mempr_off() { + unsafe { + if libc::mprotect( + PAGE_ADDR, + PAGE_SIZE.try_into().unwrap_unchecked(), + libc::PROT_READ | libc::PROT_WRITE, + ) != 0 + { + std::process::exit(-20); + } + // TODO: Allow doing 2 consecutive pages at once if the next page is + // also owned by the same machine, since accesses might span the page + // boundary! + } + // If this fails somehow we're doomed + if signal::raise(signal::SIGSTOP).is_err() { + std::process::exit(-21); + } +} + +/// Reenables protection on the page set by `PAGE_ADDR`. +/// +/// SAFETY: See `mempr_off()`. +pub unsafe extern "C" fn mempr_on() { + unsafe { + if libc::mprotect(PAGE_ADDR, PAGE_SIZE.try_into().unwrap_unchecked(), libc::PROT_NONE) != 0 + { + std::process::exit(-22); + } + } + if signal::raise(signal::SIGSTOP).is_err() { + std::process::exit(-23); + } +}