| // 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(); |
| } |