Skip to content

Commit dc87a81

Browse files
authored
Merge pull request #4591 from Patrick-6/miri-genmc-assume
Improve GenMC mode scheduling and add assume statements
2 parents 785308c + f190897 commit dc87a81

17 files changed

+403
-80
lines changed

doc/genmc.md

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
# **(WIP)** Documentation for Miri-GenMC
22

3+
**NOTE: GenMC mode is not yet fully implemented, and has [several correctness issues](https://github.com/rust-lang/miri/issues/4572). Using GenMC mode currently requires manually compiling Miri, see [Usage](#usage).**
4+
5+
36
[GenMC](https://github.com/MPI-SWS/genmc) is a stateless model checker for exploring concurrent executions of a program.
47
Miri-GenMC integrates that model checker into Miri.
58

6-
**NOTE: Currently, no actual GenMC functionality is part of Miri, this is still WIP.**
9+
Miri in GenMC mode takes a program as input like regular Miri, but instead of running it once, the program is executed repeatedly, until all possible executions allowed by the Rust memory model are explored.
10+
This includes all possible thread interleavings and all allowed return values for atomic operations, including cases that are very rare to encounter on actual hardware.
11+
(However, this does not include other sources of non-determinism, such as the absolute addresses of allocations.
12+
It is hence still possible to have latent bugs in a test case even if they passed GenMC.)
713

8-
<!-- FIXME(genmc): add explanation. -->
14+
GenMC requires the input program to be bounded, i.e., have finitely many possible executions, otherwise it will not terminate.
15+
Any loops that may run infinitely must be replaced or bounded (see below).
16+
17+
GenMC makes use of Dynamic Partial Order Reduction (DPOR) to reduce the number of executions that must be explored, but the runtime can still be super-exponential in the size of the input program (number of threads and amount of interaction between threads).
18+
Large programs may not be verifiable in a reasonable amount of time.
919

1020
## Usage
1121

1222
For testing/developing Miri-GenMC:
23+
- install all [dependencies required by GenMC](https://github.com/MPI-SWS/genmc?tab=readme-ov-file#dependencies)
1324
- clone the Miri repo.
1425
- build Miri-GenMC with `./miri build --features=genmc`.
1526
- OR: install Miri-GenMC in the current system with `./miri install --features=genmc`
@@ -50,6 +61,66 @@ Note that `cargo miri test` in GenMC mode is currently not supported.
5061

5162
<!-- FIXME(genmc): add tips for using Miri-GenMC more efficiently. -->
5263

64+
### Eliminating unbounded loops
65+
66+
As mentioned above, GenMC requires all loops to be bounded.
67+
Otherwise, it is not possible to exhaustively explore all executions.
68+
Currently, Miri-GenMC has no support for automatically bounding loops, so this needs to be done manually.
69+
70+
#### Bounding loops without side effects
71+
72+
The easiest case is that of a loop that simply spins until it observes a certain condition, without any side effects.
73+
Such loops can be limited to one iteration, as demonstrated by the following example:
74+
75+
```rust
76+
#[cfg(miri)]
77+
unsafe extern "Rust" {
78+
// This is a special function that Miri provides.
79+
// It blocks the thread calling this function if the condition is false.
80+
pub unsafe fn miri_genmc_assume(condition: bool);
81+
}
82+
83+
// This functions loads an atomic boolean in a loop until it is true.
84+
// GenMC will explore all executions where this does 1, 2, ..., ∞ loads, which means the verification will never terminate.
85+
fn spin_until_true(flag: &AtomicBool) {
86+
while !flag.load(Relaxed) {
87+
std::hint::spin_loop();
88+
}
89+
}
90+
91+
// By replacing this loop with an assume statement, the only executions that will be explored are those with exactly 1 load that observes the expected value.
92+
// Incorrect use of assume statements can lead GenMC to miss important executions, so it is marked `unsafe`.
93+
fn spin_until_true_genmc(flag: &AtomicBool) {
94+
unsafe { miri_genmc_assume(flag.load(Relaxed)) };
95+
}
96+
```
97+
98+
#### Bounding loops with side effects
99+
100+
Some loops do contain side effects, meaning the number of explored iterations affects the rest of the program.
101+
Replacing the loop with one iteration like we did above would mean we miss all those possible executions.
102+
103+
In such a case, the loop can be limited to a fixed number of iterations instead.
104+
The choice of iteration limit trades off verification time for possibly missing bugs requiring more iterations.
105+
106+
```rust
107+
/// The loop in this function has a side effect, which is to increment the counter for the number of iterations.
108+
/// Instead of replacing the loop entirely (which would miss all executions with `count > 0`), we limit the loop to at most 3 iterations.
109+
fn count_until_true_genmc(flag: &AtomicBool) -> u64 {
110+
let mut count = 0;
111+
while !flag.load(Relaxed) {
112+
count += 1;
113+
std::hint::spin_loop();
114+
// Any execution that takes more than 3 iterations will not be explored.
115+
unsafe { miri_genmc_assume(count <= 3) };
116+
}
117+
count
118+
}
119+
```
120+
121+
<!-- FIXME: update the code above once Miri supports a loop bounding features like GenMC's `--unroll=N`. -->
122+
<!-- FIXME: update this section once Miri-GenMC supports automatic program transformations (like spinloop-assume replacement). -->
123+
53124
## Limitations
54125

55126
Some or all of these limitations might get removed in the future:

genmc-sys/cpp/include/MiriInterface.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,13 @@ struct MiriGenmcShim : private GenMCDriver {
134134
void handle_thread_finish(ThreadId thread_id, uint64_t ret_val);
135135
void handle_thread_kill(ThreadId thread_id);
136136

137+
/**** Blocking instructions ****/
138+
/// Inform GenMC that the thread should be blocked.
139+
/// Note: this function is currently hardcoded for `AssumeType::User`, corresponding to user
140+
/// supplied assume statements. This can become a parameter once more types of assumes are
141+
/// added.
142+
void handle_assume_block(ThreadId thread_id);
143+
137144
/***** Exploration related functionality *****/
138145

139146
/** Ask the GenMC scheduler for a new thread to schedule and return whether the execution is

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@
3030
#include <memory>
3131
#include <utility>
3232

33+
/**** Blocking instructions ****/
34+
35+
void MiriGenmcShim::handle_assume_block(ThreadId thread_id) {
36+
GenMCDriver::handleAssume(inc_pos(thread_id), AssumeType::User);
37+
}
38+
3339
/**** Memory access handling ****/
3440

3541
[[nodiscard]] auto MiriGenmcShim::handle_load(

genmc-sys/src/lib.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,9 +258,11 @@ mod ffi {
258258
/// Corresponds to GenMC's type with the same name.
259259
/// Should only be modified if changed by GenMC.
260260
enum ActionKind {
261-
/// Any Mir terminator that's atomic and has load semantics.
261+
/// Any MIR terminator that's atomic and that may have load semantics.
262+
/// This includes functions with atomic properties, such as `pthread_create`.
263+
/// If the exact type of the terminator cannot be determined, load is a safe default `Load`.
262264
Load,
263-
/// Anything that's not a `Load`.
265+
/// Anything that's definitely not a `Load`.
264266
NonLoad,
265267
}
266268

@@ -412,6 +414,12 @@ mod ffi {
412414
fn handle_thread_finish(self: Pin<&mut MiriGenmcShim>, thread_id: i32, ret_val: u64);
413415
fn handle_thread_kill(self: Pin<&mut MiriGenmcShim>, thread_id: i32);
414416

417+
/**** Blocking instructions ****/
418+
/// Inform GenMC that the thread should be blocked.
419+
/// Note: this function is currently hardcoded for `AssumeType::User`, corresponding to user supplied assume statements.
420+
/// This can become a parameter once more types of assumes are added.
421+
fn handle_assume_block(self: Pin<&mut MiriGenmcShim>, thread_id: i32);
422+
415423
/***** Exploration related functionality *****/
416424

417425
/// Ask the GenMC scheduler for a new thread to schedule and

src/concurrency/genmc/dummy.rs

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use rustc_abi::{Align, Size};
22
use rustc_const_eval::interpret::{AllocId, InterpCx, InterpResult};
33

4+
pub use self::intercept::EvalContextExt as GenmcEvalContextExt;
45
pub use self::run::run_genmc_mode;
56
use crate::intrinsics::AtomicRmwOp;
67
use crate::{
7-
AtomicFenceOrd, AtomicReadOrd, AtomicRwOrd, AtomicWriteOrd, MemoryKind, MiriMachine, Scalar,
8-
ThreadId, ThreadManager, VisitProvenance, VisitWith,
8+
AtomicFenceOrd, AtomicReadOrd, AtomicRwOrd, AtomicWriteOrd, MemoryKind, MiriMachine, OpTy,
9+
Scalar, ThreadId, ThreadManager, VisitProvenance, VisitWith,
910
};
1011

1112
#[derive(Clone, Copy, Debug)]
@@ -36,9 +37,27 @@ mod run {
3637
}
3738
}
3839

40+
mod intercept {
41+
use super::*;
42+
43+
impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
44+
pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
45+
fn handle_genmc_verifier_assume(&mut self, _condition: &OpTy<'tcx>) -> InterpResult<'tcx> {
46+
unreachable!();
47+
}
48+
}
49+
}
50+
3951
impl GenmcCtx {
4052
// We don't provide the `new` function in the dummy module.
4153

54+
pub(crate) fn schedule_thread<'tcx>(
55+
&self,
56+
_ecx: &InterpCx<'tcx, MiriMachine<'tcx>>,
57+
) -> InterpResult<'tcx, Option<ThreadId>> {
58+
unreachable!()
59+
}
60+
4261
/**** Memory access handling ****/
4362

4463
pub(super) fn set_ongoing_action_data_race_free(&self, _enable: bool) {
@@ -191,26 +210,6 @@ impl GenmcCtx {
191210
) -> InterpResult<'tcx> {
192211
unreachable!()
193212
}
194-
195-
/**** Scheduling functionality ****/
196-
197-
pub fn schedule_thread<'tcx>(
198-
&self,
199-
_ecx: &InterpCx<'tcx, MiriMachine<'tcx>>,
200-
) -> InterpResult<'tcx, ThreadId> {
201-
unreachable!()
202-
}
203-
204-
/**** Blocking instructions ****/
205-
206-
#[allow(unused)]
207-
pub(crate) fn handle_verifier_assume<'tcx>(
208-
&self,
209-
_machine: &MiriMachine<'tcx>,
210-
_condition: bool,
211-
) -> InterpResult<'tcx, ()> {
212-
unreachable!()
213-
}
214213
}
215214

216215
impl VisitProvenance for GenmcCtx {

src/concurrency/genmc/intercept.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use tracing::debug;
2+
3+
use crate::concurrency::thread::EvalContextExt as _;
4+
use crate::{
5+
BlockReason, InterpResult, MachineCallback, MiriInterpCx, OpTy, UnblockKind, VisitProvenance,
6+
VisitWith, callback, interp_ok,
7+
};
8+
9+
// Handling of code intercepted by Miri in GenMC mode, such as assume statement or `std::sync::Mutex`.
10+
11+
/// Other functionality not directly related to event handling
12+
impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
13+
pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
14+
/// Handle an `assume` statement. This will tell GenMC to block the current thread if the `condition` is false.
15+
/// Returns `true` if the current thread should be blocked in Miri too.
16+
fn handle_genmc_verifier_assume(&mut self, condition: &OpTy<'tcx>) -> InterpResult<'tcx> {
17+
let this = self.eval_context_mut();
18+
let condition_bool = this.read_scalar(condition)?.to_bool()?;
19+
debug!("GenMC: handle_genmc_verifier_assume, condition: {condition:?} = {condition_bool}");
20+
if condition_bool {
21+
return interp_ok(());
22+
}
23+
let genmc_ctx = this.machine.data_race.as_genmc_ref().unwrap();
24+
genmc_ctx.handle_assume_block(&this.machine)?;
25+
this.block_thread(
26+
BlockReason::Genmc,
27+
None,
28+
callback!(
29+
@capture<'tcx> {}
30+
|_this, unblock: UnblockKind| {
31+
assert_eq!(unblock, UnblockKind::Ready);
32+
unreachable!("GenMC should never unblock a thread blocked by an `assume`.");
33+
}
34+
),
35+
);
36+
interp_ok(())
37+
}
38+
}

src/concurrency/genmc/mod.rs

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@ use crate::{
2929
mod config;
3030
mod global_allocations;
3131
mod helper;
32+
mod intercept;
3233
mod run;
3334
pub(crate) mod scheduling;
3435
mod thread_id_map;
3536

3637
pub use genmc_sys::GenmcParams;
3738

3839
pub use self::config::GenmcConfig;
40+
pub use self::intercept::EvalContextExt as GenmcEvalContextExt;
3941
pub use self::run::run_genmc_mode;
4042

4143
#[derive(Debug)]
@@ -732,17 +734,6 @@ impl GenmcCtx {
732734
self.exec_state.exit_status.set(Some(exit_status));
733735
interp_ok(())
734736
}
735-
736-
/**** Blocking instructions ****/
737-
738-
#[allow(unused)]
739-
pub(crate) fn handle_verifier_assume<'tcx>(
740-
&self,
741-
machine: &MiriMachine<'tcx>,
742-
condition: bool,
743-
) -> InterpResult<'tcx, ()> {
744-
if condition { interp_ok(()) } else { self.handle_user_block(machine) }
745-
}
746737
}
747738

748739
impl GenmcCtx {
@@ -903,8 +894,13 @@ impl GenmcCtx {
903894
/// Handle a user thread getting blocked.
904895
/// This may happen due to an manual `assume` statement added by a user
905896
/// or added by some automated program transformation, e.g., for spinloops.
906-
fn handle_user_block<'tcx>(&self, _machine: &MiriMachine<'tcx>) -> InterpResult<'tcx> {
907-
todo!()
897+
fn handle_assume_block<'tcx>(&self, machine: &MiriMachine<'tcx>) -> InterpResult<'tcx> {
898+
let curr_thread = machine.threads.active_thread();
899+
let genmc_curr_thread =
900+
self.exec_state.thread_id_manager.borrow().get_genmc_tid(curr_thread);
901+
debug!("GenMC: assume statement, blocking thread {curr_thread:?} ({genmc_curr_thread:?})");
902+
self.handle.borrow_mut().pin_mut().handle_assume_block(genmc_curr_thread);
903+
interp_ok(())
908904
}
909905
}
910906

0 commit comments

Comments
 (0)