Auto merge of #3886 - RalfJung:freebsd-pthread, r=RalfJung

support pthread primitives on FreeBSD

Fixes https://github.com/rust-lang/miri/issues/3571: makes out pthread implementation support FreeBSD.

FreeBSD sets PHTREAD_MUTEX_DEFAULT == PHTREAD_MUTEX_ERRORCK, so the logic for handling "default vs explicitly set mutex kind" had to be adjusted.

The rest is just some general cleanup of the pthread logic, and I realized that we can enable the std::sync tests on Solarish.
This commit is contained in:
bors 2024-09-15 10:10:14 +00:00
commit 75921d2ca4
5 changed files with 70 additions and 131 deletions

View File

@ -150,10 +150,10 @@ case $HOST_TARGET in
# Partially supported targets (tier 2)
BASIC="empty_main integer vec string btreemap hello hashmap heap_alloc align" # ensures we have the basics: stdout/stderr, system allocator, randomness (for HashMap initialization)
UNIX="panic/panic panic/unwind concurrency/simple atomic libc-mem libc-misc libc-random env num_cpus" # the things that are very similar across all Unixes, and hence easily supported there
TEST_TARGET=x86_64-unknown-freebsd run_tests_minimal $BASIC $UNIX threadname libc-time fs
TEST_TARGET=i686-unknown-freebsd run_tests_minimal $BASIC $UNIX threadname libc-time fs
TEST_TARGET=x86_64-unknown-illumos run_tests_minimal $BASIC $UNIX threadname pthread available-parallelism libc-time tls
TEST_TARGET=x86_64-pc-solaris run_tests_minimal $BASIC $UNIX threadname pthread available-parallelism libc-time tls
TEST_TARGET=x86_64-unknown-freebsd run_tests_minimal $BASIC $UNIX threadname pthread libc-time fs
TEST_TARGET=i686-unknown-freebsd run_tests_minimal $BASIC $UNIX threadname pthread libc-time fs
TEST_TARGET=x86_64-unknown-illumos run_tests_minimal $BASIC $UNIX threadname pthread sync available-parallelism libc-time tls
TEST_TARGET=x86_64-pc-solaris run_tests_minimal $BASIC $UNIX threadname pthread sync available-parallelism libc-time tls
TEST_TARGET=aarch64-linux-android run_tests_minimal $BASIC $UNIX
TEST_TARGET=wasm32-wasip2 run_tests_minimal empty_main wasm heap_alloc libc-mem
TEST_TARGET=wasm32-unknown-unknown run_tests_minimal empty_main wasm

View File

