From 446629dcb448bb6d5991d54a5c8698c59272093d Mon Sep 17 00:00:00 2001 From: The Miri Cronjob Bot Date: Sun, 5 Apr 2026 05:29:21 +0000 Subject: [PATCH 01/20] Prepare for merging from rust-lang/rust This updates the rust-version file to c92036b45babceee1d8d31cac5536207a26369a5. --- src/tools/miri/rust-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/miri/rust-version b/src/tools/miri/rust-version index af66f0a4d9eab..1083529bf3e27 100644 --- a/src/tools/miri/rust-version +++ b/src/tools/miri/rust-version @@ -1 +1 @@ -55e86c996809902e8bbad512cfb4d2c18be446d9 +c92036b45babceee1d8d31cac5536207a26369a5 From 82b637b450c20b043298dfd1eb042979c3569322 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Sun, 5 Apr 2026 08:22:37 +0200 Subject: [PATCH 02/20] float test: increase ULP tolerance --- src/tools/miri/tests/pass/float.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tools/miri/tests/pass/float.rs b/src/tools/miri/tests/pass/float.rs index 8961693f32393..601f1729a4a0d 100644 --- a/src/tools/miri/tests/pass/float.rs +++ b/src/tools/miri/tests/pass/float.rs @@ -47,8 +47,9 @@ macro_rules! assert_approx_eq { }}; ($a:expr, $b: expr) => { - // accept up to 8ULP (4ULP for host floats and 4ULP for miri artificial error). - assert_approx_eq!($a, $b, 8); + // Accept up to 12ULP (4ULP for miri artificial error and the rest for host floats). + // We saw failures on an i686-linux host with a limit of 8! + assert_approx_eq!($a, $b, 12); }; } From 0b6423de6219f3d581cd707417cc3cc261cdb66f Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Mon, 6 Apr 2026 09:31:02 +0200 Subject: [PATCH 03/20] float test: double error tolerance with imprecise ops on both sides --- src/tools/miri/tests/pass/float.rs | 52 ++++++++++++++++++------------ 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/src/tools/miri/tests/pass/float.rs b/src/tools/miri/tests/pass/float.rs index 601f1729a4a0d..e57bbeb938198 100644 --- a/src/tools/miri/tests/pass/float.rs +++ b/src/tools/miri/tests/pass/float.rs @@ -22,6 +22,10 @@ use std::hint::black_box; mod utils; use utils::check_nondet; +/// Error tolerance in ULP. +/// Miri adds 4 ULP of errors itself, and we allow for 4 additional ULP of host float error. +const ERR_TOLERANCE: i32 = 8; + /// Compare the two floats, allowing for $ulp many ULPs of error. /// /// ULP means "Units in the Last Place" or "Units of Least Precision". @@ -33,7 +37,7 @@ use utils::check_nondet; /// have a large value difference, their ULP can still be 1, so they are still "approximatly equal", /// but the EPSILON check would have failed. macro_rules! assert_approx_eq { - ($a:expr, $b:expr, $ulp:expr) => {{ + ($a:expr, $b:expr, $ulp:expr $( , )?) => {{ let (actual, expected) = ($a, $b); let allowed_ulp_diff = $ulp; let _force_same_type = actual == expected; @@ -46,10 +50,10 @@ macro_rules! assert_approx_eq { }; }}; - ($a:expr, $b: expr) => { + ($a:expr, $b: expr $( , )?) => { // Accept up to 12ULP (4ULP for miri artificial error and the rest for host floats). // We saw failures on an i686-linux host with a limit of 8! - assert_approx_eq!($a, $b, 12); + assert_approx_eq!($a, $b, ERR_TOLERANCE); }; } @@ -1196,10 +1200,10 @@ fn libm() { assert_approx_eq!(f16_consts::FRAC_PI_6.sin(), 0.5); assert_approx_eq!(f32_consts::FRAC_PI_6.sin(), 0.5); assert_approx_eq!(f64_consts::FRAC_PI_6.sin(), 0.5); - // Increase error tolerance to 16ULP because of the extra operation. - assert_approx_eq!(f16_consts::FRAC_PI_4.sin().asin(), f16_consts::FRAC_PI_4, 16); - assert_approx_eq!(f32_consts::FRAC_PI_4.sin().asin(), f32_consts::FRAC_PI_4, 16); - assert_approx_eq!(f64_consts::FRAC_PI_4.sin().asin(), f64_consts::FRAC_PI_4, 16); + // Increase error tolerance because of the extra operation. + assert_approx_eq!(f16_consts::FRAC_PI_4.sin().asin(), f16_consts::FRAC_PI_4, 2 * ERR_TOLERANCE); + assert_approx_eq!(f32_consts::FRAC_PI_4.sin().asin(), f32_consts::FRAC_PI_4, 2 * ERR_TOLERANCE); + assert_approx_eq!(f64_consts::FRAC_PI_4.sin().asin(), f64_consts::FRAC_PI_4, 2 * ERR_TOLERANCE); assert_biteq(0.0f16.asin(), 0.0f16, "asin(+0) = +0"); assert_biteq((-0.0f16).asin(), -0.0, "asin(-0) = -0"); assert_biteq(0.0f32.asin(), 0.0f32, "asin(+0) = +0"); @@ -1239,10 +1243,10 @@ fn libm() { assert_approx_eq!(f16_consts::FRAC_PI_3.cos(), 0.5); assert_approx_eq!(f32_consts::FRAC_PI_3.cos(), 0.5); assert_approx_eq!(f64_consts::FRAC_PI_3.cos(), 0.5); - // Increase error tolerance to 16ULP because of the extra operation. - assert_approx_eq!(f16_consts::FRAC_PI_4.cos().acos(), f16_consts::FRAC_PI_4, 16); - assert_approx_eq!(f32_consts::FRAC_PI_4.cos().acos(), f32_consts::FRAC_PI_4, 16); - assert_approx_eq!(f64_consts::FRAC_PI_4.cos().acos(), f64_consts::FRAC_PI_4, 16); + // Increase error tolerance because of the extra operation. + assert_approx_eq!(f16_consts::FRAC_PI_4.cos().acos(), f16_consts::FRAC_PI_4, 2 * ERR_TOLERANCE); + assert_approx_eq!(f32_consts::FRAC_PI_4.cos().acos(), f32_consts::FRAC_PI_4, 2 * ERR_TOLERANCE); + assert_approx_eq!(f64_consts::FRAC_PI_4.cos().acos(), f64_consts::FRAC_PI_4, 2 * ERR_TOLERANCE); assert_biteq(1.0f16.acos(), 0.0, "acos(1) = 0"); assert_biteq(1.0f32.acos(), 0.0, "acos(1) = 0"); assert_biteq(1.0f64.acos(), 0.0, "acos(1) = 0"); @@ -1263,9 +1267,10 @@ fn libm() { assert_approx_eq!(1.0f16.tan(), 1.557408f16); assert_approx_eq!(1.0f32.tan(), 1.557408f32); assert_approx_eq!(1.0f64.tan(), 1.5574077246549023f64); - assert_approx_eq!(1.0_f16, 1.0_f16.tan().atan()); - assert_approx_eq!(1.0_f32, 1.0_f32.tan().atan()); - assert_approx_eq!(1.0_f64, 1.0_f64.tan().atan()); + // Increase error tolerance because of the extra operation. + assert_approx_eq!(1.0_f16, 1.0_f16.tan().atan(), 2 * ERR_TOLERANCE); + assert_approx_eq!(1.0_f32, 1.0_f32.tan().atan(), 2 * ERR_TOLERANCE); + assert_approx_eq!(1.0_f64, 1.0_f64.tan().atan(), 2 * ERR_TOLERANCE); assert_approx_eq!(1.0f16.atan2(2.0f16), 0.46364761f16); assert_approx_eq!(1.0f32.atan2(2.0f32), 0.46364761f32); assert_approx_eq!(1.0f32.atan2(2.0f32), 0.46364761f32); @@ -1316,17 +1321,21 @@ fn libm() { fixed_atan2_cases!(f32); fixed_atan2_cases!(f64); + // Imprecise operations on both sides needs higher error tolerance. assert_approx_eq!( 1.0f16.tanh(), - (1.0 - f16_consts::E.powi(-2)) / (1.0 + f16_consts::E.powi(-2)) + (1.0 - f16_consts::E.powi(-2)) / (1.0 + f16_consts::E.powi(-2)), + 2 * ERR_TOLERANCE, ); assert_approx_eq!( 1.0f32.tanh(), - (1.0 - f32_consts::E.powi(-2)) / (1.0 + f32_consts::E.powi(-2)) + (1.0 - f32_consts::E.powi(-2)) / (1.0 + f32_consts::E.powi(-2)), + 2 * ERR_TOLERANCE, ); assert_approx_eq!( 1.0f64.tanh(), - (1.0 - f64_consts::E.powi(-2)) / (1.0 + f64_consts::E.powi(-2)) + (1.0 - f64_consts::E.powi(-2)) / (1.0 + f64_consts::E.powi(-2)), + 2 * ERR_TOLERANCE, ); assert_eq!(f16::INFINITY.tanh(), 1.0); assert_eq!(f16::NEG_INFINITY.tanh(), -1.0); @@ -1349,15 +1358,16 @@ fn libm() { assert_eq!(2.0f16.ln_gamma(), (0.0, 1)); assert_eq!(2.0f32.ln_gamma(), (0.0, 1)); assert_eq!(2.0f64.ln_gamma(), (0.0, 1)); - // Gamma(-0.5) = -2*sqrt(π) + // Gamma(-0.5) = -2*sqrt(π), then apply `ln` on both sides. + // This has imprecise float ops on both sides so we double the error tolerance. let (val, sign) = (-0.5f16).ln_gamma(); - assert_approx_eq!(val, (2.0 * f16_consts::PI.sqrt()).ln()); + assert_approx_eq!(val, (2.0 * f16_consts::PI.sqrt()).ln(), 2 * ERR_TOLERANCE); assert_eq!(sign, -1); let (val, sign) = (-0.5f32).ln_gamma(); - assert_approx_eq!(val, (2.0 * f32_consts::PI.sqrt()).ln()); + assert_approx_eq!(val, (2.0 * f32_consts::PI.sqrt()).ln(), 2 * ERR_TOLERANCE); assert_eq!(sign, -1); let (val, sign) = (-0.5f64).ln_gamma(); - assert_approx_eq!(val, (2.0 * f64_consts::PI.sqrt()).ln()); + assert_approx_eq!(val, (2.0 * f64_consts::PI.sqrt()).ln(), 2 * ERR_TOLERANCE); assert_eq!(sign, -1); assert_approx_eq!(1.0f16.erf(), 0.84270079294971486934122063508260926f16); From 7a501c524068df36ed676933a50bcebf47cc792a Mon Sep 17 00:00:00 2001 From: The Miri Cronjob Bot Date: Tue, 7 Apr 2026 05:29:27 +0000 Subject: [PATCH 04/20] Prepare for merging from rust-lang/rust This updates the rust-version file to bcded331651b60a0383b3ff51db4f24c4495ac53. --- src/tools/miri/rust-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/miri/rust-version b/src/tools/miri/rust-version index 1083529bf3e27..f26ffe8363518 100644 --- a/src/tools/miri/rust-version +++ b/src/tools/miri/rust-version @@ -1 +1 @@ -c92036b45babceee1d8d31cac5536207a26369a5 +bcded331651b60a0383b3ff51db4f24c4495ac53 From 51dec7515bde5e5ac67814fc0693de1504a73d29 Mon Sep 17 00:00:00 2001 From: The Miri Cronjob Bot Date: Tue, 7 Apr 2026 05:38:33 +0000 Subject: [PATCH 05/20] fmt --- src/tools/miri/src/bin/miri.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tools/miri/src/bin/miri.rs b/src/tools/miri/src/bin/miri.rs index 36cf640cb8449..bb52bde6fe7b2 100644 --- a/src/tools/miri/src/bin/miri.rs +++ b/src/tools/miri/src/bin/miri.rs @@ -309,8 +309,9 @@ impl rustc_driver::Callbacks for MiriDepCompilerCalls { // We need to add #[used] symbols to exported_symbols for `lookup_link_section`. // FIXME handle this somehow in rustc itself to avoid this hack. local_providers.queries.exported_non_generic_symbols = |tcx, LocalCrate| { - let reachable_set = tcx - .with_stable_hashing_context(|mut hcx| tcx.reachable_set(()).to_sorted(&mut hcx, true)); + let reachable_set = tcx.with_stable_hashing_context(|mut hcx| { + tcx.reachable_set(()).to_sorted(&mut hcx, true) + }); tcx.arena.alloc_from_iter( // This is based on: // https://github.com/rust-lang/rust/blob/2962e7c0089d5c136f4e9600b7abccfbbde4973d/compiler/rustc_codegen_ssa/src/back/symbol_export.rs#L62-L63 From bff890be849589b643bf200a094cf9be0173dd2e Mon Sep 17 00:00:00 2001 From: The Miri Cronjob Bot Date: Wed, 8 Apr 2026 05:30:05 +0000 Subject: [PATCH 06/20] Prepare for merging from rust-lang/rust This updates the rust-version file to 30d0309fa821f7a0984a9629e0d227ca3c0d2eda. --- src/tools/miri/rust-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/miri/rust-version b/src/tools/miri/rust-version index f26ffe8363518..ac3099d845931 100644 --- a/src/tools/miri/rust-version +++ b/src/tools/miri/rust-version @@ -1 +1 @@ -bcded331651b60a0383b3ff51db4f24c4495ac53 +30d0309fa821f7a0984a9629e0d227ca3c0d2eda From f373acf3376e1b6d38b37a5f95ac9f85795dff60 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Wed, 8 Apr 2026 08:59:10 +0200 Subject: [PATCH 07/20] disable new build dir layout again due to problems --- src/tools/miri/ci/ci.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tools/miri/ci/ci.sh b/src/tools/miri/ci/ci.sh index 9bacbbcf4597c..553d63eebc813 100755 --- a/src/tools/miri/ci/ci.sh +++ b/src/tools/miri/ci/ci.sh @@ -28,7 +28,8 @@ begingroup "Building Miri" export RUSTFLAGS="-D warnings" export CARGO_INCREMENTAL=0 export CARGO_EXTRA_FLAGS="--locked" -export CARGO_UNSTABLE_BUILD_DIR_NEW_LAYOUT=true +# FIXME: disabled since it causes libstd build failures +#export CARGO_UNSTABLE_BUILD_DIR_NEW_LAYOUT=true # Determine configuration for installed build (used by test-cargo-miri and `./miri bench`). # We use the default set of features for this. From 66580a38d9433b36d3e0c67e2176bf97dd74b78f Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Wed, 8 Apr 2026 16:31:20 +0200 Subject: [PATCH 08/20] genmc-sys: remove always_configure hack --- src/tools/miri/genmc-sys/build.rs | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/tools/miri/genmc-sys/build.rs b/src/tools/miri/genmc-sys/build.rs index 4fc3ce94fb8be..04a8e6854fc08 100644 --- a/src/tools/miri/genmc-sys/build.rs +++ b/src/tools/miri/genmc-sys/build.rs @@ -31,8 +31,8 @@ mod downloading { pub(crate) const GENMC_COMMIT: &str = "22d3d0b44dedb4e8e1aae3330e546465e4664529"; /// Ensure that a local GenMC repo is present and set to the correct commit. - /// Return the path of the GenMC repo and whether the checked out commit was changed. - pub(crate) fn download_genmc() -> (PathBuf, bool) { + /// Return the path of the GenMC repo clone. + pub(crate) fn download_genmc() -> PathBuf { let Ok(genmc_download_path) = PathBuf::from_str(GENMC_DOWNLOAD_PATH); let commit_oid = Oid::from_str(GENMC_COMMIT).expect("Commit should be valid."); @@ -44,7 +44,7 @@ mod downloading { && head_commit.id() == commit_oid { // Fast path: The expected commit is already checked out. - return (genmc_download_path, false); + return genmc_download_path; } // Check if the local repository already contains the commit we need, download it otherwise. let commit = update_local_repo(&repo, commit_oid); @@ -61,7 +61,7 @@ mod downloading { } }; - (genmc_download_path, true) + genmc_download_path } fn get_remote(repo: &Repository) -> Remote<'_> { @@ -141,7 +141,7 @@ mod downloading { } /// Build the GenMC model checker library and the Rust-C++ interop library with cxx.rs -fn compile_cpp_dependencies(genmc_path: &Path, always_configure: bool) { +fn compile_cpp_dependencies(genmc_path: &Path) { // Give each step a separate build directory to prevent interference. let out_dir = PathBuf::from(std::env::var("OUT_DIR").as_deref().unwrap()); let genmc_build_dir = out_dir.join("genmc"); @@ -156,7 +156,6 @@ fn compile_cpp_dependencies(genmc_path: &Path, always_configure: bool) { let mut config = cmake::Config::new(genmc_path); config - .always_configure(always_configure) // We force running the configure step when the GenMC commit changed. .out_dir(genmc_build_dir) .profile(GENMC_CMAKE_PROFILE) .define("BUILD_LLI", "OFF") @@ -208,8 +207,7 @@ fn main() { } // Select which path to use for the GenMC repo: - let (genmc_path, always_configure) = if let Some(genmc_src_path) = option_env!("GENMC_SRC_PATH") - { + let genmc_path = if let Some(genmc_src_path) = option_env!("GENMC_SRC_PATH") { let genmc_src_path = PathBuf::from_str(&genmc_src_path).expect("GENMC_SRC_PATH should contain a valid path"); assert!( @@ -219,18 +217,14 @@ fn main() { ); // Rebuild files in the given path change. println!("cargo::rerun-if-changed={}", genmc_src_path.display()); - // We disable `always_configure` when working with a local repository, - // since it increases compile times when working on `genmc-sys`. - (genmc_src_path, false) + genmc_src_path } else { // Download GenMC if required and ensure that the correct commit is checked out. - // If anything changed in the downloaded repository (e.g., the commit), - // we set `always_configure` to ensure there are no weird configs from previous builds. downloading::download_genmc() }; // Build all required components: - compile_cpp_dependencies(&genmc_path, always_configure); + compile_cpp_dependencies(&genmc_path); // Only rebuild if anything changes: // Note that we don't add the downloaded GenMC repo, since that should never be modified From df1b8b8c181ea30e577026deca8a1200e66de3b4 Mon Sep 17 00:00:00 2001 From: WhySoBad <49595640+WhySoBad@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:08:03 +0200 Subject: [PATCH 09/20] fix: fix panic when multiple threads block on same fd --- src/tools/miri/src/concurrency/blocking_io.rs | 153 ++++++++++++++---- src/tools/miri/src/concurrency/thread.rs | 10 +- src/tools/miri/src/shims/files.rs | 13 +- src/tools/miri/src/shims/mod.rs | 2 +- src/tools/miri/src/shims/unix/socket.rs | 4 + .../pass-dep/libc/libc-blocking-io-same-fd.rs | 55 +++++++ 6 files changed, 206 insertions(+), 31 deletions(-) create mode 100644 src/tools/miri/tests/pass-dep/libc/libc-blocking-io-same-fd.rs diff --git a/src/tools/miri/src/concurrency/blocking_io.rs b/src/tools/miri/src/concurrency/blocking_io.rs index 221d0495cd162..9a09c5448c0de 100644 --- a/src/tools/miri/src/concurrency/blocking_io.rs +++ b/src/tools/miri/src/concurrency/blocking_io.rs @@ -5,6 +5,7 @@ use mio::event::Source; use mio::{Events, Interest, Poll, Token}; use rustc_data_structures::fx::FxHashMap; +use crate::shims::FdId; use crate::*; /// Capacity of the event queue which can be polled at a time. @@ -16,6 +17,17 @@ const IO_EVENT_CAPACITY: usize = 16; pub trait WithSource { /// Invoke `f` on the source inside `self`. fn with_source(&self, f: &mut dyn FnMut(&mut dyn Source) -> io::Result<()>) -> io::Result<()>; + + /// Unique identifier of the file description to which the + /// source belongs. + fn id(&self) -> FdId; +} + +#[derive(Debug, Hash, PartialEq, Clone, Copy, Eq, PartialOrd, Ord)] +pub enum InterestReason { + /// A thread is blocked on this source and wants to get unblocked + /// when the interest on the source is fulfilled. + ThreadBlocked(ThreadId), } /// Manager for managing blocking host I/O in a non-blocking manner. @@ -34,9 +46,9 @@ pub struct BlockingIoManager { /// This is not part of the state and only stored to avoid allocating a /// new buffer for every poll. events: Events, - /// Map between threads which are currently blocked and the - /// underlying I/O source. - sources: FxHashMap>, + /// Map from source ids to the actual sources and the registered interests for + /// every source. + sources: FxHashMap, FxHashMap)>, } impl BlockingIoManager { @@ -59,8 +71,12 @@ impl BlockingIoManager { /// specified duration. /// - If the timeout is [`None`] the poll blocks indefinitely until an event occurs. /// - /// Returns all threads that are ready because they received an I/O event. - pub fn poll(&mut self, timeout: Option) -> Result, io::Error> { + /// Returns all interest reasons which are fulfilled through an I/O event together with the + /// sources they are registered for. + pub fn poll( + &mut self, + timeout: Option, + ) -> Result, io::Error> { let poll = self.poll.as_mut().expect("Blocking I/O should not be called with isolation enabled"); @@ -70,56 +86,137 @@ impl BlockingIoManager { let ready = self .events .iter() - .map(|event| { + .flat_map(|event| { let token = event.token(); - ThreadId::new_unchecked(token.0.try_into().unwrap()) + let fd_id = FdId::new_unchecked(token.0); + let (_, interests) = self.sources.get(&fd_id).expect("Source should be registered"); + interests + .iter() + .filter(|(_, interest)| { + // Only retain interests of the source which are fulfilled by + // the current event. + (event.is_readable() && interest.is_readable()) + || (event.is_writable() && interest.is_writable()) + || (event.is_aio() && interest.is_aio()) + || (event.is_lio() && interest.is_lio()) + || (event.is_priority() && interest.is_priority()) + }) + .map(move |(reason, _)| (*reason, fd_id)) }) .collect::>(); - // Deregister all ready sources as we only want to receive one event per thread. - ready.iter().for_each(|thread_id| self.deregister(*thread_id)); - Ok(ready) } - /// Register a blocking I/O source for a thread together with it's poll interests. - /// - /// The source will be deregistered automatically once an event for it is received. + /// Register an interest for a blocking I/O source. /// /// As the OS can always produce spurious wake-ups, it's the callers responsibility to /// verify the requested I/O interests are really ready and to register again if they're not. - pub fn register(&mut self, source: Box, thread: ThreadId, interests: Interest) { + /// + /// It's assumed that no interest is already registered for this source with the same reason! + pub fn register( + &mut self, + source: Box, + reason: InterestReason, + interest: Interest, + ) { let poll = self.poll.as_ref().expect("Blocking I/O should not be called with isolation enabled"); - let token = Token(thread.to_u32().to_usize()); + let id = source.id(); + let token = Token(id.to_usize()); + + let Some((_, current_interests)) = self.sources.get_mut(&id) else { + // Treat errors from registering as fatal. On UNIX hosts this can only + // fail due to system resource errors (e.g. ENOMEM or ENOSPC). + source + .with_source(&mut |source| source.register(poll.registry(), token, interest)) + .unwrap(); + + self.sources.insert(id, (source, FxHashMap::from_iter([(reason, interest)]))); + return; + }; + + // The source is already registered. We need to check whether we need to + // reregister because the provided interest contains new interests for the source. + + let old_interest = + interest_union(current_interests).expect("Source should contain at least one interest"); + + current_interests + .try_insert(reason, interest) + .unwrap_or_else(|_| panic!("Interest reason should be unique")); - // Treat errors from registering as fatal. On UNIX hosts this can only + let new_interest = old_interest.add(interest); + if new_interest == old_interest { + // The overall interests in the source did not change and thus we + // don't need to reregister the source. + return; + } + + // The overall interests in the source changed. We need to reregister + // the source with the updated interests. + + // Treat errors from reregistering as fatal. On UNIX hosts this can only // fail due to system resource errors (e.g. ENOMEM or ENOSPC). source - .with_source(&mut |source| source.register(poll.registry(), token, interests)) + .with_source(&mut |source| source.reregister(poll.registry(), token, new_interest)) .unwrap(); - self.sources - .try_insert(thread, source) - .unwrap_or_else(|_| panic!("A thread cannot be registered twice at the same time")); } - /// Deregister the event source for a thread. Returns the kind of I/O the thread was - /// blocked on. - fn deregister(&mut self, thread: ThreadId) { + /// Deregister an interest from a blocking I/O source. + /// + /// The source and the interest on this source are assumed to be registered! + pub fn deregister(&mut self, source_id: FdId, reason: InterestReason) { let poll = self.poll.as_ref().expect("Blocking I/O should not be called with isolation enabled"); - let Some(source) = self.sources.remove(&thread) else { - panic!("Attempt to deregister a token which isn't registered") + let token = Token(source_id.to_usize()); + let (source, current_interests) = + self.sources.get_mut(&source_id).expect("Source should be registered"); + let old_interest = + interest_union(current_interests).expect("Source should contain at least one interest"); + + current_interests + .remove(&reason) + .unwrap_or_else(|| panic!("Interest reason should be registered for source")); + + let Some(new_interest) = interest_union(current_interests) else { + // There are no longer any interests in this source. + // We can thus deregister the source from the poll. + + // Treat errors from deregistering as fatal. On UNIX hosts this can only + // fail due to system resource errors (e.g. ENOMEM or ENOSPC). + source.with_source(&mut |source| source.deregister(poll.registry())).unwrap(); + self.sources.remove(&source_id); + return; }; - // Treat errors from deregistering as fatal. On UNIX hosts this can only + if new_interest == old_interest { + // The overall interests in the source did not change and thus we + // don't need to reregister the source. + return; + } + + // The overall interests in the source changed. We need to reregister + // the source with the updated interests. + + // Treat errors from reregistering as fatal. On UNIX hosts this can only // fail due to system resource errors (e.g. ENOMEM or ENOSPC). - source.with_source(&mut |source| source.deregister(poll.registry())).unwrap(); + source + .with_source(&mut |source| source.reregister(poll.registry(), token, new_interest)) + .unwrap(); } } +/// Get the union of all interests for a source. +fn interest_union(interests: &FxHashMap) -> Option { + interests + .values() + .copied() + .fold(None, |acc, interest| acc.map(|acc: Interest| acc.add(interest)).or(Some(interest))) +} + impl<'tcx> EvalContextExt<'tcx> for MiriInterpCx<'tcx> {} pub trait EvalContextExt<'tcx>: MiriInterpCxExt<'tcx> { /// Block the current thread until some interests on an I/O source @@ -140,7 +237,7 @@ pub trait EvalContextExt<'tcx>: MiriInterpCxExt<'tcx> { let this = self.eval_context_mut(); this.machine.blocking_io.register( Box::new(source), - this.machine.threads.active_thread(), + InterestReason::ThreadBlocked(this.machine.threads.active_thread()), interests, ); this.block_thread(BlockReason::IO, timeout, callback); diff --git a/src/tools/miri/src/concurrency/thread.rs b/src/tools/miri/src/concurrency/thread.rs index ee74e06815945..f978fd81a7269 100644 --- a/src/tools/miri/src/concurrency/thread.rs +++ b/src/tools/miri/src/concurrency/thread.rs @@ -18,6 +18,7 @@ use rustc_span::{DUMMY_SP, Span}; use rustc_target::spec::Os; use crate::concurrency::GlobalDataRaceHandler; +use crate::concurrency::blocking_io::InterestReason; use crate::shims::tls; use crate::*; @@ -822,7 +823,14 @@ trait EvalContextPrivExt<'tcx>: MiriInterpCxExt<'tcx> { Err(e) => panic!("unexpected error while polling: {e}"), }; - ready.into_iter().try_for_each(|thread_id| this.unblock_thread(thread_id, BlockReason::IO)) + ready.into_iter().try_for_each(|(reason, source_id)| { + match reason { + InterestReason::ThreadBlocked(thread_id) => { + this.machine.blocking_io.deregister(source_id, reason); + this.unblock_thread(thread_id, BlockReason::IO) + } + } + }) } /// Find all threads with expired timeouts, unblock them and execute their timeout callbacks. diff --git a/src/tools/miri/src/shims/files.rs b/src/tools/miri/src/shims/files.rs index 29088ed67135a..d123397dda286 100644 --- a/src/tools/miri/src/shims/files.rs +++ b/src/tools/miri/src/shims/files.rs @@ -16,9 +16,20 @@ use crate::*; /// is definitely unique, the address would expose interpreter internal state when used /// for sorting things. So instead we generate a unique id per file description which is the same /// for all `dup`licates and is never reused. -#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Ord, PartialOrd)] +#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct FdId(usize); +impl FdId { + pub fn to_usize(self) -> usize { + self.0 + } + + /// Create a new fd id from a `usize` without checking if this fd exists. + pub fn new_unchecked(id: usize) -> Self { + Self(id) + } +} + #[derive(Debug, Clone)] struct FdIdWith { id: FdId, diff --git a/src/tools/miri/src/shims/mod.rs b/src/tools/miri/src/shims/mod.rs index 281dfc758f4e7..6a7c12038d749 100644 --- a/src/tools/miri/src/shims/mod.rs +++ b/src/tools/miri/src/shims/mod.rs @@ -23,7 +23,7 @@ pub mod time; pub mod tls; pub mod unwind; -pub use self::files::FdTable; +pub use self::files::{FdId, FdTable}; #[cfg(all(feature = "native-lib", unix))] pub use self::native_lib::trace::{init_sv, register_retcode_sv}; pub use self::unix::{DirTable, EpollInterestTable}; diff --git a/src/tools/miri/src/shims/unix/socket.rs b/src/tools/miri/src/shims/unix/socket.rs index bbdf6fc54b2cf..e151e1efb16d6 100644 --- a/src/tools/miri/src/shims/unix/socket.rs +++ b/src/tools/miri/src/shims/unix/socket.rs @@ -1398,4 +1398,8 @@ impl WithSource for FileDescriptionRef { _ => unreachable!(), } } + + fn id(&self) -> FdId { + self.id() + } } diff --git a/src/tools/miri/tests/pass-dep/libc/libc-blocking-io-same-fd.rs b/src/tools/miri/tests/pass-dep/libc/libc-blocking-io-same-fd.rs new file mode 100644 index 0000000000000..e08905825bfeb --- /dev/null +++ b/src/tools/miri/tests/pass-dep/libc/libc-blocking-io-same-fd.rs @@ -0,0 +1,55 @@ +//@ignore-target: windows # No libc socket on Windows +//@compile-flags: -Zmiri-disable-isolation + +#[path = "../../utils/libc.rs"] +mod libc_utils; +use std::thread; +use std::time::Duration; + +use libc_utils::*; + +// This tests that the blocking I/O implementation works when multiple threads block on the +// same fd at the same time. + +fn main() { + let (server_sockfd, addr) = net::make_listener_ipv4(0).unwrap(); + let client_sockfd = + unsafe { errno_result(libc::socket(libc::AF_INET, libc::SOCK_STREAM, 0)).unwrap() }; + + // Spawn the server thread. + let server_thread = thread::spawn(move || { + let (peerfd, _) = net::accept_ipv4(server_sockfd).unwrap(); + + // Yield back to reader threads to ensure that we have + // two threads being blocked on the same fd at the same time. + thread::sleep(Duration::from_millis(10)); + + let mut buffer = [22; 128]; + let bytes_written = unsafe { + errno_result(net::send_all(peerfd, buffer.as_mut_ptr().cast(), buffer.len(), 0)) + .unwrap() + }; + assert_eq!(bytes_written as usize, 128); + }); + + net::connect_ipv4(client_sockfd, addr); + + let reader_thread = thread::spawn(move || { + let mut buffer = [0; 8]; + let bytes_read = unsafe { + errno_result(net::recv_all(client_sockfd, buffer.as_mut_ptr().cast(), buffer.len(), 0)) + .unwrap() + }; + assert_eq!(bytes_read, 8); + }); + + let mut buffer = [0; 8]; + let bytes_read = unsafe { + errno_result(net::recv_all(client_sockfd, buffer.as_mut_ptr().cast(), buffer.len(), 0)) + .unwrap() + }; + assert_eq!(bytes_read, 8); + + reader_thread.join().unwrap(); + server_thread.join().unwrap(); +} From 3c4dfb4bbb15fa3002dc3aff2dc52babee879701 Mon Sep 17 00:00:00 2001 From: WhySoBad <49595640+WhySoBad@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:10:43 +0200 Subject: [PATCH 10/20] docs: add comment when registering new source --- src/tools/miri/src/concurrency/blocking_io.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tools/miri/src/concurrency/blocking_io.rs b/src/tools/miri/src/concurrency/blocking_io.rs index 9a09c5448c0de..4904d53c09796 100644 --- a/src/tools/miri/src/concurrency/blocking_io.rs +++ b/src/tools/miri/src/concurrency/blocking_io.rs @@ -127,6 +127,8 @@ impl BlockingIoManager { let token = Token(id.to_usize()); let Some((_, current_interests)) = self.sources.get_mut(&id) else { + // The source is not yet registered. + // Treat errors from registering as fatal. On UNIX hosts this can only // fail due to system resource errors (e.g. ENOMEM or ENOSPC). source From beaf7ee35a0685b4cdd8ada061442dfcdd3c29ff Mon Sep 17 00:00:00 2001 From: WhySoBad <49595640+WhySoBad@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:27:39 +0200 Subject: [PATCH 11/20] feat: also check that received buffers match --- .../miri/tests/pass-dep/libc/libc-blocking-io-same-fd.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/tools/miri/tests/pass-dep/libc/libc-blocking-io-same-fd.rs b/src/tools/miri/tests/pass-dep/libc/libc-blocking-io-same-fd.rs index e08905825bfeb..ec4287539b7ed 100644 --- a/src/tools/miri/tests/pass-dep/libc/libc-blocking-io-same-fd.rs +++ b/src/tools/miri/tests/pass-dep/libc/libc-blocking-io-same-fd.rs @@ -24,7 +24,7 @@ fn main() { // two threads being blocked on the same fd at the same time. thread::sleep(Duration::from_millis(10)); - let mut buffer = [22; 128]; + let mut buffer = [22u8; 128]; let bytes_written = unsafe { errno_result(net::send_all(peerfd, buffer.as_mut_ptr().cast(), buffer.len(), 0)) .unwrap() @@ -35,20 +35,22 @@ fn main() { net::connect_ipv4(client_sockfd, addr); let reader_thread = thread::spawn(move || { - let mut buffer = [0; 8]; + let mut buffer = [0u8; 8]; let bytes_read = unsafe { errno_result(net::recv_all(client_sockfd, buffer.as_mut_ptr().cast(), buffer.len(), 0)) .unwrap() }; assert_eq!(bytes_read, 8); + assert_eq!(&buffer, &[22u8; 8]); }); - let mut buffer = [0; 8]; + let mut buffer = [0u8; 8]; let bytes_read = unsafe { errno_result(net::recv_all(client_sockfd, buffer.as_mut_ptr().cast(), buffer.len(), 0)) .unwrap() }; assert_eq!(bytes_read, 8); + assert_eq!(&buffer, &[22u8; 8]); reader_thread.join().unwrap(); server_thread.join().unwrap(); From 5c57c5a65c2ab7b14fbe86744793b38f3f5c7ac6 Mon Sep 17 00:00:00 2001 From: zjp Date: Thu, 9 Apr 2026 03:43:49 +0000 Subject: [PATCH 12/20] fix the bitfield meanings of stacked_borrow item --- src/tools/miri/src/borrow_tracker/stacked_borrows/item.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tools/miri/src/borrow_tracker/stacked_borrows/item.rs b/src/tools/miri/src/borrow_tracker/stacked_borrows/item.rs index 13846710615eb..479338788b5ff 100644 --- a/src/tools/miri/src/borrow_tracker/stacked_borrows/item.rs +++ b/src/tools/miri/src/borrow_tracker/stacked_borrows/item.rs @@ -7,9 +7,9 @@ use crate::borrow_tracker::BorTag; pub struct Item(u64); // An Item contains 3 bitfields: -// * Bits 0-61 store a BorTag. -// * Bits 61-63 store a Permission. -// * Bit 64 stores a flag which indicates if we might have a protector. +// * Bits 0-60 store a BorTag (61 bits). +// * Bits 61-62 store a Permission (2 bits). +// * Bit 63 stores a flag which indicates if we might have a protector (1 bit). // This is purely an optimization: if the bit is set, the tag *might* be // in `protected_tags`, but if the bit is not set then the tag is definitely // not in `protected_tags`. From 630cdae69d5c78a58a998c3888ba35c30e68b7b8 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Thu, 9 Apr 2026 15:53:46 +0200 Subject: [PATCH 13/20] Prepare for merging from rust-lang/rust This updates the rust-version file to 4c4205163abcbd08948b3efab796c543ba1ea687. --- src/tools/miri/rust-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/miri/rust-version b/src/tools/miri/rust-version index ac3099d845931..38f153f78d026 100644 --- a/src/tools/miri/rust-version +++ b/src/tools/miri/rust-version @@ -1 +1 @@ -30d0309fa821f7a0984a9629e0d227ca3c0d2eda +4c4205163abcbd08948b3efab796c543ba1ea687 From 4e6323b756407131890f9de45b34d28696d3cce5 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Thu, 9 Apr 2026 15:54:20 +0200 Subject: [PATCH 14/20] re-enable new cargo build dir layout testing --- src/tools/miri/ci/ci.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/tools/miri/ci/ci.sh b/src/tools/miri/ci/ci.sh index 553d63eebc813..9bacbbcf4597c 100755 --- a/src/tools/miri/ci/ci.sh +++ b/src/tools/miri/ci/ci.sh @@ -28,8 +28,7 @@ begingroup "Building Miri" export RUSTFLAGS="-D warnings" export CARGO_INCREMENTAL=0 export CARGO_EXTRA_FLAGS="--locked" -# FIXME: disabled since it causes libstd build failures -#export CARGO_UNSTABLE_BUILD_DIR_NEW_LAYOUT=true +export CARGO_UNSTABLE_BUILD_DIR_NEW_LAYOUT=true # Determine configuration for installed build (used by test-cargo-miri and `./miri bench`). # We use the default set of features for this. From c3c931a1fcd4753b91b8fe22aa300227d09d9ab9 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Thu, 9 Apr 2026 19:55:34 +0200 Subject: [PATCH 15/20] genmc tests: remove a hack that is no longer needed --- src/tools/miri/tests/genmc/fail/loom/store_buffering.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/tools/miri/tests/genmc/fail/loom/store_buffering.rs b/src/tools/miri/tests/genmc/fail/loom/store_buffering.rs index 4955dfd8a77ad..fc522dd013fa1 100644 --- a/src/tools/miri/tests/genmc/fail/loom/store_buffering.rs +++ b/src/tools/miri/tests/genmc/fail/loom/store_buffering.rs @@ -29,10 +29,6 @@ fn miri_start(_argc: isize, _argv: *const *const u8) -> isize { let x = AtomicUsize::new(0); let y = AtomicUsize::new(0); - // FIXME(genmc,HACK): remove these initializing writes once Miri-GenMC supports mixed atomic-non-atomic accesses. - x.store(0, Relaxed); - y.store(0, Relaxed); - let mut a: usize = 1234; let mut b: usize = 1234; unsafe { From a75e80c1fdee4771b5f30f181140cd192f095a44 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Thu, 9 Apr 2026 19:55:42 +0200 Subject: [PATCH 16/20] remove unused type --- src/tools/miri/tests/fail/weak_memory/weak_uninit.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/tools/miri/tests/fail/weak_memory/weak_uninit.rs b/src/tools/miri/tests/fail/weak_memory/weak_uninit.rs index 7a4e038fabf79..f70ed0cbab8a8 100644 --- a/src/tools/miri/tests/fail/weak_memory/weak_uninit.rs +++ b/src/tools/miri/tests/fail/weak_memory/weak_uninit.rs @@ -10,13 +10,6 @@ use std::sync::atomic::*; use std::thread::spawn; -#[allow(dead_code)] -#[derive(Copy, Clone)] -struct EvilSend(pub T); - -unsafe impl Send for EvilSend {} -unsafe impl Sync for EvilSend {} - // We can't create static items because we need to run each test multiple times. fn static_uninit_atomic() -> &'static AtomicUsize { unsafe { Box::leak(Box::new_uninit()).assume_init_ref() } From 4e7ec9144f5bbce75f23fd1886a7a6cd4a9ff254 Mon Sep 17 00:00:00 2001 From: WhySoBad <49595640+WhySoBad@users.noreply.github.com> Date: Thu, 9 Apr 2026 21:30:56 +0200 Subject: [PATCH 17/20] fix: implement feedback --- src/tools/miri/src/concurrency/blocking_io.rs | 100 ++++++++---------- src/tools/miri/src/concurrency/thread.rs | 10 +- src/tools/miri/src/shims/mod.rs | 2 +- src/tools/miri/src/shims/unix/fd.rs | 2 +- src/tools/miri/src/shims/unix/socket.rs | 6 +- 5 files changed, 54 insertions(+), 66 deletions(-) diff --git a/src/tools/miri/src/concurrency/blocking_io.rs b/src/tools/miri/src/concurrency/blocking_io.rs index 4904d53c09796..f37cc20600902 100644 --- a/src/tools/miri/src/concurrency/blocking_io.rs +++ b/src/tools/miri/src/concurrency/blocking_io.rs @@ -5,7 +5,7 @@ use mio::event::Source; use mio::{Events, Interest, Poll, Token}; use rustc_data_structures::fx::FxHashMap; -use crate::shims::FdId; +use crate::shims::{FdId, FileDescriptionRef}; use crate::*; /// Capacity of the event queue which can be polled at a time. @@ -17,17 +17,15 @@ const IO_EVENT_CAPACITY: usize = 16; pub trait WithSource { /// Invoke `f` on the source inside `self`. fn with_source(&self, f: &mut dyn FnMut(&mut dyn Source) -> io::Result<()>) -> io::Result<()>; - - /// Unique identifier of the file description to which the - /// source belongs. - fn id(&self) -> FdId; } #[derive(Debug, Hash, PartialEq, Clone, Copy, Eq, PartialOrd, Ord)] -pub enum InterestReason { - /// A thread is blocked on this source and wants to get unblocked - /// when the interest on the source is fulfilled. - ThreadBlocked(ThreadId), +/// An interest receiver is associated with an [`Interest`] and an action +/// to be done when the [`Interest`] is fulfilled. +pub enum InterestReceiver { + /// The specified thread should be unblocked when the interest + /// associated with the receiver is fulfilled. + UnblockThread(ThreadId), } /// Manager for managing blocking host I/O in a non-blocking manner. @@ -46,9 +44,12 @@ pub struct BlockingIoManager { /// This is not part of the state and only stored to avoid allocating a /// new buffer for every poll. events: Events, - /// Map from source ids to the actual sources and the registered interests for - /// every source. - sources: FxHashMap, FxHashMap)>, + /// Map from source ids to the actual sources and their registered receivers + /// together with their associated interests. + sources: FxHashMap< + FdId, + (FileDescriptionRef, FxHashMap), + >, } impl BlockingIoManager { @@ -71,12 +72,12 @@ impl BlockingIoManager { /// specified duration. /// - If the timeout is [`None`] the poll blocks indefinitely until an event occurs. /// - /// Returns all interest reasons which are fulfilled through an I/O event together with the - /// sources they are registered for. + /// Returns the [`InterestReceiver`]s for all file descriptions which received an I/O event together + /// with the file description they were registered for. pub fn poll( &mut self, timeout: Option, - ) -> Result, io::Error> { + ) -> Result)>, io::Error> { let poll = self.poll.as_mut().expect("Blocking I/O should not be called with isolation enabled"); @@ -89,19 +90,11 @@ impl BlockingIoManager { .flat_map(|event| { let token = event.token(); let fd_id = FdId::new_unchecked(token.0); - let (_, interests) = self.sources.get(&fd_id).expect("Source should be registered"); - interests - .iter() - .filter(|(_, interest)| { - // Only retain interests of the source which are fulfilled by - // the current event. - (event.is_readable() && interest.is_readable()) - || (event.is_writable() && interest.is_writable()) - || (event.is_aio() && interest.is_aio()) - || (event.is_lio() && interest.is_lio()) - || (event.is_priority() && interest.is_priority()) - }) - .map(move |(reason, _)| (*reason, fd_id)) + let (source, interests) = + self.sources.get(&fd_id).expect("Source should be registered"); + // Because we allow spurious wake-ups, we mark all interests as ready even + // though some may not have been fulfilled. + interests.keys().map(move |receiver| (*receiver, source.clone())) }) .collect::>(); @@ -116,14 +109,14 @@ impl BlockingIoManager { /// It's assumed that no interest is already registered for this source with the same reason! pub fn register( &mut self, - source: Box, - reason: InterestReason, + source_fd: FileDescriptionRef, + receiver: InterestReceiver, interest: Interest, ) { let poll = self.poll.as_ref().expect("Blocking I/O should not be called with isolation enabled"); - let id = source.id(); + let id = source_fd.id(); let token = Token(id.to_usize()); let Some((_, current_interests)) = self.sources.get_mut(&id) else { @@ -131,11 +124,11 @@ impl BlockingIoManager { // Treat errors from registering as fatal. On UNIX hosts this can only // fail due to system resource errors (e.g. ENOMEM or ENOSPC). - source - .with_source(&mut |source| source.register(poll.registry(), token, interest)) + source_fd + .with_source(&mut |source| poll.registry().register(source, token, interest)) .unwrap(); - self.sources.insert(id, (source, FxHashMap::from_iter([(reason, interest)]))); + self.sources.insert(id, (source_fd, FxHashMap::from_iter([(receiver, interest)]))); return; }; @@ -146,42 +139,42 @@ impl BlockingIoManager { interest_union(current_interests).expect("Source should contain at least one interest"); current_interests - .try_insert(reason, interest) - .unwrap_or_else(|_| panic!("Interest reason should be unique")); + .try_insert(receiver, interest) + .unwrap_or_else(|_| panic!("Receiver should be unique")); let new_interest = old_interest.add(interest); if new_interest == old_interest { // The overall interests in the source did not change and thus we - // don't need to reregister the source. + // don't need to reregister it. return; } // The overall interests in the source changed. We need to reregister - // the source with the updated interests. + // it with the updated interests. // Treat errors from reregistering as fatal. On UNIX hosts this can only // fail due to system resource errors (e.g. ENOMEM or ENOSPC). - source - .with_source(&mut |source| source.reregister(poll.registry(), token, new_interest)) + source_fd + .with_source(&mut |source| poll.registry().reregister(source, token, new_interest)) .unwrap(); } /// Deregister an interest from a blocking I/O source. /// - /// The source and the interest on this source are assumed to be registered! - pub fn deregister(&mut self, source_id: FdId, reason: InterestReason) { + /// The receiver is assumed to be registered for the provided source! + pub fn deregister(&mut self, source_id: FdId, receiver: InterestReceiver) { let poll = self.poll.as_ref().expect("Blocking I/O should not be called with isolation enabled"); let token = Token(source_id.to_usize()); - let (source, current_interests) = + let (fd, current_interests) = self.sources.get_mut(&source_id).expect("Source should be registered"); let old_interest = interest_union(current_interests).expect("Source should contain at least one interest"); current_interests - .remove(&reason) - .unwrap_or_else(|| panic!("Interest reason should be registered for source")); + .remove(&receiver) + .unwrap_or_else(|| panic!("Receiver should be registered for source")); let Some(new_interest) = interest_union(current_interests) else { // There are no longer any interests in this source. @@ -189,30 +182,29 @@ impl BlockingIoManager { // Treat errors from deregistering as fatal. On UNIX hosts this can only // fail due to system resource errors (e.g. ENOMEM or ENOSPC). - source.with_source(&mut |source| source.deregister(poll.registry())).unwrap(); + fd.with_source(&mut |source| poll.registry().deregister(source)).unwrap(); self.sources.remove(&source_id); return; }; if new_interest == old_interest { // The overall interests in the source did not change and thus we - // don't need to reregister the source. + // don't need to reregister it. return; } // The overall interests in the source changed. We need to reregister - // the source with the updated interests. + // it with the updated interests. // Treat errors from reregistering as fatal. On UNIX hosts this can only // fail due to system resource errors (e.g. ENOMEM or ENOSPC). - source - .with_source(&mut |source| source.reregister(poll.registry(), token, new_interest)) + fd.with_source(&mut |source| poll.registry().reregister(source, token, new_interest)) .unwrap(); } } /// Get the union of all interests for a source. -fn interest_union(interests: &FxHashMap) -> Option { +fn interest_union(interests: &FxHashMap) -> Option { interests .values() .copied() @@ -231,15 +223,15 @@ pub trait EvalContextExt<'tcx>: MiriInterpCxExt<'tcx> { #[inline] fn block_thread_for_io( &mut self, - source: impl WithSource + 'static, + source_fd: FileDescriptionRef, interests: Interest, timeout: Option<(TimeoutClock, TimeoutAnchor, Duration)>, callback: DynUnblockCallback<'tcx>, ) { let this = self.eval_context_mut(); this.machine.blocking_io.register( - Box::new(source), - InterestReason::ThreadBlocked(this.machine.threads.active_thread()), + source_fd, + InterestReceiver::UnblockThread(this.machine.threads.active_thread()), interests, ); this.block_thread(BlockReason::IO, timeout, callback); diff --git a/src/tools/miri/src/concurrency/thread.rs b/src/tools/miri/src/concurrency/thread.rs index f978fd81a7269..db00e53645c80 100644 --- a/src/tools/miri/src/concurrency/thread.rs +++ b/src/tools/miri/src/concurrency/thread.rs @@ -18,7 +18,7 @@ use rustc_span::{DUMMY_SP, Span}; use rustc_target::spec::Os; use crate::concurrency::GlobalDataRaceHandler; -use crate::concurrency::blocking_io::InterestReason; +use crate::concurrency::blocking_io::InterestReceiver; use crate::shims::tls; use crate::*; @@ -823,10 +823,10 @@ trait EvalContextPrivExt<'tcx>: MiriInterpCxExt<'tcx> { Err(e) => panic!("unexpected error while polling: {e}"), }; - ready.into_iter().try_for_each(|(reason, source_id)| { - match reason { - InterestReason::ThreadBlocked(thread_id) => { - this.machine.blocking_io.deregister(source_id, reason); + ready.into_iter().try_for_each(|(receiver, source)| { + match receiver { + InterestReceiver::UnblockThread(thread_id) => { + this.machine.blocking_io.deregister(source.id(), receiver); this.unblock_thread(thread_id, BlockReason::IO) } } diff --git a/src/tools/miri/src/shims/mod.rs b/src/tools/miri/src/shims/mod.rs index 6a7c12038d749..e15134fa01bb0 100644 --- a/src/tools/miri/src/shims/mod.rs +++ b/src/tools/miri/src/shims/mod.rs @@ -23,7 +23,7 @@ pub mod time; pub mod tls; pub mod unwind; -pub use self::files::{FdId, FdTable}; +pub use self::files::{FdId, FdTable, FileDescriptionRef}; #[cfg(all(feature = "native-lib", unix))] pub use self::native_lib::trace::{init_sv, register_retcode_sv}; pub use self::unix::{DirTable, EpollInterestTable}; diff --git a/src/tools/miri/src/shims/unix/fd.rs b/src/tools/miri/src/shims/unix/fd.rs index 460015d4c3ccc..065f040cd3e1d 100644 --- a/src/tools/miri/src/shims/unix/fd.rs +++ b/src/tools/miri/src/shims/unix/fd.rs @@ -15,7 +15,7 @@ use crate::shims::unix::*; use crate::*; #[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub(crate) enum FlockOp { +pub enum FlockOp { SharedLock { nonblocking: bool }, ExclusiveLock { nonblocking: bool }, Unlock, diff --git a/src/tools/miri/src/shims/unix/socket.rs b/src/tools/miri/src/shims/unix/socket.rs index e151e1efb16d6..41a510cfe9b85 100644 --- a/src/tools/miri/src/shims/unix/socket.rs +++ b/src/tools/miri/src/shims/unix/socket.rs @@ -1388,7 +1388,7 @@ impl VisitProvenance for FileDescriptionRef { fn visit_provenance(&self, _visit: &mut VisitWith<'_>) {} } -impl WithSource for FileDescriptionRef { +impl WithSource for Socket { fn with_source(&self, f: &mut dyn FnMut(&mut dyn Source) -> io::Result<()>) -> io::Result<()> { let mut state = self.state.borrow_mut(); match &mut *state { @@ -1398,8 +1398,4 @@ impl WithSource for FileDescriptionRef { _ => unreachable!(), } } - - fn id(&self) -> FdId { - self.id() - } } From 085e4f258ca455efd07035d0d42bf9aa598f0cf4 Mon Sep 17 00:00:00 2001 From: WhySoBad <49595640+WhySoBad@users.noreply.github.com> Date: Thu, 9 Apr 2026 22:01:25 +0200 Subject: [PATCH 18/20] fix: fix doc comment --- src/tools/miri/src/concurrency/blocking_io.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/miri/src/concurrency/blocking_io.rs b/src/tools/miri/src/concurrency/blocking_io.rs index f37cc20600902..509ae7643384e 100644 --- a/src/tools/miri/src/concurrency/blocking_io.rs +++ b/src/tools/miri/src/concurrency/blocking_io.rs @@ -72,7 +72,7 @@ impl BlockingIoManager { /// specified duration. /// - If the timeout is [`None`] the poll blocks indefinitely until an event occurs. /// - /// Returns the [`InterestReceiver`]s for all file descriptions which received an I/O event together + /// Returns the interest receivers for all file descriptions which received an I/O event together /// with the file description they were registered for. pub fn poll( &mut self, From 0b7f2254beeae81994fa7b9f1891036dfcef554e Mon Sep 17 00:00:00 2001 From: WhySoBad <49595640+WhySoBad@users.noreply.github.com> Date: Fri, 10 Apr 2026 21:06:22 +0200 Subject: [PATCH 19/20] fix: implement feedback --- src/tools/miri/src/concurrency/blocking_io.rs | 41 ++++++------------- src/tools/miri/src/shims/files.rs | 2 +- .../pass-dep/libc/libc-blocking-io-same-fd.rs | 5 +-- 3 files changed, 16 insertions(+), 32 deletions(-) diff --git a/src/tools/miri/src/concurrency/blocking_io.rs b/src/tools/miri/src/concurrency/blocking_io.rs index 509ae7643384e..2f7e75e9442cc 100644 --- a/src/tools/miri/src/concurrency/blocking_io.rs +++ b/src/tools/miri/src/concurrency/blocking_io.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use std::io; use std::time::Duration; @@ -19,12 +20,11 @@ pub trait WithSource { fn with_source(&self, f: &mut dyn FnMut(&mut dyn Source) -> io::Result<()>) -> io::Result<()>; } +/// An interest receiver defines the action that should be taken when +/// the associated [`Interest`] is fulfilled. #[derive(Debug, Hash, PartialEq, Clone, Copy, Eq, PartialOrd, Ord)] -/// An interest receiver is associated with an [`Interest`] and an action -/// to be done when the [`Interest`] is fulfilled. pub enum InterestReceiver { - /// The specified thread should be unblocked when the interest - /// associated with the receiver is fulfilled. + /// The specified thread should be unblocked. UnblockThread(ThreadId), } @@ -46,10 +46,8 @@ pub struct BlockingIoManager { events: Events, /// Map from source ids to the actual sources and their registered receivers /// together with their associated interests. - sources: FxHashMap< - FdId, - (FileDescriptionRef, FxHashMap), - >, + sources: + BTreeMap, FxHashMap)>, } impl BlockingIoManager { @@ -59,7 +57,7 @@ impl BlockingIoManager { let manager = Self { poll: communicate.then_some(Poll::new()?), events: Events::with_capacity(IO_EVENT_CAPACITY), - sources: FxHashMap::default(), + sources: BTreeMap::default(), }; Ok(manager) } @@ -89,9 +87,11 @@ impl BlockingIoManager { .iter() .flat_map(|event| { let token = event.token(); + // We know all tokens are valid `FdId`. let fd_id = FdId::new_unchecked(token.0); let (source, interests) = self.sources.get(&fd_id).expect("Source should be registered"); + assert_eq!(source.id(), fd_id); // Because we allow spurious wake-ups, we mark all interests as ready even // though some may not have been fulfilled. interests.keys().map(move |receiver| (*receiver, source.clone())) @@ -143,14 +143,8 @@ impl BlockingIoManager { .unwrap_or_else(|_| panic!("Receiver should be unique")); let new_interest = old_interest.add(interest); - if new_interest == old_interest { - // The overall interests in the source did not change and thus we - // don't need to reregister it. - return; - } - // The overall interests in the source changed. We need to reregister - // it with the updated interests. + // Reregister the source since the overall interests might have changed. // Treat errors from reregistering as fatal. On UNIX hosts this can only // fail due to system resource errors (e.g. ENOMEM or ENOSPC). @@ -169,8 +163,6 @@ impl BlockingIoManager { let token = Token(source_id.to_usize()); let (fd, current_interests) = self.sources.get_mut(&source_id).expect("Source should be registered"); - let old_interest = - interest_union(current_interests).expect("Source should contain at least one interest"); current_interests .remove(&receiver) @@ -187,14 +179,7 @@ impl BlockingIoManager { return; }; - if new_interest == old_interest { - // The overall interests in the source did not change and thus we - // don't need to reregister it. - return; - } - - // The overall interests in the source changed. We need to reregister - // it with the updated interests. + // Reregister the source since the overall interests might have changed. // Treat errors from reregistering as fatal. On UNIX hosts this can only // fail due to system resource errors (e.g. ENOMEM or ENOSPC). @@ -203,7 +188,7 @@ impl BlockingIoManager { } } -/// Get the union of all interests for a source. +/// Get the union of all interests for a source. Returns `None` if the map is empty. fn interest_union(interests: &FxHashMap) -> Option { interests .values() @@ -223,7 +208,7 @@ pub trait EvalContextExt<'tcx>: MiriInterpCxExt<'tcx> { #[inline] fn block_thread_for_io( &mut self, - source_fd: FileDescriptionRef, + source_fd: FileDescriptionRef, interests: Interest, timeout: Option<(TimeoutClock, TimeoutAnchor, Duration)>, callback: DynUnblockCallback<'tcx>, diff --git a/src/tools/miri/src/shims/files.rs b/src/tools/miri/src/shims/files.rs index d123397dda286..5468fd3037425 100644 --- a/src/tools/miri/src/shims/files.rs +++ b/src/tools/miri/src/shims/files.rs @@ -16,7 +16,7 @@ use crate::*; /// is definitely unique, the address would expose interpreter internal state when used /// for sorting things. So instead we generate a unique id per file description which is the same /// for all `dup`licates and is never reused. -#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Ord, PartialOrd)] pub struct FdId(usize); impl FdId { diff --git a/src/tools/miri/tests/pass-dep/libc/libc-blocking-io-same-fd.rs b/src/tools/miri/tests/pass-dep/libc/libc-blocking-io-same-fd.rs index ec4287539b7ed..d4bae144f2213 100644 --- a/src/tools/miri/tests/pass-dep/libc/libc-blocking-io-same-fd.rs +++ b/src/tools/miri/tests/pass-dep/libc/libc-blocking-io-same-fd.rs @@ -1,10 +1,9 @@ //@ignore-target: windows # No libc socket on Windows -//@compile-flags: -Zmiri-disable-isolation +//@compile-flags: -Zmiri-disable-isolation -Zmiri-fixed-schedule #[path = "../../utils/libc.rs"] mod libc_utils; use std::thread; -use std::time::Duration; use libc_utils::*; @@ -22,7 +21,7 @@ fn main() { // Yield back to reader threads to ensure that we have // two threads being blocked on the same fd at the same time. - thread::sleep(Duration::from_millis(10)); + thread::yield_now(); let mut buffer = [22u8; 128]; let bytes_written = unsafe { From 6d4d7a6daa15fd56115549aaa0fa7296a5338de6 Mon Sep 17 00:00:00 2001 From: WhySoBad <49595640+WhySoBad@users.noreply.github.com> Date: Fri, 10 Apr 2026 23:19:58 +0200 Subject: [PATCH 20/20] fix: deregister I/O sources in blocking I/O manager --- src/tools/miri/src/concurrency/blocking_io.rs | 3 +++ src/tools/miri/src/concurrency/thread.rs | 8 +++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/tools/miri/src/concurrency/blocking_io.rs b/src/tools/miri/src/concurrency/blocking_io.rs index 2f7e75e9442cc..ba1e52cd39712 100644 --- a/src/tools/miri/src/concurrency/blocking_io.rs +++ b/src/tools/miri/src/concurrency/blocking_io.rs @@ -98,6 +98,9 @@ impl BlockingIoManager { }) .collect::>(); + // Deregister all ready sources as we only want to receive one event per receiver. + ready.iter().for_each(|(receiver, source)| self.deregister(source.id(), *receiver)); + Ok(ready) } diff --git a/src/tools/miri/src/concurrency/thread.rs b/src/tools/miri/src/concurrency/thread.rs index db00e53645c80..e9458cc3f4568 100644 --- a/src/tools/miri/src/concurrency/thread.rs +++ b/src/tools/miri/src/concurrency/thread.rs @@ -823,12 +823,10 @@ trait EvalContextPrivExt<'tcx>: MiriInterpCxExt<'tcx> { Err(e) => panic!("unexpected error while polling: {e}"), }; - ready.into_iter().try_for_each(|(receiver, source)| { + ready.into_iter().try_for_each(|(receiver, _source)| { match receiver { - InterestReceiver::UnblockThread(thread_id) => { - this.machine.blocking_io.deregister(source.id(), receiver); - this.unblock_thread(thread_id, BlockReason::IO) - } + InterestReceiver::UnblockThread(thread_id) => + this.unblock_thread(thread_id, BlockReason::IO), } }) }