Skip to main content

miri/shims/windows/
fs.rs

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