Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c9613f8

Browse files
authoredApr 10, 2025
Rollup merge of rust-lang#139630 - RalfJung:miri-sync, r=RalfJung
Miri subtree update r? `@ghost` a sync is needed to fix the miri-test-libstd failures
2 parents 9b9d098 + 955d92f commit c9613f8

File tree

27 files changed

+1532
-229
lines changed

27 files changed

+1532
-229
lines changed
 

‎Cargo.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2312,6 +2312,7 @@ name = "miri"
23122312
version = "0.1.0"
23132313
dependencies = [
23142314
"aes",
2315+
"bitflags",
23152316
"chrono",
23162317
"chrono-tz",
23172318
"colored",

‎src/tools/miri/Cargo.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,7 @@ name = "miri"
538538
version = "0.1.0"
539539
dependencies = [
540540
"aes",
541+
"bitflags",
541542
"chrono",
542543
"chrono-tz",
543544
"colored",

‎src/tools/miri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ measureme = "12"
2626
chrono = { version = "0.4.38", default-features = false }
2727
chrono-tz = "0.10"
2828
directories = "6"
29+
bitflags = "2.6"
2930

3031
# Copied from `compiler/rustc/Cargo.toml`.
3132
# But only for some targets, it fails for others. Rustc configures this in its CI, but we can't

‎src/tools/miri/ci/ci.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ case $HOST_TARGET in
164164
# Partially supported targets (tier 2)
165165
BASIC="empty_main integer heap_alloc libc-mem vec string btreemap" # ensures we have the basics: pre-main code, system allocator
166166
UNIX="hello panic/panic panic/unwind concurrency/simple atomic libc-mem libc-misc libc-random env num_cpus" # the things that are very similar across all Unixes, and hence easily supported there
167-
TEST_TARGET=x86_64-unknown-freebsd run_tests_minimal $BASIC $UNIX time hashmap random threadname pthread fs libc-pipe
167+
TEST_TARGET=x86_64-unknown-freebsd run_tests_minimal $BASIC $UNIX time hashmap random threadname pthread fs libc-pipe concurrency sync
168168
TEST_TARGET=i686-unknown-freebsd run_tests_minimal $BASIC $UNIX time hashmap random threadname pthread fs libc-pipe
169169
TEST_TARGET=aarch64-linux-android run_tests_minimal $BASIC $UNIX time hashmap random sync concurrency thread epoll eventfd
170170
TEST_TARGET=wasm32-wasip2 run_tests_minimal $BASIC wasm

‎src/tools/miri/rust-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
25a615bf829b9f6d6f22da537e3851043f92e5f2
1+
7d7de5bf3c3cbf9c2c5bbc5cbfb9197a8a427d35

‎src/tools/miri/src/shims/files.rs

Lines changed: 105 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::any::Any;
22
use std::collections::BTreeMap;
3-
use std::io::{IsTerminal, SeekFrom, Write};
3+
use std::fs::{File, Metadata};
4+
use std::io::{IsTerminal, Seek, SeekFrom, Write};
45
use std::marker::CoercePointee;
56
use std::ops::Deref;
67
use std::rc::{Rc, Weak};
@@ -192,7 +193,7 @@ pub trait FileDescription: std::fmt::Debug + FileDescriptionExt {
192193
false
193194
}
194195

195-
fn as_unix(&self) -> &dyn UnixFileDescription {
196+
fn as_unix<'tcx>(&self, _ecx: &MiriInterpCx<'tcx>) -> &dyn UnixFileDescription {
196197
panic!("Not a unix file descriptor: {}", self.name());
197198
}
198199
}
@@ -278,6 +279,97 @@ impl FileDescription for io::Stderr {
278279
}
279280
}
280281

282+
#[derive(Debug)]
283+
pub struct FileHandle {
284+
pub(crate) file: File,
285+
pub(crate) writable: bool,
286+
}
287+
288+
impl FileDescription for FileHandle {
289+
fn name(&self) -> &'static str {
290+
"file"
291+
}
292+
293+
fn read<'tcx>(
294+
self: FileDescriptionRef<Self>,
295+
communicate_allowed: bool,
296+
ptr: Pointer,
297+
len: usize,
298+
ecx: &mut MiriInterpCx<'tcx>,
299+
finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
300+
) -> InterpResult<'tcx> {
301+
assert!(communicate_allowed, "isolation should have prevented even opening a file");
302+
303+
let result = ecx.read_from_host(&self.file, len, ptr)?;
304+
finish.call(ecx, result)
305+
}
306+
307+
fn write<'tcx>(
308+
self: FileDescriptionRef<Self>,
309+
communicate_allowed: bool,
310+
ptr: Pointer,
311+
len: usize,
312+
ecx: &mut MiriInterpCx<'tcx>,
313+
finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
314+
) -> InterpResult<'tcx> {
315+
assert!(communicate_allowed, "isolation should have prevented even opening a file");
316+
317+
let result = ecx.write_to_host(&self.file, len, ptr)?;
318+
finish.call(ecx, result)
319+
}
320+
321+
fn seek<'tcx>(
322+
&self,
323+
communicate_allowed: bool,
324+
offset: SeekFrom,
325+
) -> InterpResult<'tcx, io::Result<u64>> {
326+
assert!(communicate_allowed, "isolation should have prevented even opening a file");
327+
interp_ok((&mut &self.file).seek(offset))
328+
}
329+
330+
fn close<'tcx>(
331+
self,
332+
communicate_allowed: bool,
333+
_ecx: &mut MiriInterpCx<'tcx>,
334+
) -> InterpResult<'tcx, io::Result<()>> {
335+
assert!(communicate_allowed, "isolation should have prevented even opening a file");
336+
// We sync the file if it was opened in a mode different than read-only.
337+
if self.writable {
338+
// `File::sync_all` does the checks that are done when closing a file. We do this to
339+
// to handle possible errors correctly.
340+
let result = self.file.sync_all();
341+
// Now we actually close the file and return the result.
342+
drop(self.file);
343+
interp_ok(result)
344+
} else {
345+
// We drop the file, this closes it but ignores any errors
346+
// produced when closing it. This is done because
347+
// `File::sync_all` cannot be done over files like
348+
// `/dev/urandom` which are read-only. Check
349+
// https://github.com/rust-lang/miri/issues/999#issuecomment-568920439
350+
// for a deeper discussion.
351+
drop(self.file);
352+
interp_ok(Ok(()))
353+
}
354+
}
355+
356+
fn metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result<Metadata>> {
357+
interp_ok(self.file.metadata())
358+
}
359+
360+
fn is_tty(&self, communicate_allowed: bool) -> bool {
361+
communicate_allowed && self.file.is_terminal()
362+
}
363+
364+
fn as_unix<'tcx>(&self, ecx: &MiriInterpCx<'tcx>) -> &dyn UnixFileDescription {
365+
assert!(
366+
ecx.target_os_is_unix(),
367+
"unix file operations are only available for unix targets"
368+
);
369+
self
370+
}
371+
}
372+
281373
/// Like /dev/null
282374
#[derive(Debug)]
283375
pub struct NullOutput;
@@ -300,10 +392,13 @@ impl FileDescription for NullOutput {
300392
}
301393
}
302394

395+
/// Internal type of a file-descriptor - this is what [`FdTable`] expects
396+
pub type FdNum = i32;
397+
303398
/// The file descriptor table
304399
#[derive(Debug)]
305400
pub struct FdTable {
306-
pub fds: BTreeMap<i32, DynFileDescriptionRef>,
401+
pub fds: BTreeMap<FdNum, DynFileDescriptionRef>,
307402
/// Unique identifier for file description, used to differentiate between various file description.
308403
next_file_description_id: FdId,
309404
}
@@ -339,21 +434,21 @@ impl FdTable {
339434
}
340435

