about summary refs log tree commit diff
diff options
context:
space:
mode:
authorRune Tynan <runetynan@gmail.com>2024-12-01 19:33:41 -0800
committerRune Tynan <runetynan@gmail.com>2025-04-08 21:11:58 -0700
commit209ce5919514d40d00ec9b5e3e672f5de2e5be9c (patch)
tree56a21a864fcfb5bd37ab877e4e3798a7a94403ca
parent92e6f08fadaca753f8de948ad4b9094deeb99c25 (diff)
downloadrust-209ce5919514d40d00ec9b5e3e672f5de2e5be9c.tar.gz
rust-209ce5919514d40d00ec9b5e3e672f5de2e5be9c.zip
Implement trivial file operations - opening and closing handles. Just enough to get file metadata.
-rw-r--r--src/tools/miri/Cargo.lock1
-rw-r--r--src/tools/miri/Cargo.toml1
-rw-r--r--src/tools/miri/src/shims/files.rs115
-rw-r--r--src/tools/miri/src/shims/time.rs36
-rw-r--r--src/tools/miri/src/shims/unix/fd.rs6
-rw-r--r--src/tools/miri/src/shims/unix/fs.rs94
-rw-r--r--src/tools/miri/src/shims/unix/linux_like/epoll.rs4
-rw-r--r--src/tools/miri/src/shims/unix/linux_like/eventfd.rs2
-rw-r--r--src/tools/miri/src/shims/unix/unnamed_socket.rs2
-rw-r--r--src/tools/miri/src/shims/windows/foreign_items.rs32
-rw-r--r--src/tools/miri/src/shims/windows/fs.rs402
-rw-r--r--src/tools/miri/src/shims/windows/handle.rs61
-rw-r--r--src/tools/miri/src/shims/windows/mod.rs2
-rw-r--r--src/tools/miri/src/shims/windows/thread.rs2
-rw-r--r--src/tools/miri/test_dependencies/Cargo.toml2
-rw-r--r--src/tools/miri/tests/fail-dep/concurrency/windows_join_main.rs2
-rw-r--r--src/tools/miri/tests/pass-dep/shims/windows-fs.rs198
-rw-r--r--src/tools/miri/tests/pass/shims/fs.rs36
18 files changed, 838 insertions, 160 deletions
diff --git a/src/tools/miri/Cargo.lock b/src/tools/miri/Cargo.lock
index bb228962ecc..0c6f4a3dd06 100644
--- a/src/tools/miri/Cargo.lock
+++ b/src/tools/miri/Cargo.lock
@@ -538,6 +538,7 @@ name = "miri"
 version = "0.1.0"
 dependencies = [
  "aes",
+ "bitflags",
  "chrono",
  "chrono-tz",
  "colored",
diff --git a/src/tools/miri/Cargo.toml b/src/tools/miri/Cargo.toml
index 5bb07648f75..e9ee19b7932 100644
--- a/src/tools/miri/Cargo.toml
+++ b/src/tools/miri/Cargo.toml
@@ -26,6 +26,7 @@ measureme = "12"
 chrono = { version = "0.4.38", default-features = false }
 chrono-tz = "0.10"
 directories = "6"
+bitflags = "2.6"
 
 # Copied from `compiler/rustc/Cargo.toml`.
 # But only for some targets, it fails for others. Rustc configures this in its CI, but we can't
diff --git a/src/tools/miri/src/shims/files.rs b/src/tools/miri/src/shims/files.rs
index 6b4f4cdc922..42603e784bb 100644
--- a/src/tools/miri/src/shims/files.rs
+++ b/src/tools/miri/src/shims/files.rs
@@ -1,6 +1,7 @@
 use std::any::Any;
 use std::collections::BTreeMap;
-use std::io::{IsTerminal, SeekFrom, Write};
+use std::fs::{File, Metadata};
+use std::io::{IsTerminal, Seek, SeekFrom, Write};
 use std::marker::CoercePointee;
 use std::ops::Deref;
 use std::rc::{Rc, Weak};
@@ -192,7 +193,7 @@ pub trait FileDescription: std::fmt::Debug + FileDescriptionExt {
         false
     }
 
-    fn as_unix(&self) -> &dyn UnixFileDescription {
+    fn as_unix<'tcx>(&self, _ecx: &MiriInterpCx<'tcx>) -> &dyn UnixFileDescription {
         panic!("Not a unix file descriptor: {}", self.name());
     }
 }
@@ -278,6 +279,97 @@ impl FileDescription for io::Stderr {
     }
 }
 
+#[derive(Debug)]
+pub struct FileHandle {
+    pub(crate) file: File,
+    pub(crate) writable: bool,
+}
+
+impl FileDescription for FileHandle {
+    fn name(&self) -> &'static str {
+        "file"
+    }
+
+    fn read<'tcx>(
+        self: FileDescriptionRef<Self>,
+        communicate_allowed: bool,
+        ptr: Pointer,
+        len: usize,
+        ecx: &mut MiriInterpCx<'tcx>,
+        finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
+    ) -> InterpResult<'tcx> {
+        assert!(communicate_allowed, "isolation should have prevented even opening a file");
+
+        let result = ecx.read_from_host(&self.file, len, ptr)?;
+        finish.call(ecx, result)
+    }
+
+    fn write<'tcx>(
+        self: FileDescriptionRef<Self>,
+        communicate_allowed: bool,
+        ptr: Pointer,
+        len: usize,
+        ecx: &mut MiriInterpCx<'tcx>,
+        finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
+    ) -> InterpResult<'tcx> {
+        assert!(communicate_allowed, "isolation should have prevented even opening a file");
+
+        let result = ecx.write_to_host(&self.file, len, ptr)?;
+        finish.call(ecx, result)
+    }
+
+    fn seek<'tcx>(
+        &self,
+        communicate_allowed: bool,
+        offset: SeekFrom,
+    ) -> InterpResult<'tcx, io::Result<u64>> {
+        assert!(communicate_allowed, "isolation should have prevented even opening a file");
+        interp_ok((&mut &self.file).seek(offset))
+    }
+
+    fn close<'tcx>(
+        self,
+        communicate_allowed: bool,
+        _ecx: &mut MiriInterpCx<'tcx>,
+    ) -> InterpResult<'tcx, io::Result<()>> {
+        assert!(communicate_allowed, "isolation should have prevented even opening a file");
+        // We sync the file if it was opened in a mode different than read-only.
+        if self.writable {
+            // `File::sync_all` does the checks that are done when closing a file. We do this to
+            // to handle possible errors correctly.
+            let result = self.file.sync_all();
+            // Now we actually close the file and return the result.
+            drop(self.file);
+            interp_ok(result)
+        } else {
+            // We drop the file, this closes it but ignores any errors
+            // produced when closing it. This is done because
+            // `File::sync_all` cannot be done over files like
+            // `/dev/urandom` which are read-only. Check
+            // https://github.com/rust-lang/miri/issues/999#issuecomment-568920439
+            // for a deeper discussion.
+            drop(self.file);
+            interp_ok(Ok(()))
+        }
+    }
+
+    fn metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result<Metadata>> {
+        interp_ok(self.file.metadata())
+    }
+
+    fn is_tty(&self, communicate_allowed: bool) -> bool {
+        communicate_allowed && self.file.is_terminal()
+    }
+
+    fn as_unix<'tcx>(&self, ecx: &MiriInterpCx<'tcx>) -> &dyn UnixFileDescription {
+        assert!(
+            ecx.target_os_is_unix(),
+            "unix file operations are only available for unix targets"
+        );
+        self
+    }
+}
+
 /// Like /dev/null
 #[derive(Debug)]
 pub struct NullOutput;
