blob: 8401b0dc7027cedc79ec732eab07fdf436d3b17c [file] [log] [blame]
// Copyright Microsoft and CHERIoT Contributors.
// SPDX-License-Identifier: MIT
#define TEST_NAME "Locks"
#include "tests.hh"
#include <cheri.hh>
#include <errno.h>
#include <locks.hh>
#include <thread.h>
#include <thread_pool.h>
using namespace CHERI;
using namespace thread_pool;
namespace
{
FlagLock flagLock;
FlagLockPriorityInherited flagLockPriorityInherited;
TicketLock ticketLock;
cheriot::atomic<bool> modified;
cheriot::atomic<int> counter;
/**
* Test that a lock meets the minimum requirements: it actually provides
* mutual exclusion.
*/
template<typename Lock>
void test_lock(Lock &lock)
{
modified = false;
debug_log("Acquiring lock in {}", __PRETTY_FUNCTION__);
{
LockGuard g{lock};
async([&]() {
LockGuard g{lock};
modified = true;
});
sleep(2);
TEST(modified == false,
"flag concurrently modified while lock is held!");
}
sleep(1);
while (!modified)
{
debug_log("Other thread not finished, yielding");
sleep(1);
}
}
/// Test that try_lock fails after a timeout
template<typename Lock>
void test_trylock(Lock &lock)
{
LockGuard g{lock};
debug_log("Trying to acquire already-held lock in {}",
__PRETTY_FUNCTION__);
Timeout t{1};
TEST(lock.try_lock(&t) == false,
"Trying to acquire lock spuriously succeeded");
if constexpr (!std::is_same_v<Lock, FlagLockPriorityInherited>)
{
TEST(t.elapsed >= 1, "Sleep slept for {} ticks", t.elapsed);
}
}
/**
* Test that destructing a lock automatically wakes up all waiters,
* failing them to acquire the lock.
*/
template<typename Lock>
void test_destruct_lock_wake_up(Lock &lock)
{
modified = false;
Timeout t{1};
TEST(lock.try_lock(&t), "Failed to acquire uncontended lock");
// Try to acquire the lock in a background thread
async([&]() {
// Make sure that we don't prevent the thread pool
// making progress if this test fails.
// The generous timer makes sure that we reach the
// modified == true assert before the timeout.
Timeout t2{20};
TEST(lock.try_lock(&t2) == false,
"Lock acquisition should not succeed!");
// When the lock is upgraded in destruction mode,
// `lock` will return failure.
modified = true;
});
// Give the thread a chance to run
sleep(1);
// Upgrade the lock to destruction mode
lock.upgrade_for_destruction();
// Give the waiter a chance to wake up
sleep(1);
// Check that the destruction mode woke up the waiters
TEST(modified == true, "Destruction mode did not wake up waiters!");
// Reset the lock in case other tests use it.
// Note: in practice, lock.upgrade_for_destruction() would be
// followed by a free() operation, not by unlock(). However
// unlock() comes handy here to remove the destruction flag to
// let other tests run properly with the same lock object
lock.unlock();
}
/**
* Test that a lock with the destruction bit set cannot be acquired
* anymore.
*
* Note: here, plug at the C API to be able to check C error codes.
*/
void test_destruct_flag_lock_acquire()
{
static FlagLockState flagLockState;
static FlagLockState priorityFlagLockState;
Timeout t{5};
int ret = flaglock_trylock(&t, &flagLockState);
TEST(ret == 0, "Flag lock trylock failed with error {}", ret);
// Upgrade the lock to destruction mode
flaglock_upgrade_for_destruction(&flagLockState);
// Check that we now fail to grab the lock with the right error
TEST(flaglock_trylock(&t, &flagLockState) == -ENOENT,
"Acquiring the lock did not fail with -ENOENT although it is in "
"destruction mode");
// Now, do the same tests with the priority inheriting flag lock
ret = flaglock_priority_inheriting_trylock(&t, &priorityFlagLockState);
TEST(ret == 0,
"Priority inheriting flag lock trylock failed with error {}",
ret);
flaglock_upgrade_for_destruction(&priorityFlagLockState);
TEST(flaglock_priority_inheriting_trylock(&t, &priorityFlagLockState) ==
-ENOENT,
"Acquiring the lock did not fail with -ENOENT although it is in "
"destruction mode");
}
void test_recursive_mutex()
{
static RecursiveMutexState recursiveMutex;
static std::atomic<bool> done{false};
debug_log("Testing recursive mutex");
Timeout t{5};
int ret = recursivemutex_trylock(&t, &recursiveMutex);
TEST(ret == 0, "Recursive mutex trylock failed with error {}", ret);
debug_log("Aquiring recursive mutex again");
ret = recursivemutex_trylock(&t, &recursiveMutex);
TEST(ret == 0,
"Recursive mutex trylock failed on mutex owned by this thread "
"with error {}",
ret);
// we don't expect above to block as there is no contention.
TEST(t.elapsed == 0,
"Recursive mutex trylock slept for {} ticks",
t.elapsed);
async([]() {
Timeout t{0};
debug_log(
"Trying to acquire recursive mutex in other thread (timeout 0)");
int ret = recursivemutex_trylock(&t, &recursiveMutex);
TEST(ret != 0,
"Recursive mutex trylock succeeded on mutex owned by another "
"thread");
debug_log("Trying to acquire recursive mutex in other thread "
"(unlimited timeout)");
t = UnlimitedTimeout;
ret = recursivemutex_trylock(&t, &recursiveMutex);
TEST(ret == 0,
"Recursive mutex failed after mutex was unlocked by "
"another thread");
debug_log("Other thread acquired recursive mutex");
done = true;
});
// Give other thread a chance to run
sleep(2);
// Check that it hasn't acquired the lock yet as we still have it
TEST(
done == false,
"Recursive mutex trylock succeeded on mutex owned by another thread");
// Unlock once, should still not release the lock
debug_log("Releasing recurisve mutex once");
recursivemutex_unlock(&recursiveMutex);
sleep(2);
TEST(
done == false,
"Recursive mutex trylock succeeded on mutex owned by another thread");
debug_log("Releasing recurisve mutex again");
recursivemutex_unlock(&recursiveMutex);
sleep(2);
TEST(done == true,
"Recursive mutex acquire failed from other thread after mutex was "
"unlocked");
}
/**
* Test that the ticket lock gives the ordering guarantees that it should.
*/
void test_ticket_lock_ordering()
{
debug_log("Starting ticket-lock ordering tests");
{
LockGuard g{ticketLock};
async([&]() {
LockGuard g{ticketLock};
TEST(counter == 0,
"Ticket lock acquired out of order, counter is {}, "
"expected 0",
counter.load());
counter = 1;
});
async([&]() {
sleep(5);
LockGuard g{ticketLock};
TEST(counter == 1,
"Ticket lock acquired out of order, counter is {}, "
"expected 1",
counter.load());
counter = 2;
});
// Make sure both other threads are blocked on the ticket lock.
sleep(10);
}
// We should not be allowed to run until both of the other threads have
// run.
LockGuard g{ticketLock};
TEST(counter == 2,
"Ticket lock acquired out of order, counter is {}, expected 2",
counter.load());
}
/**
* Test ticket lock behavior on overflow.
*
* Note: here, plug at the C API to be able to set the lock's starting
* state. Ideally we would be able to do that externally from the C++
* API without setting a specific internal state, but this requires us
* to loop `lock` and `unlock` for about 2^32 iterations, which takes
* too long on both the simulator and the FPGA.
*/
void test_ticket_lock_overflow()
{
static TicketLockState ticketLockState;
counter = 0;
// Put the ticket lock in an unlocked state where it is about
// to overflow.
ticketLockState.current = std::numeric_limits<uint32_t>::max() - 2;
ticketLockState.next = std::numeric_limits<uint32_t>::max() - 2;
// Take the lock on the main thread.
ticketlock_lock(&ticketLockState);
// Now we should have (current: max-2, next: max-1).
// Now create two more threads which will try to get hold of
// the lock, thereby overflowing the `next` counter.
async([&]() {
ticketlock_lock(&ticketLockState);
// Now we should have (current: max-2, next: max).
counter++;
ticketlock_unlock(&ticketLockState);
// Now we should have (current: max, next: 0).
});
async([&]() {
ticketlock_lock(&ticketLockState);
// We just overflowed.
// Now we should have (current: max-2, next: 0).
counter++;
ticketlock_unlock(&ticketLockState);
// Now we should have (current: 0, next: 0).
});
// Give the threads a chance to run and increment `next`.
sleep(20);
// Release the lock on the main thread.
ticketlock_unlock(&ticketLockState);
// Now we should have (current: max-1, next: 0).
// Give the threads a chance to wake up.
sleep(20);
// The final state should be (current: 0, next: 0).
// Check that unlocking woke up the waiters. Subsequent tests
// will generally fail if this fails, because one or all of the
// two threads may still be live and deadlocked.
TEST(counter.load() == 2,
"Ticket lock deadlocked because of overflow, expected 2 threads "
"to wake up, got {}",
counter.load());
}
} // namespace
void test_locks()
{
test_lock(flagLock);
test_lock(flagLockPriorityInherited);
test_lock(ticketLock);
test_trylock(flagLock);
test_trylock(flagLockPriorityInherited);
test_destruct_lock_wake_up(flagLock);
test_destruct_lock_wake_up(flagLockPriorityInherited);
test_destruct_flag_lock_acquire();
test_ticket_lock_ordering();
test_ticket_lock_overflow();
test_recursive_mutex();
}