chromium/chromeos/ash/components/memory/pagemap_unittest.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chromeos/ash/components/memory/pagemap.h"

#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <chrono>
#include <random>

#include "base/files/scoped_file.h"
#include "base/memory/page_size.h"
#include "base/posix/eintr_wrapper.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash {
namespace memory {

namespace {
template <typename T, size_t N>
constexpr size_t countof(T (&array)[N]) {
  return N;
}

}  // namespace

class PagemapTest : public testing::Test {
 public:
  void SetUp() override {
    // Use a memfd so we can truncate it to arbitrary lengths to simulate a real
    // pagemap file in procfs.
    pagemap_.fd_.reset(memfd_create("pagemap_test", MFD_CLOEXEC));
    ASSERT_TRUE(pagemap_.fd_.is_valid());
  }

  void TearDown() override {}

 protected:
  // Set the size of the pagemap appropriate for this many pages.
  void CreateStorageForPages(uint64_t pages,
                             void** start_address,
                             void** end_address) {
    ASSERT_NE(
        ftruncate(pagemap_.fd_.get(), pages * sizeof(Pagemap::PagemapEntry)),
        -1);
    *start_address = 0x0;
    *end_address =
        reinterpret_cast<char*>(*start_address) + base::GetPageSize() * pages;
  }

  void PutEntries(void* address, uint64_t* entries, size_t size) {
    ASSERT_EQ(HANDLE_EINTR(pwrite(
                  pagemap_.fd_.get(), entries, sizeof(entries[0]) * size,
                  reinterpret_cast<off_t>(address) / base::GetPageSize())),
              static_cast<ssize_t>(sizeof(entries[0]) * size));
  }