@@ -300,10 +392,13 @@ impl FileDescription for NullOutput {
     }
 }
 
+/// Internal type of a file-descriptor - this is what [`FdTable`] expects
+pub type FdNum = i32;
+
 /// The file descriptor table
 #[derive(Debug)]
 pub struct FdTable {
-    pub fds: BTreeMap<i32, DynFileDescriptionRef>,
+    pub fds: BTreeMap<FdNum, DynFileDescriptionRef>,
     /// Unique identifier for file description, used to differentiate between various file description.
     next_file_description_id: FdId,
 }
@@ -339,12 +434,12 @@ impl FdTable {
     }
 
     /// Insert a new file description to the FdTable.
-    pub fn insert_new(&mut self, fd: impl FileDescription) -> i32 {
+    pub fn insert_new(&mut self, fd: impl FileDescription) -> FdNum {
         let fd_ref = self.new_ref(fd);
         self.insert(fd_ref)
     }
 
-    pub fn insert(&mut self, fd_ref: DynFileDescriptionRef) -> i32 {
+    pub fn insert(&mut self, fd_ref: DynFileDescriptionRef) -> FdNum {
         self.insert_with_min_num(fd_ref, 0)
     }
 
@@ -352,8 +447,8 @@ impl FdTable {
     pub fn insert_with_min_num(
         &mut self,
         file_handle: DynFileDescriptionRef,
-        min_fd_num: i32,
-    ) -> i32 {
+        min_fd_num: FdNum,
+    ) -> FdNum {
         // 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
@@ -379,16 +474,16 @@ impl FdTable {
         new_fd_num
     }
 
-    pub fn get(&self, fd_num: i32) -> Option<DynFileDescriptionRef> {
+    pub fn get(&self, fd_num: FdNum) -> Option<DynFileDescriptionRef> {
         let fd = self.fds.get(&fd_num)?;
         Some(fd.clone())
     }
 
-    pub fn remove(&mut self, fd_num: i32) -> Option<DynFileDescriptionRef> {
+    pub fn remove(&mut self, fd_num: FdNum) -> Option<DynFileDescriptionRef> {
         self.fds.remove(&fd_num)
     }
 
-    pub fn is_fd_num(&self, fd_num: i32) -> bool {
+    pub fn is_fd_num(&self, fd_num: FdNum) -> bool {
         self.fds.contains_key(&fd_num)
     }
 }
diff --git a/src/tools/miri/src/shims/time.rs b/src/tools/miri/src/shims/time.rs
index d7c445b47cb..fb80a36af9f 100644
--- a/src/tools/miri/src/shims/time.rs
+++ b/src/tools/miri/src/shims/time.rs
@@ -219,16 +219,8 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
 
         let filetime = this.deref_pointer_as(LPFILETIME_op, this.windows_ty_layout("FILETIME"))?;
 
-        let NANOS_PER_SEC = this.eval_windows_u64("time", "NANOS_PER_SEC");
-        let INTERVALS_PER_SEC = this.eval_windows_u64("time", "INTERVALS_PER_SEC");
-        let INTERVALS_TO_UNIX_EPOCH = this.eval_windows_u64("time", "INTERVALS_TO_UNIX_EPOCH");
-        let NANOS_PER_INTERVAL = NANOS_PER_SEC / INTERVALS_PER_SEC;
-        let SECONDS_TO_UNIX_EPOCH = INTERVALS_TO_UNIX_EPOCH / INTERVALS_PER_SEC;
-
-        let duration = system_time_to_duration(&SystemTime::now())?
-            + Duration::from_secs(SECONDS_TO_UNIX_EPOCH);
-        let duration_ticks = u64::try_from(duration.as_nanos() / u128::from(NANOS_PER_INTERVAL))
-            .map_err(|_| err_unsup_format!("programs running more than 2^64 Windows ticks after the Windows epoch are not supported"))?;
+        let duration = this.system_time_since_windows_epoch(&SystemTime::now())?;
+        let duration_ticks = this.windows_ticks_for(duration)?;
 
         let dwLowDateTime = u32::try_from(duration_ticks & 0x00000000FFFFFFFF).unwrap();
         let dwHighDateTime = u32::try_from((duration_ticks & 0xFFFFFFFF00000000) >> 32).unwrap();
@@ -281,6 +273,30 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
         interp_ok(Scalar::from_i32(-1)) // Return non-zero on success
     }
 
+    #[allow(non_snake_case, clippy::arithmetic_side_effects)]
+    fn system_time_since_windows_epoch(&self, time: &SystemTime) -> InterpResult<'tcx, Duration> {
+        let this = self.eval_context_ref();
+
+        let INTERVALS_PER_SEC = this.eval_windows_u64("time", "INTERVALS_PER_SEC");
+        let INTERVALS_TO_UNIX_EPOCH = this.eval_windows_u64("time", "INTERVALS_TO_UNIX_EPOCH");
+        let SECONDS_TO_UNIX_EPOCH = INTERVALS_TO_UNIX_EPOCH / INTERVALS_PER_SEC;
+
+        interp_ok(system_time_to_duration(time)? + Duration::from_secs(SECONDS_TO_UNIX_EPOCH))
+    }
+
+    #[allow(non_snake_case, clippy::arithmetic_side_effects)]
+    fn windows_ticks_for(&self, duration: Duration) -> InterpResult<'tcx, u64> {
+        let this = self.eval_context_ref();
+
+        let NANOS_PER_SEC = this.eval_windows_u64("time", "NANOS_PER_SEC");
+        let INTERVALS_PER_SEC = this.eval_windows_u64("time", "INTERVALS_PER_SEC");
+        let NANOS_PER_INTERVAL = NANOS_PER_SEC / INTERVALS_PER_SEC;
+
+        let ticks = u64::try_from(duration.as_nanos() / u128::from(NANOS_PER_INTERVAL))
+            .map_err(|_| err_unsup_format!("programs running more than 2^64 Windows ticks after the Windows epoch are not supported"))?;
+        interp_ok(ticks)
+    }
+
     fn mach_absolute_time(&self) -> InterpResult<'tcx, Scalar> {
         let this = self.eval_context_ref();
 
diff --git a/src/tools/miri/src/shims/unix/fd.rs b/src/tools/miri/src/shims/unix/fd.rs
index 3f85b9ae9bd..41be9df7e2d 100644
--- a/src/tools/miri/src/shims/unix/fd.rs
+++ b/src/tools/miri/src/shims/unix/fd.rs
@@ -121,7 +121,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
             throw_unsup_format!("unsupported flags {:#x}", op);
         };
 
-        let result = fd.as_unix().flock(this.machine.communicate(), parsed_op)?;
+        let result = fd.as_unix(this).flock(this.machine.communicate(), parsed_op)?;
         // return `0` if flock is successful
         let result = result.map(|()| 0i32);
         interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
@@ -273,7 +273,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
                 let Ok(offset) = u64::try_from(offset) else {
                     return this.set_last_error_and_return(LibcError("EINVAL"), dest);
                 };
-                fd.as_unix().pread(communicate, offset, buf, count, this, finish)?
+                fd.as_unix(this).pread(communicate, offset, buf, count, this, finish)?
             }
         };
         interp_ok(())
@@ -333,7 +333,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
                 let Ok(offset) = u64::try_from(offset) else {
                     return this.set_last_error_and_return(LibcError("EINVAL"), dest);
                 };
-                fd.as_unix().pwrite(communicate, buf, count, offset, this, finish)?
+                fd.as_unix(this).pwrite(communicate, buf, count, offset, this, finish)?
             }
         };
         interp_ok(())
