From d69b8e2b48cd5e3ac4f8ee7af995241ea7c3a010 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Fri, 13 Jun 2025 00:08:58 +0000 Subject: [PATCH] Add shims for `gettid`-esque functions Various platforms provide a function to return the current OS thread ID, but they all use a slightly different name. Add shims for these functions for Apple, FreeBSD, and Windows, with tests to account for those and a few more platforms that are not yet supported by Miri. The syscall and extern symbol is included as well on Linux. These should be useful in general but will also help support printing the OS thread ID in panic messages [1]. [1]: https://github.com/rust-lang/rust/pull/115746 Squashed commit from Ralf: try_from_scalar: extend comment --- src/tools/miri/src/helpers.rs | 1 - src/tools/miri/src/shims/env.rs | 22 +++ src/tools/miri/src/shims/extern_static.rs | 2 +- src/tools/miri/src/shims/unix/env.rs | 49 ++++- .../src/shims/unix/freebsd/foreign_items.rs | 5 + .../src/shims/unix/linux/foreign_items.rs | 4 +- .../miri/src/shims/unix/linux_like/syscall.rs | 6 + .../src/shims/unix/macos/foreign_items.rs | 5 + .../miri/src/shims/windows/foreign_items.rs | 17 ++ src/tools/miri/src/shims/windows/handle.rs | 4 + .../concurrency/windows_thread_invalid.rs | 9 + .../concurrency/windows_thread_invalid.stderr | 13 ++ src/tools/miri/tests/pass-dep/shims/gettid.rs | 183 ++++++++++++++++++ .../miri/tests/pass/alloc-access-tracking.rs | 4 +- 14 files changed, 312 insertions(+), 12 deletions(-) create mode 100644 src/tools/miri/tests/fail-dep/concurrency/windows_thread_invalid.rs create mode 100644 src/tools/miri/tests/fail-dep/concurrency/windows_thread_invalid.stderr create mode 100644 src/tools/miri/tests/pass-dep/shims/gettid.rs diff --git a/src/tools/miri/src/helpers.rs b/src/tools/miri/src/helpers.rs index fb34600fa37d..4821edb0942a 100644 --- a/src/tools/miri/src/helpers.rs +++ b/src/tools/miri/src/helpers.rs @@ -1337,7 +1337,6 @@ where /// Check that the number of varargs is at least the minimum what we expect. /// Fixed args should not be included. -/// Use `check_vararg_fixed_arg_count` to extract the varargs slice from full function arguments. pub fn check_min_vararg_count<'a, 'tcx, const N: usize>( name: &'a str, args: &'a [OpTy<'tcx>], diff --git a/src/tools/miri/src/shims/env.rs b/src/tools/miri/src/shims/env.rs index e99a8fd6e8c0..9dfb1ebb90a8 100644 --- a/src/tools/miri/src/shims/env.rs +++ b/src/tools/miri/src/shims/env.rs @@ -110,8 +110,30 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { } } + /// Get the process identifier. fn get_pid(&self) -> u32 { let this = self.eval_context_ref(); if this.machine.communicate() { std::process::id() } else { 1000 } } + + /// Get an "OS" thread ID for the current thread. + fn get_current_tid(&self) -> u32 { + let this = self.eval_context_ref(); + self.get_tid(this.machine.threads.active_thread()) + } + + /// Get an "OS" thread ID for any thread. + fn get_tid(&self, thread: ThreadId) -> u32 { + let this = self.eval_context_ref(); + let index = thread.to_u32(); + let target_os = &this.tcx.sess.target.os; + if target_os == "linux" || target_os == "netbsd" { + // On Linux, the main thread has PID == TID so we uphold this. NetBSD also appears + // to exhibit the same behavior, though I can't find a citation. + this.get_pid().strict_add(index) + } else { + // Other platforms do not display any relationship between PID and TID. + index + } + } } diff --git a/src/tools/miri/src/shims/extern_static.rs b/src/tools/miri/src/shims/extern_static.rs index a2ea3dbd88b1..5cf9265c7040 100644 --- a/src/tools/miri/src/shims/extern_static.rs +++ b/src/tools/miri/src/shims/extern_static.rs @@ -66,7 +66,7 @@ impl<'tcx> MiriMachine<'tcx> { ecx, &["__cxa_thread_atexit_impl", "__clock_gettime64"], )?; - Self::weak_symbol_extern_statics(ecx, &["getrandom", "statx"])?; + Self::weak_symbol_extern_statics(ecx, &["getrandom", "gettid", "statx"])?; } "freebsd" => { Self::null_ptr_extern_statics(ecx, &["__cxa_thread_atexit_impl"])?; diff --git a/src/tools/miri/src/shims/unix/env.rs b/src/tools/miri/src/shims/unix/env.rs index 604fb0974d29..a0e5d3f0127e 100644 --- a/src/tools/miri/src/shims/unix/env.rs +++ b/src/tools/miri/src/shims/unix/env.rs @@ -274,15 +274,52 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { interp_ok(Scalar::from_u32(this.get_pid())) } - fn linux_gettid(&mut self) -> InterpResult<'tcx, Scalar> { + /// The `gettid`-like function for Unix platforms that take no parameters and return a 32-bit + /// integer. It is not always named "gettid". + fn unix_gettid(&mut self, link_name: &str) -> InterpResult<'tcx, Scalar> { let this = self.eval_context_ref(); - this.assert_target_os("linux", "gettid"); + this.assert_target_os_is_unix(link_name); - let index = this.machine.threads.active_thread().to_u32(); + // For most platforms the return type is an `i32`, but some are unsigned. The TID + // will always be positive so we don't need to differentiate. + interp_ok(Scalar::from_u32(this.get_current_tid())) + } - // Compute a TID for this thread, ensuring that the main thread has PID == TID. - let tid = this.get_pid().strict_add(index); + /// The Apple-specific `int pthread_threadid_np(pthread_t thread, uint64_t *thread_id)`, which + /// allows querying the ID for arbitrary threads, identified by their pthread_t. + /// + /// API documentation: . + fn apple_pthread_threadip_np( + &mut self, + thread_op: &OpTy<'tcx>, + tid_op: &OpTy<'tcx>, + ) -> InterpResult<'tcx, Scalar> { + let this = self.eval_context_mut(); + this.assert_target_os("macos", "pthread_threadip_np"); - interp_ok(Scalar::from_u32(tid)) + let tid_dest = this.read_pointer(tid_op)?; + if this.ptr_is_null(tid_dest)? { + // If NULL is passed, an error is immediately returned + return interp_ok(this.eval_libc("EINVAL")); + } + + let thread = this.read_scalar(thread_op)?.to_int(this.libc_ty_layout("pthread_t").size)?; + let thread = if thread == 0 { + // Null thread ID indicates that we are querying the active thread. + this.machine.threads.active_thread() + } else { + // Our pthread_t is just the raw ThreadId. + let Ok(thread) = this.thread_id_try_from(thread) else { + return interp_ok(this.eval_libc("ESRCH")); + }; + thread + }; + + let tid = this.get_tid(thread); + let tid_dest = this.deref_pointer_as(tid_op, this.machine.layouts.u64)?; + this.write_int(tid, &tid_dest)?; + + // Possible errors have been handled, return success. + interp_ok(Scalar::from_u32(0)) } } diff --git a/src/tools/miri/src/shims/unix/freebsd/foreign_items.rs b/src/tools/miri/src/shims/unix/freebsd/foreign_items.rs index 42502d5bf09a..33564a2f84c7 100644 --- a/src/tools/miri/src/shims/unix/freebsd/foreign_items.rs +++ b/src/tools/miri/src/shims/unix/freebsd/foreign_items.rs @@ -56,6 +56,11 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { }; this.write_scalar(res, dest)?; } + "pthread_getthreadid_np" => { + let [] = this.check_shim(abi, CanonAbi::C, link_name, args)?; + let result = this.unix_gettid(link_name.as_str())?; + this.write_scalar(result, dest)?; + } "cpuset_getaffinity" => { // The "same" kind of api as `sched_getaffinity` but more fine grained control for FreeBSD specifically. diff --git a/src/tools/miri/src/shims/unix/linux/foreign_items.rs b/src/tools/miri/src/shims/unix/linux/foreign_items.rs index aeaff1cb13a5..b3e99e6cc68f 100644 --- a/src/tools/miri/src/shims/unix/linux/foreign_items.rs +++ b/src/tools/miri/src/shims/unix/linux/foreign_items.rs @@ -18,7 +18,7 @@ use crate::*; const TASK_COMM_LEN: u64 = 16; pub fn is_dyn_sym(name: &str) -> bool { - matches!(name, "statx") + matches!(name, "gettid" | "statx") } impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {} @@ -117,7 +117,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { } "gettid" => { let [] = this.check_shim(abi, CanonAbi::C, link_name, args)?; - let result = this.linux_gettid()?; + let result = this.unix_gettid(link_name.as_str())?; this.write_scalar(result, dest)?; } diff --git a/src/tools/miri/src/shims/unix/linux_like/syscall.rs b/src/tools/miri/src/shims/unix/linux_like/syscall.rs index d42d6b9073ec..d3534e6e1bc6 100644 --- a/src/tools/miri/src/shims/unix/linux_like/syscall.rs +++ b/src/tools/miri/src/shims/unix/linux_like/syscall.rs @@ -4,6 +4,7 @@ use rustc_span::Symbol; use rustc_target::callconv::FnAbi; use crate::helpers::check_min_vararg_count; +use crate::shims::unix::env::EvalContextExt; use crate::shims::unix::linux_like::eventfd::EvalContextExt as _; use crate::shims::unix::linux_like::sync::futex; use crate::*; @@ -24,6 +25,7 @@ pub fn syscall<'tcx>( let sys_getrandom = ecx.eval_libc("SYS_getrandom").to_target_usize(ecx)?; let sys_futex = ecx.eval_libc("SYS_futex").to_target_usize(ecx)?; let sys_eventfd2 = ecx.eval_libc("SYS_eventfd2").to_target_usize(ecx)?; + let sys_gettid = ecx.eval_libc("SYS_gettid").to_target_usize(ecx)?; match ecx.read_target_usize(op)? { // `libc::syscall(NR_GETRANDOM, buf.as_mut_ptr(), buf.len(), GRND_NONBLOCK)` @@ -53,6 +55,10 @@ pub fn syscall<'tcx>( let result = ecx.eventfd(initval, flags)?; ecx.write_int(result.to_i32()?, dest)?; } + num if num == sys_gettid => { + let result = ecx.unix_gettid("SYS_gettid")?; + ecx.write_int(result.to_u32()?, dest)?; + } num => { throw_unsup_format!("syscall: unsupported syscall number {num}"); } diff --git a/src/tools/miri/src/shims/unix/macos/foreign_items.rs b/src/tools/miri/src/shims/unix/macos/foreign_items.rs index ae921a013a40..233037180910 100644 --- a/src/tools/miri/src/shims/unix/macos/foreign_items.rs +++ b/src/tools/miri/src/shims/unix/macos/foreign_items.rs @@ -222,6 +222,11 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { }; this.write_scalar(res, dest)?; } + "pthread_threadid_np" => { + let [thread, tid_ptr] = this.check_shim(abi, CanonAbi::C, link_name, args)?; + let res = this.apple_pthread_threadip_np(thread, tid_ptr)?; + this.write_scalar(res, dest)?; + } // Synchronization primitives "os_sync_wait_on_address" => { diff --git a/src/tools/miri/src/shims/windows/foreign_items.rs b/src/tools/miri/src/shims/windows/foreign_items.rs index de10357f5fae..959abc0bacaa 100644 --- a/src/tools/miri/src/shims/windows/foreign_items.rs +++ b/src/tools/miri/src/shims/windows/foreign_items.rs @@ -629,6 +629,23 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { this.write_scalar(name, &name_ptr)?; this.write_scalar(res, dest)?; } + "GetThreadId" => { + let [handle] = this.check_shim(abi, sys_conv, link_name, args)?; + let handle = this.read_handle(handle, "GetThreadId")?; + let thread = match handle { + Handle::Thread(thread) => thread, + Handle::Pseudo(PseudoHandle::CurrentThread) => this.active_thread(), + _ => this.invalid_handle("GetThreadDescription")?, + }; + let tid = this.get_tid(thread); + this.write_scalar(Scalar::from_u32(tid), dest)?; + } + "GetCurrentThreadId" => { + let [] = this.check_shim(abi, sys_conv, link_name, args)?; + let thread = this.active_thread(); + let tid = this.get_tid(thread); + this.write_scalar(Scalar::from_u32(tid), dest)?; + } // Miscellaneous "ExitProcess" => { diff --git a/src/tools/miri/src/shims/windows/handle.rs b/src/tools/miri/src/shims/windows/handle.rs index 1e30bf25ed92..8a965ea316d7 100644 --- a/src/tools/miri/src/shims/windows/handle.rs +++ b/src/tools/miri/src/shims/windows/handle.rs @@ -166,6 +166,10 @@ impl Handle { /// Structurally invalid handles return [`HandleError::InvalidHandle`]. /// If the handle is structurally valid but semantically invalid, e.g. a for non-existent thread /// ID, returns [`HandleError::ThreadNotFound`]. + /// + /// This function is deliberately private; shims should always use `read_handle`. + /// That enforces handle validity even when Windows does not: for now, we argue invalid + /// handles are always a bug and programmers likely want to know about them. fn try_from_scalar<'tcx>( handle: Scalar, cx: &MiriInterpCx<'tcx>, diff --git a/src/tools/miri/tests/fail-dep/concurrency/windows_thread_invalid.rs b/src/tools/miri/tests/fail-dep/concurrency/windows_thread_invalid.rs new file mode 100644 index 000000000000..2e0729c9b496 --- /dev/null +++ b/src/tools/miri/tests/fail-dep/concurrency/windows_thread_invalid.rs @@ -0,0 +1,9 @@ +//! Ensure we error if thread functions are called with invalid handles +//@only-target: windows # testing Windows API + +use windows_sys::Win32::System::Threading::GetThreadId; + +fn main() { + let _tid = unsafe { GetThreadId(std::ptr::dangling_mut()) }; + //~^ ERROR: invalid handle +} diff --git a/src/tools/miri/tests/fail-dep/concurrency/windows_thread_invalid.stderr b/src/tools/miri/tests/fail-dep/concurrency/windows_thread_invalid.stderr new file mode 100644 index 000000000000..8d4b049b7402 --- /dev/null +++ b/src/tools/miri/tests/fail-dep/concurrency/windows_thread_invalid.stderr @@ -0,0 +1,13 @@ +error: abnormal termination: invalid handle 1 passed to GetThreadId + --> tests/fail-dep/concurrency/windows_thread_invalid.rs:LL:CC + | +LL | let _tid = unsafe { GetThreadId(std::ptr::dangling_mut()) }; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ abnormal termination occurred here + | + = note: BACKTRACE: + = note: inside `main` at tests/fail-dep/concurrency/windows_thread_invalid.rs:LL:CC + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to 1 previous error + diff --git a/src/tools/miri/tests/pass-dep/shims/gettid.rs b/src/tools/miri/tests/pass-dep/shims/gettid.rs new file mode 100644 index 000000000000..b7a2fa49ef86 --- /dev/null +++ b/src/tools/miri/tests/pass-dep/shims/gettid.rs @@ -0,0 +1,183 @@ +//! Test for `gettid` and similar functions for retrieving an OS thread ID. +//@ revisions: with_isolation without_isolation +//@ [without_isolation] compile-flags: -Zmiri-disable-isolation + +#![feature(linkage)] + +fn gettid() -> u64 { + cfg_if::cfg_if! { + if #[cfg(any(target_os = "android", target_os = "linux"))] { + gettid_linux_like() + } else if #[cfg(target_os = "nto")] { + unsafe { libc::gettid() as u64 } + } else if #[cfg(target_os = "openbsd")] { + unsafe { libc::getthrid() as u64 } + } else if #[cfg(target_os = "freebsd")] { + unsafe { libc::pthread_getthreadid_np() as u64 } + } else if #[cfg(target_os = "netbsd")] { + unsafe { libc::_lwp_self() as u64 } + } else if #[cfg(any(target_os = "solaris", target_os = "illumos"))] { + // On Solaris and Illumos, the `pthread_t` is the OS TID. + unsafe { libc::pthread_self() as u64 } + } else if #[cfg(target_vendor = "apple")] { + let mut id = 0u64; + let status: libc::c_int = unsafe { libc::pthread_threadid_np(0, &mut id) }; + assert_eq!(status, 0); + id + } else if #[cfg(windows)] { + use windows_sys::Win32::System::Threading::GetCurrentThreadId; + unsafe { GetCurrentThreadId() as u64 } + } else { + compile_error!("platform has no gettid") + } + } +} + +/// Test the libc function, the syscall, and the extern symbol. +#[cfg(any(target_os = "android", target_os = "linux"))] +fn gettid_linux_like() -> u64 { + unsafe extern "C" { + #[linkage = "extern_weak"] + static gettid: Option libc::pid_t>; + } + + let from_libc = unsafe { libc::gettid() as u64 }; + let from_sys = unsafe { libc::syscall(libc::SYS_gettid) as u64 }; + let from_static = unsafe { gettid.unwrap()() as u64 }; + + assert_eq!(from_libc, from_sys); + assert_eq!(from_libc, from_static); + + from_libc +} + +/// Specific platforms can query the tid of arbitrary threads from a `pthread_t` / `HANDLE` +#[cfg(any(target_vendor = "apple", windows))] +mod queried { + use std::ffi::c_void; + use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; + use std::{ptr, thread}; + + use super::*; + + static SPAWNED_TID: AtomicU64 = AtomicU64::new(0); + static CAN_JOIN: AtomicBool = AtomicBool::new(false); + + /// Save this thread's TID, give the spawning thread a chance to query it separately before + /// being joined. + fn thread_body() { + SPAWNED_TID.store(gettid(), Ordering::Relaxed); + + // Spin until the main thread has a chance to read this thread's ID + while !CAN_JOIN.load(Ordering::Relaxed) { + thread::yield_now(); + } + } + + /// Spawn a thread, query then return its TID. + #[cfg(target_vendor = "apple")] + fn spawn_update_join() -> u64 { + extern "C" fn thread_start(_data: *mut c_void) -> *mut c_void { + thread_body(); + ptr::null_mut() + } + + let mut t: libc::pthread_t = 0; + let mut spawned_tid_from_handle = 0u64; + + unsafe { + let res = libc::pthread_create(&mut t, ptr::null(), thread_start, ptr::null_mut()); + assert_eq!(res, 0, "failed to spawn thread"); + + let res = libc::pthread_threadid_np(t, &mut spawned_tid_from_handle); + assert_eq!(res, 0, "failed to query thread ID"); + CAN_JOIN.store(true, Ordering::Relaxed); + + let res = libc::pthread_join(t, ptr::null_mut()); + assert_eq!(res, 0, "failed to join thread"); + + // Apple also has two documented return values for invalid threads and null pointers + let res = libc::pthread_threadid_np(libc::pthread_t::MAX, &mut 0); + assert_eq!(res, libc::ESRCH, "expected ESRCH for invalid TID"); + let res = libc::pthread_threadid_np(0, ptr::null_mut()); + assert_eq!(res, libc::EINVAL, "invalid EINVAL for a null pointer"); + } + + spawned_tid_from_handle + } + + /// Spawn a thread, query then return its TID. + #[cfg(windows)] + fn spawn_update_join() -> u64 { + use windows_sys::Win32::Foundation::WAIT_FAILED; + use windows_sys::Win32::System::Threading::{ + CreateThread, GetThreadId, INFINITE, WaitForSingleObject, + }; + + extern "system" fn thread_start(_data: *mut c_void) -> u32 { + thread_body(); + 0 + } + + let spawned_tid_from_handle; + let mut tid_at_spawn = 0u32; + + unsafe { + let handle = + CreateThread(ptr::null(), 0, Some(thread_start), ptr::null(), 0, &mut tid_at_spawn); + assert!(!handle.is_null(), "failed to spawn thread"); + + spawned_tid_from_handle = GetThreadId(handle); + assert_ne!(spawned_tid_from_handle, 0, "failed to query thread ID"); + CAN_JOIN.store(true, Ordering::Relaxed); + + let res = WaitForSingleObject(handle, INFINITE); + assert_ne!(res, WAIT_FAILED, "failed to join thread"); + } + + // Windows also indirectly returns the TID from `CreateThread`, ensure that matches up. + assert_eq!(spawned_tid_from_handle, tid_at_spawn); + + spawned_tid_from_handle.into() + } + + pub fn check() { + let spawned_tid_from_handle = spawn_update_join(); + let spawned_tid_from_self = SPAWNED_TID.load(Ordering::Relaxed); + let current_tid = gettid(); + + // Ensure that we got a different thread ID. + assert_ne!(spawned_tid_from_handle, current_tid); + + // Ensure that we get the same result from `gettid` and from querying a thread's handle + assert_eq!(spawned_tid_from_handle, spawned_tid_from_self); + } +} + +fn main() { + let tid = gettid(); + + std::thread::spawn(move || { + assert_ne!(gettid(), tid); + }); + + // Test that in isolation mode a deterministic value will be returned. + // The value is not important, we only care that whatever the value is, + // won't change from execution to execution. + if cfg!(with_isolation) { + if cfg!(target_os = "linux") { + // Linux starts the TID at the PID, which is 1000. + assert_eq!(tid, 1000); + } else { + // Other platforms start counting from 0. + assert_eq!(tid, 0); + } + } + + // On Linux and NetBSD, the first TID is the PID. + #[cfg(any(target_os = "linux", target_os = "netbsd"))] + assert_eq!(tid, unsafe { libc::getpid() } as u64); + + #[cfg(any(target_vendor = "apple", windows))] + queried::check(); +} diff --git a/src/tools/miri/tests/pass/alloc-access-tracking.rs b/src/tools/miri/tests/pass/alloc-access-tracking.rs index 9eba0ca171bc..c47063bef03c 100644 --- a/src/tools/miri/tests/pass/alloc-access-tracking.rs +++ b/src/tools/miri/tests/pass/alloc-access-tracking.rs @@ -1,7 +1,7 @@ #![no_std] #![no_main] -//@compile-flags: -Zmiri-track-alloc-id=19 -Zmiri-track-alloc-accesses -Cpanic=abort -//@normalize-stderr-test: "id 19" -> "id $$ALLOC" +//@compile-flags: -Zmiri-track-alloc-id=21 -Zmiri-track-alloc-accesses -Cpanic=abort +//@normalize-stderr-test: "id 21" -> "id $$ALLOC" //@only-target: linux # alloc IDs differ between OSes (due to extern static allocations) extern "Rust" {