Skip to main content

miri/shims/windows/
fs.rs

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