Skip to main content

miri/shims/windows/
fs.rs

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