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