about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2024-07-14 21:14:02 +0000
committerbors <bors@rust-lang.org>2024-07-14 21:14:02 +0000
commitd9284afea99e0969a0e692b9e9fd61ea4ba21366 (patch)
tree1b0f6f632be00a909aead2a31bbce3ff58301db6
parentf8e4ac0760216724f69b1ebc369bf03da56a7520 (diff)
parente90f04731a382a27962a30113f8283a59a6ab87e (diff)
downloadrust-d9284afea99e0969a0e692b9e9fd61ea4ba21366.tar.gz
rust-d9284afea99e0969a0e692b9e9fd61ea4ba21366.zip
Auto merge of #127726 - RalfJung:miri-sync, r=RalfJung
Miri subtree update

r? `@ghost`
-rw-r--r--src/tools/miri/cargo-miri/src/phases.rs26
-rwxr-xr-xsrc/tools/miri/ci/ci.sh4
-rw-r--r--src/tools/miri/rust-version2
-rw-r--r--src/tools/miri/src/bin/miri.rs3
-rw-r--r--src/tools/miri/src/borrow_tracker/stacked_borrows/stack.rs12
-rw-r--r--src/tools/miri/src/concurrency/cpu_affinity.rs90
-rw-r--r--src/tools/miri/src/concurrency/mod.rs1
-rw-r--r--src/tools/miri/src/concurrency/sync.rs21
-rw-r--r--src/tools/miri/src/concurrency/thread.rs5
-rw-r--r--src/tools/miri/src/eval.rs3
-rw-r--r--src/tools/miri/src/helpers.rs55
-rw-r--r--src/tools/miri/src/intrinsics/mod.rs4
-rw-r--r--src/tools/miri/src/lib.rs1
-rw-r--r--src/tools/miri/src/machine.rs23
-rw-r--r--src/tools/miri/src/provenance_gc.rs11
-rw-r--r--src/tools/miri/src/shims/tls.rs65
-rw-r--r--src/tools/miri/src/shims/unix/fd.rs7
-rw-r--r--src/tools/miri/src/shims/unix/foreign_items.rs95
-rw-r--r--src/tools/miri/src/shims/unix/linux/foreign_items.rs13
-rw-r--r--src/tools/miri/src/shims/unix/macos/foreign_items.rs24
-rw-r--r--src/tools/miri/src/shims/unix/macos/mod.rs1
-rw-r--r--src/tools/miri/src/shims/unix/macos/sync.rs107
-rw-r--r--src/tools/miri/src/shims/unix/sync.rs2
-rwxr-xr-xsrc/tools/miri/test-cargo-miri/run-test.py20
-rw-r--r--src/tools/miri/test-cargo-miri/test.multiple_targets.stdout.ref22
-rw-r--r--src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_assert_not_owner.rs13
-rw-r--r--src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_assert_not_owner.stderr13
-rw-r--r--src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_assert_owner.rs12
-rw-r--r--src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_assert_owner.stderr13
-rw-r--r--src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_reentrant.rs13
-rw-r--r--src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_reentrant.stderr13
-rw-r--r--src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_unowned.rs12
-rw-r--r--src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_unowned.stderr13
-rw-r--r--src/tools/miri/tests/fail-dep/libc/affinity.rs17
-rw-r--r--src/tools/miri/tests/fail-dep/libc/affinity.stderr20
-rw-r--r--src/tools/miri/tests/fail-dep/libc/libc-read-and-uninit-premature-eof.rs27
-rw-r--r--src/tools/miri/tests/fail-dep/libc/libc-read-and-uninit-premature-eof.stderr15
-rw-r--r--src/tools/miri/tests/fail/dangling_pointers/dangling_pointer_to_raw_pointer.rs20
-rw-r--r--src/tools/miri/tests/fail/dangling_pointers/dangling_pointer_to_raw_pointer.stderr20
-rw-r--r--src/tools/miri/tests/pass-dep/concurrency/apple-os-unfair-lock.rs25
-rw-r--r--src/tools/miri/tests/pass-dep/libc/libc-affinity.rs218
-rw-r--r--src/tools/miri/tests/pass-dep/libc/libc-fs.rs35
-rw-r--r--src/tools/miri/tests/pass/float.rs642
-rw-r--r--src/tools/miri/tests/pass/tls/macos_tlv_atexit.rs43
-rw-r--r--src/tools/miri/tests/pass/tls/macos_tlv_atexit.stdout7
45 files changed, 1531 insertions, 277 deletions
diff --git a/src/tools/miri/cargo-miri/src/phases.rs b/src/tools/miri/cargo-miri/src/phases.rs
index 8d48b9c8ad1..3743446e276 100644
--- a/src/tools/miri/cargo-miri/src/phases.rs
+++ b/src/tools/miri/cargo-miri/src/phases.rs
@@ -104,9 +104,17 @@ pub fn phase_cargo_miri(mut args: impl Iterator<Item = String>) {
             miri_for_host()
         )
     });
-    let host = &rustc_version.host;
-    let target = get_arg_flag_value("--target");
-    let target = target.as_ref().unwrap_or(host);
+    let mut targets = get_arg_flag_values("--target").collect::<Vec<_>>();
+    // If `targets` is empty, we need to add a `--target $HOST` flag ourselves, and also ensure
+    // that the host target is indeed setup.
+    let target_flag = if targets.is_empty() {
+        let host = &rustc_version.host;
+        targets.push(host.clone());
+        Some(host)
+    } else {
+        // We don't need to add a `--target` flag, we just forward the user's flags.
+        None
+    };
 
     // If cleaning the target directory & sysroot cache,
     // delete them then exit. There is no reason to setup a new
@@ -118,8 +126,11 @@ pub fn phase_cargo_miri(mut args: impl Iterator<Item = String>) {
         return;
     }
 
-    // We always setup.
-    let miri_sysroot = setup(&subcommand, target, &rustc_version, verbose, quiet);
+    for target in &targets {
+        // We always setup.
+        setup(&subcommand, target.as_str(), &rustc_version, verbose, quiet);
+    }
+    let miri_sysroot = get_sysroot_dir();
 
     // Invoke actual cargo for the job, but with different flags.
     // We re-use `cargo test` and `cargo run`, which makes target and binary handling very easy but
@@ -155,10 +166,9 @@ pub fn phase_cargo_miri(mut args: impl Iterator<Item = String>) {
     // This is needed to make the `target.runner` settings do something,
     // and it later helps us detect which crates are proc-macro/build-script
     // (host crates) and which crates are needed for the program itself.
-    if get_arg_flag_value("--target").is_none() {
-        // No target given. Explicitly pick the host.
+    if let Some(target_flag) = target_flag {
         cmd.arg("--target");
-        cmd.arg(host);
+        cmd.arg(target_flag);
     }
 
     // Set ourselves as runner for al binaries invoked by cargo.
diff --git a/src/tools/miri/ci/ci.sh b/src/tools/miri/ci/ci.sh
index 67985f9b7d6..5e75638f467 100755
--- a/src/tools/miri/ci/ci.sh
+++ b/src/tools/miri/ci/ci.sh
@@ -41,9 +41,11 @@ function run_tests {
   if [ -n "${TEST_TARGET-}" ]; then
     begingroup "Testing foreign architecture $TEST_TARGET"
     TARGET_FLAG="--target $TEST_TARGET"
+    MULTI_TARGET_FLAG=""
   else
     begingroup "Testing host architecture"
     TARGET_FLAG=""
+    MULTI_TARGET_FLAG="--multi-target"
   fi
 
   ## ui test suite
@@ -93,7 +95,7 @@ function run_tests {
     echo 'build.rustc-wrapper = "thisdoesnotexist"' > .cargo/config.toml
   fi
   # Run the actual test
-  time ${PYTHON} test-cargo-miri/run-test.py $TARGET_FLAG
+  time ${PYTHON} test-cargo-miri/run-test.py $TARGET_FLAG $MULTI_TARGET_FLAG
   # Clean up
   unset RUSTC MIRI
   rm -rf .cargo
diff --git a/src/tools/miri/rust-version b/src/tools/miri/rust-version
index 5a35166769e..e90d3732ca5 100644
--- a/src/tools/miri/rust-version
+++ b/src/tools/miri/rust-version
@@ -1 +1 @@
-66b4f0021bfb11a8c20d084c99a40f4a78ce1d38
+99b7134389e9766462601a2fc4013840b9d31745
diff --git a/src/tools/miri/src/bin/miri.rs b/src/tools/miri/src/bin/miri.rs
index 9d8e44ce409..9f3fa075f38 100644
--- a/src/tools/miri/src/bin/miri.rs
+++ b/src/tools/miri/src/bin/miri.rs
@@ -592,6 +592,9 @@ fn main() {
             let num_cpus = param
                 .parse::<u32>()
                 .unwrap_or_else(|err| show_error!("-Zmiri-num-cpus requires a `u32`: {}", err));
+            if !(1..=miri::MAX_CPUS).contains(&usize::try_from(num_cpus).unwrap()) {
+                show_error!("-Zmiri-num-cpus must be in the range 1..={}", miri::MAX_CPUS);
+            }
             miri_config.num_cpus = num_cpus;
         } else if let Some(param) = arg.strip_prefix("-Zmiri-force-page-size=") {
             let page_size = param.parse::<u64>().unwrap_or_else(|err| {
diff --git a/src/tools/miri/src/borrow_tracker/stacked_borrows/stack.rs b/src/tools/miri/src/borrow_tracker/stacked_borrows/stack.rs
index f65f49a75dd..774b36919fe 100644
--- a/src/tools/miri/src/borrow_tracker/stacked_borrows/stack.rs
+++ b/src/tools/miri/src/borrow_tracker/stacked_borrows/stack.rs
@@ -136,8 +136,16 @@ impl StackCache {
 
 impl PartialEq for Stack {
     fn eq(&self, other: &Self) -> bool {
-        // All the semantics of Stack are in self.borrows, everything else is caching
-        self.borrows == other.borrows
+        let Stack {
+            borrows,
+            unknown_bottom,
+            // The cache is ignored for comparison.
+            #[cfg(feature = "stack-cache")]
+                cache: _,
+            #[cfg(feature = "stack-cache")]
+                unique_range: _,
+        } = self;
+        *borrows == other.borrows && *unknown_bottom == other.unknown_bottom
     }
 }
 
diff --git a/src/tools/miri/src/concurrency/cpu_affinity.rs b/src/tools/miri/src/concurrency/cpu_affinity.rs
new file mode 100644
index 00000000000..8df26d718bf
--- /dev/null
+++ b/src/tools/miri/src/concurrency/cpu_affinity.rs
@@ -0,0 +1,90 @@
+use rustc_middle::ty::layout::LayoutOf;
+use rustc_target::abi::Endian;
+
+use crate::*;
+
+/// The maximum number of CPUs supported by miri.
+///
+/// This value is compatible with the libc `CPU_SETSIZE` constant and corresponds to the number
+/// of CPUs that a `cpu_set_t` can contain.
+///
+/// Real machines can have more CPUs than this number, and there exist APIs to set their affinity,
+/// but this is not currently supported by miri.
+pub const MAX_CPUS: usize = 1024;
+
+/// A thread's CPU affinity mask determines the set of CPUs on which it is eligible to run.
+// the actual representation depends on the target's endianness and pointer width.
+// See CpuAffinityMask::set for details
+#[derive(Clone)]
+pub(crate) struct CpuAffinityMask([u8; Self::CPU_MASK_BYTES]);
+
+impl CpuAffinityMask {
+    pub(crate) const CPU_MASK_BYTES: usize = MAX_CPUS / 8;
+
+    pub fn new<'tcx>(cx: &impl LayoutOf<'tcx>, cpu_count: u32) -> Self {
+        let mut this = Self([0; Self::CPU_MASK_BYTES]);
+
+        // the default affinity mask includes only the available CPUs
+        for i in 0..cpu_count as usize {
+            this.set(cx, i);
+        }
+
+        this
+    }
+
+    pub fn chunk_size<'tcx>(cx: &impl LayoutOf<'tcx>) -> u64 {
+        // The actual representation of the CpuAffinityMask is [c_ulong; _].
+        let ulong = helpers::path_ty_layout(cx, &["core", "ffi", "c_ulong"]);
+        ulong.size.bytes()
+    }
+
+    fn set<'tcx>(&mut self, cx: &impl LayoutOf<'tcx>, cpu: usize) {
+        // we silently ignore CPUs that are out of bounds. This matches the behavior of
+        // `sched_setaffinity` with a mask that specifies more than `CPU_SETSIZE` CPUs.
+        if cpu >= MAX_CPUS {
+            return;
+        }
+
+        // The actual representation of the CpuAffinityMask is [c_ulong; _].
+        // Within the array elements, we need to use the endianness of the target.
+        let target = &cx.tcx().sess.target;
+        match Self::chunk_size(cx) {
+            4 => {
+                let start = cpu / 32 * 4; // first byte of the correct u32
+                let chunk = self.0[start..].first_chunk_mut::<4>().unwrap();
+                let offset = cpu % 32;
+                *chunk = match target.options.endian {
+                    Endian::Little => (u32::from_le_bytes(*chunk) | 1 << offset).to_le_bytes(),
+                    Endian::Big => (u32::from_be_bytes(*chunk) | 1 << offset).to_be_bytes(),
+                };
+            }
+            8 => {
+                let start = cpu / 64 * 8; // first byte of the correct u64
+                let chunk = self.0[start..].first_chunk_mut::<8>().unwrap();
+                let offset = cpu % 64;
+                *chunk = match target.options.endian {
+                    Endian::Little => (u64::from_le_bytes(*chunk) | 1 << offset).to_le_bytes(),
+                    Endian::Big => (u64::from_be_bytes(*chunk) | 1 << offset).to_be_bytes(),
+                };
+            }
+            other => bug!("chunk size not supported: {other}"),
+        };
+    }
+
+    pub fn as_slice(&self) -> &[u8] {
+        self.0.as_slice()
+    }
+
+    pub fn from_array<'tcx>(
+        cx: &impl LayoutOf<'tcx>,
+        cpu_count: u32,
+        bytes: [u8; Self::CPU_MASK_BYTES],
+    ) -> Option<Self> {
+        // mask by what CPUs are actually available
+        let default = Self::new(cx, cpu_count);
+        let masked = std::array::from_fn(|i| bytes[i] & default.0[i]);
+
+        // at least one thread must be set for the input to be valid
+        masked.iter().any(|b| *b != 0).then_some(Self(masked))
+    }
+}
diff --git a/src/tools/miri/src/concurrency/mod.rs b/src/tools/miri/src/concurrency/mod.rs
index 822d173ac06..17789fe9f87 100644
--- a/src/tools/miri/src/concurrency/mod.rs
+++ b/src/tools/miri/src/concurrency/mod.rs
@@ -1,3 +1,4 @@
+pub mod cpu_affinity;
 pub mod data_race;
 pub mod init_once;
 mod range_object_map;
diff --git a/src/tools/miri/src/concurrency/sync.rs b/src/tools/miri/src/concurrency/sync.rs
index 91865a2192c..d0c9a4600e8 100644
--- a/src/tools/miri/src/concurrency/sync.rs
+++ b/src/tools/miri/src/concurrency/sync.rs
@@ -269,7 +269,7 @@ pub(super) trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {
         let this = self.eval_context_mut();
         if this.mutex_is_locked(mutex) {
             assert_ne!(this.mutex_get_owner(mutex), this.active_thread());
-            this.mutex_enqueue_and_block(mutex, retval, dest);
+            this.mutex_enqueue_and_block(mutex, Some((retval, dest)));
         } else {
             // We can have it right now!
             this.mutex_lock(mutex);
@@ -390,9 +390,15 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
     }
 
     /// Put the thread into the queue waiting for the mutex.
-    /// Once the Mutex becomes available, `retval` will be written to `dest`.
+    ///
+    /// Once the Mutex becomes available and if it exists, `retval_dest.0` will
+    /// be written to `retval_dest.1`.
     #[inline]
-    fn mutex_enqueue_and_block(&mut self, id: MutexId, retval: Scalar, dest: MPlaceTy<'tcx>) {
+    fn mutex_enqueue_and_block(
+        &mut self,
+        id: MutexId,
+        retval_dest: Option<(Scalar, MPlaceTy<'tcx>)>,
+    ) {
         let this = self.eval_context_mut();
         assert!(this.mutex_is_locked(id), "queing on unlocked mutex");
         let thread = this.active_thread();
@@ -403,13 +409,16 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
             callback!(
                 @capture<'tcx> {
                     id: MutexId,
-                    retval: Scalar,
-                    dest: MPlaceTy<'tcx>,
+                    retval_dest: Option<(Scalar, MPlaceTy<'tcx>)>,
                 }
                 @unblock = |this| {
                     assert!(!this.mutex_is_locked(id));
                     this.mutex_lock(id);
-                    this.write_scalar(retval, &dest)?;
+
+                    if let Some((retval, dest)) = retval_dest {
+                        this.write_scalar(retval, &dest)?;
+                    }
+
                     Ok(())
                 }
             ),
diff --git a/src/tools/miri/src/concurrency/thread.rs b/src/tools/miri/src/concurrency/thread.rs
index 718daf93ea0..a53dd7eac1e 100644
--- a/src/tools/miri/src/concurrency/thread.rs
+++ b/src/tools/miri/src/concurrency/thread.rs
@@ -936,6 +936,11 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
         // After this all accesses will be treated as occurring in the new thread.
         let old_thread_id = this.machine.threads.set_active_thread_id(new_thread_id);
 
+        // The child inherits its parent's cpu affinity.
+        if let Some(cpuset) = this.machine.thread_cpu_affinity.get(&old_thread_id).cloned() {
+            this.machine.thread_cpu_affinity.insert(new_thread_id, cpuset);
+        }
+
         // Perform the function pointer load in the new thread frame.
         let instance = this.get_ptr_fn(start_routine)?.as_instance()?;
 
diff --git a/src/tools/miri/src/eval.rs b/src/tools/miri/src/eval.rs
index 9142b8b5fdb..2184a4426c8 100644
--- a/src/tools/miri/src/eval.rs
+++ b/src/tools/miri/src/eval.rs
@@ -282,7 +282,8 @@ pub fn create_ecx<'tcx>(
     })?;
 
     // Make sure we have MIR. We check MIR for some stable monomorphic function in libcore.
-    let sentinel = ecx.try_resolve_path(&["core", "ascii", "escape_default"], Namespace::ValueNS);
+    let sentinel =
+        helpers::try_resolve_path(tcx, &["core", "ascii", "escape_default"], Namespace::ValueNS);
     if !matches!(sentinel, Some(s) if tcx.is_mir_available(s.def.def_id())) {
         tcx.dcx().fatal(
             "the current sysroot was built without `-Zalways-encode-mir`, or libcore seems missing. \
diff --git a/src/tools/miri/src/helpers.rs b/src/tools/miri/src/helpers.rs
index 590e8984e99..ba094c988e5 100644
--- a/src/tools/miri/src/helpers.rs
+++ b/src/tools/miri/src/helpers.rs
@@ -18,6 +18,7 @@ use rustc_middle::middle::codegen_fn_attrs::CodegenFnAttrFlags;
 use rustc_middle::middle::dependency_format::Linkage;
 use rustc_middle::middle::exported_symbols::ExportedSymbol;
 use rustc_middle::mir;
+use rustc_middle::ty::layout::MaybeResult;
 use rustc_middle::ty::{
     self,
     layout::{LayoutOf, TyAndLayout},
@@ -159,6 +160,35 @@ fn try_resolve_did(tcx: TyCtxt<'_>, path: &[&str], namespace: Option<Namespace>)
     None
 }
 
+/// Gets an instance for a path; fails gracefully if the path does not exist.
+pub fn try_resolve_path<'tcx>(
+    tcx: TyCtxt<'tcx>,
+    path: &[&str],
+    namespace: Namespace,
+) -> Option<ty::Instance<'tcx>> {
+    let did = try_resolve_did(tcx, path, Some(namespace))?;
+    Some(ty::Instance::mono(tcx, did))
+}
+
+/// Gets an instance for a path.
+#[track_caller]
+pub fn resolve_path<'tcx>(
+    tcx: TyCtxt<'tcx>,
+    path: &[&str],
+    namespace: Namespace,
+) -> ty::Instance<'tcx> {
+    try_resolve_path(tcx, path, namespace)
+        .unwrap_or_else(|| panic!("failed to find required Rust item: {path:?}"))
+}
+
+/// Gets the layout of a type at a path.
+#[track_caller]
+pub fn path_ty_layout<'tcx>(cx: &impl LayoutOf<'tcx>, path: &[&str]) -> TyAndLayout<'tcx> {
+    let ty =
+        resolve_path(cx.tcx(), path, Namespace::TypeNS).ty(cx.tcx(), ty::ParamEnv::reveal_all());
+    cx.layout_of(ty).to_result().ok().unwrap()
+}
+
 /// Call `f` for each exported symbol.
 pub fn iter_exported_symbols<'tcx>(
     tcx: TyCtxt<'tcx>,
@@ -259,23 +289,10 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
         try_resolve_did(*self.eval_context_ref().tcx, path, None).is_some()
     }
 
-    /// Gets an instance for a path; fails gracefully if the path does not exist.
-    fn try_resolve_path(&self, path: &[&str], namespace: Namespace) -> Option<ty::Instance<'tcx>> {
-        let tcx = self.eval_context_ref().tcx.tcx;
-        let did = try_resolve_did(tcx, path, Some(namespace))?;
-        Some(ty::Instance::mono(tcx, did))
-    }
-
-    /// Gets an instance for a path.
-    fn resolve_path(&self, path: &[&str], namespace: Namespace) -> ty::Instance<'tcx> {
-        self.try_resolve_path(path, namespace)
-            .unwrap_or_else(|| panic!("failed to find required Rust item: {path:?}"))
-    }
-
     /// Evaluates the scalar at the specified path.
     fn eval_path(&self, path: &[&str]) -> OpTy<'tcx> {
         let this = self.eval_context_ref();