  Pagemap pagemap_{0};
};

// See: https://www.kernel.org/doc/Documentation/vm/pagemap.txt for more
// information.
//
// We make sure that we only have 55 bits set of the PFN which is why we and it
// with 2^55 - 1.
#define PFN(VALUE)                                                \
  ((reinterpret_cast<uint64_t>(reinterpret_cast<void*>(VALUE))) & \
   ((static_cast<uint64_t>(1) << 55) - 1))
#define SOFT_DIRTY static_cast<uint64_t>(1) << 55
#define EXCLUSIVELY_MAPPED static_cast<uint64_t>(1) << 56
#define FILE_PAGE static_cast<uint64_t>(1) << 61
#define SWAPPED static_cast<uint64_t>(1) << 62
#define PRESENT static_cast<uint64_t>(1) << 63

#define PFN_FROM_TYPE_AND_OFFSET(SWAP_TYPE, OFFSET) \
  ((SWAP_TYPE & 0x1F) | (OFFSET << 5))

TEST_F(PagemapTest, Basic) {
  void* start_address;
  void* end_address;
  CreateStorageForPages(4, &start_address, &end_address);

  uint64_t page_map_entries[] = {PFN(8675309) | SOFT_DIRTY | PRESENT,
                                 PFN(1234) | SWAPPED,
                                 PFN(5551212) | SOFT_DIRTY | FILE_PAGE,
                                 PFN(0xF00) | PRESENT | EXCLUSIVELY_MAPPED};
  PutEntries(start_address, page_map_entries, countof(page_map_entries));

  // We now have 4 pages populated in the page map from start address to 4 *
  // pagesize();

  // Validate that the Pagemap class can interpret them correctly.
  std::vector<Pagemap::PagemapEntry> entries;
  entries.resize(countof(page_map_entries));

  ASSERT_TRUE(pagemap_.GetEntries(
      reinterpret_cast<uint64_t>(start_address),
      countof(page_map_entries) * base::GetPageSize(), &entries));

  ASSERT_EQ(
      PFN_FROM_TYPE_AND_OFFSET(entries[0].swap_type, entries[0].swap_offset),
      8675309u);
  ASSERT_EQ(entries[0].pte_soft_dirty, true);
  ASSERT_EQ(entries[0].page_present, true);
  ASSERT_EQ(entries[0].page_swapped, false);
  ASSERT_EQ(entries[0].page_exclusively_mapped, false);
  ASSERT_EQ(entries[0].page_file_or_shared_anon, false);

  ASSERT_EQ(
      PFN_FROM_TYPE_AND_OFFSET(entries[1].swap_type, entries[1].swap_offset),
      1234u);
  ASSERT_EQ(entries[1].pte_soft_dirty, false);
  ASSERT_EQ(entries[1].page_present, false);
  ASSERT_EQ(entries[1].page_swapped, true);
  ASSERT_EQ(entries[1].page_exclusively_mapped, false);
  ASSERT_EQ(entries[1].page_file_or_shared_anon, false);

  ASSERT_EQ(
      PFN_FROM_TYPE_AND_OFFSET(entries[2].swap_type, entries[2].swap_offset),
      5551212u);
  ASSERT_EQ(entries[2].pte_soft_dirty, true);
  ASSERT_EQ(entries[2].page_present, false);
  ASSERT_EQ(entries[2].page_swapped, false);
  ASSERT_EQ(entries[2].page_exclusively_mapped, false);
  ASSERT_EQ(entries[2].page_file_or_shared_anon, true);

  ASSERT_EQ(
      PFN_FROM_TYPE_AND_OFFSET(entries[3].swap_type, entries[3].swap_offset),
      0xF00u);
  ASSERT_EQ(entries[3].pte_soft_dirty, false);
  ASSERT_EQ(entries[3].page_present, true);
  ASSERT_EQ(entries[3].page_swapped, false);
  ASSERT_EQ(entries[3].page_exclusively_mapped, true);
  ASSERT_EQ(entries[3].page_file_or_shared_anon, false);
}

TEST_F(PagemapTest, MidRangeRead) {
  // This test will validate that we can read only a portion of the pages.
  void* start_address;
  void* end_address;
  CreateStorageForPages(4, &start_address, &end_address);

  uint64_t page_map_entries[] = {PFN(8675309) | SOFT_DIRTY | PRESENT,
                                 PFN(1234) | SWAPPED,
                                 PFN(5551212) | SOFT_DIRTY | FILE_PAGE,
                                 PFN(0xF00) | PRESENT | EXCLUSIVELY_MAPPED};
  PutEntries(start_address, page_map_entries, countof(page_map_entries));

  // We will read the two middle pages.
  std::vector<Pagemap::PagemapEntry> entries;
  ASSERT_TRUE(pagemap_.GetEntries(
      reinterpret_cast<uint64_t>(start_address) + base::GetPageSize(),
      2 * base::GetPageSize(), &entries));
  ASSERT_EQ(entries.size(), 2u);

  ASSERT_EQ(
      PFN_FROM_TYPE_AND_OFFSET(entries[0].swap_type, entries[0].swap_offset),
      1234u);
  ASSERT_EQ(entries[0].pte_soft_dirty, false);
  ASSERT_EQ(entries[0].page_present, false);
  ASSERT_EQ(entries[0].page_swapped, true);
  ASSERT_EQ(entries[0].page_exclusively_mapped, false);
  ASSERT_EQ(entries[0].page_file_or_shared_anon, false);

  ASSERT_EQ(
      PFN_FROM_TYPE_AND_OFFSET(entries[1].swap_type, entries[1].swap_offset),
      5551212u);
  ASSERT_EQ(entries[1].pte_soft_dirty, true);
  ASSERT_EQ(entries[1].page_present, false);
  ASSERT_EQ(entries[1].page_swapped, false);
  ASSERT_EQ(entries[1].page_exclusively_mapped, false);
  ASSERT_EQ(entries[1].page_file_or_shared_anon, true);
}

TEST_F(PagemapTest, VectorResizedWhenIncorrectlySized) {
  // This test validates that passing in an incorrectly sized vector is handled
  // automatically.
  void* start_address;
  void* end_address;
  CreateStorageForPages(4, &start_address, &end_address);

  uint64_t page_map_entries[] = {PFN(8675309) | SOFT_DIRTY | PRESENT,
                                 PFN(1234) | SWAPPED,
                                 PFN(5551212) | SOFT_DIRTY | FILE_PAGE,
                                 PFN(0xF00) | PRESENT | EXCLUSIVELY_MAPPED};
  PutEntries(start_address, page_map_entries, countof(page_map_entries));

  // We will read the two middle pages.
  std::vector<Pagemap::PagemapEntry> entries;
  entries.resize(1);  // This is too short, it will be automatically resized.

  ASSERT_TRUE(pagemap_.GetEntries(
      reinterpret_cast<uint64_t>(start_address) + base::GetPageSize(),
      2 * base::GetPageSize(), &entries));
  ASSERT_EQ(entries.size(), 2u);

  ASSERT_EQ(
      PFN_FROM_TYPE_AND_OFFSET(entries[0].swap_type, entries[0].swap_offset),
      1234u);
  ASSERT_EQ(entries[0].pte_soft_dirty, false);
  ASSERT_EQ(entries[0].page_present, false);
  ASSERT_EQ(entries[0].page_swapped, true);
  ASSERT_EQ(entries[0].page_exclusively_mapped, false);
  ASSERT_EQ(entries[0].page_file_or_shared_anon, false);

  ASSERT_EQ(
      PFN_FROM_TYPE_AND_OFFSET(entries[1].swap_type, entries[1].swap_offset),
      5551212u);
  ASSERT_EQ(entries[1].pte_soft_dirty, true);
  ASSERT_EQ(entries[1].page_present, false);
  ASSERT_EQ(entries[1].page_swapped, false);
  ASSERT_EQ(entries[1].page_exclusively_mapped, false);
  ASSERT_EQ(entries[1].page_file_or_shared_anon, true);
}

}  // namespace memory
}  // namespace ash