Skip to content

Commit c77a2c6

Browse files
committed
implement libc::sched_getaffinity and libc::sched_setaffinity
1 parent 8f03c97 commit c77a2c6

File tree

11 files changed

+457
-14
lines changed

11 files changed

+457
-14
lines changed

src/tools/miri/src/bin/miri.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,9 @@ fn main() {
592592
let num_cpus = param
593593
.parse::<u32>()
594594
.unwrap_or_else(|err| show_error!("-Zmiri-num-cpus requires a `u32`: {}", err));
595+
if !(1..=miri::MAX_CPUS).contains(&usize::try_from(num_cpus).unwrap()) {
596+
show_error!("-Zmiri-num-cpus must be in the range 1..={}", miri::MAX_CPUS);
597+
}
595598
miri_config.num_cpus = num_cpus;
596599
} else if let Some(param) = arg.strip_prefix("-Zmiri-force-page-size=") {
597600
let page_size = param.parse::<u64>().unwrap_or_else(|err| {
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
use crate::bug;
2+
use rustc_target::abi::Endian;
3+
4+
/// The maximum number of CPUs supported by miri.
5+
///
6+
/// This value is compatible with the libc `CPU_SETSIZE` constant and corresponds to the number
7+
/// of CPUs that a `cpu_set_t` can contain.
8+
///
9+
/// Real machines can have more CPUs than this number, and there exist APIs to set their affinity,
10+
/// but this is not currently supported by miri.
11+
pub const MAX_CPUS: usize = 1024;
12+
13+
/// A thread's CPU affinity mask determines the set of CPUs on which it is eligible to run.
14+
// the actual representation depends on the target's endianness and pointer width.
15+
// See CpuAffinityMask::set for details
16+
#[derive(Clone)]
17+
pub(crate) struct CpuAffinityMask([u8; Self::CPU_MASK_BYTES]);
18+
19+
impl CpuAffinityMask {
20+
pub(crate) const CPU_MASK_BYTES: usize = MAX_CPUS / 8;
21+
22+
pub fn new(target: &rustc_target::spec::Target, cpu_count: u32) -> Self {
23+
let mut this = Self([0; Self::CPU_MASK_BYTES]);
24+
25+
// the default affinity mask includes only the available CPUs
26+
for i in 0..cpu_count as usize {
27+
this.set(target, i);
28+
}
29+
30+
this
31+
}
32+
33+
pub fn chunk_size(target: &rustc_target::spec::Target) -> u64 {
34+
// The actual representation of the CpuAffinityMask is [c_ulong; _], in practice either
35+
//
36+
// - [u32; 32] on 32-bit platforms
37+
// - [u64; 16] everywhere else
38+
39+
// FIXME: this should be `size_of::<core::ffi::c_ulong>()`
40+
u64::from(target.pointer_width / 8)
41+
}
42+
43+
fn set(&mut self, target: &rustc_target::spec::Target, cpu: usize) {
44+
// we silently ignore CPUs that are out of bounds. This matches the behavior of
45+
// `sched_setaffinity` with a mask that specifies more than `CPU_SETSIZE` CPUs.
46+
if cpu >= MAX_CPUS {
47+
return;
48+
}
49+
50+
// The actual representation of the CpuAffinityMask is [c_ulong; _], in practice either
51+
//
52+
// - [u32; 32] on 32-bit platforms
53+
// - [u64; 16] everywhere else
54+
//
55+
// Within the array elements, we need to use the endianness of the target.
56+
match Self::chunk_size(target) {
57+
4 => {
58+
let start = cpu / 32 * 4; // first byte of the correct u32
59+
let chunk = self.0[start..].first_chunk_mut::<4>().unwrap();
60+
let offset = cpu % 32;
61+
*chunk = match target.options.endian {
62+
Endian::Little => (u32::from_le_bytes(*chunk) | 1 << offset).to_le_bytes(),
63+
Endian::Big => (u32::from_be_bytes(*chunk) | 1 << offset).to_be_bytes(),
64+
};
65+
}
66+
8 => {
67+
let start = cpu / 64 * 8; // first byte of the correct u64
68+
let chunk = self.0[start..].first_chunk_mut::<8>().unwrap();
69+
let offset = cpu % 64;
70+
*chunk = match target.options.endian {
71+
Endian::Little => (u64::from_le_bytes(*chunk) | 1 << offset).to_le_bytes(),
72+
Endian::Big => (u64::from_be_bytes(*chunk) | 1 << offset).to_be_bytes(),
73+
};
74+
}
75+
other => bug!("other chunk sizes are not supported: {other}"),
76+
};
77+
}
78+
79+
pub fn as_slice(&self) -> &[u8] {
80+
self.0.as_slice()
81+
}
82+
83+
pub fn from_array(
84+
target: &rustc_target::spec::Target,
85+
cpu_count: u32,
86+
bytes: [u8; Self::CPU_MASK_BYTES],
87+
) -> Option<Self> {
88+
// mask by what CPUs are actually available
89+
let default = Self::new(target, cpu_count);
90+
let masked = std::array::from_fn(|i| bytes[i] & default.0[i]);
91+
92+
// at least one thread must be set for the input to be valid
93+
masked.iter().any(|b| *b != 0).then_some(Self(masked))
94+
}
95+
}

src/tools/miri/src/concurrency/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod cpu_affinity;
12
pub mod data_race;
23
pub mod init_once;
34
mod range_object_map;

src/tools/miri/src/concurrency/thread.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,6 +936,11 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
936936
// After this all accesses will be treated as occurring in the new thread.
937937
let old_thread_id = this.machine.threads.set_active_thread_id(new_thread_id);
938938

939+
// The child inherits its parent's cpu affinity.
940+
if let Some(cpuset) = this.machine.thread_cpu_affinity.get(&old_thread_id).cloned() {
941+
this.machine.thread_cpu_affinity.insert(new_thread_id, cpuset);
942+
}
943+
939944
// Perform the function pointer load in the new thread frame.
940945
let instance = this.get_ptr_fn(start_routine)?.as_instance()?;
941946

src/tools/miri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ pub use crate::borrow_tracker::{
129129
};
130130
pub use crate::clock::{Clock, Instant};
131131
pub use crate::concurrency::{
132+
cpu_affinity::MAX_CPUS,
132133
data_race::{AtomicFenceOrd, AtomicReadOrd, AtomicRwOrd, AtomicWriteOrd, EvalContextExt as _},
133134
init_once::{EvalContextExt as _, InitOnceId},
134135
sync::{CondvarId, EvalContextExt as _, MutexId, RwLockId, SynchronizationObjects},

src/tools/miri/src/machine.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use rustc_target::spec::abi::Abi;
3030

3131
use crate::{
3232
concurrency::{
33+
cpu_affinity::{self, CpuAffinityMask},
3334
data_race::{self, NaReadType, NaWriteType},
3435
weak_memory,
3536
},
@@ -471,6 +472,12 @@ pub struct MiriMachine<'tcx> {
471472

472473
/// The set of threads.
473474
pub(crate) threads: ThreadManager<'tcx>,
475+
476+
/// Stores which thread is eligible to run on which CPUs.
477+
/// This has no effect at all, it is just tracked to produce the correct result
478+
/// in `sched_getaffinity`
479+
pub(crate) thread_cpu_affinity: FxHashMap<ThreadId, CpuAffinityMask>,
480+
474481
/// The state of the primitive synchronization objects.
475482
pub(crate) sync: SynchronizationObjects,
476483

@@ -627,6 +634,20 @@ impl<'tcx> MiriMachine<'tcx> {
627634
let stack_addr = if tcx.pointer_size().bits() < 32 { page_size } else { page_size * 32 };
628635
let stack_size =
629636
if tcx.pointer_size().bits() < 32 { page_size * 4 } else { page_size * 16 };
637+
assert!(
638+
usize::try_from(config.num_cpus).unwrap() <= cpu_affinity::MAX_CPUS,
639+
"miri only supports up to {} CPUs, but {} were configured",
640+
cpu_affinity::MAX_CPUS,
641+
config.num_cpus
642+
);
643+
let threads = ThreadManager::default();
644+
let mut thread_cpu_affinity = FxHashMap::default();
645+
if matches!(&*tcx.sess.target.os, "linux" | "freebsd" | "android") {
646+
thread_cpu_affinity.insert(
647+
threads.active_thread(),
648+
CpuAffinityMask::new(&tcx.sess.target, config.num_cpus),
649+
);
650+
}
630651
MiriMachine {
631652
tcx,
632653
borrow_tracker,
@@ -644,7 +665,8 @@ impl<'tcx> MiriMachine<'tcx> {
644665
fds: shims::FdTable::new(config.mute_stdout_stderr),
645666
dirs: Default::default(),
646667
layouts,
647-
threads: ThreadManager::default(),
668+
threads,
669+
thread_cpu_affinity,
648670
sync: SynchronizationObjects::default(),
649671
static_roots: Vec::new(),
650672
profiler,
@@ -765,6 +787,7 @@ impl VisitProvenance for MiriMachine<'_> {
765787
#[rustfmt::skip]
766788
let MiriMachine {
767789
threads,
790+
thread_cpu_affinity: _,
768791
sync: _,
769792
tls,
770793
env_vars,

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

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ use std::str;
33

44
use rustc_middle::ty::layout::LayoutOf;
55
use rustc_span::Symbol;
6+
use rustc_target::abi::Size;
67
use rustc_target::spec::abi::Abi;
78

9+
use crate::concurrency::cpu_affinity::CpuAffinityMask;
810
use crate::shims::alloc::EvalContextExt as _;
911
use crate::shims::unix::*;
1012
use crate::*;
@@ -571,6 +573,101 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
571573
let result = this.nanosleep(req, rem)?;
572574
this.write_scalar(Scalar::from_i32(result), dest)?;
573575
}
576+
"sched_getaffinity" => {
577+
// Currently this function does not exist on all Unixes, e.g. on macOS.
578+
if !matches!(&*this.tcx.sess.target.os, "linux" | "freebsd" | "android") {
579+
throw_unsup_format!(
580+
"`sched_getaffinity` is not supported on {}",
581+
this.tcx.sess.target.os
582+
);
583+
}
584+
585+
let [pid, cpusetsize, mask] =
586+
this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
587+
let pid = this.read_scalar(pid)?.to_u32()?;
588+
let cpusetsize = this.read_target_usize(cpusetsize)?;
589+
let mask = this.read_pointer(mask)?;
590+
591+
// TODO: when https://github.com/rust-lang/miri/issues/3730 is fixed this should use its notion of tid/pid
592+
let thread_id = match pid {
593+
0 => this.active_thread(),
594+
_ => throw_unsup_format!("`sched_getaffinity` is only supported with a pid of 0 (indicating the current thread)"),
595+
};
596+
597+
// The actual representation of the CpuAffinityMask is [c_ulong; _], in practice either
598+
//
599+
// - [u32; 32] on 32-bit platforms
600+
// - [u64; 16] everywhere else
601+
let chunk_size = CpuAffinityMask::chunk_size(&this.tcx.sess.target);
602+
603+
if this.ptr_is_null(mask)? {
604+
let einval = this.eval_libc("EFAULT");
605+
this.set_last_error(einval)?;
606+
this.write_scalar(Scalar::from_i32(-1), dest)?;
607+
} else if cpusetsize == 0 || cpusetsize.checked_rem(chunk_size).unwrap() != 0 {
608+
// we only copy whole chunks of size_of::<c_ulong>()
609+
let einval = this.eval_libc("EINVAL");
610+
this.set_last_error(einval)?;
611+
this.write_scalar(Scalar::from_i32(-1), dest)?;
612+
} else if let Some(cpuset) = this.machine.thread_cpu_affinity.get(&thread_id) {
613+
let cpuset = cpuset.clone();
614+
// we only copy whole chunks of size_of::<c_ulong>()
615+
let byte_count = Ord::min(cpuset.as_slice().len(), cpusetsize.try_into().unwrap());
616+
this.write_bytes_ptr(mask, cpuset.as_slice()[..byte_count].iter().copied())?;
617+
this.write_scalar(Scalar::from_i32(0), dest)?;
618+
} else {
619+
// The thread whose ID is pid could not be found
620+
let einval = this.eval_libc("ESRCH");
621+
this.set_last_error(einval)?;
622+
this.write_scalar(Scalar::from_i32(-1), dest)?;
623+
}
624+
}
625+
"sched_setaffinity" => {
626+
// Currently this function does not exist on all Unixes, e.g. on macOS.
627+
if !matches!(&*this.tcx.sess.target.os, "linux" | "freebsd" | "android") {
628+
throw_unsup_format!(
629+
"`sched_setaffinity` is not supported on {}",
630+
this.tcx.sess.target.os
631+
);
632+
}
633+
634+
let [pid, cpusetsize, mask] =
635+
this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
636+
let pid = this.read_scalar(pid)?.to_u32()?;
637+
let cpusetsize = this.read_target_usize(cpusetsize)?;
638+
let mask = this.read_pointer(mask)?;
639+
640+
// TODO: when https://github.com/rust-lang/miri/issues/3730 is fixed this should use its notion of tid/pid
641+
let thread_id = match pid {
642+
0 => this.active_thread(),
643+
_ => throw_unsup_format!("`sched_setaffinity` is only supported with a pid of 0 (indicating the current thread)"),
644+
};
645+
646+
#[allow(clippy::map_entry)]
647+
if this.ptr_is_null(mask)? {
648+
let einval = this.eval_libc("EFAULT");
649+
this.set_last_error(einval)?;
650+
this.write_scalar(Scalar::from_i32(-1), dest)?;
651+
} else {
652+
// NOTE: cpusetsize might be smaller than `CpuAffinityMask::CPU_MASK_BYTES`
653+
let bits_slice = this.read_bytes_ptr_strip_provenance(mask, Size::from_bytes(cpusetsize))?;
654+
// This ignores the bytes beyond `CpuAffinityMask::CPU_MASK_BYTES`
655+
let bits_array: [u8;CpuAffinityMask::CPU_MASK_BYTES] =
656+
std::array::from_fn(|i| bits_slice.get(i).copied().unwrap_or(0));
657+
match CpuAffinityMask::from_array(&this.tcx.sess.target, this.machine.num_cpus, bits_array) {
658+
Some(cpuset) => {
659+
this.machine.thread_cpu_affinity.insert(thread_id, cpuset);
660+
this.write_scalar(Scalar::from_i32(0), dest)?;
661+
}
662+
None => {
663+
// The intersection between the mask and the available CPUs was empty.
664+
let einval = this.eval_libc("EINVAL");
665+
this.set_last_error(einval)?;
666+
this.write_scalar(Scalar::from_i32(-1), dest)?;
667+
}
668+
}
669+
}
670+
}
574671

575672
// Miscellaneous
576673
"isatty" => {

src/tools/miri/src/shims/unix/linux/foreign_items.rs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -178,19 +178,6 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
178178

179179
this.write_scalar(Scalar::from_i32(SIGRTMAX), dest)?;
180180
}
181-
"sched_getaffinity" => {
182-
// This shim isn't useful, aside from the fact that it makes `num_cpus`
183-
// fall back to `sysconf` where it will successfully determine the number of CPUs.
184-
let [pid, cpusetsize, mask] =
185-
this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
186-
this.read_scalar(pid)?.to_i32()?;
187-
this.read_target_usize(cpusetsize)?;
188-
this.deref_pointer_as(mask, this.libc_ty_layout("cpu_set_t"))?;
189-
// FIXME: we just return an error.
190-
let einval = this.eval_libc("EINVAL");
191-
this.set_last_error(einval)?;
192-
this.write_scalar(Scalar::from_i32(-1), dest)?;
193-
}
194181

195182
// Incomplete shims that we "stub out" just to get pre-main initialization code to work.
196183
// These shims are enabled only when the caller is in the standard library.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//@ignore-target-windows: only very limited libc on Windows
2+
//@ignore-target-apple: `sched_setaffinity` is not supported on macOS
3+
//@compile-flags: -Zmiri-disable-isolation -Zmiri-num-cpus=4
4+
5+
fn main() {
6+
use libc::{cpu_set_t, sched_setaffinity};
7+
8+
use std::mem::size_of;
9+
10+
// If pid is zero, then the calling thread is used.
11+
const PID: i32 = 0;
12+
13+
let cpuset: cpu_set_t = unsafe { core::mem::MaybeUninit::zeroed().assume_init() };
14+
15+
let err = unsafe { sched_setaffinity(PID, size_of::<cpu_set_t>() + 1, &cpuset) }; //~ ERROR: memory access failed
16+
assert_eq!(err, 0);
17+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
error: Undefined Behavior: memory access failed: ALLOC has size 128, so pointer to 129 bytes starting at offset 0 is out-of-bounds
2+
--> $DIR/affinity.rs:LL:CC
3+
|
4+
LL | let err = unsafe { sched_setaffinity(PID, size_of::<cpu_set_t>() + 1, &cpuset) };
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ memory access failed: ALLOC has size 128, so pointer to 129 bytes starting at offset 0 is out-of-bounds
6+
|
7+
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
8+
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
9+
help: ALLOC was allocated here:
10+
--> $DIR/affinity.rs:LL:CC
11+
|
12+
LL | let cpuset: cpu_set_t = unsafe { core::mem::MaybeUninit::zeroed().assume_init() };
13+
| ^^^^^^
14+
= note: BACKTRACE (of the first span):
15+
= note: inside `main` at $DIR/affinity.rs:LL:CC
16+
17+
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
18+
19+
error: aborting due to 1 previous error
20+

0 commit comments

Comments
 (0)