diff --git a/src/tools/miri/src/shims/unix/fs.rs b/src/tools/miri/src/shims/unix/fs.rs
index f8e0c638c90..fc0f57694a7 100644
--- a/src/tools/miri/src/shims/unix/fs.rs
+++ b/src/tools/miri/src/shims/unix/fs.rs
@@ -2,10 +2,9 @@
 
 use std::borrow::Cow;
 use std::fs::{
-    DirBuilder, File, FileType, Metadata, OpenOptions, ReadDir, read_dir, remove_dir, remove_file,
-    rename,
+    DirBuilder, File, FileType, OpenOptions, ReadDir, read_dir, remove_dir, remove_file, rename,
 };
-use std::io::{self, ErrorKind, IsTerminal, Read, Seek, SeekFrom, Write};
+use std::io::{self, ErrorKind, Read, Seek, SeekFrom, Write};
 use std::path::{Path, PathBuf};
 use std::time::SystemTime;
 
@@ -14,98 +13,11 @@ use rustc_data_structures::fx::FxHashMap;
 
 use self::shims::time::system_time_to_duration;
 use crate::helpers::check_min_vararg_count;
-use crate::shims::files::{EvalContextExt as _, FileDescription, FileDescriptionRef};
+use crate::shims::files::FileHandle;
 use crate::shims::os_str::bytes_to_os_str;
 use crate::shims::unix::fd::{FlockOp, UnixFileDescription};
 use crate::*;
 
-#[derive(Debug)]
-struct FileHandle {
-    file: File,
-    writable: bool,
-}
-
-impl FileDescription for FileHandle {
-    fn name(&self) -> &'static str {
-        "file"
-    }
-
-    fn read<'tcx>(
-        self: FileDescriptionRef<Self>,
-        communicate_allowed: bool,
-        ptr: Pointer,
-        len: usize,
-        ecx: &mut MiriInterpCx<'tcx>,
-        finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
-    ) -> InterpResult<'tcx> {
-        assert!(communicate_allowed, "isolation should have prevented even opening a file");
-
-        let result = ecx.read_from_host(&self.file, len, ptr)?;
-        finish.call(ecx, result)
-    }
-
-    fn write<'tcx>(
-        self: FileDescriptionRef<Self>,
-        communicate_allowed: bool,
-        ptr: Pointer,
-        len: usize,
-        ecx: &mut MiriInterpCx<'tcx>,
-        finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
-    ) -> InterpResult<'tcx> {
-        assert!(communicate_allowed, "isolation should have prevented even opening a file");
-
-        let result = ecx.write_to_host(&self.file, len, ptr)?;
-        finish.call(ecx, result)
-    }
-
-    fn seek<'tcx>(
-        &self,
-        communicate_allowed: bool,
-        offset: SeekFrom,
-    ) -> InterpResult<'tcx, io::Result<u64>> {
-        assert!(communicate_allowed, "isolation should have prevented even opening a file");
-        interp_ok((&mut &self.file).seek(offset))
-    }
-
-    fn close<'tcx>(
-        self,
-        communicate_allowed: bool,
-        _ecx: &mut MiriInterpCx<'tcx>,
-    ) -> InterpResult<'tcx, io::Result<()>> {
-        assert!(communicate_allowed, "isolation should have prevented even opening a file");
-        // We sync the file if it was opened in a mode different than read-only.
-        if self.writable {
-            // `File::sync_all` does the checks that are done when closing a file. We do this to
-            // to handle possible errors correctly.
-            let result = self.file.sync_all();
-            // Now we actually close the file and return the result.
-            drop(self.file);
-            interp_ok(result)
-        } else {
-            // We drop the file, this closes it but ignores any errors
-            // produced when closing it. This is done because
-            // `File::sync_all` cannot be done over files like
-            // `/dev/urandom` which are read-only. Check
-            // https://github.com/rust-lang/miri/issues/999#issuecomment-568920439
-            // for a deeper discussion.
-            drop(self.file);
-            interp_ok(Ok(()))
-        }
-    }
-
-    fn metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result<Metadata>> {
-        interp_ok(self.file.metadata())
-    }
-
-    fn is_tty(&self, communicate_allowed: bool) -> bool {
-        communicate_allowed && self.file.is_terminal()
-    }
-
-    fn as_unix(&self) -> &dyn UnixFileDescription {
-        self
-    }
-}
-
 impl UnixFileDescription for FileHandle {
     fn pread<'tcx>(
         &self,
diff --git a/src/tools/miri/src/shims/unix/linux_like/epoll.rs b/src/tools/miri/src/shims/unix/linux_like/epoll.rs
index de8bcb54aef..b489595b4cd 100644
--- a/src/tools/miri/src/shims/unix/linux_like/epoll.rs
+++ b/src/tools/miri/src/shims/unix/linux_like/epoll.rs
@@ -153,7 +153,7 @@ impl FileDescription for Epoll {
         interp_ok(Ok(()))
     }
 
-    fn as_unix(&self) -> &dyn UnixFileDescription {
+    fn as_unix<'tcx>(&self, _ecx: &MiriInterpCx<'tcx>) -> &dyn UnixFileDescription {
         self
     }
 }
@@ -590,7 +590,7 @@ fn check_and_update_one_event_interest<'tcx>(
     ecx: &MiriInterpCx<'tcx>,
 ) -> InterpResult<'tcx, bool> {
     // Get the bitmask of ready events for a file description.
-    let ready_events_bitmask = fd_ref.as_unix().get_epoll_ready_events()?.get_event_bitmask(ecx);
+    let ready_events_bitmask = fd_ref.as_unix(ecx).get_epoll_ready_events()?.get_event_bitmask(ecx);
     let epoll_event_interest = interest.borrow();
     let epfd = epoll_event_interest.weak_epfd.upgrade().unwrap();
     // This checks if any of the events specified in epoll_event_interest.events
diff --git a/src/tools/miri/src/shims/unix/linux_like/eventfd.rs b/src/tools/miri/src/shims/unix/linux_like/eventfd.rs
index 936d436bd82..ee7deb8d383 100644
--- a/src/tools/miri/src/shims/unix/linux_like/eventfd.rs
+++ b/src/tools/miri/src/shims/unix/linux_like/eventfd.rs
@@ -100,7 +100,7 @@ impl FileDescription for EventFd {
         eventfd_write(buf_place, self, ecx, finish)
     }
 
-    fn as_unix(&self) -> &dyn UnixFileDescription {
+    fn as_unix<'tcx>(&self, _ecx: &MiriInterpCx<'tcx>) -> &dyn UnixFileDescription {
         self
     }
 }
diff --git a/src/tools/miri/src/shims/unix/unnamed_socket.rs b/src/tools/miri/src/shims/unix/unnamed_socket.rs
index e183bfdf0e1..135d8f6bee7 100644
--- a/src/tools/miri/src/shims/unix/unnamed_socket.rs
+++ b/src/tools/miri/src/shims/unix/unnamed_socket.rs
@@ -107,7 +107,7 @@ impl FileDescription for AnonSocket {
         anonsocket_write(self, ptr, len, ecx, finish)
     }
 
-    fn as_unix(&self) -> &dyn UnixFileDescription {
+    fn as_unix<'tcx>(&self, _ecx: &MiriInterpCx<'tcx>) -> &dyn UnixFileDescription {
         self
     }
 }
diff --git a/src/tools/miri/src/shims/windows/foreign_items.rs b/src/tools/miri/src/shims/windows/foreign_items.rs
index dda30209275..33b65404239 100644
--- a/src/tools/miri/src/shims/windows/foreign_items.rs
+++ b/src/tools/miri/src/shims/windows/foreign_items.rs
@@ -241,6 +241,32 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
                 };
                 this.write_scalar(result, dest)?;
             }
