about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2024-04-03 07:29:41 +0000
committerbors <bors@rust-lang.org>2024-04-03 07:29:41 +0000
commit9c502781ff57e46c331f8db41446d3cebb242681 (patch)
treed4779531f4b08a718ea4d6ecf471a43e4ea32427
parent82d9326b8d7d0b13c3bf7af49f6fe4bb9980a02d (diff)
parent229d41731a8a25f9cd630d86250533431aefe7d6 (diff)
downloadrust-9c502781ff57e46c331f8db41446d3cebb242681.tar.gz
rust-9c502781ff57e46c331f8db41446d3cebb242681.zip
Auto merge of #3441 - RalfJung:fds, r=RalfJung
shims/unix: split general FD management from FS access

`fd.rs` was a mix of general file descriptor infrastructure and file system access. Split those things up properly.

Also add a `socket.rs` file where support for sockets can go eventually. For now it just contains the socketpair stub.
-rw-r--r--src/tools/miri/src/machine.rs18
-rw-r--r--src/tools/miri/src/shims/unix/fd.rs429
-rw-r--r--src/tools/miri/src/shims/unix/foreign_items.rs57
-rw-r--r--src/tools/miri/src/shims/unix/fs.rs607
-rw-r--r--src/tools/miri/src/shims/unix/linux/fd.rs67
-rw-r--r--src/tools/miri/src/shims/unix/linux/fd/epoll.rs2
-rw-r--r--src/tools/miri/src/shims/unix/linux/fd/event.rs2
-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.rs5
-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.rs66
12 files changed, 663 insertions, 633 deletions
diff --git a/src/tools/miri/src/machine.rs b/src/tools/miri/src/machine.rs
index 2137de6a29b..14b7afcc971 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);
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..bc9348ee0e8
--- /dev/null
+++ b/src/tools/miri/src/shims/unix/fd.rs
@@ -0,0 +1,429 @@
+//! 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 {
+        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..b1e1aec5880 100644
--- a/src/tools/miri/src/shims/unix/foreign_items.rs
+++ b/src/tools/miri/src/shims/unix/foreign_items.rs
@@ -8,8 +8,10 @@ use rustc_target::spec::abi::Abi;
 
 use crate::*;
 use shims::foreign_items::EmulateForeignItemResult;
+use shims::unix::fd::EvalContextExt as _;
 use shims::unix::fs::EvalContextExt as _;
 use shims::unix::mem::EvalContextExt as _;
+use shims::unix::socket::EvalContextExt as _;
 use shims::unix::sync::EvalContextExt as _;
 use shims::unix::thread::EvalContextExt as _;
 
@@ -51,7 +53,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 +81,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 +100,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 +223,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 +611,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/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/fd.rs
index 22fbb6da95a..7d5177e5c42 100644
--- a/src/tools/miri/src/shims/unix/linux/fd.rs
+++ b/src/tools/miri/src/shims/unix/linux/fd.rs
@@ -1,17 +1,12 @@
 use std::cell::Cell;
 
-use rustc_middle::ty::ScalarInt;
-
+use crate::shims::unix::*;
 use crate::*;
 use epoll::{Epoll, EpollEvent};
 use event::Event;
-use socketpair::SocketPair;
-
-use shims::unix::fs::EvalContextExt as _;
 
 pub mod epoll;
 pub mod event;
-pub mod socketpair;
 
 impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {}
 pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
@@ -35,7 +30,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 +74,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 +82,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 +93,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 +145,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,7 +153,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()?))
         }
     }
 
@@ -203,51 +198,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
             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()) }));
+        let fd = this.machine.fds.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/fd/epoll.rs b/src/tools/miri/src/shims/unix/linux/fd/epoll.rs
index 8c5aed6def6..f2da76ca98d 100644
--- a/src/tools/miri/src/shims/unix/linux/fd/epoll.rs
+++ b/src/tools/miri/src/shims/unix/linux/fd/epoll.rs
@@ -1,6 +1,6 @@
 use crate::*;
 
-use crate::shims::unix::fs::FileDescriptor;
+use crate::shims::unix::FileDescriptor;
 
 use rustc_data_structures::fx::FxHashMap;
 use std::io;
diff --git a/src/tools/miri/src/shims/unix/linux/fd/event.rs b/src/tools/miri/src/shims/unix/linux/fd/event.rs
index 49408fda3ae..0eb4befd52f 100644
--- a/src/tools/miri/src/shims/unix/linux/fd/event.rs
+++ b/src/tools/miri/src/shims/unix/linux/fd/event.rs
@@ -1,4 +1,4 @@
-use crate::shims::unix::fs::FileDescriptor;
+use crate::shims::unix::FileDescriptor;
 
 use rustc_const_eval::interpret::InterpResult;
 use rustc_middle::ty::TyCtxt;
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..7e600f4c54b 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,12 @@ 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::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")
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..aa06425ffa1
--- /dev/null
+++ b/src/tools/miri/src/shims/unix/socket.rs
@@ -0,0 +1,66 @@
+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.
+/// FIXME: show proper errors when trying to send/receive
+#[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))
+    }
+}