Commit 0b813507 authored by Brian Geffon's avatar Brian Geffon Committed by Commit Bot

CrOS: Add a swap storage interface/implementation.

This adds a Swap File interface which is a generic interface
which contains operations to write to, read from, and drop
contents from a swap file. We also add three different swap
file implementations on top of the standard swap file, which
include a compressed, encrypted, and compressed + encrypted swap
file.

The compressed swap file implementation is a standard gzip
implementation. The encrypted swap file uses AES 256 GCM SIV
mode with a 256bit ephemeral key which is randomly generated
and a randomly generated nonce. All swap files will write to
/mnt/stateful_partition/unencrypted/swap if it exists otherwise
swap file creation will fail. The key never leaves the internal
EncryptedSwapFile implementation.

The swap files are created as O_TMPFILE | O_EXCL meaning they
do not actually link and O_EXCL prevents them being linked to
the file system, so all space is reclaimed when the file
descriptor is closed.

Disk space can be incrementally reclaimed after memory has
been read out of the swap file using the DropFromSwap API.
It uses fallocate(2) with FALLOC_FL_PUNCH_HOLE.

Bug: 1067833
Change-Id: Ifeacec0ed5571507f80add2cf43fdd4063478459
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2142474Reviewed-by: default avatarIlya Sherman <isherman@chromium.org>
Reviewed-by: default avatarSteven Bennetts <stevenjb@chromium.org>
Reviewed-by: default avatarRobert Sesek <rsesek@chromium.org>
Reviewed-by: default avatarMatthew Denton <mpdenton@chromium.org>
Commit-Queue: Brian Geffon <bgeffon@chromium.org>
Cr-Commit-Position: refs/heads/master@{#759044}
parent df8111a3
...@@ -46,9 +46,12 @@ component("chromeos") { ...@@ -46,9 +46,12 @@ component("chromeos") {
"//chromeos/dbus", "//chromeos/dbus",
"//chromeos/dbus/constants", "//chromeos/dbus/constants",
"//components/policy/proto", "//components/policy/proto",
"//crypto",
"//crypto:platform",
"//google_apis", "//google_apis",
"//services/network/public/cpp:cpp", "//services/network/public/cpp:cpp",
"//third_party/protobuf:protobuf_lite", "//third_party/protobuf:protobuf_lite",
"//third_party/zlib/google:compression_utils",
] ]
sources = [ sources = [
"hugepage_text/hugepage_text.cc", "hugepage_text/hugepage_text.cc",
...@@ -61,6 +64,9 @@ component("chromeos") { ...@@ -61,6 +64,9 @@ component("chromeos") {
"memory/pagemap.h", "memory/pagemap.h",
"memory/swap_configuration.cc", "memory/swap_configuration.cc",
"memory/swap_configuration.h", "memory/swap_configuration.h",
"memory/userspace_swap/region.h",
"memory/userspace_swap/swap_storage.cc",
"memory/userspace_swap/swap_storage.h",
"policy/weekly_time/time_utils.cc", "policy/weekly_time/time_utils.cc",
"policy/weekly_time/time_utils.h", "policy/weekly_time/time_utils.h",
"policy/weekly_time/weekly_time.cc", "policy/weekly_time/weekly_time.cc",
...@@ -184,6 +190,8 @@ test("chromeos_unittests") { ...@@ -184,6 +190,8 @@ test("chromeos_unittests") {
"//components/policy/proto", "//components/policy/proto",
"//components/prefs:test_support", "//components/prefs:test_support",
"//components/proxy_config", "//components/proxy_config",
"//crypto",
"//crypto:platform",
"//dbus:test_support", "//dbus:test_support",
"//google_apis", "//google_apis",
"//mojo/core/embedder", "//mojo/core/embedder",
...@@ -198,6 +206,7 @@ test("chromeos_unittests") { ...@@ -198,6 +206,7 @@ test("chromeos_unittests") {
] ]
sources = [ sources = [
"memory/pagemap_unittest.cc", "memory/pagemap_unittest.cc",
"memory/userspace_swap/swap_storage_unittest.cc",
"policy/weekly_time/time_utils_unittest.cc", "policy/weekly_time/time_utils_unittest.cc",
"policy/weekly_time/weekly_time_interval_unittest.cc", "policy/weekly_time/weekly_time_interval_unittest.cc",
"policy/weekly_time/weekly_time_unittest.cc", "policy/weekly_time/weekly_time_unittest.cc",
......
# These deps should be only the required deps for //chromeos/memory
include_rules = [
"+third_party/zlib/google",
]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROMEOS_MEMORY_USERSPACE_SWAP_REGION_H_
#define CHROMEOS_MEMORY_USERSPACE_SWAP_REGION_H_
#include <sys/uio.h>
#include <cstdint>
#include <vector>
#include "base/containers/span.h"
#include "base/numerics/checked_math.h"
#include "base/strings/string_piece.h"
namespace chromeos {
namespace memory {
namespace userspace_swap {
// A region describes a block of memory.
class Region {
public:
uintptr_t address = 0;
uintptr_t length = 0;
Region() = default;
Region(Region&&) = default;
Region(const Region&) = default;
Region& operator=(const Region&) = default;
Region& operator=(Region&&) = default;
// To avoid requiring callers to cast pointers or integral types to Regions,
// we're flexible and allow any pointer type or any integral type. We convert
// them to the types needed for a Region. This simplifies calling code
// tremendously. Static asserts enforce that the types are valid.
template <typename Address, typename Length>
Region(Address* address, Length length)
: address(reinterpret_cast<uintptr_t>(const_cast<Address*>(address))),
length(length) {
static_assert(std::is_integral<Length>::value,
"length must be an integral type");
static_assert(sizeof(Length) <= sizeof(uintptr_t),
"Length cannot be longer than uint64_t");
// Verify that the end of this region is valid and wouldn't overflow if we
// added length to the address.
CHECK((base::CheckedNumeric<uintptr_t>(this->address) + this->length)
.IsValid());
}
template <typename Address, typename Length>
Region(Address address, Length length)
: Region(reinterpret_cast<void*>(address), length) {
static_assert(sizeof(Address) <= sizeof(void*),
"Address cannot be longer than a pointer type");
}
template <typename Address>
Region(Address address) : Region(address, 1) {
static_assert(
std::is_integral<Address>::value || std::is_pointer<Address>::value,
"Adress must be integral or pointer type");
}
template <typename T>
Region(const std::vector<T>& vec)
: Region(vec.data(), vec.size() * sizeof(T)) {}
// AsIovec will return the iovec representation of this Region.
struct iovec AsIovec() const {
return {.iov_base = reinterpret_cast<void*>(address), .iov_len = length};
}
base::StringPiece AsStringPiece() const {
return base::StringPiece(reinterpret_cast<char*>(address), length);
}
template <typename T>
base::span<T> AsSpan() const {
return base::span<T>(reinterpret_cast<T*>(address), length);
}
bool operator<(const Region& other) const {
// Because the standard library treats equality as !less(a,b) && !less(b,a)
// our definition of less than will be that this has to be FULLY before
// other. Overlapping regions are not allowed and are explicitly checked
// before inserting by using find() any overlap would return equal, this
// also has the property that you can search for a Region of length 1 to
// find the mapping for a fault.
return ((address + length - 1) < other.address);
}
};
} // namespace userspace_swap
} // namespace memory
} // namespace chromeos
#endif // CHROMEOS_MEMORY_USERSPACE_SWAP_REGION_H_
This diff is collapsed.
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROMEOS_MEMORY_USERSPACE_SWAP_SWAP_STORAGE_H_
#define CHROMEOS_MEMORY_USERSPACE_SWAP_SWAP_STORAGE_H_
#include "base/files/file_path.h"
#include "base/files/scoped_file.h"
#include "base/macros.h"
#include "base/memory/weak_ptr.h"
#include "base/synchronization/lock.h"
#include "chromeos/chromeos_export.h"
#include "chromeos/memory/userspace_swap/region.h"
namespace chromeos {
namespace memory {
namespace userspace_swap {
// SwapFile is the implementation for a disk backed swap file. This class is
// thread safe as synchronization is handled internally where necessary.
class CHROMEOS_EXPORT SwapFile {
public:
virtual ~SwapFile();
enum Type {
// kStandard is a normal file without compression or encryption.
kStandard = 0,
// kCompressed is an optional compression layer.
kCompressed = (1 << 1),
// kEncrypted is an optional encryption layer.
kEncrypted = (1 << 2),
// You can use both modes with a bitwise or kCompressed | kEncrypted.
};
// Create a new swap file, this can only be called from the browser as
// renderer seccomp policies would not allow it. Note: kEncrypted is
// required for ALL swap files, this call will fail without kEncrypted.
static std::unique_ptr<SwapFile> Create(Type type);
// GetBackingStoreFreeSpaceKB() returns the number of KB free on the backing
// device.
static uint64_t GetBackingStoreFreeSpaceKB();
// WriteToSwap will write a memory region from |source| into the swap file.
// Upon successful completion the method will return true and |swap_region|
// will contain the Region for where it was written in swap. This method will
// return false on error and errno will be set.
virtual bool WriteToSwap(const Region& source, Region* swap_region);
// ReadFromSwap reads the |swap_region| from the swap file writing it into
// |dest|. The return value is the number of bytes read from the swap file. On
// error ReadFromSwap will return -1 and errno will be set.
virtual ssize_t ReadFromSwap(const Region& swap_region, const Region& dest);
// DropFromSwap can be used to reclaim the disk blocks for |swap_region|, it
// punches a hole in the file to accomplish this and may not immediately be
// reflected when the block is still partially in use by another region. This
// method will return false on failure and errno will be set.
virtual bool DropFromSwap(const Region& swap_region);
// GetUsageKB returns the number of KiB the swap file is currently using on
// disk.
virtual uint64_t GetUsageKB() const;
// ReleaseFD is used for donating the internal fd.
base::ScopedFD ReleaseFD();
protected:
static bool GetDirectoryForSwapFile(base::FilePath* file_path);
// Given an FD to an already open swap file wrap it into a SwapFile class,
// this is primarily for ease of testing each implementation.
static std::unique_ptr<SwapFile> WrapFD(base::ScopedFD swap_fd, Type type);
explicit SwapFile(base::ScopedFD fd);
base::ScopedFD fd_;
private:
friend class SwapStorageTest;
// We use this lock to serialize WriteToSwap calls. (Concurrent reads and
// drops are safe, because they use syscalls which do not rely on the file
// pointer, specifically pread(2) and fallocate(2) respectively).
base::Lock write_lock_;
DISALLOW_COPY_AND_ASSIGN(SwapFile);
};
} // namespace userspace_swap
} // namespace memory
} // namespace chromeos
#endif // CHROMEOS_MEMORY_USERSPACE_SWAP_SWAP_STORAGE_H_
// Copyright (c) 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chromeos/memory/userspace_swap/swap_storage.h"
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <chrono>
#include <random>
#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/logging.h"
#include "base/posix/eintr_wrapper.h"
#include "chromeos/memory/userspace_swap/region.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace chromeos {
namespace memory {
namespace userspace_swap {
class SwapStorageTest : public testing::Test,
public testing::WithParamInterface<SwapFile::Type> {
public:
SwapStorageTest()
: random_(/* seen prng */
std::chrono::system_clock::now().time_since_epoch().count()) {}
protected:
void SetUp() override {
// Start by creating a temporary file we will use for this test and then
// wrapping it in the swap type we're testing.
base::FilePath temp_file;
ASSERT_TRUE(base::CreateTemporaryFile(&temp_file));
base::ScopedFD swap_fd;
swap_fd.reset(HANDLE_EINTR(open(temp_file.MaybeAsASCII().c_str(),
O_RDWR | O_CLOEXEC, S_IRUSR | S_IWUSR)));
ASSERT_NE(unlink(temp_file.MaybeAsASCII().c_str()), -1);
ASSERT_TRUE(swap_fd.is_valid());
// Wrap this FD in the swap file type for this test.
swap_ = SwapFile::WrapFD(std::move(swap_fd), GetParam());
ASSERT_NE(swap_, nullptr);
}
void FillRandom(std::string* str, int size) {
for (int j = 0; j < size; ++j) {
str->append(1, static_cast<char>(random_() % 255));
}
ASSERT_EQ(str->size(), static_cast<std::string::size_type>(size));
}
std::default_random_engine random_;
std::unique_ptr<SwapFile> swap_;
};
// We test all the different variations of a swap file to make sure all
// functionality is properly implemented, this will test:
// - Standard swap file with no compression or encryption.
// - A compressed swap file.
// - An encrypted swap file.
// - A compressed and encrypted swap file.
INSTANTIATE_TEST_SUITE_P(
SwapStorageTest,
SwapStorageTest,
testing::Values(
SwapFile::Type::kStandard /* no compression or encryption */,
SwapFile::Type::kCompressed,
SwapFile::Type::kEncrypted,
SwapFile::Type::kEncrypted | SwapFile::Type::kCompressed));
TEST_P(SwapStorageTest, SimpleWriteRead) {
std::string buffer = "hello world";
// Swap address can be at index 0 so we use UINT64_MAX to differentiate.
Region swap_region(std::numeric_limits<uint64_t>::max(), 0);
// Write it to swap and validate we also got back sane values for swap pos and
// length.
ASSERT_TRUE(swap_->WriteToSwap(Region(buffer.c_str(), buffer.length()),
&swap_region));
ASSERT_NE(swap_region.address, std::numeric_limits<uint64_t>::max());
ASSERT_NE(swap_region.length, 0u);
// Read the region from swap in [swap_pos, swap_pos + swap_len]
char read_buf[buffer.length()];
memset(read_buf, 0, sizeof(read_buf));
ASSERT_EQ(
swap_->ReadFromSwap(swap_region, Region(read_buf, sizeof(read_buf))),
static_cast<ssize_t>(sizeof(read_buf)));
// We should have correctly read back what we wrote.
ASSERT_EQ(memcmp(read_buf, buffer.c_str(), sizeof(read_buf)), 0);
}
TEST_P(SwapStorageTest, ManyWriteRead) {
// Write 1000 random length buffers and then read them back in a random order
// and make sure they are as expected.
std::vector<std::pair<std::string, Region>> buffers;
constexpr int kNumBuffers = 1000;
buffers.reserve(kNumBuffers);
for (int i = 0; i < kNumBuffers; ++i) {
// Choose a random length between 1 byte and 10KB
int buffer_len = (random_() % (10 << 10)) + 1;
std::string buf;
buf.reserve(buffer_len);
FillRandom(&buf, buffer_len);
// Swap pos can be at index 0 so we use UINT64_MAX to differentiate.
Region swap_region(std::numeric_limits<uint64_t>::max(), 0);
// Write it to swap and validate we also got back sane values for swap pos
// and length.
ASSERT_TRUE(
swap_->WriteToSwap(Region(buf.c_str(), buf.size()), &swap_region));
ASSERT_NE(swap_region.address, std::numeric_limits<uint64_t>::max());
ASSERT_NE(swap_region.length, 0u);
// Save where this buffer was written.
buffers.emplace_back(std::move(buf), swap_region);
}
// Shuffle the ordering of the buffers so we read them back in a random order.
std::shuffle(buffers.begin(), buffers.end(), random_);
// Read back all the regions and verify.
for (const auto& buf : buffers) {
char read_buf[buf.first.size()];
memset(read_buf, 0, sizeof(read_buf));
ASSERT_EQ(swap_->ReadFromSwap(/* Region */ buf.second,
Region(read_buf, sizeof(read_buf))),
static_cast<ssize_t>(sizeof(read_buf)));
// We should have correctly read back what we wrote.
ASSERT_EQ(memcmp(read_buf, buf.first.c_str(), sizeof(read_buf)), 0);
// Now drop it from the swap.
ASSERT_TRUE(swap_->DropFromSwap(/* Region */ buf.second));
}
}
TEST_P(SwapStorageTest, DropFromSwap) {
// This test validates that we can drop what we wrote from the swap file and
// the block size will return to the original size.
uint64_t block_size_kb_before = swap_->GetUsageKB();
std::string buffer;
// We want to fill buffer with what should be many blocks of random data so we
// can fully observe the growing and shrinking size.
FillRandom(&buffer, 32 * (4 << 10));
// Swap pos can be at index 0 so we use UINT64_MAX to differentiate.
Region swap_region(std::numeric_limits<uint64_t>::max(), 0);
// Write it to swap and validate we also got back sane values for swap pos and
// length.
ASSERT_TRUE(
swap_->WriteToSwap(Region(buffer.c_str(), buffer.size()), &swap_region));
ASSERT_NE(swap_region.address, std::numeric_limits<uint64_t>::max());
ASSERT_NE(swap_region.length, 0u);
uint64_t block_size_kb = swap_->GetUsageKB();
ASSERT_GT(block_size_kb, block_size_kb_before);
// Read the region from swap in [swap_pos, swap_pos + swap_len]
char read_buf[buffer.length()];
memset(read_buf, 0, sizeof(read_buf));
ASSERT_EQ(
swap_->ReadFromSwap(swap_region, Region(read_buf, sizeof(read_buf))),
static_cast<ssize_t>(sizeof(read_buf)));
// We should have correctly read back what we wrote.
ASSERT_EQ(memcmp(read_buf, buffer.c_str(), sizeof(read_buf)), 0);
// Now we will drop it.
ASSERT_TRUE(swap_->DropFromSwap(swap_region));
// Finally check the size.
uint64_t block_size_kb_end = swap_->GetUsageKB();
ASSERT_LT(block_size_kb_end, block_size_kb);
}
} // namespace userspace_swap
} // namespace memory
} // namespace chromeos
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment