Skip to main content

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