@ -11,7 +11,7 @@ use crate::*;
#[inline]
fn mutexattr_kind_offset<'tcx>(ecx: &MiriInterpCx<'tcx>) -> InterpResult<'tcx, u64> {
Ok(match &*ecx.tcx.sess.target.os {
"linux" | "illumos" | "solaris" | "macos" => 0,
"linux" | "illumos" | "solaris" | "macos" | "freebsd" => 0,
os => throw_unsup_format!("`pthread_mutexattr` is not supported on {os}"),
})
}
@ -43,21 +43,11 @@ fn mutexattr_set_kind<'tcx>(
)
}
/// A flag that allows to distinguish `PTHREAD_MUTEX_NORMAL` from
/// `PTHREAD_MUTEX_DEFAULT`. Since in `glibc` they have the same numeric values,
/// but different behaviour, we need a way to distinguish them. We do this by
/// setting this bit flag to the `PTHREAD_MUTEX_NORMAL` mutexes. See the comment
/// in `pthread_mutexattr_settype` function.
const PTHREAD_MUTEX_NORMAL_FLAG: i32 = 0x8000000;
fn is_mutex_kind_default<'tcx>(ecx: &MiriInterpCx<'tcx>, kind: i32) -> InterpResult<'tcx, bool> {
Ok(kind == ecx.eval_libc_i32("PTHREAD_MUTEX_DEFAULT"))
}
fn is_mutex_kind_normal<'tcx>(ecx: &MiriInterpCx<'tcx>, kind: i32) -> InterpResult<'tcx, bool> {
let mutex_normal_kind = ecx.eval_libc_i32("PTHREAD_MUTEX_NORMAL");
Ok(kind == (mutex_normal_kind | PTHREAD_MUTEX_NORMAL_FLAG))
}
/// To differentiate "the mutex kind has not been changed" from
/// "the mutex kind has been set to PTHREAD_MUTEX_DEFAULT and that is
/// equal to some other mutex kind", we make the default value of this
/// field *not* PTHREAD_MUTEX_DEFAULT but this special flag.
const PTHREAD_MUTEX_KIND_UNCHANGED: i32 = 0x8000000;
/// The mutex kind.
#[derive(Debug, Clone, Copy)]
@ -78,13 +68,15 @@ pub struct AdditionalMutexData {
pub address: u64,
}
// pthread_mutex_t is between 24 and 48 bytes, depending on the platform.
// pthread_mutex_t is between 4 and 48 bytes, depending on the platform.
// We ignore the platform layout and store our own fields:
// - id: u32
fn mutex_id_offset<'tcx>(ecx: &MiriInterpCx<'tcx>) -> InterpResult<'tcx, u64> {
// When adding a new OS, make sure we also support all its static initializers in
// `mutex_kind_from_static_initializer`!
let offset = match &*ecx.tcx.sess.target.os {
"linux" | "illumos" | "solaris" => 0,
"linux" | "illumos" | "solaris" | "freebsd" => 0,
// macOS stores a signature in the first bytes, so we have to move to offset 4.
"macos" => 4,
os => throw_unsup_format!("`pthread_mutex` is not supported on {os}"),
@ -113,7 +105,7 @@ fn mutex_id_offset<'tcx>(ecx: &MiriInterpCx<'tcx>) -> InterpResult<'tcx, u64> {
check_static_initializer("PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP");
check_static_initializer("PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP");
}
"illumos" | "solaris" | "macos" => {
"illumos" | "solaris" | "macos" | "freebsd" => {
// No non-standard initializers.
}
os => throw_unsup_format!("`pthread_mutex` is not supported on {os}"),
@ -127,11 +119,10 @@ fn mutex_id_offset<'tcx>(ecx: &MiriInterpCx<'tcx>) -> InterpResult<'tcx, u64> {
fn mutex_create<'tcx>(
ecx: &mut MiriInterpCx<'tcx>,
mutex_ptr: &OpTy<'tcx>,
kind: i32,
kind: MutexKind,
) -> InterpResult<'tcx> {
let mutex = ecx.deref_pointer(mutex_ptr)?;
let address = mutex.ptr().addr().bytes();
let kind = translate_kind(ecx, kind)?;
let data = Box::new(AdditionalMutexData { address, kind });
ecx.mutex_create(&mutex, mutex_id_offset(ecx)?, Some(data))?;
Ok(())
@ -151,7 +142,7 @@ fn mutex_get_id<'tcx>(
let id = ecx.mutex_get_or_create_id(&mutex, mutex_id_offset(ecx)?, |ecx| {
// This is called if a static initializer was used and the lock has not been assigned
// an ID yet. We have to determine the mutex kind from the static initializer.
let kind = kind_from_static_initializer(ecx, &mutex)?;
let kind = mutex_kind_from_static_initializer(ecx, &mutex)?;
Ok(Some(Box::new(AdditionalMutexData { kind, address })))
})?;
@ -168,40 +159,51 @@ fn mutex_get_id<'tcx>(
}
/// Returns the kind of a static initializer.
fn kind_from_static_initializer<'tcx>(
fn mutex_kind_from_static_initializer<'tcx>(
ecx: &MiriInterpCx<'tcx>,
mutex: &MPlaceTy<'tcx>,
) -> InterpResult<'tcx, MutexKind> {
// Only linux has static initializers other than PTHREAD_MUTEX_DEFAULT.
let kind = match &*ecx.tcx.sess.target.os {
Ok(match &*ecx.tcx.sess.target.os {
// Only linux has static initializers other than PTHREAD_MUTEX_DEFAULT.
"linux" => {
let offset = if ecx.pointer_size().bytes() == 8 { 16 } else { 12 };
let kind_place =
mutex.offset(Size::from_bytes(offset), ecx.machine.layouts.i32, ecx)?;
ecx.read_scalar(&kind_place)?.to_i32()?
let kind = ecx.read_scalar(&kind_place)?.to_i32()?;
// Here we give PTHREAD_MUTEX_DEFAULT priority so that
// PTHREAD_MUTEX_INITIALIZER behaves like `pthread_mutex_init` with a NULL argument.
if kind == ecx.eval_libc_i32("PTHREAD_MUTEX_DEFAULT") {
MutexKind::Default
} else {
mutex_translate_kind(ecx, kind)?
}
}
"illumos" | "solaris" | "macos" => ecx.eval_libc_i32("PTHREAD_MUTEX_DEFAULT"),
os => throw_unsup_format!("`pthread_mutex` is not supported on {os}"),
};
translate_kind(ecx, kind)
_ => MutexKind::Default,
})
}
fn translate_kind<'tcx>(ecx: &MiriInterpCx<'tcx>, kind: i32) -> InterpResult<'tcx, MutexKind> {
Ok(if is_mutex_kind_default(ecx, kind)? {
MutexKind::Default
} else if is_mutex_kind_normal(ecx, kind)? {
fn mutex_translate_kind<'tcx>(
ecx: &MiriInterpCx<'tcx>,
kind: i32,
) -> InterpResult<'tcx, MutexKind> {
Ok(if kind == (ecx.eval_libc_i32("PTHREAD_MUTEX_NORMAL")) {
MutexKind::Normal
} else if kind == ecx.eval_libc_i32("PTHREAD_MUTEX_ERRORCHECK") {
MutexKind::ErrorCheck
} else if kind == ecx.eval_libc_i32("PTHREAD_MUTEX_RECURSIVE") {
MutexKind::Recursive
} else if kind == ecx.eval_libc_i32("PTHREAD_MUTEX_DEFAULT")
|| kind == PTHREAD_MUTEX_KIND_UNCHANGED
{
// We check this *last* since PTHREAD_MUTEX_DEFAULT may be numerically equal to one of the
// others, and we want an explicit `mutexattr_settype` to work as expected.
MutexKind::Default
} else {
throw_unsup_format!("unsupported type of mutex: {kind}");
})
}
// pthread_rwlock_t is between 32 and 56 bytes, depending on the platform.
// pthread_rwlock_t is between 4 and 56 bytes, depending on the platform.
// We ignore the platform layout and store our own fields:
// - id: u32
@ -214,7 +216,7 @@ pub struct AdditionalRwLockData {
fn rwlock_id_offset<'tcx>(ecx: &MiriInterpCx<'tcx>) -> InterpResult<'tcx, u64> {
let offset = match &*ecx.tcx.sess.target.os {
"linux" | "illumos" | "solaris" => 0,
"linux" | "illumos" | "solaris" | "freebsd" => 0,
// macOS stores a signature in the first bytes, so we have to move to offset 4.
"macos" => 4,
os => throw_unsup_format!("`pthread_rwlock` is not supported on {os}"),
@ -267,7 +269,7 @@ fn rwlock_get_id<'tcx>(
#[inline]
fn condattr_clock_offset<'tcx>(ecx: &MiriInterpCx<'tcx>) -> InterpResult<'tcx, u64> {
Ok(match &*ecx.tcx.sess.target.os {
"linux" | "illumos" | "solaris" => 0,
"linux" | "illumos" | "solaris" | "freebsd" => 0,
// macOS does not have a clock attribute.
os => throw_unsup_format!("`pthread_condattr` clock field is not supported on {os}"),
})
@ -286,11 +288,11 @@ fn condattr_get_clock_id<'tcx>(
.to_i32()
}
fn translate_clock_id<'tcx>(ecx: &MiriInterpCx<'tcx>, raw_id: i32) -> InterpResult<'tcx, ClockId> {
// To ensure compatibility with PTHREAD_COND_INITIALIZER on all platforms,
// we can't just compare with CLOCK_REALTIME: on Solarish, PTHREAD_COND_INITIALIZER
// makes the clock 0 but CLOCK_REALTIME is 3.
Ok(if raw_id == ecx.eval_libc_i32("CLOCK_REALTIME") || raw_id == 0 {
fn cond_translate_clock_id<'tcx>(
ecx: &MiriInterpCx<'tcx>,
raw_id: i32,
) -> InterpResult<'tcx, ClockId> {
Ok(if raw_id == ecx.eval_libc_i32("CLOCK_REALTIME") {
ClockId::Realtime
} else if raw_id == ecx.eval_libc_i32("CLOCK_MONOTONIC") {
ClockId::Monotonic
@ -313,14 +315,13 @@ fn condattr_set_clock_id<'tcx>(
)
}
// pthread_cond_t.
// pthread_cond_t can be only 4 bytes in size, depending on the platform.
// We ignore the platform layout and store our own fields:
// - id: u32
// - clock: i32
fn cond_id_offset<'tcx>(ecx: &MiriInterpCx<'tcx>) -> InterpResult<'tcx, u64> {
let offset = match &*ecx.tcx.sess.target.os {
"linux" | "illumos" | "solaris" => 0,
"linux" | "illumos" | "solaris" | "freebsd" => 0,
// macOS stores a signature in the first bytes, so we have to move to offset 4.
"macos" => 4,
os => throw_unsup_format!("`pthread_cond` is not supported on {os}"),
@ -344,30 +345,6 @@ fn cond_id_offset<'tcx>(ecx: &MiriInterpCx<'tcx>) -> InterpResult<'tcx, u64> {
Ok(offset)
}
fn cond_clock_offset<'tcx>(ecx: &MiriInterpCx<'tcx>) -> u64 {
// macOS doesn't have a clock attribute, but to keep the code uniform we store
// a clock ID in the pthread_cond_t anyway. There's enough space.
let offset = 8;
// Sanity-check this against PTHREAD_COND_INITIALIZER (but only once):
// the clock must start out as CLOCK_REALTIME.
static SANITY: AtomicBool = AtomicBool::new(false);
if !SANITY.swap(true, Ordering::Relaxed) {
let static_initializer = ecx.eval_path(&["libc", "PTHREAD_COND_INITIALIZER"]);
let id_field = static_initializer
.offset(Size::from_bytes(offset), ecx.machine.layouts.i32, ecx)
.unwrap();
let id = ecx.read_scalar(&id_field).unwrap().to_i32().unwrap();
let id = translate_clock_id(ecx, id).expect("static initializer should be valid");
assert!(
matches!(id, ClockId::Realtime),
"PTHREAD_COND_INITIALIZER is incompatible with our pthread_cond layout: clock is not CLOCK_REALTIME"
);
}
offset
}
#[derive(Debug, Clone, Copy)]
enum ClockId {
Realtime,
@ -390,14 +367,9 @@ fn cond_get_id<'tcx>(
) -> InterpResult<'tcx, CondvarId> {
let cond = ecx.deref_pointer(cond_ptr)?;
let address = cond.ptr().addr().bytes();
let id = ecx.condvar_get_or_create_id(&cond, cond_id_offset(ecx)?, |ecx| {
let raw_id = if ecx.tcx.sess.target.os == "macos" {
ecx.eval_libc_i32("CLOCK_REALTIME")
} else {
cond_get_clock_id(ecx, cond_ptr)?
};
let clock_id = translate_clock_id(ecx, raw_id)?;
Ok(Some(Box::new(AdditionalCondData { address, clock_id })))
let id = ecx.condvar_get_or_create_id(&cond, cond_id_offset(ecx)?, |_ecx| {
// This used the static initializer. The clock there is always CLOCK_REALTIME.
Ok(Some(Box::new(AdditionalCondData { address, clock_id: ClockId::Realtime })))
})?;
// Check that the mutex has not been moved since last use.
@ -411,26 +383,12 @@ fn cond_get_id<'tcx>(
Ok(id)
}
fn cond_get_clock_id<'tcx>(
ecx: &MiriInterpCx<'tcx>,
cond_ptr: &OpTy<'tcx>,
) -> InterpResult<'tcx, i32> {
ecx.deref_pointer_and_read(
cond_ptr,
cond_clock_offset(ecx),
ecx.libc_ty_layout("pthread_cond_t"),
ecx.machine.layouts.i32,
)?
.to_i32()
}
impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
fn pthread_mutexattr_init(&mut self, attr_op: &OpTy<'tcx>) -> InterpResult<'tcx, ()> {
let this = self.eval_context_mut();
let default_kind = this.eval_libc_i32("PTHREAD_MUTEX_DEFAULT");
mutexattr_set_kind(this, attr_op, default_kind)?;
mutexattr_set_kind(this, attr_op, PTHREAD_MUTEX_KIND_UNCHANGED)?;
Ok(())
}
@ -443,30 +401,13 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
let this = self.eval_context_mut();
let kind = this.read_scalar(kind_op)?.to_i32()?;
if kind == this.eval_libc_i32("PTHREAD_MUTEX_NORMAL") {
// In `glibc` implementation, the numeric values of
// `PTHREAD_MUTEX_NORMAL` and `PTHREAD_MUTEX_DEFAULT` are equal.
// However, a mutex created by explicitly passing
// `PTHREAD_MUTEX_NORMAL` type has in some cases different behaviour
// from the default mutex for which the type was not explicitly
// specified. For a more detailed discussion, please see
// https://github.com/rust-lang/miri/issues/1419.
//
// To distinguish these two cases in already constructed mutexes, we
// use the same trick as glibc: for the case when
// `pthread_mutexattr_settype` is called explicitly, we set the
// `PTHREAD_MUTEX_NORMAL_FLAG` flag.
let normal_kind = kind | PTHREAD_MUTEX_NORMAL_FLAG;
// Check that after setting the flag, the kind is distinguishable
// from all other kinds.
assert_ne!(normal_kind, this.eval_libc_i32("PTHREAD_MUTEX_DEFAULT"));
assert_ne!(normal_kind, this.eval_libc_i32("PTHREAD_MUTEX_ERRORCHECK"));
assert_ne!(normal_kind, this.eval_libc_i32("PTHREAD_MUTEX_RECURSIVE"));
mutexattr_set_kind(this, attr_op, normal_kind)?;
} else if kind == this.eval_libc_i32("PTHREAD_MUTEX_DEFAULT")
if kind == this.eval_libc_i32("PTHREAD_MUTEX_NORMAL")
|| kind == this.eval_libc_i32("PTHREAD_MUTEX_DEFAULT")
|| kind == this.eval_libc_i32("PTHREAD_MUTEX_ERRORCHECK")
|| kind == this.eval_libc_i32("PTHREAD_MUTEX_RECURSIVE")
{
// Make sure we do not mix this up with the "unchanged" kind.
assert_ne!(kind, PTHREAD_MUTEX_KIND_UNCHANGED);
mutexattr_set_kind(this, attr_op, kind)?;
} else {
let einval = this.eval_libc_i32("EINVAL");
@ -510,9 +451,9 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
let attr = this.read_pointer(attr_op)?;
let kind = if this.ptr_is_null(attr)? {
this.eval_libc_i32("PTHREAD_MUTEX_DEFAULT")
MutexKind::Default
} else {
mutexattr_get_kind(this, attr_op)?
mutex_translate_kind(this, mutexattr_get_kind(this, attr_op)?)?
};
mutex_create(this, mutex_op, kind)?;
@ -624,15 +565,14 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
fn pthread_mutex_destroy(&mut self, mutex_op: &OpTy<'tcx>) -> InterpResult<'tcx, ()> {
let this = self.eval_context_mut();
// Reading the field also has the side-effect that we detect double-`destroy`
// since we make the field unint below.
let id = mutex_get_id(this, mutex_op)?;
if this.mutex_is_locked(id) {
throw_ub_format!("destroyed a locked mutex");
}
// Destroying an uninit pthread_mutex is UB, so check to make sure it's not uninit.
mutex_get_id(this, mutex_op)?;
// This might lead to false positives, see comment in pthread_mutexattr_destroy
this.write_uninit(
&this.deref_pointer_as(mutex_op, this.libc_ty_layout("pthread_mutex_t"))?,
@ -734,15 +674,14 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
fn pthread_rwlock_destroy(&mut self, rwlock_op: &OpTy<'tcx>) -> InterpResult<'tcx, ()> {
let this = self.eval_context_mut();
// Reading the field also has the side-effect that we detect double-`destroy`
// since we make the field unint below.
let id = rwlock_get_id(this, rwlock_op)?;
if this.rwlock_is_locked(id) {
throw_ub_format!("destroyed a locked rwlock");
}
// Destroying an uninit pthread_rwlock is UB, so check to make sure it's not uninit.
rwlock_get_id(this, rwlock_op)?;
// This might lead to false positives, see comment in pthread_mutexattr_destroy
this.write_uninit(
&this.deref_pointer_as(rwlock_op, this.libc_ty_layout("pthread_rwlock_t"))?,
@ -832,7 +771,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
} else {
condattr_get_clock_id(this, attr_op)?
};
let clock_id = translate_clock_id(this, clock_id)?;
let clock_id = cond_translate_clock_id(this, clock_id)?;
let cond = this.deref_pointer(cond_op)?;
let address = cond.ptr().addr().bytes();
@ -930,11 +869,10 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
}
fn pthread_cond_destroy(&mut self, cond_op: &OpTy<'tcx>) -> InterpResult<'tcx, ()> {
//NOTE: Destroying an uninit pthread_cond is UB. Make sure it's not uninit,
// by accessing at least once all of its fields that we use.
let this = self.eval_context_mut();
// Reading the field also has the side-effect that we detect double-`destroy`
// since we make the field unint below.
let id = cond_get_id(this, cond_op)?;
if this.condvar_is_awaited(id) {
throw_ub_format!("destroying an awaited conditional variable");

View File

@ -4,7 +4,8 @@
fn main() {
unsafe {
let mutexattr: libc::pthread_mutexattr_t = std::mem::zeroed();
let mut mutexattr: libc::pthread_mutexattr_t = std::mem::zeroed();
assert_eq!(libc::pthread_mutexattr_init(&mut mutexattr as *mut _), 0);
let mut mutex: libc::pthread_mutex_t = std::mem::zeroed();
assert_eq!(libc::pthread_mutex_init(&mut mutex as *mut _, &mutexattr as *const _), 0);
assert_eq!(libc::pthread_mutex_lock(&mut mutex as *mut _), 0);