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>(&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 MoveFileExW(
494        &mut self,
495        existing_name: &OpTy<'tcx>,
496        new_name: &OpTy<'tcx>,
497        flags: &OpTy<'tcx>,
498    ) -> InterpResult<'tcx, Scalar> {
499        let this = self.eval_context_mut();
500
501        let existing_name = this.read_path_from_wide_str(this.read_pointer(existing_name)?)?;
502        let new_name = this.read_path_from_wide_str(this.read_pointer(new_name)?)?;
503
504        let flags = this.read_scalar(flags)?.to_u32()?;
505
506        // Flag to indicate whether we should replace an existing file.
507        // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-movefileexw
508        let movefile_replace_existing = this.eval_windows_u32("c", "MOVEFILE_REPLACE_EXISTING");
509
510        if flags != movefile_replace_existing {
511            throw_unsup_format!("MoveFileExW: Unsupported `dwFlags` value {}", flags);
512        }
513
514        match std::fs::rename(existing_name, new_name) {
515            Ok(_) => interp_ok(this.eval_windows("c", "TRUE")),
516            Err(e) => {
517                this.set_last_error(e)?;
518                interp_ok(this.eval_windows("c", "FALSE"))
519            }
520        }
521    }
522
523    fn DeleteFileW(
524        &mut self,
525        file_name: &OpTy<'tcx>, // LPCWSTR
526    ) -> InterpResult<'tcx, Scalar> {
527        // ^ Returns BOOL (i32 on Windows)
528        let this = self.eval_context_mut();
529        this.assert_target_os(Os::Windows, "DeleteFileW");
530        this.check_no_isolation("`DeleteFileW`")?;
531
532        let file_name = this.read_path_from_wide_str(this.read_pointer(file_name)?)?;
533        match std::fs::remove_file(file_name) {
534            Ok(_) => interp_ok(this.eval_windows("c", "TRUE")),
535            Err(e) => {
536                this.set_last_error(e)?;
537                interp_ok(this.eval_windows("c", "FALSE"))
538            }
539        }
540    }
541
542    fn NtWriteFile(
543        &mut self,
544        handle: &OpTy<'tcx>,          // HANDLE
545        event: &OpTy<'tcx>,           // HANDLE
546        apc_routine: &OpTy<'tcx>,     // PIO_APC_ROUTINE
547        apc_ctx: &OpTy<'tcx>,         // PVOID
548        io_status_block: &OpTy<'tcx>, // PIO_STATUS_BLOCK
549        buf: &OpTy<'tcx>,             // PVOID
550        n: &OpTy<'tcx>,               // ULONG
551        byte_offset: &OpTy<'tcx>,     // PLARGE_INTEGER
552        key: &OpTy<'tcx>,             // PULONG
553        dest: &MPlaceTy<'tcx>,        // return type: NTSTATUS
554    ) -> InterpResult<'tcx, ()> {
555        let this = self.eval_context_mut();
556        let handle = this.read_handle(handle, "NtWriteFile")?;
557        let event = this.read_handle(event, "NtWriteFile")?;
558        let apc_routine = this.read_pointer(apc_routine)?;
559        let apc_ctx = this.read_pointer(apc_ctx)?;
560        let buf = this.read_pointer(buf)?;
561        let count = this.read_scalar(n)?.to_u32()?;
562        let byte_offset = this.read_target_usize(byte_offset)?; // is actually a pointer, but we only support null
563        let key = this.read_pointer(key)?;
564        let io_status_block =
565            this.deref_pointer_as(io_status_block, this.windows_ty_layout("IO_STATUS_BLOCK"))?;
566
567        if event != Handle::Null {
568            throw_unsup_format!(
569                "`NtWriteFile` `Event` parameter is non-null, which is unsupported"
570            );
571        }
572
573        if !this.ptr_is_null(apc_routine)? {
574            throw_unsup_format!(
575                "`NtWriteFile` `ApcRoutine` parameter is non-null, which is unsupported"
576            );
577        }
578
579        if !this.ptr_is_null(apc_ctx)? {
580            throw_unsup_format!(
581                "`NtWriteFile` `ApcContext` parameter is non-null, which is unsupported"
582            );
583        }
584
585        if byte_offset != 0 {
586            throw_unsup_format!(
587                "`NtWriteFile` `ByteOffset` parameter is non-null, which is unsupported"
588            );
589        }
590
591        if !this.ptr_is_null(key)? {
592            throw_unsup_format!("`NtWriteFile` `Key` parameter is non-null, which is unsupported");
593        }
594
595        let Handle::File(fd) = handle else { this.invalid_handle("NtWriteFile")? };
596
597        let Some(desc) = this.machine.fds.get(fd) else { this.invalid_handle("NtWriteFile")? };
598
599        // Windows writes the output code to IO_STATUS_BLOCK.Status, and number of bytes written
600        // to IO_STATUS_BLOCK.Information.
601        // The status block value and the returned value don't need to match - but
602        // for the cases implemented by miri so far, we can choose to decide that they do.
603        let io_status = {
604            let anon = this.project_field_named(&io_status_block, "Anonymous")?;
605            this.project_field_named(&anon, "Status")?
606        };
607        let io_status_info = this.project_field_named(&io_status_block, "Information")?;
608
609        // It seems like short writes are not a thing on Windows, so we don't truncate `count` here.
610        // FIXME: if we are on a Unix host, short host writes are still visible to the program!
611
612        let finish = {
613            let io_status = io_status.clone();
614            let io_status_info = io_status_info.clone();
615            let dest = dest.clone();
616            callback!(
617                @capture<'tcx> {
618                    count: u32,
619                    io_status: MPlaceTy<'tcx>,
620                    io_status_info: MPlaceTy<'tcx>,
621                    dest: MPlaceTy<'tcx>,
622                }
623                |this, result: Result<usize, IoError>| {
624                    match result {
625                        Ok(read_size) => {
626                            assert!(read_size <= count.try_into().unwrap());
627                            // This must fit since `count` fits.
628                            this.write_int(u64::try_from(read_size).unwrap(), &io_status_info)?;
629                            this.write_int(0, &io_status)?;
630                            this.write_int(0, &dest)
631                        }
632                        Err(e) => {
633                            this.write_int(0, &io_status_info)?;
634                            let status = e.into_ntstatus();
635                            this.write_int(status, &io_status)?;
636                            this.write_int(status, &dest)
637                        }
638                }}
639            )
640        };
641        desc.write(this.machine.communicate(), buf, count.try_into().unwrap(), this, finish)?;
642
643        // Return status is written to `dest` and `io_status_block` on callback completion.
644        interp_ok(())
645    }
646
647    fn NtReadFile(
648        &mut self,
649        handle: &OpTy<'tcx>,          // HANDLE
650        event: &OpTy<'tcx>,           // HANDLE
651        apc_routine: &OpTy<'tcx>,     // PIO_APC_ROUTINE
652        apc_ctx: &OpTy<'tcx>,         // PVOID
653        io_status_block: &OpTy<'tcx>, // PIO_STATUS_BLOCK
654        buf: &OpTy<'tcx>,             // PVOID
655        n: &OpTy<'tcx>,               // ULONG
656        byte_offset: &OpTy<'tcx>,     // PLARGE_INTEGER
657        key: &OpTy<'tcx>,             // PULONG
658        dest: &MPlaceTy<'tcx>,        // return type: NTSTATUS
659    ) -> InterpResult<'tcx, ()> {
660        let this = self.eval_context_mut();
661        let handle = this.read_handle(handle, "NtReadFile")?;
662        let event = this.read_handle(event, "NtReadFile")?;
663        let apc_routine = this.read_pointer(apc_routine)?;
664        let apc_ctx = this.read_pointer(apc_ctx)?;
665        let buf = this.read_pointer(buf)?;
666        let count = this.read_scalar(n)?.to_u32()?;
667        let byte_offset = this.read_target_usize(byte_offset)?; // is actually a pointer, but we only support null
668        let key = this.read_pointer(key)?;
669        let io_status_block =
670            this.deref_pointer_as(io_status_block, this.windows_ty_layout("IO_STATUS_BLOCK"))?;
671
672        if event != Handle::Null {
673            throw_unsup_format!("`NtReadFile` `Event` parameter is non-null, which is unsupported");
674        }
675
676        if !this.ptr_is_null(apc_routine)? {
677            throw_unsup_format!(
678                "`NtReadFile` `ApcRoutine` parameter is non-null, which is unsupported"
679            );
680        }
681
682        if !this.ptr_is_null(apc_ctx)? {
683            throw_unsup_format!(
684                "`NtReadFile` `ApcContext` parameter is non-null, which is unsupported"
685            );
686        }
687
688        if byte_offset != 0 {
689            throw_unsup_format!(
690                "`NtReadFile` `ByteOffset` parameter is non-null, which is unsupported"
691            );
692        }
693
694        if !this.ptr_is_null(key)? {
695            throw_unsup_format!("`NtReadFile` `Key` parameter is non-null, which is unsupported");
696        }
697
698        // See NtWriteFile above for commentary on this
699        let io_status = {
700            let anon = this.project_field_named(&io_status_block, "Anonymous")?;
701            this.project_field_named(&anon, "Status")?
702        };
703        let io_status_info = this.project_field_named(&io_status_block, "Information")?;
704
705        let Handle::File(fd) = handle else { this.invalid_handle("NtWriteFile")? };
706
707        let Some(desc) = this.machine.fds.get(fd) else { this.invalid_handle("NtReadFile")? };
708
709        // It seems like short reads are not a thing on Windows, so we don't truncate `count` here.
710        // FIXME: if we are on a Unix host, short host reads are still visible to the program!
711
712        let finish = {
713            let io_status = io_status.clone();
714            let io_status_info = io_status_info.clone();
715            let dest = dest.clone();
716            callback!(
717                @capture<'tcx> {
718                    count: u32,
719                    io_status: MPlaceTy<'tcx>,
720                    io_status_info: MPlaceTy<'tcx>,
721                    dest: MPlaceTy<'tcx>,
722                }
723                |this, result: Result<usize, IoError>| {
724                    match result {
725                        Ok(read_size) => {
726                            assert!(read_size <= count.try_into().unwrap());
727                            // This must fit since `count` fits.
728                            this.write_int(u64::try_from(read_size).unwrap(), &io_status_info)?;
729                            this.write_int(0, &io_status)?;
730                            this.write_int(0, &dest)
731                        }
732                        Err(e) => {
733                            this.write_int(0, &io_status_info)?;
734                            let status = e.into_ntstatus();
735                            this.write_int(status, &io_status)?;
736                            this.write_int(status, &dest)
737                        }
738                }}
739            )
740        };
741        desc.read(this.machine.communicate(), buf, count.try_into().unwrap(), this, finish)?;
742
743        // See NtWriteFile for commentary on this
744        interp_ok(())
745    }
746
747    fn SetFilePointerEx(
748        &mut self,
749        file: &OpTy<'tcx>,         // HANDLE
750        dist_to_move: &OpTy<'tcx>, // LARGE_INTEGER
751        new_fp: &OpTy<'tcx>,       // PLARGE_INTEGER
752        move_method: &OpTy<'tcx>,  // DWORD
753    ) -> InterpResult<'tcx, Scalar> {
754        // ^ Returns BOOL (i32 on Windows)
755        let this = self.eval_context_mut();
756        let file = this.read_handle(file, "SetFilePointerEx")?;
757        let dist_to_move = this.read_scalar(dist_to_move)?.to_i64()?;
758        let new_fp_ptr = this.read_pointer(new_fp)?;
759        let move_method = this.read_scalar(move_method)?.to_u32()?;
760
761        let Handle::File(fd) = file else { this.invalid_handle("SetFilePointerEx")? };
762
763        let Some(desc) = this.machine.fds.get(fd) else {
764            throw_unsup_format!("`SetFilePointerEx` is only supported on file backed handles");
765        };
766
767        let file_begin = this.eval_windows_u32("c", "FILE_BEGIN");
768        let file_current = this.eval_windows_u32("c", "FILE_CURRENT");
769        let file_end = this.eval_windows_u32("c", "FILE_END");
770
771        let seek = if move_method == file_begin {
772            SeekFrom::Start(dist_to_move.try_into().unwrap())
773        } else if move_method == file_current {
774            SeekFrom::Current(dist_to_move)
775        } else if move_method == file_end {
776            SeekFrom::End(dist_to_move)
777        } else {
778            throw_unsup_format!("Invalid move method: {move_method}")
779        };
780
781        match desc.seek(this.machine.communicate(), seek)? {
782            Ok(n) => {
783                if !this.ptr_is_null(new_fp_ptr)? {
784                    this.write_scalar(
785                        Scalar::from_i64(n.try_into().unwrap()),
786                        &this.deref_pointer_as(new_fp, this.machine.layouts.i64)?,
787                    )?;
788                }
789                interp_ok(this.eval_windows("c", "TRUE"))
790            }
791            Err(e) => {
792                this.set_last_error(e)?;
793                interp_ok(this.eval_windows("c", "FALSE"))
794            }
795        }
796    }
797}
798
799/// Windows FILETIME is measured in 100-nanosecs since 1601
800fn extract_windows_epoch<'tcx>(
801    ecx: &MiriInterpCx<'tcx>,
802    time: io::Result<SystemTime>,
803) -> InterpResult<'tcx, Option<(u32, u32)>> {
804    match time.ok() {
805        Some(time) => {
806            let duration = ecx.system_time_since_windows_epoch(&time)?;
807            let duration_ticks = ecx.windows_ticks_for(duration)?;
808            #[expect(clippy::as_conversions)]
809            interp_ok(Some((duration_ticks as u32, (duration_ticks >> 32) as u32)))
810        }
811        None => interp_ok(None),
812    }
813}
814
815fn write_filetime_field<'tcx>(
816    cx: &mut MiriInterpCx<'tcx>,
817    val: &MPlaceTy<'tcx>,
818    name: &str,
819    (low, high): (u32, u32),
820) -> InterpResult<'tcx> {
821    cx.write_int_fields_named(
822        &[("dwLowDateTime", low.into()), ("dwHighDateTime", high.into())],
823        &cx.project_field_named(val, name)?,
824    )
825}