+            "CreateFileW" => {
+                let [
+                    file_name,
+                    desired_access,
+                    share_mode,
+                    security_attributes,
+                    creation_disposition,
+                    flags_and_attributes,
+                    template_file,
+                ] = this.check_shim(abi, sys_conv, link_name, args)?;
+                let handle = this.CreateFileW(
+                    file_name,
+                    desired_access,
+                    share_mode,
+                    security_attributes,
+                    creation_disposition,
+                    flags_and_attributes,
+                    template_file,
+                )?;
+                this.write_scalar(handle.to_scalar(this), dest)?;
+            }
+            "GetFileInformationByHandle" => {
+                let [handle, info] = this.check_shim(abi, sys_conv, link_name, args)?;
+                let res = this.GetFileInformationByHandle(handle, info)?;
+                this.write_scalar(res, dest)?;
+            }
 
             // Allocation
             "HeapAlloc" => {
@@ -493,7 +519,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
             "SetThreadDescription" => {
                 let [handle, name] = this.check_shim(abi, sys_conv, link_name, args)?;
 
-                let handle = this.read_handle(handle)?;
+                let handle = this.read_handle(handle, "SetThreadDescription")?;
                 let name = this.read_wide_str(this.read_pointer(name)?)?;
 
                 let thread = match handle {
@@ -508,7 +534,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
             "GetThreadDescription" => {
                 let [handle, name_ptr] = this.check_shim(abi, sys_conv, link_name, args)?;
 
-                let handle = this.read_handle(handle)?;
+                let handle = this.read_handle(handle, "GetThreadDescription")?;
                 let name_ptr = this.deref_pointer_as(name_ptr, this.machine.layouts.mut_raw_ptr)?; // the pointer where we should store the ptr to the name
 
                 let thread = match handle {
@@ -618,7 +644,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
                 let [handle, filename, size] = this.check_shim(abi, sys_conv, link_name, args)?;
                 this.check_no_isolation("`GetModuleFileNameW`")?;
 
-                let handle = this.read_handle(handle)?;
+                let handle = this.read_handle(handle, "GetModuleFileNameW")?;
                 let filename = this.read_pointer(filename)?;
                 let size = this.read_scalar(size)?.to_u32()?;
 
diff --git a/src/tools/miri/src/shims/windows/fs.rs b/src/tools/miri/src/shims/windows/fs.rs
new file mode 100644
index 00000000000..32bab548969
--- /dev/null
+++ b/src/tools/miri/src/shims/windows/fs.rs
@@ -0,0 +1,402 @@
+use std::fs::{Metadata, OpenOptions};
+use std::io;
+use std::path::PathBuf;
+use std::time::SystemTime;
+
+use bitflags::bitflags;
+
+use crate::shims::files::{FileDescription, FileHandle};
+use crate::shims::windows::handle::{EvalContextExt as _, Handle};
+use crate::*;
+
+#[derive(Debug)]
+pub struct DirHandle {
+    pub(crate) path: PathBuf,
+}
+
+impl FileDescription for DirHandle {
+    fn name(&self) -> &'static str {
+        "directory"
+    }
+
+    fn metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result<Metadata>> {
+        interp_ok(self.path.metadata())
+    }
+
+    fn close<'tcx>(
+        self,
+        _communicate_allowed: bool,
+        _ecx: &mut MiriInterpCx<'tcx>,
+    ) -> InterpResult<'tcx, io::Result<()>> {
+        interp_ok(Ok(()))
+    }
+}
+
+/// Windows supports handles without any read/write/delete permissions - these handles can get
+/// metadata, but little else. We represent that by storing the metadata from the time the handle
+/// was opened.
+#[derive(Debug)]
+pub struct MetadataHandle {
+    pub(crate) meta: Metadata,
+}
+
+impl FileDescription for MetadataHandle {
+    fn name(&self) -> &'static str {
+        "metadata-only"
+    }
+
+    fn metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result<Metadata>> {
+        interp_ok(Ok(self.meta.clone()))
+    }
+
+    fn close<'tcx>(
+        self,
+        _communicate_allowed: bool,
+        _ecx: &mut MiriInterpCx<'tcx>,
+    ) -> InterpResult<'tcx, io::Result<()>> {
+        interp_ok(Ok(()))
+    }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+enum CreationDisposition {
+    CreateAlways,
+    CreateNew,
+    OpenAlways,
+    OpenExisting,
+    TruncateExisting,
+}
+
+impl CreationDisposition {
+    fn new<'tcx>(
+        value: u32,
+        ecx: &mut MiriInterpCx<'tcx>,
+    ) -> InterpResult<'tcx, CreationDisposition> {
+        let create_always = ecx.eval_windows_u32("c", "CREATE_ALWAYS");
+        let create_new = ecx.eval_windows_u32("c", "CREATE_NEW");
+        let open_always = ecx.eval_windows_u32("c", "OPEN_ALWAYS");
+        let open_existing = ecx.eval_windows_u32("c", "OPEN_EXISTING");
+        let truncate_existing = ecx.eval_windows_u32("c", "TRUNCATE_EXISTING");
+
+        let out = if value == create_always {
+            CreationDisposition::CreateAlways
+        } else if value == create_new {
+            CreationDisposition::CreateNew
+        } else if value == open_always {
+            CreationDisposition::OpenAlways
+        } else if value == open_existing {
+            CreationDisposition::OpenExisting
+        } else if value == truncate_existing {
+            CreationDisposition::TruncateExisting
+        } else {
+            throw_unsup_format!("CreateFileW: Unsupported creation disposition: {value}");
+        };
+        interp_ok(out)
+    }
+}
+
+bitflags! {
+    #[derive(PartialEq)]
+    struct FileAttributes: u32 {
+        const ZERO = 0;
+        const NORMAL = 1 << 0;
+        /// This must be passed to allow getting directory handles. If not passed, we error on trying
+        /// to open directories
+        const BACKUP_SEMANTICS = 1 << 1;
+        /// Open a reparse point as a regular file - this is basically similar to 'readlink' in Unix
+        /// terminology. A reparse point is a file with custom logic when navigated to, of which
+        /// a symlink is one specific example.
+        const OPEN_REPARSE = 1 << 2;
+    }
+}
+
+impl FileAttributes {
+    fn new<'tcx>(
+        mut value: u32,
+        ecx: &mut MiriInterpCx<'tcx>,
+    ) -> InterpResult<'tcx, FileAttributes> {
+        let file_attribute_normal = ecx.eval_windows_u32("c", "FILE_ATTRIBUTE_NORMAL");
+        let file_flag_backup_semantics = ecx.eval_windows_u32("c", "FILE_FLAG_BACKUP_SEMANTICS");
+        let file_flag_open_reparse_point =
+            ecx.eval_windows_u32("c", "FILE_FLAG_OPEN_REPARSE_POINT");
+
+        let mut out = FileAttributes::ZERO;
+        if value & file_flag_backup_semantics != 0 {
+            value &= !file_flag_backup_semantics;
+            out |= FileAttributes::BACKUP_SEMANTICS;
+        }
+        if value & file_flag_open_reparse_point != 0 {
+            value &= !file_flag_open_reparse_point;
+            out |= FileAttributes::OPEN_REPARSE;
+        }
+        if value & file_attribute_normal != 0 {
+            value &= !file_attribute_normal;
+            out |= FileAttributes::NORMAL;
+        }
+
+        if value != 0 {
+            throw_unsup_format!("CreateFileW: Unsupported flags_and_attributes: {value}");
+        }
+
+        if out == FileAttributes::ZERO {
+            // NORMAL is equivalent to 0. Avoid needing to check both cases by unifying the two.
+            out = FileAttributes::NORMAL;
+        }
+        interp_ok(out)
+    }
+}
+
+impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
+#[allow(non_snake_case)]
+pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
+    fn CreateFileW(
+        &mut self,
+        file_name: &OpTy<'tcx>,            // LPCWSTR
+        desired_access: &OpTy<'tcx>,       // DWORD
+        share_mode: &OpTy<'tcx>,           // DWORD
+        security_attributes: &OpTy<'tcx>,  // LPSECURITY_ATTRIBUTES
+        creation_disposition: &OpTy<'tcx>, // DWORD
+        flags_and_attributes: &OpTy<'tcx>, // DWORD
+        template_file: &OpTy<'tcx>,        // HANDLE
+    ) -> InterpResult<'tcx, Handle> {
+        // ^ Returns HANDLE
+        use CreationDisposition::*;
+
+        let this = self.eval_context_mut();
+        this.assert_target_os("windows", "CreateFileW");
+        this.check_no_isolation("`CreateFileW`")?;
+
+        // This function appears to always set the error to 0. This is important for some flag
+        // combinations, which may set error code on success.
+        this.set_last_error(IoError::Raw(Scalar::from_i32(0)))?;
+
+        let file_name = this.read_path_from_wide_str(this.read_pointer(file_name)?)?;
+        let mut desired_access = this.read_scalar(desired_access)?.to_u32()?;
+        let share_mode = this.read_scalar(share_mode)?.to_u32()?;
+        let security_attributes = this.read_pointer(security_attributes)?;
+        let creation_disposition = this.read_scalar(creation_disposition)?.to_u32()?;
+        let flags_and_attributes = this.read_scalar(flags_and_attributes)?.to_u32()?;
+        let template_file = this.read_target_usize(template_file)?;
+
+        let generic_read = this.eval_windows_u32("c", "GENERIC_READ");
+        let generic_write = this.eval_windows_u32("c", "GENERIC_WRITE");
+
+        let file_share_delete = this.eval_windows_u32("c", "FILE_SHARE_DELETE");
+        let file_share_read = this.eval_windows_u32("c", "FILE_SHARE_READ");
+        let file_share_write = this.eval_windows_u32("c", "FILE_SHARE_WRITE");
+
+        let creation_disposition = CreationDisposition::new(creation_disposition, this)?;
+        let attributes = FileAttributes::new(flags_and_attributes, this)?;
+
+        if share_mode != (file_share_delete | file_share_read | file_share_write) {
+            throw_unsup_format!("CreateFileW: Unsupported share mode: {share_mode}");
+        }
+        if !this.ptr_is_null(security_attributes)? {
+            throw_unsup_format!("CreateFileW: Security attributes are not supported");
+        }
+
+        if attributes.contains(FileAttributes::OPEN_REPARSE) && creation_disposition == CreateAlways
+        {
+            throw_machine_stop!(TerminationInfo::Abort("Invalid CreateFileW argument combination: FILE_FLAG_OPEN_REPARSE_POINT with CREATE_ALWAYS".to_string()));
+        }
+
+        if template_file != 0 {
+            throw_unsup_format!("CreateFileW: Template files are not supported");
+        }
+
+        // We need to know if the file is a directory to correctly open directory handles.
+        // This is racy, but currently the stdlib doesn't appear to offer a better solution.
+        let is_dir = file_name.is_dir();
+
+        // BACKUP_SEMANTICS is how Windows calls the act of opening a directory handle.
+        if !attributes.contains(FileAttributes::BACKUP_SEMANTICS) && is_dir {
+            this.set_last_error(IoError::WindowsError("ERROR_ACCESS_DENIED"))?;
+            return interp_ok(Handle::Invalid);
+        }
+
+        let desired_read = desired_access & generic_read != 0;
+        let desired_write = desired_access & generic_write != 0;
+
+        let mut options = OpenOptions::new();
+        if desired_read {
+            desired_access &= !generic_read;
+            options.read(true);
+        }
+        if desired_write {
+            desired_access &= !generic_write;
+            options.write(true);
+        }
+
+        if desired_access != 0 {
+            throw_unsup_format!(
+                "CreateFileW: Unsupported bits set for access mode: {desired_access:#x}"
+            );
+        }
+
+        // Per the documentation:
+        // If the specified file exists and is writable, the function truncates the file,
+        // the function succeeds, and last-error code is set to ERROR_ALREADY_EXISTS.
+        // If the specified file does not exist and is a valid path, a new file is created,
+        // the function succeeds, and the last-error code is set to zero.
+        // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew
+        //
+        // This is racy, but there doesn't appear to be an std API that both succeeds if a
+        // file exists but tells us it isn't new. Either we accept racing one way or another,
+        // or we use an iffy heuristic like file creation time. This implementation prefers
+        // to fail in the direction of erroring more often.
+        if let CreateAlways | OpenAlways = creation_disposition
+            && file_name.exists()
+        {
+            this.set_last_error(IoError::WindowsError("ERROR_ALREADY_EXISTS"))?;
+        }
+
+        let handle = if is_dir {
+            // Open this as a directory.
+            let fd_num = this.machine.fds.insert_new(DirHandle { path: file_name });
+            Ok(Handle::File(fd_num))
+        } else if creation_disposition == OpenExisting && !(desired_read || desired_write) {
+            // Windows supports handles with no permissions. These allow things such as reading
+            // metadata, but not file content.
+            file_name.metadata().map(|meta| {
+                let fd_num = this.machine.fds.insert_new(MetadataHandle { meta });
+                Handle::File(fd_num)
+            })
+        } else {
+            // Open this as a standard file.
+            match creation_disposition {
+                CreateAlways | OpenAlways => {
+                    options.create(true);
+                    if creation_disposition == CreateAlways {
+                        options.truncate(true);
+                    }
+                }
+                CreateNew => {
+                    options.create_new(true);
+                    // Per `create_new` documentation:
+                    // The file must be opened with write or append access in order to create a new file.
+                    // https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.create_new
+                    if !desired_write {
+                        options.append(true);
+                    }
+                }
+                OpenExisting => {} // Default options
+                TruncateExisting => {
+                    options.truncate(true);
+                }
+            }
+
+            options.open(file_name).map(|file| {
+                let fd_num =
+                    this.machine.fds.insert_new(FileHandle { file, writable: desired_write });
+                Handle::File(fd_num)
+            })
+        };
+
+        match handle {
+            Ok(handle) => interp_ok(handle),
+            Err(e) => {
+                this.set_last_error(e)?;
+                interp_ok(Handle::Invalid)
+            }
+        }
+    }
+
+    fn GetFileInformationByHandle(
+        &mut self,
+        file: &OpTy<'tcx>,             // HANDLE
+        file_information: &OpTy<'tcx>, // LPBY_HANDLE_FILE_INFORMATION
+    ) -> InterpResult<'tcx, Scalar> {
+        // ^ Returns BOOL (i32 on Windows)
+        let this = self.eval_context_mut();
+        this.assert_target_os("windows", "GetFileInformationByHandle");
+        this.check_no_isolation("`GetFileInformationByHandle`")?;
+
+        let file = this.read_handle(file, "GetFileInformationByHandle")?;
+        let file_information = this.deref_pointer_as(
+            file_information,
+            this.windows_ty_layout("BY_HANDLE_FILE_INFORMATION"),
+        )?;
+
+        let fd_num = if let Handle::File(fd_num) = file {
+            fd_num
+        } else {
+            this.invalid_handle("GetFileInformationByHandle")?
+        };
+
+        let Some(desc) = this.machine.fds.get(fd_num) else {
+            this.invalid_handle("GetFileInformationByHandle")?
+        };
+
+        let metadata = match desc.metadata()? {
+            Ok(meta) => meta,
+            Err(e) => {
+                this.set_last_error(e)?;
+                return interp_ok(this.eval_windows("c", "FALSE"));
+            }
+        };
+
+        let size = metadata.len();
+
+        let file_type = metadata.file_type();
+        let attributes = if file_type.is_dir() {
+            this.eval_windows_u32("c", "FILE_ATTRIBUTE_DIRECTORY")
+        } else if file_type.is_file() {
+            this.eval_windows_u32("c", "FILE_ATTRIBUTE_NORMAL")
+        } else {
+            this.eval_windows_u32("c", "FILE_ATTRIBUTE_DEVICE")
+        };
+
+        // Per the Windows documentation:
+        // "If the underlying file system does not support the [...] time, this member is zero (0)."
+        // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/ns-fileapi-by_handle_file_information
+        let created = extract_windows_epoch(this, metadata.created())?.unwrap_or((0, 0));
+        let accessed = extract_windows_epoch(this, metadata.accessed())?.unwrap_or((0, 0));
+        let written = extract_windows_epoch(this, metadata.modified())?.unwrap_or((0, 0));
+
+        this.write_int_fields_named(&[("dwFileAttributes", attributes.into())], &file_information)?;
+        write_filetime_field(this, &file_information, "ftCreationTime", created)?;
+        write_filetime_field(this, &file_information, "ftLastAccessTime", accessed)?;
+        write_filetime_field(this, &file_information, "ftLastWriteTime", written)?;
+        this.write_int_fields_named(
+            &[
+                ("dwVolumeSerialNumber", 0),
+                ("nFileSizeHigh", (size >> 32).into()),
+                ("nFileSizeLow", (size & 0xFFFFFFFF).into()),
+                ("nNumberOfLinks", 1),
+                ("nFileIndexHigh", 0),
+                ("nFileIndexLow", 0),
+            ],
+            &file_information,
+        )?;
+
+        interp_ok(this.eval_windows("c", "TRUE"))
+    }
+}
+
+/// Windows FILETIME is measured in 100-nanosecs since 1601
+fn extract_windows_epoch<'tcx>(
+    ecx: &MiriInterpCx<'tcx>,
+    time: io::Result<SystemTime>,
+) -> InterpResult<'tcx, Option<(u32, u32)>> {
+    match time.ok() {
+        Some(time) => {
+            let duration = ecx.system_time_since_windows_epoch(&time)?;
+            let duration_ticks = ecx.windows_ticks_for(duration)?;
+            #[allow(clippy::cast_possible_truncation)]
+            interp_ok(Some((duration_ticks as u32, (duration_ticks >> 32) as u32)))
+        }
+        None => interp_ok(None),
+    }
+}
+
+fn write_filetime_field<'tcx>(
+    cx: &mut MiriInterpCx<'tcx>,
+    val: &MPlaceTy<'tcx>,
+    name: &str,
+    (low, high): (u32, u32),
+) -> InterpResult<'tcx> {
+    cx.write_int_fields_named(
+        &[("dwLowDateTime", low.into()), ("dwHighDateTime", high.into())],
+        &cx.project_field_named(val, name)?,
+    )
+}
diff --git a/src/tools/miri/src/shims/windows/handle.rs b/src/tools/miri/src/shims/windows/handle.rs
index cac67c888f8..eec6c62bebc 100644
--- a/src/tools/miri/src/shims/windows/handle.rs
+++ b/src/tools/miri/src/shims/windows/handle.rs
@@ -1,9 +1,9 @@
 use std::mem::variant_count;
