Rollup merge of #139630 - RalfJung:miri-sync, r=RalfJung
Miri subtree update r? `@ghost` a sync is needed to fix the miri-test-libstd failures
This commit is contained in:
commit
c9613f87ef
27 changed files with 1532 additions and 229 deletions
|
|
@ -538,6 +538,7 @@ name = "miri"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"bitflags",
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"colored",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ measureme = "12"
|
|||
chrono = { version = "0.4.38", default-features = false }
|
||||
chrono-tz = "0.10"
|
||||
directories = "6"
|
||||
bitflags = "2.6"
|
||||
|
||||
# Copied from `compiler/rustc/Cargo.toml`.
|
||||
# But only for some targets, it fails for others. Rustc configures this in its CI, but we can't
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ case $HOST_TARGET in
|
|||
# Partially supported targets (tier 2)
|
||||
BASIC="empty_main integer heap_alloc libc-mem vec string btreemap" # ensures we have the basics: pre-main code, system allocator
|
||||
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
|
||||
TEST_TARGET=x86_64-unknown-freebsd run_tests_minimal $BASIC $UNIX time hashmap random threadname pthread fs libc-pipe
|
||||
TEST_TARGET=x86_64-unknown-freebsd run_tests_minimal $BASIC $UNIX time hashmap random threadname pthread fs libc-pipe concurrency sync
|
||||
TEST_TARGET=i686-unknown-freebsd run_tests_minimal $BASIC $UNIX time hashmap random threadname pthread fs libc-pipe
|
||||
TEST_TARGET=aarch64-linux-android run_tests_minimal $BASIC $UNIX time hashmap random sync concurrency thread epoll eventfd
|
||||
TEST_TARGET=wasm32-wasip2 run_tests_minimal $BASIC wasm
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
25a615bf829b9f6d6f22da537e3851043f92e5f2
|
||||
7d7de5bf3c3cbf9c2c5bbc5cbfb9197a8a427d35
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use std::any::Any;
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::{IsTerminal, SeekFrom, Write};
|
||||
use std::fs::{File, Metadata};
|
||||
use std::io::{IsTerminal, Seek, SeekFrom, Write};
|
||||
use std::marker::CoercePointee;
|
||||
use std::ops::Deref;
|
||||
use std::rc::{Rc, Weak};
|
||||
|
|
@ -192,7 +193,7 @@ pub trait FileDescription: std::fmt::Debug + FileDescriptionExt {
|
|||
false
|
||||
}
|
||||
|
||||
fn as_unix(&self) -> &dyn UnixFileDescription {
|
||||
fn as_unix<'tcx>(&self, _ecx: &MiriInterpCx<'tcx>) -> &dyn UnixFileDescription {
|
||||
panic!("Not a unix file descriptor: {}", self.name());
|
||||
}
|
||||
}
|
||||
|
|
@ -278,6 +279,97 @@ impl FileDescription for io::Stderr {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FileHandle {
|
||||
pub(crate) file: File,
|
||||
pub(crate) writable: bool,
|
||||
}
|
||||
|
||||
impl FileDescription for FileHandle {
|
||||
fn name(&self) -> &'static str {
|
||||
"file"
|
||||
}
|
||||
|
||||
fn read<'tcx>(
|
||||
self: FileDescriptionRef<Self>,
|
||||
communicate_allowed: bool,
|
||||
ptr: Pointer,
|
||||
len: usize,
|
||||
ecx: &mut MiriInterpCx<'tcx>,
|
||||
finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
|
||||
) -> InterpResult<'tcx> {
|
||||
assert!(communicate_allowed, "isolation should have prevented even opening a file");
|
||||
|
||||
let result = ecx.read_from_host(&self.file, len, ptr)?;
|
||||
finish.call(ecx, result)
|
||||
}
|
||||
|
||||
fn write<'tcx>(
|
||||
self: FileDescriptionRef<Self>,
|
||||
communicate_allowed: bool,
|
||||
ptr: Pointer,
|
||||
len: usize,
|
||||
ecx: &mut MiriInterpCx<'tcx>,
|
||||
finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
|
||||
) -> InterpResult<'tcx> {
|
||||
assert!(communicate_allowed, "isolation should have prevented even opening a file");
|
||||
|
||||
let result = ecx.write_to_host(&self.file, len, ptr)?;
|
||||
finish.call(ecx, result)
|
||||
}
|
||||
|
||||
fn seek<'tcx>(
|
||||
&self,
|
||||
communicate_allowed: bool,
|
||||
offset: SeekFrom,
|
||||
) -> InterpResult<'tcx, io::Result<u64>> {
|
||||
assert!(communicate_allowed, "isolation should have prevented even opening a file");
|
||||
interp_ok((&mut &self.file).seek(offset))
|
||||
}
|
||||
|
||||
fn close<'tcx>(
|
||||
self,
|
||||
communicate_allowed: bool,
|
||||
_ecx: &mut MiriInterpCx<'tcx>,
|
||||
) -> InterpResult<'tcx, io::Result<()>> {
|
||||
assert!(communicate_allowed, "isolation should have prevented even opening a file");
|
||||
// We sync the file if it was opened in a mode different than read-only.
|
||||
if self.writable {
|
||||
// `File::sync_all` does the checks that are done when closing a file. We do this to
|
||||
// to handle possible errors correctly.
|
||||
let result = self.file.sync_all();
|
||||
// Now we actually close the file and return the result.
|
||||
drop(self.file);
|
||||
interp_ok(result)
|
||||
} else {
|
||||
// We drop the file, this closes it but ignores any errors
|
||||
// produced when closing it. This is done because
|
||||
// `File::sync_all` cannot be done over files like
|
||||
// `/dev/urandom` which are read-only. Check
|
||||
// https://github.com/rust-lang/miri/issues/999#issuecomment-568920439
|
||||
// for a deeper discussion.
|
||||
drop(self.file);
|
||||
interp_ok(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
fn metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result<Metadata>> {
|
||||
interp_ok(self.file.metadata())
|
||||
}
|
||||
|
||||
fn is_tty(&self, communicate_allowed: bool) -> bool {
|
||||
communicate_allowed && self.file.is_terminal()
|
||||
}
|
||||
|
||||
fn as_unix<'tcx>(&self, ecx: &MiriInterpCx<'tcx>) -> &dyn UnixFileDescription {
|
||||
assert!(
|
||||
ecx.target_os_is_unix(),
|
||||
"unix file operations are only available for unix targets"
|
||||
);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Like /dev/null
|
||||
#[derive(Debug)]
|
||||
pub struct NullOutput;
|
||||
|
|
@ -300,10 +392,13 @@ impl FileDescription for NullOutput {
|
|||
}
|
||||
}
|
||||
|
||||
/// Internal type of a file-descriptor - this is what [`FdTable`] expects
|
||||
pub type FdNum = i32;
|
||||
|
||||
/// The file descriptor table
|
||||
#[derive(Debug)]
|
||||
pub struct FdTable {
|
||||
pub fds: BTreeMap<i32, DynFileDescriptionRef>,
|
||||
pub fds: BTreeMap<FdNum, DynFileDescriptionRef>,
|
||||
/// Unique identifier for file description, used to differentiate between various file description.
|
||||
next_file_description_id: FdId,
|
||||
}
|
||||
|
|
@ -339,12 +434,12 @@ impl FdTable {
|
|||
}
|
||||
|
||||
/// Insert a new file description to the FdTable.
|
||||
pub fn insert_new(&mut self, fd: impl FileDescription) -> i32 {
|
||||
pub fn insert_new(&mut self, fd: impl FileDescription) -> FdNum {
|
||||
let fd_ref = self.new_ref(fd);
|
||||
self.insert(fd_ref)
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, fd_ref: DynFileDescriptionRef) -> i32 {
|
||||
pub fn insert(&mut self, fd_ref: DynFileDescriptionRef) -> FdNum {
|
||||
self.insert_with_min_num(fd_ref, 0)
|
||||
}
|
||||
|
||||
|
|
@ -352,8 +447,8 @@ impl FdTable {
|
|||
pub fn insert_with_min_num(
|
||||
&mut self,
|
||||
file_handle: DynFileDescriptionRef,
|
||||
min_fd_num: i32,
|
||||
) -> i32 {
|
||||
min_fd_num: FdNum,
|
||||
) -> FdNum {
|
||||
// Find the lowest unused FD, starting from min_fd. If the first such unused FD is in
|
||||
// between used FDs, the find_map combinator will return it. If the first such unused FD
|
||||
// is after all other used FDs, the find_map combinator will return None, and we will use
|
||||
|
|
@ -379,16 +474,16 @@ impl FdTable {
|
|||
new_fd_num
|
||||
}
|
||||
|
||||
pub fn get(&self, fd_num: i32) -> Option<DynFileDescriptionRef> {
|
||||
pub fn get(&self, fd_num: FdNum) -> Option<DynFileDescriptionRef> {
|
||||
let fd = self.fds.get(&fd_num)?;
|
||||
Some(fd.clone())
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, fd_num: i32) -> Option<DynFileDescriptionRef> {
|
||||
pub fn remove(&mut self, fd_num: FdNum) -> Option<DynFileDescriptionRef> {
|
||||
self.fds.remove(&fd_num)
|
||||
}
|
||||
|
||||
pub fn is_fd_num(&self, fd_num: i32) -> bool {
|
||||
pub fn is_fd_num(&self, fd_num: FdNum) -> bool {
|
||||
self.fds.contains_key(&fd_num)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -219,16 +219,8 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
|
||||
let filetime = this.deref_pointer_as(LPFILETIME_op, this.windows_ty_layout("FILETIME"))?;
|
||||
|
||||
let NANOS_PER_SEC = this.eval_windows_u64("time", "NANOS_PER_SEC");
|
||||
let INTERVALS_PER_SEC = this.eval_windows_u64("time", "INTERVALS_PER_SEC");
|
||||
let INTERVALS_TO_UNIX_EPOCH = this.eval_windows_u64("time", "INTERVALS_TO_UNIX_EPOCH");
|
||||
let NANOS_PER_INTERVAL = NANOS_PER_SEC / INTERVALS_PER_SEC;
|
||||
let SECONDS_TO_UNIX_EPOCH = INTERVALS_TO_UNIX_EPOCH / INTERVALS_PER_SEC;
|
||||
|
||||
let duration = system_time_to_duration(&SystemTime::now())?
|
||||
+ Duration::from_secs(SECONDS_TO_UNIX_EPOCH);
|
||||
let duration_ticks = u64::try_from(duration.as_nanos() / u128::from(NANOS_PER_INTERVAL))
|
||||
.map_err(|_| err_unsup_format!("programs running more than 2^64 Windows ticks after the Windows epoch are not supported"))?;
|
||||
let duration = this.system_time_since_windows_epoch(&SystemTime::now())?;
|
||||
let duration_ticks = this.windows_ticks_for(duration)?;
|
||||
|
||||
let dwLowDateTime = u32::try_from(duration_ticks & 0x00000000FFFFFFFF).unwrap();
|
||||
let dwHighDateTime = u32::try_from((duration_ticks & 0xFFFFFFFF00000000) >> 32).unwrap();
|
||||
|
|
@ -281,6 +273,30 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
interp_ok(Scalar::from_i32(-1)) // Return non-zero on success
|
||||
}
|
||||
|
||||
#[allow(non_snake_case, clippy::arithmetic_side_effects)]
|
||||
fn system_time_since_windows_epoch(&self, time: &SystemTime) -> InterpResult<'tcx, Duration> {
|
||||
let this = self.eval_context_ref();
|
||||
|
||||
let INTERVALS_PER_SEC = this.eval_windows_u64("time", "INTERVALS_PER_SEC");
|
||||
let INTERVALS_TO_UNIX_EPOCH = this.eval_windows_u64("time", "INTERVALS_TO_UNIX_EPOCH");
|
||||
let SECONDS_TO_UNIX_EPOCH = INTERVALS_TO_UNIX_EPOCH / INTERVALS_PER_SEC;
|
||||
|
||||
interp_ok(system_time_to_duration(time)? + Duration::from_secs(SECONDS_TO_UNIX_EPOCH))
|
||||
}
|
||||
|
||||
#[allow(non_snake_case, clippy::arithmetic_side_effects)]
|
||||
fn windows_ticks_for(&self, duration: Duration) -> InterpResult<'tcx, u64> {
|
||||
let this = self.eval_context_ref();
|
||||
|
||||
let NANOS_PER_SEC = this.eval_windows_u64("time", "NANOS_PER_SEC");
|
||||
let INTERVALS_PER_SEC = this.eval_windows_u64("time", "INTERVALS_PER_SEC");
|
||||
let NANOS_PER_INTERVAL = NANOS_PER_SEC / INTERVALS_PER_SEC;
|
||||
|
||||
let ticks = u64::try_from(duration.as_nanos() / u128::from(NANOS_PER_INTERVAL))
|
||||
.map_err(|_| err_unsup_format!("programs running more than 2^64 Windows ticks after the Windows epoch are not supported"))?;
|
||||
interp_ok(ticks)
|
||||
}
|
||||
|
||||
fn mach_absolute_time(&self) -> InterpResult<'tcx, Scalar> {
|
||||
let this = self.eval_context_ref();
|
||||
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
throw_unsup_format!("unsupported flags {:#x}", op);
|
||||
};
|
||||
|
||||
let result = fd.as_unix().flock(this.machine.communicate(), parsed_op)?;
|
||||
let result = fd.as_unix(this).flock(this.machine.communicate(), parsed_op)?;
|
||||
// return `0` if flock is successful
|
||||
let result = result.map(|()| 0i32);
|
||||
interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
|
||||
|
|
@ -273,7 +273,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
let Ok(offset) = u64::try_from(offset) else {
|
||||
return this.set_last_error_and_return(LibcError("EINVAL"), dest);
|
||||
};
|
||||
fd.as_unix().pread(communicate, offset, buf, count, this, finish)?
|
||||
fd.as_unix(this).pread(communicate, offset, buf, count, this, finish)?
|
||||
}
|
||||
};
|
||||
interp_ok(())
|
||||
|
|
@ -333,7 +333,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
let Ok(offset) = u64::try_from(offset) else {
|
||||
return this.set_last_error_and_return(LibcError("EINVAL"), dest);
|
||||
};
|
||||
fd.as_unix().pwrite(communicate, buf, count, offset, this, finish)?
|
||||
fd.as_unix(this).pwrite(communicate, buf, count, offset, this, finish)?
|
||||
}
|
||||
};
|
||||
interp_ok(())
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use rustc_middle::ty::Ty;
|
|||
use rustc_span::Symbol;
|
||||
use rustc_target::callconv::{Conv, FnAbi};
|
||||
|
||||
use super::sync::EvalContextExt as _;
|
||||
use crate::shims::unix::*;
|
||||
use crate::*;
|
||||
|
||||
|
|
@ -55,6 +56,13 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
this.write_scalar(res, dest)?;
|
||||
}
|
||||
|
||||
// Synchronization primitives
|
||||
"_umtx_op" => {
|
||||
let [obj, op, val, uaddr, uaddr2] =
|
||||
this.check_shim(abi, Conv::C, link_name, args)?;
|
||||
this._umtx_op(obj, op, val, uaddr, uaddr2, dest)?;
|
||||
}
|
||||
|
||||
// File related shims
|
||||
// For those, we both intercept `func` and `call@FBSD_1.0` symbols cases
|
||||
// since freebsd 12 the former form can be expected.
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
pub mod foreign_items;
|
||||
pub mod sync;
|
||||
|
|
|
|||
251
src/tools/miri/src/shims/unix/freebsd/sync.rs
Normal file
251
src/tools/miri/src/shims/unix/freebsd/sync.rs
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
//! Contains FreeBSD-specific synchronization functions
|
||||
|
||||
use core::time::Duration;
|
||||
|
||||
use crate::concurrency::sync::FutexRef;
|
||||
use crate::*;
|
||||
|
||||
pub struct FreeBsdFutex {
|
||||
futex: FutexRef,
|
||||
}
|
||||
|
||||
/// Extended variant of the `timespec` struct.
|
||||
pub struct UmtxTime {
|
||||
timeout: Duration,
|
||||
abs_time: bool,
|
||||
timeout_clock: TimeoutClock,
|
||||
}
|
||||
|
||||
impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
|
||||
pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
||||
/// 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.
|
||||
/// This is used for futex operations on FreeBSD.
|
||||
///
|
||||
/// `obj`: a pointer to the futex object (can be a lot of things, mostly *AtomicU32)
|
||||
/// `op`: the futex operation to run
|
||||
/// `val`: the current value of the object as a `c_long` (for wait/wake)
|
||||
/// `uaddr`: `op`-specific optional parameter, pointer-sized integer or pointer to an `op`-specific struct
|
||||
/// `uaddr2`: `op`-specific optional parameter, pointer-sized integer or pointer to an `op`-specific struct
|
||||
/// `dest`: the place this syscall returns to, 0 for success, -1 for failure
|
||||
///
|
||||
/// # Note
|
||||
/// Curently only the WAIT and WAKE operations are implemented.
|
||||
fn _umtx_op(
|
||||
&mut self,
|
||||
obj: &OpTy<'tcx>,
|
||||
op: &OpTy<'tcx>,
|
||||
val: &OpTy<'tcx>,
|
||||
uaddr: &OpTy<'tcx>,
|
||||
uaddr2: &OpTy<'tcx>,
|
||||
dest: &MPlaceTy<'tcx>,
|
||||
) -> InterpResult<'tcx> {
|
||||
let this = self.eval_context_mut();
|
||||
|
||||
let obj = this.read_pointer(obj)?;
|
||||
let op = this.read_scalar(op)?.to_i32()?;
|
||||
let val = this.read_target_usize(val)?;
|
||||
let uaddr = this.read_target_usize(uaddr)?;
|
||||
let uaddr2 = this.read_pointer(uaddr2)?;
|
||||
|
||||
let wait = this.eval_libc_i32("UMTX_OP_WAIT");
|
||||
let wait_uint = this.eval_libc_i32("UMTX_OP_WAIT_UINT");
|
||||
let wait_uint_private = this.eval_libc_i32("UMTX_OP_WAIT_UINT_PRIVATE");
|
||||
|
||||
let wake = this.eval_libc_i32("UMTX_OP_WAKE");
|
||||
let wake_private = this.eval_libc_i32("UMTX_OP_WAKE_PRIVATE");
|
||||
|
||||
let timespec_layout = this.libc_ty_layout("timespec");
|
||||
let umtx_time_layout = this.libc_ty_layout("_umtx_time");
|
||||
assert!(
|
||||
timespec_layout.size != umtx_time_layout.size,
|
||||
"`struct timespec` and `struct _umtx_time` should have different sizes."
|
||||
);
|
||||
|
||||
match op {
|
||||
// UMTX_OP_WAIT_UINT and UMTX_OP_WAIT_UINT_PRIVATE only differ in whether they work across
|
||||
// processes or not. For Miri, we can treat them the same.
|
||||
op if op == wait || op == wait_uint || op == wait_uint_private => {
|
||||
let obj_layout =
|
||||
if op == wait { this.machine.layouts.isize } else { this.machine.layouts.u32 };
|
||||
let obj = this.ptr_to_mplace(obj, obj_layout);
|
||||
|
||||
// Read the Linux futex wait implementation in Miri to understand why this fence is needed.
|
||||
this.atomic_fence(AtomicFenceOrd::SeqCst)?;
|
||||
let obj_val = this
|
||||
.read_scalar_atomic(&obj, AtomicReadOrd::Acquire)?
|
||||
.to_bits(obj_layout.size)?; // isize and u32 can have different sizes
|
||||
|
||||
if obj_val == u128::from(val) {
|
||||
// This cannot fail since we already did an atomic acquire read on that pointer.
|
||||
// Acquire reads are only allowed on mutable memory.
|
||||
let futex_ref = this
|
||||
.get_sync_or_init(obj.ptr(), |_| FreeBsdFutex { futex: Default::default() })
|
||||
.unwrap()
|
||||
.futex
|
||||
.clone();
|
||||
|
||||
// From the manual:
|
||||
// The timeout is specified by passing either the address of `struct timespec`, or its
|
||||
// extended variant, `struct _umtx_time`, as the `uaddr2` argument of _umtx_op().
|
||||
// They are distinguished by the `uaddr` value, which must be equal
|
||||
// to the size of the structure pointed to by `uaddr2`, casted to uintptr_t.
|
||||
let timeout = if this.ptr_is_null(uaddr2)? {
|
||||
// no timeout parameter
|
||||
None
|
||||
} else {
|
||||
if uaddr == umtx_time_layout.size.bytes() {
|
||||
// `uaddr2` points to a `struct _umtx_time`.
|
||||
let umtx_time_place = this.ptr_to_mplace(uaddr2, umtx_time_layout);
|
||||
|
||||
let umtx_time = match this.read_umtx_time(&umtx_time_place)? {
|
||||
Some(ut) => ut,
|
||||
None => {
|
||||
return this
|
||||
.set_last_error_and_return(LibcError("EINVAL"), dest);
|
||||
}
|
||||
};
|
||||
|
||||
let anchor = if umtx_time.abs_time {
|
||||
TimeoutAnchor::Absolute
|
||||
} else {
|
||||
TimeoutAnchor::Relative
|
||||
};
|
||||
|
||||
Some((umtx_time.timeout_clock, anchor, umtx_time.timeout))
|
||||
} else if uaddr == timespec_layout.size.bytes() {
|
||||
// RealTime clock can't be used in isolation mode.
|
||||
this.check_no_isolation("`_umtx_op` with `timespec` timeout")?;
|
||||
|
||||
// `uaddr2` points to a `struct timespec`.
|
||||
let timespec = this.ptr_to_mplace(uaddr2, timespec_layout);
|
||||
let duration = match this.read_timespec(×pec)? {
|
||||
Some(duration) => duration,
|
||||
None => {
|
||||
return this
|
||||
.set_last_error_and_return(LibcError("EINVAL"), dest);
|
||||
}
|
||||
};
|
||||
|
||||
// FreeBSD does not seem to document which clock is used when the timeout
|
||||
// is passed as a `struct timespec*`. Based on discussions online and the source
|
||||
// code (umtx_copyin_umtx_time() in kern_umtx.c), it seems to default to CLOCK_REALTIME,
|
||||
// so that's what we also do.
|
||||
// Discussion in golang: https://github.com/golang/go/issues/17168#issuecomment-250235271
|
||||
Some((TimeoutClock::RealTime, TimeoutAnchor::Relative, duration))
|
||||
} else {
|
||||
return this.set_last_error_and_return(LibcError("EINVAL"), dest);
|
||||
}
|
||||
};
|
||||
|
||||
let dest = dest.clone();
|
||||
this.futex_wait(
|
||||
futex_ref,
|
||||
u32::MAX, // we set the bitset to include all bits
|
||||
timeout,
|
||||
callback!(
|
||||
@capture<'tcx> {
|
||||
dest: MPlaceTy<'tcx>,
|
||||
}
|
||||
|ecx, unblock: UnblockKind| match unblock {
|
||||
UnblockKind::Ready => {
|
||||
// From the manual:
|
||||
// If successful, all requests, except UMTX_SHM_CREAT and UMTX_SHM_LOOKUP
|
||||
// sub-requests of the UMTX_OP_SHM request, will return zero.
|
||||
ecx.write_int(0, &dest)
|
||||
}
|
||||
UnblockKind::TimedOut => {
|
||||
ecx.set_last_error_and_return(LibcError("ETIMEDOUT"), &dest)
|
||||
}
|
||||
}
|
||||
),
|
||||
);
|
||||
interp_ok(())
|
||||
} else {
|
||||
// The manual doesn’t specify what should happen if the futex value doesn’t match the expected one.
|
||||
// On FreeBSD 14.2, testing shows that WAIT operations return 0 even when the value is incorrect.
|
||||
this.write_int(0, dest)?;
|
||||
interp_ok(())
|
||||
}
|
||||
}
|
||||
// UMTX_OP_WAKE and UMTX_OP_WAKE_PRIVATE only differ in whether they work across
|
||||
// processes or not. For Miri, we can treat them the same.
|
||||
op if op == wake || op == wake_private => {
|
||||
let Some(futex_ref) =
|
||||
this.get_sync_or_init(obj, |_| FreeBsdFutex { futex: Default::default() })
|
||||
else {
|
||||
// From Linux implemenation:
|
||||
// No AllocId, or no live allocation at that AllocId.
|
||||
// Return an error code. (That seems nicer than silently doing something non-intuitive.)
|
||||
// This means that if an address gets reused by a new allocation,
|
||||
// we'll use an independent futex queue for this... that seems acceptable.
|
||||
return this.set_last_error_and_return(LibcError("EFAULT"), dest);
|
||||
};
|
||||
let futex_ref = futex_ref.futex.clone();
|
||||
|
||||
// Saturating cast for when usize is smaller than u64.
|
||||
let count = usize::try_from(val).unwrap_or(usize::MAX);
|
||||
|
||||
// Read the Linux futex wake implementation in Miri to understand why this fence is needed.
|
||||
this.atomic_fence(AtomicFenceOrd::SeqCst)?;
|
||||
|
||||
// `_umtx_op` doesn't return the amount of woken threads.
|
||||
let _woken = this.futex_wake(
|
||||
&futex_ref,
|
||||
u32::MAX, // we set the bitset to include all bits
|
||||
count,
|
||||
)?;
|
||||
|
||||
// From the manual:
|
||||
// If successful, all requests, except UMTX_SHM_CREAT and UMTX_SHM_LOOKUP
|
||||
// sub-requests of the UMTX_OP_SHM request, will return zero.
|
||||
this.write_int(0, dest)?;
|
||||
interp_ok(())
|
||||
}
|
||||
op => {
|
||||
throw_unsup_format!("Miri does not support `_umtx_op` syscall with op={}", op)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a `_umtx_time` struct.
|
||||
/// Returns `None` if the underlying `timespec` struct is invalid.
|
||||
fn read_umtx_time(&mut self, ut: &MPlaceTy<'tcx>) -> InterpResult<'tcx, Option<UmtxTime>> {
|
||||
let this = self.eval_context_mut();
|
||||
// Only flag allowed is UMTX_ABSTIME.
|
||||
let abs_time = this.eval_libc_u32("UMTX_ABSTIME");
|
||||
|
||||
let timespec_place = this.project_field(ut, 0)?;
|
||||
// Inner `timespec` must still be valid.
|
||||
let duration = match this.read_timespec(×pec_place)? {
|
||||
Some(dur) => dur,
|
||||
None => return interp_ok(None),
|
||||
};
|
||||
|
||||
let flags_place = this.project_field(ut, 1)?;
|
||||
let flags = this.read_scalar(&flags_place)?.to_u32()?;
|
||||
let abs_time_flag = flags == abs_time;
|
||||
|
||||
let clock_id_place = this.project_field(ut, 2)?;
|
||||
let clock_id = this.read_scalar(&clock_id_place)?.to_i32()?;
|
||||
let timeout_clock = this.translate_umtx_time_clock_id(clock_id)?;
|
||||
|
||||
interp_ok(Some(UmtxTime { timeout: duration, abs_time: abs_time_flag, timeout_clock }))
|
||||
}
|
||||
|
||||
/// Translate raw FreeBSD clockid to a Miri TimeoutClock.
|
||||
/// FIXME: share this code with the pthread and clock_gettime shims.
|
||||
fn translate_umtx_time_clock_id(&mut self, raw_id: i32) -> InterpResult<'tcx, TimeoutClock> {
|
||||
let this = self.eval_context_mut();
|
||||
|
||||
let timeout = if raw_id == this.eval_libc_i32("CLOCK_REALTIME") {
|
||||
// RealTime clock can't be used in isolation mode.
|
||||
this.check_no_isolation("`_umtx_op` with `CLOCK_REALTIME` timeout")?;
|
||||
TimeoutClock::RealTime
|
||||
} else if raw_id == this.eval_libc_i32("CLOCK_MONOTONIC") {
|
||||
TimeoutClock::Monotonic
|
||||
} else {
|
||||
throw_unsup_format!("unsupported clock id {raw_id}");
|
||||
};
|
||||
interp_ok(timeout)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,9 @@
|
|||
|
||||
use std::borrow::Cow;
|
||||
use std::fs::{
|
||||
DirBuilder, File, FileType, Metadata, OpenOptions, ReadDir, read_dir, remove_dir, remove_file,
|
||||
rename,
|
||||
DirBuilder, File, FileType, OpenOptions, ReadDir, read_dir, remove_dir, remove_file, rename,
|
||||
};
|
||||
use std::io::{self, ErrorKind, IsTerminal, Read, Seek, SeekFrom, Write};
|
||||
use std::io::{self, ErrorKind, Read, Seek, SeekFrom, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::SystemTime;
|
||||
|
||||
|
|
@ -14,98 +13,11 @@ use rustc_data_structures::fx::FxHashMap;
|
|||
|
||||
use self::shims::time::system_time_to_duration;
|
||||
use crate::helpers::check_min_vararg_count;
|
||||
use crate::shims::files::{EvalContextExt as _, FileDescription, FileDescriptionRef};
|
||||
use crate::shims::files::FileHandle;
|
||||
use crate::shims::os_str::bytes_to_os_str;
|
||||
use crate::shims::unix::fd::{FlockOp, UnixFileDescription};
|
||||
use crate::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FileHandle {
|
||||
file: File,
|
||||
writable: bool,
|
||||
}
|
||||
|
||||
impl FileDescription for FileHandle {
|
||||
fn name(&self) -> &'static str {
|
||||
"file"
|
||||
}
|
||||
|
||||
fn read<'tcx>(
|
||||
self: FileDescriptionRef<Self>,
|
||||
communicate_allowed: bool,
|
||||
ptr: Pointer,
|
||||
len: usize,
|
||||
ecx: &mut MiriInterpCx<'tcx>,
|
||||
finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
|
||||
) -> InterpResult<'tcx> {
|
||||
assert!(communicate_allowed, "isolation should have prevented even opening a file");
|
||||
|
||||
let result = ecx.read_from_host(&self.file, len, ptr)?;
|
||||
finish.call(ecx, result)
|
||||
}
|
||||
|
||||
fn write<'tcx>(
|
||||
self: FileDescriptionRef<Self>,
|
||||
communicate_allowed: bool,
|
||||
ptr: Pointer,
|
||||
len: usize,
|
||||
ecx: &mut MiriInterpCx<'tcx>,
|
||||
finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
|
||||
) -> InterpResult<'tcx> {
|
||||
assert!(communicate_allowed, "isolation should have prevented even opening a file");
|
||||
|
||||
let result = ecx.write_to_host(&self.file, len, ptr)?;
|
||||
finish.call(ecx, result)
|
||||
}
|
||||
|
||||
fn seek<'tcx>(
|
||||
&self,
|
||||
communicate_allowed: bool,
|
||||
offset: SeekFrom,
|
||||
) -> InterpResult<'tcx, io::Result<u64>> {
|
||||
assert!(communicate_allowed, "isolation should have prevented even opening a file");
|
||||
interp_ok((&mut &self.file).seek(offset))
|
||||
}
|
||||
|
||||
fn close<'tcx>(
|
||||
self,
|
||||
communicate_allowed: bool,
|
||||
_ecx: &mut MiriInterpCx<'tcx>,
|
||||
) -> InterpResult<'tcx, io::Result<()>> {
|
||||
assert!(communicate_allowed, "isolation should have prevented even opening a file");
|
||||
// We sync the file if it was opened in a mode different than read-only.
|
||||
if self.writable {
|
||||
// `File::sync_all` does the checks that are done when closing a file. We do this to
|
||||
// to handle possible errors correctly.
|
||||
let result = self.file.sync_all();
|
||||
// Now we actually close the file and return the result.
|
||||
drop(self.file);
|
||||
interp_ok(result)
|
||||
} else {
|
||||
// We drop the file, this closes it but ignores any errors
|
||||
// produced when closing it. This is done because
|
||||
// `File::sync_all` cannot be done over files like
|
||||
// `/dev/urandom` which are read-only. Check
|
||||
// https://github.com/rust-lang/miri/issues/999#issuecomment-568920439
|
||||
// for a deeper discussion.
|
||||
drop(self.file);
|
||||
interp_ok(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
fn metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result<Metadata>> {
|
||||
interp_ok(self.file.metadata())
|
||||
}
|
||||
|
||||
fn is_tty(&self, communicate_allowed: bool) -> bool {
|
||||
communicate_allowed && self.file.is_terminal()
|
||||
}
|
||||
|
||||
fn as_unix(&self) -> &dyn UnixFileDescription {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl UnixFileDescription for FileHandle {
|
||||
fn pread<'tcx>(
|
||||
&self,
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ impl FileDescription for Epoll {
|
|||
interp_ok(Ok(()))
|
||||
}
|
||||
|
||||
fn as_unix(&self) -> &dyn UnixFileDescription {
|
||||
fn as_unix<'tcx>(&self, _ecx: &MiriInterpCx<'tcx>) -> &dyn UnixFileDescription {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -590,7 +590,7 @@ fn check_and_update_one_event_interest<'tcx>(
|
|||
ecx: &MiriInterpCx<'tcx>,
|
||||
) -> InterpResult<'tcx, bool> {
|
||||
// Get the bitmask of ready events for a file description.
|
||||
let ready_events_bitmask = fd_ref.as_unix().get_epoll_ready_events()?.get_event_bitmask(ecx);
|
||||
let ready_events_bitmask = fd_ref.as_unix(ecx).get_epoll_ready_events()?.get_event_bitmask(ecx);
|
||||
let epoll_event_interest = interest.borrow();
|
||||
let epfd = epoll_event_interest.weak_epfd.upgrade().unwrap();
|
||||
// This checks if any of the events specified in epoll_event_interest.events
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ impl FileDescription for EventFd {
|
|||
eventfd_write(buf_place, self, ecx, finish)
|
||||
}
|
||||
|
||||
fn as_unix(&self) -> &dyn UnixFileDescription {
|
||||
fn as_unix<'tcx>(&self, _ecx: &MiriInterpCx<'tcx>) -> &dyn UnixFileDescription {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ impl FileDescription for AnonSocket {
|
|||
anonsocket_write(self, ptr, len, ecx, finish)
|
||||
}
|
||||
|
||||
fn as_unix(&self) -> &dyn UnixFileDescription {
|
||||
fn as_unix<'tcx>(&self, _ecx: &MiriInterpCx<'tcx>) -> &dyn UnixFileDescription {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,14 +9,9 @@ use rustc_target::callconv::{Conv, FnAbi};
|
|||
|
||||
use self::shims::windows::handle::{Handle, PseudoHandle};
|
||||
use crate::shims::os_str::bytes_to_os_str;
|
||||
use crate::shims::windows::handle::HandleError;
|
||||
use crate::shims::windows::*;
|
||||
use crate::*;
|
||||
|
||||
// The NTSTATUS STATUS_INVALID_HANDLE (0xC0000008) encoded as a HRESULT by setting the N bit.
|
||||
// (https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/0642cb2f-2075-4469-918c-4441e69c548a)
|
||||
const STATUS_INVALID_HANDLE: u32 = 0xD0000008;
|
||||
|
||||
pub fn is_dyn_sym(name: &str) -> bool {
|
||||
// std does dynamic detection for these symbols
|
||||
matches!(
|
||||
|
|
@ -26,57 +21,107 @@ pub fn is_dyn_sym(name: &str) -> bool {
|
|||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn win_absolute<'tcx>(path: &Path) -> InterpResult<'tcx, io::Result<PathBuf>> {
|
||||
fn win_get_full_path_name<'tcx>(path: &Path) -> InterpResult<'tcx, io::Result<PathBuf>> {
|
||||
// We are on Windows so we can simply let the host do this.
|
||||
interp_ok(path::absolute(path))
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[expect(clippy::get_first, clippy::arithmetic_side_effects)]
|
||||
fn win_absolute<'tcx>(path: &Path) -> InterpResult<'tcx, io::Result<PathBuf>> {
|
||||
// We are on Unix, so we need to implement parts of the logic ourselves.
|
||||
fn win_get_full_path_name<'tcx>(path: &Path) -> InterpResult<'tcx, io::Result<PathBuf>> {
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use rustc_data_structures::fx::FxHashSet;
|
||||
|
||||
// We are on Unix, so we need to implement parts of the logic ourselves. `path` will use `/`
|
||||
// separators, and the result should also use `/`.
|
||||
// See <https://chrisdenton.github.io/omnipath/Overview.html#absolute-win32-paths> for more
|
||||
// information about Windows paths.
|
||||
// This does not handle all corner cases correctly, see
|
||||
// <https://github.com/rust-lang/miri/pull/4262#issuecomment-2792168853> for more cursed
|
||||
// examples.
|
||||
let bytes = path.as_os_str().as_encoded_bytes();
|
||||
// If it starts with `//` (these were backslashes but are already converted)
|
||||
// then this is a magic special path, we just leave it unchanged.
|
||||
if bytes.get(0).copied() == Some(b'/') && bytes.get(1).copied() == Some(b'/') {
|
||||
// If it starts with `//./` or `//?/` then this is a magic special path, we just leave it
|
||||
// unchanged.
|
||||
if bytes.get(0).copied() == Some(b'/')
|
||||
&& bytes.get(1).copied() == Some(b'/')
|
||||
&& matches!(bytes.get(2), Some(b'.' | b'?'))
|
||||
&& bytes.get(3).copied() == Some(b'/')
|
||||
{
|
||||
return interp_ok(Ok(path.into()));
|
||||
};
|
||||
// Special treatment for Windows' magic filenames: they are treated as being relative to `\\.\`.
|
||||
let magic_filenames = &[
|
||||
"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8",
|
||||
"COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
||||
];
|
||||
if magic_filenames.iter().any(|m| m.as_bytes() == bytes) {
|
||||
let mut result: Vec<u8> = br"//./".into();
|
||||
let is_unc = bytes.starts_with(b"//");
|
||||
// Special treatment for Windows' magic filenames: they are treated as being relative to `//./`.
|
||||
static MAGIC_FILENAMES: LazyLock<FxHashSet<&'static str>> = LazyLock::new(|| {
|
||||
FxHashSet::from_iter([
|
||||
"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7",
|
||||
"COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
||||
])
|
||||
});
|
||||
if str::from_utf8(bytes).is_ok_and(|s| MAGIC_FILENAMES.contains(&*s.to_ascii_uppercase())) {
|
||||
let mut result: Vec<u8> = b"//./".into();
|
||||
result.extend(bytes);
|
||||
return interp_ok(Ok(bytes_to_os_str(&result)?.into()));
|
||||
}
|
||||
// Otherwise we try to do something kind of close to what Windows does, but this is probably not
|
||||
// right in all cases. We iterate over the components between `/`, and remove trailing `.`,
|
||||
// except that trailing `..` remain unchanged.
|
||||
let mut result = vec![];
|
||||
// right in all cases.
|
||||
let mut result: Vec<&[u8]> = vec![]; // will be a vecot of components, joined by `/`.
|
||||
let mut bytes = bytes; // the remaining bytes to process
|
||||
loop {
|
||||
let len = bytes.iter().position(|&b| b == b'/').unwrap_or(bytes.len());
|
||||
let mut component = &bytes[..len];
|
||||
if len >= 2 && component[len - 1] == b'.' && component[len - 2] != b'.' {
|
||||
// Strip trailing `.`
|
||||
let mut stop = false;
|
||||
while !stop {
|
||||
// Find next component, and advance `bytes`.
|
||||
let mut component = match bytes.iter().position(|&b| b == b'/') {
|
||||
Some(pos) => {
|
||||
let (component, tail) = bytes.split_at(pos);
|
||||
bytes = &tail[1..]; // remove the `/`.
|
||||
component
|
||||
}
|
||||
None => {
|
||||
// There's no more `/`.
|
||||
stop = true;
|
||||
let component = bytes;
|
||||
bytes = &[];
|
||||
component
|
||||
}
|
||||
};
|
||||
// `NUL` and only `NUL` also gets changed to be relative to `//./` later in the path.
|
||||
// (This changed with Windows 11; previously, all magic filenames behaved like this.)
|
||||
// Also, this does not apply to UNC paths.
|
||||
if !is_unc && component.eq_ignore_ascii_case(b"NUL") {
|
||||
let mut result: Vec<u8> = b"//./".into();
|
||||
result.extend(component);
|
||||
return interp_ok(Ok(bytes_to_os_str(&result)?.into()));
|
||||
}
|
||||
// Deal with `..` -- Windows handles this entirely syntactically.
|
||||
if component == b".." {
|
||||
// Remove previous component, unless we are at the "root" already, then just ignore the `..`.
|
||||
let is_root = {
|
||||
// Paths like `/C:`.
|
||||
result.len() == 2 && matches!(result[0], []) && matches!(result[1], [_, b':'])
|
||||
} || {
|
||||
// Paths like `//server/share`
|
||||
result.len() == 4 && matches!(result[0], []) && matches!(result[1], [])
|
||||
};
|
||||
if !is_root {
|
||||
result.pop();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Preserve this component.
|
||||
// Strip trailing `.`, but preserve trailing `..`. But not for UNC paths!
|
||||
let len = component.len();
|
||||
if !is_unc && len >= 2 && component[len - 1] == b'.' && component[len - 2] != b'.' {
|
||||
component = &component[..len - 1];
|
||||
}
|
||||
// Add this component to output.
|
||||
result.extend(component);
|
||||
// Prepare next iteration.
|
||||
if len < bytes.len() {
|
||||
// There's a component after this; add `/` and process remaining bytes.
|
||||
result.push(b'/');
|
||||
bytes = &bytes[len + 1..];
|
||||
continue;
|
||||
} else {
|
||||
// This was the last component and it did not have a trailing `/`.
|
||||
break;
|
||||
}
|
||||
result.push(component);
|
||||
}
|
||||
// Let the host `absolute` function do working-dir handling
|
||||
// Drive letters must be followed by a `/`.
|
||||
if result.len() == 2 && matches!(result[0], []) && matches!(result[1], [_, b':']) {
|
||||
result.push(&[]);
|
||||
}
|
||||
// Let the host `absolute` function do working-dir handling.
|
||||
let result = result.join(&b'/');
|
||||
interp_ok(path::absolute(bytes_to_os_str(&result)?))
|
||||
}
|
||||
|
||||
|
|
@ -231,7 +276,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
}
|
||||
|
||||
let filename = this.read_path_from_wide_str(filename)?;
|
||||
let result = match win_absolute(&filename)? {
|
||||
let result = match win_get_full_path_name(&filename)? {
|
||||
Err(err) => {
|
||||
this.set_last_error(err)?;
|
||||
Scalar::from_u32(0) // return zero upon failure
|
||||
|
|
@ -246,6 +291,32 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
};
|
||||
this.write_scalar(result, dest)?;
|
||||
}
|
||||
"CreateFileW" => {
|
||||
let [
|
||||
file_name,
|
||||
desired_access,
|
||||
share_mode,
|
||||
security_attributes,
|
||||
creation_disposition,
|
||||
flags_and_attributes,
|
||||
template_file,
|
||||
] = this.check_shim(abi, sys_conv, link_name, args)?;
|
||||
let handle = this.CreateFileW(
|
||||
file_name,
|
||||
desired_access,
|
||||
share_mode,
|
||||
security_attributes,
|
||||
creation_disposition,
|
||||
flags_and_attributes,
|
||||
template_file,
|
||||
)?;
|
||||
this.write_scalar(handle.to_scalar(this), dest)?;
|
||||
}
|
||||
"GetFileInformationByHandle" => {
|
||||
let [handle, info] = this.check_shim(abi, sys_conv, link_name, args)?;
|
||||
let res = this.GetFileInformationByHandle(handle, info)?;
|
||||
this.write_scalar(res, dest)?;
|
||||
}
|
||||
|
||||
// Allocation
|
||||
"HeapAlloc" => {
|
||||
|
|
@ -498,52 +569,37 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
"SetThreadDescription" => {
|
||||
let [handle, name] = this.check_shim(abi, sys_conv, link_name, args)?;
|
||||
|
||||
let handle = this.read_scalar(handle)?;
|
||||
let handle = this.read_handle(handle, "SetThreadDescription")?;
|
||||
let name = this.read_wide_str(this.read_pointer(name)?)?;
|
||||
|
||||
let thread = match Handle::try_from_scalar(handle, this)? {
|
||||
Ok(Handle::Thread(thread)) => Ok(thread),
|
||||
Ok(Handle::Pseudo(PseudoHandle::CurrentThread)) => Ok(this.active_thread()),
|
||||
Ok(_) | Err(HandleError::InvalidHandle) =>
|
||||
this.invalid_handle("SetThreadDescription")?,
|
||||
Err(HandleError::ThreadNotFound(e)) => Err(e),
|
||||
let thread = match handle {
|
||||
Handle::Thread(thread) => thread,
|
||||
Handle::Pseudo(PseudoHandle::CurrentThread) => this.active_thread(),
|
||||
_ => this.invalid_handle("SetThreadDescription")?,
|
||||
};
|
||||
let res = match thread {
|
||||
Ok(thread) => {
|
||||
// FIXME: use non-lossy conversion
|
||||
this.set_thread_name(thread, String::from_utf16_lossy(&name).into_bytes());
|
||||
Scalar::from_u32(0)
|
||||
}
|
||||
Err(_) => Scalar::from_u32(STATUS_INVALID_HANDLE),
|
||||
};
|
||||
|
||||
this.write_scalar(res, dest)?;
|
||||
// FIXME: use non-lossy conversion
|
||||
this.set_thread_name(thread, String::from_utf16_lossy(&name).into_bytes());
|
||||
this.write_scalar(Scalar::from_u32(0), dest)?;
|
||||
}
|
||||
"GetThreadDescription" => {
|
||||
let [handle, name_ptr] = this.check_shim(abi, sys_conv, link_name, args)?;
|
||||
|
||||
let handle = this.read_scalar(handle)?;
|
||||
let handle = this.read_handle(handle, "GetThreadDescription")?;
|
||||
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
|
||||
|
||||
let thread = match Handle::try_from_scalar(handle, this)? {
|
||||
Ok(Handle::Thread(thread)) => Ok(thread),
|
||||
Ok(Handle::Pseudo(PseudoHandle::CurrentThread)) => Ok(this.active_thread()),
|
||||
Ok(_) | Err(HandleError::InvalidHandle) =>
|
||||
this.invalid_handle("GetThreadDescription")?,
|
||||
Err(HandleError::ThreadNotFound(e)) => Err(e),
|
||||
};
|
||||
let (name, res) = match thread {
|
||||
Ok(thread) => {
|
||||
// Looks like the default thread name is empty.
|
||||
let name = this.get_thread_name(thread).unwrap_or(b"").to_owned();
|
||||
let name = this.alloc_os_str_as_wide_str(
|
||||
bytes_to_os_str(&name)?,
|
||||
MiriMemoryKind::WinLocal.into(),
|
||||
)?;
|
||||
(Scalar::from_maybe_pointer(name, this), Scalar::from_u32(0))
|
||||
}
|
||||
Err(_) => (Scalar::null_ptr(this), Scalar::from_u32(STATUS_INVALID_HANDLE)),
|
||||
let thread = match handle {
|
||||
Handle::Thread(thread) => thread,
|
||||
Handle::Pseudo(PseudoHandle::CurrentThread) => this.active_thread(),
|
||||
_ => this.invalid_handle("GetThreadDescription")?,
|
||||
};
|
||||
// Looks like the default thread name is empty.
|
||||
let name = this.get_thread_name(thread).unwrap_or(b"").to_owned();
|
||||
let name = this.alloc_os_str_as_wide_str(
|
||||
bytes_to_os_str(&name)?,
|
||||
MiriMemoryKind::WinLocal.into(),
|
||||
)?;
|
||||
let name = Scalar::from_maybe_pointer(name, this);
|
||||
let res = Scalar::from_u32(0);
|
||||
|
||||
this.write_scalar(name, &name_ptr)?;
|
||||
this.write_scalar(res, dest)?;
|
||||
|
|
@ -638,11 +694,11 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
let [handle, filename, size] = this.check_shim(abi, sys_conv, link_name, args)?;
|
||||
this.check_no_isolation("`GetModuleFileNameW`")?;
|
||||
|
||||
let handle = this.read_target_usize(handle)?;
|
||||
let handle = this.read_handle(handle, "GetModuleFileNameW")?;
|
||||
let filename = this.read_pointer(filename)?;
|
||||
let size = this.read_scalar(size)?.to_u32()?;
|
||||
|
||||
if handle != 0 {
|
||||
if handle != Handle::Null {
|
||||
throw_unsup_format!("`GetModuleFileNameW` only supports the NULL handle");
|
||||
}
|
||||
|
||||
|
|
|
|||
402
src/tools/miri/src/shims/windows/fs.rs
Normal file
402
src/tools/miri/src/shims/windows/fs.rs
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
use std::fs::{Metadata, OpenOptions};
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use bitflags::bitflags;
|
||||
|
||||
use crate::shims::files::{FileDescription, FileHandle};
|
||||
use crate::shims::windows::handle::{EvalContextExt as _, Handle};
|
||||
use crate::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DirHandle {
|
||||
pub(crate) path: PathBuf,
|
||||
}
|
||||
|
||||
impl FileDescription for DirHandle {
|
||||
fn name(&self) -> &'static str {
|
||||
"directory"
|
||||
}
|
||||
|
||||
fn metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result<Metadata>> {
|
||||
interp_ok(self.path.metadata())
|
||||
}
|
||||
|
||||
fn close<'tcx>(
|
||||
self,
|
||||
_communicate_allowed: bool,
|
||||
_ecx: &mut MiriInterpCx<'tcx>,
|
||||
) -> InterpResult<'tcx, io::Result<()>> {
|
||||
interp_ok(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Windows supports handles without any read/write/delete permissions - these handles can get
|
||||
/// metadata, but little else. We represent that by storing the metadata from the time the handle
|
||||
/// was opened.
|
||||
#[derive(Debug)]
|
||||
pub struct MetadataHandle {
|
||||
pub(crate) meta: Metadata,
|
||||
}
|
||||
|
||||
impl FileDescription for MetadataHandle {
|
||||
fn name(&self) -> &'static str {
|
||||
"metadata-only"
|
||||
}
|
||||
|
||||
fn metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result<Metadata>> {
|
||||
interp_ok(Ok(self.meta.clone()))
|
||||
}
|
||||
|
||||
fn close<'tcx>(
|
||||
self,
|
||||
_communicate_allowed: bool,
|
||||
_ecx: &mut MiriInterpCx<'tcx>,
|
||||
) -> InterpResult<'tcx, io::Result<()>> {
|
||||
interp_ok(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
enum CreationDisposition {
|
||||
CreateAlways,
|
||||
CreateNew,
|
||||
OpenAlways,
|
||||
OpenExisting,
|
||||
TruncateExisting,
|
||||
}
|
||||
|
||||
impl CreationDisposition {
|
||||
fn new<'tcx>(
|
||||
value: u32,
|
||||
ecx: &mut MiriInterpCx<'tcx>,
|
||||
) -> InterpResult<'tcx, CreationDisposition> {
|
||||
let create_always = ecx.eval_windows_u32("c", "CREATE_ALWAYS");
|
||||
let create_new = ecx.eval_windows_u32("c", "CREATE_NEW");
|
||||
let open_always = ecx.eval_windows_u32("c", "OPEN_ALWAYS");
|
||||
let open_existing = ecx.eval_windows_u32("c", "OPEN_EXISTING");
|
||||
let truncate_existing = ecx.eval_windows_u32("c", "TRUNCATE_EXISTING");
|
||||
|
||||
let out = if value == create_always {
|
||||
CreationDisposition::CreateAlways
|
||||
} else if value == create_new {
|
||||
CreationDisposition::CreateNew
|
||||
} else if value == open_always {
|
||||
CreationDisposition::OpenAlways
|
||||
} else if value == open_existing {
|
||||
CreationDisposition::OpenExisting
|
||||
} else if value == truncate_existing {
|
||||
CreationDisposition::TruncateExisting
|
||||
} else {
|
||||
throw_unsup_format!("CreateFileW: Unsupported creation disposition: {value}");
|
||||
};
|
||||
interp_ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(PartialEq)]
|
||||
struct FileAttributes: u32 {
|
||||
const ZERO = 0;
|
||||
const NORMAL = 1 << 0;
|
||||
/// This must be passed to allow getting directory handles. If not passed, we error on trying
|
||||
/// to open directories
|
||||
const BACKUP_SEMANTICS = 1 << 1;
|
||||
/// Open a reparse point as a regular file - this is basically similar to 'readlink' in Unix
|
||||
/// terminology. A reparse point is a file with custom logic when navigated to, of which
|
||||
/// a symlink is one specific example.
|
||||
const OPEN_REPARSE = 1 << 2;
|
||||
}
|
||||
}
|
||||
|
||||
impl FileAttributes {
|
||||
fn new<'tcx>(
|
||||
mut value: u32,
|
||||
ecx: &mut MiriInterpCx<'tcx>,
|
||||
) -> InterpResult<'tcx, FileAttributes> {
|
||||
let file_attribute_normal = ecx.eval_windows_u32("c", "FILE_ATTRIBUTE_NORMAL");
|
||||
let file_flag_backup_semantics = ecx.eval_windows_u32("c", "FILE_FLAG_BACKUP_SEMANTICS");
|
||||
let file_flag_open_reparse_point =
|
||||
ecx.eval_windows_u32("c", "FILE_FLAG_OPEN_REPARSE_POINT");
|
||||
|
||||
let mut out = FileAttributes::ZERO;
|
||||
if value & file_flag_backup_semantics != 0 {
|
||||
value &= !file_flag_backup_semantics;
|
||||
out |= FileAttributes::BACKUP_SEMANTICS;
|
||||
}
|
||||
if value & file_flag_open_reparse_point != 0 {
|
||||
value &= !file_flag_open_reparse_point;
|
||||
out |= FileAttributes::OPEN_REPARSE;
|
||||
}
|
||||
if value & file_attribute_normal != 0 {
|
||||
value &= !file_attribute_normal;
|
||||
out |= FileAttributes::NORMAL;
|
||||
}
|
||||
|
||||
if value != 0 {
|
||||
throw_unsup_format!("CreateFileW: Unsupported flags_and_attributes: {value}");
|
||||
}
|
||||
|
||||
if out == FileAttributes::ZERO {
|
||||
// NORMAL is equivalent to 0. Avoid needing to check both cases by unifying the two.
|
||||
out = FileAttributes::NORMAL;
|
||||
}
|
||||
interp_ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
|
||||
#[allow(non_snake_case)]
|
||||
pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
||||
fn CreateFileW(
|
||||
&mut self,
|
||||
file_name: &OpTy<'tcx>, // LPCWSTR
|
||||
desired_access: &OpTy<'tcx>, // DWORD
|
||||
share_mode: &OpTy<'tcx>, // DWORD
|
||||
security_attributes: &OpTy<'tcx>, // LPSECURITY_ATTRIBUTES
|
||||
creation_disposition: &OpTy<'tcx>, // DWORD
|
||||
flags_and_attributes: &OpTy<'tcx>, // DWORD
|
||||
template_file: &OpTy<'tcx>, // HANDLE
|
||||
) -> InterpResult<'tcx, Handle> {
|
||||
// ^ Returns HANDLE
|
||||
use CreationDisposition::*;
|
||||
|
||||
let this = self.eval_context_mut();
|
||||
this.assert_target_os("windows", "CreateFileW");
|
||||
this.check_no_isolation("`CreateFileW`")?;
|
||||
|
||||
// This function appears to always set the error to 0. This is important for some flag
|
||||
// combinations, which may set error code on success.
|
||||
this.set_last_error(IoError::Raw(Scalar::from_i32(0)))?;
|
||||
|
||||
let file_name = this.read_path_from_wide_str(this.read_pointer(file_name)?)?;
|
||||
let mut desired_access = this.read_scalar(desired_access)?.to_u32()?;
|
||||
let share_mode = this.read_scalar(share_mode)?.to_u32()?;
|
||||
let security_attributes = this.read_pointer(security_attributes)?;
|
||||
let creation_disposition = this.read_scalar(creation_disposition)?.to_u32()?;
|
||||
let flags_and_attributes = this.read_scalar(flags_and_attributes)?.to_u32()?;
|
||||
let template_file = this.read_target_usize(template_file)?;
|
||||
|
||||
let generic_read = this.eval_windows_u32("c", "GENERIC_READ");
|
||||
let generic_write = this.eval_windows_u32("c", "GENERIC_WRITE");
|
||||
|
||||
let file_share_delete = this.eval_windows_u32("c", "FILE_SHARE_DELETE");
|
||||
let file_share_read = this.eval_windows_u32("c", "FILE_SHARE_READ");
|
||||
let file_share_write = this.eval_windows_u32("c", "FILE_SHARE_WRITE");
|
||||
|
||||
let creation_disposition = CreationDisposition::new(creation_disposition, this)?;
|
||||
let attributes = FileAttributes::new(flags_and_attributes, this)?;
|
||||
|
||||
if share_mode != (file_share_delete | file_share_read | file_share_write) {
|
||||
throw_unsup_format!("CreateFileW: Unsupported share mode: {share_mode}");
|
||||
}
|
||||
if !this.ptr_is_null(security_attributes)? {
|
||||
throw_unsup_format!("CreateFileW: Security attributes are not supported");
|
||||
}
|
||||
|
||||
if attributes.contains(FileAttributes::OPEN_REPARSE) && creation_disposition == CreateAlways
|
||||
{
|
||||
throw_machine_stop!(TerminationInfo::Abort("Invalid CreateFileW argument combination: FILE_FLAG_OPEN_REPARSE_POINT with CREATE_ALWAYS".to_string()));
|
||||
}
|
||||
|
||||
if template_file != 0 {
|
||||
throw_unsup_format!("CreateFileW: Template files are not supported");
|
||||
}
|
||||
|
||||
// We need to know if the file is a directory to correctly open directory handles.
|
||||
// This is racy, but currently the stdlib doesn't appear to offer a better solution.
|
||||
let is_dir = file_name.is_dir();
|
||||
|
||||
// BACKUP_SEMANTICS is how Windows calls the act of opening a directory handle.
|
||||
if !attributes.contains(FileAttributes::BACKUP_SEMANTICS) && is_dir {
|
||||
this.set_last_error(IoError::WindowsError("ERROR_ACCESS_DENIED"))?;
|
||||
return interp_ok(Handle::Invalid);
|
||||
}
|
||||
|
||||
let desired_read = desired_access & generic_read != 0;
|
||||
let desired_write = desired_access & generic_write != 0;
|
||||
|
||||
let mut options = OpenOptions::new();
|
||||
if desired_read {
|
||||
desired_access &= !generic_read;
|
||||
options.read(true);
|
||||
}
|
||||
if desired_write {
|
||||
desired_access &= !generic_write;
|
||||
options.write(true);
|
||||
}
|
||||
|
||||
if desired_access != 0 {
|
||||
throw_unsup_format!(
|
||||
"CreateFileW: Unsupported bits set for access mode: {desired_access:#x}"
|
||||
);
|
||||
}
|
||||
|
||||
// Per the documentation:
|
||||
// If the specified file exists and is writable, the function truncates the file,
|
||||
// the function succeeds, and last-error code is set to ERROR_ALREADY_EXISTS.
|
||||
// If the specified file does not exist and is a valid path, a new file is created,
|
||||
// the function succeeds, and the last-error code is set to zero.
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew
|
||||
//
|
||||
// This is racy, but there doesn't appear to be an std API that both succeeds if a
|
||||
// file exists but tells us it isn't new. Either we accept racing one way or another,
|
||||
// or we use an iffy heuristic like file creation time. This implementation prefers
|
||||
// to fail in the direction of erroring more often.
|
||||
if let CreateAlways | OpenAlways = creation_disposition
|
||||
&& file_name.exists()
|
||||
{
|
||||
this.set_last_error(IoError::WindowsError("ERROR_ALREADY_EXISTS"))?;
|
||||
}
|
||||
|
||||
let handle = if is_dir {
|
||||
// Open this as a directory.
|
||||
let fd_num = this.machine.fds.insert_new(DirHandle { path: file_name });
|
||||
Ok(Handle::File(fd_num))
|
||||
} else if creation_disposition == OpenExisting && !(desired_read || desired_write) {
|
||||
// Windows supports handles with no permissions. These allow things such as reading
|
||||
// metadata, but not file content.
|
||||
file_name.metadata().map(|meta| {
|
||||
let fd_num = this.machine.fds.insert_new(MetadataHandle { meta });
|
||||
Handle::File(fd_num)
|
||||
})
|
||||
} else {
|
||||
// Open this as a standard file.
|
||||
match creation_disposition {
|
||||
CreateAlways | OpenAlways => {
|
||||
options.create(true);
|
||||
if creation_disposition == CreateAlways {
|
||||
options.truncate(true);
|
||||
}
|
||||
}
|
||||
CreateNew => {
|
||||
options.create_new(true);
|
||||
// Per `create_new` documentation:
|
||||
// The file must be opened with write or append access in order to create a new file.
|
||||
// https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.create_new
|
||||
if !desired_write {
|
||||
options.append(true);
|
||||
}
|
||||
}
|
||||
OpenExisting => {} // Default options
|
||||
TruncateExisting => {
|
||||
options.truncate(true);
|
||||
}
|
||||
}
|
||||
|
||||
options.open(file_name).map(|file| {
|
||||
let fd_num =
|
||||
this.machine.fds.insert_new(FileHandle { file, writable: desired_write });
|
||||
Handle::File(fd_num)
|
||||
})
|
||||
};
|
||||
|
||||
match handle {
|
||||
Ok(handle) => interp_ok(handle),
|
||||
Err(e) => {
|
||||
this.set_last_error(e)?;
|
||||
interp_ok(Handle::Invalid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn GetFileInformationByHandle(
|
||||
&mut self,
|
||||
file: &OpTy<'tcx>, // HANDLE
|
||||
file_information: &OpTy<'tcx>, // LPBY_HANDLE_FILE_INFORMATION
|
||||
) -> InterpResult<'tcx, Scalar> {
|
||||
// ^ Returns BOOL (i32 on Windows)
|
||||
let this = self.eval_context_mut();
|
||||
this.assert_target_os("windows", "GetFileInformationByHandle");
|
||||
this.check_no_isolation("`GetFileInformationByHandle`")?;
|
||||
|
||||
let file = this.read_handle(file, "GetFileInformationByHandle")?;
|
||||
let file_information = this.deref_pointer_as(
|
||||
file_information,
|
||||
this.windows_ty_layout("BY_HANDLE_FILE_INFORMATION"),
|
||||
)?;
|
||||
|
||||
let fd_num = if let Handle::File(fd_num) = file {
|
||||
fd_num
|
||||
} else {
|
||||
this.invalid_handle("GetFileInformationByHandle")?
|
||||
};
|
||||
|
||||
let Some(desc) = this.machine.fds.get(fd_num) else {
|
||||
this.invalid_handle("GetFileInformationByHandle")?
|
||||
};
|
||||
|
||||
let metadata = match desc.metadata()? {
|
||||
Ok(meta) => meta,
|
||||
Err(e) => {
|
||||
this.set_last_error(e)?;
|
||||
return interp_ok(this.eval_windows("c", "FALSE"));
|
||||
}
|
||||
};
|
||||
|
||||
let size = metadata.len();
|
||||
|
||||
let file_type = metadata.file_type();
|
||||
let attributes = if file_type.is_dir() {
|
||||
this.eval_windows_u32("c", "FILE_ATTRIBUTE_DIRECTORY")
|
||||
} else if file_type.is_file() {
|
||||
this.eval_windows_u32("c", "FILE_ATTRIBUTE_NORMAL")
|
||||
} else {
|
||||
this.eval_windows_u32("c", "FILE_ATTRIBUTE_DEVICE")
|
||||
};
|
||||
|
||||
// Per the Windows documentation:
|
||||
// "If the underlying file system does not support the [...] time, this member is zero (0)."
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/ns-fileapi-by_handle_file_information
|
||||
let created = extract_windows_epoch(this, metadata.created())?.unwrap_or((0, 0));
|
||||
let accessed = extract_windows_epoch(this, metadata.accessed())?.unwrap_or((0, 0));
|
||||
let written = extract_windows_epoch(this, metadata.modified())?.unwrap_or((0, 0));
|
||||
|
||||
this.write_int_fields_named(&[("dwFileAttributes", attributes.into())], &file_information)?;
|
||||
write_filetime_field(this, &file_information, "ftCreationTime", created)?;
|
||||
write_filetime_field(this, &file_information, "ftLastAccessTime", accessed)?;
|
||||
write_filetime_field(this, &file_information, "ftLastWriteTime", written)?;
|
||||
this.write_int_fields_named(
|
||||
&[
|
||||
("dwVolumeSerialNumber", 0),
|
||||
("nFileSizeHigh", (size >> 32).into()),
|
||||
("nFileSizeLow", (size & 0xFFFFFFFF).into()),
|
||||
("nNumberOfLinks", 1),
|
||||
("nFileIndexHigh", 0),
|
||||
("nFileIndexLow", 0),
|
||||
],
|
||||
&file_information,
|
||||
)?;
|
||||
|
||||
interp_ok(this.eval_windows("c", "TRUE"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Windows FILETIME is measured in 100-nanosecs since 1601
|
||||
fn extract_windows_epoch<'tcx>(
|
||||
ecx: &MiriInterpCx<'tcx>,
|
||||
time: io::Result<SystemTime>,
|
||||
) -> InterpResult<'tcx, Option<(u32, u32)>> {
|
||||
match time.ok() {
|
||||
Some(time) => {
|
||||
let duration = ecx.system_time_since_windows_epoch(&time)?;
|
||||
let duration_ticks = ecx.windows_ticks_for(duration)?;
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
interp_ok(Some((duration_ticks as u32, (duration_ticks >> 32) as u32)))
|
||||
}
|
||||
None => interp_ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_filetime_field<'tcx>(
|
||||
cx: &mut MiriInterpCx<'tcx>,
|
||||
val: &MPlaceTy<'tcx>,
|
||||
name: &str,
|
||||
(low, high): (u32, u32),
|
||||
) -> InterpResult<'tcx> {
|
||||
cx.write_int_fields_named(
|
||||
&[("dwLowDateTime", low.into()), ("dwHighDateTime", high.into())],
|
||||
&cx.project_field_named(val, name)?,
|
||||
)
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ use std::mem::variant_count;
|
|||
use rustc_abi::HasDataLayout;
|
||||
|
||||
use crate::concurrency::thread::ThreadNotFound;
|
||||
use crate::shims::files::FdNum;
|
||||
use crate::*;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
|
|
@ -16,6 +17,8 @@ pub enum Handle {
|
|||
Null,
|
||||
Pseudo(PseudoHandle),
|
||||
Thread(ThreadId),
|
||||
File(FdNum),
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl PseudoHandle {
|
||||
|
|
@ -47,12 +50,18 @@ impl Handle {
|
|||
const NULL_DISCRIMINANT: u32 = 0;
|
||||
const PSEUDO_DISCRIMINANT: u32 = 1;
|
||||
const THREAD_DISCRIMINANT: u32 = 2;
|
||||
const FILE_DISCRIMINANT: u32 = 3;
|
||||
// Chosen to ensure Handle::Invalid encodes to -1. Update this value if there are ever more than
|
||||
// 8 discriminants.
|
||||
const INVALID_DISCRIMINANT: u32 = 7;
|
||||
|
||||
fn discriminant(self) -> u32 {
|
||||
match self {
|
||||
Self::Null => Self::NULL_DISCRIMINANT,
|
||||
Self::Pseudo(_) => Self::PSEUDO_DISCRIMINANT,
|
||||
Self::Thread(_) => Self::THREAD_DISCRIMINANT,
|
||||
Self::File(_) => Self::FILE_DISCRIMINANT,
|
||||
Self::Invalid => Self::INVALID_DISCRIMINANT,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -61,17 +70,27 @@ impl Handle {
|
|||
Self::Null => 0,
|
||||
Self::Pseudo(pseudo_handle) => pseudo_handle.value(),
|
||||
Self::Thread(thread) => thread.to_u32(),
|
||||
#[expect(clippy::cast_sign_loss)]
|
||||
Self::File(fd) => fd as u32,
|
||||
// INVALID_HANDLE_VALUE is -1. This fact is explicitly declared or implied in several
|
||||
// pages of Windows documentation.
|
||||
// 1: https://learn.microsoft.com/en-us/dotnet/api/microsoft.win32.safehandles.safefilehandle?view=net-9.0
|
||||
// 2: https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/get-osfhandle?view=msvc-170
|
||||
Self::Invalid => 0x1FFFFFFF,
|
||||
}
|
||||
}
|
||||
|
||||
fn packed_disc_size() -> u32 {
|
||||
// ceil(log2(x)) is how many bits it takes to store x numbers
|
||||
// ceil(log2(x)) is how many bits it takes to store x numbers.
|
||||
// We ensure that INVALID_HANDLE_VALUE (0xFFFFFFFF) decodes to Handle::Invalid.
|
||||
// see https://devblogs.microsoft.com/oldnewthing/20230914-00/?p=108766 for more detail on
|
||||
// INVALID_HANDLE_VALUE.
|
||||
let variant_count = variant_count::<Self>();
|
||||
|
||||
// however, std's ilog2 is floor(log2(x))
|
||||
// However, std's ilog2 is floor(log2(x)).
|
||||
let floor_log2 = variant_count.ilog2();
|
||||
|
||||
// we need to add one for non powers of two to compensate for the difference
|
||||
// We need to add one for non powers of two to compensate for the difference.
|
||||
#[expect(clippy::arithmetic_side_effects)] // cannot overflow
|
||||
if variant_count.is_power_of_two() { floor_log2 } else { floor_log2 + 1 }
|
||||
}
|
||||
|
|
@ -105,6 +124,13 @@ impl Handle {
|
|||
Self::NULL_DISCRIMINANT if data == 0 => Some(Self::Null),
|
||||
Self::PSEUDO_DISCRIMINANT => Some(Self::Pseudo(PseudoHandle::from_value(data)?)),
|
||||
Self::THREAD_DISCRIMINANT => Some(Self::Thread(ThreadId::new_unchecked(data))),
|
||||
#[expect(clippy::cast_possible_wrap)]
|
||||
Self::FILE_DISCRIMINANT => {
|
||||
// This cast preserves all bits.
|
||||
assert_eq!(size_of_val(&data), size_of::<FdNum>());
|
||||
Some(Self::File(data as FdNum))
|
||||
}
|
||||
Self::INVALID_DISCRIMINANT => Some(Self::Invalid),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -139,7 +165,7 @@ impl Handle {
|
|||
/// Structurally invalid handles return [`HandleError::InvalidHandle`].
|
||||
/// If the handle is structurally valid but semantically invalid, e.g. a for non-existent thread
|
||||
/// ID, returns [`HandleError::ThreadNotFound`].
|
||||
pub fn try_from_scalar<'tcx>(
|
||||
fn try_from_scalar<'tcx>(
|
||||
handle: Scalar,
|
||||
cx: &MiriInterpCx<'tcx>,
|
||||
) -> InterpResult<'tcx, Result<Self, HandleError>> {
|
||||
|
|
@ -171,6 +197,27 @@ impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
|
|||
|
||||
#[allow(non_snake_case)]
|
||||
pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
||||
/// Convert a scalar into a structured `Handle`.
|
||||
/// If the handle is invalid, or references a non-existent item, execution is aborted.
|
||||
#[track_caller]
|
||||
fn read_handle(&self, handle: &OpTy<'tcx>, function_name: &str) -> InterpResult<'tcx, Handle> {
|
||||
let this = self.eval_context_ref();
|
||||
let handle = this.read_scalar(handle)?;
|
||||
match Handle::try_from_scalar(handle, this)? {
|
||||
Ok(handle) => interp_ok(handle),
|
||||
Err(HandleError::InvalidHandle) =>
|
||||
throw_machine_stop!(TerminationInfo::Abort(format!(
|
||||
"invalid handle {} passed to {function_name}",
|
||||
handle.to_target_isize(this)?,
|
||||
))),
|
||||
Err(HandleError::ThreadNotFound(_)) =>
|
||||
throw_machine_stop!(TerminationInfo::Abort(format!(
|
||||
"invalid thread ID {} passed to {function_name}",
|
||||
handle.to_target_isize(this)?,
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid_handle(&mut self, function_name: &str) -> InterpResult<'tcx, !> {
|
||||
throw_machine_stop!(TerminationInfo::Abort(format!(
|
||||
"invalid handle passed to `{function_name}`"
|
||||
|
|
@ -180,15 +227,38 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
fn CloseHandle(&mut self, handle_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
|
||||
let this = self.eval_context_mut();
|
||||
|
||||
let handle = this.read_scalar(handle_op)?;
|
||||
let ret = match Handle::try_from_scalar(handle, this)? {
|
||||
Ok(Handle::Thread(thread)) => {
|
||||
let handle = this.read_handle(handle_op, "CloseHandle")?;
|
||||
let ret = match handle {
|
||||
Handle::Thread(thread) => {
|
||||
this.detach_thread(thread, /*allow_terminated_joined*/ true)?;
|
||||
this.eval_windows("c", "TRUE")
|
||||
}
|
||||
Handle::File(fd_num) =>
|
||||
if let Some(fd) = this.machine.fds.remove(fd_num) {
|
||||
let err = fd.close_ref(this.machine.communicate(), this)?;
|
||||
if let Err(e) = err {
|
||||
this.set_last_error(e)?;
|
||||
this.eval_windows("c", "FALSE")
|
||||
} else {
|
||||
this.eval_windows("c", "TRUE")
|
||||
}
|
||||
} else {
|
||||
this.invalid_handle("CloseHandle")?
|
||||
},
|
||||
_ => this.invalid_handle("CloseHandle")?,
|
||||
};
|
||||
|
||||
interp_ok(ret)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_invalid_encoding() {
|
||||
// Ensure the invalid handle encodes to `u32::MAX`/`INVALID_HANDLE_VALUE`.
|
||||
assert_eq!(Handle::Invalid.to_packed(), u32::MAX)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
pub mod foreign_items;
|
||||
|
||||
mod env;
|
||||
mod fs;
|
||||
mod handle;
|
||||
mod sync;
|
||||
mod thread;
|
||||
|
||||
// All the Windows-specific extension traits
|
||||
pub use self::env::{EvalContextExt as _, WindowsEnvVars};
|
||||
pub use self::fs::EvalContextExt as _;
|
||||
pub use self::handle::EvalContextExt as _;
|
||||
pub use self::sync::EvalContextExt as _;
|
||||
pub use self::thread::EvalContextExt as _;
|
||||
|
|
|
|||
|
|
@ -62,14 +62,14 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
) -> InterpResult<'tcx, Scalar> {
|
||||
let this = self.eval_context_mut();
|
||||
|
||||
let handle = this.read_scalar(handle_op)?;
|
||||
let handle = this.read_handle(handle_op, "WaitForSingleObject")?;
|
||||
let timeout = this.read_scalar(timeout_op)?.to_u32()?;
|
||||
|
||||
let thread = match Handle::try_from_scalar(handle, this)? {
|
||||
Ok(Handle::Thread(thread)) => thread,
|
||||
let thread = match handle {
|
||||
Handle::Thread(thread) => thread,
|
||||
// Unlike on posix, the outcome of joining the current thread is not documented.
|
||||
// On current Windows, it just deadlocks.
|
||||
Ok(Handle::Pseudo(PseudoHandle::CurrentThread)) => this.active_thread(),
|
||||
Handle::Pseudo(PseudoHandle::CurrentThread) => this.active_thread(),
|
||||
_ => this.invalid_handle("WaitForSingleObject")?,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,6 @@ page_size = "0.6"
|
|||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "net", "fs", "sync", "signal", "io-util"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.59", features = [ "Win32_Foundation", "Win32_System_Threading" ] }
|
||||
windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_System_Threading", "Win32_Storage_FileSystem", "Win32_Security"] }
|
||||
|
||||
[workspace]
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use windows_sys::Win32::System::Threading::{INFINITE, WaitForSingleObject};
|
|||
// XXX HACK: This is how miri represents the handle for thread 0.
|
||||
// This value can be "legitimately" obtained by using `GetCurrentThread` with `DuplicateHandle`
|
||||
// but miri does not implement `DuplicateHandle` yet.
|
||||
const MAIN_THREAD: HANDLE = (2i32 << 30) as HANDLE;
|
||||
const MAIN_THREAD: HANDLE = (2i32 << 29) as HANDLE;
|
||||
|
||||
fn main() {
|
||||
thread::spawn(|| {
|
||||
|
|
|
|||
260
src/tools/miri/tests/pass-dep/concurrency/freebsd-futex.rs
Normal file
260
src/tools/miri/tests/pass-dep/concurrency/freebsd-futex.rs
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
//@only-target: freebsd
|
||||
//@compile-flags: -Zmiri-preemption-rate=0 -Zmiri-disable-isolation
|
||||
|
||||
use std::mem::{self, MaybeUninit};
|
||||
use std::ptr::{self, addr_of};
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::time::Instant;
|
||||
use std::{io, thread};
|
||||
|
||||
fn wait_wake() {
|
||||
fn wake_nobody() {
|
||||
// Current thread waits on futex.
|
||||
// New thread wakes up 0 threads waiting on that futex.
|
||||
// Current thread should time out.
|
||||
static mut FUTEX: u32 = 0;
|
||||
|
||||
let waker = thread::spawn(|| {
|
||||
unsafe {
|
||||
assert_eq!(
|
||||
libc::_umtx_op(
|
||||
addr_of!(FUTEX) as *mut _,
|
||||
libc::UMTX_OP_WAKE_PRIVATE,
|
||||
0, // wake up 0 waiters
|
||||
ptr::null_mut::<libc::c_void>(),
|
||||
ptr::null_mut::<libc::c_void>(),
|
||||
),
|
||||
0
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 10ms should be enough.
|
||||
let mut timeout = libc::timespec { tv_sec: 0, tv_nsec: 10_000_000 };
|
||||
let timeout_size_arg =
|
||||
ptr::without_provenance_mut::<libc::c_void>(mem::size_of::<libc::timespec>());
|
||||
unsafe {
|
||||
assert_eq!(
|
||||
libc::_umtx_op(
|
||||
addr_of!(FUTEX) as *mut _,
|
||||
libc::UMTX_OP_WAIT_UINT_PRIVATE,
|
||||
0,
|
||||
timeout_size_arg,
|
||||
&mut timeout as *mut _ as _,
|
||||
),
|
||||
-1
|
||||
);
|
||||
// Main thread did not get woken up, so it timed out.
|
||||
assert_eq!(io::Error::last_os_error().raw_os_error().unwrap(), libc::ETIMEDOUT);
|
||||
}
|
||||
|
||||
waker.join().unwrap();
|
||||
}
|
||||
|
||||
fn wake_two_of_three() {
|
||||
// We create 2 threads that wait on a futex with a 100ms timeout.
|
||||
// The main thread wakes up 2 threads waiting on this futex and after this
|
||||
// checks that only those threads woke up and the other one timed out.
|
||||
static mut FUTEX: u32 = 0;
|
||||
|
||||
fn waiter() -> bool {
|
||||
let mut timeout = libc::timespec { tv_sec: 0, tv_nsec: 100_000_000 };
|
||||
let timeout_size_arg =
|
||||
ptr::without_provenance_mut::<libc::c_void>(mem::size_of::<libc::timespec>());
|
||||
unsafe {
|
||||
libc::_umtx_op(
|
||||
addr_of!(FUTEX) as *mut _,
|
||||
libc::UMTX_OP_WAIT_UINT_PRIVATE,
|
||||
0, // FUTEX is 0
|
||||
timeout_size_arg,
|
||||
&mut timeout as *mut _ as _,
|
||||
);
|
||||
// Return true if this thread woke up.
|
||||
io::Error::last_os_error().raw_os_error().unwrap() != libc::ETIMEDOUT
|
||||
}
|
||||
}
|
||||
|
||||
let t1 = thread::spawn(waiter);
|
||||
let t2 = thread::spawn(waiter);
|
||||
let t3 = thread::spawn(waiter);
|
||||
|
||||
// Run all the waiters, so they can go to sleep.
|
||||
thread::yield_now();
|
||||
|
||||
// Wake up 2 thread and make sure 1 is still waiting.
|
||||
unsafe {
|
||||
assert_eq!(
|
||||
libc::_umtx_op(
|
||||
addr_of!(FUTEX) as *mut _,
|
||||
libc::UMTX_OP_WAKE_PRIVATE,
|
||||
2,
|
||||
ptr::null_mut::<libc::c_void>(),
|
||||
ptr::null_mut::<libc::c_void>(),
|
||||
),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
// Treat the booleans as numbers to simplify checking how many threads were woken up.
|
||||
let t1 = t1.join().unwrap() as usize;
|
||||
let t2 = t2.join().unwrap() as usize;
|
||||
let t3 = t3.join().unwrap() as usize;
|
||||
let woken_up_count = t1 + t2 + t3;
|
||||
assert!(woken_up_count == 2, "Expected 2 threads to wake up got: {woken_up_count}");
|
||||
}
|
||||
|
||||
wake_nobody();
|
||||
wake_two_of_three();
|
||||
}
|
||||
|
||||
fn wake_dangling() {
|
||||
let futex = Box::new(0);
|
||||
let ptr: *const u32 = &*futex;
|
||||
drop(futex);
|
||||
|
||||
// Expect error since this is now "unmapped" memory.
|
||||
unsafe {
|
||||
assert_eq!(
|
||||
libc::_umtx_op(
|
||||
ptr as *const AtomicU32 as *mut _,
|
||||
libc::UMTX_OP_WAKE_PRIVATE,
|
||||
0,
|
||||
ptr::null_mut::<libc::c_void>(),
|
||||
ptr::null_mut::<libc::c_void>(),
|
||||
),
|
||||
-1
|
||||
);
|
||||
assert_eq!(io::Error::last_os_error().raw_os_error().unwrap(), libc::EFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
fn wait_wrong_val() {
|
||||
let futex: u32 = 123;
|
||||
|
||||
// Wait with a wrong value just returns 0
|
||||
unsafe {
|
||||
assert_eq!(
|
||||
libc::_umtx_op(
|
||||
ptr::from_ref(&futex).cast_mut().cast(),
|
||||
libc::UMTX_OP_WAIT_UINT_PRIVATE,
|
||||
456,
|
||||
ptr::null_mut::<libc::c_void>(),
|
||||
ptr::null_mut::<libc::c_void>(),
|
||||
),
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn wait_relative_timeout() {
|
||||
fn without_timespec() {
|
||||
let start = Instant::now();
|
||||
|
||||
let futex: u32 = 123;
|
||||
|
||||
let mut timeout = libc::timespec { tv_sec: 0, tv_nsec: 200_000_000 };
|
||||
let timeout_size_arg =
|
||||
ptr::without_provenance_mut::<libc::c_void>(mem::size_of::<libc::timespec>());
|
||||
// Wait for 200ms, with nobody waking us up early
|
||||
unsafe {
|
||||
assert_eq!(
|
||||
libc::_umtx_op(
|
||||
ptr::from_ref(&futex).cast_mut().cast(),
|
||||
libc::UMTX_OP_WAIT_UINT_PRIVATE,
|
||||
123,
|
||||
timeout_size_arg,
|
||||
&mut timeout as *mut _ as _,
|
||||
),
|
||||
-1
|
||||
);
|
||||
assert_eq!(io::Error::last_os_error().raw_os_error().unwrap(), libc::ETIMEDOUT);
|
||||
}
|
||||
|
||||
assert!((200..1000).contains(&start.elapsed().as_millis()));
|
||||
}
|
||||
|
||||
fn with_timespec() {
|
||||
let futex: u32 = 123;
|
||||
let mut timeout = libc::_umtx_time {
|
||||
_timeout: libc::timespec { tv_sec: 0, tv_nsec: 200_000_000 },
|
||||
_flags: 0,
|
||||
_clockid: libc::CLOCK_MONOTONIC as u32,
|
||||
};
|
||||
let timeout_size_arg =
|
||||
ptr::without_provenance_mut::<libc::c_void>(mem::size_of::<libc::_umtx_time>());
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
// Wait for 200ms, with nobody waking us up early
|
||||
unsafe {
|
||||
assert_eq!(
|
||||
libc::_umtx_op(
|
||||
ptr::from_ref(&futex).cast_mut().cast(),
|
||||
libc::UMTX_OP_WAIT_UINT_PRIVATE,
|
||||
123,
|
||||
timeout_size_arg,
|
||||
&mut timeout as *mut _ as _,
|
||||
),
|
||||
-1
|
||||
);
|
||||
assert_eq!(io::Error::last_os_error().raw_os_error().unwrap(), libc::ETIMEDOUT);
|
||||
}
|
||||
assert!((200..1000).contains(&start.elapsed().as_millis()));
|
||||
}
|
||||
|
||||
without_timespec();
|
||||
with_timespec();
|
||||
}
|
||||
|
||||
fn wait_absolute_timeout() {
|
||||
let start = Instant::now();
|
||||
|
||||
// Get the current monotonic timestamp as timespec.
|
||||
let mut timeout = unsafe {
|
||||
let mut now: MaybeUninit<libc::timespec> = MaybeUninit::uninit();
|
||||
assert_eq!(libc::clock_gettime(libc::CLOCK_MONOTONIC, now.as_mut_ptr()), 0);
|
||||
now.assume_init()
|
||||
};
|
||||
|
||||
// Add 200ms.
|
||||
timeout.tv_nsec += 200_000_000;
|
||||
if timeout.tv_nsec > 1_000_000_000 {
|
||||
timeout.tv_nsec -= 1_000_000_000;
|
||||
timeout.tv_sec += 1;
|
||||
}
|
||||
|
||||
// Create umtx_timeout struct with that absolute timeout.
|
||||
let umtx_timeout = libc::_umtx_time {
|
||||
_timeout: timeout,
|
||||
_flags: libc::UMTX_ABSTIME,
|
||||
_clockid: libc::CLOCK_MONOTONIC as u32,
|
||||
};
|
||||
let umtx_timeout_ptr = &umtx_timeout as *const _;
|
||||
let umtx_timeout_size = ptr::without_provenance_mut(mem::size_of_val(&umtx_timeout));
|
||||
|
||||
let futex: u32 = 123;
|
||||
|
||||
// Wait for 200ms from now, with nobody waking us up early.
|
||||
unsafe {
|
||||
assert_eq!(
|
||||
libc::_umtx_op(
|
||||
ptr::from_ref(&futex).cast_mut().cast(),
|
||||
libc::UMTX_OP_WAIT_UINT_PRIVATE,
|
||||
123,
|
||||
umtx_timeout_size,
|
||||
umtx_timeout_ptr as *mut _,
|
||||
),
|
||||
-1
|
||||
);
|
||||
assert_eq!(io::Error::last_os_error().raw_os_error().unwrap(), libc::ETIMEDOUT);
|
||||
}
|
||||
assert!((200..1000).contains(&start.elapsed().as_millis()));
|
||||
}
|
||||
|
||||
fn main() {
|
||||
wait_wake();
|
||||
wake_dangling();
|
||||
wait_wrong_val();
|
||||
wait_relative_timeout();
|
||||
wait_absolute_timeout();
|
||||
}
|
||||
198
src/tools/miri/tests/pass-dep/shims/windows-fs.rs
Normal file
198
src/tools/miri/tests/pass-dep/shims/windows-fs.rs
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
//@only-target: windows # this directly tests windows-only functions
|
||||
//@compile-flags: -Zmiri-disable-isolation
|
||||
#![allow(nonstandard_style)]
|
||||
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use std::path::Path;
|
||||
use std::ptr;
|
||||
|
||||
#[path = "../../utils/mod.rs"]
|
||||
mod utils;
|
||||
|
||||
use windows_sys::Win32::Foundation::{
|
||||
CloseHandle, ERROR_ALREADY_EXISTS, GENERIC_READ, GENERIC_WRITE, GetLastError,
|
||||
};
|
||||
use windows_sys::Win32::Storage::FileSystem::{
|
||||
BY_HANDLE_FILE_INFORMATION, CREATE_ALWAYS, CREATE_NEW, CreateFileW, FILE_ATTRIBUTE_DIRECTORY,
|
||||
FILE_ATTRIBUTE_NORMAL, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT,
|
||||
FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, GetFileInformationByHandle, OPEN_ALWAYS,
|
||||
OPEN_EXISTING,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
unsafe {
|
||||
test_create_dir_file();
|
||||
test_create_normal_file();
|
||||
test_create_always_twice();
|
||||
test_open_always_twice();
|
||||
test_open_dir_reparse();
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn test_create_dir_file() {
|
||||
let temp = utils::tmp();
|
||||
let raw_path = to_wide_cstr(&temp);
|
||||
// Open the `temp` directory.
|
||||
let handle = CreateFileW(
|
||||
raw_path.as_ptr(),
|
||||
GENERIC_READ,
|
||||
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
ptr::null_mut(),
|
||||
OPEN_EXISTING,
|
||||
FILE_FLAG_BACKUP_SEMANTICS,
|
||||
ptr::null_mut(),
|
||||
);
|
||||
assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
|
||||
let mut info = std::mem::zeroed::<BY_HANDLE_FILE_INFORMATION>();
|
||||
if GetFileInformationByHandle(handle, &mut info) == 0 {
|
||||
panic!("Failed to get file information")
|
||||
};
|
||||
assert!(info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0);
|
||||
if CloseHandle(handle) == 0 {
|
||||
panic!("Failed to close file")
|
||||
};
|
||||
}
|
||||
|
||||
unsafe fn test_create_normal_file() {
|
||||
let temp = utils::tmp().join("test.txt");
|
||||
let raw_path = to_wide_cstr(&temp);
|
||||
let handle = CreateFileW(
|
||||
raw_path.as_ptr(),
|
||||
GENERIC_READ | GENERIC_WRITE,
|
||||
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
ptr::null_mut(),
|
||||
CREATE_NEW,
|
||||
0,
|
||||
ptr::null_mut(),
|
||||
);
|
||||
assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
|
||||
let mut info = std::mem::zeroed::<BY_HANDLE_FILE_INFORMATION>();
|
||||
if GetFileInformationByHandle(handle, &mut info) == 0 {
|
||||
panic!("Failed to get file information: {}", GetLastError())
|
||||
};
|
||||
assert!(info.dwFileAttributes & FILE_ATTRIBUTE_NORMAL != 0);
|
||||
if CloseHandle(handle) == 0 {
|
||||
panic!("Failed to close file")
|
||||
};
|
||||
|
||||
// Test metadata-only handle
|
||||
let handle = CreateFileW(
|
||||
raw_path.as_ptr(),
|
||||
0,
|
||||
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
ptr::null_mut(),
|
||||
OPEN_EXISTING,
|
||||
0,
|
||||
ptr::null_mut(),
|
||||
);
|
||||
assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
|
||||
let mut info = std::mem::zeroed::<BY_HANDLE_FILE_INFORMATION>();
|
||||
if GetFileInformationByHandle(handle, &mut info) == 0 {
|
||||
panic!("Failed to get file information: {}", GetLastError())
|
||||
};
|
||||
assert!(info.dwFileAttributes & FILE_ATTRIBUTE_NORMAL != 0);
|
||||
if CloseHandle(handle) == 0 {
|
||||
panic!("Failed to close file")
|
||||
};
|
||||
}
|
||||
|
||||
/// Tests that CREATE_ALWAYS sets the error value correctly based on whether the file already exists
|
||||
unsafe fn test_create_always_twice() {
|
||||
let temp = utils::tmp().join("test_create_always.txt");
|
||||
let raw_path = to_wide_cstr(&temp);
|
||||
let handle = CreateFileW(
|
||||
raw_path.as_ptr(),
|
||||
GENERIC_READ | GENERIC_WRITE,
|
||||
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
ptr::null_mut(),
|
||||
CREATE_ALWAYS,
|
||||
0,
|
||||
ptr::null_mut(),
|
||||
);
|
||||
assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
|
||||
assert_eq!(GetLastError(), 0);
|
||||
if CloseHandle(handle) == 0 {
|
||||
panic!("Failed to close file")
|
||||
};
|
||||
|
||||
let handle = CreateFileW(
|
||||
raw_path.as_ptr(),
|
||||
GENERIC_READ | GENERIC_WRITE,
|
||||
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
ptr::null_mut(),
|
||||
CREATE_ALWAYS,
|
||||
0,
|
||||
ptr::null_mut(),
|
||||
);
|
||||
assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
|
||||
assert_eq!(GetLastError(), ERROR_ALREADY_EXISTS);
|
||||
if CloseHandle(handle) == 0 {
|
||||
panic!("Failed to close file")
|
||||
};
|
||||
}
|
||||
|
||||
/// Tests that OPEN_ALWAYS sets the error value correctly based on whether the file already exists
|
||||
unsafe fn test_open_always_twice() {
|
||||
let temp = utils::tmp().join("test_open_always.txt");
|
||||
let raw_path = to_wide_cstr(&temp);
|
||||
let handle = CreateFileW(
|
||||
raw_path.as_ptr(),
|
||||
GENERIC_READ | GENERIC_WRITE,
|
||||
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
ptr::null_mut(),
|
||||
OPEN_ALWAYS,
|
||||
0,
|
||||
ptr::null_mut(),
|
||||
);
|
||||
assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
|
||||
assert_eq!(GetLastError(), 0);
|
||||
if CloseHandle(handle) == 0 {
|
||||
panic!("Failed to close file")
|
||||
};
|
||||
|
||||
let handle = CreateFileW(
|
||||
raw_path.as_ptr(),
|
||||
GENERIC_READ | GENERIC_WRITE,
|
||||
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
ptr::null_mut(),
|
||||
OPEN_ALWAYS,
|
||||
0,
|
||||
ptr::null_mut(),
|
||||
);
|
||||
assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
|
||||
assert_eq!(GetLastError(), ERROR_ALREADY_EXISTS);
|
||||
if CloseHandle(handle) == 0 {
|
||||
panic!("Failed to close file")
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Once we support more of the std API, it would be nice to test against an actual symlink
|
||||
unsafe fn test_open_dir_reparse() {
|
||||
let temp = utils::tmp();
|
||||
let raw_path = to_wide_cstr(&temp);
|
||||
// Open the `temp` directory.
|
||||
let handle = CreateFileW(
|
||||
raw_path.as_ptr(),
|
||||
GENERIC_READ,
|
||||
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
ptr::null_mut(),
|
||||
OPEN_EXISTING,
|
||||
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
|
||||
ptr::null_mut(),
|
||||
);
|
||||
assert_ne!(handle.addr(), usize::MAX, "CreateFileW Failed: {}", GetLastError());
|
||||
let mut info = std::mem::zeroed::<BY_HANDLE_FILE_INFORMATION>();
|
||||
if GetFileInformationByHandle(handle, &mut info) == 0 {
|
||||
panic!("Failed to get file information")
|
||||
};
|
||||
assert!(info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0);
|
||||
if CloseHandle(handle) == 0 {
|
||||
panic!("Failed to close file")
|
||||
};
|
||||
}
|
||||
|
||||
fn to_wide_cstr(path: &Path) -> Vec<u16> {
|
||||
let mut raw_path = path.as_os_str().encode_wide().collect::<Vec<_>>();
|
||||
raw_path.extend([0, 0]);
|
||||
raw_path
|
||||
}
|
||||
|
|
@ -6,7 +6,11 @@ mod utils;
|
|||
|
||||
#[track_caller]
|
||||
fn assert_absolute_eq(in_: &str, out: &str) {
|
||||
assert_eq!(absolute(in_).unwrap().as_os_str(), Path::new(out).as_os_str());
|
||||
assert_eq!(
|
||||
absolute(in_).unwrap().as_os_str(),
|
||||
Path::new(out).as_os_str(),
|
||||
"incorrect absolute path for {in_:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn test_absolute() {
|
||||
|
|
@ -29,11 +33,28 @@ fn test_absolute() {
|
|||
assert_absolute_eq(r"\\?\C:\path\to\file", r"\\?\C:\path\to\file");
|
||||
assert_absolute_eq(r"\\?\UNC\server\share\to\file", r"\\?\UNC\server\share\to\file");
|
||||
assert_absolute_eq(r"\\?\PIPE\name", r"\\?\PIPE\name");
|
||||
assert_absolute_eq(r"\\server\share\NUL", r"\\server\share\NUL");
|
||||
// This fails on Windows 10 hosts. FIXME: enable this once GHA runners are on Windows 11.
|
||||
//assert_absolute_eq(r"C:\path\to\COM1", r"C:\path\to\COM1");
|
||||
// Verbatim paths are always unchanged, no matter what.
|
||||
assert_absolute_eq(r"\\?\path.\to/file..", r"\\?\path.\to/file..");
|
||||
|
||||
// Trailing dot is removed here.
|
||||
assert_absolute_eq(r"C:\path..\to.\file.", r"C:\path..\to\file");
|
||||
// `..` is resolved here.
|
||||
assert_absolute_eq(r"C:\path\to\..\file", r"C:\path\file");
|
||||
assert_absolute_eq(r"C:\path\to\..\..\file", r"C:\file");
|
||||
assert_absolute_eq(r"C:\path\to\..\..\..\..\..\..\file", r"C:\file");
|
||||
assert_absolute_eq(r"C:\..", r"C:\");
|
||||
assert_absolute_eq(r"\\server\share\to\path\with\..\file", r"\\server\share\to\path\file");
|
||||
assert_absolute_eq(r"\\server\share\to\..\..\..\..\file", r"\\server\share\file");
|
||||
assert_absolute_eq(r"\\server\share\..", r"\\server\share");
|
||||
// Magic filenames.
|
||||
assert_absolute_eq(r"NUL", r"\\.\NUL");
|
||||
assert_absolute_eq(r"nul", r"\\.\nul");
|
||||
assert_absolute_eq(r"COM1", r"\\.\COM1");
|
||||
assert_absolute_eq(r"com1", r"\\.\com1");
|
||||
assert_absolute_eq(r"C:\path\to\NUL", r"\\.\NUL");
|
||||
assert_absolute_eq(r"C:\path\to\nul", r"\\.\nul");
|
||||
} else {
|
||||
panic!("unsupported OS");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
//@ignore-target: windows # File handling is not implemented yet
|
||||
//@compile-flags: -Zmiri-disable-isolation
|
||||
|
||||
#![feature(io_error_more)]
|
||||
|
|
@ -18,20 +17,23 @@ mod utils;
|
|||
|
||||
fn main() {
|
||||
test_path_conversion();
|
||||
test_file();
|
||||
test_file_clone();
|
||||
test_file_create_new();
|
||||
test_seek();
|
||||
test_metadata();
|
||||
test_file_set_len();
|
||||
test_file_sync();
|
||||
test_errors();
|
||||
test_rename();
|
||||
test_directory();
|
||||
test_canonicalize();
|
||||
test_from_raw_os_error();
|
||||
#[cfg(unix)]
|
||||
test_pread_pwrite();
|
||||
// Windows file handling is very incomplete.
|
||||
if cfg!(not(windows)) {
|
||||
test_file();
|
||||
test_file_create_new();
|
||||
test_seek();
|
||||
test_file_clone();
|
||||
test_metadata();
|
||||
test_file_set_len();
|
||||
test_file_sync();
|
||||
test_errors();
|
||||
test_rename();
|
||||
test_directory();
|
||||
test_canonicalize();
|
||||
test_from_raw_os_error();
|
||||
#[cfg(unix)]
|
||||
test_pread_pwrite();
|
||||
}
|
||||
}
|
||||
|
||||
fn test_path_conversion() {
|
||||
|
|
@ -144,10 +146,10 @@ fn test_metadata() {
|
|||
let path = utils::prepare_with_content("miri_test_fs_metadata.txt", bytes);
|
||||
|
||||
// Test that metadata of an absolute path is correct.
|
||||
check_metadata(bytes, &path).unwrap();
|
||||
check_metadata(bytes, &path).expect("absolute path metadata");
|
||||
// Test that metadata of a relative path is correct.
|
||||
std::env::set_current_dir(path.parent().unwrap()).unwrap();
|
||||
check_metadata(bytes, Path::new(path.file_name().unwrap())).unwrap();
|
||||
check_metadata(bytes, Path::new(path.file_name().unwrap())).expect("relative path metadata");
|
||||
|
||||
// Removing file should succeed.
|
||||
remove_file(&path).unwrap();
|
||||
|
|
|
|||
|
|
@ -40,3 +40,9 @@ unless = ["S-blocked", "S-waiting-on-team", "S-waiting-on-review"]
|
|||
|
||||
# Automatically close and reopen PRs made by bots to run CI on them
|
||||
[bot-pull-requests]
|
||||
|
||||
# Canonicalize issue numbers to avoid closing the wrong issue when upstreaming this subtree
|
||||
[canonicalize-issue-links]
|
||||
|
||||
# Prevents mentions in commits to avoid users being spammed
|
||||
[no-mentions]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue