Tool Features¶
Available Memory Models¶
By default, GenMC verifies programs under RC11. However, apart from RC11, GenMC also supports other models like SC and IMM. The difference between these memory models (as far as allowed outcomes are concerned) can be seen in the following program:
/* file: lb.c */
#include <stdlib.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <assert.h>
atomic_int x;
atomic_int y;
void *thread_1(void *unused)
{
int a = atomic_load_explicit(&x, memory_order_relaxed);
atomic_store_explicit(&y, 1, memory_order_relaxed);
return NULL;
}
void *thread_2(void *unused)
{
int b = atomic_load_explicit(&y, memory_order_relaxed);
atomic_store_explicit(&x, 1, memory_order_relaxed);
return NULL;
}
int main()
{
pthread_t t1, t2;
if (pthread_create(&t1, NULL, thread_1, NULL))
abort();
if (pthread_create(&t2, NULL, thread_2, NULL))
abort();
return 0;
}
Under RC11, an execution where both a = 1 and b = 1 is forbidden,
whereas such an execution is allowed under IMM. To account for such
behaviors, GenMC tracks dependencies between program instructions thus
leading to a constant overhead when verifying programs under models
like IMM.
Note on Language Memory Models vs Hardware Memory Models¶
RC11 is a language-level memory model while IMM is a hardware memory model. Subsequently, the verification results produced by GenMC for the two models should be interpreted somewhat differently.
What this means in practice is that, when verifying programs under RC11, the input file is assumed to be the very source code the user wrote. A successful verification result under RC11 holds all the way down to the actual executable, due to the guarantees provided by RC11 [Lahav et al. 2017].
On the other hand, when verifying programs under IMM, the input file is assumed to be the assembly code run by the processor (or, more precisely, a program in IMM's intermediate language). And while GenMC allows the input file to be a C file (as in the case of RC11), it assumes that this C file corresponds to an assembly file that is the result of the compilation of some program in IMM's language. In other words, program correctness is not preserved across compilation for IMM inputs.
Race Detection and Memory Errors¶
For memory models that define the notion of a race, GenMC will report
executions containing races as erroneous. For instance, under RC11,
the following program is racy, as there is no happens-before between
the write of x in the first thread and the non-atomic read of x in
the second thread (even though the latter causally depends on the
former).
/* file: race.c */
#include <stdlib.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <assert.h>
atomic_int x;
void *thread_1(void *unused)
{
atomic_store_explicit(&x, 1, memory_order_relaxed);
return NULL;
}
void *thread_2(void *unused)
{
int a, b;
a = atomic_load_explicit(&x, memory_order_relaxed);
if (a == 1)
b = *((int *) &x);
return NULL;
}
int main()
{
pthread_t t1, t2;
if (pthread_create(&t1, NULL, thread_1, NULL))
abort();
if (pthread_create(&t2, NULL, thread_2, NULL))
abort();
return 0;
}
Additionally, for all memory models, GenMC detects some memory races like accessing memory that has been already freed, accessing dynamic memory that has not been allocated, or freeing an already freed chunk of memory.
Race detection can be completely disabled by means of
-disable-race-detection, which may yield better performance for
certain programs.
Barrier-Aware Model Checking (BAM)¶
GenMC v0.6 comes with built-in support for pthread_barrier_t
functions (see Supported APIs) via BAM [Kokologiannakis and
Vafeiadis 2021]. As an example
of BAM in action, consider the following program:
/* file: bam.c */
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <stdatomic.h>
#include <genmc.h>
#include <assert.h>
#ifndef N
# define N 2
#endif
pthread_barrier_t b;
atomic_int x;
void *thread_n(void *unused)
{
++x;
pthread_barrier_wait(&b);
assert(x == N);
return NULL;
}
int main()
{
pthread_t t[N];
pthread_barrier_init(&b, NULL, N);
for (int i = 0u; i < N; i++) {
if (pthread_create(&t[i], NULL, thread_n, NULL))
abort();
}
return 0;
}
Running GenMC on the program above results in the following output:
As can be seen, GenMC treats barrier_wait calls as no-ops, and they
do not lead to any additional explorations. (The two executions
explored correspond to the possible ways in which x can be
incremented).
However, if we disable BAM by means of the -disable-bam switch, we
get the following output:
Number of complete executions explored: 4
Number of blocked executions seen: 4
Total wall-clock time: 0.01s
Note that while BAM can lead to the exploration of exponentially fewer
executions, it can only be used if the result of the barrier_wait is
not used. If it is, then using -disable-bam is necessary, as GenMC
currently does not enforce this limitation.
State-Space Bounding¶
Under SC, GenMC can bound the state-space exploration using either preemption bounding [Marmanis et al. 2023a] and round-robin bounding [Marmanis and Vafeiadis 2023b].
For instance, in the following program, running GenMC with --sc
--bound=0 --bound-type=context will avoid exploring executions that
have one or more (preemptive) context-switches.
/* file: bound.c */
#include <stdlib.h>
#include <pthread.h>
#include <stdatomic.h>
atomic_int x;
void * thread_1(void * unused)
{
atomic_store(&x, 1);
atomic_store(&x, 2);
return NULL;
}
void * thread_2(void * unused)
{
atomic_load(&x);
return NULL;
}
int main()
{
pthread_t t1, t2;
if (pthread_create(&t1, NULL, thread_1, NULL))
abort();
if (pthread_create(&t2, NULL, thread_2, NULL))
abort();
return 0;
}
To guarantee that no execution within the bound is missed, some executions that exceed the bound might also be explored, and are reported appropriately:
The default bounding type (round), on the other hand, only explores
executions within the given bound. For instance, when running GenMC
with --sc --bound=0, only the single SC execution that can be
obtained with zero rounds (i.e., in one go) using a left-to-right
round-robin scheduler will be explored.
Note that, while the round-robin bound does not explore executions that exceed the limit, the number of executions grows more rapidly as the bound increases (compared to context bounding).
Symmetry Reduction¶
GenMC also employs an experimental symmetry reduction mechanism [Kokologiannakis et al. 2024], which is beneficial to use when threads are running the same code.
For instance, in the following program, GenMC explores only one execution instead of 6.
/* file: sr.c */
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <stdatomic.h>
#include <genmc.h>
#include <assert.h>
atomic_int x;
void *thread_n(void *unused)
{
++x;
return NULL;
}
int main()
{
pthread_t t1, t2, t3;
if (pthread_create(&t1, NULL, thread_n, NULL))
abort();
if (pthread_create(&t2, NULL, thread_n, NULL))
abort();
if (pthread_create(&t3, NULL, thread_n, NULL))
abort();
return 0;
}
In order for symmetry reduction to actually take place, the spawned threads need to share exactly the same code, have exactly the same arguments, and also there must not be any memory access (at the LLVM-IR level) between the spawn instructions.
To make GenMC use symmetry reduction, one can use the primitive
__VERIFIER_spawn_symmetric(fun, tid) (defined in genmc.h), the
last argument of which is the thread identifier of the last
(previously spawned) symmetric predecessor.
Checking Liveness¶
GenMC can also check for liveness violations in programs with spinloops. Consider the following simple program:
/* file: liveness.c */
#include <stdlib.h>
#include <pthread.h>
#include <stdatomic.h>
atomic_int x;
void *thread_1(void *unused)
{
while (!x)
;
return NULL;
}
int main()
{
pthread_t t1;
if (pthread_create(&t1, NULL, thread_1, NULL))
abort();
return 0;
}
Since there are no writes to x, the loop in thread_1 above will
never terminate. Indeed, running GenMC with -check-liveness produces
a relevant error report:
Error detected: Liveness violation!
Event (1, 4) in graph:
<-1, 0> main:
(0, 0): B
(0, 1): TC [forks 1] L.19
(0, 2): E
<0, 1> thread_1:
(1, 0): B
(1, 1): LOOP_BEGIN
(1, 2): SPIN_START
(1, 3): Rsc (x, 0) [INIT] L.9
(1, 4): BLOCK [spinloop]
Non-terminating spinloop: thread 1
Number of complete executions explored: 0
Number of blocked executions seen: 1
Total wall-clock time: 0.07s
The -check-liveness switch will automatically check for liveness
violations in all loops that have been captured by the spin-assume
transformation.
Checking Linearizability¶
GenMC implements the Relinche algorithm [Golovin et
al. 2025] for checking (bounded)
linearizability. This algorithm requires using GenMC with the "Most
Parallel Client" (MPC). Such a client for queues and stacks is already
provided in GenMC's test suite (e.g.,
tests/correct/relinche/queue/mpc.c).
To use the client e.g., for a queue, we have to provide a queue
implementation that defines the following methods: init_queue(),
enqueue(), dequeue() and clear_queue(). For instance, an
implementation of a Herlihy-Wing queue can be seen below:
/* file: hw-queue.c */
#include <stdatomic.h>
#include <assert.h>
#include <genmc.h>
#include <stdbool.h>
#define MAX_NODES 0xff
typedef struct _queue_t {
_Atomic(int) tail;
_Atomic(unsigned int) nodes[MAX_NODES] ;
} queue_t;
void init_queue(queue_t *q, int num_threads)
{
}
void clear_queue(queue_t *q, int num_threads)
{
}
void enqueue(queue_t *q, unsigned int val)
{
int i = atomic_fetch_add_explicit(&q->tail, 1, release);
assert(i + 1 < MAX_NODES);
atomic_store_explicit(&q->nodes[i + 1], val, release);
}
bool dequeue(queue_t *q, unsigned int *ret)
{
bool success = false;
while (!success) {
int tail = atomic_load_explicit(&q->tail, acquire);
for (int i = 0; i <= tail; ++i) {
if (atomic_load_explicit(&q->nodes[i], acquire) == 0)
continue;
#ifdef BUG /* Linearizability bug */
unsigned int v = atomic_exchange_explicit(&q->nodes[i], 0, acquire);
#else
unsigned int v = atomic_exchange_explicit(&q->nodes[i], 0, acq_rel);
#endif
if (v != 0) {
*ret = v;
success = true;
break;
}
}
__VERIFIER_assume(success);
}
return *ret;
}
To check correctness of the above implementation, we first have to
provide a specification. GenMC can produce such a specification file
from a reference implementation, but we can also use one of the
predefined specification files (e.g.,
tests/correct/queue/queue_spec_rc11.in) as follows:
genmc -rc11 -disable-mm-detector --check-lin-spec=spec.in -- -DRTN=2 -DWTN=2 -include hw-queue.c mpc.c"
Doing so with the above implementation and spec file, will check for
linearizability of all clients with two dequeue and two enqueue
operations. GenMC produces the following output:
GenMC v0.10.3 (LLVM 16.0.6)
Copyright (C) 2024 MPI-SWS. All rights reserved.
*** Compilation complete.
*** Transformation complete.
Tip: Estimating state-space size. For better performance, you can use --disable-estimation.
*** Estimation complete.
Total executions estimate: 33 (+- 37)
Time to completion estimate: 0.03s
*** Verification complete.
No errors were detected.
Number of complete executions explored: 6
Number of blocked executions seen: 10
Number of checked hints: 1
Relinche time: 0.00s
Total wall-clock time: 0.03s