about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/tools/miri/.github/workflows/ci.yml28
-rw-r--r--src/tools/miri/CONTRIBUTING.md18
-rwxr-xr-xsrc/tools/miri/ci/ci.sh102
-rw-r--r--src/tools/miri/miri-script/src/commands.rs2
-rw-r--r--src/tools/miri/rust-version2
-rw-r--r--src/tools/miri/src/borrow_tracker/tree_borrows/diagnostics.rs4
-rw-r--r--src/tools/miri/src/borrow_tracker/tree_borrows/tree.rs2
-rw-r--r--src/tools/miri/src/borrow_tracker/tree_borrows/tree/tests.rs6
-rw-r--r--src/tools/miri/src/machine.rs26
-rw-r--r--src/tools/miri/src/shims/unix/fd.rs431
-rw-r--r--src/tools/miri/src/shims/unix/foreign_items.rs60
-rw-r--r--src/tools/miri/src/shims/unix/freebsd/foreign_items.rs3
-rw-r--r--src/tools/miri/src/shims/unix/fs.rs607
-rw-r--r--src/tools/miri/src/shims/unix/linux/epoll.rs (renamed from src/tools/miri/src/shims/unix/linux/fd.rs)156
-rw-r--r--src/tools/miri/src/shims/unix/linux/eventfd.rs125
-rw-r--r--src/tools/miri/src/shims/unix/linux/fd/epoll.rs47
-rw-r--r--src/tools/miri/src/shims/unix/linux/fd/event.rs72
-rw-r--r--src/tools/miri/src/shims/unix/linux/fd/socketpair.rs28
-rw-r--r--src/tools/miri/src/shims/unix/linux/foreign_items.rs77
-rw-r--r--src/tools/miri/src/shims/unix/linux/mod.rs3
-rw-r--r--src/tools/miri/src/shims/unix/macos/foreign_items.rs3
-rw-r--r--src/tools/miri/src/shims/unix/mod.rs12
-rw-r--r--src/tools/miri/src/shims/unix/socket.rs65
-rwxr-xr-xsrc/tools/miri/test-cargo-miri/run-test.py4
-rw-r--r--src/tools/miri/tests/extern-so/libcode.version9
-rw-r--r--src/tools/miri/tests/extern-so/libtest.map12
-rw-r--r--src/tools/miri/tests/fail/extern_static.stderr4
-rw-r--r--src/tools/miri/tests/fail/extern_static_in_const.stderr4
-rw-r--r--src/tools/miri/tests/fail/extern_static_wrong_size.rs2
-rw-r--r--src/tools/miri/tests/fail/extern_static_wrong_size.stderr4
-rw-r--r--src/tools/miri/tests/fail/issue-miri-3288-ice-symbolic-alignment-extern-static.stderr4
-rw-r--r--src/tools/miri/tests/fail/tree_borrows/spurious_read.rs2
-rw-r--r--src/tools/miri/tests/ui.rs20
33 files changed, 990 insertions, 954 deletions
diff --git a/src/tools/miri/.github/workflows/ci.yml b/src/tools/miri/.github/workflows/ci.yml
index 2c5868d5b37..b0dab9f509d 100644
--- a/src/tools/miri/.github/workflows/ci.yml
+++ b/src/tools/miri/.github/workflows/ci.yml
@@ -24,8 +24,8 @@ jobs:
         include:
           - os: ubuntu-latest
             host_target: x86_64-unknown-linux-gnu
-          - os: macos-latest
-            host_target: x86_64-apple-darwin
+          - os: macos-14
+            host_target: aarch64-apple-darwin
           - os: windows-latest
             host_target: i686-pc-windows-msvc
     runs-on: ${{ matrix.os }}
@@ -49,15 +49,16 @@ jobs:
         with:
           path: |
             # Taken from <https://doc.rust-lang.org/nightly/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci>.
-            ~/.cargo/bin
+            # Cache package/registry information
             ~/.cargo/registry/index
             ~/.cargo/registry/cache
             ~/.cargo/git/db
-            # contains package information of crates installed via `cargo install`.
+            # Cache installed binaries
+            ~/.cargo/bin
             ~/.cargo/.crates.toml
             ~/.cargo/.crates2.json
-          key: ${{ runner.os }}-cargo-reset20230315-${{ hashFiles('**/Cargo.lock') }}
-          restore-keys: ${{ runner.os }}-cargo-reset20230315
+          key: cargo-${{ runner.os }}-reset20240331-${{ hashFiles('**/Cargo.lock') }}
+          restore-keys: cargo-${{ runner.os }}-reset20240331
 
       - name: Install rustup-toolchain-install-master
         if: ${{ steps.cache.outputs.cache-hit != 'true' }}
@@ -98,15 +99,16 @@ jobs:
         with:
           path: |
             # Taken from <https://doc.rust-lang.org/nightly/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci>.
-            ~/.cargo/bin
+            # Cache package/registry information
             ~/.cargo/registry/index
             ~/.cargo/registry/cache
             ~/.cargo/git/db
-            # contains package information of crates installed via `cargo install`.
+            # Cache installed binaries
+            ~/.cargo/bin
             ~/.cargo/.crates.toml
             ~/.cargo/.crates2.json
-          key: ${{ runner.os }}-cargo-reset20230315-${{ hashFiles('**/Cargo.lock') }}
-          restore-keys: ${{ runner.os }}-cargo-reset20230315
+          key: cargo-${{ runner.os }}-reset20240331-${{ hashFiles('**/Cargo.lock') }}
+          restore-keys: cargo-${{ runner.os }}-reset20240331
 
       - name: Install rustup-toolchain-install-master
         if: ${{ steps.cache.outputs.cache-hit != 'true' }}
@@ -130,6 +132,10 @@ jobs:
         run: ./miri fmt --check
       - name: clippy
         run: ./miri clippy -- -D warnings
+      - name: clippy (no features)
+        run: ./miri clippy --no-default-features -- -D warnings
+      - name: clippy (all features)
+        run: ./miri clippy --all-features -- -D warnings
       - name: rustdoc
         run: RUSTDOCFLAGS="-Dwarnings" ./miri cargo doc --document-private-items
 
@@ -189,7 +195,7 @@ jobs:
         with:
           fetch-depth: 256 # get a bit more of the history
       - name: install josh-proxy
-        run: RUSTFLAGS="--cap-lints warn" cargo +stable install josh-proxy --git https://github.com/josh-project/josh --tag r22.12.06
+        run: RUSTFLAGS="--cap-lints warn" cargo +stable install josh-proxy --git https://github.com/josh-project/josh --tag r23.12.04
       - name: setup bot git name and email
         run: |
           git config --global user.name 'The Miri Cronjob Bot'
