use tracing::trace;
pub(crate) const FUTEX_WAITERS: u32 = 0x8000_0000;
pub(crate) const FUTEX_OWNER_DIED: u32 = 0x4000_0000;
pub(crate) const FUTEX_TID_MASK: u32 = 0x3fff_ffff;
pub(crate) const ROBUST_LIST_LIMIT: usize = 2048;
pub(crate) const HEAD_LIST_OFFSET: usize = 0;
pub(crate) const HEAD_FUTEX_OFFSET_OFFSET: usize = 8;
pub(crate) const HEAD_LIST_OP_PENDING_OFFSET: usize = 16;
pub(crate) const ROBUST_LIST_HEAD_LEN: usize = 24;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct RobustEntry {
pub address: usize,
pub is_pi: bool,
}
impl RobustEntry {
pub(crate) fn decode(raw: u64) -> Self {
Self {
address: (raw & !1u64) as usize,
is_pi: raw & 1 != 0,
}
}
pub(crate) fn is_null(&self) -> bool {
self.address == 0
}
}
pub(crate) fn futex_word_address(entry: usize, futex_offset: i64) -> Option<usize> {
let offset = isize::try_from(futex_offset).ok()?;
let sum = entry.checked_add_signed(offset)?;
(sum != 0).then_some(sum)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct FutexDeathTransition {
pub new_value: u32,
pub wake_one: bool,
}
pub(crate) fn futex_death_transition(
uval: u32,
owner_tid: u32,
is_pi: bool,
) -> Option<FutexDeathTransition> {
if uval & FUTEX_TID_MASK != owner_tid & FUTEX_TID_MASK {
return None;
}
Some(FutexDeathTransition {
new_value: (uval & FUTEX_WAITERS) | FUTEX_OWNER_DIED,
wake_one: !is_pi && uval & FUTEX_WAITERS != 0,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DeathStep {
Continue,
Abort,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FutexCasOutcome {
Stored,
Deferred,
Changed(u32),
Faulted,
}
pub(crate) trait RobustDeathEffects {
fn read_u64(&mut self, address: usize) -> Option<u64>;
fn read_u32(&mut self, address: usize) -> Option<u32>;
fn compare_and_swap(&mut self, address: usize, expected: u32, desired: u32) -> FutexCasOutcome;
async fn wake_one(&mut self, address: usize, observed: u32);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct WalkOutcome {
pub entries_visited: usize,
pub head_unreadable: bool,
pub aborted: bool,
pub next_faulted: bool,
pub truncated: bool,
pub cas_retries: usize,
}
pub(crate) async fn exit_robust_list<E: RobustDeathEffects>(
effects: &mut E,
head: usize,
owner_tid: u32,
) -> WalkOutcome {
let mut outcome = WalkOutcome::default();
let (Some(first_raw), Some(futex_offset), Some(pending_raw)) = (
head.checked_add(HEAD_LIST_OFFSET)
.and_then(|at| effects.read_u64(at)),
head.checked_add(HEAD_FUTEX_OFFSET_OFFSET)
.and_then(|at| effects.read_u64(at)),
head.checked_add(HEAD_LIST_OP_PENDING_OFFSET)
.and_then(|at| effects.read_u64(at)),
) else {
outcome.head_unreadable = true;
return outcome;
};
let futex_offset = futex_offset as i64;
let mut entry = RobustEntry::decode(first_raw);
let pending = {
let decoded = RobustEntry::decode(pending_raw);
(!decoded.is_null()).then_some(decoded)
};
let mut limit = ROBUST_LIST_LIMIT;
while entry.address != head {
let next = effects.read_u64(entry.address).map(RobustEntry::decode);
if pending.map(|slot| slot.address) != Some(entry.address) {
outcome.entries_visited += 1;
if handle_futex_death(
effects,
entry,
futex_offset,
owner_tid,
false,
&mut outcome.cas_retries,
)
.await
== DeathStep::Abort
{
outcome.aborted = true;
return outcome;
}
}
let Some(next) = next else {
outcome.next_faulted = true;
return outcome;
};
entry = next;
limit -= 1;
if limit == 0 {
outcome.truncated = true;
break;
}
}
if let Some(pending) = pending {
outcome.entries_visited += 1;
if handle_futex_death(
effects,
pending,
futex_offset,
owner_tid,
true,
&mut outcome.cas_retries,
)
.await
== DeathStep::Abort
{
outcome.aborted = true;
}
}
outcome
}
async fn handle_futex_death<E: RobustDeathEffects>(
effects: &mut E,
entry: RobustEntry,
futex_offset: i64,
owner_tid: u32,
pending_op: bool,
cas_retries: &mut usize,
) -> DeathStep {
let Some(word) = futex_word_address(entry.address, futex_offset) else {
return DeathStep::Abort;
};
if word % std::mem::size_of::<u32>() != 0 {
trace!("robust-list entry has a misaligned futex word {:#x}", word);
return DeathStep::Abort;
}
loop {
let Some(uval) = effects.read_u32(word) else {
return DeathStep::Abort;
};
if pending_op && !entry.is_pi && uval & FUTEX_TID_MASK == 0 {
effects.wake_one(word, uval).await;
return DeathStep::Continue;
}
let Some(transition) = futex_death_transition(uval, owner_tid, entry.is_pi) else {
return DeathStep::Continue;
};
match effects.compare_and_swap(word, uval, transition.new_value) {
FutexCasOutcome::Stored | FutexCasOutcome::Deferred => {
if transition.wake_one {
effects.wake_one(word, transition.new_value).await;
}
return DeathStep::Continue;
}
FutexCasOutcome::Changed(observed) => {
*cas_retries += 1;
trace!(
"robust-list futex word {:#x} moved {:#x} -> {:#x} under the walk; retrying",
word, uval, observed,
);
}
FutexCasOutcome::Faulted => return DeathStep::Abort,
}
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::collections::VecDeque;
use super::*;
const GLIBC_FUTEX_OFFSET: i64 = -32;
const OWNER: u32 = 5;
#[derive(Debug, Default)]
struct FakeGuest {
bytes: BTreeMap<usize, u8>,
unreadable: BTreeSet<usize>,
unwritable: BTreeSet<usize>,
races: BTreeMap<usize, VecDeque<u32>>,
races_before_write: BTreeMap<usize, VecDeque<u32>>,
defer_owner_death_to_backend: bool,
marks: Vec<(usize, u32, u32)>,
wakes: Vec<(usize, u32)>,
}
impl FakeGuest {
fn race_after_read(&mut self, address: usize, value: u32) {
self.races.entry(address).or_default().push_back(value);
}
fn race_before_write(&mut self, address: usize, value: u32) {
self.races_before_write
.entry(address)
.or_default()
.push_back(value);
}
fn put_u64(&mut self, address: usize, value: u64) {
for (i, byte) in value.to_le_bytes().into_iter().enumerate() {
self.bytes.insert(address + i, byte);
}
}
fn put_u32(&mut self, address: usize, value: u32) {
for (i, byte) in value.to_le_bytes().into_iter().enumerate() {
self.bytes.insert(address + i, byte);
}
}
fn get_u32(&self, address: usize) -> Option<u32> {
let mut buf = [0u8; 4];
for (i, slot) in buf.iter_mut().enumerate() {
*slot = *self.bytes.get(&(address + i))?;
}
Some(u32::from_le_bytes(buf))
}
fn head(&mut self, head: usize, first: u64, pending: u64) {
self.put_u64(head + HEAD_LIST_OFFSET, first);
self.put_u64(head + HEAD_FUTEX_OFFSET_OFFSET, GLIBC_FUTEX_OFFSET as u64);
self.put_u64(head + HEAD_LIST_OP_PENDING_OFFSET, pending);
}
fn node(&mut self, node: usize, next: u64, word: u32) {
self.put_u64(node, next);
self.put_u32(node - 32, word);
}
fn marked_words(&self) -> Vec<usize> {
self.marks.iter().map(|mark| mark.0).collect()
}
fn woken_words(&self) -> Vec<usize> {
self.wakes.iter().map(|wake| wake.0).collect()
}
}
impl RobustDeathEffects for FakeGuest {
fn read_u64(&mut self, address: usize) -> Option<u64> {
if self.unreadable.contains(&address) {
return None;
}
let mut buf = [0u8; 8];
for (i, slot) in buf.iter_mut().enumerate() {
*slot = *self.bytes.get(&(address + i))?;
}
Some(u64::from_le_bytes(buf))
}
fn read_u32(&mut self, address: usize) -> Option<u32> {
if self.unreadable.contains(&address) {
return None;
}
let value = self.get_u32(address)?;
if let Some(raced) = self.races.get_mut(&address).and_then(VecDeque::pop_front) {
self.put_u32(address, raced);
}
Some(value)
}
fn compare_and_swap(
&mut self,
address: usize,
expected: u32,
desired: u32,
) -> FutexCasOutcome {
if self.unwritable.contains(&address) {
return FutexCasOutcome::Faulted;
}
let Some(observed) = self.get_u32(address) else {
return FutexCasOutcome::Faulted;
};
if observed != expected {
return FutexCasOutcome::Changed(observed);
}
if let Some(raced) = self
.races_before_write
.get_mut(&address)
.and_then(VecDeque::pop_front)
{
self.put_u32(address, raced);
}
if self.defer_owner_death_to_backend {
return FutexCasOutcome::Deferred;
}
self.put_u32(address, desired);
self.marks.push((address, expected, desired));
FutexCasOutcome::Stored
}
async fn wake_one(&mut self, address: usize, observed: u32) {
self.wakes.push((address, observed));
}
}
fn walk(guest: &mut FakeGuest, head: usize, owner: u32) -> WalkOutcome {
futures::executor::block_on(exit_robust_list(guest, head, owner))
}
#[test]
fn pi_bit_is_carried_in_the_pointer_low_bit() {
assert_eq!(
RobustEntry::decode(0x0040_4120),
RobustEntry {
address: 0x0040_4120,
is_pi: false
}
);
assert_eq!(
RobustEntry::decode(0x0040_4121),
RobustEntry {
address: 0x0040_4120,
is_pi: true
}
);
assert!(RobustEntry::decode(0).is_null());
assert!(!RobustEntry::decode(0x0040_4120).is_null());
}
#[test]
fn glibc_negative_futex_offset_resolves_to_the_lock_word() {
assert_eq!(
futex_word_address(0x0040_4120, GLIBC_FUTEX_OFFSET),
Some(0x0040_4100)
);
}
#[test]
fn implausible_word_addresses_are_refused_rather_than_wrapping() {
assert_eq!(futex_word_address(8, GLIBC_FUTEX_OFFSET), None);
assert_eq!(futex_word_address(0, 0), None);
assert_eq!(futex_word_address(usize::MAX, i64::MAX), None);
}
#[test]
fn a_word_owned_by_another_thread_is_left_alone() {
assert_eq!(futex_death_transition(FUTEX_WAITERS | 7, 5, false), None);
assert_eq!(futex_death_transition(0, 5, false), None);
}
#[test]
fn owner_death_marks_the_word_and_wakes_one_waiter() {
let transition = futex_death_transition(0x8000_0005, 5, false).unwrap();
assert_eq!(transition.new_value, FUTEX_WAITERS | FUTEX_OWNER_DIED);
assert!(transition.wake_one);
}
#[test]
fn owner_death_without_waiters_marks_the_word_but_wakes_nobody() {
let transition = futex_death_transition(5, 5, false).unwrap();
assert_eq!(transition.new_value, FUTEX_OWNER_DIED);
assert!(!transition.wake_one);
}
#[test]
fn pi_entries_are_marked_but_never_plain_woken() {
let transition = futex_death_transition(0x8000_0005, 5, true).unwrap();
assert_eq!(transition.new_value, FUTEX_WAITERS | FUTEX_OWNER_DIED);
assert!(!transition.wake_one);
}
#[test]
fn an_already_dead_word_is_not_re_marked() {
let once = futex_death_transition(0x8000_0005, 5, false).unwrap();
assert_eq!(futex_death_transition(once.new_value, 5, false), None);
}
#[test]
fn high_tid_words_compare_under_the_tid_mask_only() {
let uval = FUTEX_WAITERS | FUTEX_OWNER_DIED | 0x0000_1234;
assert!(futex_death_transition(uval, 0x0000_1234, false).is_some());
assert!(futex_death_transition(uval, 0x0000_1235, false).is_none());
}
#[test]
fn an_empty_list_touches_nothing() {
let mut guest = FakeGuest::default();
guest.head(0x1000, 0x1000, 0);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert_eq!(outcome, WalkOutcome::default());
assert!(guest.marks.is_empty());
assert!(guest.wakes.is_empty());
}
#[test]
fn one_held_mutex_with_a_waiter_is_marked_then_woken() {
let mut guest = FakeGuest::default();
guest.head(0x1000, 0x2020, 0);
guest.node(0x2020, 0x1000, FUTEX_WAITERS | OWNER);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert_eq!(outcome.entries_visited, 1);
assert!(!outcome.aborted && !outcome.truncated && !outcome.next_faulted);
assert_eq!(
guest.marks,
vec![(
0x2000,
FUTEX_WAITERS | OWNER,
FUTEX_WAITERS | FUTEX_OWNER_DIED
)]
);
assert_eq!(
guest.wakes,
vec![(0x2000, FUTEX_WAITERS | FUTEX_OWNER_DIED)]
);
assert_eq!(
guest.get_u32(0x2000),
Some(FUTEX_WAITERS | FUTEX_OWNER_DIED)
);
}
#[test]
fn several_entries_are_marked_in_guest_pointer_order() {
let mut guest = FakeGuest::default();
guest.head(0x1000, 0x2020, 0);
guest.node(0x2020, 0x3020, FUTEX_WAITERS | OWNER);
guest.node(0x3020, 0x4020, OWNER);
guest.node(0x4020, 0x1000, FUTEX_WAITERS | OWNER);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert_eq!(outcome.entries_visited, 3);
assert_eq!(guest.marked_words(), vec![0x2000, 0x3000, 0x4000]);
assert_eq!(guest.woken_words(), vec![0x2000, 0x4000]);
}
#[test]
fn entries_owned_by_another_thread_are_skipped_without_stopping_the_walk() {
let mut guest = FakeGuest::default();
guest.head(0x1000, 0x2020, 0);
guest.node(0x2020, 0x3020, FUTEX_WAITERS | 7);
guest.node(0x3020, 0x1000, FUTEX_WAITERS | OWNER);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert_eq!(outcome.entries_visited, 2);
assert_eq!(guest.marked_words(), vec![0x3000]);
assert_eq!(guest.get_u32(0x2000), Some(FUTEX_WAITERS | 7));
}
#[test]
fn a_pi_entry_is_marked_but_not_woken_during_a_walk() {
let mut guest = FakeGuest::default();
guest.head(0x1000, 0x2021, 0); guest.node(0x2020, 0x1000, FUTEX_WAITERS | OWNER);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert_eq!(outcome.entries_visited, 1);
assert_eq!(guest.marks.len(), 1);
assert!(guest.wakes.is_empty());
}
#[test]
fn the_pending_slot_is_handled_last_and_only_once() {
let mut guest = FakeGuest::default();
guest.head(0x1000, 0x2020, 0x3020);
guest.node(0x2020, 0x3020, FUTEX_WAITERS | OWNER);
guest.node(0x3020, 0x1000, FUTEX_WAITERS | OWNER);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert_eq!(outcome.entries_visited, 2);
assert_eq!(
guest.marked_words(),
vec![0x2000, 0x3000],
"the pending word must be marked exactly once, after the list"
);
}
#[test]
fn a_pending_op_on_a_zero_word_wakes_without_marking_it_dead() {
let mut guest = FakeGuest::default();
guest.head(0x1000, 0x1000, 0x3020);
guest.node(0x3020, 0x1000, 0);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert_eq!(outcome.entries_visited, 1);
assert_eq!(guest.wakes, vec![(0x3000, 0)]);
assert!(
guest.marks.is_empty(),
"setting FUTEX_OWNER_DIED on a free word would corrupt user-space state"
);
assert_eq!(guest.get_u32(0x3000), Some(0));
}
#[test]
fn a_pending_op_with_no_owner_wakes_with_flag_bits_set() {
for flags in [
FUTEX_OWNER_DIED,
FUTEX_WAITERS,
FUTEX_OWNER_DIED | FUTEX_WAITERS,
] {
let mut guest = FakeGuest::default();
guest.head(0x1000, 0x1000, 0x3020);
guest.node(0x3020, 0x1000, flags);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert_eq!(outcome.entries_visited, 1);
assert_eq!(guest.wakes, vec![(0x3000, flags)]);
assert!(guest.marks.is_empty());
assert_eq!(guest.get_u32(0x3000), Some(flags));
}
}
#[test]
fn a_zero_word_in_the_list_body_is_not_a_pending_op_wake() {
let mut guest = FakeGuest::default();
guest.head(0x1000, 0x2020, 0);
guest.node(0x2020, 0x1000, 0);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert_eq!(outcome.entries_visited, 1);
assert!(
guest.wakes.is_empty(),
"pending_op is false for entries reached through the list body"
);
assert!(guest.marks.is_empty());
}
#[test]
fn a_pi_pending_op_on_a_zero_word_does_not_wake() {
let mut guest = FakeGuest::default();
guest.head(0x1000, 0x1000, 0x3021); guest.node(0x3020, 0x1000, 0);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert_eq!(outcome.entries_visited, 1);
assert!(guest.wakes.is_empty(), "the kernel guards this on `!pi`");
assert!(guest.marks.is_empty());
}
#[test]
fn the_walk_bound_is_the_kernel_s_literal_robust_list_limit() {
assert_eq!(ROBUST_LIST_LIMIT, 2048);
}
#[test]
fn a_cycle_is_bounded_by_robust_list_limit() {
let mut guest = FakeGuest::default();
guest.head(0x1000, 0x2020, 0);
guest.node(0x2020, 0x3020, FUTEX_WAITERS | 7);
guest.node(0x3020, 0x2020, FUTEX_WAITERS | 7);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert!(outcome.truncated);
assert_eq!(
outcome.entries_visited, 2048,
"a cyclic list must be followed for exactly the kernel's 2048 entries"
);
}
#[test]
fn a_futex_re_owned_between_the_read_and_the_write_is_left_to_its_new_owner() {
let mut guest = FakeGuest::default();
guest.head(0x1000, 0x2020, 0);
guest.node(0x2020, 0x1000, FUTEX_WAITERS | OWNER);
guest.race_after_read(0x2000, FUTEX_WAITERS | 7);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert_eq!(
guest.marks,
vec![],
"thread 7's live mutex must not be stamped FUTEX_OWNER_DIED"
);
assert_eq!(
guest.get_u32(0x2000),
Some(FUTEX_WAITERS | 7),
"the word must still be thread 7's"
);
assert!(guest.wakes.is_empty());
assert_eq!(outcome.entries_visited, 1);
assert_eq!(outcome.cas_retries, 1);
}
#[test]
fn backend_exit_cleanup_does_not_overwrite_a_store_after_the_final_read() {
let mut guest = FakeGuest {
defer_owner_death_to_backend: true,
..FakeGuest::default()
};
guest.head(0x1000, 0x2020, 0);
guest.node(0x2020, 0x1000, FUTEX_WAITERS | OWNER);
guest.race_before_write(0x2000, FUTEX_WAITERS | 7);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert_eq!(outcome.cas_retries, 0);
assert_eq!(guest.get_u32(0x2000), Some(FUTEX_WAITERS | 7));
assert!(guest.marks.is_empty());
assert_eq!(guest.wakes.len(), 1);
}
#[test]
fn fake_guest_exposes_the_separate_read_write_window() {
let mut guest = FakeGuest::default();
guest.put_u32(0x2000, FUTEX_WAITERS | OWNER);
guest.race_before_write(0x2000, FUTEX_WAITERS | 7);
let outcome = guest.compare_and_swap(
0x2000,
FUTEX_WAITERS | OWNER,
FUTEX_WAITERS | FUTEX_OWNER_DIED,
);
assert_eq!(outcome, FutexCasOutcome::Stored);
assert_eq!(
guest.get_u32(0x2000),
Some(FUTEX_WAITERS | FUTEX_OWNER_DIED),
"the fake must expose that a separate write overwrites the intervening store"
);
assert_eq!(
guest.marks,
vec![(
0x2000,
FUTEX_WAITERS | OWNER,
FUTEX_WAITERS | FUTEX_OWNER_DIED
)]
);
}
#[test]
fn a_waiter_arriving_between_the_read_and_the_write_is_still_woken() {
let mut guest = FakeGuest::default();
guest.head(0x1000, 0x2020, 0);
guest.node(0x2020, 0x1000, OWNER);
guest.race_after_read(0x2000, FUTEX_WAITERS | OWNER);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert_eq!(
guest.wakes,
vec![(0x2000, FUTEX_WAITERS | FUTEX_OWNER_DIED)],
"the arriving waiter must be woken; the wake decision has to be \
recomputed from the re-read word"
);
assert_eq!(
guest.marks,
vec![(
0x2000,
FUTEX_WAITERS | OWNER,
FUTEX_WAITERS | FUTEX_OWNER_DIED
)],
"the stored value must be recomputed from the re-read word, so \
FUTEX_WAITERS survives"
);
assert_eq!(
guest.get_u32(0x2000),
Some(FUTEX_WAITERS | FUTEX_OWNER_DIED)
);
assert_eq!(outcome.cas_retries, 1);
}
#[test]
fn the_walk_retries_until_the_word_stops_moving() {
let mut guest = FakeGuest::default();
guest.head(0x1000, 0x2020, 0);
guest.node(0x2020, 0x1000, OWNER);
guest.race_after_read(0x2000, FUTEX_WAITERS | OWNER);
guest.race_after_read(0x2000, OWNER);
guest.race_after_read(0x2000, FUTEX_WAITERS | OWNER);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert_eq!(
guest.get_u32(0x2000),
Some(FUTEX_WAITERS | FUTEX_OWNER_DIED)
);
assert_eq!(guest.marked_words(), vec![0x2000]);
assert_eq!(
outcome.cas_retries, 3,
"one retry per store that landed inside the window"
);
}
#[test]
fn a_word_freed_between_the_read_and_the_write_is_not_marked_dead() {
let mut guest = FakeGuest::default();
guest.head(0x1000, 0x2020, 0);
guest.node(0x2020, 0x1000, FUTEX_WAITERS | OWNER);
guest.race_after_read(0x2000, 0);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert_eq!(
guest.get_u32(0x2000),
Some(0),
"stamping FUTEX_OWNER_DIED on a free word is the corruption the \
comparison prevents"
);
assert!(guest.marks.is_empty() && guest.wakes.is_empty());
assert_eq!(outcome.cas_retries, 1);
}
#[test]
fn an_uncontended_word_is_still_marked_when_nothing_races() {
let mut guest = FakeGuest::default();
guest.head(0x1000, 0x2020, 0);
guest.node(0x2020, 0x1000, OWNER);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert_eq!(outcome.cas_retries, 0);
assert_eq!(guest.marks, vec![(0x2000, OWNER, FUTEX_OWNER_DIED)]);
assert!(guest.wakes.is_empty());
}
#[test]
fn an_unreadable_head_aborts_before_any_effect() {
let mut guest = FakeGuest::default();
guest.head(0x1000, 0x2020, 0);
guest.node(0x2020, 0x1000, FUTEX_WAITERS | OWNER);
guest.unreadable.insert(0x1000 + HEAD_FUTEX_OFFSET_OFFSET);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert!(outcome.head_unreadable);
assert_eq!(outcome.entries_visited, 0);
assert!(guest.marks.is_empty() && guest.wakes.is_empty());
}
#[test]
fn a_head_with_no_backing_memory_aborts() {
let mut guest = FakeGuest::default();
let outcome = walk(&mut guest, 0x7fff_0000, OWNER);
assert!(outcome.head_unreadable);
assert!(guest.marks.is_empty() && guest.wakes.is_empty());
}
#[test]
fn a_misaligned_futex_word_is_refused_and_aborts_the_walk() {
let mut guest = FakeGuest::default();
guest.put_u64(0x1000 + HEAD_LIST_OFFSET, 0x2020);
guest.put_u64(0x1000 + HEAD_FUTEX_OFFSET_OFFSET, (-31i64) as u64);
guest.put_u64(0x1000 + HEAD_LIST_OP_PENDING_OFFSET, 0);
guest.put_u64(0x2020, 0x3020);
guest.put_u32(0x2001, FUTEX_WAITERS | OWNER);
guest.put_u64(0x3020, 0x1000);
guest.put_u32(0x3001, FUTEX_WAITERS | OWNER);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert!(outcome.aborted);
assert_eq!(outcome.entries_visited, 1);
assert!(
guest.marks.is_empty() && guest.wakes.is_empty(),
"an unaligned word must not be read-modify-written"
);
}
#[test]
fn a_faulting_futex_word_aborts_the_rest_of_the_walk() {
let mut guest = FakeGuest::default();
guest.head(0x1000, 0x2020, 0);
guest.node(0x2020, 0x3020, FUTEX_WAITERS | OWNER);
guest.node(0x3020, 0x1000, FUTEX_WAITERS | OWNER);
guest.unreadable.insert(0x2000);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert!(outcome.aborted);
assert_eq!(outcome.entries_visited, 1);
assert!(
guest.marks.is_empty() && guest.wakes.is_empty(),
"Linux returns from the whole walk; it does not continue fail-open"
);
}
#[test]
fn an_unwritable_futex_word_aborts_the_rest_of_the_walk() {
let mut guest = FakeGuest::default();
guest.head(0x1000, 0x2020, 0);
guest.node(0x2020, 0x3020, FUTEX_WAITERS | OWNER);
guest.node(0x3020, 0x1000, FUTEX_WAITERS | OWNER);
guest.unwritable.insert(0x2000);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert!(outcome.aborted);
assert!(guest.marks.is_empty());
assert!(guest.wakes.is_empty());
assert_eq!(guest.get_u32(0x3000), Some(FUTEX_WAITERS | OWNER));
}
#[test]
fn an_abort_also_skips_the_pending_slot() {
let mut guest = FakeGuest::default();
guest.head(0x1000, 0x2020, 0x4020);
guest.node(0x2020, 0x1000, FUTEX_WAITERS | OWNER);
guest.node(0x4020, 0x1000, FUTEX_WAITERS | OWNER);
guest.unreadable.insert(0x2000);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert!(outcome.aborted);
assert!(
guest.wakes.is_empty() && guest.marks.is_empty(),
"Linux's bare `return` skips the trailing pending handling too"
);
}
#[test]
fn a_faulting_next_pointer_still_handles_the_current_entry() {
let mut guest = FakeGuest::default();
guest.head(0x1000, 0x2020, 0);
guest.node(0x2020, 0x3020, FUTEX_WAITERS | OWNER);
guest.unreadable.insert(0x2020);
let outcome = walk(&mut guest, 0x1000, OWNER);
assert!(outcome.next_faulted);
assert_eq!(outcome.entries_visited, 1);
assert_eq!(
guest.marked_words(),
vec![0x2000],
"the kernel checks fetch_robust_entry's rc only after handling the entry"
);
}
}