blob: 38331108942d23cb3a20a5c3db656af479141bae [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT
#include "test_helper.h"
namespace vm_unittest {
namespace {
// Helper class for managing a PmmNode with real pages. AllocRange and AllocContiguous are not
// supported by the managed PmmNode object. Only a single instance can exist at a time.
class ManagedPmmNode {
public:
static constexpr size_t kNumPages = 64;
static constexpr size_t kDefaultWatermark = kNumPages / 2;
static constexpr size_t kDefaultDebounce = 2;
// Number of pages to alloc from the default config to put the node in a low mem state.
static constexpr size_t kDefaultLowMemAlloc = ManagedPmmNode::kNumPages -
ManagedPmmNode::kDefaultWatermark +
ManagedPmmNode::kDefaultDebounce;
explicit ManagedPmmNode(const uint64_t* watermarks = kDefaultArray, uint8_t watermark_count = 1,
uint64_t debounce = kDefaultDebounce) {
list_node list = LIST_INITIAL_VALUE(list);
ZX_ASSERT(pmm_alloc_pages(kNumPages, 0, &list) == ZX_OK);
vm_page_t* page;
list_for_every_entry (&list, page, vm_page_t, queue_node) {
page->set_state(vm_page_state::FREE);
}
node_.AddFreePages(&list);
ZX_ASSERT(node_.InitReclamation(watermarks, watermark_count, debounce * PAGE_SIZE, this,
StateCallback) == ZX_OK);
node_.InitRequestThread();
}
~ManagedPmmNode() {
list_node list = LIST_INITIAL_VALUE(list);
zx_status_t status = node_.AllocPages(kNumPages, 0, &list);
ASSERT(status == ZX_OK);
vm_page_t* page;
list_for_every_entry (&list, page, vm_page_t, queue_node) {
page->set_state(vm_page_state::ALLOC);
}
pmm_free(&list);
}
uint8_t cur_level() const { return cur_level_; }
PmmNode& node() { return node_; }
private:
PmmNode node_;
uint8_t cur_level_ = MAX_WATERMARK_COUNT + 1;
static void StateCallback(void* context, uint8_t level) {
ManagedPmmNode* instance = reinterpret_cast<ManagedPmmNode*>(context);
instance->cur_level_ = level;
}
static constexpr uint64_t kDefaultArray[1] = {kDefaultWatermark * PAGE_SIZE};
};
} // namespace
// Allocates a single page, translates it to a vm_page_t and frees it.
static bool pmm_smoke_test() {
BEGIN_TEST;
paddr_t pa;
vm_page_t* page;
zx_status_t status = pmm_alloc_page(0, &page, &pa);
ASSERT_EQ(ZX_OK, status, "pmm_alloc single page");
ASSERT_NONNULL(page, "pmm_alloc single page");
ASSERT_NE(0u, pa, "pmm_alloc single page");
vm_page_t* page2 = paddr_to_vm_page(pa);
ASSERT_EQ(page2, page, "paddr_to_vm_page on single page");
pmm_free_page(page);
END_TEST;
}
// Allocates one page and frees it.
static bool pmm_alloc_contiguous_one_test() {
BEGIN_TEST;
list_node list = LIST_INITIAL_VALUE(list);
paddr_t pa;
size_t count = 1U;
zx_status_t status = pmm_alloc_contiguous(count, 0, PAGE_SIZE_SHIFT, &pa, &list);
ASSERT_EQ(ZX_OK, status, "pmm_alloc_contiguous returned failure\n");
ASSERT_EQ(count, list_length(&list), "pmm_alloc_contiguous list size is wrong");
ASSERT_NONNULL(paddr_to_physmap(pa));
pmm_free(&list);
END_TEST;
}
// Allocates more than one page and frees them.
static bool pmm_node_multi_alloc_test() {
BEGIN_TEST;
ManagedPmmNode node;
static constexpr size_t alloc_count = ManagedPmmNode::kNumPages / 2;
list_node list = LIST_INITIAL_VALUE(list);
zx_status_t status = node.node().AllocPages(alloc_count, 0, &list);
EXPECT_EQ(ZX_OK, status, "pmm_alloc_pages a few pages");
EXPECT_EQ(alloc_count, list_length(&list), "pmm_alloc_pages a few pages list count");
status = node.node().AllocPages(alloc_count, 0, &list);
EXPECT_EQ(ZX_OK, status, "pmm_alloc_pages a few pages");
EXPECT_EQ(2 * alloc_count, list_length(&list), "pmm_alloc_pages a few pages list count");
node.node().FreeList(&list);
END_TEST;
}
// Allocates one page from the bulk allocation api.
static bool pmm_node_singlton_list_test() {
BEGIN_TEST;
ManagedPmmNode node;
list_node list = LIST_INITIAL_VALUE(list);
zx_status_t status = node.node().AllocPages(1, 0, &list);
EXPECT_EQ(ZX_OK, status, "pmm_alloc_pages a few pages");
EXPECT_EQ(1ul, list_length(&list), "pmm_alloc_pages a few pages list count");
node.node().FreeList(&list);
END_TEST;
}
// Allocates too many pages and makes sure it fails nicely.
static bool pmm_node_oversized_alloc_test() {
BEGIN_TEST;
ManagedPmmNode node;
list_node list = LIST_INITIAL_VALUE(list);
zx_status_t status = node.node().AllocPages(ManagedPmmNode::kNumPages + 1, 0, &list);
EXPECT_EQ(ZX_ERR_NO_MEMORY, status, "pmm_alloc_pages failed to alloc");
EXPECT_TRUE(list_is_empty(&list), "pmm_alloc_pages list is empty");
END_TEST;
}
// Checks the correctness of the reported watermark level.
static bool pmm_node_watermark_level_test() {
BEGIN_TEST;
ManagedPmmNode node;
list_node list = LIST_INITIAL_VALUE(list);
EXPECT_EQ(node.cur_level(), 1);
while (node.node().CountFreePages() >
(ManagedPmmNode::kDefaultWatermark - ManagedPmmNode::kDefaultDebounce) + 1) {
vm_page_t* page;
zx_status_t status = node.node().AllocPage(0, &page, nullptr);
EXPECT_EQ(ZX_OK, status);
EXPECT_EQ(node.cur_level(), 1);
list_add_tail(&list, &page->queue_node);
}
vm_page_t* page;
zx_status_t status = node.node().AllocPage(0, &page, nullptr);
EXPECT_EQ(ZX_OK, status);
EXPECT_EQ(node.cur_level(), 0);
list_add_tail(&list, &page->queue_node);
while (!list_is_empty(&list)) {
node.node().FreePage(list_remove_head_type(&list, vm_page_t, queue_node));
uint8_t expected = node.node().CountFreePages() >=
ManagedPmmNode::kDefaultWatermark + ManagedPmmNode::kDefaultDebounce;
EXPECT_EQ(node.cur_level(), expected);
}
END_TEST;
}
// Checks the multiple watermark case given in the documentation for |pmm_init_reclamation|.
static bool pmm_node_multi_watermark_level_test() {
BEGIN_TEST;
uint64_t watermarks[4] = {20 * PAGE_SIZE, 40 * PAGE_SIZE, 45 * PAGE_SIZE, 55 * PAGE_SIZE};
ManagedPmmNode node(watermarks, 4, 15);
list_node list = LIST_INITIAL_VALUE(list);
EXPECT_EQ(node.cur_level(), 4);
auto consume_fn = [&](uint64_t level, uint64_t lower_limit) -> bool {
while (node.node().CountFreePages() > lower_limit) {
EXPECT_EQ(node.cur_level(), level);
vm_page_t* page;
zx_status_t status = node.node().AllocPage(0, &page, nullptr);
EXPECT_EQ(ZX_OK, status);
list_add_tail(&list, &page->queue_node);
}
return true;
};
EXPECT_TRUE(consume_fn(4, 40));
EXPECT_TRUE(consume_fn(2, 25));
EXPECT_TRUE(consume_fn(1, 5));
auto release_fn = [&](uint64_t level, uint64_t upper_limit) -> bool {
while (node.node().CountFreePages() < upper_limit) {
EXPECT_EQ(node.cur_level(), level);
node.node().FreePage(list_remove_head_type(&list, vm_page_t, queue_node));
}
return true;
};
EXPECT_TRUE(release_fn(0, 35));
EXPECT_TRUE(release_fn(1, 55));
EXPECT_TRUE(release_fn(4, node.kNumPages));
END_TEST;
}
// A more abstract test for multiple watermarks.
static bool pmm_node_multi_watermark_level_test2() {
BEGIN_TEST;
static constexpr uint64_t kInterval = 7;
uint64_t watermarks[MAX_WATERMARK_COUNT];
for (unsigned i = 0; i < MAX_WATERMARK_COUNT; i++) {
watermarks[i] = (i + 1) * kInterval * PAGE_SIZE;
}
static_assert(kInterval * MAX_WATERMARK_COUNT < ManagedPmmNode::kNumPages);
ManagedPmmNode node(watermarks, MAX_WATERMARK_COUNT);
list_node list = LIST_INITIAL_VALUE(list);
EXPECT_EQ(node.cur_level(), MAX_WATERMARK_COUNT);
uint64_t count = ManagedPmmNode::kNumPages;
while (node.node().CountFreePages() > 0) {
vm_page_t* page;
zx_status_t status = node.node().AllocPage(0, &page, nullptr);
EXPECT_EQ(ZX_OK, status);
list_add_tail(&list, &page->queue_node);
count--;
uint64_t expected = ktl::min(static_cast<uint64_t>(MAX_WATERMARK_COUNT),
(count + ManagedPmmNode::kDefaultDebounce - 1) / kInterval);
EXPECT_EQ(node.cur_level(), expected);
}
vm_page_t* page;
zx_status_t status = node.node().AllocPage(0, &page, nullptr);
EXPECT_EQ(ZX_ERR_NO_MEMORY, status);
EXPECT_EQ(node.cur_level(), 0);
while (!list_is_empty(&list)) {
node.node().FreePage(list_remove_head_type(&list, vm_page_t, queue_node));
count++;
uint64_t expected = ktl::min(static_cast<uint64_t>(MAX_WATERMARK_COUNT),
count > ManagedPmmNode::kDefaultDebounce
? (count - ManagedPmmNode::kDefaultDebounce) / kInterval
: 0);
EXPECT_EQ(node.cur_level(), expected);
}
END_TEST;
}
// Checks sync allocation failure when the node is in a low-memory state.
static bool pmm_node_oom_sync_alloc_failure_test() {
BEGIN_TEST;
ManagedPmmNode node;
list_node list = LIST_INITIAL_VALUE(list);
// Put the node in an oom state and make sure allocation fails.
zx_status_t status = node.node().AllocPages(ManagedPmmNode::kDefaultLowMemAlloc, 0, &list);
EXPECT_EQ(ZX_OK, status);
EXPECT_EQ(node.cur_level(), 0);
vm_page_t* page;
status = node.node().AllocPage(PMM_ALLOC_DELAY_OK, &page, nullptr);
EXPECT_EQ(status, ZX_ERR_NO_MEMORY);
// Free the list and make sure allocations work again.
node.node().FreeList(&list);
status = node.node().AllocPage(PMM_ALLOC_DELAY_OK, &page, nullptr);
EXPECT_EQ(ZX_OK, status);
node.node().FreePage(page);
END_TEST;
}
// Checks async allocation queued while the node is in a low-memory state.
static bool pmm_node_delayed_alloc_test() {
BEGIN_TEST;
ManagedPmmNode node;
list_node list = LIST_INITIAL_VALUE(list);
zx_status_t status = node.node().AllocPages(ManagedPmmNode::kDefaultLowMemAlloc, 0, &list);
EXPECT_EQ(ZX_OK, status);
EXPECT_EQ(node.cur_level(), 0);
vm_page_t* page;
status = node.node().AllocPage(PMM_ALLOC_DELAY_OK, &page, nullptr);
EXPECT_EQ(status, ZX_ERR_NO_MEMORY);
static constexpr uint64_t kOffset = 1;
static constexpr uint64_t kLen = 3 * ManagedPmmNode::kDefaultDebounce;
TestPageRequest request(&node.node(), kOffset, kLen);
node.node().AllocPages(0, request.request());
EXPECT_EQ(node.cur_level(), 0);
for (unsigned i = 0; i < 2 * ManagedPmmNode::kDefaultDebounce; i++) {
node.node().FreePage(list_remove_head_type(&list, vm_page, queue_node));
}
EXPECT_EQ(node.cur_level(), 1);
uint64_t expected_off, expected_len, actual_supplied;
request.WaitForAvailable(&expected_off, &expected_len, &actual_supplied);
EXPECT_EQ(expected_off, kOffset);
EXPECT_EQ(expected_len, kLen);
EXPECT_EQ(actual_supplied, 2 * ManagedPmmNode::kDefaultDebounce);
EXPECT_EQ(request.drop_ref_evt().Wait(Deadline::no_slack(ZX_TIME_INFINITE_PAST)),
ZX_ERR_TIMED_OUT);
node.node().FreeList(&list);
request.WaitForAvailable(&expected_off, &expected_len, &actual_supplied);
EXPECT_EQ(expected_off, kOffset + 2 * ManagedPmmNode::kDefaultDebounce);
EXPECT_EQ(expected_len, kLen - 2 * ManagedPmmNode::kDefaultDebounce);
EXPECT_EQ(actual_supplied, kLen - 2 * ManagedPmmNode::kDefaultDebounce);
EXPECT_EQ(request.drop_ref_evt().Wait(Deadline::no_slack(ZX_TIME_INFINITE)), ZX_OK);
EXPECT_EQ(list_length(request.page_list()), kLen);
node.node().FreeList(request.page_list());
END_TEST;
}
// Checks async allocation queued while the node is not in a low-memory state.
static bool pmm_node_delayed_alloc_no_lowmem_test() {
BEGIN_TEST;
ManagedPmmNode node;
TestPageRequest request(&node.node(), 0, 1);
node.node().AllocPages(0, request.request());
uint64_t expected_off, expected_len, actual_supplied;
request.WaitForAvailable(&expected_off, &expected_len, &actual_supplied);
EXPECT_EQ(expected_off, 0ul);
EXPECT_EQ(expected_len, 1ul);
EXPECT_EQ(actual_supplied, 1ul);
EXPECT_EQ(request.drop_ref_evt().Wait(Deadline::no_slack(ZX_TIME_INFINITE)), ZX_OK);
EXPECT_EQ(list_length(request.page_list()), 1ul);
node.node().FreeList(request.page_list());
END_TEST;
}
// Checks swapping out the page_request_t backing a request, either before the request
// starts being serviced or while the request is being serviced (depending on |early|).
static bool pmm_node_delayed_alloc_swap_test_helper(bool early) {
BEGIN_TEST;
ManagedPmmNode node;
list_node list = LIST_INITIAL_VALUE(list);
zx_status_t status = node.node().AllocPages(ManagedPmmNode::kDefaultLowMemAlloc, 0, &list);
EXPECT_EQ(ZX_OK, status);
EXPECT_EQ(node.cur_level(), 0);
vm_page_t* page;
status = node.node().AllocPage(PMM_ALLOC_DELAY_OK, &page, nullptr);
EXPECT_EQ(status, ZX_ERR_NO_MEMORY);
TestPageRequest request(&node.node(), 0, 1);
node.node().AllocPages(0, request.request());
page_request_t new_mem = *request.request();
if (early) {
node.node().SwapRequest(request.request(), &new_mem);
}
EXPECT_EQ(node.cur_level(), 0);
for (unsigned i = 0; i < 2 * ManagedPmmNode::kDefaultDebounce; i++) {
node.node().FreePage(list_remove_head_type(&list, vm_page, queue_node));
}
EXPECT_EQ(node.cur_level(), 1);
if (!early) {
EXPECT_EQ(request.on_pages_avail_evt().Wait(Deadline::infinite()), ZX_OK);
node.node().SwapRequest(request.request(), &new_mem);
}
uint64_t expected_off, expected_len, actual_supplied;
request.WaitForAvailable(&expected_off, &expected_len, &actual_supplied);
EXPECT_EQ(expected_off, 0ul);
EXPECT_EQ(expected_len, 1ul);
EXPECT_EQ(actual_supplied, 1ul);
EXPECT_EQ(request.drop_ref_evt().Wait(Deadline::infinite()), ZX_OK);
EXPECT_EQ(list_length(request.page_list()), 1ul);
node.node().FreeList(&list);
node.node().FreeList(request.page_list());
END_TEST;
}
static bool pmm_node_delayed_alloc_swap_early_test() {
return pmm_node_delayed_alloc_swap_test_helper(true);
}
static bool pmm_node_delayed_alloc_swap_late_test() {
return pmm_node_delayed_alloc_swap_test_helper(false);
}
// Checks cancelling the page_request_t backing a request, either before the request
// starts being serviced or while the request is being serviced (depending on |early|).
static bool pmm_node_delayed_alloc_clear_test_helper(bool early) {
BEGIN_TEST;
ManagedPmmNode node;
list_node list = LIST_INITIAL_VALUE(list);
zx_status_t status = node.node().AllocPages(ManagedPmmNode::kDefaultLowMemAlloc, 0, &list);
EXPECT_EQ(ZX_OK, status);
EXPECT_EQ(node.cur_level(), 0);
vm_page_t* page;
status = node.node().AllocPage(PMM_ALLOC_DELAY_OK, &page, nullptr);
EXPECT_EQ(status, ZX_ERR_NO_MEMORY);
TestPageRequest request(&node.node(), 0, 1);
node.node().AllocPages(0, request.request());
if (early) {
EXPECT_TRUE(request.Cancel());
}
EXPECT_EQ(node.cur_level(), 0);
for (unsigned i = 0; i < 2 * ManagedPmmNode::kDefaultDebounce; i++) {
node.node().FreePage(list_remove_head_type(&list, vm_page, queue_node));
}
EXPECT_EQ(node.cur_level(), 1);
if (!early) {
EXPECT_EQ(request.on_pages_avail_evt().Wait(Deadline::infinite()), ZX_OK);
EXPECT_FALSE(request.Cancel());
EXPECT_EQ(request.drop_ref_evt().Wait(Deadline::infinite()), ZX_OK);
} else {
EXPECT_EQ(request.drop_ref_evt().Wait(Deadline::no_slack(ZX_TIME_INFINITE_PAST)),
ZX_ERR_TIMED_OUT);
request.drop_ref_evt().Signal();
}
EXPECT_EQ(list_length(request.page_list()), 0ul);
node.node().FreeList(&list);
END_TEST;
}
static bool pmm_node_delayed_alloc_clear_early_test() {
return pmm_node_delayed_alloc_clear_test_helper(true);
}
static bool pmm_node_delayed_alloc_clear_late_test() {
return pmm_node_delayed_alloc_clear_test_helper(false);
}
static bool pmm_checker_test_with_fill_size(size_t fill_size) {
BEGIN_TEST;
PmmChecker checker;
// Starts off unarmed.
EXPECT_FALSE(checker.IsArmed());
// Borrow a real page from the PMM, ask the checker to validate it. See that because the checker
// is not armed, |ValidatePattern| still returns true even though the page has no pattern.
vm_page_t* page;
EXPECT_EQ(pmm_alloc_page(0, &page), ZX_OK);
page->set_state(vm_page_state::FREE);
auto p = static_cast<uint8_t*>(paddr_to_physmap(page->paddr()));
memset(p, 0, PAGE_SIZE);
EXPECT_TRUE(checker.ValidatePattern(page));
checker.AssertPattern(page);
// Set the fill size and see that |GetFillSize| returns the size.
checker.SetFillSize(fill_size);
EXPECT_EQ(fill_size, checker.GetFillSize());
// Arm the checker and see that |ValidatePattern| returns false.
checker.Arm();
EXPECT_TRUE(checker.IsArmed());
EXPECT_FALSE(checker.ValidatePattern(page));
// Fill with pattern one less than the fill size and see that it does not pass validation.
memset(p, 0, fill_size - 1);
EXPECT_FALSE(checker.ValidatePattern(page));
// Fill with the full pattern and see that it validates.
checker.FillPattern(page);
for (size_t i = 0; i < fill_size; ++i) {
EXPECT_NE(0, p[i]);
}
EXPECT_TRUE(checker.ValidatePattern(page));
// Corrupt the page after the first |fill_size| bytes and see that the corruption is not detected.
if (fill_size < PAGE_SIZE) {
p[fill_size] = 1;
EXPECT_TRUE(checker.ValidatePattern(page));
}
// Corrupt the page within the first |fill_size| bytes and see that the corruption is detected.
p[fill_size - 1] = 1;
EXPECT_FALSE(checker.ValidatePattern(page));
// Disarm the checker and see that it now passes.
checker.Disarm();
EXPECT_FALSE(checker.IsArmed());
EXPECT_TRUE(checker.ValidatePattern(page));
checker.AssertPattern(page);
page->set_state(vm_page_state::ALLOC);
pmm_free_page(page);
END_TEST;
}
static bool pmm_checker_test() {
BEGIN_TEST;
EXPECT_TRUE(pmm_checker_test_with_fill_size(8));
EXPECT_TRUE(pmm_checker_test_with_fill_size(16));
EXPECT_TRUE(pmm_checker_test_with_fill_size(512));
EXPECT_TRUE(pmm_checker_test_with_fill_size(PAGE_SIZE));
END_TEST;
}
static bool pmm_checker_action_from_string_test() {
BEGIN_TEST;
EXPECT_FALSE(PmmChecker::ActionFromString(""));
EXPECT_FALSE(PmmChecker::ActionFromString("blah"));
EXPECT_EQ(static_cast<uint32_t>(PmmChecker::Action::OOPS),
static_cast<uint32_t>(PmmChecker::ActionFromString("oops").value()));
EXPECT_EQ(static_cast<uint32_t>(PmmChecker::Action::PANIC),
static_cast<uint32_t>(PmmChecker::ActionFromString("panic").value()));
END_TEST;
}
static bool pmm_checker_is_valid_fill_size_test() {
BEGIN_TEST;
EXPECT_FALSE(PmmChecker::IsValidFillSize(0));
EXPECT_FALSE(PmmChecker::IsValidFillSize(7));
EXPECT_FALSE(PmmChecker::IsValidFillSize(9));
EXPECT_FALSE(PmmChecker::IsValidFillSize(PAGE_SIZE + 8));
EXPECT_FALSE(PmmChecker::IsValidFillSize(PAGE_SIZE * 2));
EXPECT_TRUE(PmmChecker::IsValidFillSize(8));
EXPECT_TRUE(PmmChecker::IsValidFillSize(16));
EXPECT_TRUE(PmmChecker::IsValidFillSize(24));
EXPECT_TRUE(PmmChecker::IsValidFillSize(512));
EXPECT_TRUE(PmmChecker::IsValidFillSize(PAGE_SIZE));
END_TEST;
}
static bool pmm_get_arena_info_test() {
BEGIN_TEST;
const size_t num_arenas = pmm_num_arenas();
ASSERT_GT(num_arenas, 0u);
fbl::AllocChecker ac;
auto buffer = ktl::unique_ptr<pmm_arena_info_t[]>(new (&ac) pmm_arena_info_t[num_arenas]);
ASSERT(ac.check());
const size_t buffer_size = num_arenas * sizeof(pmm_arena_info_t);
// Not enough room for one.
zx_status_t status = pmm_get_arena_info(1, 0, buffer.get(), sizeof(pmm_arena_info_t) - 1);
ASSERT_EQ(status, ZX_ERR_BUFFER_TOO_SMALL);
// Asking for none.
status = pmm_get_arena_info(0, 0, buffer.get(), buffer_size);
ASSERT_EQ(status, ZX_ERR_OUT_OF_RANGE);
// Asking for more than exist.
status = pmm_get_arena_info(num_arenas + 1, 0, buffer.get(), buffer_size);
ASSERT_EQ(status, ZX_ERR_OUT_OF_RANGE);
// Attempting to skip them all.
status = pmm_get_arena_info(1, num_arenas, buffer.get(), buffer_size);
ASSERT_EQ(status, ZX_ERR_OUT_OF_RANGE);
// Asking for one.
status = pmm_get_arena_info(1, 0, buffer.get(), buffer_size);
ASSERT_EQ(status, ZX_OK);
// Asking for them all.
status = pmm_get_arena_info(num_arenas, 0, buffer.get(), buffer_size);
ASSERT_EQ(status, ZX_OK);
// See they are in ascending order by base.
paddr_t prev = 0;
for (unsigned i = 0; i < num_arenas; ++i) {
if (i == 0) {
ASSERT_GE(buffer[i].base, prev);
} else {
ASSERT_GT(buffer[i].base, prev);
}
prev = buffer[i].base;
ASSERT_GT(buffer[i].size, 0u);
}
END_TEST;
}
static void SetPageStateRange(enum vm_page_state state, vm_page_t* start, int count) {
for (int i = 0; i < count; ++i) {
(start + i)->set_state(state);
}
}
static bool pmm_arena_find_free_contiguous_test() {
BEGIN_TEST;
static constexpr size_t kNumPages = 8;
const vaddr_t base = 0x1001000;
const pmm_arena_info_t info{"test arena", 0, base, kNumPages * PAGE_SIZE};
vm_page_t page_array[kNumPages]{};
PmmArena arena;
ASSERT_EQ(ZX_OK, arena.InitForTest(info, page_array));
// page_array is as follow (0 == free, 1 == allocated):
//
// [00000000]
//
// Ask for some sizes and alignments that can't possibly succeed.
ASSERT_EQ(nullptr, arena.FindFreeContiguous(kNumPages + 1, PAGE_SIZE_SHIFT));
ASSERT_EQ(nullptr, arena.FindFreeContiguous(kNumPages + 2, PAGE_SIZE_SHIFT));
ASSERT_EQ(nullptr, arena.FindFreeContiguous(kNumPages + 3, PAGE_SIZE_SHIFT));
ASSERT_EQ(nullptr, arena.FindFreeContiguous(kNumPages + 4, PAGE_SIZE_SHIFT));
ASSERT_EQ(nullptr, arena.FindFreeContiguous(1, 24)); // 16MB aligned
ASSERT_EQ(nullptr, arena.FindFreeContiguous(1, 25)); // 32MB aligned
ASSERT_EQ(nullptr, arena.FindFreeContiguous(1, 26)); // 64MB aligned
ASSERT_EQ(nullptr, arena.FindFreeContiguous(1, 27)); // 128MB aligned
// [00000000]
//
// Ask for 4 pages, aligned on a 2-page boundary. See that the first page is skipped.
vm_page_t* result = arena.FindFreeContiguous(4, PAGE_SIZE_SHIFT + 1);
ASSERT_EQ(&page_array[1], result);
SetPageStateRange(vm_page_state::ALLOC, result, 4);
// [01111000]
//
// Ask for various sizes and see that they all fail.
ASSERT_EQ(nullptr, arena.FindFreeContiguous(4, PAGE_SIZE_SHIFT));
ASSERT_EQ(nullptr, arena.FindFreeContiguous(5, PAGE_SIZE_SHIFT));
ASSERT_EQ(nullptr, arena.FindFreeContiguous(6, PAGE_SIZE_SHIFT));
ASSERT_EQ(nullptr, arena.FindFreeContiguous(7, PAGE_SIZE_SHIFT));
ASSERT_EQ(nullptr, arena.FindFreeContiguous(8, PAGE_SIZE_SHIFT));
ASSERT_EQ(nullptr, arena.FindFreeContiguous(9, PAGE_SIZE_SHIFT));
// [01111000]
//
// Ask for 3 pages.
result = arena.FindFreeContiguous(3, PAGE_SIZE_SHIFT);
ASSERT_EQ(&page_array[5], result);
SetPageStateRange(vm_page_state::ALLOC, result, 3);
// [01111111]
//
// Ask for various sizes and see that they all fail.
ASSERT_EQ(nullptr, arena.FindFreeContiguous(2, PAGE_SIZE_SHIFT));
ASSERT_EQ(nullptr, arena.FindFreeContiguous(3, PAGE_SIZE_SHIFT));
ASSERT_EQ(nullptr, arena.FindFreeContiguous(4, PAGE_SIZE_SHIFT));
// [01111111]
//
// Ask for the last remaining page.
result = arena.FindFreeContiguous(1, PAGE_SIZE_SHIFT);
ASSERT_EQ(&page_array[0], result);
SetPageStateRange(vm_page_state::ALLOC, result, 1);
// [11111111]
//
// See there are none left.
ASSERT_EQ(nullptr, arena.FindFreeContiguous(1, PAGE_SIZE_SHIFT));
END_TEST;
}
static bool pq_add_remove() {
BEGIN_TEST;
PageQueues pq;
// Pretend we have an allocated page
vm_page_t test_page = {};
test_page.set_state(vm_page_state::OBJECT);
// Need a VMO to claim our pager backed page is in
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::Create(0, 0, PAGE_SIZE, &vmo);
ASSERT_EQ(ZX_OK, status);
// Put the page in each queue and make sure it shows up
pq.SetWired(&test_page);
EXPECT_TRUE(pq.DebugPageIsWired(&test_page));
EXPECT_TRUE(pq.DebugQueueCounts() == ((PageQueues::Counts){{0}, 0, 0, 1, 0}));
pq.Remove(&test_page);
EXPECT_FALSE(pq.DebugPageIsWired(&test_page));
EXPECT_TRUE(pq.DebugQueueCounts() == ((PageQueues::Counts){{0}, 0, 0, 0, 0}));
pq.SetUnswappable(&test_page);
EXPECT_TRUE(pq.DebugPageIsUnswappable(&test_page));
EXPECT_TRUE(pq.DebugQueueCounts() == ((PageQueues::Counts){{0}, 0, 1, 0, 0}));
pq.Remove(&test_page);
EXPECT_FALSE(pq.DebugPageIsUnswappable(&test_page));
EXPECT_TRUE(pq.DebugQueueCounts() == ((PageQueues::Counts){{0}, 0, 0, 0, 0}));
// Pretend we have some kind of pointer to a VmObjectPaged (this will never get dereferenced)
pq.SetPagerBacked(&test_page, vmo->DebugGetCowPages().get(), 0);
EXPECT_TRUE(pq.DebugPageIsPagerBacked(&test_page));
EXPECT_TRUE(pq.DebugQueueCounts() == ((PageQueues::Counts){{1, 0, 0, 0}, 0, 0, 0, 0}));
pq.Remove(&test_page);
EXPECT_FALSE(pq.DebugPageIsPagerBacked(&test_page));
EXPECT_TRUE(pq.DebugQueueCounts() == ((PageQueues::Counts){{0}, 0, 0, 0, 0}));
END_TEST;
}
static bool pq_move_queues() {
BEGIN_TEST;
PageQueues pq;
// Pretend we have an allocated page
vm_page_t test_page = {};
test_page.set_state(vm_page_state::OBJECT);
// Need a VMO to claim our pager backed page is in
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::Create(0, 0, PAGE_SIZE, &vmo);
ASSERT_EQ(ZX_OK, status);
// Move the page between queues.
pq.SetWired(&test_page);
EXPECT_TRUE(pq.DebugPageIsWired(&test_page));
EXPECT_TRUE(pq.DebugQueueCounts() == ((PageQueues::Counts){{0}, 0, 0, 1, 0}));
pq.MoveToUnswappable(&test_page);
EXPECT_FALSE(pq.DebugPageIsWired(&test_page));
EXPECT_TRUE(pq.DebugPageIsUnswappable(&test_page));
EXPECT_TRUE(pq.DebugQueueCounts() == ((PageQueues::Counts){{0}, 0, 1, 0, 0}));
pq.MoveToPagerBacked(&test_page, vmo->DebugGetCowPages().get(), 0);
EXPECT_FALSE(pq.DebugPageIsUnswappable(&test_page));
EXPECT_TRUE(pq.DebugPageIsPagerBacked(&test_page));
EXPECT_TRUE(pq.DebugQueueCounts() == ((PageQueues::Counts){{1, 0, 0, 0}, 0, 0, 0, 0}));
pq.MoveToPagerBackedInactive(&test_page);
EXPECT_FALSE(pq.DebugPageIsPagerBacked(&test_page));
EXPECT_TRUE(pq.DebugPageIsPagerBackedInactive(&test_page));
EXPECT_TRUE(pq.DebugQueueCounts() == ((PageQueues::Counts){{0}, 1, 0, 0, 0}));
// Verify that the inactive page is first in line for eviction.
auto backlink = pq.PeekPagerBacked(PageQueues::kNumPagerBacked - 1);
EXPECT_TRUE(backlink != ktl::nullopt && backlink->page == &test_page);
pq.MoveToWired(&test_page);
EXPECT_FALSE(pq.DebugPageIsPagerBackedInactive(&test_page));
EXPECT_FALSE(pq.DebugPageIsPagerBacked(&test_page));
EXPECT_TRUE(pq.DebugPageIsWired(&test_page));
EXPECT_TRUE(pq.DebugQueueCounts() == ((PageQueues::Counts){{0}, 0, 0, 1, 0}));
pq.Remove(&test_page);
EXPECT_TRUE(pq.DebugQueueCounts() == ((PageQueues::Counts){{0}, 0, 0, 0, 0}));
END_TEST;
}
static bool pq_move_self_queue() {
BEGIN_TEST;
PageQueues pq;
// Pretend we have an allocated page
vm_page_t test_page = {};
test_page.set_state(vm_page_state::OBJECT);
// Move the page into the queue it is already in.
pq.SetWired(&test_page);
EXPECT_TRUE(pq.DebugPageIsWired(&test_page));
EXPECT_TRUE(pq.DebugQueueCounts() == ((PageQueues::Counts){{0}, 0, 0, 1, 0}));
pq.MoveToWired(&test_page);
EXPECT_TRUE(pq.DebugPageIsWired(&test_page));
EXPECT_TRUE(pq.DebugQueueCounts() == ((PageQueues::Counts){{0}, 0, 0, 1, 0}));
pq.Remove(&test_page);
EXPECT_TRUE(pq.DebugQueueCounts() == ((PageQueues::Counts){{0}, 0, 0, 0, 0}));
END_TEST;
}
static bool pq_rotate_queue() {
BEGIN_TEST;
PageQueues pq;
// Pretend we have a couple of allocated pages.
vm_page_t wired_page = {};
vm_page_t pager_page = {};
wired_page.set_state(vm_page_state::OBJECT);
pager_page.set_state(vm_page_state::OBJECT);
// Need a VMO to claim our pager backed page is in.
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::Create(0, 0, PAGE_SIZE, &vmo);
ASSERT_EQ(ZX_OK, status);
// Put the pages in and validate initial state.
pq.SetWired(&wired_page);
pq.SetPagerBacked(&pager_page, vmo->DebugGetCowPages().get(), 0);
EXPECT_TRUE(pq.DebugPageIsWired(&wired_page));
size_t queue;
EXPECT_TRUE(pq.DebugPageIsPagerBacked(&pager_page, &queue));
EXPECT_TRUE(pq.DebugQueueCounts() == ((PageQueues::Counts){{1, 0, 0, 0}, 0, 0, 1, 0}));
EXPECT_EQ(queue, 0u);
// Gradually rotate the queue.
pq.RotatePagerBackedQueues();
EXPECT_TRUE(pq.DebugPageIsWired(&wired_page));
EXPECT_TRUE(pq.DebugPageIsPagerBacked(&pager_page, &queue));
EXPECT_TRUE(pq.DebugQueueCounts() == ((PageQueues::Counts){{0, 1, 0, 0}, 0, 0, 1, 0}));
EXPECT_EQ(queue, 1u);
pq.RotatePagerBackedQueues();
EXPECT_TRUE(pq.DebugQueueCounts() == ((PageQueues::Counts){{0, 0, 1, 0}, 0, 0, 1, 0}));
pq.RotatePagerBackedQueues();
EXPECT_TRUE(pq.DebugQueueCounts() == ((PageQueues::Counts){{0, 0, 0, 1}, 0, 0, 1, 0}));
// Further rotations should not move the page.
pq.RotatePagerBackedQueues();
EXPECT_TRUE(pq.DebugPageIsWired(&wired_page));
EXPECT_TRUE(pq.DebugPageIsPagerBacked(&pager_page));
EXPECT_TRUE(pq.DebugQueueCounts() == ((PageQueues::Counts){{0, 0, 0, 1}, 0, 0, 1, 0}));
// Moving the page should bring it back to the first queue.
pq.MoveToPagerBacked(&pager_page, vmo->DebugGetCowPages().get(), 0);
EXPECT_TRUE(pq.DebugPageIsWired(&wired_page));
EXPECT_TRUE(pq.DebugPageIsPagerBacked(&pager_page));
EXPECT_TRUE(pq.DebugQueueCounts() == ((PageQueues::Counts){{1, 0, 0, 0}, 0, 0, 1, 0}));
// Just double check one rotation.
pq.RotatePagerBackedQueues();
EXPECT_TRUE(pq.DebugQueueCounts() == ((PageQueues::Counts){{0, 1, 0, 0}, 0, 0, 1, 0}));
pq.Remove(&wired_page);
pq.Remove(&pager_page);
END_TEST;
}
static bool physmap_for_each_gap_test() {
BEGIN_TEST;
struct Gap {
vaddr_t base;
size_t size;
};
fbl::Vector<Gap> actual_gaps;
fbl::AllocChecker ac;
auto PushBack = [&](vaddr_t base, size_t size) {
actual_gaps.push_back({base, size}, &ac);
ASSERT(ac.check());
};
{
// No arenas, [ ].
actual_gaps.reset();
physmap_for_each_gap(PushBack, nullptr, 0);
// One gap covering the entire physmap.
ASSERT_EQ(actual_gaps.size(), 1u);
ASSERT_EQ(actual_gaps[0].base, PHYSMAP_BASE);
ASSERT_EQ(actual_gaps[0].size, PHYSMAP_SIZE);
}
{
// One arena, no gaps, [A].
actual_gaps.reset();
pmm_arena_info_t arenas[] = {
{"test-arena", 0, PHYSMAP_BASE_PHYS, PHYSMAP_SIZE},
};
physmap_for_each_gap(PushBack, arenas, ktl::size(arenas));
// No gaps.
ASSERT_EQ(actual_gaps.size(), 0u);
}
{
// One arena, gap at bottom, [ A].
actual_gaps.reset();
const size_t gap_size = 0x1000;
const size_t arena_size = PHYSMAP_SIZE - gap_size;
pmm_arena_info_t arenas[] = {
{"test-arena", 0, PHYSMAP_BASE_PHYS + gap_size, arena_size},
};
physmap_for_each_gap(PushBack, arenas, ktl::size(arenas));
// One gap.
ASSERT_EQ(actual_gaps.size(), 1u);
ASSERT_EQ(actual_gaps[0].base, PHYSMAP_BASE);
ASSERT_EQ(actual_gaps[0].size, gap_size);
}
{
// One arena, gap at top, [A ].
actual_gaps.reset();
const size_t gap_size = 0x5000;
const size_t arena_size = PHYSMAP_SIZE - gap_size;
pmm_arena_info_t arenas[] = {
{"test-arena", 0, PHYSMAP_BASE_PHYS, arena_size},
};
physmap_for_each_gap(PushBack, arenas, ktl::size(arenas));
// One gap.
ASSERT_EQ(actual_gaps.size(), 1u);
ASSERT_EQ(actual_gaps[0].base, PHYSMAP_BASE + arena_size);
ASSERT_EQ(actual_gaps[0].size, gap_size);
}
{
// Two arenas, no gaps, [AA].
actual_gaps.reset();
const size_t size = PHYSMAP_SIZE / 2;
pmm_arena_info_t arenas[] = {
{"test-arena", 0, PHYSMAP_BASE_PHYS, size},
{"test-arena", 0, PHYSMAP_BASE_PHYS + size, size},
};
physmap_for_each_gap(PushBack, arenas, ktl::size(arenas));
// No gaps.
ASSERT_EQ(actual_gaps.size(), 0u);
}
{
// Two arenas, three gaps, [ A A ].
actual_gaps.reset();
const size_t gap1_size = 0x300000;
const size_t arena1_offset = gap1_size;
const size_t arena1_size = 0x1000000;
const size_t gap2_size = 0x35000;
const size_t arena2_offset = gap1_size + arena1_size + gap2_size;
const size_t arena2_size = 0xff1000000;
pmm_arena_info_t arenas[] = {
{"test-arena", 0, PHYSMAP_BASE_PHYS + arena1_offset, arena1_size},
{"test-arena", 0, PHYSMAP_BASE_PHYS + arena2_offset, arena2_size},
};
physmap_for_each_gap(PushBack, arenas, ktl::size(arenas));
// Three gaps.
ASSERT_EQ(actual_gaps.size(), 3u);
ASSERT_EQ(actual_gaps[0].base, PHYSMAP_BASE);
ASSERT_EQ(actual_gaps[0].size, gap1_size);
ASSERT_EQ(actual_gaps[1].base, PHYSMAP_BASE + arena1_offset + arena1_size);
ASSERT_EQ(actual_gaps[1].size, gap2_size);
const size_t arena3_offset = gap1_size + arena1_size + gap2_size + arena2_size;
ASSERT_EQ(actual_gaps[2].base, PHYSMAP_BASE + arena3_offset);
ASSERT_EQ(actual_gaps[2].size, PHYSMAP_SIZE - arena3_offset);
}
END_TEST;
}
#if __has_feature(address_sanitizer)
static bool kasan_detects_use_after_free() {
BEGIN_TEST;
// TODO(fxbug.dev/30033): Enable on arm64 when kasan poisoning works there.
#if defined(__x86_64__)
ManagedPmmNode node;
vm_page_t* page;
paddr_t paddr;
zx_status_t status = node.node().AllocPage(PMM_ALLOC_DELAY_OK, &page, &paddr);
ASSERT_EQ(ZX_OK, status, "pmm_alloc_page one page");
ASSERT_NE(paddr, 0UL);
EXPECT_EQ(0UL, asan_region_is_poisoned(reinterpret_cast<uintptr_t>(paddr_to_physmap(paddr)),
PAGE_SIZE));
node.node().FreePage(page);
EXPECT_TRUE(asan_entire_region_is_poisoned(reinterpret_cast<uintptr_t>(paddr_to_physmap(paddr)),
PAGE_SIZE));
#endif
END_TEST;
}
#endif // __has_feature(address_sanitizer)
UNITTEST_START_TESTCASE(pmm_tests)
VM_UNITTEST(pmm_smoke_test)
VM_UNITTEST(pmm_alloc_contiguous_one_test)
VM_UNITTEST(pmm_node_multi_alloc_test)
VM_UNITTEST(pmm_node_singlton_list_test)
VM_UNITTEST(pmm_node_oversized_alloc_test)
VM_UNITTEST(pmm_node_watermark_level_test)
VM_UNITTEST(pmm_node_multi_watermark_level_test)
VM_UNITTEST(pmm_node_multi_watermark_level_test2)
VM_UNITTEST(pmm_node_oom_sync_alloc_failure_test)
VM_UNITTEST(pmm_node_delayed_alloc_test)
VM_UNITTEST(pmm_node_delayed_alloc_no_lowmem_test)
VM_UNITTEST(pmm_node_delayed_alloc_swap_early_test)
VM_UNITTEST(pmm_node_delayed_alloc_swap_late_test)
VM_UNITTEST(pmm_node_delayed_alloc_clear_early_test)
VM_UNITTEST(pmm_node_delayed_alloc_clear_late_test)
VM_UNITTEST(pmm_checker_test)
VM_UNITTEST(pmm_checker_action_from_string_test)
VM_UNITTEST(pmm_checker_is_valid_fill_size_test)
VM_UNITTEST(pmm_get_arena_info_test)
VM_UNITTEST(pmm_arena_find_free_contiguous_test)
UNITTEST_END_TESTCASE(pmm_tests, "pmm", "Physical memory manager tests")
UNITTEST_START_TESTCASE(page_queues_tests)
VM_UNITTEST(pq_add_remove)
VM_UNITTEST(pq_move_queues)
VM_UNITTEST(pq_move_self_queue)
VM_UNITTEST(pq_rotate_queue)
UNITTEST_END_TESTCASE(page_queues_tests, "pq", "PageQueues tests")
UNITTEST_START_TESTCASE(physmap_tests)
VM_UNITTEST(physmap_for_each_gap_test)
UNITTEST_END_TESTCASE(physmap_tests, "physmap", "physmap tests")
#if __has_feature(address_sanitizer)
UNITTEST_START_TESTCASE(kasan_pmm_tests)
VM_UNITTEST(kasan_detects_use_after_free)
UNITTEST_END_TESTCASE(kasan_pmm_tests, "kasan_pmm", "kasan pmm tests")
#endif // __has_feature(address_sanitizer)
} // namespace vm_unittest