Skip to content

Commit afdb725

Browse files
committed
Add distinction between 'reset' and 'lock not acquired' for mutex lock. Improve comments. Add mutex deadlock test using std threads.
1 parent 9ab5846 commit afdb725

File tree

8 files changed

+346
-16
lines changed

8 files changed

+346
-16
lines changed

genmc-sys/cpp/include/MiriInterface.hpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,11 +366,18 @@ inline CompareExchangeResult from_error(std::unique_ptr<std::string> error) {
366366

367367
namespace MutexLockResultExt {
368368
inline MutexLockResult ok(bool is_lock_acquired) {
369-
return MutexLockResult { /* error: */ nullptr, is_lock_acquired };
369+
return MutexLockResult { /* error: */ nullptr, /* is_reset: */ false, is_lock_acquired };
370+
}
371+
372+
inline MutexLockResult reset() {
373+
return MutexLockResult { /* error: */ nullptr,
374+
/* is_reset: */ true,
375+
/* is_lock_acquired: */ false };
370376
}
371377

372378
inline MutexLockResult from_error(std::unique_ptr<std::string> error) {
373379
return MutexLockResult { /* error: */ std::move(error),
380+
/* is_reset: */ false,
374381
/* is_lock_acquired: */ false };
375382
}
376383
} // namespace MutexLockResultExt

genmc-sys/cpp/src/MiriInterface/Mutex.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ auto MiriGenmcShim::handle_mutex_lock(ThreadId thread_id, uint64_t address, uint
4848
// ("Mutex already locked"), Miri should call the handle function again once the current thread
4949
// is scheduled by GenMC the next time.
5050
if (std::holds_alternative<Reset>(load_ret))
51-
return MutexLockResultExt::ok(false);
51+
return MutexLockResultExt::reset();
5252

5353
const auto* ret_val = std::get_if<SVal>(&load_ret);
5454
ERROR_ON(!ret_val, "Unimplemented: mutex lock returned unexpected result.");
@@ -77,7 +77,8 @@ auto MiriGenmcShim::handle_mutex_lock(ThreadId thread_id, uint64_t address, uint
7777
);
7878
} else {
7979
// We did not acquire the mutex, so we tell GenMC to block the thread until we can acquire
80-
// it.
80+
// it. GenMC determines this based on the annotation we pass with the load further up in
81+
// this function, namely when that load will read a value other than `MUTEX_LOCKED`.
8182
GenMCDriver::handleAssume(inc_pos(thread_id), AssumeType::Spinloop);
8283
}
8384
return MutexLockResultExt::ok(is_lock_acquired);

genmc-sys/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,8 @@ mod ffi {
259259
struct MutexLockResult {
260260
/// If there was an error, it will be stored in `error`, otherwise it is `None`.
261261
error: UniquePtr<CxxString>,
262+
/// If true, GenMC determined that we should retry the mutex lock operation once the thread attempting to lock is scheduled again.
263+
is_reset: bool,
262264
/// Indicate whether the lock was acquired by this thread.
263265
is_lock_acquired: bool,
264266
}

src/concurrency/genmc/intercept.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use rustc_middle::{throw_unsup_format, ty};
1+
use rustc_middle::ty;
22
use tracing::debug;
33

44
use crate::concurrency::genmc::MAX_ACCESS_SIZE;
@@ -42,13 +42,18 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {
4242
// FIXME(genmc): improve error handling.
4343
throw_ub_format!("{}", error.to_string_lossy());
4444
}
45+
debug!(
46+
"GenMC: Mutex::lock(): is_reset: {}, is_lock_acquired: {}",
47+
result.is_reset, result.is_lock_acquired
48+
);
4549
if result.is_lock_acquired {
46-
debug!("GenMC: handling Mutex::lock: success: lock acquired.");
4750
return interp_ok(());
4851
}
49-
debug!("GenMC: handling Mutex::lock failed, blocking thread");
52+
// If we did not acquire the mutex and GenMC did not tell us to reset and try again, we have a deadlock.
53+
if !result.is_reset {
54+
throw_machine_stop!(TerminationInfo::Deadlock);
55+
}
5056
// NOTE: We don't write anything back to Miri's memory, the Mutex state is handled only by GenMC.
51-
5257
this.block_thread(
5358
crate::BlockReason::Genmc,
5459
None,
@@ -72,10 +77,10 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {
7277
// FIXME(genmc): improve error handling.
7378
throw_ub_format!("{}", error.to_string_lossy());
7479
}
75-
// FIXME(genmc): The reported error message is bad, it does not point to the second lock call involved in the deadlock.
76-
// FIXME(genmc): there can be cases where not acquiring a mutex after the second attempt is *not* a deadlock. Reliably detecting deadlocks requires extra analysis (in GenMC).
80+
assert!(!result.is_reset, "We should not get a reset on the second lock attempt.");
81+
// If we did not acquire the mutex and GenMC did not tell us to reset and try again, we have a deadlock.
7782
if !result.is_lock_acquired {
78-
throw_unsup_format!("Could not lock Mutex, which may indicate a deadlock. (GenMC mode does not fully support deadlock detection yet).")
83+
throw_machine_stop!(TerminationInfo::Deadlock);
7984
}
8085
interp_ok(())
8186
}
@@ -107,7 +112,11 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {
107112
// FIXME(genmc): improve error handling.
108113
throw_ub_format!("{}", error.to_string_lossy());
109114
}
110-
debug!("GenMC: Mutex::try_lock(): is_lock_acquired: {}", result.is_lock_acquired);
115+
debug!(
116+
"GenMC: Mutex::try_lock(): is_reset: {}, is_lock_acquired: {}",
117+
result.is_reset, result.is_lock_acquired
118+
);
119+
assert!(!result.is_reset);
111120
// Write the return value of try_lock, i.e., whether we acquired the mutex.
112121
this.write_scalar(Scalar::from_bool(result.is_lock_acquired), dest)?;
113122
// NOTE: We don't write anything back to Miri's memory where the Mutex is located, that state is handled only by GenMC.

tests/genmc/fail/intercept/mutex_deadlock.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//@compile-flags: -Zmiri-genmc -Zmiri-disable-stacked-borrows
2-
//@error-in-other-file: unsupported operation
2+
//@error-in-other-file: the evaluated program deadlocked
3+
//@error-in-other-file: the evaluated program deadlocked
4+
//@error-in-other-file: the evaluated program deadlocked
35

46
// Test that we can detect a deadlock involving `std::sync::Mutex` in GenMC mode.
57
// FIXME(genmc): The error message for such deadlocks is currently not good, it does not show both involved lock call locations.

tests/genmc/fail/intercept/mutex_deadlock.stderr

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,64 @@
11
Running GenMC Verification...
2-
error: unsupported operation: Could not lock Mutex, which may indicate a deadlock. (GenMC mode does not fully support deadlock detection yet).
2+
error: the evaluated program deadlocked
33
--> RUSTLIB/std/src/sync/poison/mutex.rs:LL:CC
44
|
55
LL | self.inner.lock();
6-
| ^ unsupported operation occurred here
6+
| ^^^^^^^^^^^^^^^^^ this thread got stuck here
7+
|
8+
= note: BACKTRACE on thread `unnamed-ID`:
9+
= note: inside `std::sync::Mutex::<u64>::lock` at RUSTLIB/std/src/sync/poison/mutex.rs:LL:CC
10+
note: inside closure
11+
--> tests/genmc/fail/intercept/mutex_deadlock.rs:LL:CC
12+
|
13+
LL | let mut y = Y.lock().unwrap();
14+
| ^^^^^^^^
15+
= note: inside `<std::boxed::Box<{closure@tests/genmc/fail/intercept/mutex_deadlock.rs:LL:CC}> as std::ops::FnOnce<()>>::call_once` at RUSTLIB/alloc/src/boxed.rs:LL:CC
16+
note: inside `genmc::spawn_pthread_closure::thread_func::<{closure@tests/genmc/fail/intercept/mutex_deadlock.rs:LL:CC}>`
17+
--> tests/genmc/fail/intercept/../../../utils/genmc.rs:LL:CC
18+
|
19+
LL | f();
20+
| ^^^
21+
22+
error: the evaluated program deadlocked
23+
--> tests/genmc/fail/intercept/../../../utils/genmc.rs:LL:CC
24+
|
25+
LL | if unsafe { libc::pthread_join(thread_id, std::ptr::null_mut()) } != 0 {
26+
| ^ this thread got stuck here
27+
|
28+
= note: BACKTRACE:
29+
= note: inside `genmc::join_pthread` at tests/genmc/fail/intercept/../../../utils/genmc.rs:LL:CC
30+
note: inside closure
31+
--> tests/genmc/fail/intercept/../../../utils/genmc.rs:LL:CC
32+
|
33+
LL | let _ = thread_ids.map(|id| join_pthread(id));
34+
| ^^^^^^^^^^^^^^^^
35+
= note: inside closure at RUSTLIB/core/src/ops/try_trait.rs:LL:CC
36+
= note: inside `<std::iter::Map<std::array::drain::Drain<'_, u64>, {closure@std::ops::try_trait::NeverShortCircuit<()>::wrap_mut_1<u64, {closure@tests/genmc/fail/intercept/../../../utils/genmc.rs:LL:CC}>::{closure#0}}> as std::iter::traits::unchecked_iterator::UncheckedIterator>::next_unchecked` at RUSTLIB/core/src/iter/adapters/map.rs:LL:CC
37+
= note: inside closure at RUSTLIB/core/src/array/mod.rs:LL:CC
38+
= note: inside `std::array::try_from_fn_erased::<(), std::ops::try_trait::NeverShortCircuit<()>, {closure@std::array::try_from_trusted_iterator::next<std::ops::try_trait::NeverShortCircuit<()>, std::iter::Map<std::array::drain::Drain<'_, u64>, {closure@std::ops::try_trait::NeverShortCircuit<()>::wrap_mut_1<u64, {closure@tests/genmc/fail/intercept/../../../utils/genmc.rs:LL:CC}>::{closure#0}}>>::{closure#0}}>` at RUSTLIB/core/src/array/mod.rs:LL:CC
39+
= note: inside `std::array::try_from_fn::<std::ops::try_trait::NeverShortCircuit<()>, 2, {closure@std::array::try_from_trusted_iterator::next<std::ops::try_trait::NeverShortCircuit<()>, std::iter::Map<std::array::drain::Drain<'_, u64>, {closure@std::ops::try_trait::NeverShortCircuit<()>::wrap_mut_1<u64, {closure@tests/genmc/fail/intercept/../../../utils/genmc.rs:LL:CC}>::{closure#0}}>>::{closure#0}}>` at RUSTLIB/core/src/array/mod.rs:LL:CC
40+
= note: inside `std::array::try_from_trusted_iterator::<(), std::ops::try_trait::NeverShortCircuit<()>, 2, std::iter::Map<std::array::drain::Drain<'_, u64>, {closure@std::ops::try_trait::NeverShortCircuit<()>::wrap_mut_1<u64, {closure@tests/genmc/fail/intercept/../../../utils/genmc.rs:LL:CC}>::{closure#0}}>>` at RUSTLIB/core/src/array/mod.rs:LL:CC
41+
= note: inside closure at RUSTLIB/core/src/array/mod.rs:LL:CC
42+
= note: inside `std::array::drain::drain_array_with::<u64, std::ops::try_trait::NeverShortCircuit<[(); 2]>, 2, {closure@std::array::<impl [u64; 2]>::try_map<std::ops::try_trait::NeverShortCircuit<()>, {closure@std::ops::try_trait::NeverShortCircuit<()>::wrap_mut_1<u64, {closure@tests/genmc/fail/intercept/../../../utils/genmc.rs:LL:CC}>::{closure#0}}>::{closure#0}}>` at RUSTLIB/core/src/array/drain.rs:LL:CC
43+
= note: inside `std::array::<impl [u64; 2]>::try_map::<std::ops::try_trait::NeverShortCircuit<()>, {closure@std::ops::try_trait::NeverShortCircuit<()>::wrap_mut_1<u64, {closure@tests/genmc/fail/intercept/../../../utils/genmc.rs:LL:CC}>::{closure#0}}>` at RUSTLIB/core/src/array/mod.rs:LL:CC
44+
= note: inside `std::array::<impl [u64; 2]>::map::<{closure@tests/genmc/fail/intercept/../../../utils/genmc.rs:LL:CC}, ()>` at RUSTLIB/core/src/array/mod.rs:LL:CC
45+
note: inside `genmc::join_pthreads::<TAG>`
46+
--> tests/genmc/fail/intercept/../../../utils/genmc.rs:LL:CC
47+
|
48+
LL | let _ = thread_ids.map(|id| join_pthread(id));
49+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
50+
note: inside `miri_start`
51+
--> tests/genmc/fail/intercept/mutex_deadlock.rs:LL:CC
52+
|
53+
LL | join_pthreads([t0, t1]);
54+
| ^^^^^^^^^^^^^^^^^^^^^^^
55+
56+
error: the evaluated program deadlocked
57+
--> RUSTLIB/std/src/sync/poison/mutex.rs:LL:CC
58+
|
59+
LL | self.inner.lock();
60+
| ^ this thread got stuck here
761
|
8-
= help: this is likely not a bug in the program; it indicates that the program performed an operation that Miri does not support
962
= note: BACKTRACE on thread `unnamed-ID`:
1063
= note: inside `std::sync::Mutex::<u64>::lock` at RUSTLIB/std/src/sync/poison/mutex.rs:LL:CC
1164
note: inside closure
@@ -22,5 +75,5 @@ LL | f();
2275

2376
note: add `-Zmiri-genmc-print-genmc-output` to MIRIFLAGS to see the detailed GenMC error report
2477

25-
error: aborting due to 1 previous error
78+
error: aborting due to 3 previous errors
2679

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//@compile-flags: -Zmiri-genmc -Zmiri-disable-stacked-borrows
2+
//@error-in-other-file: the evaluated program deadlocked
3+
//@error-in-other-file: the evaluated program deadlocked
4+
//@error-in-other-file: the evaluated program deadlocked
5+
6+
// Test that we can detect a deadlock involving `std::sync::Mutex` in GenMC mode.
7+
// FIXME(genmc): The error message for such deadlocks is currently not good, it does not show both involved lock call locations.
8+
9+
use std::sync::Mutex;
10+
11+
static X: Mutex<u64> = Mutex::new(0);
12+
static Y: Mutex<u64> = Mutex::new(0);
13+
14+
fn main() {
15+
let t0 = std::thread::spawn(|| {
16+
let mut x = X.lock().unwrap();
17+
let mut y = Y.lock().unwrap();
18+
*x += 1;
19+
*y += 1;
20+
});
21+
let t1 = std::thread::spawn(|| {
22+
let mut y = Y.lock().unwrap();
23+
let mut x = X.lock().unwrap();
24+
*x += 1;
25+
*y += 1;
26+
});
27+
t0.join().unwrap();
28+
t1.join().unwrap();
29+
}

0 commit comments

Comments
 (0)