341436
/// Insert a new file description to the FdTable.
342-
pub fn insert_new(&mut self, fd: impl FileDescription) -> i32 {
437+
pub fn insert_new(&mut self, fd: impl FileDescription) -> FdNum {
343438
let fd_ref = self.new_ref(fd);
344439
self.insert(fd_ref)
345440
}
346441

347-
pub fn insert(&mut self, fd_ref: DynFileDescriptionRef) -> i32 {
442+
pub fn insert(&mut self, fd_ref: DynFileDescriptionRef) -> FdNum {
348443
self.insert_with_min_num(fd_ref, 0)
349444
}
350445

351446
/// Insert a file description, giving it a file descriptor that is at least `min_fd_num`.
352447
pub fn insert_with_min_num(
353448
&mut self,
354449
file_handle: DynFileDescriptionRef,
355-
min_fd_num: i32,
356-
) -> i32 {
450+
min_fd_num: FdNum,
451+
) -> FdNum {
357452
// Find the lowest unused FD, starting from min_fd. If the first such unused FD is in
358453
// between used FDs, the find_map combinator will return it. If the first such unused FD
359454
// is after all other used FDs, the find_map combinator will return None, and we will use
@@ -379,16 +474,16 @@ impl FdTable {
379474
new_fd_num
380475
}
381476

382-
pub fn get(&self, fd_num: i32) -> Option<DynFileDescriptionRef> {
477+
pub fn get(&self, fd_num: FdNum) -> Option<DynFileDescriptionRef> {
383478
let fd = self.fds.get(&fd_num)?;
384479
Some(fd.clone())
385480
}
386481

387-
pub fn remove(&mut self, fd_num: i32) -> Option<DynFileDescriptionRef> {
482+
pub fn remove(&mut self, fd_num: FdNum) -> Option<DynFileDescriptionRef> {
388483
self.fds.remove(&fd_num)
389484
}
390485

391-
pub fn is_fd_num(&self, fd_num: i32) -> bool {
486+
pub fn is_fd_num(&self, fd_num: FdNum) -> bool {
392487
self.fds.contains_key(&fd_num)
393488
}
394489
}

‎src/tools/miri/src/shims/time.rs

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -219,16 +219,8 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
219219

220220
let filetime = this.deref_pointer_as(LPFILETIME_op, this.windows_ty_layout("FILETIME"))?;
221221

222-
let NANOS_PER_SEC = this.eval_windows_u64("time", "NANOS_PER_SEC");
223-
let INTERVALS_PER_SEC = this.eval_windows_u64("time", "INTERVALS_PER_SEC");
224-
let INTERVALS_TO_UNIX_EPOCH = this.eval_windows_u64("time", "INTERVALS_TO_UNIX_EPOCH");
225-
let NANOS_PER_INTERVAL = NANOS_PER_SEC / INTERVALS_PER_SEC;
226-
let SECONDS_TO_UNIX_EPOCH = INTERVALS_TO_UNIX_EPOCH / INTERVALS_PER_SEC;
227-
228-
let duration = system_time_to_duration(&SystemTime::now())?
229-
+ Duration::from_secs(SECONDS_TO_UNIX_EPOCH);
230-
let duration_ticks = u64::try_from(duration.as_nanos() / u128::from(NANOS_PER_INTERVAL))
231-
.map_err(|_| err_unsup_format!("programs running more than 2^64 Windows ticks after the Windows epoch are not supported"))?;
222+
let duration = this.system_time_since_windows_epoch(&SystemTime::now())?;
223+
let duration_ticks = this.windows_ticks_for(duration)?;
232224

233225
let dwLowDateTime = u32::try_from(duration_ticks & 0x00000000FFFFFFFF).unwrap();
234226
let dwHighDateTime = u32::try_from((duration_ticks & 0xFFFFFFFF00000000) >> 32).unwrap();
@@ -281,6 +273,30 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
281273
interp_ok(Scalar::from_i32(-1)) // Return non-zero on success
282274
}
283275

276+
#[allow(non_snake_case, clippy::arithmetic_side_effects)]
277+
fn system_time_since_windows_epoch(&self, time: &SystemTime) -> InterpResult<'tcx, Duration> {
278+
let this = self.eval_context_ref();
279+
280+
let INTERVALS_PER_SEC = this.eval_windows_u64("time", "INTERVALS_PER_SEC");
281+
let INTERVALS_TO_UNIX_EPOCH = this.eval_windows_u64("time", "INTERVALS_TO_UNIX_EPOCH");
282+
let SECONDS_TO_UNIX_EPOCH = INTERVALS_TO_UNIX_EPOCH / INTERVALS_PER_SEC;
283+
284+
interp_ok(system_time_to_duration(time)? + Duration::from_secs(SECONDS_TO_UNIX_EPOCH))
285+
}
286+
287+
#[allow(non_snake_case, clippy::arithmetic_side_effects)]
288+
fn windows_ticks_for(&self, duration: Duration) -> InterpResult<'tcx, u64> {
289+
let this = self.eval_context_ref();
290+
291+
let NANOS_PER_SEC = this.eval_windows_u64("time", "NANOS_PER_SEC");
292+
let INTERVALS_PER_SEC = this.eval_windows_u64("time", "INTERVALS_PER_SEC");
293+
let NANOS_PER_INTERVAL = NANOS_PER_SEC / INTERVALS_PER_SEC;
294+
295+
let ticks = u64::try_from(duration.as_nanos() / u128::from(NANOS_PER_INTERVAL))
296+
.map_err(|_| err_unsup_format!("programs running more than 2^64 Windows ticks after the Windows epoch are not supported"))?;
297+
interp_ok(ticks)
298+
}
299+
284300
fn mach_absolute_time(&self) -> InterpResult<'tcx, Scalar> {
285301
let this = self.eval_context_ref();
286302

‎src/tools/miri/src/shims/unix/fd.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
121121
throw_unsup_format!("unsupported flags {:#x}", op);
122122
};
123123

124-
let result = fd.as_unix().flock(this.machine.communicate(), parsed_op)?;
124+
let result = fd.as_unix(this).flock(this.machine.communicate(), parsed_op)?;
125125
// return `0` if flock is successful
126126
let result = result.map(|()| 0i32);
127127
interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
@@ -273,7 +273,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
273273
let Ok(offset) = u64::try_from(offset) else {
274274
return this.set_last_error_and_return(LibcError("EINVAL"), dest);
275275
};
276-
fd.as_unix().pread(communicate, offset, buf, count, this, finish)?
276+
fd.as_unix(this).pread(communicate, offset, buf, count, this, finish)?
277277
}
278278
};
279279
interp_ok(())
@@ -333,7 +333,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
333333
let Ok(offset) = u64::try_from(offset) else {
334334
return this.set_last_error_and_return(LibcError("EINVAL"), dest);
335335
};
336-
fd.as_unix().pwrite(communicate, buf, count, offset, this, finish)?
336+
fd.as_unix(this).pwrite(communicate, buf, count, offset, this, finish)?
337337
}
338338
};
339339
interp_ok(())

‎src/tools/miri/src/shims/unix/freebsd/foreign_items.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use rustc_middle::ty::Ty;
22
use rustc_span::Symbol;
33
use rustc_target::callconv::{Conv, FnAbi};
44

5+
use super::sync::EvalContextExt as _;
56
use crate::shims::unix::*;
67
use crate::*;
78

@@ -55,6 +56,13 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
5556
this.write_scalar(res, dest)?;
5657
}
5758

