blob: 6780ac3a1e2b50c320eb0d6e3b78356fd7fcb4ec [file] [log] [blame]
// 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.
#include "chromeos/memory/userspace_swap/swap_storage.h"
#include <fcntl.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/vfs.h>
#include <unistd.h>
#include <cstring>
#include "base/bind.h"
#include "base/containers/span.h"
#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/no_destructor.h"
#include "base/numerics/checked_math.h"
#include "base/path_service.h"
#include "base/posix/eintr_wrapper.h"
#include "base/process/process_metrics.h"
#include "base/task/post_task.h"
#include "base/threading/scoped_blocking_call.h"
#include "crypto/aead.h"
#include "crypto/random.h"
#include "third_party/zlib/google/compression_utils.h"
namespace chromeos {
namespace memory {
namespace userspace_swap {
namespace {
// Adds a compression layer to a SwapFile.
class CompressedSwapFile : public SwapFile {
public:
~CompressedSwapFile() override;
// SwapFile impl:
bool WriteToSwap(const Region& src, Region* swap_region) override;
ssize_t ReadFromSwap(const Region& swap_region, const Region& dest) override;
protected:
friend class SwapFile;
friend class EncryptedCompressedSwapFile;
explicit CompressedSwapFile(base::ScopedFD fd);
// Compress will compress the region |src| into the region |dest| returning
// true if successful. Upon successful completion |compressed_size| will
// contain the number of compressed bytes written to |dest|.
static bool Compress(const Region& src,
const Region& dest,
size_t* compressed_size);
// Decompress will decompress the region |src| into |dest|. It is expected
// that |dest| be large enough to hold the decompressed buffer, the return
// value is the number of decompressed bytes.
static ssize_t Decompress(const Region& src, const Region& dest);
private:
DISALLOW_COPY_AND_ASSIGN(CompressedSwapFile);
};
// Adds an encryption layer to a SwapFile.
class EncryptedSwapFile : public SwapFile {
public:
~EncryptedSwapFile() override;
// SwapFile impl:
bool WriteToSwap(const Region& src, Region* swap_region) override;
ssize_t ReadFromSwap(const Region& swap_region, const Region& dest) override;
protected:
friend class SwapFile;
explicit EncryptedSwapFile(base::ScopedFD fd);
// This key and nonce are random and ephemeral.
crypto::Aead aead_;
std::vector<uint8_t> key_;
std::vector<uint8_t> nonce_;
private:
DISALLOW_COPY_AND_ASSIGN(EncryptedSwapFile);
};
// Adds a encryption layer to a compressed swap file.
class EncryptedCompressedSwapFile : public EncryptedSwapFile {
public:
~EncryptedCompressedSwapFile() override;
// SwapFile impl:
bool WriteToSwap(const Region& src, Region* swap_region) override;
ssize_t ReadFromSwap(const Region& swap_region, const Region& dest) override;
protected:
friend class SwapFile;
explicit EncryptedCompressedSwapFile(base::ScopedFD fd);
private:
DISALLOW_COPY_AND_ASSIGN(EncryptedCompressedSwapFile);
};
// Because for some inputs that aren't compressible it can result in a size
// that's slightly larger, we allow for this.
constexpr size_t kCompressionExtra = 32 << 10;
} // namespace
// Static
std::unique_ptr<SwapFile> SwapFile::Create(Type type) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
// We enforce that the file is encrypted.
CHECK(type & Type::kEncrypted);
base::FilePath directory;
if (!GetDirectoryForSwapFile(&directory)) {
return nullptr;
}
// We open the file with O_TMPFILE which creates an unnamed inode and anything
// written to the file will be lost when this fd is closed. O_EXCL prevents
// this file from being linked to the filesystem. Note, O_EXCL behaves
// differently because of O_TMPFILE. For more information on this see man 2
// open.
base::ScopedFD swap_fd(HANDLE_EINTR(
open(directory.value().c_str(), O_TMPFILE | O_EXCL | O_RDWR | O_CLOEXEC,
S_IRUSR | S_IWUSR)));
if (!swap_fd.is_valid()) {
PLOG(ERROR) << "Unable to open a temporary swap file in " << directory;
return nullptr;
}
return SwapFile::WrapFD(std::move(swap_fd), type);
}
std::unique_ptr<SwapFile> SwapFile::WrapFD(base::ScopedFD swap_fd, Type type) {
std::unique_ptr<SwapFile> swap;
if (type == (Type::kCompressed | Type::kEncrypted)) {
swap.reset(new EncryptedCompressedSwapFile(std::move(swap_fd)));
} else if (type == Type::kCompressed) {
swap.reset(new CompressedSwapFile(std::move(swap_fd)));
} else if (type == Type::kEncrypted) {
swap.reset(new EncryptedSwapFile(std::move(swap_fd)));
} else {
swap.reset(new SwapFile(std::move(swap_fd)));
}
return swap;
}
SwapFile::~SwapFile() = default;
SwapFile::SwapFile(base::ScopedFD fd) : fd_(std::move(fd)) {}
base::ScopedFD SwapFile::ReleaseFD() {
return std::move(fd_);
}
uint64_t SwapFile::GetUsageKB() const {
struct stat statbuf = {};
fstat(fd_.get(), &statbuf);
// fstat returns the number of 512byte blocks, we convert to KB.
return (statbuf.st_blocks * 512) >> 10;
}
// Static
bool SwapFile::GetDirectoryForSwapFile(base::FilePath* file_path) {
// We cache the file path so we don't have to repeatedly call these functions.
static base::FilePath cached_path = []() -> base::FilePath {
// We try to look for the unecrypted swap folder first, if it doesn't exist
// we will fall back the user's home directory. If that happens it means
// we're encrypted before writing to an encrypted file system so we log a
// warning.
const base::FilePath swap_folder(
"/mnt/stateful_partition/unencrypted/swap/");
if (base::PathExists(swap_folder)) {
return swap_folder;
}
LOG(WARNING) << "Swap folder " << swap_folder
<< " did not exist so userspace swap will be be disabled";
return base::FilePath();
}();
if (!cached_path.empty()) {
*file_path = cached_path;
return true;
}
return false;
}
// Static
uint64_t SwapFile::GetBackingStoreFreeSpaceKB() {
base::FilePath swap_file_dir;
if (!GetDirectoryForSwapFile(&swap_file_dir)) {
return 0;
}
struct statfs buf = {};
if (statfs(swap_file_dir.value().c_str(), &buf) < 0) {
PLOG(ERROR) << "Unable to get backing store space freespace for swap";
return 0;
}
// Convert number of blocks to KB.
return (buf.f_bavail * buf.f_bsize) >> 10;
}
bool SwapFile::WriteToSwap(const Region& src, Region* swap_region) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
// Writes are the only operations that cannot happen concurrently. Writes use
// write(2) and adjust the file pointer so this is a critical section. Reads
// and drops use position pread(2)/fallocate(2) and can safely be performed
// concurrently as they only access those regions and do not affect the file
// pointer.
base::AutoLock scoped_lock(write_lock_);
// We capture the current file pointer to determine where we started writing
// at.
DCHECK(swap_region);
swap_region->address = lseek(fd_.get(), 0, SEEK_CUR);
swap_region->length = 0;
while (swap_region->length < src.length) {
int bytes_written = HANDLE_EINTR(write(
fd_.get(), reinterpret_cast<char*>(src.address) + swap_region->length,
src.length - swap_region->length));
if (bytes_written <= 0) {
// We want the user to see errno from the write(2) call and not from
// lseek(2) should it also fail.
int write_failed_errno = errno;
// Seek back the file pointer as anything partially written is not
// tracked and would have been wasted file space.
lseek(fd_.get(), swap_region->address, SEEK_SET);
// We want the user to see errno from the write(2) call and not from
// lseek(2).
errno = write_failed_errno;
swap_region->address = 0;
swap_region->length = 0;
return false;
}
swap_region->length += bytes_written;
}
return true;
}
ssize_t SwapFile::ReadFromSwap(const Region& swap_region, const Region& dest) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
CHECK_EQ(swap_region.length, dest.length);
uint64_t bytes_read = 0;
while (bytes_read < swap_region.length) {
int64_t res = HANDLE_EINTR(pread(
fd_.get(), reinterpret_cast<char*>(dest.address) + bytes_read,
swap_region.length - bytes_read, swap_region.address + bytes_read));
if (res <= 0) {
return res;
}
bytes_read += res;
}
return bytes_read;
}
bool SwapFile::DropFromSwap(const Region& swap_region) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
int res = HANDLE_EINTR(fallocate(fd_.get(),
FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE,
swap_region.address, swap_region.length));
if (res < 0) {
return false;
}
return true;
}
CompressedSwapFile::~CompressedSwapFile() = default;
CompressedSwapFile::CompressedSwapFile(base::ScopedFD fd)
: SwapFile(std::move(fd)) {}
// Static
inline bool CompressedSwapFile::Compress(const Region& src,
const Region& dest,
size_t* compressed_size) {
return compression::GzipCompress(src.AsStringPiece(),
reinterpret_cast<char*>(dest.address),
dest.length, compressed_size,
/* malloc_fn= */ nullptr,
/* free_fn= */ nullptr);
}
bool CompressedSwapFile::WriteToSwap(const Region& src, Region* swap_region) {
// We use a larger buffer because in some situations the compression can
// actually be larger than the input, while this is very rare we allow for it.
uint64_t buf_size =
(base::CheckedNumeric<uint64_t>(src.length) + kCompressionExtra)
.ValueOrDie();
std::vector<uint8_t> compressed_buf(buf_size);
size_t compressed_size = 0;
// Compress src into compressed buf.
if (!CompressedSwapFile::Compress(src, Region(compressed_buf),
&compressed_size)) {
errno = EIO;
return false;
}
compressed_buf.resize(compressed_size);
// Now write our compressed buffer to disk.
return SwapFile::WriteToSwap(Region(compressed_buf), swap_region);
}
// Static
inline ssize_t CompressedSwapFile::Decompress(const Region& src,
const Region& dest) {
uint32_t uncompressed_size =
compression::GetUncompressedSize(src.AsStringPiece());
CHECK_EQ(dest.length, uncompressed_size);
if (!compression::GzipUncompress(src.AsStringPiece(), dest.AsStringPiece())) {
errno = EIO;
return -1;
}
return uncompressed_size;
}
ssize_t CompressedSwapFile::ReadFromSwap(const Region& swap_region,
const Region& dest) {
// Read from disk and then decompress directly into the buffer.
std::vector<uint8_t> compressed_buf(swap_region.length);
ssize_t read_res =
SwapFile::ReadFromSwap(swap_region, Region(compressed_buf));
if (read_res != static_cast<ssize_t>(swap_region.length)) {
return read_res;
}
compressed_buf.resize(read_res);
return CompressedSwapFile::Decompress(Region(compressed_buf), dest);
}
EncryptedSwapFile::~EncryptedSwapFile() {
memset(key_.data(), 0, key_.size());
memset(nonce_.data(), 0, nonce_.size());
}
EncryptedSwapFile::EncryptedSwapFile(base::ScopedFD fd)
: SwapFile(std::move(fd)), aead_(crypto::Aead::AES_256_GCM_SIV) {
key_.resize(aead_.KeyLength());
nonce_.resize(aead_.NonceLength());
CHECK_EQ(aead_.KeyLength(), key_.size());
CHECK_EQ(aead_.NonceLength(), nonce_.size());
crypto::RandBytes(nonce_);
crypto::RandBytes(key_);
aead_.Init(key_);
}
bool EncryptedSwapFile::WriteToSwap(const Region& src, Region* swap_region) {
std::vector<uint8_t> cipher_text =
aead_.Seal(src.AsSpan<const uint8_t>(), nonce_,
/* additional data */ base::span<const uint8_t>());
if (cipher_text.empty()) {
LOG(ERROR) << "Unable to encrypt region";
errno = EIO;
return false;
}
// Write the encrypted contents to disk.
return SwapFile::WriteToSwap(Region(cipher_text), swap_region);
}
ssize_t EncryptedSwapFile::ReadFromSwap(const Region& swap_region,
const Region& dest) {
// Start by reading the contents from the swap file and then decrypt it.
std::vector<uint8_t> cipher_text(swap_region.length);
ssize_t read_bytes = SwapFile::ReadFromSwap(swap_region, Region(cipher_text));
if (read_bytes != static_cast<ssize_t>(swap_region.length)) {
errno = EIO;
return -1;
}
cipher_text.resize(read_bytes);
base::Optional<std::vector<uint8_t>> decrypted =
aead_.Open(cipher_text, nonce_,
/* additional data */ base::span<const uint8_t>());
if (!decrypted) {
LOG(ERROR) << "Decryption failure";
errno = EIO;
return -1;
}
if (dest.length < decrypted.value().size()) {
LOG(ERROR) << "Decryption buffer too small";
errno = ENOMEM;
return -1;
}
memcpy(reinterpret_cast<void*>(dest.address), decrypted.value().data(),
decrypted.value().size());
return decrypted.value().size();
}
EncryptedCompressedSwapFile::~EncryptedCompressedSwapFile() = default;
EncryptedCompressedSwapFile::EncryptedCompressedSwapFile(base::ScopedFD fd)
: EncryptedSwapFile(std::move(fd)) {}
ssize_t EncryptedCompressedSwapFile::ReadFromSwap(const Region& swap_region,
const Region& dest) {
// First read from the encrypted swap file then decompress. Because
// compression may have resulted in a size which is larger than the original
// payload for some rare inputs we allow for this.
uint64_t buf_size =
(base::CheckedNumeric<uint64_t>(dest.length) + kCompressionExtra)
.ValueOrDie();
std::vector<uint8_t> compressed_buf(buf_size);
ssize_t read_res =
EncryptedSwapFile::ReadFromSwap(swap_region, Region(compressed_buf));
if (read_res == -1) {
PLOG(ERROR) << "Read failed " << read_res;
return read_res;
}
compressed_buf.resize(read_res);
// Decompress directly into the destination region.
return CompressedSwapFile::Decompress(Region(compressed_buf), dest);
}
bool EncryptedCompressedSwapFile::WriteToSwap(const Region& src,
Region* swap_region) {
// First compress the memory and then write to encrypted swap file.
// We use a larger buffer because in some situations the compression can
// actually be larger than the input, while this is very rare we allow for it.
uint64_t buf_size =
(base::CheckedNumeric<uint64_t>(src.length) + kCompressionExtra)
.ValueOrDie();
std::vector<uint8_t> compressed_buf(buf_size);
size_t compressed_size = 0;
if (!CompressedSwapFile::Compress(src, Region(compressed_buf),
&compressed_size)) {
return false;
}
compressed_buf.resize(compressed_size);
// Now write to the EncryptedSwapFile.
return EncryptedSwapFile::WriteToSwap(Region(compressed_buf), swap_region);
}
} // namespace userspace_swap
} // namespace memory
} // namespace chromeos