blob: 7db652ef90edf34d022bab12b0b659d325f0ca1c [file] [log] [blame]
// Copyright Microsoft and CHERIoT Contributors.
// SPDX-License-Identifier: MIT
#define TEST_NAME "List"
#include "tests.hh"
#include <ds/linked_list.h>
using CHERI::Capability;
namespace
{
/**
* Example class we want to link into a doubly linked list.
*
* The class contains a single integer for the purposes of the test.
*
* `ds::linked_list` is an intrusive list: we embed the list node into
* the class we want to link. There are various implementations of the
* list nodes. Here we use the most simple one
* (`ds::linked_list::cell::Pointer`) which relies on two pointers
* `next` and `prev`.
*/
struct LinkedObject
{
using ObjectRing = ds::linked_list::cell::Pointer;
int data;
/**
* List node: links objects into the doubly-linked list.
*/
ObjectRing ring __attribute__((__cheri_no_subobject_bounds__)) = {};
/**
* Container-of for the above field. This is used to retrieve the
* corresponding object from a list element.
*/
__always_inline static struct LinkedObject *from_ring(ObjectRing *c)
{
return reinterpret_cast<struct LinkedObject *>(
reinterpret_cast<uintptr_t>(c) -
offsetof(struct LinkedObject, ring));
}
};
} // namespace
/**
* `ds::linked_list`s are circular, doubly-linked collections. While they can
* stand on their own as rings of objects, it is sometimes convenient to create
* a designated 'sentinel' node that participates in the collection without
* being part of the collection:
*
* - a sentinel node provides pointers to the effective head and tail of the
* collection (the successor and predecessor of the sentinel, respectively)
*
* - a sentinel allows not having to special-case 'the collection is empty' in
* as many places as some other representations (that is, collections with
* sentinels need fewer NULL pointer checks)
*
* - a sentinel provides many handy functions to operate on the list
*
* Note: do not allocate the sentinel (or any list cell) on the stack, because
* it would lead some list nodes to hold a pointer to a stack value, i.e., to
* an invalid capability. This would manifest as a crash while using the list.
*/
ds::linked_list::Sentinel<LinkedObject::ObjectRing> objects = {};
void test_list()
{
debug_log("Testing the list implementation.");
// Number of elements we will add to the list in the test. Must be
// divisible by two.
static constexpr int NumberOfListElements = 30;
auto heapAtStart = heap_quota_remaining(MALLOC_CAPABILITY);
TEST(objects.is_empty(), "Newly created list is not empty");
// Create heap-allocated objects, and link them into the linked list.
for (int i = 0; i < NumberOfListElements; i++)
{
Timeout t{UnlimitedTimeout};
LinkedObject *o = static_cast<LinkedObject *>(
heap_allocate(&t, MALLOC_CAPABILITY, sizeof(LinkedObject)));
TEST(Capability{o}.is_valid(), "Cannot allocate linked object");
// Use the object integer as an index.
o->data = i;
// The list node has not yet been initialized.
o->ring.cell_reset();
// Test that we can retrieve the object from the link node and
// that this results in a capability which is identical to what
// we got from the allocator.
TEST(Capability{o} == Capability{LinkedObject::from_ring(&(o->ring))},
"The container of method does not return the right object");
TEST(Capability{LinkedObject::from_ring(&(o->ring))}.is_valid(),
"Capability retrieved from `from_ring` is invalid");
// Add the new object to the list through the sentinel node.
objects.append(&(o->ring));
}
TEST(!objects.is_empty(), "The list is empty after adding objects");
// Test that the sentinel can be used to retrieve the first and last
// elements of the list as expected.
TEST(LinkedObject::from_ring(objects.last())->data ==
NumberOfListElements - 1,
"Last element of the list is incorrect, expected {}, got {}",
NumberOfListElements - 1,
LinkedObject::from_ring(objects.last())->data);
TEST(objects.last()->cell_next() == &objects.sentinel,
"Last element in not followed by the sentinel");
TEST(objects.last() == objects.sentinel.cell_prev(),
"Sentinel is not preceeded by the last element");
TEST(LinkedObject::from_ring(objects.first())->data == 0,
"First element of the list is incorrect, expected {}, got {}",
0,
LinkedObject::from_ring(objects.last())->data);
TEST(objects.first()->cell_prev() == &objects.sentinel,
"First element in not preceeded by the sentinel");
TEST(objects.first() == objects.sentinel.cell_next(),
"Sentinel is not followed by the first element");
// Test that we can go through the list by following `cell_next`
// pointers as expected.
int counter = 0;
// While at it, retrieve a pointer to the middle element which we will
// use to cleave the list later.
LinkedObject::ObjectRing *middle = nullptr;
// We reach the sentinel when we have gone through all elements of the
// list.
for (auto *cell = objects.first(); cell != &objects.sentinel;
cell = cell->cell_next())
{
struct LinkedObject *o = LinkedObject::from_ring(cell);
TEST(
o->data == counter,
"Ordering of elements in the list is incorrect, expected {}, got {}",
o->data,
counter);
if (counter == NumberOfListElements / 2)
{
middle = cell;
}
counter++;
}
TEST(middle != nullptr, "Could not find middle element of the list");
// Cut the list in the middle. `middle` is now a handle to the (valid)
// collection of objects [middle, last] that have become detached from
// the sentinel.
ds::linked_list::remove(middle, objects.last());
// This should leave us with a list of size `NumberOfListElements / 2`.
counter = 0;
for (auto *cell = objects.first(); cell != &objects.sentinel;
cell = cell->cell_next())
{
counter++;
}
TEST(counter == NumberOfListElements / 2,
"Cleaving didn't leave a list with the right number of elements");
// Now remove (and free) a single element from the list.
TEST(LinkedObject::from_ring(objects.first())->data == 0,
"First element of the list is incorrect, expected {}, got {}",
0,
LinkedObject::from_ring(objects.first())->data);
// We must keep a reference to the removed object to free it, as
// `remove` returns a pointer to the residual list (return value which
// we do not use here), not to the removed element.
LinkedObject::ObjectRing *removedCell = objects.first();
ds::linked_list::remove(objects.first());
heap_free(MALLOC_CAPABILITY, LinkedObject::from_ring(removedCell));
TEST(LinkedObject::from_ring(objects.first())->data == 1,
"First element of the list is incorrect after removing the first "
"element, expected {}, got {}",
1,
LinkedObject::from_ring(objects.first())->data);
TEST(objects.first()->cell_prev() == &objects.sentinel,
"First element in not preceeded by the sentinel after removing the "
"first object");
// We are done with the list, free it.
counter = 0;
LinkedObject::ObjectRing *cell = objects.first();
while (cell != &objects.sentinel)
{
struct LinkedObject *o = LinkedObject::from_ring(cell);
cell = cell->cell_next();
heap_free(MALLOC_CAPABILITY, o);
counter++;
}
TEST(counter == (NumberOfListElements / 2) - 1,
"Incorrect number of elements freed, expected {}, got {}",
(NumberOfListElements / 2) - 1,
counter);
// Now that the list is freed, reset the sentinel.
objects.reset();
TEST(objects.is_empty(), "Reset-ed list is not empty");
// We must also free the span of the list which we removed earlier.
// This time use the `::search` method to go through the collection.
ds::linked_list::search(
middle, [&counter](LinkedObject::ObjectRing *&cell) {
// `unsafe_remove` does not update the node pointers of the
// removed cell. This is great here because we will free the
// object anyways. We could also use `remove` here.
auto l = ds::linked_list::unsafe_remove(cell);
heap_free(MALLOC_CAPABILITY, LinkedObject::from_ring(cell));
// `l` is the predecessor of `cell` in the residual ring, so
// this does exactly what we want when `::search` iterates.
cell = l;
counter++;
return false;
});
// `::search` does not visit the element passed (`middle`)
heap_free(MALLOC_CAPABILITY, LinkedObject::from_ring(middle));
counter++;
TEST(counter == NumberOfListElements - 1,
"Incorrect number of elements freed, expected {}, got {}",
NumberOfListElements - 1,
counter);
// Check that we didn't leak anything in the process
auto heapAtEnd = heap_quota_remaining(MALLOC_CAPABILITY);
TEST(heapAtStart == heapAtEnd,
"The list leaked {} bytes ({} vs. {})",
heapAtEnd - heapAtStart,
heapAtStart,
heapAtEnd);
debug_log("Done testing the list.");
}