59+
// Synchronization primitives
60+
"_umtx_op" => {
61+
let [obj, op, val, uaddr, uaddr2] =
62+
this.check_shim(abi, Conv::C, link_name, args)?;
63+
this._umtx_op(obj, op, val, uaddr, uaddr2, dest)?;
64+
}
65+
5866
// File related shims
5967
// For those, we both intercept `func` and `call@FBSD_1.0` symbols cases
6068
// since freebsd 12 the former form can be expected.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pub mod foreign_items;
2+
pub mod sync;
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
//! Contains FreeBSD-specific synchronization functions
2+
3+
use core::time::Duration;
4+
5+
use crate::concurrency::sync::FutexRef;
6+
use crate::*;
7+
8+
pub struct FreeBsdFutex {
9+
futex: FutexRef,
10+
}
11+
12+
/// Extended variant of the `timespec` struct.
13+
pub struct UmtxTime {
14+
timeout: Duration,
15+
abs_time: bool,
16+
timeout_clock: TimeoutClock,
17+
}
18+
19+
impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
20+
pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
21+
/// Implementation of the FreeBSD [`_umtx_op`](https://man.freebsd.org/cgi/man.cgi?query=_umtx_op&sektion=2&manpath=FreeBSD+14.2-RELEASE+and+Ports) syscall.
22+
/// This is used for futex operations on FreeBSD.
23+
///
24+
/// `obj`: a pointer to the futex object (can be a lot of things, mostly *AtomicU32)
25+
/// `op`: the futex operation to run
26+
/// `val`: the current value of the object as a `c_long` (for wait/wake)
27+
/// `uaddr`: `op`-specific optional parameter, pointer-sized integer or pointer to an `op`-specific struct
28+
/// `uaddr2`: `op`-specific optional parameter, pointer-sized integer or pointer to an `op`-specific struct
29+
/// `dest`: the place this syscall returns to, 0 for success, -1 for failure
30+
///
31+
/// # Note
32+
/// Curently only the WAIT and WAKE operations are implemented.
33+
fn _umtx_op(
34+
&mut self,
35+
obj: &OpTy<'tcx>,
36+
op: &OpTy<'tcx>,
37+
val: &OpTy<'tcx>,
38+
uaddr: &OpTy<'tcx>,
39+
uaddr2: &OpTy<'tcx>,
40+
dest: &MPlaceTy<'tcx>,
41+
) -> InterpResult<'tcx> {
42+
let this = self.eval_context_mut();
43+
44+
let obj = this.read_pointer(obj)?;
45+
let op = this.read_scalar(op)?.to_i32()?;
46+
let val = this.read_target_usize(val)?;
47+
let uaddr = this.read_target_usize(uaddr)?;
48+
let uaddr2 = this.read_pointer(uaddr2)?;
49+
50+
let wait = this.eval_libc_i32("UMTX_OP_WAIT");
51+
let wait_uint = this.eval_libc_i32("UMTX_OP_WAIT_UINT");
52+
let wait_uint_private = this.eval_libc_i32("UMTX_OP_WAIT_UINT_PRIVATE");
53+
54+
let wake = this.eval_libc_i32("UMTX_OP_WAKE");
55+
let wake_private = this.eval_libc_i32("UMTX_OP_WAKE_PRIVATE");
56+
57+
let timespec_layout = this.libc_ty_layout("timespec");
58+
let umtx_time_layout = this.libc_ty_layout("_umtx_time");
59+
assert!(
60+
timespec_layout.size != umtx_time_layout.size,
61+
"`struct timespec` and `struct _umtx_time` should have different sizes."
62+
);
63+
64+
match op {
65+
// UMTX_OP_WAIT_UINT and UMTX_OP_WAIT_UINT_PRIVATE only differ in whether they work across
66+
// processes or not. For Miri, we can treat them the same.
67+
op if op == wait || op == wait_uint || op == wait_uint_private => {
68+
let obj_layout =
69+
if op == wait { this.machine.layouts.isize } else { this.machine.layouts.u32 };
70+
let obj = this.ptr_to_mplace(obj, obj_layout);
71+
72+
// Read the Linux futex wait implementation in Miri to understand why this fence is needed.
73+
this.atomic_fence(AtomicFenceOrd::SeqCst)?;
74+
let obj_val = this
75+
.read_scalar_atomic(&obj, AtomicReadOrd::Acquire)?
76+
.to_bits(obj_layout.size)?; // isize and u32 can have different sizes
77+
78+
if obj_val == u128::from(val) {
79+
// This cannot fail since we already did an atomic acquire read on that pointer.
80+
// Acquire reads are only allowed on mutable memory.
81+
let futex_ref = this
82+
.get_sync_or_init(obj.ptr(), |_| FreeBsdFutex { futex: Default::default() })
83+
.unwrap()
84+
.futex
85+
.clone();
86+
87+
// From the manual:
88+
// The timeout is specified by passing either the address of `struct timespec`, or its
89+
// extended variant, `struct _umtx_time`, as the `uaddr2` argument of _umtx_op().
90+
// They are distinguished by the `uaddr` value, which must be equal
91+
// to the size of the structure pointed to by `uaddr2`, casted to uintptr_t.
92+
let timeout = if this.ptr_is_null(uaddr2)? {
93+
// no timeout parameter
94+
None
95+
} else {
96+
if uaddr == umtx_time_layout.size.bytes() {
97+
// `uaddr2` points to a `struct _umtx_time`.
98+
let umtx_time_place = this.ptr_to_mplace(uaddr2, umtx_time_layout);
99+
100+
let umtx_time = match this.read_umtx_time(&umtx_time_place)? {
101+
Some(ut) => ut,
102+
None => {
103+
return this
104+
.set_last_error_and_return(LibcError("EINVAL"), dest);
105+
}
106+
};
107+
108+
let anchor = if umtx_time.abs_time {
109+
TimeoutAnchor::Absolute
110+
} else {
111+
TimeoutAnchor::Relative
112+
};
113+
114+
Some((umtx_time.timeout_clock, anchor, umtx_time.timeout))
115+
} else if uaddr == timespec_layout.size.bytes() {
116+
// RealTime clock can't be used in isolation mode.
117+
this.check_no_isolation("`_umtx_op` with `timespec` timeout")?;
118+
119+
// `uaddr2` points to a `struct timespec`.
120+
let timespec = this.ptr_to_mplace(uaddr2, timespec_layout);
121+
let duration = match this.read_timespec(&timespec)? {
122+
Some(duration) => duration,
123+
None => {
124+
return this
125+
.set_last_error_and_return(LibcError("EINVAL"), dest);
126+
}
127+
};
128+
129+
// FreeBSD does not seem to document which clock is used when the timeout
130+
// is passed as a `struct timespec*`. Based on discussions online and the source
131+
// code (umtx_copyin_umtx_time() in kern_umtx.c), it seems to default to CLOCK_REALTIME,
132+
// so that's what we also do.
133+
// Discussion in golang: https://github.com/golang/go/issues/17168#issuecomment-250235271
134+
Some((TimeoutClock::RealTime, TimeoutAnchor::Relative, duration))
135+
} else {
136+
return this.set_last_error_and_return(LibcError("EINVAL"), dest);
137+
}
138+
};
139+
140+
let dest = dest.clone();
141+
this.futex_wait(
142+
futex_ref,
143+
u32::MAX, // we set the bitset to include all bits
144+
timeout,
145+
callback!(
146+
@capture<'tcx> {
147+
dest: MPlaceTy<'tcx>,
148+
}
149+
|ecx, unblock: UnblockKind| match unblock {
150+
UnblockKind::Ready => {
151+
// From the manual:
152+
// If successful, all requests, except UMTX_SHM_CREAT and UMTX_SHM_LOOKUP
153+
// sub-requests of the UMTX_OP_SHM request, will return zero.
154+
ecx.write_int(0, &dest)
155+
}
156+
UnblockKind::TimedOut => {
157+
ecx.set_last_error_and_return(LibcError("ETIMEDOUT"), &dest)
158+
}
159+
}
160+
),
161+
);
162+
interp_ok(())
163+
} else {
164+
// The manual doesn’t specify what should happen if the futex value doesn’t match the expected one.
165+
// On FreeBSD 14.2, testing shows that WAIT operations return 0 even when the value is incorrect.
166+
this.write_int(0, dest)?;
167+
interp_ok(())
168+
}
169+
}
170+
// UMTX_OP_WAKE and UMTX_OP_WAKE_PRIVATE only differ in whether they work across
171+
// processes or not. For Miri, we can treat them the same.
172+
op if op == wake || op == wake_private => {
173+
let Some(futex_ref) =
174+
this.get_sync_or_init(obj, |_| FreeBsdFutex { futex: Default::default() })
175+
else {
176+
// From Linux implemenation:
177+
// No AllocId, or no live allocation at that AllocId.
178+
// Return an error code. (That seems nicer than silently doing something non-intuitive.)
179+
// This means that if an address gets reused by a new allocation,
180+
// we'll use an independent futex queue for this... that seems acceptable.
181+
return this.set_last_error_and_return(LibcError("EFAULT"), dest);
182+
};
183+
let futex_ref = futex_ref.futex.clone();
184+
185+
// Saturating cast for when usize is smaller than u64.
186+
let count = usize::try_from(val).unwrap_or(usize::MAX);
187+
188+
// Read the Linux futex wake implementation in Miri to understand why this fence is needed.
189+
this.atomic_fence(AtomicFenceOrd::SeqCst)?;
190+
191+
// `_umtx_op` doesn't return the amount of woken threads.
192+
let _woken = this.futex_wake(
193+
&futex_ref,
194+
u32::MAX, // we set the bitset to include all bits
195+
count,
196+
)?;
197+
198+
// From the manual:
199+
// If successful, all requests, except UMTX_SHM_CREAT and UMTX_SHM_LOOKUP
200+
// sub-requests of the UMTX_OP_SHM request, will return zero.
201+
this.write_int(0, dest)?;
202+
interp_ok(())
203+
}
204+
op => {
205+
throw_unsup_format!("Miri does not support `_umtx_op` syscall with op={}", op)
206+
}
207+
}
208+
}
209+
210+
/// Parses a `_umtx_time` struct.
211+
/// Returns `None` if the underlying `timespec` struct is invalid.
212+
fn read_umtx_time(&mut self, ut: &MPlaceTy<'tcx>) -> InterpResult<'tcx, Option<UmtxTime>> {
213+
let this = self.eval_context_mut();
214+
// Only flag allowed is UMTX_ABSTIME.
215+
let abs_time = this.eval_libc_u32("UMTX_ABSTIME");
216+
217+
let timespec_place = this.project_field(ut, 0)?;
218+
// Inner `timespec` must still be valid.
219+
let duration = match this.read_timespec(&timespec_place)? {
220+
Some(dur) => dur,
221+
None => return interp_ok(None),
222+
};
223+
224+
let flags_place = this.project_field(ut, 1)?;
225+
let flags = this.read_scalar(&flags_place)?.to_u32()?;
226+
let abs_time_flag = flags == abs_time;
227+
228+
let clock_id_place = this.project_field(ut, 2)?;
229+
let clock_id = this.read_scalar(&clock_id_place)?.to_i32()?;
230+
let timeout_clock = this.translate_umtx_time_clock_id(clock_id)?;
231+
232+
interp_ok(Some(UmtxTime { timeout: duration, abs_time: abs_time_flag, timeout_clock }))
233+
}
234+
235+
/// Translate raw FreeBSD clockid to a Miri TimeoutClock.
236+
/// FIXME: share this code with the pthread and clock_gettime shims.
237+
fn translate_umtx_time_clock_id(&mut self, raw_id: i32) -> InterpResult<'tcx, TimeoutClock> {
238+
let this = self.eval_context_mut();
239+
240+
let timeout = if raw_id == this.eval_libc_i32("CLOCK_REALTIME") {
241+
// RealTime clock can't be used in isolation mode.
242+
this.check_no_isolation("`_umtx_op` with `CLOCK_REALTIME` timeout")?;
243+
TimeoutClock::RealTime
244+
} else if raw_id == this.eval_libc_i32("CLOCK_MONOTONIC") {
245+
TimeoutClock::Monotonic
246+
} else {
247+
throw_unsup_format!("unsupported clock id {raw_id}");
248+
};
249+
interp_ok(timeout)
250+
}
251+
}

‎src/tools/miri/src/shims/unix/fs.rs

Lines changed: 3 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
33
use std::borrow::Cow;
44
use std::fs::{
5-
DirBuilder, File, FileType, Metadata, OpenOptions, ReadDir, read_dir, remove_dir, remove_file,
6-
rename,
5+
DirBuilder, File, FileType, OpenOptions, ReadDir, read_dir, remove_dir, remove_file, rename,
76
};
8-
use std::io::{self, ErrorKind, IsTerminal, Read, Seek, SeekFrom, Write};
7+
use std::io::{self, ErrorKind, Read, Seek, SeekFrom, Write};
98
use std::path::{Path, PathBuf};
109
use std::time::SystemTime;
1110

@@ -14,98 +13,11 @@ use rustc_data_structures::fx::FxHashMap;
1413

1514
use self::shims::time::system_time_to_duration;
1615
use crate::helpers::check_min_vararg_count;
17-
use crate::shims::files::{EvalContextExt as _, FileDescription, FileDescriptionRef};
16+
use crate::shims::files::FileHandle;
1817
use crate::shims::os_str::bytes_to_os_str;
1918
use crate::shims::unix::fd::{FlockOp, UnixFileDescription};
2019
use crate::*;
2120

