use parking_lot::Mutex;
use std::backtrace::Backtrace;
use std::cell::Cell;
use std::collections::HashMap;
use std::fmt::Write;
use std::sync::atomic::{AtomicU64, Ordering};
use std::thread::ThreadId;
use crate::implementation::LockMetadata;
use crate::synchronizer::{SynchronizerReadLock, SynchronizerType, SynchronizerWriteLock};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum Mode {
Shared,
Exclusive,
}
impl std::fmt::Display for Mode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Mode::Shared => f.write_str("shared (read)"),
Mode::Exclusive => f.write_str("exclusive (write)"),
}
}
}
pub(crate) struct LockId(AtomicU64);
static NEXT_LOCK_ID: AtomicU64 = AtomicU64::new(1);
static NEXT_HELD_SEQ: AtomicU64 = AtomicU64::new(1);
impl LockId {
pub(crate) const fn unassigned() -> Self {
LockId(AtomicU64::new(0))
}
fn get(&self) -> u64 {
match self.0.load(Ordering::Relaxed) {
0 => {
let new = NEXT_LOCK_ID.fetch_add(1, Ordering::Relaxed);
match self
.0
.compare_exchange(0, new, Ordering::Relaxed, Ordering::Relaxed)
{
Ok(_) => new,
Err(existing) => existing,
}
}
id => id,
}
}
}
impl Drop for LockId {
fn drop(&mut self) {
let id = *self.0.get_mut();
if id != 0 {
forget_lock(id);
}
}
}
fn forget_lock(id: u64) {
let mut guard = STATE.lock();
let Some(state) = guard.as_mut() else {
return;
};
state.names.remove(&id);
state.edges.remove(&id);
state.edges.retain(|_, targets| {
targets.remove(&id);
!targets.is_empty()
});
}
pub(crate) struct HeldToken {
thread: ThreadId,
seq: u64,
}
struct HeldEntry {
id: u64,
seq: u64,
mode: Mode,
}
#[derive(Default)]
struct DetectorState {
held: HashMap<ThreadId, Vec<HeldEntry>>,
edges: HashMap<u64, HashMap<u64, Backtrace>>,
names: HashMap<u64, &'static str>,
}
static STATE: Mutex<Option<DetectorState>> = Mutex::new(None);
#[cfg(test)]
fn state_sizes() -> (usize, usize, usize) {
let guard = STATE.lock();
match guard.as_ref() {
Some(state) => (
state.edges.len(),
state.edges.values().map(|t| t.len()).sum(),
state.names.len(),
),
None => (0, 0, 0),
}
}
thread_local! {
static ABORTING: Cell<bool> = const { Cell::new(false) };
}
fn set_aborting() {
ABORTING.with(|c| c.set(true));
}
fn clear_aborting() {
ABORTING.with(|c| c.set(false));
}
pub(crate) fn is_aborting() -> bool {
ABORTING.with(Cell::get)
}
fn tracked_mode(sync_type: SynchronizerType, write: bool) -> Option<Mode> {
match sync_type {
SynchronizerType::Arc | SynchronizerType::Rcu => None,
SynchronizerType::RwLock => Some(if write { Mode::Exclusive } else { Mode::Shared }),
SynchronizerType::Mutex => Some(Mode::Exclusive),
}
}
pub(crate) fn check_blocking_acquire(
metadata: &LockMetadata,
sync_type: SynchronizerType,
write: bool,
name: &'static str,
) {
clear_aborting();
let Some(mode) = tracked_mode(sync_type, write) else {
return;
};
let id = metadata.lock_id.get();
let thread = std::thread::current().id();
let mut guard = STATE.lock();
let state = guard.get_or_insert_with(Default::default);
state.names.entry(id).or_insert(name);
let mut held_ids: Vec<u64> = Vec::new();
let mut relock: Option<Mode> = None;
if let Some(held) = state.held.get(&thread) {
for entry in held {
if entry.id == id {
relock = Some(entry.mode);
}
if !held_ids.contains(&entry.id) {
held_ids.push(entry.id);
}
}
}
if let Some(held_mode) = relock {
let msg = relock_message(state, id, held_mode, mode);
drop(guard);
set_aborting();
panic!("{}", msg);
}
for from in held_ids {
if let Some(targets) = state.edges.get(&from) {
if targets.contains_key(&id) {
continue;
}
}
if let Some(path) = find_path(&state.edges, id, from) {
let msg = cycle_message(state, from, id, mode, &path);
drop(guard);
set_aborting();
panic!("{}", msg);
}
state
.edges
.entry(from)
.or_default()
.insert(id, Backtrace::force_capture());
}
}
fn register_held(metadata: &LockMetadata, mode: Mode) -> HeldToken {
clear_aborting();
let id = metadata.lock_id.get();
let thread = std::thread::current().id();
let seq = NEXT_HELD_SEQ.fetch_add(1, Ordering::Relaxed);
let mut guard = STATE.lock();
let state = guard.get_or_insert_with(Default::default);
state
.held
.entry(thread)
.or_default()
.push(HeldEntry { id, seq, mode });
HeldToken { thread, seq }
}
pub(crate) fn release_held(token: HeldToken) {
let mut guard = STATE.lock();
let Some(state) = guard.as_mut() else {
return;
};
if let Some(entries) = state.held.get_mut(&token.thread) {
if let Some(pos) = entries.iter().rposition(|e| e.seq == token.seq) {
entries.remove(pos);
}
if entries.is_empty() {
state.held.remove(&token.thread);
}
}
}
pub(crate) fn read_guard_token<T: ?Sized>(
lock: &SynchronizerReadLock<LockMetadata, T>,
) -> Option<HeldToken> {
match lock {
SynchronizerReadLock::Arc(..) | SynchronizerReadLock::ReadCopyUpdate(..) => None,
SynchronizerReadLock::RwLock(metadata, _) => Some(register_held(metadata, Mode::Shared)),
SynchronizerReadLock::Mutex(metadata, _) => Some(register_held(metadata, Mode::Exclusive)),
}
}
pub(crate) fn write_guard_token<T: ?Sized>(
lock: &SynchronizerWriteLock<LockMetadata, T>,
) -> Option<HeldToken> {
match lock {
SynchronizerWriteLock::Arc(..) | SynchronizerWriteLock::ReadCopyUpdate(..) => None,
SynchronizerWriteLock::RwLock(metadata, _) | SynchronizerWriteLock::Mutex(metadata, _) => {
Some(register_held(metadata, Mode::Exclusive))
}
}
}
fn find_path(
edges: &HashMap<u64, HashMap<u64, Backtrace>>,
from: u64,
to: u64,
) -> Option<Vec<u64>> {
let mut parents: HashMap<u64, u64> = HashMap::new();
let mut queue = std::collections::VecDeque::new();
queue.push_back(from);
while let Some(node) = queue.pop_front() {
if node == to {
let mut path = vec![to];
let mut cur = to;
while cur != from {
cur = parents[&cur];
path.push(cur);
}
path.reverse();
return Some(path);
}
if let Some(targets) = edges.get(&node) {
for &next in targets.keys() {
if next != from && !parents.contains_key(&next) {
parents.insert(next, node);
queue.push_back(next);
}
}
}
}
None
}
fn render_backtrace(backtrace: &Backtrace) -> String {
let full = backtrace.to_string();
let mut frames: Vec<Vec<&str>> = Vec::new();
for line in full.lines() {
let is_frame_start = line
.trim_start()
.split_once(':')
.is_some_and(|(n, _)| !n.is_empty() && n.chars().all(|c| c.is_ascii_digit()));
if is_frame_start || frames.is_empty() {
frames.push(vec![line]);
} else {
frames.last_mut().expect("non-empty").push(line);
}
}
let is_internal = |f: &Vec<&str>| {
f.iter().any(|l| {
l.contains("backtrace") && !l.contains("__rust_begin_short_backtrace")
|| l.contains("keepcalm::")
})
};
let start = frames.iter().position(|f| !is_internal(f)).unwrap_or(0);
let first_scaffold = frames.iter().position(|f| {
f.iter()
.any(|l| l.contains("__rust_begin_short_backtrace") || l.contains("lang_start"))
});
let end = first_scaffold.unwrap_or(frames.len());
if start >= end {
return full;
}
frames[start..end]
.iter()
.flat_map(|f| f.iter().copied())
.collect::<Vec<_>>()
.join("\n")
}
fn label(state: &DetectorState, id: u64) -> String {
match state.names.get(&id) {
Some(name) => format!("#{id} ({name})"),
None => format!("#{id}"),
}
}
fn relock_message(state: &DetectorState, id: u64, held_mode: Mode, mode: Mode) -> String {
let consequence = if held_mode == Mode::Shared && mode == Mode::Shared {
"This may deadlock: parking_lot read locks are fair and will block a \
recursive read whenever a writer is waiting."
} else {
"This will deadlock."
};
let lock = label(state, id);
format!(
"keepcalm deadlock detection: lock {lock} is already held by this thread!\n\
\x20 currently held as: {held_mode}\n\
\x20 attempting to re-acquire as: {mode}\n\
\x20 {consequence}\n\
\x20 Note that a projection shares the lock of its root container."
)
}
fn cycle_message(
state: &DetectorState,
held: u64,
acquiring: u64,
mode: Mode,
path: &[u64],
) -> String {
let acquiring_label = label(state, acquiring);
let held_label = label(state, held);
let mut msg = format!(
"keepcalm deadlock detection: lock-order cycle detected!\n\
\x20 this thread is about to acquire lock {acquiring_label} ({mode}) while holding lock {held_label},\n\
\x20 but the opposite ordering has already been observed:\n"
);
for pair in path.windows(2) {
let (from, to) = (pair[0], pair[1]);
let (from_label, to_label) = (label(state, from), label(state, to));
let _ = write!(
msg,
"\n lock {to_label} was acquired while lock {from_label} was held, first observed at:\n"
);
match state.edges.get(&from).and_then(|m| m.get(&to)) {
Some(backtrace) => {
let _ = writeln!(msg, "{}", render_backtrace(backtrace));
}
None => msg.push_str(" <backtrace unavailable>\n"),
}
}
msg.push_str(
"\n Acquiring these locks in a consistent order on every code path will fix this.",
);
msg
}
#[cfg(test)]
mod test {
use crate::{Shared, SharedMut};
#[test]
fn consistent_order_is_silent() {
let a = SharedMut::new(1);
let b = SharedMut::new(2);
for _ in 0..3 {
let _ga = a.write();
let _gb = b.write();
}
}
#[test]
#[should_panic(expected = "lock-order cycle detected")]
fn inverted_order_panics() {
let a = SharedMut::new(1);
let b = SharedMut::new(2);
{
let _ga = a.write();
let _gb = b.write();
}
let _gb = b.write();
let _ga = a.write();
}
#[test]
#[should_panic(expected = "lock-order cycle detected")]
fn transitive_cycle_panics() {
let a = SharedMut::new(1);
let b = SharedMut::new(2);
let c = SharedMut::new(3);
{
let _ga = a.write();
let _gb = b.write();
}
{
let _gb = b.write();
let _gc = c.write();
}
let _gc = c.write();
let _ga = a.write();
}
#[test]
#[should_panic(expected = "already held by this thread")]
fn rwlock_reentrant_read_panics() {
let a = SharedMut::new(1);
let _g1 = a.read();
let _g2 = a.read();
}
#[test]
#[should_panic(expected = "already held by this thread")]
fn projection_shares_root_lock() {
let root = SharedMut::new(("hello".to_string(), 1usize));
let projected = root.project(crate::project!(x: (String, usize), x.0));
let _root_guard = root.read();
let _proj_guard = projected.read();
}
#[test]
#[should_panic(expected = "lock-order cycle detected")]
fn projection_participates_in_cycles() {
let a = SharedMut::new((1, 2));
let a0 = a.project(crate::project!(x: (i32, i32), x.0));
let b = SharedMut::new(3);
{
let _ga = a0.write();
let _gb = b.write();
}
let _gb = b.write();
let _ga = a.write();
}
#[test]
fn try_lock_is_exempt() {
let a = SharedMut::new(1);
let b = SharedMut::new(2);
{
let _ga = a.write();
let _gb = b.try_write().expect("uncontended");
}
{
let _gb = b.write();
let _ga = a.write();
}
let _gb = b.try_write().expect("uncontended");
let _ga = a.write();
}
#[test]
fn arc_and_rcu_are_exempt() {
let arc = Shared::new(1);
let rcu = SharedMut::new_rcu(2);
let a = SharedMut::new(3);
let _g1 = arc.read();
let _g2 = rcu.read();
let _g3 = rcu.write();
let _g4 = arc.read();
let _g5 = a.write();
let _g6 = rcu.write();
}
#[test]
fn cross_thread_drop_releases() {
let a = SharedMut::new(1);
let guard = a.write_send();
std::thread::scope(|s| {
s.spawn(move || drop(guard));
});
let _g = a.write();
}
#[test]
#[should_panic(expected = "lock-order cycle detected")]
fn set_participates_in_order_detection() {
let a = SharedMut::new(1);
let b = SharedMut::new(2);
{
let _ga = a.write();
b.set(20); }
let _gb = b.write();
a.set(10); }
#[test]
#[should_panic(expected = "already held by this thread")]
fn const_global_projection_shares_root_lock() {
use crate::SharedGlobalMut;
static G: SharedGlobalMut<(u32, u32)> = SharedGlobalMut::new((1, 2));
static P0: crate::SharedMut<u32> = crate::project_global!(mut G => 0);
let _root = G.read();
let _proj = P0.read();
}
#[test]
fn cycle_panic_does_not_poison_held_lock() {
let a = SharedMut::new(1);
let b = SharedMut::new(2);
{
let _ga = a.write();
let _gb = b.write();
}
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _gb = b.write();
let _ga = a.write(); }));
assert!(result.is_err(), "expected a lockdep panic");
assert_eq!(*b.read(), 2);
assert_eq!(*a.read(), 1);
*b.write() = 20;
assert_eq!(*b.read(), 20);
}
#[test]
fn report_names_locks_by_type() {
struct AlphaState(#[allow(dead_code)] u32);
struct BetaState(#[allow(dead_code)] u32);
let a = SharedMut::new(AlphaState(1));
let b = SharedMut::new(BetaState(2));
{
let _ga = a.write();
let _gb = b.write();
}
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _gb = b.write();
let _ga = a.write();
}));
let err = result.expect_err("expected a lockdep panic");
let msg = err
.downcast_ref::<String>()
.expect("panic message should be a String");
assert!(
msg.contains("AlphaState"),
"message should name AlphaState: {msg}"
);
assert!(
msg.contains("BetaState"),
"message should name BetaState: {msg}"
);
}
#[test]
fn dropped_locks_are_forgotten() {
let (_, edges_before, names_before) = super::state_sizes();
for _ in 0..1000 {
let a = SharedMut::new(1);
let b = SharedMut::new(2);
let _ga = a.write();
let _gb = b.write();
}
let (_, edges_after, names_after) = super::state_sizes();
assert!(
edges_after <= edges_before + 100,
"edges leaked across churn: {edges_before} -> {edges_after}"
);
assert!(
names_after <= names_before + 100,
"names leaked across churn: {names_before} -> {names_after}"
);
}
#[test]
fn global_locks_are_tracked() {
use crate::SharedGlobalMut;
static GLOBAL: SharedGlobalMut<usize> = SharedGlobalMut::new(1);
let a = SharedMut::new(2);
for _ in 0..2 {
let _g1 = GLOBAL.write();
let _g2 = a.write();
}
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _g2 = a.write();
let _g1 = GLOBAL.write();
}));
let err = result.expect_err("expected a lock-order cycle panic");
let msg = err
.downcast_ref::<String>()
.expect("panic message should be a String");
assert!(msg.contains("lock-order cycle detected"), "message: {msg}");
}
}