| // 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 <lib/page/size.h> |
| #include <pow2.h> |
| #include <zircon/errors.h> |
| |
| #include <arch/kernel_aspace.h> |
| #include <ktl/initializer_list.h> |
| #include <ktl/limits.h> |
| #include <vm/vm.h> |
| #include <vm/vm_address_region_enumerator.h> |
| |
| #include "test_helper.h" |
| |
| #include <ktl/enforce.h> |
| |
| namespace vm_unittest { |
| |
| struct KernelRegion { |
| const char* name; |
| vaddr_t base; |
| size_t size; |
| arch_mmu_flags_t arch_mmu_flags; |
| }; |
| |
| const ktl::array kernel_regions = { |
| KernelRegion{ |
| .name = "kernel_code", |
| .base = (vaddr_t)__code_start, |
| .size = RoundUpPageSize((uintptr_t)__code_end - (uintptr_t)__code_start), |
| .arch_mmu_flags = ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_EXECUTE, |
| }, |
| KernelRegion{ |
| .name = "kernel_rodata", |
| .base = (vaddr_t)__rodata_start, |
| .size = RoundUpPageSize((uintptr_t)__rodata_end - (uintptr_t)__rodata_start), |
| .arch_mmu_flags = ARCH_MMU_FLAG_PERM_READ, |
| }, |
| KernelRegion{ |
| .name = "kernel_relro", |
| .base = (vaddr_t)__relro_start, |
| .size = RoundUpPageSize((uintptr_t)__relro_end - (uintptr_t)__relro_start), |
| .arch_mmu_flags = ARCH_MMU_FLAG_PERM_READ, |
| }, |
| KernelRegion{ |
| .name = "kernel_data_bss", |
| .base = (vaddr_t)__data_start, |
| .size = RoundUpPageSize((uintptr_t)_end - (uintptr_t)__data_start), |
| .arch_mmu_flags = ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE, |
| }, |
| }; |
| |
| // Wrapper for harvesting access bits that informs the page queues |
| static void harvest_access_bits(VmAspace::NonTerminalAction non_terminal_action, |
| VmAspace::TerminalAction terminal_action) { |
| AutoVmScannerDisable scanner_disable; |
| VmAspace::HarvestAllUserAccessedBits(non_terminal_action, terminal_action); |
| } |
| |
| // Consume the (scalar) value, ensuring that the operation to calculate the value can not be |
| // optimized out/ deemed as unused by the compiler. I.e. this function can be used as a wrapper to a |
| // calculation to ensure it will be in the binary. |
| template <typename T> |
| void ConsumeValue(T value) { |
| // The compiler must materialize the value into a register, since it doesn't |
| // know that the register's value isn't actually used. |
| __asm__ volatile("" : : "r"(value)); |
| } |
| |
| // Allocates a contiguous region in kernel space, reads/writes it, |
| // then destroys it. |
| static bool vmm_alloc_contiguous_smoke_test() { |
| BEGIN_TEST; |
| static const size_t alloc_size = 256 * 1024; |
| |
| // allocate a region of memory |
| void* ptr; |
| auto kaspace = VmAspace::kernel_aspace(); |
| auto err = kaspace->AllocContiguous("test", alloc_size, &ptr, 0, VmAspace::VMM_FLAG_COMMIT, |
| kArchRwFlags); |
| ASSERT_EQ(ZX_OK, err, "VmAspace::AllocContiguous region of memory"); |
| ASSERT_NONNULL(ptr, "VmAspace::AllocContiguous region of memory"); |
| |
| // fill with known pattern and test |
| if (!fill_and_test(ptr, alloc_size)) { |
| all_ok = false; |
| } |
| |
| // test that it is indeed contiguous |
| unittest_printf("testing that region is contiguous\n"); |
| paddr_t last_pa = 0; |
| for (size_t i = 0; i < alloc_size / kPageSize; i++) { |
| paddr_t pa = vaddr_to_paddr((uint8_t*)ptr + i * kPageSize); |
| if (last_pa != 0) { |
| EXPECT_EQ(pa, last_pa + kPageSize, "region is contiguous"); |
| } |
| |
| last_pa = pa; |
| } |
| |
| // free the region |
| err = kaspace->FreeRegion(reinterpret_cast<vaddr_t>(ptr)); |
| EXPECT_EQ(ZX_OK, err, "VmAspace::FreeRegion region of memory"); |
| END_TEST; |
| } |
| |
| // Allocates a new address space and creates a few regions in it, |
| // then destroys it. |
| static bool multiple_regions_test() { |
| BEGIN_TEST; |
| |
| user_inout_ptr<void> ptr{nullptr}; |
| static const size_t alloc_size = 16 * 1024; |
| |
| fbl::RefPtr<VmAspace> aspace = VmAspace::Create(VmAspace::Type::User, "test aspace"); |
| ASSERT_NONNULL(aspace, "VmAspace::Create pointer"); |
| |
| VmAspace* old_aspace = Thread::Current::active_aspace(); |
| vmm_set_active_aspace(aspace.get()); |
| |
| // allocate region 0 |
| zx_status_t err = AllocUser(aspace.get(), "test0", alloc_size, &ptr); |
| ASSERT_EQ(ZX_OK, err, "VmAspace::Alloc region of memory"); |
| |
| // fill with known pattern and test |
| if (!fill_and_test_user(ptr, alloc_size)) { |
| all_ok = false; |
| } |
| |
| // allocate region 1 |
| err = AllocUser(aspace.get(), "test1", alloc_size, &ptr); |
| ASSERT_EQ(ZX_OK, err, "VmAspace::Alloc region of memory"); |
| |
| // fill with known pattern and test |
| if (!fill_and_test_user(ptr, alloc_size)) { |
| all_ok = false; |
| } |
| |
| // allocate region 2 |
| err = AllocUser(aspace.get(), "test2", alloc_size, &ptr); |
| ASSERT_EQ(ZX_OK, err, "VmAspace::Alloc region of memory"); |
| |
| // fill with known pattern and test |
| if (!fill_and_test_user(ptr, alloc_size)) { |
| all_ok = false; |
| } |
| |
| vmm_set_active_aspace(old_aspace); |
| |
| // free the address space all at once |
| err = aspace->Destroy(); |
| EXPECT_EQ(ZX_OK, err, "VmAspace::Destroy"); |
| END_TEST; |
| } |
| |
| static bool vmm_alloc_contiguous_missing_flag_commit_fails() { |
| BEGIN_TEST; |
| // should have VmAspace::VMM_FLAG_COMMIT |
| const uint zero_vmm_flags = 0; |
| void* ptr; |
| zx_status_t err = VmAspace::kernel_aspace()->AllocContiguous("test", 4096, &ptr, 0, |
| zero_vmm_flags, kArchRwFlags); |
| ASSERT_EQ(ZX_ERR_INVALID_ARGS, err); |
| END_TEST; |
| } |
| |
| static bool vmm_alloc_contiguous_zero_size_fails() { |
| BEGIN_TEST; |
| const size_t zero_size = 0; |
| void* ptr; |
| zx_status_t err = VmAspace::kernel_aspace()->AllocContiguous( |
| "test", zero_size, &ptr, 0, VmAspace::VMM_FLAG_COMMIT, kArchRwFlags); |
| ASSERT_EQ(ZX_ERR_INVALID_ARGS, err); |
| END_TEST; |
| } |
| |
| // Allocates a vm address space object directly, allows it to go out of scope. |
| static bool vmaspace_create_smoke_test() { |
| BEGIN_TEST; |
| auto aspace = VmAspace::Create(VmAspace::Type::User, "test aspace"); |
| zx_status_t err = aspace->Destroy(); |
| EXPECT_EQ(ZX_OK, err, "VmAspace::Destroy"); |
| END_TEST; |
| } |
| |
| static bool vmaspace_create_invalid_ranges() { |
| BEGIN_TEST; |
| |
| // These are defined in vm_aspace.cc. |
| #define GUEST_PHYSICAL_ASPACE_BASE 0UL |
| #define GUEST_PHYSICAL_ASPACE_SIZE (1UL << MMU_GUEST_SIZE_SHIFT) |
| |
| // Test when base < valid base. |
| EXPECT_NULL(VmAspace::Create(USER_ASPACE_BASE - 1, 4096, VmAspace::Type::User, "test", |
| VmAspace::ShareOpt::None)); |
| EXPECT_NULL(VmAspace::Create(KERNEL_ASPACE_BASE - 1, 4096, VmAspace::Type::Kernel, "test", |
| VmAspace::ShareOpt::None)); |
| EXPECT_NULL(VmAspace::Create(GUEST_PHYSICAL_ASPACE_BASE - 1, 4096, VmAspace::Type::GuestPhysical, |
| "test", VmAspace::ShareOpt::None)); |
| |
| // Test when base + size exceeds valid range. |
| EXPECT_NULL(VmAspace::Create(USER_ASPACE_BASE, USER_ASPACE_SIZE + 1, VmAspace::Type::User, "test", |
| VmAspace::ShareOpt::None)); |
| EXPECT_NULL(VmAspace::Create(KERNEL_ASPACE_BASE, KERNEL_ASPACE_SIZE + 1, VmAspace::Type::Kernel, |
| "test", VmAspace::ShareOpt::None)); |
| EXPECT_NULL(VmAspace::Create(GUEST_PHYSICAL_ASPACE_BASE, GUEST_PHYSICAL_ASPACE_SIZE + 1, |
| VmAspace::Type::GuestPhysical, "test", VmAspace::ShareOpt::None)); |
| |
| END_TEST; |
| } |
| |
| // Allocates a vm address space object directly, maps something on it, |
| // allows it to go out of scope. |
| static bool vmaspace_alloc_smoke_test() { |
| BEGIN_TEST; |
| auto aspace = VmAspace::Create(VmAspace::Type::User, "test aspace2"); |
| |
| user_inout_ptr<void> ptr{nullptr}; |
| auto err = AllocUser(aspace.get(), "test", kPageSize, &ptr); |
| ASSERT_EQ(ZX_OK, err, "allocating region\n"); |
| |
| // destroy the aspace, which should drop all the internal refs to it |
| err = aspace->Destroy(); |
| EXPECT_EQ(ZX_OK, err, "VmAspace::Destroy"); |
| |
| // drop the ref held by this pointer |
| aspace.reset(); |
| END_TEST; |
| } |
| |
| // Touch mappings in an aspace and ensure we can correctly harvest the accessed bits. |
| // This test takes an optional tag that is placed in the top byte of the address when performing a |
| // user_copy. |
| static bool vmaspace_accessed_test(uint8_t tag) { |
| BEGIN_TEST; |
| |
| AutoVmScannerDisable scanner_disable; |
| |
| // Create some memory we can map touch to test accessed tracking on. Needs to be created from |
| // user pager backed memory as harvesting is allowed to be limited to just that. |
| vm_page_t* page; |
| fbl::RefPtr<VmObjectPaged> vmo; |
| zx_status_t status = |
| make_committed_pager_vmo(1, /*trap_dirty=*/false, /*resizable=*/false, &page, &vmo); |
| ASSERT_EQ(ZX_OK, status); |
| auto mem = testing::UserMemory::Create(vmo, tag); |
| |
| ASSERT_EQ(ZX_OK, mem->CommitAndMap(kPageSize)); |
| |
| // Initial accessed state is undefined, so harvest it away. |
| harvest_access_bits(VmAspace::NonTerminalAction::Retain, |
| VmAspace::TerminalAction::UpdateAgeAndHarvest); |
| |
| // Grab the current queue for the page and then rotate the page queues. This means any future, |
| // correct, access harvesting should result in a new page queue. |
| uint8_t current_queue = page->object.get_page_queue_ref().load(); |
| pmm_page_queues()->RotateReclaimQueues(); |
| |
| // Read from the mapping to (hopefully) set the accessed bit. |
| ConsumeValue(mem->get<int>(0)); |
| // Harvest it to move it in the page queue. |
| harvest_access_bits(VmAspace::NonTerminalAction::Retain, |
| VmAspace::TerminalAction::UpdateAgeAndHarvest); |
| |
| EXPECT_NE(current_queue, page->object.get_page_queue_ref().load()); |
| current_queue = page->object.get_page_queue_ref().load(); |
| |
| // Rotating and harvesting again should not make the queue change since we have not accessed it. |
| pmm_page_queues()->RotateReclaimQueues(); |
| harvest_access_bits(VmAspace::NonTerminalAction::Retain, |
| VmAspace::TerminalAction::UpdateAgeAndHarvest); |
| EXPECT_EQ(current_queue, page->object.get_page_queue_ref().load()); |
| |
| // Set the accessed bit again, and make sure it does now harvest. |
| pmm_page_queues()->RotateReclaimQueues(); |
| ConsumeValue(mem->get<int>(0)); |
| harvest_access_bits(VmAspace::NonTerminalAction::Retain, |
| VmAspace::TerminalAction::UpdateAgeAndHarvest); |
| EXPECT_NE(current_queue, page->object.get_page_queue_ref().load()); |
| |
| // Set the accessed bit and update age without harvesting. |
| ConsumeValue(mem->get<int>(0)); |
| harvest_access_bits(VmAspace::NonTerminalAction::Retain, VmAspace::TerminalAction::UpdateAge); |
| current_queue = page->object.get_page_queue_ref().load(); |
| |
| // Now if we rotate and update again, we should re-age the page. |
| pmm_page_queues()->RotateReclaimQueues(); |
| harvest_access_bits(VmAspace::NonTerminalAction::Retain, VmAspace::TerminalAction::UpdateAge); |
| EXPECT_NE(current_queue, page->object.get_page_queue_ref().load()); |
| current_queue = page->object.get_page_queue_ref().load(); |
| pmm_page_queues()->RotateReclaimQueues(); |
| harvest_access_bits(VmAspace::NonTerminalAction::Retain, VmAspace::TerminalAction::UpdateAge); |
| EXPECT_NE(current_queue, page->object.get_page_queue_ref().load()); |
| |
| END_TEST; |
| } |
| |
| static bool vmaspace_accessed_test_untagged() { return vmaspace_accessed_test(0); } |
| |
| #if defined(__aarch64__) |
| // Rerun the `vmaspace_accessed_test` tests with tags in the top byte of user pointers. This tests |
| // that the subsequent accessed faults are handled successfully, even if the FAR contains a tag. |
| static bool vmaspace_accessed_test_tagged() { return vmaspace_accessed_test(0xAB); } |
| #endif |
| |
| // Ensure that if a user requested VMO read/write operation would hit a page that has had its |
| // accessed bits harvested that any resulting fault (on ARM) can be handled. |
| static bool vmaspace_usercopy_accessed_fault_test() { |
| BEGIN_TEST; |
| |
| AutoVmScannerDisable scanner_disable; |
| |
| // Create some memory we can map touch to test accessed tracking on. Needs to be created from |
| // user pager backed memory as harvesting is allowed to be limited to just that. |
| vm_page_t* page; |
| fbl::RefPtr<VmObjectPaged> mapping_vmo; |
| zx_status_t status = |
| make_committed_pager_vmo(1, /*trap_dirty=*/false, /*resizable=*/false, &page, &mapping_vmo); |
| ASSERT_EQ(ZX_OK, status); |
| auto mem = testing::UserMemory::Create(mapping_vmo); |
| |
| ASSERT_EQ(ZX_OK, mem->CommitAndMap(kPageSize)); |
| |
| // Need a separate VMO to read/write from. |
| fbl::RefPtr<VmObjectPaged> vmo; |
| status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, kPageSize, &vmo); |
| ASSERT_EQ(status, ZX_OK); |
| |
| // Touch the mapping to make sure it is committed and mapped. |
| mem->put<char>(42); |
| |
| // Harvest any accessed bits. |
| harvest_access_bits(VmAspace::NonTerminalAction::Retain, |
| VmAspace::TerminalAction::UpdateAgeAndHarvest); |
| |
| // Read from the VMO into the mapping that has been harvested. |
| auto [read_status, read_actual] = |
| vmo->ReadUser(mem->user_out<char>(), 0, sizeof(char), VmObjectReadWriteOptions::None); |
| ASSERT_EQ(read_status, ZX_OK); |
| ASSERT_EQ(read_actual, sizeof(char)); |
| |
| END_TEST; |
| } |
| |
| // Test that page tables that do not get accessed can be successfully unmapped and freed. |
| static bool vmaspace_free_unaccessed_page_tables_test() { |
| BEGIN_TEST; |
| |
| // Disable for RISC-V for now, since the ArchMmmu code for this architecture currently |
| // does not track accessed bits in intermediate page tables, and thus has no reasonable |
| // way to honor NonTerminalAction::FreeUnaccessed on harvest calls. |
| #if defined(__riscv) |
| printf("Skipping on RISC-V\n"); |
| return true; |
| #endif |
| |
| AutoVmScannerDisable scanner_disable; |
| |
| fbl::RefPtr<VmObjectPaged> vmo; |
| constexpr size_t kNumPages = 512 * 3; |
| constexpr size_t kMiddlePage = kNumPages / 2; |
| constexpr size_t kMiddleOffset = kMiddlePage * kPageSize; |
| ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, kPageSize * kNumPages, &vmo)); |
| |
| // Construct an additional aspace to use for mappings and touching pages. This allows us to |
| // control whether the aspace is considered active, which can effect reclamation and scanning. |
| fbl::RefPtr<VmAspace> aspace = VmAspace::Create(VmAspace::Type::User, "test-aspace"); |
| ASSERT_NONNULL(aspace); |
| |
| auto cleanup_aspace = fit::defer([&aspace]() { aspace->Destroy(); }); |
| |
| auto mem = testing::UserMemory::CreateInAspace(vmo, aspace); |
| |
| // Put the state we need to share in a struct so we can easily share it with the thread. |
| struct State { |
| testing::UserMemory* mem = nullptr; |
| AutounsignalEvent touch_event; |
| AutounsignalEvent complete_event; |
| ktl::atomic<bool> running = true; |
| } state; |
| state.mem = &*mem; |
| |
| // Spin up a kernel thread in the aspace we made. This thread will just continuously wait on an |
| // event, touching the mapping whenever it is signaled. |
| auto thread_body = [](void* arg) -> int { |
| State* state = static_cast<State*>(arg); |
| |
| while (state->running) { |
| state->touch_event.Wait(Deadline::infinite()); |
| // Check running again so we do not try and touch mem if attempting to shutdown suddenly. |
| if (state->running) { |
| state->mem->put<char>(42, kMiddleOffset); |
| // Signal the event back |
| state->complete_event.Signal(); |
| } |
| } |
| return 0; |
| }; |
| |
| Thread* thread = Thread::Create("test-thread", thread_body, &state, DEFAULT_PRIORITY); |
| ASSERT_NONNULL(thread); |
| aspace->AttachToThread(thread); |
| thread->Resume(); |
| |
| auto cleanup_thread = fit::defer([&state, thread]() { |
| state.running = false; |
| state.touch_event.Signal(); |
| thread->Join(nullptr, ZX_TIME_INFINITE); |
| }); |
| |
| // Helper to synchronously wait for the thread to perform a touch. |
| auto touch = [&state]() { |
| state.touch_event.Signal(); |
| state.complete_event.Wait(Deadline::infinite()); |
| }; |
| |
| EXPECT_OK(mem->CommitAndMap(kPageSize, kMiddleOffset)); |
| |
| // Touch the mapping to ensure its accessed. |
| touch(); |
| |
| // Attempting to map should fail, as it's already mapped. |
| EXPECT_EQ(ZX_ERR_ALREADY_EXISTS, mem->CommitAndMap(kPageSize, kMiddleOffset)); |
| |
| touch(); |
| // Harvest the accessed information, this should not actually unmap it, even if we ask it to. |
| harvest_access_bits(VmAspace::NonTerminalAction::FreeUnaccessed, |
| VmAspace::TerminalAction::UpdateAgeAndHarvest); |
| EXPECT_EQ(ZX_ERR_ALREADY_EXISTS, mem->CommitAndMap(kPageSize, kMiddleOffset)); |
| |
| touch(); |
| // Harvest the accessed information, then attempt to do it again so that it gets unmapped. |
| harvest_access_bits(VmAspace::NonTerminalAction::FreeUnaccessed, |
| VmAspace::TerminalAction::UpdateAgeAndHarvest); |
| harvest_access_bits(VmAspace::NonTerminalAction::FreeUnaccessed, |
| VmAspace::TerminalAction::UpdateAgeAndHarvest); |
| EXPECT_OK(mem->CommitAndMap(kPageSize, kMiddleOffset)); |
| |
| // Touch the mapping to ensure its accessed. |
| touch(); |
| |
| // Harvest the page accessed information, but retain the non-terminals. |
| harvest_access_bits(VmAspace::NonTerminalAction::Retain, |
| VmAspace::TerminalAction::UpdateAgeAndHarvest); |
| // We can do this a few times. |
| harvest_access_bits(VmAspace::NonTerminalAction::Retain, |
| VmAspace::TerminalAction::UpdateAgeAndHarvest); |
| harvest_access_bits(VmAspace::NonTerminalAction::Retain, |
| VmAspace::TerminalAction::UpdateAgeAndHarvest); |
| // Now if we attempt to free unaccessed the non-terminal should still be accessed and so nothing |
| // should get unmapped. |
| harvest_access_bits(VmAspace::NonTerminalAction::FreeUnaccessed, |
| VmAspace::TerminalAction::UpdateAgeAndHarvest); |
| EXPECT_EQ(ZX_ERR_ALREADY_EXISTS, mem->CommitAndMap(kPageSize, kMiddleOffset)); |
| |
| // If we are not requesting a free, then we should be able to harvest repeatedly. |
| EXPECT_EQ(ZX_ERR_ALREADY_EXISTS, mem->CommitAndMap(kPageSize, kMiddleOffset)); |
| harvest_access_bits(VmAspace::NonTerminalAction::Retain, |
| VmAspace::TerminalAction::UpdateAgeAndHarvest); |
| EXPECT_EQ(ZX_ERR_ALREADY_EXISTS, mem->CommitAndMap(kPageSize, kMiddleOffset)); |
| harvest_access_bits(VmAspace::NonTerminalAction::Retain, |
| VmAspace::TerminalAction::UpdateAgeAndHarvest); |
| EXPECT_EQ(ZX_ERR_ALREADY_EXISTS, mem->CommitAndMap(kPageSize, kMiddleOffset)); |
| harvest_access_bits(VmAspace::NonTerminalAction::Retain, |
| VmAspace::TerminalAction::UpdateAgeAndHarvest); |
| |
| END_TEST; |
| } |
| |
| // Touch mappings in both the shared and restricted region of a unified aspace and ensure we can |
| // correctly harvest accessed bits. |
| static bool vmaspace_unified_accessed_test() { |
| BEGIN_TEST; |
| |
| // Disable for RISC-V for now, since the ArchMmmu code for this architecture currently |
| // does not track accessed bits in intermediate page tables, and thus has no reasonable |
| // way to honor NonTerminalAction::FreeUnaccessed on harvest calls. |
| #if defined(__riscv) |
| printf("Skipping on RISC-V\n"); |
| return true; |
| #endif |
| |
| AutoVmScannerDisable scanner_disable; |
| |
| // Create a unified aspace. |
| constexpr vaddr_t kPrivateAspaceBase = USER_ASPACE_BASE; |
| constexpr vaddr_t kPrivateAspaceSize = USER_RESTRICTED_ASPACE_SIZE; |
| constexpr vaddr_t kSharedAspaceBase = kPrivateAspaceBase + kPrivateAspaceSize + kPageSize; |
| constexpr vaddr_t kSharedAspaceSize = USER_ASPACE_BASE + USER_ASPACE_SIZE - kSharedAspaceBase; |
| fbl::RefPtr<VmAspace> restricted_aspace = |
| VmAspace::Create(kPrivateAspaceBase, kPrivateAspaceSize, VmAspace::Type::User, |
| "test restricted aspace", VmAspace::ShareOpt::Restricted); |
| fbl::RefPtr<VmAspace> shared_aspace = |
| VmAspace::Create(kSharedAspaceBase, kSharedAspaceSize, VmAspace::Type::User, |
| "test shared aspace", VmAspace::ShareOpt::Shared); |
| fbl::RefPtr<VmAspace> unified_aspace = |
| VmAspace::CreateUnified(shared_aspace.get(), restricted_aspace.get(), "test unified aspace"); |
| auto cleanup_aspace = fit::defer([&restricted_aspace, &shared_aspace, &unified_aspace]() { |
| unified_aspace->Destroy(); |
| restricted_aspace->Destroy(); |
| shared_aspace->Destroy(); |
| }); |
| |
| // Create regions of user memory that we can touch in both the shared and restricted regions. |
| constexpr uint64_t kSize = 4 * kPageSize; |
| fbl::RefPtr<VmObjectPaged> shared_vmo; |
| fbl::RefPtr<VmObjectPaged> restricted_vmo; |
| ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, kSize, &shared_vmo)); |
| ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, kSize, &restricted_vmo)); |
| ktl::unique_ptr<testing::UserMemory> shared_mem = |
| testing::UserMemory::CreateInAspace(shared_vmo, shared_aspace); |
| ktl::unique_ptr<testing::UserMemory> restricted_mem = |
| testing::UserMemory::CreateInAspace(restricted_vmo, restricted_aspace); |
| |
| // Commit and map these regions to avoid page faults when we call `put` later on. We have to do |
| // this because the `put` function invokes a `copy_to_user` that may trigger a page fault, which |
| // the fault handler will try to resolve using the thread's current aspace. That aspace, in turn, |
| // will be the unified aspace, which cannot resolve faults. |
| constexpr uint64_t kMiddleOffset = kSize / 2; |
| EXPECT_OK(shared_mem->CommitAndMap(kPageSize, kMiddleOffset)); |
| EXPECT_OK(restricted_mem->CommitAndMap(kPageSize, kMiddleOffset)); |
| |
| // Switch to the unified aspace. |
| // NOTE: This test takes care to not trigger any page faults or accessed faults from this point |
| // on, because the unified aspace cannot resolve faults. For a user thread, we would rely on the |
| // process dispatcher to look up if the faulting address lies in the shared aspace or the |
| // restricted aspace, and use one of those to resolve the fault instead. We cannot exercise that |
| // behavior from within a kernel unit test. |
| VmAspace* old_aspace = Thread::Current::Get()->active_aspace(); |
| vmm_set_active_aspace(unified_aspace.get()); |
| auto reset_old_aspace = fit::defer([&old_aspace]() { vmm_set_active_aspace(old_aspace); }); |
| |
| #if defined __x86_64__ |
| // Touch the shared and restricted regions via the unified aspace. This will guarantee that |
| // the accessed bits are set on x86, where the hardware sets the accessed bits. |
| // On ARM, where we use software managed accessed bits, the CommitAndMap above will already have |
| // set them. |
| shared_mem->put<char>(42, kMiddleOffset); |
| restricted_mem->put<char>(42, kMiddleOffset); |
| #endif |
| |
| // Harvest the accessed information. This should not actually unmap the pages. |
| harvest_access_bits(VmAspace::NonTerminalAction::FreeUnaccessed, |
| VmAspace::TerminalAction::UpdateAgeAndHarvest); |
| EXPECT_EQ(ZX_ERR_ALREADY_EXISTS, shared_mem->CommitAndMap(kPageSize, kMiddleOffset)); |
| EXPECT_EQ(ZX_ERR_ALREADY_EXISTS, restricted_mem->CommitAndMap(kPageSize, kMiddleOffset)); |
| |
| #if defined __x86_64__ |
| // Touch the memory again so that the accessed bits are guaranteed to be set. We must do this |
| // because `CommitAndMap` does not set the accessed flag on x86. |
| // |
| // We specifically want to avoid doing this on ARM because the harvest will have cleared accessed |
| // bits, and this `put` now will trigger an accessed fault on the unified aspace, which cannot |
| // resolve any faults. Moreover, the CommitAndMap above will have already set the non-terminal |
| // accessed bits on the walk down to the page. |
| shared_mem->put<char>(43, kMiddleOffset); |
| restricted_mem->put<char>(43, kMiddleOffset); |
| #endif |
| |
| // Harvest the accessed information, then attempt to do it again so that it gets unmapped. |
| // The first `harvest_access_bits` call will clear the accessed bits, and the second will unmap |
| // the memory. |
| harvest_access_bits(VmAspace::NonTerminalAction::FreeUnaccessed, |
| VmAspace::TerminalAction::UpdateAgeAndHarvest); |
| harvest_access_bits(VmAspace::NonTerminalAction::FreeUnaccessed, |
| VmAspace::TerminalAction::UpdateAgeAndHarvest); |
| EXPECT_OK(shared_mem->CommitAndMap(kPageSize, kMiddleOffset)); |
| EXPECT_OK(restricted_mem->CommitAndMap(kPageSize, kMiddleOffset)); |
| |
| END_TEST; |
| } |
| |
| // Tests that VmMappings that are marked mergeable behave correctly. |
| static bool vmaspace_merge_mapping_test() { |
| BEGIN_TEST; |
| |
| fbl::RefPtr<VmAspace> aspace = VmAspace::Create(VmAspace::Type::User, "test aspace"); |
| |
| // Create a sub VMAR we'll use for all our testing. |
| fbl::RefPtr<VmAddressRegion> vmar; |
| ASSERT_OK(aspace->RootVmar()->CreateSubVmar( |
| 0, kPageSize * 64, 0, |
| VMAR_FLAG_CAN_MAP_SPECIFIC | VMAR_FLAG_CAN_MAP_READ | VMAR_FLAG_CAN_MAP_WRITE, "test vmar", |
| &vmar)); |
| |
| // Create two different vmos to make mappings into. |
| fbl::RefPtr<VmObjectPaged> vmo1; |
| ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, kPageSize * 4, &vmo1)); |
| fbl::RefPtr<VmObjectPaged> vmo2; |
| ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, kPageSize * 4, &vmo2)); |
| |
| // Declare some enums to make writing test cases more readable instead of having lots of bools. |
| enum MmuFlags { FLAG_TYPE_1, FLAG_TYPE_2 }; |
| enum MarkMerge { MERGE, NO_MERGE }; |
| enum MergeResult { MERGES_LEFT, DOES_NOT_MERGE }; |
| enum BeyondStreamSize { OK, FAULT }; |
| |
| // To avoid boilerplate declare some tests in a data driven way. |
| struct { |
| struct { |
| uint64_t vmar_offset; |
| fbl::RefPtr<VmObjectPaged> vmo; |
| uint64_t vmo_offset; |
| MmuFlags flags; |
| BeyondStreamSize beyond_stream_size; |
| MergeResult merge_result; |
| |
| } mappings[3]; |
| } cases[] = { |
| // Simple two mapping merge |
| {{{0, vmo1, 0, FLAG_TYPE_1, OK, DOES_NOT_MERGE}, |
| {kPageSize, vmo1, kPageSize, FLAG_TYPE_1, OK, MERGES_LEFT}, |
| {}}}, |
| // Simple three mapping merge |
| {{{0, vmo1, 0, FLAG_TYPE_1, OK, DOES_NOT_MERGE}, |
| {kPageSize, vmo1, kPageSize, FLAG_TYPE_1, OK, MERGES_LEFT}, |
| {kPageSize * 2, vmo1, kPageSize * 2, FLAG_TYPE_1, OK, MERGES_LEFT}}}, |
| // Different mapping flags should block merge |
| {{{0, vmo1, 0, FLAG_TYPE_2, OK, DOES_NOT_MERGE}, |
| {kPageSize, vmo1, kPageSize, FLAG_TYPE_1, OK, DOES_NOT_MERGE}, |
| {kPageSize * 2, vmo1, kPageSize * 2, FLAG_TYPE_1, OK, MERGES_LEFT}}}, |
| // Discontiguous aspace, but contiguous vmo should not work. |
| {{{0, vmo1, 0, FLAG_TYPE_1, OK, DOES_NOT_MERGE}, |
| {kPageSize * 2, vmo1, kPageSize, FLAG_TYPE_1, OK, DOES_NOT_MERGE}, |
| {}}}, |
| // Similar discontiguous vmo, but contiguous aspace should not work. |
| {{{0, vmo1, 0, FLAG_TYPE_1, OK, DOES_NOT_MERGE}, |
| {kPageSize, vmo1, kPageSize * 2, FLAG_TYPE_1, OK, DOES_NOT_MERGE}, |
| {}}}, |
| // Leaving a contiguous hole also does not work, mapping needs to actually join. |
| {{{0, vmo1, 0, FLAG_TYPE_1, OK, DOES_NOT_MERGE}, |
| {kPageSize * 2, vmo1, kPageSize * 2, FLAG_TYPE_1, OK, DOES_NOT_MERGE}, |
| {}}}, |
| // Different vmo should not work. |
| {{{0, vmo2, 0, FLAG_TYPE_1, OK, DOES_NOT_MERGE}, |
| {kPageSize, vmo1, kPageSize, FLAG_TYPE_1, OK, DOES_NOT_MERGE}, |
| {kPageSize * 2, vmo1, kPageSize * 2, FLAG_TYPE_1, OK, MERGES_LEFT}}}, |
| // Two fault-beyond-stream-size mapping merge |
| {{{0, vmo1, 0, FLAG_TYPE_1, FAULT, DOES_NOT_MERGE}, |
| {kPageSize, vmo1, kPageSize, FLAG_TYPE_1, FAULT, MERGES_LEFT}, |
| {}}}, |
| // Can't merge adjacent mappings if only one has fault-beyond-stream-size. |
| {{{0, vmo1, 0, FLAG_TYPE_1, FAULT, DOES_NOT_MERGE}, |
| {kPageSize, vmo1, kPageSize, FLAG_TYPE_1, OK, DOES_NOT_MERGE}, |
| {}}}, |
| |
| }; |
| |
| for (auto& test : cases) { |
| // Want to test all combinations of placing the mappings in subvmars, we just choose this by |
| // iterating all the binary representations of 3 digits. |
| for (int sub_vmar_comination = 0; sub_vmar_comination < 0b1000; sub_vmar_comination++) { |
| const int use_subvmar[3] = {BIT_SET(sub_vmar_comination, 0), BIT_SET(sub_vmar_comination, 1), |
| BIT_SET(sub_vmar_comination, 2)}; |
| // Iterate all orders of marking mergeable. For 3 mappings there are 6 possibilities. |
| for (int merge_order_combination = 0; merge_order_combination < 6; |
| merge_order_combination++) { |
| const bool even_merge = (merge_order_combination % 2) == 0; |
| const int first_merge = merge_order_combination / 2; |
| const int merge_order[3] = {first_merge, (first_merge + (even_merge ? 1 : 2)) % 3, |
| (first_merge + (even_merge ? 2 : 1)) % 3}; |
| |
| // Instantiate the requested mappings. |
| fbl::RefPtr<VmAddressRegion> vmars[3]; |
| fbl::RefPtr<VmMapping> mappings[3]; |
| MergeResult merge_result[3] = {DOES_NOT_MERGE, DOES_NOT_MERGE, DOES_NOT_MERGE}; |
| for (int i = 0; i < 3; i++) { |
| if (test.mappings[i].vmo) { |
| arch_mmu_flags_t mmu_flags = |
| ARCH_MMU_FLAG_PERM_READ | |
| (test.mappings[i].flags == FLAG_TYPE_1 ? ARCH_MMU_FLAG_PERM_WRITE : 0); |
| uint vmar_flags = VMAR_FLAG_SPECIFIC | (test.mappings[i].beyond_stream_size == FAULT |
| ? VMAR_FLAG_FAULT_BEYOND_STREAM_SIZE |
| : 0); |
| if (use_subvmar[i]) { |
| ASSERT_OK(vmar->CreateSubVmar(test.mappings[i].vmar_offset, kPageSize, 0, |
| VMAR_FLAG_SPECIFIC | VMAR_FLAG_CAN_MAP_SPECIFIC | |
| VMAR_FLAG_CAN_MAP_READ | VMAR_FLAG_CAN_MAP_WRITE, |
| "sub vmar", &vmars[i])); |
| auto map_result = |
| vmars[i]->CreateVmMapping(0, kPageSize, 0, vmar_flags, test.mappings[i].vmo, |
| test.mappings[i].vmo_offset, mmu_flags, "test mapping"); |
| ASSERT_OK(map_result.status_value()); |
| mappings[i] = ktl::move(map_result->mapping); |
| } else { |
| auto map_result = vmar->CreateVmMapping( |
| test.mappings[i].vmar_offset, kPageSize, 0, vmar_flags, test.mappings[i].vmo, |
| test.mappings[i].vmo_offset, mmu_flags, "test mapping"); |
| ASSERT_OK(map_result.status_value()); |
| mappings[i] = ktl::move(map_result->mapping); |
| } |
| } |
| // By default we assume merging happens as declared in the test, unless either this our |
| // immediate left is in a subvmar, in which case merging is blocked. |
| if (use_subvmar[i] || (i > 0 && use_subvmar[i - 1])) { |
| merge_result[i] = DOES_NOT_MERGE; |
| } else { |
| merge_result[i] = test.mappings[i].merge_result; |
| } |
| } |
| |
| // As we merge track expected mapping sizes and what we have merged |
| bool merged[3] = {false, false, false}; |
| size_t expected_size[3] = {kPageSize, kPageSize, kPageSize}; |
| // Mark each mapping as mergeable based on merge_order |
| for (const auto& mapping : merge_order) { |
| if (test.mappings[mapping].vmo) { |
| VmMapping::MarkMergeable(ktl::move(mappings[mapping])); |
| merged[mapping] = true; |
| // See if we have anything pending from the right |
| if (mapping < 2 && merged[mapping + 1] && merge_result[mapping + 1] == MERGES_LEFT) { |
| expected_size[mapping] += expected_size[mapping + 1]; |
| expected_size[mapping + 1] = 0; |
| } |
| // See if we should merge to the left. |
| if (merge_result[mapping] == MERGES_LEFT && mapping > 0 && merged[mapping - 1]) { |
| if (expected_size[mapping - 1] == 0) { |
| expected_size[mapping - 2] += expected_size[mapping]; |
| } else { |
| expected_size[mapping - 1] += expected_size[mapping]; |
| } |
| expected_size[mapping] = 0; |
| } |
| } |
| // Validate sizes to ensure any expected merging happened. |
| for (int j = 0; j < 3; j++) { |
| if (test.mappings[j].vmo) { |
| Guard<CriticalMutex> guard{vmar->lock()}; |
| VmMapping* map = vmar->FindMappingLocked(test.mappings[j].vmar_offset + vmar->base()); |
| ASSERT_NONNULL(map); |
| AssertHeld(map->lock_ref()); |
| if (expected_size[j] != 0) { |
| EXPECT_EQ(map->size(), expected_size[j]); |
| EXPECT_EQ(map->base(), vmar->base() + test.mappings[j].vmar_offset); |
| } |
| } |
| } |
| } |
| |
| // Destroy any mappings and VMARs. |
| for (int i = 0; i < 3; i++) { |
| if (test.mappings[i].vmo) { |
| EXPECT_OK(vmar->Unmap(vmar->base() + test.mappings[i].vmar_offset, kPageSize, |
| VmAddressRegionOpChildren::Yes)); |
| } |
| } |
| } |
| } |
| } |
| |
| // Cleanup the address space. |
| EXPECT_OK(vmar->Destroy()); |
| EXPECT_OK(aspace->Destroy()); |
| END_TEST; |
| } |
| |
| // Test that memory priority gets propagated through hierarchies and into newly created objects. |
| static bool vmaspace_priority_propagation_test() { |
| BEGIN_TEST; |
| |
| fbl::RefPtr<VmAspace> aspace = VmAspace::Create(VmAspace::Type::User, "test-aspace"); |
| ASSERT_NONNULL(aspace); |
| |
| // Create VMAR and a VMO and map it in. |
| fbl::RefPtr<VmAddressRegion> vmar; |
| ASSERT_OK(aspace->RootVmar()->CreateSubVmar( |
| 0, kPageSize * 64, 0, |
| VMAR_FLAG_CAN_MAP_SPECIFIC | VMAR_FLAG_CAN_MAP_READ | VMAR_FLAG_CAN_MAP_WRITE, "test vmar", |
| &vmar)); |
| |
| fbl::RefPtr<VmObjectPaged> vmo; |
| zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, kPageSize * 4, &vmo); |
| ASSERT_OK(status); |
| |
| auto mapping_result = |
| vmar->CreateVmMapping(0, kPageSize * 4, 0, 0, vmo, 0, kArchRwUserFlags, "test-mapping"); |
| ASSERT_OK(mapping_result.status_value()); |
| |
| // Set the priority in our vmar and validate it propagates to the VMO and the aspace. |
| status = vmar->SetMemoryPriority(VmAddressRegion::MemoryPriority::HIGH); |
| EXPECT_OK(status); |
| |
| EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(aspace->IsHighMemoryPriority()); |
| |
| // Create a new VMAR and VMO and map them into the high priority vmar. Memory priority should |
| // propagate. |
| fbl::RefPtr<VmAddressRegion> sub_vmar; |
| ASSERT_OK(vmar->CreateSubVmar( |
| 0, kPageSize * 16, 0, |
| VMAR_FLAG_CAN_MAP_SPECIFIC | VMAR_FLAG_CAN_MAP_READ | VMAR_FLAG_CAN_MAP_WRITE, |
| "test sub-vmar", &sub_vmar)); |
| |
| fbl::RefPtr<VmObjectPaged> vmo2; |
| status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, kPageSize * 4, &vmo2); |
| ASSERT_OK(status); |
| |
| auto mapping2_result = |
| sub_vmar->CreateVmMapping(0, kPageSize * 4, 0, 0, vmo2, 0, kArchRwUserFlags, "test-mapping"); |
| ASSERT_OK(mapping2_result.status_value()); |
| EXPECT_TRUE(vmo2->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| |
| // Change the priority of the sub vmar. It should not effect the original vmar / vmo priority. |
| status = sub_vmar->SetMemoryPriority(VmAddressRegion::MemoryPriority::DEFAULT); |
| EXPECT_OK(status); |
| EXPECT_FALSE(vmo2->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| |
| EXPECT_OK(vmar->Destroy()); |
| EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_OK(aspace->Destroy()); |
| |
| END_TEST; |
| } |
| |
| // Test that unmapping parts of a mapping preserves priority. |
| static bool vmaspace_priority_unmap_test() { |
| BEGIN_TEST; |
| |
| fbl::RefPtr<VmAspace> aspace = VmAspace::Create(VmAspace::Type::User, "test-aspace"); |
| ASSERT_NONNULL(aspace); |
| |
| // Create VMAR and a VMO and map it in. |
| fbl::RefPtr<VmAddressRegion> vmar; |
| ASSERT_OK(aspace->RootVmar()->CreateSubVmar( |
| 0, kPageSize * 64, 0, |
| VMAR_FLAG_CAN_MAP_SPECIFIC | VMAR_FLAG_CAN_MAP_READ | VMAR_FLAG_CAN_MAP_WRITE, "test vmar", |
| &vmar)); |
| |
| fbl::RefPtr<VmObjectPaged> vmo; |
| zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, kPageSize * 8, &vmo); |
| ASSERT_OK(status); |
| |
| auto mapping_result = |
| vmar->CreateVmMapping(0, kPageSize * 8, 0, 0, vmo, 0, kArchRwUserFlags, "test-mapping"); |
| ASSERT_OK(mapping_result.status_value()); |
| |
| // Set the priority in our vmar and validate it propagates to the VMO and the aspace. |
| status = vmar->SetMemoryPriority(VmAddressRegion::MemoryPriority::HIGH); |
| EXPECT_OK(status); |
| |
| EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(aspace->IsHighMemoryPriority()); |
| |
| const vaddr_t base = mapping_result->base; |
| |
| // Unmap one page from either end of the mapping, ensuring memory priority did not change. |
| EXPECT_OK(vmar->Unmap(base, kPageSize, VmAddressRegionOpChildren::No)); |
| EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(aspace->IsHighMemoryPriority()); |
| |
| EXPECT_OK(vmar->Unmap(base + kPageSize * 7, kPageSize, VmAddressRegionOpChildren::No)); |
| EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(aspace->IsHighMemoryPriority()); |
| |
| // Unmap a page from the middle. This will split this into two mappings. |
| EXPECT_OK(vmar->Unmap(base + kPageSize * 4, kPageSize, VmAddressRegionOpChildren::No)); |
| EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(aspace->IsHighMemoryPriority()); |
| // Now completely unmap one portion. This will destroy one of the mappings, but the VMO should |
| // still have priority from the other mapping that was previously split. |
| EXPECT_OK(vmar->Unmap(base + kPageSize, kPageSize * 3, VmAddressRegionOpChildren::No)); |
| EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(aspace->IsHighMemoryPriority()); |
| |
| // Unmapping the rest of the other portion should finally cause the priority to be removed. |
| EXPECT_OK(vmar->Unmap(base + kPageSize * 5, kPageSize * 2, VmAddressRegionOpChildren::No)); |
| EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(aspace->IsHighMemoryPriority()); |
| |
| EXPECT_OK(aspace->Destroy()); |
| |
| END_TEST; |
| } |
| |
| // Test that overwriting a mapping maintains priority counts. |
| static bool vmaspace_priority_mapping_overwrite_test() { |
| BEGIN_TEST; |
| |
| fbl::RefPtr<VmAspace> aspace = VmAspace::Create(VmAspace::Type::User, "test-aspace"); |
| ASSERT_NONNULL(aspace); |
| |
| // Create VMAR and a VMO and map it in. |
| fbl::RefPtr<VmAddressRegion> vmar; |
| ASSERT_OK(aspace->RootVmar()->CreateSubVmar( |
| 0, kPageSize * 64, 0, |
| VMAR_FLAG_CAN_MAP_SPECIFIC | VMAR_FLAG_CAN_MAP_READ | VMAR_FLAG_CAN_MAP_WRITE, "test vmar", |
| &vmar)); |
| |
| fbl::RefPtr<VmObjectPaged> vmo; |
| zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, kPageSize, &vmo); |
| ASSERT_OK(status); |
| |
| auto mapping_result = |
| vmar->CreateVmMapping(0, kPageSize, 0, 0, vmo, 0, kArchRwUserFlags, "test-mapping"); |
| ASSERT_OK(mapping_result.status_value()); |
| fbl::RefPtr<VmMapping> mapping = ktl::move(mapping_result->mapping); |
| |
| status = vmar->SetMemoryPriority(VmAddressRegion::MemoryPriority::HIGH); |
| EXPECT_OK(status); |
| |
| EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(aspace->IsHighMemoryPriority()); |
| |
| // Overwrite the mapping with a new one from a new VMO. |
| fbl::RefPtr<VmObjectPaged> vmo2; |
| status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, kPageSize, &vmo2); |
| ASSERT_OK(status); |
| |
| mapping_result = vmar->CreateVmMapping(mapping->base() - vmar->base(), mapping->size(), 0, |
| VMAR_FLAG_SPECIFIC_OVERWRITE, vmo2, 0, kArchRwUserFlags, |
| "test-mapping2"); |
| ASSERT_OK(mapping_result.status_value()); |
| |
| // Original VMO should have lost its priority, and the VMO for our new mapping should have gained. |
| EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(vmo2->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(aspace->IsHighMemoryPriority()); |
| |
| EXPECT_OK(aspace->Destroy()); |
| |
| END_TEST; |
| } |
| |
| static bool vmaspace_priority_merged_mapping_test() { |
| BEGIN_TEST; |
| |
| fbl::RefPtr<VmAspace> aspace = VmAspace::Create(VmAspace::Type::User, "test-aspace"); |
| ASSERT_NONNULL(aspace); |
| |
| fbl::RefPtr<VmAddressRegion> vmar; |
| ASSERT_OK(aspace->RootVmar()->CreateSubVmar( |
| 0, kPageSize * 64, 0, |
| VMAR_FLAG_CAN_MAP_SPECIFIC | VMAR_FLAG_CAN_MAP_READ | VMAR_FLAG_CAN_MAP_WRITE, "test vmar", |
| &vmar)); |
| |
| zx_status_t status = vmar->SetMemoryPriority(VmAddressRegion::MemoryPriority::HIGH); |
| EXPECT_OK(status); |
| |
| fbl::RefPtr<VmObjectPaged> vmo; |
| status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, kPageSize * 2, &vmo); |
| ASSERT_OK(status); |
| |
| // Create a mapping for the first page of the VMO, and mark it mergeable |
| auto mapping_result = vmar->CreateVmMapping(kPageSize, kPageSize, 0, VMAR_FLAG_SPECIFIC_OVERWRITE, |
| vmo, 0, kArchRwUserFlags, "test-mapping"); |
| ASSERT_OK(mapping_result.status_value()); |
| |
| EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(aspace->IsHighMemoryPriority()); |
| |
| VmMapping::MarkMergeable(ktl::move(mapping_result->mapping)); |
| |
| // Map in the second page. |
| mapping_result = vmar->CreateVmMapping(kPageSize * 2, kPageSize, 0, VMAR_FLAG_SPECIFIC_OVERWRITE, |
| vmo, kPageSize, kArchRwUserFlags, "test-mapping"); |
| ASSERT_OK(mapping_result.status_value()); |
| |
| VmMapping::MarkMergeable(ktl::move(mapping_result->mapping)); |
| |
| // Query the vmar, should have a single mapping of the combined size. |
| fbl::RefPtr<VmAddressRegionOrMapping> region = vmar->FindRegion(vmar->base() + kPageSize); |
| ASSERT(region); |
| fbl::RefPtr<VmMapping> map = region->as_vm_mapping(); |
| ASSERT(map); |
| EXPECT_EQ(static_cast<size_t>(kPageSize * 2u), map->size()); |
| |
| // Now destroy the mapping and check the VMO loses priority. |
| EXPECT_OK(map->Destroy()); |
| |
| EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(aspace->IsHighMemoryPriority()); |
| |
| EXPECT_OK(aspace->Destroy()); |
| |
| END_TEST; |
| } |
| |
| static bool vmaspace_priority_bidir_clone_test() { |
| BEGIN_TEST; |
| |
| fbl::RefPtr<VmAspace> aspace = VmAspace::Create(VmAspace::Type::User, "test-aspace"); |
| ASSERT_NONNULL(aspace); |
| |
| fbl::RefPtr<VmAddressRegion> vmar; |
| ASSERT_OK(aspace->RootVmar()->CreateSubVmar( |
| 0, kPageSize * 64, 0, |
| VMAR_FLAG_CAN_MAP_SPECIFIC | VMAR_FLAG_CAN_MAP_READ | VMAR_FLAG_CAN_MAP_WRITE, "test vmar", |
| &vmar)); |
| |
| zx_status_t status = vmar->SetMemoryPriority(VmAddressRegion::MemoryPriority::HIGH); |
| EXPECT_OK(status); |
| |
| fbl::RefPtr<VmObjectPaged> vmo; |
| status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, kPageSize * 2, &vmo); |
| ASSERT_OK(status); |
| |
| auto mapping_result = vmar->CreateVmMapping(kPageSize, kPageSize, 0, VMAR_FLAG_SPECIFIC_OVERWRITE, |
| vmo, 0, kArchRwUserFlags, "test-mapping"); |
| ASSERT_OK(mapping_result.status_value()); |
| |
| EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(aspace->IsHighMemoryPriority()); |
| |
| // Create a clone of the VMO. |
| fbl::RefPtr<VmObject> vmo_child; |
| status = vmo->CreateClone(Resizability::NonResizable, SnapshotType::Full, 0, kPageSize, true, |
| &vmo_child); |
| ASSERT_OK(status); |
| VmObjectPaged* childp = reinterpret_cast<VmObjectPaged*>(vmo_child.get()); |
| |
| // Child should not have priority. |
| EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_FALSE(childp->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| |
| // Destroying the clone should leave memory priority unchanged of the original. |
| vmo_child.reset(); |
| EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(aspace->IsHighMemoryPriority()); |
| |
| // Remove the mapping. |
| EXPECT_OK(mapping_result->mapping->Destroy()); |
| EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| |
| // Create a new clone of the VMO and map in the clone. |
| status = vmo->CreateClone(Resizability::NonResizable, SnapshotType::Full, 0, kPageSize, true, |
| &vmo_child); |
| ASSERT_OK(status); |
| childp = reinterpret_cast<VmObjectPaged*>(vmo_child.get()); |
| EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_FALSE(childp->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| mapping_result = vmar->CreateVmMapping(kPageSize, kPageSize, 0, VMAR_FLAG_SPECIFIC_OVERWRITE, |
| vmo_child, 0, kArchRwUserFlags, "test-mapping"); |
| ASSERT_OK(mapping_result.status_value()); |
| |
| EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(childp->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| |
| // Now destroy the parent VMO and ensure child retains priority. |
| vmo.reset(); |
| EXPECT_TRUE(childp->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| |
| EXPECT_OK(aspace->Destroy()); |
| EXPECT_FALSE(childp->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| |
| END_TEST; |
| } |
| |
| static bool vmaspace_priority_slice_test() { |
| BEGIN_TEST; |
| |
| fbl::RefPtr<VmAspace> aspace = VmAspace::Create(VmAspace::Type::User, "test-aspace"); |
| ASSERT_NONNULL(aspace); |
| |
| fbl::RefPtr<VmAddressRegion> vmar; |
| ASSERT_OK(aspace->RootVmar()->CreateSubVmar( |
| 0, kPageSize * 64, 0, |
| VMAR_FLAG_CAN_MAP_SPECIFIC | VMAR_FLAG_CAN_MAP_READ | VMAR_FLAG_CAN_MAP_WRITE, "test vmar", |
| &vmar)); |
| |
| zx_status_t status = vmar->SetMemoryPriority(VmAddressRegion::MemoryPriority::HIGH); |
| EXPECT_OK(status); |
| |
| fbl::RefPtr<VmObjectPaged> vmo; |
| status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, kPageSize * 2, &vmo); |
| ASSERT_OK(status); |
| |
| auto mapping_result = vmar->CreateVmMapping(kPageSize, kPageSize, 0, VMAR_FLAG_SPECIFIC_OVERWRITE, |
| vmo, 0, kArchRwUserFlags, "test-mapping"); |
| ASSERT_OK(mapping_result.status_value()); |
| |
| EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(aspace->IsHighMemoryPriority()); |
| |
| // Create a slice of the VMO. |
| fbl::RefPtr<VmObject> vmo_slice; |
| status = vmo->CreateChildSlice(0, kPageSize, true, &vmo_slice); |
| ASSERT_OK(status); |
| VmObjectPaged* slicep = reinterpret_cast<VmObjectPaged*>(vmo_slice.get()); |
| |
| // Slice inherits priority. |
| EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(slicep->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| |
| // Change priority of the VMAR should remove from the VMO. |
| EXPECT_OK(vmar->SetMemoryPriority(VmAddressRegion::MemoryPriority::DEFAULT)); |
| EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_FALSE(aspace->IsHighMemoryPriority()); |
| |
| // Re-enable priority and verify. |
| EXPECT_OK(vmar->SetMemoryPriority(VmAddressRegion::MemoryPriority::HIGH)); |
| EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(slicep->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(aspace->IsHighMemoryPriority()); |
| |
| // Destroy slice and unmap. |
| vmo_slice.reset(); |
| |
| EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| |
| EXPECT_OK(aspace->Destroy()); |
| EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| |
| END_TEST; |
| } |
| |
| static bool vmaspace_priority_pager_test() { |
| BEGIN_TEST; |
| |
| fbl::RefPtr<VmAspace> aspace = VmAspace::Create(VmAspace::Type::User, "test-aspace"); |
| ASSERT_NONNULL(aspace); |
| |
| fbl::RefPtr<VmAddressRegion> vmar; |
| ASSERT_OK(aspace->RootVmar()->CreateSubVmar( |
| 0, kPageSize * 64, 0, |
| VMAR_FLAG_CAN_MAP_SPECIFIC | VMAR_FLAG_CAN_MAP_READ | VMAR_FLAG_CAN_MAP_WRITE, "test vmar", |
| &vmar)); |
| |
| zx_status_t status = vmar->SetMemoryPriority(VmAddressRegion::MemoryPriority::HIGH); |
| EXPECT_OK(status); |
| |
| fbl::RefPtr<VmObjectPaged> vmo; |
| status = make_committed_pager_vmo(1, false, false, nullptr, &vmo); |
| ASSERT_OK(status); |
| |
| // Create a clone of the VMO. |
| fbl::RefPtr<VmObject> vmo_child; |
| status = vmo->CreateClone(Resizability::NonResizable, SnapshotType::OnWrite, 0, kPageSize, true, |
| &vmo_child); |
| ASSERT_OK(status); |
| VmObjectPaged* childp = reinterpret_cast<VmObjectPaged*>(vmo_child.get()); |
| |
| // Map in the clone. |
| auto mapping_result = vmar->CreateVmMapping(kPageSize, kPageSize, 0, VMAR_FLAG_SPECIFIC_OVERWRITE, |
| vmo_child, 0, kArchRwUserFlags, "test-mapping"); |
| ASSERT_OK(mapping_result.status_value()); |
| |
| // Validate the root and clone received the priority. |
| EXPECT_TRUE(childp->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| |
| // Create a second child of the root. |
| fbl::RefPtr<VmObject> vmo_child2; |
| status = vmo->CreateClone(Resizability::NonResizable, SnapshotType::OnWrite, 0, kPageSize, true, |
| &vmo_child); |
| ASSERT_OK(status); |
| VmObjectPaged* childp2 = reinterpret_cast<VmObjectPaged*>(vmo_child.get()); |
| |
| // This child should not have any priority. |
| EXPECT_FALSE(childp2->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| |
| // Destroy it should leave the rest of the tree unchanged. |
| vmo_child2.reset(); |
| EXPECT_TRUE(childp->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| |
| // Remove priority and validate. |
| EXPECT_OK(vmar->SetMemoryPriority(VmAddressRegion::MemoryPriority::DEFAULT)); |
| |
| EXPECT_FALSE(childp->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| |
| EXPECT_OK(aspace->Destroy()); |
| |
| END_TEST; |
| } |
| |
| static bool vmaspace_priority_reference_test() { |
| BEGIN_TEST; |
| |
| fbl::RefPtr<VmAspace> aspace = VmAspace::Create(VmAspace::Type::User, "test-aspace"); |
| ASSERT_NONNULL(aspace); |
| |
| fbl::RefPtr<VmAddressRegion> vmar; |
| ASSERT_OK(aspace->RootVmar()->CreateSubVmar( |
| 0, kPageSize * 64, 0, |
| VMAR_FLAG_CAN_MAP_SPECIFIC | VMAR_FLAG_CAN_MAP_READ | VMAR_FLAG_CAN_MAP_WRITE, "test vmar", |
| &vmar)); |
| |
| zx_status_t status = vmar->SetMemoryPriority(VmAddressRegion::MemoryPriority::HIGH); |
| EXPECT_OK(status); |
| |
| fbl::RefPtr<VmObjectPaged> vmo; |
| status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, kPageSize * 2, &vmo); |
| ASSERT_OK(status); |
| |
| auto mapping_result = vmar->CreateVmMapping(kPageSize, kPageSize, 0, VMAR_FLAG_SPECIFIC_OVERWRITE, |
| vmo, 0, kArchRwUserFlags, "test-mapping"); |
| ASSERT_OK(mapping_result.status_value()); |
| |
| EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(aspace->IsHighMemoryPriority()); |
| |
| // Create a reference of the VMO. |
| fbl::RefPtr<VmObject> vmo_reference; |
| status = |
| vmo->CreateChildReference(Resizability::NonResizable, 0, 0, true, nullptr, &vmo_reference); |
| ASSERT_OK(status); |
| VmObjectPaged* refp = reinterpret_cast<VmObjectPaged*>(vmo_reference.get()); |
| |
| // Reference should have same priority. |
| EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(refp->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| |
| // Remove the original mapping. |
| mapping_result->mapping->Destroy(); |
| EXPECT_FALSE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_FALSE(refp->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| |
| // Now map in the reference. |
| mapping_result = vmar->CreateVmMapping(kPageSize, kPageSize, 0, VMAR_FLAG_SPECIFIC_OVERWRITE, |
| vmo_reference, 0, kArchRwUserFlags, "test-mapping"); |
| ASSERT_OK(mapping_result.status_value()); |
| |
| // Reference and vmo should have same priority. |
| EXPECT_TRUE(vmo->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| EXPECT_TRUE(refp->DebugGetCowPages()->DebugIsHighMemoryPriority()); |
| |
| EXPECT_OK(aspace->Destroy()); |
| |
| END_TEST; |
| } |
| |
| // Tests that memory attribution works as expected in a nested aspace hierarchy. |
| static bool vmaspace_nested_attribution_test() { |
| BEGIN_TEST; |
| |
| AutoVmScannerDisable scanner_disable; |
| |
| fbl::RefPtr<VmAspace> aspace = VmAspace::Create(VmAspace::Type::User, "test-aspace"); |
| ASSERT_NONNULL(aspace); |
| |
| // 8 page vmar. |
| fbl::RefPtr<VmAddressRegion> vmar; |
| ASSERT_OK(aspace->RootVmar()->CreateSubVmar( |
| 0, kPageSize * 8, 0, |
| VMAR_FLAG_CAN_MAP_SPECIFIC | VMAR_FLAG_CAN_MAP_READ | VMAR_FLAG_CAN_MAP_WRITE, "test vmar", |
| &vmar)); |
| |
| // Child vmar that covers the first 4 pages of the previous vmar. |
| fbl::RefPtr<VmAddressRegion> subvmar1; |
| ASSERT_OK(vmar->CreateSubVmar( |
| 0, kPageSize * 4, 0, |
| VMAR_FLAG_CAN_MAP_SPECIFIC | VMAR_FLAG_CAN_MAP_READ | VMAR_FLAG_CAN_MAP_WRITE, "test vmar", |
| &subvmar1)); |
| |
| // Grandchild vmar that covers the first 2 pages of the child. |
| fbl::RefPtr<VmAddressRegion> subvmar2; |
| ASSERT_OK(subvmar1->CreateSubVmar( |
| 0, kPageSize * 2, 0, |
| VMAR_FLAG_CAN_MAP_SPECIFIC | VMAR_FLAG_CAN_MAP_READ | VMAR_FLAG_CAN_MAP_WRITE, "test vmar", |
| &subvmar2)); |
| |
| // Make 2 page vmo. |
| fbl::RefPtr<VmObjectPaged> vmo; |
| zx_status_t status = |
| VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, VmObjectPaged::kResizable, 2 * kPageSize, &vmo); |
| ASSERT_EQ(ZX_OK, status); |
| |
| // Map VMO to grandchild. |
| EXPECT_EQ(aspace->is_user(), true); |
| auto mapping_result = |
| subvmar2->CreateVmMapping(0, 2 * kPageSize, 0, 0, vmo, 0, kArchRwUserFlags, "test-mapping"); |
| EXPECT_EQ(ZX_OK, mapping_result.status_value()); |
| fbl::RefPtr<VmMapping> mapping = ktl::move(mapping_result->mapping); |
| |
| // Commit 2 pages into mapping. |
| status = vmo->CommitRange(0, 2 * kPageSize); |
| ASSERT_EQ(ZX_OK, status); |
| |
| // Verify that the two pages are counted for the parent vmar chain. |
| ASSERT_TRUE(make_private_attribution_counts(2ul * kPageSize, 0) == |
| mapping->GetAttributedMemory()); |
| ASSERT_TRUE(make_private_attribution_counts(2ul * kPageSize, 0) == |
| subvmar2->GetAttributedMemory()); |
| ASSERT_TRUE(make_private_attribution_counts(2ul * kPageSize, 0) == |
| subvmar1->GetAttributedMemory()); |
| ASSERT_TRUE(make_private_attribution_counts(2ul * kPageSize, 0) == vmar->GetAttributedMemory()); |
| |
| END_TEST; |
| } |
| |
| // Tests that memory attribution at the VmMapping layer behaves as expected under commits and |
| // decommits on the vmo range. |
| static bool vm_mapping_attribution_commit_decommit_test() { |
| BEGIN_TEST; |
| AutoVmScannerDisable scanner_disable; |
| |
| using AttributionCounts = VmObject::AttributionCounts; |
| // Create a test VmAspace to temporarily switch to for creating test mappings. |
| fbl::RefPtr<VmAspace> aspace = VmAspace::Create(VmAspace::Type::User, "test-aspace"); |
| ASSERT_NONNULL(aspace); |
| |
| // Create a VMO to map. |
| fbl::RefPtr<VmObjectPaged> vmo; |
| zx_status_t status = |
| VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, VmObjectPaged::kResizable, 16 * kPageSize, &vmo); |
| ASSERT_EQ(ZX_OK, status); |
| |
| EXPECT_TRUE(vmo->GetAttributedMemory() == AttributionCounts{}); |
| EXPECT_TRUE(verify_continuous_attribution_bytes(*vmo, 0)); |
| |
| // Map the left half of the VMO. |
| EXPECT_EQ(aspace->is_user(), true); |
| auto mapping_result = aspace->RootVmar()->CreateVmMapping(0, 8 * kPageSize, 0, 0, vmo, 0, |
| kArchRwUserFlags, "test-mapping"); |
| EXPECT_EQ(ZX_OK, mapping_result.status_value()); |
| fbl::RefPtr<VmMapping> mapping = ktl::move(mapping_result->mapping); |
| |
| EXPECT_TRUE(vmo->GetAttributedMemory() == AttributionCounts{}); |
| EXPECT_TRUE(verify_continuous_attribution_bytes(*vmo, 0)); |
| EXPECT_TRUE(mapping->GetAttributedMemory() == AttributionCounts{}); |
| |
| // Commit pages a little into the mapping, and past it. |
| status = vmo->CommitRange(4 * kPageSize, 8 * kPageSize); |
| ASSERT_EQ(ZX_OK, status); |
| EXPECT_TRUE(vmo->GetAttributedMemory() == make_private_attribution_counts(8ul * kPageSize, 0)); |
| EXPECT_TRUE(mapping->GetAttributedMemory() == |
| make_private_attribution_counts(4ul * kPageSize, 0)); |
| |
| // Decommit the pages committed above, returning the VMO to zero committed pages. |
| status = vmo->DecommitRange(4 * kPageSize, 8 * kPageSize); |
| ASSERT_EQ(ZX_OK, status); |
| EXPECT_TRUE(vmo->GetAttributedMemory() == AttributionCounts{}); |
| EXPECT_TRUE(verify_continuous_attribution_bytes(*vmo, 0)); |
| EXPECT_TRUE(mapping->GetAttributedMemory() == AttributionCounts{}); |
| |
| // Commit some pages in the VMO again. |
| status = vmo->CommitRange(0, 10 * kPageSize); |
| ASSERT_EQ(ZX_OK, status); |
| EXPECT_TRUE(vmo->GetAttributedMemory() == make_private_attribution_counts(10ul * kPageSize, 0)); |
| EXPECT_TRUE(verify_continuous_attribution_bytes(*vmo, 10ul * kPageSize)); |
| EXPECT_TRUE(mapping->GetAttributedMemory() == |
| make_private_attribution_counts(8ul * kPageSize, 0)); |
| |
| // Decommit pages in the vmo via the mapping. |
| status = mapping->DecommitRange(0, mapping->size()); |
| ASSERT_EQ(ZX_OK, status); |
| EXPECT_TRUE(vmo->GetAttributedMemory() == make_private_attribution_counts(2ul * kPageSize, 0)); |
| EXPECT_TRUE(verify_continuous_attribution_bytes(*vmo, 2ul * kPageSize)); |
| EXPECT_TRUE(mapping->GetAttributedMemory() == AttributionCounts{}); |
| |
| // Destroy the mapping. |
| status = mapping->Destroy(); |
| ASSERT_EQ(ZX_OK, status); |
| EXPECT_TRUE(vmo->GetAttributedMemory() == make_private_attribution_counts(2ul * kPageSize, 0)); |
| EXPECT_TRUE(verify_continuous_attribution_bytes(*vmo, 2ul * kPageSize)); |
| EXPECT_TRUE((vm::AttributionCounts{}) == mapping->GetAttributedMemory()); |
| |
| // Free the test address space. |
| status = aspace->Destroy(); |
| EXPECT_EQ(ZX_OK, status); |
| |
| END_TEST; |
| } |
| |
| // Tests that memory attribution at the VmMapping layer behaves as expected under map and unmap |
| // operations on the mapping. |
| static bool vm_mapping_attribution_map_unmap_test() { |
| BEGIN_TEST; |
| AutoVmScannerDisable scanner_disable; |
| |
| using AttributionCounts = VmObject::AttributionCounts; |
| // Create a test VmAspace to temporarily switch to for creating test mappings. |
| fbl::RefPtr<VmAspace> aspace = VmAspace::Create(VmAspace::Type::User, "test-aspace"); |
| ASSERT_NONNULL(aspace); |
| |
| // Create a VMO to map. |
| fbl::RefPtr<VmObjectPaged> vmo; |
| zx_status_t status = |
| VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, VmObjectPaged::kResizable, 16 * kPageSize, &vmo); |
| ASSERT_EQ(ZX_OK, status); |
| |
| EXPECT_TRUE(vmo->GetAttributedMemory() == AttributionCounts{}); |
| EXPECT_TRUE(verify_continuous_attribution_bytes(*vmo, 0)); |
| |
| // Map the left half of the VMO. |
| EXPECT_EQ(aspace->is_user(), true); |
| auto mapping_result = aspace->RootVmar()->CreateVmMapping(0, 8 * kPageSize, 0, 0, vmo, 0, |
| kArchRwUserFlags, "test-mapping"); |
| EXPECT_EQ(ZX_OK, mapping_result.status_value()); |
| fbl::RefPtr<VmMapping> mapping = ktl::move(mapping_result->mapping); |
| |
| EXPECT_TRUE(vmo->GetAttributedMemory() == AttributionCounts{}); |
| EXPECT_TRUE(verify_continuous_attribution_bytes(*vmo, 0)); |
| EXPECT_TRUE(mapping->GetAttributedMemory() == AttributionCounts{}); |
| |
| // Commit pages in the vmo via the mapping. |
| status = mapping->MapRange(0, mapping->size(), true); |
| ASSERT_EQ(ZX_OK, status); |
| EXPECT_TRUE(vmo->GetAttributedMemory() == make_private_attribution_counts(8ul * kPageSize, 0)); |
| EXPECT_TRUE(verify_continuous_attribution_bytes(*vmo, 8ul * kPageSize)); |
| EXPECT_TRUE(mapping->GetAttributedMemory() == |
| make_private_attribution_counts(8ul * kPageSize, 0)); |
| |
| // Unmap from the right end of the mapping. |
| auto old_base = mapping->base(); |
| status = mapping->DebugUnmap(mapping->base() + mapping->size() - kPageSize, kPageSize); |
| ASSERT_EQ(ZX_OK, status); |
| mapping = aspace->FindRegion(old_base)->as_vm_mapping(); |
| ASSERT_TRUE(mapping); |
| EXPECT_EQ(old_base, mapping->base()); |
| EXPECT_EQ(7ul * kPageSize, mapping->size()); |
| EXPECT_TRUE(vmo->GetAttributedMemory() == make_private_attribution_counts(8ul * kPageSize, 0)); |
| EXPECT_TRUE(verify_continuous_attribution_bytes(*vmo, 8ul * kPageSize)); |
| EXPECT_TRUE(mapping->GetAttributedMemory() == |
| make_private_attribution_counts(7ul * kPageSize, 0)); |
| |
| // Unmap from the center of the mapping. |
| status = mapping->DebugUnmap(mapping->base() + 4 * kPageSize, kPageSize); |
| ASSERT_EQ(ZX_OK, status); |
| mapping = aspace->FindRegion(old_base)->as_vm_mapping(); |
| ASSERT_TRUE(mapping); |
| EXPECT_EQ(old_base, mapping->base()); |
| EXPECT_EQ(4ul * kPageSize, mapping->size()); |
| EXPECT_TRUE(vmo->GetAttributedMemory() == make_private_attribution_counts(8ul * kPageSize, 0)); |
| EXPECT_TRUE(verify_continuous_attribution_bytes(*vmo, 8ul * kPageSize)); |
| EXPECT_TRUE(mapping->GetAttributedMemory() == |
| make_private_attribution_counts(4ul * kPageSize, 0)); |
| |
| // Unmap from the left end of the mapping. |
| status = mapping->DebugUnmap(mapping->base(), kPageSize); |
| ASSERT_EQ(ZX_OK, status); |
| mapping = aspace->FindRegion(old_base + kPageSize)->as_vm_mapping(); |
| ASSERT_TRUE(mapping); |
| EXPECT_NE(old_base, mapping->base()); |
| EXPECT_EQ(3ul * kPageSize, mapping->size()); |
| EXPECT_TRUE(vmo->GetAttributedMemory() == make_private_attribution_counts(8ul * kPageSize, 0)); |
| EXPECT_TRUE(verify_continuous_attribution_bytes(*vmo, 8ul * kPageSize)); |
| EXPECT_TRUE(mapping->GetAttributedMemory() == |
| make_private_attribution_counts(3ul * kPageSize, 0)); |
| |
| // Free the test address space. |
| status = aspace->Destroy(); |
| EXPECT_EQ(ZX_OK, status); |
| |
| END_TEST; |
| } |
| |
| // Tests that memory attribution at the VmMapping layer behaves as expected when adjacent mappings |
| // are merged. |
| static bool vm_mapping_attribution_merge_test() { |
| BEGIN_TEST; |
| AutoVmScannerDisable scanner_disable; |
| |
| using AttributionCounts = VmObject::AttributionCounts; |
| // Create a test VmAspace to temporarily switch to for creating test mappings. |
| fbl::RefPtr<VmAspace> aspace = VmAspace::Create(VmAspace::Type::User, "test-aspace"); |
| ASSERT_NONNULL(aspace); |
| EXPECT_EQ(aspace->is_user(), true); |
| |
| // Create a VMO to map. |
| fbl::RefPtr<VmObjectPaged> vmo; |
| zx_status_t status = |
| VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, VmObjectPaged::kResizable, 16 * kPageSize, &vmo); |
| ASSERT_EQ(ZX_OK, status); |
| |
| EXPECT_TRUE(vmo->GetAttributedMemory() == AttributionCounts{}); |
| EXPECT_TRUE(verify_continuous_attribution_bytes(*vmo, 0)); |
| |
| // Create some contiguous mappings, marked unmergeable (default behavior) to begin with. |
| struct { |
| fbl::RefPtr<VmMapping> ref = nullptr; |
| AttributionCounts expected_attribution_counts; |
| } mappings[4]; |
| |
| uint64_t offset = 0; |
| static constexpr uint64_t kSize = 4 * kPageSize; |
| for (int i = 0; i < 4; i++) { |
| auto mapping_result = aspace->RootVmar()->CreateVmMapping( |
| offset, kSize, 0, VMAR_FLAG_SPECIFIC, vmo, offset, kArchRwUserFlags, "test-mapping"); |
| ASSERT_EQ(ZX_OK, mapping_result.status_value()); |
| mappings[i].ref = ktl::move(mapping_result->mapping); |
| EXPECT_TRUE(vmo->GetAttributedMemory() == AttributionCounts{}); |
| EXPECT_TRUE(verify_continuous_attribution_bytes(*vmo, 0)); |
| EXPECT_TRUE(mappings[i].ref->GetAttributedMemory() == mappings[i].expected_attribution_counts); |
| offset += kSize; |
| } |
| EXPECT_EQ(offset, 16ul * kPageSize); |
| |
| // Commit pages in the VMO. |
| status = vmo->CommitRange(0, 16 * kPageSize); |
| ASSERT_EQ(ZX_OK, status); |
| for (int i = 0; i < 4; i++) { |
| mappings[i].expected_attribution_counts = make_private_attribution_counts(4ul * kPageSize, 0); |
| EXPECT_TRUE(vmo->GetAttributedMemory() == make_private_attribution_counts(16ul * kPageSize, 0)); |
| EXPECT_TRUE(mappings[i].ref->GetAttributedMemory() == mappings[i].expected_attribution_counts); |
| } |
| |
| // Mark mappings 0 and 2 mergeable. This should not change anything since they're separated by an |
| // unmergeable mapping. |
| VmMapping::MarkMergeable(ktl::move(mappings[0].ref)); |
| VmMapping::MarkMergeable(ktl::move(mappings[2].ref)); |
| for (int i = 0; i < 4; i++) { |
| EXPECT_TRUE(vmo->GetAttributedMemory() == make_private_attribution_counts(16ul * kPageSize, 0)); |
| fbl::RefPtr<VmMapping> map = |
| aspace->FindRegion(aspace->RootVmar()->base() + kSize * i)->as_vm_mapping(); |
| ASSERT_TRUE(map); |
| EXPECT_TRUE(map->GetAttributedMemory() == mappings[i].expected_attribution_counts); |
| } |
| |
| // Mark mapping 3 mergeable. This will merge mappings 2 and 3, destroying mapping 3 and moving all |
| // of its pages into mapping 2. |
| VmMapping::MarkMergeable(ktl::move(mappings[3].ref)); |
| mappings[2].expected_attribution_counts += mappings[3].expected_attribution_counts; |
| for (int i = 0; i < 3; i++) { |
| EXPECT_TRUE(vmo->GetAttributedMemory() == make_private_attribution_counts(16ul * kPageSize, 0)); |
| fbl::RefPtr<VmMapping> map = |
| aspace->FindRegion(aspace->RootVmar()->base() + kSize * i)->as_vm_mapping(); |
| ASSERT_TRUE(map); |
| EXPECT_TRUE(map->GetAttributedMemory() == mappings[i].expected_attribution_counts); |
| } |
| |
| // Mark mapping 1 mergeable. This will merge mappings 0, 1 and 2, with only mapping 0 surviving |
| // the merge. All the VMO's pages will have been moved to mapping 0. |
| VmMapping::MarkMergeable(ktl::move(mappings[1].ref)); |
| mappings[0].expected_attribution_counts += mappings[1].expected_attribution_counts; |
| mappings[0].expected_attribution_counts += mappings[2].expected_attribution_counts; |
| EXPECT_TRUE(vmo->GetAttributedMemory() == make_private_attribution_counts(16ul * kPageSize, 0)); |
| fbl::RefPtr<VmMapping> map = aspace->FindRegion(aspace->RootVmar()->base())->as_vm_mapping(); |
| ASSERT_TRUE(map); |
| EXPECT_TRUE(map->GetAttributedMemory() == mappings[0].expected_attribution_counts); |
| |
| // Free the test address space. |
| status = aspace->Destroy(); |
| EXPECT_EQ(ZX_OK, status); |
| |
| END_TEST; |
| } |
| |
| static bool vm_mapping_sparse_mapping_test() { |
| BEGIN_TEST; |
| |
| AutoVmScannerDisable scanner_disable; |
| |
| // Create a large memory mapping with an empty backing VMO. Although this is a large virtual |
| // address range, our later attempts to map it should be efficient. |
| const size_t kMemorySize = 16 * GB; |
| auto memory = testing::UserMemory::Create(kMemorySize); |
| |
| // Memory backing the user memory is currently empty, so attempting to map in it should succeed, |
| // albeit with nothing populated. |
| EXPECT_OK(memory->MapExisting(kMemorySize)); |
| |
| // Commit a page in the middle, then re-map the whole thing and ensure the mapping is there. |
| uint64_t val = 42; |
| EXPECT_OK(memory->VmoWrite(&val, kMemorySize / 2, sizeof(val))); |
| EXPECT_OK(memory->MapExisting(kMemorySize)); |
| EXPECT_EQ(val, memory->get<uint64_t>(kMemorySize / 2 / sizeof(uint64_t))); |
| |
| // Do the same test, but this time with the pages at the start and end of the range. |
| EXPECT_OK(memory->VmoWrite(&val, 0, sizeof(val))); |
| EXPECT_OK(memory->VmoWrite(&val, kMemorySize - kPageSize, sizeof(val))); |
| EXPECT_OK(memory->MapExisting(kMemorySize)); |
| EXPECT_EQ(val, memory->get<uint64_t>(0)); |
| EXPECT_EQ(val, memory->get<uint64_t>((kMemorySize - kPageSize) / sizeof(uint64_t))); |
| |
| END_TEST; |
| } |
| |
| static bool vm_mapping_page_fault_optimisation_test() { |
| BEGIN_TEST; |
| |
| AutoVmScannerDisable scanner_disable; |
| |
| constexpr uint64_t kMaxOptPages = VmMapping::kPageFaultMaxOptimisticPages; |
| |
| // Size the allocation of the VMO / mapping to be double the optimistic extension so we can |
| // validate that it is limited by the optimistic cap, not the size of the VMO. |
| constexpr size_t alloc_size = kMaxOptPages * 2 * kPageSize; |
| static const uint8_t align_pow2 = log2_floor(alloc_size); |
| |
| // Mapped & fully committed VMO. |
| fbl::RefPtr<VmObjectPaged> committed_vmo; |
| ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, alloc_size, &committed_vmo)); |
| |
| ktl::unique_ptr<testing::UserMemory> mapping = |
| testing::UserMemory::Create(committed_vmo, 0, align_pow2); |
| ASSERT_NONNULL(mapping); |
| |
| committed_vmo->CommitRange(0, alloc_size); |
| |
| // Trigger a page fault on the first page in the VMO/Mapping. |
| mapping->put(42); |
| |
| // Optimisation will fault the minimum of kMaxOptPages pages and the end of the VMO, protection |
| // range, mapping or page table. We have ensured that all of these will be > kMaxOptPages in this |
| // case. |
| ASSERT_TRUE(verify_mapped_page_range(mapping->base(), alloc_size, kMaxOptPages)); |
| |
| // Mapped but not committed VMO. |
| fbl::RefPtr<VmObjectPaged> uncommitted_vmo; |
| ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, alloc_size, &uncommitted_vmo)); |
| |
| ktl::unique_ptr<testing::UserMemory> mapping2 = |
| testing::UserMemory::Create(uncommitted_vmo, 0, align_pow2); |
| ASSERT_NONNULL(mapping2); |
| |
| // Trigger a page fault on the first page in the VMO/Mapping. |
| mapping2->put(42); |
| |
| // As the VMO is uncommitted, only the requested page should have been faulted. |
| ASSERT_TRUE(verify_mapped_page_range(mapping2->base(), alloc_size, 1)); |
| |
| // Single committed page. |
| fbl::RefPtr<VmObjectPaged> onepage_committed_vmo; |
| ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, alloc_size, &onepage_committed_vmo)); |
| |
| ktl::unique_ptr<testing::UserMemory> mapping3 = |
| testing::UserMemory::Create(onepage_committed_vmo, 0, align_pow2); |
| ASSERT_NONNULL(mapping3); |
| |
| onepage_committed_vmo->CommitRange(0, kPageSize); |
| |
| // Trigger a page fault on the first page in the VMO/Mapping. |
| mapping3->put(42); |
| |
| // Only the requested page should have been faulted. |
| ASSERT_TRUE(verify_mapped_page_range(mapping3->base(), alloc_size, 1)); |
| |
| // 4 committed pages. |
| static_assert(4 <= kMaxOptPages); |
| fbl::RefPtr<VmObjectPaged> partially_committed_vmo; |
| ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, alloc_size, &partially_committed_vmo)); |
| |
| ktl::unique_ptr<testing::UserMemory> mapping4 = |
| testing::UserMemory::Create(partially_committed_vmo, 0, align_pow2); |
| ASSERT_NONNULL(mapping4); |
| |
| partially_committed_vmo->CommitRange(0, 4 * kPageSize); |
| |
| // Trigger a page fault on the first page in the VMO/Mapping. |
| mapping4->put(42); |
| |
| // Only the already committed pages should be committed. |
| ASSERT_TRUE(verify_mapped_page_range(mapping4->base(), alloc_size, 4)); |
| |
| END_TEST; |
| } |
| |
| // Validate that the page fault optimisation correctly respects page table boundaries. |
| static bool vm_mapping_page_fault_optimization_pt_limit_test() { |
| BEGIN_TEST; |
| |
| AutoVmScannerDisable scanner_disable; |
| |
| constexpr size_t kMaxOptPages = VmMapping::kPageFaultMaxOptimisticPages; |
| // Size our top level vmar allocation to be two page tables in size, ensuring that we will both |
| // have a page table boundary crossing in the allocation, as well as some amount of allocation on |
| // either side of it. |
| constexpr size_t kPageTableSize = ArchVmAspace::NextUserPageTableOffset(0); |
| constexpr size_t kVmarSize = kPageTableSize * 2; |
| // Align our allocation on a page table boundary, ensuring we have 1 page table worth of space |
| // before and after our PT crossing point. |
| constexpr size_t kVmarAlign = log2_floor(kPageTableSize); |
| // Size the allocation of the VMO / mapping to be double the optimistic extension so we can |
| // validate that it is limited by the optimistic cap, not the size of the VMO. |
| constexpr size_t kMapSize = kMaxOptPages * 2 * kPageSize; |
| |
| // Allocate our large top level vmar in root vmar of the current aspace. |
| fbl::RefPtr<VmAddressRegion> root_vmar = Thread::Current::active_aspace()->RootVmar(); |
| fbl::RefPtr<VmAddressRegion> vmar; |
| ASSERT_OK(root_vmar->CreateSubVmar(0, kVmarSize, kVmarAlign, |
| VMAR_FLAG_CAN_MAP_READ | VMAR_FLAG_CAN_MAP_WRITE | |
| VMAR_FLAG_CAN_MAP_EXECUTE | VMAR_FLAG_CAN_MAP_SPECIFIC, |
| "unittest", &vmar)); |
| auto cleanup_vmar = fit::defer([&] { vmar->Destroy(); }); |
| |
| const vaddr_t next_pt_base = ArchVmAspace::NextUserPageTableOffset(vmar->base()); |
| // If our alignment was specified correctly the next pt should be exactly one pt from our base. |
| ASSERT_EQ(vmar->base() + kPageTableSize, next_pt_base); |
| |
| // Try touching at different distances from the start of the next page table and validate that |
| // mappings are not added beyond it. |
| for (size_t page_offset = 0; page_offset <= kMaxOptPages + 1; page_offset++) { |
| // Create a subvmar at the correct offset that will precisely hold our mapping. |
| fbl::RefPtr<VmAddressRegion> sub_vmar; |
| const size_t offset = kPageTableSize - kPageSize * page_offset; |
| ASSERT_OK(vmar->CreateSubVmar(offset, kMapSize, 0, |
| VMAR_FLAG_CAN_MAP_READ | VMAR_FLAG_CAN_MAP_WRITE | |
| VMAR_FLAG_CAN_MAP_EXECUTE | VMAR_FLAG_SPECIFIC, |
| "unittest", &sub_vmar)); |
| auto cleanup_sub_vmar = fit::defer([&] { sub_vmar->Destroy(); }); |
| |
| fbl::RefPtr<VmObjectPaged> committed_vmo; |
| ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, kMapSize, &committed_vmo)); |
| |
| ktl::unique_ptr<testing::UserMemory> mapping = |
| testing::UserMemory::CreateInVmar(committed_vmo, sub_vmar); |
| ASSERT_NONNULL(mapping); |
| |
| committed_vmo->CommitRange(0, kMapSize); |
| |
| // Trigger a page fault on the first page of the mapping. |
| mapping->put(42); |
| |
| // We expect the number of pages that are mapped in to be clipped at the page table boundary, |
| // which would be |page_offset|. The two exceptions to this are if page_offset is greater than |
| // kMaxOptPages, in which case that becomes the cap, or if the page_offset is 0, in which case |
| // we are actually at the *start* of the next page table, and so kMaxOptPages should get mapped. |
| const size_t kExpectedPages = |
| page_offset == 0 ? kMaxOptPages : ktl::min(kMaxOptPages, page_offset); |
| |
| ASSERT_TRUE(verify_mapped_page_range(mapping->base(), kMapSize, kExpectedPages)); |
| } |
| |
| END_TEST; |
| } |
| |
| static bool vm_mapping_page_fault_range_test() { |
| BEGIN_TEST; |
| |
| AutoVmScannerDisable scanner_disable; |
| |
| constexpr size_t kTestPages = VmMapping::kPageFaultMaxOptimisticPages * 2; |
| constexpr size_t kAllocSize = kTestPages * kPageSize; |
| constexpr uint kReadFlags = VMM_PF_FLAG_USER; |
| constexpr uint kWriteFlags = VMM_PF_FLAG_USER | VMM_PF_FLAG_WRITE; |
| // Aligning the mapping is for when testing the optimistic fault handler to ensure that there are |
| // no spurious failures due to crossing a page table boundary. |
| static const uint8_t align_pow2 = log2_floor(kAllocSize); |
| |
| fbl::RefPtr<VmObjectPaged> vmo; |
| ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, VmObjectPaged::kResizable, kAllocSize, &vmo)); |
| |
| ktl::unique_ptr<testing::UserMemory> mapping = testing::UserMemory::Create(vmo, 0, align_pow2); |
| ASSERT_NONNULL(mapping); |
| |
| // Faulting even 1 additional page should prevent optimistic faulting. |
| { |
| // Decommit and recommit the VMO to ensure no page table mappings. |
| EXPECT_OK(vmo->DecommitRange(0, kAllocSize)); |
| EXPECT_OK(vmo->CommitRange(0, kAllocSize)); |
| EXPECT_TRUE(verify_mapped_page_range(mapping->base(), kAllocSize, 0)); |
| |
| // Fault a two page range should only give two pages. |
| EXPECT_OK(Thread::Current::SoftFaultInRange(mapping->base(), kReadFlags, kPageSize * 2)); |
| EXPECT_TRUE(verify_mapped_page_range(mapping->base(), kAllocSize, 2)); |
| |
| // Reset and fault a single page to validate optimistic faulting would otherwise have happened. |
| EXPECT_OK(vmo->DecommitRange(0, kAllocSize)); |
| EXPECT_OK(vmo->CommitRange(0, kAllocSize)); |
| EXPECT_TRUE(verify_mapped_page_range(mapping->base(), kAllocSize, 0)); |
| EXPECT_OK(Thread::Current::SoftFaultInRange(mapping->base(), kReadFlags, kPageSize)); |
| EXPECT_TRUE(verify_mapped_page_range(mapping->base(), kAllocSize, |
| VmMapping::kPageFaultMaxOptimisticPages)); |
| } |
| |
| // Will map in pages that are not committed on read without allocating. |
| { |
| // Start with one page committed. |
| EXPECT_OK(vmo->DecommitRange(0, kAllocSize)); |
| EXPECT_OK(vmo->CommitRange(0, kPageSize)); |
| EXPECT_TRUE(verify_mapped_page_range(mapping->base(), kAllocSize, 0)); |
| EXPECT_TRUE(vmo->GetAttributedMemory() == make_private_attribution_counts(kPageSize, 0)); |
| |
| // Read faulting the range should map without allocating. |
| EXPECT_OK(Thread::Current::SoftFaultInRange(mapping->base(), kReadFlags, kAllocSize)); |
| EXPECT_TRUE(verify_mapped_page_range(mapping->base(), kAllocSize, kTestPages)); |
| EXPECT_TRUE(vmo->GetAttributedMemory() == make_private_attribution_counts(kPageSize, 0)); |
| } |
| |
| // Write faulting should cause allocations |
| { |
| // Start with one page committed. |
| EXPECT_OK(vmo->DecommitRange(0, kAllocSize)); |
| EXPECT_OK(vmo->CommitRange(0, kPageSize)); |
| EXPECT_TRUE(verify_mapped_page_range(mapping->base(), kAllocSize, 0)); |
| EXPECT_TRUE(vmo->GetAttributedMemory() == make_private_attribution_counts(kPageSize, 0)); |
| |
| // Write faulting the range should both map and allocate the pages. |
| EXPECT_OK(Thread::Current::SoftFaultInRange(mapping->base(), kWriteFlags, kAllocSize)); |
| EXPECT_TRUE(verify_mapped_page_range(mapping->base(), kAllocSize, kTestPages)); |
| EXPECT_TRUE(vmo->GetAttributedMemory() == make_private_attribution_counts(kAllocSize, 0)); |
| } |
| |
| // Faulting a partial range should not overrun |
| { |
| EXPECT_OK(vmo->DecommitRange(0, kAllocSize)); |
| EXPECT_TRUE(verify_mapped_page_range(mapping->base(), kAllocSize, 0)); |
| EXPECT_TRUE(vmo->GetAttributedMemory() == make_private_attribution_counts(0, 0)); |
| |
| // Write faulting the range should both map and allocate the requested pages, but no more. |
| EXPECT_OK(Thread::Current::SoftFaultInRange(mapping->base(), kWriteFlags, kAllocSize / 2)); |
| EXPECT_TRUE(verify_mapped_page_range(mapping->base(), kAllocSize, kTestPages / 2)); |
| EXPECT_TRUE(vmo->GetAttributedMemory() == make_private_attribution_counts(kAllocSize / 2, 0)); |
| } |
| |
| // Should not error if > VMO length. |
| { |
| EXPECT_OK(vmo->DecommitRange(0, kAllocSize)); |
| // Shrink the VMO so that it is smaller than the mapping. |
| EXPECT_OK(vmo->Resize(kAllocSize / 2)); |
| EXPECT_TRUE(verify_mapped_page_range(mapping->base(), kAllocSize, 0)); |
| |
| // Attempt to fault the entire mapping range, which is now larger than the VMO. |
| EXPECT_OK(Thread::Current::SoftFaultInRange(mapping->base(), kWriteFlags, kAllocSize)); |
| // Only half should have been mapped and what is now the whole VMO should be committed. |
| EXPECT_TRUE(verify_mapped_page_range(mapping->base(), kAllocSize, kTestPages / 2)); |
| EXPECT_TRUE(vmo->GetAttributedMemory() == make_private_attribution_counts(kAllocSize / 2, 0)); |
| |
| // Restore the VMO size. |
| EXPECT_OK(vmo->Resize(kAllocSize)); |
| } |
| |
| // Will respect protection boundaries. |
| { |
| EXPECT_OK(vmo->DecommitRange(0, kAllocSize)); |
| // Remove write permissions from half the mapping. |
| EXPECT_OK(mapping->Protect(ARCH_MMU_FLAG_PERM_READ, kAllocSize / 2)); |
| |
| // Attempt to write fault the entire mapping. |
| EXPECT_OK(Thread::Current::SoftFaultInRange(mapping->base(), kWriteFlags, kAllocSize)); |
| // Only the writable half should have been mapped and committed. |
| EXPECT_TRUE(verify_mapped_page_range(mapping->base(), kAllocSize, kTestPages / 2)); |
| EXPECT_TRUE(vmo->GetAttributedMemory() == make_private_attribution_counts(kAllocSize / 2, 0)); |
| |
| // Reset protections. |
| EXPECT_OK(mapping->Protect(ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE)); |
| } |
| |
| // Will mark modified if even one writable page is mapped even if mapping aborts early due to an |
| // error. |
| { |
| // Create a pager backed VMO with one page committed and map it. |
| fbl::RefPtr<VmObjectPaged> paged_vmo; |
| ASSERT_OK( |
| make_partially_committed_pager_vmo(kTestPages, 1, false, false, true, nullptr, &paged_vmo)); |
| ktl::unique_ptr<testing::UserMemory> paged_mapping = testing::UserMemory::Create(paged_vmo); |
| |
| // Consume any existing modified flag. |
| zx_pager_vmo_stats_t stats; |
| EXPECT_OK(paged_vmo->QueryPagerVmoStats(true, &stats)); |
| EXPECT_OK(paged_vmo->QueryPagerVmoStats(true, &stats)); |
| EXPECT_EQ(stats.modified, 0u); |
| |
| // Perform a fault that will have to generate a page request. To avoid blocking on the page |
| // request we must directly call the PageFaultLocked method instead of the VmAspace fault. |
| { |
| const vaddr_t base = paged_mapping->base(); |
| ktl::pair<zx_status_t, uint32_t> result; |
| size_t retry_count = 0; |
| do { |
| Guard<CriticalMutex> guard{paged_mapping->mapping()->lock()}; |
| MultiPageRequest page_request; |
| // Although the first page is supplied to paged_vmo, attempting to map it could still fail |
| // due to either it being deduped to a marker, or it being a loaned page and needing to be |
| // swapped. Both of these cases require an allocation, which could need to wait. This wait |
| // request should only be due to the pmm random delayed allocations, and so we can just |
| // ignore it and try again. |
| result = paged_mapping->mapping()->PageFaultLocked(base, kWriteFlags, kTestPages - 1, |
| &page_request); |
| page_request.CancelRequests(); |
| retry_count++; |
| } while (result.first == ZX_ERR_SHOULD_WAIT && result.second == 0 && retry_count < 100); |
| EXPECT_EQ(result.first, ZX_ERR_SHOULD_WAIT); |
| EXPECT_EQ(result.second, 1u); |
| } |
| |
| // The one previously committed page should have been mapped in and the VMO marked modified. |
| EXPECT_TRUE(verify_mapped_page_range(paged_mapping->base(), kAllocSize, 1)); |
| |
| EXPECT_OK(paged_vmo->QueryPagerVmoStats(true, &stats)); |
| EXPECT_EQ(stats.modified, ZX_PAGER_VMO_STATS_MODIFIED); |
| } |
| |
| // Read fault on copy-on-write hierarchy with some leaf pages will map both parent and child |
| // pages without committing extra pages into the child. |
| { |
| EXPECT_OK(vmo->CommitRange(0, kAllocSize)); |
| // Create a snapshot with some committed pages and map it in. |
| fbl::RefPtr<VmObject> child_vmo; |
| ASSERT_OK(vmo->CreateClone(Resizability::NonResizable, SnapshotType::Full, 0, kAllocSize, true, |
| &child_vmo)); |
| EXPECT_OK(child_vmo->CommitRange(0, kPageSize)); |
| EXPECT_OK(child_vmo->CommitRange(kAllocSize / 2, kPageSize)); |
| ktl::unique_ptr<testing::UserMemory> child_mapping = testing::UserMemory::Create(child_vmo); |
| |
| // Read fault the entire range. Everything should get mapped with the child's memory attribution |
| // being unchanged. |
| VmObject::AttributionCounts original_counts = child_vmo->GetAttributedMemory(); |
| EXPECT_OK(Thread::Current::SoftFaultInRange(child_mapping->base(), kReadFlags, kAllocSize)); |
| EXPECT_TRUE(verify_mapped_page_range(child_mapping->base(), kAllocSize, kTestPages)); |
| EXPECT_TRUE(original_counts == child_vmo->GetAttributedMemory()); |
| EXPECT_TRUE(verify_continuous_attribution_bytes( |
| *child_vmo, original_counts.uncompressed_bytes + original_counts.compressed_bytes)); |
| } |
| |
| // Calling ReadUser will fault the requested range. |
| { |
| EXPECT_OK(vmo->DecommitRange(0, kAllocSize)); |
| vmo->CommitRange(0, kAllocSize); |
| |
| ASSERT_TRUE(verify_mapped_page_range(mapping->base(), kAllocSize, 0)); |
| |
| auto [status, read_actual] = vmo->ReadUser( |
| mapping->user_out<char>(), 0, sizeof(char[kPageSize * 2]), VmObjectReadWriteOptions::None); |
| ASSERT_EQ(status, ZX_OK); |
| ASSERT_EQ(read_actual, sizeof(char[kPageSize * 2])); |
| |
| // The page fault optimisation should not have been triggered so the exact range is mapped. |
| ASSERT_TRUE(verify_mapped_page_range(mapping->base(), kAllocSize, 2)); |
| } |
| |
| END_TEST; |
| } |
| |
| // Attempt force a high priority region to be writeable. |
| static bool vm_mapping_force_writeable_high_priority() { |
| BEGIN_TEST; |
| |
| fbl::RefPtr<VmAspace> aspace = VmAspace::Create(VmAspace::Type::User, "test-aspace"); |
| ASSERT_NONNULL(aspace); |
| |
| // Create VMAR and a VMO and map it in. |
| fbl::RefPtr<VmAddressRegion> vmar; |
| ASSERT_OK(aspace->RootVmar()->CreateSubVmar( |
| 0, kPageSize * 64, 0, |
| VMAR_FLAG_CAN_MAP_SPECIFIC | VMAR_FLAG_CAN_MAP_READ | VMAR_FLAG_CAN_MAP_WRITE, "test vmar", |
| &vmar)); |
| |
| auto cleanup_sub_vmar = fit::defer([&] { vmar->Destroy(); }); |
| |
| fbl::RefPtr<VmObjectPaged> vmo; |
| zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, kPageSize * 4, &vmo); |
| ASSERT_OK(status); |
| |
| // Create a read-only user mapping. Since there is no ARCH_MMU_FLAG_PERM_WRITE, ForceWritable |
| // won't trivially succeed. |
| constexpr uint kArchReadUserFlags = ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_USER; |
| auto mapping_result = |
| vmar->CreateVmMapping(0, kPageSize * 4, 0, 0, vmo, 0, kArchReadUserFlags, "test-mapping"); |
| ASSERT_OK(mapping_result.status_value()); |
| |
| status = vmar->SetMemoryPriority(VmAddressRegion::MemoryPriority::HIGH); |
| EXPECT_OK(status); |
| |
| zx::result<fbl::RefPtr<VmMapping>> force_result = mapping_result->mapping->ForceWritable(); |
| EXPECT_OK(force_result.status_value()); |
| |
| END_TEST; |
| } |
| |
| using ArchUnmapOptions = ArchVmAspaceInterface::ArchUnmapOptions; |
| |
| static bool arch_noncontiguous_map() { |
| BEGIN_TEST; |
| |
| // Get some phys pages to test on |
| paddr_t phys[3]; |
| struct list_node phys_list = LIST_INITIAL_VALUE(phys_list); |
| zx_status_t status = pmm_alloc_pages(ktl::size(phys), 0, &phys_list); |
| ASSERT_EQ(ZX_OK, status, "non contig map alloc"); |
| { |
| size_t i = 0; |
| vm_page_t* p; |
| list_for_every_entry (&phys_list, p, vm_page_t, queue_node) { |
| phys[i] = p->paddr(); |
| ++i; |
| } |
| } |
| |
| { |
| constexpr vaddr_t base = USER_ASPACE_BASE + 10 * kPageSize; |
| |
| ArchVmAspace aspace(USER_ASPACE_BASE, USER_ASPACE_SIZE, 0); |
| status = aspace.Init(); |
| ASSERT_EQ(ZX_OK, status, "failed to init aspace\n"); |
| |
| // Attempt to map a set of vm_page_t |
| status = aspace.Map(base, phys, ktl::size(phys), ARCH_MMU_FLAG_PERM_READ, |
| ArchVmAspace::ExistingEntryAction::Error); |
| ASSERT_EQ(ZX_OK, status, "failed first map\n"); |
| |
| // Expect that the map succeeded |
| for (size_t i = 0; i < ktl::size(phys); ++i) { |
| paddr_t paddr; |
| arch_mmu_flags_t mmu_flags; |
| status = aspace.Query(base + i * kPageSize, &paddr, &mmu_flags); |
| EXPECT_EQ(ZX_OK, status, "bad first map\n"); |
| EXPECT_EQ(phys[i], paddr, "bad first map\n"); |
| EXPECT_EQ(ARCH_MMU_FLAG_PERM_READ, mmu_flags, "bad first map\n"); |
| } |
| |
| // Attempt to map again, should fail |
| status = aspace.Map(base, phys, ktl::size(phys), ARCH_MMU_FLAG_PERM_READ, |
| ArchVmAspace::ExistingEntryAction::Error); |
| EXPECT_EQ(ZX_ERR_ALREADY_EXISTS, status, "double map\n"); |
| |
| // Attempt to map partially overlapping, should fail |
| status = aspace.Map(base + 2 * kPageSize, phys, ktl::size(phys), ARCH_MMU_FLAG_PERM_READ, |
| ArchVmAspace::ExistingEntryAction::Error); |
| EXPECT_EQ(ZX_ERR_ALREADY_EXISTS, status, "double map\n"); |
| status = aspace.Map(base - 2 * kPageSize, phys, ktl::size(phys), ARCH_MMU_FLAG_PERM_READ, |
| ArchVmAspace::ExistingEntryAction::Error); |
| EXPECT_EQ(ZX_ERR_ALREADY_EXISTS, status, "double map\n"); |
| |
| // No entries should have been created by the partial failures |
| status = aspace.Query(base - 2 * kPageSize, nullptr, nullptr); |
| EXPECT_EQ(ZX_ERR_NOT_FOUND, status, "bad first map\n"); |
| status = aspace.Query(base - kPageSize, nullptr, nullptr); |
| EXPECT_EQ(ZX_ERR_NOT_FOUND, status, "bad first map\n"); |
| status = aspace.Query(base + 3 * kPageSize, nullptr, nullptr); |
| EXPECT_EQ(ZX_ERR_NOT_FOUND, status, "bad first map\n"); |
| status = aspace.Query(base + 4 * kPageSize, nullptr, nullptr); |
| EXPECT_EQ(ZX_ERR_NOT_FOUND, status, "bad first map\n"); |
| |
| // Unmap all remaining entries |
| // The partial failures did not create any new entries, so only entries |
| // created by the first map should be unmapped. |
| status = aspace.Unmap(base, ktl::size(phys), ArchUnmapOptions::Enlarge); |
| ASSERT_EQ(ZX_OK, status, "failed unmap\n"); |
| |
| status = aspace.Destroy(); |
| EXPECT_EQ(ZX_OK, status, "failed to destroy aspace\n"); |
| } |
| |
| pmm_free(&phys_list); |
| |
| END_TEST; |
| } |
| |
| static bool arch_noncontiguous_map_with_upgrade() { |
| BEGIN_TEST; |
| |
| // Get some phys pages to test on |
| paddr_t phys[3]; |
| struct list_node phys_list = LIST_INITIAL_VALUE(phys_list); |
| zx_status_t status = pmm_alloc_pages(ktl::size(phys), 0, &phys_list); |
| ASSERT_EQ(ZX_OK, status, "non contig map alloc"); |
| { |
| size_t i = 0; |
| vm_page_t* p; |
| list_for_every_entry (&phys_list, p, vm_page_t, queue_node) { |
| phys[i] = p->paddr(); |
| ++i; |
| } |
| } |
| |
| { |
| constexpr vaddr_t base = USER_ASPACE_BASE + 10 * kPageSize; |
| constexpr vaddr_t window_base = base - 2 * kPageSize; |
| |
| ArchVmAspace aspace(USER_ASPACE_BASE, USER_ASPACE_SIZE, 0); |
| status = aspace.Init(); |
| ASSERT_EQ(ZX_OK, status, "failed to init aspace\n"); |
| |
| // Attempt to map a set of vm_page_t |
| status = aspace.Map(base, phys, ktl::size(phys), ARCH_MMU_FLAG_PERM_READ, |
| ArchVmAspace::ExistingEntryAction::Error); |
| ASSERT_EQ(ZX_OK, status, "failed first map\n"); |
| |
| // Attempt to map with upgrades allowed, should succeed |
| status = |
| aspace.Map(base, phys, ktl::size(phys), ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE, |
| ArchVmAspace::ExistingEntryAction::Upgrade); |
| EXPECT_EQ(ZX_OK, status, "map upgrade failed\n"); |
| |
| // Attempt to map with upgrades allowed, should succeed but not remap anything |
| // b/c downgrade to read not allowed |
| status = aspace.Map(base, phys, ktl::size(phys), ARCH_MMU_FLAG_PERM_READ, |
| ArchVmAspace::ExistingEntryAction::Upgrade); |
| EXPECT_EQ(ZX_OK, status, "map upgrade failed\n"); |
| |
| // Expect that the upgrade maps succeeded |
| for (size_t i = 0; i < ktl::size(phys); ++i) { |
| const uint MAP_WINDOW_PADDR_INDEX[ktl::size(phys)] = {0, 1, 2}; |
| constexpr uint MAP_WINDOW_MMU_FLAGS[ktl::size(phys)] = { |
| ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE, |
| ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE, |
| ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE}; |
| |
| paddr_t paddr; |
| arch_mmu_flags_t mmu_flags; |
| status = aspace.Query(base + i * kPageSize, &paddr, &mmu_flags); |
| EXPECT_EQ(ZX_OK, status, "bad map upgrade\n"); |
| EXPECT_EQ(phys[MAP_WINDOW_PADDR_INDEX[i]], paddr, "bad map upgrade\n"); |
| EXPECT_EQ(MAP_WINDOW_MMU_FLAGS[i], mmu_flags, "bad map upgrade\n"); |
| } |
| |
| // Attempt to map partially overlapping with upgrades allowed, should succeed |
| status = aspace.Map(base + 2 * kPageSize, phys, ktl::size(phys), |
| ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE, |
| ArchVmAspace::ExistingEntryAction::Upgrade); |
| EXPECT_EQ(ZX_OK, status, "map upgrade failed\n"); |
| status = aspace.Map(base - 2 * kPageSize, phys, ktl::size(phys), ARCH_MMU_FLAG_PERM_READ, |
| ArchVmAspace::ExistingEntryAction::Upgrade); |
| EXPECT_EQ(ZX_OK, status, "map upgrade failed\n"); |
| |
| // Expect that the `Upgrade` maps succeeded |
| // We check the entire [base - 2, base + 4] "window" covered by the partial maps |
| constexpr size_t MAP_WINDOW_SIZE = 7; |
| for (size_t i = 0; i < MAP_WINDOW_SIZE; ++i) { |
| const uint MAP_WINDOW_PADDR_INDEX[MAP_WINDOW_SIZE] = {0, 1, 2, 1, 0, 1, 2}; |
| constexpr uint MAP_WINDOW_MMU_FLAGS[MAP_WINDOW_SIZE] = { |
| ARCH_MMU_FLAG_PERM_READ, |
| ARCH_MMU_FLAG_PERM_READ, |
| ARCH_MMU_FLAG_PERM_READ, |
| ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE, |
| ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE, |
| ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE, |
| ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE}; |
| |
| paddr_t paddr; |
| arch_mmu_flags_t mmu_flags; |
| status = aspace.Query(window_base + i * kPageSize, &paddr, &mmu_flags); |
| EXPECT_EQ(ZX_OK, status, "bad map upgrade\n"); |
| EXPECT_EQ(phys[MAP_WINDOW_PADDR_INDEX[i]], paddr, "bad map upgrade\n"); |
| EXPECT_EQ(MAP_WINDOW_MMU_FLAGS[i], mmu_flags, "bad map upgrade\n"); |
| } |
| |
| // Unmap any remaining entries |
| status = aspace.Unmap(window_base, MAP_WINDOW_SIZE, ArchUnmapOptions::Enlarge); |
| ASSERT_EQ(ZX_OK, status, "failed unmap\n"); |
| |
| status = aspace.Destroy(); |
| EXPECT_EQ(ZX_OK, status, "failed to destroy aspace\n"); |
| } |
| |
| pmm_free(&phys_list); |
| |
| END_TEST; |
| } |
| |
| // Get the mmu_flags of the given vaddr of the given aspace. |
| // |
| // Return 0 if the page is unmapped or on error. |
| static arch_mmu_flags_t get_vaddr_flags(ArchVmAspace* aspace, vaddr_t vaddr) { |
| paddr_t unused_paddr; |
| arch_mmu_flags_t mmu_flags; |
| if (aspace->Query(vaddr, &unused_paddr, &mmu_flags) != ZX_OK) { |
| return 0; |
| } |
| return mmu_flags; |
| } |
| |
| // Determine if the given page is mapped in. |
| static bool is_vaddr_mapped(ArchVmAspace* aspace, vaddr_t vaddr) { |
| return get_vaddr_flags(aspace, vaddr) != 0; |
| } |
| |
| static bool arch_vm_aspace_protect_split_pages() { |
| BEGIN_TEST; |
| |
| constexpr uint kReadOnly = ARCH_MMU_FLAG_PERM_READ; |
| constexpr uint kReadWrite = ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE; |
| |
| // Create a basic address space, starting from vaddr 0. |
| ArchVmAspace aspace(0, USER_ASPACE_SIZE, 0); |
| ASSERT_OK(aspace.Init()); |
| auto cleanup = fit::defer([&]() { |
| aspace.Unmap(0, USER_ASPACE_SIZE / kPageSize, ArchUnmapOptions::Enlarge); |
| aspace.Destroy(); |
| }); |
| |
| // Map in a large contiguous area, which should be mapped by two large pages. |
| static_assert(ZX_MAX_PAGE_SIZE > kPageSize); |
| constexpr size_t kRegionSize = 16ul * 1024 * 1024 * 1024; // 16 GiB. |
| ASSERT_OK( |
| aspace.MapContiguous(/*vaddr=*/0, /*paddr=*/0, /*count=*/kRegionSize / kPageSize, kReadOnly)); |
| |
| // Attempt to protect a subrange in the middle of the region, which will require splitting |
| // pages. |
| constexpr vaddr_t kProtectedRange = kRegionSize / 2 - kPageSize; |
| constexpr size_t kProtectedPages = 2; |
| ASSERT_OK(aspace.Protect(kProtectedRange, /*count=*/kProtectedPages, kReadWrite, |
| ArchUnmapOptions::Enlarge)); |
| |
| // Ensure the pages inside the range changed. |
| EXPECT_EQ(get_vaddr_flags(&aspace, kProtectedRange), kReadWrite); |
| EXPECT_EQ(get_vaddr_flags(&aspace, kProtectedRange + kPageSize), kReadWrite); |
| |
| // Ensure the pages surrounding the range did not change. |
| EXPECT_EQ(get_vaddr_flags(&aspace, kProtectedRange - kPageSize), kReadOnly); |
| EXPECT_EQ(get_vaddr_flags(&aspace, kProtectedRange + kProtectedPages * kPageSize), kReadOnly); |
| |
| END_TEST; |
| } |
| |
| static bool arch_vm_aspace_protect_split_pages_out_of_memory() { |
| BEGIN_TEST; |
| |
| constexpr uint kReadOnly = ARCH_MMU_FLAG_PERM_READ; |
| constexpr uint kReadWrite = ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE; |
| |
| // Create a custom allocator that we can cause to stop returning allocations. |
| // |
| // ArchVmAspace doesn't allow us to send state to the allocator, so we use a |
| // global static here to control the allocator. |
| static bool allow_allocations; |
| auto allocator = +[](uint alloc_flags, vm_page** p, paddr_t* pa) -> zx_status_t { |
| if (!allow_allocations) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| return pmm_alloc_page(0, p, pa); |
| }; |
| allow_allocations = true; |
| |
| // Create a basic address space, starting from vaddr 0. |
| ArchVmAspace aspace(0, USER_ASPACE_SIZE, 0, allocator); |
| ASSERT_OK(aspace.Init()); |
| auto cleanup = fit::defer([&]() { |
| aspace.Unmap(0, USER_ASPACE_SIZE / kPageSize, ArchUnmapOptions::Enlarge); |
| aspace.Destroy(); |
| }); |
| |
| // Map in a large contiguous area, large enough to use large pages to fill. |
| constexpr size_t kRegionSize = 16ul * 1024 * 1024 * 1024; // 16 GiB. |
| ASSERT_OK( |
| aspace.MapContiguous(/*vaddr=*/0, /*paddr=*/0, /*count=*/kRegionSize / kPageSize, kReadOnly)); |
| |
| // Prevent further allocations. |
| allow_allocations = false; |
| |
| // Attempt to protect a subrange in the middle of the region, which will require splitting |
| // pages. Expect this to fail. |
| constexpr vaddr_t kProtectedRange = kRegionSize / 2 - kPageSize; |
| constexpr size_t kProtectedSize = 2 * kPageSize; |
| zx_status_t status = |
| aspace.Protect(kProtectedRange, /*count=*/2, kReadWrite, ArchUnmapOptions::Enlarge); |
| EXPECT_EQ(status, ZX_ERR_NO_MEMORY); |
| |
| // The pages surrounding our protect range should still be mapped. |
| EXPECT_EQ(get_vaddr_flags(&aspace, kProtectedRange - kPageSize), kReadOnly); |
| EXPECT_EQ(get_vaddr_flags(&aspace, kProtectedRange + kProtectedSize), kReadOnly); |
| |
| // The pages we tried to protect should still be mapped, albeit permissions might |
| // be changed. |
| EXPECT_TRUE(is_vaddr_mapped(&aspace, kProtectedRange)); |
| EXPECT_TRUE(is_vaddr_mapped(&aspace, kProtectedRange + kPageSize)); |
| |
| END_TEST; |
| } |
| |
| // Test to make sure all the vm kernel regions (code, rodata, data, bss, etc.) are correctly mapped |
| // in vm and have the correct arch_mmu_flags. This test also check that all gaps are contained |
| // within a VMAR. |
| static bool vm_kernel_region_test() { |
| BEGIN_TEST; |
| |
| fbl::RefPtr<VmAddressRegionOrMapping> kernel_vmar = |
| VmAspace::kernel_aspace()->RootVmar()->FindRegion( |
| reinterpret_cast<vaddr_t>(__executable_start)); |
| EXPECT_NE(kernel_vmar.get(), nullptr); |
| EXPECT_FALSE(kernel_vmar->is_mapping()); |
| for (vaddr_t base = reinterpret_cast<vaddr_t>(__executable_start); |
| base < reinterpret_cast<vaddr_t>(_end); base += kPageSize) { |
| bool within_region = false; |
| for (const auto& kernel_region : kernel_regions) { |
| // This would not overflow because the region base and size are hard-coded. |
| if (kernel_region.size != 0 && base >= kernel_region.base && |
| base + kPageSize <= kernel_region.base + kernel_region.size) { |
| // If this page exists within a kernel region, then it should be within a VmMapping with |
| // the correct arch MMU flags. |
| within_region = true; |
| fbl::RefPtr<VmAddressRegionOrMapping> region = |
| kernel_vmar->as_vm_address_region()->FindRegion(base); |
| // Every page from __code_start to _end should either be a VmMapping or a VMAR. |
| EXPECT_NE(region.get(), nullptr); |
| EXPECT_TRUE(region->is_mapping()); |
| Guard<CriticalMutex> guard{region->as_vm_mapping()->lock()}; |
| EXPECT_EQ(kernel_region.arch_mmu_flags, |
| region->as_vm_mapping()->arch_mmu_flags_locked(base)); |
| break; |
| } |
| } |
| if (!within_region) { |
| auto region = VmAspace::kernel_aspace()->RootVmar()->FindRegion(base); |
| EXPECT_EQ(region.get(), kernel_vmar.get()); |
| } |
| } |
| |
| END_TEST; |
| } |
| |
| class TestRegionList; |
| |
| class TestRegion : public fbl::RefCounted<TestRegion> { |
| public: |
| TestRegion(vaddr_t base, size_t size, TestRegionList const& list) |
| : list_(list), base_(base), size_(size) {} |
| ~TestRegion() = default; |
| |
| vaddr_t base() const { return base_; } |
| size_t size() const { return size_; } |
| |
| vaddr_t base_locked() const { return base_; } |
| size_t size_locked() const { return size_; } |
| |
| Lock<CriticalMutex>* lock() const; |
| Lock<CriticalMutex>& lock_ref() const; |
| |
| private: |
| friend class TestRegionList; |
| // Simulates aspace for templated code |
| TestRegionList const& list_; |
| vaddr_t base_; |
| size_t size_; |
| }; |
| |
| class TestRegionList : public fbl::RefCounted<TestRegionList> { |
| public: |
| TestRegionList() : guard_(&lock_) {} |
| Lock<CriticalMutex>* lock() const TA_RET_CAP(lock_) { return &lock_; } |
| Lock<CriticalMutex>& lock_ref() const TA_RET_CAP(lock_) { return lock_; } |
| |
| RegionList<TestRegion>* get_regions() { return ®ions_; } |
| |
| void insert_region(vaddr_t base, size_t size) { |
| fbl::AllocChecker ac; |
| auto test_region = fbl::AdoptRef(new (&ac) TestRegion(base, size, *this)); |
| ASSERT(ac.check()); |
| zx_status_t status = regions_.InsertRegion(ktl::move(test_region)); |
| ASSERT(status == ZX_OK); |
| } |
| |
| bool remove_region(vaddr_t base) { |
| auto region = regions_.FindRegion(base); |
| if (region == nullptr) { |
| return false; |
| } |
| regions_.RemoveRegion(region); |
| return true; |
| } |
| |
| private: |
| friend class TestRegion; |
| mutable DECLARE_CRITICAL_MUTEX(TestRegionList) lock_; |
| Guard<CriticalMutex> guard_; |
| RegionList<TestRegion> regions_; |
| }; |
| |
| Lock<CriticalMutex>* TestRegion::lock() const TA_RET_CAP(list_.lock()) { return list_.lock(); } |
| Lock<CriticalMutex>& TestRegion::lock_ref() const TA_RET_CAP(list_.lock()) { |
| return list_.lock_ref(); |
| } |
| |
| static bool region_list_get_alloc_spot_test() { |
| BEGIN_TEST; |
| |
| TestRegionList test_list; |
| auto regions = test_list.get_regions(); |
| vaddr_t base = 0xFFFF000000000000; |
| vaddr_t size = 0x0001000000000000; |
| vaddr_t alloc_spot = 0; |
| // Set the align to be 0x1000. |
| uint8_t align_pow2 = 12; |
| // Allocate 1 page, should be allocated at [+0, +0x1000]. |
| size_t alloc_size = 0x1000; |
| zx_status_t status = regions->GetAllocSpot(&alloc_spot, align_pow2, /*entropy=*/0, alloc_size, |
| base, size, /*prng=*/nullptr); |
| EXPECT_EQ(ZX_OK, status); |
| EXPECT_EQ(base, alloc_spot); |
| |
| test_list.insert_region(alloc_spot, alloc_size); |
| |
| // Manually insert a sub region at [+0x2000, 0x3000]. |
| test_list.insert_region(base + 0x2000, alloc_size); |
| |
| // Try to allocate 2 page, since the gap is too small, we would allocate at [0x3000, 0x5000]. |
| alloc_size = 0x2000; |
| status = regions->GetAllocSpot(&alloc_spot, align_pow2, /*entropy=*/0, alloc_size, base, size, |
| /*prng=*/nullptr); |
| EXPECT_EQ(ZX_OK, status); |
| EXPECT_EQ(base + 0x3000, alloc_spot); |
| test_list.insert_region(alloc_spot, alloc_size); |
| |
| EXPECT_TRUE(test_list.remove_region(base + 0x2000)); |
| |
| // After we remove the region, we now have a gap at [0x1000, 0x3000]. |
| alloc_size = 0x2000; |
| status = regions->GetAllocSpot(&alloc_spot, align_pow2, /*entropy=*/0, alloc_size, base, size, |
| /*prng=*/nullptr); |
| EXPECT_EQ(ZX_OK, status); |
| EXPECT_EQ(base + 0x1000, alloc_spot); |
| test_list.insert_region(alloc_spot, alloc_size); |
| |
| // Now we have fill all the gaps, next region should start at 0x5000. |
| alloc_size = 0x1000; |
| status = regions->GetAllocSpot(&alloc_spot, align_pow2, /*entropy=*/0, alloc_size, base, size, |
| /*prng=*/nullptr); |
| EXPECT_EQ(ZX_OK, status); |
| EXPECT_EQ(base + 0x5000, alloc_spot); |
| test_list.insert_region(alloc_spot, alloc_size); |
| |
| // Test for possible overflow cases. We try to allocate all the rest of the spaces. The last |
| // region should be from [0x6000, base + size - 1], we should be able to find this region and |
| // allocate all the size from it. |
| alloc_size = size - 0x6000; |
| status = regions->GetAllocSpot(&alloc_spot, align_pow2, /*entropy=*/0, alloc_size, base, size, |
| /*prng=*/nullptr); |
| EXPECT_EQ(ZX_OK, status); |
| EXPECT_EQ(base + 0x6000, alloc_spot); |
| |
| END_TEST; |
| } |
| |
| static bool region_list_get_alloc_spot_no_memory_test() { |
| BEGIN_TEST; |
| |
| TestRegionList test_list; |
| auto regions = test_list.get_regions(); |
| vaddr_t base = 0xFFFF000000000000; |
| vaddr_t size = 0x0001000000000000; |
| // Set the align to be 0x1000. |
| uint8_t align_pow2 = 12; |
| |
| test_list.insert_region(base, size - 0x1000); |
| |
| size_t alloc_size = 0x2000; |
| vaddr_t alloc_spot = 0; |
| // There is only a 1 page gap, and we are asking for two pages, so ZX_ERR_NO_RESOURCES should be |
| // returned. |
| zx_status_t status = |
| regions->GetAllocSpot(&alloc_spot, align_pow2, /*entropy=*/0, alloc_size, base, size, |
| /*prng=*/nullptr); |
| EXPECT_EQ(ZX_ERR_NO_RESOURCES, status); |
| |
| END_TEST; |
| } |
| |
| static bool region_list_find_region_test() { |
| BEGIN_TEST; |
| |
| TestRegionList test_list; |
| auto regions = test_list.get_regions(); |
| vaddr_t base = 0xFFFF000000000000; |
| |
| auto region = regions->FindRegion(base); |
| EXPECT_EQ(region, nullptr); |
| |
| test_list.insert_region(base + 0x1000, 0x1000); |
| |
| region = regions->FindRegion(base + 1); |
| EXPECT_EQ(region, nullptr); |
| |
| region = regions->FindRegion(base + 0x1001); |
| EXPECT_NE(region, nullptr); |
| EXPECT_EQ(base + 0x1000, region->base()); |
| EXPECT_EQ((size_t)0x1000, region->size()); |
| |
| END_TEST; |
| } |
| |
| static bool region_list_include_or_higher_test() { |
| BEGIN_TEST; |
| |
| TestRegionList test_list; |
| auto regions = test_list.get_regions(); |
| vaddr_t base = 0xFFFF000000000000; |
| |
| test_list.insert_region(base + 0x1000, 0x1000); |
| |
| auto itr = regions->IncludeOrHigher(base + 1); |
| EXPECT_TRUE(itr.IsValid()); |
| EXPECT_EQ(base + 0x1000, (*itr).second->base()); |
| EXPECT_EQ((size_t)0x1000, (*itr).second->size()); |
| |
| itr = regions->IncludeOrHigher(base + 0x1001); |
| EXPECT_TRUE(itr.IsValid()); |
| EXPECT_EQ(base + 0x1000, (*itr).second->base()); |
| EXPECT_EQ((size_t)0x1000, (*itr).second->size()); |
| |
| itr = regions->IncludeOrHigher(base + 0x2000); |
| EXPECT_FALSE(itr.IsValid()); |
| |
| END_TEST; |
| } |
| |
| static bool region_list_upper_bound_test() { |
| BEGIN_TEST; |
| |
| TestRegionList test_list; |
| auto regions = test_list.get_regions(); |
| vaddr_t base = 0xFFFF000000000000; |
| |
| test_list.insert_region(base + 0x1000, 0x1000); |
| |
| auto itr = regions->UpperBound(base + 0xFFF); |
| EXPECT_TRUE(itr.IsValid()); |
| EXPECT_EQ(base + 0x1000, (*itr).second->base()); |
| EXPECT_EQ((size_t)0x1000, (*itr).second->size()); |
| |
| itr = regions->UpperBound(base + 0x1000); |
| EXPECT_FALSE(itr.IsValid()); |
| |
| END_TEST; |
| } |
| |
| static bool region_list_is_range_available_test() { |
| BEGIN_TEST; |
| |
| TestRegionList test_list; |
| auto regions = test_list.get_regions(); |
| vaddr_t base = 0xFFFF000000000000; |
| |
| test_list.insert_region(base + 0x1000, 0x1000); |
| test_list.insert_region(base + 0x3000, 0x1000); |
| |
| EXPECT_TRUE(regions->IsRangeAvailable(base, 0x1000)); |
| EXPECT_FALSE(regions->IsRangeAvailable(base, 0x1001)); |
| EXPECT_FALSE(regions->IsRangeAvailable(base + 1, 0x1000)); |
| EXPECT_TRUE(regions->IsRangeAvailable(base + 0x2000, 1)); |
| EXPECT_FALSE(regions->IsRangeAvailable(base + 0x1FFF, 0x2000)); |
| |
| EXPECT_TRUE(regions->IsRangeAvailable(0xFFFFFFFFFFFFFFFF, 1)); |
| EXPECT_FALSE(regions->IsRangeAvailable(base, 0x0001000000000000)); |
| |
| END_TEST; |
| } |
| |
| // Helper class for writing tests against the pausable VmAddressRegionEnumerator |
| template <VmAddressRegionEnumeratorType Type> |
| class EnumeratorTestHelper { |
| public: |
| EnumeratorTestHelper() = default; |
| ~EnumeratorTestHelper() { Destroy(); } |
| zx_status_t Init(fbl::RefPtr<VmAspace> aspace) TA_EXCL(lock()) { |
| Destroy(); |
| zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, GB, &vmo_); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| status = aspace->RootVmar()->CreateSubVmar( |
| 0, GB, 0, VMAR_FLAG_CAN_MAP_SPECIFIC | VMAR_FLAG_CAN_MAP_READ, "test vmar", &test_vmar_); |
| if (status != ZX_OK) { |
| return status; |
| } |
| return ZX_OK; |
| } |
| |
| struct ChildRegion { |
| bool mapping; |
| size_t page_offset_begin; |
| size_t page_offset_end; |
| }; |
| zx_status_t AddRegions(ktl::initializer_list<ChildRegion>&& regions) TA_EXCL(lock()) { |
| for (auto& region : regions) { |
| ASSERT(region.page_offset_end > region.page_offset_begin); |
| const size_t offset = region.page_offset_begin * kPageSize; |
| const vaddr_t vaddr = test_vmar_->base() + offset; |
| // See if there's a child VMAR that we should be making this in instead of our test root. |
| fbl::RefPtr<VmAddressRegion> vmar = test_vmar_; |
| auto next_region = [&]() { |
| auto child_region = vmar->FindRegion(vaddr); |
| if (child_region) { |
| return child_region->as_vm_address_region(); |
| } |
| return fbl::RefPtr<VmAddressRegion>(); |
| }; |
| while (auto next = next_region()) { |
| vmar = next; |
| } |
| // Create either a mapping or vmar as requested. |
| const size_t size = (region.page_offset_end - region.page_offset_begin) * kPageSize; |
| const size_t relative_offset = vaddr - vmar->base(); |
| zx_status_t status; |
| if (region.mapping) { |
| auto new_mapping_result = vmar->CreateVmMapping( |
| relative_offset, size, 0, VMAR_FLAG_CAN_MAP_READ | VMAR_FLAG_SPECIFIC, vmo_, 0, |
| ARCH_MMU_FLAG_PERM_READ, "mapping"); |
| status = new_mapping_result.status_value(); |
| } else { |
| fbl::RefPtr<VmAddressRegion> new_vmar; |
| status = vmar->CreateSubVmar( |
| relative_offset, size, 0, |
| VMAR_FLAG_CAN_MAP_READ | VMAR_FLAG_SPECIFIC | VMAR_FLAG_CAN_MAP_SPECIFIC, "vmar", |
| &new_vmar); |
| } |
| if (status != ZX_OK) { |
| return status; |
| } |
| } |
| return ZX_OK; |
| } |
| using RegionEnumerator = VmAddressRegionEnumerator<Type, VmAddressRegion>; |
| RegionEnumerator Enumerator(size_t page_offset_begin, size_t page_offset_end) TA_REQ(lock()) { |
| const vaddr_t min_addr = test_vmar_->base() + page_offset_begin * kPageSize; |
| const vaddr_t max_addr = test_vmar_->base() + page_offset_end * kPageSize; |
| return VmAddressRegionEnumerator<Type, VmAddressRegion>(*test_vmar_, min_addr, max_addr); |
| } |
| |
| void Resume(RegionEnumerator& enumerator) TA_REQ(lock()) { |
| AssertHeld(enumerator.lock_ref()); |
| enumerator.resume(); |
| } |
| |
| bool ExpectRegions(RegionEnumerator& enumerator, ktl::initializer_list<ChildRegion>&& regions) |
| TA_REQ(lock()) { |
| AssertHeld(enumerator.lock_ref()); |
| for (auto& region : regions) { |
| ASSERT(region.page_offset_end > region.page_offset_begin); |
| auto next = enumerator.next(); |
| if (!next.has_value()) { |
| return false; |
| } |
| AssertHeld(next->region_or_mapping->lock_ref()); |
| if (region.mapping != next->region_or_mapping->is_mapping()) { |
| return false; |
| } |
| if (next->region_or_mapping->base() != |
| test_vmar_->base() + region.page_offset_begin * kPageSize) { |
| return false; |
| } |
| if (next->region_or_mapping->size() != |
| (region.page_offset_end - region.page_offset_begin) * kPageSize) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| zx_status_t Unmap(size_t page_offset_begin, size_t page_offset_end) TA_EXCL(lock()) { |
| ASSERT(page_offset_end > page_offset_begin); |
| const vaddr_t vaddr = test_vmar_->base() + page_offset_begin * kPageSize; |
| const size_t size = (page_offset_end - page_offset_begin) * kPageSize; |
| // Attempt to unmap, walking down into child vmars if the unmap fails due to it causing a |
| // subvmar to be partially unmapped. |
| fbl::RefPtr<VmAddressRegion> vmar = test_vmar_; |
| do { |
| zx_status_t status = vmar->Unmap(vaddr, size, VmAddressRegionOpChildren::Yes); |
| if (status != ZX_ERR_INVALID_ARGS) { |
| return status; |
| } |
| fbl::RefPtr<VmAddressRegionOrMapping> next = vmar->FindRegion(vaddr); |
| if (!next) { |
| return status; |
| } |
| vmar = next->as_vm_address_region(); |
| } while (vmar); |
| return ZX_ERR_NOT_FOUND; |
| } |
| |
| Lock<CriticalMutex>* lock() const TA_RET_CAP(test_vmar_->lock()) { return test_vmar_->lock(); } |
| |
| private: |
| void Destroy() { |
| if (test_vmar_) { |
| test_vmar_->Destroy(); |
| test_vmar_.reset(); |
| } |
| vmo_.reset(); |
| } |
| fbl::RefPtr<VmObjectPaged> vmo_; |
| fbl::RefPtr<VmAddressRegion> test_vmar_; |
| }; |
| |
| static bool address_region_enumerator_test() { |
| BEGIN_TEST; |
| |
| fbl::RefPtr<VmAspace> aspace = VmAspace::Create(VmAspace::Type::User, "test aspace"); |
| |
| // Smoke test of a single region. |
| { |
| EnumeratorTestHelper<VmAddressRegionEnumeratorType::VmarsAndMappings> test; |
| ASSERT_OK(test.Init(aspace)); |
| EXPECT_OK(test.AddRegions({{true, 0, 1}})); |
| Guard<CriticalMutex> guard{test.lock()}; |
| auto enumerator = test.Enumerator(0, 1); |
| AssertHeld(enumerator.lock_ref()); |
| EXPECT_TRUE(test.ExpectRegions(enumerator, {{true, 0, 1}})); |
| EXPECT_FALSE(enumerator.next().has_value()); |
| } |
| // Unmap while iterating a subvmar and resume in the parent. |
| { |
| EnumeratorTestHelper<VmAddressRegionEnumeratorType::VmarsAndMappings> test; |
| ASSERT_OK(test.Init(aspace)); |
| EXPECT_OK( |
| test.AddRegions({{false, 0, 7}, {true, 1, 2}, {true, 3, 4}, {true, 5, 6}, {true, 7, 8}})); |
| Guard<CriticalMutex> guard{test.lock()}; |
| auto enumerator = test.Enumerator(0, 10); |
| AssertHeld(enumerator.lock_ref()); |
| EXPECT_TRUE(test.ExpectRegions(enumerator, {{false, 0, 7}, {true, 1, 2}})); |
| enumerator.pause(); |
| // Unmap the entire subvmar we created |
| guard.CallUnlocked([&test] { test.Unmap(0, 7); }); |
| test.Resume(enumerator); |
| // Last mapping should still be there. |
| EXPECT_TRUE(test.ExpectRegions(enumerator, {{true, 7, 8}})); |
| EXPECT_FALSE(enumerator.next().has_value()); |
| } |
| // Pause immediately without enumerating when the start is a subvmar. |
| { |
| EnumeratorTestHelper<VmAddressRegionEnumeratorType::VmarsAndMappings> test; |
| ASSERT_OK(test.Init(aspace)); |
| EXPECT_OK(test.AddRegions({{false, 0, 2}, {true, 1, 2}})); |
| Guard<CriticalMutex> guard{test.lock()}; |
| auto enumerator = test.Enumerator(0, 2); |
| AssertHeld(enumerator.lock_ref()); |
| enumerator.pause(); |
| test.Resume(enumerator); |
| EXPECT_TRUE(test.ExpectRegions(enumerator, {{false, 0, 2}, {true, 1, 2}})); |
| EXPECT_FALSE(enumerator.next().has_value()); |
| } |
| // Add future mapping. |
| { |
| EnumeratorTestHelper<VmAddressRegionEnumeratorType::VmarsAndMappings> test; |
| ASSERT_OK(test.Init(aspace)); |
| EXPECT_OK(test.AddRegions({{true, 0, 1}, {true, 1, 2}})); |
| Guard<CriticalMutex> guard{test.lock()}; |
| auto enumerator = test.Enumerator(0, 3); |
| AssertHeld(enumerator.lock_ref()); |
| EXPECT_TRUE(test.ExpectRegions(enumerator, {{true, 0, 1}})); |
| enumerator.pause(); |
| guard.CallUnlocked([&test] { test.AddRegions({{true, 2, 3}}); }); |
| test.Resume(enumerator); |
| EXPECT_TRUE(test.ExpectRegions(enumerator, {{true, 1, 2}, {true, 2, 3}})); |
| EXPECT_FALSE(enumerator.next().has_value()); |
| } |
| // Replace the next mapping. |
| { |
| EnumeratorTestHelper<VmAddressRegionEnumeratorType::VmarsAndMappings> test; |
| ASSERT_OK(test.Init(aspace)); |
| EXPECT_OK(test.AddRegions({{true, 0, 1}, {true, 1, 2}})); |
| Guard<CriticalMutex> guard{test.lock()}; |
| auto enumerator = test.Enumerator(0, 3); |
| AssertHeld(enumerator.lock_ref()); |
| EXPECT_TRUE(test.ExpectRegions(enumerator, {{true, 0, 1}})); |
| enumerator.pause(); |
| guard.CallUnlocked([&test] { |
| test.Unmap(1, 2); |
| test.AddRegions({{true, 1, 3}}); |
| }); |
| test.Resume(enumerator); |
| EXPECT_TRUE(test.ExpectRegions(enumerator, {{true, 1, 3}})); |
| EXPECT_FALSE(enumerator.next().has_value()); |
| } |
| // Add earlier regions. |
| { |
| EnumeratorTestHelper<VmAddressRegionEnumeratorType::VmarsAndMappings> test; |
| ASSERT_OK(test.Init(aspace)); |
| EXPECT_OK(test.AddRegions({{true, 2, 3}, {true, 3, 4}})); |
| Guard<CriticalMutex> guard{test.lock()}; |
| auto enumerator = test.Enumerator(0, 4); |
| AssertHeld(enumerator.lock_ref()); |
| EXPECT_TRUE(test.ExpectRegions(enumerator, {{true, 2, 3}})); |
| enumerator.pause(); |
| guard.CallUnlocked([&test] { test.AddRegions({{true, 0, 1}, {true, 1, 32}}); }); |
| test.Resume(enumerator); |
| EXPECT_TRUE(test.ExpectRegions(enumerator, {{true, 3, 4}})); |
| EXPECT_FALSE(enumerator.next().has_value()); |
| } |
| // Replace current. |
| { |
| EnumeratorTestHelper<VmAddressRegionEnumeratorType::VmarsAndMappings> test; |
| ASSERT_OK(test.Init(aspace)); |
| EXPECT_OK(test.AddRegions({{true, 1, 2}, {true, 2, 3}})); |
| Guard<CriticalMutex> guard{test.lock()}; |
| auto enumerator = test.Enumerator(0, 3); |
| AssertHeld(enumerator.lock_ref()); |
| EXPECT_TRUE(test.ExpectRegions(enumerator, {{true, 1, 2}})); |
| enumerator.pause(); |
| guard.CallUnlocked([&test] { |
| test.Unmap(1, 2); |
| test.AddRegions({{true, 0, 2}}); |
| }); |
| test.Resume(enumerator); |
| EXPECT_TRUE(test.ExpectRegions(enumerator, {{true, 2, 3}})); |
| EXPECT_FALSE(enumerator.next().has_value()); |
| } |
| // Replace current and next with a single mapping. |
| { |
| EnumeratorTestHelper<VmAddressRegionEnumeratorType::VmarsAndMappings> test; |
| ASSERT_OK(test.Init(aspace)); |
| EXPECT_OK(test.AddRegions({{true, 1, 2}, {true, 2, 3}})); |
| Guard<CriticalMutex> guard{test.lock()}; |
| auto enumerator = test.Enumerator(0, 3); |
| AssertHeld(enumerator.lock_ref()); |
| EXPECT_TRUE(test.ExpectRegions(enumerator, {{true, 1, 2}})); |
| enumerator.pause(); |
| guard.CallUnlocked([&test] { |
| test.Unmap(1, 3); |
| test.AddRegions({{true, 0, 3}}); |
| }); |
| test.Resume(enumerator); |
| EXPECT_FALSE(test.ExpectRegions(enumerator, {{true, 0, 3}})); |
| EXPECT_FALSE(enumerator.next().has_value()); |
| } |
| // Start enumerating part way into a mapping. |
| { |
| EnumeratorTestHelper<VmAddressRegionEnumeratorType::MappingsOnly> test; |
| ASSERT_OK(test.Init(aspace)); |
| EXPECT_OK(test.AddRegions({{false, 0, 6}, {true, 0, 2}, {true, 2, 4}, {true, 6, 7}})); |
| Guard<CriticalMutex> guard{test.lock()}; |
| auto enumerator = test.Enumerator(3, 7); |
| AssertHeld(enumerator.lock_ref()); |
| enumerator.pause(); |
| test.Resume(enumerator); |
| EXPECT_TRUE(test.ExpectRegions(enumerator, {{true, 2, 4}, {true, 6, 7}})); |
| EXPECT_FALSE(enumerator.next().has_value()); |
| } |
| // Delete depth that was just yielded |
| { |
| EnumeratorTestHelper<VmAddressRegionEnumeratorType::VmarsAndMappings> test; |
| ASSERT_OK(test.Init(aspace)); |
| EXPECT_OK(test.AddRegions({{false, 0, 10}, {false, 0, 9}, {false, 0, 8}, {false, 0, 7}})); |
| Guard<CriticalMutex> guard{test.lock()}; |
| auto enumerator = test.Enumerator(0, 10); |
| AssertHeld(enumerator.lock_ref()); |
| EXPECT_TRUE(test.ExpectRegions(enumerator, {{false, 0, 10}, {false, 0, 9}})); |
| enumerator.pause(); |
| guard.CallUnlocked([&test] { |
| ASSERT(test.Unmap(0, 9) == ZX_OK); |
| ASSERT(test.AddRegions({{false, 0, 8}, {false, 0, 7}}) == ZX_OK); |
| }); |
| test.Resume(enumerator); |
| // Subtree was deleted and the new one will not be yielded. |
| EXPECT_FALSE(enumerator.next().has_value()); |
| } |
| // Delete next depth to be yielded |
| { |
| EnumeratorTestHelper<VmAddressRegionEnumeratorType::VmarsAndMappings> test; |
| ASSERT_OK(test.Init(aspace)); |
| EXPECT_OK(test.AddRegions({{false, 0, 10}, {false, 0, 9}, {false, 0, 8}, {false, 0, 7}})); |
| Guard<CriticalMutex> guard{test.lock()}; |
| auto enumerator = test.Enumerator(0, 10); |
| AssertHeld(enumerator.lock_ref()); |
| EXPECT_TRUE(test.ExpectRegions(enumerator, {{false, 0, 10}, {false, 0, 9}})); |
| enumerator.pause(); |
| guard.CallUnlocked([&test] { |
| ASSERT(test.Unmap(0, 8) == ZX_OK); |
| ASSERT(test.AddRegions({{false, 0, 7}}) == ZX_OK); |
| }); |
| test.Resume(enumerator); |
| // Subtree was deleted and the new one will not be yielded. |
| EXPECT_FALSE(enumerator.next().has_value()); |
| } |
| // Nested mapping enumeration. |
| { |
| EnumeratorTestHelper<VmAddressRegionEnumeratorType::MappingsOnly> test; |
| ASSERT_OK(test.Init(aspace)); |
| EXPECT_OK(test.AddRegions({{false, 0, 1000}, |
| {false, 0, 100}, |
| {false, 100, 200}, |
| {false, 200, 300}, |
| {true, 10, 20}, |
| {true, 110, 120}, |
| {true, 210, 220}})); |
| Guard<CriticalMutex> guard{test.lock()}; |
| // Attempt to query for each mapping using its exact location. |
| for (const auto& probe : {10u, 110u, 210u}) { |
| auto enumerator = test.Enumerator(probe, probe + 5); |
| AssertHeld(enumerator.lock_ref()); |
| // The enumerator will accurately yield only this mapping. |
| // Furthermore, because mappings and VMARs are sorted, the enumerator naturally |
| // short-circuits as soon as `itr_->base() >= max_addr_` without iterating the rest of the |
| // tree. |
| EXPECT_TRUE(test.ExpectRegions(enumerator, {{true, probe, probe + 10}})); |
| EXPECT_FALSE(enumerator.next().has_value()); |
| } |
| } |
| // Sub-VMAR entirely below min_addr_ is skipped. |
| { |
| EnumeratorTestHelper<VmAddressRegionEnumeratorType::MappingsOnly> test; |
| ASSERT_OK(test.Init(aspace)); |
| EXPECT_OK( |
| test.AddRegions({{false, 0, 100}, {true, 10, 20}, {false, 100, 200}, {true, 110, 120}})); |
| Guard<CriticalMutex> guard{test.lock()}; |
| // Querying starting at 105. This should skip descending into the first sub-VMAR (0..100) |
| // because all its children are < min_addr_, and correctly fall through to traverse the next |
| // sibling VMAR. |
| auto enumerator = test.Enumerator(105, 120); |
| AssertHeld(enumerator.lock_ref()); |
| EXPECT_TRUE(test.ExpectRegions(enumerator, {{true, 110, 120}})); |
| EXPECT_FALSE(enumerator.next().has_value()); |
| } |
| |
| EXPECT_OK(aspace->Destroy()); |
| |
| END_TEST; |
| } |
| |
| // Doesn't do anything, just prints all aspaces. |
| // Should be run after all other tests so that people can manually comb |
| // through the output for leaked test aspaces. |
| static bool dump_all_aspaces() { |
| BEGIN_TEST; |
| |
| // Remove for debugging. |
| END_TEST; |
| |
| unittest_printf("verify there are no test aspaces left around\n"); |
| VmAspace::DumpAllAspaces(/*verbose*/ true); |
| END_TEST; |
| } |
| |
| // Check if a range of addresses is accessible to the user. If `spectre_validation` is true, this is |
| // done by checking if `validate_user_accessible_range` returns {0,0}. Otherwise, check using |
| // `is_user_accessible_range`. |
| static bool check_user_accessible_range(vaddr_t vaddr, size_t len, bool spectre_validation) { |
| if (spectre_validation) { |
| // If the address and length were not modified, then the pair is valid. |
| vaddr_t old_vaddr = vaddr; |
| size_t old_len = len; |
| internal::validate_user_accessible_range(&vaddr, &len); |
| return vaddr == old_vaddr && len == old_len; |
| } |
| |
| return is_user_accessible_range(vaddr, len); |
| } |
| |
| static bool check_user_accessible_range_test(bool spectre_validation) { |
| BEGIN_TEST; |
| vaddr_t va; |
| size_t len; |
| |
| // Test address of zero. |
| va = 0; |
| len = kPageSize; |
| EXPECT_TRUE(check_user_accessible_range(va, len, spectre_validation)); |
| |
| // Test address and length of zero (both are valid). |
| va = 0; |
| len = 0; |
| EXPECT_TRUE(check_user_accessible_range(va, len, spectre_validation)); |
| |
| // Test very end of address space and zero length (this is invalid since the start has bit 55 set |
| // despite zero length). |
| va = ktl::numeric_limits<uint64_t>::max(); |
| len = 0; |
| EXPECT_FALSE(check_user_accessible_range(va, len, spectre_validation)); |
| |
| // Test a regular user address. |
| va = USER_ASPACE_BASE; |
| len = kPageSize; |
| EXPECT_TRUE(check_user_accessible_range(va, len, spectre_validation)); |
| |
| // Test zero-length on a regular user address. |
| va = USER_ASPACE_BASE; |
| len = 0; |
| EXPECT_TRUE(check_user_accessible_range(va, len, spectre_validation)); |
| |
| // Test overflow past 64 bits. |
| va = USER_ASPACE_BASE; |
| len = ktl::numeric_limits<uint64_t>::max() - va + 1; |
| EXPECT_FALSE(check_user_accessible_range(va, len, spectre_validation)); |
| |
| #if defined(__aarch64__) |
| |
| // On aarch64, an address is accessible to the user if bit 55 is zero. |
| |
| // Test starting on a bad user address. |
| constexpr vaddr_t kBadAddrMask = UINT64_C(1) << 55; |
| va = kBadAddrMask | USER_ASPACE_BASE; |
| len = kPageSize; |
| EXPECT_FALSE(check_user_accessible_range(va, len, spectre_validation)); |
| |
| // Test zero-length on a bad user address. |
| va = kBadAddrMask | USER_ASPACE_BASE; |
| len = 0; |
| EXPECT_FALSE(check_user_accessible_range(va, len, spectre_validation)); |
| |
| // Test 2^55 is in the range of [va, va+len), ending on a bad user address. |
| va = USER_ASPACE_BASE; |
| len = kBadAddrMask; |
| EXPECT_FALSE(check_user_accessible_range(va, len, spectre_validation)); |
| |
| // Test this returns false if any address within the range of [va, va+len) |
| // contains a value where bit 55 is set. This also implies there are many |
| // gaps in ranges above 2^56. |
| // |
| // Here both the start and end values are valid, but this range contains an |
| // address that is invalid. |
| va = 0; |
| len = 0x17f'ffff'ffff'ffff; // Bits 0-56 (except 55) are set. |
| ASSERT_TRUE(is_user_accessible(va)); |
| ASSERT_TRUE(is_user_accessible(va + len)); |
| EXPECT_FALSE(check_user_accessible_range(va, len, spectre_validation)); |
| |
| // Test the range of the largest value less than 2^55 and the smallest value |
| // greater than 2^55 where bit 55 == 0. |
| va = (UINT64_C(1) << 55) - 1; |
| len = 0x80'0000'0000'0001; // End = va + len = 2^56. |
| EXPECT_FALSE(check_user_accessible_range(va, len, spectre_validation)); |
| |
| // Be careful not to just check that 2^55 is in the range. We really want to |
| // check whenever bit 55 is flipped in the range. |
| va = 0x17f'ffff'ffff'ffff; // Start above 2^56. Bit 55 is not set. |
| len = 0x80'0000'0000'0001; // End = va + len = 0x200'0000'0000'0000. This is above 2^56 and bit |
| // 55 also is not set. |
| ASSERT_TRUE(is_user_accessible(va)); |
| ASSERT_TRUE(is_user_accessible(va + len)); |
| EXPECT_FALSE(check_user_accessible_range(va, len, spectre_validation)); |
| |
| va = USER_ASPACE_BASE; |
| len = (UINT64_C(1) << 57) + 1; |
| EXPECT_FALSE(check_user_accessible_range(va, len, spectre_validation)); |
| |
| // Test a range above 2^56 where bit 55 is never set. |
| va = 0x170'0000'0000'0000; |
| len = 0xf'ffff'ffff'ffff; |
| EXPECT_TRUE(check_user_accessible_range(va, len, spectre_validation)); |
| |
| // Test a range right below 2^55 where bit 55 is never set. |
| va = 0x70'0000'0000'0000; |
| len = 0xf'ffff'ffff'ffff; |
| EXPECT_TRUE(check_user_accessible_range(va, len, spectre_validation)); |
| |
| // Test the last valid user space address with a tag of 0. |
| va = ktl::numeric_limits<uint64_t>::max(); |
| va &= ~(UINT64_C(0xFF) << 56); // Set tag to zero. |
| va &= ~kBadAddrMask; // Ensure valid user address. |
| len = 0; |
| EXPECT_TRUE(check_user_accessible_range(va, len, spectre_validation)); |
| |
| #elif defined(__x86_64__) |
| |
| // On x86_64, an address is accessible to the user if bits 48-63 are zero. |
| |
| // Test a bad user address. |
| constexpr vaddr_t kBadAddrMask = UINT64_C(1) << 48; |
| va = kBadAddrMask | USER_ASPACE_BASE; |
| len = kPageSize; |
| EXPECT_FALSE(check_user_accessible_range(va, len, spectre_validation)); |
| |
| // Test zero-length on a bad user address. |
| va = kBadAddrMask | USER_ASPACE_BASE; |
| len = 0; |
| EXPECT_FALSE(check_user_accessible_range(va, len, spectre_validation)); |
| |
| // Test ending on a bad user address. |
| va = USER_ASPACE_BASE; |
| len = kBadAddrMask; |
| EXPECT_FALSE(check_user_accessible_range(va, len, spectre_validation)); |
| |
| #endif |
| |
| END_TEST; |
| } |
| |
| static bool arch_is_user_accessible_range() { return check_user_accessible_range_test(false); } |
| |
| static bool validate_user_address_range() { return check_user_accessible_range_test(true); } |
| |
| UNITTEST_START_TESTCASE(aspace_tests) |
| VM_UNITTEST(vmm_alloc_contiguous_smoke_test) |
| VM_UNITTEST(multiple_regions_test) |
| VM_UNITTEST(vmm_alloc_contiguous_missing_flag_commit_fails) |
| VM_UNITTEST(vmm_alloc_contiguous_zero_size_fails) |
| VM_UNITTEST(vmaspace_create_smoke_test) |
| VM_UNITTEST(vmaspace_create_invalid_ranges) |
| VM_UNITTEST(vmaspace_alloc_smoke_test) |
| VM_UNITTEST(vmaspace_accessed_test_untagged) |
| #if defined(__aarch64__) |
| VM_UNITTEST(vmaspace_accessed_test_tagged) |
| #endif |
| VM_UNITTEST(vmaspace_unified_accessed_test) |
| VM_UNITTEST(vmaspace_usercopy_accessed_fault_test) |
| VM_UNITTEST(vmaspace_free_unaccessed_page_tables_test) |
| VM_UNITTEST(vmaspace_merge_mapping_test) |
| VM_UNITTEST(vmaspace_priority_propagation_test) |
| VM_UNITTEST(vmaspace_priority_unmap_test) |
| VM_UNITTEST(vmaspace_priority_mapping_overwrite_test) |
| VM_UNITTEST(vmaspace_priority_merged_mapping_test) |
| VM_UNITTEST(vmaspace_priority_bidir_clone_test) |
| VM_UNITTEST(vmaspace_priority_slice_test) |
| VM_UNITTEST(vmaspace_priority_pager_test) |
| VM_UNITTEST(vmaspace_priority_reference_test) |
| VM_UNITTEST(vmaspace_nested_attribution_test) |
| VM_UNITTEST(vm_mapping_attribution_commit_decommit_test) |
| VM_UNITTEST(vm_mapping_attribution_map_unmap_test) |
| VM_UNITTEST(vm_mapping_attribution_merge_test) |
| VM_UNITTEST(vm_mapping_sparse_mapping_test) |
| VM_UNITTEST(vm_mapping_page_fault_optimisation_test) |
| VM_UNITTEST(vm_mapping_page_fault_optimization_pt_limit_test) |
| VM_UNITTEST(vm_mapping_page_fault_range_test) |
| VM_UNITTEST(vm_mapping_force_writeable_high_priority) |
| VM_UNITTEST(arch_is_user_accessible_range) |
| VM_UNITTEST(validate_user_address_range) |
| VM_UNITTEST(arch_noncontiguous_map) |
| VM_UNITTEST(arch_noncontiguous_map_with_upgrade) |
| VM_UNITTEST(arch_vm_aspace_protect_split_pages) |
| VM_UNITTEST(arch_vm_aspace_protect_split_pages_out_of_memory) |
| VM_UNITTEST(vm_kernel_region_test) |
| VM_UNITTEST(region_list_get_alloc_spot_test) |
| VM_UNITTEST(region_list_get_alloc_spot_no_memory_test) |
| VM_UNITTEST(region_list_find_region_test) |
| VM_UNITTEST(region_list_include_or_higher_test) |
| VM_UNITTEST(region_list_upper_bound_test) |
| VM_UNITTEST(region_list_is_range_available_test) |
| VM_UNITTEST(address_region_enumerator_test) |
| VM_UNITTEST(dump_all_aspaces) // Run last |
| UNITTEST_END_TESTCASE(aspace_tests, "aspace", "VmAspace / ArchVmAspace / VMAR tests") |
| |
| } // namespace vm_unittest |