about summary refs log tree commit diff
diff options
context:
space:
mode:
authorRune Tynan <runetynan@gmail.com>2025-04-09 18:55:36 -0700
committerRalf Jung <post@ralfj.de>2025-05-22 09:11:08 +0200
commit1e796370508c67dd7a9cf68c7dc6f2b199312372 (patch)
tree2d75671ae78f886ac4d2dc0adcff2a24687b2845
parent268a3694244e53c93f8dde8f58f19caf54734360 (diff)
downloadrust-1e796370508c67dd7a9cf68c7dc6f2b199312372.tar.gz
rust-1e796370508c67dd7a9cf68c7dc6f2b199312372.zip
Implement file read/write on Windows
-rw-r--r--src/tools/miri/src/machine.rs5
-rw-r--r--src/tools/miri/src/shims/io_error.rs24
-rw-r--r--src/tools/miri/src/shims/windows/foreign_items.rs110
-rw-r--r--src/tools/miri/src/shims/windows/fs.rs262
-rw-r--r--src/tools/miri/src/shims/windows/handle.rs24
-rw-r--r--src/tools/miri/test_dependencies/Cargo.toml9
-rw-r--r--src/tools/miri/tests/pass-dep/shims/windows-fs.rs97
-rw-r--r--src/tools/miri/tests/pass/shims/fs.rs10
8 files changed, 460 insertions, 81 deletions
diff --git a/src/tools/miri/src/machine.rs b/src/tools/miri/src/machine.rs
index 1c6c7894cb4..f75adffd950 100644
--- a/src/tools/miri/src/machine.rs
+++ b/src/tools/miri/src/machine.rs
@@ -544,9 +544,6 @@ pub struct MiriMachine<'tcx> {
     /// Failure rate of compare_exchange_weak, between 0.0 and 1.0
     pub(crate) cmpxchg_weak_failure_rate: f64,
 
-    /// Corresponds to -Zmiri-mute-stdout-stderr and doesn't write the output but acts as if it succeeded.
-    pub(crate) mute_stdout_stderr: bool,
-
     /// The probability of the active thread being preempted at the end of each basic block.
     pub(crate) preemption_rate: f64,
 