22-
#[derive(Debug)]
23-
struct FileHandle {
24-
file: File,
25-
writable: bool,
26-
}
27-
28-
impl FileDescription for FileHandle {
29-
fn name(&self) -> &'static str {
30-
"file"
31-
}
32-
33-
fn read<'tcx>(
34-
self: FileDescriptionRef<Self>,
35-
communicate_allowed: bool,
36-
ptr: Pointer,
37-
len: usize,
38-
ecx: &mut MiriInterpCx<'tcx>,
39-
finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
40-
) -> InterpResult<'tcx> {
41-
assert!(communicate_allowed, "isolation should have prevented even opening a file");
42-
43-
let result = ecx.read_from_host(&self.file, len, ptr)?;
44-
finish.call(ecx, result)
45-
}
46-
47-
fn write<'tcx>(
48-
self: FileDescriptionRef<Self>,
49-
communicate_allowed: bool,
50-
ptr: Pointer,
51-
len: usize,
52-
ecx: &mut MiriInterpCx<'tcx>,
53-
finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
54-
) -> InterpResult<'tcx> {
55-
assert!(communicate_allowed, "isolation should have prevented even opening a file");
56-
57-
let result = ecx.write_to_host(&self.file, len, ptr)?;
58-
finish.call(ecx, result)
59-
}
60-
61-
fn seek<'tcx>(
62-
&self,
63-
communicate_allowed: bool,
64-
offset: SeekFrom,
65-
) -> InterpResult<'tcx, io::Result<u64>> {
66-
assert!(communicate_allowed, "isolation should have prevented even opening a file");
67-
interp_ok((&mut &self.file).seek(offset))
68-
}
69-
70-
fn close<'tcx>(
71-
self,
72-
communicate_allowed: bool,
73-
_ecx: &mut MiriInterpCx<'tcx>,
74-
) -> InterpResult<'tcx, io::Result<()>> {
75-
assert!(communicate_allowed, "isolation should have prevented even opening a file");
76-
// We sync the file if it was opened in a mode different than read-only.
77-
if self.writable {
78-
// `File::sync_all` does the checks that are done when closing a file. We do this to
79-
// to handle possible errors correctly.
80-
let result = self.file.sync_all();
81-
// Now we actually close the file and return the result.
82-
drop(self.file);
83-
interp_ok(result)
84-
} else {
85-
// We drop the file, this closes it but ignores any errors
86-
// produced when closing it. This is done because
87-
// `File::sync_all` cannot be done over files like
88-
// `/dev/urandom` which are read-only. Check
89-
// https://github.com/rust-lang/miri/issues/999#issuecomment-568920439
90-
// for a deeper discussion.
91-
drop(self.file);
92-
interp_ok(Ok(()))
93-
}
94-
}
95-
96-
fn metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result<Metadata>> {
97-
interp_ok(self.file.metadata())
98-
}
99-
100-
fn is_tty(&self, communicate_allowed: bool) -> bool {
101-
communicate_allowed && self.file.is_terminal()
102-
}
103-
104-
fn as_unix(&self) -> &dyn UnixFileDescription {
105-
self
106-
}
107-
}
108-
10921
impl UnixFileDescription for FileHandle {
11022
fn pread<'tcx>(
11123
&self,

‎src/tools/miri/src/shims/unix/linux_like/epoll.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ impl FileDescription for Epoll {
153153
interp_ok(Ok(()))
154154
}
155155

156-
fn as_unix(&self) -> &dyn UnixFileDescription {
156+
fn as_unix<'tcx>(&self, _ecx: &MiriInterpCx<'tcx>) -> &dyn UnixFileDescription {
157157
self
158158
}
159159
}
@@ -590,7 +590,7 @@ fn check_and_update_one_event_interest<'tcx>(
590590
ecx: &MiriInterpCx<'tcx>,
591591
) -> InterpResult<'tcx, bool> {
592592
// Get the bitmask of ready events for a file description.
593-
let ready_events_bitmask = fd_ref.as_unix().get_epoll_ready_events()?.get_event_bitmask(ecx);
593+
let ready_events_bitmask = fd_ref.as_unix(ecx).get_epoll_ready_events()?.get_event_bitmask(ecx);
594594
let epoll_event_interest = interest.borrow();
595595
let epfd = epoll_event_interest.weak_epfd.upgrade().unwrap();
596596
// This checks if any of the events specified in epoll_event_interest.events

‎src/tools/miri/src/shims/unix/linux_like/eventfd.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ impl FileDescription for EventFd {
100100
eventfd_write(buf_place, self, ecx, finish)
101101
}
102102

103-
fn as_unix(&self) -> &dyn UnixFileDescription {
103+
fn as_unix<'tcx>(&self, _ecx: &MiriInterpCx<'tcx>) -> &dyn UnixFileDescription {
104104
self
105105
}
106106
}

‎src/tools/miri/src/shims/unix/unnamed_socket.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ impl FileDescription for AnonSocket {
107107
anonsocket_write(self, ptr, len, ecx, finish)
108108
}
109109

110-
fn as_unix(&self) -> &dyn UnixFileDescription {
110+
fn as_unix<'tcx>(&self, _ecx: &MiriInterpCx<'tcx>) -> &dyn UnixFileDescription {
111111
self
112112
}
113113
}

‎src/tools/miri/src/shims/windows/foreign_items.rs

Lines changed: 133 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,9 @@ use rustc_target::callconv::{Conv, FnAbi};
99

1010
use self::shims::windows::handle::{Handle, PseudoHandle};
1111
use crate::shims::os_str::bytes_to_os_str;
12-
use crate::shims::windows::handle::HandleError;
1312
use crate::shims::windows::*;
1413
use crate::*;
1514