-        let instance = this.resolve_path(path, Namespace::ValueNS);
+        let instance = resolve_path(*this.tcx, path, Namespace::ValueNS);
         // We don't give a span -- this isn't actually used directly by the program anyway.
         let const_val = this.eval_global(instance).unwrap_or_else(|err| {
             panic!("failed to evaluate required Rust item: {path:?}\n{err:?}")
@@ -344,19 +361,13 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
                 "`libc` crate is not reliably available on Windows targets; Miri should not use it there"
             );
         }
-        let ty = this
-            .resolve_path(&["libc", name], Namespace::TypeNS)
-            .ty(*this.tcx, ty::ParamEnv::reveal_all());
-        this.layout_of(ty).unwrap()
+        path_ty_layout(this, &["libc", name])
     }
 
     /// Helper function to get the `TyAndLayout` of a `windows` type
     fn windows_ty_layout(&self, name: &str) -> TyAndLayout<'tcx> {
         let this = self.eval_context_ref();
-        let ty = this
-            .resolve_path(&["std", "sys", "pal", "windows", "c", name], Namespace::TypeNS)
-            .ty(*this.tcx, ty::ParamEnv::reveal_all());
-        this.layout_of(ty).unwrap()
+        path_ty_layout(this, &["std", "sys", "pal", "windows", "c", name])
     }
 
     /// Project to the given *named* field (which must be a struct or union type).
