switch Rcu to the arcu crate

The arcu crate is a more general implementation of the Rcu I implemented in here in scryer. It contains some bug-fixes regarding race-conditions in the Rcu update function, which could cause leaks and uses after free.

Source of the problem was the Relaxed load/strore/update of the reference count in side the Arc not being properly ordered with other load/stores.
This commit is contained in:
Bennet Bleßmann
2024-07-06 14:49:19 +02:00
parent fcb41542c3
commit d87400afa0
6 changed files with 41 additions and 249 deletions

7
Cargo.lock generated
View File

@@ -108,6 +108,12 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "arcu"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8727c0fb4c436605c8f11c579ec86edcb729134aec4ee66e454efd99a91859f"
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.5.2" version = "0.5.2"
@@ -2558,6 +2564,7 @@ dependencies = [
name = "scryer-prolog" name = "scryer-prolog"
version = "0.9.4" version = "0.9.4"
dependencies = [ dependencies = [
"arcu",
"assert_cmd", "assert_cmd",
"base64 0.12.3", "base64 0.12.3",
"bit-set", "bit-set",

View File

@@ -74,6 +74,7 @@ static_assertions = "1.1.0"
serde_json = "1.0.95" serde_json = "1.0.95"
serde = "1.0.159" serde = "1.0.159"
arcu = { version = "0.1.1", features = ["thread_local_counter"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
crossterm = { version = "0.20.0", optional = true } crossterm = { version = "0.20.0", optional = true }

View File

@@ -7,12 +7,14 @@ use crate::machine::machine_indices::*;
use crate::machine::streams::*; use crate::machine::streams::*;
use crate::parser::char_reader::CharReader; use crate::parser::char_reader::CharReader;
use crate::raw_block::*; use crate::raw_block::*;
use crate::rcu::Rcu;
use crate::rcu::RcuRef;
use crate::read::*; use crate::read::*;
use crate::types::UntypedArenaPtr; use crate::types::UntypedArenaPtr;
use crate::parser::dashu::{Integer, Rational}; use crate::parser::dashu::{Integer, Rational};
use arcu::atomic::Arcu;
use arcu::epoch_counters::GlobalEpochCounterPool;
use arcu::rcu_ref::RcuRef;
use arcu::Rcu;
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use std::cell::UnsafeCell; use std::cell::UnsafeCell;
@@ -79,7 +81,7 @@ impl RawBlockTraits for F64Table {
#[derive(Debug)] #[derive(Debug)]
pub struct F64Table { pub struct F64Table {
block: Rcu<RawBlock<F64Table>>, block: Arcu<RawBlock<F64Table>, GlobalEpochCounterPool>,
update: Mutex<()>, update: Mutex<()>,
} }
@@ -93,7 +95,7 @@ pub fn lookup_float(
.upgrade() .upgrade()
.expect("We should only be looking up floats while there is a float table"); .expect("We should only be looking up floats while there is a float table");
RcuRef::try_map(f64table.block.active_epoch(), |raw_block| unsafe { RcuRef::try_map(f64table.block.read(), |raw_block| unsafe {
raw_block raw_block
.base .base
.add(offset.0) .add(offset.0)
@@ -118,7 +120,7 @@ impl F64Table {
atom_table atom_table
} else { } else {
let atom_table = Arc::new(Self { let atom_table = Arc::new(Self {
block: Rcu::new(RawBlock::new()), block: Arcu::new(RawBlock::new(), GlobalEpochCounterPool),
update: Mutex::new(()), update: Mutex::new(()),
}); });
*guard = Arc::downgrade(&atom_table); *guard = Arc::downgrade(&atom_table);
@@ -133,7 +135,7 @@ impl F64Table {
// we don't have an index table for lookups as AtomTable does so // we don't have an index table for lookups as AtomTable does so
// just get the epoch after we take the upgrade lock // just get the epoch after we take the upgrade lock
let mut block_epoch = self.block.active_epoch(); let mut block_epoch = self.block.read();
let mut ptr; let mut ptr;
@@ -143,7 +145,7 @@ impl F64Table {
if ptr.is_null() { if ptr.is_null() {
let new_block = block_epoch.grow_new().unwrap(); let new_block = block_epoch.grow_new().unwrap();
self.block.replace(new_block); self.block.replace(new_block);
block_epoch = self.block.active_epoch(); block_epoch = self.block.read();
} else { } else {
break; break;
} }

View File

@@ -2,7 +2,6 @@
use crate::parser::ast::MAX_ARITY; use crate::parser::ast::MAX_ARITY;
use crate::raw_block::*; use crate::raw_block::*;
use crate::rcu::{Rcu, RcuRef};
use crate::types::*; use crate::types::*;
use std::cmp::Ordering; use std::cmp::Ordering;
@@ -16,6 +15,10 @@ use std::sync::Mutex;
use std::sync::RwLock; use std::sync::RwLock;
use std::sync::Weak; use std::sync::Weak;
use arcu::atomic::Arcu;
use arcu::epoch_counters::GlobalEpochCounterPool;
use arcu::rcu_ref::RcuRef;
use arcu::Rcu;
use indexmap::IndexSet; use indexmap::IndexSet;
use scryer_modular_bitfield::prelude::*; use scryer_modular_bitfield::prelude::*;
@@ -180,7 +183,7 @@ impl Atom {
let atom_table = let atom_table =
arc_atom_table().expect("We should only have an Atom while there is an AtomTable"); arc_atom_table().expect("We should only have an Atom while there is an AtomTable");
AtomTableRef::try_map(atom_table.inner.active_epoch(), |buf| unsafe { AtomTableRef::try_map(atom_table.inner.read(), |buf| unsafe {
let ptr = buf let ptr = buf
.block .block
.base .base
@@ -278,17 +281,17 @@ impl Ord for Atom {
#[derive(Debug)] #[derive(Debug)]
pub struct InnerAtomTable { pub struct InnerAtomTable {
block: RawBlock<AtomTable>, block: RawBlock<AtomTable>,
pub table: Rcu<IndexSet<Atom>>, pub table: Arcu<IndexSet<Atom>, GlobalEpochCounterPool>,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct AtomTable { pub struct AtomTable {
inner: Rcu<InnerAtomTable>, inner: Arcu<InnerAtomTable, GlobalEpochCounterPool>,
// this lock is taking during resizing // this lock is taking during resizing
update: Mutex<()>, update: Mutex<()>,
} }
pub type AtomTableRef<M> = RcuRef<InnerAtomTable, M>; pub type AtomTableRef<M> = arcu::rcu_ref::RcuRef<InnerAtomTable, M>;
impl InnerAtomTable { impl InnerAtomTable {
#[inline(always)] #[inline(always)]
@@ -296,7 +299,7 @@ impl InnerAtomTable {
STATIC_ATOMS_MAP STATIC_ATOMS_MAP
.get(string) .get(string)
.cloned() .cloned()
.or_else(|| self.table.active_epoch().get(string).cloned()) .or_else(|| self.table.read().get(string).cloned())
} }
} }
@@ -314,10 +317,13 @@ impl AtomTable {
atom_table atom_table
} else { } else {
let atom_table = Arc::new(Self { let atom_table = Arc::new(Self {
inner: Rcu::new(InnerAtomTable { inner: Arcu::new(
InnerAtomTable {
block: RawBlock::new(), block: RawBlock::new(),
table: Rcu::new(IndexSet::new()), table: Arcu::new(IndexSet::new(), GlobalEpochCounterPool),
}), },
GlobalEpochCounterPool,
),
update: Mutex::new(()), update: Mutex::new(()),
}); });
*guard = Arc::downgrade(&atom_table); *guard = Arc::downgrade(&atom_table);
@@ -327,13 +333,13 @@ impl AtomTable {
} }
pub fn active_table(&self) -> RcuRef<IndexSet<Atom>, IndexSet<Atom>> { pub fn active_table(&self) -> RcuRef<IndexSet<Atom>, IndexSet<Atom>> {
self.inner.active_epoch().table.active_epoch() self.inner.read().table.read()
} }
pub fn build_with(atom_table: &AtomTable, string: &str) -> Atom { pub fn build_with(atom_table: &AtomTable, string: &str) -> Atom {
loop { loop {
let mut block_epoch = atom_table.inner.active_epoch(); let mut block_epoch = atom_table.inner.read();
let mut table_epoch = block_epoch.table.active_epoch(); let mut table_epoch = block_epoch.table.read();
if let Some(atom) = block_epoch.lookup_str(string) { if let Some(atom) = block_epoch.lookup_str(string) {
return atom; return atom;
@@ -342,10 +348,8 @@ impl AtomTable {
// take a lock to prevent concurrent updates // take a lock to prevent concurrent updates
let update_guard = atom_table.update.lock().unwrap(); let update_guard = atom_table.update.lock().unwrap();
let is_same_allocation = let is_same_allocation = RcuRef::same_epoch(&block_epoch, &atom_table.inner.read());
RcuRef::same_epoch(&block_epoch, &atom_table.inner.active_epoch()); let is_same_atom_list = RcuRef::same_epoch(&table_epoch, &block_epoch.table.read());
let is_same_atom_list =
RcuRef::same_epoch(&table_epoch, &block_epoch.table.active_epoch());
if !(is_same_allocation && is_same_atom_list) { if !(is_same_allocation && is_same_atom_list) {
// some other thread raced us between our lookup and // some other thread raced us between our lookup and
@@ -364,14 +368,14 @@ impl AtomTable {
if ptr.is_null() { if ptr.is_null() {
// garbage collection would go here // garbage collection would go here
let new_block = block_epoch.block.grow_new().unwrap(); let new_block = block_epoch.block.grow_new().unwrap();
let new_table = Rcu::new(table_epoch.clone()); let new_table = Arcu::new(table_epoch.clone(), GlobalEpochCounterPool);
let new_alloc = InnerAtomTable { let new_alloc = InnerAtomTable {
block: new_block, block: new_block,
table: new_table, table: new_table,
}; };
atom_table.inner.replace(new_alloc); atom_table.inner.replace(new_alloc);
block_epoch = atom_table.inner.active_epoch(); block_epoch = atom_table.inner.read();
table_epoch = block_epoch.table.active_epoch(); table_epoch = block_epoch.table.read();
} else { } else {
break ptr; break ptr;
} }

View File

@@ -42,8 +42,6 @@ pub mod types;
use instructions::instr; use instructions::instr;
mod rcu;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;

View File

@@ -1,220 +0,0 @@
use std::{
cell::OnceCell,
fmt::Debug,
mem::ManuallyDrop,
ops::Deref,
ptr::NonNull,
sync::{
atomic::{AtomicPtr, AtomicU8},
Arc, RwLock, Weak,
},
};
// the epoch counters of all threads that have ever accessed an Rcu
// threads that have finished will have a dangling Weak reference and can be cleand up
// having this be shared between all Rcu's is a tradeof,
// writes will be slower as more epoch counters need to be waited for
// reads should be faster as a thread only needs to register itself once on the first read
//
static EPOCH_COUNTERS: RwLock<Vec<Weak<AtomicU8>>> = RwLock::new(Vec::new());
thread_local! {
// odd value means the current thread is about to access the active_epoch of an Rcu
// a thread has a single epoch counter for all Rcu it accesses,
// as a thread can only access one Rcu at a time
static THREAD_EPOCH_COUNTER: OnceCell<Arc<AtomicU8>> = const { OnceCell::new() };
}
pub struct Rcu<T> {
active_value: AtomicPtr<T>,
}
impl<T: std::fmt::Debug> std::fmt::Debug for Rcu<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let active_epoch = self.active_epoch();
f.debug_struct("Rcu")
.field("active_value", &active_epoch)
.finish()
}
}
impl<T> Rcu<T> {
pub fn new(initial_value: T) -> Self {
Rcu {
active_value: AtomicPtr::new(Arc::into_raw(Arc::new(initial_value)).cast_mut()),
}
}
pub fn active_epoch(&self) -> RcuRef<T, T> {
THREAD_EPOCH_COUNTER.with(|epoch_counter| {
let epoch_counter = epoch_counter.get_or_init(|| {
let epoch_counter = Arc::new(AtomicU8::new(0));
// register the current threads epoch counter on init
EPOCH_COUNTERS
.write()
.unwrap()
.push(Arc::downgrade(&epoch_counter));
epoch_counter
});
let old = epoch_counter.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
assert!(old % 2 == 0, "Old Epoch counter value should be even!");
});
let arc_ptr = self.active_value.load(std::sync::atomic::Ordering::Acquire);
let arc = unsafe {
// Safety:
// - the ptr was created in Rcu::new or Rcu::replace with Arc::into_raw
// - the Rcu is responsible for of the arc's strong refrences
// - the Rcu is alive as this function takes a reference to the Rcu
// - replace will wait with decrementing the old values strong count until our epoich counter is even again
Arc::increment_strong_count(arc_ptr);
// Safety:
// - the ptr was created in Rcu::new or Rcu::replace with Arc::into_raw
// - we have just ensured an additional strong count by incrementing the count
Arc::from_raw(arc_ptr)
};
THREAD_EPOCH_COUNTER.with(|epoch_counter| {
let old = epoch_counter
.get().expect("we initialized the OnceCell when we incremented the epoch counter the fist time")
.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
assert!(old % 2 != 0, "Old Epoch counter value should be odd!");
});
RcuRef {
data: arc.deref().into(),
arc,
}
}
/*
* replace the Rcu'S content with a new value
*
* This does not syncronize write and last to update the active_value pointer wins,
* all writes that do not win will be lost, though not leaked.
* This will block untill the old value can be reclaimed,
* i.e. all threads whitnest to be in the read critical sections
* have been witnest to have left the critical section at least once
*/
pub fn replace(&self, new_value: T) {
let arc_ptr = self.active_value.swap(
Arc::into_raw(Arc::new(new_value)).cast_mut(),
std::sync::atomic::Ordering::AcqRel,
);
// maually drop as we need to ensure not to drop the arc while
// we have not witnest all threads to be or have been outside the read critical section
// i.e. even epoch counter or different odd epoch counter
// Safety:
// - the ptr was created in Rcu::new or Rcu::replace with Arc::into_raw
// - the Rcu itself holds one strong count
let arc = unsafe { ManuallyDrop::new(Arc::from_raw(arc_ptr)) };
let epochs = EPOCH_COUNTERS.read().unwrap().clone();
let mut epochs = epochs
.into_iter()
.flat_map(|elem| {
let arc = elem.upgrade()?;
let init_val = arc.load(std::sync::atomic::Ordering::Acquire);
if init_val % 2 == 0 {
// already even can be ignored
return None;
}
// odd initial value thread is in read critical section
// need to wait for the value to change before we can drop the arc
Some((init_val, elem))
})
.collect::<Vec<_>>();
while !epochs.is_empty() {
epochs.retain(|elem| {
let Some(arc) = elem.1.upgrade() else {
// as the thread is dead it can't have a ref to old arc
return false;
};
// the epoch counter has not changed so the thread is still in the same instance of the critical section
// any different value is ok as
// - even values indicate the thread is outside the critical section
// - a diffrent odd value indicates the thread has left the critical section and can subsequently only read the new active_value
arc.load(std::sync::atomic::Ordering::Acquire) == elem.0
})
}
// Safety:
// - we have not dropped the arc another way
// - we witnessed all threads either with an even epoch count or with a new odd count
// as such they must have left the critical section at some point
ManuallyDrop::into_inner(arc);
}
}
pub struct RcuRef<T, M>
where
T: ?Sized,
M: ?Sized,
{
arc: Arc<T>,
data: NonNull<M>,
}
impl<T: ?Sized, M: ?Sized + Debug> Debug for RcuRef<T, M> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RcuRef")
.field("data", &self.deref())
.finish()
}
}
// use assoiated functions rather than methods so that we don't overlap
// with functions of the Deref Target type
impl<T: ?Sized, M: ?Sized> RcuRef<T, M> {
pub fn map<N: ?Sized, F: for<'a> FnOnce(&'a M) -> &'a N>(referece: Self, f: F) -> RcuRef<T, N> {
RcuRef {
arc: referece.arc,
data: f(unsafe { referece.data.as_ref() }).into(),
}
}
pub fn try_map<N: ?Sized, F: for<'a> FnOnce(&'a M) -> Option<&'a N>>(
referece: Self,
f: F,
) -> Option<RcuRef<T, N>> {
let val = f(unsafe { referece.data.as_ref() })?;
Some(RcuRef {
arc: Arc::clone(&referece.arc),
data: val.into(),
})
}
pub fn same_epoch<M2>(this: &Self, other: &RcuRef<T, M2>) -> bool {
Arc::ptr_eq(&this.arc, &other.arc)
}
pub fn ptr_eq(this: &Self, other: &Self) -> bool {
std::ptr::addr_eq(this.data.as_ptr(), other.data.as_ptr())
}
pub fn clone(this: &Self) -> Self {
Self {
arc: Arc::clone(&this.arc),
data: this.data,
}
}
pub fn get_root(this: &Self) -> &T {
&this.arc
}
}
impl<T: ?Sized, M: ?Sized> Deref for RcuRef<T, M> {
type Target = M;
fn deref(&self) -> &Self::Target {
// Safety: The pointer points into the arc we are holding
// while we are alive so is the target
// as the content is in an Rcu no mutable acess is given out
unsafe { self.data.as_ref() }
}
}