Merge pull request #4401 from nia-e/barebones-ptrace

trace: add barebones ptrace setup
This commit is contained in:
Oli Scherer 2025-06-18 09:55:53 +00:00 committed by GitHub
commit e857c655ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 938 additions and 39 deletions

View file

@ -80,6 +80,15 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "bitflags"
version = "2.9.0"
@ -150,6 +159,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.40"
@ -224,7 +239,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys",
"windows-sys 0.59.0",
]
[[package]]
@ -243,7 +258,7 @@ dependencies = [
"libc",
"once_cell",
"unicode-width 0.2.0",
"windows-sys",
"windows-sys 0.59.0",
]
[[package]]
@ -298,7 +313,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys",
"windows-sys 0.59.0",
]
[[package]]
@ -314,7 +329,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
dependencies = [
"libc",
"windows-sys",
"windows-sys 0.59.0",
]
[[package]]
@ -333,6 +348,12 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "generic-array"
version = "0.14.7"
@ -400,6 +421,25 @@ dependencies = [
"generic-array",
]
[[package]]
name = "ipc-channel"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb8251fb7bcd9ccd3725ed8deae9fe7db8e586495c9eb5b0c52e6233e5e75ea"
dependencies = [
"bincode",
"crossbeam-channel",
"fnv",
"lazy_static",
"libc",
"mio",
"rand 0.8.5",
"serde",
"tempfile",
"uuid",
"windows",
]
[[package]]
name = "itoa"
version = "1.0.15"
@ -533,6 +573,18 @@ dependencies = [
"adler",
]
[[package]]
name = "mio"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
]
[[package]]
name = "miri"
version = "0.1.0"
@ -544,19 +596,34 @@ dependencies = [
"colored",
"directories",
"getrandom 0.3.2",
"ipc-channel",
"libc",
"libffi",
"libloading",
"measureme",
"nix",
"rand 0.9.0",
"regex",
"rustc_version",
"serde",
"smallvec",
"tempfile",
"tikv-jemalloc-sys",
"ui_test",
]
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -748,6 +815,8 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
@ -757,11 +826,21 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
dependencies = [
"rand_chacha",
"rand_chacha 0.9.0",
"rand_core 0.9.3",
"zerocopy",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
@ -777,6 +856,9 @@ name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.15",
]
[[package]]
name = "rand_core"
@ -879,7 +961,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
"windows-sys 0.59.0",
]
[[package]]
@ -993,7 +1075,7 @@ dependencies = [
"getrandom 0.3.2",
"once_cell",
"rustix",
"windows-sys",
"windows-sys 0.59.0",
]
[[package]]
@ -1147,6 +1229,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "uuid"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [
"getrandom 0.3.2",
]
[[package]]
name = "valuable"
version = "0.1.1"
@ -1241,6 +1332,79 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "windows"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
dependencies = [
"windows-core",
"windows-targets",
]
[[package]]
name = "windows-core"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
"windows-implement",
"windows-interface",
"windows-result",
"windows-strings",
"windows-targets",
]
[[package]]
name = "windows-implement"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result",
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.59.0"

View file

@ -40,6 +40,11 @@ libc = "0.2"
libffi = "4.0.0"
libloading = "0.8"
[target.'cfg(target_os = "linux")'.dependencies]
nix = { version = "0.30.1", features = ["mman", "ptrace", "signal"] }
ipc-channel = "0.19.0"
serde = { version = "1.0.219", features = ["derive"] }
[dev-dependencies]
ui_test = "0.29.1"
colored = "2"

View file

@ -419,6 +419,11 @@ to Miri failing to detect cases of undefined behavior in a program.
Finally, the flag is **unsound** in the sense that Miri stops tracking details such as
initialization and provenance on memory shared with native code, so it is easily possible to write
code that has UB which is missed by Miri.
* `-Zmiri-force-old-native-lib-mode` disables the WIP improved native code access tracking. If for
whatever reason enabling native calls leads to odd behaviours or causes Miri to panic, disabling
the tracer *might* fix this. This will likely be removed once the tracer has been adequately
battle-tested. Note that this flag is only meaningful on Linux systems; other Unixes (currently)
exclusively use the old native-lib code.
* `-Zmiri-measureme=<name>` enables `measureme` profiling for the interpreted program.
This can be used to find which parts of your program are executing slowly under Miri.
The profile is written out to a file inside a directory called `<name>`, and can be processed

View file

@ -266,6 +266,18 @@ impl IsolatedAlloc {
alloc::dealloc(ptr, layout);
}
}
/// Returns a vector of page addresses managed by the allocator.
pub fn pages(&self) -> Vec<usize> {
let mut pages: Vec<_> =
self.page_ptrs.clone().into_iter().map(|p| p.expose_provenance()).collect();
for (ptr, size) in &self.huge_ptrs {
for i in 0..size / self.page_size {
pages.push(ptr.expose_provenance().strict_add(i * self.page_size));
}
}
pages
}
}
#[cfg(test)]

View file

