From 5dd05009260d7678b8f7b0f3515a515013f9b03a Mon Sep 17 00:00:00 2001 From: Chris Denton Date: Mon, 13 Apr 2026 17:37:16 +0000 Subject: [PATCH 01/24] Windows: Cache the pipe filesystem handle Also use the `\Device\NamedPipe\` directly instead of the symlink to it. --- .../std/src/sys/process/windows/child_pipe.rs | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/library/std/src/sys/process/windows/child_pipe.rs b/library/std/src/sys/process/windows/child_pipe.rs index b848435ac275f..311272fe05fa5 100644 --- a/library/std/src/sys/process/windows/child_pipe.rs +++ b/library/std/src/sys/process/windows/child_pipe.rs @@ -1,6 +1,8 @@ use crate::io::{self, BorrowedCursor, IoSlice, IoSliceMut}; use crate::ops::Neg; use crate::os::windows::prelude::*; +use crate::sync::atomic::Atomic; +use crate::sync::atomic::Ordering::Relaxed; use crate::sys::handle::Handle; use crate::sys::{FromInner, IntoInner, api, c}; use crate::{mem, ptr}; @@ -70,10 +72,15 @@ pub(super) fn child_pipe(ours_readable: bool, their_handle_inheritable: bool) -> let mut object_attributes = c::OBJECT_ATTRIBUTES::default(); object_attributes.Length = size_of::() as u32; - // Open a handle to the pipe filesystem (`\??\PIPE\`). - // This will be used when creating a new annon pipe. - let pipe_fs = { - let path = api::unicode_str!(r"\??\PIPE\"); + // Open a handle to the pipe filesystem (`\Device\NamedPipe\`) and cache it. + // This will be used when creating a new anonymous pipe. + static PIPE_FS: Atomic = Atomic::::new(ptr::null_mut()); + let pipe_fs = if let handle = PIPE_FS.load(Relaxed) + && !handle.is_null() + { + handle + } else { + let path = api::unicode_str!(r"\Device\NamedPipe\"); object_attributes.ObjectName = path.as_ptr(); let mut pipe_fs = ptr::null_mut(); let status = c::NtOpenFile( @@ -85,7 +92,13 @@ pub(super) fn child_pipe(ours_readable: bool, their_handle_inheritable: bool) -> c::FILE_SYNCHRONOUS_IO_NONALERT, // synchronous access ); if c::nt_success(status) { - Handle::from_raw_handle(pipe_fs) + match PIPE_FS.compare_exchange(ptr::null_mut(), pipe_fs, Relaxed, Relaxed) { + Ok(_) => pipe_fs, + Err(existing) => { + c::CloseHandle(pipe_fs); + existing + } + } } else { return Err(io::Error::from_raw_os_error(c::RtlNtStatusToDosError(status) as i32)); } @@ -104,7 +117,7 @@ pub(super) fn child_pipe(ours_readable: bool, their_handle_inheritable: bool) -> let ours = { // Use the pipe filesystem as the root directory. // With no name provided, an anonymous pipe will be created. - object_attributes.RootDirectory = pipe_fs.as_raw_handle(); + object_attributes.RootDirectory = pipe_fs; // A negative timeout value is a relative time (rather than an absolute time). // The time is given in 100's of nanoseconds so this is 50 milliseconds. From bb9788bd42f459094ec6839f151530bba66c0149 Mon Sep 17 00:00:00 2001 From: Elichai Turkel Date: Tue, 14 Apr 2026 19:11:41 +0300 Subject: [PATCH 02/24] Add _mm512_permutexvar_epi64 shim --- src/tools/miri/src/shims/x86/avx512.rs | 4 ++-- src/tools/miri/src/shims/x86/mod.rs | 23 ++++++++++++++---- .../pass/shims/x86/intrinsics-x86-avx512.rs | 24 +++++++++++++++++++ 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/tools/miri/src/shims/x86/avx512.rs b/src/tools/miri/src/shims/x86/avx512.rs index 8e1d22d723e79..fe4adf971c0d7 100644 --- a/src/tools/miri/src/shims/x86/avx512.rs +++ b/src/tools/miri/src/shims/x86/avx512.rs @@ -104,8 +104,8 @@ pub(super) trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { pmaddbw(this, left, right, dest)?; } - // Used to implement the _mm512_permutexvar_epi32 function. - "permvar.si.512" => { + // Used to implement the _mm512_permutexvar_epi32/_mm512_permutexvar_epi64 functions. + "permvar.si.512" | "permvar.di.512" => { let [left, right] = this.check_shim_sig_lenient(abi, CanonAbi::C, link_name, args)?; diff --git a/src/tools/miri/src/shims/x86/mod.rs b/src/tools/miri/src/shims/x86/mod.rs index ce6538c8ca273..e6e7f4b6f09f9 100644 --- a/src/tools/miri/src/shims/x86/mod.rs +++ b/src/tools/miri/src/shims/x86/mod.rs @@ -1056,12 +1056,22 @@ fn pmaddbw<'tcx>( interp_ok(()) } -/// Shuffle 32-bit integers in `values` across lanes using the corresponding -/// index in `indices`, and store the results in dst. +/// Shuffle elements in `values` across lanes using the corresponding index in +/// `indices`, and store the results in `dest`. +/// +/// This helper is shared by both the 32-bit-lane and 64-bit-lane AVX +/// permute-by-index intrinsics. The element type is taken from `values` and +/// `dest`, while the index lanes are interpreted at their full width (`i32` or +/// `i64`, depending on the intrinsic). +/// +/// For a vector with `N` lanes, only the low `log2(N)` bits of each index are +/// used. Equivalently, lane `i` of the result is copied from +/// `values[indices[i] & (N - 1)]`. /// /// /// /// +/// fn permute<'tcx>( ecx: &mut crate::MiriInterpCx<'tcx>, values: &OpTy<'tcx>, @@ -1075,18 +1085,21 @@ fn permute<'tcx>( // fn permd(a: u32x8, b: u32x8) -> u32x8; // fn permps(a: __m256, b: i32x8) -> __m256; // fn vpermd(a: i32x16, idx: i32x16) -> i32x16; + // fn vpermq(a: i64x8, b: i64x8) -> i64x8; assert_eq!(dest_len, values_len); assert_eq!(dest_len, indices_len); // Only use the lower 3 bits to index into a vector with 8 lanes, // or the lower 4 bits when indexing into a 16-lane vector. assert!(dest_len.is_power_of_two()); - let mask = u32::try_from(dest_len).unwrap().strict_sub(1); + let mask = u128::from(dest_len).strict_sub(1); for i in 0..dest_len { let dest = ecx.project_index(&dest, i)?; - let index = ecx.read_scalar(&ecx.project_index(&indices, i)?)?.to_u32()?; - let element = ecx.project_index(&values, (index & mask).into())?; + let index_place = ecx.project_index(&indices, i)?; + let index = ecx.read_scalar(&index_place)?.to_uint(index_place.layout.size)?; + // `mask` is at most `dest_len - 1` which fits in a `u64`, so this cannot fail. + let element = ecx.project_index(&values, u64::try_from(index & mask).unwrap())?; ecx.copy_op(&element, &dest)?; } diff --git a/src/tools/miri/tests/pass/shims/x86/intrinsics-x86-avx512.rs b/src/tools/miri/tests/pass/shims/x86/intrinsics-x86-avx512.rs index e1e23eda84281..0417a4cbc6791 100644 --- a/src/tools/miri/tests/pass/shims/x86/intrinsics-x86-avx512.rs +++ b/src/tools/miri/tests/pass/shims/x86/intrinsics-x86-avx512.rs @@ -219,6 +219,30 @@ unsafe fn test_avx512() { } test_mm512_permutexvar_epi32(); + #[target_feature(enable = "avx512f")] + unsafe fn test_mm512_permutexvar_epi64() { + let a = _mm512_setr_epi64(100, 200, 300, 400, 500, 600, 700, 800); + + // Mirrors stdarch's basic sanity check. + let idx = _mm512_set1_epi64(1); + let r = _mm512_permutexvar_epi64(idx, a); + let e = _mm512_set1_epi64(200); + assert_eq_m512i(r, e); + + // This must permute across the full 512-bit register, not within 128-bit lanes. + let idx = _mm512_setr_epi64(7, 0, 5, 2, 6, 1, 4, 3); + let r = _mm512_permutexvar_epi64(idx, a); + let e = _mm512_setr_epi64(800, 100, 600, 300, 700, 200, 500, 400); + assert_eq_m512i(r, e); + + // Only the low 3 bits of each 64-bit index are used. + let idx = _mm512_setr_epi64(8, 15, -1, i64::MIN, 0, 1, 2, 3); + let r = _mm512_permutexvar_epi64(idx, a); + let e = _mm512_setr_epi64(100, 800, 800, 100, 100, 200, 300, 400); + assert_eq_m512i(r, e); + } + test_mm512_permutexvar_epi64(); + #[target_feature(enable = "avx512bw")] unsafe fn test_mm512_shuffle_epi8() { #[rustfmt::skip] From c5673329278dd3f5e3c7e838b9595cfbcd02fda5 Mon Sep 17 00:00:00 2001 From: Mahdi Ali-Raihan Date: Sat, 11 Apr 2026 16:15:29 -0400 Subject: [PATCH 03/24] Implemented PermissionsExt ACP on Windows, which provides functions/utilities to observe, set, and create a Permissions struct with certain file attributes --- library/std/src/os/windows/fs.rs | 66 ++++++++++++++++++++++++++++++- library/std/src/sys/fs/windows.rs | 10 +++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/library/std/src/os/windows/fs.rs b/library/std/src/os/windows/fs.rs index 7fd46b31f7d83..54d5cafe15ec6 100644 --- a/library/std/src/os/windows/fs.rs +++ b/library/std/src/os/windows/fs.rs @@ -4,11 +4,11 @@ #![stable(feature = "rust1", since = "1.0.0")] -use crate::fs::{self, Metadata, OpenOptions}; +use crate::fs::{self, Metadata, OpenOptions, Permissions}; use crate::io::BorrowedCursor; use crate::path::Path; use crate::sealed::Sealed; -use crate::sys::{AsInner, AsInnerMut, IntoInner}; +use crate::sys::{AsInner, AsInnerMut, FromInner, IntoInner}; use crate::time::SystemTime; use crate::{io, sys}; @@ -368,6 +368,68 @@ impl OpenOptionsExt2 for OpenOptions { } } +/// Windows-specific extensions to [`fs::Permissions`]. This extension trait +/// provides extra utilities to shows what Windows file attributes are enabled +/// in [`Permissions`] and to manually set file attributes on [`Permissions`]. +/// +/// See Microsoft's [`File Attribute Constants`] page to know what file +/// attribute metadata are defined and stored on Windows files. +/// +/// [`Permissions`]: fs::Permissions +/// [`File Attribute Constants`]: +/// https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants +/// +/// # Example +/// +/// ```no_run +/// #![feature(windows_permissions_ext)] +/// use std::fs::Permissions; +/// use std::os::windows::fs::PermissionsExt; +/// +/// const FILE_ATTRIBUTE_SYSTEM: u32 = 0x4; +/// const FILE_ATTRIBUTE_ARCHIVE: u32 = 0x20; +/// let my_file_attr = FILE_ATTRIBUTE_SYSTEM | FILE_ATTRIBUTE_ARCHIVE; +/// let mut permissions = Permissions::from_file_attributes(my_file_attr); +/// assert_eq!(permissions.file_attributes(), my_file_attr); +/// +/// const FILE_ATTRIBUTE_HIDDEN: u32 = 0x2; +/// let new_file_attr = permissions.file_attributes() | FILE_ATTRIBUTE_HIDDEN; +/// permissions.set_file_attributes(new_file_attr); +/// assert_eq!(permissions.file_attributes(), new_file_attr); +/// ``` +#[unstable(feature = "windows_permissions_ext", issue = "152956")] +pub trait PermissionsExt: Sealed { + /// Returns the file attribute bits. + #[unstable(feature = "windows_permissions_ext", issue = "152956")] + fn file_attributes(&self) -> u32; + + /// Sets the file attribute bits. + #[unstable(feature = "windows_permissions_ext", issue = "152956")] + fn set_file_attributes(&mut self, mask: u32); + + /// Creates a new instance from the given file attribute bits. + #[unstable(feature = "windows_permissions_ext", issue = "152956")] + fn from_file_attributes(mask: u32) -> Self; +} + +#[unstable(feature = "windows_permissions_ext", issue = "152956")] +impl Sealed for fs::Permissions {} + +#[unstable(feature = "windows_permissions_ext", issue = "152956")] +impl PermissionsExt for fs::Permissions { + fn file_attributes(&self) -> u32 { + self.as_inner().file_attributes() + } + + fn set_file_attributes(&mut self, mask: u32) { + *self = Permissions::from_inner(FromInner::from_inner(mask)); + } + + fn from_file_attributes(mask: u32) -> Self { + Permissions::from_inner(FromInner::from_inner(mask)) + } +} + /// Windows-specific extensions to [`fs::Metadata`]. /// /// The data members that this trait exposes correspond to the members diff --git a/library/std/src/sys/fs/windows.rs b/library/std/src/sys/fs/windows.rs index 74854cdeb498d..e0b02670264d9 100644 --- a/library/std/src/sys/fs/windows.rs +++ b/library/std/src/sys/fs/windows.rs @@ -1167,6 +1167,16 @@ impl FilePermissions { self.attrs &= !c::FILE_ATTRIBUTE_READONLY; } } + + pub fn file_attributes(&self) -> u32 { + self.attrs as u32 + } +} + +impl FromInner for FilePermissions { + fn from_inner(attrs: u32) -> FilePermissions { + FilePermissions { attrs } + } } impl FileTimes { From e1ef601a730e172dfa5a390500fed499cd94ed9a Mon Sep 17 00:00:00 2001 From: Zac Harrold Date: Tue, 21 Apr 2026 09:03:22 +1000 Subject: [PATCH 04/24] Adjust Documentation for `RawOsError` The hyperlink to `std::io::Error` will not be valid when moved to `core::io`. There is also a typo which I might as well fix while I'm here. --- library/std/src/io/error.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/std/src/io/error.rs b/library/std/src/io/error.rs index 6f565bb37c53b..35e2c8a293048 100644 --- a/library/std/src/io/error.rs +++ b/library/std/src/io/error.rs @@ -140,11 +140,11 @@ enum ErrorData { Custom(C), } -/// The type of raw OS error codes returned by [`Error::raw_os_error`]. +/// The type of raw OS error codes. /// /// This is an [`i32`] on all currently supported platforms, but platforms /// added in the future (such as UEFI) may use a different primitive type like -/// [`usize`]. Use `as`or [`into`] conversions where applicable to ensure maximum +/// [`usize`]. Use `as` or [`into`] conversions where applicable to ensure maximum /// portability. /// /// [`into`]: Into::into From 4cb72b3ca534f6119b40e6f7545e38f1dfde0f4a Mon Sep 17 00:00:00 2001 From: Zac Harrold Date: Tue, 21 Apr 2026 09:01:40 +1000 Subject: [PATCH 05/24] Adjust Usage of `RawOsError` Inconsistently referenced through `std::sys` and `std::io`. Choosing `std::io` as the canonical source to make migration to `core::io` cleaner. --- library/std/src/sys/io/error/motor.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/library/std/src/sys/io/error/motor.rs b/library/std/src/sys/io/error/motor.rs index 7d612d817cdd7..3c22d5fcb7b06 100644 --- a/library/std/src/sys/io/error/motor.rs +++ b/library/std/src/sys/io/error/motor.rs @@ -1,7 +1,6 @@ use crate::io; -use crate::sys::io::RawOsError; -pub fn errno() -> RawOsError { +pub fn errno() -> io::RawOsError { // Not used in Motor OS because it is ambiguous: Motor OS // is micro-kernel-based, and I/O happens via a shared-memory // ring buffer, so an I/O operation that on a unix is a syscall @@ -57,7 +56,7 @@ pub fn decode_error_kind(code: io::RawOsError) -> io::ErrorKind { } } -pub fn error_string(errno: RawOsError) -> String { +pub fn error_string(errno: io::RawOsError) -> String { let error: moto_rt::Error = match errno { x if x < 0 => moto_rt::Error::Unknown, x if x > u16::MAX.into() => moto_rt::Error::Unknown, From 7653e5ea01c18a8245df9dd48ea706c112f69b28 Mon Sep 17 00:00:00 2001 From: Zac Harrold Date: Tue, 21 Apr 2026 09:04:58 +1000 Subject: [PATCH 06/24] Move `RawOsError` to `core::io` --- library/core/src/io/error.rs | 14 ++++++++++++++ library/core/src/io/mod.rs | 2 ++ library/std/src/io/error.rs | 13 ++----------- library/std/src/lib.rs | 1 + library/std/src/sys/io/error/mod.rs | 5 ----- library/std/src/sys/io/mod.rs | 2 +- 6 files changed, 20 insertions(+), 17 deletions(-) diff --git a/library/core/src/io/error.rs b/library/core/src/io/error.rs index fe12de2952f0a..ff50b2822d05a 100644 --- a/library/core/src/io/error.rs +++ b/library/core/src/io/error.rs @@ -2,6 +2,20 @@ use crate::fmt; +/// The type of raw OS error codes. +/// +/// This is an [`i32`] on all currently supported platforms, but platforms +/// added in the future (such as UEFI) may use a different primitive type like +/// [`usize`]. Use `as` or [`into`] conversions where applicable to ensure maximum +/// portability. +/// +/// [`into`]: Into::into +#[unstable(feature = "raw_os_error_ty", issue = "107792")] +pub type RawOsError = cfg_select! { + target_os = "uefi" => usize, + _ => i32, +}; + /// A list specifying general categories of I/O error. /// /// This list is intended to grow over time and it is not recommended to diff --git a/library/core/src/io/mod.rs b/library/core/src/io/mod.rs index c34421523b643..2d8273dd1b2d0 100644 --- a/library/core/src/io/mod.rs +++ b/library/core/src/io/mod.rs @@ -7,3 +7,5 @@ mod error; pub use self::borrowed_buf::{BorrowedBuf, BorrowedCursor}; #[unstable(feature = "core_io", issue = "154046")] pub use self::error::ErrorKind; +#[unstable(feature = "raw_os_error_ty", issue = "107792")] +pub use self::error::RawOsError; diff --git a/library/std/src/io/error.rs b/library/std/src/io/error.rs index 35e2c8a293048..360ca83c65a91 100644 --- a/library/std/src/io/error.rs +++ b/library/std/src/io/error.rs @@ -3,6 +3,8 @@ mod tests; #[stable(feature = "rust1", since = "1.0.0")] pub use core::io::ErrorKind; +#[unstable(feature = "raw_os_error_ty", issue = "107792")] +pub use core::io::RawOsError; // On 64-bit platforms, `io::Error` may use a bit-packed representation to // reduce size. However, this representation assumes that error codes are @@ -140,17 +142,6 @@ enum ErrorData { Custom(C), } -/// The type of raw OS error codes. -/// -/// This is an [`i32`] on all currently supported platforms, but platforms -/// added in the future (such as UEFI) may use a different primitive type like -/// [`usize`]. Use `as` or [`into`] conversions where applicable to ensure maximum -/// portability. -/// -/// [`into`]: Into::into -#[unstable(feature = "raw_os_error_ty", issue = "107792")] -pub type RawOsError = sys::io::RawOsError; - // `#[repr(align(4))]` is probably redundant, it should have that value or // higher already. We include it just because repr_bitpacked.rs's encoding // requires an alignment >= 4 (note that `#[repr(align)]` will not reduce the diff --git a/library/std/src/lib.rs b/library/std/src/lib.rs index 807befec1ad11..7287613bde46c 100644 --- a/library/std/src/lib.rs +++ b/library/std/src/lib.rs @@ -363,6 +363,7 @@ #![feature(ptr_as_uninit)] #![feature(ptr_mask)] #![feature(random)] +#![feature(raw_os_error_ty)] #![feature(slice_internals)] #![feature(slice_ptr_get)] #![feature(slice_range)] diff --git a/library/std/src/sys/io/error/mod.rs b/library/std/src/sys/io/error/mod.rs index d7a0b9b4b301d..4fca658a7dcaa 100644 --- a/library/std/src/sys/io/error/mod.rs +++ b/library/std/src/sys/io/error/mod.rs @@ -48,8 +48,3 @@ cfg_select! { pub use generic::*; } } - -pub type RawOsError = cfg_select! { - target_os = "uefi" => usize, - _ => i32, -}; diff --git a/library/std/src/sys/io/mod.rs b/library/std/src/sys/io/mod.rs index b3587ab63696a..445bcdef0aa1f 100644 --- a/library/std/src/sys/io/mod.rs +++ b/library/std/src/sys/io/mod.rs @@ -62,7 +62,7 @@ pub use error::errno_location; target_os = "wasi", ))] pub use error::set_errno; -pub use error::{RawOsError, decode_error_kind, errno, error_string, is_interrupted}; +pub use error::{decode_error_kind, errno, error_string, is_interrupted}; pub use io_slice::{IoSlice, IoSliceMut}; pub use is_terminal::is_terminal; pub use kernel_copy::{CopyState, kernel_copy}; From a6ec2947a3b94bf9b7bb9e9ee2ad0d82f580e069 Mon Sep 17 00:00:00 2001 From: Chris Denton Date: Tue, 21 Apr 2026 04:15:26 +0000 Subject: [PATCH 07/24] Explicitly note that we're leaking a handle --- library/std/src/sys/process/windows/child_pipe.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/library/std/src/sys/process/windows/child_pipe.rs b/library/std/src/sys/process/windows/child_pipe.rs index 311272fe05fa5..8d71e1c61f82d 100644 --- a/library/std/src/sys/process/windows/child_pipe.rs +++ b/library/std/src/sys/process/windows/child_pipe.rs @@ -72,8 +72,12 @@ pub(super) fn child_pipe(ours_readable: bool, their_handle_inheritable: bool) -> let mut object_attributes = c::OBJECT_ATTRIBUTES::default(); object_attributes.Length = size_of::() as u32; - // Open a handle to the pipe filesystem (`\Device\NamedPipe\`) and cache it. + // Open a handle to the pipe filesystem (`\Device\NamedPipe\`). // This will be used when creating a new anonymous pipe. + // + // We cache the handle once so we can reuse it without needing to reopen it each time. + // NOTE: this means the handle may appear to be leaked but that's fine because + // it's only one handle and the OS will clean it up when the process exits. static PIPE_FS: Atomic = Atomic::::new(ptr::null_mut()); let pipe_fs = if let handle = PIPE_FS.load(Relaxed) && !handle.is_null() From 2f73eeea8b0f6a848c3a0cdba977a0783c57e4b9 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Wed, 22 Apr 2026 17:46:49 +0200 Subject: [PATCH 08/24] unnamed_socket: do not introduce artifical short reads/writes --- src/tools/miri/src/shims/unix/unnamed_socket.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/tools/miri/src/shims/unix/unnamed_socket.rs b/src/tools/miri/src/shims/unix/unnamed_socket.rs index 74d6abf7b63d4..2bec5640ab1b0 100644 --- a/src/tools/miri/src/shims/unix/unnamed_socket.rs +++ b/src/tools/miri/src/shims/unix/unnamed_socket.rs @@ -126,11 +126,13 @@ impl FileDescription for AnonSocket { } fn short_fd_operations(&self) -> bool { - // Pipes guarantee that sufficiently small accesses are not broken apart: - // . - // For now, we don't bother checking for the size, and just entirely disable - // short accesses on pipes. - matches!(self.fd_type, AnonSocketType::Socketpair) + // Linux de-facto guarantees (or at least, applications like tokio assume [1, 2]) that + // when a read/write on a streaming socket comes back short, the kernel buffer is + // empty/full. SO we can't do short reads/writes here. + // + // [1]: https://github.com/tokio-rs/tokio/blob/6c03e03898d71eca976ee1ad8481cf112ae722ba/tokio/src/io/poll_evented.rs#L182 + // [2]: https://github.com/tokio-rs/tokio/blob/6c03e03898d71eca976ee1ad8481cf112ae722ba/tokio/src/io/poll_evented.rs#L240 + false } fn as_unix<'tcx>(&self, _ecx: &MiriInterpCx<'tcx>) -> &dyn UnixFileDescription { From 17aae34a1fcc2b6de13eb4735ae293b4627a402c Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Wed, 22 Apr 2026 17:49:12 +0200 Subject: [PATCH 09/24] rename unnamed_socket -> virtual_socket --- src/tools/miri/src/concurrency/thread.rs | 4 +- src/tools/miri/src/shims/unix/mod.rs | 4 +- .../{unnamed_socket.rs => virtual_socket.rs} | 84 +++++++++---------- 3 files changed, 46 insertions(+), 46 deletions(-) rename src/tools/miri/src/shims/unix/{unnamed_socket.rs => virtual_socket.rs} (90%) diff --git a/src/tools/miri/src/concurrency/thread.rs b/src/tools/miri/src/concurrency/thread.rs index e9458cc3f4568..f72bfe031d36b 100644 --- a/src/tools/miri/src/concurrency/thread.rs +++ b/src/tools/miri/src/concurrency/thread.rs @@ -111,8 +111,8 @@ pub enum BlockReason { Epoll, /// Blocked on eventfd. Eventfd, - /// Blocked on unnamed_socket. - UnnamedSocket, + /// Blocked on virtual socket. + VirtualSocket, /// Blocked on an IO operation. IO, /// Blocked for any reason related to GenMC, such as `assume` statements (GenMC mode only). diff --git a/src/tools/miri/src/shims/unix/mod.rs b/src/tools/miri/src/shims/unix/mod.rs index 5ea49926fb9fd..c55a28bfa7b2a 100644 --- a/src/tools/miri/src/shims/unix/mod.rs +++ b/src/tools/miri/src/shims/unix/mod.rs @@ -7,7 +7,7 @@ mod mem; mod socket; mod sync; mod thread; -mod unnamed_socket; +mod virtual_socket; mod android; mod freebsd; @@ -25,7 +25,7 @@ pub use self::mem::EvalContextExt as _; pub use self::socket::EvalContextExt as _; pub use self::sync::EvalContextExt as _; pub use self::thread::{EvalContextExt as _, ThreadNameResult}; -pub use self::unnamed_socket::EvalContextExt as _; +pub use self::virtual_socket::EvalContextExt as _; // Make up some constants. const UID: u32 = 1000; diff --git a/src/tools/miri/src/shims/unix/unnamed_socket.rs b/src/tools/miri/src/shims/unix/virtual_socket.rs similarity index 90% rename from src/tools/miri/src/shims/unix/unnamed_socket.rs rename to src/tools/miri/src/shims/unix/virtual_socket.rs index 2bec5640ab1b0..51092b80c1c68 100644 --- a/src/tools/miri/src/shims/unix/unnamed_socket.rs +++ b/src/tools/miri/src/shims/unix/virtual_socket.rs @@ -1,6 +1,6 @@ -//! This implements "anonymous" sockets, that do not correspond to anything on the host system and +//! This implements "virtual" sockets, that do not correspond to anything on the host system and //! are entirely implemented inside Miri. -//! We also use the same infrastructure to implement unnamed pipes. +//! This is used to implement `socketpair` and `pipe`. use std::cell::{Cell, OnceCell, RefCell}; use std::collections::VecDeque; @@ -22,7 +22,7 @@ use crate::*; const MAX_SOCKETPAIR_BUFFER_CAPACITY: usize = 0x34000; #[derive(Debug, PartialEq)] -enum AnonSocketType { +enum VirtualSocketType { // Either end of the socketpair fd. Socketpair, // Read end of the pipe. @@ -31,16 +31,16 @@ enum AnonSocketType { PipeWrite, } -/// One end of a pair of connected unnamed sockets. +/// One end of a pair of connected virtual sockets. #[derive(Debug)] -struct AnonSocket { +struct VirtualSocket { /// The buffer we are reading from, or `None` if this is the writing end of a pipe. /// (In that case, the peer FD will be the reading end of that pipe.) readbuf: Option>, - /// The `AnonSocket` file descriptor that is our "peer", and that holds the buffer we are + /// The `VirtualSocket` file descriptor that is our "peer", and that holds the buffer we are /// writing to. This is a weak reference because the other side may be closed before us; all /// future writes will then trigger EPIPE. - peer_fd: OnceCell>, + peer_fd: OnceCell>, /// Indicates whether the peer has lost data when the file description is closed. /// This flag is set to `true` if the peer's `readbuf` is non-empty at the time /// of closure. @@ -53,8 +53,8 @@ struct AnonSocket { blocked_write_tid: RefCell>, /// Whether this fd is non-blocking or not. is_nonblock: Cell, - // Differentiate between different AnonSocket fd types. - fd_type: AnonSocketType, + // Differentiate between different virtual socket fd types. + fd_type: VirtualSocketType, } #[derive(Debug)] @@ -69,17 +69,17 @@ impl Buffer { } } -impl AnonSocket { - fn peer_fd(&self) -> &WeakFileDescriptionRef { +impl VirtualSocket { + fn peer_fd(&self) -> &WeakFileDescriptionRef { self.peer_fd.get().unwrap() } } -impl FileDescription for AnonSocket { +impl FileDescription for VirtualSocket { fn name(&self) -> &'static str { match self.fd_type { - AnonSocketType::Socketpair => "socketpair", - AnonSocketType::PipeRead | AnonSocketType::PipeWrite => "pipe", + VirtualSocketType::Socketpair => "socketpair", + VirtualSocketType::PipeRead | VirtualSocketType::PipeWrite => "pipe", } } @@ -111,7 +111,7 @@ impl FileDescription for AnonSocket { ecx: &mut MiriInterpCx<'tcx>, finish: DynMachineCallback<'tcx, Result>, ) -> InterpResult<'tcx> { - anonsocket_read(self, ptr, len, ecx, finish) + virtual_socket_read(self, ptr, len, ecx, finish) } fn write<'tcx>( @@ -122,7 +122,7 @@ impl FileDescription for AnonSocket { ecx: &mut MiriInterpCx<'tcx>, finish: DynMachineCallback<'tcx, Result>, ) -> InterpResult<'tcx> { - anonsocket_write(self, ptr, len, ecx, finish) + virtual_socket_write(self, ptr, len, ecx, finish) } fn short_fd_operations(&self) -> bool { @@ -147,13 +147,13 @@ impl FileDescription for AnonSocket { // fd is closed, so we need to look at the original type of this socket, not at whether // the peer socket still exists. match self.fd_type { - AnonSocketType::Socketpair => { + VirtualSocketType::Socketpair => { flags |= ecx.eval_libc_i32("O_RDWR"); } - AnonSocketType::PipeRead => { + VirtualSocketType::PipeRead => { flags |= ecx.eval_libc_i32("O_RDONLY"); } - AnonSocketType::PipeWrite => { + VirtualSocketType::PipeWrite => { flags |= ecx.eval_libc_i32("O_WRONLY"); } } @@ -192,9 +192,9 @@ impl FileDescription for AnonSocket { } } -/// Write to AnonSocket based on the space available and return the written byte size. -fn anonsocket_write<'tcx>( - self_ref: FileDescriptionRef, +/// Write to VirtualSocket based on the space available and return the written byte size. +fn virtual_socket_write<'tcx>( + self_ref: FileDescriptionRef, ptr: Pointer, len: usize, ecx: &mut MiriInterpCx<'tcx>, @@ -230,11 +230,11 @@ fn anonsocket_write<'tcx>( // Block the current thread; only keep a weak ref for this. let weak_self_ref = FileDescriptionRef::downgrade(&self_ref); ecx.block_thread( - BlockReason::UnnamedSocket, + BlockReason::VirtualSocket, None, callback!( @capture<'tcx> { - weak_self_ref: WeakFileDescriptionRef, + weak_self_ref: WeakFileDescriptionRef, ptr: Pointer, len: usize, finish: DynMachineCallback<'tcx, Result>, @@ -244,7 +244,7 @@ fn anonsocket_write<'tcx>( // If we got unblocked, then our peer successfully upgraded its weak // ref to us. That means we can also upgrade our weak ref. let self_ref = weak_self_ref.upgrade().unwrap(); - anonsocket_write(self_ref, ptr, len, this, finish) + virtual_socket_write(self_ref, ptr, len, this, finish) } ), ); @@ -268,7 +268,7 @@ fn anonsocket_write<'tcx>( let waiting_threads = std::mem::take(&mut *peer_fd.blocked_read_tid.borrow_mut()); // FIXME: We can randomize the order of unblocking. for thread_id in waiting_threads { - ecx.unblock_thread(thread_id, BlockReason::UnnamedSocket)?; + ecx.unblock_thread(thread_id, BlockReason::VirtualSocket)?; } // Notify epoll waiters: we might be no longer writable, peer might now be readable. // The notification to the peer seems to be always sent on Linux, even if the @@ -281,9 +281,9 @@ fn anonsocket_write<'tcx>( interp_ok(()) } -/// Read from AnonSocket and return the number of bytes read. -fn anonsocket_read<'tcx>( - self_ref: FileDescriptionRef, +/// Read from VirtualSocket and return the number of bytes read. +fn virtual_socket_read<'tcx>( + self_ref: FileDescriptionRef, ptr: Pointer, len: usize, ecx: &mut MiriInterpCx<'tcx>, @@ -318,11 +318,11 @@ fn anonsocket_read<'tcx>( // Block the current thread; only keep a weak ref for this. let weak_self_ref = FileDescriptionRef::downgrade(&self_ref); ecx.block_thread( - BlockReason::UnnamedSocket, + BlockReason::VirtualSocket, None, callback!( @capture<'tcx> { - weak_self_ref: WeakFileDescriptionRef, + weak_self_ref: WeakFileDescriptionRef, ptr: Pointer, len: usize, finish: DynMachineCallback<'tcx, Result>, @@ -332,7 +332,7 @@ fn anonsocket_read<'tcx>( // If we got unblocked, then our peer successfully upgraded its weak // ref to us. That means we can also upgrade our weak ref. let self_ref = weak_self_ref.upgrade().unwrap(); - anonsocket_read(self_ref, ptr, len, this, finish) + virtual_socket_read(self_ref, ptr, len, this, finish) } ), ); @@ -365,7 +365,7 @@ fn anonsocket_read<'tcx>( let waiting_threads = std::mem::take(&mut *peer_fd.blocked_write_tid.borrow_mut()); // FIXME: We can randomize the order of unblocking. for thread_id in waiting_threads { - ecx.unblock_thread(thread_id, BlockReason::UnnamedSocket)?; + ecx.unblock_thread(thread_id, BlockReason::VirtualSocket)?; } // Notify epoll waiters: peer is now writable. // Linux seems to always notify the peer if the read buffer is now empty. @@ -381,7 +381,7 @@ fn anonsocket_read<'tcx>( interp_ok(()) } -impl UnixFileDescription for AnonSocket { +impl UnixFileDescription for VirtualSocket { fn epoll_active_events<'tcx>(&self) -> InterpResult<'tcx, EpollEvents> { // We only check the status of EPOLLIN, EPOLLOUT, EPOLLHUP and EPOLLRDHUP flags. // If other event flags need to be supported in the future, the check should be added here. @@ -489,23 +489,23 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { // Generate file descriptions. let fds = &mut this.machine.fds; - let fd0 = fds.new_ref(AnonSocket { + let fd0 = fds.new_ref(VirtualSocket { readbuf: Some(RefCell::new(Buffer::new())), peer_fd: OnceCell::new(), peer_lost_data: Cell::new(false), blocked_read_tid: RefCell::new(Vec::new()), blocked_write_tid: RefCell::new(Vec::new()), is_nonblock: Cell::new(is_sock_nonblock), - fd_type: AnonSocketType::Socketpair, + fd_type: VirtualSocketType::Socketpair, }); - let fd1 = fds.new_ref(AnonSocket { + let fd1 = fds.new_ref(VirtualSocket { readbuf: Some(RefCell::new(Buffer::new())), peer_fd: OnceCell::new(), peer_lost_data: Cell::new(false), blocked_read_tid: RefCell::new(Vec::new()), blocked_write_tid: RefCell::new(Vec::new()), is_nonblock: Cell::new(is_sock_nonblock), - fd_type: AnonSocketType::Socketpair, + fd_type: VirtualSocketType::Socketpair, }); // Make the file descriptions point to each other. @@ -559,23 +559,23 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { // Generate file descriptions. // pipefd[0] refers to the read end of the pipe. let fds = &mut this.machine.fds; - let fd0 = fds.new_ref(AnonSocket { + let fd0 = fds.new_ref(VirtualSocket { readbuf: Some(RefCell::new(Buffer::new())), peer_fd: OnceCell::new(), peer_lost_data: Cell::new(false), blocked_read_tid: RefCell::new(Vec::new()), blocked_write_tid: RefCell::new(Vec::new()), is_nonblock: Cell::new(is_nonblock), - fd_type: AnonSocketType::PipeRead, + fd_type: VirtualSocketType::PipeRead, }); - let fd1 = fds.new_ref(AnonSocket { + let fd1 = fds.new_ref(VirtualSocket { readbuf: None, peer_fd: OnceCell::new(), peer_lost_data: Cell::new(false), blocked_read_tid: RefCell::new(Vec::new()), blocked_write_tid: RefCell::new(Vec::new()), is_nonblock: Cell::new(is_nonblock), - fd_type: AnonSocketType::PipeWrite, + fd_type: VirtualSocketType::PipeWrite, }); // Make the file descriptions point to each other. From bb80f4e4ba883e2fec2d442f2ee2cc0ed984f8d6 Mon Sep 17 00:00:00 2001 From: bjorn3 <17426603+bjorn3@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:04:02 +0200 Subject: [PATCH 10/24] Use windows-sys 0.61 in tests This version no longer requires using big import libraries and instead uses raw-dylib. --- src/tools/miri/tests/deps/Cargo.lock | 88 +++------------------------- src/tools/miri/tests/deps/Cargo.toml | 2 +- 2 files changed, 8 insertions(+), 82 deletions(-) diff --git a/src/tools/miri/tests/deps/Cargo.lock b/src/tools/miri/tests/deps/Cargo.lock index 2a1468d55ad0d..1c31cc3f6120e 100644 --- a/src/tools/miri/tests/deps/Cargo.lock +++ b/src/tools/miri/tests/deps/Cargo.lock @@ -45,7 +45,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -281,7 +281,7 @@ checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -298,7 +298,7 @@ dependencies = [ "page_size", "tempfile", "tokio", - "windows-sys 0.60.2", + "windows-sys", ] [[package]] @@ -383,7 +383,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -463,7 +463,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -487,7 +487,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -503,7 +503,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -666,15 +666,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -684,71 +675,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/src/tools/miri/tests/deps/Cargo.toml b/src/tools/miri/tests/deps/Cargo.toml index fd301fc5cf321..73b3025212ec5 100644 --- a/src/tools/miri/tests/deps/Cargo.toml +++ b/src/tools/miri/tests/deps/Cargo.toml @@ -26,7 +26,7 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "net", futures = { version = "0.3.0", default-features = false, features = ["alloc", "async-await"] } [target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.60", features = [ +windows-sys = { version = "0.61", features = [ "Win32_Foundation", "Win32_System_Threading", "Win32_Storage_FileSystem", From c77b198e4bef465ad220b794ca4686ff988aceec Mon Sep 17 00:00:00 2001 From: WhySoBad <49595640+WhySoBad@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:37:08 +0200 Subject: [PATCH 11/24] chore: disable short reads/writes for TCP sockets --- src/tools/miri/src/shims/unix/socket.rs | 41 +++++------------ .../miri/tests/pass-dep/libc/libc-socket.rs | 46 ------------------- 2 files changed, 11 insertions(+), 76 deletions(-) diff --git a/src/tools/miri/src/shims/unix/socket.rs b/src/tools/miri/src/shims/unix/socket.rs index 9d7d5a32f127b..c553cd1f70e8b 100644 --- a/src/tools/miri/src/shims/unix/socket.rs +++ b/src/tools/miri/src/shims/unix/socket.rs @@ -7,7 +7,6 @@ use std::{io, iter}; use mio::Interest; use mio::event::Source; use mio::net::{TcpListener, TcpStream}; -use rand::Rng; use rustc_abi::Size; use rustc_const_eval::interpret::{InterpResult, interp_ok}; use rustc_middle::throw_unsup_format; @@ -168,8 +167,13 @@ impl FileDescription for Socket { } fn short_fd_operations(&self) -> bool { - // Short accesses on TCP sockets are realistic and expected to happen. - true + // Linux de-facto guarantees (or at least, applications like tokio assume [1, 2]) that + // when a read/write on a streaming socket comes back short, the kernel buffer is + // empty/full. SO we can't do short reads/writes here. + // + // [1]: https://github.com/tokio-rs/tokio/blob/6c03e03898d71eca976ee1ad8481cf112ae722ba/tokio/src/io/poll_evented.rs#L182 + // [2]: https://github.com/tokio-rs/tokio/blob/6c03e03898d71eca976ee1ad8481cf112ae722ba/tokio/src/io/poll_evented.rs#L240 + false } fn as_unix<'tcx>(&self, _ecx: &MiriInterpCx<'tcx>) -> &dyn UnixFileDescription { @@ -652,18 +656,6 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { return this.set_last_error_and_return(LibcError("ENOTSOCK"), dest); }; - // Non-deterministically decide to further reduce the length, simulating a partial send. - // We avoid reducing the write size to 0: the docs seem to be entirely fine with that, - // but the standard library is not (https://github.com/rust-lang/rust/issues/145959). - let length = if this.machine.short_fd_operations - && length >= 2 - && this.machine.rng.get_mut().random() - { - length / 2 - } else { - length - }; - let mut is_op_non_block = false; // Interpret the flag. Every flag we recognize is "subtracted" from `flags`, so @@ -774,21 +766,6 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { return this.set_last_error_and_return(LibcError("ENOTSOCK"), dest); }; - // Non-deterministically decide to further reduce the length, simulating a partial receive. - // We don't simulate partial receives for lengths < 2 because the man page states that a - // return value of zero can only be returned in some special cases: - // "When a stream socket peer has performed an orderly shutdown, the return value will be 0 - // (the traditional "end-of-file" return). [...] The value 0 may also be returned if the - // requested number of bytes to receive from a stream socket was 0." - let length = if this.machine.short_fd_operations - && length >= 2 - && this.machine.rng.get_mut().random() - { - length / 2 // since `length` is at least 2, the result is still at least 1 - } else { - length - }; - let mut should_peek = false; let mut is_op_non_block = false; @@ -1502,6 +1479,8 @@ trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> { // This is a *non-blocking* write. let result = this.write_to_host(stream, length, buffer_ptr)?; + // FIXME: When the host does a short write, we should emit an epoll edge -- at least for targets for which tokio assumes no short writes: + // match result { Err(IoError::HostError(e)) if e.kind() == io::ErrorKind::NotConnected => { // On Windows hosts, `send` can return WSAENOTCONN where EAGAIN or EWOULDBLOCK @@ -1578,6 +1557,8 @@ trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> { length, buffer_ptr, )?; + // FIXME: When the host does a short read, we should emit an epoll edge -- at least for targets for which tokio assumes no short reads: + // match result { Err(IoError::HostError(e)) if e.kind() == io::ErrorKind::NotConnected => { // On Windows hosts, `recv` can return WSAENOTCONN where EAGAIN or EWOULDBLOCK diff --git a/src/tools/miri/tests/pass-dep/libc/libc-socket.rs b/src/tools/miri/tests/pass-dep/libc/libc-socket.rs index 64c1e8d4c3a6e..c87677a756a00 100644 --- a/src/tools/miri/tests/pass-dep/libc/libc-socket.rs +++ b/src/tools/miri/tests/pass-dep/libc/libc-socket.rs @@ -11,7 +11,6 @@ use std::thread; use std::time::Duration; use libc_utils::*; -use utils::check_nondet; const TEST_BYTES: &[u8] = b"these are some test bytes!"; @@ -36,7 +35,6 @@ fn main() { test_accept_connect(); test_send_peek_recv(); - test_partial_send_recv(); test_write_read(); test_getsockname_ipv4(); @@ -295,50 +293,6 @@ fn test_send_peek_recv() { server_thread.join().unwrap(); } -/// Test that we actually do partial sends and partial receives for sockets. -fn test_partial_send_recv() { - let (server_sockfd, addr) = net::make_listener_ipv4().unwrap(); - let client_sockfd = - unsafe { errno_result(libc::socket(libc::AF_INET, libc::SOCK_STREAM, 0)).unwrap() }; - - // Spawn the server thread. - let server_thread = thread::spawn(move || { - let (peerfd, _) = net::accept_ipv4(server_sockfd).unwrap(); - - // Yield back to client to test that we do incomplete writes. - thread::sleep(Duration::from_millis(10)); - - // We know the buffer contains enough bytes to test incomplete reads. - - // Ensure we sometimes do incomplete reads. - check_nondet(|| { - let mut buffer = [0u8; 4]; - let bytes_read = - unsafe { errno_result(libc::read(peerfd, buffer.as_mut_ptr().cast(), 4)).unwrap() }; - bytes_read == 4 - }); - }); - - net::connect_ipv4(client_sockfd, addr).unwrap(); - - // Ensure we sometimes do incomplete writes. - check_nondet(|| { - let bytes_written = - unsafe { errno_result(libc::write(client_sockfd, [0; 4].as_ptr().cast(), 4)).unwrap() }; - bytes_written == 4 - }); - - let buffer = [0u8; 100_000]; - // Write a lot of bytes into the socket such that we can test - // incomplete reads. - unsafe { - errno_result(libc_utils::write_all(client_sockfd, buffer.as_ptr().cast(), buffer.len())) - .unwrap() - }; - - server_thread.join().unwrap(); -} - /// Test writing bytes into a connected stream and then reading them /// from the other end. /// We want to test this because `write` and `read` should be the same as From d49039c8e2eb19ecdb04f76e57248adf4a043aa1 Mon Sep 17 00:00:00 2001 From: Asuna Date: Thu, 23 Apr 2026 16:39:32 +0000 Subject: [PATCH 12/24] Use `AtomicUsize` instead of `AtomicBool` to test weak atomic Some architectures, such as RISC-V and LoongArch, lack support for native byte-sized atomic operations, so weak operations fallback to non-weak operations and are actually emulated by LL/SC loop, which never fail. --- src/tools/miri/tests/pass/atomic.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tools/miri/tests/pass/atomic.rs b/src/tools/miri/tests/pass/atomic.rs index d8ac5114f27fa..2231affc0c618 100644 --- a/src/tools/miri/tests/pass/atomic.rs +++ b/src/tools/miri/tests/pass/atomic.rs @@ -185,12 +185,12 @@ fn atomic_ptr() { } fn weak_sometimes_fails() { - let atomic = AtomicBool::new(false); + let atomic = AtomicUsize::new(0); let tries = 100; for _ in 0..tries { let cur = atomic.load(Relaxed); - // Try (weakly) to flip the flag. - if atomic.compare_exchange_weak(cur, !cur, Relaxed, Relaxed).is_err() { + // Try (weakly) to modify the flag. + if atomic.compare_exchange_weak(cur, cur + 1, Relaxed, Relaxed).is_err() { // We failed, so return and skip the panic. return; } From e917fdccb435d1f1a517c58a78c415e41634b9a9 Mon Sep 17 00:00:00 2001 From: dianne Date: Thu, 5 Mar 2026 10:26:20 -0800 Subject: [PATCH 13/24] add test --- tests/ui/pin/dont-deref-coerce-pinned-value.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 tests/ui/pin/dont-deref-coerce-pinned-value.rs diff --git a/tests/ui/pin/dont-deref-coerce-pinned-value.rs b/tests/ui/pin/dont-deref-coerce-pinned-value.rs new file mode 100644 index 0000000000000..d902b68ac4925 --- /dev/null +++ b/tests/ui/pin/dont-deref-coerce-pinned-value.rs @@ -0,0 +1,13 @@ +//! Regression test for : when there's a type +//! expectation on `pin!`'s result, make sure we don't deref-coerce the argument to +//! `Pin::new_unchecked` to get its type to match up. That violates the pinning invariant, leading +//! to unsoundness! +//@ check-pass + +use std::pin::{Pin, pin}; + +fn wrong_pin(data: &mut T, callback: impl FnOnce(Pin<&mut T>)) { + callback(pin!(data)); +} + +fn main() {} From c96d970f2ddb4978627bb8ec23b33d64f8933274 Mon Sep 17 00:00:00 2001 From: Rudi Heitbaum Date: Fri, 24 Apr 2026 01:33:16 +0000 Subject: [PATCH 14/24] bump openssl-sys to support OpenSSL 4.0.x The previously pinned version of openssl-sys is not compatible with OpenSSL 4.0.x. - `openssl-sys`: 0.9.111 -> 0.9.114 --- src/tools/miri/Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/miri/Cargo.lock b/src/tools/miri/Cargo.lock index 25005693117ea..3a810a6415d15 100644 --- a/src/tools/miri/Cargo.lock +++ b/src/tools/miri/Cargo.lock @@ -1013,9 +1013,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.111" +version = "0.9.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" dependencies = [ "cc", "libc", From 5276fcd28eaaf3edbaa170990de622f3a9fe0bb1 Mon Sep 17 00:00:00 2001 From: dianne Date: Thu, 5 Mar 2026 10:28:12 -0800 Subject: [PATCH 15/24] prevent deref coercions in `pin!` --- library/core/src/lib.rs | 1 + library/core/src/pin.rs | 21 ++++++++++++-- .../iterators/iter-macro-not-async-closure.rs | 2 ++ .../iter-macro-not-async-closure.stderr | 28 +++++++++++++++++-- .../ui/pin/dont-deref-coerce-pinned-value.rs | 2 +- .../pin/dont-deref-coerce-pinned-value.stderr | 24 ++++++++++++++++ 6 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 tests/ui/pin/dont-deref-coerce-pinned-value.stderr diff --git a/library/core/src/lib.rs b/library/core/src/lib.rs index 2b4ac66212e5b..79694c46f2dcc 100644 --- a/library/core/src/lib.rs +++ b/library/core/src/lib.rs @@ -157,6 +157,7 @@ #![feature(no_core)] #![feature(optimize_attribute)] #![feature(pattern_types)] +#![feature(pin_macro_internals)] #![feature(prelude_import)] #![feature(repr_simd)] #![feature(rustc_attrs)] diff --git a/library/core/src/pin.rs b/library/core/src/pin.rs index b65e40ef46758..f7224df849c8f 100644 --- a/library/core/src/pin.rs +++ b/library/core/src/pin.rs @@ -2021,14 +2021,29 @@ unsafe impl PinCoerceUnsized for Pin {} /// [`Box::pin`]: ../../std/boxed/struct.Box.html#method.pin #[stable(feature = "pin_macro", since = "1.68.0")] #[rustc_macro_transparency = "semiopaque"] -#[allow_internal_unstable(super_let)] +#[allow_internal_unstable(pin_macro_internals, super_let)] #[rustc_diagnostic_item = "pin_macro"] // `super` gets removed by rustfmt #[rustfmt::skip] pub macro pin($value:expr $(,)?) { - { + 'p: { super let mut pinned = $value; // SAFETY: The value is pinned: it is the local above which cannot be named outside this macro. - unsafe { $crate::pin::Pin::new_unchecked(&mut pinned) } + break 'p unsafe { $crate::pin::Pin::new_unchecked(&mut pinned) }; + + // HACK: We need to ensure that, given `$value: T`, `pin!($value)` has type `Pin<&mut T>`. + // Otherwise, it's possible for a type annotation on the result of `pin!` to unsoundly add + // deref coercions. E.g. for `$value: &mut T`, we could get `pin!($value): Pin<&mut T>`, + // violating the pinning invariant; see . + #[expect(unreachable_code)] + $crate::pin::unreachable_pin_macro_type_constraint(pinned) } } + +/// Helper for `pin!` to enforce its type signature. +/// See . +#[unstable(feature = "pin_macro_internals", issue = "none")] +#[doc(hidden)] +pub fn unreachable_pin_macro_type_constraint<'a, T>(_: T) -> Pin<&'a mut T> { + unreachable!() +} diff --git a/tests/ui/iterators/iter-macro-not-async-closure.rs b/tests/ui/iterators/iter-macro-not-async-closure.rs index 634391883ea73..38ea33ccd7732 100644 --- a/tests/ui/iterators/iter-macro-not-async-closure.rs +++ b/tests/ui/iterators/iter-macro-not-async-closure.rs @@ -27,6 +27,8 @@ fn main() { //~^^ ERROR AsyncFnOnce()` is not satisfied //~^^^ ERROR AsyncFnOnce()` is not satisfied //~^^^^ ERROR AsyncFnOnce()` is not satisfied + //~^^^^^ ERROR AsyncFnOnce()` is not satisfied + //~^^^^^^ ERROR AsyncFnOnce()` is not satisfied x.poll(&mut Context::from_waker(Waker::noop())); //~^ ERROR AsyncFnOnce()` is not satisfied } diff --git a/tests/ui/iterators/iter-macro-not-async-closure.stderr b/tests/ui/iterators/iter-macro-not-async-closure.stderr index 906ebd482fb6f..735003207793a 100644 --- a/tests/ui/iterators/iter-macro-not-async-closure.stderr +++ b/tests/ui/iterators/iter-macro-not-async-closure.stderr @@ -49,7 +49,31 @@ LL | async fn call_async_once(f: impl AsyncFnOnce()) { | ^^^^^^^^^^^^^ required by this bound in `call_async_once` error[E0277]: the trait bound `{gen closure@$DIR/iter-macro-not-async-closure.rs:19:21: 19:28}: AsyncFnOnce()` is not satisfied - --> $DIR/iter-macro-not-async-closure.rs:30:5 + --> $DIR/iter-macro-not-async-closure.rs:25:13 + | +LL | let x = pin!(call_async_once(f)); + | ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `AsyncFnOnce()` is not implemented for `{gen closure@$DIR/iter-macro-not-async-closure.rs:19:21: 19:28}` + | +note: required by a bound in `call_async_once` + --> $DIR/iter-macro-not-async-closure.rs:14:34 + | +LL | async fn call_async_once(f: impl AsyncFnOnce()) { + | ^^^^^^^^^^^^^ required by this bound in `call_async_once` + +error[E0277]: the trait bound `{gen closure@$DIR/iter-macro-not-async-closure.rs:19:21: 19:28}: AsyncFnOnce()` is not satisfied + --> $DIR/iter-macro-not-async-closure.rs:25:13 + | +LL | let x = pin!(call_async_once(f)); + | ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `AsyncFnOnce()` is not implemented for `{gen closure@$DIR/iter-macro-not-async-closure.rs:19:21: 19:28}` + | +note: required by a bound in `call_async_once` + --> $DIR/iter-macro-not-async-closure.rs:14:34 + | +LL | async fn call_async_once(f: impl AsyncFnOnce()) { + | ^^^^^^^^^^^^^ required by this bound in `call_async_once` + +error[E0277]: the trait bound `{gen closure@$DIR/iter-macro-not-async-closure.rs:19:21: 19:28}: AsyncFnOnce()` is not satisfied + --> $DIR/iter-macro-not-async-closure.rs:32:5 | LL | x.poll(&mut Context::from_waker(Waker::noop())); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `AsyncFnOnce()` is not implemented for `{gen closure@$DIR/iter-macro-not-async-closure.rs:19:21: 19:28}` @@ -60,6 +84,6 @@ note: required by a bound in `call_async_once` LL | async fn call_async_once(f: impl AsyncFnOnce()) { | ^^^^^^^^^^^^^ required by this bound in `call_async_once` -error: aborting due to 5 previous errors +error: aborting due to 7 previous errors For more information about this error, try `rustc --explain E0277`. diff --git a/tests/ui/pin/dont-deref-coerce-pinned-value.rs b/tests/ui/pin/dont-deref-coerce-pinned-value.rs index d902b68ac4925..02730b54f407a 100644 --- a/tests/ui/pin/dont-deref-coerce-pinned-value.rs +++ b/tests/ui/pin/dont-deref-coerce-pinned-value.rs @@ -2,12 +2,12 @@ //! expectation on `pin!`'s result, make sure we don't deref-coerce the argument to //! `Pin::new_unchecked` to get its type to match up. That violates the pinning invariant, leading //! to unsoundness! -//@ check-pass use std::pin::{Pin, pin}; fn wrong_pin(data: &mut T, callback: impl FnOnce(Pin<&mut T>)) { callback(pin!(data)); + //~^ ERROR: mismatched types } fn main() {} diff --git a/tests/ui/pin/dont-deref-coerce-pinned-value.stderr b/tests/ui/pin/dont-deref-coerce-pinned-value.stderr new file mode 100644 index 0000000000000..1d5afaa3ad717 --- /dev/null +++ b/tests/ui/pin/dont-deref-coerce-pinned-value.stderr @@ -0,0 +1,24 @@ +error[E0308]: mismatched types + --> $DIR/dont-deref-coerce-pinned-value.rs:9:14 + | +LL | fn wrong_pin(data: &mut T, callback: impl FnOnce(Pin<&mut T>)) { + | - expected this type parameter +LL | callback(pin!(data)); + | ^^^^^^^^^^ + | | + | expected type parameter `T`, found `&mut T` + | arguments to this function are incorrect + | + = note: expected type parameter `_` + found mutable reference `&mut _` +help: the return type of this call is `&mut T` due to the type of the argument passed + --> $DIR/dont-deref-coerce-pinned-value.rs:9:14 + | +LL | callback(pin!(data)); + | ^^^^^^^^^^ this argument influences the return type of `unreachable_pin_macro_type_constraint` +note: function defined here + --> $SRC_DIR/core/src/pin.rs:LL:COL + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0308`. From e509d19dc37e43a4f9e71a03685c6f3d2f878ac4 Mon Sep 17 00:00:00 2001 From: The Miri Cronjob Bot Date: Fri, 24 Apr 2026 05:43:10 +0000 Subject: [PATCH 16/24] Prepare for merging from rust-lang/rust This updates the rust-version file to 9836b06b55f5389f605ee7766eeecd9f17a86cb5. --- src/tools/miri/rust-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/miri/rust-version b/src/tools/miri/rust-version index e9fc6c4cd023e..5ab8e6c9d9ba7 100644 --- a/src/tools/miri/rust-version +++ b/src/tools/miri/rust-version @@ -1 +1 @@ -e22c616e4e87914135c1db261a03e0437255335e +9836b06b55f5389f605ee7766eeecd9f17a86cb5 From 058a8b9fa1a9a79422787cb3b1cab101dc0f4e14 Mon Sep 17 00:00:00 2001 From: enthropy7 <221884178+enthropy7@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:40:13 +0300 Subject: [PATCH 17/24] Support fstat on non-file-backed FDs --- src/tools/miri/src/shims/files.rs | 28 ++++- src/tools/miri/src/shims/unix/fs.rs | 82 +++++++++---- .../miri/src/shims/unix/linux_like/epoll.rs | 10 +- .../miri/src/shims/unix/linux_like/eventfd.rs | 10 +- src/tools/miri/src/shims/unix/mod.rs | 2 +- .../miri/src/shims/unix/virtual_socket.rs | 13 ++- src/tools/miri/src/shims/windows/fs.rs | 6 +- .../pass-dep/libc/libc-fstat-non-file.rs | 108 ++++++++++++++++++ 8 files changed, 226 insertions(+), 33 deletions(-) create mode 100644 src/tools/miri/tests/pass-dep/libc/libc-fstat-non-file.rs diff --git a/src/tools/miri/src/shims/files.rs b/src/tools/miri/src/shims/files.rs index 5468fd3037425..d007853ead2fe 100644 --- a/src/tools/miri/src/shims/files.rs +++ b/src/tools/miri/src/shims/files.rs @@ -1,6 +1,6 @@ use std::any::Any; use std::collections::BTreeMap; -use std::fs::{File, Metadata}; +use std::fs::File; use std::io::{ErrorKind, IsTerminal, Read, Seek, SeekFrom, Write}; use std::marker::CoercePointee; use std::ops::Deref; @@ -9,7 +9,7 @@ use std::{fs, io}; use rustc_abi::Size; -use crate::shims::unix::UnixFileDescription; +use crate::shims::unix::{FileMetadata, UnixFileDescription}; use crate::*; /// A unique id for file descriptions. While we could use the address, considering that @@ -209,10 +209,23 @@ pub trait FileDescription: std::fmt::Debug + FileDescriptionExt { throw_unsup_format!("cannot close {}", self.name()); } - fn metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result> { + /// Returns the host `fs::Metadata` for this FD, if available. + /// Used by host-aware shims like Windows's `GetFileInformationByHandle`. + /// Unrelated to Unix `fstat`, which goes through `fstat()`. + fn host_metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result> { throw_unsup_format!("obtaining metadata is only supported on file-backed file descriptors"); } + /// Return the metadata describing this FD for the `fstat`/`statx` family of syscalls. + /// File-backed FDs should call `FileMetadata::from_meta` with their host metadata. + /// Non-file-backed FDs should call `FileMetadata::synthetic` with an appropriate mode. + fn fstat<'tcx>( + &self, + _ecx: &mut MiriInterpCx<'tcx>, + ) -> InterpResult<'tcx, Result> { + throw_unsup_format!("fstat is not supported on {}", self.name()); + } + fn is_tty(&self, _communicate_allowed: bool) -> bool { // Most FDs are not tty's and the consequence of a wrong `false` are minor, // so we use a default impl here. @@ -432,10 +445,17 @@ impl FileDescription for FileHandle { } } - fn metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result> { + fn host_metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result> { interp_ok(self.file.metadata()) } + fn fstat<'tcx>( + &self, + ecx: &mut MiriInterpCx<'tcx>, + ) -> InterpResult<'tcx, Result> { + FileMetadata::from_meta(ecx, self.file.metadata()) + } + fn is_tty(&self, communicate_allowed: bool) -> bool { communicate_allowed && self.file.is_terminal() } diff --git a/src/tools/miri/src/shims/unix/fs.rs b/src/tools/miri/src/shims/unix/fs.rs index 5adc5932883ef..d318d3cecdb5e 100644 --- a/src/tools/miri/src/shims/unix/fs.rs +++ b/src/tools/miri/src/shims/unix/fs.rs @@ -229,17 +229,22 @@ trait EvalContextExtPrivate<'tcx>: crate::MiriInterpCxExt<'tcx> { let (access_sec, access_nsec) = metadata.accessed.unwrap_or((0, 0)); let (created_sec, created_nsec) = metadata.created.unwrap_or((0, 0)); let (modified_sec, modified_nsec) = metadata.modified.unwrap_or((0, 0)); - let mode = metadata.mode.to_uint(this.libc_ty_layout("mode_t").size)?; // We do *not* use `deref_pointer_as` here since determining the right pointee type // is highly non-trivial: it depends on which exact alias of the function was invoked // (e.g. `fstat` vs `fstat64`), and then on FreeBSD it also depends on the ABI level // which can be different between the libc used by std and the libc used by everyone else. let buf = this.deref_pointer(buf_op)?; + + // `libc::S_IF*` constants are of type `mode_t`, which varies in width across targets + // (`u16` on macOS, `u32` on Linux). Read the scalar using `mode_t`'s size on the target. + let mode_t_size = this.libc_ty_layout("mode_t").size; + let mode: u32 = metadata.mode.to_uint(mode_t_size)?.try_into().unwrap(); + this.write_int_fields_named( &[ ("st_dev", metadata.dev.into()), - ("st_mode", mode.try_into().unwrap()), + ("st_mode", mode.into()), ("st_nlink", 0), ("st_ino", 0), ("st_uid", metadata.uid.into()), @@ -343,6 +348,34 @@ trait EvalContextExtPrivate<'tcx>: crate::MiriInterpCxExt<'tcx> { } } +fn file_type_to_mode_name(file_type: std::fs::FileType) -> &'static str { + #[cfg(unix)] + use std::os::unix::fs::FileTypeExt; + + if file_type.is_file() { + "S_IFREG" + } else if file_type.is_dir() { + "S_IFDIR" + } else if file_type.is_symlink() { + "S_IFLNK" + } else { + // Certain file types are only available when the host is a Unix system. + #[cfg(unix)] + { + if file_type.is_socket() { + return "S_IFSOCK"; + } else if file_type.is_fifo() { + return "S_IFIFO"; + } else if file_type.is_char_device() { + return "S_IFCHR"; + } else if file_type.is_block_device() { + return "S_IFBLK"; + } + } + "S_IFREG" + } +} + impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {} pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { fn open( @@ -747,13 +780,12 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { Err(err) => return this.set_last_error_and_return_i32(err), }; - // The `mode` field specifies the type of the file and the permissions over the file for - // the owner, its group and other users. Given that we can only provide the file type - // without using platform specific methods, we only set the bits corresponding to the file - // type. This should be an `__u16` but `libc` provides its values as `u32`. + // `statx.stx_mode` is `__u16`. `libc::S_IF*` are of type `mode_t`, which varies in + // width across targets (`u16` on macOS, `u32` on Linux). Read using `mode_t`'s size. + let mode_t_size = this.libc_ty_layout("mode_t").size; let mode: u16 = metadata .mode - .to_u32()? + .to_uint(mode_t_size)? .try_into() .unwrap_or_else(|_| bug!("libc contains bad value for constant")); @@ -1632,7 +1664,7 @@ fn extract_sec_and_nsec<'tcx>( /// Stores a file's metadata in order to avoid code duplication in the different metadata related /// shims. -struct FileMetadata { +pub struct FileMetadata { mode: Scalar, size: u64, created: Option<(u64, u32)>, @@ -1662,13 +1694,28 @@ impl FileMetadata { let Some(fd) = ecx.machine.fds.get(fd_num) else { return interp_ok(Err(LibcError("EBADF"))); }; + fd.fstat(ecx) + } - let metadata = fd.metadata()?; - drop(fd); - FileMetadata::from_meta(ecx, metadata) + pub(crate) fn synthetic<'tcx>( + ecx: &mut MiriInterpCx<'tcx>, + mode_name: &str, + size: u64, + ) -> InterpResult<'tcx, Result> { + let mode = ecx.eval_libc(mode_name); + interp_ok(Ok(FileMetadata { + mode, + size, + created: None, + accessed: None, + modified: None, + dev: 0, + uid: 0, + gid: 0, + })) } - fn from_meta<'tcx>( + pub(crate) fn from_meta<'tcx>( ecx: &mut MiriInterpCx<'tcx>, metadata: Result, ) -> InterpResult<'tcx, Result> { @@ -1680,16 +1727,7 @@ impl FileMetadata { }; let file_type = metadata.file_type(); - - let mode_name = if file_type.is_file() { - "S_IFREG" - } else if file_type.is_dir() { - "S_IFDIR" - } else { - "S_IFLNK" - }; - - let mode = ecx.eval_libc(mode_name); + let mode = ecx.eval_libc(file_type_to_mode_name(file_type)); let size = metadata.len(); diff --git a/src/tools/miri/src/shims/unix/linux_like/epoll.rs b/src/tools/miri/src/shims/unix/linux_like/epoll.rs index 7480db00d6ed3..48085fa6ae26a 100644 --- a/src/tools/miri/src/shims/unix/linux_like/epoll.rs +++ b/src/tools/miri/src/shims/unix/linux_like/epoll.rs @@ -10,7 +10,7 @@ use crate::concurrency::VClock; use crate::shims::files::{ DynFileDescriptionRef, FdId, FdNum, FileDescription, FileDescriptionRef, WeakFileDescriptionRef, }; -use crate::shims::unix::UnixFileDescription; +use crate::shims::unix::{FileMetadata, UnixFileDescription}; use crate::*; type EpollEventKey = (FdId, FdNum); @@ -119,6 +119,14 @@ impl FileDescription for Epoll { "epoll" } + fn fstat<'tcx>( + &self, + ecx: &mut MiriInterpCx<'tcx>, + ) -> InterpResult<'tcx, Result> { + // On Linux, epoll is an "anonymous inode" reported as S_IFREG. + FileMetadata::synthetic(ecx, "S_IFREG", 0) + } + fn destroy<'tcx>( mut self, self_id: FdId, diff --git a/src/tools/miri/src/shims/unix/linux_like/eventfd.rs b/src/tools/miri/src/shims/unix/linux_like/eventfd.rs index d374a1e75f72e..03bac1e7270da 100644 --- a/src/tools/miri/src/shims/unix/linux_like/eventfd.rs +++ b/src/tools/miri/src/shims/unix/linux_like/eventfd.rs @@ -5,8 +5,8 @@ use std::io::ErrorKind; use crate::concurrency::VClock; use crate::shims::files::{FdId, FileDescription, FileDescriptionRef, WeakFileDescriptionRef}; -use crate::shims::unix::UnixFileDescription; use crate::shims::unix::linux_like::epoll::{EpollEvents, EvalContextExt as _}; +use crate::shims::unix::{FileMetadata, UnixFileDescription}; use crate::*; /// Maximum value that the eventfd counter can hold. @@ -37,6 +37,14 @@ impl FileDescription for EventFd { "event" } + fn fstat<'tcx>( + &self, + ecx: &mut MiriInterpCx<'tcx>, + ) -> InterpResult<'tcx, Result> { + // On Linux, eventfd is an "anonymous inode" reported as S_IFREG. + FileMetadata::synthetic(ecx, "S_IFREG", 0) + } + fn destroy<'tcx>( self, _self_id: FdId, diff --git a/src/tools/miri/src/shims/unix/mod.rs b/src/tools/miri/src/shims/unix/mod.rs index c55a28bfa7b2a..9e8fa2a12d494 100644 --- a/src/tools/miri/src/shims/unix/mod.rs +++ b/src/tools/miri/src/shims/unix/mod.rs @@ -19,7 +19,7 @@ mod solarish; // All the Unix-specific extension traits pub use self::env::{EvalContextExt as _, UnixEnvVars}; pub use self::fd::{EvalContextExt as _, UnixFileDescription}; -pub use self::fs::{DirTable, EvalContextExt as _}; +pub use self::fs::{DirTable, EvalContextExt as _, FileMetadata}; pub use self::linux_like::epoll::EpollInterestTable; pub use self::mem::EvalContextExt as _; pub use self::socket::EvalContextExt as _; diff --git a/src/tools/miri/src/shims/unix/virtual_socket.rs b/src/tools/miri/src/shims/unix/virtual_socket.rs index 51092b80c1c68..16eba61c56ba8 100644 --- a/src/tools/miri/src/shims/unix/virtual_socket.rs +++ b/src/tools/miri/src/shims/unix/virtual_socket.rs @@ -12,8 +12,8 @@ use crate::concurrency::VClock; use crate::shims::files::{ EvalContextExt as _, FdId, FileDescription, FileDescriptionRef, WeakFileDescriptionRef, }; -use crate::shims::unix::UnixFileDescription; use crate::shims::unix::linux_like::epoll::{EpollEvents, EvalContextExt as _}; +use crate::shims::unix::{FileMetadata, UnixFileDescription}; use crate::*; /// The maximum capacity of the socketpair buffer in bytes. @@ -83,6 +83,17 @@ impl FileDescription for VirtualSocket { } } + fn fstat<'tcx>( + &self, + ecx: &mut MiriInterpCx<'tcx>, + ) -> InterpResult<'tcx, Result> { + let mode_name = match self.fd_type { + VirtualSocketType::Socketpair => "S_IFSOCK", + VirtualSocketType::PipeRead | VirtualSocketType::PipeWrite => "S_IFIFO", + }; + FileMetadata::synthetic(ecx, mode_name, 0) + } + fn destroy<'tcx>( self, _self_id: FdId, diff --git a/src/tools/miri/src/shims/windows/fs.rs b/src/tools/miri/src/shims/windows/fs.rs index 1ee93cf911c5a..ddbb2b1de018a 100644 --- a/src/tools/miri/src/shims/windows/fs.rs +++ b/src/tools/miri/src/shims/windows/fs.rs @@ -22,7 +22,7 @@ impl FileDescription for DirHandle { "directory" } - fn metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result> { + fn host_metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result> { interp_ok(self.path.metadata()) } @@ -49,7 +49,7 @@ impl FileDescription for MetadataHandle { "metadata-only" } - fn metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result> { + fn host_metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result> { interp_ok(Ok(self.meta.clone())) } @@ -328,7 +328,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { this.invalid_handle("GetFileInformationByHandle")? }; - let metadata = match desc.metadata()? { + let metadata = match desc.host_metadata()? { Ok(meta) => meta, Err(e) => { this.set_last_error(e)?; diff --git a/src/tools/miri/tests/pass-dep/libc/libc-fstat-non-file.rs b/src/tools/miri/tests/pass-dep/libc/libc-fstat-non-file.rs new file mode 100644 index 0000000000000..c29c8ceaa1dfc --- /dev/null +++ b/src/tools/miri/tests/pass-dep/libc/libc-fstat-non-file.rs @@ -0,0 +1,108 @@ +//@ignore-target: windows # No libc fstat on non-file FDs on Windows +//@compile-flags: -Zmiri-disable-isolation + +use std::mem::MaybeUninit; + +#[path = "../../utils/libc.rs"] +mod libc_utils; +use libc_utils::errno_check; + +fn main() { + test_fstat_socketpair(); + test_fstat_pipe(); + #[cfg(target_os = "linux")] + test_fstat_eventfd(); + #[cfg(target_os = "linux")] + test_fstat_epoll(); +} + +/// Calls fstat and returns a reference to the result. +/// We use `assume_init_ref` rather than `assume_init` because not all fields +/// of `libc::stat` may be written by fstat (e.g. `st_lspare` on macOS). +fn do_fstat(fd: i32, buf: &mut MaybeUninit) -> &libc::stat { + let res = unsafe { libc::fstat(fd, buf.as_mut_ptr()) }; + assert_eq!(res, 0, "fstat failed on fd {}", fd); + unsafe { buf.assume_init_ref() } +} + +fn assert_stat_fields_are_accessible(stat: &libc::stat) { + let _st_nlink = stat.st_nlink; + let _st_blksize = stat.st_blksize; + let _st_blocks = stat.st_blocks; + let _st_ino = stat.st_ino; + let _st_dev = stat.st_dev; + let _st_uid = stat.st_uid; + let _st_gid = stat.st_gid; + let _st_rdev = stat.st_rdev; + let _st_atime = stat.st_atime; + let _st_mtime = stat.st_mtime; + let _st_ctime = stat.st_ctime; + let _st_atime_nsec = stat.st_atime_nsec; + let _st_mtime_nsec = stat.st_mtime_nsec; + let _st_ctime_nsec = stat.st_ctime_nsec; +} + +/// Test fstat on socketpair file descriptors. +fn test_fstat_socketpair() { + let mut fds = [0i32; 2]; + errno_check(unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()) }); + + for fd in fds.iter() { + let mut buf = MaybeUninit::uninit(); + let stat = do_fstat(*fd, &mut buf); + assert_eq!( + stat.st_mode & libc::S_IFMT, + libc::S_IFSOCK, + "socketpair should have S_IFSOCK mode" + ); + assert_eq!(stat.st_size, 0, "socketpair should have size 0"); + assert_stat_fields_are_accessible(stat); + } + + errno_check(unsafe { libc::close(fds[0]) }); + errno_check(unsafe { libc::close(fds[1]) }); +} + +/// Test fstat on pipe file descriptors. +fn test_fstat_pipe() { + let mut fds = [0i32; 2]; + errno_check(unsafe { libc::pipe(fds.as_mut_ptr()) }); + + for fd in fds.iter() { + let mut buf = MaybeUninit::uninit(); + let stat = do_fstat(*fd, &mut buf); + assert_eq!(stat.st_mode & libc::S_IFMT, libc::S_IFIFO, "pipe should have S_IFIFO mode"); + assert_eq!(stat.st_size, 0, "pipe should have size 0"); + assert_stat_fields_are_accessible(stat); + } + + errno_check(unsafe { libc::close(fds[0]) }); + errno_check(unsafe { libc::close(fds[1]) }); +} + +/// Test fstat on eventfd file descriptors (Linux only). +#[cfg(target_os = "linux")] +fn test_fstat_eventfd() { + let flags = libc::EFD_CLOEXEC | libc::EFD_NONBLOCK; + let fd = libc_utils::errno_result(unsafe { libc::eventfd(0, flags) }).unwrap(); + + let mut buf = MaybeUninit::uninit(); + let stat = do_fstat(fd, &mut buf); + assert_eq!(stat.st_size, 0, "eventfd should have size 0"); + assert_stat_fields_are_accessible(stat); + + errno_check(unsafe { libc::close(fd) }); +} + +/// Test fstat on epoll file descriptors (Linux only). +#[cfg(target_os = "linux")] +fn test_fstat_epoll() { + let fd = libc_utils::errno_result(unsafe { libc::epoll_create1(libc::EPOLL_CLOEXEC) }).unwrap(); + + let mut buf = MaybeUninit::uninit(); + let stat = do_fstat(fd, &mut buf); + assert_eq!(stat.st_size, 0, "epoll should have size 0"); + assert_stat_fields_are_accessible(stat); + + errno_check(unsafe { libc::close(fd) }); +} From efaf46022404ebecd9878afbc21f84ddf1dae1c4 Mon Sep 17 00:00:00 2001 From: Cheeshian Chuah Date: Sun, 26 Apr 2026 11:05:05 +0800 Subject: [PATCH 18/24] Mention DEPRECATED_LLVM_INTRINSIC lint for internal use --- compiler/rustc_lint_defs/src/builtin.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/compiler/rustc_lint_defs/src/builtin.rs b/compiler/rustc_lint_defs/src/builtin.rs index b027872dd99cc..68a220a7224d4 100644 --- a/compiler/rustc_lint_defs/src/builtin.rs +++ b/compiler/rustc_lint_defs/src/builtin.rs @@ -5639,7 +5639,12 @@ declare_lint! { /// LLVM periodically updates its list of intrinsics. Deprecated intrinsics are unlikely /// to be removed, but they may optimize less well than their new versions, so it's /// best to use the new version. Also, some deprecated intrinsics might have buggy - /// behavior + /// behavior. + /// + /// This `link_llvm_intrinsics` lint is intended to be used internally only, and requires the + /// `#![feature(link_llvm_intrinsics)]` internal feature gate. For more information, see [its chapter in + /// the Unstable Book](https://doc.rust-lang.org/unstable-book/language-features/link-llvm-intrinsics.html) + /// and [its tracking issue](https://github.com/rust-lang/rust/issues/29602). pub DEPRECATED_LLVM_INTRINSIC, Allow, "detects uses of deprecated LLVM intrinsics", From 2c16f9edf5633bc9dc791b6faf65402aba277f4b Mon Sep 17 00:00:00 2001 From: cclfmht Date: Sun, 26 Apr 2026 10:50:05 +0000 Subject: [PATCH 19/24] Suggest enclosing format string with `""` under special cases * Suggest enclosing format string under special cases This commit add suggestions about enclosing format string when it falls into the following cases: `{}`, `{:?}`, `{:#?}`. * Add HELP annotations in the UI test --- compiler/rustc_builtin_macros/src/format.rs | 26 ++++++- ...y-block-unit-tuple-suggestion-130170.fixed | 4 +- ...-block-unit-tuple-suggestion-130170.stderr | 10 +++ .../macros/suggest-enclosing-format-string.rs | 27 +++++++ .../suggest-enclosing-format-string.stderr | 72 +++++++++++++++++++ 5 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 tests/ui/macros/suggest-enclosing-format-string.rs create mode 100644 tests/ui/macros/suggest-enclosing-format-string.stderr diff --git a/compiler/rustc_builtin_macros/src/format.rs b/compiler/rustc_builtin_macros/src/format.rs index 7d01868645a09..8e055c855c4f1 100644 --- a/compiler/rustc_builtin_macros/src/format.rs +++ b/compiler/rustc_builtin_macros/src/format.rs @@ -173,6 +173,16 @@ fn make_format_args( style: fmt_style, uncooked_symbol: uncooked_fmt_str, } = { + // Extract snippet so that we can check cases `{}`, `{:?}` and `{:#?}` and emit help for + // them later. + let snippet = if let ExprKind::Block(b, None) = &efmt.kind + && b.stmts.len() <= 1 + { + Some(ecx.sess.source_map().span_to_snippet(unexpanded_fmt_span)) + } else { + None + }; + let ExpandResult::Ready(mac) = expr_to_spanned_string(ecx, efmt.clone(), msg) else { return ExpandResult::Retry(()); }; @@ -222,12 +232,26 @@ fn make_format_args( }); } sugg_fmt = sugg_fmt.trim_end().to_string(); - err.span_suggestion( + err.span_suggestion_verbose( unexpanded_fmt_span.shrink_to_lo(), "you might be missing a string literal to format with", format!("\"{sugg_fmt}\", "), Applicability::MaybeIncorrect, ); + + if let Some(Ok(snippet)) = snippet.as_ref() { + match snippet.as_str() { + "{}" | "{:?}" | "{:#?}" => { + err.span_suggestion_verbose( + unexpanded_fmt_span, + format!("you might want to enclose `{snippet}` with `\"\"`"), + format!("\"{snippet}\""), + Applicability::MaybeIncorrect, + ); + } + _ => {} + }; + } } } err.emit() diff --git a/tests/ui/macros/format-empty-block-unit-tuple-suggestion-130170.fixed b/tests/ui/macros/format-empty-block-unit-tuple-suggestion-130170.fixed index 1ca5125fe8bc9..e7b443470b6ca 100644 --- a/tests/ui/macros/format-empty-block-unit-tuple-suggestion-130170.fixed +++ b/tests/ui/macros/format-empty-block-unit-tuple-suggestion-130170.fixed @@ -2,9 +2,9 @@ fn main() { let s = "123"; - println!("{:?} {} {}", {}, "sss", s); + println!("{:?} {} {}", "{}", "sss", s); //~^ ERROR format argument must be a string literal - println!("{:?}", {}); + println!("{:?}", "{}"); //~^ ERROR format argument must be a string literal println!("{} {} {} {:?}", s, "sss", s, {}); //~^ ERROR format argument must be a string literal diff --git a/tests/ui/macros/format-empty-block-unit-tuple-suggestion-130170.stderr b/tests/ui/macros/format-empty-block-unit-tuple-suggestion-130170.stderr index 81fca8c03cc1f..8a64db9218f39 100644 --- a/tests/ui/macros/format-empty-block-unit-tuple-suggestion-130170.stderr +++ b/tests/ui/macros/format-empty-block-unit-tuple-suggestion-130170.stderr @@ -8,6 +8,11 @@ help: you might be missing a string literal to format with | LL | println!("{:?} {} {}", {}, "sss", s); | +++++++++++++ +help: you might want to enclose `{}` with `""` + | +LL - println!({}, "sss", s); +LL + println!("{}", "sss", s); + | error: format argument must be a string literal --> $DIR/format-empty-block-unit-tuple-suggestion-130170.rs:7:14 @@ -19,6 +24,11 @@ help: you might be missing a string literal to format with | LL | println!("{:?}", {}); | +++++++ +help: you might want to enclose `{}` with `""` + | +LL - println!({}); +LL + println!("{}"); + | error: format argument must be a string literal --> $DIR/format-empty-block-unit-tuple-suggestion-130170.rs:9:14 diff --git a/tests/ui/macros/suggest-enclosing-format-string.rs b/tests/ui/macros/suggest-enclosing-format-string.rs new file mode 100644 index 0000000000000..ab1c6d2a1a19c --- /dev/null +++ b/tests/ui/macros/suggest-enclosing-format-string.rs @@ -0,0 +1,27 @@ +// Suggest enclosing the format string with `""` when it is one of `{}`, `{:?}`, and `{:#?}`. + +#[derive(Debug)] +enum UwU { + QwQ, + AwA, + QAQ, +} + +fn main() { + println!({}, UwU::QwQ); + //~^ ERROR format argument must be a string literal + //~| HELP you might be missing a string literal to format with + //~| HELP you might want to enclose `{}` with `""` + println!({:?}, UwU::QwQ); + //~^ ERROR expected expression, found `:` + //~| ERROR format argument must be a string literal + //~| HELP you might be missing a string literal to format with + //~| HELP maybe write a path separator here + //~| HELP you might want to enclose `{:?}` with `""` + println!({:#?}, UwU::QwQ); + //~^ ERROR expected expression, found `:` + //~| ERROR format argument must be a string literal + //~| HELP you might be missing a string literal to format with + //~| HELP maybe write a path separator here + //~| HELP you might want to enclose `{:#?}` with `""` +} diff --git a/tests/ui/macros/suggest-enclosing-format-string.stderr b/tests/ui/macros/suggest-enclosing-format-string.stderr new file mode 100644 index 0000000000000..64f46d39af343 --- /dev/null +++ b/tests/ui/macros/suggest-enclosing-format-string.stderr @@ -0,0 +1,72 @@ +error: format argument must be a string literal + --> $DIR/suggest-enclosing-format-string.rs:11:14 + | +LL | println!({}, UwU::QwQ); + | ^^ + | +help: you might be missing a string literal to format with + | +LL | println!("{:?} {}", {}, UwU::QwQ); + | ++++++++++ +help: you might want to enclose `{}` with `""` + | +LL - println!({}, UwU::QwQ); +LL + println!("{}", UwU::QwQ); + | + +error: expected expression, found `:` + --> $DIR/suggest-enclosing-format-string.rs:15:15 + | +LL | println!({:?}, UwU::QwQ); + | ^ expected expression + | +help: maybe write a path separator here + | +LL | println!({::?}, UwU::QwQ); + | + + +error: format argument must be a string literal + --> $DIR/suggest-enclosing-format-string.rs:15:14 + | +LL | println!({:?}, UwU::QwQ); + | ^^^^ + | +help: you might be missing a string literal to format with + | +LL | println!("{} {}", {:?}, UwU::QwQ); + | ++++++++ +help: you might want to enclose `{:?}` with `""` + | +LL - println!({:?}, UwU::QwQ); +LL + println!("{:?}", UwU::QwQ); + | + +error: expected expression, found `:` + --> $DIR/suggest-enclosing-format-string.rs:21:15 + | +LL | println!({:#?}, UwU::QwQ); + | ^ expected expression + | +help: maybe write a path separator here + | +LL | println!({::#?}, UwU::QwQ); + | + + +error: format argument must be a string literal + --> $DIR/suggest-enclosing-format-string.rs:21:14 + | +LL | println!({:#?}, UwU::QwQ); + | ^^^^^ + | +help: you might be missing a string literal to format with + | +LL | println!("{} {}", {:#?}, UwU::QwQ); + | ++++++++ +help: you might want to enclose `{:#?}` with `""` + | +LL - println!({:#?}, UwU::QwQ); +LL + println!("{:#?}", UwU::QwQ); + | + +error: aborting due to 5 previous errors + From c91a363db10281bf55e0e4a0dd88e5b04f924fb7 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Sun, 26 Apr 2026 12:56:45 +0200 Subject: [PATCH 20/24] merge fstat and metadata functions --- src/tools/miri/src/lib.rs | 5 +- src/tools/miri/src/shims/files.rs | 32 +++------ src/tools/miri/src/shims/unix/fs.rs | 70 ++++++++++--------- .../miri/src/shims/unix/linux_like/epoll.rs | 9 ++- .../miri/src/shims/unix/linux_like/eventfd.rs | 9 ++- src/tools/miri/src/shims/unix/mod.rs | 2 +- .../miri/src/shims/unix/virtual_socket.rs | 9 ++- src/tools/miri/src/shims/windows/fs.rs | 22 ++++-- 8 files changed, 75 insertions(+), 83 deletions(-) diff --git a/src/tools/miri/src/lib.rs b/src/tools/miri/src/lib.rs index f41e3c20a7d54..5f3bf4a9e9e3e 100644 --- a/src/tools/miri/src/lib.rs +++ b/src/tools/miri/src/lib.rs @@ -105,8 +105,9 @@ pub use rustc_const_eval::interpret::*; // Resolve ambiguity. #[doc(no_inline)] pub use rustc_const_eval::interpret::{self, AllocMap, Provenance as _}; -use rustc_log::tracing::{self, info, trace}; -use rustc_middle::{bug, span_bug}; +pub use rustc_data_structures::either::Either; +pub use rustc_log::tracing::{self, info, trace}; +pub use rustc_middle::{bug, span_bug}; #[cfg(all(feature = "native-lib", unix))] pub mod native_lib { diff --git a/src/tools/miri/src/shims/files.rs b/src/tools/miri/src/shims/files.rs index d007853ead2fe..04b84e6f3e67c 100644 --- a/src/tools/miri/src/shims/files.rs +++ b/src/tools/miri/src/shims/files.rs @@ -9,7 +9,7 @@ use std::{fs, io}; use rustc_abi::Size; -use crate::shims::unix::{FileMetadata, UnixFileDescription}; +use crate::shims::unix::UnixFileDescription; use crate::*; /// A unique id for file descriptions. While we could use the address, considering that @@ -209,23 +209,14 @@ pub trait FileDescription: std::fmt::Debug + FileDescriptionExt { throw_unsup_format!("cannot close {}", self.name()); } - /// Returns the host `fs::Metadata` for this FD, if available. - /// Used by host-aware shims like Windows's `GetFileInformationByHandle`. - /// Unrelated to Unix `fstat`, which goes through `fstat()`. - fn host_metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result> { + /// Returns the metadata for this FD, if available. + /// This is either host metadata, or a non-file-backed-FD type. + /// The latter is for new represented as a string storing a `libc` name so we only + /// support that kind of metadata on Unix targets. + fn metadata<'tcx>(&self) -> InterpResult<'tcx, Either, &'static str>> { throw_unsup_format!("obtaining metadata is only supported on file-backed file descriptors"); } - /// Return the metadata describing this FD for the `fstat`/`statx` family of syscalls. - /// File-backed FDs should call `FileMetadata::from_meta` with their host metadata. - /// Non-file-backed FDs should call `FileMetadata::synthetic` with an appropriate mode. - fn fstat<'tcx>( - &self, - _ecx: &mut MiriInterpCx<'tcx>, - ) -> InterpResult<'tcx, Result> { - throw_unsup_format!("fstat is not supported on {}", self.name()); - } - fn is_tty(&self, _communicate_allowed: bool) -> bool { // Most FDs are not tty's and the consequence of a wrong `false` are minor, // so we use a default impl here. @@ -445,15 +436,8 @@ impl FileDescription for FileHandle { } } - fn host_metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result> { - interp_ok(self.file.metadata()) - } - - fn fstat<'tcx>( - &self, - ecx: &mut MiriInterpCx<'tcx>, - ) -> InterpResult<'tcx, Result> { - FileMetadata::from_meta(ecx, self.file.metadata()) + fn metadata<'tcx>(&self) -> InterpResult<'tcx, Either, &'static str>> { + interp_ok(Either::Left(self.file.metadata())) } fn is_tty(&self, communicate_allowed: bool) -> bool { diff --git a/src/tools/miri/src/shims/unix/fs.rs b/src/tools/miri/src/shims/unix/fs.rs index d318d3cecdb5e..0988edaef2b0f 100644 --- a/src/tools/miri/src/shims/unix/fs.rs +++ b/src/tools/miri/src/shims/unix/fs.rs @@ -348,34 +348,6 @@ trait EvalContextExtPrivate<'tcx>: crate::MiriInterpCxExt<'tcx> { } } -fn file_type_to_mode_name(file_type: std::fs::FileType) -> &'static str { - #[cfg(unix)] - use std::os::unix::fs::FileTypeExt; - - if file_type.is_file() { - "S_IFREG" - } else if file_type.is_dir() { - "S_IFDIR" - } else if file_type.is_symlink() { - "S_IFLNK" - } else { - // Certain file types are only available when the host is a Unix system. - #[cfg(unix)] - { - if file_type.is_socket() { - return "S_IFSOCK"; - } else if file_type.is_fifo() { - return "S_IFIFO"; - } else if file_type.is_char_device() { - return "S_IFCHR"; - } else if file_type.is_block_device() { - return "S_IFBLK"; - } - } - "S_IFREG" - } -} - impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {} pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { fn open( @@ -1662,9 +1634,37 @@ fn extract_sec_and_nsec<'tcx>( } } +fn file_type_to_mode_name(file_type: std::fs::FileType) -> &'static str { + #[cfg(unix)] + use std::os::unix::fs::FileTypeExt; + + if file_type.is_file() { + "S_IFREG" + } else if file_type.is_dir() { + "S_IFDIR" + } else if file_type.is_symlink() { + "S_IFLNK" + } else { + // Certain file types are only available when the host is a Unix system. + #[cfg(unix)] + { + if file_type.is_socket() { + return "S_IFSOCK"; + } else if file_type.is_fifo() { + return "S_IFIFO"; + } else if file_type.is_char_device() { + return "S_IFCHR"; + } else if file_type.is_block_device() { + return "S_IFBLK"; + } + } + "S_IFREG" + } +} + /// Stores a file's metadata in order to avoid code duplication in the different metadata related /// shims. -pub struct FileMetadata { +struct FileMetadata { mode: Scalar, size: u64, created: Option<(u64, u32)>, @@ -1694,18 +1694,20 @@ impl FileMetadata { let Some(fd) = ecx.machine.fds.get(fd_num) else { return interp_ok(Err(LibcError("EBADF"))); }; - fd.fstat(ecx) + match fd.metadata()? { + Either::Left(host) => Self::from_meta(ecx, host), + Either::Right(name) => Self::synthetic(ecx, name), + } } - pub(crate) fn synthetic<'tcx>( + fn synthetic<'tcx>( ecx: &mut MiriInterpCx<'tcx>, mode_name: &str, - size: u64, ) -> InterpResult<'tcx, Result> { let mode = ecx.eval_libc(mode_name); interp_ok(Ok(FileMetadata { mode, - size, + size: 0, created: None, accessed: None, modified: None, @@ -1715,7 +1717,7 @@ impl FileMetadata { })) } - pub(crate) fn from_meta<'tcx>( + fn from_meta<'tcx>( ecx: &mut MiriInterpCx<'tcx>, metadata: Result, ) -> InterpResult<'tcx, Result> { diff --git a/src/tools/miri/src/shims/unix/linux_like/epoll.rs b/src/tools/miri/src/shims/unix/linux_like/epoll.rs index 48085fa6ae26a..bd07e13d47fbb 100644 --- a/src/tools/miri/src/shims/unix/linux_like/epoll.rs +++ b/src/tools/miri/src/shims/unix/linux_like/epoll.rs @@ -10,7 +10,7 @@ use crate::concurrency::VClock; use crate::shims::files::{ DynFileDescriptionRef, FdId, FdNum, FileDescription, FileDescriptionRef, WeakFileDescriptionRef, }; -use crate::shims::unix::{FileMetadata, UnixFileDescription}; +use crate::shims::unix::UnixFileDescription; use crate::*; type EpollEventKey = (FdId, FdNum); @@ -119,12 +119,11 @@ impl FileDescription for Epoll { "epoll" } - fn fstat<'tcx>( + fn metadata<'tcx>( &self, - ecx: &mut MiriInterpCx<'tcx>, - ) -> InterpResult<'tcx, Result> { + ) -> InterpResult<'tcx, Either, &'static str>> { // On Linux, epoll is an "anonymous inode" reported as S_IFREG. - FileMetadata::synthetic(ecx, "S_IFREG", 0) + interp_ok(Either::Right("S_IFREG")) } fn destroy<'tcx>( diff --git a/src/tools/miri/src/shims/unix/linux_like/eventfd.rs b/src/tools/miri/src/shims/unix/linux_like/eventfd.rs index 03bac1e7270da..7cccbd0e275c5 100644 --- a/src/tools/miri/src/shims/unix/linux_like/eventfd.rs +++ b/src/tools/miri/src/shims/unix/linux_like/eventfd.rs @@ -5,8 +5,8 @@ use std::io::ErrorKind; use crate::concurrency::VClock; use crate::shims::files::{FdId, FileDescription, FileDescriptionRef, WeakFileDescriptionRef}; +use crate::shims::unix::UnixFileDescription; use crate::shims::unix::linux_like::epoll::{EpollEvents, EvalContextExt as _}; -use crate::shims::unix::{FileMetadata, UnixFileDescription}; use crate::*; /// Maximum value that the eventfd counter can hold. @@ -37,12 +37,11 @@ impl FileDescription for EventFd { "event" } - fn fstat<'tcx>( + fn metadata<'tcx>( &self, - ecx: &mut MiriInterpCx<'tcx>, - ) -> InterpResult<'tcx, Result> { + ) -> InterpResult<'tcx, Either, &'static str>> { // On Linux, eventfd is an "anonymous inode" reported as S_IFREG. - FileMetadata::synthetic(ecx, "S_IFREG", 0) + interp_ok(Either::Right("S_IFREG")) } fn destroy<'tcx>( diff --git a/src/tools/miri/src/shims/unix/mod.rs b/src/tools/miri/src/shims/unix/mod.rs index 9e8fa2a12d494..c55a28bfa7b2a 100644 --- a/src/tools/miri/src/shims/unix/mod.rs +++ b/src/tools/miri/src/shims/unix/mod.rs @@ -19,7 +19,7 @@ mod solarish; // All the Unix-specific extension traits pub use self::env::{EvalContextExt as _, UnixEnvVars}; pub use self::fd::{EvalContextExt as _, UnixFileDescription}; -pub use self::fs::{DirTable, EvalContextExt as _, FileMetadata}; +pub use self::fs::{DirTable, EvalContextExt as _}; pub use self::linux_like::epoll::EpollInterestTable; pub use self::mem::EvalContextExt as _; pub use self::socket::EvalContextExt as _; diff --git a/src/tools/miri/src/shims/unix/virtual_socket.rs b/src/tools/miri/src/shims/unix/virtual_socket.rs index 16eba61c56ba8..51bd30840ffbd 100644 --- a/src/tools/miri/src/shims/unix/virtual_socket.rs +++ b/src/tools/miri/src/shims/unix/virtual_socket.rs @@ -12,8 +12,8 @@ use crate::concurrency::VClock; use crate::shims::files::{ EvalContextExt as _, FdId, FileDescription, FileDescriptionRef, WeakFileDescriptionRef, }; +use crate::shims::unix::UnixFileDescription; use crate::shims::unix::linux_like::epoll::{EpollEvents, EvalContextExt as _}; -use crate::shims::unix::{FileMetadata, UnixFileDescription}; use crate::*; /// The maximum capacity of the socketpair buffer in bytes. @@ -83,15 +83,14 @@ impl FileDescription for VirtualSocket { } } - fn fstat<'tcx>( + fn metadata<'tcx>( &self, - ecx: &mut MiriInterpCx<'tcx>, - ) -> InterpResult<'tcx, Result> { + ) -> InterpResult<'tcx, Either, &'static str>> { let mode_name = match self.fd_type { VirtualSocketType::Socketpair => "S_IFSOCK", VirtualSocketType::PipeRead | VirtualSocketType::PipeWrite => "S_IFIFO", }; - FileMetadata::synthetic(ecx, mode_name, 0) + interp_ok(Either::Right(mode_name)) } fn destroy<'tcx>( diff --git a/src/tools/miri/src/shims/windows/fs.rs b/src/tools/miri/src/shims/windows/fs.rs index ddbb2b1de018a..a6efc36d75f6c 100644 --- a/src/tools/miri/src/shims/windows/fs.rs +++ b/src/tools/miri/src/shims/windows/fs.rs @@ -22,8 +22,10 @@ impl FileDescription for DirHandle { "directory" } - fn host_metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result> { - interp_ok(self.path.metadata()) + fn metadata<'tcx>( + &self, + ) -> InterpResult<'tcx, Either, &'static str>> { + interp_ok(Either::Left(self.path.metadata())) } fn destroy<'tcx>( @@ -49,8 +51,10 @@ impl FileDescription for MetadataHandle { "metadata-only" } - fn host_metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result> { - interp_ok(Ok(self.meta.clone())) + fn metadata<'tcx>( + &self, + ) -> InterpResult<'tcx, Either, &'static str>> { + interp_ok(Either::Left(Ok(self.meta.clone()))) } fn destroy<'tcx>( @@ -328,12 +332,16 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { this.invalid_handle("GetFileInformationByHandle")? }; - let metadata = match desc.host_metadata()? { - Ok(meta) => meta, - Err(e) => { + let metadata = match desc.metadata()? { + Either::Left(Ok(meta)) => meta, + Either::Left(Err(e)) => { this.set_last_error(e)?; return interp_ok(this.eval_windows("c", "FALSE")); } + Either::Right(_mode) => + throw_unsup_format!( + "`GetFileInformationByHandle` is not supported on non-file-backed handles" + ), }; let size = metadata.len(); From f107bb85a21bbf5824854fbf0f2546505f4732a0 Mon Sep 17 00:00:00 2001 From: Zalathar Date: Sun, 26 Apr 2026 21:33:38 +1000 Subject: [PATCH 21/24] Regression test for improper spans in inclusive-range suggestions --- tests/ui/parser/range-inclusive-suggestion-span.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/ui/parser/range-inclusive-suggestion-span.rs diff --git a/tests/ui/parser/range-inclusive-suggestion-span.rs b/tests/ui/parser/range-inclusive-suggestion-span.rs new file mode 100644 index 0000000000000..eed115d0ac89c --- /dev/null +++ b/tests/ui/parser/range-inclusive-suggestion-span.rs @@ -0,0 +1,14 @@ +#![crate_type = "rlib"] + +// Suggestions for range patterns should not perform span manipulations that +// assume the range token is ASCII, because it could have been recovered from +// similar-looking Unicode characters. +// +// Regression test for . + +// FIXME: The ICE is fixed in a subsequent commit. +//@ known-bug: #155799 +//@ failure-status: 101 + +// These dots are U+00B7 MIDDLE DOT, not an ASCII period. +fn dot_dot_dot() { ··· } From 9ceed255b5c4356ab6093e98ba3e4fe2a405f98b Mon Sep 17 00:00:00 2001 From: Zalathar Date: Sun, 26 Apr 2026 21:02:20 +1000 Subject: [PATCH 22/24] Avoid improper spans when `...` or `..=` is recovered from non-ASCII This avoids an ICE due to indexing into the middle of a multi-byte character. --- compiler/rustc_parse/src/errors.rs | 10 +- compiler/rustc_parse/src/parser/pat.rs | 12 +- .../parser/range-inclusive-suggestion-span.rs | 40 ++- .../range-inclusive-suggestion-span.stderr | 334 ++++++++++++++++++ 4 files changed, 378 insertions(+), 18 deletions(-) create mode 100644 tests/ui/parser/range-inclusive-suggestion-span.stderr diff --git a/compiler/rustc_parse/src/errors.rs b/compiler/rustc_parse/src/errors.rs index cc1e0ff85dae4..1829592d6d16e 100644 --- a/compiler/rustc_parse/src/errors.rs +++ b/compiler/rustc_parse/src/errors.rs @@ -1140,14 +1140,13 @@ pub(crate) struct InclusiveRangeMatchArrow { #[primary_span] pub arrow: Span, #[label("this is parsed as an inclusive range `..=`")] - pub span: Span, #[suggestion( "add a space between the pattern and `=>`", style = "verbose", - code = " ", + code = ".. =", applicability = "machine-applicable" )] - pub after_pat: Span, + pub span: Span, } #[derive(Diagnostic)] @@ -1155,14 +1154,13 @@ pub(crate) struct InclusiveRangeMatchArrow { #[note("inclusive ranges must be bounded at the end (`..=b` or `a..=b`)")] pub(crate) struct InclusiveRangeNoEnd { #[primary_span] - pub span: Span, #[suggestion( "use `..` instead", - code = "", + code = "..", applicability = "machine-applicable", style = "verbose" )] - pub suggestion: Span, + pub span: Span, } #[derive(Subdiagnostic)] diff --git a/compiler/rustc_parse/src/parser/pat.rs b/compiler/rustc_parse/src/parser/pat.rs index b5c33d740872b..f36127ec8f0a8 100644 --- a/compiler/rustc_parse/src/parser/pat.rs +++ b/compiler/rustc_parse/src/parser/pat.rs @@ -1225,7 +1225,7 @@ impl<'a> Parser<'a> { pub(super) fn inclusive_range_with_incorrect_end(&mut self) -> ErrorGuaranteed { let tok = &self.token; let span = self.prev_token.span; - // If the user typed "..==" instead of "..=", we want to give them + // If the user typed "..==" or "...=" instead of "..=", we want to give them // a specific error message telling them to use "..=". // If they typed "..=>", suggest they use ".. =>". // Otherwise, we assume that they meant to type a half open exclusive @@ -1243,14 +1243,10 @@ impl<'a> Parser<'a> { self.dcx().emit_err(InclusiveRangeExtraEquals { span: span_with_eq }) } - token::Gt if no_space => { - let after_pat = span.with_hi(span.hi() - BytePos(1)).shrink_to_hi(); - self.dcx().emit_err(InclusiveRangeMatchArrow { span, arrow: tok.span, after_pat }) + token::Gt if self.prev_token.kind == token::DotDotEq && no_space => { + self.dcx().emit_err(InclusiveRangeMatchArrow { span, arrow: tok.span }) } - _ => self.dcx().emit_err(InclusiveRangeNoEnd { - span, - suggestion: span.with_lo(span.hi() - BytePos(1)), - }), + _ => self.dcx().emit_err(InclusiveRangeNoEnd { span }), } } diff --git a/tests/ui/parser/range-inclusive-suggestion-span.rs b/tests/ui/parser/range-inclusive-suggestion-span.rs index eed115d0ac89c..06d1e42a13753 100644 --- a/tests/ui/parser/range-inclusive-suggestion-span.rs +++ b/tests/ui/parser/range-inclusive-suggestion-span.rs @@ -6,9 +6,41 @@ // // Regression test for . -// FIXME: The ICE is fixed in a subsequent commit. -//@ known-bug: #155799 -//@ failure-status: 101 - // These dots are U+00B7 MIDDLE DOT, not an ASCII period. fn dot_dot_dot() { ··· } +//~^ ERROR unknown start of token +//~| ERROR unknown start of token +//~| ERROR unknown start of token +//~| ERROR unexpected token: `...` +//~| ERROR inclusive range with no end + +fn dot_dot_dot_eq() { ···= } +//~^ ERROR unknown start of token +//~| ERROR unknown start of token +//~| ERROR unknown start of token +//~| ERROR unexpected token: `...` +//~| ERROR unexpected `=` after inclusive range + +fn dot_dot_dot_gt() { ···> } +//~^ ERROR unknown start of token +//~| ERROR unknown start of token +//~| ERROR unknown start of token +//~| ERROR unexpected token: `...` +//~| ERROR inclusive range with no end +//~| ERROR expected one of `;` or `}`, found `>` + +fn dot_dot_eq() { ··= } +//~^ ERROR unknown start of token +//~| ERROR unknown start of token +//~| ERROR inclusive range with no end + +fn dot_dot_eq_eq() { ··== } +//~^ ERROR unknown start of token +//~| ERROR unknown start of token +//~| ERROR unexpected `=` after inclusive range + +fn dot_dot_eq_gt() { ··=> } +//~^ ERROR unknown start of token +//~| ERROR unknown start of token +//~| ERROR unexpected `>` after inclusive range +//~| ERROR expected one of `;` or `}`, found `>` diff --git a/tests/ui/parser/range-inclusive-suggestion-span.stderr b/tests/ui/parser/range-inclusive-suggestion-span.stderr new file mode 100644 index 0000000000000..3743288589354 --- /dev/null +++ b/tests/ui/parser/range-inclusive-suggestion-span.stderr @@ -0,0 +1,334 @@ +error: unknown start of token: \u{b7} + --> $DIR/range-inclusive-suggestion-span.rs:10:20 + | +LL | fn dot_dot_dot() { ··· } + | ^^^ + | + = note: character appears 2 more times +help: Unicode character '·' (Middle Dot) looks like '.' (Period), but it is not + | +LL - fn dot_dot_dot() { ··· } +LL + fn dot_dot_dot() { ... } + | + +error: unknown start of token: \u{b7} + --> $DIR/range-inclusive-suggestion-span.rs:10:21 + | +LL | fn dot_dot_dot() { ··· } + | ^^ + | + = note: character appears once more +help: Unicode character '·' (Middle Dot) looks like '.' (Period), but it is not + | +LL - fn dot_dot_dot() { ··· } +LL + fn dot_dot_dot() { ·.. } + | + +error: unknown start of token: \u{b7} + --> $DIR/range-inclusive-suggestion-span.rs:10:22 + | +LL | fn dot_dot_dot() { ··· } + | ^ + | +help: Unicode character '·' (Middle Dot) looks like '.' (Period), but it is not + | +LL - fn dot_dot_dot() { ··· } +LL + fn dot_dot_dot() { ··. } + | + +error: unknown start of token: \u{b7} + --> $DIR/range-inclusive-suggestion-span.rs:17:23 + | +LL | fn dot_dot_dot_eq() { ···= } + | ^^^ + | + = note: character appears 2 more times +help: Unicode character '·' (Middle Dot) looks like '.' (Period), but it is not + | +LL - fn dot_dot_dot_eq() { ···= } +LL + fn dot_dot_dot_eq() { ...= } + | + +error: unknown start of token: \u{b7} + --> $DIR/range-inclusive-suggestion-span.rs:17:24 + | +LL | fn dot_dot_dot_eq() { ···= } + | ^^ + | + = note: character appears once more +help: Unicode character '·' (Middle Dot) looks like '.' (Period), but it is not + | +LL - fn dot_dot_dot_eq() { ···= } +LL + fn dot_dot_dot_eq() { ·..= } + | + +error: unknown start of token: \u{b7} + --> $DIR/range-inclusive-suggestion-span.rs:17:25 + | +LL | fn dot_dot_dot_eq() { ···= } + | ^ + | +help: Unicode character '·' (Middle Dot) looks like '.' (Period), but it is not + | +LL - fn dot_dot_dot_eq() { ···= } +LL + fn dot_dot_dot_eq() { ··.= } + | + +error: unknown start of token: \u{b7} + --> $DIR/range-inclusive-suggestion-span.rs:24:23 + | +LL | fn dot_dot_dot_gt() { ···> } + | ^^^ + | + = note: character appears 2 more times +help: Unicode character '·' (Middle Dot) looks like '.' (Period), but it is not + | +LL - fn dot_dot_dot_gt() { ···> } +LL + fn dot_dot_dot_gt() { ...> } + | + +error: unknown start of token: \u{b7} + --> $DIR/range-inclusive-suggestion-span.rs:24:24 + | +LL | fn dot_dot_dot_gt() { ···> } + | ^^ + | + = note: character appears once more +help: Unicode character '·' (Middle Dot) looks like '.' (Period), but it is not + | +LL - fn dot_dot_dot_gt() { ···> } +LL + fn dot_dot_dot_gt() { ·..> } + | + +error: unknown start of token: \u{b7} + --> $DIR/range-inclusive-suggestion-span.rs:24:25 + | +LL | fn dot_dot_dot_gt() { ···> } + | ^ + | +help: Unicode character '·' (Middle Dot) looks like '.' (Period), but it is not + | +LL - fn dot_dot_dot_gt() { ···> } +LL + fn dot_dot_dot_gt() { ··.> } + | + +error: unknown start of token: \u{b7} + --> $DIR/range-inclusive-suggestion-span.rs:32:19 + | +LL | fn dot_dot_eq() { ··= } + | ^^ + | + = note: character appears once more +help: Unicode character '·' (Middle Dot) looks like '.' (Period), but it is not + | +LL - fn dot_dot_eq() { ··= } +LL + fn dot_dot_eq() { ..= } + | + +error: unknown start of token: \u{b7} + --> $DIR/range-inclusive-suggestion-span.rs:32:20 + | +LL | fn dot_dot_eq() { ··= } + | ^ + | +help: Unicode character '·' (Middle Dot) looks like '.' (Period), but it is not + | +LL - fn dot_dot_eq() { ··= } +LL + fn dot_dot_eq() { ·.= } + | + +error: unknown start of token: \u{b7} + --> $DIR/range-inclusive-suggestion-span.rs:37:22 + | +LL | fn dot_dot_eq_eq() { ··== } + | ^^ + | + = note: character appears once more +help: Unicode character '·' (Middle Dot) looks like '.' (Period), but it is not + | +LL - fn dot_dot_eq_eq() { ··== } +LL + fn dot_dot_eq_eq() { ..== } + | + +error: unknown start of token: \u{b7} + --> $DIR/range-inclusive-suggestion-span.rs:37:23 + | +LL | fn dot_dot_eq_eq() { ··== } + | ^ + | +help: Unicode character '·' (Middle Dot) looks like '.' (Period), but it is not + | +LL - fn dot_dot_eq_eq() { ··== } +LL + fn dot_dot_eq_eq() { ·.== } + | + +error: unknown start of token: \u{b7} + --> $DIR/range-inclusive-suggestion-span.rs:42:22 + | +LL | fn dot_dot_eq_gt() { ··=> } + | ^^ + | + = note: character appears once more +help: Unicode character '·' (Middle Dot) looks like '.' (Period), but it is not + | +LL - fn dot_dot_eq_gt() { ··=> } +LL + fn dot_dot_eq_gt() { ..=> } + | + +error: unknown start of token: \u{b7} + --> $DIR/range-inclusive-suggestion-span.rs:42:23 + | +LL | fn dot_dot_eq_gt() { ··=> } + | ^ + | +help: Unicode character '·' (Middle Dot) looks like '.' (Period), but it is not + | +LL - fn dot_dot_eq_gt() { ··=> } +LL + fn dot_dot_eq_gt() { ·.=> } + | + +error: unexpected token: `...` + --> $DIR/range-inclusive-suggestion-span.rs:10:20 + | +LL | fn dot_dot_dot() { ··· } + | ^^^ + | +help: use `..` for an exclusive range + | +LL - fn dot_dot_dot() { ··· } +LL + fn dot_dot_dot() { .. } + | +help: or `..=` for an inclusive range + | +LL - fn dot_dot_dot() { ··· } +LL + fn dot_dot_dot() { ..= } + | + +error[E0586]: inclusive range with no end + --> $DIR/range-inclusive-suggestion-span.rs:10:20 + | +LL | fn dot_dot_dot() { ··· } + | ^^^ + | + = note: inclusive ranges must be bounded at the end (`..=b` or `a..=b`) +help: use `..` instead + | +LL - fn dot_dot_dot() { ··· } +LL + fn dot_dot_dot() { .. } + | + +error: unexpected token: `...` + --> $DIR/range-inclusive-suggestion-span.rs:17:23 + | +LL | fn dot_dot_dot_eq() { ···= } + | ^^^ + | +help: use `..` for an exclusive range + | +LL - fn dot_dot_dot_eq() { ···= } +LL + fn dot_dot_dot_eq() { ..= } + | +help: or `..=` for an inclusive range + | +LL - fn dot_dot_dot_eq() { ···= } +LL + fn dot_dot_dot_eq() { ..== } + | + +error: unexpected `=` after inclusive range + --> $DIR/range-inclusive-suggestion-span.rs:17:23 + | +LL | fn dot_dot_dot_eq() { ···= } + | ^^^^ + | + = note: inclusive ranges end with a single equals sign (`..=`) +help: use `..=` instead + | +LL - fn dot_dot_dot_eq() { ···= } +LL + fn dot_dot_dot_eq() { ..= } + | + +error: unexpected token: `...` + --> $DIR/range-inclusive-suggestion-span.rs:24:23 + | +LL | fn dot_dot_dot_gt() { ···> } + | ^^^ + | +help: use `..` for an exclusive range + | +LL - fn dot_dot_dot_gt() { ···> } +LL + fn dot_dot_dot_gt() { ..> } + | +help: or `..=` for an inclusive range + | +LL - fn dot_dot_dot_gt() { ···> } +LL + fn dot_dot_dot_gt() { ..=> } + | + +error[E0586]: inclusive range with no end + --> $DIR/range-inclusive-suggestion-span.rs:24:23 + | +LL | fn dot_dot_dot_gt() { ···> } + | ^^^ + | + = note: inclusive ranges must be bounded at the end (`..=b` or `a..=b`) +help: use `..` instead + | +LL - fn dot_dot_dot_gt() { ···> } +LL + fn dot_dot_dot_gt() { ..> } + | + +error: expected one of `;` or `}`, found `>` + --> $DIR/range-inclusive-suggestion-span.rs:24:26 + | +LL | fn dot_dot_dot_gt() { ···> } + | ^ expected one of `;` or `}` + +error[E0586]: inclusive range with no end + --> $DIR/range-inclusive-suggestion-span.rs:32:19 + | +LL | fn dot_dot_eq() { ··= } + | ^^^ + | + = note: inclusive ranges must be bounded at the end (`..=b` or `a..=b`) +help: use `..` instead + | +LL - fn dot_dot_eq() { ··= } +LL + fn dot_dot_eq() { .. } + | + +error: unexpected `=` after inclusive range + --> $DIR/range-inclusive-suggestion-span.rs:37:22 + | +LL | fn dot_dot_eq_eq() { ··== } + | ^^^^ + | + = note: inclusive ranges end with a single equals sign (`..=`) +help: use `..=` instead + | +LL - fn dot_dot_eq_eq() { ··== } +LL + fn dot_dot_eq_eq() { ..= } + | + +error: unexpected `>` after inclusive range + --> $DIR/range-inclusive-suggestion-span.rs:42:25 + | +LL | fn dot_dot_eq_gt() { ··=> } + | ---^ + | | + | this is parsed as an inclusive range `..=` + | +help: add a space between the pattern and `=>` + | +LL - fn dot_dot_eq_gt() { ··=> } +LL + fn dot_dot_eq_gt() { .. => } + | + +error: expected one of `;` or `}`, found `>` + --> $DIR/range-inclusive-suggestion-span.rs:42:25 + | +LL | fn dot_dot_eq_gt() { ··=> } + | ^ expected one of `;` or `}` + +error: aborting due to 26 previous errors + +For more information about this error, try `rustc --explain E0586`. From 48fe89f9941ba0b79618e44e0176909ad85decb7 Mon Sep 17 00:00:00 2001 From: mejrs <59372212+mejrs@users.noreply.github.com> Date: Sun, 26 Apr 2026 14:23:53 +0200 Subject: [PATCH 23/24] add default field values to diagnostic FormatArgs --- .../src/diagnostics/conflict_errors.rs | 9 +-------- compiler/rustc_expand/src/lib.rs | 1 + compiler/rustc_expand/src/mbe/diagnostics.rs | 13 +------------ compiler/rustc_hir/src/attrs/diagnostic.rs | 7 ++++--- compiler/rustc_hir/src/lib.rs | 1 + compiler/rustc_resolve/src/imports.rs | 7 +------ 6 files changed, 9 insertions(+), 29 deletions(-) diff --git a/compiler/rustc_borrowck/src/diagnostics/conflict_errors.rs b/compiler/rustc_borrowck/src/diagnostics/conflict_errors.rs index 2735c800c4a38..f7d35f3ff3b4b 100644 --- a/compiler/rustc_borrowck/src/diagnostics/conflict_errors.rs +++ b/compiler/rustc_borrowck/src/diagnostics/conflict_errors.rs @@ -156,14 +156,7 @@ impl<'infcx, 'tcx> MirBorrowckCtxt<'_, 'infcx, 'tcx> { .collect(); generic_args.push((kw::SelfUpper, this.clone())); - let args = FormatArgs { - this, - // Unused - this_sugared: String::new(), - // Unused - item_context: "", - generic_args, - }; + let args = FormatArgs { this, generic_args, .. }; let CustomDiagnostic { message, label, notes, parent_label: _ } = directive.eval(None, &args); diff --git a/compiler/rustc_expand/src/lib.rs b/compiler/rustc_expand/src/lib.rs index d2ac7103bccb8..5068501a0e2d1 100644 --- a/compiler/rustc_expand/src/lib.rs +++ b/compiler/rustc_expand/src/lib.rs @@ -1,6 +1,7 @@ // tidy-alphabetical-start #![allow(internal_features)] #![feature(associated_type_defaults)] +#![feature(default_field_values)] #![feature(macro_metavar_expr)] #![feature(proc_macro_diagnostic)] #![feature(proc_macro_internals)] diff --git a/compiler/rustc_expand/src/mbe/diagnostics.rs b/compiler/rustc_expand/src/mbe/diagnostics.rs index 4e7e51c0a43cb..16b21b062cce7 100644 --- a/compiler/rustc_expand/src/mbe/diagnostics.rs +++ b/compiler/rustc_expand/src/mbe/diagnostics.rs @@ -77,19 +77,8 @@ pub(super) fn failed_to_match_macro( let CustomDiagnostic { message: custom_message, label: custom_label, notes: custom_notes, .. } = { - let macro_name = name.to_string(); on_unmatch_args - .map(|directive| { - directive.eval( - None, - &FormatArgs { - this: macro_name.clone(), - this_sugared: macro_name, - item_context: "macro invocation", - generic_args: Vec::new(), - }, - ) - }) + .map(|directive| directive.eval(None, &FormatArgs { this: name.to_string(), .. })) .unwrap_or_default() }; diff --git a/compiler/rustc_hir/src/attrs/diagnostic.rs b/compiler/rustc_hir/src/attrs/diagnostic.rs index 66cdf2be8fc4d..2beafee54541f 100644 --- a/compiler/rustc_hir/src/attrs/diagnostic.rs +++ b/compiler/rustc_hir/src/attrs/diagnostic.rs @@ -218,10 +218,11 @@ impl FormatString { /// ``` #[derive(Debug)] pub struct FormatArgs { + /// The name of the item the attribute is on. pub this: String, - pub this_sugared: String, - pub item_context: &'static str, - pub generic_args: Vec<(Symbol, String)>, + pub this_sugared: String = String::new(), + pub item_context: &'static str = "", + pub generic_args: Vec<(Symbol, String)> = Vec::new(), } #[derive(Clone, Debug, HashStable_Generic, Encodable, Decodable, PrintAttribute)] diff --git a/compiler/rustc_hir/src/lib.rs b/compiler/rustc_hir/src/lib.rs index c2d9f879cd601..7c2bf3c5b2797 100644 --- a/compiler/rustc_hir/src/lib.rs +++ b/compiler/rustc_hir/src/lib.rs @@ -7,6 +7,7 @@ #![feature(closure_track_caller)] #![feature(const_default)] #![feature(const_trait_impl)] +#![feature(default_field_values)] #![feature(derive_const)] #![feature(exhaustive_patterns)] #![feature(never_type)] diff --git a/compiler/rustc_resolve/src/imports.rs b/compiler/rustc_resolve/src/imports.rs index 198ec4080816b..b6ba35f0f3db8 100644 --- a/compiler/rustc_resolve/src/imports.rs +++ b/compiler/rustc_resolve/src/imports.rs @@ -886,12 +886,7 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> { let args = FormatArgs { this, - // Unused - this_sugared: String::new(), - // Unused - item_context: "", - // Unused - generic_args: Vec::new(), + .. }; let CustomDiagnostic { message, label, notes, .. } = directive.eval(None, &args); From 28c079ae40dbac7ab4a88ef7fe766bd9339e6857 Mon Sep 17 00:00:00 2001 From: Qai Juang <237468078+qaijuang@users.noreply.github.com> Date: Sun, 26 Apr 2026 14:30:46 +0000 Subject: [PATCH 24/24] Suggest `.iter()` for shared projections * Suggest `.iter()` for shared projections * address few nits * a few improvements --- .../rustc_hir_typeck/src/expr_use_visitor.rs | 26 ++++++- .../rustc_hir_typeck/src/method/suggest.rs | 73 ++++++++++++++++++- .../collect-without-into-iter-call.rs | 38 +++++++++- .../collect-without-into-iter-call.stderr | 50 ++++++++++++- 4 files changed, 178 insertions(+), 9 deletions(-) diff --git a/compiler/rustc_hir_typeck/src/expr_use_visitor.rs b/compiler/rustc_hir_typeck/src/expr_use_visitor.rs index f3d0b4d000c28..a6129d97a328a 100644 --- a/compiler/rustc_hir_typeck/src/expr_use_visitor.rs +++ b/compiler/rustc_hir_typeck/src/expr_use_visitor.rs @@ -2,7 +2,7 @@ //! normal visitor, which just walks the entire body in one shot, the //! `ExprUseVisitor` determines how expressions are being used. //! -//! In the compiler, this is only used for upvar inference, but there +//! In the compiler, this is only used for upvar inference and diagnostics, but there //! are many uses within clippy. use std::cell::{Ref, RefCell}; @@ -1855,3 +1855,27 @@ impl<'tcx, Cx: TypeInformationCtxt<'tcx>, D: Delegate<'tcx>> ExprUseVisitor<'tcx } } } + +struct ExprPlaceDelegate; + +impl<'tcx> Delegate<'tcx> for ExprPlaceDelegate { + fn consume(&mut self, _: &PlaceWithHirId<'tcx>, _: HirId) {} + + fn use_cloned(&mut self, _: &PlaceWithHirId<'tcx>, _: HirId) {} + + fn borrow(&mut self, _: &PlaceWithHirId<'tcx>, _: HirId, _: ty::BorrowKind) {} + + fn mutate(&mut self, _: &PlaceWithHirId<'tcx>, _: HirId) {} + + fn fake_read(&mut self, _: &PlaceWithHirId<'tcx>, _: FakeReadCause, _: HirId) {} +} + +/// Categorizes `expr` as a place for diagnostic suggestions. +/// +/// This should be used for diagnostics purpose only. +pub(crate) fn expr_place<'tcx>( + fcx: &FnCtxt<'_, 'tcx>, + expr: &hir::Expr<'_>, +) -> Result, ErrorGuaranteed> { + ExprUseVisitor::new(fcx, ExprPlaceDelegate).cat_expr(expr) +} diff --git a/compiler/rustc_hir_typeck/src/method/suggest.rs b/compiler/rustc_hir_typeck/src/method/suggest.rs index c0613eef52c37..7cf4822e4df67 100644 --- a/compiler/rustc_hir_typeck/src/method/suggest.rs +++ b/compiler/rustc_hir_typeck/src/method/suggest.rs @@ -48,6 +48,7 @@ use tracing::{debug, info, instrument}; use super::probe::{AutorefOrPtrAdjustment, IsSuggestion, Mode, ProbeScope}; use super::{CandidateSource, MethodError, NoMatchData}; use crate::errors::{self, CandidateTraitNote, NoAssociatedItem}; +use crate::expr_use_visitor::expr_place; use crate::method::probe::UnsatisfiedPredicates; use crate::{Expectation, FnCtxt}; @@ -189,6 +190,70 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> { false } + // Pick the iterator method to suggest: `.into_iter()` by default, and + // `.iter()`/`.iter_mut()` for projections through references. + fn preferred_iterator_method( + &self, + source: SelfSource<'tcx>, + rcvr_ty: Ty<'tcx>, + ) -> Option { + let SelfSource::MethodCall(rcvr_expr) = source else { + return Some(sym::into_iter); + }; + + let rcvr_expr = rcvr_expr.peel_drop_temps().peel_blocks(); + let Ok(place_with_id) = expr_place(self, rcvr_expr) else { + return None; + }; + + let mut projection_mutability = None; + for pointer_ty in place_with_id.place.deref_tys() { + match self.structurally_resolve_type(rcvr_expr.span, pointer_ty).kind() { + ty::Ref(.., Mutability::Not) => { + projection_mutability = Some(Mutability::Not); + break; + } + ty::Ref(.., Mutability::Mut) => { + projection_mutability.get_or_insert(Mutability::Mut); + } + ty::RawPtr(..) => return None, + _ => {} + } + } + + // Keep `.into_iter()` for receivers like `&Vec<_>`; only projections that + // dereference a reference need to switch to `iter`/`iter_mut`. + let Some(projection_mutability) = projection_mutability else { + return Some(sym::into_iter); + }; + + let call_expr = self.tcx.hir_expect_expr(self.tcx.parent_hir_id(rcvr_expr.hir_id)); + // `IntoIterator` does not imply inherent `iter`/`iter_mut` methods. + let has_method = |method_name| { + self.lookup_probe_for_diagnostic( + Ident::with_dummy_span(method_name), + rcvr_ty, + call_expr, + ProbeScope::TraitsInScope, + None, + ) + .is_ok() + }; + + match projection_mutability { + Mutability::Not => has_method(sym::iter).then_some(sym::iter), + Mutability::Mut => { + if has_method(sym::iter_mut) { + Some(sym::iter_mut) + } else if has_method(sym::iter) { + Some(sym::iter) + } else { + None + } + } + } + } + #[instrument(level = "debug", skip(self))] pub(crate) fn report_method_error( &self, @@ -855,10 +920,12 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> { } else if self.impl_into_iterator_should_be_iterator(rcvr_ty, span, unsatisfied_predicates) { err.span_label(span, format!("`{rcvr_ty}` is not an iterator")); - if !span.in_external_macro(self.tcx.sess.source_map()) { + if !span.in_external_macro(self.tcx.sess.source_map()) + && let Some(method_name) = self.preferred_iterator_method(source, rcvr_ty) + { err.multipart_suggestion( - "call `.into_iter()` first", - vec![(span.shrink_to_lo(), format!("into_iter()."))], + format!("call `.{method_name}()` first"), + vec![(span.shrink_to_lo(), format!("{method_name}()."))], Applicability::MaybeIncorrect, ); } diff --git a/tests/ui/did_you_mean/collect-without-into-iter-call.rs b/tests/ui/did_you_mean/collect-without-into-iter-call.rs index ee4d75615bd01..ca4b104e63476 100644 --- a/tests/ui/did_you_mean/collect-without-into-iter-call.rs +++ b/tests/ui/did_you_mean/collect-without-into-iter-call.rs @@ -1,12 +1,14 @@ -// Tests that the compiler suggests an `into_iter` call when an `Iterator` method -// is called on something that implements `IntoIterator` +// Tests that the compiler suggests an iterator method when an `Iterator` method +// is called on something that implements `IntoIterator`. fn main() { let items = items(); let other_items = items.map(|i| i + 1); //~^ ERROR no method named `map` found for opaque type `impl IntoIterator` in the current scope + //~| HELP: call `.into_iter()` first let vec: Vec = items.collect(); //~^ ERROR no method named `collect` found for opaque type `impl IntoIterator` in the current scope + //~| HELP: call `.into_iter()` first } fn items() -> impl IntoIterator { @@ -16,4 +18,36 @@ fn items() -> impl IntoIterator { fn process(items: impl IntoIterator) -> Vec { items.collect() //~^ ERROR no method named `collect` found for type parameter `impl IntoIterator` in the current scope + //~| HELP: call `.into_iter()` first +} + +// Regression test for https://github.com/rust-lang/rust/issues/155365 +struct Demo { + contents: Vec, +} + +impl Demo { + fn count_odds(&self) -> usize { + self.contents.filter(|v| *v % 2 == 1).count() + //~^ ERROR no method named `filter` found for struct `Vec` in the current scope + //~| HELP: call `.iter()` first + } + + fn increment(&mut self) { + self.contents.for_each(|v| *v += 1) + //~^ ERROR no method named `for_each` found for struct `Vec` in the current scope + //~| HELP: call `.iter_mut()` first + } +} + +fn count_odds_param(contents: &Vec) -> usize { + contents.filter(|v| *v % 2 == 1).count() + //~^ ERROR no method named `filter` found for reference `&Vec` in the current scope + //~| HELP: call `.into_iter()` first +} + +fn count_odds_explicit_deref(contents: &Vec) -> usize { + (*contents).filter(|v| *v % 2 == 1).count() + //~^ ERROR no method named `filter` found for struct `Vec` in the current scope + //~| HELP: call `.iter()` first } diff --git a/tests/ui/did_you_mean/collect-without-into-iter-call.stderr b/tests/ui/did_you_mean/collect-without-into-iter-call.stderr index 797bd1e9e6f1b..f3acaa5322673 100644 --- a/tests/ui/did_you_mean/collect-without-into-iter-call.stderr +++ b/tests/ui/did_you_mean/collect-without-into-iter-call.stderr @@ -10,7 +10,7 @@ LL | let other_items = items.into_iter().map(|i| i + 1); | ++++++++++++ error[E0599]: no method named `collect` found for opaque type `impl IntoIterator` in the current scope - --> $DIR/collect-without-into-iter-call.rs:8:31 + --> $DIR/collect-without-into-iter-call.rs:9:31 | LL | let vec: Vec = items.collect(); | ^^^^^^^ `impl IntoIterator` is not an iterator @@ -21,7 +21,7 @@ LL | let vec: Vec = items.into_iter().collect(); | ++++++++++++ error[E0599]: no method named `collect` found for type parameter `impl IntoIterator` in the current scope - --> $DIR/collect-without-into-iter-call.rs:17:11 + --> $DIR/collect-without-into-iter-call.rs:19:11 | LL | items.collect() | ^^^^^^^ `impl IntoIterator` is not an iterator @@ -31,6 +31,50 @@ help: call `.into_iter()` first LL | items.into_iter().collect() | ++++++++++++ -error: aborting due to 3 previous errors +error[E0599]: no method named `filter` found for struct `Vec` in the current scope + --> $DIR/collect-without-into-iter-call.rs:31:23 + | +LL | self.contents.filter(|v| *v % 2 == 1).count() + | ^^^^^^ `Vec` is not an iterator + | +help: call `.iter()` first + | +LL | self.contents.iter().filter(|v| *v % 2 == 1).count() + | +++++++ + +error[E0599]: no method named `for_each` found for struct `Vec` in the current scope + --> $DIR/collect-without-into-iter-call.rs:37:23 + | +LL | self.contents.for_each(|v| *v += 1) + | ^^^^^^^^ `Vec` is not an iterator + | +help: call `.iter_mut()` first + | +LL | self.contents.iter_mut().for_each(|v| *v += 1) + | +++++++++++ + +error[E0599]: no method named `filter` found for reference `&Vec` in the current scope + --> $DIR/collect-without-into-iter-call.rs:44:14 + | +LL | contents.filter(|v| *v % 2 == 1).count() + | ^^^^^^ `&Vec` is not an iterator + | +help: call `.into_iter()` first + | +LL | contents.into_iter().filter(|v| *v % 2 == 1).count() + | ++++++++++++ + +error[E0599]: no method named `filter` found for struct `Vec` in the current scope + --> $DIR/collect-without-into-iter-call.rs:50:17 + | +LL | (*contents).filter(|v| *v % 2 == 1).count() + | ^^^^^^ `Vec` is not an iterator + | +help: call `.iter()` first + | +LL | (*contents).iter().filter(|v| *v % 2 == 1).count() + | +++++++ + +error: aborting due to 7 previous errors For more information about this error, try `rustc --explain E0599`.