diff --git a/src/tools/miri/src/intrinsics/mod.rs b/src/tools/miri/src/intrinsics/mod.rs
index 313eac36337..9cd776c9371 100644
--- a/src/tools/miri/src/intrinsics/mod.rs
+++ b/src/tools/miri/src/intrinsics/mod.rs
@@ -392,10 +392,10 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
                         bug!("float_finite: non-float input type {}", x.layout.ty)
                     };
                     Ok(match fty {
-                        FloatTy::F16 => unimplemented!("f16_f128"),
+                        FloatTy::F16 => x.to_scalar().to_f16()?.is_finite(),
                         FloatTy::F32 => x.to_scalar().to_f32()?.is_finite(),
                         FloatTy::F64 => x.to_scalar().to_f64()?.is_finite(),
-                        FloatTy::F128 => unimplemented!("f16_f128"),
+                        FloatTy::F128 => x.to_scalar().to_f128()?.is_finite(),
                     })
                 };
                 match (float_finite(&a)?, float_finite(&b)?) {
diff --git a/src/tools/miri/src/lib.rs b/src/tools/miri/src/lib.rs
index 8da00861f90..7fb68d782f1 100644
--- a/src/tools/miri/src/lib.rs
+++ b/src/tools/miri/src/lib.rs
@@ -129,6 +129,7 @@ pub use crate::borrow_tracker::{
 };
 pub use crate::clock::{Clock, Instant};
 pub use crate::concurrency::{
+    cpu_affinity::MAX_CPUS,
     data_race::{AtomicFenceOrd, AtomicReadOrd, AtomicRwOrd, AtomicWriteOrd, EvalContextExt as _},
     init_once::{EvalContextExt as _, InitOnceId},
     sync::{CondvarId, EvalContextExt as _, MutexId, RwLockId, SynchronizationObjects},
diff --git a/src/tools/miri/src/machine.rs b/src/tools/miri/src/machine.rs
index 714d97823f4..adb84593561 100644
--- a/src/tools/miri/src/machine.rs
+++ b/src/tools/miri/src/machine.rs
@@ -30,6 +30,7 @@ use rustc_target::spec::abi::Abi;
 
 use crate::{
     concurrency::{
+        cpu_affinity::{self, CpuAffinityMask},
         data_race::{self, NaReadType, NaWriteType},
         weak_memory,
     },
@@ -471,6 +472,12 @@ pub struct MiriMachine<'tcx> {
 
     /// The set of threads.
     pub(crate) threads: ThreadManager<'tcx>,
+
+    /// Stores which thread is eligible to run on which CPUs.
+    /// This has no effect at all, it is just tracked to produce the correct result
+    /// in `sched_getaffinity`
+    pub(crate) thread_cpu_affinity: FxHashMap<ThreadId, CpuAffinityMask>,
+
     /// The state of the primitive synchronization objects.
     pub(crate) sync: SynchronizationObjects,
 
@@ -627,6 +634,18 @@ impl<'tcx> MiriMachine<'tcx> {
         let stack_addr = if tcx.pointer_size().bits() < 32 { page_size } else { page_size * 32 };
         let stack_size =
             if tcx.pointer_size().bits() < 32 { page_size * 4 } else { page_size * 16 };
+        assert!(
+            usize::try_from(config.num_cpus).unwrap() <= cpu_affinity::MAX_CPUS,
+            "miri only supports up to {} CPUs, but {} were configured",
+            cpu_affinity::MAX_CPUS,
+            config.num_cpus
+        );
+        let threads = ThreadManager::default();
+        let mut thread_cpu_affinity = FxHashMap::default();
+        if matches!(&*tcx.sess.target.os, "linux" | "freebsd" | "android") {
+            thread_cpu_affinity
+                .insert(threads.active_thread(), CpuAffinityMask::new(&layout_cx, config.num_cpus));
+        }
         MiriMachine {
             tcx,
             borrow_tracker,
@@ -644,7 +663,8 @@ impl<'tcx> MiriMachine<'tcx> {
             fds: shims::FdTable::new(config.mute_stdout_stderr),
             dirs: Default::default(),
             layouts,
-            threads: ThreadManager::default(),
+            threads,
+            thread_cpu_affinity,
             sync: SynchronizationObjects::default(),
             static_roots: Vec::new(),
             profiler,
@@ -765,6 +785,7 @@ impl VisitProvenance for MiriMachine<'_> {
         #[rustfmt::skip]
         let MiriMachine {
             threads,
+            thread_cpu_affinity: _,
             sync: _,
             tls,
             env_vars,
diff --git a/src/tools/miri/src/provenance_gc.rs b/src/tools/miri/src/provenance_gc.rs
index af980ca4819..8edd80744dd 100644
--- a/src/tools/miri/src/provenance_gc.rs
+++ b/src/tools/miri/src/provenance_gc.rs
@@ -30,6 +30,17 @@ impl<T: VisitProvenance> VisitProvenance for Option<T> {
     }
 }
 
+impl<A, B> VisitProvenance for (A, B)
+where
+    A: VisitProvenance,
+    B: VisitProvenance,
+{
+    fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
+        self.0.visit_provenance(visit);
+        self.1.visit_provenance(visit);
+    }
+}
+
 impl<T: VisitProvenance> VisitProvenance for std::cell::RefCell<T> {
     fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
         self.borrow().visit_provenance(visit)
diff --git a/src/tools/miri/src/shims/tls.rs b/src/tools/miri/src/shims/tls.rs
index c91386fa877..87074468789 100644
--- a/src/tools/miri/src/shims/tls.rs
+++ b/src/tools/miri/src/shims/tls.rs
@@ -36,9 +36,9 @@ pub struct TlsData<'tcx> {
     /// pthreads-style thread-local storage.
     keys: BTreeMap<TlsKey, TlsEntry<'tcx>>,
 
-    /// A single per thread destructor of the thread local storage (that's how
-    /// things work on macOS) with a data argument.
-    macos_thread_dtors: BTreeMap<ThreadId, (ty::Instance<'tcx>, Scalar)>,
+    /// On macOS, each thread holds a list of destructor functions with their
+    /// respective data arguments.
+    macos_thread_dtors: BTreeMap<ThreadId, Vec<(ty::Instance<'tcx>, Scalar)>>,
 }
 
 impl<'tcx> Default for TlsData<'tcx> {
@@ -119,26 +119,15 @@ impl<'tcx> TlsData<'tcx> {
         }
     }
 
-    /// Set the thread wide destructor of the thread local storage for the given
-    /// thread. This function is used to implement `_tlv_atexit` shim on MacOS.
-    ///
-    /// Thread wide dtors are available only on MacOS. There is one destructor
-    /// per thread as can be guessed from the following comment in the
-    /// [`_tlv_atexit`
-    /// implementation](https://github.com/opensource-apple/dyld/blob/195030646877261f0c8c7ad8b001f52d6a26f514/src/threadLocalVariables.c#L389):
-    ///
-    /// NOTE: this does not need locks because it only operates on current thread data
-    pub fn set_macos_thread_dtor(
+    /// Add a thread local storage destructor for the given thread. This function
+    /// is used to implement the `_tlv_atexit` shim on MacOS.
+    pub fn add_macos_thread_dtor(
         &mut self,
         thread: ThreadId,
         dtor: ty::Instance<'tcx>,
         data: Scalar,
     ) -> InterpResult<'tcx> {
-        if self.macos_thread_dtors.insert(thread, (dtor, data)).is_some() {
-            throw_unsup_format!(
-                "setting more than one thread local storage destructor for the same thread is not supported"
-            );
-        }
+        self.macos_thread_dtors.entry(thread).or_default().push((dtor, data));
         Ok(())
     }
 
@@ -202,6 +191,10 @@ impl<'tcx> TlsData<'tcx> {
         for TlsEntry { data, .. } in self.keys.values_mut() {
             data.remove(&thread_id);
         }
+
+        if let Some(dtors) = self.macos_thread_dtors.remove(&thread_id) {
+            assert!(dtors.is_empty(), "the destructors should have already been run");
+        }
     }
 }
 
@@ -212,7 +205,7 @@ impl VisitProvenance for TlsData<'_> {
         for scalar in keys.values().flat_map(|v| v.data.values()) {
             scalar.visit_provenance(visit);
         }
-        for (_, scalar) in macos_thread_dtors.values() {
+        for (_, scalar) in macos_thread_dtors.values().flatten() {
             scalar.visit_provenance(visit);
         }
     }
@@ -225,6 +218,7 @@ pub struct TlsDtorsState<'tcx>(TlsDtorsStatePriv<'tcx>);
 enum TlsDtorsStatePriv<'tcx> {
     #[default]
     Init,
+    MacOsDtors,
     PthreadDtors(RunningDtorState),
     /// For Windows Dtors, we store the list of functions that we still have to call.
     /// These are functions from the magic `.CRT$XLB` linker section.
@@ -243,11 +237,10 @@ impl<'tcx> TlsDtorsState<'tcx> {
                 Init => {
                     match this.tcx.sess.target.os.as_ref() {
                         "macos" => {
-                            // The macOS thread wide destructor runs "before any TLS slots get
-                            // freed", so do that first.
-                            this.schedule_macos_tls_dtor()?;
-                            // When that destructor is done, go on with the pthread dtors.
-                            break 'new_state PthreadDtors(Default::default());
+                            // macOS has a _tlv_atexit function that allows
+                            // registering destructors without associated keys.
+                            // These are run first.
+                            break 'new_state MacOsDtors;
                         }
                         _ if this.target_os_is_unix() => {
                             // All other Unixes directly jump to running the pthread dtors.
@@ -266,6 +259,14 @@ impl<'tcx> TlsDtorsState<'tcx> {
                         }
                     }
                 }
+                MacOsDtors => {
+                    match this.schedule_macos_tls_dtor()? {
+                        Poll::Pending => return Ok(Poll::Pending),
+                        // After all macOS destructors are run, the system switches
+                        // to destroying the pthread destructors.
+                        Poll::Ready(()) => break 'new_state PthreadDtors(Default::default()),
+                    }
+                }
                 PthreadDtors(state) => {
                     match this.schedule_next_pthread_tls_dtor(state)? {
                         Poll::Pending => return Ok(Poll::Pending), // just keep going
@@ -328,12 +329,15 @@ trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
         Ok(())
     }
 
-    /// Schedule the MacOS thread destructor of the thread local storage to be
-    /// executed.
-    fn schedule_macos_tls_dtor(&mut self) -> InterpResult<'tcx> {
+    /// Schedule the macOS thread local storage destructors to be executed.
+    fn schedule_macos_tls_dtor(&mut self) -> InterpResult<'tcx, Poll<()>> {
         let this = self.eval_context_mut();
         let thread_id = this.active_thread();
-        if let Some((instance, data)) = this.machine.tls.macos_thread_dtors.remove(&thread_id) {
+        // macOS keeps track of TLS destructors in a stack. If a destructor
+        // registers another destructor, it will be run next.
+        // See https://github.com/apple-oss-distributions/dyld/blob/d552c40cd1de105f0ec95008e0e0c0972de43456/dyld/DyldRuntimeState.cpp#L2277
+        let dtor = this.machine.tls.macos_thread_dtors.get_mut(&thread_id).and_then(Vec::pop);
+        if let Some((instance, data)) = dtor {
             trace!("Running macos dtor {:?} on {:?} at {:?}", instance, data, thread_id);
 
             this.call_function(
@@ -343,8 +347,11 @@ trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
                 None,
                 StackPopCleanup::Root { cleanup: true },
             )?;
+
+            return Ok(Poll::Pending);
         }
-        Ok(())
+
+        Ok(Poll::Ready(()))
     }
 
     /// Schedule a pthread TLS destructor. Returns `true` if found
diff --git a/src/tools/miri/src/shims/unix/fd.rs b/src/tools/miri/src/shims/unix/fd.rs
index 7f6a0978103..8fb046b5e64 100644
--- a/src/tools/miri/src/shims/unix/fd.rs
+++ b/src/tools/miri/src/shims/unix/fd.rs
@@ -419,7 +419,12 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
         match result {
             Ok(read_bytes) => {
                 // If reading to `bytes` did not fail, we write those bytes to the buffer.
-                this.write_bytes_ptr(buf, bytes)?;
+                // Crucially, if fewer than `bytes.len()` bytes were read, only write
+                // that much into the output buffer!
+                this.write_bytes_ptr(
+                    buf,
+                    bytes[..usize::try_from(read_bytes).unwrap()].iter().copied(),
+                )?;
                 Ok(read_bytes)
             }
             Err(e) => {
diff --git a/src/tools/miri/src/shims/unix/foreign_items.rs b/src/tools/miri/src/shims/unix/foreign_items.rs
index 2421f9244f3..3a18d622033 100644
--- a/src/tools/miri/src/shims/unix/foreign_items.rs
+++ b/src/tools/miri/src/shims/unix/foreign_items.rs
@@ -3,8 +3,10 @@ use std::str;
 
 use rustc_middle::ty::layout::LayoutOf;
 use rustc_span::Symbol;
+use rustc_target::abi::Size;
 use rustc_target::spec::abi::Abi;
 
+use crate::concurrency::cpu_affinity::CpuAffinityMask;
 use crate::shims::alloc::EvalContextExt as _;
 use crate::shims::unix::*;
 use crate::*;
@@ -571,6 +573,99 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
                 let result = this.nanosleep(req, rem)?;
                 this.write_scalar(Scalar::from_i32(result), dest)?;
             }
+            "sched_getaffinity" => {
+                // Currently this function does not exist on all Unixes, e.g. on macOS.
+                if !matches!(&*this.tcx.sess.target.os, "linux" | "freebsd" | "android") {
+                    throw_unsup_format!(
+                        "`sched_getaffinity` is not supported on {}",
+                        this.tcx.sess.target.os
+                    );
+                }
+
+                let [pid, cpusetsize, mask] =
+                    this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
+                let pid = this.read_scalar(pid)?.to_u32()?;
+                let cpusetsize = this.read_target_usize(cpusetsize)?;
+                let mask = this.read_pointer(mask)?;
+
+                // TODO: when https://github.com/rust-lang/miri/issues/3730 is fixed this should use its notion of tid/pid
+                let thread_id = match pid {
+                    0 => this.active_thread(),
+                    _ => throw_unsup_format!("`sched_getaffinity` is only supported with a pid of 0 (indicating the current thread)"),
+                };
+
+                // The mask is stored in chunks, and the size must be a whole number of chunks.
+                let chunk_size = CpuAffinityMask::chunk_size(this);
+
+                if this.ptr_is_null(mask)? {
+                    let einval = this.eval_libc("EFAULT");
+                    this.set_last_error(einval)?;
+                    this.write_scalar(Scalar::from_i32(-1), dest)?;
+                } else if cpusetsize == 0 || cpusetsize.checked_rem(chunk_size).unwrap() != 0 {
+                    // we only copy whole chunks of size_of::<c_ulong>()
+                    let einval = this.eval_libc("EINVAL");
+                    this.set_last_error(einval)?;
+                    this.write_scalar(Scalar::from_i32(-1), dest)?;
+                } else if let Some(cpuset) = this.machine.thread_cpu_affinity.get(&thread_id) {
+                    let cpuset = cpuset.clone();
+                    // we only copy whole chunks of size_of::<c_ulong>()
+                    let byte_count = Ord::min(cpuset.as_slice().len(), cpusetsize.try_into().unwrap());
+                    this.write_bytes_ptr(mask, cpuset.as_slice()[..byte_count].iter().copied())?;
+                    this.write_scalar(Scalar::from_i32(0), dest)?;
+                } else {
+                    // The thread whose ID is pid could not be found
+                    let einval = this.eval_libc("ESRCH");
+                    this.set_last_error(einval)?;
+                    this.write_scalar(Scalar::from_i32(-1), dest)?;
+                }
+            }
+            "sched_setaffinity" => {
+                // Currently this function does not exist on all Unixes, e.g. on macOS.
+                if !matches!(&*this.tcx.sess.target.os, "linux" | "freebsd" | "android") {
+                    throw_unsup_format!(
+                        "`sched_setaffinity` is not supported on {}",
+                        this.tcx.sess.target.os
+                    );
+                }
+
+                let [pid, cpusetsize, mask] =
+                    this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
+                let pid = this.read_scalar(pid)?.to_u32()?;
+                let cpusetsize = this.read_target_usize(cpusetsize)?;
+                let mask = this.read_pointer(mask)?;
+
+                // TODO: when https://github.com/rust-lang/miri/issues/3730 is fixed this should use its notion of tid/pid
+                let thread_id = match pid {
+                    0 => this.active_thread(),
+                    _ => throw_unsup_format!("`sched_setaffinity` is only supported with a pid of 0 (indicating the current thread)"),
+                };
+
+                if this.ptr_is_null(mask)? {
+                    let einval = this.eval_libc("EFAULT");
+                    this.set_last_error(einval)?;
+                    this.write_scalar(Scalar::from_i32(-1), dest)?;
+                } else {
+                    // NOTE: cpusetsize might be smaller than `CpuAffinityMask::CPU_MASK_BYTES`.
+                    // Any unspecified bytes are treated as zero here (none of the CPUs are configured).
+                    // This is not exactly documented, so we assume that this is the behavior in practice.
+                    let bits_slice = this.read_bytes_ptr_strip_provenance(mask, Size::from_bytes(cpusetsize))?;
+                    // This ignores the bytes beyond `CpuAffinityMask::CPU_MASK_BYTES`
+                    let bits_array: [u8; CpuAffinityMask::CPU_MASK_BYTES] =
+                        std::array::from_fn(|i| bits_slice.get(i).copied().unwrap_or(0));
+                    match CpuAffinityMask::from_array(this, this.machine.num_cpus, bits_array) {
+                        Some(cpuset) => {
+                            this.machine.thread_cpu_affinity.insert(thread_id, cpuset);
+                            this.write_scalar(Scalar::from_i32(0), dest)?;
+                        }
+                        None => {
+                            // The intersection between the mask and the available CPUs was empty.
+                            let einval = this.eval_libc("EINVAL");
+                            this.set_last_error(einval)?;
+                            this.write_scalar(Scalar::from_i32(-1), dest)?;
+                        }
+                    }
+                }
+            }
 
             // Miscellaneous
             "isatty" => {
diff --git a/src/tools/miri/src/shims/unix/linux/foreign_items.rs b/src/tools/miri/src/shims/unix/linux/foreign_items.rs
index e31d43d9190..95bee38cd78 100644
--- a/src/tools/miri/src/shims/unix/linux/foreign_items.rs
+++ b/src/tools/miri/src/shims/unix/linux/foreign_items.rs
@@ -178,19 +178,6 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
 
                 this.write_scalar(Scalar::from_i32(SIGRTMAX), dest)?;
             }
-            "sched_getaffinity" => {
-                // This shim isn't useful, aside from the fact that it makes `num_cpus`
-                // fall back to `sysconf` where it will successfully determine the number of CPUs.
-                let [pid, cpusetsize, mask] =
-                    this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
-                this.read_scalar(pid)?.to_i32()?;
-                this.read_target_usize(cpusetsize)?;
-                this.deref_pointer_as(mask, this.libc_ty_layout("cpu_set_t"))?;
-                // FIXME: we just return an error.
-                let einval = this.eval_libc("EINVAL");
-                this.set_last_error(einval)?;
-                this.write_scalar(Scalar::from_i32(-1), dest)?;
-            }
 
             // Incomplete shims that we "stub out" just to get pre-main initialization code to work.
             // These shims are enabled only when the caller is in the standard library.
diff --git a/src/tools/miri/src/shims/unix/macos/foreign_items.rs b/src/tools/miri/src/shims/unix/macos/foreign_items.rs
index 25002f0a611..47b887dec94 100644
--- a/src/tools/miri/src/shims/unix/macos/foreign_items.rs
+++ b/src/tools/miri/src/shims/unix/macos/foreign_items.rs
@@ -1,6 +1,7 @@
 use rustc_span::Symbol;
 use rustc_target::spec::abi::Abi;
 
+use super::sync::EvalContextExt as _;
 use crate::shims::unix::*;
 use crate::*;
 
@@ -132,7 +133,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
                 let dtor = this.get_ptr_fn(dtor)?.as_instance()?;
                 let data = this.read_scalar(data)?;
                 let active_thread = this.active_thread();
-                this.machine.tls.set_macos_thread_dtor(active_thread, dtor, data)?;
+                this.machine.tls.add_macos_thread_dtor(active_thread, dtor, data)?;
             }
 
             // Querying system information
@@ -174,6 +175,27 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
                 this.write_scalar(res, dest)?;
             }
 
+            "os_unfair_lock_lock" => {
+                let [lock_op] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
+                this.os_unfair_lock_lock(lock_op)?;
+            }
+            "os_unfair_lock_trylock" => {
+                let [lock_op] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
+                this.os_unfair_lock_trylock(lock_op, dest)?;
+            }
+            "os_unfair_lock_unlock" => {
+                let [lock_op] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
+                this.os_unfair_lock_unlock(lock_op)?;
+            }
+            "os_unfair_lock_assert_owner" => {
+                let [lock_op] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
+                this.os_unfair_lock_assert_owner(lock_op)?;
+            }
+            "os_unfair_lock_assert_not_owner" => {
+                let [lock_op] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
+                this.os_unfair_lock_assert_not_owner(lock_op)?;
+            }
+
             _ => return Ok(EmulateItemResult::NotSupported),
         };
 
diff --git a/src/tools/miri/src/shims/unix/macos/mod.rs b/src/tools/miri/src/shims/unix/macos/mod.rs
index 09c6507b24f..50fb2b9d328 100644
--- a/src/tools/miri/src/shims/unix/macos/mod.rs
+++ b/src/tools/miri/src/shims/unix/macos/mod.rs
@@ -1 +1,2 @@
 pub mod foreign_items;
+pub mod sync;
diff --git a/src/tools/miri/src/shims/unix/macos/sync.rs b/src/tools/miri/src/shims/unix/macos/sync.rs
new file mode 100644
index 00000000000..5e5fccb587b
--- /dev/null
+++ b/src/tools/miri/src/shims/unix/macos/sync.rs
@@ -0,0 +1,107 @@
+//! Contains macOS-specific synchronization functions.
+//!
+//! For `os_unfair_lock`, see the documentation
+//! <https://developer.apple.com/documentation/os/synchronization?language=objc>
+//! and in case of underspecification its implementation
+//! <https://github.com/apple-oss-distributions/libplatform/blob/a00a4cc36da2110578bcf3b8eeeeb93dcc7f4e11/src/os/lock.c#L645>.
+//!
+//! Note that we don't emulate every edge-case behaviour of the locks. Notably,
+//! we don't abort when locking a lock owned by a thread that has already exited
+//! and we do not detect copying of the lock, but macOS doesn't guarantee anything
+//! in that case either.
+
+use crate::*;
+
+impl<'tcx> EvalContextExtPriv<'tcx> for crate::MiriInterpCx<'tcx> {}
+trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {
+    fn os_unfair_lock_getid(&mut self, lock_op: &OpTy<'tcx>) -> InterpResult<'tcx, MutexId> {
+        let this = self.eval_context_mut();
+        // os_unfair_lock holds a 32-bit value, is initialized with zero and
+        // must be assumed to be opaque. Therefore, we can just store our
+        // internal mutex ID in the structure without anyone noticing.
+        this.mutex_get_or_create_id(lock_op, this.libc_ty_layout("os_unfair_lock"), 0)
+    }
+}
+
+impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
+pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
+    fn os_unfair_lock_lock(&mut self, lock_op: &OpTy<'tcx>) -> InterpResult<'tcx> {
+        let this = self.eval_context_mut();
+
+        let id = this.os_unfair_lock_getid(lock_op)?;
+        if this.mutex_is_locked(id) {
+            if this.mutex_get_owner(id) == this.active_thread() {
+                // Matching the current macOS implementation: abort on reentrant locking.
+                throw_machine_stop!(TerminationInfo::Abort(
+                    "attempted to lock an os_unfair_lock that is already locked by the current thread".to_owned()
+                ));
+            }
+
+            this.mutex_enqueue_and_block(id, None);
+        } else {
+            this.mutex_lock(id);
+        }
+
+        Ok(())
+    }
+
+    fn os_unfair_lock_trylock(
+        &mut self,
+        lock_op: &OpTy<'tcx>,
+        dest: &MPlaceTy<'tcx>,
+    ) -> InterpResult<'tcx> {
+        let this = self.eval_context_mut();
+
+        let id = this.os_unfair_lock_getid(lock_op)?;
+        if this.mutex_is_locked(id) {
+            // Contrary to the blocking lock function, this does not check for
+            // reentrancy.
+            this.write_scalar(Scalar::from_bool(false), dest)?;
+        } else {
+            this.mutex_lock(id);
+            this.write_scalar(Scalar::from_bool(true), dest)?;
+        }
+
+        Ok(())
+    }
+
+    fn os_unfair_lock_unlock(&mut self, lock_op: &OpTy<'tcx>) -> InterpResult<'tcx> {
+        let this = self.eval_context_mut();
+
+        let id = this.os_unfair_lock_getid(lock_op)?;
+        if this.mutex_unlock(id)?.is_none() {
+            // Matching the current macOS implementation: abort.
+            throw_machine_stop!(TerminationInfo::Abort(
+                "attempted to unlock an os_unfair_lock not owned by the current thread".to_owned()
+            ));
+        }
+
+        Ok(())
+    }
+
+    fn os_unfair_lock_assert_owner(&mut self, lock_op: &OpTy<'tcx>) -> InterpResult<'tcx> {
+        let this = self.eval_context_mut();
+
+        let id = this.os_unfair_lock_getid(lock_op)?;
+        if !this.mutex_is_locked(id) || this.mutex_get_owner(id) != this.active_thread() {
+            throw_machine_stop!(TerminationInfo::Abort(
+                "called os_unfair_lock_assert_owner on an os_unfair_lock not owned by the current thread".to_owned()
+            ));
+        }
+
+        Ok(())
+    }
+
+    fn os_unfair_lock_assert_not_owner(&mut self, lock_op: &OpTy<'tcx>) -> InterpResult<'tcx> {
+        let this = self.eval_context_mut();
+
+        let id = this.os_unfair_lock_getid(lock_op)?;
+        if this.mutex_is_locked(id) && this.mutex_get_owner(id) == this.active_thread() {
+            throw_machine_stop!(TerminationInfo::Abort(
+                "called os_unfair_lock_assert_not_owner on an os_unfair_lock owned by the current thread".to_owned()
+            ));
+        }
+
+        Ok(())
+    }
+}
diff --git a/src/tools/miri/src/shims/unix/sync.rs b/src/tools/miri/src/shims/unix/sync.rs
index be6732b1b67..e8653117ae9 100644
--- a/src/tools/miri/src/shims/unix/sync.rs
+++ b/src/tools/miri/src/shims/unix/sync.rs
@@ -473,7 +473,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
         let ret = if this.mutex_is_locked(id) {
             let owner_thread = this.mutex_get_owner(id);
             if owner_thread != this.active_thread() {
-                this.mutex_enqueue_and_block(id, Scalar::from_i32(0), dest.clone());
+                this.mutex_enqueue_and_block(id, Some((Scalar::from_i32(0), dest.clone())));
                 return Ok(());
             } else {
                 // Trying to acquire the same mutex again.
diff --git a/src/tools/miri/test-cargo-miri/run-test.py b/src/tools/miri/test-cargo-miri/run-test.py
index d855c333a75..5b77092979d 100755
--- a/src/tools/miri/test-cargo-miri/run-test.py
+++ b/src/tools/miri/test-cargo-miri/run-test.py
@@ -22,12 +22,17 @@ def fail(msg):
     print("\nTEST FAIL: {}".format(msg))
     sys.exit(1)
 
-def cargo_miri(cmd, quiet = True):
+def cargo_miri(cmd, quiet = True, targets = None):
     args = ["cargo", "miri", cmd] + CARGO_EXTRA_FLAGS
     if quiet:
         args += ["-q"]
-    if ARGS.target:
+
+    if targets is not None:
+        for target in targets:
+            args.extend(("--target", target))
+    elif ARGS.target is not None:
         args += ["--target", ARGS.target]
+
     return args
 
 def normalize_stdout(str):
@@ -186,10 +191,21 @@ def test_cargo_miri_test():
         default_ref, "test.stderr-empty.ref",
         env={'MIRIFLAGS': "-Zmiri-permissive-provenance"},
     )
+    if ARGS.multi_target:
+        test_cargo_miri_multi_target()
+
+
+def test_cargo_miri_multi_target():
+    test("`cargo miri test` (multiple targets)",
+        cargo_miri("test", targets = ["aarch64-unknown-linux-gnu", "s390x-unknown-linux-gnu"]),
+        "test.multiple_targets.stdout.ref", "test.stderr-empty.ref",
+        env={'MIRIFLAGS': "-Zmiri-permissive-provenance"},
+    )
 
 args_parser = argparse.ArgumentParser(description='`cargo miri` testing')
 args_parser.add_argument('--target', help='the target to test')
 args_parser.add_argument('--bless', help='bless the reference files', action='store_true')
+args_parser.add_argument('--multi-target', help='run tests related to multiple targets', action='store_true')
 ARGS = args_parser.parse_args()
 
 os.chdir(os.path.dirname(os.path.realpath(__file__)))
diff --git a/src/tools/miri/test-cargo-miri/test.multiple_targets.stdout.ref b/src/tools/miri/test-cargo-miri/test.multiple_targets.stdout.ref
new file mode 100644
index 00000000000..567c5db07d0
--- /dev/null
+++ b/src/tools/miri/test-cargo-miri/test.multiple_targets.stdout.ref
@@ -0,0 +1,22 @@
+
+running 2 tests
+..
+test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
+
+
+running 2 tests
+..
+test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
+
+imported main
+imported main
+
+running 6 tests
+...i..
+test result: ok. 5 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in $TIME
+
+
+running 6 tests
+...i..
+test result: ok. 5 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in $TIME
+
diff --git a/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_assert_not_owner.rs b/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_assert_not_owner.rs
new file mode 100644
index 00000000000..d6604f37139
--- /dev/null
+++ b/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_assert_not_owner.rs
@@ -0,0 +1,13 @@
+//@ only-target-darwin
+
+use std::cell::UnsafeCell;
+
+fn main() {
+    let lock = UnsafeCell::new(libc::OS_UNFAIR_LOCK_INIT);
+
+    unsafe {
+        libc::os_unfair_lock_lock(lock.get());
+        libc::os_unfair_lock_assert_not_owner(lock.get());
+        //~^ error: abnormal termination: called os_unfair_lock_assert_not_owner on an os_unfair_lock owned by the current thread
+    }
+}
diff --git a/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_assert_not_owner.stderr b/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_assert_not_owner.stderr
new file mode 100644
index 00000000000..7e890681c43
--- /dev/null
+++ b/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_assert_not_owner.stderr
@@ -0,0 +1,13 @@
+error: abnormal termination: called os_unfair_lock_assert_not_owner on an os_unfair_lock owned by the current thread
+  --> $DIR/apple_os_unfair_lock_assert_not_owner.rs:LL:CC
+   |
+LL |         libc::os_unfair_lock_assert_not_owner(lock.get());
+   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ called os_unfair_lock_assert_not_owner on an os_unfair_lock owned by the current thread
+   |
+   = note: BACKTRACE:
+   = note: inside `main` at $DIR/apple_os_unfair_lock_assert_not_owner.rs:LL:CC
+
+note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
+
+error: aborting due to 1 previous error
+
diff --git a/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_assert_owner.rs b/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_assert_owner.rs
new file mode 100644
index 00000000000..ddd8b572eaf
--- /dev/null
+++ b/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_assert_owner.rs
@@ -0,0 +1,12 @@
+//@ only-target-darwin
+
+use std::cell::UnsafeCell;
+
+fn main() {
+    let lock = UnsafeCell::new(libc::OS_UNFAIR_LOCK_INIT);
+
+    unsafe {
+        libc::os_unfair_lock_assert_owner(lock.get());
+        //~^ error: abnormal termination: called os_unfair_lock_assert_owner on an os_unfair_lock not owned by the current thread
+    }
+}
diff --git a/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_assert_owner.stderr b/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_assert_owner.stderr
new file mode 100644
index 00000000000..3724f7996fb
--- /dev/null
+++ b/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_assert_owner.stderr
@@ -0,0 +1,13 @@
+error: abnormal termination: called os_unfair_lock_assert_owner on an os_unfair_lock not owned by the current thread
+  --> $DIR/apple_os_unfair_lock_assert_owner.rs:LL:CC
+   |
+LL |         libc::os_unfair_lock_assert_owner(lock.get());
+   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ called os_unfair_lock_assert_owner on an os_unfair_lock not owned by the current thread
+   |
+   = note: BACKTRACE:
+   = note: inside `main` at $DIR/apple_os_unfair_lock_assert_owner.rs:LL:CC
+
+note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
+
+error: aborting due to 1 previous error
+
diff --git a/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_reentrant.rs b/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_reentrant.rs
new file mode 100644
index 00000000000..eb98adeba07
--- /dev/null
+++ b/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_reentrant.rs
@@ -0,0 +1,13 @@
+//@ only-target-darwin
+
+use std::cell::UnsafeCell;
+
+fn main() {
+    let lock = UnsafeCell::new(libc::OS_UNFAIR_LOCK_INIT);
+
+    unsafe {
+        libc::os_unfair_lock_lock(lock.get());
+        libc::os_unfair_lock_lock(lock.get());
+        //~^ error: abnormal termination: attempted to lock an os_unfair_lock that is already locked by the current thread
+    }
+}
diff --git a/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_reentrant.stderr b/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_reentrant.stderr
new file mode 100644
index 00000000000..644462a1b05
--- /dev/null
+++ b/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_reentrant.stderr
@@ -0,0 +1,13 @@
+error: abnormal termination: attempted to lock an os_unfair_lock that is already locked by the current thread
+  --> $DIR/apple_os_unfair_lock_reentrant.rs:LL:CC
+   |
+LL |         libc::os_unfair_lock_lock(lock.get());
+   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ attempted to lock an os_unfair_lock that is already locked by the current thread
+   |
+   = note: BACKTRACE:
+   = note: inside `main` at $DIR/apple_os_unfair_lock_reentrant.rs:LL:CC
+
+note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
+
+error: aborting due to 1 previous error
+
diff --git a/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_unowned.rs b/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_unowned.rs
new file mode 100644
index 00000000000..aed467552ab
--- /dev/null
+++ b/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_unowned.rs
@@ -0,0 +1,12 @@
+//@ only-target-darwin
+
+use std::cell::UnsafeCell;
+
+fn main() {
+    let lock = UnsafeCell::new(libc::OS_UNFAIR_LOCK_INIT);
+
+    unsafe {
+        libc::os_unfair_lock_unlock(lock.get());
+        //~^ error: abnormal termination: attempted to unlock an os_unfair_lock not owned by the current thread
+    }
+}
diff --git a/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_unowned.stderr b/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_unowned.stderr
new file mode 100644
index 00000000000..6a8d12fa807
--- /dev/null
+++ b/src/tools/miri/tests/fail-dep/concurrency/apple_os_unfair_lock_unowned.stderr
@@ -0,0 +1,13 @@
+error: abnormal termination: attempted to unlock an os_unfair_lock not owned by the current thread
+  --> $DIR/apple_os_unfair_lock_unowned.rs:LL:CC
+   |
+LL |         libc::os_unfair_lock_unlock(lock.get());
+   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ attempted to unlock an os_unfair_lock not owned by the current thread
+   |
+   = note: BACKTRACE:
+   = note: inside `main` at $DIR/apple_os_unfair_lock_unowned.rs:LL:CC
+
+note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
+
+error: aborting due to 1 previous error
+
diff --git a/src/tools/miri/tests/fail-dep/libc/affinity.rs b/src/tools/miri/tests/fail-dep/libc/affinity.rs
new file mode 100644
index 00000000000..c41d1d18018
--- /dev/null
+++ b/src/tools/miri/tests/fail-dep/libc/affinity.rs
@@ -0,0 +1,17 @@
+//@ignore-target-windows: only very limited libc on Windows
+//@ignore-target-apple: `sched_setaffinity` is not supported on macOS
+//@compile-flags: -Zmiri-disable-isolation -Zmiri-num-cpus=4
+
+fn main() {
+    use libc::{cpu_set_t, sched_setaffinity};
+
+    use std::mem::size_of;
+
+    // If pid is zero, then the calling thread is used.
+    const PID: i32 = 0;
+
+    let cpuset: cpu_set_t = unsafe { core::mem::MaybeUninit::zeroed().assume_init() };
+
+    let err = unsafe { sched_setaffinity(PID, size_of::<cpu_set_t>() + 1, &cpuset) }; //~ ERROR: memory access failed
+    assert_eq!(err, 0);
+}
diff --git a/src/tools/miri/tests/fail-dep/libc/affinity.stderr b/src/tools/miri/tests/fail-dep/libc/affinity.stderr
new file mode 100644
index 00000000000..c01f15800fa
--- /dev/null
+++ b/src/tools/miri/tests/fail-dep/libc/affinity.stderr
@@ -0,0 +1,20 @@
+error: Undefined Behavior: memory access failed: ALLOC has size 128, so pointer to 129 bytes starting at offset 0 is out-of-bounds
+  --> $DIR/affinity.rs:LL:CC
+   |
+LL |     let err = unsafe { sched_setaffinity(PID, size_of::<cpu_set_t>() + 1, &cpuset) };
+   |                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ memory access failed: ALLOC has size 128, so pointer to 129 bytes starting at offset 0 is out-of-bounds
+   |
+   = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
+   = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
+help: ALLOC was allocated here:
+  --> $DIR/affinity.rs:LL:CC
+   |
+LL |     let cpuset: cpu_set_t = unsafe { core::mem::MaybeUninit::zeroed().assume_init() };
+   |         ^^^^^^
+   = note: BACKTRACE (of the first span):
+   = note: inside `main` at $DIR/affinity.rs:LL:CC
+
+note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
+
+error: aborting due to 1 previous error
+
diff --git a/src/tools/miri/tests/fail-dep/libc/libc-read-and-uninit-premature-eof.rs b/src/tools/miri/tests/fail-dep/libc/libc-read-and-uninit-premature-eof.rs
new file mode 100644
index 00000000000..98ef454c88b
--- /dev/null
+++ b/src/tools/miri/tests/fail-dep/libc/libc-read-and-uninit-premature-eof.rs
@@ -0,0 +1,27 @@
+//! We test that if we requested to read 4 bytes, but actually read 3 bytes,
+//! then 3 bytes (not 4) will be initialized.
+//@ignore-target-windows: no file system support on Windows
+//@compile-flags: -Zmiri-disable-isolation
+
+use std::ffi::CString;
+use std::fs::remove_file;
+use std::mem::MaybeUninit;
+
+#[path = "../../utils/mod.rs"]
+mod utils;
+
+fn main() {
+    let path =
+        utils::prepare_with_content("fail-libc-read-and-uninit-premature-eof.txt", &[1u8, 2, 3]);
+    let cpath = CString::new(path.clone().into_os_string().into_encoded_bytes()).unwrap();
+    unsafe {
+        let fd = libc::open(cpath.as_ptr(), libc::O_RDONLY);
+        assert_ne!(fd, -1);
+        let mut buf: MaybeUninit<[u8; 4]> = std::mem::MaybeUninit::uninit();
+        // Read 4 bytes from a 3-byte file.
+        assert_eq!(libc::read(fd, buf.as_mut_ptr().cast::<std::ffi::c_void>(), 4), 3);
+        buf.assume_init(); //~ERROR: Undefined Behavior: constructing invalid value at .value[3]: encountered uninitialized memory, but expected an integer
+        assert_eq!(libc::close(fd), 0);
+    }
+    remove_file(&path).unwrap();
+}
diff --git a/src/tools/miri/tests/fail-dep/libc/libc-read-and-uninit-premature-eof.stderr b/src/tools/miri/tests/fail-dep/libc/libc-read-and-uninit-premature-eof.stderr
new file mode 100644
index 00000000000..e4c7aba07e3
--- /dev/null
+++ b/src/tools/miri/tests/fail-dep/libc/libc-read-and-uninit-premature-eof.stderr
@@ -0,0 +1,15 @@
+error: Undefined Behavior: constructing invalid value at .value[3]: encountered uninitialized memory, but expected an integer
+  --> $DIR/libc-read-and-uninit-premature-eof.rs:LL:CC
+   |
+LL | ...   buf.assume_init();
+   |       ^^^^^^^^^^^^^^^^^ constructing invalid value at .value[3]: encountered uninitialized memory, but expected an integer
+   |
+   = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
+   = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
+   = note: BACKTRACE:
+   = note: inside `main` at $DIR/libc-read-and-uninit-premature-eof.rs:LL:CC
+
+note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
+
+error: aborting due to 1 previous error
+
diff --git a/src/tools/miri/tests/fail/dangling_pointers/dangling_pointer_to_raw_pointer.rs b/src/tools/miri/tests/fail/dangling_pointers/dangling_pointer_to_raw_pointer.rs
new file mode 100644
index 00000000000..023bce1616b
--- /dev/null
+++ b/src/tools/miri/tests/fail/dangling_pointers/dangling_pointer_to_raw_pointer.rs
@@ -0,0 +1,20 @@
+#![feature(raw_ref_op)]
+#![feature(strict_provenance)]
+use std::ptr;
+
+fn direct_raw(x: *const (i32, i32)) -> *const i32 {
+    unsafe { &raw const (*x).0 }
+}
+
+// Ensure that if a raw pointer is created via an intermediate
+// reference, we catch that. (Just in case someone decides to
+// desugar this differenly or so.)
+fn via_ref(x: *const (i32, i32)) -> *const i32 {
+    unsafe { &(*x).0 as *const i32 } //~ERROR: dangling pointer
+}
+
+fn main() {
+    let ptr = ptr::without_provenance(0x10);
+    direct_raw(ptr); // this is fine
+    via_ref(ptr); // this is not
+}
diff --git a/src/tools/miri/tests/fail/dangling_pointers/dangling_pointer_to_raw_pointer.stderr b/src/tools/miri/tests/fail/dangling_pointers/dangling_pointer_to_raw_pointer.stderr
new file mode 100644
index 00000000000..37f2bb39557
--- /dev/null
+++ b/src/tools/miri/tests/fail/dangling_pointers/dangling_pointer_to_raw_pointer.stderr
@@ -0,0 +1,20 @@
+error: Undefined Behavior: out-of-bounds pointer use: 0x10[noalloc] is a dangling pointer (it has no provenance)
+  --> $DIR/dangling_pointer_to_raw_pointer.rs:LL:CC
+   |
+LL |     unsafe { &(*x).0 as *const i32 }
+   |              ^^^^^^^ out-of-bounds pointer use: 0x10[noalloc] is a dangling pointer (it has no provenance)
+   |
+   = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
+   = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
+   = note: BACKTRACE:
+   = note: inside `via_ref` at $DIR/dangling_pointer_to_raw_pointer.rs:LL:CC
+note: inside `main`
+  --> $DIR/dangling_pointer_to_raw_pointer.rs:LL:CC
+   |
+LL |     via_ref(ptr); // this is not
+   |     ^^^^^^^^^^^^
+
+note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
+
+error: aborting due to 1 previous error
+
diff --git a/src/tools/miri/tests/pass-dep/concurrency/apple-os-unfair-lock.rs b/src/tools/miri/tests/pass-dep/concurrency/apple-os-unfair-lock.rs
new file mode 100644
index 00000000000..c2b9c37bbfb
--- /dev/null
+++ b/src/tools/miri/tests/pass-dep/concurrency/apple-os-unfair-lock.rs
@@ -0,0 +1,25 @@
+//@ only-target-darwin
+
+use std::cell::UnsafeCell;
+
+fn main() {
+    let lock = UnsafeCell::new(libc::OS_UNFAIR_LOCK_INIT);
+
+    unsafe {
+        libc::os_unfair_lock_lock(lock.get());
+        libc::os_unfair_lock_assert_owner(lock.get());
+        assert!(!libc::os_unfair_lock_trylock(lock.get()));
+        libc::os_unfair_lock_unlock(lock.get());
+
+        libc::os_unfair_lock_assert_not_owner(lock.get());
+    }
+
+    // `os_unfair_lock`s can be moved and leaked.
+    // In the real implementation, even moving it while locked is possible
+    // (and "forks" the lock, i.e. old and new location have independent wait queues);
+    // Miri behavior differs here and anyway none of this is documented.
+    let lock = lock;
+    let locked = unsafe { libc::os_unfair_lock_trylock(lock.get()) };
+    assert!(locked);
+    let _lock = lock;
+}
diff --git a/src/tools/miri/tests/pass-dep/libc/libc-affinity.rs b/src/tools/miri/tests/pass-dep/libc/libc-affinity.rs
new file mode 100644
index 00000000000..0e482ab2601
--- /dev/null
+++ b/src/tools/miri/tests/pass-dep/libc/libc-affinity.rs
@@ -0,0 +1,218 @@
+//@ignore-target-windows: only very limited libc on Windows
+//@ignore-target-apple: `sched_{g, s}etaffinity` are not supported on macOS
+//@compile-flags: -Zmiri-disable-isolation -Zmiri-num-cpus=4
+#![feature(io_error_more)]
+#![feature(pointer_is_aligned_to)]
+#![feature(strict_provenance)]
+
+use libc::{cpu_set_t, sched_getaffinity, sched_setaffinity};
+use std::mem::{size_of, size_of_val};
+
+// If pid is zero, then the calling thread is used.
+const PID: i32 = 0;
+
+fn null_pointers() {
+    let err = unsafe { sched_getaffinity(PID, size_of::<cpu_set_t>(), std::ptr::null_mut()) };
+    assert_eq!(err, -1);
+
+    let err = unsafe { sched_setaffinity(PID, size_of::<cpu_set_t>(), std::ptr::null()) };
+    assert_eq!(err, -1);
+}
+
+fn configure_no_cpus() {
+    let cpu_count = std::thread::available_parallelism().unwrap().get();
+
+    let mut cpuset: cpu_set_t = unsafe { core::mem::MaybeUninit::zeroed().assume_init() };
+
+    // configuring no CPUs will fail
+    let err = unsafe { sched_setaffinity(PID, size_of::<cpu_set_t>(), &cpuset) };
+    assert_eq!(err, -1);
+    assert_eq!(std::io::Error::last_os_error().kind(), std::io::ErrorKind::InvalidInput);
+
+    // configuring no (physically available) CPUs will fail
+    unsafe { libc::CPU_SET(cpu_count, &mut cpuset) };
+    let err = unsafe { sched_setaffinity(PID, size_of::<cpu_set_t>(), &cpuset) };
+    assert_eq!(err, -1);
+    assert_eq!(std::io::Error::last_os_error().kind(), std::io::ErrorKind::InvalidInput);
+}
+
+fn configure_unavailable_cpu() {
+    let cpu_count = std::thread::available_parallelism().unwrap().get();
+
+    // Safety: valid value for this type
+    let mut cpuset: cpu_set_t = unsafe { core::mem::MaybeUninit::zeroed().assume_init() };
+
+    let err = unsafe { sched_getaffinity(PID, size_of::<cpu_set_t>(), &mut cpuset) };
+    assert_eq!(err, 0);
+
+    // by default, only available CPUs are configured
+    for i in 0..cpu_count {
+        assert!(unsafe { libc::CPU_ISSET(i, &cpuset) });
+    }
+    assert!(unsafe { !libc::CPU_ISSET(cpu_count, &cpuset) });
+
+    // configure CPU that we don't have
+    unsafe { libc::CPU_SET(cpu_count, &mut cpuset) };
+
+    let err = unsafe { sched_setaffinity(PID, size_of::<cpu_set_t>(), &cpuset) };
+    assert_eq!(err, 0);
+
+    let err = unsafe { sched_getaffinity(PID, size_of::<cpu_set_t>(), &mut cpuset) };
+    assert_eq!(err, 0);
+
+    // the CPU is not set because it is not available
+    assert!(!unsafe { libc::CPU_ISSET(cpu_count, &cpuset) });
+}
+
+fn large_set() {
+    // rust's libc does not currently implement dynamic cpu set allocation
+    // and related functions like `CPU_ZERO_S`. So we have to be creative
+
+    // i.e. this has 2048 bits, twice the standard number
+    let mut cpuset = [u64::MAX; 32];
+
+    let err = unsafe { sched_setaffinity(PID, size_of_val(&cpuset), cpuset.as_ptr().cast()) };
+    assert_eq!(err, 0);
+
+    let err = unsafe { sched_getaffinity(PID, size_of_val(&cpuset), cpuset.as_mut_ptr().cast()) };
+    assert_eq!(err, 0);
+}
+
+fn get_small_cpu_mask() {
+    let mut cpuset: cpu_set_t = unsafe { core::mem::MaybeUninit::zeroed().assume_init() };
+
+    // should be 4 on 32-bit systems and 8 otherwise for systems that implement sched_getaffinity
+    let step = size_of::<std::ffi::c_ulong>();
+
+    for i in (0..=2).map(|x| x * step) {
+        if i == 0 {
+            // 0 always fails
+            let err = unsafe { sched_getaffinity(PID, i, &mut cpuset) };
+            assert_eq!(err, -1, "fail for {}", i);
+            assert_eq!(std::io::Error::last_os_error().kind(), std::io::ErrorKind::InvalidInput);
+        } else {
+            // other whole multiples of the size of c_ulong works
+            let err = unsafe { sched_getaffinity(PID, i, &mut cpuset) };
+            assert_eq!(err, 0, "fail for {i}");
+        }
+
+        // anything else returns an error
+        for j in 1..step {
+            let err = unsafe { sched_getaffinity(PID, i + j, &mut cpuset) };
+            assert_eq!(err, -1, "success for {}", i + j);
+            assert_eq!(std::io::Error::last_os_error().kind(), std::io::ErrorKind::InvalidInput);
+        }
+    }
+}
+
+fn set_small_cpu_mask() {
+    let mut cpuset: cpu_set_t = unsafe { core::mem::MaybeUninit::zeroed().assume_init() };
+
+    let err = unsafe { sched_getaffinity(PID, size_of::<cpu_set_t>(), &mut cpuset) };
+    assert_eq!(err, 0);
+
+    // setting a mask of size 0 is invalid
+    let err = unsafe { sched_setaffinity(PID, 0, &cpuset) };
+    assert_eq!(err, -1);
+    assert_eq!(std::io::Error::last_os_error().kind(), std::io::ErrorKind::InvalidInput);
+
+    // on LE systems, any other number of bytes (at least up to `size_of<cpu_set_t>()`) will work.
+    // on BE systems the CPUs 0..8 are stored in the right-most byte of the first chunk. If that
+    // byte is not included, no valid CPUs are configured. We skip those cases.
+    let cpu_zero_included_length =
+        if cfg!(target_endian = "little") { 1 } else { core::mem::size_of::<std::ffi::c_ulong>() };
+
+    for i in cpu_zero_included_length..24 {
+        let err = unsafe { sched_setaffinity(PID, i, &cpuset) };
+        assert_eq!(err, 0, "fail for {i}");
+    }
+}
+
+fn set_custom_cpu_mask() {
+    let cpu_count = std::thread::available_parallelism().unwrap().get();
+
+    assert!(cpu_count > 1, "this test cannot do anything interesting with just one thread");
+
+    let mut cpuset: cpu_set_t = unsafe { core::mem::MaybeUninit::zeroed().assume_init() };
+
+    // at the start, thread 1 should be set
+    let err = unsafe { sched_getaffinity(PID, size_of::<cpu_set_t>(), &mut cpuset) };
+    assert_eq!(err, 0);
+    assert!(unsafe { libc::CPU_ISSET(1, &cpuset) });
+
+    // make a valid mask
+    unsafe { libc::CPU_ZERO(&mut cpuset) };
+    unsafe { libc::CPU_SET(0, &mut cpuset) };
+
+    // giving a smaller mask is fine
+    let err = unsafe { sched_setaffinity(PID, 8, &cpuset) };
+    assert_eq!(err, 0);
+
+    // and actually disables other threads
+    let err = unsafe { sched_getaffinity(PID, size_of::<cpu_set_t>(), &mut cpuset) };
+    assert_eq!(err, 0);
+    assert!(unsafe { !libc::CPU_ISSET(1, &cpuset) });
+
+    // it is important that we reset the cpu mask now for future tests
+    for i in 0..cpu_count {
+        unsafe { libc::CPU_SET(i, &mut cpuset) };
+    }
+
+    let err = unsafe { sched_setaffinity(PID, size_of::<cpu_set_t>(), &cpuset) };
+    assert_eq!(err, 0);
+}
+
+fn parent_child() {
+    let cpu_count = std::thread::available_parallelism().unwrap().get();
+
+    assert!(cpu_count > 1, "this test cannot do anything interesting with just one thread");
+
+    // configure the parent thread to only run only on CPU 0
+    let mut parent_cpuset: cpu_set_t = unsafe { core::mem::MaybeUninit::zeroed().assume_init() };
+    unsafe { libc::CPU_SET(0, &mut parent_cpuset) };
+
+    let err = unsafe { sched_setaffinity(PID, size_of::<cpu_set_t>(), &parent_cpuset) };
+    assert_eq!(err, 0);
+
+    std::thread::scope(|spawner| {
+        spawner.spawn(|| {
+            let mut cpuset: cpu_set_t = unsafe { core::mem::MaybeUninit::zeroed().assume_init() };
+
+            let err = unsafe { sched_getaffinity(PID, size_of::<cpu_set_t>(), &mut cpuset) };
+            assert_eq!(err, 0);
+
+            // the child inherits its parent's set
+            assert!(unsafe { libc::CPU_ISSET(0, &cpuset) });
+            assert!(unsafe { !libc::CPU_ISSET(1, &cpuset) });
+
+            // configure cpu 1 for the child
+            unsafe { libc::CPU_SET(1, &mut cpuset) };
+        });
+    });
+
+    let err = unsafe { sched_getaffinity(PID, size_of::<cpu_set_t>(), &mut parent_cpuset) };
+    assert_eq!(err, 0);
+
+    // the parent's set should be unaffected
+    assert!(unsafe { !libc::CPU_ISSET(1, &parent_cpuset) });
+
+    // it is important that we reset the cpu mask now for future tests
+    let mut cpuset = parent_cpuset;
+    for i in 0..cpu_count {
+        unsafe { libc::CPU_SET(i, &mut cpuset) };
+    }
+
+    let err = unsafe { sched_setaffinity(PID, size_of::<cpu_set_t>(), &cpuset) };
+    assert_eq!(err, 0);
+}
+
+fn main() {
+    null_pointers();
+    configure_no_cpus();
+    configure_unavailable_cpu();
+    large_set();
+    get_small_cpu_mask();
+    set_small_cpu_mask();
+    set_custom_cpu_mask();
+    parent_child();
+}
diff --git a/src/tools/miri/tests/pass-dep/libc/libc-fs.rs b/src/tools/miri/tests/pass-dep/libc/libc-fs.rs
index da685e5c6b7..eddea92353e 100644
--- a/src/tools/miri/tests/pass-dep/libc/libc-fs.rs
+++ b/src/tools/miri/tests/pass-dep/libc/libc-fs.rs
@@ -36,6 +36,7 @@ fn main() {
     #[cfg(target_os = "linux")]
     test_sync_file_range();
     test_isatty();
+    test_read_and_uninit();
 }
 
 fn test_file_open_unix_allow_two_args() {
@@ -388,3 +389,37 @@ fn test_isatty() {
         remove_file(&path).unwrap();
     }
 }
+
+fn test_read_and_uninit() {
+    use std::mem::MaybeUninit;
+    {
+        // We test that libc::read initializes its buffer.
+        let path = utils::prepare_with_content("pass-libc-read-and-uninit.txt", &[1u8, 2, 3]);
+        let cpath = CString::new(path.clone().into_os_string().into_encoded_bytes()).unwrap();
+        unsafe {
+            let fd = libc::open(cpath.as_ptr(), libc::O_RDONLY);
+            assert_ne!(fd, -1);
+            let mut buf: MaybeUninit<[u8; 2]> = std::mem::MaybeUninit::uninit();
+            assert_eq!(libc::read(fd, buf.as_mut_ptr().cast::<std::ffi::c_void>(), 2), 2);
+            let buf = buf.assume_init();
+            assert_eq!(buf, [1, 2]);
+            assert_eq!(libc::close(fd), 0);
+        }
+        remove_file(&path).unwrap();
+    }
+    {
+        // We test that if we requested to read 4 bytes, but actually read 3 bytes, then
+        // 3 bytes (not 4) will be overwritten, and remaining byte will be left as-is.
+        let path = utils::prepare_with_content("pass-libc-read-and-uninit-2.txt", &[1u8, 2, 3]);
+        let cpath = CString::new(path.clone().into_os_string().into_encoded_bytes()).unwrap();
+        unsafe {
+            let fd = libc::open(cpath.as_ptr(), libc::O_RDONLY);
+            assert_ne!(fd, -1);
+            let mut buf = [42u8; 5];
+            assert_eq!(libc::read(fd, buf.as_mut_ptr().cast::<std::ffi::c_void>(), 4), 3);
+            assert_eq!(buf, [1, 2, 3, 42, 42]);
+            assert_eq!(libc::close(fd), 0);
+        }
+        remove_file(&path).unwrap();
+    }
+}
diff --git a/src/tools/miri/tests/pass/float.rs b/src/tools/miri/tests/pass/float.rs
index 5464627fa14..6ab18a5345e 100644
--- a/src/tools/miri/tests/pass/float.rs
+++ b/src/tools/miri/tests/pass/float.rs
@@ -4,8 +4,11 @@
 #![feature(f128)]
 #![feature(f16)]
 #![allow(arithmetic_overflow)]
+#![allow(internal_features)]
 
-use std::fmt::Debug;
+use std::any::type_name;
+use std::cmp::min;
+use std::fmt::{Debug, Display, LowerHex};
 use std::hint::black_box;
 use std::{f32, f64};
 
@@ -29,15 +32,41 @@ fn main() {
     test_algebraic();
 }
 
-// Helper function to avoid promotion so that this tests "run-time" casts, not CTFE.
-// Doesn't make a big difference when running this in Miri, but it means we can compare this
-// with the LLVM backend by running `rustc -Zmir-opt-level=0 -Zsaturating-float-casts`.
-#[track_caller]
-#[inline(never)]
-fn assert_eq<T: PartialEq + Debug>(x: T, y: T) {
-    assert_eq!(x, y);
+trait Float: Copy + PartialEq + Debug {
+    /// The unsigned integer with the same bit width as this float
+    type Int: Copy + PartialEq + LowerHex + Debug;
+    const BITS: u32 = size_of::<Self>() as u32 * 8;
+    const EXPONENT_BITS: u32 = Self::BITS - Self::SIGNIFICAND_BITS - 1;
+    const SIGNIFICAND_BITS: u32;
+
+    /// The saturated (all ones) value of the exponent (infinity representation)
+    const EXPONENT_SAT: u32 = (1 << Self::EXPONENT_BITS) - 1;
+
+    /// The exponent bias value (max representable positive exponent)
+    const EXPONENT_BIAS: u32 = Self::EXPONENT_SAT >> 1;
+
+    fn to_bits(self) -> Self::Int;
+}
+
+macro_rules! impl_float {
+    ($ty:ty, $ity:ty) => {
+        impl Float for $ty {
+            type Int = $ity;
+            // Just get this from std's value, which includes the implicit digit
+            const SIGNIFICAND_BITS: u32 = <$ty>::MANTISSA_DIGITS - 1;
+
+            fn to_bits(self) -> Self::Int {
+                self.to_bits()
+            }
+        }
+    };
 }
 
+impl_float!(f16, u16);
+impl_float!(f32, u32);
+impl_float!(f64, u64);
+impl_float!(f128, u128);
+
 trait FloatToInt<Int>: Copy {
     fn cast(self) -> Int;
     unsafe fn cast_unchecked(self) -> Int;
@@ -58,19 +87,61 @@ macro_rules! float_to_int {
     };
 }
 
+float_to_int!(f16 => i8, u8, i16, u16, i32, u32, i64, u64, i128, u128);
 float_to_int!(f32 => i8, u8, i16, u16, i32, u32, i64, u64, i128, u128);
 float_to_int!(f64 => i8, u8, i16, u16, i32, u32, i64, u64, i128, u128);
+float_to_int!(f128 => i8, u8, i16, u16, i32, u32, i64, u64, i128, u128);
 
 /// Test this cast both via `as` and via `approx_unchecked` (i.e., it must not saturate).
 #[track_caller]
 #[inline(never)]
-fn test_both_cast<F, I>(x: F, y: I)
+fn test_both_cast<F, I>(x: F, y: I, msg: impl Display)
 where
     F: FloatToInt<I>,
     I: PartialEq + Debug,
 {
-    assert_eq!(x.cast(), y);
-    assert_eq!(unsafe { x.cast_unchecked() }, y);
+    let f_tname = type_name::<F>();
+    let i_tname = type_name::<I>();
+    assert_eq!(x.cast(), y, "{f_tname} -> {i_tname}: {msg}");
+    assert_eq!(unsafe { x.cast_unchecked() }, y, "{f_tname} -> {i_tname}: {msg}",);
+}
+
+/// Helper function to avoid promotion so that this tests "run-time" casts, not CTFE.
+/// Doesn't make a big difference when running this in Miri, but it means we can compare this
+/// with the LLVM backend by running `rustc -Zmir-opt-level=0 -Zsaturating-float-casts`.
+#[track_caller]
+#[inline(never)]
+fn assert_eq<T: PartialEq + Debug>(x: T, y: T) {
+    assert_eq!(x, y);
+}
+
+/// The same as `assert_eq` except prints a specific message on failure
+#[track_caller]
+#[inline(never)]
+fn assert_eq_msg<T: PartialEq + Debug>(x: T, y: T, msg: impl Display) {
+    assert_eq!(x, y, "{msg}");
+}
+
+/// Check that floats have bitwise equality
+fn assert_biteq<F: Float>(a: F, b: F, msg: impl Display) {
+    let ab = a.to_bits();
+    let bb = b.to_bits();
+    let tname = type_name::<F>();
+    let width = (2 + F::BITS / 4) as usize;
+    assert_eq_msg::<F::Int>(
+        ab,
+        bb,
+        format_args!("({ab:#0width$x} != {bb:#0width$x}) {tname}: {msg}"),
+    );
+}
+
+/// Check that two floats have equality
+fn assert_feq<F: Float>(a: F, b: F, msg: impl Display) {
+    let ab = a.to_bits();
+    let bb = b.to_bits();
+    let tname = type_name::<F>();
+    let width = (2 + F::BITS / 4) as usize;
+    assert_eq_msg::<F>(a, b, format_args!("({ab:#0width$x} != {bb:#0width$x}) {tname}: {msg}"));
 }
 
 fn basic() {
@@ -148,155 +219,368 @@ fn basic() {
     assert_eq!(34.2f64.abs(), 34.2f64);
 }
 
-/// Many of these test values are taken from
+/// Test casts from floats to ints and back
+macro_rules! test_ftoi_itof {
+    (
+        f: $fty:ty,
+        i: $ity:ty,
+        // Int min and max as float literals
+        imin_f: $imin_f:literal,
+        imax_f: $imax_f:literal $(,)?
+    ) => {{
+        /// By default we test float to int `as` casting as well as to_int_unchecked
+        fn assert_ftoi(f: $fty, i: $ity, msg: &str) {
+            #[allow(unused_comparisons)]
+            if <$ity>::MIN >= 0 && f < 0.0 {
+                // If `ity` is signed and `f` is negative, it is unrepresentable so skip
+                // unchecked casts.
+                assert_ftoi_unrep(f, i, msg);
+            } else {
+                test_both_cast::<$fty, $ity>(f, i, msg);
+            }
+        }
+
+        /// Unrepresentable values only get tested with `as` casting, not unchecked
+        fn assert_ftoi_unrep(f: $fty, i: $ity, msg: &str) {
+            assert_eq_msg::<$ity>(
+                f as $ity,
+                i,
+                format_args!("{} -> {}: {msg}", stringify!($fty), stringify!($ity)),
+            );
+        }
+
+        /// Int to float checks
+        fn assert_itof(i: $ity, f: $fty, msg: &str) {
+            assert_eq_msg::<$fty>(
+                i as $fty,
+                f,
+                format_args!("{} -> {}: {msg}", stringify!($ity), stringify!($fty)),
+            );
+        }
+
+        /// Check both float to int and int to float
+        fn assert_bidir(f: $fty, i: $ity, msg: &str) {
+            assert_ftoi(f, i, msg);
+            assert_itof(i, f, msg);
+        }
+
+        /// Check both float to int and int to float for unrepresentable numbers
+        fn assert_bidir_unrep(f: $fty, i: $ity, msg: &str) {
+            assert_ftoi_unrep(f, i, msg);
+            assert_itof(i, f, msg);
+        }
+
+        let fbits = <$fty>::BITS;
+        let fsig_bits = <$fty>::SIGNIFICAND_BITS;
+        let ibits = <$ity>::BITS;
+        let imax: $ity = <$ity>::MAX;
+        let imin: $ity = <$ity>::MIN;
+        let izero: $ity = 0;
+        #[allow(unused_comparisons)]
+        let isigned = <$ity>::MIN < 0;
+
+        #[allow(overflowing_literals)]
+        let imin_f: $fty = $imin_f;
+        #[allow(overflowing_literals)]
+        let imax_f: $fty = $imax_f;
+
+        // If an integer can fit entirely in the mantissa (counting the hidden bit), every value
+        // can be represented exactly.
+        let all_ints_exact_rep = ibits <= fsig_bits + 1;
+
+        // We can represent the full range of the integer (but possibly not every value) without
+        // saturating to infinity if `1 << (I::BITS - 1)` (single one in the MSB position) is
+        // within the float's dynamic range.
+        let int_range_rep = ibits - 1 < <$fty>::EXPONENT_BIAS;
+
+        // Skip unchecked cast when int min/max would be unrepresentable
+        let assert_ftoi_big = if all_ints_exact_rep { assert_ftoi } else { assert_ftoi_unrep };
+        let assert_bidir_big = if all_ints_exact_rep { assert_bidir } else { assert_bidir_unrep };
+
+        // Near zero representations
+        assert_bidir(0.0, 0, "zero");
+        assert_ftoi(-0.0, 0, "negative zero");
+        assert_ftoi(1.0, 1, "one");
+        assert_ftoi(-1.0, izero.saturating_sub(1), "negative one");
+        assert_ftoi(1.0 - <$fty>::EPSILON, 0, "1.0 - ε");
+        assert_ftoi(1.0 + <$fty>::EPSILON, 1, "1.0 + ε");
+        assert_ftoi(-1.0 + <$fty>::EPSILON, 0, "-1.0 + ε");
+        assert_ftoi(-1.0 - <$fty>::EPSILON, izero.saturating_sub(1), "-1.0 - ε");
+        assert_ftoi(<$fty>::from_bits(0x1), 0, "min subnormal");
+        assert_ftoi(<$fty>::from_bits(0x1 | 1 << (fbits - 1)), 0, "min neg subnormal");
+
+        // Spot checks. Use `saturating_sub` to create negative integers so that unsigned
+        // integers stay at zero.
+        assert_ftoi(0.9, 0, "0.9");
+        assert_ftoi(-0.9, 0, "-0.9");
+        assert_ftoi(1.1, 1, "1.1");
+        assert_ftoi(-1.1, izero.saturating_sub(1), "-1.1");
+        assert_ftoi(1.9, 1, "1.9");
+        assert_ftoi(-1.9, izero.saturating_sub(1), "-1.9");
+        assert_ftoi(5.0, 5, "5.0");
+        assert_ftoi(-5.0, izero.saturating_sub(5), "-5.0");
+        assert_ftoi(5.9, 5, "5.0");
+        assert_ftoi(-5.9, izero.saturating_sub(5), "-5.0");
+
+        // Exercise the middle of the integer's bit range. A power of two fits as long as the
+        // exponent can fit its log2, so cap at the maximum representable power of two (which
+        // is the exponent's bias).
+        let half_i_max: $ity = 1 << min(ibits / 2, <$fty>::EXPONENT_BIAS);
+        let half_i_min = izero.saturating_sub(half_i_max);
+        assert_bidir(half_i_max as $fty, half_i_max, "half int max");
+        assert_bidir(half_i_min as $fty, half_i_min, "half int min");
+
+        // Integer limits
+        assert_bidir_big(imax_f, imax, "i max");
+        assert_bidir_big(imin_f, imin, "i min");
+
+        // We need a small perturbation to test against that does not round up to the next
+        // integer. `f16` needs a smaller perturbation since it only has resolution for ~1 decimal
+        // place near 10^3.
+        let perturb = if fbits < 32 { 0.9 } else { 0.99 };
+        assert_ftoi_big(imax_f + perturb, <$ity>::MAX, "slightly above i max");
+        assert_ftoi_big(imin_f - perturb, <$ity>::MIN, "slightly below i min");
+
+        // Tests for when we can represent the integer's magnitude
+        if int_range_rep {
+            // If the float can represent values larger than the integer, float extremes
+            // will saturate.
+            assert_ftoi_unrep(<$fty>::MAX, imax, "f max");
+            assert_ftoi_unrep(<$fty>::MIN, imin, "f min");
+
+            // Max representable power of 10
+            let pow10_max = (10 as $ity).pow(imax.ilog10());
+
+            // If the power of 10 should be representable (fits in a mantissa), check it
+            if ibits - pow10_max.leading_zeros() - pow10_max.trailing_zeros() <= fsig_bits + 1 {
+                assert_bidir(pow10_max as $fty, pow10_max, "pow10 max");
+            }
+        }
+
+        // Test rounding the largest and smallest integers, but skip this when
+        // all integers have an exact representation (it's less interesting then and the arithmetic gets more complicated).
+        if int_range_rep && !all_ints_exact_rep {
+            // The maximum representable integer is a saturated mantissa (including the implicit
+            // bit), shifted into the int's leftmost position.
+            //
+            // Positive signed integers never use their top bit, so shift by one bit fewer.
+            let sat_mantissa: $ity = (1 << (fsig_bits + 1)) - 1;
+            let adj = if isigned { 1 } else { 0 };
+            let max_rep = sat_mantissa << (sat_mantissa.leading_zeros() - adj);
+
+            // This value should roundtrip exactly
+            assert_bidir(max_rep as $fty, max_rep, "max representable int");
+
+            // The cutoff for where to round to `imax` is halfway between the maximum exactly
+            // representable integer and `imax`. This should round down (to `max_rep`),
+            // i.e., `max_rep as $fty == max_non_sat as $fty`.
+            let max_non_sat = max_rep + ((imax - max_rep) / 2);
+            assert_bidir(max_non_sat as $fty, max_rep, "max non saturating int");
+
+            // So the next value up should round up to the maximum value of the integer
+            assert_bidir_unrep((max_non_sat + 1) as $fty, imax, "min infinite int");
+
+            if isigned {
+                // Floats can always represent the minimum signed number if they can fit the
+                // exponent, because it is just a `1` in the MSB. So, no negative int -> float
+                // conversion will round to negative infinity (if the exponent fits).
+                //
+                // Since `imin` is thus the minimum representable value, we test rounding near
+                // the next value. This happens to be the opposite of the maximum representable
+                // value, and it should roundtrip exactly.
+                let next_min_rep = max_rep.wrapping_neg();
+                assert_bidir(next_min_rep as $fty, next_min_rep, "min representable above imin");
+
+                // Following a similar pattern as for positive numbers, halfway between this value
+                // and `imin` should round back to `next_min_rep`.
+                let min_non_sat = imin - ((imin - next_min_rep) / 2) + 1;
+                assert_bidir(
+                    min_non_sat as $fty,
+                    next_min_rep,
+                    "min int that does not round to imin",
+                );
+
+                // And then anything else saturates to the minimum value.
+                assert_bidir_unrep(
+                    (min_non_sat - 1) as $fty,
+                    imin,
+                    "max negative int that rounds to imin",
+                );
+            }
+        }
+
+        // Check potentially saturating int ranges. (`imax_f` here will be `$fty::INFINITY` if
+        // it cannot be represented as a finite value.)
+        assert_itof(imax, imax_f, "imax");
+        assert_itof(imin, imin_f, "imin");
+
+        // Float limits
+        assert_ftoi_unrep(<$fty>::INFINITY, imax, "f inf");
+        assert_ftoi_unrep(<$fty>::NEG_INFINITY, imin, "f neg inf");
+        assert_ftoi_unrep(<$fty>::NAN, 0, "f nan");
+        assert_ftoi_unrep(-<$fty>::NAN, 0, "f neg nan");
+    }};
+}
+
+/// Test casts from one float to another
+macro_rules! test_ftof {
+    (
+        f1: $f1:ty,
+        f2: $f2:ty $(,)?
+    ) => {{
+        type F2Int = <$f2 as Float>::Int;
+
+        let f1zero: $f1 = 0.0;
+        let f2zero: $f2 = 0.0;
+        let f1five: $f1 = 5.0;
+        let f2five: $f2 = 5.0;
+
+        assert_biteq((f1zero as $f2), f2zero, "0.0");
+        assert_biteq(((-f1zero) as $f2), (-f2zero), "-0.0");
+        assert_biteq((f1five as $f2), f2five, "5.0");
+        assert_biteq(((-f1five) as $f2), (-f2five), "-5.0");
+
+        assert_feq(<$f1>::INFINITY as $f2, <$f2>::INFINITY, "max -> inf");
+        assert_feq(<$f1>::NEG_INFINITY as $f2, <$f2>::NEG_INFINITY, "max -> inf");
+        assert!((<$f1>::NAN as $f2).is_nan(), "{} -> {} nan", stringify!($f1), stringify!($f2));
+
+        let min_sub_casted = <$f1>::from_bits(0x1) as $f2;
+        let min_neg_sub_casted = <$f1>::from_bits(0x1 | 1 << (<$f1>::BITS - 1)) as $f2;
+
+        if <$f1>::BITS > <$f2>::BITS {
+            assert_feq(<$f1>::MAX as $f2, <$f2>::INFINITY, "max -> inf");
+            assert_feq(<$f1>::MIN as $f2, <$f2>::NEG_INFINITY, "max -> inf");
+            assert_biteq(min_sub_casted, f2zero, "min subnormal -> 0.0");
+            assert_biteq(min_neg_sub_casted, -f2zero, "min neg subnormal -> -0.0");
+        } else {
+            // When increasing precision, the minimum subnormal will just roll to the next
+            // exponent. This exponent will be the current exponent (with bias), plus
+            // `sig_bits - 1` to account for the implicit change in exponent (since the
+            // mantissa starts with 0).
+            let sub_casted = <$f2>::from_bits(
+                ((<$f2>::EXPONENT_BIAS - (<$f1>::EXPONENT_BIAS + <$f1>::SIGNIFICAND_BITS - 1))
+                    as F2Int)
+                    << <$f2>::SIGNIFICAND_BITS,
+            );
+            assert_biteq(min_sub_casted, sub_casted, "min subnormal");
+            assert_biteq(min_neg_sub_casted, -sub_casted, "min neg subnormal");
+        }
+    }};
+}
+
+/// Many of these test patterns were adapted from the values in
 /// https://github.com/WebAssembly/testsuite/blob/master/conversions.wast.
 fn casts() {
-    // f32 -> i8
-    test_both_cast::<f32, i8>(127.99, 127);
-    test_both_cast::<f32, i8>(-128.99, -128);
-
-    // f32 -> i32
-    test_both_cast::<f32, i32>(0.0, 0);
-    test_both_cast::<f32, i32>(-0.0, 0);
-    test_both_cast::<f32, i32>(/*0x1p-149*/ f32::from_bits(0x00000001), 0);
-    test_both_cast::<f32, i32>(/*-0x1p-149*/ f32::from_bits(0x80000001), 0);
-    test_both_cast::<f32, i32>(/*0x1.19999ap+0*/ f32::from_bits(0x3f8ccccd), 1);
-    test_both_cast::<f32, i32>(/*-0x1.19999ap+0*/ f32::from_bits(0xbf8ccccd), -1);
-    test_both_cast::<f32, i32>(1.9, 1);
-    test_both_cast::<f32, i32>(-1.9, -1);
-    test_both_cast::<f32, i32>(5.0, 5);
-    test_both_cast::<f32, i32>(-5.0, -5);
-    test_both_cast::<f32, i32>(2147483520.0, 2147483520);
-    test_both_cast::<f32, i32>(-2147483648.0, -2147483648);
-    // unrepresentable casts
-    assert_eq::<i32>(2147483648.0f32 as i32, i32::MAX);
-    assert_eq::<i32>(-2147483904.0f32 as i32, i32::MIN);
-    assert_eq::<i32>(f32::MAX as i32, i32::MAX);
-    assert_eq::<i32>(f32::MIN as i32, i32::MIN);
-    assert_eq::<i32>(f32::INFINITY as i32, i32::MAX);
-    assert_eq::<i32>(f32::NEG_INFINITY as i32, i32::MIN);
-    assert_eq::<i32>(f32::NAN as i32, 0);
-    assert_eq::<i32>((-f32::NAN) as i32, 0);
-
-    // f32 -> u32
-    test_both_cast::<f32, u32>(0.0, 0);
-    test_both_cast::<f32, u32>(-0.0, 0);
-    test_both_cast::<f32, u32>(-0.9999999, 0);
-    test_both_cast::<f32, u32>(/*0x1p-149*/ f32::from_bits(0x1), 0);
-    test_both_cast::<f32, u32>(/*-0x1p-149*/ f32::from_bits(0x80000001), 0);
-    test_both_cast::<f32, u32>(/*0x1.19999ap+0*/ f32::from_bits(0x3f8ccccd), 1);
-    test_both_cast::<f32, u32>(1.9, 1);
-    test_both_cast::<f32, u32>(5.0, 5);
-    test_both_cast::<f32, u32>(2147483648.0, 0x8000_0000);
-    test_both_cast::<f32, u32>(4294967040.0, 0u32.wrapping_sub(256));
-    test_both_cast::<f32, u32>(/*-0x1.ccccccp-1*/ f32::from_bits(0xbf666666), 0);
-    test_both_cast::<f32, u32>(/*-0x1.fffffep-1*/ f32::from_bits(0xbf7fffff), 0);
-    test_both_cast::<f32, u32>((u32::MAX - 128) as f32, u32::MAX - 255); // rounding loss
-    // unrepresentable casts
-    assert_eq::<u32>((u32::MAX - 127) as f32 as u32, u32::MAX); // rounds up and then becomes unrepresentable
-    assert_eq::<u32>(4294967296.0f32 as u32, u32::MAX);
-    assert_eq::<u32>(-5.0f32 as u32, 0);
-    assert_eq::<u32>(f32::MAX as u32, u32::MAX);
-    assert_eq::<u32>(f32::MIN as u32, 0);
-    assert_eq::<u32>(f32::INFINITY as u32, u32::MAX);
-    assert_eq::<u32>(f32::NEG_INFINITY as u32, 0);
-    assert_eq::<u32>(f32::NAN as u32, 0);
-    assert_eq::<u32>((-f32::NAN) as u32, 0);
-
-    // f32 -> i64
-    test_both_cast::<f32, i64>(4294967296.0, 4294967296);
-    test_both_cast::<f32, i64>(-4294967296.0, -4294967296);
-    test_both_cast::<f32, i64>(9223371487098961920.0, 9223371487098961920);
-    test_both_cast::<f32, i64>(-9223372036854775808.0, -9223372036854775808);
-
-    // f64 -> i8
-    test_both_cast::<f64, i8>(127.99, 127);
-    test_both_cast::<f64, i8>(-128.99, -128);
-
-    // f64 -> i32
-    test_both_cast::<f64, i32>(0.0, 0);
-    test_both_cast::<f64, i32>(-0.0, 0);
-    test_both_cast::<f64, i32>(/*0x1.199999999999ap+0*/ f64::from_bits(0x3ff199999999999a), 1);
-    test_both_cast::<f64, i32>(
-        /*-0x1.199999999999ap+0*/ f64::from_bits(0xbff199999999999a),
-        -1,
-    );
-    test_both_cast::<f64, i32>(1.9, 1);
-    test_both_cast::<f64, i32>(-1.9, -1);
-    test_both_cast::<f64, i32>(1e8, 100_000_000);
-    test_both_cast::<f64, i32>(2147483647.0, 2147483647);
-    test_both_cast::<f64, i32>(-2147483648.0, -2147483648);
-    // unrepresentable casts
-    assert_eq::<i32>(2147483648.0f64 as i32, i32::MAX);
-    assert_eq::<i32>(-2147483649.0f64 as i32, i32::MIN);
-
-    // f64 -> i64
-    test_both_cast::<f64, i64>(0.0, 0);
-    test_both_cast::<f64, i64>(-0.0, 0);
-    test_both_cast::<f64, i64>(/*0x0.0000000000001p-1022*/ f64::from_bits(0x1), 0);
-    test_both_cast::<f64, i64>(
-        /*-0x0.0000000000001p-1022*/ f64::from_bits(0x8000000000000001),
-        0,
-    );
-    test_both_cast::<f64, i64>(/*0x1.199999999999ap+0*/ f64::from_bits(0x3ff199999999999a), 1);
-    test_both_cast::<f64, i64>(
-        /*-0x1.199999999999ap+0*/ f64::from_bits(0xbff199999999999a),
-        -1,
-    );
-    test_both_cast::<f64, i64>(5.0, 5);
-    test_both_cast::<f64, i64>(5.9, 5);
-    test_both_cast::<f64, i64>(-5.0, -5);
-    test_both_cast::<f64, i64>(-5.9, -5);
-    test_both_cast::<f64, i64>(4294967296.0, 4294967296);
-    test_both_cast::<f64, i64>(-4294967296.0, -4294967296);
-    test_both_cast::<f64, i64>(9223372036854774784.0, 9223372036854774784);
-    test_both_cast::<f64, i64>(-9223372036854775808.0, -9223372036854775808);
-    // unrepresentable casts
-    assert_eq::<i64>(9223372036854775808.0f64 as i64, i64::MAX);
-    assert_eq::<i64>(-9223372036854777856.0f64 as i64, i64::MIN);
-    assert_eq::<i64>(f64::MAX as i64, i64::MAX);
-    assert_eq::<i64>(f64::MIN as i64, i64::MIN);
-    assert_eq::<i64>(f64::INFINITY as i64, i64::MAX);
-    assert_eq::<i64>(f64::NEG_INFINITY as i64, i64::MIN);
-    assert_eq::<i64>(f64::NAN as i64, 0);
-    assert_eq::<i64>((-f64::NAN) as i64, 0);
-
-    // f64 -> u64
-    test_both_cast::<f64, u64>(0.0, 0);
-    test_both_cast::<f64, u64>(-0.0, 0);
-    test_both_cast::<f64, u64>(-0.99999999999, 0);
-    test_both_cast::<f64, u64>(5.0, 5);
-    test_both_cast::<f64, u64>(1e16, 10000000000000000);
-    test_both_cast::<f64, u64>((u64::MAX - 1024) as f64, u64::MAX - 2047); // rounding loss
-    test_both_cast::<f64, u64>(9223372036854775808.0, 9223372036854775808);
-    // unrepresentable casts
-    assert_eq::<u64>(-5.0f64 as u64, 0);
-    assert_eq::<u64>((u64::MAX - 1023) as f64 as u64, u64::MAX); // rounds up and then becomes unrepresentable
-    assert_eq::<u64>(18446744073709551616.0f64 as u64, u64::MAX);
-    assert_eq::<u64>(f64::MAX as u64, u64::MAX);
-    assert_eq::<u64>(f64::MIN as u64, 0);
-    assert_eq::<u64>(f64::INFINITY as u64, u64::MAX);
-    assert_eq::<u64>(f64::NEG_INFINITY as u64, 0);
-    assert_eq::<u64>(f64::NAN as u64, 0);
-    assert_eq::<u64>((-f64::NAN) as u64, 0);
-
-    // f64 -> i128
-    assert_eq::<i128>(f64::MAX as i128, i128::MAX);
-    assert_eq::<i128>(f64::MIN as i128, i128::MIN);
-
-    // f64 -> u128
-    assert_eq::<u128>(f64::MAX as u128, u128::MAX);
-    assert_eq::<u128>(f64::MIN as u128, 0);
+    /* int <-> float generic tests */
+
+    test_ftoi_itof! { f: f16, i: i8, imin_f: -128.0, imax_f: 127.0 };
+    test_ftoi_itof! { f: f16, i: u8, imin_f: 0.0, imax_f: 255.0 };
+    test_ftoi_itof! { f: f16, i: i16, imin_f: -32_768.0, imax_f: 32_767.0 };
+    test_ftoi_itof! { f: f16, i: u16, imin_f: 0.0, imax_f: 65_535.0 };
+    test_ftoi_itof! { f: f16, i: i32, imin_f: -2_147_483_648.0, imax_f: 2_147_483_647.0 };
+    test_ftoi_itof! { f: f16, i: u32, imin_f: 0.0, imax_f: 4_294_967_295.0 };
+    test_ftoi_itof! {
+        f: f16,
+        i: i64,
+        imin_f: -9_223_372_036_854_775_808.0,
+        imax_f: 9_223_372_036_854_775_807.0
+    };
+    test_ftoi_itof! { f: f16, i: u64, imin_f: 0.0, imax_f: 18_446_744_073_709_551_615.0 };
+    test_ftoi_itof! {
+        f: f16,
+        i: i128,
+        imin_f: -170_141_183_460_469_231_731_687_303_715_884_105_728.0,
+        imax_f: 170_141_183_460_469_231_731_687_303_715_884_105_727.0,
+    };
+    test_ftoi_itof! {
+        f: f16,
+        i: u128,
+        imin_f: 0.0,
+        imax_f: 340_282_366_920_938_463_463_374_607_431_768_211_455.0
+    };
+
+    test_ftoi_itof! { f: f32, i: i8, imin_f: -128.0, imax_f: 127.0 };
+    test_ftoi_itof! { f: f32, i: u8, imin_f: 0.0, imax_f: 255.0 };
+    test_ftoi_itof! { f: f32, i: i16, imin_f: -32_768.0, imax_f: 32_767.0 };
+    test_ftoi_itof! { f: f32, i: u16, imin_f: 0.0, imax_f: 65_535.0 };
+    test_ftoi_itof! { f: f32, i: i32, imin_f: -2_147_483_648.0, imax_f: 2_147_483_647.0 };
+    test_ftoi_itof! { f: f32, i: u32, imin_f: 0.0, imax_f: 4_294_967_295.0 };
+    test_ftoi_itof! {
+        f: f32,
+        i: i64,
+        imin_f: -9_223_372_036_854_775_808.0,
+        imax_f: 9_223_372_036_854_775_807.0
+    };
+    test_ftoi_itof! { f: f32, i: u64, imin_f: 0.0, imax_f: 18_446_744_073_709_551_615.0 };
+    test_ftoi_itof! {
+        f: f32,
+        i: i128,
+        imin_f: -170_141_183_460_469_231_731_687_303_715_884_105_728.0,
+        imax_f: 170_141_183_460_469_231_731_687_303_715_884_105_727.0,
+    };
+    test_ftoi_itof! {
+        f: f32,
+        i: u128,
+        imin_f: 0.0,
+        imax_f: 340_282_366_920_938_463_463_374_607_431_768_211_455.0
+    };
+
+    test_ftoi_itof! { f: f64, i: i8, imin_f: -128.0, imax_f: 127.0 };
+    test_ftoi_itof! { f: f64, i: u8, imin_f: 0.0, imax_f: 255.0 };
+    test_ftoi_itof! { f: f64, i: i16, imin_f: -32_768.0, imax_f: 32_767.0 };
+    test_ftoi_itof! { f: f64, i: u16, imin_f: 0.0, imax_f: 65_535.0 };
+    test_ftoi_itof! { f: f64, i: i32, imin_f: -2_147_483_648.0, imax_f: 2_147_483_647.0 };
+    test_ftoi_itof! { f: f64, i: u32, imin_f: 0.0, imax_f: 4_294_967_295.0 };
+    test_ftoi_itof! {
+        f: f64,
+        i: i64,
+        imin_f: -9_223_372_036_854_775_808.0,
+        imax_f: 9_223_372_036_854_775_807.0
+    };
+    test_ftoi_itof! { f: f64, i: u64, imin_f: 0.0, imax_f: 18_446_744_073_709_551_615.0 };
+    test_ftoi_itof! {
+        f: f64,
+        i: i128,
+        imin_f: -170_141_183_460_469_231_731_687_303_715_884_105_728.0,
+        imax_f: 170_141_183_460_469_231_731_687_303_715_884_105_727.0,
+    };
+    test_ftoi_itof! {
+        f: f64,
+        i: u128,
+        imin_f: 0.0,
+        imax_f: 340_282_366_920_938_463_463_374_607_431_768_211_455.0
+    };
+
+    test_ftoi_itof! { f: f128, i: i8, imin_f: -128.0, imax_f: 127.0 };
+    test_ftoi_itof! { f: f128, i: u8, imin_f: 0.0, imax_f: 255.0 };
+    test_ftoi_itof! { f: f128, i: i16, imin_f: -32_768.0, imax_f: 32_767.0 };
+    test_ftoi_itof! { f: f128, i: u16, imin_f: 0.0, imax_f: 65_535.0 };
+    test_ftoi_itof! { f: f128, i: i32, imin_f: -2_147_483_648.0, imax_f: 2_147_483_647.0 };
+    test_ftoi_itof! { f: f128, i: u32, imin_f: 0.0, imax_f: 4_294_967_295.0 };
+    test_ftoi_itof! {
+        f: f128,
+        i: i64,
+        imin_f: -9_223_372_036_854_775_808.0,
+        imax_f: 9_223_372_036_854_775_807.0
+    };
+    test_ftoi_itof! { f: f128, i: u64, imin_f: 0.0, imax_f: 18_446_744_073_709_551_615.0 };
+    test_ftoi_itof! {
+        f: f128,
+        i: i128,
+        imin_f: -170_141_183_460_469_231_731_687_303_715_884_105_728.0,
+        imax_f: 170_141_183_460_469_231_731_687_303_715_884_105_727.0,
+    };
+    test_ftoi_itof! {
+        f: f128,
+        i: u128,
+        imin_f: 0.0,
+        imax_f: 340_282_366_920_938_463_463_374_607_431_768_211_455.0
+    };
+
+    /* int <-> float spot checks */
 
     // int -> f32
-    assert_eq::<f32>(127i8 as f32, 127.0);
-    assert_eq::<f32>(2147483647i32 as f32, 2147483648.0);
-    assert_eq::<f32>((-2147483648i32) as f32, -2147483648.0);
     assert_eq::<f32>(1234567890i32 as f32, /*0x1.26580cp+30*/ f32::from_bits(0x4e932c06));
-    assert_eq::<f32>(16777217i32 as f32, 16777216.0);
-    assert_eq::<f32>((-16777217i32) as f32, -16777216.0);
-    assert_eq::<f32>(16777219i32 as f32, 16777220.0);
-    assert_eq::<f32>((-16777219i32) as f32, -16777220.0);
     assert_eq::<f32>(
         0x7fffff4000000001i64 as f32,
         /*0x1.fffffep+62*/ f32::from_bits(0x5effffff),
@@ -313,36 +597,33 @@ fn casts() {
         0xffdfffffdfffffffu64 as i64 as f32,
         /*-0x1.000002p+53*/ f32::from_bits(0xda000001),
     );
-    assert_eq::<f32>(i128::MIN as f32, -170141183460469231731687303715884105728.0f32);
-    assert_eq::<f32>(u128::MAX as f32, f32::INFINITY); // saturation
 
     // int -> f64
-    assert_eq::<f64>(127i8 as f64, 127.0);
-    assert_eq::<f64>(i16::MIN as f64, -32768.0f64);
-    assert_eq::<f64>(2147483647i32 as f64, 2147483647.0);
-    assert_eq::<f64>(-2147483648i32 as f64, -2147483648.0);
     assert_eq::<f64>(987654321i32 as f64, 987654321.0);
-    assert_eq::<f64>(9223372036854775807i64 as f64, 9223372036854775807.0);
-    assert_eq::<f64>(-9223372036854775808i64 as f64, -9223372036854775808.0);
     assert_eq::<f64>(4669201609102990i64 as f64, 4669201609102990.0); // Feigenbaum (?)
     assert_eq::<f64>(9007199254740993i64 as f64, 9007199254740992.0);
     assert_eq::<f64>(-9007199254740993i64 as f64, -9007199254740992.0);
     assert_eq::<f64>(9007199254740995i64 as f64, 9007199254740996.0);
     assert_eq::<f64>(-9007199254740995i64 as f64, -9007199254740996.0);
-    assert_eq::<f64>(u128::MAX as f64, 340282366920938463463374607431768211455.0f64); // even that fits...
+
+    /* float -> float generic tests */
+
+    test_ftof! { f1: f16, f2: f32 };
+    test_ftof! { f1: f16, f2: f64 };
+    test_ftof! { f1: f16, f2: f128 };
+    test_ftof! { f1: f32, f2: f16 };
+    test_ftof! { f1: f32, f2: f64 };
+    test_ftof! { f1: f32, f2: f128 };
+    test_ftof! { f1: f64, f2: f16 };
+    test_ftof! { f1: f64, f2: f32 };
+    test_ftof! { f1: f64, f2: f128 };
+    test_ftof! { f1: f128, f2: f16 };
+    test_ftof! { f1: f128, f2: f32 };
+    test_ftof! { f1: f128, f2: f64 };
+
+    /* float -> float spot checks */
 
     // f32 -> f64
-    assert_eq::<u64>((0.0f32 as f64).to_bits(), 0.0f64.to_bits());
-    assert_eq::<u64>(((-0.0f32) as f64).to_bits(), (-0.0f64).to_bits());
-    assert_eq::<f64>(5.0f32 as f64, 5.0f64);
-    assert_eq::<f64>(
-        /*0x1p-149*/ f32::from_bits(0x1) as f64,
-        /*0x1p-149*/ f64::from_bits(0x36a0000000000000),
-    );
-    assert_eq::<f64>(
-        /*-0x1p-149*/ f32::from_bits(0x80000001) as f64,
-        /*-0x1p-149*/ f64::from_bits(0xb6a0000000000000),
-    );
     assert_eq::<f64>(
         /*0x1.fffffep+127*/ f32::from_bits(0x7f7fffff) as f64,
         /*0x1.fffffep+127*/ f64::from_bits(0x47efffffe0000000),
@@ -359,15 +640,8 @@ fn casts() {
         /*0x1.8f867ep+125*/ f32::from_bits(0x7e47c33f) as f64,
         6.6382536710104395e+37,
     );
-    assert_eq::<f64>(f32::INFINITY as f64, f64::INFINITY);
-    assert_eq::<f64>(f32::NEG_INFINITY as f64, f64::NEG_INFINITY);
 
     // f64 -> f32
-    assert_eq::<u32>((0.0f64 as f32).to_bits(), 0.0f32.to_bits());
-    assert_eq::<u32>(((-0.0f64) as f32).to_bits(), (-0.0f32).to_bits());
-    assert_eq::<f32>(5.0f64 as f32, 5.0f32);
-    assert_eq::<f32>(/*0x0.0000000000001p-1022*/ f64::from_bits(0x1) as f32, 0.0);
-    assert_eq::<f32>(/*-0x0.0000000000001p-1022*/ (-f64::from_bits(0x1)) as f32, -0.0);
     assert_eq::<f32>(
         /*0x1.fffffe0000000p-127*/ f64::from_bits(0x380fffffe0000000) as f32,
         /*0x1p-149*/ f32::from_bits(0x800000),
@@ -376,10 +650,6 @@ fn casts() {
         /*0x1.4eae4f7024c7p+108*/ f64::from_bits(0x46b4eae4f7024c70) as f32,
         /*0x1.4eae5p+108*/ f32::from_bits(0x75a75728),
     );
-    assert_eq::<f32>(f64::MAX as f32, f32::INFINITY);
-    assert_eq::<f32>(f64::MIN as f32, f32::NEG_INFINITY);
-    assert_eq::<f32>(f64::INFINITY as f32, f32::INFINITY);
-    assert_eq::<f32>(f64::NEG_INFINITY as f32, f32::NEG_INFINITY);
 }
 
 fn ops() {
diff --git a/src/tools/miri/tests/pass/tls/macos_tlv_atexit.rs b/src/tools/miri/tests/pass/tls/macos_tlv_atexit.rs
new file mode 100644
index 00000000000..845f50c1eba
--- /dev/null
+++ b/src/tools/miri/tests/pass/tls/macos_tlv_atexit.rs
@@ -0,0 +1,43 @@
+//@only-target-darwin
+
+use std::thread;
+
+extern "C" {
+    fn _tlv_atexit(dtor: unsafe extern "C" fn(*mut u8), arg: *mut u8);
+}
+
+fn register<F>(f: F)
+where
+    F: FnOnce() + 'static,
+{
+    // This will receive the pointer passed into `_tlv_atexit`, which is the
+    // original `f` but boxed up.
+    unsafe extern "C" fn run<F>(ptr: *mut u8)
+    where
+        F: FnOnce() + 'static,
+    {
+        let f = unsafe { Box::from_raw(ptr as *mut F) };
+        f()
+    }
+
+    unsafe {
+        _tlv_atexit(run::<F>, Box::into_raw(Box::new(f)) as *mut u8);
+    }
+}
+
+fn main() {
+    thread::spawn(|| {
+        register(|| println!("dtor 2"));
+        register(|| println!("dtor 1"));
+        println!("exiting thread");
+    })
+    .join()
+    .unwrap();
+
+    println!("exiting main");
+    register(|| println!("dtor 5"));
+    register(|| {
+        println!("registering dtor in dtor 3");
+        register(|| println!("dtor 4"));
+    });
+}
diff --git a/src/tools/miri/tests/pass/tls/macos_tlv_atexit.stdout b/src/tools/miri/tests/pass/tls/macos_tlv_atexit.stdout
new file mode 100644
index 00000000000..89d6ca25935
--- /dev/null
+++ b/src/tools/miri/tests/pass/tls/macos_tlv_atexit.stdout
@@ -0,0 +1,7 @@
+exiting thread
+dtor 1
+dtor 2
+exiting main
+registering dtor in dtor 3
+dtor 4
+dtor 5