blob: 5709d0d8cf8d57ba2cf00b9398a73a723813192e [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 <lib/fit/defer.h>
#include "test_helper.h"
namespace vm_unittest {
// Creates a vm object.
static bool vmo_create_test() {
BEGIN_TEST;
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, PAGE_SIZE, &vmo);
ASSERT_EQ(status, ZX_OK);
ASSERT_TRUE(vmo);
EXPECT_FALSE(vmo->is_contiguous(), "vmo is not contig\n");
EXPECT_FALSE(vmo->is_resizable(), "vmo is not resizable\n");
END_TEST;
}
static bool vmo_create_maximum_size() {
BEGIN_TEST;
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, 0xfffffffffffe0000, &vmo);
EXPECT_EQ(status, ZX_OK, "should be ok\n");
status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, 0xfffffffffffe1000, &vmo);
EXPECT_EQ(status, ZX_ERR_OUT_OF_RANGE, "should be too large\n");
END_TEST;
}
// Helper that tests if all pages in a vmo in the specified range pass the given predicate.
template <typename F>
static bool AllPagesMatch(VmObject* vmo, F pred, uint64_t offset, uint64_t len) {
bool pred_matches = true;
zx_status_t status =
vmo->Lookup(offset, len, [&pred, &pred_matches](uint64_t offset, paddr_t pa) {
const vm_page_t* p = paddr_to_vm_page(pa);
if (!pred(p)) {
pred_matches = false;
return ZX_ERR_STOP;
}
return ZX_ERR_NEXT;
});
return status == ZX_OK ? pred_matches : false;
}
static bool PagesInAnyUnswappableQueue(VmObject* vmo, uint64_t offset, uint64_t len) {
return AllPagesMatch(
vmo, [](const vm_page_t* p) { return pmm_page_queues()->DebugPageIsAnyUnswappable(p); },
offset, len);
}
static bool PagesInWiredQueue(VmObject* vmo, uint64_t offset, uint64_t len) {
return AllPagesMatch(
vmo, [](const vm_page_t* p) { return pmm_page_queues()->DebugPageIsWired(p); }, offset, len);
}
// Creates a vm object, commits memory.
static bool vmo_commit_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
static const size_t alloc_size = PAGE_SIZE * 16;
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, alloc_size, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
auto ret = vmo->CommitRange(0, alloc_size);
ASSERT_EQ(ZX_OK, ret, "committing vm object\n");
EXPECT_EQ(ROUNDUP_PAGE_SIZE(alloc_size), PAGE_SIZE * vmo->AttributedPages(),
"committing vm object\n");
EXPECT_TRUE(PagesInAnyUnswappableQueue(vmo.get(), 0, alloc_size));
END_TEST;
}
// Creates paged VMOs, pins them, and tries operations that should unpin.
static bool vmo_pin_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
static const size_t alloc_size = PAGE_SIZE * 16;
for (uint32_t is_loaning_enabled = 0; is_loaning_enabled < 2; ++is_loaning_enabled) {
bool loaning_was_enabled = pmm_physical_page_borrowing_config()->is_loaning_enabled();
pmm_physical_page_borrowing_config()->set_loaning_enabled(!!is_loaning_enabled);
auto cleanup = fit::defer([loaning_was_enabled] {
pmm_physical_page_borrowing_config()->set_loaning_enabled(loaning_was_enabled);
});
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status;
status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, VmObjectPaged::kResizable, alloc_size, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
status = vmo->CommitRangePinned(PAGE_SIZE, alloc_size);
EXPECT_EQ(ZX_ERR_OUT_OF_RANGE, status, "pinning out of range\n");
status = vmo->CommitRangePinned(PAGE_SIZE, 0);
EXPECT_EQ(ZX_ERR_INVALID_ARGS, status, "pinning range of len 0\n");
status = vmo->CommitRangePinned(PAGE_SIZE, 3 * PAGE_SIZE);
EXPECT_EQ(ZX_OK, status, "pinning range\n");
EXPECT_TRUE(PagesInWiredQueue(vmo.get(), PAGE_SIZE, 3 * PAGE_SIZE));
status = vmo->DecommitRange(PAGE_SIZE, 3 * PAGE_SIZE);
EXPECT_EQ(ZX_ERR_BAD_STATE, status, "decommitting pinned range\n");
status = vmo->DecommitRange(PAGE_SIZE, PAGE_SIZE);
EXPECT_EQ(ZX_ERR_BAD_STATE, status, "decommitting pinned range\n");
status = vmo->DecommitRange(3 * PAGE_SIZE, PAGE_SIZE);
EXPECT_EQ(ZX_ERR_BAD_STATE, status, "decommitting pinned range\n");
vmo->Unpin(PAGE_SIZE, 3 * PAGE_SIZE);
EXPECT_TRUE(PagesInAnyUnswappableQueue(vmo.get(), PAGE_SIZE, 3 * PAGE_SIZE));
status = vmo->DecommitRange(PAGE_SIZE, 3 * PAGE_SIZE);
EXPECT_EQ(ZX_OK, status, "decommitting unpinned range\n");
status = vmo->CommitRangePinned(PAGE_SIZE, 3 * PAGE_SIZE);
EXPECT_EQ(ZX_OK, status, "pinning range after decommit\n");
EXPECT_TRUE(PagesInWiredQueue(vmo.get(), PAGE_SIZE, 3 * PAGE_SIZE));
status = vmo->Resize(0);
EXPECT_EQ(ZX_ERR_BAD_STATE, status, "resizing pinned range\n");
vmo->Unpin(PAGE_SIZE, 3 * PAGE_SIZE);
status = vmo->Resize(0);
EXPECT_EQ(ZX_OK, status, "resizing unpinned range\n");
}
END_TEST;
}
// Creates contiguous VMOs, pins them, and tries operations that should unpin.
static bool vmo_pin_contiguous_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
static const size_t alloc_size = PAGE_SIZE * 16;
for (uint32_t is_loaning_enabled = 0; is_loaning_enabled < 2; ++is_loaning_enabled) {
bool loaning_was_enabled = pmm_physical_page_borrowing_config()->is_loaning_enabled();
pmm_physical_page_borrowing_config()->set_loaning_enabled(!!is_loaning_enabled);
auto cleanup = fit::defer([loaning_was_enabled] {
pmm_physical_page_borrowing_config()->set_loaning_enabled(loaning_was_enabled);
});
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status;
status = VmObjectPaged::CreateContiguous(PMM_ALLOC_FLAG_ANY, alloc_size,
/*alignment_log2=*/0, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
status = vmo->CommitRangePinned(PAGE_SIZE, alloc_size);
EXPECT_EQ(ZX_ERR_OUT_OF_RANGE, status, "pinning out of range\n");
status = vmo->CommitRangePinned(PAGE_SIZE, 0);
EXPECT_EQ(ZX_ERR_INVALID_ARGS, status, "pinning range of len 0\n");
status = vmo->CommitRangePinned(PAGE_SIZE, 3 * PAGE_SIZE);
EXPECT_EQ(ZX_OK, status, "pinning range\n");
EXPECT_TRUE(PagesInWiredQueue(vmo.get(), PAGE_SIZE, 3 * PAGE_SIZE));
status = vmo->DecommitRange(PAGE_SIZE, 3 * PAGE_SIZE);
if (!is_loaning_enabled) {
EXPECT_EQ(ZX_ERR_NOT_SUPPORTED, status, "decommitting pinned range\n");
} else {
EXPECT_EQ(ZX_ERR_BAD_STATE, status, "decommitting pinned range\n");
}
status = vmo->DecommitRange(PAGE_SIZE, PAGE_SIZE);
if (!is_loaning_enabled) {
EXPECT_EQ(ZX_ERR_NOT_SUPPORTED, status, "decommitting pinned range\n");
} else {
EXPECT_EQ(ZX_ERR_BAD_STATE, status, "decommitting pinned range\n");
}
status = vmo->DecommitRange(3 * PAGE_SIZE, PAGE_SIZE);
if (!is_loaning_enabled) {
EXPECT_EQ(ZX_ERR_NOT_SUPPORTED, status, "decommitting pinned range\n");
} else {
EXPECT_EQ(ZX_ERR_BAD_STATE, status, "decommitting pinned range\n");
}
vmo->Unpin(PAGE_SIZE, 3 * PAGE_SIZE);
EXPECT_TRUE(PagesInAnyUnswappableQueue(vmo.get(), PAGE_SIZE, 3 * PAGE_SIZE));
status = vmo->DecommitRange(PAGE_SIZE, 3 * PAGE_SIZE);
if (!is_loaning_enabled) {
EXPECT_EQ(ZX_ERR_NOT_SUPPORTED, status, "decommitting unpinned range\n");
} else {
EXPECT_EQ(ZX_OK, status, "decommitting unpinned range\n");
}
status = vmo->CommitRangePinned(PAGE_SIZE, 3 * PAGE_SIZE);
EXPECT_EQ(ZX_OK, status, "pinning range after decommit\n");
EXPECT_TRUE(PagesInWiredQueue(vmo.get(), PAGE_SIZE, 3 * PAGE_SIZE));
vmo->Unpin(PAGE_SIZE, 3 * PAGE_SIZE);
}
END_TEST;
}
// Creates a page VMO and pins the same pages multiple times
static bool vmo_multiple_pin_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
static const size_t alloc_size = PAGE_SIZE * 16;
for (uint32_t is_ppb_enabled = 0; is_ppb_enabled < 2; ++is_ppb_enabled) {
bool loaning_was_enabled = pmm_physical_page_borrowing_config()->is_loaning_enabled();
bool borrowing_was_enabled =
pmm_physical_page_borrowing_config()->is_borrowing_in_supplypages_enabled();
pmm_physical_page_borrowing_config()->set_loaning_enabled(is_ppb_enabled);
pmm_physical_page_borrowing_config()->set_borrowing_in_supplypages_enabled(is_ppb_enabled);
auto cleanup = fit::defer([loaning_was_enabled, borrowing_was_enabled] {
pmm_physical_page_borrowing_config()->set_loaning_enabled(loaning_was_enabled);
pmm_physical_page_borrowing_config()->set_borrowing_in_supplypages_enabled(
borrowing_was_enabled);
});
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status;
status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, alloc_size, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
status = vmo->CommitRangePinned(0, alloc_size);
EXPECT_EQ(ZX_OK, status, "pinning whole range\n");
EXPECT_TRUE(PagesInWiredQueue(vmo.get(), 0, alloc_size));
status = vmo->CommitRangePinned(PAGE_SIZE, 4 * PAGE_SIZE);
EXPECT_EQ(ZX_OK, status, "pinning subrange\n");
EXPECT_TRUE(PagesInWiredQueue(vmo.get(), 0, alloc_size));
for (unsigned int i = 1; i < VM_PAGE_OBJECT_MAX_PIN_COUNT; ++i) {
status = vmo->CommitRangePinned(0, PAGE_SIZE);
EXPECT_EQ(ZX_OK, status, "pinning first page max times\n");
}
status = vmo->CommitRangePinned(0, PAGE_SIZE);
EXPECT_EQ(ZX_ERR_UNAVAILABLE, status, "page is pinned too much\n");
vmo->Unpin(0, alloc_size);
EXPECT_TRUE(PagesInWiredQueue(vmo.get(), PAGE_SIZE, 4 * PAGE_SIZE));
EXPECT_TRUE(PagesInAnyUnswappableQueue(vmo.get(), 5 * PAGE_SIZE, alloc_size - 5 * PAGE_SIZE));
status = vmo->DecommitRange(PAGE_SIZE, 4 * PAGE_SIZE);
EXPECT_EQ(ZX_ERR_BAD_STATE, status, "decommitting pinned range\n");
status = vmo->DecommitRange(5 * PAGE_SIZE, alloc_size - 5 * PAGE_SIZE);
EXPECT_EQ(ZX_OK, status, "decommitting unpinned range\n");
vmo->Unpin(PAGE_SIZE, 4 * PAGE_SIZE);
status = vmo->DecommitRange(PAGE_SIZE, 4 * PAGE_SIZE);
EXPECT_EQ(ZX_OK, status, "decommitting unpinned range\n");
for (unsigned int i = 2; i < VM_PAGE_OBJECT_MAX_PIN_COUNT; ++i) {
vmo->Unpin(0, PAGE_SIZE);
}
status = vmo->DecommitRange(0, PAGE_SIZE);
EXPECT_EQ(ZX_ERR_BAD_STATE, status, "decommitting unpinned range\n");
vmo->Unpin(0, PAGE_SIZE);
status = vmo->DecommitRange(0, PAGE_SIZE);
EXPECT_EQ(ZX_OK, status, "decommitting unpinned range\n");
}
END_TEST;
}
// Creates a contiguous VMO and pins the same pages multiple times
static bool vmo_multiple_pin_contiguous_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
static const size_t alloc_size = PAGE_SIZE * 16;
for (uint32_t is_ppb_enabled = 0; is_ppb_enabled < 2; ++is_ppb_enabled) {
bool loaning_was_enabled = pmm_physical_page_borrowing_config()->is_loaning_enabled();
bool borrowing_was_enabled =
pmm_physical_page_borrowing_config()->is_borrowing_in_supplypages_enabled();
pmm_physical_page_borrowing_config()->set_loaning_enabled(is_ppb_enabled);
pmm_physical_page_borrowing_config()->set_borrowing_in_supplypages_enabled(is_ppb_enabled);
auto cleanup = fit::defer([loaning_was_enabled, borrowing_was_enabled] {
pmm_physical_page_borrowing_config()->set_loaning_enabled(loaning_was_enabled);
pmm_physical_page_borrowing_config()->set_borrowing_in_supplypages_enabled(
borrowing_was_enabled);
});
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status;
status = VmObjectPaged::CreateContiguous(PMM_ALLOC_FLAG_ANY, alloc_size,
/*alignment_log2=*/0, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
status = vmo->CommitRangePinned(0, alloc_size);
EXPECT_EQ(ZX_OK, status, "pinning whole range\n");
EXPECT_TRUE(PagesInWiredQueue(vmo.get(), 0, alloc_size));
status = vmo->CommitRangePinned(PAGE_SIZE, 4 * PAGE_SIZE);
EXPECT_EQ(ZX_OK, status, "pinning subrange\n");
EXPECT_TRUE(PagesInWiredQueue(vmo.get(), 0, alloc_size));
for (unsigned int i = 1; i < VM_PAGE_OBJECT_MAX_PIN_COUNT; ++i) {
status = vmo->CommitRangePinned(0, PAGE_SIZE);
EXPECT_EQ(ZX_OK, status, "pinning first page max times\n");
}
status = vmo->CommitRangePinned(0, PAGE_SIZE);
EXPECT_EQ(ZX_ERR_UNAVAILABLE, status, "page is pinned too much\n");
vmo->Unpin(0, alloc_size);
EXPECT_TRUE(PagesInWiredQueue(vmo.get(), PAGE_SIZE, 4 * PAGE_SIZE));
EXPECT_TRUE(PagesInAnyUnswappableQueue(vmo.get(), 5 * PAGE_SIZE, alloc_size - 5 * PAGE_SIZE));
status = vmo->DecommitRange(PAGE_SIZE, 4 * PAGE_SIZE);
if (!is_ppb_enabled) {
EXPECT_EQ(ZX_ERR_NOT_SUPPORTED, status, "decommitting pinned range\n");
} else {
EXPECT_EQ(ZX_ERR_BAD_STATE, status, "decommitting pinned range\n");
}
status = vmo->DecommitRange(5 * PAGE_SIZE, alloc_size - 5 * PAGE_SIZE);
if (!is_ppb_enabled) {
EXPECT_EQ(ZX_ERR_NOT_SUPPORTED, status, "decommitting unpinned range\n");
} else {
EXPECT_EQ(ZX_OK, status, "decommitting unpinned range\n");
}
vmo->Unpin(PAGE_SIZE, 4 * PAGE_SIZE);
status = vmo->DecommitRange(PAGE_SIZE, 4 * PAGE_SIZE);
if (!is_ppb_enabled) {
EXPECT_EQ(ZX_ERR_NOT_SUPPORTED, status, "decommitting unpinned range\n");
} else {
EXPECT_EQ(ZX_OK, status, "decommitting unpinned range\n");
}
for (unsigned int i = 2; i < VM_PAGE_OBJECT_MAX_PIN_COUNT; ++i) {
vmo->Unpin(0, PAGE_SIZE);
}
status = vmo->DecommitRange(0, PAGE_SIZE);
if (!is_ppb_enabled) {
EXPECT_EQ(ZX_ERR_NOT_SUPPORTED, status, "decommitting unpinned range\n");
} else {
EXPECT_EQ(ZX_ERR_BAD_STATE, status, "decommitting unpinned range\n");
}
vmo->Unpin(0, PAGE_SIZE);
status = vmo->DecommitRange(0, PAGE_SIZE);
if (!is_ppb_enabled) {
EXPECT_EQ(ZX_ERR_NOT_SUPPORTED, status, "decommitting unpinned range\n");
} else {
EXPECT_EQ(ZX_OK, status, "decommitting unpinned range\n");
}
}
END_TEST;
}
// Creates a vm object, commits odd sized memory.
static bool vmo_odd_size_commit_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
static const size_t alloc_size = 15;
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, alloc_size, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
auto ret = vmo->CommitRange(0, alloc_size);
EXPECT_EQ(ZX_OK, ret, "committing vm object\n");
EXPECT_EQ(ROUNDUP_PAGE_SIZE(alloc_size), PAGE_SIZE * vmo->AttributedPages(),
"committing vm object\n");
END_TEST;
}
static bool vmo_create_physical_test() {
BEGIN_TEST;
paddr_t pa;
vm_page_t* vm_page;
zx_status_t status = pmm_alloc_page(0, &vm_page, &pa);
uint32_t cache_policy;
ASSERT_EQ(ZX_OK, status, "vm page allocation\n");
ASSERT_TRUE(vm_page);
fbl::RefPtr<VmObjectPhysical> vmo;
status = VmObjectPhysical::Create(pa, PAGE_SIZE, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
cache_policy = vmo->GetMappingCachePolicy();
EXPECT_EQ(ARCH_MMU_FLAG_UNCACHED, cache_policy, "check initial cache policy");
EXPECT_TRUE(vmo->is_contiguous(), "check contiguous");
vmo.reset();
pmm_free_page(vm_page);
END_TEST;
}
static bool vmo_physical_pin_test() {
BEGIN_TEST;
paddr_t pa;
vm_page_t* vm_page;
zx_status_t status = pmm_alloc_page(0, &vm_page, &pa);
ASSERT_EQ(ZX_OK, status);
fbl::RefPtr<VmObjectPhysical> vmo;
status = VmObjectPhysical::Create(pa, PAGE_SIZE, &vmo);
// Validate we can pin the range.
EXPECT_EQ(ZX_OK, vmo->CommitRangePinned(0, PAGE_SIZE));
// Pinning out side should fail.
EXPECT_EQ(ZX_ERR_OUT_OF_RANGE, vmo->CommitRangePinned(PAGE_SIZE, PAGE_SIZE));
// Unpin for physical VMOs does not currently do anything, but still call it to be API correct.
vmo->Unpin(0, PAGE_SIZE);
vmo.reset();
pmm_free_page(vm_page);
END_TEST;
}
// Creates a vm object that commits contiguous memory.
static bool vmo_create_contiguous_test() {
BEGIN_TEST;
static const size_t alloc_size = PAGE_SIZE * 16;
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::CreateContiguous(PMM_ALLOC_FLAG_ANY, alloc_size, 0, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
EXPECT_TRUE(vmo->is_contiguous(), "vmo is contig\n");
// Contiguous VMOs don't implicitly pin.
EXPECT_FALSE(PagesInWiredQueue(vmo.get(), 0, alloc_size));
paddr_t last_pa;
auto lookup_func = [&last_pa](uint64_t offset, paddr_t pa) {
if (offset != 0 && last_pa + PAGE_SIZE != pa) {
return ZX_ERR_BAD_STATE;
}
last_pa = pa;
return ZX_ERR_NEXT;
};
status = vmo->Lookup(0, alloc_size, lookup_func);
paddr_t first_pa;
paddr_t second_pa;
EXPECT_EQ(status, ZX_OK, "vmo lookup\n");
EXPECT_EQ(ZX_OK, vmo->LookupContiguous(0, alloc_size, &first_pa));
EXPECT_EQ(first_pa + alloc_size - PAGE_SIZE, last_pa);
EXPECT_EQ(ZX_OK, vmo->LookupContiguous(PAGE_SIZE, PAGE_SIZE, &second_pa));
EXPECT_EQ(first_pa + PAGE_SIZE, second_pa);
EXPECT_EQ(ZX_ERR_INVALID_ARGS, vmo->LookupContiguous(42, PAGE_SIZE, nullptr));
EXPECT_EQ(ZX_ERR_OUT_OF_RANGE,
vmo->LookupContiguous(alloc_size - PAGE_SIZE, PAGE_SIZE * 2, nullptr));
END_TEST;
}
// Make sure decommitting pages from a contiguous VMO is allowed, and that we get back the correct
// pages when committing pages back into a contiguous VMO, even if another VMO was (temporarily)
// using those pages.
static bool vmo_contiguous_decommit_test() {
BEGIN_TEST;
bool loaning_was_enabled = pmm_physical_page_borrowing_config()->is_loaning_enabled();
bool borrowing_was_enabled =
pmm_physical_page_borrowing_config()->is_borrowing_in_supplypages_enabled();
pmm_physical_page_borrowing_config()->set_loaning_enabled(true);
pmm_physical_page_borrowing_config()->set_borrowing_in_supplypages_enabled(true);
auto cleanup = fit::defer([loaning_was_enabled, borrowing_was_enabled] {
pmm_physical_page_borrowing_config()->set_loaning_enabled(loaning_was_enabled);
pmm_physical_page_borrowing_config()->set_borrowing_in_supplypages_enabled(
borrowing_was_enabled);
});
static const size_t alloc_size = PAGE_SIZE * 16;
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::CreateContiguous(PMM_ALLOC_FLAG_ANY, alloc_size, 0, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
paddr_t base_pa = static_cast<paddr_t>(-1);
status = vmo->Lookup(0, PAGE_SIZE, [&base_pa](size_t offset, paddr_t pa) {
ASSERT(base_pa == static_cast<paddr_t>(-1));
ASSERT(offset == 0);
base_pa = pa;
return ZX_ERR_NEXT;
});
ASSERT_EQ(status, ZX_OK, "stash base pa works\n");
ASSERT_TRUE(base_pa != static_cast<paddr_t>(-1));
bool borrowed_seen = false;
bool page_expected[alloc_size / PAGE_SIZE];
for (bool& present : page_expected) {
// Default to true.
present = true;
}
// Make sure expected pages (and only expected pages) are present and consistent with start
// physical address of contiguous VMO.
auto verify_expected_pages = [vmo, base_pa, &borrowed_seen, &page_expected]() -> void {
auto cow = vmo->DebugGetCowPages();
bool page_seen[alloc_size / PAGE_SIZE] = {};
auto lookup_func = [base_pa, &page_seen](size_t offset, paddr_t pa) {
ASSERT(!page_seen[offset / PAGE_SIZE]);
page_seen[offset / PAGE_SIZE] = true;
if (pa - base_pa != offset) {
return ZX_ERR_BAD_STATE;
}
return ZX_ERR_NEXT;
};
zx_status_t status = vmo->Lookup(0, alloc_size, lookup_func);
ASSERT_MSG(status == ZX_OK, "vmo->Lookup() failed - status: %d\n", status);
for (uint64_t offset = 0; offset < alloc_size; offset += PAGE_SIZE) {
uint64_t page_index = offset / PAGE_SIZE;
ASSERT_MSG(page_expected[page_index] == page_seen[page_index],
"page_expected[page_index] != page_seen[page_index]\n");
vm_page_t* page_from_cow = cow->DebugGetPage(offset);
vm_page_t* page_from_pmm = paddr_to_vm_page(base_pa + offset);
ASSERT(page_from_pmm);
if (page_expected[page_index]) {
ASSERT(page_from_cow);
ASSERT(page_from_cow == page_from_pmm);
ASSERT(cow->DebugIsPage(offset));
ASSERT(!page_from_pmm->loaned);
} else {
ASSERT(!page_from_cow);
ASSERT(cow->DebugIsEmpty(offset));
ASSERT(page_from_pmm->loaned);
if (!page_from_pmm->is_free()) {
// It's not in cow, and it's not free, so note that we observed a borrowed page.
borrowed_seen = true;
}
}
ASSERT(!page_from_pmm->loan_cancelled);
}
};
verify_expected_pages();
auto track_decommit = [vmo, &page_expected, &verify_expected_pages](uint64_t start_offset,
uint64_t size) {
ASSERT(IS_PAGE_ALIGNED(start_offset));
ASSERT(IS_PAGE_ALIGNED(size));
uint64_t end_offset = start_offset + size;
for (uint64_t offset = start_offset; offset < end_offset; offset += PAGE_SIZE) {
page_expected[offset / PAGE_SIZE] = false;
}
verify_expected_pages();
};
auto track_commit = [vmo, &page_expected, &verify_expected_pages](uint64_t start_offset,
uint64_t size) {
ASSERT(IS_PAGE_ALIGNED(start_offset));
ASSERT(IS_PAGE_ALIGNED(size));
uint64_t end_offset = start_offset + size;
for (uint64_t offset = start_offset; offset < end_offset; offset += PAGE_SIZE) {
page_expected[offset / PAGE_SIZE] = true;
}
verify_expected_pages();
};
status = vmo->DecommitRange(PAGE_SIZE, 4 * PAGE_SIZE);
ASSERT_EQ(status, ZX_OK, "decommit of contiguous VMO pages works\n");
track_decommit(PAGE_SIZE, 4 * PAGE_SIZE);
status = vmo->DecommitRange(0, 4 * PAGE_SIZE);
ASSERT_EQ(status, ZX_OK,
"decommit of contiguous VMO pages overlapping non-present pages works\n");
track_decommit(0, 4 * PAGE_SIZE);
status = vmo->DecommitRange(alloc_size - PAGE_SIZE, PAGE_SIZE);
ASSERT_EQ(status, ZX_OK, "decommit at end of contiguous VMO works\n");
track_decommit(alloc_size - PAGE_SIZE, PAGE_SIZE);
status = vmo->DecommitRange(0, alloc_size);
ASSERT_EQ(status, ZX_OK, "decommit all overlapping non-present pages\n");
track_decommit(0, alloc_size);
// Due to concurrent activity of the system, we may not be able to allocate the loaned pages into
// a VMO we're creating here, and depending on timing, we may also not observe the pages being
// borrowed. However, it shouldn't take many tries, if we continue to allocate non-pinned pages
// to a VMO repeatedly, since loaned pages are preferred for allocations that can use them.
//
// We pay attention to whether ASAN is enabled in order to apply a strategy that's optimized for
// pages being put on the head (normal) or tail (ASAN) of the free list
// (PmmNode::free_loaned_list_).
// Reset borrowed_seen since we should be able to see borrowing _within_ the loop below, mainly
// so we can also have the loop below do a CommitRange() to reclaim before the borrowing VMO is
// deleted.
borrowed_seen = false;
zx_time_t complain_deadline = current_time() + ZX_SEC(5);
uint32_t loop_count = 0;
while (!borrowed_seen || loop_count < 5) {
// Not super small, in case we end up needing to do multiple iterations of the loop to see the
// pages being borrowed, and ASAN is enabled which could require more iterations of this loop
// if this size were smaller. Also hopefully not big enough to fail on small-ish devices.
constexpr uint64_t kBorrowingVmoPages = 64;
vm_page_t* pages[kBorrowingVmoPages];
fbl::RefPtr<VmObjectPaged> borrowing_vmo;
status = make_committed_pager_vmo(kBorrowingVmoPages, /*trap_dirty=*/false, /*resizable=*/false,
&pages[0], &borrowing_vmo);
ASSERT_EQ(status, ZX_OK);
// Updates borrowing_seen to true, if any pages of vmo are seen to be borrowed (maybe by
// borrowing_vmo, or maybe by some other VMO; we don't care which here).
verify_expected_pages();
// We want the last iteration of the loop to have seen borrowing itself, so we're sure the else
// case below runs (to commit) before the borrowing VMO is deleted.
if (loop_count < 5) {
borrowed_seen = false;
}
if (!borrowed_seen || loop_count < 5) {
if constexpr (!__has_feature(address_sanitizer)) {
// By committing and de-committing in the loop, we put the pages we're paying attention to
// back at the head of the free_loaned_list_, so the next iteration of the loop is more
// likely to see them being borrowed (by allocating them).
status = vmo->CommitRange(0, 4 * PAGE_SIZE);
ASSERT_EQ(status, ZX_OK, "temp commit back to contiguous VMO, to remove from free list\n");
track_commit(0, 4 * PAGE_SIZE);
status = vmo->DecommitRange(0, 4 * PAGE_SIZE);
ASSERT_EQ(status, ZX_OK, "decommit back to free list at head of free list\n");
track_decommit(0, 4 * PAGE_SIZE);
} else {
// By _not_ committing and de-committing in the loop, the pages we're allocating in a loop
// will eventually work through the free_loaned_list_, even if a large contiguous VMO was
// decomitted at an inconvenient time.
}
zx_time_t now = current_time();
if (now > complain_deadline) {
dprintf(INFO, "!borrowed_seen is persisting longer than expected; still trying...\n");
complain_deadline = now + ZX_SEC(5);
}
} else {
// This covers the case where a page is reclaimed before being freed from the borrowing VMO.
// And by forcing an iteration with loop_count >= 1 with the last iteration of the loop seeing
// borrowing durign the last iteration, we cover the case where we free the pages from the
// borrowing VMO before reclaiming.
status = vmo->CommitRange(0, alloc_size);
ASSERT_EQ(status, ZX_OK, "committed pages back into contiguous VMO\n");
track_commit(0, alloc_size);
}
++loop_count;
}
status = vmo->DecommitRange(0, alloc_size);
ASSERT_EQ(status, ZX_OK, "decommit from contiguous VMO\n");
status = vmo->CommitRange(0, alloc_size);
ASSERT_EQ(status, ZX_OK, "committed pages back into contiguous VMO\n");
track_commit(0, alloc_size);
END_TEST;
}
static bool vmo_contiguous_decommit_disabled_test() {
BEGIN_TEST;
bool loaning_was_enabled = pmm_physical_page_borrowing_config()->is_loaning_enabled();
pmm_physical_page_borrowing_config()->set_loaning_enabled(false);
auto cleanup = fit::defer([loaning_was_enabled] {
pmm_physical_page_borrowing_config()->set_loaning_enabled(loaning_was_enabled);
});
static const size_t alloc_size = PAGE_SIZE * 16;
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::CreateContiguous(PMM_ALLOC_FLAG_ANY, alloc_size, 0, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
status = vmo->DecommitRange(PAGE_SIZE, 4 * PAGE_SIZE);
ASSERT_EQ(status, ZX_ERR_NOT_SUPPORTED, "decommit fails as expected\n");
status = vmo->DecommitRange(0, 4 * PAGE_SIZE);
ASSERT_EQ(status, ZX_ERR_NOT_SUPPORTED, "decommit fails as expected\n");
status = vmo->DecommitRange(alloc_size - PAGE_SIZE, PAGE_SIZE);
ASSERT_EQ(status, ZX_ERR_NOT_SUPPORTED, "decommit fails as expected\n");
END_TEST;
}
static bool vmo_contiguous_decommit_enabled_test() {
BEGIN_TEST;
bool loaning_was_enabled = pmm_physical_page_borrowing_config()->is_loaning_enabled();
pmm_physical_page_borrowing_config()->set_loaning_enabled(true);
auto cleanup = fit::defer([loaning_was_enabled] {
pmm_physical_page_borrowing_config()->set_loaning_enabled(loaning_was_enabled);
});
static const size_t alloc_size = PAGE_SIZE * 16;
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::CreateContiguous(PMM_ALLOC_FLAG_ANY, alloc_size, 0, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
auto ka = VmAspace::kernel_aspace();
void* ptr;
auto ret = ka->MapObjectInternal(vmo, "test", 0, alloc_size, &ptr, 0, VmAspace::VMM_FLAG_COMMIT,
kArchRwFlags);
ASSERT_EQ(ZX_OK, ret, "mapping object");
auto cleanup_mapping = fit::defer([&ka, ptr] {
auto err = ka->FreeRegion((vaddr_t)ptr);
DEBUG_ASSERT(err == ZX_OK);
});
uint8_t* base = reinterpret_cast<uint8_t*>(ptr);
for (uint64_t offset = 0; offset < alloc_size; offset += PAGE_SIZE) {
memset(&base[offset], 0x42, PAGE_SIZE);
}
paddr_t base_pa = -1;
status = vmo->Lookup(0, PAGE_SIZE, [&base_pa](uint64_t offset, paddr_t pa) {
DEBUG_ASSERT(offset == 0);
base_pa = pa;
return ZX_ERR_NEXT;
});
ASSERT_EQ(status, ZX_OK);
ASSERT_TRUE(base_pa != static_cast<paddr_t>(-1));
status = vmo->DecommitRange(PAGE_SIZE, 4 * PAGE_SIZE);
ASSERT_EQ(status, ZX_OK, "decommit pretends to work\n");
status = vmo->DecommitRange(0, 4 * PAGE_SIZE);
ASSERT_EQ(status, ZX_OK, "decommit pretends to work\n");
status = vmo->DecommitRange(alloc_size - PAGE_SIZE, PAGE_SIZE);
ASSERT_EQ(status, ZX_OK, "decommit pretends to work\n");
// Make sure decommit removed pages. Make sure pages which are present are the correct physical
// address.
for (uint64_t offset = 0; offset < alloc_size; offset += PAGE_SIZE) {
bool page_absent = true;
status = vmo->Lookup(offset, PAGE_SIZE,
[base_pa, offset, &page_absent](uint64_t lookup_offset, paddr_t pa) {
page_absent = false;
DEBUG_ASSERT(offset == lookup_offset);
DEBUG_ASSERT(base_pa + lookup_offset == pa);
return ZX_ERR_NEXT;
});
bool absent_expected = (offset < 5 * PAGE_SIZE) || (offset == alloc_size - PAGE_SIZE);
ASSERT_EQ(absent_expected, page_absent);
}
END_TEST;
}
// Creats a vm object, maps it, precommitted.
static bool vmo_precommitted_map_test() {
BEGIN_TEST;
static const size_t alloc_size = PAGE_SIZE * 16;
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, alloc_size, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
auto ka = VmAspace::kernel_aspace();
void* ptr;
auto ret = ka->MapObjectInternal(vmo, "test", 0, alloc_size, &ptr, 0, VmAspace::VMM_FLAG_COMMIT,
kArchRwFlags);
ASSERT_EQ(ZX_OK, ret, "mapping object");
// fill with known pattern and test
if (!fill_and_test(ptr, alloc_size)) {
all_ok = false;
}
auto err = ka->FreeRegion((vaddr_t)ptr);
EXPECT_EQ(ZX_OK, err, "unmapping object");
END_TEST;
}
// Creates a vm object, maps it, demand paged.
static bool vmo_demand_paged_map_test() {
BEGIN_TEST;
static const size_t alloc_size = PAGE_SIZE * 16;
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, alloc_size, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
fbl::RefPtr<VmAspace> aspace = VmAspace::Create(VmAspace::Type::User, "test aspace");
ASSERT_NONNULL(aspace, "VmAspace::Create pointer");
VmAspace* old_aspace = Thread::Current::Get()->aspace();
auto cleanup_aspace = fit::defer([&]() {
vmm_set_active_aspace(old_aspace);
ASSERT(aspace->Destroy() == ZX_OK);
});
vmm_set_active_aspace(aspace.get());
static constexpr const uint kArchFlags = kArchRwFlags | ARCH_MMU_FLAG_PERM_USER;
fbl::RefPtr<VmMapping> mapping;
status = aspace->RootVmar()->CreateVmMapping(0, alloc_size, 0, 0, vmo, 0, kArchFlags, "test",
&mapping);
ASSERT_EQ(status, ZX_OK, "mapping object");
auto uptr = make_user_inout_ptr(reinterpret_cast<void*>(mapping->base()));
// fill with known pattern and test
if (!fill_and_test_user(uptr, alloc_size)) {
all_ok = false;
}
// cleanup_aspace destroys the whole space now.
END_TEST;
}
// Creates a vm object, maps it, drops ref before unmapping.
static bool vmo_dropped_ref_test() {
BEGIN_TEST;
static const size_t alloc_size = PAGE_SIZE * 16;
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, alloc_size, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
auto ka = VmAspace::kernel_aspace();
void* ptr;
auto ret = ka->MapObjectInternal(ktl::move(vmo), "test", 0, alloc_size, &ptr, 0,
VmAspace::VMM_FLAG_COMMIT, kArchRwFlags);
ASSERT_EQ(ret, ZX_OK, "mapping object");
EXPECT_NULL(vmo, "dropped ref to object");
// fill with known pattern and test
if (!fill_and_test(ptr, alloc_size)) {
all_ok = false;
}
auto err = ka->FreeRegion((vaddr_t)ptr);
EXPECT_EQ(ZX_OK, err, "unmapping object");
END_TEST;
}
// Creates a vm object, maps it, fills it with data, unmaps,
// maps again somewhere else.
static bool vmo_remap_test() {
BEGIN_TEST;
static const size_t alloc_size = PAGE_SIZE * 16;
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, alloc_size, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
auto ka = VmAspace::kernel_aspace();
void* ptr;
auto ret = ka->MapObjectInternal(vmo, "test", 0, alloc_size, &ptr, 0, VmAspace::VMM_FLAG_COMMIT,
kArchRwFlags);
ASSERT_EQ(ZX_OK, ret, "mapping object");
// fill with known pattern and test
if (!fill_and_test(ptr, alloc_size)) {
all_ok = false;
}
auto err = ka->FreeRegion((vaddr_t)ptr);
EXPECT_EQ(ZX_OK, err, "unmapping object");
// map it again
ret = ka->MapObjectInternal(vmo, "test", 0, alloc_size, &ptr, 0, VmAspace::VMM_FLAG_COMMIT,
kArchRwFlags);
ASSERT_EQ(ret, ZX_OK, "mapping object");
// test that the pattern is still valid
bool result = test_region((uintptr_t)ptr, ptr, alloc_size);
EXPECT_TRUE(result, "testing region for corruption");
err = ka->FreeRegion((vaddr_t)ptr);
EXPECT_EQ(ZX_OK, err, "unmapping object");
END_TEST;
}
// Creates a vm object, maps it, fills it with data, maps it a second time and
// third time somwehere else.
static bool vmo_double_remap_test() {
BEGIN_TEST;
static const size_t alloc_size = PAGE_SIZE * 16;
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, alloc_size, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
auto ka = VmAspace::kernel_aspace();
void* ptr;
auto ret = ka->MapObjectInternal(vmo, "test0", 0, alloc_size, &ptr, 0, VmAspace::VMM_FLAG_COMMIT,
kArchRwFlags);
ASSERT_EQ(ZX_OK, ret, "mapping object");
// fill with known pattern and test
if (!fill_and_test(ptr, alloc_size)) {
all_ok = false;
}
// map it again
void* ptr2;
ret = ka->MapObjectInternal(vmo, "test1", 0, alloc_size, &ptr2, 0, VmAspace::VMM_FLAG_COMMIT,
kArchRwFlags);
ASSERT_EQ(ret, ZX_OK, "mapping object second time");
EXPECT_NE(ptr, ptr2, "second mapping is different");
// test that the pattern is still valid
bool result = test_region((uintptr_t)ptr, ptr2, alloc_size);
EXPECT_TRUE(result, "testing region for corruption");
// map it a third time with an offset
void* ptr3;
static const size_t alloc_offset = PAGE_SIZE;
ret = ka->MapObjectInternal(vmo, "test2", alloc_offset, alloc_size - alloc_offset, &ptr3, 0,
VmAspace::VMM_FLAG_COMMIT, kArchRwFlags);
ASSERT_EQ(ret, ZX_OK, "mapping object third time");
EXPECT_NE(ptr3, ptr2, "third mapping is different");
EXPECT_NE(ptr3, ptr, "third mapping is different");
// test that the pattern is still valid
int mc = memcmp((uint8_t*)ptr + alloc_offset, ptr3, alloc_size - alloc_offset);
EXPECT_EQ(0, mc, "testing region for corruption");
ret = ka->FreeRegion((vaddr_t)ptr3);
EXPECT_EQ(ZX_OK, ret, "unmapping object third time");
ret = ka->FreeRegion((vaddr_t)ptr2);
EXPECT_EQ(ZX_OK, ret, "unmapping object second time");
ret = ka->FreeRegion((vaddr_t)ptr);
EXPECT_EQ(ZX_OK, ret, "unmapping object");
END_TEST;
}
static bool vmo_read_write_smoke_test() {
BEGIN_TEST;
static const size_t alloc_size = PAGE_SIZE * 16;
// create object
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, alloc_size, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
// create test buffer
fbl::AllocChecker ac;
fbl::Vector<uint8_t> a;
a.reserve(alloc_size, &ac);
ASSERT_TRUE(ac.check());
fill_region(99, a.data(), alloc_size);
// write to it, make sure it seems to work with valid args
zx_status_t err = vmo->Write(a.data(), 0, 0);
EXPECT_EQ(ZX_OK, err, "writing to object");
err = vmo->Write(a.data(), 0, 37);
EXPECT_EQ(ZX_OK, err, "writing to object");
err = vmo->Write(a.data(), 99, 37);
EXPECT_EQ(ZX_OK, err, "writing to object");
// can't write past end
err = vmo->Write(a.data(), 0, alloc_size + 47);
EXPECT_EQ(ZX_ERR_OUT_OF_RANGE, err, "writing to object");
// can't write past end
err = vmo->Write(a.data(), 31, alloc_size + 47);
EXPECT_EQ(ZX_ERR_OUT_OF_RANGE, err, "writing to object");
// should return an error because out of range
err = vmo->Write(a.data(), alloc_size + 99, 42);
EXPECT_EQ(ZX_ERR_OUT_OF_RANGE, err, "writing to object");
// map the object
auto ka = VmAspace::kernel_aspace();
uint8_t* ptr;
err = ka->MapObjectInternal(vmo, "test", 0, alloc_size, (void**)&ptr, 0,
VmAspace::VMM_FLAG_COMMIT, kArchRwFlags);
ASSERT_EQ(ZX_OK, err, "mapping object");
// write to it at odd offsets
err = vmo->Write(a.data(), 31, 4197);
EXPECT_EQ(ZX_OK, err, "writing to object");
int cmpres = memcmp(ptr + 31, a.data(), 4197);
EXPECT_EQ(0, cmpres, "reading from object");
// write to it, filling the object completely
err = vmo->Write(a.data(), 0, alloc_size);
EXPECT_EQ(ZX_OK, err, "writing to object");
// test that the data was actually written to it
bool result = test_region(99, ptr, alloc_size);
EXPECT_TRUE(result, "writing to object");
// unmap it
ka->FreeRegion((vaddr_t)ptr);
// test that we can read from it
fbl::Vector<uint8_t> b;
b.reserve(alloc_size, &ac);
ASSERT_TRUE(ac.check(), "can't allocate buffer");
err = vmo->Read(b.data(), 0, alloc_size);
EXPECT_EQ(ZX_OK, err, "reading from object");
// validate the buffer is valid
cmpres = memcmp(b.data(), a.data(), alloc_size);
EXPECT_EQ(0, cmpres, "reading from object");
// read from it at an offset
err = vmo->Read(b.data(), 31, 4197);
EXPECT_EQ(ZX_OK, err, "reading from object");
cmpres = memcmp(b.data(), a.data() + 31, 4197);
EXPECT_EQ(0, cmpres, "reading from object");
END_TEST;
}
static bool vmo_cache_test() {
BEGIN_TEST;
paddr_t pa;
vm_page_t* vm_page;
zx_status_t status = pmm_alloc_page(0, &vm_page, &pa);
auto ka = VmAspace::kernel_aspace();
uint32_t cache_policy = ARCH_MMU_FLAG_UNCACHED_DEVICE;
uint32_t cache_policy_get;
void* ptr;
ASSERT_TRUE(vm_page);
// Test that the flags set/get properly
{
fbl::RefPtr<VmObjectPhysical> vmo;
status = VmObjectPhysical::Create(pa, PAGE_SIZE, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
cache_policy_get = vmo->GetMappingCachePolicy();
EXPECT_NE(cache_policy, cache_policy_get, "check initial cache policy");
EXPECT_EQ(ZX_OK, vmo->SetMappingCachePolicy(cache_policy), "try set");
cache_policy_get = vmo->GetMappingCachePolicy();
EXPECT_EQ(cache_policy, cache_policy_get, "compare flags");
}
// Test valid flags
for (uint32_t i = 0; i <= ARCH_MMU_FLAG_CACHE_MASK; i++) {
fbl::RefPtr<VmObjectPhysical> vmo;
status = VmObjectPhysical::Create(pa, PAGE_SIZE, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
EXPECT_EQ(ZX_OK, vmo->SetMappingCachePolicy(cache_policy), "try setting valid flags");
}
// Test invalid flags
for (uint32_t i = ARCH_MMU_FLAG_CACHE_MASK + 1; i < 32; i++) {
fbl::RefPtr<VmObjectPhysical> vmo;
status = VmObjectPhysical::Create(pa, PAGE_SIZE, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
EXPECT_EQ(ZX_ERR_INVALID_ARGS, vmo->SetMappingCachePolicy(i), "try set with invalid flags");
}
// Test valid flags with invalid flags
{
fbl::RefPtr<VmObjectPhysical> vmo;
status = VmObjectPhysical::Create(pa, PAGE_SIZE, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
EXPECT_EQ(ZX_ERR_INVALID_ARGS, vmo->SetMappingCachePolicy(cache_policy | 0x5), "bad 0x5");
EXPECT_EQ(ZX_ERR_INVALID_ARGS, vmo->SetMappingCachePolicy(cache_policy | 0xA), "bad 0xA");
EXPECT_EQ(ZX_ERR_INVALID_ARGS, vmo->SetMappingCachePolicy(cache_policy | 0x55), "bad 0x55");
EXPECT_EQ(ZX_ERR_INVALID_ARGS, vmo->SetMappingCachePolicy(cache_policy | 0xAA), "bad 0xAA");
}
// Test that changing policy while mapped is blocked
{
fbl::RefPtr<VmObjectPhysical> vmo;
status = VmObjectPhysical::Create(pa, PAGE_SIZE, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
ASSERT_EQ(ZX_OK,
ka->MapObjectInternal(vmo, "test", 0, PAGE_SIZE, (void**)&ptr, 0,
VmAspace::VMM_FLAG_COMMIT, kArchRwFlags),
"map vmo");
EXPECT_EQ(ZX_ERR_BAD_STATE, vmo->SetMappingCachePolicy(cache_policy), "set flags while mapped");
EXPECT_EQ(ZX_OK, ka->FreeRegion((vaddr_t)ptr), "unmap vmo");
EXPECT_EQ(ZX_OK, vmo->SetMappingCachePolicy(cache_policy), "set flags after unmapping");
ASSERT_EQ(ZX_OK,
ka->MapObjectInternal(vmo, "test", 0, PAGE_SIZE, (void**)&ptr, 0,
VmAspace::VMM_FLAG_COMMIT, kArchRwFlags),
"map vmo again");
EXPECT_EQ(ZX_OK, ka->FreeRegion((vaddr_t)ptr), "unmap vmo");
}
pmm_free_page(vm_page);
END_TEST;
}
static bool vmo_lookup_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
static const size_t alloc_size = PAGE_SIZE * 16;
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, alloc_size, &vmo);
ASSERT_EQ(status, ZX_OK, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
size_t pages_seen = 0;
auto lookup_fn = [&pages_seen](size_t offset, paddr_t pa) {
pages_seen++;
return ZX_ERR_NEXT;
};
status = vmo->Lookup(0, alloc_size, lookup_fn);
EXPECT_EQ(ZX_OK, status);
EXPECT_EQ(0u, pages_seen, "lookup on uncommitted pages\n");
pages_seen = 0;
status = vmo->CommitRange(PAGE_SIZE, PAGE_SIZE);
EXPECT_EQ(ZX_OK, status, "committing vm object\n");
EXPECT_EQ(static_cast<size_t>(1), vmo->AttributedPages(), "committing vm object\n");
// Should not see any pages in the early range.
status = vmo->Lookup(0, PAGE_SIZE, lookup_fn);
EXPECT_EQ(ZX_OK, status);
EXPECT_EQ(0u, pages_seen, "lookup on partially committed pages\n");
pages_seen = 0;
// Should see a committed page if looking at any range covering the committed.
status = vmo->Lookup(0, alloc_size, lookup_fn);
EXPECT_EQ(ZX_OK, status);
EXPECT_EQ(1u, pages_seen, "lookup on partially committed pages\n");
pages_seen = 0;
status = vmo->Lookup(PAGE_SIZE, alloc_size - PAGE_SIZE, lookup_fn);
EXPECT_EQ(ZX_OK, status);
EXPECT_EQ(1u, pages_seen, "lookup on partially committed pages\n");
pages_seen = 0;
status = vmo->Lookup(PAGE_SIZE, PAGE_SIZE, lookup_fn);
EXPECT_EQ(ZX_OK, status);
EXPECT_EQ(1u, pages_seen, "lookup on partially committed pages\n");
pages_seen = 0;
// Contiguous lookups of single pages should also succeed
status = vmo->LookupContiguous(PAGE_SIZE, PAGE_SIZE, nullptr);
EXPECT_EQ(ZX_OK, status, "contiguous lookup of single page\n");
// Commit the rest
status = vmo->CommitRange(0, alloc_size);
EXPECT_EQ(ZX_OK, status, "committing vm object\n");
EXPECT_EQ(alloc_size, PAGE_SIZE * vmo->AttributedPages(), "committing vm object\n");
status = vmo->Lookup(0, alloc_size, lookup_fn);
EXPECT_EQ(ZX_OK, status, "lookup on partially committed pages\n");
EXPECT_EQ(alloc_size / PAGE_SIZE, pages_seen, "lookup on partially committed pages\n");
status = vmo->LookupContiguous(0, PAGE_SIZE, nullptr);
EXPECT_EQ(ZX_OK, status, "contiguous lookup of single page\n");
status = vmo->LookupContiguous(0, alloc_size, nullptr);
EXPECT_NE(ZX_OK, status, "contiguous lookup of multiple pages\n");
END_TEST;
}
static bool vmo_lookup_slice_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
constexpr size_t kAllocSize = PAGE_SIZE * 16;
constexpr size_t kCommitOffset = PAGE_SIZE * 4;
constexpr size_t kSliceOffset = PAGE_SIZE;
constexpr size_t kSliceSize = kAllocSize - kSliceOffset;
fbl::RefPtr<VmObjectPaged> vmo;
ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, kAllocSize, &vmo));
ASSERT_TRUE(vmo);
// Commit a page in the vmo.
ASSERT_OK(vmo->CommitRange(kCommitOffset, PAGE_SIZE));
// Create a slice that is offset slightly.
fbl::RefPtr<VmObject> slice;
ASSERT_OK(vmo->CreateChildSlice(kSliceOffset, kSliceSize, false, &slice));
ASSERT_TRUE(slice);
// Query the slice and validate we see one page at the offset relative to us, not the parent it is
// committed in.
uint64_t offset_seen = UINT64_MAX;
auto lookup_fn = [&offset_seen](size_t offset, paddr_t pa) {
ASSERT(offset_seen == UINT64_MAX);
offset_seen = offset;
return ZX_ERR_NEXT;
};
EXPECT_OK(slice->Lookup(0, kSliceSize, lookup_fn));
EXPECT_EQ(offset_seen, kCommitOffset - kSliceOffset);
END_TEST;
}
static bool vmo_lookup_clone_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
static const size_t page_count = 4;
static const size_t alloc_size = PAGE_SIZE * page_count;
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, alloc_size, &vmo);
ASSERT_EQ(ZX_OK, status, "vmobject creation\n");
ASSERT_TRUE(vmo, "vmobject creation\n");
vmo->set_user_id(ZX_KOID_KERNEL);
// Commit the whole original VMO and the first and last page of the clone.
status = vmo->CommitRange(0, alloc_size);
ASSERT_EQ(ZX_OK, status, "vmobject creation\n");
fbl::RefPtr<VmObject> clone;
status = vmo->CreateClone(Resizability::NonResizable, CloneType::Snapshot, 0, alloc_size, false,
&clone);
ASSERT_EQ(ZX_OK, status, "vmobject creation\n");
ASSERT_TRUE(clone, "vmobject creation\n");
clone->set_user_id(ZX_KOID_KERNEL);
status = clone->CommitRange(0, PAGE_SIZE);
ASSERT_EQ(ZX_OK, status, "vmobject creation\n");
status = clone->CommitRange(alloc_size - PAGE_SIZE, PAGE_SIZE);
ASSERT_EQ(ZX_OK, status, "vmobject creation\n");
// Lookup the paddrs for both VMOs.
paddr_t vmo_lookup[page_count] = {};
paddr_t clone_lookup[page_count] = {};
auto vmo_lookup_func = [&vmo_lookup](uint64_t offset, paddr_t pa) {
vmo_lookup[offset / PAGE_SIZE] = pa;
return ZX_ERR_NEXT;
};
auto clone_lookup_func = [&clone_lookup](uint64_t offset, paddr_t pa) {
clone_lookup[offset / PAGE_SIZE] = pa;
return ZX_ERR_NEXT;
};
status = vmo->Lookup(0, alloc_size, vmo_lookup_func);
EXPECT_EQ(ZX_OK, status, "vmo lookup\n");
status = clone->Lookup(0, alloc_size, clone_lookup_func);
EXPECT_EQ(ZX_OK, status, "vmo lookup\n");
// The original VMO is now copy-on-write so we should see none of its pages,
// and we should only see the two pages that explicitly committed into the clone.
for (unsigned i = 0; i < page_count; i++) {
EXPECT_EQ(0ul, vmo_lookup[i], "Bad paddr\n");
if (i == 0 || i == page_count - 1) {
EXPECT_NE(0ul, clone_lookup[i], "Bad paddr\n");
}
}
END_TEST;
}
static bool vmo_clone_removes_write_test() {
BEGIN_TEST;
// Create and map a VMO.
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, PAGE_SIZE, &vmo);
EXPECT_EQ(ZX_OK, status, "vmo create");
auto ka = VmAspace::kernel_aspace();
void* ptr;
status = ka->MapObjectInternal(vmo, "test", 0, PAGE_SIZE, &ptr, 0, VmAspace::VMM_FLAG_COMMIT,
kArchRwFlags);
EXPECT_EQ(ZX_OK, status, "map vmo");
// Query the aspace and validate there is a writable mapping.
paddr_t paddr_writable;
uint mmu_flags;
status = ka->arch_aspace().Query(reinterpret_cast<vaddr_t>(ptr), &paddr_writable, &mmu_flags);
EXPECT_EQ(ZX_OK, status, "query aspace");
EXPECT_TRUE(mmu_flags & ARCH_MMU_FLAG_PERM_WRITE, "mapping is writable check");
// Clone the VMO, which causes the parent to have to downgrade any mappings to read-only so that
// copy-on-write can take place. Need to set a fake user id so that the COW creation code is
// happy.
vmo->set_user_id(42);
fbl::RefPtr<VmObject> clone;
status =
vmo->CreateClone(Resizability::NonResizable, CloneType::Snapshot, 0, PAGE_SIZE, true, &clone);
EXPECT_EQ(ZX_OK, status, "create clone");
// Aspace should now have a read only mapping with the same underlying page.
paddr_t paddr_readable;
status = ka->arch_aspace().Query(reinterpret_cast<vaddr_t>(ptr), &paddr_readable, &mmu_flags);
EXPECT_EQ(ZX_OK, status, "query aspace");
EXPECT_FALSE(mmu_flags & ARCH_MMU_FLAG_PERM_WRITE, "mapping is read only check");
EXPECT_EQ(paddr_writable, paddr_readable, "mapping has same page");
// Cleanup.
status = ka->FreeRegion(reinterpret_cast<vaddr_t>(ptr));
EXPECT_EQ(ZX_OK, status, "unmapping object");
END_TEST;
}
static bool vmo_zero_scan_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
auto mem = testing::UserMemory::Create(PAGE_SIZE);
ASSERT_NONNULL(mem);
const auto& user_aspace = mem->aspace();
ASSERT_NONNULL(user_aspace);
ASSERT_TRUE(user_aspace->is_user());
// Initially uncommitted, which should not count as having zero pages.
EXPECT_EQ(0u, mem->vmo()->ScanForZeroPages(false));
// Validate that this mapping reads as zeros
EXPECT_EQ(ZX_OK, user_aspace->SoftFault(mem->base(), 0u));
EXPECT_EQ(0, mem->get<int32_t>());
// Reading from the page should not have committed anything, zero or otherwise.
EXPECT_EQ(0u, mem->vmo()->ScanForZeroPages(false));
// IF we write to the page, this should make it committed.
EXPECT_EQ(ZX_OK, user_aspace->SoftFault(mem->base(), VMM_PF_FLAG_WRITE));
mem->put<int32_t>(0);
EXPECT_EQ(1u, mem->vmo()->ScanForZeroPages(false));
// Check that changing the contents effects the zero page count.
EXPECT_EQ(ZX_OK, user_aspace->SoftFault(mem->base(), VMM_PF_FLAG_WRITE));
mem->put<int32_t>(42);
EXPECT_EQ(0u, mem->vmo()->ScanForZeroPages(false));
EXPECT_EQ(ZX_OK, user_aspace->SoftFault(mem->base(), VMM_PF_FLAG_WRITE));
mem->put<int32_t>(0);
EXPECT_EQ(1u, mem->vmo()->ScanForZeroPages(false));
// Scanning should drop permissions in the hardware page table from write to read-only.
paddr_t paddr_readable;
uint mmu_flags;
EXPECT_EQ(ZX_OK, user_aspace->SoftFault(mem->base(), VMM_PF_FLAG_WRITE));
mem->put<int32_t>(0);
zx_status_t status = user_aspace->arch_aspace().Query(mem->base(), &paddr_readable, &mmu_flags);
EXPECT_EQ(ZX_OK, status);
EXPECT_TRUE(mmu_flags & ARCH_MMU_FLAG_PERM_WRITE);
mem->vmo()->ScanForZeroPages(false);
status = user_aspace->arch_aspace().Query(mem->base(), &paddr_readable, &mmu_flags);
EXPECT_EQ(ZX_OK, status);
EXPECT_FALSE(mmu_flags & ARCH_MMU_FLAG_PERM_WRITE);
// Pinning the page should prevent it from being counted.
EXPECT_EQ(1u, mem->vmo()->ScanForZeroPages(false));
EXPECT_EQ(ZX_OK, mem->vmo()->CommitRangePinned(0, PAGE_SIZE));
EXPECT_EQ(0u, mem->vmo()->ScanForZeroPages(false));
mem->vmo()->Unpin(0, PAGE_SIZE);
EXPECT_EQ(1u, mem->vmo()->ScanForZeroPages(false));
// Creating a kernel mapping should prevent any counting from occurring.
VmAspace* kernel_aspace = VmAspace::kernel_aspace();
void* ptr;
status = kernel_aspace->MapObjectInternal(mem->vmo(), "test", 0, PAGE_SIZE, &ptr, 0,
VmAspace::VMM_FLAG_COMMIT, kArchRwFlags);
EXPECT_EQ(ZX_OK, status);
EXPECT_EQ(0u, mem->vmo()->ScanForZeroPages(false));
kernel_aspace->FreeRegion(reinterpret_cast<vaddr_t>(ptr));
EXPECT_EQ(1u, mem->vmo()->ScanForZeroPages(false));
// Actually evict the page now and check that the attribution count goes down and the eviction
// event count goes up.
EXPECT_EQ(1u, mem->vmo()->AttributedPages());
EXPECT_EQ(0u, mem->vmo()->EvictionEventCount());
EXPECT_EQ(1u, mem->vmo()->ScanForZeroPages(true));
EXPECT_EQ(0u, mem->vmo()->AttributedPages());
EXPECT_EQ(1u, mem->vmo()->EvictionEventCount());
END_TEST;
}
static bool vmo_move_pages_on_access_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
fbl::RefPtr<VmObjectPaged> vmo;
vm_page_t* page;
zx_status_t status =
make_committed_pager_vmo(1, /*trap_dirty=*/false, /*resizable=*/false, &page, &vmo);
ASSERT_EQ(ZX_OK, status);
// Our page should now be in a pager backed page queue.
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page));
PageRequest request;
// If we lookup the page then it should be moved to specifically the first page queue.
status = vmo->GetPageBlocking(0, VMM_PF_FLAG_SW_FAULT, nullptr, nullptr, nullptr);
EXPECT_EQ(ZX_OK, status);
size_t queue;
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page, &queue));
EXPECT_EQ(0u, queue);
// Rotate the queues and check the page moves.
pmm_page_queues()->RotatePagerBackedQueues();
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page, &queue));
EXPECT_EQ(1u, queue);
// Touching the page should move it back to the first queue.
status = vmo->GetPageBlocking(0, VMM_PF_FLAG_SW_FAULT, nullptr, nullptr, nullptr);
EXPECT_EQ(ZX_OK, status);
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page, &queue));
EXPECT_EQ(0u, queue);
// Touching pages in a child should also move the page to the front of the queues.
fbl::RefPtr<VmObject> child;
status = vmo->CreateClone(Resizability::NonResizable, CloneType::PrivatePagerCopy, 0, PAGE_SIZE,
true, &child);
ASSERT_EQ(ZX_OK, status);
status = child->GetPageBlocking(0, VMM_PF_FLAG_SW_FAULT, nullptr, nullptr, nullptr);
EXPECT_EQ(ZX_OK, status);
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page, &queue));
EXPECT_EQ(0u, queue);
pmm_page_queues()->RotatePagerBackedQueues();
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page, &queue));
EXPECT_EQ(1u, queue);
status = child->GetPageBlocking(0, VMM_PF_FLAG_SW_FAULT, nullptr, nullptr, nullptr);
EXPECT_EQ(ZX_OK, status);
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page, &queue));
EXPECT_EQ(0u, queue);
END_TEST;
}
static bool vmo_eviction_hints_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
// Create a pager-backed VMO with a single page.
fbl::RefPtr<VmObjectPaged> vmo;
vm_page_t* page;
zx_status_t status =
make_committed_pager_vmo(1, /*trap_dirty=*/false, /*resizable=*/false, &page, &vmo);
ASSERT_EQ(ZX_OK, status);
// Newly created page should be in the first pager backed page queue.
size_t queue;
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page, &queue));
EXPECT_EQ(0u, queue);
// Hint that the page is not needed.
ASSERT_OK(vmo->HintRange(0, PAGE_SIZE, VmObject::EvictionHint::DontNeed));
// The page should now have moved to the DontNeed queue.
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(page));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBackedDontNeed(page));
// Hint that the page is always needed.
ASSERT_OK(vmo->HintRange(0, PAGE_SIZE, VmObject::EvictionHint::AlwaysNeed));
// If the page was loaned, it will be replaced with a non-loaned page now.
page = vmo->DebugGetPage(0);
// The page should now have moved to the first LRU queue.
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBackedDontNeed(page));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page, &queue));
EXPECT_EQ(0u, queue);
// Evicting the page should fail.
ASSERT_FALSE(vmo->DebugGetCowPages()->RemovePageForEviction(
page, 0, VmCowPages::EvictionHintAction::Follow));
// Hint that the page is not needed again.
ASSERT_OK(vmo->HintRange(0, PAGE_SIZE, VmObject::EvictionHint::DontNeed));
// HintRange() is allowed to replace the page.
page = vmo->DebugGetPage(0);
// The page should now have moved to the DontNeed queue.
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(page));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBackedDontNeed(page));
// We should still not be able to evict the page, the AlwaysNeed hint is sticky.
ASSERT_FALSE(vmo->DebugGetCowPages()->RemovePageForEviction(
page, 0, VmCowPages::EvictionHintAction::Follow));
// Accessing the page should move it out of the DontNeed queue.
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBackedDontNeed(page));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page, &queue));
EXPECT_EQ(0u, queue);
// Verify that the page can be rotated as normal.
pmm_page_queues()->RotatePagerBackedQueues();
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page, &queue));
EXPECT_EQ(1u, queue);
// Touching the page should move it back to the first queue.
status = vmo->GetPageBlocking(0, VMM_PF_FLAG_SW_FAULT, nullptr, nullptr, nullptr);
EXPECT_EQ(ZX_OK, status);
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page, &queue));
EXPECT_EQ(0u, queue);
// We should still not be able to evict the page, the AlwaysNeed hint is sticky.
ASSERT_FALSE(vmo->DebugGetCowPages()->RemovePageForEviction(
page, 0, VmCowPages::EvictionHintAction::Follow));
// We should be able to evict the page when told to override the hint.
ASSERT_TRUE(vmo->DebugGetCowPages()->RemovePageForEviction(
page, 0, VmCowPages::EvictionHintAction::Ignore));
pmm_free_page(page);
END_TEST;
}
static bool vmo_always_need_evicts_loaned_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
// Depending on which loaned page we get, it may not still be loaned at the time HintRange() is
// called, so try a few times and make sure we see non-loaned after HintRange() for all the tries.
const uint32_t kTryCount = 30;
for (uint32_t try_ordinal = 0; try_ordinal < kTryCount; ++try_ordinal) {
bool loaning_was_enabled = pmm_physical_page_borrowing_config()->is_loaning_enabled();
bool borrowing_was_enabled =
pmm_physical_page_borrowing_config()->is_borrowing_in_supplypages_enabled();
pmm_physical_page_borrowing_config()->set_loaning_enabled(true);
pmm_physical_page_borrowing_config()->set_borrowing_in_supplypages_enabled(true);
auto cleanup = fit::defer([loaning_was_enabled, borrowing_was_enabled] {
pmm_physical_page_borrowing_config()->set_loaning_enabled(loaning_was_enabled);
pmm_physical_page_borrowing_config()->set_borrowing_in_supplypages_enabled(
borrowing_was_enabled);
});
zx_status_t status;
fbl::RefPtr<VmObjectPaged> vmo;
vm_page_t* page;
const uint32_t kPagesToLoan = 10;
fbl::RefPtr<VmObjectPaged> contiguous_vmos[kPagesToLoan];
uint32_t iteration_count = 0;
const uint32_t kMaxIterations = 2000;
do {
// Before we call make_committed_pager_vmo(), we create a few 1-page contiguous VMOs and
// decommit them, to increase the chance that make_committed_pager_vmo() picks up a loaned
// page, so we'll get to replace that page during HintRange() below. The decommit (loaning)
// is best effort in case loaning is disabled.
for (uint32_t i = 0; i < kPagesToLoan; ++i) {
status = VmObjectPaged::CreateContiguous(PMM_ALLOC_FLAG_ANY, PAGE_SIZE,
/*alignment_log2=*/0, &contiguous_vmos[i]);
ASSERT_EQ(ZX_OK, status);
status = contiguous_vmos[i]->DecommitRange(0, PAGE_SIZE);
ASSERT_TRUE(status == ZX_OK || status == ZX_ERR_NOT_SUPPORTED);
}
// Create a pager-backed VMO with a single page.
status = make_committed_pager_vmo(1, /*trap_dirty=*/false, /*resizable=*/false, &page, &vmo);
ASSERT_EQ(ZX_OK, status);
++iteration_count;
} while (!pmm_is_loaned(page) && iteration_count < kMaxIterations);
// If we hit this iteration count, something almost certainly went wrong...
ASSERT_TRUE(iteration_count < kMaxIterations);
// At this point we can't be absolutely certain that the page will stay loaned depending on
// which loaned page we got, so we run this in an outer loop that tries this a few times.
// Hint that the page is always needed.
ASSERT_OK(vmo->HintRange(0, PAGE_SIZE, VmObject::EvictionHint::AlwaysNeed));
// If the page was still loaned, it will be replaced with a non-loaned page now.
page = vmo->DebugGetPage(0);
ASSERT_FALSE(pmm_is_loaned(page));
}
END_TEST;
}
static bool vmo_eviction_hints_clone_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
// Create a pager-backed VMO with two pages. We will fork a page in a clone later.
fbl::RefPtr<VmObjectPaged> vmo;
vm_page_t* pages[2];
zx_status_t status =
make_committed_pager_vmo(2, /*trap_dirty=*/false, /*resizable=*/false, pages, &vmo);
ASSERT_EQ(ZX_OK, status);
// Newly created pages should be in the first pager backed page queue.
size_t queue;
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(pages[0], &queue));
EXPECT_EQ(0u, queue);
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(pages[1], &queue));
EXPECT_EQ(0u, queue);
// Create a clone.
fbl::RefPtr<VmObject> clone;
status = vmo->CreateClone(Resizability::NonResizable, CloneType::PrivatePagerCopy, 0,
2 * PAGE_SIZE, true, &clone);
ASSERT_EQ(ZX_OK, status);
// Use the clone to perform a bunch of hinting operations on the first page.
// Hint that the page is not needed.
ASSERT_OK(clone->HintRange(0, PAGE_SIZE, VmObject::EvictionHint::DontNeed));
// The page should now have moved to the DontNeed queue.
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(pages[0]));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBackedDontNeed(pages[0]));
// Hint that the page is always needed.
ASSERT_OK(clone->HintRange(0, PAGE_SIZE, VmObject::EvictionHint::AlwaysNeed));
// If the page was loaned, it will be replaced with a non-loaned page now.
pages[0] = vmo->DebugGetPage(0);
// The page should now have moved to the first LRU queue.
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBackedDontNeed(pages[0]));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(pages[0], &queue));
EXPECT_EQ(0u, queue);
// Evicting the page should fail.
ASSERT_FALSE(vmo->DebugGetCowPages()->RemovePageForEviction(
pages[0], 0, VmCowPages::EvictionHintAction::Follow));
// Hinting should also work via a clone of a clone.
fbl::RefPtr<VmObject> clone2;
status = clone->CreateClone(Resizability::NonResizable, CloneType::PrivatePagerCopy, 0,
2 * PAGE_SIZE, true, &clone2);
ASSERT_EQ(ZX_OK, status);
// Hint that the page is not needed.
ASSERT_OK(clone2->HintRange(0, PAGE_SIZE, VmObject::EvictionHint::DontNeed));
// The page should now have moved to the DontNeed queue.
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(pages[0]));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBackedDontNeed(pages[0]));
// Hint that the page is always needed.
ASSERT_OK(clone2->HintRange(0, PAGE_SIZE, VmObject::EvictionHint::AlwaysNeed));
// If the page was loaned, it will be replaced with a non-loaned page now.
pages[0] = vmo->DebugGetPage(0);
// The page should now have moved to the first LRU queue.
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBackedDontNeed(pages[0]));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(pages[0], &queue));
EXPECT_EQ(0u, queue);
// Evicting the page should fail.
ASSERT_FALSE(vmo->DebugGetCowPages()->RemovePageForEviction(
pages[0], 0, VmCowPages::EvictionHintAction::Follow));
// Verify that hinting still works via the parent VMO.
// Hint that the page is not needed again.
ASSERT_OK(vmo->HintRange(0, PAGE_SIZE, VmObject::EvictionHint::DontNeed));
// The page should now have moved to the DontNeed queue.
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(pages[0]));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBackedDontNeed(pages[0]));
// Fork the page in the clone. And make sure hints no longer apply.
uint64_t data = 0xff;
clone->Write(&data, 0, sizeof(data));
EXPECT_EQ(1u, clone->AttributedPages());
// The write will have moved the page to the first page queue, because the page is still accessed
// in order to perform the fork. So hint using the parent again to move to the DontNeed queue.
ASSERT_OK(vmo->HintRange(0, PAGE_SIZE, VmObject::EvictionHint::DontNeed));
// The page should now have moved to the DontNeed queue.
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(pages[0]));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBackedDontNeed(pages[0]));
// Hint that the page is always needed via the clone.
ASSERT_OK(clone->HintRange(0, PAGE_SIZE, VmObject::EvictionHint::AlwaysNeed));
// The page should still be in the DontNeed queue.
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(pages[0]));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBackedDontNeed(pages[0]));
// Hint that the page is always needed via the second level clone.
ASSERT_OK(clone2->HintRange(0, PAGE_SIZE, VmObject::EvictionHint::AlwaysNeed));
// This should move the page out of the the DontNeed queue. Since we forked the page in the
// intermediate clone *after* this clone was created, it will still refer to the original page,
// which is the same as the page in the root.
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(pages[0]));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBackedDontNeed(pages[0]));
// Create another clone that sees the forked page.
// Hinting through this clone should have no effect, since it will see the forked page.
fbl::RefPtr<VmObject> clone3;
status = clone->CreateClone(Resizability::NonResizable, CloneType::PrivatePagerCopy, 0,
2 * PAGE_SIZE, true, &clone3);
ASSERT_EQ(ZX_OK, status);
// Move the page back to the DontNeed queue first.
ASSERT_OK(vmo->HintRange(0, PAGE_SIZE, VmObject::EvictionHint::DontNeed));
// The page should now have moved to the DontNeed queue.
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(pages[0]));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBackedDontNeed(pages[0]));
// Hint through clone3.
ASSERT_OK(clone3->HintRange(0, PAGE_SIZE, VmObject::EvictionHint::AlwaysNeed));
// The page should still be in the DontNeed queue.
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(pages[0]));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBackedDontNeed(pages[0]));
// Hint on the second page using clone3. This page hasn't been forked by the intermediate clone.
// So clone3 should still be able to see the root page.
// First verify that the page is still in queue 0.
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(pages[1], &queue));
EXPECT_EQ(0u, queue);
// Hint DontNeed through clone 3.
ASSERT_OK(clone3->HintRange(PAGE_SIZE, PAGE_SIZE, VmObject::EvictionHint::DontNeed));
// The page should have moved to the DontNeed queue.
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(pages[1]));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBackedDontNeed(pages[1]));
END_TEST;
}
static bool vmo_eviction_test() {
BEGIN_TEST;
// Disable the page scanner as this test would be flaky if our pages get evicted by someone else.
AutoVmScannerDisable scanner_disable;
// Make two pager backed vmos
fbl::RefPtr<VmObjectPaged> vmo;
fbl::RefPtr<VmObjectPaged> vmo2;
vm_page_t* page;
vm_page_t* page2;
zx_status_t status =
make_committed_pager_vmo(1, /*trap_dirty=*/false, /*resizable=*/false, &page, &vmo);
ASSERT_EQ(ZX_OK, status);
status = make_committed_pager_vmo(1, /*trap_dirty=*/false, /*resizable=*/false, &page2, &vmo2);
ASSERT_EQ(ZX_OK, status);
// Shouldn't be able to evict pages from the wrong VMO.
ASSERT_FALSE(vmo->DebugGetCowPages()->RemovePageForEviction(
page2, 0, VmCowPages::EvictionHintAction::Follow));
ASSERT_FALSE(vmo2->DebugGetCowPages()->RemovePageForEviction(
page, 0, VmCowPages::EvictionHintAction::Follow));
// We stack-own loaned pages from RemovePageForEviction() to pmm_free_page().
__UNINITIALIZED StackOwnedLoanedPagesInterval raii_interval;
// Eviction should actually drop the number of committed pages.
EXPECT_EQ(1u, vmo2->AttributedPages());
ASSERT_TRUE(vmo2->DebugGetCowPages()->RemovePageForEviction(
page2, 0, VmCowPages::EvictionHintAction::Follow));
EXPECT_EQ(0u, vmo2->AttributedPages());
pmm_free_page(page2);
EXPECT_GT(vmo2->EvictionEventCount(), 0u);
// Pinned pages should not be evictable.
status = vmo->CommitRangePinned(0, PAGE_SIZE);
EXPECT_EQ(ZX_OK, status);
ASSERT_FALSE(vmo->DebugGetCowPages()->RemovePageForEviction(
page, 0, VmCowPages::EvictionHintAction::Follow));
vmo->Unpin(0, PAGE_SIZE);
END_TEST;
}
// This test exists to provide a location for VmObjectPaged::DebugValidatePageSplits to be
// regularly called so that it doesn't bitrot. Additionally it *might* detect VMO object corruption,
// but it's primary goal is to test the implementation of DebugValidatePageSplits
static bool vmo_validate_page_splits_test() {
BEGIN_TEST;
zx_status_t status = VmObject::ForEach([](const VmObject& vmo) -> zx_status_t {
if (vmo.is_paged()) {
const VmObjectPaged& paged = static_cast<const VmObjectPaged&>(vmo);
if (!paged.DebugValidatePageSplits()) {
return ZX_ERR_INTERNAL;
}
}
return ZX_OK;
});
// Although DebugValidatePageSplits says to panic as soon as possible if it returns false, this
// test errs on side of assuming that the validation is broken, and not the hierarchy, and so does
// not panic. Either way the test still fails, this is just more graceful.
EXPECT_EQ(ZX_OK, status);
END_TEST;
}
// Tests that page attribution caching behaves as expected under various cloning behaviors -
// creation of snapshot clones and slices, removal of clones, committing pages in the original vmo
// and in the clones.
static bool vmo_attribution_clones_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, 4 * PAGE_SIZE, &vmo);
ASSERT_EQ(ZX_OK, status);
// Dummy user id to keep the cloning code happy.
vmo->set_user_id(0xff);
uint64_t expected_gen_count = 1;
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 0u));
// Commit the first two pages. This should increment the generation count by 2 (one per
// LookupPagesLocked() call that results in a page getting committed).
status = vmo->CommitRange(0, 2 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
expected_gen_count += 2;
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 2u));
// Create a clone that sees the second and third pages.
fbl::RefPtr<VmObject> clone;
status = vmo->CreateClone(Resizability::NonResizable, CloneType::Snapshot, PAGE_SIZE,
2 * PAGE_SIZE, true, &clone);
ASSERT_EQ(ZX_OK, status);
clone->set_user_id(0xfc);
// Creation of the clone should increment the generation count.
++expected_gen_count;
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 2u));
EXPECT_EQ(true, verify_object_page_attribution(clone.get(), expected_gen_count, 0u));
// Commit both pages in the clone. This should increment the generation count by the no. of pages
// committed in the clone.
status = clone->CommitRange(0, 2 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
expected_gen_count += 2;
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 2u));
EXPECT_EQ(true, verify_object_page_attribution(clone.get(), expected_gen_count, 2u));
// Commit the last page in the original vmo, which should increment the generation count by 1.
status = vmo->CommitRange(3 * PAGE_SIZE, PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
++expected_gen_count;
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 3u));
// Create a slice that sees all four pages of the original vmo.
fbl::RefPtr<VmObject> slice;
status = vmo->CreateChildSlice(0, 4 * PAGE_SIZE, true, &slice);
ASSERT_EQ(ZX_OK, status);
slice->set_user_id(0xf5);
// Creation of the slice should increment the generation count.
++expected_gen_count;
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 3u));
EXPECT_EQ(true, verify_object_page_attribution(clone.get(), expected_gen_count, 2u));
EXPECT_EQ(true, verify_object_page_attribution(slice.get(), expected_gen_count, 0u));
// Committing the slice's last page is a no-op (as the page is already committed) and should *not*
// increment the generation count.
status = slice->CommitRange(3 * PAGE_SIZE, PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 3u));
// Committing the remaining 3 pages in the slice will commit pages in the original vmo, and should
// increment the generation count by 3 (1 per page committed).
status = slice->CommitRange(0, 4 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
expected_gen_count += 3;
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 4u));
EXPECT_EQ(true, verify_object_page_attribution(clone.get(), expected_gen_count, 2u));
EXPECT_EQ(true, verify_object_page_attribution(slice.get(), expected_gen_count, 0u));
// Removing the clone should increment the generation count.
clone.reset();
++expected_gen_count;
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 4u));
EXPECT_EQ(true, verify_object_page_attribution(slice.get(), expected_gen_count, 0u));
// Removing the slice should increment the generation count.
slice.reset();
++expected_gen_count;
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 4u));
END_TEST;
}
// Tests that page attribution caching behaves as expected under various operations performed on the
// vmo that can change its page list - committing / decommitting pages, reading / writing, zero
// range, resizing.
static bool vmo_attribution_ops_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
for (uint32_t is_ppb_enabled = 0; is_ppb_enabled < 2; ++is_ppb_enabled) {
dprintf(INFO, "is_ppb_enabled: %u\n", is_ppb_enabled);
bool loaning_was_enabled = pmm_physical_page_borrowing_config()->is_loaning_enabled();
bool borrowing_was_enabled =
pmm_physical_page_borrowing_config()->is_borrowing_in_supplypages_enabled();
pmm_physical_page_borrowing_config()->set_loaning_enabled(is_ppb_enabled);
pmm_physical_page_borrowing_config()->set_borrowing_in_supplypages_enabled(is_ppb_enabled);
auto cleanup = fit::defer([loaning_was_enabled, borrowing_was_enabled] {
pmm_physical_page_borrowing_config()->set_loaning_enabled(loaning_was_enabled);
pmm_physical_page_borrowing_config()->set_borrowing_in_supplypages_enabled(
borrowing_was_enabled);
});
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status;
status =
VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, VmObjectPaged::kResizable, 4 * PAGE_SIZE, &vmo);
ASSERT_EQ(ZX_OK, status);
uint64_t expected_gen_count = 1;
uint64_t expected_page_count;
expected_page_count = 0;
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
// Committing pages should increment the generation count.
status = vmo->CommitRange(0, 4 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
expected_gen_count += 4;
expected_page_count = 4;
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
// Committing the same range again will be a no-op, and should *not* increment the generation
// count.
status = vmo->CommitRange(0, 4 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
expected_page_count = 4;
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
// Decommitting pages should increment the generation count.
status = vmo->DecommitRange(0, 4 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
++expected_gen_count;
expected_page_count = 0;
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
// Committing again should increment the generation count.
status = vmo->CommitRange(0, 4 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
expected_gen_count += 4;
expected_page_count = 4;
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
// Decommitting pages should increment the generation count.
status = vmo->DecommitRange(0, 4 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
++expected_gen_count;
expected_page_count = 0;
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
fbl::AllocChecker ac;
fbl::Vector<uint8_t> buf;
buf.reserve(2 * PAGE_SIZE, &ac);
ASSERT_TRUE(ac.check());
// Read the first two pages.
status = vmo->Read(buf.data(), 0, 2 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
// Since these are zero pages being read, this won't commit any pages in
// the vmo and should not increment the generation count, and shouldn't increase the number of
// pages.
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
// Write the first two pages. This will commit 2 pages and should increment the generation
// count.
status = vmo->Write(buf.data(), 0, 2 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
expected_gen_count += 2;
DEBUG_ASSERT(expected_page_count == 0);
expected_page_count += 2;
DEBUG_ASSERT(expected_page_count == 2);
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
// Write the last two pages. This will commit 2 pages and should increment the generation
// count.
status = vmo->Write(buf.data(), 2 * PAGE_SIZE, 2 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
expected_gen_count += 2;
expected_page_count += 2;
DEBUG_ASSERT(expected_page_count == 4);
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
// Resizing the vmo should increment the generation count.
status = vmo->Resize(2 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
++expected_gen_count;
expected_page_count -= 2;
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
// Zero'ing the range will decommit pages, and should increment the generation count.
status = vmo->ZeroRange(0, 2 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
++expected_gen_count;
expected_page_count -= 2;
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
}
END_TEST;
}
// Tests that page attribution caching behaves as expected under various operations performed on a
// contiguous vmo that can change its page list - committing / decommitting pages, reading /
// writing, zero range, resizing.
static bool vmo_attribution_ops_contiguous_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
for (uint32_t is_ppb_enabled = 0; is_ppb_enabled < 2; ++is_ppb_enabled) {
dprintf(INFO, "is_ppb_enabled: %u\n", is_ppb_enabled);
bool loaning_was_enabled = pmm_physical_page_borrowing_config()->is_loaning_enabled();
bool borrowing_was_enabled =
pmm_physical_page_borrowing_config()->is_borrowing_in_supplypages_enabled();
pmm_physical_page_borrowing_config()->set_loaning_enabled(is_ppb_enabled);
pmm_physical_page_borrowing_config()->set_borrowing_in_supplypages_enabled(is_ppb_enabled);
auto cleanup = fit::defer([loaning_was_enabled, borrowing_was_enabled] {
pmm_physical_page_borrowing_config()->set_loaning_enabled(loaning_was_enabled);
pmm_physical_page_borrowing_config()->set_borrowing_in_supplypages_enabled(
borrowing_was_enabled);
});
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status;
status = VmObjectPaged::CreateContiguous(PMM_ALLOC_FLAG_ANY, 4 * PAGE_SIZE,
/*alignment_log2=*/0, &vmo);
ASSERT_EQ(ZX_OK, status);
uint64_t expected_gen_count = 1;
uint64_t expected_page_count;
expected_page_count = 4;
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
// Committing pages should increment the generation count.
status = vmo->CommitRange(0, 4 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
// expected_gen_count doesn't change because the pages are already committed.
expected_page_count = 4;
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
// Committing the same range again will be a no-op, and should *not* increment the generation
// count.
status = vmo->CommitRange(0, 4 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
expected_page_count = 4;
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
// Decommitting pages should increment the generation count.
status = vmo->DecommitRange(0, 4 * PAGE_SIZE);
if (!is_ppb_enabled) {
ASSERT_EQ(ZX_ERR_NOT_SUPPORTED, status);
// No change because DecommitRange() failed (as expected).
DEBUG_ASSERT(expected_page_count == 4);
} else {
ASSERT_EQ(ZX_OK, status);
++expected_gen_count;
expected_page_count = 0;
}
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
// Committing again should increment the generation count.
status = vmo->CommitRange(0, 4 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
if (!is_ppb_enabled) {
// expected_gen_count and expected_page_count don't change because the pages are already
// present
DEBUG_ASSERT(expected_page_count == 4);
} else {
expected_gen_count++;
expected_page_count = 4;
}
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
// Decommitting pages should increment the generation count.
status = vmo->DecommitRange(0, 4 * PAGE_SIZE);
if (!is_ppb_enabled) {
ASSERT_EQ(ZX_ERR_NOT_SUPPORTED, status);
// expected_gen_count and expected_page_count don't change because we're zeroing not
// decommitting
DEBUG_ASSERT(expected_page_count == 4);
} else {
ASSERT_EQ(ZX_OK, status);
++expected_gen_count;
expected_page_count = 0;
}
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
fbl::AllocChecker ac;
fbl::Vector<uint8_t> buf;
buf.reserve(2 * PAGE_SIZE, &ac);
ASSERT_TRUE(ac.check());
// Read the first two pages.
status = vmo->Read(buf.data(), 0, 2 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
// Since these are zero pages being read, this won't commit any pages in
// the vmo and should not increment the generation count, and shouldn't increase the number of
// pages.
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
// Write the first two pages. This will commit 2 pages and should increment the generation
// count.
status = vmo->Write(buf.data(), 0, 2 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
if (!is_ppb_enabled) {
// expected_gen_count and expected_page_count don't change because the pages are already
// present
DEBUG_ASSERT(expected_page_count == 4);
} else {
expected_gen_count += 2;
DEBUG_ASSERT(expected_page_count == 0);
expected_page_count += 2;
DEBUG_ASSERT(expected_page_count == 2);
}
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
// Write the last two pages. This will commit 2 pages and should increment the generation
// count.
status = vmo->Write(buf.data(), 2 * PAGE_SIZE, 2 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
if (!is_ppb_enabled) {
// expected_gen_count and expected_page_count don't change because the pages are already
// present
DEBUG_ASSERT(expected_page_count == 4);
} else {
expected_gen_count += 2;
expected_page_count += 2;
DEBUG_ASSERT(expected_page_count == 4);
}
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
// Zero'ing the range will decommit pages, and should increment the generation count. In the
// case of contiguous VMOs, we don't decommit pages (so far), but we do bump the generation
// count.
status = vmo->ZeroRange(0, 2 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
++expected_gen_count;
// Zeroing doesn't decommit pages of contiguous VMOs (nor does it commit pages).
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
// Decommitting pages should increment the generation count.
status = vmo->DecommitRange(0, 2 * PAGE_SIZE);
if (!is_ppb_enabled) {
ASSERT_EQ(ZX_ERR_NOT_SUPPORTED, status);
DEBUG_ASSERT(expected_page_count == 4);
} else {
ASSERT_EQ(ZX_OK, status);
++expected_gen_count;
// We were able to decommit two pages.
expected_page_count = 2;
}
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
// Zero'ing a decommitted range (if is_ppb_enabled is true) should not commit any new pages.
// Empty slots in a decommitted contiguous VMO are zero by default, as the physical page
// provider will zero these pages on supply.
status = vmo->ZeroRange(0, 2 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
++expected_gen_count;
// The page count should remain unchanged.
EXPECT_EQ(true,
verify_object_page_attribution(vmo.get(), expected_gen_count, expected_page_count));
}
END_TEST;
}
// Tests that page attribution caching behaves as expected for operations specific to pager-backed
// vmo's - supplying pages, creating COW clones.
static bool vmo_attribution_pager_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
static const size_t kNumPages = 2;
static const size_t alloc_size = kNumPages * PAGE_SIZE;
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status =
make_uncommitted_pager_vmo(kNumPages, /*trap_dirty=*/false, /*resizable=*/false, &vmo);
ASSERT_EQ(ZX_OK, status);
// Dummy user id to keep the cloning code happy.
vmo->set_user_id(0xff);
uint64_t expected_gen_count = 1;
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 0u));
// Create an aux VMO to transfer pages into the pager-backed vmo.
fbl::RefPtr<VmObjectPaged> aux_vmo;
status =
VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, VmObjectPaged::kResizable, alloc_size, &aux_vmo);
ASSERT_EQ(ZX_OK, status);
uint64_t aux_expected_gen_count = 1;
EXPECT_EQ(true, verify_object_page_attribution(aux_vmo.get(), aux_expected_gen_count, 0u));
// Committing pages in the aux vmo should increment its generation count.
status = aux_vmo->CommitRange(0, alloc_size);
ASSERT_EQ(ZX_OK, status);
aux_expected_gen_count += 2;
EXPECT_EQ(true, verify_object_page_attribution(aux_vmo.get(), aux_expected_gen_count, 2u));
// Taking pages from the aux vmo should increment its generation count.
VmPageSpliceList page_list;
status = aux_vmo->TakePages(0, PAGE_SIZE, &page_list);
ASSERT_EQ(ZX_OK, status);
++aux_expected_gen_count;
EXPECT_EQ(true, verify_object_page_attribution(aux_vmo.get(), aux_expected_gen_count, 1u));
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 0u));
// Supplying pages to the pager-backed vmo should increment the generation count.
status = vmo->SupplyPages(0, PAGE_SIZE, &page_list);
ASSERT_EQ(ZX_OK, status);
++expected_gen_count;
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 1u));
EXPECT_EQ(true, verify_object_page_attribution(aux_vmo.get(), aux_expected_gen_count, 1u));
aux_vmo.reset();
// Create a COW clone that sees the first page.
fbl::RefPtr<VmObject> clone;
status = vmo->CreateClone(Resizability::NonResizable, CloneType::PrivatePagerCopy, 0, PAGE_SIZE,
true, &clone);
ASSERT_EQ(ZX_OK, status);
clone->set_user_id(0xfc);
// Creation of the clone should increment the generation count.
++expected_gen_count;
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 1u));
EXPECT_EQ(true, verify_object_page_attribution(clone.get(), expected_gen_count, 0u));
// Committing the clone should increment the generation count.
status = clone->CommitRange(0, PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
++expected_gen_count;
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 1u));
EXPECT_EQ(true, verify_object_page_attribution(clone.get(), expected_gen_count, 1u));
// Removal of the clone should increment the generation count.
clone.reset();
++expected_gen_count;
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 1u));
END_TEST;
}
// Tests that page attribution caching behaves as expected when a pager-backed vmo's page is
// evicted.
static bool vmo_attribution_evict_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
fbl::RefPtr<VmObjectPaged> vmo;
vm_page_t* page;
zx_status_t status =
make_committed_pager_vmo(1, /*trap_dirty=*/false, /*resizable=*/false, &page, &vmo);
ASSERT_EQ(ZX_OK, status);
uint64_t expected_gen_count = 2;
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 1u));
// We stack-own loaned pages from RemovePageForEviction() to pmm_free_page().
__UNINITIALIZED StackOwnedLoanedPagesInterval raii_interval;
// Evicting the page should increment the generation count.
ASSERT_TRUE(vmo->DebugGetCowPages()->RemovePageForEviction(
page, 0, VmCowPages::EvictionHintAction::Follow));
pmm_free_page(page);
++expected_gen_count;
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 0u));
END_TEST;
}
// Tests that page attribution caching behaves as expected when zero pages are deduped, changing the
// no. of committed pages in the vmo.
static bool vmo_attribution_dedup_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, 2 * PAGE_SIZE, &vmo);
ASSERT_EQ(ZX_OK, status);
uint64_t expected_gen_count = 1;
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 0u));
// Committing pages should increment the generation count.
status = vmo->CommitRange(0, 2 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
expected_gen_count += 2;
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 2u));
vm_page_t* page;
status = vmo->GetPageBlocking(0, 0, nullptr, &page, nullptr);
ASSERT_EQ(ZX_OK, status);
// Dedupe the first page. This should increment the generation count.
auto vmop = static_cast<VmObjectPaged*>(vmo.get());
ASSERT_TRUE(vmop->DebugGetCowPages()->DedupZeroPage(page, 0));
++expected_gen_count;
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 1u));
// Dedupe the second page. This should increment the generation count.
status = vmo->GetPageBlocking(PAGE_SIZE, 0, nullptr, &page, nullptr);
ASSERT_EQ(ZX_OK, status);
ASSERT_TRUE(vmop->DebugGetCowPages()->DedupZeroPage(page, PAGE_SIZE));
++expected_gen_count;
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 0u));
// Commit the range again.
status = vmo->CommitRange(0, 2 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
expected_gen_count += 2;
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 2u));
// Scan for zero pages, returning only the count (without triggering any reclamation). This should
// *not* change the generation count.
ASSERT_EQ(2u, vmo->ScanForZeroPages(false));
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 2u));
// Scan for zero pages and reclaim them. This should change the generation count.
ASSERT_EQ(2u, vmo->ScanForZeroPages(true));
++expected_gen_count;
EXPECT_EQ(true, verify_object_page_attribution(vmo.get(), expected_gen_count, 0u));
END_TEST;
}
// Test that a VmObjectPaged that is only referenced by its children gets removed by effectively
// merging into its parent and re-homing all the children. This should also drop any VmCowPages
// being held open.
static bool vmo_parent_merge_test() {
BEGIN_TEST;
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, PAGE_SIZE, &vmo);
ASSERT_EQ(ZX_OK, status);
// Set a user ID for testing.
vmo->set_user_id(42);
fbl::RefPtr<VmObject> child;
status = vmo->CreateClone(Resizability::NonResizable, CloneType::Snapshot, 0, PAGE_SIZE, false,
&child);
ASSERT_EQ(ZX_OK, status);
child->set_user_id(43);
EXPECT_EQ(0u, vmo->parent_user_id());
EXPECT_EQ(42u, vmo->user_id());
EXPECT_EQ(43u, child->user_id());
EXPECT_EQ(42u, child->parent_user_id());
// Dropping the parent should re-home the child to an empty parent.
vmo.reset();
EXPECT_EQ(43u, child->user_id());
EXPECT_EQ(0u, child->parent_user_id());
child.reset();
// Recreate a more interesting 3 level hierarchy with vmo->child->(child2,child3)
status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, PAGE_SIZE, &vmo);
ASSERT_EQ(ZX_OK, status);
vmo->set_user_id(42);
status = vmo->CreateClone(Resizability::NonResizable, CloneType::Snapshot, 0, PAGE_SIZE, false,
&child);
ASSERT_EQ(ZX_OK, status);
child->set_user_id(43);
fbl::RefPtr<VmObject> child2;
status = child->CreateClone(Resizability::NonResizable, CloneType::Snapshot, 0, PAGE_SIZE, false,
&child2);
ASSERT_EQ(ZX_OK, status);
child2->set_user_id(44);
fbl::RefPtr<VmObject> child3;
status = child->CreateClone(Resizability::NonResizable, CloneType::Snapshot, 0, PAGE_SIZE, false,
&child3);
ASSERT_EQ(ZX_OK, status);
child3->set_user_id(45);
EXPECT_EQ(0u, vmo->parent_user_id());
EXPECT_EQ(42u, child->parent_user_id());
EXPECT_EQ(43u, child2->parent_user_id());
EXPECT_EQ(43u, child3->parent_user_id());
// Drop the intermediate child, child2+3 should get re-homed to vmo
child.reset();
EXPECT_EQ(42u, child2->parent_user_id());
EXPECT_EQ(42u, child3->parent_user_id());
END_TEST;
}
// Test that the discardable VMO's lock count is updated as expected via lock and unlock ops.
static bool vmo_lock_count_test() {
BEGIN_TEST;
// Create a vmo to lock and unlock from multiple threads.
fbl::RefPtr<VmObjectPaged> vmo;
constexpr uint64_t kSize = 3 * PAGE_SIZE;
zx_status_t status =
VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, VmObjectPaged::kDiscardable, kSize, &vmo);
ASSERT_EQ(ZX_OK, status);
constexpr int kNumThreads = 5;
Thread* threads[kNumThreads];
struct thread_state {
VmObjectPaged* vmo;
bool did_unlock;
} state[kNumThreads];
for (int i = 0; i < kNumThreads; i++) {
state[i].vmo = vmo.get();
state[i].did_unlock = false;
threads[i] = Thread::Create(
"worker",
[](void* arg) -> int {
zx_status_t status;
auto state = static_cast<struct thread_state*>(arg);
// Randomly decide between try-lock and lock.
if (rand() % 2) {
if ((status = state->vmo->TryLockRange(0, kSize)) != ZX_OK) {
return status;
}
} else {
zx_vmo_lock_state_t lock_state = {};
if ((status = state->vmo->LockRange(0, kSize, &lock_state)) != ZX_OK) {
return status;
}
}
// Randomly decide whether to unlock, or leave the vmo locked.
if (rand() % 2) {
if ((status = state->vmo->UnlockRange(0, kSize)) != ZX_OK) {
return status;
}
state->did_unlock = true;
}
return 0;
},
&state[i], DEFAULT_PRIORITY);
}
for (auto& t : threads) {
t->Resume();
}
for (auto& t : threads) {
int ret;
t->Join(&ret, ZX_TIME_INFINITE);
EXPECT_EQ(0, ret);
}
uint64_t expected_lock_count = kNumThreads;
for (auto& s : state) {
if (s.did_unlock) {
expected_lock_count--;
}
}
EXPECT_EQ(expected_lock_count, vmo->DebugGetCowPages()->DebugGetLockCount());
END_TEST;
}
// Tests the state transitions for a discardable VMO. Verifies that a discardable VMO is discarded
// only when unlocked, and can be locked / unlocked again after the discard.
static bool vmo_discardable_states_test() {
BEGIN_TEST;
fbl::RefPtr<VmObjectPaged> vmo;
constexpr uint64_t kSize = 3 * PAGE_SIZE;
zx_status_t status =
VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, VmObjectPaged::kDiscardable, kSize, &vmo);
ASSERT_EQ(ZX_OK, status);
// A newly created discardable vmo is not on any list yet.
EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsUnreclaimable());
EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsReclaimable());
EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsDiscarded());
// Lock and commit all pages.
EXPECT_EQ(ZX_OK, vmo->TryLockRange(0, kSize));
EXPECT_EQ(ZX_OK, vmo->CommitRange(0, kSize));
EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsUnreclaimable());
EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsReclaimable());
EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsDiscarded());
// List to collect any pages freed during the test, and free them to the PMM before exiting.
list_node_t freed_list;
list_initialize(&freed_list);
auto cleanup_freed_list = fit::defer([&freed_list]() { pmm_free(&freed_list); });
// Cannot discard when locked.
EXPECT_EQ(0u, vmo->DebugGetCowPages()->DiscardPages(0, &freed_list));
// Unlock.
EXPECT_EQ(ZX_OK, vmo->UnlockRange(0, kSize));
EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsReclaimable());
EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsUnreclaimable());
EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsDiscarded());
// Should be able to discard now.
EXPECT_EQ(kSize / PAGE_SIZE, vmo->DebugGetCowPages()->DiscardPages(0, &freed_list));
EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsDiscarded());
EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsUnreclaimable());
EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsReclaimable());
// Try lock should fail after discard.
EXPECT_EQ(ZX_ERR_UNAVAILABLE, vmo->TryLockRange(0, kSize));
// Lock should succeed.
zx_vmo_lock_state_t lock_state = {};
EXPECT_EQ(ZX_OK, vmo->LockRange(0, kSize, &lock_state));
EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsUnreclaimable());
EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsReclaimable());
EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsDiscarded());
// Verify the lock state returned.
EXPECT_EQ(0u, lock_state.offset);
EXPECT_EQ(kSize, lock_state.size);
EXPECT_EQ(0u, lock_state.discarded_offset);
EXPECT_EQ(kSize, lock_state.discarded_size);
EXPECT_EQ(ZX_OK, vmo->CommitRange(0, kSize));
// Unlock.
EXPECT_EQ(ZX_OK, vmo->UnlockRange(0, kSize));
EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsReclaimable());
EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsUnreclaimable());
EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsDiscarded());
// Lock again and verify the lock state returned without a discard.
EXPECT_EQ(ZX_OK, vmo->LockRange(0, kSize, &lock_state));
EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsUnreclaimable());
EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsReclaimable());
EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsDiscarded());
EXPECT_EQ(0u, lock_state.offset);
EXPECT_EQ(kSize, lock_state.size);
EXPECT_EQ(0u, lock_state.discarded_offset);
EXPECT_EQ(0u, lock_state.discarded_size);
// Unlock and discard again.
EXPECT_EQ(ZX_OK, vmo->UnlockRange(0, kSize));
EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsReclaimable());
EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsUnreclaimable());
EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsDiscarded());
// Cannot discard if recently unlocked.
EXPECT_EQ(0u, vmo->DebugGetCowPages()->DiscardPages(ZX_TIME_INFINITE, &freed_list));
EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsReclaimable());
EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsUnreclaimable());
EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsDiscarded());
EXPECT_EQ(kSize / PAGE_SIZE, vmo->DebugGetCowPages()->DiscardPages(0, &freed_list));
EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsDiscarded());
EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsUnreclaimable());
EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsReclaimable());
END_TEST;
}
// Test that an unlocked discardable VMO can be discarded as expected.
static bool vmo_discard_test() {
BEGIN_TEST;
// Create a resizable discardable vmo.
fbl::RefPtr<VmObjectPaged> vmo;
constexpr uint64_t kSize = 3 * PAGE_SIZE;
zx_status_t status = VmObjectPaged::Create(
PMM_ALLOC_FLAG_ANY, VmObjectPaged::kDiscardable | VmObjectPaged::kResizable, kSize, &vmo);
ASSERT_EQ(ZX_OK, status);
EXPECT_EQ(kSize, vmo->size());
// Lock and commit all pages. Verify the size.
EXPECT_EQ(ZX_OK, vmo->TryLockRange(0, kSize));
EXPECT_EQ(ZX_OK, vmo->CommitRange(0, kSize));
EXPECT_EQ(kSize, vmo->size());
EXPECT_EQ(kSize / PAGE_SIZE, vmo->AttributedPages());
// List to collect any pages freed during the test, and free them to the PMM before exiting.
list_node_t freed_list;
list_initialize(&freed_list);
auto cleanup_freed_list = fit::defer([&freed_list]() { pmm_free(&freed_list); });
// Cannot discard when locked.
EXPECT_EQ(0u, vmo->DebugGetCowPages()->DiscardPages(0, &freed_list));
EXPECT_EQ(kSize / PAGE_SIZE, vmo->AttributedPages());
// Unlock.
EXPECT_EQ(ZX_OK, vmo->UnlockRange(0, kSize));
EXPECT_EQ(kSize, vmo->size());
// Should be able to discard now.
EXPECT_EQ(kSize / PAGE_SIZE, vmo->DebugGetCowPages()->DiscardPages(0, &freed_list));
EXPECT_EQ(0u, vmo->AttributedPages());
// Verify that the size is not affected.
EXPECT_EQ(kSize, vmo->size());
// Resize the discarded vmo.
constexpr uint64_t kNewSize = 5 * PAGE_SIZE;
EXPECT_EQ(ZX_OK, vmo->Resize(kNewSize));
EXPECT_EQ(kNewSize, vmo->size());
EXPECT_EQ(0u, vmo->AttributedPages());
// Lock the vmo.
zx_vmo_lock_state_t lock_state = {};
EXPECT_EQ(ZX_OK, vmo->LockRange(0, kNewSize, &lock_state));
EXPECT_EQ(kNewSize, vmo->size());
EXPECT_EQ(0u, vmo->AttributedPages());
// Commit and pin some pages, then unlock.
EXPECT_EQ(ZX_OK, vmo->CommitRangePinned(0, kSize));
EXPECT_EQ(kSize / PAGE_SIZE, vmo->AttributedPages());
EXPECT_EQ(ZX_OK, vmo->UnlockRange(0, kNewSize));
// Cannot discard a vmo with pinned pages.
EXPECT_EQ(0u, vmo->DebugGetCowPages()->DiscardPages(0, &freed_list));
EXPECT_EQ(kNewSize, vmo->size());
EXPECT_EQ(kSize / PAGE_SIZE, vmo->AttributedPages());
// Unpin the pages. Should be able to discard now.
vmo->Unpin(0, kSize);
EXPECT_EQ(kSize / PAGE_SIZE, vmo->DebugGetCowPages()->DiscardPages(0, &freed_list));
EXPECT_EQ(kNewSize, vmo->size());
EXPECT_EQ(0u, vmo->AttributedPages());
// Lock and commit pages. Unlock.
EXPECT_EQ(ZX_OK, vmo->LockRange(0, kNewSize, &lock_state));
EXPECT_EQ(ZX_OK, vmo->CommitRange(0, kNewSize));
EXPECT_EQ(ZX_OK, vmo->UnlockRange(0, kNewSize));
// Cannot discard if recently unlocked.
EXPECT_EQ(0u, vmo->DebugGetCowPages()->DiscardPages(ZX_TIME_INFINITE, &freed_list));
EXPECT_EQ(kNewSize / PAGE_SIZE, vmo->AttributedPages());
// Cannot discard a non-discardable vmo.
vmo.reset();
status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, VmObjectPaged::kResizable, kSize, &vmo);
ASSERT_EQ(ZX_OK, status);
ASSERT_EQ(0u, vmo->DebugGetCowPages()->DebugGetLockCount());
EXPECT_EQ(0u, vmo->DebugGetCowPages()->DiscardPages(0, &freed_list));
END_TEST;
}
// Test operations on a discarded VMO and verify expected failures.
static bool vmo_discard_failure_test() {
BEGIN_TEST;
fbl::RefPtr<VmObjectPaged> vmo;
constexpr uint64_t kSize = 5 * PAGE_SIZE;
zx_status_t status =
VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, VmObjectPaged::kDiscardable, kSize, &vmo);
ASSERT_EQ(ZX_OK, status);
fbl::AllocChecker ac;
fbl::Vector<uint8_t> buf;
buf.reserve(kSize, &ac);
ASSERT_TRUE(ac.check());
fbl::Vector<uint8_t> fill;
fill.reserve(kSize, &ac);
ASSERT_TRUE(ac.check());
fill_region(0x77, fill.data(), kSize);
// Lock and commit all pages, write something and read it back to verify.
EXPECT_EQ(ZX_OK, vmo->TryLockRange(0, kSize));
EXPECT_EQ(ZX_OK, vmo->Write(fill.data(), 0, kSize));
EXPECT_EQ(kSize / PAGE_SIZE, vmo->AttributedPages());
EXPECT_EQ(ZX_OK, vmo->Read(buf.data(), 0, kSize));
EXPECT_EQ(0, memcmp(fill.data(), buf.data(), kSize));
// Create a test user aspace to map the vmo.
fbl::RefPtr<VmAspace> aspace = VmAspace::Create(VmAspace::Type::User, "test aspace");
ASSERT_NONNULL(aspace);
VmAspace* old_aspace = Thread::Current::Get()->aspace();
auto cleanup_aspace = fit::defer([&]() {
vmm_set_active_aspace(old_aspace);
ASSERT(aspace->Destroy() == ZX_OK);
});
vmm_set_active_aspace(aspace.get());
// Map the vmo.
fbl::RefPtr<VmMapping> mapping;
constexpr uint64_t kMapSize = 3 * PAGE_SIZE;
static constexpr const uint kArchFlags = kArchRwFlags | ARCH_MMU_FLAG_PERM_USER;
status = aspace->RootVmar()->CreateVmMapping(0, kMapSize, 0, 0, vmo, kSize - kMapSize, kArchFlags,
"test", &mapping);
ASSERT_EQ(ZX_OK, status);
// Fill with a known pattern through the mapping, and verify the contents.
auto uptr = make_user_inout_ptr(reinterpret_cast<void*>(mapping->base()));
fill_region_user(0x88, uptr, kMapSize);
EXPECT_TRUE(test_region_user(0x88, uptr, kMapSize));
// List to collect any pages freed during the test, and free them to the PMM before exiting.
list_node_t freed_list;
list_initialize(&freed_list);
auto cleanup_freed_list = fit::defer([&freed_list]() { pmm_free(&freed_list); });
// Unlock and discard.
EXPECT_EQ(ZX_OK, vmo->UnlockRange(0, kSize));
EXPECT_EQ(kSize / PAGE_SIZE, vmo->DebugGetCowPages()->DiscardPages(0, &freed_list));
EXPECT_EQ(0u, vmo->AttributedPages());
EXPECT_EQ(kSize, vmo->size());
// Reads, writes, commits and pins should fail now.
EXPECT_EQ(ZX_ERR_NOT_FOUND, vmo->Read(buf.data(), 0, kSize));
EXPECT_EQ(0u, vmo->AttributedPages());
EXPECT_EQ(ZX_ERR_NOT_FOUND, vmo->Write(buf.data(), 0, kSize));
EXPECT_EQ(0u, vmo->AttributedPages());
EXPECT_EQ(ZX_ERR_NOT_FOUND, vmo->CommitRange(0, kSize));
EXPECT_EQ(0u, vmo->AttributedPages());
EXPECT_EQ(ZX_ERR_NOT_FOUND, vmo->CommitRangePinned(0, kSize));
EXPECT_EQ(0u, vmo->AttributedPages());
// Decommit and ZeroRange should trivially succeed.
EXPECT_EQ(ZX_OK, vmo->DecommitRange(0, kSize));
EXPECT_EQ(0u, vmo->AttributedPages());
EXPECT_EQ(ZX_OK, vmo->ZeroRange(0, kSize));
EXPECT_EQ(0u, vmo->AttributedPages());
// Creating a mapping succeeds.
fbl::RefPtr<VmMapping> mapping2;
status = aspace->RootVmar()->CreateVmMapping(0, kMapSize, 0, 0, vmo, kSize - kMapSize, kArchFlags,
"test2", &mapping2);
ASSERT_EQ(ZX_OK, status);
EXPECT_EQ(0u, vmo->AttributedPages());
// Lock the vmo again.
zx_vmo_lock_state_t lock_state = {};
EXPECT_EQ(ZX_OK, vmo->LockRange(0, kSize, &lock_state));
EXPECT_EQ(0u, vmo->AttributedPages());
EXPECT_EQ(kSize, vmo->size());
// Should be able to read now. Verify that previous contents are lost and zeros are read.
EXPECT_EQ(ZX_OK, vmo->Read(buf.data(), 0, kSize));
memset(fill.data(), 0, kSize);
EXPECT_EQ(0, memcmp(fill.data(), buf.data(), kSize));
EXPECT_EQ(0u, vmo->AttributedPages());
// Write should succeed as well.
fill_region(0x99, fill.data(), kSize);
EXPECT_EQ(ZX_OK, vmo->Write(fill.data(), 0, kSize));
EXPECT_EQ(kSize / PAGE_SIZE, vmo->AttributedPages());
// Verify contents via the mapping.
fill_region_user(0xaa, uptr, kMapSize);
EXPECT_TRUE(test_region_user(0xaa, uptr, kMapSize));
// Verify contents via the second mapping created when discarded.
uptr = make_user_inout_ptr(reinterpret_cast<void*>(mapping2->base()));
EXPECT_TRUE(test_region_user(0xaa, uptr, kMapSize));
// The unmapped pages should still be intact after the Write() above.
EXPECT_EQ(ZX_OK, vmo->Read(buf.data(), 0, kSize - kMapSize));
EXPECT_EQ(0, memcmp(fill.data(), buf.data(), kSize - kMapSize));
END_TEST;
}
static bool vmo_discardable_counts_test() {
BEGIN_TEST;
constexpr int kNumVmos = 10;
fbl::RefPtr<VmObjectPaged> vmos[kNumVmos];
// Create some discardable vmos.
zx_status_t status;
for (int i = 0; i < kNumVmos; i++) {
status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, VmObjectPaged::kDiscardable,
(i + 1) * PAGE_SIZE, &vmos[i]);
ASSERT_EQ(ZX_OK, status);
}
VmCowPages::DiscardablePageCounts expected = {};
// List to collect any pages freed during the test, and free them to the PMM before exiting.
list_node_t freed_list;
list_initialize(&freed_list);
auto cleanup_freed_list = fit::defer([&freed_list]() { pmm_free(&freed_list); });
// Lock all vmos. Unlock a few. And discard a few unlocked ones.
// Compute the expected page counts as a result of these operations.
for (int i = 0; i < kNumVmos; i++) {
EXPECT_EQ(ZX_OK, vmos[i]->TryLockRange(0, (i + 1) * PAGE_SIZE));
EXPECT_EQ(ZX_OK, vmos[i]->CommitRange(0, (i + 1) * PAGE_SIZE));
if (rand() % 2) {
EXPECT_EQ(ZX_OK, vmos[i]->UnlockRange(0, (i + 1) * PAGE_SIZE));
if (rand() % 2) {
// Discarded pages won't show up under locked or unlocked counts.
EXPECT_EQ(static_cast<uint64_t>(i + 1),
vmos[i]->DebugGetCowPages()->DiscardPages(0, &freed_list));
} else {
// Unlocked but not discarded.
expected.unlocked += (i + 1);
}
} else {
// Locked.
expected.locked += (i + 1);
}
}
VmCowPages::DiscardablePageCounts counts = VmCowPages::DebugDiscardablePageCounts();
// There might be other discardable vmos in the rest of the system, so the actual page counts
// might be higher than the expected counts.
EXPECT_LE(expected.locked, counts.locked);
EXPECT_LE(expected.unlocked, counts.unlocked);
END_TEST;
}
static bool vmo_lookup_pages_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
fbl::RefPtr<VmObjectPaged> vmo;
// This test assumes does some division and then offsetting of kMaxPages and as a consequence
// assumes that kMaxPages is at least 8. This is not a static assert as we don't want to preclude
// testing and running the kernel with lower max pages.
ASSERT_GE(VmObject::LookupInfo::kMaxPages, 8ul);
constexpr uint64_t kSize = VmObject::LookupInfo::kMaxPages * 2 * PAGE_SIZE;
zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, kSize, &vmo);
ASSERT_EQ(ZX_OK, status);
// Commit half the range so we can do some contiguous lookups.
EXPECT_OK(vmo->CommitRange(0, kSize / 4));
VmObject::LookupInfo info;
// Lookup the exact range we committed.
EXPECT_OK(vmo_lookup_pages(vmo.get(), 0, 0, VmObject::DirtyTrackingAction::None,
VmObject::LookupInfo::kMaxPages / 2, nullptr, &info));
EXPECT_EQ(info.num_pages, VmObject::LookupInfo::kMaxPages / 2);
EXPECT_TRUE(info.writable);
// Attempt to lookup more, should see the truncated actual committed range.
EXPECT_OK(vmo_lookup_pages(vmo.get(), 0, 0, VmObject::DirtyTrackingAction::None,
VmObject::LookupInfo::kMaxPages, nullptr, &info));
EXPECT_EQ(info.num_pages, VmObject::LookupInfo::kMaxPages / 2);
EXPECT_TRUE(info.writable);
// Perform a lookup so that there's only a single committed page visible.
EXPECT_OK(vmo_lookup_pages(vmo.get(), kSize / 4 - PAGE_SIZE, 0,
VmObject::DirtyTrackingAction::None, VmObject::LookupInfo::kMaxPages,
nullptr, &info));
EXPECT_EQ(info.num_pages, 1ul);
EXPECT_TRUE(info.writable);
// Writing shouldn't commit later pages once the first has been satisfied.
EXPECT_OK(vmo_lookup_pages(
vmo.get(), kSize / 4 - PAGE_SIZE, VMM_PF_FLAG_WRITE | VMM_PF_FLAG_SW_FAULT,
VmObject::DirtyTrackingAction::None, VmObject::LookupInfo::kMaxPages / 2, nullptr, &info));
EXPECT_EQ(info.num_pages, 1ul);
EXPECT_TRUE(info.writable);
// If there is no page then writing without a fault should fail.
EXPECT_EQ(ZX_ERR_NOT_FOUND, vmo_lookup_pages(vmo.get(), kSize / 4, VMM_PF_FLAG_WRITE,
VmObject::DirtyTrackingAction::None,
VmObject::LookupInfo::kMaxPages, nullptr, &info));
// Then should be able to fault it in.
EXPECT_OK(vmo_lookup_pages(vmo.get(), kSize / 4, VMM_PF_FLAG_WRITE | VMM_PF_FLAG_SW_FAULT,
VmObject::DirtyTrackingAction::None, VmObject::LookupInfo::kMaxPages,
nullptr, &info));
EXPECT_EQ(info.num_pages, 1ul);
EXPECT_TRUE(info.writable);
// Create a hierarchy now to do some more interesting read lookups.
fbl::RefPtr<VmObject> child1;
ASSERT_OK(
vmo->CreateClone(Resizability::NonResizable, CloneType::Snapshot, 0, kSize, false, &child1));
EXPECT_OK(child1->CommitRange(kSize / 8, PAGE_SIZE));
fbl::RefPtr<VmObject> child2;
ASSERT_OK(child1->CreateClone(Resizability::NonResizable, CloneType::Snapshot, 0, kSize, false,
&child2));
// Should be able to get runs of pages up to the intermediate page in child1.
EXPECT_OK(vmo_lookup_pages(child2.get(), 0, 0, VmObject::DirtyTrackingAction::None,
VmObject::LookupInfo::kMaxPages, nullptr, &info));
EXPECT_EQ(info.num_pages, VmObject::LookupInfo::kMaxPages / 4);
EXPECT_FALSE(info.writable);
// The single page in child1
EXPECT_OK(vmo_lookup_pages(child2.get(), kSize / 8, 0, VmObject::DirtyTrackingAction::None,
VmObject::LookupInfo::kMaxPages, nullptr, &info));
EXPECT_EQ(info.num_pages, 1ul);
EXPECT_FALSE(info.writable);
// Then the remainder of the run.
EXPECT_OK(vmo_lookup_pages(child2.get(), kSize / 8 + PAGE_SIZE, 0,
VmObject::DirtyTrackingAction::None, VmObject::LookupInfo::kMaxPages,
nullptr, &info));
EXPECT_EQ(info.num_pages, VmObject::LookupInfo::kMaxPages / 4);
EXPECT_FALSE(info.writable);
END_TEST;
}
static bool vmo_write_does_not_commit_test() {
BEGIN_TEST;
// Create a vmo and commit a page to it.
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, PAGE_SIZE, &vmo);
ASSERT_OK(status);
uint64_t val = 42;
EXPECT_OK(vmo->Write(&val, 0, sizeof(val)));
// Create a CoW clone of the vmo.
fbl::RefPtr<VmObject> clone;
status = vmo->CreateClone(Resizability::NonResizable, CloneType::Snapshot, 0, PAGE_SIZE, false,
&clone);
// Querying the page for read in the clone should return it.
EXPECT_OK(clone->GetPageBlocking(0, 0, nullptr, nullptr, nullptr));
// Querying for write, without any fault flags, should not work as the page is not committed in
// the clone.
EXPECT_EQ(ZX_ERR_NOT_FOUND,
clone->GetPageBlocking(0, VMM_PF_FLAG_WRITE, nullptr, nullptr, nullptr));
// Adding a fault flag should cause the lookup to succeed.
EXPECT_OK(clone->GetPageBlocking(0, VMM_PF_FLAG_WRITE | VMM_PF_FLAG_SW_FAULT, nullptr, nullptr,
nullptr));
END_TEST;
}
static bool vmo_stack_owned_loaned_pages_interval_test() {
BEGIN_TEST;
// So far it doesn't seem like we have any way to directly observe priority inheritance happening.
// Really that's more of an OwnedWaitQueue concern, but it might be nice to double check here that
// a page owning thread can be boosted while there's a higher priority waiter.
// This test isn't stress, but have a few threads to check on multiple waiters per page and
// multiple waiters per stack_owner.
constexpr uint32_t kFakePageCount = 8;
constexpr uint32_t kWaitingThreadsPerPage = 2;
constexpr uint32_t kWaitingThreadCount = kFakePageCount * kWaitingThreadsPerPage;
struct OwningThread {
Thread* thread = nullptr;
vm_page_t pages[kFakePageCount] = {};
ktl::atomic<bool> ownership_acquired = false;
ktl::atomic<bool> release_stack_ownership = false;
} ot;
for (auto& page : ot.pages) {
EXPECT_EQ(vm_page_state::FREE, page.state());
// Normally this would be under PmmLock; only for testing.
page.loaned = true;
}
// Test no pages stack owned in a given interval.
{ // scope raii_interval
StackOwnedLoanedPagesInterval raii_interval;
DEBUG_ASSERT(&StackOwnedLoanedPagesInterval::current() == &raii_interval);
} // ~raii_interval
// Test page stack owned but never waited on. Hold thread_lock for this to get a failure if the
// thread_lock is ever acquired for this scenario. Normally the thread_lock would not be held
// for these steps.
{ // scope thread_lock_guard, raii_interval
Guard<MonitoredSpinLock, IrqSave> thread_lock_guard{ThreadLock::Get(), SOURCE_TAG};
StackOwnedLoanedPagesInterval raii_interval;
DEBUG_ASSERT(&StackOwnedLoanedPagesInterval::current() == &raii_interval);
ot.pages[0].object.set_stack_owner(&StackOwnedLoanedPagesInterval::current());
ot.pages[0].object.clear_stack_owner();
} // ~raii_interval, ~thread_lock_guard
// Test pages stack owned each with multiple waiters.
ot.thread = Thread::Create(
"owning_thread",
[](void* arg) -> int {
OwningThread& ot = *reinterpret_cast<OwningThread*>(arg);
StackOwnedLoanedPagesInterval raii_interval;
for (auto& page : ot.pages) {
DEBUG_ASSERT(&StackOwnedLoanedPagesInterval::current() == &raii_interval);
page.object.set_stack_owner(&StackOwnedLoanedPagesInterval::current());
}
ot.ownership_acquired.store(true);
// We spin here on purpose instead of waiting or sleeping. We want to see the owning
// thread's priority reflect the waiting thread's priority.
while (!ot.release_stack_ownership) {
}
for (auto& page : ot.pages) {
page.object.clear_stack_owner();
}
// ~raii_interval
return 0;
},
&ot, LOWEST_PRIORITY);
ot.thread->Resume();
while (!ot.ownership_acquired.load()) {
}
struct WaitingThread {
OwningThread* ot = nullptr;
uint32_t i = 0;
Thread* thread = nullptr;
ktl::atomic<bool> waiting_thread_started = false;
ktl::atomic<bool> waiting_thread_wait_done = false;
} waiting_threads[kWaitingThreadCount] = {};
for (uint32_t i = 0; i < kWaitingThreadCount; ++i) {
auto& wt = waiting_threads[i];
wt.ot = &ot;
wt.i = i;
wt.thread = Thread::Create(
"waiting_thread",
[](void* arg) -> int {
WaitingThread& wt = *reinterpret_cast<WaitingThread*>(arg);
wt.waiting_thread_started.store(true);
StackOwnedLoanedPagesInterval::WaitUntilContiguousPageNotStackOwned(
&wt.ot->pages[wt.i / kWaitingThreadsPerPage]);
wt.waiting_thread_wait_done.store(true);
return 0;
},
&wt, DEFAULT_PRIORITY);
}
for (auto& wt : waiting_threads) {
wt.thread->Resume();
}
for (auto& wt : waiting_threads) {
while (!wt.waiting_thread_started.load()) {
}
}
Thread::Current::SleepRelative(ZX_MSEC(100));
for (auto& wt : waiting_threads) {
EXPECT_FALSE(wt.waiting_thread_wait_done.load());
}
ot.release_stack_ownership.store(true);
for (auto& wt : waiting_threads) {
while (!wt.waiting_thread_wait_done.load()) {
}
}
int ret;
ot.thread->Join(&ret, ZX_TIME_INFINITE);
EXPECT_EQ(0, ret);
for (auto& wt : waiting_threads) {
wt.thread->Join(&ret, ZX_TIME_INFINITE);
EXPECT_EQ(0, ret);
}
END_TEST;
}
static bool vmo_dirty_pages_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
// Create a pager-backed VMO with a single page.
fbl::RefPtr<VmObjectPaged> vmo;
vm_page_t* page;
ASSERT_OK(make_committed_pager_vmo(1, /*trap_dirty=*/true, /*resizable=*/false, &page, &vmo));
// Newly created page should be in the first pager backed page queue.
size_t queue;
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page, &queue));
EXPECT_EQ(0u, queue);
// Rotate the queues and check the page moves.
pmm_page_queues()->RotatePagerBackedQueues();
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page, &queue));
EXPECT_EQ(1u, queue);
// Accessing the page should move it back to the first queue.
EXPECT_OK(vmo->GetPageBlocking(0, VMM_PF_FLAG_SW_FAULT, nullptr, nullptr, nullptr));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page, &queue));
EXPECT_EQ(0u, queue);
// Now simulate a write to the page. This should move the page to the dirty queue.
ASSERT_OK(vmo->DirtyPages(0, PAGE_SIZE));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(page));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBackedDirty(page));
// Should not be able to evict a dirty page.
ASSERT_FALSE(vmo->DebugGetCowPages()->RemovePageForEviction(
page, 0, VmCowPages::EvictionHintAction::Follow));
// Accessing the page again should not move the page out of the dirty queue.
EXPECT_OK(vmo->GetPageBlocking(0, VMM_PF_FLAG_SW_FAULT, nullptr, nullptr, nullptr));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(page));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBackedDirty(page));
END_TEST;
}
static bool vmo_dirty_pages_writeback_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
// Create a pager-backed VMO with a single page.
fbl::RefPtr<VmObjectPaged> vmo;
vm_page_t* page;
ASSERT_OK(make_committed_pager_vmo(1, /*trap_dirty=*/true, /*resizable=*/false, &page, &vmo));
// Newly created page should be in the first pager backed page queue.
size_t queue;
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page, &queue));
EXPECT_EQ(0u, queue);
// Now simulate a write to the page. This should move the page to the dirty queue.
ASSERT_OK(vmo->DirtyPages(0, PAGE_SIZE));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(page));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBackedDirty(page));
// Should not be able to evict a dirty page.
ASSERT_FALSE(vmo->DebugGetCowPages()->RemovePageForEviction(
page, 0, VmCowPages::EvictionHintAction::Follow));
// Begin writeback on the page. This should still keep the page in the dirty queue.
ASSERT_OK(vmo->WritebackBegin(0, PAGE_SIZE));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(page));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBackedDirty(page));
// Should not be able to evict a dirty page.
ASSERT_FALSE(vmo->DebugGetCowPages()->RemovePageForEviction(
page, 0, VmCowPages::EvictionHintAction::Follow));
// Accessing the page should not move the page out of the dirty queue either.
ASSERT_OK(vmo->GetPageBlocking(0, VMM_PF_FLAG_SW_FAULT, nullptr, nullptr, nullptr));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(page));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBackedDirty(page));
// Should not be able to evict a dirty page.
ASSERT_FALSE(vmo->DebugGetCowPages()->RemovePageForEviction(
page, 0, VmCowPages::EvictionHintAction::Follow));
// End writeback on the page. This should finally move the page out of the dirty queue.
ASSERT_OK(vmo->WritebackEnd(0, PAGE_SIZE));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBackedDirty(page));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page, &queue));
EXPECT_EQ(0u, queue);
// We should be able to rotate the page as usual.
pmm_page_queues()->RotatePagerBackedQueues();
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page, &queue));
EXPECT_EQ(1u, queue);
// Another write moves the page back to the Dirty queue.
ASSERT_OK(vmo->DirtyPages(0, PAGE_SIZE));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(page));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBackedDirty(page));
// Clean the page again, and try to evict it.
ASSERT_OK(vmo->WritebackBegin(0, PAGE_SIZE));
ASSERT_OK(vmo->WritebackEnd(0, PAGE_SIZE));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBackedDirty(page));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page, &queue));
EXPECT_EQ(0u, queue);
// We should now be able to evict the page.
ASSERT_TRUE(vmo->DebugGetCowPages()->RemovePageForEviction(
page, 0, VmCowPages::EvictionHintAction::Follow));
END_TEST;
}
static bool vmo_dirty_pages_with_hints_test() {
BEGIN_TEST;
AutoVmScannerDisable scanner_disable;
// Create a pager-backed VMO with a single page.
fbl::RefPtr<VmObjectPaged> vmo;
vm_page_t* page;
ASSERT_OK(make_committed_pager_vmo(1, /*trap_dirty=*/true, /*resizable=*/false, &page, &vmo));
// Newly created page should be in the first pager backed page queue.
size_t queue;
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page, &queue));
EXPECT_EQ(0u, queue);
// Now simulate a write to the page. This should move the page to the dirty queue.
ASSERT_OK(vmo->DirtyPages(0, PAGE_SIZE));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(page));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBackedDirty(page));
// Hint DontNeed on the page. It should remain in the dirty queue.
ASSERT_OK(vmo->HintRange(0, PAGE_SIZE, VmObject::EvictionHint::DontNeed));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBackedDontNeed(page));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(page));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBackedDirty(page));
// Should not be able to evict a dirty page.
ASSERT_FALSE(vmo->DebugGetCowPages()->RemovePageForEviction(
page, 0, VmCowPages::EvictionHintAction::Follow));
// Hint AlwaysNeed on the page. It should remain in the dirty queue.
ASSERT_OK(vmo->HintRange(0, PAGE_SIZE, VmObject::EvictionHint::AlwaysNeed));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(page));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBackedDontNeed(page));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBackedDirty(page));
// Clean the page.
ASSERT_OK(vmo->WritebackBegin(0, PAGE_SIZE));
ASSERT_OK(vmo->WritebackEnd(0, PAGE_SIZE));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBackedDirty(page));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page, &queue));
EXPECT_EQ(0u, queue);
// Eviction should fail still because we hinted AlwaysNeed previously.
ASSERT_FALSE(vmo->DebugGetCowPages()->RemovePageForEviction(
page, 0, VmCowPages::EvictionHintAction::Follow));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBackedDirty(page));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page, &queue));
EXPECT_EQ(0u, queue);
// Eviction should succeed if we ignore the hint.
ASSERT_TRUE(vmo->DebugGetCowPages()->RemovePageForEviction(
page, 0, VmCowPages::EvictionHintAction::Ignore));
// Reset the vmo and retry some of the same actions as before, this time dirtying
// the page *after* hinting.
vmo.reset();
ASSERT_OK(make_committed_pager_vmo(1, /*trap_dirty=*/true, /*resizable=*/false, &page, &vmo));
// Newly created page should be in the first pager backed page queue.
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(page, &queue));
EXPECT_EQ(0u, queue);
// Hint DontNeed on the page. This should move the page to the DontNeed queue.
ASSERT_OK(vmo->HintRange(0, PAGE_SIZE, VmObject::EvictionHint::DontNeed));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(page));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBackedDirty(page));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBackedDontNeed(page));
// Write to the page now. This should move it to the dirty queue.
ASSERT_OK(vmo->DirtyPages(0, PAGE_SIZE));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(page));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBackedDontNeed(page));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBackedDirty(page));
// Should not be able to evict a dirty page.
ASSERT_FALSE(vmo->DebugGetCowPages()->RemovePageForEviction(
page, 0, VmCowPages::EvictionHintAction::Follow));
END_TEST;
}
// Tests that pinning pager-backed pages retains backlink information.
static bool vmo_pinning_backlink_test() {
BEGIN_TEST;
// Disable the page scanner as this test would be flaky if our pages get evicted by someone else.
AutoVmScannerDisable scanner_disable;
// Create a pager-backed VMO with two pages, so we can verify a non-zero offset value.
fbl::RefPtr<VmObjectPaged> vmo;
vm_page_t* pages[2];
zx_status_t status =
make_committed_pager_vmo(2, /*trap_dirty=*/false, /*resizable=*/false, pages, &vmo);
ASSERT_EQ(ZX_OK, status);
// Pages should be in the pager queue.
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(pages[0]));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(pages[1]));
// Verify backlink information.
auto cow = vmo->DebugGetCowPages().get();
EXPECT_EQ(cow, pages[0]->object.get_object());
EXPECT_EQ(0u, pages[0]->object.get_page_offset());
EXPECT_EQ(cow, pages[1]->object.get_object());
EXPECT_EQ(static_cast<uint64_t>(PAGE_SIZE), pages[1]->object.get_page_offset());
// Pin the pages.
status = vmo->CommitRangePinned(0, 2 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
// Pages might get swapped out on pinning if they were loaned. Look them up again.
pages[0] = vmo->DebugGetPage(0);
pages[1] = vmo->DebugGetPage(PAGE_SIZE);
// Pages should be in the wired queue.
EXPECT_TRUE(pmm_page_queues()->DebugPageIsWired(pages[0]));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsWired(pages[1]));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(pages[0]));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsPagerBacked(pages[1]));
// Moving to the wired queue should retain backlink information.
EXPECT_EQ(cow, pages[0]->object.get_object());
EXPECT_EQ(0u, pages[0]->object.get_page_offset());
EXPECT_EQ(cow, pages[1]->object.get_object());
EXPECT_EQ(static_cast<uint64_t>(PAGE_SIZE), pages[1]->object.get_page_offset());
// Unpin the pages.
vmo->Unpin(0, 2 * PAGE_SIZE);
// Pages should be back in the pager queue.
EXPECT_FALSE(pmm_page_queues()->DebugPageIsWired(pages[0]));
EXPECT_FALSE(pmm_page_queues()->DebugPageIsWired(pages[1]));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(pages[0]));
EXPECT_TRUE(pmm_page_queues()->DebugPageIsPagerBacked(pages[1]));
// Verify backlink information again.
EXPECT_EQ(cow, pages[0]->object.get_object());
EXPECT_EQ(0u, pages[0]->object.get_page_offset());
EXPECT_EQ(cow, pages[1]->object.get_object());
EXPECT_EQ(static_cast<uint64_t>(PAGE_SIZE), pages[1]->object.get_page_offset());
END_TEST;
}
// Tests that the supply_zero_offset is updated as expected on VMO resize.
static bool vmo_supply_zero_offset_test() {
BEGIN_TEST;
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status =
make_uncommitted_pager_vmo(50, /*trap_dirty=*/false, /*resizable=*/true, &vmo);
ASSERT_EQ(ZX_OK, status);
uint64_t expected = 50 * PAGE_SIZE;
EXPECT_EQ(expected, vmo->DebugGetCowPages()->DebugGetSupplyZeroOffset());
// Resizing up should not alter the zero offset.
status = vmo->Resize(100 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
EXPECT_EQ(expected, vmo->DebugGetCowPages()->DebugGetSupplyZeroOffset());
// Resizing down to beyond the zero offset should not alter the zero offset.
status = vmo->Resize(80 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
EXPECT_EQ(expected, vmo->DebugGetCowPages()->DebugGetSupplyZeroOffset());
// Resizing down to the zero offset should not alter the zero offset.
status = vmo->Resize(50 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
EXPECT_EQ(expected, vmo->DebugGetCowPages()->DebugGetSupplyZeroOffset());
// Resizing down to less than the zero offset should shrink the zero offset.
status = vmo->Resize(30 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
expected = 30 * PAGE_SIZE;
EXPECT_EQ(expected, vmo->DebugGetCowPages()->DebugGetSupplyZeroOffset());
// Randomly resize a few times and check that the zero offset at the end is the lowest size set.
size_t min = vmo->DebugGetCowPages()->DebugGetSupplyZeroOffset() / PAGE_SIZE;
for (int i = 0; i < 100; i++) {
size_t num_pages = rand() % 100;
status = vmo->Resize(num_pages * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
if (num_pages < min) {
min = num_pages;
}
}
EXPECT_EQ(min * PAGE_SIZE, vmo->DebugGetCowPages()->DebugGetSupplyZeroOffset());
// Zero offset is not applicable to clones of pager backed VMOs.
fbl::RefPtr<VmObject> clone;
const uint64_t size = vmo->size();
status = vmo->CreateClone(Resizability::Resizable, CloneType::PrivatePagerCopy, 0,
size * PAGE_SIZE, true, &clone);
ASSERT_EQ(ZX_OK, status);
auto cow = reinterpret_cast<VmObjectPaged*>(clone.get())->DebugGetCowPages();
EXPECT_EQ(UINT64_MAX, cow->DebugGetSupplyZeroOffset());
// Resizing the clone should not alter its zero offset either.
status = clone->Resize(size * 2 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
EXPECT_EQ(UINT64_MAX, cow->DebugGetSupplyZeroOffset());
status = clone->Resize(size / 2 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
EXPECT_EQ(UINT64_MAX, cow->DebugGetSupplyZeroOffset());
cow.reset();
clone.reset();
// Zero offset is not applicable to non pager backed VMOs.
vmo.reset();
status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, VmObjectPaged::kResizable, PAGE_SIZE, &vmo);
ASSERT_EQ(ZX_OK, status);
EXPECT_EQ(UINT64_MAX, vmo->DebugGetCowPages()->DebugGetSupplyZeroOffset());
// Resizing a non pager backed VMO should not alter its zero offset.
status = vmo->Resize(2 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
EXPECT_EQ(UINT64_MAX, vmo->DebugGetCowPages()->DebugGetSupplyZeroOffset());
status = vmo->Resize(PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
EXPECT_EQ(UINT64_MAX, vmo->DebugGetCowPages()->DebugGetSupplyZeroOffset());
END_TEST;
}
static bool is_page_zero(vm_page_t* page) {
auto* base = reinterpret_cast<uint64_t*>(paddr_to_physmap(page->paddr()));
for (size_t i = 0; i < PAGE_SIZE / sizeof(uint64_t); i++) {
if (base[i] != 0)
return false;
}
return true;
}
// Tests that ZeroRange does not remove pinned pages. Regression test for fxbug.dev/101608.
static bool vmo_zero_pinned_test() {
BEGIN_TEST;
// Create a non pager-backed VMO.
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, PAGE_SIZE, &vmo);
ASSERT_EQ(ZX_OK, status);
// Pin the page.
status = vmo->CommitRangePinned(0, PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
// Write non-zero content to the page.
vm_page_t* page = vmo->DebugGetPage(0);
*reinterpret_cast<uint8_t*>(paddr_to_physmap(page->paddr())) = 0xff;
// Zero the page and check that it is not removed.
status = vmo->ZeroRange(0, PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
EXPECT_EQ(page, vmo->DebugGetPage(0));
// The page should be zero.
EXPECT_TRUE(is_page_zero(page));
vmo->Unpin(0, PAGE_SIZE);
// Create a pager-backed VMO.
fbl::RefPtr<VmObjectPaged> pager_vmo;
vm_page_t* old_page;
status =
make_committed_pager_vmo(1, /*trap_dirty=*/false, /*resizable=*/true, &old_page, &pager_vmo);
ASSERT_EQ(ZX_OK, status);
// Pin the page.
status = pager_vmo->CommitRangePinned(0, PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
// Write non-zero content to the page. Lookup the page again, as pinning might have switched out
// the page if it was originally loaned.
old_page = pager_vmo->DebugGetPage(0);
*reinterpret_cast<uint8_t*>(paddr_to_physmap(old_page->paddr())) = 0xff;
// Zero the page and check that it is not removed.
status = pager_vmo->ZeroRange(0, PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
EXPECT_EQ(old_page, pager_vmo->DebugGetPage(0));
// The page should be zero.
EXPECT_TRUE(is_page_zero(old_page));
// Resize the VMO up, and pin a page in the newly extended range.
status = pager_vmo->Resize(2 * PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
status = pager_vmo->CommitRangePinned(PAGE_SIZE, PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
// Write non-zero content to the page.
vm_page_t* new_page = pager_vmo->DebugGetPage(PAGE_SIZE);
*reinterpret_cast<uint8_t*>(paddr_to_physmap(new_page->paddr())) = 0xff;
// Zero the new page, and ensure that it is not removed.
status = pager_vmo->ZeroRange(PAGE_SIZE, PAGE_SIZE);
ASSERT_EQ(ZX_OK, status);
EXPECT_EQ(new_page, pager_vmo->DebugGetPage(PAGE_SIZE));
// The page should be zero.
EXPECT_TRUE(is_page_zero(new_page));
pager_vmo->Unpin(0, 2 * PAGE_SIZE);
END_TEST;
}
UNITTEST_START_TESTCASE(vmo_tests)
VM_UNITTEST(vmo_create_test)
VM_UNITTEST(vmo_create_maximum_size)
VM_UNITTEST(vmo_pin_test)
VM_UNITTEST(vmo_pin_contiguous_test)
VM_UNITTEST(vmo_multiple_pin_test)
VM_UNITTEST(vmo_multiple_pin_contiguous_test)
VM_UNITTEST(vmo_commit_test)
VM_UNITTEST(vmo_odd_size_commit_test)
VM_UNITTEST(vmo_create_physical_test)
VM_UNITTEST(vmo_physical_pin_test)
VM_UNITTEST(vmo_create_contiguous_test)
VM_UNITTEST(vmo_contiguous_decommit_test)
VM_UNITTEST(vmo_contiguous_decommit_disabled_test)
VM_UNITTEST(vmo_contiguous_decommit_enabled_test)
VM_UNITTEST(vmo_precommitted_map_test)
VM_UNITTEST(vmo_demand_paged_map_test)
VM_UNITTEST(vmo_dropped_ref_test)
VM_UNITTEST(vmo_remap_test)
VM_UNITTEST(vmo_double_remap_test)
VM_UNITTEST(vmo_read_write_smoke_test)
VM_UNITTEST(vmo_cache_test)
VM_UNITTEST(vmo_lookup_test)
VM_UNITTEST(vmo_lookup_slice_test)
VM_UNITTEST(vmo_lookup_clone_test)
VM_UNITTEST(vmo_clone_removes_write_test)
VM_UNITTEST(vmo_zero_scan_test)
VM_UNITTEST(vmo_move_pages_on_access_test)
VM_UNITTEST(vmo_eviction_hints_test)
VM_UNITTEST(vmo_always_need_evicts_loaned_test)
VM_UNITTEST(vmo_eviction_hints_clone_test)
VM_UNITTEST(vmo_eviction_test)
VM_UNITTEST(vmo_validate_page_splits_test)
VM_UNITTEST(vmo_attribution_clones_test)
VM_UNITTEST(vmo_attribution_ops_test)
VM_UNITTEST(vmo_attribution_ops_contiguous_test)
VM_UNITTEST(vmo_attribution_pager_test)
VM_UNITTEST(vmo_attribution_evict_test)
VM_UNITTEST(vmo_attribution_dedup_test)
VM_UNITTEST(vmo_parent_merge_test)
VM_UNITTEST(vmo_lock_count_test)
VM_UNITTEST(vmo_discardable_states_test)
VM_UNITTEST(vmo_discard_test)
VM_UNITTEST(vmo_discard_failure_test)
VM_UNITTEST(vmo_discardable_counts_test)
VM_UNITTEST(vmo_lookup_pages_test)
VM_UNITTEST(vmo_write_does_not_commit_test)
VM_UNITTEST(vmo_stack_owned_loaned_pages_interval_test)
VM_UNITTEST(vmo_dirty_pages_test)
VM_UNITTEST(vmo_dirty_pages_writeback_test)
VM_UNITTEST(vmo_dirty_pages_with_hints_test)
VM_UNITTEST(vmo_pinning_backlink_test)
VM_UNITTEST(vmo_supply_zero_offset_test)
VM_UNITTEST(vmo_zero_pinned_test)
UNITTEST_END_TESTCASE(vmo_tests, "vmo", "VmObject tests")
} // namespace vm_unittest