diff --git a/src/tools/miri/CONTRIBUTING.md b/src/tools/miri/CONTRIBUTING.md
index 3416fb0d9ba..60bc1d5282d 100644
--- a/src/tools/miri/CONTRIBUTING.md
+++ b/src/tools/miri/CONTRIBUTING.md
@@ -241,18 +241,20 @@ We use the [`josh` proxy](https://github.com/josh-project/josh) to transmit chan
 rustc and Miri repositories. You can install it as follows:
 
 ```sh
-cargo +stable install josh-proxy --git https://github.com/josh-project/josh --tag r22.12.06
+RUSTFLAGS="--cap-lints=warn" cargo +stable install josh-proxy --git https://github.com/josh-project/josh --tag r23.12.04
 ```
 
 Josh will automatically be started and stopped by `./miri`.
 
 ### Importing changes from the rustc repo
 
+*Note: this usually happens automatically, so these steps rarely have to be done by hand.*
+
 We assume we start on an up-to-date master branch in the Miri repo.
 
 ```sh
 # Fetch and merge rustc side of the history. Takes ca 5 min the first time.
-# This will also update the 'rustc-version' file.
+# This will also update the `rustc-version` file.
 ./miri rustc-pull
 # Update local toolchain and apply formatting.
 ./miri toolchain && ./miri fmt
@@ -266,12 +268,6 @@ needed.
 
 ### Exporting changes to the rustc repo
 
-Keep in mind that pushing is the most complicated job that josh has to do -- pulling just filters
-the rustc history, but pushing needs to construct a new rustc history that would filter to the given
-Miri history! To avoid problems, it is a good idea to always pull immediately before you push. If
-you are getting strange errors, chances are you are running into [this josh
-bug](https://github.com/josh-project/josh/issues/998). In that case, please get in touch on Zulip.
-
 We will use the josh proxy to push to your fork of rustc. Run the following in the Miri repo,
 assuming we are on an up-to-date master branch:
 
@@ -280,9 +276,9 @@ assuming we are on an up-to-date master branch:
 ./miri rustc-push YOUR_NAME miri
 ```
 
-This will create a new branch called 'miri' in your fork, and the output should
-include a link to create a rustc PR that will integrate those changes into the
-main repository.
+This will create a new branch called `miri` in your fork, and the output should include a link that
+creates a rustc PR to integrate those changes into the main repository. If that PR has conflicts,
+you need to pull rustc changes into Miri first, and then re-do the rustc push.
 
 If this fails due to authentication problems, it can help to make josh push via ssh instead of
 https. Add the following to your `.gitconfig`:
diff --git a/src/tools/miri/ci/ci.sh b/src/tools/miri/ci/ci.sh
index cccf10a7d70..f8ba612750e 100755
--- a/src/tools/miri/ci/ci.sh
+++ b/src/tools/miri/ci/ci.sh
@@ -27,60 +27,59 @@ export RUSTFLAGS="-D warnings"
 export CARGO_INCREMENTAL=0
 export CARGO_EXTRA_FLAGS="--locked"
 
-# Determine configuration for installed build
+# Determine configuration for installed build (used by test-cargo-miri).
 echo "Installing release version of Miri"
-./miri install
-
-echo "Checking various feature flag configurations"
-./miri check --no-default-features # make sure this can be built
-./miri check # and this, too
-# `--all-features` is used for the build below, so no extra check needed.
+time ./miri install
 
 # Prepare debug build for direct `./miri` invocations.
 # We enable all features to make sure the Stacked Borrows consistency check runs.
 echo "Building debug version of Miri"
 export CARGO_EXTRA_FLAGS="$CARGO_EXTRA_FLAGS --all-features"
-./miri build --all-targets # the build that all the `./miri test` below will use
+time ./miri build --all-targets # the build that all the `./miri test` below will use
 
 endgroup
 
-# Test
+# Run tests. Recognizes these variables:
+# - MIRI_TEST_TARGET: the target to test. Empty for host target.
+# - GC_STRESS: if non-empty, run the GC stress test for the main test suite.
+# - MIR_OPT: if non-empty, re-run test `pass` tests with mir-opt-level=4
+# - MANY_SEEDS: if set to N, run the "many-seeds" tests N times
+# - TEST_BENCH: if non-empty, check that the benchmarks all build
+# - CARGO_MIRI_ENV: if non-empty, set some env vars and config to potentially confuse cargo-miri
 function run_tests {
-  if [ -n "${MIRI_TEST_TARGET:-}" ]; then
+  if [ -n "${MIRI_TEST_TARGET-}" ]; then
     begingroup "Testing foreign architecture $MIRI_TEST_TARGET"
   else
     begingroup "Testing host architecture"
   fi
 
   ## ui test suite
-  # On the host, also stress-test the GC.
-  if [ -z "${MIRI_TEST_TARGET:-}" ]; then
-    MIRIFLAGS="${MIRIFLAGS:-} -Zmiri-provenance-gc=1" ./miri test
+  if [ -n "${GC_STRESS-}" ]; then
+    time MIRIFLAGS="${MIRIFLAGS-} -Zmiri-provenance-gc=1" ./miri test
   else
-    ./miri test
+    time ./miri test
   fi
 
-  # Host-only tests
-  if [ -z "${MIRI_TEST_TARGET:-}" ]; then
-    # Running these on all targets is unlikely to catch more problems and would
-    # cost a lot of CI time.
-
+  ## advanced tests
+  if [ -n "${MIR_OPT-}" ]; then
     # Tests with optimizations (`-O` is what cargo passes, but crank MIR optimizations up all the
     # way, too).
     # Optimizations change diagnostics (mostly backtraces), so we don't check
     # them. Also error locations change so we don't run the failing tests.
     # We explicitly enable debug-assertions here, they are disabled by -O but we have tests
     # which exist to check that we panic on debug assertion failures.
-    MIRIFLAGS="${MIRIFLAGS:-} -O -Zmir-opt-level=4 -Cdebug-assertions=yes" MIRI_SKIP_UI_CHECKS=1 ./miri test -- tests/{pass,panic}
-
+    time MIRIFLAGS="${MIRIFLAGS-} -O -Zmir-opt-level=4 -Cdebug-assertions=yes" MIRI_SKIP_UI_CHECKS=1 ./miri test -- tests/{pass,panic}
+  fi
+  if [ -n "${MANY_SEEDS-}" ]; then
     # Also run some many-seeds tests. 64 seeds means this takes around a minute per test.
     # (Need to invoke via explicit `bash -c` for Windows.)
-    for FILE in tests/many-seeds/*.rs; do
-      MIRI_SEEDS=64 ./miri many-seeds "$BASH" -c "./miri run '$FILE'"
+    time for FILE in tests/many-seeds/*.rs; do
+      MIRI_SEEDS=$MANY_SEEDS ./miri many-seeds "$BASH" -c "./miri run '$FILE'"
     done
-
+  fi
+  if [ -n "${TEST_BENCH-}" ]; then
     # Check that the benchmarks build and run, but without actually benchmarking.
-    HYPERFINE="'$BASH' -c" ./miri bench
+    time HYPERFINE="'$BASH' -c" ./miri bench
   fi
 
   ## test-cargo-miri
@@ -91,16 +90,18 @@ function run_tests {
     PYTHON=python
   fi
   # Some environment setup that attempts to confuse the heck out of cargo-miri.
-  if [ "$HOST_TARGET" = x86_64-unknown-linux-gnu ]; then
-    # These act up on Windows (`which miri` produces a filename that does not exist?!?),
-    # so let's do this only on Linux. Also makes sure things work without these set.
-    export RUSTC=$(which rustc) # Produces a warning unless we also set MIRI
+  if [ -n "${CARGO_MIRI_ENV-}" ]; then
+    # These act up on Windows (`which miri` produces a filename that does not exist?!?).
+    # RUSTC is the main thing to set (it changes the first argument our wrapper will see).
+    # Unless MIRI is also set, that produces a warning.
+    export RUSTC=$(which rustc)
     export MIRI=$(rustc +miri --print sysroot)/bin/miri
+    # We entirely ignore other wrappers.
+    mkdir -p .cargo
+    echo 'build.rustc-wrapper = "thisdoesnotexist"' > .cargo/config.toml
   fi
-  mkdir -p .cargo
-  echo 'build.rustc-wrapper = "thisdoesnotexist"' > .cargo/config.toml
   # Run the actual test
-  ${PYTHON} test-cargo-miri/run-test.py
+  time ${PYTHON} test-cargo-miri/run-test.py
   # Clean up
   unset RUSTC MIRI
   rm -rf .cargo
@@ -109,7 +110,7 @@ function run_tests {
 }
 
 function run_tests_minimal {
-  if [ -n "${MIRI_TEST_TARGET:-}" ]; then
+  if [ -n "${MIRI_TEST_TARGET-}" ]; then
     begingroup "Testing MINIMAL foreign architecture $MIRI_TEST_TARGET: only testing $@"
   else
     echo "run_tests_minimal requires MIRI_TEST_TARGET to be set"
@@ -126,38 +127,49 @@ function run_tests_minimal {
 
 ## Main Testing Logic ##
 
-# Host target.
-run_tests
-
-# Extra targets.
 # In particular, fully cover all tier 1 targets.
 case $HOST_TARGET in
   x86_64-unknown-linux-gnu)
+    # Host
+    GC_STRESS=1 MIR_OPT=1 MANY_SEEDS=64 TEST_BENCH=1 CARGO_MIRI_ENV=1 run_tests
+    # Extra tier 1
     MIRI_TEST_TARGET=i686-unknown-linux-gnu run_tests
     MIRI_TEST_TARGET=aarch64-unknown-linux-gnu run_tests
-    MIRI_TEST_TARGET=aarch64-apple-darwin run_tests
+    MIRI_TEST_TARGET=x86_64-apple-darwin run_tests
     MIRI_TEST_TARGET=i686-pc-windows-gnu run_tests
     MIRI_TEST_TARGET=x86_64-pc-windows-gnu run_tests
+    # Extra tier 2
+    MIRI_TEST_TARGET=aarch64-apple-darwin run_tests
     MIRI_TEST_TARGET=arm-unknown-linux-gnueabi run_tests
-    # Some targets are only partially supported.
+    # Partially supported targets (tier 2)
     MIRI_TEST_TARGET=x86_64-unknown-freebsd run_tests_minimal hello integer vec panic/panic concurrency/simple pthread-threadname libc-getentropy libc-getrandom libc-misc libc-fs atomic env align num_cpus
     MIRI_TEST_TARGET=i686-unknown-freebsd run_tests_minimal hello integer vec panic/panic concurrency/simple pthread-threadname libc-getentropy libc-getrandom libc-misc libc-fs atomic env align num_cpus
-
     MIRI_TEST_TARGET=aarch64-linux-android run_tests_minimal hello integer vec panic/panic
     MIRI_TEST_TARGET=wasm32-wasi run_tests_minimal no_std integer strings wasm
     MIRI_TEST_TARGET=wasm32-unknown-unknown run_tests_minimal no_std integer strings wasm
-    MIRI_TEST_TARGET=thumbv7em-none-eabihf run_tests_minimal no_std # no_std embedded architecture
-    MIRI_TEST_TARGET=tests/avr.json MIRI_NO_STD=1 run_tests_minimal no_std # JSON target file
+    MIRI_TEST_TARGET=thumbv7em-none-eabihf run_tests_minimal no_std
+    # Custom target JSON file
+    MIRI_TEST_TARGET=tests/avr.json MIRI_NO_STD=1 run_tests_minimal no_std
     ;;
-  x86_64-apple-darwin)
+  aarch64-apple-darwin)
+    # Host (tier 2)
+    GC_STRESS=1 MIR_OPT=1 MANY_SEEDS=64 TEST_BENCH=1 CARGO_MIRI_ENV=1 run_tests
+    # Extra tier 1
+    MIRI_TEST_TARGET=x86_64-pc-windows-msvc CARGO_MIRI_ENV=1 run_tests
+    # Extra tier 2
     MIRI_TEST_TARGET=s390x-unknown-linux-gnu run_tests # big-endian architecture
-    MIRI_TEST_TARGET=x86_64-pc-windows-msvc run_tests
     ;;
   i686-pc-windows-msvc)
+    # Host
+    # Only smoke-test `many-seeds`; 64 runs take 15min here!
+    GC_STRESS=1 MIR_OPT=1 MANY_SEEDS=1 TEST_BENCH=1 run_tests
+    # Extra tier 1
+    # We really want to ensure a Linux target works on a Windows host,
+    # and a 64bit target works on a 32bit host.
     MIRI_TEST_TARGET=x86_64-unknown-linux-gnu run_tests
     ;;
   *)
-    echo "FATAL: unknown OS"
+    echo "FATAL: unknown host target: $HOST_TARGET"
     exit 1
     ;;
 esac
diff --git a/src/tools/miri/miri-script/src/commands.rs b/src/tools/miri/miri-script/src/commands.rs
index 58deac66560..55b3b62819f 100644
--- a/src/tools/miri/miri-script/src/commands.rs
+++ b/src/tools/miri/miri-script/src/commands.rs
@@ -297,7 +297,7 @@ impl Command {
         };
         // Prepare the branch. Pushing works much better if we use as base exactly
         // the commit that we pulled from last time, so we use the `rust-version`
-        // file as a good approximation of that.
+        // file to find out which commit that would be.
         println!("Preparing {github_user}/rust (base: {base})...");
         if cmd!(sh, "git fetch https://github.com/{github_user}/rust {branch}")
             .ignore_stderr()
diff --git a/src/tools/miri/rust-version b/src/tools/miri/rust-version
index 187756851c7..6ad8fba723c 100644
--- a/src/tools/miri/rust-version
+++ b/src/tools/miri/rust-version
@@ -1 +1 @@
-5baf1e13f568b61e121953bf6a3d09faee7dd446
+23d47dba319331d4418827cfbb8c1af283497d3c
diff --git a/src/tools/miri/src/borrow_tracker/tree_borrows/diagnostics.rs b/src/tools/miri/src/borrow_tracker/tree_borrows/diagnostics.rs
index 2690bc026a1..b9f0b5bc17a 100644
--- a/src/tools/miri/src/borrow_tracker/tree_borrows/diagnostics.rs
+++ b/src/tools/miri/src/borrow_tracker/tree_borrows/diagnostics.rs
@@ -48,7 +48,7 @@ impl AccessCause {
 /// Complete data for an event:
 #[derive(Clone, Debug)]
 pub struct Event {
-    /// Transformation of permissions that occured because of this event.
+    /// Transformation of permissions that occurred because of this event.
     pub transition: PermTransition,
     /// Kind of the access that triggered this event.
     pub access_cause: AccessCause,
@@ -58,7 +58,7 @@ pub struct Event {
     /// `None` means that this is an implicit access to the entire allocation
     /// (used for the implicit read on protector release).
     pub access_range: Option<AllocRange>,
-    /// The transition recorded by this event only occured on a subrange of
+    /// The transition recorded by this event only occurred on a subrange of
     /// `access_range`: a single access on `access_range` triggers several events,
     /// each with their own mutually disjoint `transition_range`. No-op transitions
     /// should not be recorded as events, so the union of all `transition_range` is not
diff --git a/src/tools/miri/src/borrow_tracker/tree_borrows/tree.rs b/src/tools/miri/src/borrow_tracker/tree_borrows/tree.rs
index 0fea78daa88..2470624181e 100644
--- a/src/tools/miri/src/borrow_tracker/tree_borrows/tree.rs
+++ b/src/tools/miri/src/borrow_tracker/tree_borrows/tree.rs
@@ -473,7 +473,7 @@ impl Tree {
         let rperms = {
             let mut perms = UniValMap::default();
             // We manually set it to `Active` on all in-bounds positions.
-            // We also ensure that it is initalized, so that no `Active` but
+            // We also ensure that it is initialized, so that no `Active` but
             // not yet initialized nodes exist. Essentially, we pretend there
             // was a write that initialized these to `Active`.
             perms.insert(root_idx, LocationState::new_init(Permission::new_active()));
diff --git a/src/tools/miri/src/borrow_tracker/tree_borrows/tree/tests.rs b/src/tools/miri/src/borrow_tracker/tree_borrows/tree/tests.rs
index f568850d8db..6777f41ac2d 100644
--- a/src/tools/miri/src/borrow_tracker/tree_borrows/tree/tests.rs
+++ b/src/tools/miri/src/borrow_tracker/tree_borrows/tree/tests.rs
@@ -75,7 +75,7 @@ fn protected_enforces_noalias() {
     }
 }
 
-/// We are going to exhaustively test the possibily of inserting
+/// We are going to exhaustively test the possibility of inserting
 /// a spurious read in some code.
 ///
 /// We choose some pointer `x` through which we want a spurious read to be inserted.
@@ -270,7 +270,7 @@ mod spurious_read {
             match self {
                 TestEvent::Access(acc) => write!(f, "{acc}"),
                 // The fields of the `Ret` variants just serve to make them
-                // impossible to instanciate via the `RetX = NoRet` type; we can
+                // impossible to instantiate via the `RetX = NoRet` type; we can
                 // always ignore their value.
                 TestEvent::RetX(_) => write!(f, "ret x"),
                 TestEvent::RetY(_) => write!(f, "ret y"),
@@ -395,7 +395,7 @@ mod spurious_read {
             match evt {
                 TestEvent::Access(acc) => self.perform_test_access(acc),
                 // The fields of the `Ret` variants just serve to make them
-                // impossible to instanciate via the `RetX = NoRet` type; we can
+                // impossible to instantiate via the `RetX = NoRet` type; we can
                 // always ignore their value.
                 TestEvent::RetX(_) => self.end_protector_x(),
                 TestEvent::RetY(_) => self.end_protector_y(),
diff --git a/src/tools/miri/src/machine.rs b/src/tools/miri/src/machine.rs
index 2137de6a29b..ff081328a72 100644
--- a/src/tools/miri/src/machine.rs
+++ b/src/tools/miri/src/machine.rs
@@ -31,7 +31,7 @@ use rustc_target::spec::abi::Abi;
 
 use crate::{
     concurrency::{data_race, weak_memory},
-    shims::unix::FileHandler,
+    shims::unix::FdTable,
     *,
 };
 
@@ -463,9 +463,9 @@ pub struct MiriMachine<'mir, 'tcx> {
     pub(crate) validate: bool,
 
     /// The table of file descriptors.
-    pub(crate) file_handler: shims::unix::FileHandler,
+    pub(crate) fds: shims::unix::FdTable,
     /// The table of directory descriptors.
-    pub(crate) dir_handler: shims::unix::DirHandler,
+    pub(crate) dirs: shims::unix::DirTable,
 
     /// This machine's monotone clock.
     pub(crate) clock: Clock,
@@ -640,8 +640,8 @@ impl<'mir, 'tcx> MiriMachine<'mir, 'tcx> {
             tls: TlsData::default(),
             isolated_op: config.isolated_op,
             validate: config.validate,
-            file_handler: FileHandler::new(config.mute_stdout_stderr),
-            dir_handler: Default::default(),
+            fds: FdTable::new(config.mute_stdout_stderr),
+            dirs: Default::default(),
             layouts,
             threads: ThreadManager::default(),
             static_roots: Vec::new(),
@@ -774,11 +774,11 @@ impl VisitProvenance for MiriMachine<'_, '_> {
             argv,
             cmd_line,
             extern_statics,
-            dir_handler,
+            dirs,
             borrow_tracker,
             data_race,
             alloc_addresses,
-            file_handler,
+            fds,
             tcx: _,
             isolated_op: _,
             validate: _,
@@ -817,8 +817,8 @@ impl VisitProvenance for MiriMachine<'_, '_> {
         threads.visit_provenance(visit);
         tls.visit_provenance(visit);
         env_vars.visit_provenance(visit);
-        dir_handler.visit_provenance(visit);
-        file_handler.visit_provenance(visit);
+        dirs.visit_provenance(visit);
+        fds.visit_provenance(visit);
         data_race.visit_provenance(visit);
         borrow_tracker.visit_provenance(visit);
         alloc_addresses.visit_provenance(visit);
@@ -1066,7 +1066,7 @@ impl<'mir, 'tcx> Machine<'mir, 'tcx> for MiriMachine<'mir, 'tcx> {
             let extern_decl_layout = ecx.tcx.layout_of(ty::ParamEnv::empty().and(def_ty)).unwrap();
             if extern_decl_layout.size != shim_size || extern_decl_layout.align.abi != shim_align {
                 throw_unsup_format!(
-                    "`extern` static `{name}` from crate `{krate}` has been declared \
+                    "extern static `{link_name}` has been declared as `{krate}::{name}` \
                     with a size of {decl_size} bytes and alignment of {decl_align} bytes, \
                     but Miri emulates it via an extern static shim \
                     with a size of {shim_size} bytes and alignment of {shim_align} bytes",
@@ -1080,11 +1080,7 @@ impl<'mir, 'tcx> Machine<'mir, 'tcx> for MiriMachine<'mir, 'tcx> {
             }
             Ok(ptr)
         } else {
-            throw_unsup_format!(
-                "`extern` static `{name}` from crate `{krate}` is not supported by Miri",
-                name = ecx.tcx.def_path_str(def_id),
-                krate = ecx.tcx.crate_name(def_id.krate),
-            )
+            throw_unsup_format!("extern static `{link_name}` is not supported by Miri",)
         }
     }
 
diff --git a/src/tools/miri/src/shims/unix/fd.rs b/src/tools/miri/src/shims/unix/fd.rs
new file mode 100644
index 00000000000..a5fe38b902d
--- /dev/null
+++ b/src/tools/miri/src/shims/unix/fd.rs
@@ -0,0 +1,431 @@
+//! General management of file descriptors, and support for
+//! standard file descriptors (stdin/stdout/stderr).
+
+use std::any::Any;
+use std::collections::BTreeMap;
+use std::io::{self, ErrorKind, IsTerminal, Read, SeekFrom, Write};
+
+use rustc_middle::ty::TyCtxt;
+use rustc_target::abi::Size;
+
+use crate::shims::unix::*;
+use crate::*;
+
+/// Represents an open file descriptor.
+pub trait FileDescriptor: std::fmt::Debug + Any {
+    fn name(&self) -> &'static str;
+
+    fn read<'tcx>(
+        &mut self,
+        _communicate_allowed: bool,
+        _bytes: &mut [u8],
+        _tcx: TyCtxt<'tcx>,
+    ) -> InterpResult<'tcx, io::Result<usize>> {
+        throw_unsup_format!("cannot read from {}", self.name());
+    }
+
+    fn write<'tcx>(
+        &self,
+        _communicate_allowed: bool,
+        _bytes: &[u8],
+        _tcx: TyCtxt<'tcx>,
+    ) -> InterpResult<'tcx, io::Result<usize>> {
+        throw_unsup_format!("cannot write to {}", self.name());
+    }
+
+    fn seek<'tcx>(
+        &mut self,
+        _communicate_allowed: bool,
+        _offset: SeekFrom,
+    ) -> InterpResult<'tcx, io::Result<u64>> {
+        throw_unsup_format!("cannot seek on {}", self.name());
+    }
+
+    fn close<'tcx>(
+        self: Box<Self>,
+        _communicate_allowed: bool,
+    ) -> InterpResult<'tcx, io::Result<i32>> {
+        throw_unsup_format!("cannot close {}", self.name());
+    }
+
+    /// Return a new file descriptor *that refers to the same underlying object*.
+    fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>>;
+
+    fn is_tty(&self, _communicate_allowed: bool) -> bool {
+        // Most FDs are not tty's and the consequence of a wrong `false` are minor,
+        // so we use a default impl here.
+        false
+    }
+}
+
+impl dyn FileDescriptor {
+    #[inline(always)]
+    pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
+        (self as &dyn Any).downcast_ref()
+    }
+
+    #[inline(always)]
+    pub fn downcast_mut<T: Any>(&mut self) -> Option<&mut T> {
+        (self as &mut dyn Any).downcast_mut()
+    }
+}
+
+impl FileDescriptor for io::Stdin {
+    fn name(&self) -> &'static str {
+        "stdin"
+    }
+
+    fn read<'tcx>(
+        &mut self,
+        communicate_allowed: bool,
+        bytes: &mut [u8],
+        _tcx: TyCtxt<'tcx>,
+    ) -> InterpResult<'tcx, io::Result<usize>> {
+        if !communicate_allowed {
+            // We want isolation mode to be deterministic, so we have to disallow all reads, even stdin.
+            helpers::isolation_abort_error("`read` from stdin")?;
+        }
+        Ok(Read::read(self, bytes))
+    }
+
+    fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>> {
+        Ok(Box::new(io::stdin()))
+    }
+
+    fn is_tty(&self, communicate_allowed: bool) -> bool {
+        communicate_allowed && self.is_terminal()
+    }
+}
+
+impl FileDescriptor for io::Stdout {
+    fn name(&self) -> &'static str {
+        "stdout"
+    }
+
+    fn write<'tcx>(
+        &self,
+        _communicate_allowed: bool,
+        bytes: &[u8],
+        _tcx: TyCtxt<'tcx>,
+    ) -> InterpResult<'tcx, io::Result<usize>> {
+        // We allow writing to stderr even with isolation enabled.
+        let result = Write::write(&mut { self }, bytes);
+        // Stdout is buffered, flush to make sure it appears on the
+        // screen.  This is the write() syscall of the interpreted
+        // program, we want it to correspond to a write() syscall on
+        // the host -- there is no good in adding extra buffering
+        // here.
+        io::stdout().flush().unwrap();
+
+        Ok(result)
+    }
+
+    fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>> {
+        Ok(Box::new(io::stdout()))
+    }
+
+    fn is_tty(&self, communicate_allowed: bool) -> bool {
+        communicate_allowed && self.is_terminal()
+    }
+}
+
+impl FileDescriptor for io::Stderr {
+    fn name(&self) -> &'static str {
+        "stderr"
+    }
+
+    fn write<'tcx>(
+        &self,
+        _communicate_allowed: bool,
+        bytes: &[u8],
+        _tcx: TyCtxt<'tcx>,
+    ) -> InterpResult<'tcx, io::Result<usize>> {
+        // We allow writing to stderr even with isolation enabled.
+        // No need to flush, stderr is not buffered.
+        Ok(Write::write(&mut { self }, bytes))
+    }
+
+    fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>> {
+        Ok(Box::new(io::stderr()))
+    }
+
+    fn is_tty(&self, communicate_allowed: bool) -> bool {
+        communicate_allowed && self.is_terminal()
+    }
+}
+
+/// Like /dev/null
+#[derive(Debug)]
+pub struct NullOutput;
+
+impl FileDescriptor for NullOutput {
+    fn name(&self) -> &'static str {
+        "stderr and stdout"
+    }
+
+    fn write<'tcx>(
+        &self,
+        _communicate_allowed: bool,
+        bytes: &[u8],
+        _tcx: TyCtxt<'tcx>,
+    ) -> InterpResult<'tcx, io::Result<usize>> {
+        // We just don't write anything, but report to the user that we did.
+        Ok(Ok(bytes.len()))
+    }
+
+    fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>> {
+        Ok(Box::new(NullOutput))
+    }
+}
+
+/// The file descriptor table
+#[derive(Debug)]
+pub struct FdTable {
+    pub fds: BTreeMap<i32, Box<dyn FileDescriptor>>,
+}
+
+impl VisitProvenance for FdTable {
+    fn visit_provenance(&self, _visit: &mut VisitWith<'_>) {
+        // All our FileDescriptor do not have any tags.
+    }
+}
+
+impl FdTable {
+    pub(crate) fn new(mute_stdout_stderr: bool) -> FdTable {
+        let mut fds: BTreeMap<_, Box<dyn FileDescriptor>> = BTreeMap::new();
+        fds.insert(0i32, Box::new(io::stdin()));
+        if mute_stdout_stderr {
+            fds.insert(1i32, Box::new(NullOutput));
+            fds.insert(2i32, Box::new(NullOutput));
+        } else {
+            fds.insert(1i32, Box::new(io::stdout()));
+            fds.insert(2i32, Box::new(io::stderr()));
+        }
+        FdTable { fds }
+    }
+
+    pub fn insert_fd(&mut self, file_handle: Box<dyn FileDescriptor>) -> i32 {
+        self.insert_fd_with_min_fd(file_handle, 0)
+    }
+
+    /// Insert a new FD that is at least `min_fd`.
+    pub fn insert_fd_with_min_fd(
+        &mut self,
+        file_handle: Box<dyn FileDescriptor>,
+        min_fd: i32,
+    ) -> i32 {
+        // Find the lowest unused FD, starting from min_fd. If the first such unused FD is in
+        // between used FDs, the find_map combinator will return it. If the first such unused FD
+        // is after all other used FDs, the find_map combinator will return None, and we will use
+        // the FD following the greatest FD thus far.
+        let candidate_new_fd =
+            self.fds.range(min_fd..).zip(min_fd..).find_map(|((fd, _fh), counter)| {
+                if *fd != counter {
+                    // There was a gap in the fds stored, return the first unused one
+                    // (note that this relies on BTreeMap iterating in key order)
+                    Some(counter)
+                } else {
+                    // This fd is used, keep going
+                    None
+                }
+            });
+        let new_fd = candidate_new_fd.unwrap_or_else(|| {
+            // find_map ran out of BTreeMap entries before finding a free fd, use one plus the
+            // maximum fd in the map
+            self.fds.last_key_value().map(|(fd, _)| fd.checked_add(1).unwrap()).unwrap_or(min_fd)
+        });
+
+        self.fds.try_insert(new_fd, file_handle).unwrap();
+        new_fd
+    }
+
+    pub fn get(&self, fd: i32) -> Option<&dyn FileDescriptor> {
+        Some(&**self.fds.get(&fd)?)
+    }
+
+    pub fn get_mut(&mut self, fd: i32) -> Option<&mut dyn FileDescriptor> {
+        Some(&mut **self.fds.get_mut(&fd)?)
+    }
+
+    pub fn remove(&mut self, fd: i32) -> Option<Box<dyn FileDescriptor>> {
+        self.fds.remove(&fd)
+    }
+
+    pub fn is_fd(&self, fd: i32) -> bool {
+        self.fds.contains_key(&fd)
+    }
+}
+
+impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {}
+pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
+    fn fcntl(&mut self, args: &[OpTy<'tcx, Provenance>]) -> InterpResult<'tcx, i32> {
+        let this = self.eval_context_mut();
+
+        if args.len() < 2 {
+            throw_ub_format!(
+                "incorrect number of arguments for fcntl: got {}, expected at least 2",
+                args.len()
+            );
+        }
+        let fd = this.read_scalar(&args[0])?.to_i32()?;
+        let cmd = this.read_scalar(&args[1])?.to_i32()?;
+
+        // We only support getting the flags for a descriptor.
+        if cmd == this.eval_libc_i32("F_GETFD") {
+            // Currently this is the only flag that `F_GETFD` returns. It is OK to just return the
+            // `FD_CLOEXEC` value without checking if the flag is set for the file because `std`
+            // always sets this flag when opening a file. However we still need to check that the
+            // file itself is open.
+            if this.machine.fds.is_fd(fd) {
+                Ok(this.eval_libc_i32("FD_CLOEXEC"))
+            } else {
+                this.fd_not_found()
+            }
+        } else if cmd == this.eval_libc_i32("F_DUPFD")
+            || cmd == this.eval_libc_i32("F_DUPFD_CLOEXEC")
+        {
+            // Note that we always assume the FD_CLOEXEC flag is set for every open file, in part
+            // because exec() isn't supported. The F_DUPFD and F_DUPFD_CLOEXEC commands only
+            // differ in whether the FD_CLOEXEC flag is pre-set on the new file descriptor,
+            // thus they can share the same implementation here.
+            if args.len() < 3 {
+                throw_ub_format!(
+                    "incorrect number of arguments for fcntl with cmd=`F_DUPFD`/`F_DUPFD_CLOEXEC`: got {}, expected at least 3",
+                    args.len()
+                );
+            }
+            let start = this.read_scalar(&args[2])?.to_i32()?;
+
+            match this.machine.fds.get_mut(fd) {
+                Some(file_descriptor) => {
+                    let dup_result = file_descriptor.dup();
+                    match dup_result {
+                        Ok(dup_fd) => Ok(this.machine.fds.insert_fd_with_min_fd(dup_fd, start)),
+                        Err(e) => {
+                            this.set_last_error_from_io_error(e.kind())?;
+                            Ok(-1)
+                        }
+                    }
+                }
+                None => this.fd_not_found(),
+            }
+        } else if this.tcx.sess.target.os == "macos" && cmd == this.eval_libc_i32("F_FULLFSYNC") {
+            // Reject if isolation is enabled.
+            if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
+                this.reject_in_isolation("`fcntl`", reject_with)?;
+                this.set_last_error_from_io_error(ErrorKind::PermissionDenied)?;
+                return Ok(-1);
+            }
+
+            this.ffullsync_fd(fd)
+        } else {
+            throw_unsup_format!("the {:#x} command is not supported for `fcntl`)", cmd);
+        }
+    }
+
+    fn close(&mut self, fd_op: &OpTy<'tcx, Provenance>) -> InterpResult<'tcx, Scalar<Provenance>> {
+        let this = self.eval_context_mut();
+
+        let fd = this.read_scalar(fd_op)?.to_i32()?;
+
+        Ok(Scalar::from_i32(if let Some(file_descriptor) = this.machine.fds.remove(fd) {
+            let result = file_descriptor.close(this.machine.communicate())?;
+            this.try_unwrap_io_result(result)?
+        } else {
+            this.fd_not_found()?
+        }))
+    }
+
+    /// Function used when a file descriptor does not exist. It returns `Ok(-1)`and sets
+    /// the last OS error to `libc::EBADF` (invalid file descriptor). This function uses
+    /// `T: From<i32>` instead of `i32` directly because some fs functions return different integer
+    /// types (like `read`, that returns an `i64`).
+    fn fd_not_found<T: From<i32>>(&mut self) -> InterpResult<'tcx, T> {
+        let this = self.eval_context_mut();
+        let ebadf = this.eval_libc("EBADF");
+        this.set_last_error(ebadf)?;
+        Ok((-1).into())
+    }
+
+    fn read(
+        &mut self,
+        fd: i32,
+        buf: Pointer<Option<Provenance>>,
+        count: u64,
+    ) -> InterpResult<'tcx, i64> {
+        let this = self.eval_context_mut();
+
+        // Isolation check is done via `FileDescriptor` trait.
+
+        trace!("Reading from FD {}, size {}", fd, count);
+
+        // Check that the *entire* buffer is actually valid memory.
+        this.check_ptr_access(buf, Size::from_bytes(count), CheckInAllocMsg::MemoryAccessTest)?;
+
+        // We cap the number of read bytes to the largest value that we are able to fit in both the
+        // host's and target's `isize`. This saves us from having to handle overflows later.
+        let count = count
+            .min(u64::try_from(this.target_isize_max()).unwrap())
+            .min(u64::try_from(isize::MAX).unwrap());
+        let communicate = this.machine.communicate();
+
+        if let Some(file_descriptor) = this.machine.fds.get_mut(fd) {
+            trace!("read: FD mapped to {:?}", file_descriptor);
+            // We want to read at most `count` bytes. We are sure that `count` is not negative
+            // because it was a target's `usize`. Also we are sure that its smaller than
+            // `usize::MAX` because it is bounded by the host's `isize`.
+            let mut bytes = vec![0; usize::try_from(count).unwrap()];
+            // `File::read` never returns a value larger than `count`,
+            // so this cannot fail.
+            let result = file_descriptor
+                .read(communicate, &mut bytes, *this.tcx)?
+                .map(|c| i64::try_from(c).unwrap());
+
+            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)?;
+                    Ok(read_bytes)
+                }
+                Err(e) => {
+                    this.set_last_error_from_io_error(e.kind())?;
+                    Ok(-1)
+                }
+            }
+        } else {
+            trace!("read: FD not found");
+            this.fd_not_found()
+        }
+    }
+
+    fn write(
+        &mut self,
+        fd: i32,
+        buf: Pointer<Option<Provenance>>,
+        count: u64,
+    ) -> InterpResult<'tcx, i64> {
+        let this = self.eval_context_mut();
+
+        // Isolation check is done via `FileDescriptor` trait.
+
+        // Check that the *entire* buffer is actually valid memory.
+        this.check_ptr_access(buf, Size::from_bytes(count), CheckInAllocMsg::MemoryAccessTest)?;
+
+        // We cap the number of written bytes to the largest value that we are able to fit in both the
+        // host's and target's `isize`. This saves us from having to handle overflows later.
+        let count = count
+            .min(u64::try_from(this.target_isize_max()).unwrap())
+            .min(u64::try_from(isize::MAX).unwrap());
+        let communicate = this.machine.communicate();
+
+        if let Some(file_descriptor) = this.machine.fds.get(fd) {
+            let bytes = this.read_bytes_ptr_strip_provenance(buf, Size::from_bytes(count))?;
+            let result = file_descriptor
+                .write(communicate, bytes, *this.tcx)?
+                .map(|c| i64::try_from(c).unwrap());
+            this.try_unwrap_io_result(result)
+        } else {
+            this.fd_not_found()
+        }
+    }
+}
diff --git a/src/tools/miri/src/shims/unix/foreign_items.rs b/src/tools/miri/src/shims/unix/foreign_items.rs
index 4ceda809350..3a56aa9138b 100644
--- a/src/tools/miri/src/shims/unix/foreign_items.rs
+++ b/src/tools/miri/src/shims/unix/foreign_items.rs
@@ -6,12 +6,9 @@ use rustc_span::Symbol;
 use rustc_target::abi::{Align, Size};
 use rustc_target::spec::abi::Abi;
 
+use crate::shims::unix::*;
 use crate::*;
 use shims::foreign_items::EmulateForeignItemResult;
-use shims::unix::fs::EvalContextExt as _;
-use shims::unix::mem::EvalContextExt as _;
-use shims::unix::sync::EvalContextExt as _;
-use shims::unix::thread::EvalContextExt as _;
 
 use shims::unix::freebsd::foreign_items as freebsd;
 use shims::unix::linux::foreign_items as linux;
@@ -51,7 +48,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
         // See `fn emulate_foreign_item_inner` in `shims/foreign_items.rs` for the general pattern.
         #[rustfmt::skip]
         match link_name.as_str() {
-            // Environment related shims
+            // Environment variables
             "getenv" => {
                 let [name] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
                 let result = this.getenv(name)?;
@@ -79,25 +76,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
                 this.write_scalar(Scalar::from_i32(result), dest)?;
             }
 
-            // File related shims
-            "open" | "open64" => {
-                // `open` is variadic, the third argument is only present when the second argument has O_CREAT (or on linux O_TMPFILE, but miri doesn't support that) set
-                this.check_abi_and_shim_symbol_clash(abi, Abi::C { unwind: false }, link_name)?;
-                let result = this.open(args)?;
-                this.write_scalar(Scalar::from_i32(result), dest)?;
-            }
-            "close" => {
-                let [fd] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
-                let result = this.close(fd)?;
-                this.write_scalar(result, dest)?;
-            }
-            "fcntl" => {
-                // `fcntl` is variadic. The argument count is checked based on the first argument
-                // in `this.fcntl()`, so we do not use `check_shim` here.
-                this.check_abi_and_shim_symbol_clash(abi, Abi::C { unwind: false }, link_name)?;
-                let result = this.fcntl(args)?;
-                this.write_scalar(Scalar::from_i32(result), dest)?;
-            }
+            // File descriptors
             "read" => {
                 let [fd, buf, count] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
                 let fd = this.read_scalar(fd)?.to_i32()?;
@@ -116,6 +95,26 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
                 // Now, `result` is the value we return back to the program.
                 this.write_scalar(Scalar::from_target_isize(result, this), dest)?;
             }
+            "close" => {
+                let [fd] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
+                let result = this.close(fd)?;
+                this.write_scalar(result, dest)?;
+            }
+            "fcntl" => {
+                // `fcntl` is variadic. The argument count is checked based on the first argument
+                // in `this.fcntl()`, so we do not use `check_shim` here.
+                this.check_abi_and_shim_symbol_clash(abi, Abi::C { unwind: false }, link_name)?;
+                let result = this.fcntl(args)?;
+                this.write_scalar(Scalar::from_i32(result), dest)?;
+            }
+
+            // File and file system access
+            "open" | "open64" => {
+                // `open` is variadic, the third argument is only present when the second argument has O_CREAT (or on linux O_TMPFILE, but miri doesn't support that) set
+                this.check_abi_and_shim_symbol_clash(abi, Abi::C { unwind: false }, link_name)?;
+                let result = this.open(args)?;
+                this.write_scalar(Scalar::from_i32(result), dest)?;
+            }
             "unlink" => {
                 let [path] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
                 let result = this.unlink(path)?;
@@ -219,7 +218,16 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
                 this.write_scalar(Scalar::from_i32(result), dest)?;
             }
 
-            // Time related shims
+            // Sockets
+            "socketpair" => {
+                let [domain, type_, protocol, sv] =
+                    this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
+
+                let result = this.socketpair(domain, type_, protocol, sv)?;
+                this.write_scalar(result, dest)?;
+            }
+
+            // Time
             "gettimeofday" => {
                 let [tv, tz] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
                 let result = this.gettimeofday(tv, tz)?;
@@ -598,7 +606,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
                 if bufsize > 256 {
                     let err = this.eval_libc("EIO");
                     this.set_last_error(err)?;
-                    this.write_scalar(Scalar::from_i32(-1), dest)?
+                    this.write_scalar(Scalar::from_i32(-1), dest)?;
                 } else {
                     this.gen_random(buf, bufsize)?;
                     this.write_scalar(Scalar::from_i32(0), dest)?;
diff --git a/src/tools/miri/src/shims/unix/freebsd/foreign_items.rs b/src/tools/miri/src/shims/unix/freebsd/foreign_items.rs
index 6814e0d4283..ffb583123d4 100644
--- a/src/tools/miri/src/shims/unix/freebsd/foreign_items.rs
+++ b/src/tools/miri/src/shims/unix/freebsd/foreign_items.rs
@@ -1,10 +1,9 @@
 use rustc_span::Symbol;
 use rustc_target::spec::abi::Abi;
 
+use crate::shims::unix::*;
 use crate::*;
 use shims::foreign_items::EmulateForeignItemResult;
-use shims::unix::fs::EvalContextExt as _;
-use shims::unix::thread::EvalContextExt as _;
 
 pub fn is_dyn_sym(_name: &str) -> bool {
     false
diff --git a/src/tools/miri/src/shims/unix/fs.rs b/src/tools/miri/src/shims/unix/fs.rs
index b141ca4a019..31076fdfaf6 100644
--- a/src/tools/miri/src/shims/unix/fs.rs
+++ b/src/tools/miri/src/shims/unix/fs.rs
@@ -1,6 +1,6 @@
-use std::any::Any;
+//! File and file system access
+
 use std::borrow::Cow;
-use std::collections::BTreeMap;
 use std::fs::{
     read_dir, remove_dir, remove_file, rename, DirBuilder, File, FileType, OpenOptions, ReadDir,
 };
@@ -13,70 +13,16 @@ use rustc_middle::ty::TyCtxt;
 use rustc_target::abi::Size;
 
 use crate::shims::os_str::bytes_to_os_str;
+use crate::shims::unix::*;
 use crate::*;
 use shims::time::system_time_to_duration;
 
 #[derive(Debug)]
-pub struct FileHandle {
+struct FileHandle {
     file: File,
     writable: bool,
 }
 
-pub trait FileDescriptor: std::fmt::Debug + Any {
-    fn name(&self) -> &'static str;
-
-    fn read<'tcx>(
-        &mut self,
-        _communicate_allowed: bool,
-        _bytes: &mut [u8],
-        _tcx: TyCtxt<'tcx>,
-    ) -> InterpResult<'tcx, io::Result<usize>> {
-        throw_unsup_format!("cannot read from {}", self.name());
-    }
-
-    fn write<'tcx>(
-        &self,
-        _communicate_allowed: bool,
-        _bytes: &[u8],
-        _tcx: TyCtxt<'tcx>,
-    ) -> InterpResult<'tcx, io::Result<usize>> {
-        throw_unsup_format!("cannot write to {}", self.name());
-    }
-
-    fn seek<'tcx>(
-        &mut self,
-        _communicate_allowed: bool,
-        _offset: SeekFrom,
-    ) -> InterpResult<'tcx, io::Result<u64>> {
-        throw_unsup_format!("cannot seek on {}", self.name());
-    }
-
-    fn close<'tcx>(
-        self: Box<Self>,
-        _communicate_allowed: bool,
-    ) -> InterpResult<'tcx, io::Result<i32>> {
-        throw_unsup_format!("cannot close {}", self.name());
-    }
-
-    fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>>;
-
-    fn is_tty(&self, _communicate_allowed: bool) -> bool {
-        false
-    }
-}
-
-impl dyn FileDescriptor {
-    #[inline(always)]
-    pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
-        (self as &dyn Any).downcast_ref()
-    }
-
-    #[inline(always)]
-    pub fn downcast_mut<T: Any>(&mut self) -> Option<&mut T> {
-        (self as &mut dyn Any).downcast_mut()
-    }
-}
-
 impl FileDescriptor for FileHandle {
     fn name(&self) -> &'static str {
         "FILE"
@@ -147,172 +93,6 @@ impl FileDescriptor for FileHandle {
     }
 }
 
-impl FileDescriptor for io::Stdin {
-    fn name(&self) -> &'static str {
-        "stdin"
-    }
-
-    fn read<'tcx>(
-        &mut self,
-        communicate_allowed: bool,
-        bytes: &mut [u8],
-        _tcx: TyCtxt<'tcx>,
-    ) -> InterpResult<'tcx, io::Result<usize>> {
-        if !communicate_allowed {
-            // We want isolation mode to be deterministic, so we have to disallow all reads, even stdin.
-            helpers::isolation_abort_error("`read` from stdin")?;
-        }
-        Ok(Read::read(self, bytes))
-    }
-
-    fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>> {
-        Ok(Box::new(io::stdin()))
-    }
-
-    fn is_tty(&self, communicate_allowed: bool) -> bool {
-        communicate_allowed && self.is_terminal()
-    }
-}
-
-impl FileDescriptor for io::Stdout {
-    fn name(&self) -> &'static str {
-        "stdout"
-    }
-
-    fn write<'tcx>(
-        &self,
-        _communicate_allowed: bool,
-        bytes: &[u8],
-        _tcx: TyCtxt<'tcx>,
-    ) -> InterpResult<'tcx, io::Result<usize>> {
-        // We allow writing to stderr even with isolation enabled.
-        let result = Write::write(&mut { self }, bytes);
-        // Stdout is buffered, flush to make sure it appears on the
-        // screen.  This is the write() syscall of the interpreted
-        // program, we want it to correspond to a write() syscall on
-        // the host -- there is no good in adding extra buffering
-        // here.
-        io::stdout().flush().unwrap();
-
-        Ok(result)
-    }
-
-    fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>> {
-        Ok(Box::new(io::stdout()))
-    }
-
-    fn is_tty(&self, communicate_allowed: bool) -> bool {
-        communicate_allowed && self.is_terminal()
-    }
-}
-
-impl FileDescriptor for io::Stderr {
-    fn name(&self) -> &'static str {
-        "stderr"
-    }
-
-    fn write<'tcx>(
-        &self,
-        _communicate_allowed: bool,
-        bytes: &[u8],
-        _tcx: TyCtxt<'tcx>,
-    ) -> InterpResult<'tcx, io::Result<usize>> {
-        // We allow writing to stderr even with isolation enabled.
-        // No need to flush, stderr is not buffered.
-        Ok(Write::write(&mut { self }, bytes))
-    }
-
-    fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>> {
-        Ok(Box::new(io::stderr()))
-    }
-
-    fn is_tty(&self, communicate_allowed: bool) -> bool {
-        communicate_allowed && self.is_terminal()
-    }
-}
-
-#[derive(Debug)]
-struct NullOutput;
-
-impl FileDescriptor for NullOutput {
-    fn name(&self) -> &'static str {
-        "stderr and stdout"
-    }
-
-    fn write<'tcx>(
-        &self,
-        _communicate_allowed: bool,
-        bytes: &[u8],
-        _tcx: TyCtxt<'tcx>,
-    ) -> InterpResult<'tcx, io::Result<usize>> {
-        // We just don't write anything, but report to the user that we did.
-        Ok(Ok(bytes.len()))
-    }
-
-    fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>> {
-        Ok(Box::new(NullOutput))
-    }
-}
-
-#[derive(Debug)]
-pub struct FileHandler {
-    pub handles: BTreeMap<i32, Box<dyn FileDescriptor>>,
-}
-
-impl VisitProvenance for FileHandler {
-    fn visit_provenance(&self, _visit: &mut VisitWith<'_>) {
-        // All our FileDescriptor do not have any tags.
-    }
-}
-
-impl FileHandler {
-    pub(crate) fn new(mute_stdout_stderr: bool) -> FileHandler {
-        let mut handles: BTreeMap<_, Box<dyn FileDescriptor>> = BTreeMap::new();
-        handles.insert(0i32, Box::new(io::stdin()));
-        if mute_stdout_stderr {
-            handles.insert(1i32, Box::new(NullOutput));
-            handles.insert(2i32, Box::new(NullOutput));
-        } else {
-            handles.insert(1i32, Box::new(io::stdout()));
-            handles.insert(2i32, Box::new(io::stderr()));
-        }
-        FileHandler { handles }
-    }
-
-    pub fn insert_fd(&mut self, file_handle: Box<dyn FileDescriptor>) -> i32 {
-        self.insert_fd_with_min_fd(file_handle, 0)
-    }
-
-    fn insert_fd_with_min_fd(&mut self, file_handle: Box<dyn FileDescriptor>, min_fd: i32) -> i32 {
-        // Find the lowest unused FD, starting from min_fd. If the first such unused FD is in
-        // between used FDs, the find_map combinator will return it. If the first such unused FD
-        // is after all other used FDs, the find_map combinator will return None, and we will use
-        // the FD following the greatest FD thus far.
-        let candidate_new_fd =
-            self.handles.range(min_fd..).zip(min_fd..).find_map(|((fd, _fh), counter)| {
-                if *fd != counter {
-                    // There was a gap in the fds stored, return the first unused one
-                    // (note that this relies on BTreeMap iterating in key order)
-                    Some(counter)
-                } else {
-                    // This fd is used, keep going
-                    None
-                }
-            });
-        let new_fd = candidate_new_fd.unwrap_or_else(|| {
-            // find_map ran out of BTreeMap entries before finding a free fd, use one plus the
-            // maximum fd in the map
-            self.handles
-                .last_key_value()
-                .map(|(fd, _)| fd.checked_add(1).unwrap())
-                .unwrap_or(min_fd)
-        });
-
-        self.handles.try_insert(new_fd, file_handle).unwrap();
-        new_fd
-    }
-}
-
 impl<'mir, 'tcx: 'mir> EvalContextExtPrivate<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {}
 trait EvalContextExtPrivate<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
     fn macos_stat_write_buf(
@@ -411,10 +191,11 @@ trait EvalContextExtPrivate<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx
 
 /// An open directory, tracked by DirHandler.
 #[derive(Debug)]
-pub struct OpenDir {
+struct OpenDir {
     /// The directory reader on the host.
     read_dir: ReadDir,
-    /// The most recent entry returned by readdir()
+    /// The most recent entry returned by readdir().
+    /// Will be freed by the next call.
     entry: Pointer<Option<Provenance>>,
 }
 
@@ -425,8 +206,11 @@ impl OpenDir {
     }
 }
 
+/// The table of open directories.
+/// Curiously, Unix/POSIX does not unify this into the "file descriptor" concept... everything
+/// is a file, except a directory is not?
 #[derive(Debug)]
-pub struct DirHandler {
+pub struct DirTable {
     /// Directory iterators used to emulate libc "directory streams", as used in opendir, readdir,
     /// and closedir.
     ///
@@ -441,7 +225,7 @@ pub struct DirHandler {
     next_id: u64,
 }
 
-impl DirHandler {
+impl DirTable {
     #[allow(clippy::arithmetic_side_effects)]
     fn insert_new(&mut self, read_dir: ReadDir) -> u64 {
         let id = self.next_id;
@@ -451,9 +235,9 @@ impl DirHandler {
     }
 }
 
-impl Default for DirHandler {
-    fn default() -> DirHandler {
-        DirHandler {
+impl Default for DirTable {
+    fn default() -> DirTable {
+        DirTable {
             streams: FxHashMap::default(),
             // Skip 0 as an ID, because it looks like a null pointer to libc
             next_id: 1,
@@ -461,9 +245,9 @@ impl Default for DirHandler {
     }
 }
 
-impl VisitProvenance for DirHandler {
+impl VisitProvenance for DirTable {
     fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
-        let DirHandler { streams, next_id: _ } = self;
+        let DirTable { streams, next_id: _ } = self;
 
         for dir in streams.values() {
             dir.entry.visit_provenance(visit);
@@ -615,200 +399,13 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
         }
 
         let fd = options.open(path).map(|file| {
-            let fh = &mut this.machine.file_handler;
+            let fh = &mut this.machine.fds;
             fh.insert_fd(Box::new(FileHandle { file, writable }))
         });
 
         this.try_unwrap_io_result(fd)
     }
 
-    fn fcntl(&mut self, args: &[OpTy<'tcx, Provenance>]) -> InterpResult<'tcx, i32> {
-        let this = self.eval_context_mut();
-
-        if args.len() < 2 {
-            throw_ub_format!(
-                "incorrect number of arguments for fcntl: got {}, expected at least 2",
-                args.len()
-            );
-        }
-        let fd = this.read_scalar(&args[0])?.to_i32()?;
-        let cmd = this.read_scalar(&args[1])?.to_i32()?;
-
-        // We only support getting the flags for a descriptor.
-        if cmd == this.eval_libc_i32("F_GETFD") {
-            // Currently this is the only flag that `F_GETFD` returns. It is OK to just return the
-            // `FD_CLOEXEC` value without checking if the flag is set for the file because `std`
-            // always sets this flag when opening a file. However we still need to check that the
-            // file itself is open.
-            if this.machine.file_handler.handles.contains_key(&fd) {
-                Ok(this.eval_libc_i32("FD_CLOEXEC"))
-            } else {
-                this.handle_not_found()
-            }
-        } else if cmd == this.eval_libc_i32("F_DUPFD")
-            || cmd == this.eval_libc_i32("F_DUPFD_CLOEXEC")
-        {
-            // Note that we always assume the FD_CLOEXEC flag is set for every open file, in part
-            // because exec() isn't supported. The F_DUPFD and F_DUPFD_CLOEXEC commands only
-            // differ in whether the FD_CLOEXEC flag is pre-set on the new file descriptor,
-            // thus they can share the same implementation here.
-            if args.len() < 3 {
-                throw_ub_format!(
-                    "incorrect number of arguments for fcntl with cmd=`F_DUPFD`/`F_DUPFD_CLOEXEC`: got {}, expected at least 3",
-                    args.len()
-                );
-            }
-            let start = this.read_scalar(&args[2])?.to_i32()?;
-
-            let fh = &mut this.machine.file_handler;
-
-            match fh.handles.get_mut(&fd) {
-                Some(file_descriptor) => {
-                    let dup_result = file_descriptor.dup();
-                    match dup_result {
-                        Ok(dup_fd) => Ok(fh.insert_fd_with_min_fd(dup_fd, start)),
-                        Err(e) => {
-                            this.set_last_error_from_io_error(e.kind())?;
-                            Ok(-1)
-                        }
-                    }
-                }
-                None => this.handle_not_found(),
-            }
-        } else if this.tcx.sess.target.os == "macos" && cmd == this.eval_libc_i32("F_FULLFSYNC") {
-            // Reject if isolation is enabled.
-            if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
-                this.reject_in_isolation("`fcntl`", reject_with)?;
-                this.set_last_error_from_io_error(ErrorKind::PermissionDenied)?;
-                return Ok(-1);
-            }
-
-            if let Some(file_descriptor) = this.machine.file_handler.handles.get(&fd) {
-                // FIXME: Support fullfsync for all FDs
-                let FileHandle { file, writable } =
-                    file_descriptor.downcast_ref::<FileHandle>().ok_or_else(|| {
-                        err_unsup_format!(
-                            "`F_FULLFSYNC` is only supported on file-backed file descriptors"
-                        )
-                    })?;
-                let io_result = maybe_sync_file(file, *writable, File::sync_all);
-                this.try_unwrap_io_result(io_result)
-            } else {
-                this.handle_not_found()
-            }
-        } else {
-            throw_unsup_format!("the {:#x} command is not supported for `fcntl`)", cmd);
-        }
-    }
-
-    fn close(&mut self, fd_op: &OpTy<'tcx, Provenance>) -> InterpResult<'tcx, Scalar<Provenance>> {
-        let this = self.eval_context_mut();
-
-        let fd = this.read_scalar(fd_op)?.to_i32()?;
-
-        Ok(Scalar::from_i32(
-            if let Some(file_descriptor) = this.machine.file_handler.handles.remove(&fd) {
-                let result = file_descriptor.close(this.machine.communicate())?;
-                this.try_unwrap_io_result(result)?
-            } else {
-                this.handle_not_found()?
-            },
-        ))
-    }
-
-    /// Function used when a handle is not found inside `FileHandler`. It returns `Ok(-1)`and sets
-    /// the last OS error to `libc::EBADF` (invalid file descriptor). This function uses
-    /// `T: From<i32>` instead of `i32` directly because some fs functions return different integer
-    /// types (like `read`, that returns an `i64`).
-    fn handle_not_found<T: From<i32>>(&mut self) -> InterpResult<'tcx, T> {
-        let this = self.eval_context_mut();
-        let ebadf = this.eval_libc("EBADF");
-        this.set_last_error(ebadf)?;
-        Ok((-1).into())
-    }
-
-    fn read(
-        &mut self,
-        fd: i32,
-        buf: Pointer<Option<Provenance>>,
-        count: u64,
-    ) -> InterpResult<'tcx, i64> {
-        let this = self.eval_context_mut();
-
-        // Isolation check is done via `FileDescriptor` trait.
-
-        trace!("Reading from FD {}, size {}", fd, count);
-
-        // Check that the *entire* buffer is actually valid memory.
-        this.check_ptr_access(buf, Size::from_bytes(count), CheckInAllocMsg::MemoryAccessTest)?;
-
-        // We cap the number of read bytes to the largest value that we are able to fit in both the
-        // host's and target's `isize`. This saves us from having to handle overflows later.
-        let count = count
-            .min(u64::try_from(this.target_isize_max()).unwrap())
-            .min(u64::try_from(isize::MAX).unwrap());
-        let communicate = this.machine.communicate();
-
-        if let Some(file_descriptor) = this.machine.file_handler.handles.get_mut(&fd) {
-            trace!("read: FD mapped to {:?}", file_descriptor);
-            // We want to read at most `count` bytes. We are sure that `count` is not negative
-            // because it was a target's `usize`. Also we are sure that its smaller than
-            // `usize::MAX` because it is bounded by the host's `isize`.
-            let mut bytes = vec![0; usize::try_from(count).unwrap()];
-            // `File::read` never returns a value larger than `count`,
-            // so this cannot fail.
-            let result = file_descriptor
-                .read(communicate, &mut bytes, *this.tcx)?
-                .map(|c| i64::try_from(c).unwrap());
-
-            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)?;
-                    Ok(read_bytes)
-                }
-                Err(e) => {
-                    this.set_last_error_from_io_error(e.kind())?;
-                    Ok(-1)
-                }
-            }
-        } else {
-            trace!("read: FD not found");
-            this.handle_not_found()
-        }
-    }
-
-    fn write(
-        &mut self,
-        fd: i32,
-        buf: Pointer<Option<Provenance>>,
-        count: u64,
-    ) -> InterpResult<'tcx, i64> {
-        let this = self.eval_context_mut();
-
-        // Isolation check is done via `FileDescriptor` trait.
-
-        // Check that the *entire* buffer is actually valid memory.
-        this.check_ptr_access(buf, Size::from_bytes(count), CheckInAllocMsg::MemoryAccessTest)?;
-
-        // We cap the number of written bytes to the largest value that we are able to fit in both the
-        // host's and target's `isize`. This saves us from having to handle overflows later.
-        let count = count
-            .min(u64::try_from(this.target_isize_max()).unwrap())
-            .min(u64::try_from(isize::MAX).unwrap());
-        let communicate = this.machine.communicate();
-
-        if let Some(file_descriptor) = this.machine.file_handler.handles.get(&fd) {
-            let bytes = this.read_bytes_ptr_strip_provenance(buf, Size::from_bytes(count))?;
-            let result = file_descriptor
-                .write(communicate, bytes, *this.tcx)?
-                .map(|c| i64::try_from(c).unwrap());
-            this.try_unwrap_io_result(result)
-        } else {
-            this.handle_not_found()
-        }
-    }
-
     fn lseek64(
         &mut self,
         fd: i32,
@@ -832,16 +429,14 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
         };
 
         let communicate = this.machine.communicate();
-        Ok(Scalar::from_i64(
-            if let Some(file_descriptor) = this.machine.file_handler.handles.get_mut(&fd) {
-                let result = file_descriptor
-                    .seek(communicate, seek_from)?
-                    .map(|offset| i64::try_from(offset).unwrap());
-                this.try_unwrap_io_result(result)?
-            } else {
-                this.handle_not_found()?
-            },
-        ))
+        Ok(Scalar::from_i64(if let Some(file_descriptor) = this.machine.fds.get_mut(fd) {
+            let result = file_descriptor
+                .seek(communicate, seek_from)?
+                .map(|offset| i64::try_from(offset).unwrap());
+            this.try_unwrap_io_result(result)?
+        } else {
+            this.fd_not_found()?
+        }))
     }
 
     fn unlink(&mut self, path_op: &OpTy<'tcx, Provenance>) -> InterpResult<'tcx, i32> {
@@ -970,7 +565,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
         if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
             this.reject_in_isolation("`fstat`", reject_with)?;
             // Set error code as "EBADF" (bad fd)
-            return Ok(Scalar::from_i32(this.handle_not_found()?));
+            return Ok(Scalar::from_i32(this.fd_not_found()?));
         }
 
         let metadata = match FileMetadata::from_fd(this, fd)? {
@@ -1269,7 +864,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
 
         match result {
             Ok(dir_iter) => {
-                let id = this.machine.dir_handler.insert_new(dir_iter);
+                let id = this.machine.dirs.insert_new(dir_iter);
 
                 // The libc API for opendir says that this method returns a pointer to an opaque
                 // structure, but we are returning an ID number. Thus, pass it as a scalar of
@@ -1301,7 +896,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
             return Ok(Scalar::null_ptr(this));
         }
 
-        let open_dir = this.machine.dir_handler.streams.get_mut(&dirp).ok_or_else(|| {
+        let open_dir = this.machine.dirs.streams.get_mut(&dirp).ok_or_else(|| {
             err_unsup_format!("the DIR pointer passed to readdir64 did not come from opendir")
         })?;
 
@@ -1366,7 +961,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
             }
         };
 
-        let open_dir = this.machine.dir_handler.streams.get_mut(&dirp).unwrap();
+        let open_dir = this.machine.dirs.streams.get_mut(&dirp).unwrap();
         let old_entry = std::mem::replace(&mut open_dir.entry, entry);
         this.free(old_entry, MiriMemoryKind::Runtime)?;
 
@@ -1391,10 +986,10 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
         if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
             this.reject_in_isolation("`readdir_r`", reject_with)?;
             // Set error code as "EBADF" (bad fd)
-            return Ok(Scalar::from_i32(this.handle_not_found()?));
+            return Ok(Scalar::from_i32(this.fd_not_found()?));
         }
 
-        let open_dir = this.machine.dir_handler.streams.get_mut(&dirp).ok_or_else(|| {
+        let open_dir = this.machine.dirs.streams.get_mut(&dirp).ok_or_else(|| {
             err_unsup_format!("the DIR pointer passed to readdir_r did not come from opendir")
         })?;
         Ok(Scalar::from_i32(match open_dir.read_dir.next() {
@@ -1507,15 +1102,15 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
         if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
             this.reject_in_isolation("`closedir`", reject_with)?;
             // Set error code as "EBADF" (bad fd)
-            return this.handle_not_found();
+            return this.fd_not_found();
         }
 
-        if let Some(open_dir) = this.machine.dir_handler.streams.remove(&dirp) {
+        if let Some(open_dir) = this.machine.dirs.streams.remove(&dirp) {
             this.free(open_dir.entry, MiriMemoryKind::Runtime)?;
             drop(open_dir);
             Ok(0)
         } else {
-            this.handle_not_found()
+            this.fd_not_found()
         }
     }
 
@@ -1526,37 +1121,35 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
         if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
             this.reject_in_isolation("`ftruncate64`", reject_with)?;
             // Set error code as "EBADF" (bad fd)
-            return Ok(Scalar::from_i32(this.handle_not_found()?));
+            return Ok(Scalar::from_i32(this.fd_not_found()?));
         }
 
-        Ok(Scalar::from_i32(
-            if let Some(file_descriptor) = this.machine.file_handler.handles.get_mut(&fd) {
-                // FIXME: Support ftruncate64 for all FDs
-                let FileHandle { file, writable } =
-                    file_descriptor.downcast_ref::<FileHandle>().ok_or_else(|| {
-                        err_unsup_format!(
-                            "`ftruncate64` is only supported on file-backed file descriptors"
-                        )
-                    })?;
-                if *writable {
-                    if let Ok(length) = length.try_into() {
-                        let result = file.set_len(length);
-                        this.try_unwrap_io_result(result.map(|_| 0i32))?
-                    } else {
-                        let einval = this.eval_libc("EINVAL");
-                        this.set_last_error(einval)?;
-                        -1
-                    }
+        Ok(Scalar::from_i32(if let Some(file_descriptor) = this.machine.fds.get_mut(fd) {
+            // FIXME: Support ftruncate64 for all FDs
+            let FileHandle { file, writable } =
+                file_descriptor.downcast_ref::<FileHandle>().ok_or_else(|| {
+                    err_unsup_format!(
+                        "`ftruncate64` is only supported on file-backed file descriptors"
+                    )
+                })?;
+            if *writable {
+                if let Ok(length) = length.try_into() {
+                    let result = file.set_len(length);
+                    this.try_unwrap_io_result(result.map(|_| 0i32))?
                 } else {
-                    // The file is not writable
                     let einval = this.eval_libc("EINVAL");
                     this.set_last_error(einval)?;
                     -1
                 }
             } else {
-                this.handle_not_found()?
-            },
-        ))
+                // The file is not writable
+                let einval = this.eval_libc("EINVAL");
+                this.set_last_error(einval)?;
+                -1
+            }
+        } else {
+            this.fd_not_found()?
+        }))
     }
 
     fn fsync(&mut self, fd_op: &OpTy<'tcx, Provenance>) -> InterpResult<'tcx, i32> {
@@ -1573,20 +1166,24 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
         if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
             this.reject_in_isolation("`fsync`", reject_with)?;
             // Set error code as "EBADF" (bad fd)
-            return this.handle_not_found();
+            return this.fd_not_found();
         }
 
-        if let Some(file_descriptor) = this.machine.file_handler.handles.get(&fd) {
-            // FIXME: Support fsync for all FDs
-            let FileHandle { file, writable } =
-                file_descriptor.downcast_ref::<FileHandle>().ok_or_else(|| {
-                    err_unsup_format!("`fsync` is only supported on file-backed file descriptors")
-                })?;
-            let io_result = maybe_sync_file(file, *writable, File::sync_all);
-            this.try_unwrap_io_result(io_result)
-        } else {
-            this.handle_not_found()
-        }
+        return self.ffullsync_fd(fd);
+    }
+
+    fn ffullsync_fd(&mut self, fd: i32) -> InterpResult<'tcx, i32> {
+        let this = self.eval_context_mut();
+        let Some(file_descriptor) = this.machine.fds.get(fd) else {
+            return Ok(this.fd_not_found()?);
+        };
+        // Only regular files support synchronization.
+        let FileHandle { file, writable } =
+            file_descriptor.downcast_ref::<FileHandle>().ok_or_else(|| {
+                err_unsup_format!("`fsync` is only supported on file-backed file descriptors")
+            })?;
+        let io_result = maybe_sync_file(file, *writable, File::sync_all);
+        this.try_unwrap_io_result(io_result)
     }
 
     fn fdatasync(&mut self, fd_op: &OpTy<'tcx, Provenance>) -> InterpResult<'tcx, i32> {
@@ -1598,22 +1195,19 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
         if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
             this.reject_in_isolation("`fdatasync`", reject_with)?;
             // Set error code as "EBADF" (bad fd)
-            return this.handle_not_found();
+            return this.fd_not_found();
         }
 
-        if let Some(file_descriptor) = this.machine.file_handler.handles.get(&fd) {
-            // FIXME: Support fdatasync for all FDs
-            let FileHandle { file, writable } =
-                file_descriptor.downcast_ref::<FileHandle>().ok_or_else(|| {
-                    err_unsup_format!(
-                        "`fdatasync` is only supported on file-backed file descriptors"
-                    )
-                })?;
-            let io_result = maybe_sync_file(file, *writable, File::sync_data);
-            this.try_unwrap_io_result(io_result)
-        } else {
-            this.handle_not_found()
-        }
+        let Some(file_descriptor) = this.machine.fds.get(fd) else {
+            return Ok(this.fd_not_found()?);
+        };
+        // Only regular files support synchronization.
+        let FileHandle { file, writable } =
+            file_descriptor.downcast_ref::<FileHandle>().ok_or_else(|| {
+                err_unsup_format!("`fdatasync` is only supported on file-backed file descriptors")
+            })?;
+        let io_result = maybe_sync_file(file, *writable, File::sync_data);
+        this.try_unwrap_io_result(io_result)
     }
 
     fn sync_file_range(
@@ -1648,22 +1242,21 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
         if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
             this.reject_in_isolation("`sync_file_range`", reject_with)?;
             // Set error code as "EBADF" (bad fd)
-            return Ok(Scalar::from_i32(this.handle_not_found()?));
+            return Ok(Scalar::from_i32(this.fd_not_found()?));
         }
 
-        if let Some(file_descriptor) = this.machine.file_handler.handles.get(&fd) {
-            // FIXME: Support sync_data_range for all FDs
-            let FileHandle { file, writable } =
-                file_descriptor.downcast_ref::<FileHandle>().ok_or_else(|| {
-                    err_unsup_format!(
-                        "`sync_data_range` is only supported on file-backed file descriptors"
-                    )
-                })?;
-            let io_result = maybe_sync_file(file, *writable, File::sync_data);
-            Ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
-        } else {
-            Ok(Scalar::from_i32(this.handle_not_found()?))
-        }
+        let Some(file_descriptor) = this.machine.fds.get(fd) else {
+            return Ok(Scalar::from_i32(this.fd_not_found()?));
+        };
+        // Only regular files support synchronization.
+        let FileHandle { file, writable } =
+            file_descriptor.downcast_ref::<FileHandle>().ok_or_else(|| {
+                err_unsup_format!(
+                    "`sync_data_range` is only supported on file-backed file descriptors"
+                )
+            })?;
+        let io_result = maybe_sync_file(file, *writable, File::sync_data);
+        Ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
     }
 
     fn readlink(
@@ -1720,7 +1313,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
         // "returns 1 if fd is an open file descriptor referring to a terminal;
         // otherwise 0 is returned, and errno is set to indicate the error"
         let fd = this.read_scalar(miri_fd)?.to_i32()?;
-        let error = if let Some(fd) = this.machine.file_handler.handles.get(&fd) {
+        let error = if let Some(fd) = this.machine.fds.get(fd) {
             if fd.is_tty(this.machine.communicate()) {
                 return Ok(Scalar::from_i32(1));
             } else {
@@ -1897,7 +1490,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
 
             match file {
                 Ok(f) => {
-                    let fh = &mut this.machine.file_handler;
+                    let fh = &mut this.machine.fds;
                     let fd = fh.insert_fd(Box::new(FileHandle { file: f, writable: true }));
                     return Ok(fd);
                 }
@@ -1963,7 +1556,7 @@ impl FileMetadata {
         ecx: &mut MiriInterpCx<'_, 'tcx>,
         fd: i32,
     ) -> InterpResult<'tcx, Option<FileMetadata>> {
-        let option = ecx.machine.file_handler.handles.get(&fd);
+        let option = ecx.machine.fds.get(fd);
         let file = match option {
             Some(file_descriptor) =>
                 &file_descriptor
@@ -1974,7 +1567,7 @@ impl FileMetadata {
                         )
                     })?
                     .file,
-            None => return ecx.handle_not_found().map(|_: i32| None),
+            None => return ecx.fd_not_found().map(|_: i32| None),
         };
         let metadata = file.metadata();
 
diff --git a/src/tools/miri/src/shims/unix/linux/fd.rs b/src/tools/miri/src/shims/unix/linux/epoll.rs
index 22fbb6da95a..5161d91ca36 100644
--- a/src/tools/miri/src/shims/unix/linux/fd.rs
+++ b/src/tools/miri/src/shims/unix/linux/epoll.rs
@@ -1,17 +1,52 @@
-use std::cell::Cell;
+use std::io;
 
-use rustc_middle::ty::ScalarInt;
+use rustc_data_structures::fx::FxHashMap;
 
+use crate::shims::unix::*;
 use crate::*;
-use epoll::{Epoll, EpollEvent};
-use event::Event;
-use socketpair::SocketPair;
 
-use shims::unix::fs::EvalContextExt as _;
+/// An `Epoll` file descriptor connects file handles and epoll events
+#[derive(Clone, Debug, Default)]
+struct Epoll {
+    /// The file descriptors we are watching, and what we are watching for.
+    file_descriptors: FxHashMap<i32, EpollEvent>,
+}
+
+/// Epoll Events associate events with data.
+/// These fields are currently unused by miri.
+/// This matches the `epoll_event` struct defined
+/// by the epoll_ctl man page. For more information
+/// see the man page:
+///
+/// <https://man7.org/linux/man-pages/man2/epoll_ctl.2.html>
+#[derive(Clone, Debug)]
+struct EpollEvent {
+    #[allow(dead_code)]
+    events: u32,
+    /// `Scalar<Provenance>` is used to represent the
+    /// `epoll_data` type union.
+    #[allow(dead_code)]
+    data: Scalar<Provenance>,
+}
+
+impl FileDescriptor for Epoll {
+    fn name(&self) -> &'static str {
+        "epoll"
+    }
 
-pub mod epoll;
-pub mod event;
-pub mod socketpair;
+    fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>> {
+        // FIXME: this is probably wrong -- check if the `dup`ed descriptor truly uses an
+        // independent event set.
+        Ok(Box::new(self.clone()))
+    }
+
+    fn close<'tcx>(
+        self: Box<Self>,
+        _communicate_allowed: bool,
+    ) -> InterpResult<'tcx, io::Result<i32>> {
+        Ok(Ok(0))
+    }
+}
 
 impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {}
 pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
@@ -35,7 +70,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
             throw_unsup_format!("epoll_create1 flags {flags} are not implemented");
         }
 
-        let fd = this.machine.file_handler.insert_fd(Box::new(Epoll::default()));
+        let fd = this.machine.fds.insert_fd(Box::new(Epoll::default()));
         Ok(Scalar::from_i32(fd))
     }
 
@@ -79,7 +114,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
             let data = this.read_scalar(&data)?;
             let event = EpollEvent { events, data };
 
-            if let Some(epfd) = this.machine.file_handler.handles.get_mut(&epfd) {
+            if let Some(epfd) = this.machine.fds.get_mut(epfd) {
                 let epfd = epfd
                     .downcast_mut::<Epoll>()
                     .ok_or_else(|| err_unsup_format!("non-epoll FD passed to `epoll_ctl`"))?;
@@ -87,10 +122,10 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
                 epfd.file_descriptors.insert(fd, event);
                 Ok(Scalar::from_i32(0))
             } else {
-                Ok(Scalar::from_i32(this.handle_not_found()?))
+                Ok(Scalar::from_i32(this.fd_not_found()?))
             }
         } else if op == epoll_ctl_del {
-            if let Some(epfd) = this.machine.file_handler.handles.get_mut(&epfd) {
+            if let Some(epfd) = this.machine.fds.get_mut(epfd) {
                 let epfd = epfd
                     .downcast_mut::<Epoll>()
                     .ok_or_else(|| err_unsup_format!("non-epoll FD passed to `epoll_ctl`"))?;
@@ -98,7 +133,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
                 epfd.file_descriptors.remove(&fd);
                 Ok(Scalar::from_i32(0))
             } else {
-                Ok(Scalar::from_i32(this.handle_not_found()?))
+                Ok(Scalar::from_i32(this.fd_not_found()?))
             }
         } else {
             let einval = this.eval_libc("EINVAL");
@@ -150,7 +185,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
         let _maxevents = this.read_scalar(maxevents)?.to_i32()?;
         let _timeout = this.read_scalar(timeout)?.to_i32()?;
 
-        if let Some(epfd) = this.machine.file_handler.handles.get_mut(&epfd) {
+        if let Some(epfd) = this.machine.fds.get_mut(epfd) {
             let _epfd = epfd
                 .downcast_mut::<Epoll>()
                 .ok_or_else(|| err_unsup_format!("non-epoll FD passed to `epoll_wait`"))?;
@@ -158,96 +193,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
             // FIXME return number of events ready when scheme for marking events ready exists
             throw_unsup_format!("returning ready events from epoll_wait is not yet implemented");
         } else {
-            Ok(Scalar::from_i32(this.handle_not_found()?))
+            Ok(Scalar::from_i32(this.fd_not_found()?))
         }
     }
-
-    /// This function creates an `Event` that is used as an event wait/notify mechanism by
-    /// user-space applications, and by the kernel to notify user-space applications of events.
-    /// The `Event` contains an `u64` counter maintained by the kernel. The counter is initialized
-    /// with the value specified in the `initval` argument.
-    ///
-    /// A new file descriptor referring to the `Event` is returned. The `read`, `write`, `poll`,
-    /// `select`, and `close` operations can be performed on the file descriptor. For more
-    /// information on these operations, see the man page linked below.
-    ///
-    /// The `flags` are not currently implemented for eventfd.
-    /// The `flags` may be bitwise ORed to change the behavior of `eventfd`:
-    /// `EFD_CLOEXEC` - Set the close-on-exec (`FD_CLOEXEC`) flag on the new file descriptor.
-    /// `EFD_NONBLOCK` - Set the `O_NONBLOCK` file status flag on the new open file description.
-    /// `EFD_SEMAPHORE` - miri does not support semaphore-like semantics.
-    ///
-    /// <https://linux.die.net/man/2/eventfd>
-    #[expect(clippy::needless_if)]
-    fn eventfd(
-        &mut self,
-        val: &OpTy<'tcx, Provenance>,
-        flags: &OpTy<'tcx, Provenance>,
-    ) -> InterpResult<'tcx, Scalar<Provenance>> {
-        let this = self.eval_context_mut();
-
-        let val = this.read_scalar(val)?.to_u32()?;
-        let flags = this.read_scalar(flags)?.to_i32()?;
-
-        let efd_cloexec = this.eval_libc_i32("EFD_CLOEXEC");
-        let efd_nonblock = this.eval_libc_i32("EFD_NONBLOCK");
-        let efd_semaphore = this.eval_libc_i32("EFD_SEMAPHORE");
-
-        if flags & (efd_cloexec | efd_nonblock | efd_semaphore) == 0 {
-            throw_unsup_format!("{flags} is unsupported");
-        }
-        // FIXME handle the cloexec and nonblock flags
-        if flags & efd_cloexec == efd_cloexec {}
-        if flags & efd_nonblock == efd_nonblock {}
-        if flags & efd_semaphore == efd_semaphore {
-            throw_unsup_format!("EFD_SEMAPHORE is unsupported");
-        }
-
-        let fh = &mut this.machine.file_handler;
-        let fd = fh.insert_fd(Box::new(Event { val: Cell::new(val.into()) }));
-        Ok(Scalar::from_i32(fd))
-    }
-
-    /// Currently this function creates new `SocketPair`s without specifying the domain, type, or
-    /// protocol of the new socket and these are stored in the socket values `sv` argument.
-    ///
-    /// This function creates an unnamed pair of connected sockets in the specified domain, of the
-    /// specified type, and using the optionally specified protocol.
-    ///
-    /// The `domain` argument specified a communication domain; this selects the protocol family
-    /// used for communication. The socket `type` specifies the communication semantics.
-    /// The `protocol` specifies a particular protocol to use with the socket. Normally there's
-    /// only a single protocol supported for a particular socket type within a given protocol
-    /// family, in which case `protocol` can be specified as 0. It is possible that many protocols
-    /// exist and in that case, a particular protocol must be specified.
-    ///
-    /// For more information on the arguments see the socket manpage:
-    /// <https://linux.die.net/man/2/socket>
-    ///
-    /// <https://linux.die.net/man/2/socketpair>
-    fn socketpair(
-        &mut self,
-        domain: &OpTy<'tcx, Provenance>,
-        type_: &OpTy<'tcx, Provenance>,
-        protocol: &OpTy<'tcx, Provenance>,
-        sv: &OpTy<'tcx, Provenance>,
-    ) -> InterpResult<'tcx, Scalar<Provenance>> {
-        let this = self.eval_context_mut();
-
-        let _domain = this.read_scalar(domain)?.to_i32()?;
-        let _type_ = this.read_scalar(type_)?.to_i32()?;
-        let _protocol = this.read_scalar(protocol)?.to_i32()?;
-        let sv = this.deref_pointer(sv)?;
-
-        let fh = &mut this.machine.file_handler;
-        let sv0 = fh.insert_fd(Box::new(SocketPair));
-        let sv0 = ScalarInt::try_from_int(sv0, sv.layout.size).unwrap();
-        let sv1 = fh.insert_fd(Box::new(SocketPair));
-        let sv1 = ScalarInt::try_from_int(sv1, sv.layout.size).unwrap();
-
-        this.write_scalar(sv0, &sv)?;
-        this.write_scalar(sv1, &sv.offset(sv.layout.size, sv.layout, this)?)?;
-
-        Ok(Scalar::from_i32(0))
-    }
 }
diff --git a/src/tools/miri/src/shims/unix/linux/eventfd.rs b/src/tools/miri/src/shims/unix/linux/eventfd.rs
new file mode 100644
index 00000000000..0f28b69ac4a
--- /dev/null
+++ b/src/tools/miri/src/shims/unix/linux/eventfd.rs
@@ -0,0 +1,125 @@
+//! Linux `eventfd` implementation.
+//! Currently just a stub.
+use std::cell::Cell;
+use std::io;
+
+use rustc_middle::ty::TyCtxt;
+use rustc_target::abi::Endian;
+
+use crate::shims::unix::*;
+use crate::*;
+
+/// A kind of file descriptor created by `eventfd`.
+/// The `Event` type isn't currently written to by `eventfd`.
+/// The interface is meant to keep track of objects associated
+/// with a file descriptor. For more information see the man
+/// page below:
+///
+/// <https://man.netbsd.org/eventfd.2>
+#[derive(Debug)]
+struct Event {
+    /// The object contains an unsigned 64-bit integer (uint64_t) counter that is maintained by the
+    /// kernel. This counter is initialized with the value specified in the argument initval.
+    val: Cell<u64>,
+}
+
+impl FileDescriptor for Event {
+    fn name(&self) -> &'static str {
+        "event"
+    }
+
+    fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>> {
+        // FIXME: this is wrong, the new and old FD should refer to the same event object!
+        Ok(Box::new(Event { val: self.val.clone() }))
+    }
+
+    fn close<'tcx>(
+        self: Box<Self>,
+        _communicate_allowed: bool,
+    ) -> InterpResult<'tcx, io::Result<i32>> {
+        Ok(Ok(0))
+    }
+
+    /// A write call adds the 8-byte integer value supplied in
+    /// its buffer (in native endianness) to the counter.  The maximum value that may be
+    /// stored in the counter is the largest unsigned 64-bit value
+    /// minus 1 (i.e., 0xfffffffffffffffe).  If the addition would
+    /// cause the counter's value to exceed the maximum, then the
+    /// write either blocks until a read is performed on the
+    /// file descriptor, or fails with the error EAGAIN if the
+    /// file descriptor has been made nonblocking.
+
+    /// A write fails with the error EINVAL if the size of the
+    /// supplied buffer is less than 8 bytes, or if an attempt is
+    /// made to write the value 0xffffffffffffffff.
+    fn write<'tcx>(
+        &self,
+        _communicate_allowed: bool,
+        bytes: &[u8],
+        tcx: TyCtxt<'tcx>,
+    ) -> InterpResult<'tcx, io::Result<usize>> {
+        let v1 = self.val.get();
+        let bytes: [u8; 8] = bytes.try_into().unwrap(); // FIXME fail gracefully when this has the wrong size
+        // Convert from target endianness to host endianness.
+        let num = match tcx.sess.target.endian {
+            Endian::Little => u64::from_le_bytes(bytes),
+            Endian::Big => u64::from_be_bytes(bytes),
+        };
+        // FIXME handle blocking when addition results in exceeding the max u64 value
+        // or fail with EAGAIN if the file descriptor is nonblocking.
+        let v2 = v1.checked_add(num).unwrap();
+        self.val.set(v2);
+        assert_eq!(8, bytes.len());
+        Ok(Ok(8))
+    }
+}
+
+impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {}
+pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
+    /// This function creates an `Event` that is used as an event wait/notify mechanism by
+    /// user-space applications, and by the kernel to notify user-space applications of events.
+    /// The `Event` contains an `u64` counter maintained by the kernel. The counter is initialized
+    /// with the value specified in the `initval` argument.
+    ///
+    /// A new file descriptor referring to the `Event` is returned. The `read`, `write`, `poll`,
+    /// `select`, and `close` operations can be performed on the file descriptor. For more
+    /// information on these operations, see the man page linked below.
+    ///
+    /// The `flags` are not currently implemented for eventfd.
+    /// The `flags` may be bitwise ORed to change the behavior of `eventfd`:
+    /// `EFD_CLOEXEC` - Set the close-on-exec (`FD_CLOEXEC`) flag on the new file descriptor.
+    /// `EFD_NONBLOCK` - Set the `O_NONBLOCK` file status flag on the new open file description.
+    /// `EFD_SEMAPHORE` - miri does not support semaphore-like semantics.
+    ///
+    /// <https://linux.die.net/man/2/eventfd>
+    fn eventfd(
+        &mut self,
+        val: &OpTy<'tcx, Provenance>,
+        flags: &OpTy<'tcx, Provenance>,
+    ) -> InterpResult<'tcx, Scalar<Provenance>> {
+        let this = self.eval_context_mut();
+
+        let val = this.read_scalar(val)?.to_u32()?;
+        let flags = this.read_scalar(flags)?.to_i32()?;
+
+        let efd_cloexec = this.eval_libc_i32("EFD_CLOEXEC");
+        let efd_nonblock = this.eval_libc_i32("EFD_NONBLOCK");
+        let efd_semaphore = this.eval_libc_i32("EFD_SEMAPHORE");
+
+        if flags & (efd_cloexec | efd_nonblock | efd_semaphore) != flags {
+            throw_unsup_format!("eventfd: flag {flags:#x} is unsupported");
+        }
+        if flags & efd_cloexec == efd_cloexec {
+            // cloexec does nothing as we don't support `exec`
+        }
+        if flags & efd_nonblock == efd_nonblock {
+            // FIXME remember the nonblock flag
+        }
+        if flags & efd_semaphore == efd_semaphore {
+            throw_unsup_format!("eventfd: EFD_SEMAPHORE is unsupported");
+        }
+
+        let fd = this.machine.fds.insert_fd(Box::new(Event { val: Cell::new(val.into()) }));
+        Ok(Scalar::from_i32(fd))
+    }
+}
diff --git a/src/tools/miri/src/shims/unix/linux/fd/epoll.rs b/src/tools/miri/src/shims/unix/linux/fd/epoll.rs
deleted file mode 100644
index 8c5aed6def6..00000000000
--- a/src/tools/miri/src/shims/unix/linux/fd/epoll.rs
+++ /dev/null
@@ -1,47 +0,0 @@
-use crate::*;
-
-use crate::shims::unix::fs::FileDescriptor;
-
-use rustc_data_structures::fx::FxHashMap;
-use std::io;
-
-/// An `Epoll` file descriptor connects file handles and epoll events
-#[derive(Clone, Debug, Default)]
-pub struct Epoll {
-    /// The file descriptors we are watching, and what we are watching for.
-    pub file_descriptors: FxHashMap<i32, EpollEvent>,
-}
-
-/// Epoll Events associate events with data.
-/// These fields are currently unused by miri.
-/// This matches the `epoll_event` struct defined
-/// by the epoll_ctl man page. For more information
-/// see the man page:
-///
-/// <https://man7.org/linux/man-pages/man2/epoll_ctl.2.html>
-#[derive(Clone, Debug)]
-pub struct EpollEvent {
-    #[allow(dead_code)]
-    pub events: u32,
-    /// `Scalar<Provenance>` is used to represent the
-    /// `epoll_data` type union.
-    #[allow(dead_code)]
-    pub data: Scalar<Provenance>,
-}
-
-impl FileDescriptor for Epoll {
-    fn name(&self) -> &'static str {
-        "epoll"
-    }
-
-    fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>> {
-        Ok(Box::new(self.clone()))
-    }
-
-    fn close<'tcx>(
-        self: Box<Self>,
-        _communicate_allowed: bool,
-    ) -> InterpResult<'tcx, io::Result<i32>> {
-        Ok(Ok(0))
-    }
-}
diff --git a/src/tools/miri/src/shims/unix/linux/fd/event.rs b/src/tools/miri/src/shims/unix/linux/fd/event.rs
deleted file mode 100644
index 49408fda3ae..00000000000
--- a/src/tools/miri/src/shims/unix/linux/fd/event.rs
+++ /dev/null
@@ -1,72 +0,0 @@
-use crate::shims::unix::fs::FileDescriptor;
-
-use rustc_const_eval::interpret::InterpResult;
-use rustc_middle::ty::TyCtxt;
-use rustc_target::abi::Endian;
-
-use std::cell::Cell;
-use std::io;
-
-/// A kind of file descriptor created by `eventfd`.
-/// The `Event` type isn't currently written to by `eventfd`.
-/// The interface is meant to keep track of objects associated
-/// with a file descriptor. For more information see the man
-/// page below:
-///
-/// <https://man.netbsd.org/eventfd.2>
-#[derive(Debug)]
-pub struct Event {
-    /// The object contains an unsigned 64-bit integer (uint64_t) counter that is maintained by the
-    /// kernel. This counter is initialized with the value specified in the argument initval.
-    pub val: Cell<u64>,
-}
-
-impl FileDescriptor for Event {
-    fn name(&self) -> &'static str {
-        "event"
-    }
-
-    fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>> {
-        Ok(Box::new(Event { val: self.val.clone() }))
-    }
-
-    fn close<'tcx>(
-        self: Box<Self>,
-        _communicate_allowed: bool,
-    ) -> InterpResult<'tcx, io::Result<i32>> {
-        Ok(Ok(0))
-    }
-
-    /// A write call adds the 8-byte integer value supplied in
-    /// its buffer (in native endianness) to the counter.  The maximum value that may be
-    /// stored in the counter is the largest unsigned 64-bit value
-    /// minus 1 (i.e., 0xfffffffffffffffe).  If the addition would
-    /// cause the counter's value to exceed the maximum, then the
-    /// write either blocks until a read is performed on the
-    /// file descriptor, or fails with the error EAGAIN if the
-    /// file descriptor has been made nonblocking.
-
-    /// A write fails with the error EINVAL if the size of the
-    /// supplied buffer is less than 8 bytes, or if an attempt is
-    /// made to write the value 0xffffffffffffffff.
-    fn write<'tcx>(
-        &self,
-        _communicate_allowed: bool,
-        bytes: &[u8],
-        tcx: TyCtxt<'tcx>,
-    ) -> InterpResult<'tcx, io::Result<usize>> {
-        let v1 = self.val.get();
-        let bytes: [u8; 8] = bytes.try_into().unwrap(); // FIXME fail gracefully when this has the wrong size
-        // Convert from target endianness to host endianness.
-        let num = match tcx.sess.target.endian {
-            Endian::Little => u64::from_le_bytes(bytes),
-            Endian::Big => u64::from_be_bytes(bytes),
-        };
-        // FIXME handle blocking when addition results in exceeding the max u64 value
-        // or fail with EAGAIN if the file descriptor is nonblocking.
-        let v2 = v1.checked_add(num).unwrap();
-        self.val.set(v2);
-        assert_eq!(8, bytes.len());
-        Ok(Ok(8))
-    }
-}
diff --git a/src/tools/miri/src/shims/unix/linux/fd/socketpair.rs b/src/tools/miri/src/shims/unix/linux/fd/socketpair.rs
deleted file mode 100644
index 6adae88235f..00000000000
--- a/src/tools/miri/src/shims/unix/linux/fd/socketpair.rs
+++ /dev/null
@@ -1,28 +0,0 @@
-use crate::*;
-
-use crate::shims::unix::fs::FileDescriptor;
-
-use std::io;
-
-/// Pair of connected sockets.
-///
-/// We currently don't allow sending any data through this pair, so this can be just a dummy.
-#[derive(Debug)]
-pub struct SocketPair;
-
-impl FileDescriptor for SocketPair {
-    fn name(&self) -> &'static str {
-        "socketpair"
-    }
-
-    fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>> {
-        Ok(Box::new(SocketPair))
-    }
-
-    fn close<'tcx>(
-        self: Box<Self>,
-        _communicate_allowed: bool,
-    ) -> InterpResult<'tcx, io::Result<i32>> {
-        Ok(Ok(0))
-    }
-}
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 d13ada0f4cf..497e8bee70e 100644
--- a/src/tools/miri/src/shims/unix/linux/foreign_items.rs
+++ b/src/tools/miri/src/shims/unix/linux/foreign_items.rs
@@ -3,15 +3,13 @@ use rustc_target::spec::abi::Abi;
 
 use crate::machine::SIGRTMAX;
 use crate::machine::SIGRTMIN;
+use crate::shims::unix::*;
 use crate::*;
 use shims::foreign_items::EmulateForeignItemResult;
-use shims::unix::fs::EvalContextExt as _;
-use shims::unix::linux::fd::EvalContextExt as _;
+use shims::unix::linux::epoll::EvalContextExt as _;
+use shims::unix::linux::eventfd::EvalContextExt as _;
 use shims::unix::linux::mem::EvalContextExt as _;
 use shims::unix::linux::sync::futex;
-use shims::unix::mem::EvalContextExt as _;
-use shims::unix::sync::EvalContextExt as _;
-use shims::unix::thread::EvalContextExt as _;
 
 pub fn is_dyn_sym(name: &str) -> bool {
     matches!(name, "getrandom")
@@ -31,34 +29,20 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
         // See `fn emulate_foreign_item_inner` in `shims/foreign_items.rs` for the general pattern.
 
         match link_name.as_str() {
-            // errno
-            "__errno_location" => {
-                let [] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
-                let errno_place = this.last_error_place()?;
-                this.write_scalar(errno_place.to_ref(this).to_scalar(), dest)?;
-            }
-
             // File related shims (but also see "syscall" below for statx)
             "readdir64" => {
                 let [dirp] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
                 let result = this.linux_readdir64(dirp)?;
                 this.write_scalar(result, dest)?;
             }
-            "mmap64" => {
-                let [addr, length, prot, flags, fd, offset] =
-                    this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
-                let offset = this.read_scalar(offset)?.to_i64()?;
-                let ptr = this.mmap(addr, length, prot, flags, fd, offset.into())?;
-                this.write_scalar(ptr, dest)?;
-            }
-
-            // Linux-only
             "sync_file_range" => {
                 let [fd, offset, nbytes, flags] =
                     this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
                 let result = this.sync_file_range(fd, offset, nbytes, flags)?;
                 this.write_scalar(result, dest)?;
             }
+
+            // epoll, eventfd
             "epoll_create1" => {
                 let [flag] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
                 let result = this.epoll_create1(flag)?;
@@ -82,29 +66,6 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
                 let result = this.eventfd(val, flag)?;
                 this.write_scalar(result, dest)?;
             }
-            "mremap" => {
-                let [old_address, old_size, new_size, flags] =
-                    this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
-                let ptr = this.mremap(old_address, old_size, new_size, flags)?;
-                this.write_scalar(ptr, dest)?;
-            }
-            "socketpair" => {
-                let [domain, type_, protocol, sv] =
-                    this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
-
-                let result = this.socketpair(domain, type_, protocol, sv)?;
-                this.write_scalar(result, dest)?;
-            }
-            "__libc_current_sigrtmin" => {
-                let [] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
-
-                this.write_scalar(Scalar::from_i32(SIGRTMIN), dest)?;
-            }
-            "__libc_current_sigrtmax" => {
-                let [] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
-
-                this.write_scalar(Scalar::from_i32(SIGRTMAX), dest)?;
-            }
 
             // Threading
             "pthread_condattr_setclock" => {
@@ -206,6 +167,34 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
                     this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
                 getrandom(this, ptr, len, flags, dest)?;
             }
+            "mmap64" => {
+                let [addr, length, prot, flags, fd, offset] =
+                    this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
+                let offset = this.read_scalar(offset)?.to_i64()?;
+                let ptr = this.mmap(addr, length, prot, flags, fd, offset.into())?;
+                this.write_scalar(ptr, dest)?;
+            }
+            "mremap" => {
+                let [old_address, old_size, new_size, flags] =
+                    this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
+                let ptr = this.mremap(old_address, old_size, new_size, flags)?;
+                this.write_scalar(ptr, dest)?;
+            }
+            "__errno_location" => {
+                let [] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
+                let errno_place = this.last_error_place()?;
+                this.write_scalar(errno_place.to_ref(this).to_scalar(), dest)?;
+            }
+            "__libc_current_sigrtmin" => {
+                let [] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
+
+                this.write_scalar(Scalar::from_i32(SIGRTMIN), dest)?;
+            }
+            "__libc_current_sigrtmax" => {
+                let [] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
+
+                this.write_scalar(Scalar::from_i32(SIGRTMAX), 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/linux/mod.rs b/src/tools/miri/src/shims/unix/linux/mod.rs
index fe18f1a32fd..84b604eb9b8 100644
--- a/src/tools/miri/src/shims/unix/linux/mod.rs
+++ b/src/tools/miri/src/shims/unix/linux/mod.rs
@@ -1,4 +1,5 @@
-pub mod fd;
+pub mod epoll;
+pub mod eventfd;
 pub mod foreign_items;
 pub mod mem;
 pub mod sync;
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 3af01eb44d8..53a02bf5e0b 100644
--- a/src/tools/miri/src/shims/unix/macos/foreign_items.rs
+++ b/src/tools/miri/src/shims/unix/macos/foreign_items.rs
@@ -1,10 +1,9 @@
 use rustc_span::Symbol;
 use rustc_target::spec::abi::Abi;
 
+use crate::shims::unix::*;
 use crate::*;
 use shims::foreign_items::EmulateForeignItemResult;
-use shims::unix::fs::EvalContextExt as _;
-use shims::unix::thread::EvalContextExt as _;
 
 pub fn is_dyn_sym(_name: &str) -> bool {
     false
diff --git a/src/tools/miri/src/shims/unix/mod.rs b/src/tools/miri/src/shims/unix/mod.rs
index 638473da02b..2bc41e1a62d 100644
--- a/src/tools/miri/src/shims/unix/mod.rs
+++ b/src/tools/miri/src/shims/unix/mod.rs
@@ -1,7 +1,9 @@
 pub mod foreign_items;
 
+mod fd;
 mod fs;
 mod mem;
+mod socket;
 mod sync;
 mod thread;
 
@@ -9,7 +11,15 @@ mod freebsd;
 mod linux;
 mod macos;
 
-pub use fs::{DirHandler, FileHandler};
+pub use fd::{FdTable, FileDescriptor};
+pub use fs::DirTable;
+// All the unix-specific extension traits
+pub use fd::EvalContextExt as _;
+pub use fs::EvalContextExt as _;
+pub use mem::EvalContextExt as _;
+pub use socket::EvalContextExt as _;
+pub use sync::EvalContextExt as _;
+pub use thread::EvalContextExt as _;
 
 // Make up some constants.
 const UID: u32 = 1000;
diff --git a/src/tools/miri/src/shims/unix/socket.rs b/src/tools/miri/src/shims/unix/socket.rs
new file mode 100644
index 00000000000..84ddd746fb5
--- /dev/null
+++ b/src/tools/miri/src/shims/unix/socket.rs
@@ -0,0 +1,65 @@
+use std::io;
+
+use crate::shims::unix::*;
+use crate::*;
+
+/// Pair of connected sockets.
+///
+/// We currently don't allow sending any data through this pair, so this can be just a dummy.
+#[derive(Debug)]
+struct SocketPair;
+
+impl FileDescriptor for SocketPair {
+    fn name(&self) -> &'static str {
+        "socketpair"
+    }
+
+    fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>> {
+        Ok(Box::new(SocketPair))
+    }
+
+    fn close<'tcx>(
+        self: Box<Self>,
+        _communicate_allowed: bool,
+    ) -> InterpResult<'tcx, io::Result<i32>> {
+        Ok(Ok(0))
+    }
+}
+
+impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {}
+pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
+    /// Currently this function this function is a stub. Eventually we need to
+    /// properly implement an FD type for sockets and have this function create
+    /// two sockets and associated FDs such that writing to one will produce
+    /// data that can be read from the other.
+    ///
+    /// For more information on the arguments see the socketpair manpage:
+    /// <https://linux.die.net/man/2/socketpair>
+    fn socketpair(
+        &mut self,
+        domain: &OpTy<'tcx, Provenance>,
+        type_: &OpTy<'tcx, Provenance>,
+        protocol: &OpTy<'tcx, Provenance>,
+        sv: &OpTy<'tcx, Provenance>,
+    ) -> InterpResult<'tcx, Scalar<Provenance>> {
+        let this = self.eval_context_mut();
+
+        let _domain = this.read_scalar(domain)?.to_i32()?;
+        let _type_ = this.read_scalar(type_)?.to_i32()?;
+        let _protocol = this.read_scalar(protocol)?.to_i32()?;
+        let sv = this.deref_pointer(sv)?;
+
+        // FIXME: fail on unsupported inputs
+
+        let fds = &mut this.machine.fds;
+        let sv0 = fds.insert_fd(Box::new(SocketPair));
+        let sv0 = Scalar::try_from_int(sv0, sv.layout.size).unwrap();
+        let sv1 = fds.insert_fd(Box::new(SocketPair));
+        let sv1 = Scalar::try_from_int(sv1, sv.layout.size).unwrap();
+
+        this.write_scalar(sv0, &sv)?;
+        this.write_scalar(sv1, &sv.offset(sv.layout.size, sv.layout, this)?)?;
+
+        Ok(Scalar::from_i32(0))
+    }
+}
diff --git a/src/tools/miri/test-cargo-miri/run-test.py b/src/tools/miri/test-cargo-miri/run-test.py
index 21479bc4d58..cac11dff775 100755
--- a/src/tools/miri/test-cargo-miri/run-test.py
+++ b/src/tools/miri/test-cargo-miri/run-test.py
@@ -31,11 +31,11 @@ def cargo_miri(cmd, quiet = True):
 
 def normalize_stdout(str):
     str = str.replace("src\\", "src/") # normalize paths across platforms
-    str = re.sub("finished in \d+\.\d\ds", "finished in $TIME", str) # the time keeps changing, obviously
+    str = re.sub("finished in \\d+\\.\\d\\ds", "finished in $TIME", str) # the time keeps changing, obviously
     return str
 
 def normalize_stderr(str):
-    str = re.sub("Preparing a sysroot for Miri \(target: [a-z0-9_-]+\)\.\.\. done\n", "", str) # remove leading cargo-miri setup output
+    str = re.sub("Preparing a sysroot for Miri \\(target: [a-z0-9_-]+\\)\\.\\.\\. done\n", "", str) # remove leading cargo-miri setup output
     return str
 
 def check_output(actual, path, name):
diff --git a/src/tools/miri/tests/extern-so/libcode.version b/src/tools/miri/tests/extern-so/libcode.version
deleted file mode 100644
index 0f04b9aaebb..00000000000
--- a/src/tools/miri/tests/extern-so/libcode.version
+++ /dev/null
@@ -1,9 +0,0 @@
-CODEABI_1.0 {
-    global: *add_one_int*; 
-        *printer*; 
-        *test_stack_spill*; 
-        *get_unsigned_int*; 
-        *add_int16*;
-        *add_short_to_long*;
-    local: *;
-};
diff --git a/src/tools/miri/tests/extern-so/libtest.map b/src/tools/miri/tests/extern-so/libtest.map
new file mode 100644
index 00000000000..a57a4dc149f
--- /dev/null
+++ b/src/tools/miri/tests/extern-so/libtest.map
@@ -0,0 +1,12 @@
+CODEABI_1.0 {
+    # Define which symbols to export.
+    global:
+        add_one_int;
+        printer;
+        test_stack_spill;
+        get_unsigned_int;
+        add_int16;
+        add_short_to_long;
+    # The rest remains private.
+    local: *;
+};
diff --git a/src/tools/miri/tests/fail/extern_static.stderr b/src/tools/miri/tests/fail/extern_static.stderr
index c3de4dadb0a..34bb273f04b 100644
--- a/src/tools/miri/tests/fail/extern_static.stderr
+++ b/src/tools/miri/tests/fail/extern_static.stderr
@@ -1,8 +1,8 @@
-error: unsupported operation: `extern` static `FOO` from crate `extern_static` is not supported by Miri
+error: unsupported operation: extern static `FOO` is not supported by Miri
   --> $DIR/extern_static.rs:LL:CC
    |
 LL |     let _val = unsafe { std::ptr::addr_of!(FOO) };
-   |                                            ^^^ `extern` static `FOO` from crate `extern_static` is not supported by Miri
+   |                                            ^^^ extern static `FOO` is not supported by Miri
    |
    = help: this is likely not a bug in the program; it indicates that the program performed an operation that the interpreter does not support
    = note: BACKTRACE:
diff --git a/src/tools/miri/tests/fail/extern_static_in_const.stderr b/src/tools/miri/tests/fail/extern_static_in_const.stderr
index 23f775f2588..45d1632cce2 100644
--- a/src/tools/miri/tests/fail/extern_static_in_const.stderr
+++ b/src/tools/miri/tests/fail/extern_static_in_const.stderr
@@ -1,8 +1,8 @@
-error: unsupported operation: `extern` static `E` from crate `extern_static_in_const` is not supported by Miri
+error: unsupported operation: extern static `E` is not supported by Miri
   --> $DIR/extern_static_in_const.rs:LL:CC
    |
 LL |     let _val = X;
-   |                ^ `extern` static `E` from crate `extern_static_in_const` is not supported by Miri
+   |                ^ extern static `E` is not supported by Miri
    |
    = help: this is likely not a bug in the program; it indicates that the program performed an operation that the interpreter does not support
    = note: BACKTRACE:
diff --git a/src/tools/miri/tests/fail/extern_static_wrong_size.rs b/src/tools/miri/tests/fail/extern_static_wrong_size.rs
index 17061f0e5c8..fee3c38c25e 100644
--- a/src/tools/miri/tests/fail/extern_static_wrong_size.rs
+++ b/src/tools/miri/tests/fail/extern_static_wrong_size.rs
@@ -6,5 +6,5 @@ extern "C" {
 }
 
 fn main() {
-    let _val = unsafe { environ }; //~ ERROR: /has been declared with a size of 1 bytes and alignment of 1 bytes, but Miri emulates it via an extern static shim with a size of [48] bytes and alignment of [48] bytes/
+    let _val = unsafe { environ }; //~ ERROR: /with a size of 1 bytes and alignment of 1 bytes, but Miri emulates it via an extern static shim with a size of [48] bytes and alignment of [48] bytes/
 }
diff --git a/src/tools/miri/tests/fail/extern_static_wrong_size.stderr b/src/tools/miri/tests/fail/extern_static_wrong_size.stderr
index c935a548f80..27699d780c2 100644
--- a/src/tools/miri/tests/fail/extern_static_wrong_size.stderr
+++ b/src/tools/miri/tests/fail/extern_static_wrong_size.stderr
@@ -1,8 +1,8 @@
-error: unsupported operation: `extern` static `environ` from crate `extern_static_wrong_size` has been declared with a size of 1 bytes and alignment of 1 bytes, but Miri emulates it via an extern static shim with a size of N bytes and alignment of N bytes
+error: unsupported operation: extern static `environ` has been declared as `extern_static_wrong_size::environ` with a size of 1 bytes and alignment of 1 bytes, but Miri emulates it via an extern static shim with a size of N bytes and alignment of N bytes
   --> $DIR/extern_static_wrong_size.rs:LL:CC
    |
 LL |     let _val = unsafe { environ };
-   |                         ^^^^^^^ `extern` static `environ` from crate `extern_static_wrong_size` has been declared with a size of 1 bytes and alignment of 1 bytes, but Miri emulates it via an extern static shim with a size of N bytes and alignment of N bytes
+   |                         ^^^^^^^ extern static `environ` has been declared as `extern_static_wrong_size::environ` with a size of 1 bytes and alignment of 1 bytes, but Miri emulates it via an extern static shim with a size of N bytes and alignment of N bytes
    |
    = help: this is likely not a bug in the program; it indicates that the program performed an operation that the interpreter does not support
    = note: BACKTRACE:
diff --git a/src/tools/miri/tests/fail/issue-miri-3288-ice-symbolic-alignment-extern-static.stderr b/src/tools/miri/tests/fail/issue-miri-3288-ice-symbolic-alignment-extern-static.stderr
index a4249d2e881..e5dbe749884 100644
--- a/src/tools/miri/tests/fail/issue-miri-3288-ice-symbolic-alignment-extern-static.stderr
+++ b/src/tools/miri/tests/fail/issue-miri-3288-ice-symbolic-alignment-extern-static.stderr
@@ -1,8 +1,8 @@
-error: unsupported operation: `extern` static `_dispatch_queue_attr_concurrent` from crate `issue_miri_3288_ice_symbolic_alignment_extern_static` is not supported by Miri
+error: unsupported operation: extern static `_dispatch_queue_attr_concurrent` is not supported by Miri
   --> $DIR/issue-miri-3288-ice-symbolic-alignment-extern-static.rs:LL:CC
    |
 LL |     let _val = *DISPATCH_QUEUE_CONCURRENT;
-   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^ `extern` static `_dispatch_queue_attr_concurrent` from crate `issue_miri_3288_ice_symbolic_alignment_extern_static` is not supported by Miri
+   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^ extern static `_dispatch_queue_attr_concurrent` is not supported by Miri
    |
    = help: this is likely not a bug in the program; it indicates that the program performed an operation that the interpreter does not support
    = note: BACKTRACE:
diff --git a/src/tools/miri/tests/fail/tree_borrows/spurious_read.rs b/src/tools/miri/tests/fail/tree_borrows/spurious_read.rs
index 3f39dcb4b76..50ef0ceef91 100644
--- a/src/tools/miri/tests/fail/tree_borrows/spurious_read.rs
+++ b/src/tools/miri/tests/fail/tree_borrows/spurious_read.rs
@@ -89,7 +89,7 @@ fn retagx_retagy_retx_writey_rety() {
     // - retag `y` protected
     // - (wait for the other thread to return so that there is no foreign protector when we write)
     // - attempt a write through `y`.
-    // - (UB should have occured by now, but the next step would be to
+    // - (UB should have occurred by now, but the next step would be to
     //    remove `y`'s protector)
     let thread_y = thread::spawn(move || {
         let b = (2, by);
diff --git a/src/tools/miri/tests/ui.rs b/src/tools/miri/tests/ui.rs
index a75fa4cf986..7e8d1401183 100644
--- a/src/tools/miri/tests/ui.rs
+++ b/src/tools/miri/tests/ui.rs
@@ -1,9 +1,10 @@
-use colored::*;
-use regex::bytes::Regex;
 use std::ffi::OsString;
 use std::num::NonZeroUsize;
 use std::path::{Path, PathBuf};
 use std::{env, process::Command};
+
+use colored::*;
+use regex::bytes::Regex;
 use ui_test::color_eyre::eyre::{Context, Result};
 use ui_test::{
     status_emitter, CommandBuilder, Config, Format, Match, Mode, OutputConflictHandling,
@@ -44,12 +45,15 @@ fn build_so_for_c_ffi_tests() -> PathBuf {
             // This is to avoid automatically adding `malloc`, etc.
             // Source: https://anadoxin.org/blog/control-over-symbol-exports-in-gcc.html/
             "-fPIC",
-            "-Wl,--version-script=tests/extern-so/libcode.version",
+            "-Wl,--version-script=tests/extern-so/libtest.map",
         ])
         .output()
         .expect("failed to generate shared object file for testing external C function calls");
     if !cc_output.status.success() {
-        panic!("error in generating shared object file for testing external C function calls");
+        panic!(
+            "error in generating shared object file for testing external C function calls:\n{}",
+            String::from_utf8_lossy(&cc_output.stderr),
+        );
     }
     so_file_path
 }
@@ -120,10 +124,10 @@ fn run_tests(
     config.program.args.push("--target".into());
     config.program.args.push(target.into());
 
-    // If we're on linux, and we're testing the extern-so functionality,
-    // then build the shared object file for testing external C function calls
-    // and push the relevant compiler flag.
-    if cfg!(target_os = "linux") && path.starts_with("tests/extern-so/") {
+    // If we're testing the extern-so functionality, then build the shared object file for testing
+    // external C function calls and push the relevant compiler flag.
+    if path.starts_with("tests/extern-so/") {
+        assert!(cfg!(target_os = "linux"));
         let so_file_path = build_so_for_c_ffi_tests();
         let mut flag = std::ffi::OsString::from("-Zmiri-extern-so-file=");
         flag.push(so_file_path.into_os_string());