@@ -722,7 +719,6 @@ impl<'tcx> MiriMachine<'tcx> {
             track_alloc_accesses: config.track_alloc_accesses,
             check_alignment: config.check_alignment,
             cmpxchg_weak_failure_rate: config.cmpxchg_weak_failure_rate,
-            mute_stdout_stderr: config.mute_stdout_stderr,
             preemption_rate: config.preemption_rate,
             report_progress: config.report_progress,
             basic_block_count: 0,
@@ -925,7 +921,6 @@ impl VisitProvenance for MiriMachine<'_> {
             track_alloc_accesses: _,
             check_alignment: _,
             cmpxchg_weak_failure_rate: _,
-            mute_stdout_stderr: _,
             preemption_rate: _,
             report_progress: _,
             basic_block_count: _,
diff --git a/src/tools/miri/src/shims/io_error.rs b/src/tools/miri/src/shims/io_error.rs
index acf3f74a93d..e597b527cb7 100644
--- a/src/tools/miri/src/shims/io_error.rs
+++ b/src/tools/miri/src/shims/io_error.rs
@@ -1,4 +1,5 @@
 use std::io;
+use std::io::ErrorKind;
 
 use crate::*;
 
@@ -13,6 +14,29 @@ pub enum IoError {
 }
 pub use self::IoError::*;
 
+impl IoError {
+    pub(crate) fn into_ntstatus(self) -> i32 {
+        let raw = match self {
+            HostError(e) =>
+                match e.kind() {
+                    // STATUS_MEDIA_WRITE_PROTECTED
+                    ErrorKind::ReadOnlyFilesystem => 0xC00000A2u32,
+                    // STATUS_FILE_INVALID
+                    ErrorKind::InvalidInput => 0xC0000098,
+                    // STATUS_DISK_FULL
+                    ErrorKind::QuotaExceeded => 0xC000007F,
+                    // STATUS_ACCESS_DENIED
+                    ErrorKind::PermissionDenied => 0xC0000022,
+                    // For the default error code we arbitrarily pick 0xC0000185, STATUS_IO_DEVICE_ERROR.
+                    _ => 0xC0000185,
+                },
+            // For the default error code we arbitrarily pick 0xC0000185, STATUS_IO_DEVICE_ERROR.
+            _ => 0xC0000185,
+        };
+        raw.cast_signed()
+    }
+}
+
 impl From<io::Error> for IoError {
     fn from(value: io::Error) -> Self {
         IoError::HostError(value)
diff --git a/src/tools/miri/src/shims/windows/foreign_items.rs b/src/tools/miri/src/shims/windows/foreign_items.rs
index c80858c6363..d822dd07fcd 100644
--- a/src/tools/miri/src/shims/windows/foreign_items.rs
+++ b/src/tools/miri/src/shims/windows/foreign_items.rs
@@ -195,69 +195,52 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
 
             // File related shims
             "NtWriteFile" => {
-                if !this.frame_in_std() {
-                    throw_unsup_format!(
-                        "`NtWriteFile` support is crude and just enough for stdout to work"
-                    );
-                }
-
                 let [
                     handle,
-                    _event,
-                    _apc_routine,
-                    _apc_context,
+                    event,
+                    apc_routine,
+                    apc_context,
                     io_status_block,
                     buf,
                     n,
                     byte_offset,
-                    _key,
+                    key,
                 ] = this.check_shim(abi, sys_conv, link_name, args)?;
-                let handle = this.read_target_isize(handle)?;
-                let buf = this.read_pointer(buf)?;
-                let n = this.read_scalar(n)?.to_u32()?;
-                let byte_offset = this.read_target_usize(byte_offset)?; // is actually a pointer
-                let io_status_block = this
-                    .deref_pointer_as(io_status_block, this.windows_ty_layout("IO_STATUS_BLOCK"))?;
-
-                if byte_offset != 0 {
-                    throw_unsup_format!(
-                        "`NtWriteFile` `ByteOffset` parameter is non-null, which is unsupported"
-                    );
-                }
-
-                let written = if handle == -11 || handle == -12 {
-                    // stdout/stderr
-                    use io::Write;
-
-                    let buf_cont =
-                        this.read_bytes_ptr_strip_provenance(buf, Size::from_bytes(u64::from(n)))?;
-                    let res = if this.machine.mute_stdout_stderr {
-                        Ok(buf_cont.len())
-                    } else if handle == -11 {
-                        io::stdout().write(buf_cont)
-                    } else {
-                        io::stderr().write(buf_cont)
-                    };
-                    // We write at most `n` bytes, which is a `u32`, so we cannot have written more than that.
-                    res.ok().map(|n| u32::try_from(n).unwrap())
-                } else {
-                    throw_unsup_format!(
-                        "on Windows, writing to anything except stdout/stderr is not supported"
-                    )
-                };
-                // We have to put the result into io_status_block.
-                if let Some(n) = written {
-                    let io_status_information =
-                        this.project_field_named(&io_status_block, "Information")?;
-                    this.write_scalar(
-                        Scalar::from_target_usize(n.into(), this),
-                        &io_status_information,
-                    )?;
-                }
-                // Return whether this was a success. >= 0 is success.
-                // For the error code we arbitrarily pick 0xC0000185, STATUS_IO_DEVICE_ERROR.
-                this.write_scalar(
-                    Scalar::from_u32(if written.is_some() { 0 } else { 0xC0000185u32 }),
+                this.NtWriteFile(
+                    handle,
+                    event,
+                    apc_routine,
+                    apc_context,
+                    io_status_block,
+                    buf,
+                    n,
+                    byte_offset,
+                    key,
+                    dest,
+                )?;
+            }
+            "NtReadFile" => {
+                let [
+                    handle,
+                    event,
+                    apc_routine,
+                    apc_context,
+                    io_status_block,
+                    buf,
+                    n,
+                    byte_offset,
+                    key,
+                ] = this.check_shim(abi, sys_conv, link_name, args)?;
+                this.NtReadFile(
+                    handle,
+                    event,
+                    apc_routine,
+                    apc_context,
+                    io_status_block,
+                    buf,
+                    n,
+                    byte_offset,
+                    key,
                     dest,
                 )?;
             }
@@ -322,6 +305,13 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
                 let res = this.DeleteFileW(file_name)?;
                 this.write_scalar(res, dest)?;
             }
+            "SetFilePointerEx" => {
+                let [file, distance_to_move, new_file_pointer, move_method] =
+                    this.check_shim(abi, sys_conv, link_name, args)?;
+                let res =
+                    this.SetFilePointerEx(file, distance_to_move, new_file_pointer, move_method)?;
+                this.write_scalar(res, dest)?;
+            }
 
             // Allocation
             "HeapAlloc" => {
@@ -700,12 +690,8 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
             }
             "GetStdHandle" => {
                 let [which] = this.check_shim(abi, sys_conv, link_name, args)?;
-                let which = this.read_scalar(which)?.to_i32()?;
-                // We just make this the identity function, so we know later in `NtWriteFile` which
-                // one it is. This is very fake, but libtest needs it so we cannot make it a
-                // std-only shim.
-                // FIXME: this should return real HANDLEs when io support is added
-                this.write_scalar(Scalar::from_target_isize(which.into(), this), dest)?;
+                let res = this.GetStdHandle(which)?;
+                this.write_scalar(res, dest)?;
             }
             "CloseHandle" => {
                 let [handle] = this.check_shim(abi, sys_conv, link_name, args)?;
diff --git a/src/tools/miri/src/shims/windows/fs.rs b/src/tools/miri/src/shims/windows/fs.rs
index 0ac82d55b10..72e016c12e9 100644
--- a/src/tools/miri/src/shims/windows/fs.rs
+++ b/src/tools/miri/src/shims/windows/fs.rs
@@ -1,5 +1,6 @@
 use std::fs::{Metadata, OpenOptions};
 use std::io;
+use std::io::SeekFrom;
 use std::path::PathBuf;
 use std::time::SystemTime;
 
@@ -390,6 +391,267 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
             }
         }
     }
+
+    fn NtWriteFile(
+        &mut self,
+        handle: &OpTy<'tcx>,          // HANDLE
+        event: &OpTy<'tcx>,           // HANDLE
+        apc_routine: &OpTy<'tcx>,     // PIO_APC_ROUTINE
+        apc_ctx: &OpTy<'tcx>,         // PVOID
+        io_status_block: &OpTy<'tcx>, // PIO_STATUS_BLOCK
+        buf: &OpTy<'tcx>,             // PVOID
+        n: &OpTy<'tcx>,               // ULONG
+        byte_offset: &OpTy<'tcx>,     // PLARGE_INTEGER
+        key: &OpTy<'tcx>,             // PULONG
+        dest: &MPlaceTy<'tcx>,        // return type: NTSTATUS
+    ) -> InterpResult<'tcx, ()> {
+        let this = self.eval_context_mut();
+        let handle = this.read_handle(handle, "NtWriteFile")?;
+        let event = this.read_handle(event, "NtWriteFile")?;
+        let apc_routine = this.read_pointer(apc_routine)?;
+        let apc_ctx = this.read_pointer(apc_ctx)?;
+        let buf = this.read_pointer(buf)?;
+        let count = this.read_scalar(n)?.to_u32()?;
+        let byte_offset = this.read_target_usize(byte_offset)?; // is actually a pointer, but we only support null
+        let key = this.read_pointer(key)?;
+        let io_status_block =
+            this.deref_pointer_as(io_status_block, this.windows_ty_layout("IO_STATUS_BLOCK"))?;
+
+        if event != Handle::Null {
+            throw_unsup_format!(
+                "`NtWriteFile` `Event` parameter is non-null, which is unsupported"
+            );
+        }
+
+        if !this.ptr_is_null(apc_routine)? {
+            throw_unsup_format!(
+                "`NtWriteFile` `ApcRoutine` parameter is non-null, which is unsupported"
+            );
+        }
+
+        if !this.ptr_is_null(apc_ctx)? {
+            throw_unsup_format!(
+                "`NtWriteFile` `ApcContext` parameter is non-null, which is unsupported"
+            );
+        }
+
+        if byte_offset != 0 {
+            throw_unsup_format!(
+                "`NtWriteFile` `ByteOffset` parameter is non-null, which is unsupported"
+            );
+        }
+
+        if !this.ptr_is_null(key)? {
+            throw_unsup_format!("`NtWriteFile` `Key` parameter is non-null, which is unsupported");
+        }
+
+        let fd = match handle {
+            Handle::File(fd) => fd,
+            _ => this.invalid_handle("NtWriteFile")?,
+        };
+
+        let Some(desc) = this.machine.fds.get(fd) else { this.invalid_handle("NtWriteFile")? };
+
+        // Windows writes the output code to IO_STATUS_BLOCK.Status, and number of bytes written
+        // to IO_STATUS_BLOCK.Information.
+        // The status block value and the returned value don't need to match - but
+        // for the cases implemented by miri so far, we can choose to decide that they do.
+        let io_status = {
+            let anon = this.project_field_named(&io_status_block, "Anonymous")?;
+            this.project_field_named(&anon, "Status")?
+        };
+        let io_status_info = this.project_field_named(&io_status_block, "Information")?;
+
+        let finish = {
+            let io_status = io_status.clone();
+            let io_status_info = io_status_info.clone();
+            let dest = dest.clone();
+            callback!(
+                @capture<'tcx> {
+                    count: u32,
+                    io_status: MPlaceTy<'tcx>,
+                    io_status_info: MPlaceTy<'tcx>,
+                    dest: MPlaceTy<'tcx>,
+                }
+                |this, result: Result<usize, IoError>| {
+                    match result {
+                        Ok(read_size) => {
+                            assert!(read_size <= count.try_into().unwrap());
+                            // This must fit since `count` fits.
+                            this.write_int(u64::try_from(read_size).unwrap(), &io_status_info)?;
+                            this.write_int(0, &io_status)?;
+                            this.write_int(0, &dest)
+                        }
+                        Err(e) => {
+                            this.write_int(0, &io_status_info)?;
+                            let status = e.into_ntstatus();
+                            this.write_int(status, &io_status)?;
+                            this.write_int(status, &dest)
+                        }
+                }}
+            )
+        };
+
+        desc.write(this.machine.communicate(), buf, count.try_into().unwrap(), this, finish)?;
+
+        // Return status is written to `dest` and `io_status_block` on callback completion.
+        interp_ok(())
+    }
+
+    fn NtReadFile(
+        &mut self,
+        handle: &OpTy<'tcx>,          // HANDLE
+        event: &OpTy<'tcx>,           // HANDLE
+        apc_routine: &OpTy<'tcx>,     // PIO_APC_ROUTINE
+        apc_ctx: &OpTy<'tcx>,         // PVOID
+        io_status_block: &OpTy<'tcx>, // PIO_STATUS_BLOCK
+        buf: &OpTy<'tcx>,             // PVOID
+        n: &OpTy<'tcx>,               // ULONG
+        byte_offset: &OpTy<'tcx>,     // PLARGE_INTEGER
+        key: &OpTy<'tcx>,             // PULONG
+        dest: &MPlaceTy<'tcx>,        // return type: NTSTATUS
+    ) -> InterpResult<'tcx, ()> {
+        let this = self.eval_context_mut();
+        let handle = this.read_handle(handle, "NtReadFile")?;
+        let event = this.read_handle(event, "NtReadFile")?;
+        let apc_routine = this.read_pointer(apc_routine)?;
+        let apc_ctx = this.read_pointer(apc_ctx)?;
+        let buf = this.read_pointer(buf)?;
+        let count = this.read_scalar(n)?.to_u32()?;
+        let byte_offset = this.read_target_usize(byte_offset)?; // is actually a pointer, but we only support null
+        let key = this.read_pointer(key)?;
+        let io_status_block =
+            this.deref_pointer_as(io_status_block, this.windows_ty_layout("IO_STATUS_BLOCK"))?;
+
+        if event != Handle::Null {
+            throw_unsup_format!("`NtReadFile` `Event` parameter is non-null, which is unsupported");
+        }
+
+        if !this.ptr_is_null(apc_routine)? {
+            throw_unsup_format!(
+                "`NtReadFile` `ApcRoutine` parameter is non-null, which is unsupported"
+            );
+        }
+
+        if !this.ptr_is_null(apc_ctx)? {
+            throw_unsup_format!(
+                "`NtReadFile` `ApcContext` parameter is non-null, which is unsupported"
+            );
+        }
+
+        if byte_offset != 0 {
+            throw_unsup_format!(
+                "`NtReadFile` `ByteOffset` parameter is non-null, which is unsupported"
+            );
+        }
+
+        if !this.ptr_is_null(key)? {
+            throw_unsup_format!("`NtReadFile` `Key` parameter is non-null, which is unsupported");
+        }
+
+        // See NtWriteFile above for commentary on this
+        let io_status = {
+            let anon = this.project_field_named(&io_status_block, "Anonymous")?;
+            this.project_field_named(&anon, "Status")?
+        };
+        let io_status_info = this.project_field_named(&io_status_block, "Information")?;
+
+        let finish = {
+            let io_status = io_status.clone();
+            let io_status_info = io_status_info.clone();
+            let dest = dest.clone();
+            callback!(
+                @capture<'tcx> {
+                    count: u32,
+                    io_status: MPlaceTy<'tcx>,
+                    io_status_info: MPlaceTy<'tcx>,
+                    dest: MPlaceTy<'tcx>,
+                }
+                |this, result: Result<usize, IoError>| {
+                    match result {
+                        Ok(read_size) => {
+                            assert!(read_size <= count.try_into().unwrap());
+                            // This must fit since `count` fits.
+                            this.write_int(u64::try_from(read_size).unwrap(), &io_status_info)?;
+                            this.write_int(0, &io_status)?;
+                            this.write_int(0, &dest)
+                        }
+                        Err(e) => {
+                            this.write_int(0, &io_status_info)?;
+                            let status = e.into_ntstatus();
+                            this.write_int(status, &io_status)?;
+                            this.write_int(status, &dest)
+                        }
+                }}
+            )
+        };
+
+        let fd = match handle {
+            Handle::File(fd) => fd,
+            _ => this.invalid_handle("NtWriteFile")?,
+        };
+
+        let Some(desc) = this.machine.fds.get(fd) else { this.invalid_handle("NtReadFile")? };
+
+        desc.read(this.machine.communicate(), buf, count.try_into().unwrap(), this, finish)?;
+
+        // See NtWriteFile for commentary on this
+        interp_ok(())
+    }
+
+    fn SetFilePointerEx(
+        &mut self,
+        file: &OpTy<'tcx>,         // HANDLE
+        dist_to_move: &OpTy<'tcx>, // LARGE_INTEGER
+        new_fp: &OpTy<'tcx>,       // PLARGE_INTEGER
+        move_method: &OpTy<'tcx>,  // DWORD
+    ) -> InterpResult<'tcx, Scalar> {
+        // ^ Returns BOOL (i32 on Windows)
+        let this = self.eval_context_mut();
+        let file = this.read_handle(file, "SetFilePointerEx")?;
+        let dist_to_move = this.read_scalar(dist_to_move)?.to_i64()?;
+        let new_fp_ptr = this.read_pointer(new_fp)?;
+        let move_method = this.read_scalar(move_method)?.to_u32()?;
+
+        let fd = match file {
+            Handle::File(fd) => fd,
+            _ => this.invalid_handle("SetFilePointerEx")?,
+        };
+
+        let Some(desc) = this.machine.fds.get(fd) else {
+            throw_unsup_format!("`SetFilePointerEx` is only supported on file backed handles");
+        };
+
+        let file_begin = this.eval_windows_u32("c", "FILE_BEGIN");
+        let file_current = this.eval_windows_u32("c", "FILE_CURRENT");
+        let file_end = this.eval_windows_u32("c", "FILE_END");
+
+        let seek = if move_method == file_begin {
+            SeekFrom::Start(dist_to_move.try_into().unwrap())
+        } else if move_method == file_current {
+            SeekFrom::Current(dist_to_move)
+        } else if move_method == file_end {
+            SeekFrom::End(dist_to_move)
+        } else {
+            throw_unsup_format!("Invalid move method: {move_method}")
+        };
+
+        match desc.seek(this.machine.communicate(), seek)? {
+            Ok(n) => {
+                if !this.ptr_is_null(new_fp_ptr)? {
+                    this.write_scalar(
+                        Scalar::from_i64(n.try_into().unwrap()),
+                        &this.deref_pointer_as(new_fp, this.machine.layouts.i64)?,
+                    )?;
+                }
+                interp_ok(this.eval_windows("c", "TRUE"))
+            }
+            Err(e) => {
+                this.set_last_error(e)?;
+                interp_ok(this.eval_windows("c", "FALSE"))
+            }
+        }
+    }
 }
 
 /// Windows FILETIME is measured in 100-nanosecs since 1601