@ -227,10 +227,11 @@ impl rustc_driver::Callbacks for MiriCompilerCalls {
} else {
let return_code = miri::eval_entry(tcx, entry_def_id, entry_type, &config, None)
.unwrap_or_else(|| {
#[cfg(target_os = "linux")]
miri::register_retcode_sv(rustc_driver::EXIT_FAILURE);
tcx.dcx().abort_if_errors();
rustc_driver::EXIT_FAILURE
});
std::process::exit(return_code);
}
@ -722,6 +723,8 @@ fn main() {
} else {
show_error!("-Zmiri-native-lib `{}` does not exist", filename);
}
} else if arg == "-Zmiri-force-old-native-lib-mode" {
miri_config.force_old_native_lib = true;
} else if let Some(param) = arg.strip_prefix("-Zmiri-num-cpus=") {
let num_cpus = param
.parse::<u32>()
@ -792,6 +795,16 @@ fn main() {
debug!("rustc arguments: {:?}", rustc_args);
debug!("crate arguments: {:?}", miri_config.args);
#[cfg(target_os = "linux")]
if !miri_config.native_lib.is_empty() && !miri_config.force_old_native_lib {
// FIXME: This should display a diagnostic / warning on error
// SAFETY: If any other threads exist at this point (namely for the ctrlc
// handler), they will not interact with anything on the main rustc/Miri
// thread in an async-signal-unsafe way such as by accessing shared
// semaphores, etc.; the handler only calls `sleep()` and `exit()`, which
// are async-signal-safe, as is accessing atomics
let _ = unsafe { miri::init_sv() };
}
run_compiler_and_exit(
&rustc_args,
&mut MiriCompilerCalls::new(miri_config, many_seeds, genmc_config),

View file

@ -133,6 +133,7 @@ pub enum NonHaltingDiagnostic {
details: bool,
},
NativeCallSharedMem,
NativeCallNoTrace,
WeakMemoryOutdatedLoad {
ptr: Pointer,
},
@ -629,6 +630,8 @@ impl<'tcx> MiriMachine<'tcx> {
Int2Ptr { .. } => ("integer-to-pointer cast".to_string(), DiagLevel::Warning),
NativeCallSharedMem =>
("sharing memory with a native function".to_string(), DiagLevel::Warning),
NativeCallNoTrace =>
("unable to trace native code memory accesses".to_string(), DiagLevel::Warning),
ExternTypeReborrow =>
("reborrow of reference to `extern type`".to_string(), DiagLevel::Warning),
CreatedPointerTag(..)
@ -664,6 +667,10 @@ impl<'tcx> MiriMachine<'tcx> {
format!("progress report: current operation being executed is here"),
Int2Ptr { .. } => format!("integer-to-pointer cast"),
NativeCallSharedMem => format!("sharing memory with a native function called via FFI"),
NativeCallNoTrace =>
format!(
"sharing memory with a native function called via FFI, and unable to use ptrace"
),
WeakMemoryOutdatedLoad { ptr } =>
format!("weak memory emulation: outdated value returned from load at {ptr}"),
ExternTypeReborrow =>
@ -710,6 +717,22 @@ impl<'tcx> MiriMachine<'tcx> {
v
}
NativeCallSharedMem => {
vec![
note!(
"when memory is shared with a native function call, Miri can only track initialisation and provenance on a best-effort basis"
),
note!(
"in particular, Miri assumes that the native call initializes all memory it has written to"
),
note!(
"Miri also assumes that any part of this memory may be a pointer that is permitted to point to arbitrary exposed memory"
),
note!(
"what this means is that Miri will easily miss Undefined Behavior related to incorrect usage of this shared memory, so you should not take a clean Miri run as a signal that your FFI code is UB-free"
),
]
}
NativeCallNoTrace => {
vec![
note!(
"when memory is shared with a native function call, Miri stops tracking initialization and provenance for that memory"
@ -723,6 +746,10 @@ impl<'tcx> MiriMachine<'tcx> {
note!(
"what this means is that Miri will easily miss Undefined Behavior related to incorrect usage of this shared memory, so you should not take a clean Miri run as a signal that your FFI code is UB-free"
),
#[cfg(target_os = "linux")]
note!(
"this is normally partially mitigated, but either -Zmiri-force-old-native-lib-mode was passed or ptrace is disabled on your system"
),
]
}
ExternTypeReborrow => {

View file

@ -150,6 +150,8 @@ pub struct MiriConfig {
pub retag_fields: RetagFields,
/// The location of the shared object files to load when calling external functions
pub native_lib: Vec<PathBuf>,
/// Whether to force using the old native lib behaviour even if ptrace might be supported.
pub force_old_native_lib: bool,
/// Run a garbage collector for BorTags every N basic blocks.
pub gc_interval: u32,
/// The number of CPUs to be reported by miri.
@ -199,6 +201,7 @@ impl Default for MiriConfig {
report_progress: None,
retag_fields: RetagFields::Yes,
native_lib: vec![],
force_old_native_lib: false,
gc_interval: 10_000,
num_cpus: 1,
page_size: None,

View file

@ -99,6 +99,9 @@ pub use rustc_const_eval::interpret::{self, AllocMap, Provenance as _};
use rustc_middle::{bug, span_bug};
use tracing::{info, trace};
#[cfg(target_os = "linux")]
pub use crate::shims::trace::{init_sv, register_retcode_sv};
// Type aliases that set the provenance parameter.
pub type Pointer = interpret::Pointer<Option<machine::Provenance>>;
pub type StrictPointer = interpret::Pointer<machine::Provenance>;

View file

@ -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};

View file

@ -1,5 +1,7 @@
//! Implements calling functions from a native library.
use std::ops::Deref;
#[cfg(target_os = "linux")]
use std::{cell::RefCell, rc::Rc};
use libffi::high::call as ffi;
use libffi::low::CodePtr;
@ -8,8 +10,16 @@ use rustc_middle::mir::interpret::Pointer;
use rustc_middle::ty::{self as ty, IntTy, UintTy};
use rustc_span::Symbol;
#[cfg(target_os = "linux")]
use crate::alloc::isolated_alloc::IsolatedAlloc;
use crate::*;
#[cfg(target_os = "linux")]
type CallResult<'tcx> =
InterpResult<'tcx, (ImmTy<'tcx>, Option<shims::trace::messages::MemEvents>)>;
#[cfg(not(target_os = "linux"))]
type CallResult<'tcx> = InterpResult<'tcx, (ImmTy<'tcx>, Option<!>)>;
impl<'tcx> EvalContextExtPriv<'tcx> for crate::MiriInterpCx<'tcx> {}
trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {
/// Call native host function and return the output as an immediate.
@ -19,8 +29,13 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {
dest: &MPlaceTy<'tcx>,
ptr: CodePtr,
libffi_args: Vec<libffi::high::Arg<'a>>,
) -> InterpResult<'tcx, ImmTy<'tcx>> {
) -> CallResult<'tcx> {
let this = self.eval_context_mut();
#[cfg(target_os = "linux")]
let alloc = this.machine.allocator.clone();
#[cfg(not(target_os = "linux"))]
let alloc = ();
let maybe_memevents;
// Call the function (`ptr`) with arguments `libffi_args`, and obtain the return value
// as the specified primitive integer type
@ -30,60 +45,71 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {
// Unsafe because of the call to native code.
// Because this is calling a C function it is not necessarily sound,
// but there is no way around this and we've checked as much as we can.
let x = unsafe { ffi::call::<i8>(ptr, libffi_args.as_slice()) };
Scalar::from_i8(x)
let x = unsafe { do_native_call::<i8>(ptr, libffi_args.as_slice(), alloc) };
maybe_memevents = x.1;
Scalar::from_i8(x.0)
}
ty::Int(IntTy::I16) => {
let x = unsafe { ffi::call::<i16>(ptr, libffi_args.as_slice()) };
Scalar::from_i16(x)
let x = unsafe { do_native_call::<i16>(ptr, libffi_args.as_slice(), alloc) };
maybe_memevents = x.1;
Scalar::from_i16(x.0)
}
ty::Int(IntTy::I32) => {
let x = unsafe { ffi::call::<i32>(ptr, libffi_args.as_slice()) };
Scalar::from_i32(x)
let x = unsafe { do_native_call::<i32>(ptr, libffi_args.as_slice(), alloc) };
maybe_memevents = x.1;
Scalar::from_i32(x.0)
}
ty::Int(IntTy::I64) => {
let x = unsafe { ffi::call::<i64>(ptr, libffi_args.as_slice()) };
Scalar::from_i64(x)
let x = unsafe { do_native_call::<i64>(ptr, libffi_args.as_slice(), alloc) };
maybe_memevents = x.1;
Scalar::from_i64(x.0)
}
ty::Int(IntTy::Isize) => {
let x = unsafe { ffi::call::<isize>(ptr, libffi_args.as_slice()) };
Scalar::from_target_isize(x.try_into().unwrap(), this)
let x = unsafe { do_native_call::<isize>(ptr, libffi_args.as_slice(), alloc) };
maybe_memevents = x.1;
Scalar::from_target_isize(x.0.try_into().unwrap(), this)
}
// uints
ty::Uint(UintTy::U8) => {
let x = unsafe { ffi::call::<u8>(ptr, libffi_args.as_slice()) };
Scalar::from_u8(x)
let x = unsafe { do_native_call::<u8>(ptr, libffi_args.as_slice(), alloc) };
maybe_memevents = x.1;
Scalar::from_u8(x.0)
}
ty::Uint(UintTy::U16) => {
let x = unsafe { ffi::call::<u16>(ptr, libffi_args.as_slice()) };
Scalar::from_u16(x)
let x = unsafe { do_native_call::<u16>(ptr, libffi_args.as_slice(), alloc) };
maybe_memevents = x.1;
Scalar::from_u16(x.0)
}
ty::Uint(UintTy::U32) => {
let x = unsafe { ffi::call::<u32>(ptr, libffi_args.as_slice()) };
Scalar::from_u32(x)
let x = unsafe { do_native_call::<u32>(ptr, libffi_args.as_slice(), alloc) };
maybe_memevents = x.1;
Scalar::from_u32(x.0)
}
ty::Uint(UintTy::U64) => {
let x = unsafe { ffi::call::<u64>(ptr, libffi_args.as_slice()) };
Scalar::from_u64(x)
let x = unsafe { do_native_call::<u64>(ptr, libffi_args.as_slice(), alloc) };
maybe_memevents = x.1;
Scalar::from_u64(x.0)
}
ty::Uint(UintTy::Usize) => {
let x = unsafe { ffi::call::<usize>(ptr, libffi_args.as_slice()) };
Scalar::from_target_usize(x.try_into().unwrap(), this)
let x = unsafe { do_native_call::<usize>(ptr, libffi_args.as_slice(), alloc) };
maybe_memevents = x.1;
Scalar::from_target_usize(x.0.try_into().unwrap(), this)
}
// Functions with no declared return type (i.e., the default return)
// have the output_type `Tuple([])`.
ty::Tuple(t_list) if (*t_list).deref().is_empty() => {
unsafe { ffi::call::<()>(ptr, libffi_args.as_slice()) };
return interp_ok(ImmTy::uninit(dest.layout));
let (_, mm) = unsafe { do_native_call::<()>(ptr, libffi_args.as_slice(), alloc) };
return interp_ok((ImmTy::uninit(dest.layout), mm));
}
ty::RawPtr(..) => {
let x = unsafe { ffi::call::<*const ()>(ptr, libffi_args.as_slice()) };
let ptr = Pointer::new(Provenance::Wildcard, Size::from_bytes(x.addr()));
let x = unsafe { do_native_call::<*const ()>(ptr, libffi_args.as_slice(), alloc) };
maybe_memevents = x.1;
let ptr = Pointer::new(Provenance::Wildcard, Size::from_bytes(x.0.addr()));
Scalar::from_pointer(ptr, this)
}
_ => throw_unsup_format!("unsupported return type for native call: {:?}", link_name),
};
interp_ok(ImmTy::from_scalar(scalar, dest.layout))
interp_ok((ImmTy::from_scalar(scalar, dest.layout), maybe_memevents))
}
/// Get the pointer to the function of the specified name in the shared object file,
@ -179,6 +205,13 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
// The first time this happens, print a warning.
if !this.machine.native_call_mem_warned.replace(true) {
// Newly set, so first time we get here.
#[cfg(target_os = "linux")]
if shims::trace::Supervisor::poll() {
this.emit_diagnostic(NonHaltingDiagnostic::NativeCallSharedMem);
} else {
this.emit_diagnostic(NonHaltingDiagnostic::NativeCallNoTrace);
}
#[cfg(not(target_os = "linux"))]
this.emit_diagnostic(NonHaltingDiagnostic::NativeCallSharedMem);
}
@ -196,12 +229,55 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
.collect::<Vec<libffi::high::Arg<'_>>>();
// Call the function and store output, depending on return type in the function signature.
let ret = this.call_native_with_args(link_name, dest, code_ptr, libffi_args)?;
let (ret, _) = this.call_native_with_args(link_name, dest, code_ptr, libffi_args)?;
this.write_immediate(*ret, dest)?;
interp_ok(true)
}
}
/// Performs the actual native call, returning the result and the events that
/// the supervisor detected (if any).
///
/// SAFETY: See `libffi::fii::call`.
#[cfg(target_os = "linux")]
unsafe fn do_native_call<T: libffi::high::CType>(
ptr: CodePtr,
args: &[ffi::Arg<'_>],
alloc: Option<Rc<RefCell<IsolatedAlloc>>>,
) -> (T, Option<shims::trace::messages::MemEvents>) {
use shims::trace::Supervisor;
unsafe {
if let Some(alloc) = alloc {
// SAFETY: We don't touch the machine memory past this point
let (guard, stack_ptr) = Supervisor::start_ffi(alloc.clone());
// SAFETY: Upheld by caller
let ret = ffi::call(ptr, args);
// SAFETY: We got the guard and stack pointer from start_ffi, and
// the allocator is the same
(ret, Supervisor::end_ffi(guard, alloc, stack_ptr))
} else {
// SAFETY: Upheld by caller
(ffi::call(ptr, args), None)
}
}
}
/// Performs the actual native call, returning the result and a `None`.
/// Placeholder for platforms that do not support the ptrace supervisor.
///
/// SAFETY: See `libffi::fii::call`.
#[cfg(not(target_os = "linux"))]
#[inline(always)]
unsafe fn do_native_call<T: libffi::high::CType>(
ptr: CodePtr,
args: &[ffi::Arg<'_>],
_alloc: (),
) -> (T, Option<!>) {
(unsafe { ffi::call(ptr, args) }, None)
}
#[derive(Debug, Clone)]
/// Enum of supported arguments to external C functions.
// We introduce this enum instead of just calling `ffi::arg` and storing a list

View file

@ -0,0 +1,238 @@
use std::cell::RefCell;
use std::rc::Rc;
use ipc_channel::ipc;
use nix::sys::{ptrace, signal};
use nix::unistd;
use super::messages::{Confirmation, MemEvents, TraceRequest};
use super::parent::{ChildListener, sv_loop};
use super::{FAKE_STACK_SIZE, StartFfiInfo};
use crate::alloc::isolated_alloc::IsolatedAlloc;
static SUPERVISOR: std::sync::Mutex<Option<Supervisor>> = std::sync::Mutex::new(None);
/// The main means of communication between the child and parent process,
/// allowing the former to send requests and get info from the latter.
pub struct Supervisor {
/// Sender for FFI-mode-related requests.
message_tx: ipc::IpcSender<TraceRequest>,
/// Used for synchronisation, allowing us to receive confirmation that the
/// parent process has handled the request from `message_tx`.
confirm_rx: ipc::IpcReceiver<Confirmation>,
/// Receiver for memory acceses that ocurred during the FFI call.
event_rx: ipc::IpcReceiver<MemEvents>,
}
/// Marker representing that an error occurred during creation of the supervisor.
#[derive(Debug)]
pub struct SvInitError;
impl Supervisor {
/// Returns `true` if the supervisor process exists, and `false` otherwise.
pub fn poll() -> bool {
SUPERVISOR.lock().unwrap().is_some()
}
/// Begins preparations for doing an FFI call. This should be called at
/// the last possible moment before entering said call. `alloc` points to
/// the allocator which handed out the memory used for this machine.
///
/// As this locks the supervisor via a mutex, no other threads may enter FFI
/// until this one returns and its guard is dropped via `end_ffi`. The
/// pointer returned should be passed to `end_ffi` to avoid a memory leak.
///
/// SAFETY: The resulting guard must be dropped *via `end_ffi`* immediately
/// after the desired call has concluded.
pub unsafe fn start_ffi(
alloc: Rc<RefCell<IsolatedAlloc>>,
) -> (std::sync::MutexGuard<'static, Option<Supervisor>>, Option<*mut [u8; FAKE_STACK_SIZE]>)
{
let mut sv_guard = SUPERVISOR.lock().unwrap();
// If the supervisor is not initialised for whatever reason, fast-fail.
// This might be desired behaviour, as even on platforms where ptracing
// is not implemented it enables us to enforce that only one FFI call
// happens at a time
let Some(sv) = sv_guard.take() else {
return (sv_guard, None);
};
// Get pointers to all the pages the supervisor must allow accesses in
// and prepare the fake stack
let page_ptrs = alloc.borrow().pages();
let raw_stack_ptr: *mut [u8; FAKE_STACK_SIZE] =
Box::leak(Box::new([0u8; FAKE_STACK_SIZE])).as_mut_ptr().cast();
let stack_ptr = raw_stack_ptr.expose_provenance();
let start_info = StartFfiInfo { page_ptrs, stack_ptr };
// Send over the info.
// NB: if we do not wait to receive a blank confirmation response, it is
// possible that the supervisor is alerted of the SIGSTOP *before* it has
// actually received the start_info, thus deadlocking! This way, we can
// enforce an ordering for these events
sv.message_tx.send(TraceRequest::StartFfi(start_info)).unwrap();
sv.confirm_rx.recv().unwrap();
*sv_guard = Some(sv);
// We need to be stopped for the supervisor to be able to make certain
// modifications to our memory - simply waiting on the recv() doesn't
// count
signal::raise(signal::SIGSTOP).unwrap();
(sv_guard, Some(raw_stack_ptr))
}
/// Undoes FFI-related preparations, allowing Miri to continue as normal, then
/// gets the memory accesses and changes performed during the FFI call. Note
/// that this may include some spurious accesses done by `libffi` itself in
/// the process of executing the function call.
///
/// SAFETY: The `sv_guard` and `raw_stack_ptr` passed must be the same ones
/// received by a prior call to `start_ffi`, and the allocator must be the
/// one passed to it also.
pub unsafe fn end_ffi(
mut sv_guard: std::sync::MutexGuard<'static, Option<Supervisor>>,
_alloc: Rc<RefCell<IsolatedAlloc>>,
raw_stack_ptr: Option<*mut [u8; FAKE_STACK_SIZE]>,
) -> Option<MemEvents> {
// We can't use IPC channels here to signal that FFI mode has ended,
// since they might allocate memory which could get us stuck in a SIGTRAP
// with no easy way out! While this could be worked around, it is much
// simpler and more robust to simply use the signals which are left for
// arbitrary usage. Since this will block until we are continued by the
// supervisor, we can assume past this point that everything is back to
// normal
signal::raise(signal::SIGUSR1).unwrap();
// If this is `None`, then `raw_stack_ptr` is None and does not need to
// be deallocated (and there's no need to worry about the guard, since
// it contains nothing)
let sv = sv_guard.take()?;
// SAFETY: Caller upholds that this pointer was allocated as a box with
// this type
unsafe {
drop(Box::from_raw(raw_stack_ptr.unwrap()));
}
// On the off-chance something really weird happens, don't block forever
let ret = sv
.event_rx
.try_recv_timeout(std::time::Duration::from_secs(5))
.map_err(|e| {
match e {
ipc::TryRecvError::IpcError(_) => (),
ipc::TryRecvError::Empty =>
eprintln!("Waiting for accesses from supervisor timed out!"),
}
})
.ok();
// Do *not* leave the supervisor empty, or else we might get another fork...
*sv_guard = Some(sv);
ret
}
}
/// Initialises the supervisor process. If this function errors, then the
/// supervisor process could not be created successfully; else, the caller
/// is now the child process and can communicate via `start_ffi`/`end_ffi`,
/// receiving back events through `get_events`.
///
/// # Safety
/// The invariants for `fork()` must be upheld by the caller.
pub unsafe fn init_sv() -> Result<(), SvInitError> {
// FIXME: Much of this could be reimplemented via the mitosis crate if we upstream the
// relevant missing bits
// On Linux, this will check whether ptrace is fully disabled by the Yama module.
// If Yama isn't running or we're not on Linux, we'll still error later, but
// this saves a very expensive fork call
let ptrace_status = std::fs::read_to_string("/proc/sys/kernel/yama/ptrace_scope");
if let Ok(stat) = ptrace_status {
if let Some(stat) = stat.chars().next() {
// Fast-error if ptrace is fully disabled on the system
if stat == '3' {
return Err(SvInitError);
}
}
}
// Initialise the supervisor if it isn't already, placing it into SUPERVISOR
let mut lock = SUPERVISOR.lock().unwrap();
if lock.is_some() {
return Ok(());
}
// Prepare the IPC channels we need
let (message_tx, message_rx) = ipc::channel().unwrap();
let (confirm_tx, confirm_rx) = ipc::channel().unwrap();
let (event_tx, event_rx) = ipc::channel().unwrap();
// SAFETY: Calling sysconf(_SC_PAGESIZE) is always safe and cannot error
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }.try_into().unwrap();
unsafe {
// TODO: Maybe use clone3() instead for better signalling of when the child exits?
// SAFETY: Caller upholds that only one thread exists.
match unistd::fork().unwrap() {
unistd::ForkResult::Parent { child } => {
// If somehow another thread does exist, prevent it from accessing the lock
// and thus breaking our safety invariants
std::mem::forget(lock);
// The child process is free to unwind, so we won't to avoid doubly freeing
// system resources
let init = std::panic::catch_unwind(|| {
let listener =
ChildListener { message_rx, attached: false, override_retcode: None };
// Trace as many things as possible, to be able to handle them as needed
let options = ptrace::Options::PTRACE_O_TRACESYSGOOD
| ptrace::Options::PTRACE_O_TRACECLONE
| ptrace::Options::PTRACE_O_TRACEFORK;
// Attach to the child process without stopping it
match ptrace::seize(child, options) {
// Ptrace works :D
Ok(_) => {
let code = sv_loop(listener, child, event_tx, confirm_tx, page_size)
.unwrap_err();
// If a return code of 0 is not explicitly given, assume something went
// wrong and return 1
std::process::exit(code.unwrap_or(1))
}
// Ptrace does not work and we failed to catch that
Err(_) => {
// If we can't ptrace, Miri continues being the parent
signal::kill(child, signal::SIGKILL).unwrap();
SvInitError
}
}
});
match init {
// The "Ok" case means that we couldn't ptrace
Ok(e) => return Err(e),
Err(p) => {
eprintln!("Supervisor process panicked!\n{p:?}");
std::process::exit(1);
}
}
}
unistd::ForkResult::Child => {
// Make sure we never get orphaned and stuck in SIGSTOP or similar
// SAFETY: prctl PR_SET_PDEATHSIG is always safe to call
let ret = libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM);
assert_eq!(ret, 0);
// First make sure the parent succeeded with ptracing us!
signal::raise(signal::SIGSTOP).unwrap();
// If we're the child process, save the supervisor info
*lock = Some(Supervisor { message_tx, confirm_rx, event_rx });
}
}
}
Ok(())
}
/// Instruct the supervisor process to return a particular code. Useful if for
/// whatever reason this code fails to be intercepted normally. In the case of
/// `abort_if_errors()` used in `bin/miri.rs`, the return code is erroneously
/// given as a 0 if this is not used.
pub fn register_retcode_sv(code: i32) {
let mut sv_guard = SUPERVISOR.lock().unwrap();
if let Some(sv) = sv_guard.take() {
sv.message_tx.send(TraceRequest::OverrideRetcode(code)).unwrap();
*sv_guard = Some(sv);
}
}

View file

@ -0,0 +1,57 @@
//! Houses the types that are directly sent across the IPC channels.
//!
//! The overall structure of a traced FFI call, from the child process's POV, is
//! as follows:
//! ```
//! message_tx.send(TraceRequest::StartFfi);
//! confirm_rx.recv();
//! raise(SIGSTOP);
//! /* do ffi call */
//! raise(SIGUSR1); // morally equivalent to some kind of "TraceRequest::EndFfi"
//! let events = event_rx.recv();
//! ```
//! `TraceRequest::OverrideRetcode` can be sent at any point in the above, including
//! before or after all of them.
//!
//! NB: sending these events out of order, skipping steps, etc. will result in
//! unspecified behaviour from the supervisor process, so use the abstractions
//! in `super::child` (namely `start_ffi()` and `end_ffi()`) to handle this. It is
//! trivially easy to cause a deadlock or crash by messing this up!
/// An IPC request sent by the child process to the parent.
///
/// The sender for this channel should live on the child process.
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub(super) enum TraceRequest {
/// Requests that tracing begins. Following this being sent, the child must
/// wait to receive a `Confirmation` on the respective channel and then
/// `raise(SIGSTOP)`.
///
/// To avoid possible issues while allocating memory for IPC channels, ending
/// the tracing is instead done via `raise(SIGUSR1)`.
StartFfi(super::StartFfiInfo),
/// Manually overrides the code that the supervisor will return upon exiting.
/// Once set, it is permanent. This can be called again to change the value.
OverrideRetcode(i32),
}
/// A marker type confirming that the supervisor has received the request to begin
/// tracing and is now waiting for a `SIGSTOP`.
///
/// The sender for this channel should live on the parent process.
#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub(super) struct Confirmation;
/// The final results of an FFI trace, containing every relevant event detected
/// by the tracer. Sent by the supervisor after receiving a `SIGUSR1` signal.
///
/// The sender for this channel should live on the parent process.
#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct MemEvents {
/// An ordered list of memory accesses that occurred. These should be assumed
/// to be overcautious; that is, if the size of an access is uncertain it is
/// pessimistically rounded up, and if the type (read/write/both) is uncertain
/// it is reported as whatever would be safest to assume; i.e. a read + maybe-write
/// becomes a read + write, etc.
pub acc_events: Vec<super::AccessEvent>,
}

View file

@ -0,0 +1,31 @@
mod child;
pub mod messages;
mod parent;
use std::ops::Range;
pub use self::child::{Supervisor, init_sv, register_retcode_sv};
/// The size used for the array into which we can move the stack pointer.
const FAKE_STACK_SIZE: usize = 1024;
/// Information needed to begin tracing.
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
struct StartFfiInfo {
/// A vector of page addresses. These should have been automatically obtained
/// with `IsolatedAlloc::pages` and prepared with `IsolatedAlloc::prepare_ffi`.
page_ptrs: Vec<usize>,
/// The address of an allocation that can serve as a temporary stack.
/// This should be a leaked `Box<[u8; FAKE_STACK_SIZE]>` cast to an int.
stack_ptr: usize,
}
/// A single memory access, conservatively overestimated
/// in case of ambiguity.
#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub enum AccessEvent {
/// A read may have occurred on no more than the specified address range.
Read(Range<usize>),
/// A write may have occurred on no more than the specified address range.
Write(Range<usize>),
}

View file

@ -0,0 +1,263 @@
use ipc_channel::ipc;
use nix::sys::{ptrace, signal, wait};
use nix::unistd;
use super::StartFfiInfo;
use super::messages::{Confirmation, MemEvents, TraceRequest};
/// The flags to use when calling `waitid()`.
/// Since bitwise OR on the nix version of these flags is implemented as a trait,
/// we can't use them directly so we do it this way
const WAIT_FLAGS: wait::WaitPidFlag =
wait::WaitPidFlag::from_bits_truncate(libc::WUNTRACED | libc::WEXITED);
/// A unified event representing something happening on the child process. Wraps
/// `nix`'s `WaitStatus` and our custom signals so it can all be done with one
/// `match` statement.
pub enum ExecEvent {
/// Child process requests that we begin monitoring it.
Start(StartFfiInfo),
/// Child requests that we stop monitoring and pass over the events we
/// detected.
End,
/// The child process with the specified pid was stopped by the given signal.
Status(unistd::Pid, signal::Signal),
/// The child process with the specified pid entered or exited a syscall.
Syscall(unistd::Pid),
/// A child process exited or was killed; if we have a return code, it is
/// specified.
Died(Option<i32>),
}
/// A listener for the FFI start info channel along with relevant state.
pub struct ChildListener {
/// The matching channel for the child's `Supervisor` struct.
pub message_rx: ipc::IpcReceiver<TraceRequest>,
/// Whether an FFI call is currently ongoing.
pub attached: bool,
/// If `Some`, overrides the return code with the given value.
pub override_retcode: Option<i32>,
}
impl Iterator for ChildListener {
type Item = ExecEvent;
// Allows us to monitor the child process by just iterating over the listener
// NB: This should never return None!
fn next(&mut self) -> Option<Self::Item> {
// Do not block if the child has nothing to report for `waitid`
let opts = WAIT_FLAGS | wait::WaitPidFlag::WNOHANG;
loop {
// Listen to any child, not just the main one. Important if we want
// to allow the C code to fork further, along with being a bit of
// defensive programming since Linux sometimes assigns threads of
// the same process different PIDs with unpredictable rules...
match wait::waitid(wait::Id::All, opts) {
Ok(stat) =>
match stat {
// Child exited normally with a specific code set
wait::WaitStatus::Exited(_, code) => {
let code = self.override_retcode.unwrap_or(code);
return Some(ExecEvent::Died(Some(code)));
}
// Child was killed by a signal, without giving a code
wait::WaitStatus::Signaled(_, _, _) =>
return Some(ExecEvent::Died(self.override_retcode)),
// Child entered a syscall. Since we're always technically
// tracing, only pass this along if we're actively
// monitoring the child
wait::WaitStatus::PtraceSyscall(pid) =>
if self.attached {
return Some(ExecEvent::Syscall(pid));
},
// Child with the given pid was stopped by the given signal.
// It's somewhat dubious when this is returned instead of
// WaitStatus::Stopped, but for our purposes they are the
// same thing.
wait::WaitStatus::PtraceEvent(pid, signal, _) =>
if self.attached {
// This is our end-of-FFI signal!
if signal == signal::SIGUSR1 {
self.attached = false;
return Some(ExecEvent::End);
} else {
return Some(ExecEvent::Status(pid, signal));
}
} else {
// Just pass along the signal
ptrace::cont(pid, signal).unwrap();
},
// Child was stopped at the given signal. Same logic as for
// WaitStatus::PtraceEvent
wait::WaitStatus::Stopped(pid, signal) =>
if self.attached {
if signal == signal::SIGUSR1 {
self.attached = false;
return Some(ExecEvent::End);
} else {
return Some(ExecEvent::Status(pid, signal));
}
} else {
ptrace::cont(pid, signal).unwrap();
},
_ => (),
},
// This case should only trigger if all children died and we
// somehow missed that, but it's best we not allow any room
// for deadlocks
Err(_) => return Some(ExecEvent::Died(None)),
}
// Similarly, do a non-blocking poll of the IPC channel
if let Ok(req) = self.message_rx.try_recv() {
match req {
TraceRequest::StartFfi(info) =>
// Should never trigger - but better to panic explicitly than deadlock!
if self.attached {
panic!("Attempting to begin FFI multiple times!");
} else {
self.attached = true;
return Some(ExecEvent::Start(info));
},
TraceRequest::OverrideRetcode(code) => self.override_retcode = Some(code),
}
}
// Not ideal, but doing anything else might sacrifice performance
std::thread::yield_now();
}
}
}
/// An error came up while waiting on the child process to do something.
#[derive(Debug)]
enum ExecError {
/// The child process died with this return code, if we have one.
Died(Option<i32>),
}
/// This is the main loop of the supervisor process. It runs in a separate
/// process from the rest of Miri (but because we fork, addresses for anything
/// created before the fork - like statics - are the same).
pub fn sv_loop(
listener: ChildListener,
init_pid: unistd::Pid,
event_tx: ipc::IpcSender<MemEvents>,
confirm_tx: ipc::IpcSender<Confirmation>,
_page_size: usize,
) -> Result<!, Option<i32>> {
// Things that we return to the child process
let mut acc_events = Vec::new();
// Memory allocated on the MiriMachine
let mut _ch_pages = Vec::new();
let mut _ch_stack = None;
// The pid of the last process we interacted with, used by default if we don't have a
// reason to use a different one
let mut curr_pid = init_pid;
// There's an initial sigstop we need to deal with
wait_for_signal(Some(curr_pid), signal::SIGSTOP, false).map_err(|e| {
match e {
ExecError::Died(code) => code,
}
})?;
ptrace::cont(curr_pid, None).unwrap();
for evt in listener {
match evt {
// start_ffi was called by the child, so prep memory
ExecEvent::Start(ch_info) => {
// All the pages that the child process is "allowed to" access
_ch_pages = ch_info.page_ptrs;
// And the fake stack it allocated for us to use later
_ch_stack = Some(ch_info.stack_ptr);
// We received the signal and are no longer in the main listener loop,
// so we can let the child move on to the end of start_ffi where it will
// raise a SIGSTOP. We need it to be signal-stopped *and waited for* in
// order to do most ptrace operations!
confirm_tx.send(Confirmation).unwrap();
// We can't trust simply calling `Pid::this()` in the child process to give the right
// PID for us, so we get it this way
curr_pid = wait_for_signal(None, signal::SIGSTOP, false).unwrap();
ptrace::syscall(curr_pid, None).unwrap();
}
// end_ffi was called by the child
ExecEvent::End => {
// Hand over the access info we traced
event_tx.send(MemEvents { acc_events }).unwrap();
// And reset our values
acc_events = Vec::new();
_ch_stack = None;
// No need to monitor syscalls anymore, they'd just be ignored
ptrace::cont(curr_pid, None).unwrap();
}
// Child process was stopped by a signal
ExecEvent::Status(pid, signal) => {
eprintln!("Process unexpectedly got {signal}; continuing...");
// In case we're not tracing
if ptrace::syscall(pid, signal).is_err() {
// If *this* fails too, something really weird happened
// and it's probably best to just panic
signal::kill(pid, signal::SIGCONT).unwrap();
}
}
// Child entered a syscall; we wait for exits inside of this, so it
// should never trigger on return from a syscall we care about
ExecEvent::Syscall(pid) => {
ptrace::syscall(pid, None).unwrap();
}
ExecEvent::Died(code) => {
return Err(code);
}
}
}
unreachable!()
}
/// Waits for `wait_signal`. If `init_cont`, it will first do a `ptrace::cont`.
/// We want to avoid that in some cases, like at the beginning of FFI.
///
/// If `pid` is `None`, only one wait will be done and `init_cont` should be false.
fn wait_for_signal(
pid: Option<unistd::Pid>,
wait_signal: signal::Signal,
init_cont: bool,
) -> Result<unistd::Pid, ExecError> {
if init_cont {
ptrace::cont(pid.unwrap(), None).unwrap();
}
// Repeatedly call `waitid` until we get the signal we want, or the process dies
loop {
let wait_id = match pid {
Some(pid) => wait::Id::Pid(pid),
None => wait::Id::All,
};
let stat = wait::waitid(wait_id, WAIT_FLAGS).map_err(|_| ExecError::Died(None))?;
let (signal, pid) = match stat {
// Report the cause of death, if we know it
wait::WaitStatus::Exited(_, code) => {
return Err(ExecError::Died(Some(code)));
}
wait::WaitStatus::Signaled(_, _, _) => return Err(ExecError::Died(None)),
wait::WaitStatus::Stopped(pid, signal) => (signal, pid),
wait::WaitStatus::PtraceEvent(pid, signal, _) => (signal, pid),
// This covers PtraceSyscall and variants that are impossible with
// the flags set (e.g. WaitStatus::StillAlive)
_ => {
ptrace::cont(pid.unwrap(), None).unwrap();
continue;
}
};
if signal == wait_signal {
return Ok(pid);
} else {
ptrace::cont(pid, None).map_err(|_| ExecError::Died(None))?;
}
}
}

View file

@ -4,8 +4,8 @@ warning: sharing memory with a native function called via FFI
LL | unsafe { print_pointer(&x) };
| ^^^^^^^^^^^^^^^^^ sharing memory with a native function
|
= help: when memory is shared with a native function call, Miri stops tracking initialization and provenance for that memory
= help: in particular, Miri assumes that the native call initializes all memory it has access to
= help: when memory is shared with a native function call, Miri can only track initialisation and provenance on a best-effort basis
= help: in particular, Miri assumes that the native call initializes all memory it has written to
= help: Miri also assumes that any part of this memory may be a pointer that is permitted to point to arbitrary exposed memory
= help: what this means is that Miri will easily miss Undefined Behavior related to incorrect usage of this shared memory, so you should not take a clean Miri run as a signal that your FFI code is UB-free
= note: BACKTRACE:

View file

@ -4,8 +4,8 @@ warning: sharing memory with a native function called via FFI
LL | unsafe { increment_int(&mut x) };
| ^^^^^^^^^^^^^^^^^^^^^ sharing memory with a native function
|
= help: when memory is shared with a native function call, Miri stops tracking initialization and provenance for that memory
= help: in particular, Miri assumes that the native call initializes all memory it has access to
= help: when memory is shared with a native function call, Miri can only track initialisation and provenance on a best-effort basis
= help: in particular, Miri assumes that the native call initializes all memory it has written to
= help: Miri also assumes that any part of this memory may be a pointer that is permitted to point to arbitrary exposed memory
= help: what this means is that Miri will easily miss Undefined Behavior related to incorrect usage of this shared memory, so you should not take a clean Miri run as a signal that your FFI code is UB-free
= note: BACKTRACE: