mirror of
https://github.com/yugr/Implib.so
synced 2026-06-08 18:33:41 +00:00
272 lines
6.1 KiB
Promela
272 lines
6.1 KiB
Promela
// TODO:
|
|
// - allow nested function calls ?
|
|
|
|
// SPIN issues:
|
|
// - not printing local variables in traces
|
|
// - no functions
|
|
// - dedicated run for each LTL property
|
|
// - no lexical scoping
|
|
// - no exists/forall quantifiers in LTL properties
|
|
// - no enum types (just single mtype)
|
|
|
|
#define INIT
|
|
|
|
// Number of concurrent threads
|
|
#define THREADS 2
|
|
|
|
#define NO_THREAD 0
|
|
|
|
// Number of library functions
|
|
#define FUNS 2
|
|
|
|
#define NO_FUN 0
|
|
|
|
// Number of calls in each thread and in library ctor
|
|
#define CALLS 2
|
|
|
|
// Max callstack size
|
|
#define MAX_DEPTH 2
|
|
|
|
// Max number of recursive mutex locks
|
|
#define MAX_LOCK 4
|
|
|
|
// Sanity checks
|
|
#if THREADS <= 0 || FUNS <= 0 || CALLS <= 0 || MAX_DEPTH <= 0 || MAX_LOCK <= 0
|
|
# error Invalid config
|
|
#endif
|
|
|
|
// Types
|
|
|
|
// LibraryState
|
|
// TODO: is this an abuse of mtype and I should switch to macro instead?
|
|
mtype {
|
|
UNLOADED, LOADING, LOADED
|
|
}
|
|
|
|
typedef Lock {
|
|
int owner;
|
|
int count;
|
|
}
|
|
|
|
typedef StackFrame {
|
|
int calls;
|
|
int callee;
|
|
}
|
|
|
|
typedef CallStack {
|
|
StackFrame frames[MAX_DEPTH]
|
|
int n;
|
|
}
|
|
|
|
// Global state
|
|
|
|
bit shim_table[FUNS]
|
|
bit lib_handle
|
|
mtype lib_state
|
|
Lock rec_lock
|
|
|
|
int terminated
|
|
|
|
// Code
|
|
|
|
proctype Thread(int tid) {
|
|
CallStack stack
|
|
|
|
stack.n = 1
|
|
stack.frames[0].calls = CALLS
|
|
stack.frames[0].callee = NO_FUN
|
|
|
|
recurse:
|
|
do
|
|
// Thread completed => terminate
|
|
:: stack.n == 0 -> break
|
|
|
|
// Thread still active
|
|
:: else -> { // L.run
|
|
if
|
|
// No more functions to call
|
|
:: stack.frames[stack.n - 1].calls == 0 -> {
|
|
stack.n--
|
|
#ifdef INIT
|
|
if
|
|
// We are in top frame
|
|
:: stack.n == 0 -> skip
|
|
|
|
// We have finished library init, return to caller
|
|
:: else -> {
|
|
// For more complex behaviors we'll need to store return adddress
|
|
lib_state = LOADED
|
|
goto return_from_recurse
|
|
}
|
|
fi
|
|
#endif
|
|
}
|
|
|
|
// Some functions left
|
|
:: else -> { // L.call
|
|
int fun
|
|
select(fun : 1 .. FUNS)
|
|
|
|
stack.frames[stack.n - 1].calls--
|
|
stack.frames[stack.n - 1].callee = fun
|
|
|
|
if
|
|
// Fast path
|
|
:: shim_table[fun - 1] -> {
|
|
// LoadBeforeUse(TLA): Library must be initialized before clients can call any of its functions via fast path
|
|
assert(lib_state == LOADED)
|
|
skip
|
|
}
|
|
|
|
// Slow path
|
|
:: else -> { // L.slow
|
|
|
|
// Enter critical section
|
|
|
|
atomic {
|
|
rec_lock.owner == NO_THREAD || rec_lock.owner == tid
|
|
assert(rec_lock.owner == NO_THREAD || rec_lock.count > 0)
|
|
rec_lock.owner = tid
|
|
rec_lock.count++
|
|
}
|
|
|
|
// Obtain library handle
|
|
|
|
if
|
|
// Handle is already set (simplest case) ?
|
|
:: lib_handle -> skip
|
|
|
|
// Load library after it's already loaded but before handle is set
|
|
// There are 2 cases: 1) library still initializing, 2) library initialized
|
|
:: !lib_handle && (lib_state == LOADING || lib_state == LOADED) -> {
|
|
lib_handle = true
|
|
}
|
|
|
|
// Load library for the first time, running global ctors
|
|
:: !lib_handle && lib_state == UNLOADED -> {
|
|
// Initialize library
|
|
|
|
#ifdef INIT
|
|
lib_state = LOADING
|
|
|
|
stack.n++
|
|
stack.frames[stack.n - 1].calls = CALLS
|
|
stack.frames[stack.n - 1].callee = NO_FUN
|
|
|
|
goto recurse
|
|
return_from_recurse:
|
|
lib_handle = true
|
|
#else
|
|
lib_state = LOADED
|
|
lib_handle = true
|
|
#endif
|
|
}
|
|
fi
|
|
|
|
// Publish shim address only in first call (this is imprecise but ok for now)
|
|
|
|
atomic {
|
|
if
|
|
:: rec_lock.count == 1 -> shim_table[stack.frames[stack.n - 1].callee - 1] = true
|
|
:: else -> skip
|
|
fi
|
|
}
|
|
|
|
// Exit critical section
|
|
|
|
assert(rec_lock.owner == tid && rec_lock.count > 0)
|
|
|
|
atomic {
|
|
rec_lock.count--
|
|
if
|
|
:: rec_lock.count == 0 -> rec_lock.owner = NO_THREAD
|
|
:: else -> skip
|
|
fi
|
|
}
|
|
} // L.slow
|
|
fi
|
|
|
|
assert(lib_state == LOADING || lib_state == LOADED)
|
|
|
|
// Function called
|
|
} // L.call
|
|
fi
|
|
} // L.run
|
|
od
|
|
|
|
terminated++
|
|
}
|
|
|
|
init {
|
|
int i
|
|
|
|
// Init global state
|
|
|
|
i = 1
|
|
do
|
|
:: i <= FUNS -> {
|
|
shim_table[i - 1] = false
|
|
i++
|
|
}
|
|
:: else -> break
|
|
od
|
|
|
|
lib_handle = false
|
|
lib_state = UNLOADED
|
|
rec_lock.owner = NO_THREAD
|
|
rec_lock.count = 0
|
|
|
|
terminated = 0
|
|
|
|
// Start threads
|
|
|
|
atomic {
|
|
i = 1
|
|
do
|
|
:: i <= THREADS -> {
|
|
run Thread(i)
|
|
i++
|
|
}
|
|
:: else -> break
|
|
od
|
|
}
|
|
|
|
// Termination(TLA): All threads terminate, lock is released and library is loaded
|
|
|
|
terminated == THREADS
|
|
|
|
assert(rec_lock.owner == NO_THREAD && rec_lock.count == 0)
|
|
assert(lib_state == LOADED && lib_handle)
|
|
}
|
|
|
|
// Invariants
|
|
|
|
never {
|
|
do
|
|
// TypeInvariant(TLA)
|
|
:: !(0 <= rec_lock.owner && rec_lock.owner <= THREADS) -> break
|
|
:: !(0 <= rec_lock.count && rec_lock.count <= MAX_LOCK) -> break
|
|
|
|
// LockInvariant(TLA)
|
|
:: rec_lock.owner == NO_THREAD ^ rec_lock.count == 0 -> break
|
|
:: rec_lock.owner == NO_THREAD && lib_state == LOADING -> break
|
|
|
|
// LibHandleCorrectness(TLA): Library handle set only if library is loaded (not necessarily initialized)
|
|
:: lib_handle && lib_state != LOADING && lib_state != LOADED -> break
|
|
|
|
:: else
|
|
od
|
|
}
|
|
|
|
ltl Prop {
|
|
[](
|
|
// NoLibResets(TLA): Library never UN-loaded
|
|
// FIXME: use X when Debian/Ubuntu support it
|
|
// (https://github.com/thomaslee/spin-debian/commit/8b2c6e3881d9b1b70a53b46ca5f637b6d57eb385)
|
|
(lib_state == LOADING -> [](lib_state == LOADING || lib_state == LOADED))
|
|
&& (lib_state == LOADED -> [](lib_state == LOADED))
|
|
)
|
|
}
|
|
|
|
// TODO: quantified predicates (LoadBeforeUse2, NoShimResets)
|