-use std::panic::Location;
 
 use rustc_abi::HasDataLayout;
 
 use crate::concurrency::thread::ThreadNotFound;
+use crate::shims::files::FdNum;
 use crate::*;
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
@@ -17,7 +17,7 @@ pub enum Handle {
     Null,
     Pseudo(PseudoHandle),
     Thread(ThreadId),
-    File(i32),
+    File(FdNum),
     Invalid,
 }
 
@@ -51,6 +51,8 @@ impl Handle {
     const PSEUDO_DISCRIMINANT: u32 = 1;
     const THREAD_DISCRIMINANT: u32 = 2;
     const FILE_DISCRIMINANT: u32 = 3;
+    // Chosen to ensure Handle::Invalid encodes to -1. Update this value if there are ever more than
+    // 8 discriminants.
     const INVALID_DISCRIMINANT: u32 = 7;
 
     fn discriminant(self) -> u32 {
@@ -70,20 +72,25 @@ impl Handle {
             Self::Thread(thread) => thread.to_u32(),
             #[expect(clippy::cast_sign_loss)]
             Self::File(fd) => fd as u32,
+            // INVALID_HANDLE_VALUE is -1. This fact is explicitly declared or implied in several
+            // pages of Windows documentation.
+            // 1: https://learn.microsoft.com/en-us/dotnet/api/microsoft.win32.safehandles.safefilehandle?view=net-9.0
+            // 2: https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/get-osfhandle?view=msvc-170
             Self::Invalid => 0x1FFFFFFF,
         }
     }
 
     fn packed_disc_size() -> u32 {
-        // ceil(log2(x)) is how many bits it takes to store x numbers
-        // We ensure that INVALID_HANDLE_VALUE (0xFFFFFFFF) decodes to Handle::Invalid
-        // see https://devblogs.microsoft.com/oldnewthing/20230914-00/?p=108766
+        // ceil(log2(x)) is how many bits it takes to store x numbers.
+        // We ensure that INVALID_HANDLE_VALUE (0xFFFFFFFF) decodes to Handle::Invalid.
+        // see https://devblogs.microsoft.com/oldnewthing/20230914-00/?p=108766 for more detail on
+        // INVALID_HANDLE_VALUE.
         let variant_count = variant_count::<Self>();
 
-        // however, std's ilog2 is floor(log2(x))
+        // However, std's ilog2 is floor(log2(x)).
         let floor_log2 = variant_count.ilog2();
 
-        // we need to add one for non powers of two to compensate for the difference
+        // We need to add one for non powers of two to compensate for the difference.
         #[expect(clippy::arithmetic_side_effects)] // cannot overflow
         if variant_count.is_power_of_two() { floor_log2 } else { floor_log2 + 1 }
     }
@@ -105,7 +112,7 @@ impl Handle {
         assert!(discriminant < 2u32.pow(disc_size));
 
         // make sure the data fits into `data_size` bits
-        assert!(data <= 2u32.pow(data_size));
+        assert!(data < 2u32.pow(data_size));
 
         // packs the data into the lower `data_size` bits
         // and packs the discriminant right above the data
@@ -118,7 +125,11 @@ impl Handle {
             Self::PSEUDO_DISCRIMINANT => Some(Self::Pseudo(PseudoHandle::from_value(data)?)),
             Self::THREAD_DISCRIMINANT => Some(Self::Thread(ThreadId::new_unchecked(data))),
             #[expect(clippy::cast_possible_wrap)]
-            Self::FILE_DISCRIMINANT => Some(Self::File(data as i32)),
+            Self::FILE_DISCRIMINANT => {
+                // This cast preserves all bits.
+                assert_eq!(size_of_val(&data), size_of::<FdNum>());
+                Some(Self::File(data as FdNum))
+            }
             Self::INVALID_DISCRIMINANT => Some(Self::Invalid),
             _ => None,
         }
@@ -154,7 +165,7 @@ impl Handle {
     /// Structurally invalid handles return [`HandleError::InvalidHandle`].
     /// If the handle is structurally valid but semantically invalid, e.g. a for non-existent thread
     /// ID, returns [`HandleError::ThreadNotFound`].
-    pub fn try_from_scalar<'tcx>(
+    fn try_from_scalar<'tcx>(
         handle: Scalar,
         cx: &MiriInterpCx<'tcx>,
     ) -> InterpResult<'tcx, Result<Self, HandleError>> {
@@ -186,22 +197,23 @@ impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
 
 #[allow(non_snake_case)]
 pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
+    /// Convert a scalar into a structured `Handle`.
+    /// If the handle is invalid, or references a non-existent item, execution is aborted.
     #[track_caller]
-    fn read_handle(&self, handle: &OpTy<'tcx>) -> InterpResult<'tcx, Handle> {
+    fn read_handle(&self, handle: &OpTy<'tcx>, function_name: &str) -> InterpResult<'tcx, Handle> {
         let this = self.eval_context_ref();
         let handle = this.read_scalar(handle)?;
         match Handle::try_from_scalar(handle, this)? {
             Ok(handle) => interp_ok(handle),
             Err(HandleError::InvalidHandle) =>
                 throw_machine_stop!(TerminationInfo::Abort(format!(
-                    "invalid handle {} at {}",
+                    "invalid handle {} passed to {function_name}",
                     handle.to_target_isize(this)?,
-                    Location::caller(),
                 ))),
             Err(HandleError::ThreadNotFound(_)) =>
                 throw_machine_stop!(TerminationInfo::Abort(format!(
-                    "invalid thread ID: {}",
-                    Location::caller()
+                    "invalid thread ID {} passed to {function_name}",
+                    handle.to_target_isize(this)?,
                 ))),
         }
     }
@@ -215,15 +227,15 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
     fn CloseHandle(&mut self, handle_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
         let this = self.eval_context_mut();
 
-        let handle = this.read_handle(handle_op)?;
+        let handle = this.read_handle(handle_op, "CloseHandle")?;
         let ret = match handle {
             Handle::Thread(thread) => {
                 this.detach_thread(thread, /*allow_terminated_joined*/ true)?;
                 this.eval_windows("c", "TRUE")
             }
-            Handle::File(fd) =>
-                if let Some(file) = this.machine.fds.get(fd) {
-                    let err = file.close(this.machine.communicate(), this)?;
+            Handle::File(fd_num) =>
+                if let Some(fd) = this.machine.fds.remove(fd_num) {
+                    let err = fd.close_ref(this.machine.communicate(), this)?;
                     if let Err(e) = err {
                         this.set_last_error(e)?;
                         this.eval_windows("c", "FALSE")
@@ -239,3 +251,14 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
         interp_ok(ret)
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_invalid_encoding() {
+        // Ensure the invalid handle encodes to `u32::MAX`/`INVALID_HANDLE_VALUE`.
+        assert_eq!(Handle::Invalid.to_packed(), u32::MAX)
+    }
+}
diff --git a/src/tools/miri/src/shims/windows/mod.rs b/src/tools/miri/src/shims/windows/mod.rs
index 892bd6924fc..442c5a0dd11 100644
--- a/src/tools/miri/src/shims/windows/mod.rs
+++ b/src/tools/miri/src/shims/windows/mod.rs
@@ -1,12 +1,14 @@
 pub mod foreign_items;
 
 mod env;
+mod fs;
 mod handle;
 mod sync;
 mod thread;
 
 // All the Windows-specific extension traits
 pub use self::env::{EvalContextExt as _, WindowsEnvVars};
+pub use self::fs::EvalContextExt as _;
 pub use self::handle::EvalContextExt as _;
 pub use self::sync::EvalContextExt as _;
 pub use self::thread::EvalContextExt as _;
diff --git a/src/tools/miri/src/shims/windows/thread.rs b/src/tools/miri/src/shims/windows/thread.rs
index 8289eea3412..d5f9ed4e968 100644
--- a/src/tools/miri/src/shims/windows/thread.rs
+++ b/src/tools/miri/src/shims/windows/thread.rs
@@ -62,7 +62,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
     ) -> InterpResult<'tcx, Scalar> {
         let this = self.eval_context_mut();
 
-        let handle = this.read_handle(handle_op)?;
+        let handle = this.read_handle(handle_op, "WaitForSingleObject")?;
         let timeout = this.read_scalar(timeout_op)?.to_u32()?;
 
         let thread = match handle {
diff --git a/src/tools/miri/test_dependencies/Cargo.toml b/src/tools/miri/test_dependencies/Cargo.toml
index 7e16592ca7a..3427ef16523 100644
--- a/src/tools/miri/test_dependencies/Cargo.toml
+++ b/src/tools/miri/test_dependencies/Cargo.toml
@@ -25,6 +25,6 @@ page_size = "0.6"
 tokio = { version = "1.24", features = ["macros", "rt-multi-thread", "time", "net", "fs", "sync", "signal", "io-util"] }
 
 [target.'cfg(windows)'.dependencies]
-windows-sys = { version = "0.59", features = [ "Win32_Foundation", "Win32_System_Threading" ] }
+windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_System_Threading", "Win32_Storage_FileSystem", "Win32_Security"] }
 
 [workspace]
diff --git a/src/tools/miri/tests/fail-dep/concurrency/windows_join_main.rs b/src/tools/miri/tests/fail-dep/concurrency/windows_join_main.rs
index 279201df867..3ee2bf14f9f 100644
--- a/src/tools/miri/tests/fail-dep/concurrency/windows_join_main.rs
+++ b/src/tools/miri/tests/fail-dep/concurrency/windows_join_main.rs
@@ -13,7 +13,7 @@ use windows_sys::Win32::System::Threading::{INFINITE, WaitForSingleObject};
 // XXX HACK: This is how miri represents the handle for thread 0.
 // This value can be "legitimately" obtained by using `GetCurrentThread` with `DuplicateHandle`
 // but miri does not implement `DuplicateHandle` yet.
-const MAIN_THREAD: HANDLE = (2i32 << 30) as HANDLE;
+const MAIN_THREAD: HANDLE = (2i32 << 29) as HANDLE;
 
 fn main() {
     thread::spawn(|| {
diff --git a/src/tools/miri/tests/pass-dep/shims/windows-fs.rs b/src/tools/miri/tests/pass-dep/shims/windows-fs.rs
new file mode 100644
index 00000000000..312df9eb115
--- /dev/null
+++ b/src/tools/miri/tests/pass-dep/shims/windows-fs.rs
@@ -0,0 +1,198 @@
+//@only-target: windows # this directly tests windows-only functions
+//@compile-flags: -Zmiri-disable-isolation
+#![allow(nonstandard_style)]
+
+use std::os::windows::ffi::OsStrExt;
+use std::path::Path;
+use std::ptr;
+
+#[path = "../../utils/mod.rs"]
+mod utils;
+
+use windows_sys::Win32::Foundation::{
+    CloseHandle, ERROR_ALREADY_EXISTS, GENERIC_READ, GENERIC_WRITE, GetLastError,
+};
+use windows_sys::Win32::Storage::FileSystem::{
+    BY_HANDLE_FILE_INFORMATION, CREATE_ALWAYS, CREATE_NEW, CreateFileW, FILE_ATTRIBUTE_DIRECTORY,
+    FILE_ATTRIBUTE_NORMAL, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT,
+    FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, GetFileInformationByHandle, OPEN_ALWAYS,
+    OPEN_EXISTING,
+};
+
+fn main() {
+    unsafe {
+        test_create_dir_file();
+        test_create_normal_file();
+        test_create_always_twice();
+        test_open_always_twice();
+        test_open_dir_reparse();
+    }
+}
+
+unsafe fn test_create_dir_file() {
+    let temp = utils::tmp();
+    let raw_path = to_wide_cstr(&temp);
+    // Open the `temp` directory.
+    let handle = CreateFileW(
+        raw_path.as_ptr(),
+        GENERIC_READ,
+        FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
+        ptr::null_mut(),
+        OPEN_EXISTING,
+        FILE_FLAG_BACKUP_SEMANTICS,
+        ptr::null_mut(),
+    );
+    assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
+    let mut info = std::mem::zeroed::<BY_HANDLE_FILE_INFORMATION>();
+    if GetFileInformationByHandle(handle, &mut info) == 0 {
+        panic!("Failed to get file information")
+    };
+    assert!(info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0);
+    if CloseHandle(handle) == 0 {
+        panic!("Failed to close file")
+    };
+}
+
+unsafe fn test_create_normal_file() {
+    let temp = utils::tmp().join("test.txt");
+    let raw_path = to_wide_cstr(&temp);
+    let handle = CreateFileW(
+        raw_path.as_ptr(),
+        GENERIC_READ | GENERIC_WRITE,
+        FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
+        ptr::null_mut(),
+        CREATE_NEW,
+        0,
+        ptr::null_mut(),
+    );
+    assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
+    let mut info = std::mem::zeroed::<BY_HANDLE_FILE_INFORMATION>();
+    if GetFileInformationByHandle(handle, &mut info) == 0 {
+        panic!("Failed to get file information: {}", GetLastError())
+    };
+    assert!(info.dwFileAttributes & FILE_ATTRIBUTE_NORMAL != 0);
+    if CloseHandle(handle) == 0 {
+        panic!("Failed to close file")
+    };
+
+    // Test metadata-only handle
+    let handle = CreateFileW(
+        raw_path.as_ptr(),
+        0,
+        FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
+        ptr::null_mut(),
+        OPEN_EXISTING,
+        0,
+        ptr::null_mut(),
+    );
+    assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
+    let mut info = std::mem::zeroed::<BY_HANDLE_FILE_INFORMATION>();
+    if GetFileInformationByHandle(handle, &mut info) == 0 {
+        panic!("Failed to get file information: {}", GetLastError())
+    };
+    assert!(info.dwFileAttributes & FILE_ATTRIBUTE_NORMAL != 0);
+    if CloseHandle(handle) == 0 {
+        panic!("Failed to close file")
+    };
+}
+
+/// Tests that CREATE_ALWAYS sets the error value correctly based on whether the file already exists
+unsafe fn test_create_always_twice() {
+    let temp = utils::tmp().join("test_create_always.txt");
+    let raw_path = to_wide_cstr(&temp);
+    let handle = CreateFileW(
+        raw_path.as_ptr(),
+        GENERIC_READ | GENERIC_WRITE,
+        FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
+        ptr::null_mut(),
+        CREATE_ALWAYS,
+        0,
+        ptr::null_mut(),
+    );
+    assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
+    assert_eq!(GetLastError(), 0);
+    if CloseHandle(handle) == 0 {
+        panic!("Failed to close file")
+    };
+
+    let handle = CreateFileW(
+        raw_path.as_ptr(),
+        GENERIC_READ | GENERIC_WRITE,
+        FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
+        ptr::null_mut(),
+        CREATE_ALWAYS,
+        0,
+        ptr::null_mut(),
+    );
+    assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
+    assert_eq!(GetLastError(), ERROR_ALREADY_EXISTS);
+    if CloseHandle(handle) == 0 {
+        panic!("Failed to close file")
+    };
+}
+
+/// Tests that OPEN_ALWAYS sets the error value correctly based on whether the file already exists
+unsafe fn test_open_always_twice() {
+    let temp = utils::tmp().join("test_open_always.txt");
+    let raw_path = to_wide_cstr(&temp);
+    let handle = CreateFileW(
+        raw_path.as_ptr(),
+        GENERIC_READ | GENERIC_WRITE,
+        FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
+        ptr::null_mut(),
+        OPEN_ALWAYS,
+        0,
+        ptr::null_mut(),
+    );
+    assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
+    assert_eq!(GetLastError(), 0);
+    if CloseHandle(handle) == 0 {
+        panic!("Failed to close file")
+    };
+
+    let handle = CreateFileW(
+        raw_path.as_ptr(),
+        GENERIC_READ | GENERIC_WRITE,
+        FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
+        ptr::null_mut(),
+        OPEN_ALWAYS,
+        0,
+        ptr::null_mut(),
+    );
+    assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
+    assert_eq!(GetLastError(), ERROR_ALREADY_EXISTS);
+    if CloseHandle(handle) == 0 {
+        panic!("Failed to close file")
+    };
+}
+
+// TODO: Once we support more of the std API, it would be nice to test against an actual symlink
+unsafe fn test_open_dir_reparse() {
+    let temp = utils::tmp();
+    let raw_path = to_wide_cstr(&temp);
+    // Open the `temp` directory.
+    let handle = CreateFileW(
+        raw_path.as_ptr(),
+        GENERIC_READ,
+        FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
+        ptr::null_mut(),
+        OPEN_EXISTING,
+        FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
+        ptr::null_mut(),
+    );
+    assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
+    let mut info = std::mem::zeroed::<BY_HANDLE_FILE_INFORMATION>();
+    if GetFileInformationByHandle(handle, &mut info) == 0 {
+        panic!("Failed to get file information")
+    };
+    assert!(info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0);
+    if CloseHandle(handle) == 0 {
+        panic!("Failed to close file")
+    };
+}
+
+fn to_wide_cstr(path: &Path) -> Vec<u16> {
+    let mut raw_path = path.as_os_str().encode_wide().collect::<Vec<_>>();
+    raw_path.extend([0, 0]);
+    raw_path
+}
diff --git a/src/tools/miri/tests/pass/shims/fs.rs b/src/tools/miri/tests/pass/shims/fs.rs
index 289c6aa2fce..6ad23055f30 100644
--- a/src/tools/miri/tests/pass/shims/fs.rs
+++ b/src/tools/miri/tests/pass/shims/fs.rs
@@ -1,4 +1,3 @@
-//@ignore-target: windows # File handling is not implemented yet
 //@compile-flags: -Zmiri-disable-isolation
 
 #![feature(io_error_more)]
@@ -18,20 +17,23 @@ mod utils;
 
 fn main() {
     test_path_conversion();
-    test_file();
-    test_file_clone();
-    test_file_create_new();
-    test_seek();
-    test_metadata();
-    test_file_set_len();
-    test_file_sync();
-    test_errors();
-    test_rename();
-    test_directory();
-    test_canonicalize();
-    test_from_raw_os_error();
-    #[cfg(unix)]
-    test_pread_pwrite();
+    // Windows file handling is very incomplete.
+    if cfg!(not(windows)) {
+        test_file();
+        test_file_create_new();
+        test_seek();
+        test_file_clone();
+        test_metadata();
+        test_file_set_len();
+        test_file_sync();
+        test_errors();
+        test_rename();
+        test_directory();
+        test_canonicalize();
+        test_from_raw_os_error();
+        #[cfg(unix)]
+        test_pread_pwrite();
+    }
 }
 
 fn test_path_conversion() {
@@ -144,10 +146,10 @@ fn test_metadata() {
     let path = utils::prepare_with_content("miri_test_fs_metadata.txt", bytes);
 
     // Test that metadata of an absolute path is correct.
-    check_metadata(bytes, &path).unwrap();
+    check_metadata(bytes, &path).expect("absolute path metadata");
     // Test that metadata of a relative path is correct.
     std::env::set_current_dir(path.parent().unwrap()).unwrap();
-    check_metadata(bytes, Path::new(path.file_name().unwrap())).unwrap();
+    check_metadata(bytes, Path::new(path.file_name().unwrap())).expect("relative path metadata");
 
     // Removing file should succeed.
     remove_file(&path).unwrap();