From 2361ebc97a5f2d96ed34ce3e07a3b601c3ad3424 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Mon, 23 Dec 2024 11:49:03 +0100 Subject: [PATCH 1/6] add -Zmiri-many-seeds flag to the driver itself --- src/bin/miri.rs | 244 +++++++++++++++++++---------- src/diagnostics.rs | 4 +- src/eval.rs | 11 +- src/shims/foreign_items.rs | 2 +- src/shims/windows/foreign_items.rs | 5 +- 5 files changed, 172 insertions(+), 94 deletions(-) diff --git a/src/bin/miri.rs b/src/bin/miri.rs index 5248c9d186..ca3704e065 100644 --- a/src/bin/miri.rs +++ b/src/bin/miri.rs @@ -26,11 +26,17 @@ extern crate rustc_span; use std::env::{self, VarError}; use std::num::NonZero; +use std::ops::Range; use std::path::PathBuf; use std::str::FromStr; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; -use miri::{BacktraceStyle, BorrowTrackerMethod, ProvenanceMode, RetagFields, ValidationMode}; +use miri::{ + BacktraceStyle, BorrowTrackerMethod, MiriConfig, ProvenanceMode, RetagFields, ValidationMode, +}; use rustc_abi::ExternAbi; +use rustc_data_structures::sync; use rustc_data_structures::sync::Lrc; use rustc_driver::Compilation; use rustc_hir::def_id::LOCAL_CRATE; @@ -52,7 +58,64 @@ use rustc_span::def_id::DefId; use tracing::debug; struct MiriCompilerCalls { - miri_config: miri::MiriConfig, + miri_config: Option, + many_seeds: Option>, +} + +impl MiriCompilerCalls { + fn new(miri_config: MiriConfig, many_seeds: Option>) -> Self { + Self { miri_config: Some(miri_config), many_seeds } + } +} + +fn entry_fn(tcx: TyCtxt<'_>) -> (DefId, EntryFnType) { + if let Some(entry_def) = tcx.entry_fn(()) { + return entry_def; + } + // Look for a symbol in the local crate named `miri_start`, and treat that as the entry point. + let sym = tcx.exported_symbols(LOCAL_CRATE).iter().find_map(|(sym, _)| { + if sym.symbol_name_for_local_instance(tcx).name == "miri_start" { Some(sym) } else { None } + }); + if let Some(ExportedSymbol::NonGeneric(id)) = sym { + let start_def_id = id.expect_local(); + let start_span = tcx.def_span(start_def_id); + + let expected_sig = ty::Binder::dummy(tcx.mk_fn_sig( + [tcx.types.isize, Ty::new_imm_ptr(tcx, Ty::new_imm_ptr(tcx, tcx.types.u8))], + tcx.types.isize, + false, + hir::Safety::Safe, + ExternAbi::Rust, + )); + + let correct_func_sig = check_function_signature( + tcx, + ObligationCause::new(start_span, start_def_id, ObligationCauseCode::Misc), + *id, + expected_sig, + ) + .is_ok(); + + if correct_func_sig { + (*id, EntryFnType::Start) + } else { + tcx.dcx().fatal( + "`miri_start` must have the following signature:\n\ + fn miri_start(argc: isize, argv: *const *const u8) -> isize", + ); + } + } else { + tcx.dcx().fatal( + "Miri can only run programs that have a main function.\n\ + Alternatively, you can export a `miri_start` function:\n\ + \n\ + #[cfg(miri)]\n\ + #[no_mangle]\n\ + fn miri_start(argc: isize, argv: *const *const u8) -> isize {\ + \n // Call the actual start function that your project implements, based on your target's conventions.\n\ + }" + ); + } } impl rustc_driver::Callbacks for MiriCompilerCalls { @@ -87,7 +150,7 @@ impl rustc_driver::Callbacks for MiriCompilerCalls { } let (entry_def_id, entry_type) = entry_fn(tcx); - let mut config = self.miri_config.clone(); + let mut config = self.miri_config.take().expect("after_analysis must only be called once"); // Add filename to `miri` arguments. config.args.insert(0, tcx.sess.io.input.filestem().to_string()); @@ -111,12 +174,31 @@ impl rustc_driver::Callbacks for MiriCompilerCalls { optimizations is usually marginal at best."); } - if let Some(return_code) = miri::eval_entry(tcx, entry_def_id, entry_type, config) { - std::process::exit(i32::try_from(return_code).expect("Return value was too large!")); + if let Some(many_seeds) = self.many_seeds.take() { + assert!(config.seed.is_none()); + sync::par_for_each_in(many_seeds, |seed| { + let mut config = config.clone(); + config.seed = Some(seed.into()); + eprintln!("Trying seed: {seed}"); + let return_code = miri::eval_entry(tcx, entry_def_id, entry_type, config) + .unwrap_or(rustc_driver::EXIT_FAILURE); + if return_code != rustc_driver::EXIT_SUCCESS { + eprintln!("FAILING SEED: {seed}"); + tcx.dcx().abort_if_errors(); // exits with a different error message + std::process::exit(return_code); + } + }); + std::process::exit(rustc_driver::EXIT_SUCCESS); + } else { + let return_code = miri::eval_entry(tcx, entry_def_id, entry_type, config) + .unwrap_or_else(|| { + tcx.dcx().abort_if_errors(); + rustc_driver::EXIT_FAILURE + }); + std::process::exit(return_code); } - tcx.dcx().abort_if_errors(); - Compilation::Stop + // Unreachable. } } @@ -241,21 +323,28 @@ fn rustc_logger_config() -> rustc_log::LoggerConfig { cfg } +/// The global logger can only be set once per process, so track +/// whether that already happened. +static LOGGER_INITED: AtomicBool = AtomicBool::new(false); + fn init_early_loggers(early_dcx: &EarlyDiagCtxt) { // Now for rustc. We only initialize `rustc` if the env var is set (so the user asked for it). // If it is not set, we avoid initializing now so that we can initialize later with our custom // settings, and *not* log anything for what happens before `miri` gets started. if env::var_os("RUSTC_LOG").is_some() { rustc_driver::init_logger(early_dcx, rustc_logger_config()); + assert!(!LOGGER_INITED.swap(true, Ordering::AcqRel)); } } fn init_late_loggers(early_dcx: &EarlyDiagCtxt, tcx: TyCtxt<'_>) { - // If `RUSTC_LOG` is not set, then `init_early_loggers` did not call - // `rustc_driver::init_logger`, so we have to do this now. - if env::var_os("RUSTC_LOG").is_none() { + // If the logger is not yet initialized, initialize it. + if !LOGGER_INITED.swap(true, Ordering::AcqRel) { rustc_driver::init_logger(early_dcx, rustc_logger_config()); } + // There's a little race condition here in many-seeds mode, where we don't wait for the thread + // that is doing the initializing. But if you want to debug things with extended logging you + // probably won't use many-seeds mode anyway. // If `MIRI_BACKTRACE` is set and `RUSTC_CTFE_BACKTRACE` is not, set `RUSTC_CTFE_BACKTRACE`. // Do this late, so we ideally only apply this to Miri's errors. @@ -270,25 +359,14 @@ fn init_late_loggers(early_dcx: &EarlyDiagCtxt, tcx: TyCtxt<'_>) { } /// Execute a compiler with the given CLI arguments and callbacks. -fn run_compiler( - mut args: Vec, - target_crate: bool, +fn run_compiler_and_exit( + args: &[String], callbacks: &mut (dyn rustc_driver::Callbacks + Send), - using_internal_features: std::sync::Arc, + using_internal_features: Arc, ) -> ! { - // Don't insert `MIRI_DEFAULT_ARGS`, in particular, `--cfg=miri`, if we are building - // a "host" crate. That may cause procedural macros (and probably build scripts) to - // depend on Miri-only symbols, such as `miri_resolve_frame`: - // https://github.com/rust-lang/miri/issues/1760 - if target_crate { - // Some options have different defaults in Miri than in plain rustc; apply those by making - // them the first arguments after the binary name (but later arguments can overwrite them). - args.splice(1..1, miri::MIRI_DEFAULT_ARGS.iter().map(ToString::to_string)); - } - // Invoke compiler, and handle return code. let exit_code = rustc_driver::catch_with_exit_code(move || { - rustc_driver::RunCompiler::new(&args, callbacks) + rustc_driver::RunCompiler::new(args, callbacks) .set_using_internal_features(using_internal_features) .run(); Ok(()) @@ -311,6 +389,18 @@ fn parse_rate(input: &str) -> Result { } } +/// Parses a seed range +/// +/// This function is used for the `-Zmiri-many-seeds` flag. It expects the range in the form +/// `..`. `` is inclusive, `` is exclusive. `` can be omitted, +/// in which case it is assumed to be `0`. +fn parse_range(val: &str) -> Result, &'static str> { + let (from, to) = val.split_once("..").ok_or("expected `from..to`")?; + let from: u32 = if from.is_empty() { 0 } else { from.parse().map_err(|_| "invalid `from`")? }; + let to: u32 = to.parse().map_err(|_| "invalid `to`")?; + Ok(from..to) +} + #[cfg(any(target_os = "linux", target_os = "macos"))] fn jemalloc_magic() { // These magic runes are copied from @@ -349,56 +439,6 @@ fn jemalloc_magic() { } } -fn entry_fn(tcx: TyCtxt<'_>) -> (DefId, EntryFnType) { - if let Some(entry_def) = tcx.entry_fn(()) { - return entry_def; - } - // Look for a symbol in the local crate named `miri_start`, and treat that as the entry point. - let sym = tcx.exported_symbols(LOCAL_CRATE).iter().find_map(|(sym, _)| { - if sym.symbol_name_for_local_instance(tcx).name == "miri_start" { Some(sym) } else { None } - }); - if let Some(ExportedSymbol::NonGeneric(id)) = sym { - let start_def_id = id.expect_local(); - let start_span = tcx.def_span(start_def_id); - - let expected_sig = ty::Binder::dummy(tcx.mk_fn_sig( - [tcx.types.isize, Ty::new_imm_ptr(tcx, Ty::new_imm_ptr(tcx, tcx.types.u8))], - tcx.types.isize, - false, - hir::Safety::Safe, - ExternAbi::Rust, - )); - - let correct_func_sig = check_function_signature( - tcx, - ObligationCause::new(start_span, start_def_id, ObligationCauseCode::Misc), - *id, - expected_sig, - ) - .is_ok(); - - if correct_func_sig { - (*id, EntryFnType::Start) - } else { - tcx.dcx().fatal( - "`miri_start` must have the following signature:\n\ - fn miri_start(argc: isize, argv: *const *const u8) -> isize", - ); - } - } else { - tcx.dcx().fatal( - "Miri can only run programs that have a main function.\n\ - Alternatively, you can export a `miri_start` function:\n\ - \n\ - #[cfg(miri)]\n\ - #[no_mangle]\n\ - fn miri_start(argc: isize, argv: *const *const u8) -> isize {\ - \n // Call the actual start function that your project implements, based on your target's conventions.\n\ - }" - ); - } -} - fn main() { #[cfg(any(target_os = "linux", target_os = "macos"))] jemalloc_magic(); @@ -431,10 +471,21 @@ fn main() { panic!("invalid `MIRI_BE_RUSTC` value: {crate_kind:?}") }; - // We cannot use `rustc_driver::main` as we need to adjust the CLI arguments. - run_compiler( - args, - target_crate, + let mut args = args; + // Don't insert `MIRI_DEFAULT_ARGS`, in particular, `--cfg=miri`, if we are building + // a "host" crate. That may cause procedural macros (and probably build scripts) to + // depend on Miri-only symbols, such as `miri_resolve_frame`: + // https://github.com/rust-lang/miri/issues/1760 + if target_crate { + // Splice in the default arguments after the program name. + // Some options have different defaults in Miri than in plain rustc; apply those by making + // them the first arguments after the binary name (but later arguments can overwrite them). + args.splice(1..1, miri::MIRI_DEFAULT_ARGS.iter().map(ToString::to_string)); + } + + // We cannot use `rustc_driver::main` as we want it to use `args` as the CLI arguments. + run_compiler_and_exit( + &args, &mut MiriBeRustCompilerCalls { target_crate }, using_internal_features, ) @@ -448,7 +499,8 @@ fn main() { init_early_loggers(&early_dcx); // Parse our arguments and split them across `rustc` and `miri`. - let mut miri_config = miri::MiriConfig::default(); + let mut many_seeds: Option> = None; + let mut miri_config = MiriConfig::default(); miri_config.env = env_snapshot; let mut rustc_args = vec![]; @@ -463,6 +515,8 @@ fn main() { if rustc_args.is_empty() { // Very first arg: binary name. rustc_args.push(arg); + // Also add the default arguments. + rustc_args.extend(miri::MIRI_DEFAULT_ARGS.iter().map(ToString::to_string)); } else if after_dashdash { // Everything that comes after `--` is forwarded to the interpreted crate. miri_config.args.push(arg); @@ -544,13 +598,19 @@ fn main() { _ => show_error!("`-Zmiri-retag-fields` can only be `all`, `none`, or `scalar`"), }; } else if let Some(param) = arg.strip_prefix("-Zmiri-seed=") { - if miri_config.seed.is_some() { - show_error!("Cannot specify -Zmiri-seed multiple times!"); - } let seed = param.parse::().unwrap_or_else(|_| { show_error!("-Zmiri-seed must be an integer that fits into u64") }); miri_config.seed = Some(seed); + } else if let Some(param) = arg.strip_prefix("-Zmiri-many-seeds=") { + let range = parse_range(param).unwrap_or_else(|err| { + show_error!( + "-Zmiri-many-seeds requires a range in the form `from..to` or `..to`: {err}" + ) + }); + many_seeds = Some(range); + } else if arg == "-Zmiri-many-seeds" { + many_seeds = Some(0..64); } else if let Some(_param) = arg.strip_prefix("-Zmiri-env-exclude=") { show_error!( "`-Zmiri-env-exclude` has been removed; unset env vars before starting Miri instead" @@ -665,13 +725,23 @@ fn main() { "Tree Borrows does not support integer-to-pointer casts, and is hence not compatible with permissive provenance" ); } + // You can set either one seed or many. + if many_seeds.is_some() && miri_config.seed.is_some() { + show_error!("Only one of `-Zmiri-seed` and `-Zmiri-many-seeds can be set"); + } + if many_seeds.is_some() && !rustc_args.iter().any(|arg| arg.starts_with("-Zthreads=")) { + // Ensure we have parallelism for many-seeds mode. + rustc_args.push(format!( + "-Zthreads={}", + std::thread::available_parallelism().map_or(1, |n| n.get()) + )); + } debug!("rustc arguments: {:?}", rustc_args); debug!("crate arguments: {:?}", miri_config.args); - run_compiler( - rustc_args, - /* target_crate: */ true, - &mut MiriCompilerCalls { miri_config }, + run_compiler_and_exit( + &rustc_args, + &mut MiriCompilerCalls::new(miri_config, many_seeds), using_internal_features, ) } diff --git a/src/diagnostics.rs b/src/diagnostics.rs index 6b5646d547..1a12d4139c 100644 --- a/src/diagnostics.rs +++ b/src/diagnostics.rs @@ -12,7 +12,7 @@ use crate::*; /// Details of premature program termination. pub enum TerminationInfo { Exit { - code: i64, + code: i32, leak_check: bool, }, Abort(String), @@ -214,7 +214,7 @@ pub fn prune_stacktrace<'tcx>( pub fn report_error<'tcx>( ecx: &InterpCx<'tcx, MiriMachine<'tcx>>, e: InterpErrorInfo<'tcx>, -) -> Option<(i64, bool)> { +) -> Option<(i32, bool)> { use InterpErrorKind::*; use UndefinedBehaviorInfo::*; diff --git a/src/eval.rs b/src/eval.rs index 1df1d08802..eaf4b30c66 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -249,6 +249,13 @@ impl<'tcx> MainThreadState<'tcx> { // Figure out exit code. let ret_place = this.machine.main_fn_ret_place.clone().unwrap(); let exit_code = this.read_target_isize(&ret_place)?; + // Rust uses `isize` but the underlying type of an exit code is `i32`. + // Do a saturating cast. + let exit_code = i32::try_from(exit_code).unwrap_or(if exit_code >= 0 { + i32::MAX + } else { + i32::MIN + }); // Deal with our thread-local memory. We do *not* want to actually free it, instead we consider TLS // to be like a global `static`, so that all memory reached by it is considered to "not leak". this.terminate_active_thread(TlsAllocAction::Leak)?; @@ -421,7 +428,7 @@ pub fn create_ecx<'tcx>( } /// Evaluates the entry function specified by `entry_id`. -/// Returns `Some(return_code)` if program executed completed. +/// Returns `Some(return_code)` if program execution completed. /// Returns `None` if an evaluation error occurred. #[expect(clippy::needless_lifetimes)] pub fn eval_entry<'tcx>( @@ -429,7 +436,7 @@ pub fn eval_entry<'tcx>( entry_id: DefId, entry_type: EntryFnType, config: MiriConfig, -) -> Option { +) -> Option { // Copy setting before we move `config`. let ignore_leaks = config.ignore_leaks; diff --git a/src/shims/foreign_items.rs b/src/shims/foreign_items.rs index 8c8850ba7e..6c8ccc8398 100644 --- a/src/shims/foreign_items.rs +++ b/src/shims/foreign_items.rs @@ -428,7 +428,7 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> { "exit" => { let [code] = this.check_shim(abi, Conv::C, link_name, args)?; let code = this.read_scalar(code)?.to_i32()?; - throw_machine_stop!(TerminationInfo::Exit { code: code.into(), leak_check: false }); + throw_machine_stop!(TerminationInfo::Exit { code, leak_check: false }); } "abort" => { let [] = this.check_shim(abi, Conv::C, link_name, args)?; diff --git a/src/shims/windows/foreign_items.rs b/src/shims/windows/foreign_items.rs index fe4d2158ff..0bf56c3d00 100644 --- a/src/shims/windows/foreign_items.rs +++ b/src/shims/windows/foreign_items.rs @@ -552,8 +552,9 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { // Miscellaneous "ExitProcess" => { let [code] = this.check_shim(abi, sys_conv, link_name, args)?; - let code = this.read_scalar(code)?.to_u32()?; - throw_machine_stop!(TerminationInfo::Exit { code: code.into(), leak_check: false }); + // Windows technically uses u32, but we unify everything to a Unix-style i32. + let code = this.read_scalar(code)?.to_i32()?; + throw_machine_stop!(TerminationInfo::Exit { code, leak_check: false }); } "SystemFunction036" => { // used by getrandom 0.1 From 04ac40c801df57059a2b68a18e3700f621885766 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Mon, 23 Dec 2024 11:55:36 +0100 Subject: [PATCH 2/6] remove many-seeds mode from cargo-miri --- README.md | 14 ++- cargo-miri/src/phases.rs | 199 +++++++++++++++------------------------ cargo-miri/src/util.rs | 73 ++++---------- 3 files changed, 104 insertions(+), 182 deletions(-) diff --git a/README.md b/README.md index 8f577295d1..5561c1bc86 100644 --- a/README.md +++ b/README.md @@ -160,14 +160,14 @@ Certain parts of the execution are picked randomly by Miri, such as the exact ba allocations are stored at and the interleaving of concurrently executing threads. Sometimes, it can be useful to explore multiple different execution, e.g. to make sure that your code does not depend on incidental "super-alignment" of new allocations and to test different thread interleavings. -This can be done with the `--many-seeds` flag: +This can be done with the `-Zmiri-many-seeds` flag: ``` -cargo miri test --many-seeds # tries the seeds in 0..64 -cargo miri test --many-seeds=0..16 +MIRIFLAGS="-Zmiri-many-seeds" cargo miri test # tries the seeds in 0..64 +MIRIFLAGS="-Zmiri-many-seeds=0..16" cargo miri test ``` -The default of 64 different seeds is quite slow, so you probably want to specify a smaller range. +The default of 64 different seeds can be quite slow, so you often want to specify a smaller range. ### Running Miri on CI @@ -317,6 +317,12 @@ environment variable. We first document the most relevant and most commonly used execution with a "permission denied" error being returned to the program. `warn` prints a full backtrace each time that happens; `warn-nobacktrace` is less verbose and shown at most once per operation. `hide` hides the warning entirely. +* `-Zmiri-many-seeds=[]..` runs the program multiple times with different seeds for Miri's + RNG. With different seeds, Miri will make different choices to resolve non-determinism such as the + order in which concurrent threads are scheduled, or the exact addresses assigned to allocations. + This is useful to find bugs that only occur under particular interleavings of concurrent threads, + or that otherwise depend on non-determinism. If the `` part is skipped, it defaults to `0`. + Can be used without a value; in that case the range defaults to `0..64`. * `-Zmiri-num-cpus` states the number of available CPUs to be reported by miri. By default, the number of available CPUs is `1`. Note that this flag does not affect how miri handles threads in any way. diff --git a/cargo-miri/src/phases.rs b/cargo-miri/src/phases.rs index 7ca4f414c2..d7b4421061 100644 --- a/cargo-miri/src/phases.rs +++ b/cargo-miri/src/phases.rs @@ -1,10 +1,10 @@ //! Implements the various phases of `cargo miri run/test`. +use std::env; use std::fs::{self, File}; -use std::io::{BufReader, Write}; +use std::io::BufReader; use std::path::{Path, PathBuf}; use std::process::Command; -use std::{env, thread}; use rustc_version::VersionMeta; @@ -24,10 +24,7 @@ Subcommands: clean Clean the Miri cache & target directory The cargo options are exactly the same as for `cargo run` and `cargo test`, respectively. -Furthermore, the following extra flags and environment variables are recognized for `run` and `test`: - - --many-seeds[=from..to] Run the program/tests many times with different seeds in the given range. - The range defaults to `0..64`. +Furthermore, the following environment variables are recognized for `run` and `test`: MIRIFLAGS Extra flags to pass to the Miri driver. Use this to pass `-Zmiri-...` flags. @@ -41,8 +38,6 @@ Examples: "; -const DEFAULT_MANY_SEEDS: &str = "0..64"; - fn show_help() { println!("{CARGO_MIRI_HELP}"); } @@ -182,17 +177,15 @@ pub fn phase_cargo_miri(mut args: impl Iterator) { let target_dir = get_target_dir(&metadata); cmd.arg("--target-dir").arg(target_dir); - // Store many-seeds argument. - let mut many_seeds = None; // *After* we set all the flags that need setting, forward everything else. Make sure to skip - // `--target-dir` (which would otherwise be set twice) and `--many-seeds` (which is our flag, not cargo's). + // `--target-dir` (which would otherwise be set twice). for arg in ArgSplitFlagValue::from_string_iter(&mut args, "--target-dir").filter_map(Result::err) { - if arg == "--many-seeds" { - many_seeds = Some(DEFAULT_MANY_SEEDS.to_owned()); - } else if let Some(val) = arg.strip_prefix("--many-seeds=") { - many_seeds = Some(val.to_owned()); + if arg == "--many-seeds" || arg.starts_with("--many-seeds=") { + show_error!( + "ERROR: the `--many-seeds` flag has been removed from cargo-miri; use MIRIFLAGS=-Zmiri-many-seeds instead" + ); } else { cmd.arg(arg); } @@ -249,9 +242,6 @@ pub fn phase_cargo_miri(mut args: impl Iterator) { // Forward some crucial information to our own re-invocations. cmd.env("MIRI_SYSROOT", miri_sysroot); cmd.env("MIRI_LOCAL_CRATES", local_crates(&metadata)); - if let Some(many_seeds) = many_seeds { - cmd.env("MIRI_MANY_SEEDS", many_seeds); - } if verbose > 0 { cmd.env("MIRI_VERBOSE", verbose.to_string()); // This makes the other phases verbose. } @@ -407,14 +397,11 @@ pub fn phase_rustc(mut args: impl Iterator, phase: RustcPhase) { // Alter the `-o` parameter so that it does not overwrite the JSON file we stored above. let mut args = env.args; - let mut out_filename = None; for i in 0..args.len() { if args[i] == "-o" { - out_filename = Some(args[i + 1].clone()); args[i + 1].push_str(".miri"); } } - let out_filename = out_filename.expect("rustdoc must pass `-o`"); cmd.args(&args); cmd.env("MIRI_BE_RUSTC", "target"); @@ -427,7 +414,7 @@ pub fn phase_rustc(mut args: impl Iterator, phase: RustcPhase) { eprintln!("[cargo-miri rustc inside rustdoc] going to run:\n{cmd:?}"); } - exec_with_pipe(cmd, &env.stdin, format!("{out_filename}.stdin")); + exec_with_pipe(cmd, &env.stdin); } return; @@ -589,111 +576,81 @@ pub fn phase_runner(mut binary_args: impl Iterator, phase: Runner } }; - let many_seeds = env::var("MIRI_MANY_SEEDS"); - run_many_seeds(many_seeds.ok(), |seed| { - let mut cmd = miri(); - - // Set missing env vars. We prefer build-time env vars over run-time ones; see - // for the kind of issue that fixes. - for (name, val) in &info.env { - // `CARGO_MAKEFLAGS` contains information about how to reach the jobserver, but by the time - // the program is being run, that jobserver no longer exists (cargo only runs the jobserver - // for the build portion of `cargo run`/`cargo test`). Hence we shouldn't forward this. - // Also see . - if name == "CARGO_MAKEFLAGS" { - continue; - } - if let Some(old_val) = env::var_os(name) { - if *old_val == *val { - // This one did not actually change, no need to re-set it. - // (This keeps the `debug_cmd` below more manageable.) - continue; - } else if verbose > 0 { - eprintln!( - "[cargo-miri runner] Overwriting run-time env var {name:?}={old_val:?} with build-time value {val:?}" - ); - } - } - cmd.env(name, val); - } + let mut cmd = miri(); - if phase != RunnerPhase::Rustdoc { - // Set the sysroot. Not necessary in rustdoc, where we already set the sysroot in - // `phase_rustdoc`. rustdoc will forward that flag when invoking rustc (i.e., us), so the - // flag is present in `info.args`. - cmd.arg("--sysroot").arg(env::var_os("MIRI_SYSROOT").unwrap()); + // Set missing env vars. We prefer build-time env vars over run-time ones; see + // for the kind of issue that fixes. + for (name, val) in &info.env { + // `CARGO_MAKEFLAGS` contains information about how to reach the jobserver, but by the time + // the program is being run, that jobserver no longer exists (cargo only runs the jobserver + // for the build portion of `cargo run`/`cargo test`). Hence we shouldn't forward this. + // Also see . + if name == "CARGO_MAKEFLAGS" { + continue; } - // Forward rustc arguments. - // We need to patch "--extern" filenames because we forced a check-only - // build without cargo knowing about that: replace `.rlib` suffix by - // `.rmeta`. - // We also need to remove `--error-format` as cargo specifies that to be JSON, - // but when we run here, cargo does not interpret the JSON any more. `--json` - // then also needs to be dropped. - let mut args = info.args.iter(); - while let Some(arg) = args.next() { - if arg == "--extern" { - forward_patched_extern_arg(&mut (&mut args).cloned(), &mut cmd); - } else if let Some(suffix) = arg.strip_prefix("--error-format") { - assert!(suffix.starts_with('=')); - // Drop this argument. - } else if let Some(suffix) = arg.strip_prefix("--json") { - assert!(suffix.starts_with('=')); - // Drop this argument. - } else { - cmd.arg(arg); + if let Some(old_val) = env::var_os(name) { + if *old_val == *val { + // This one did not actually change, no need to re-set it. + // (This keeps the `debug_cmd` below more manageable.) + continue; + } else if verbose > 0 { + eprintln!( + "[cargo-miri runner] Overwriting run-time env var {name:?}={old_val:?} with build-time value {val:?}" + ); } } - // Respect `MIRIFLAGS`. - if let Ok(a) = env::var("MIRIFLAGS") { - let args = flagsplit(&a); - cmd.args(args); - } - // Set the current seed. - if let Some(seed) = seed { - eprintln!("Trying seed: {seed}"); - cmd.arg(format!("-Zmiri-seed={seed}")); - } + cmd.env(name, val); + } - // Then pass binary arguments. - cmd.arg("--"); - cmd.args(&binary_args); - - // Make sure we use the build-time working directory for interpreting Miri/rustc arguments. - // But then we need to switch to the run-time one, which we instruct Miri to do by setting `MIRI_CWD`. - cmd.current_dir(&info.current_dir); - cmd.env("MIRI_CWD", env::current_dir().unwrap()); - - // Run it. - debug_cmd("[cargo-miri runner]", verbose, &cmd); - - match phase { - RunnerPhase::Rustdoc => { - cmd.stdin(std::process::Stdio::piped()); - // the warning is wrong, we have a `wait` inside the `scope` closure. - let mut child = cmd.spawn().expect("failed to spawn process"); - let child_stdin = child.stdin.take().unwrap(); - // Write stdin in a background thread, as it may block. - let exit_status = thread::scope(|s| { - s.spawn(|| { - let mut child_stdin = child_stdin; - // Ignore failure, it is most likely due to the process having terminated. - let _ = child_stdin.write_all(&info.stdin); - }); - child.wait().expect("failed to run command") - }); - if !exit_status.success() { - std::process::exit(exit_status.code().unwrap_or(-1)); - } - } - RunnerPhase::Cargo => { - let exit_status = cmd.status().expect("failed to run command"); - if !exit_status.success() { - std::process::exit(exit_status.code().unwrap_or(-1)); - } - } + if phase != RunnerPhase::Rustdoc { + // Set the sysroot. Not necessary in rustdoc, where we already set the sysroot in + // `phase_rustdoc`. rustdoc will forward that flag when invoking rustc (i.e., us), so the + // flag is present in `info.args`. + cmd.arg("--sysroot").arg(env::var_os("MIRI_SYSROOT").unwrap()); + } + // Forward rustc arguments. + // We need to patch "--extern" filenames because we forced a check-only + // build without cargo knowing about that: replace `.rlib` suffix by + // `.rmeta`. + // We also need to remove `--error-format` as cargo specifies that to be JSON, + // but when we run here, cargo does not interpret the JSON any more. `--json` + // then also needs to be dropped. + let mut args = info.args.iter(); + while let Some(arg) = args.next() { + if arg == "--extern" { + forward_patched_extern_arg(&mut (&mut args).cloned(), &mut cmd); + } else if let Some(suffix) = arg.strip_prefix("--error-format") { + assert!(suffix.starts_with('=')); + // Drop this argument. + } else if let Some(suffix) = arg.strip_prefix("--json") { + assert!(suffix.starts_with('=')); + // Drop this argument. + } else { + cmd.arg(arg); } - }); + } + // Respect `MIRIFLAGS`. + if let Ok(a) = env::var("MIRIFLAGS") { + let args = flagsplit(&a); + cmd.args(args); + } + + // Then pass binary arguments. + cmd.arg("--"); + cmd.args(&binary_args); + + // Make sure we use the build-time working directory for interpreting Miri/rustc arguments. + // But then we need to switch to the run-time one, which we instruct Miri to do by setting `MIRI_CWD`. + cmd.current_dir(&info.current_dir); + cmd.env("MIRI_CWD", env::current_dir().unwrap()); + + // Run it. + debug_cmd("[cargo-miri runner]", verbose, &cmd); + + match phase { + RunnerPhase::Rustdoc => exec_with_pipe(cmd, &info.stdin), + RunnerPhase::Cargo => exec(cmd), + } } pub fn phase_rustdoc(mut args: impl Iterator) { diff --git a/cargo-miri/src/util.rs b/cargo-miri/src/util.rs index 56f38de8de..43b2a1b617 100644 --- a/cargo-miri/src/util.rs +++ b/cargo-miri/src/util.rs @@ -143,43 +143,23 @@ pub fn exec(mut cmd: Command) -> ! { } } -/// Execute the `Command`, where possible by replacing the current process with a new process -/// described by the `Command`. Then exit this process with the exit code of the new process. -/// `input` is also piped to the new process's stdin, on cfg(unix) platforms by writing its -/// contents to `path` first, then setting stdin to that file. -pub fn exec_with_pipe

(mut cmd: Command, input: &[u8], path: P) -> ! -where - P: AsRef, -{ - #[cfg(unix)] - { - // Write the bytes we want to send to stdin out to a file - std::fs::write(&path, input).unwrap(); - // Open the file for reading, and set our new stdin to it - let stdin = File::open(&path).unwrap(); - cmd.stdin(stdin); - // Unlink the file so that it is fully cleaned up as soon as the new process exits - std::fs::remove_file(&path).unwrap(); - // Finally, we can hand off control. - exec(cmd) - } - #[cfg(not(unix))] - { - drop(path); // We don't need the path, we can pipe the bytes directly - cmd.stdin(std::process::Stdio::piped()); - let mut child = cmd.spawn().expect("failed to spawn process"); - let child_stdin = child.stdin.take().unwrap(); - // Write stdin in a background thread, as it may block. - let exit_status = std::thread::scope(|s| { - s.spawn(|| { - let mut child_stdin = child_stdin; - // Ignore failure, it is most likely due to the process having terminated. - let _ = child_stdin.write_all(input); - }); - child.wait().expect("failed to run command") +/// Execute the `Command`, then exit this process with the exit code of the new process. +/// `input` is also piped to the new process's stdin. +pub fn exec_with_pipe(mut cmd: Command, input: &[u8]) -> ! { + // We can't use `exec` since then the background thread will stop running. + cmd.stdin(std::process::Stdio::piped()); + let mut child = cmd.spawn().expect("failed to spawn process"); + let child_stdin = child.stdin.take().unwrap(); + // Write stdin in a background thread, as it may block. + let exit_status = std::thread::scope(|s| { + s.spawn(|| { + let mut child_stdin = child_stdin; + // Ignore failure, it is most likely due to the process having terminated. + let _ = child_stdin.write_all(input); }); - std::process::exit(exit_status.code().unwrap_or(-1)) - } + child.wait().expect("failed to run command") + }); + std::process::exit(exit_status.code().unwrap_or(-1)) } pub fn ask_to_run(mut cmd: Command, ask: bool, text: &str) { @@ -319,24 +299,3 @@ pub fn clean_target_dir(meta: &Metadata) { remove_dir_all_idem(&target_dir).unwrap_or_else(|err| show_error!("{}", err)) } - -/// Run `f` according to the many-seeds argument. In single-seed mode, `f` will only -/// be called once, with `None`. -pub fn run_many_seeds(many_seeds: Option, f: impl Fn(Option)) { - let Some(many_seeds) = many_seeds else { - return f(None); - }; - let (from, to) = many_seeds - .split_once("..") - .unwrap_or_else(|| show_error!("invalid format for `--many-seeds`: expected `from..to`")); - let from: u32 = if from.is_empty() { - 0 - } else { - from.parse().unwrap_or_else(|_| show_error!("invalid `from` in `--many-seeds=from..to")) - }; - let to: u32 = - to.parse().unwrap_or_else(|_| show_error!("invalid `to` in `--many-seeds=from..to")); - for seed in from..to { - f(Some(seed)); - } -} From f11b50e491b7a45d40f53bf8a4e6f1c26957a9e0 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Mon, 23 Dec 2024 12:13:59 +0100 Subject: [PATCH 3/6] remove --many-seeds from ./miri run --- ci/ci.sh | 4 +-- miri-script/src/commands.rs | 67 ++++++++++++++----------------------- miri-script/src/main.rs | 25 +------------- miri-script/src/util.rs | 54 +----------------------------- 4 files changed, 30 insertions(+), 120 deletions(-) diff --git a/ci/ci.sh b/ci/ci.sh index 5da83a1623..0751f86da8 100755 --- a/ci/ci.sh +++ b/ci/ci.sh @@ -66,9 +66,9 @@ function run_tests { time MIRIFLAGS="${MIRIFLAGS-} -O -Zmir-opt-level=4 -Cdebug-assertions=yes" MIRI_SKIP_UI_CHECKS=1 ./miri test $TARGET_FLAG tests/{pass,panic} fi if [ -n "${MANY_SEEDS-}" ]; then - # Also run some many-seeds tests. (Also tests `./miri run`.) + # Run many-seeds tests. (Also tests `./miri run`.) time for FILE in tests/many-seeds/*.rs; do - ./miri run "--many-seeds=0..$MANY_SEEDS" $TARGET_FLAG "$FILE" + ./miri run "-Zmiri-many-seeds=0..$MANY_SEEDS" $TARGET_FLAG "$FILE" done fi if [ -n "${TEST_BENCH-}" ]; then diff --git a/miri-script/src/commands.rs b/miri-script/src/commands.rs index 0944f23d62..75ac999e8b 100644 --- a/miri-script/src/commands.rs +++ b/miri-script/src/commands.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::ffi::{OsStr, OsString}; use std::fs::File; use std::io::{BufReader, BufWriter, Write}; -use std::ops::{Not, Range}; +use std::ops::Not; use std::path::PathBuf; use std::time::Duration; use std::{env, net, process}; @@ -178,8 +178,8 @@ impl Command { Command::Check { flags } => Self::check(flags), Command::Test { bless, flags, target, coverage } => Self::test(bless, flags, target, coverage), - Command::Run { dep, verbose, many_seeds, target, edition, flags } => - Self::run(dep, verbose, many_seeds, target, edition, flags), + Command::Run { dep, verbose, target, edition, flags } => + Self::run(dep, verbose, target, edition, flags), Command::Doc { flags } => Self::doc(flags), Command::Fmt { flags } => Self::fmt(flags), Command::Clippy { flags } => Self::clippy(flags), @@ -586,7 +586,6 @@ impl Command { fn run( dep: bool, verbose: bool, - many_seeds: Option>, target: Option, edition: Option, flags: Vec, @@ -614,48 +613,34 @@ impl Command { early_flags.push("--sysroot".into()); early_flags.push(miri_sysroot.into()); - // Compute everything needed to run the actual command. Also add MIRIFLAGS. + // Compute flags. let miri_flags = e.sh.var("MIRIFLAGS").unwrap_or_default(); let miri_flags = flagsplit(&miri_flags); let quiet_flag = if verbose { None } else { Some("--quiet") }; - // This closure runs the command with the given `seed_flag` added between the MIRIFLAGS and - // the `flags` given on the command-line. - let run_miri = |e: &MiriEnv, seed_flag: Option| -> Result<()> { - // The basic command that executes the Miri driver. - let mut cmd = if dep { - // We invoke the test suite as that has all the logic for running with dependencies. - e.cargo_cmd(".", "test") - .args(&["--test", "ui"]) - .args(quiet_flag) - .arg("--") - .args(&["--miri-run-dep-mode"]) - } else { - cmd!(e.sh, "{miri_bin}") - }; - cmd.set_quiet(!verbose); - // Add Miri flags - let mut cmd = cmd.args(&miri_flags).args(&seed_flag).args(&early_flags).args(&flags); - // For `--dep` we also need to set the target in the env var. - if dep { - if let Some(target) = &target { - cmd = cmd.env("MIRI_TEST_TARGET", target); - } - } - // And run the thing. - Ok(cmd.run()?) - }; - // Run the closure once or many times. - if let Some(seed_range) = many_seeds { - e.run_many_times(seed_range, |e, seed| { - eprintln!("Trying seed: {seed}"); - run_miri(e, Some(format!("-Zmiri-seed={seed}"))).inspect_err(|_| { - eprintln!("FAILING SEED: {seed}"); - }) - })?; + + // Run Miri. + // The basic command that executes the Miri driver. + let mut cmd = if dep { + // We invoke the test suite as that has all the logic for running with dependencies. + e.cargo_cmd(".", "test") + .args(&["--test", "ui"]) + .args(quiet_flag) + .arg("--") + .args(&["--miri-run-dep-mode"]) } else { - run_miri(&e, None)?; + cmd!(e.sh, "{miri_bin}") + }; + cmd.set_quiet(!verbose); + // Add Miri flags + let mut cmd = cmd.args(&miri_flags).args(&early_flags).args(&flags); + // For `--dep` we also need to set the target in the env var. + if dep { + if let Some(target) = &target { + cmd = cmd.env("MIRI_TEST_TARGET", target); + } } - Ok(()) + // Finally, run the thing. + Ok(cmd.run()?) } fn fmt(flags: Vec) -> Result<()> { diff --git a/miri-script/src/main.rs b/miri-script/src/main.rs index 02abb23e74..a80fed8fcb 100644 --- a/miri-script/src/main.rs +++ b/miri-script/src/main.rs @@ -4,29 +4,9 @@ mod commands; mod coverage; mod util; -use std::ops::Range; - -use anyhow::{Context, Result, anyhow, bail}; +use anyhow::{Result, bail}; use clap::{Parser, Subcommand}; -/// Parses a seed range -/// -/// This function is used for the `--many-seeds` flag. It expects the range in the form -/// `..`. `` is inclusive, `` is exclusive. `` can be omitted, -/// in which case it is assumed to be `0`. -fn parse_range(val: &str) -> anyhow::Result> { - let (from, to) = val - .split_once("..") - .ok_or_else(|| anyhow!("invalid format for `--many-seeds`: expected `from..to`"))?; - let from: u32 = if from.is_empty() { - 0 - } else { - from.parse().context("invalid `from` in `--many-seeds=from..to")? - }; - let to: u32 = to.parse().context("invalid `to` in `--many-seeds=from..to")?; - Ok(from..to) -} - #[derive(Clone, Debug, Subcommand)] pub enum Command { /// Installs the miri driver and cargo-miri to the sysroot of the active toolchain. @@ -81,9 +61,6 @@ pub enum Command { /// Show build progress. #[arg(long, short)] verbose: bool, - /// Run the driver with the seeds in the given range (`..to` or `from..to`, default: `0..64`). - #[arg(long, value_parser = parse_range)] - many_seeds: Option>, /// The cross-interpretation target. #[arg(long)] target: Option, diff --git a/miri-script/src/util.rs b/miri-script/src/util.rs index cf6529d837..c039b4827e 100644 --- a/miri-script/src/util.rs +++ b/miri-script/src/util.rs @@ -1,9 +1,7 @@ use std::ffi::{OsStr, OsString}; use std::io::BufRead; -use std::ops::Range; use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; -use std::{env, iter, thread}; +use std::{env, iter}; use anyhow::{Context, Result, anyhow, bail}; use dunce::canonicalize; @@ -29,7 +27,6 @@ pub fn flagsplit(flags: &str) -> Vec { } /// Some extra state we track for building Miri, such as the right RUSTFLAGS. -#[derive(Clone)] pub struct MiriEnv { /// miri_dir is the root of the miri repository checkout we are working in. pub miri_dir: PathBuf, @@ -240,53 +237,4 @@ impl MiriEnv { Ok(()) } - - /// Run the given closure many times in parallel with access to the shell, once for each value in the `range`. - pub fn run_many_times( - &self, - range: Range, - run: impl Fn(&Self, u32) -> Result<()> + Sync, - ) -> Result<()> { - // `next` is atomic so threads can concurrently fetch their next value to run. - let next = AtomicU32::new(range.start); - let end = range.end; // exclusive! - let failed = AtomicBool::new(false); - thread::scope(|s| { - let mut handles = Vec::new(); - // Spawn one worker per core. - for _ in 0..thread::available_parallelism()?.get() { - // Create a copy of the environment for this thread. - let local_miri = self.clone(); - let handle = s.spawn(|| -> Result<()> { - let local_miri = local_miri; // move the copy into this thread. - // Each worker thread keeps asking for numbers until we're all done. - loop { - let cur = next.fetch_add(1, Ordering::Relaxed); - if cur >= end { - // We hit the upper limit and are done. - break; - } - // Run the command with this seed. - run(&local_miri, cur).inspect_err(|_| { - // If we failed, tell everyone about this. - failed.store(true, Ordering::Relaxed); - })?; - // Check if some other command failed (in which case we'll stop as well). - if failed.load(Ordering::Relaxed) { - return Ok(()); - } - } - Ok(()) - }); - handles.push(handle); - } - // Wait for all workers to be done. - for handle in handles { - handle.join().unwrap()?; - } - // If all workers succeeded, we can't have failed. - assert!(!failed.load(Ordering::Relaxed)); - Ok(()) - }) - } } From 759f2649481969957b014b96d3ffd0cf88825648 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Mon, 23 Dec 2024 12:44:54 +0100 Subject: [PATCH 4/6] stop using process-wide state, now that we are running multiple interpreters in the same thread --- src/alloc_addresses/mod.rs | 16 ++++------- src/borrow_tracker/stacked_borrows/mod.rs | 12 ++------- src/helpers.rs | 9 ++----- src/machine.rs | 33 ++++++++++++++++++++++- src/shims/native_lib.rs | 13 +++------ src/shims/unix/sync.rs | 11 +++----- 6 files changed, 47 insertions(+), 47 deletions(-) diff --git a/src/alloc_addresses/mod.rs b/src/alloc_addresses/mod.rs index f7295fd7d8..3d0fc5590e 100644 --- a/src/alloc_addresses/mod.rs +++ b/src/alloc_addresses/mod.rs @@ -9,7 +9,6 @@ use std::cmp::max; use rand::Rng; use rustc_abi::{Align, Size}; use rustc_data_structures::fx::{FxHashMap, FxHashSet}; -use rustc_span::Span; use self::reuse_pool::ReusePool; use crate::concurrency::VClock; @@ -319,17 +318,12 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { match global_state.provenance_mode { ProvenanceMode::Default => { // The first time this happens at a particular location, print a warning. - thread_local! { - // `Span` is non-`Send`, so we use a thread-local instead. - static PAST_WARNINGS: RefCell> = RefCell::default(); + let mut int2ptr_warned = this.machine.int2ptr_warned.borrow_mut(); + let first = int2ptr_warned.is_empty(); + if int2ptr_warned.insert(this.cur_span()) { + // Newly inserted, so first time we see this span. + this.emit_diagnostic(NonHaltingDiagnostic::Int2Ptr { details: first }); } - PAST_WARNINGS.with_borrow_mut(|past_warnings| { - let first = past_warnings.is_empty(); - if past_warnings.insert(this.cur_span()) { - // Newly inserted, so first time we see this span. - this.emit_diagnostic(NonHaltingDiagnostic::Int2Ptr { details: first }); - } - }); } ProvenanceMode::Strict => { throw_machine_stop!(TerminationInfo::Int2PtrWithStrictProvenance); diff --git a/src/borrow_tracker/stacked_borrows/mod.rs b/src/borrow_tracker/stacked_borrows/mod.rs index ea75131078..bcc8668dbc 100644 --- a/src/borrow_tracker/stacked_borrows/mod.rs +++ b/src/borrow_tracker/stacked_borrows/mod.rs @@ -5,7 +5,6 @@ pub mod diagnostics; mod item; mod stack; -use std::cell::RefCell; use std::fmt::Write; use std::{cmp, mem}; @@ -822,16 +821,9 @@ trait EvalContextPrivExt<'tcx, 'ecx>: crate::MiriInterpCxExt<'tcx> { let size = match size { Some(size) => size, None => { - // The first time this happens, show a warning. - thread_local! { static WARNING_SHOWN: RefCell = const { RefCell::new(false) }; } - WARNING_SHOWN.with_borrow_mut(|shown| { - if *shown { - return; - } - // Not yet shown. Show it! - *shown = true; + if !this.machine.sb_extern_type_warned.replace(true) { this.emit_diagnostic(NonHaltingDiagnostic::ExternTypeReborrow); - }); + } return interp_ok(place.clone()); } }; diff --git a/src/helpers.rs b/src/helpers.rs index 690cb7a5b3..adfec33bea 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,6 +1,4 @@ -use std::collections::BTreeSet; use std::num::NonZero; -use std::sync::Mutex; use std::time::Duration; use std::{cmp, iter}; @@ -641,11 +639,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { match reject_with { RejectOpWith::Abort => isolation_abort_error(op_name), RejectOpWith::WarningWithoutBacktrace => { - // This exists to reduce verbosity; make sure we emit the warning at most once per - // operation. - static EMITTED_WARNINGS: Mutex> = Mutex::new(BTreeSet::new()); - - let mut emitted_warnings = EMITTED_WARNINGS.lock().unwrap(); + let mut emitted_warnings = this.machine.reject_in_isolation_warned.borrow_mut(); if !emitted_warnings.contains(op_name) { // First time we are seeing this. emitted_warnings.insert(op_name.to_owned()); @@ -653,6 +647,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { .dcx() .warn(format!("{op_name} was made to return an error due to isolation")); } + interp_ok(()) } RejectOpWith::Warning => { diff --git a/src/machine.rs b/src/machine.rs index 33cefd6076..5e8f616a37 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -3,7 +3,7 @@ use std::any::Any; use std::borrow::Cow; -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; use std::collections::hash_map::Entry; use std::path::Path; use std::{fmt, process}; @@ -595,6 +595,21 @@ pub struct MiriMachine<'tcx> { /// A cache of "data range" computations for unions (i.e., the offsets of non-padding bytes). union_data_ranges: FxHashMap, RangeSet>, + + /// Caches the sanity-checks for various pthread primitives. + pub(crate) pthread_mutex_sanity: Cell, + pub(crate) pthread_rwlock_sanity: Cell, + pub(crate) pthread_condvar_sanity: Cell, + + /// Remembers whether we already warned about an extern type with Stacked Borrows. + pub(crate) sb_extern_type_warned: Cell, + /// Remember whether we already warned about sharing memory with a native call. + #[cfg(unix)] + pub(crate) native_call_mem_warned: Cell, + /// Remembers which shims have already shown the warning about erroring in isolation. + pub(crate) reject_in_isolation_warned: RefCell>, + /// Remembers which int2ptr casts we have already warned about. + pub(crate) int2ptr_warned: RefCell>, } impl<'tcx> MiriMachine<'tcx> { @@ -732,6 +747,14 @@ impl<'tcx> MiriMachine<'tcx> { const_cache: RefCell::new(FxHashMap::default()), symbolic_alignment: RefCell::new(FxHashMap::default()), union_data_ranges: FxHashMap::default(), + pthread_mutex_sanity: Cell::new(false), + pthread_rwlock_sanity: Cell::new(false), + pthread_condvar_sanity: Cell::new(false), + sb_extern_type_warned: Cell::new(false), + #[cfg(unix)] + native_call_mem_warned: Cell::new(false), + reject_in_isolation_warned: Default::default(), + int2ptr_warned: Default::default(), } } @@ -844,6 +867,14 @@ impl VisitProvenance for MiriMachine<'_> { const_cache: _, symbolic_alignment: _, union_data_ranges: _, + pthread_mutex_sanity: _, + pthread_rwlock_sanity: _, + pthread_condvar_sanity: _, + sb_extern_type_warned: _, + #[cfg(unix)] + native_call_mem_warned: _, + reject_in_isolation_warned: _, + int2ptr_warned: _, } = self; threads.visit_provenance(visit); diff --git a/src/shims/native_lib.rs b/src/shims/native_lib.rs index 92cca88bc9..87be5a521d 100644 --- a/src/shims/native_lib.rs +++ b/src/shims/native_lib.rs @@ -1,5 +1,4 @@ //! Implements calling functions from a native library. -use std::cell::RefCell; use std::ops::Deref; use libffi::high::call as ffi; @@ -174,16 +173,10 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { continue; }; // The first time this happens, print a warning. - thread_local! { - static HAVE_WARNED: RefCell = const { RefCell::new(false) }; + if !this.machine.native_call_mem_warned.replace(true) { + // Newly set, so first time we get here. + this.emit_diagnostic(NonHaltingDiagnostic::NativeCallSharedMem); } - HAVE_WARNED.with_borrow_mut(|have_warned| { - if !*have_warned { - // Newly inserted, so first time we see this span. - this.emit_diagnostic(NonHaltingDiagnostic::NativeCallSharedMem); - *have_warned = true; - } - }); this.prepare_for_native_call(alloc_id, prov)?; } diff --git a/src/shims/unix/sync.rs b/src/shims/unix/sync.rs index 416cf020dc..5b0a9398b4 100644 --- a/src/shims/unix/sync.rs +++ b/src/shims/unix/sync.rs @@ -1,5 +1,3 @@ -use std::sync::atomic::{AtomicBool, Ordering}; - use rustc_abi::Size; use crate::concurrency::sync::LAZY_INIT_COOKIE; @@ -136,8 +134,7 @@ fn mutex_init_offset<'tcx>(ecx: &MiriInterpCx<'tcx>) -> InterpResult<'tcx, Size> // Sanity-check this against PTHREAD_MUTEX_INITIALIZER (but only once): // the `init` field must start out not equal to INIT_COOKIE. - static SANITY: AtomicBool = AtomicBool::new(false); - if !SANITY.swap(true, Ordering::Relaxed) { + if !ecx.machine.pthread_mutex_sanity.replace(true) { let check_static_initializer = |name| { let static_initializer = ecx.eval_path(&["libc", name]); let init_field = @@ -248,8 +245,7 @@ fn rwlock_init_offset<'tcx>(ecx: &MiriInterpCx<'tcx>) -> InterpResult<'tcx, Size // Sanity-check this against PTHREAD_RWLOCK_INITIALIZER (but only once): // the `init` field must start out not equal to LAZY_INIT_COOKIE. - static SANITY: AtomicBool = AtomicBool::new(false); - if !SANITY.swap(true, Ordering::Relaxed) { + if !ecx.machine.pthread_rwlock_sanity.replace(true) { let static_initializer = ecx.eval_path(&["libc", "PTHREAD_RWLOCK_INITIALIZER"]); let init_field = static_initializer.offset(offset, ecx.machine.layouts.u32, ecx).unwrap(); let init = ecx.read_scalar(&init_field).unwrap().to_u32().unwrap(); @@ -357,8 +353,7 @@ fn cond_init_offset<'tcx>(ecx: &MiriInterpCx<'tcx>) -> InterpResult<'tcx, Size> // Sanity-check this against PTHREAD_COND_INITIALIZER (but only once): // the `init` field must start out not equal to LAZY_INIT_COOKIE. - static SANITY: AtomicBool = AtomicBool::new(false); - if !SANITY.swap(true, Ordering::Relaxed) { + if !ecx.machine.pthread_condvar_sanity.replace(true) { let static_initializer = ecx.eval_path(&["libc", "PTHREAD_COND_INITIALIZER"]); let init_field = static_initializer.offset(offset, ecx.machine.layouts.u32, ecx).unwrap(); let init = ecx.read_scalar(&init_field).unwrap().to_u32().unwrap(); From 0ad5cbcb5064799dc8b55b6fb4c1afc57371233b Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Mon, 23 Dec 2024 14:48:17 +0100 Subject: [PATCH 5/6] many-seeds: add flag to keep going even after we found a failing seed --- README.md | 2 ++ src/bin/miri.rs | 29 ++++++++++++++++++++++------- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5561c1bc86..d3912b4c65 100644 --- a/README.md +++ b/README.md @@ -323,6 +323,8 @@ environment variable. We first document the most relevant and most commonly used This is useful to find bugs that only occur under particular interleavings of concurrent threads, or that otherwise depend on non-determinism. If the `` part is skipped, it defaults to `0`. Can be used without a value; in that case the range defaults to `0..64`. +* `-Zmiri-many-seeds-keep-going` tells Miri to really try all the seeds in the given range, even if + a failing seed has already been found. This is useful to determine which fraction of seeds fails. * `-Zmiri-num-cpus` states the number of available CPUs to be reported by miri. By default, the number of available CPUs is `1`. Note that this flag does not affect how miri handles threads in any way. diff --git a/src/bin/miri.rs b/src/bin/miri.rs index ca3704e065..dfee033b2a 100644 --- a/src/bin/miri.rs +++ b/src/bin/miri.rs @@ -30,7 +30,7 @@ use std::ops::Range; use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use miri::{ BacktraceStyle, BorrowTrackerMethod, MiriConfig, ProvenanceMode, RetagFields, ValidationMode, @@ -59,11 +59,16 @@ use tracing::debug; struct MiriCompilerCalls { miri_config: Option, - many_seeds: Option>, + many_seeds: Option, +} + +struct ManySeedsConfig { + seeds: Range, + keep_going: bool, } impl MiriCompilerCalls { - fn new(miri_config: MiriConfig, many_seeds: Option>) -> Self { + fn new(miri_config: MiriConfig, many_seeds: Option) -> Self { Self { miri_config: Some(miri_config), many_seeds } } } @@ -176,7 +181,8 @@ impl rustc_driver::Callbacks for MiriCompilerCalls { if let Some(many_seeds) = self.many_seeds.take() { assert!(config.seed.is_none()); - sync::par_for_each_in(many_seeds, |seed| { + let exit_code = sync::IntoDynSyncSend(AtomicI32::new(rustc_driver::EXIT_SUCCESS)); + sync::par_for_each_in(many_seeds.seeds, |seed| { let mut config = config.clone(); config.seed = Some(seed.into()); eprintln!("Trying seed: {seed}"); @@ -184,11 +190,15 @@ impl rustc_driver::Callbacks for MiriCompilerCalls { .unwrap_or(rustc_driver::EXIT_FAILURE); if return_code != rustc_driver::EXIT_SUCCESS { eprintln!("FAILING SEED: {seed}"); - tcx.dcx().abort_if_errors(); // exits with a different error message - std::process::exit(return_code); + if !many_seeds.keep_going { + // `abort_if_errors` would actually not stop, since `par_for_each` waits for the + // rest of the to finish, so we just exit immediately. + std::process::exit(return_code); + } + exit_code.store(return_code, Ordering::Relaxed); } }); - std::process::exit(rustc_driver::EXIT_SUCCESS); + std::process::exit(exit_code.0.into_inner()); } else { let return_code = miri::eval_entry(tcx, entry_def_id, entry_type, config) .unwrap_or_else(|| { @@ -500,6 +510,7 @@ fn main() { // Parse our arguments and split them across `rustc` and `miri`. let mut many_seeds: Option> = None; + let mut many_seeds_keep_going = false; let mut miri_config = MiriConfig::default(); miri_config.env = env_snapshot; @@ -611,6 +622,8 @@ fn main() { many_seeds = Some(range); } else if arg == "-Zmiri-many-seeds" { many_seeds = Some(0..64); + } else if arg == "-Zmiri-many-seeds-keep-going" { + many_seeds_keep_going = true; } else if let Some(_param) = arg.strip_prefix("-Zmiri-env-exclude=") { show_error!( "`-Zmiri-env-exclude` has been removed; unset env vars before starting Miri instead" @@ -736,6 +749,8 @@ fn main() { std::thread::available_parallelism().map_or(1, |n| n.get()) )); } + let many_seeds = + many_seeds.map(|seeds| ManySeedsConfig { seeds, keep_going: many_seeds_keep_going }); debug!("rustc arguments: {:?}", rustc_args); debug!("crate arguments: {:?}", miri_config.args); From 9e741e943c694db4291e2942609647e384dac421 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Mon, 23 Dec 2024 16:24:24 +0100 Subject: [PATCH 6/6] use std::sync::Once instead of hand-rolling a bad version of it --- src/bin/miri.rs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/bin/miri.rs b/src/bin/miri.rs index dfee033b2a..6b6fa31c67 100644 --- a/src/bin/miri.rs +++ b/src/bin/miri.rs @@ -29,8 +29,8 @@ use std::num::NonZero; use std::ops::Range; use std::path::PathBuf; use std::str::FromStr; -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; +use std::sync::atomic::{AtomicI32, Ordering}; +use std::sync::{Arc, Once}; use miri::{ BacktraceStyle, BorrowTrackerMethod, MiriConfig, ProvenanceMode, RetagFields, ValidationMode, @@ -335,26 +335,24 @@ fn rustc_logger_config() -> rustc_log::LoggerConfig { /// The global logger can only be set once per process, so track /// whether that already happened. -static LOGGER_INITED: AtomicBool = AtomicBool::new(false); +static LOGGER_INITED: Once = Once::new(); fn init_early_loggers(early_dcx: &EarlyDiagCtxt) { - // Now for rustc. We only initialize `rustc` if the env var is set (so the user asked for it). + // We only initialize `rustc` if the env var is set (so the user asked for it). // If it is not set, we avoid initializing now so that we can initialize later with our custom - // settings, and *not* log anything for what happens before `miri` gets started. + // settings, and *not* log anything for what happens before `miri` starts interpreting. if env::var_os("RUSTC_LOG").is_some() { - rustc_driver::init_logger(early_dcx, rustc_logger_config()); - assert!(!LOGGER_INITED.swap(true, Ordering::AcqRel)); + LOGGER_INITED.call_once(|| { + rustc_driver::init_logger(early_dcx, rustc_logger_config()); + }); } } fn init_late_loggers(early_dcx: &EarlyDiagCtxt, tcx: TyCtxt<'_>) { // If the logger is not yet initialized, initialize it. - if !LOGGER_INITED.swap(true, Ordering::AcqRel) { + LOGGER_INITED.call_once(|| { rustc_driver::init_logger(early_dcx, rustc_logger_config()); - } - // There's a little race condition here in many-seeds mode, where we don't wait for the thread - // that is doing the initializing. But if you want to debug things with extended logging you - // probably won't use many-seeds mode anyway. + }); // If `MIRI_BACKTRACE` is set and `RUSTC_CTFE_BACKTRACE` is not, set `RUSTC_CTFE_BACKTRACE`. // Do this late, so we ideally only apply this to Miri's errors. @@ -742,8 +740,9 @@ fn main() { if many_seeds.is_some() && miri_config.seed.is_some() { show_error!("Only one of `-Zmiri-seed` and `-Zmiri-many-seeds can be set"); } + + // Ensure we have parallelism for many-seeds mode. if many_seeds.is_some() && !rustc_args.iter().any(|arg| arg.starts_with("-Zthreads=")) { - // Ensure we have parallelism for many-seeds mode. rustc_args.push(format!( "-Zthreads={}", std::thread::available_parallelism().map_or(1, |n| n.get())