miri/shims/windows/
fs.rs

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