Skip to main content

miri/shims/windows/
fs.rs

1use std::fs::{self, Dir};
2use std::io;
3use std::io::SeekFrom;
4use std::time::SystemTime;
5
6use bitflags::bitflags;
7use rustc_abi::Size;
8use rustc_target::spec::Os;
9
10use crate::shims::files::{DirHandle, FileHandle};
11use crate::shims::windows::handle::{EvalContextExt as _, Handle};
12use crate::*;
13
14#[derive(Copy, Clone, Debug, PartialEq)]
15enum CreationDisposition {
16    CreateAlways,
17    CreateNew,
18    OpenAlways,
19    OpenExisting,
20    TruncateExisting,
21}
22
23impl CreationDisposition {
24    fn new<'tcx>(
25        value: u32,
26        ecx: &mut MiriInterpCx<'tcx>,
27    ) -> InterpResult<'tcx, CreationDisposition> {
28        let create_always = ecx.eval_windows_u32("c", "CREATE_ALWAYS");
29        let create_new = ecx.eval_windows_u32("c", "CREATE_NEW");
30        let open_always = ecx.eval_windows_u32("c", "OPEN_ALWAYS");
31        let open_existing = ecx.eval_windows_u32("c", "OPEN_EXISTING");
32        let truncate_existing = ecx.eval_windows_u32("c", "TRUNCATE_EXISTING");
33
34        let out = if value == create_always {
35            CreationDisposition::CreateAlways
36        } else if value == create_new {
37            CreationDisposition::CreateNew
38        } else if value == open_always {
39            CreationDisposition::OpenAlways
40        } else if value == open_existing {
41            CreationDisposition::OpenExisting
42        } else if value == truncate_existing {
43            CreationDisposition::TruncateExisting
44        } else {
45            throw_unsup_format!("CreateFileW: Unsupported creation disposition: {value}");
46        };
47        interp_ok(out)
48    }
49}
50
51bitflags! {
52    #[derive(PartialEq)]
53    struct FileAttributes: u32 {
54        const ZERO = 0;
55        const NORMAL = 1 << 0;
56        /// This must be passed to allow getting directory handles. If not passed, we error on trying
57        /// to open directories
58        const BACKUP_SEMANTICS = 1 << 1;
59        /// Open a reparse point as a regular file - this is basically similar to 'readlink' in Unix
60        /// terminology. A reparse point is a file with custom logic when navigated to, of which
61        /// a symlink is one specific example.
62        const OPEN_REPARSE = 1 << 2;
63    }
64}
65
66impl FileAttributes {
67    fn new<'tcx>(
68        mut value: u32,
69        ecx: &mut MiriInterpCx<'tcx>,
70    ) -> InterpResult<'tcx, FileAttributes> {
71        let file_attribute_normal = ecx.eval_windows_u32("c", "FILE_ATTRIBUTE_NORMAL");
72        let file_flag_backup_semantics = ecx.eval_windows_u32("c", "FILE_FLAG_BACKUP_SEMANTICS");
73        let file_flag_open_reparse_point =
74            ecx.eval_windows_u32("c", "FILE_FLAG_OPEN_REPARSE_POINT");
75
76        let mut out = FileAttributes::ZERO;
77        if value & file_flag_backup_semantics != 0 {
78            value &= !file_flag_backup_semantics;
79            out |= FileAttributes::BACKUP_SEMANTICS;
80        }
81        if value & file_flag_open_reparse_point != 0 {
82            value &= !file_flag_open_reparse_point;
83            out |= FileAttributes::OPEN_REPARSE;
84        }
85        if value & file_attribute_normal != 0 {
86            value &= !file_attribute_normal;
87            out |= FileAttributes::NORMAL;
88        }
89
90        if value != 0 {
91            throw_unsup_format!("CreateFileW: Unsupported flags_and_attributes: {value}");
92        }
93
94        if out == FileAttributes::ZERO {
95            // NORMAL is equivalent to 0. Avoid needing to check both cases by unifying the two.
96            out = FileAttributes::NORMAL;
97        }
98        interp_ok(out)
99    }
100}
101
102impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
103#[allow(non_snake_case)]
104pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
105    fn CreateFileW(
106        &mut self,
107        file_name: &OpTy<'tcx>,            // LPCWSTR
108        desired_access: &OpTy<'tcx>,       // DWORD
109        share_mode: &OpTy<'tcx>,           // DWORD
110        security_attributes: &OpTy<'tcx>,  // LPSECURITY_ATTRIBUTES
111        creation_disposition: &OpTy<'tcx>, // DWORD
112        flags_and_attributes: &OpTy<'tcx>, // DWORD
113        template_file: &OpTy<'tcx>,        // HANDLE
114    ) -> InterpResult<'tcx, Handle> {
115        // ^ Returns HANDLE
116        use CreationDisposition::*;
117
118        let this = self.eval_context_mut();
119        this.assert_target_os(Os::Windows, "CreateFileW");
120        this.check_no_isolation("`CreateFileW`")?;
121
122        // This function appears to always set the error to 0. This is important for some flag
123        // combinations, which may set error code on success.
124        this.set_last_error(IoError::Raw(Scalar::from_i32(0)))?;
125
126        let file_name = this.read_path_from_wide_str(this.read_pointer(file_name)?)?;
127        let mut desired_access = this.read_scalar(desired_access)?.to_u32()?;
128        let share_mode = this.read_scalar(share_mode)?.to_u32()?;
129        let security_attributes = this.read_pointer(security_attributes)?;
130        let creation_disposition = this.read_scalar(creation_disposition)?.to_u32()?;
131        let flags_and_attributes = this.read_scalar(flags_and_attributes)?.to_u32()?;
132        let template_file = this.read_target_usize(template_file)?;
133
134        let generic_read = this.eval_windows_u32("c", "GENERIC_READ");
135        let generic_write = this.eval_windows_u32("c", "GENERIC_WRITE");
136
137        let file_share_delete = this.eval_windows_u32("c", "FILE_SHARE_DELETE");
138        let file_share_read = this.eval_windows_u32("c", "FILE_SHARE_READ");
139        let file_share_write = this.eval_windows_u32("c", "FILE_SHARE_WRITE");
140
141        let creation_disposition = CreationDisposition::new(creation_disposition, this)?;
142        let attributes = FileAttributes::new(flags_and_attributes, this)?;
143
144        if share_mode != (file_share_delete | file_share_read | file_share_write) {
145            throw_unsup_format!("CreateFileW: Unsupported share mode: {share_mode}");
146        }
147        if !this.ptr_is_null(security_attributes)? {
148            throw_unsup_format!("CreateFileW: Security attributes are not supported");
149        }
150
151        if attributes.contains(FileAttributes::OPEN_REPARSE) && creation_disposition == CreateAlways
152        {
153            throw_machine_stop!(TerminationInfo::Abort("Invalid CreateFileW argument combination: FILE_FLAG_OPEN_REPARSE_POINT with CREATE_ALWAYS".to_string()));
154        }
155
156        if template_file != 0 {
157            throw_unsup_format!("CreateFileW: Template files are not supported");
158        }
159
160        // Parse desired_access
161        let mut desired_read = false;
162        if desired_access & generic_read != 0 {
163            desired_read = true;
164            desired_access &= !generic_read;
165        }
166        let mut desired_write = false;
167        if desired_access & generic_write != 0 {
168            desired_write = true;
169            desired_access &= !generic_write;
170        }
171
172        if desired_access != 0 {
173            throw_unsup_format!(
174                "CreateFileW: Unsupported bits set for access mode: {desired_access:#x}"
175            );
176        }
177
178        // We start a retry loop to deal with the `is_dir` and `exists_already` race, see below.
179        // We add a retry counter to avoid infinite loops when things go wrong.
180        let mut counter = 0u32;
181        loop {
182            if counter >= 100 {
183                panic!(
184                    "CreateFileW seems stuck in an infinite retry loop. \
185                    If you can reproduce this, please file a bug."
186                );
187            }
188            counter = counter.strict_add(1);
189
190            // We need to know if the file is a directory to correctly open directory handles.
191            // The standard library only lets us open something as a file or a directory, so
192            // we check for that and then retry if we end up with the wrong thing.
193            let is_dir = file_name.is_dir();
194
195            // BACKUP_SEMANTICS is how Windows calls the act of opening a directory handle.
196            if !attributes.contains(FileAttributes::BACKUP_SEMANTICS) && is_dir {
197                this.set_last_error(IoError::WindowsError("ERROR_ACCESS_DENIED"))?;
198                return interp_ok(Handle::Invalid);
199            }
200
201            if is_dir {
202                // Open this as a directory.
203                // FIXME: shouldn't we check `creation_disposition` here? We do know that it already
204                // exists.
205                let dir = match Dir::open(&file_name) {
206                    Ok(dir) => dir,
207                    Err(e) => {
208                        if e.kind() == io::ErrorKind::NotADirectory {
209                            // This changed from a directory to a file. Retry.
210                            continue;
211                        }
212                        this.set_last_error(e)?;
213                        return interp_ok(Handle::Invalid);
214                    }
215                };
216                #[cfg(not(bootstrap))]
217                if !dir.metadata().unwrap().is_dir() {
218                    // This changed from a directory to a file. Retry.
219                    continue;
220                }
221
222                // Windows communicates information via the error code on success.
223                if let CreateAlways | OpenAlways = creation_disposition {
224                    this.set_last_error(IoError::WindowsError("ERROR_ALREADY_EXISTS"))?;
225                }
226
227                let fd_num = this.machine.fds.insert_new(DirHandle { dir, path: file_name });
228                return interp_ok(Handle::File(fd_num));
229            } else {
230                // Per the documentation:
231                // If the specified file exists and is writable, the function truncates the file,
232                // the function succeeds, and last-error code is set to ERROR_ALREADY_EXISTS.
233                // If the specified file does not exist and is a valid path, a new file is created,
234                // the function succeeds, and the last-error code is set to zero.
235                // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew
236                //
237                // We check whether it exists before trying to open it. This is racy, but there
238                // doesn't appear to be an std API that both succeeds whether or not a file already
239                // exists and tells us whether it is new. So instead we will open the file in a way
240                // that we can verify whether our guess is correct, and retry if it is not.
241                let exists_already = file_name.exists();
242
243                // Open this as a standard file.
244                let mut options = fs::OpenOptions::new();
245                options.read(desired_read);
246                options.write(desired_write);
247                match creation_disposition {
248                    CreateAlways | OpenAlways => {
249                        // We verify `exists_already`: if we expect it to already exist, we set no
250                        // flag, thus failing if it doesn't exist. If we expect the file to not
251                        // exist, we use `create_new` to fail if it does exist.
252                        if !exists_already {
253                            options.create_new(true);
254                        }
255                        if creation_disposition == CreateAlways {
256                            options.truncate(true);
257                        }
258                    }
259                    CreateNew => {
260                        options.create_new(true);
261                        // Per `create_new` documentation:
262                        // The file must be opened with write or append access in order to create a new file.
263                        // https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.create_new
264                        if !desired_write {
265                            options.append(true);
266                        }
267                    }
268                    OpenExisting => {
269                        if !desired_read && !desired_write {
270                            // Windows supports handles with no permissions. These allow things such as
271                            // reading metadata, but not file content. This is used by `Path::metadata`.
272                            // `std` does not support this. To ensure we behave correctly as often as
273                            // possible, we open the file for reading and live with the fact that this
274                            // might incorrectly return `PermissionDenied`.
275                            // FIXME: We could probably use `OpenOptionsExt`? On a Unix host,
276                            // `O_PATH` apparently can open files for metadata use only.
277                            options.read(true);
278                        }
279                    }
280                    TruncateExisting => {
281                        options.truncate(true);
282                    }
283                }
284
285                let file = match options.open(&file_name) {
286                    Ok(file) => file,
287                    Err(e) => {
288                        let kind = e.kind();
289                        if kind == io::ErrorKind::IsADirectory {
290                            // This changed from a file to a directory. Retry.
291                            continue;
292                        }
293                        if exists_already && kind == io::ErrorKind::NotFound {
294                            // The file disappeared. Retry.
295                            continue;
296                        }
297                        if !exists_already && kind == io::ErrorKind::AlreadyExists {
298                            // The file got created by something else. Retry.
299                            continue;
300                        }
301                        this.set_last_error(e)?;
302                        return interp_ok(Handle::Invalid);
303                    }
304                };
305                if file.metadata().unwrap().is_dir() {
306                    // This changed from a file to a directory. Retry.
307                    continue;
308                }
309
310                // Windows communicates information via the error code on success.
311                if let CreateAlways | OpenAlways = creation_disposition
312                    && exists_already
313                {
314                    this.set_last_error(IoError::WindowsError("ERROR_ALREADY_EXISTS"))?;
315                }
316                let fd_num = this.machine.fds.insert_new(FileHandle {
317                    file,
318                    writable: desired_write,
319                    readable: desired_read,
320                });
321                return interp_ok(Handle::File(fd_num));
322            }
323        }
324    }
325
326    fn GetFileInformationByHandle(
327        &mut self,
328        file: &OpTy<'tcx>,             // HANDLE
329        file_information: &OpTy<'tcx>, // LPBY_HANDLE_FILE_INFORMATION
330    ) -> InterpResult<'tcx, Scalar> {
331        // ^ Returns BOOL (i32 on Windows)
332        let this = self.eval_context_mut();
333        this.assert_target_os(Os::Windows, "GetFileInformationByHandle");
334        this.check_no_isolation("`GetFileInformationByHandle`")?;
335
336        let file = this.read_handle(file, "GetFileInformationByHandle")?;
337        let file_information = this.deref_pointer_as(
338            file_information,
339            this.windows_ty_layout("BY_HANDLE_FILE_INFORMATION"),
340        )?;
341
342        let Handle::File(fd_num) = file else { this.invalid_handle("GetFileInformationByHandle")? };
343
344        let Some(desc) = this.machine.fds.get(fd_num) else {
345            this.invalid_handle("GetFileInformationByHandle")?
346        };
347
348        let metadata = match desc.metadata()? {
349            Either::Left(Ok(meta)) => meta,
350            Either::Left(Err(e)) => {
351                this.set_last_error(e)?;
352                return interp_ok(this.eval_windows("c", "FALSE"));
353            }
354            Either::Right(_mode) =>
355                throw_unsup_format!(
356                    "`GetFileInformationByHandle` is not supported on non-file-backed handles"
357                ),
358        };
359
360        let size = metadata.len();
361
362        let file_type = metadata.file_type();
363        let attributes = if file_type.is_dir() {
364            this.eval_windows_u32("c", "FILE_ATTRIBUTE_DIRECTORY")
365        } else if file_type.is_file() {
366            this.eval_windows_u32("c", "FILE_ATTRIBUTE_NORMAL")
367        } else {
368            this.eval_windows_u32("c", "FILE_ATTRIBUTE_DEVICE")
369        };
370
371        // Per the Windows documentation:
372        // "If the underlying file system does not support the [...] time, this member is zero (0)."
373        // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/ns-fileapi-by_handle_file_information
374        let created = extract_windows_epoch(this, metadata.created())?.unwrap_or((0, 0));
375        let accessed = extract_windows_epoch(this, metadata.accessed())?.unwrap_or((0, 0));
376        let written = extract_windows_epoch(this, metadata.modified())?.unwrap_or((0, 0));
377
378        this.write_int_fields_named(&[("dwFileAttributes", attributes.into())], &file_information)?;
379        write_filetime_field(this, &file_information, "ftCreationTime", created)?;
380        write_filetime_field(this, &file_information, "ftLastAccessTime", accessed)?;
381        write_filetime_field(this, &file_information, "ftLastWriteTime", written)?;
382        this.write_int_fields_named(
383            &[
384                ("dwVolumeSerialNumber", 0),
385                ("nFileSizeHigh", (size >> 32).into()),
386                ("nFileSizeLow", (size & 0xFFFFFFFF).into()),
387                ("nNumberOfLinks", 1),
388                ("nFileIndexHigh", 0),
389                ("nFileIndexLow", 0),
390            ],
391            &file_information,
392        )?;
393
394        interp_ok(this.eval_windows("c", "TRUE"))
395    }
396
397    fn SetFileInformationByHandle(
398        &mut self,
399        file: &OpTy<'tcx>,             // HANDLE
400        class: &OpTy<'tcx>,            // FILE_INFO_BY_HANDLE_CLASS
401        file_information: &OpTy<'tcx>, // LPVOID
402        buffer_size: &OpTy<'tcx>,      // DWORD
403    ) -> InterpResult<'tcx, Scalar> {
404        // ^ Returns BOOL (i32 on Windows)
405        let this = self.eval_context_mut();
406        this.assert_target_os(Os::Windows, "SetFileInformationByHandle");
407        this.check_no_isolation("`SetFileInformationByHandle`")?;
408
409        let class = this.read_scalar(class)?.to_u32()?;
410        let buffer_size = this.read_scalar(buffer_size)?.to_u32()?;
411        let file_information = this.read_pointer(file_information)?;
412        this.check_ptr_access(
413            file_information,
414            Size::from_bytes(buffer_size),
415            CheckInAllocMsg::MemoryAccess,
416        )?;
417
418        let file = this.read_handle(file, "SetFileInformationByHandle")?;
419        let Handle::File(fd_num) = file else { this.invalid_handle("SetFileInformationByHandle")? };
420        let Some(desc) = this.machine.fds.get(fd_num) else {
421            this.invalid_handle("SetFileInformationByHandle")?
422        };
423        let file = desc.downcast::<FileHandle>().ok_or_else(|| {
424            err_unsup_format!(
425                "`SetFileInformationByHandle` is only supported on file-backed file descriptors"
426            )
427        })?;
428
429        if class == this.eval_windows_u32("c", "FileEndOfFileInfo") {
430            let place = this
431                .ptr_to_mplace(file_information, this.windows_ty_layout("FILE_END_OF_FILE_INFO"));
432            let new_len =
433                this.read_scalar(&this.project_field_named(&place, "EndOfFile")?)?.to_i64()?;
434            match file.file.set_len(new_len.try_into().unwrap()) {
435                Ok(_) => interp_ok(this.eval_windows("c", "TRUE")),
436                Err(e) => {
437                    this.set_last_error(e)?;
438                    interp_ok(this.eval_windows("c", "FALSE"))
439                }
440            }
441        } else if class == this.eval_windows_u32("c", "FileAllocationInfo") {
442            // On Windows, files are somewhat similar to a `Vec` in that they have a separate
443            // "length" (called "EOF position") and "capacity" (called "allocation size").
444            // Growing the allocation size is largely a performance hint which we can
445            // ignore -- it can also be directly queried, but we currently do not support that.
446            // So we only need to do something if this operation shrinks the allocation size
447            // so far that it affects the EOF position.
448            let place = this
449                .ptr_to_mplace(file_information, this.windows_ty_layout("FILE_ALLOCATION_INFO"));
450            let new_alloc_size: u64 = this
451                .read_scalar(&this.project_field_named(&place, "AllocationSize")?)?
452                .to_i64()?
453                .try_into()
454                .unwrap();
455            let old_len = match file.file.metadata() {
456                Ok(m) => m.len(),
457                Err(e) => {
458                    this.set_last_error(e)?;
459                    return interp_ok(this.eval_windows("c", "FALSE"));
460                }
461            };
462            if new_alloc_size < old_len {
463                match file.file.set_len(new_alloc_size) {
464                    Ok(_) => interp_ok(this.eval_windows("c", "TRUE")),
465                    Err(e) => {
466                        this.set_last_error(e)?;
467                        interp_ok(this.eval_windows("c", "FALSE"))
468                    }
469                }
470            } else {
471                interp_ok(this.eval_windows("c", "TRUE"))
472            }
473        } else {
474            throw_unsup_format!(
475                "SetFileInformationByHandle: Unsupported `FileInformationClass` value {}",
476                class
477            )
478        }
479    }
480
481    fn FlushFileBuffers(
482        &mut self,
483        file: &OpTy<'tcx>, // HANDLE
484    ) -> InterpResult<'tcx, Scalar> {
485        // ^ returns BOOL (i32 on Windows)
486        let this = self.eval_context_mut();
487        this.assert_target_os(Os::Windows, "FlushFileBuffers");
488
489        let file = this.read_handle(file, "FlushFileBuffers")?;
490        let Handle::File(fd_num) = file else { this.invalid_handle("FlushFileBuffers")? };
491        let Some(desc) = this.machine.fds.get(fd_num) else {
492            this.invalid_handle("FlushFileBuffers")?
493        };
494        let file = desc.downcast::<FileHandle>().ok_or_else(|| {
495            err_unsup_format!(
496                "`FlushFileBuffers` is only supported on file-backed file descriptors"
497            )
498        })?;
499
500        if !file.writable {
501            this.set_last_error(IoError::WindowsError("ERROR_ACCESS_DENIED"))?;
502            return interp_ok(this.eval_windows("c", "FALSE"));
503        }
504
505        match file.file.sync_all() {
506            Ok(_) => interp_ok(this.eval_windows("c", "TRUE")),
507            Err(e) => {
508                this.set_last_error(e)?;
509                interp_ok(this.eval_windows("c", "FALSE"))
510            }
511        }
512    }
513
514    fn MoveFileExW(
515        &mut self,
516        existing_name: &OpTy<'tcx>,
517        new_name: &OpTy<'tcx>,
518        flags: &OpTy<'tcx>,
519    ) -> InterpResult<'tcx, Scalar> {
520        let this = self.eval_context_mut();
521
522        let existing_name = this.read_path_from_wide_str(this.read_pointer(existing_name)?)?;
523        let new_name = this.read_path_from_wide_str(this.read_pointer(new_name)?)?;
524
525        let flags = this.read_scalar(flags)?.to_u32()?;
526
527        // Flag to indicate whether we should replace an existing file.
528        // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-movefileexw
529        let movefile_replace_existing = this.eval_windows_u32("c", "MOVEFILE_REPLACE_EXISTING");
530
531        if flags != movefile_replace_existing {
532            throw_unsup_format!("MoveFileExW: Unsupported `dwFlags` value {}", flags);
533        }
534
535        match std::fs::rename(existing_name, new_name) {
536            Ok(_) => interp_ok(this.eval_windows("c", "TRUE")),
537            Err(e) => {
538                this.set_last_error(e)?;
539                interp_ok(this.eval_windows("c", "FALSE"))
540            }
541        }
542    }
543
544    fn DeleteFileW(
545        &mut self,
546        file_name: &OpTy<'tcx>, // LPCWSTR
547    ) -> InterpResult<'tcx, Scalar> {
548        // ^ Returns BOOL (i32 on Windows)
549        let this = self.eval_context_mut();
550        this.assert_target_os(Os::Windows, "DeleteFileW");
551        this.check_no_isolation("`DeleteFileW`")?;
552
553        let file_name = this.read_path_from_wide_str(this.read_pointer(file_name)?)?;
554        match std::fs::remove_file(file_name) {
555            Ok(_) => interp_ok(this.eval_windows("c", "TRUE")),
556            Err(e) => {
557                this.set_last_error(e)?;
558                interp_ok(this.eval_windows("c", "FALSE"))
559            }
560        }
561    }
562
563    fn NtWriteFile(
564        &mut self,
565        handle: &OpTy<'tcx>,          // HANDLE
566        event: &OpTy<'tcx>,           // HANDLE
567        apc_routine: &OpTy<'tcx>,     // PIO_APC_ROUTINE
568        apc_ctx: &OpTy<'tcx>,         // PVOID
569        io_status_block: &OpTy<'tcx>, // PIO_STATUS_BLOCK
570        buf: &OpTy<'tcx>,             // PVOID
571        n: &OpTy<'tcx>,               // ULONG
572        byte_offset: &OpTy<'tcx>,     // PLARGE_INTEGER
573        key: &OpTy<'tcx>,             // PULONG
574        dest: &MPlaceTy<'tcx>,        // return type: NTSTATUS
575    ) -> InterpResult<'tcx, ()> {
576        let this = self.eval_context_mut();
577        let handle = this.read_handle(handle, "NtWriteFile")?;
578        let event = this.read_handle(event, "NtWriteFile")?;
579        let apc_routine = this.read_pointer(apc_routine)?;
580        let apc_ctx = this.read_pointer(apc_ctx)?;
581        let buf = this.read_pointer(buf)?;
582        let count = this.read_scalar(n)?.to_u32()?;
583        let byte_offset = this.read_target_usize(byte_offset)?; // is actually a pointer, but we only support null
584        let key = this.read_pointer(key)?;
585        let io_status_block =
586            this.deref_pointer_as(io_status_block, this.windows_ty_layout("IO_STATUS_BLOCK"))?;
587
588        if event != Handle::Null {
589            throw_unsup_format!(
590                "`NtWriteFile` `Event` parameter is non-null, which is unsupported"
591            );
592        }
593
594        if !this.ptr_is_null(apc_routine)? {
595            throw_unsup_format!(
596                "`NtWriteFile` `ApcRoutine` parameter is non-null, which is unsupported"
597            );
598        }
599
600        if !this.ptr_is_null(apc_ctx)? {
601            throw_unsup_format!(
602                "`NtWriteFile` `ApcContext` parameter is non-null, which is unsupported"
603            );
604        }
605
606        if byte_offset != 0 {
607            throw_unsup_format!(
608                "`NtWriteFile` `ByteOffset` parameter is non-null, which is unsupported"
609            );
610        }
611
612        if !this.ptr_is_null(key)? {
613            throw_unsup_format!("`NtWriteFile` `Key` parameter is non-null, which is unsupported");
614        }
615
616        let Handle::File(fd) = handle else { this.invalid_handle("NtWriteFile")? };
617
618        let Some(desc) = this.machine.fds.get(fd) else { this.invalid_handle("NtWriteFile")? };
619
620        // Windows writes the output code to IO_STATUS_BLOCK.Status, and number of bytes written
621        // to IO_STATUS_BLOCK.Information.
622        // The status block value and the returned value don't need to match - but
623        // for the cases implemented by miri so far, we can choose to decide that they do.
624        let io_status = {
625            let anon = this.project_field_named(&io_status_block, "Anonymous")?;
626            this.project_field_named(&anon, "Status")?
627        };
628        let io_status_info = this.project_field_named(&io_status_block, "Information")?;
629
630        // It seems like short writes are not a thing on Windows, so we don't truncate `count` here.
631        // FIXME: if we are on a Unix host, short host writes are still visible to the program!
632
633        let finish = {
634            let io_status = io_status.clone();
635            let io_status_info = io_status_info.clone();
636            let dest = dest.clone();
637            callback!(
638                @capture<'tcx> {
639                    count: u32,
640                    io_status: MPlaceTy<'tcx>,
641                    io_status_info: MPlaceTy<'tcx>,
642                    dest: MPlaceTy<'tcx>,
643                }
644                |this, result: Result<usize, IoError>| {
645                    match result {
646                        Ok(read_size) => {
647                            assert!(read_size <= count.try_into().unwrap());
648                            // This must fit since `count` fits.
649                            this.write_int(u64::try_from(read_size).unwrap(), &io_status_info)?;
650                            this.write_int(0, &io_status)?;
651                            this.write_int(0, &dest)
652                        }
653                        Err(e) => {
654                            this.write_int(0, &io_status_info)?;
655                            let status = e.into_ntstatus();
656                            this.write_int(status, &io_status)?;
657                            this.write_int(status, &dest)
658                        }
659                }}
660            )
661        };
662        desc.write(this.machine.communicate(), buf, count.try_into().unwrap(), this, finish)?;
663
664        // Return status is written to `dest` and `io_status_block` on callback completion.
665        interp_ok(())
666    }
667
668    fn NtReadFile(
669        &mut self,
670        handle: &OpTy<'tcx>,          // HANDLE
671        event: &OpTy<'tcx>,           // HANDLE
672        apc_routine: &OpTy<'tcx>,     // PIO_APC_ROUTINE
673        apc_ctx: &OpTy<'tcx>,         // PVOID
674        io_status_block: &OpTy<'tcx>, // PIO_STATUS_BLOCK
675        buf: &OpTy<'tcx>,             // PVOID
676        n: &OpTy<'tcx>,               // ULONG
677        byte_offset: &OpTy<'tcx>,     // PLARGE_INTEGER
678        key: &OpTy<'tcx>,             // PULONG
679        dest: &MPlaceTy<'tcx>,        // return type: NTSTATUS
680    ) -> InterpResult<'tcx, ()> {
681        let this = self.eval_context_mut();
682        let handle = this.read_handle(handle, "NtReadFile")?;
683        let event = this.read_handle(event, "NtReadFile")?;
684        let apc_routine = this.read_pointer(apc_routine)?;
685        let apc_ctx = this.read_pointer(apc_ctx)?;
686        let buf = this.read_pointer(buf)?;
687        let count = this.read_scalar(n)?.to_u32()?;
688        let byte_offset = this.read_target_usize(byte_offset)?; // is actually a pointer, but we only support null
689        let key = this.read_pointer(key)?;
690        let io_status_block =
691            this.deref_pointer_as(io_status_block, this.windows_ty_layout("IO_STATUS_BLOCK"))?;
692
693        if event != Handle::Null {
694            throw_unsup_format!("`NtReadFile` `Event` parameter is non-null, which is unsupported");
695        }
696
697        if !this.ptr_is_null(apc_routine)? {
698            throw_unsup_format!(
699                "`NtReadFile` `ApcRoutine` parameter is non-null, which is unsupported"
700            );
701        }
702
703        if !this.ptr_is_null(apc_ctx)? {
704            throw_unsup_format!(
705                "`NtReadFile` `ApcContext` parameter is non-null, which is unsupported"
706            );
707        }
708
709        if byte_offset != 0 {
710            throw_unsup_format!(
711                "`NtReadFile` `ByteOffset` parameter is non-null, which is unsupported"
712            );
713        }
714
715        if !this.ptr_is_null(key)? {
716            throw_unsup_format!("`NtReadFile` `Key` parameter is non-null, which is unsupported");
717        }
718
719        // See NtWriteFile above for commentary on this
720        let io_status = {
721            let anon = this.project_field_named(&io_status_block, "Anonymous")?;
722            this.project_field_named(&anon, "Status")?
723        };
724        let io_status_info = this.project_field_named(&io_status_block, "Information")?;
725
726        let Handle::File(fd) = handle else { this.invalid_handle("NtWriteFile")? };
727
728        let Some(desc) = this.machine.fds.get(fd) else { this.invalid_handle("NtReadFile")? };
729
730        // It seems like short reads are not a thing on Windows, so we don't truncate `count` here.
731        // FIXME: if we are on a Unix host, short host reads are still visible to the program!
732
733        let finish = {
734            let io_status = io_status.clone();
735            let io_status_info = io_status_info.clone();
736            let dest = dest.clone();
737            callback!(
738                @capture<'tcx> {
739                    count: u32,
740                    io_status: MPlaceTy<'tcx>,
741                    io_status_info: MPlaceTy<'tcx>,
742                    dest: MPlaceTy<'tcx>,
743                }
744                |this, result: Result<usize, IoError>| {
745                    match result {
746                        Ok(read_size) => {
747                            assert!(read_size <= count.try_into().unwrap());
748                            // This must fit since `count` fits.
749                            this.write_int(u64::try_from(read_size).unwrap(), &io_status_info)?;
750                            this.write_int(0, &io_status)?;
751                            this.write_int(0, &dest)
752                        }
753                        Err(e) => {
754                            this.write_int(0, &io_status_info)?;
755                            let status = e.into_ntstatus();
756                            this.write_int(status, &io_status)?;
757                            this.write_int(status, &dest)
758                        }
759                }}
760            )
761        };
762        desc.read(this.machine.communicate(), buf, count.try_into().unwrap(), this, finish)?;
763
764        // See NtWriteFile for commentary on this
765        interp_ok(())
766    }
767
768    fn SetFilePointerEx(
769        &mut self,
770        file: &OpTy<'tcx>,         // HANDLE
771        dist_to_move: &OpTy<'tcx>, // LARGE_INTEGER
772        new_fp: &OpTy<'tcx>,       // PLARGE_INTEGER
773        move_method: &OpTy<'tcx>,  // DWORD
774    ) -> InterpResult<'tcx, Scalar> {
775        // ^ Returns BOOL (i32 on Windows)
776        let this = self.eval_context_mut();
777        let file = this.read_handle(file, "SetFilePointerEx")?;
778        let dist_to_move = this.read_scalar(dist_to_move)?.to_i64()?;
779        let new_fp_ptr = this.read_pointer(new_fp)?;
780        let move_method = this.read_scalar(move_method)?.to_u32()?;
781
782        let Handle::File(fd) = file else { this.invalid_handle("SetFilePointerEx")? };
783
784        let Some(desc) = this.machine.fds.get(fd) else {
785            throw_unsup_format!("`SetFilePointerEx` is only supported on file backed handles");
786        };
787
788        let file_begin = this.eval_windows_u32("c", "FILE_BEGIN");
789        let file_current = this.eval_windows_u32("c", "FILE_CURRENT");
790        let file_end = this.eval_windows_u32("c", "FILE_END");
791
792        let seek = if move_method == file_begin {
793            SeekFrom::Start(dist_to_move.try_into().unwrap())
794        } else if move_method == file_current {
795            SeekFrom::Current(dist_to_move)
796        } else if move_method == file_end {
797            SeekFrom::End(dist_to_move)
798        } else {
799            throw_unsup_format!("Invalid move method: {move_method}")
800        };
801
802        match desc.seek(this.machine.communicate(), seek)? {
803            Ok(n) => {
804                if !this.ptr_is_null(new_fp_ptr)? {
805                    this.write_scalar(
806                        Scalar::from_i64(n.try_into().unwrap()),
807                        &this.deref_pointer_as(new_fp, this.machine.layouts.i64)?,
808                    )?;
809                }
810                interp_ok(this.eval_windows("c", "TRUE"))
811            }
812            Err(e) => {
813                this.set_last_error(e)?;
814                interp_ok(this.eval_windows("c", "FALSE"))
815            }
816        }
817    }
818}
819
820/// Windows FILETIME is measured in 100-nanosecs since 1601
821fn extract_windows_epoch<'tcx>(
822    ecx: &MiriInterpCx<'tcx>,
823    time: io::Result<SystemTime>,
824) -> InterpResult<'tcx, Option<(u32, u32)>> {
825    match time.ok() {
826        Some(time) => {
827            let duration = ecx.system_time_since_windows_epoch(&time)?;
828            let duration_ticks = ecx.windows_ticks_for(duration)?;
829            #[expect(clippy::as_conversions)]
830            interp_ok(Some((duration_ticks as u32, (duration_ticks >> 32) as u32)))
831        }
832        None => interp_ok(None),
833    }
834}
835
836fn write_filetime_field<'tcx>(
837    cx: &mut MiriInterpCx<'tcx>,
838    val: &MPlaceTy<'tcx>,
839    name: &str,
840    (low, high): (u32, u32),
841) -> InterpResult<'tcx> {
842    cx.write_int_fields_named(
843        &[("dwLowDateTime", low.into()), ("dwHighDateTime", high.into())],
844        &cx.project_field_named(val, name)?,
845    )
846}