diff --git a/src/tools/miri/src/shims/windows/handle.rs b/src/tools/miri/src/shims/windows/handle.rs
index 09dac9c1378..5c04271fac5 100644
--- a/src/tools/miri/src/shims/windows/handle.rs
+++ b/src/tools/miri/src/shims/windows/handle.rs
@@ -220,6 +220,30 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
         )))
     }
 
+    fn GetStdHandle(&mut self, which: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
+        let this = self.eval_context_mut();
+        let which = this.read_scalar(which)?.to_i32()?;
+
+        let stdin = this.eval_windows("c", "STD_INPUT_HANDLE").to_i32()?;
+        let stdout = this.eval_windows("c", "STD_OUTPUT_HANDLE").to_i32()?;
+        let stderr = this.eval_windows("c", "STD_ERROR_HANDLE").to_i32()?;
+
+        // These values don't mean anything on Windows, but Miri unconditionally sets them up to the
+        // unix in/out/err descriptors. So we take advantage of that.
+        // Due to the `Handle` encoding, these values will not be directly exposed to the user.
+        let fd_num = if which == stdin {
+            0
+        } else if which == stdout {
+            1
+        } else if which == stderr {
+            2
+        } else {
+            throw_unsup_format!("Invalid argument to `GetStdHandle`: {which}")
+        };
+        let handle = Handle::File(fd_num);
+        interp_ok(handle.to_scalar(this))
+    }
+
     fn CloseHandle(&mut self, handle_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
         let this = self.eval_context_mut();
 
diff --git a/src/tools/miri/test_dependencies/Cargo.toml b/src/tools/miri/test_dependencies/Cargo.toml
index 653228a5e3d..fa833b51fa3 100644
--- a/src/tools/miri/test_dependencies/Cargo.toml
+++ b/src/tools/miri/test_dependencies/Cargo.toml
@@ -25,6 +25,13 @@ page_size = "0.6"
 tokio = { version = "1", 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", "Win32_Storage_FileSystem", "Win32_Security"] }
+windows-sys = { version = "0.59", features = [
+    "Win32_Foundation",
+    "Win32_System_Threading",
+    "Win32_Storage_FileSystem",
+    "Win32_Security",
+    "Win32_System_IO",
+    "Wdk_Storage_FileSystem",
+] }
 
 [workspace]
diff --git a/src/tools/miri/tests/pass-dep/shims/windows-fs.rs b/src/tools/miri/tests/pass-dep/shims/windows-fs.rs
index 698ca4e0b4b..4ca19046b67 100644
--- a/src/tools/miri/tests/pass-dep/shims/windows-fs.rs
+++ b/src/tools/miri/tests/pass-dep/shims/windows-fs.rs
@@ -2,25 +2,28 @@
 //@compile-flags: -Zmiri-disable-isolation
 #![allow(nonstandard_style)]
 
-use std::io::ErrorKind;
+use std::io::{ErrorKind, Read, Write};
 use std::os::windows::ffi::OsStrExt;
+use std::os::windows::io::AsRawHandle;
 use std::path::Path;
-use std::ptr;
+use std::{fs, ptr};
 
 #[path = "../../utils/mod.rs"]
 mod utils;
 
+use windows_sys::Wdk::Storage::FileSystem::{NtReadFile, NtWriteFile};
 use windows_sys::Win32::Foundation::{
     CloseHandle, ERROR_ACCESS_DENIED, ERROR_ALREADY_EXISTS, ERROR_IO_DEVICE, GENERIC_READ,
     GENERIC_WRITE, GetLastError, RtlNtStatusToDosError, STATUS_ACCESS_DENIED,
-    STATUS_IO_DEVICE_ERROR,
+    STATUS_IO_DEVICE_ERROR, STATUS_SUCCESS, SetLastError,
 };
 use windows_sys::Win32::Storage::FileSystem::{
     BY_HANDLE_FILE_INFORMATION, CREATE_ALWAYS, CREATE_NEW, CreateFileW, DeleteFileW,
-    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,
+    FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_NORMAL, FILE_BEGIN, FILE_CURRENT,
+    FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_DELETE, FILE_SHARE_READ,
+    FILE_SHARE_WRITE, GetFileInformationByHandle, OPEN_ALWAYS, OPEN_EXISTING, SetFilePointerEx,
 };
+use windows_sys::Win32::System::IO::IO_STATUS_BLOCK;
 
 fn main() {
     unsafe {
@@ -31,6 +34,8 @@ fn main() {
         test_open_dir_reparse();
         test_delete_file();
         test_ntstatus_to_dos();
+        test_file_read_write();
+        test_file_seek();
     }
 }
 
@@ -199,13 +204,13 @@ unsafe fn test_open_dir_reparse() {
 unsafe fn test_delete_file() {
     let temp = utils::tmp().join("test_delete_file.txt");
     let raw_path = to_wide_cstr(&temp);
-    let _ = std::fs::File::create(&temp).unwrap();
+    let _ = fs::File::create(&temp).unwrap();
 
     if DeleteFileW(raw_path.as_ptr()) == 0 {
         panic!("Failed to delete file");
     }
 
-    match std::fs::File::open(temp) {
+    match fs::File::open(temp) {
         Ok(_) => panic!("File not deleted"),
         Err(e) => assert!(e.kind() == ErrorKind::NotFound, "File not deleted"),
     }
@@ -217,6 +222,82 @@ unsafe fn test_ntstatus_to_dos() {
     assert_eq!(RtlNtStatusToDosError(STATUS_ACCESS_DENIED), ERROR_ACCESS_DENIED);
 }
 
+unsafe fn test_file_read_write() {
+    let temp = utils::tmp().join("test_file_read_write.txt");
+    let file = fs::File::create(&temp).unwrap();
+    let handle = file.as_raw_handle();
+
+    // Testing NtWriteFile doesn't clobber the error
+    SetLastError(1234);
+
+    let text = b"Example text!";
+    let mut status = std::mem::zeroed::<IO_STATUS_BLOCK>();
+    let out = NtWriteFile(
+        handle,
+        ptr::null_mut(),
+        None,
+        ptr::null_mut(),
+        &mut status,
+        text.as_ptr().cast(),
+        text.len() as u32,
+        ptr::null_mut(),
+        ptr::null_mut(),
+    );
+
+    assert_eq!(out, status.Anonymous.Status);
+    assert_eq!(out, STATUS_SUCCESS);
+    assert_eq!(GetLastError(), 1234);
+
+    let file = fs::File::open(&temp).unwrap();
+    let handle = file.as_raw_handle();
+
+    // Testing NtReadFile doesn't clobber the error
+    SetLastError(1234);
+
+    let mut buffer = vec![0; 13];
+    let out = NtReadFile(
+        handle,
+        ptr::null_mut(),
+        None,
+        ptr::null_mut(),
+        &mut status,
+        buffer.as_mut_ptr().cast(),
+        buffer.len() as u32,
+        ptr::null_mut(),
+        ptr::null_mut(),
+    );
+
+    assert_eq!(out, status.Anonymous.Status);
+    assert_eq!(out, STATUS_SUCCESS);
+    assert_eq!(buffer, text);
+    assert_eq!(GetLastError(), 1234);
+}
+
+unsafe fn test_file_seek() {
+    let temp = utils::tmp().join("test_file_seek.txt");
+    let mut file = fs::File::options().create(true).write(true).read(true).open(&temp).unwrap();
+    file.write_all(b"Hello, World!\n").unwrap();
+
+    let handle = file.as_raw_handle();
+
+    if SetFilePointerEx(handle, 7, ptr::null_mut(), FILE_BEGIN) == 0 {
+        panic!("Failed to seek");
+    }
+
+    let mut buf = vec![0; 5];
+    file.read(&mut buf).unwrap();
+    assert_eq!(buf, b"World");
+
+    let mut pos = 0;
+    if SetFilePointerEx(handle, -7, &mut pos, FILE_CURRENT) == 0 {
+        panic!("Failed to seek");
+    }
+    buf.truncate(2);
+    file.read_exact(&mut buf).unwrap();
+    assert_eq!(buf, b", ");
+    assert_eq!(pos, 5);
+}
+
 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]);
diff --git a/src/tools/miri/tests/pass/shims/fs.rs b/src/tools/miri/tests/pass/shims/fs.rs
index d0a7f245ee0..315637ff7ec 100644
--- a/src/tools/miri/tests/pass/shims/fs.rs
+++ b/src/tools/miri/tests/pass/shims/fs.rs
@@ -17,20 +17,20 @@ mod utils;
 
 fn main() {
     test_path_conversion();
+    test_file();
     test_file_create_new();
+    test_metadata();
+    test_seek();
+    test_errors();
+    test_from_raw_os_error();
     // Windows file handling is very incomplete.
     if cfg!(not(windows)) {
-        test_file();
-        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();
     }