16-
// The NTSTATUS STATUS_INVALID_HANDLE (0xC0000008) encoded as a HRESULT by setting the N bit.
17-
// (https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/0642cb2f-2075-4469-918c-4441e69c548a)
18-
const STATUS_INVALID_HANDLE: u32 = 0xD0000008;
19-
2015
pub fn is_dyn_sym(name: &str) -> bool {
2116
// std does dynamic detection for these symbols
2217
matches!(
@@ -26,57 +21,107 @@ pub fn is_dyn_sym(name: &str) -> bool {
2621
}
2722

2823
#[cfg(windows)]
29-
fn win_absolute<'tcx>(path: &Path) -> InterpResult<'tcx, io::Result<PathBuf>> {
24+
fn win_get_full_path_name<'tcx>(path: &Path) -> InterpResult<'tcx, io::Result<PathBuf>> {
3025
// We are on Windows so we can simply let the host do this.
3126
interp_ok(path::absolute(path))
3227
}
3328

3429
#[cfg(unix)]
3530
#[expect(clippy::get_first, clippy::arithmetic_side_effects)]
36-
fn win_absolute<'tcx>(path: &Path) -> InterpResult<'tcx, io::Result<PathBuf>> {
37-
// We are on Unix, so we need to implement parts of the logic ourselves.
31+
fn win_get_full_path_name<'tcx>(path: &Path) -> InterpResult<'tcx, io::Result<PathBuf>> {
32+
use std::sync::LazyLock;
33+
34+
use rustc_data_structures::fx::FxHashSet;
35+
36+
// We are on Unix, so we need to implement parts of the logic ourselves. `path` will use `/`
37+
// separators, and the result should also use `/`.
38+
// See <https://chrisdenton.github.io/omnipath/Overview.html#absolute-win32-paths> for more
39+
// information about Windows paths.
40+
// This does not handle all corner cases correctly, see
41+
// <https://github.com/rust-lang/miri/pull/4262#issuecomment-2792168853> for more cursed
42+
// examples.
3843
let bytes = path.as_os_str().as_encoded_bytes();
39-
// If it starts with `//` (these were backslashes but are already converted)
40-
// then this is a magic special path, we just leave it unchanged.
41-
if bytes.get(0).copied() == Some(b'/') && bytes.get(1).copied() == Some(b'/') {
44+
// If it starts with `//./` or `//?/` then this is a magic special path, we just leave it
45+
// unchanged.
46+
if bytes.get(0).copied() == Some(b'/')
47+
&& bytes.get(1).copied() == Some(b'/')
48+
&& matches!(bytes.get(2), Some(b'.' | b'?'))
49+
&& bytes.get(3).copied() == Some(b'/')
50+
{
4251
return interp_ok(Ok(path.into()));
4352
};
44-
// Special treatment for Windows' magic filenames: they are treated as being relative to `\\.\`.
45-
let magic_filenames = &[
46-
"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8",
47-
"COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
48-
];
49-
if magic_filenames.iter().any(|m| m.as_bytes() == bytes) {
50-
let mut result: Vec<u8> = br"//./".into();
53+
let is_unc = bytes.starts_with(b"//");
54+
// Special treatment for Windows' magic filenames: they are treated as being relative to `//./`.
55+
static MAGIC_FILENAMES: LazyLock<FxHashSet<&'static str>> = LazyLock::new(|| {
56+
FxHashSet::from_iter([
57+
"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7",
58+
"COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
59+
])
60+
});
61+
if str::from_utf8(bytes).is_ok_and(|s| MAGIC_FILENAMES.contains(&*s.to_ascii_uppercase())) {
62+
let mut result: Vec<u8> = b"//./".into();
5163
result.extend(bytes);
5264
return interp_ok(Ok(bytes_to_os_str(&result)?.into()));
5365
}
5466
// Otherwise we try to do something kind of close to what Windows does, but this is probably not
55-
// right in all cases. We iterate over the components between `/`, and remove trailing `.`,
56-
// except that trailing `..` remain unchanged.
57-
let mut result = vec![];
67+
// right in all cases.
68+
let mut result: Vec<&[u8]> = vec![]; // will be a vecot of components, joined by `/`.
5869
let mut bytes = bytes; // the remaining bytes to process
59-
loop {
60-
let len = bytes.iter().position(|&b| b == b'/').unwrap_or(bytes.len());
61-
let mut component = &bytes[..len];
62-
if len >= 2 && component[len - 1] == b'.' && component[len - 2] != b'.' {
63-
// Strip trailing `.`
64-
component = &component[..len - 1];
70+
let mut stop = false;
71+
while !stop {
72+
// Find next component, and advance `bytes`.
73+
let mut component = match bytes.iter().position(|&b| b == b'/') {
74+
Some(pos) => {
75+
let (component, tail) = bytes.split_at(pos);
76+
bytes = &tail[1..]; // remove the `/`.
77+
component
78+
}
79+
None => {
80+
// There's no more `/`.
81+
stop = true;
82+
let component = bytes;
83+
bytes = &[];
84+
component
85+
}
86+
};
87+
// `NUL` and only `NUL` also gets changed to be relative to `//./` later in the path.
88+
// (This changed with Windows 11; previously, all magic filenames behaved like this.)
89+
// Also, this does not apply to UNC paths.
90+
if !is_unc && component.eq_ignore_ascii_case(b"NUL") {
91+
let mut result: Vec<u8> = b"//./".into();
92+
result.extend(component);
93+
return interp_ok(Ok(bytes_to_os_str(&result)?.into()));
6594
}
66-
// Add this component to output.
67-
result.extend(component);
68-
// Prepare next iteration.
69-
if len < bytes.len() {
70-
// There's a component after this; add `/` and process remaining bytes.
71-
result.push(b'/');
72-
bytes = &bytes[len + 1..];
95+
// Deal with `..` -- Windows handles this entirely syntactically.
96+
if component == b".." {
97+
// Remove previous component, unless we are at the "root" already, then just ignore the `..`.
98+
let is_root = {
99+
// Paths like `/C:`.
100+
result.len() == 2 && matches!(result[0], []) && matches!(result[1], [_, b':'])
101+
} || {
102+
// Paths like `//server/share`
103+
result.len() == 4 && matches!(result[0], []) && matches!(result[1], [])
104+
};
105+
if !is_root {
106+
result.pop();
107+
}
73108
continue;
74-
} else {
75-
// This was the last component and it did not have a trailing `/`.
76-
break;
77109
}
110+
// Preserve this component.
111+
// Strip trailing `.`, but preserve trailing `..`. But not for UNC paths!
112+
let len = component.len();
113+
if !is_unc && len >= 2 && component[len - 1] == b'.' && component[len - 2] != b'.' {
114+
component = &component[..len - 1];
115+
}
116+
// Add this component to output.
117+
result.push(component);
118+
}
119+
// Drive letters must be followed by a `/`.
120+
if result.len() == 2 && matches!(result[0], []) && matches!(result[1], [_, b':']) {
121+
result.push(&[]);
78122
}
79-
// Let the host `absolute` function do working-dir handling
123+
// Let the host `absolute` function do working-dir handling.
124+
let result = result.join(&b'/');
80125
interp_ok(path::absolute(bytes_to_os_str(&result)?))
81126
}
82127

@@ -231,7 +276,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
231276
}
232277

233278
let filename = this.read_path_from_wide_str(filename)?;
234-
let result = match win_absolute(&filename)? {
279+
let result = match win_get_full_path_name(&filename)? {
235280
Err(err) => {
236281
this.set_last_error(err)?;
237282
Scalar::from_u32(0) // return zero upon failure
@@ -246,6 +291,32 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
246291
};
247292
this.write_scalar(result, dest)?;
248293
}
294+
"CreateFileW" => {
295+
let [
296+
file_name,
297+
desired_access,
298+
share_mode,
299+
security_attributes,
300+
creation_disposition,
301+
flags_and_attributes,
302+
template_file,
303+
] = this.check_shim(abi, sys_conv, link_name, args)?;
304+
let handle = this.CreateFileW(
305+
file_name,
306+
desired_access,
307+
share_mode,
308+
security_attributes,
309+
creation_disposition,
310+
flags_and_attributes,
311+
template_file,
312+
)?;
313+
this.write_scalar(handle.to_scalar(this), dest)?;
314+
}
315+
"GetFileInformationByHandle" => {
316+
let [handle, info] = this.check_shim(abi, sys_conv, link_name, args)?;
317+
let res = this.GetFileInformationByHandle(handle, info)?;
318+
this.write_scalar(res, dest)?;
319+
}
249320

250321
// Allocation
251322
"HeapAlloc" => {
@@ -498,52 +569,37 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
498569
"SetThreadDescription" => {
499570
let [handle, name] = this.check_shim(abi, sys_conv, link_name, args)?;
500571

501-
let handle = this.read_scalar(handle)?;
572+
let handle = this.read_handle(handle, "SetThreadDescription")?;
502573
let name = this.read_wide_str(this.read_pointer(name)?)?;
503574

504-
let thread = match Handle::try_from_scalar(handle, this)? {
505-
Ok(Handle::Thread(thread)) => Ok(thread),
506-
Ok(Handle::Pseudo(PseudoHandle::CurrentThread)) => Ok(this.active_thread()),
507-
Ok(_) | Err(HandleError::InvalidHandle) =>
508-
this.invalid_handle("SetThreadDescription")?,
509-
Err(HandleError::ThreadNotFound(e)) => Err(e),
510-
};
511-
let res = match thread {
512-
Ok(thread) => {
513-
// FIXME: use non-lossy conversion
514-
this.set_thread_name(thread, String::from_utf16_lossy(&name).into_bytes());
515-
Scalar::from_u32(0)
516-
}
517-
Err(_) => Scalar::from_u32(STATUS_INVALID_HANDLE),
575+
let thread = match handle {
576+
Handle::Thread(thread) => thread,
577+
Handle::Pseudo(PseudoHandle::CurrentThread) => this.active_thread(),
578+
_ => this.invalid_handle("SetThreadDescription")?,
518579
};
519-
520-
this.write_scalar(res, dest)?;
580+
// FIXME: use non-lossy conversion
581+
this.set_thread_name(thread, String::from_utf16_lossy(&name).into_bytes());
582+
this.write_scalar(Scalar::from_u32(0), dest)?;
521583
}
522584
"GetThreadDescription" => {
523585
let [handle, name_ptr] = this.check_shim(abi, sys_conv, link_name, args)?;
524586

525-
let handle = this.read_scalar(handle)?;
587+
let handle = this.read_handle(handle, "GetThreadDescription")?;
526588
let name_ptr = this.deref_pointer_as(name_ptr, this.machine.layouts.mut_raw_ptr)?; // the pointer where we should store the ptr to the name
527589

528-
let thread = match Handle::try_from_scalar(handle, this)? {
529-
Ok(Handle::Thread(thread)) => Ok(thread),
530-
Ok(Handle::Pseudo(PseudoHandle::CurrentThread)) => Ok(this.active_thread()),
531-
Ok(_) | Err(HandleError::InvalidHandle) =>
532-
this.invalid_handle("GetThreadDescription")?,
533-
Err(HandleError::ThreadNotFound(e)) => Err(e),
534-
};
535-
let (name, res) = match thread {
536-
Ok(thread) => {
537-
// Looks like the default thread name is empty.
538-
let name = this.get_thread_name(thread).unwrap_or(b"").to_owned();
539-
let name = this.alloc_os_str_as_wide_str(
540-
bytes_to_os_str(&name)?,
541-
MiriMemoryKind::WinLocal.into(),
542-
)?;
543-
(Scalar::from_maybe_pointer(name, this), Scalar::from_u32(0))
544-
}
545-
Err(_) => (Scalar::null_ptr(this), Scalar::from_u32(STATUS_INVALID_HANDLE)),
590+
let thread = match handle {
591+
Handle::Thread(thread) => thread,
592+
Handle::Pseudo(PseudoHandle::CurrentThread) => this.active_thread(),
593+
_ => this.invalid_handle("GetThreadDescription")?,
546594
};
595+
// Looks like the default thread name is empty.
596+
let name = this.get_thread_name(thread).unwrap_or(b"").to_owned();
597+
let name = this.alloc_os_str_as_wide_str(
598+
bytes_to_os_str(&name)?,
599+
MiriMemoryKind::WinLocal.into(),
600+
)?;
601+
let name = Scalar::from_maybe_pointer(name, this);
602+
let res = Scalar::from_u32(0);
547603

548604
this.write_scalar(name, &name_ptr)?;
549605
this.write_scalar(res, dest)?;
@@ -638,11 +694,11 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
638694
let [handle, filename, size] = this.check_shim(abi, sys_conv, link_name, args)?;
639695
this.check_no_isolation("`GetModuleFileNameW`")?;
640696

641-
let handle = this.read_target_usize(handle)?;
697+
let handle = this.read_handle(handle, "GetModuleFileNameW")?;
642698
let filename = this.read_pointer(filename)?;
643699
let size = this.read_scalar(size)?.to_u32()?;
644700

645-
if handle != 0 {
701+
if handle != Handle::Null {
646702
throw_unsup_format!("`GetModuleFileNameW` only supports the NULL handle");
647703
}
648704

‎src/tools/miri/src/shims/windows/fs.rs

Lines changed: 402 additions & 0 deletions
Large diffs are not rendered by default.

‎src/tools/miri/src/shims/windows/handle.rs

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::mem::variant_count;
33
use rustc_abi::HasDataLayout;
44

55
use crate::concurrency::thread::ThreadNotFound;
6+
use crate::shims::files::FdNum;
67
use crate::*;
78

89
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
@@ -16,6 +17,8 @@ pub enum Handle {
1617
Null,
1718
Pseudo(PseudoHandle),
1819
Thread(ThreadId),
20+
File(FdNum),
21+
Invalid,
1922
}
2023

2124
impl PseudoHandle {
@@ -47,12 +50,18 @@ impl Handle {
4750
const NULL_DISCRIMINANT: u32 = 0;
4851
const PSEUDO_DISCRIMINANT: u32 = 1;
4952
const THREAD_DISCRIMINANT: u32 = 2;
53+
const FILE_DISCRIMINANT: u32 = 3;
54+
// Chosen to ensure Handle::Invalid encodes to -1. Update this value if there are ever more than
55+
// 8 discriminants.
56+
const INVALID_DISCRIMINANT: u32 = 7;
5057

5158
fn discriminant(self) -> u32 {
5259
match self {
5360
Self::Null => Self::NULL_DISCRIMINANT,
5461
Self::Pseudo(_) => Self::PSEUDO_DISCRIMINANT,
5562
Self::Thread(_) => Self::THREAD_DISCRIMINANT,
63+
Self::File(_) => Self::FILE_DISCRIMINANT,
64+
Self::Invalid => Self::INVALID_DISCRIMINANT,
5665
}
5766
}
5867

@@ -61,17 +70,27 @@ impl Handle {
6170
Self::Null => 0,
6271
Self::Pseudo(pseudo_handle) => pseudo_handle.value(),
6372
Self::Thread(thread) => thread.to_u32(),
73+
#[expect(clippy::cast_sign_loss)]
74+
Self::File(fd) => fd as u32,
75+
// INVALID_HANDLE_VALUE is -1. This fact is explicitly declared or implied in several
76+
// pages of Windows documentation.
77+
// 1: https://learn.microsoft.com/en-us/dotnet/api/microsoft.win32.safehandles.safefilehandle?view=net-9.0
78+
// 2: https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/get-osfhandle?view=msvc-170
79+
Self::Invalid => 0x1FFFFFFF,
6480
}
6581
}
6682

6783
fn packed_disc_size() -> u32 {
68-
// ceil(log2(x)) is how many bits it takes to store x numbers
84+
// ceil(log2(x)) is how many bits it takes to store x numbers.
85+
// We ensure that INVALID_HANDLE_VALUE (0xFFFFFFFF) decodes to Handle::Invalid.
86+
// see https://devblogs.microsoft.com/oldnewthing/20230914-00/?p=108766 for more detail on
87+
// INVALID_HANDLE_VALUE.
6988
let variant_count = variant_count::<Self>();
7089

71-
// however, std's ilog2 is floor(log2(x))
90+
// However, std's ilog2 is floor(log2(x)).
7291
let floor_log2 = variant_count.ilog2();
7392

74-
// we need to add one for non powers of two to compensate for the difference
93+
// We need to add one for non powers of two to compensate for the difference.
7594
#[expect(clippy::arithmetic_side_effects)] // cannot overflow
7695
if variant_count.is_power_of_two() { floor_log2 } else { floor_log2 + 1 }
7796
}
@@ -105,6 +124,13 @@ impl Handle {
105124
Self::NULL_DISCRIMINANT if data == 0 => Some(Self::Null),
106125
Self::PSEUDO_DISCRIMINANT => Some(Self::Pseudo(PseudoHandle::from_value(data)?)),
107126
Self::THREAD_DISCRIMINANT => Some(Self::Thread(ThreadId::new_unchecked(data))),
127+
#[expect(clippy::cast_possible_wrap)]
128+
Self::FILE_DISCRIMINANT => {
129+
// This cast preserves all bits.
130+
assert_eq!(size_of_val(&data), size_of::<FdNum>());
131+
Some(Self::File(data as FdNum))
132+
}
133+
Self::INVALID_DISCRIMINANT => Some(Self::Invalid),
108134
_ => None,
109135
}
110136
}
@@ -139,7 +165,7 @@ impl Handle {
139165
/// Structurally invalid handles return [`HandleError::InvalidHandle`].
140166
/// If the handle is structurally valid but semantically invalid, e.g. a for non-existent thread
141167
/// ID, returns [`HandleError::ThreadNotFound`].
142-
pub fn try_from_scalar<'tcx>(
168+
fn try_from_scalar<'tcx>(
143169
handle: Scalar,
144170
cx: &MiriInterpCx<'tcx>,
145171
) -> InterpResult<'tcx, Result<Self, HandleError>> {
@@ -171,6 +197,27 @@ impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
171197

172198
#[allow(non_snake_case)]
173199
pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
200+
/// Convert a scalar into a structured `Handle`.
201+
/// If the handle is invalid, or references a non-existent item, execution is aborted.
202+
#[track_caller]
203+
fn read_handle(&self, handle: &OpTy<'tcx>, function_name: &str) -> InterpResult<'tcx, Handle> {
204+
let this = self.eval_context_ref();
205+
let handle = this.read_scalar(handle)?;
206+
match Handle::try_from_scalar(handle, this)? {
207+
Ok(handle) => interp_ok(handle),
208+
Err(HandleError::InvalidHandle) =>
209+
throw_machine_stop!(TerminationInfo::Abort(format!(
210+
"invalid handle {} passed to {function_name}",
211+
handle.to_target_isize(this)?,
212+
))),
213+
Err(HandleError::ThreadNotFound(_)) =>
214+
throw_machine_stop!(TerminationInfo::Abort(format!(
215+
"invalid thread ID {} passed to {function_name}",
216+
handle.to_target_isize(this)?,
217+
))),
218+
}
219+
}
220+
174221
fn invalid_handle(&mut self, function_name: &str) -> InterpResult<'tcx, !> {
175222
throw_machine_stop!(TerminationInfo::Abort(format!(
176223
"invalid handle passed to `{function_name}`"
@@ -180,15 +227,38 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
180227
fn CloseHandle(&mut self, handle_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
181228
let this = self.eval_context_mut();
182229

183-
let handle = this.read_scalar(handle_op)?;
184-
let ret = match Handle::try_from_scalar(handle, this)? {
185-
Ok(Handle::Thread(thread)) => {
230+
let handle = this.read_handle(handle_op, "CloseHandle")?;
231+
let ret = match handle {
232+
Handle::Thread(thread) => {
186233
this.detach_thread(thread, /*allow_terminated_joined*/ true)?;
187234
this.eval_windows("c", "TRUE")
188235
}
236+
Handle::File(fd_num) =>
237+
if let Some(fd) = this.machine.fds.remove(fd_num) {
238+
let err = fd.close_ref(this.machine.communicate(), this)?;
239+
if let Err(e) = err {
240+
this.set_last_error(e)?;
241+
this.eval_windows("c", "FALSE")
242+
} else {
243+
this.eval_windows("c", "TRUE")
244+
}
245+
} else {
246+
this.invalid_handle("CloseHandle")?
247+
},
189248
_ => this.invalid_handle("CloseHandle")?,
190249
};
191250

192251
interp_ok(ret)
193252
}
194253
}
254+
255+
#[cfg(test)]
256+
mod tests {
257+
use super::*;
258+
259+
#[test]
260+
fn test_invalid_encoding() {
261+
// Ensure the invalid handle encodes to `u32::MAX`/`INVALID_HANDLE_VALUE`.
262+
assert_eq!(Handle::Invalid.to_packed(), u32::MAX)
263+
}
264+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
pub mod foreign_items;
22

33
mod env;
4+
mod fs;
45
mod handle;
56
mod sync;
67
mod thread;
78

89
// All the Windows-specific extension traits
910
pub use self::env::{EvalContextExt as _, WindowsEnvVars};
11+
pub use self::fs::EvalContextExt as _;
1012
pub use self::handle::EvalContextExt as _;
1113
pub use self::sync::EvalContextExt as _;
1214
pub use self::thread::EvalContextExt as _;

‎src/tools/miri/src/shims/windows/thread.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,14 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
6262
) -> InterpResult<'tcx, Scalar> {
6363
let this = self.eval_context_mut();
6464

65-
let handle = this.read_scalar(handle_op)?;
65+
let handle = this.read_handle(handle_op, "WaitForSingleObject")?;
6666
let timeout = this.read_scalar(timeout_op)?.to_u32()?;
6767

68-
let thread = match Handle::try_from_scalar(handle, this)? {
69-
Ok(Handle::Thread(thread)) => thread,
68+
let thread = match handle {
69+
Handle::Thread(thread) => thread,
7070
// Unlike on posix, the outcome of joining the current thread is not documented.
7171
// On current Windows, it just deadlocks.
72-
Ok(Handle::Pseudo(PseudoHandle::CurrentThread)) => this.active_thread(),
72+
Handle::Pseudo(PseudoHandle::CurrentThread) => this.active_thread(),
7373
_ => this.invalid_handle("WaitForSingleObject")?,
7474
};
7575

‎src/tools/miri/test_dependencies/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ page_size = "0.6"
2525
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "net", "fs", "sync", "signal", "io-util"] }
2626

2727
[target.'cfg(windows)'.dependencies]
28-
windows-sys = { version = "0.59", features = [ "Win32_Foundation", "Win32_System_Threading" ] }
28+
windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_System_Threading", "Win32_Storage_FileSystem", "Win32_Security"] }
2929

3030
[workspace]

‎src/tools/miri/tests/fail-dep/concurrency/windows_join_main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use windows_sys::Win32::System::Threading::{INFINITE, WaitForSingleObject};
1313
// XXX HACK: This is how miri represents the handle for thread 0.
1414
// This value can be "legitimately" obtained by using `GetCurrentThread` with `DuplicateHandle`
1515
// but miri does not implement `DuplicateHandle` yet.
16-
const MAIN_THREAD: HANDLE = (2i32 << 30) as HANDLE;
16+
const MAIN_THREAD: HANDLE = (2i32 << 29) as HANDLE;
1717

1818
fn main() {
1919
thread::spawn(|| {
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
//@only-target: freebsd
2+
//@compile-flags: -Zmiri-preemption-rate=0 -Zmiri-disable-isolation
3+
4+
use std::mem::{self, MaybeUninit};
5+
use std::ptr::{self, addr_of};
6+
use std::sync::atomic::AtomicU32;
7+
use std::time::Instant;
8+
use std::{io, thread};
9+
10+
fn wait_wake() {
11+
fn wake_nobody() {
12+
// Current thread waits on futex.
13+
// New thread wakes up 0 threads waiting on that futex.
14+
// Current thread should time out.
15+
static mut FUTEX: u32 = 0;
16+
17+
let waker = thread::spawn(|| {
18+
unsafe {
19+
assert_eq!(
20+
libc::_umtx_op(
21+
addr_of!(FUTEX) as *mut _,
22+
libc::UMTX_OP_WAKE_PRIVATE,
23+
0, // wake up 0 waiters
24+
ptr::null_mut::<libc::c_void>(),
25+
ptr::null_mut::<libc::c_void>(),
26+
),
27+
0
28+
);
29+
}
30+
});
31+
32+
// 10ms should be enough.
33+
let mut timeout = libc::timespec { tv_sec: 0, tv_nsec: 10_000_000 };
34+
let timeout_size_arg =
35+
ptr::without_provenance_mut::<libc::c_void>(mem::size_of::<libc::timespec>());
36+
unsafe {
37+
assert_eq!(
38+
libc::_umtx_op(
39+
addr_of!(FUTEX) as *mut _,
40+
libc::UMTX_OP_WAIT_UINT_PRIVATE,
41+
0,
42+
timeout_size_arg,
43+
&mut timeout as *mut _ as _,
44+
),
45+
-1
46+
);
47+
// Main thread did not get woken up, so it timed out.
48+
assert_eq!(io::Error::last_os_error().raw_os_error().unwrap(), libc::ETIMEDOUT);
49+
}
50+
51+
waker.join().unwrap();
52+
}
53+
54+
fn wake_two_of_three() {
55+
// We create 2 threads that wait on a futex with a 100ms timeout.
56+
// The main thread wakes up 2 threads waiting on this futex and after this
57+
// checks that only those threads woke up and the other one timed out.
58+
static mut FUTEX: u32 = 0;
59+
60+
fn waiter() -> bool {
61+
let mut timeout = libc::timespec { tv_sec: 0, tv_nsec: 100_000_000 };
62+
let timeout_size_arg =
63+
ptr::without_provenance_mut::<libc::c_void>(mem::size_of::<libc::timespec>());
64+
unsafe {
65+
libc::_umtx_op(
66+
addr_of!(FUTEX) as *mut _,
67+
libc::UMTX_OP_WAIT_UINT_PRIVATE,
68+
0, // FUTEX is 0
69+
timeout_size_arg,
70+
&mut timeout as *mut _ as _,
71+
);
72+
// Return true if this thread woke up.
73+
io::Error::last_os_error().raw_os_error().unwrap() != libc::ETIMEDOUT
74+
}
75+
}
76+
77+
let t1 = thread::spawn(waiter);
78+
let t2 = thread::spawn(waiter);
79+
let t3 = thread::spawn(waiter);
80+
81+
// Run all the waiters, so they can go to sleep.
82+
thread::yield_now();
83+
84+
// Wake up 2 thread and make sure 1 is still waiting.
85+
unsafe {
86+
assert_eq!(
87+
libc::_umtx_op(
88+
addr_of!(FUTEX) as *mut _,
89+
libc::UMTX_OP_WAKE_PRIVATE,
90+
2,
91+
ptr::null_mut::<libc::c_void>(),
92+
ptr::null_mut::<libc::c_void>(),
93+
),
94+
0
95+
);
96+
}
97+
98+
// Treat the booleans as numbers to simplify checking how many threads were woken up.
99+
let t1 = t1.join().unwrap() as usize;
100+
let t2 = t2.join().unwrap() as usize;
101+
let t3 = t3.join().unwrap() as usize;
102+
let woken_up_count = t1 + t2 + t3;
103+
assert!(woken_up_count == 2, "Expected 2 threads to wake up got: {woken_up_count}");
104+
}
105+
106+
wake_nobody();
107+
wake_two_of_three();
108+
}
109+
110+
fn wake_dangling() {
111+
let futex = Box::new(0);
112+
let ptr: *const u32 = &*futex;
113+
drop(futex);
114+
115+
// Expect error since this is now "unmapped" memory.
116+
unsafe {
117+
assert_eq!(
118+
libc::_umtx_op(
119+
ptr as *const AtomicU32 as *mut _,
120+
libc::UMTX_OP_WAKE_PRIVATE,
121+
0,
122+
ptr::null_mut::<libc::c_void>(),
123+
ptr::null_mut::<libc::c_void>(),
124+
),
125+
-1
126+
);
127+
assert_eq!(io::Error::last_os_error().raw_os_error().unwrap(), libc::EFAULT);
128+
}
129+
}
130+
131+
fn wait_wrong_val() {
132+
let futex: u32 = 123;
133+
134+
// Wait with a wrong value just returns 0
135+
unsafe {
136+
assert_eq!(
137+
libc::_umtx_op(
138+
ptr::from_ref(&futex).cast_mut().cast(),
139+
libc::UMTX_OP_WAIT_UINT_PRIVATE,
140+
456,
141+
ptr::null_mut::<libc::c_void>(),
142+
ptr::null_mut::<libc::c_void>(),
143+
),
144+
0
145+
);
146+
}
147+
}
148+
149+
fn wait_relative_timeout() {
150+
fn without_timespec() {
151+
let start = Instant::now();
152+
153+
let futex: u32 = 123;
154+
155+
let mut timeout = libc::timespec { tv_sec: 0, tv_nsec: 200_000_000 };
156+
let timeout_size_arg =
157+
ptr::without_provenance_mut::<libc::c_void>(mem::size_of::<libc::timespec>());
158+
// Wait for 200ms, with nobody waking us up early
159+
unsafe {
160+
assert_eq!(
161+
libc::_umtx_op(
162+
ptr::from_ref(&futex).cast_mut().cast(),
163+
libc::UMTX_OP_WAIT_UINT_PRIVATE,
164+
123,
165+
timeout_size_arg,
166+
&mut timeout as *mut _ as _,
167+
),
168+
-1
169+
);
170+
assert_eq!(io::Error::last_os_error().raw_os_error().unwrap(), libc::ETIMEDOUT);
171+
}
172+
173+
assert!((200..1000).contains(&start.elapsed().as_millis()));
174+
}
175+
176+
fn with_timespec() {
177+
let futex: u32 = 123;
178+
let mut timeout = libc::_umtx_time {
179+
_timeout: libc::timespec { tv_sec: 0, tv_nsec: 200_000_000 },
180+
_flags: 0,
181+
_clockid: libc::CLOCK_MONOTONIC as u32,
182+
};
183+
let timeout_size_arg =
184+
ptr::without_provenance_mut::<libc::c_void>(mem::size_of::<libc::_umtx_time>());
185+
186+
let start = Instant::now();
187+
188+
// Wait for 200ms, with nobody waking us up early
189+
unsafe {
190+
assert_eq!(
191+
libc::_umtx_op(
192+
ptr::from_ref(&futex).cast_mut().cast(),
193+
libc::UMTX_OP_WAIT_UINT_PRIVATE,
194+
123,
195+
timeout_size_arg,
196+
&mut timeout as *mut _ as _,
197+
),
198+
-1
199+
);
200+
assert_eq!(io::Error::last_os_error().raw_os_error().unwrap(), libc::ETIMEDOUT);
201+
}
202+
assert!((200..1000).contains(&start.elapsed().as_millis()));
203+
}
204+
205+
without_timespec();
206+
with_timespec();
207+
}
208+
209+
fn wait_absolute_timeout() {
210+
let start = Instant::now();
211+
212+
// Get the current monotonic timestamp as timespec.
213+
let mut timeout = unsafe {
214+
let mut now: MaybeUninit<libc::timespec> = MaybeUninit::uninit();
215+
assert_eq!(libc::clock_gettime(libc::CLOCK_MONOTONIC, now.as_mut_ptr()), 0);
216+
now.assume_init()
217+
};
218+
219+
// Add 200ms.
220+
timeout.tv_nsec += 200_000_000;
221+
if timeout.tv_nsec > 1_000_000_000 {
222+
timeout.tv_nsec -= 1_000_000_000;
223+
timeout.tv_sec += 1;
224+
}
225+
226+
// Create umtx_timeout struct with that absolute timeout.
227+
let umtx_timeout = libc::_umtx_time {
228+
_timeout: timeout,
229+
_flags: libc::UMTX_ABSTIME,
230+
_clockid: libc::CLOCK_MONOTONIC as u32,
231+
};
232+
let umtx_timeout_ptr = &umtx_timeout as *const _;
233+
let umtx_timeout_size = ptr::without_provenance_mut(mem::size_of_val(&umtx_timeout));
234+
235+
let futex: u32 = 123;
236+
237+
// Wait for 200ms from now, with nobody waking us up early.
238+
unsafe {
239+
assert_eq!(
240+
libc::_umtx_op(
241+
ptr::from_ref(&futex).cast_mut().cast(),
242+
libc::UMTX_OP_WAIT_UINT_PRIVATE,
243+
123,
244+
umtx_timeout_size,
245+
umtx_timeout_ptr as *mut _,
246+
),
247+
-1
248+
);
249+
assert_eq!(io::Error::last_os_error().raw_os_error().unwrap(), libc::ETIMEDOUT);
250+
}
251+
assert!((200..1000).contains(&start.elapsed().as_millis()));
252+
}
253+
254+
fn main() {
255+
wait_wake();
256+
wake_dangling();
257+
wait_wrong_val();
258+
wait_relative_timeout();
259+
wait_absolute_timeout();
260+
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
//@only-target: windows # this directly tests windows-only functions
2+
//@compile-flags: -Zmiri-disable-isolation
3+
#![allow(nonstandard_style)]
4+
5+
use std::os::windows::ffi::OsStrExt;
6+
use std::path::Path;
7+
use std::ptr;
8+
9+
#[path = "../../utils/mod.rs"]
10+
mod utils;
11+
12+
use windows_sys::Win32::Foundation::{
13+
CloseHandle, ERROR_ALREADY_EXISTS, GENERIC_READ, GENERIC_WRITE, GetLastError,
14+
};
15+
use windows_sys::Win32::Storage::FileSystem::{
16+
BY_HANDLE_FILE_INFORMATION, CREATE_ALWAYS, CREATE_NEW, CreateFileW, FILE_ATTRIBUTE_DIRECTORY,
17+
FILE_ATTRIBUTE_NORMAL, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT,
18+
FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, GetFileInformationByHandle, OPEN_ALWAYS,
19+
OPEN_EXISTING,
20+
};
21+
22+
fn main() {
23+
unsafe {
24+
test_create_dir_file();
25+
test_create_normal_file();
26+
test_create_always_twice();
27+
test_open_always_twice();
28+
test_open_dir_reparse();
29+
}
30+
}
31+
32+
unsafe fn test_create_dir_file() {
33+
let temp = utils::tmp();
34+
let raw_path = to_wide_cstr(&temp);
35+
// Open the `temp` directory.
36+
let handle = CreateFileW(
37+
raw_path.as_ptr(),
38+
GENERIC_READ,
39+
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
40+
ptr::null_mut(),
41+
OPEN_EXISTING,
42+
FILE_FLAG_BACKUP_SEMANTICS,
43+
ptr::null_mut(),
44+
);
45+
assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
46+
let mut info = std::mem::zeroed::<BY_HANDLE_FILE_INFORMATION>();
47+
if GetFileInformationByHandle(handle, &mut info) == 0 {
48+
panic!("Failed to get file information")
49+
};
50+
assert!(info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0);
51+
if CloseHandle(handle) == 0 {
52+
panic!("Failed to close file")
53+
};
54+
}
55+
56+
unsafe fn test_create_normal_file() {
57+
let temp = utils::tmp().join("test.txt");
58+
let raw_path = to_wide_cstr(&temp);
59+
let handle = CreateFileW(
60+
raw_path.as_ptr(),
61+
GENERIC_READ | GENERIC_WRITE,
62+
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
63+
ptr::null_mut(),
64+
CREATE_NEW,
65+
0,
66+
ptr::null_mut(),
67+
);
68+
assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
69+
let mut info = std::mem::zeroed::<BY_HANDLE_FILE_INFORMATION>();
70+
if GetFileInformationByHandle(handle, &mut info) == 0 {
71+
panic!("Failed to get file information: {}", GetLastError())
72+
};
73+
assert!(info.dwFileAttributes & FILE_ATTRIBUTE_NORMAL != 0);
74+
if CloseHandle(handle) == 0 {
75+
panic!("Failed to close file")
76+
};
77+
78+
// Test metadata-only handle
79+
let handle = CreateFileW(
80+
raw_path.as_ptr(),
81+
0,
82+
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
83+
ptr::null_mut(),
84+
OPEN_EXISTING,
85+
0,
86+
ptr::null_mut(),
87+
);
88+
assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
89+
let mut info = std::mem::zeroed::<BY_HANDLE_FILE_INFORMATION>();
90+
if GetFileInformationByHandle(handle, &mut info) == 0 {
91+
panic!("Failed to get file information: {}", GetLastError())
92+
};
93+
assert!(info.dwFileAttributes & FILE_ATTRIBUTE_NORMAL != 0);
94+
if CloseHandle(handle) == 0 {
95+
panic!("Failed to close file")
96+
};
97+
}
98+
99+
/// Tests that CREATE_ALWAYS sets the error value correctly based on whether the file already exists
100+
unsafe fn test_create_always_twice() {
101+
let temp = utils::tmp().join("test_create_always.txt");
102+
let raw_path = to_wide_cstr(&temp);
103+
let handle = CreateFileW(
104+
raw_path.as_ptr(),
105+
GENERIC_READ | GENERIC_WRITE,
106+
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
107+
ptr::null_mut(),
108+
CREATE_ALWAYS,
109+
0,
110+
ptr::null_mut(),
111+
);
112+
assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
113+
assert_eq!(GetLastError(), 0);
114+
if CloseHandle(handle) == 0 {
115+
panic!("Failed to close file")
116+
};
117+
118+
let handle = CreateFileW(
119+
raw_path.as_ptr(),
120+
GENERIC_READ | GENERIC_WRITE,
121+
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
122+
ptr::null_mut(),
123+
CREATE_ALWAYS,
124+
0,
125+
ptr::null_mut(),
126+
);
127+
assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
128+
assert_eq!(GetLastError(), ERROR_ALREADY_EXISTS);
129+
if CloseHandle(handle) == 0 {
130+
panic!("Failed to close file")
131+
};
132+
}
133+
134+
/// Tests that OPEN_ALWAYS sets the error value correctly based on whether the file already exists
135+
unsafe fn test_open_always_twice() {
136+
let temp = utils::tmp().join("test_open_always.txt");
137+
let raw_path = to_wide_cstr(&temp);
138+
let handle = CreateFileW(
139+
raw_path.as_ptr(),
140+
GENERIC_READ | GENERIC_WRITE,
141+
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
142+
ptr::null_mut(),
143+
OPEN_ALWAYS,
144+
0,
145+
ptr::null_mut(),
146+
);
147+
assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
148+
assert_eq!(GetLastError(), 0);
149+
if CloseHandle(handle) == 0 {
150+
panic!("Failed to close file")
151+
};
152+
153+
let handle = CreateFileW(
154+
raw_path.as_ptr(),
155+
GENERIC_READ | GENERIC_WRITE,
156+
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
157+
ptr::null_mut(),
158+
OPEN_ALWAYS,
159+
0,
160+
ptr::null_mut(),
161+
);
162+
assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
163+
assert_eq!(GetLastError(), ERROR_ALREADY_EXISTS);
164+
if CloseHandle(handle) == 0 {
165+
panic!("Failed to close file")
166+
};
167+
}
168+
169+
// TODO: Once we support more of the std API, it would be nice to test against an actual symlink
170+
unsafe fn test_open_dir_reparse() {
171+
let temp = utils::tmp();
172+
let raw_path = to_wide_cstr(&temp);
173+
// Open the `temp` directory.
174+
let handle = CreateFileW(
175+
raw_path.as_ptr(),
176+
GENERIC_READ,
177+
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
178+
ptr::null_mut(),
179+
OPEN_EXISTING,
180+
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
181+
ptr::null_mut(),
182+
);
183+
assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
184+
let mut info = std::mem::zeroed::<BY_HANDLE_FILE_INFORMATION>();
185+
if GetFileInformationByHandle(handle, &mut info) == 0 {
186+
panic!("Failed to get file information")
187+
};
188+
assert!(info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0);
189+
if CloseHandle(handle) == 0 {
190+
panic!("Failed to close file")
191+
};
192+
}
193+
194+
fn to_wide_cstr(path: &Path) -> Vec<u16> {
195+
let mut raw_path = path.as_os_str().encode_wide().collect::<Vec<_>>();
196+
raw_path.extend([0, 0]);
197+
raw_path
198+
}

‎src/tools/miri/tests/pass/path.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ mod utils;
66

77
#[track_caller]
88
fn assert_absolute_eq(in_: &str, out: &str) {
9-
assert_eq!(absolute(in_).unwrap().as_os_str(), Path::new(out).as_os_str());
9+
assert_eq!(
10+
absolute(in_).unwrap().as_os_str(),
11+
Path::new(out).as_os_str(),
12+
"incorrect absolute path for {in_:?}"
13+
);
1014
}
1115

1216
fn test_absolute() {
@@ -29,11 +33,28 @@ fn test_absolute() {
2933
assert_absolute_eq(r"\\?\C:\path\to\file", r"\\?\C:\path\to\file");
3034
assert_absolute_eq(r"\\?\UNC\server\share\to\file", r"\\?\UNC\server\share\to\file");
3135
assert_absolute_eq(r"\\?\PIPE\name", r"\\?\PIPE\name");
36+
assert_absolute_eq(r"\\server\share\NUL", r"\\server\share\NUL");
37+
// This fails on Windows 10 hosts. FIXME: enable this once GHA runners are on Windows 11.
38+
//assert_absolute_eq(r"C:\path\to\COM1", r"C:\path\to\COM1");
3239
// Verbatim paths are always unchanged, no matter what.
3340
assert_absolute_eq(r"\\?\path.\to/file..", r"\\?\path.\to/file..");
34-
41+
// Trailing dot is removed here.
3542
assert_absolute_eq(r"C:\path..\to.\file.", r"C:\path..\to\file");
43+
// `..` is resolved here.
44+
assert_absolute_eq(r"C:\path\to\..\file", r"C:\path\file");
45+
assert_absolute_eq(r"C:\path\to\..\..\file", r"C:\file");
46+
assert_absolute_eq(r"C:\path\to\..\..\..\..\..\..\file", r"C:\file");
47+
assert_absolute_eq(r"C:\..", r"C:\");
48+
assert_absolute_eq(r"\\server\share\to\path\with\..\file", r"\\server\share\to\path\file");
49+
assert_absolute_eq(r"\\server\share\to\..\..\..\..\file", r"\\server\share\file");
50+
assert_absolute_eq(r"\\server\share\..", r"\\server\share");
51+
// Magic filenames.
52+
assert_absolute_eq(r"NUL", r"\\.\NUL");
53+
assert_absolute_eq(r"nul", r"\\.\nul");
3654
assert_absolute_eq(r"COM1", r"\\.\COM1");
55+
assert_absolute_eq(r"com1", r"\\.\com1");
56+
assert_absolute_eq(r"C:\path\to\NUL", r"\\.\NUL");
57+
assert_absolute_eq(r"C:\path\to\nul", r"\\.\nul");
3758
} else {
3859
panic!("unsupported OS");
3960
}

‎src/tools/miri/tests/pass/shims/fs.rs

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
//@ignore-target: windows # File handling is not implemented yet
21
//@compile-flags: -Zmiri-disable-isolation
32

43
#![feature(io_error_more)]
@@ -18,20 +17,23 @@ mod utils;
1817

1918
fn main() {
2019
test_path_conversion();
21-
test_file();
22-
test_file_clone();
23-
test_file_create_new();
24-
test_seek();
25-
test_metadata();
26-
test_file_set_len();
27-
test_file_sync();
28-
test_errors();
29-
test_rename();
30-
test_directory();
31-
test_canonicalize();
32-
test_from_raw_os_error();
33-
#[cfg(unix)]
34-
test_pread_pwrite();
20+
// Windows file handling is very incomplete.
21+
if cfg!(not(windows)) {
22+
test_file();
23+
test_file_create_new();
24+
test_seek();
25+
test_file_clone();
26+
test_metadata();
27+
test_file_set_len();
28+
test_file_sync();
29+
test_errors();
30+
test_rename();
31+
test_directory();
32+
test_canonicalize();
33+
test_from_raw_os_error();
34+
#[cfg(unix)]
35+
test_pread_pwrite();
36+
}
3537
}
3638

3739
fn test_path_conversion() {
@@ -144,10 +146,10 @@ fn test_metadata() {
144146
let path = utils::prepare_with_content("miri_test_fs_metadata.txt", bytes);
145147

146148
// Test that metadata of an absolute path is correct.
147-
check_metadata(bytes, &path).unwrap();
149+
check_metadata(bytes, &path).expect("absolute path metadata");
148150
// Test that metadata of a relative path is correct.
149151
std::env::set_current_dir(path.parent().unwrap()).unwrap();
150-
check_metadata(bytes, Path::new(path.file_name().unwrap())).unwrap();
152+
check_metadata(bytes, Path::new(path.file_name().unwrap())).expect("relative path metadata");
151153

152154
// Removing file should succeed.
153155
remove_file(&path).unwrap();

‎src/tools/miri/triagebot.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,9 @@ unless = ["S-blocked", "S-waiting-on-team", "S-waiting-on-review"]
4040

4141
# Automatically close and reopen PRs made by bots to run CI on them
4242
[bot-pull-requests]
43+
44+
# Canonicalize issue numbers to avoid closing the wrong issue when upstreaming this subtree
45+
[canonicalize-issue-links]
46+
47+
# Prevents mentions in commits to avoid users being spammed
48+
[no-mentions]

0 commit comments

Comments
 (0)
This repository has been archived.