blob: 7e9ec5cd52080d224be3ccffaec3a136ccb86812 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "partition_alloc/address_pool_manager.h"
#include "partition_alloc/partition_alloc_buildflags.h"
#include "partition_alloc/partition_alloc_constants.h"
#include "partition_alloc/partition_root.h"
#include "partition_alloc/thread_isolation/thread_isolation.h"
#if BUILDFLAG(ENABLE_PKEYS)
#include <link.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include "partition_alloc/address_space_stats.h"
#include "partition_alloc/page_allocator.h"
#include "partition_alloc/page_allocator_constants.h"
#include "partition_alloc/partition_alloc.h"
#include "partition_alloc/partition_alloc_base/no_destructor.h"
#include "partition_alloc/partition_alloc_forward.h"
#include "partition_alloc/thread_isolation/pkey.h"
#include "testing/gtest/include/gtest/gtest.h"
#define ISOLATED_FUNCTION extern "C" __attribute__((used))
constexpr size_t kIsolatedThreadStackSize = 64 * 1024;
constexpr int kNumPkey = 16;
constexpr size_t kTestReturnValue = 0x8765432187654321llu;
constexpr uint32_t kPKRUAllowAccessNoWrite = 0b10101010101010101010101010101000;
namespace partition_alloc::internal {
struct PA_THREAD_ISOLATED_ALIGN IsolatedGlobals {
int pkey = kInvalidPkey;
void* stack;
partition_alloc::internal::base::NoDestructor<
partition_alloc::PartitionAllocator>
allocator{};
} isolated_globals;
int ProtFromSegmentFlags(ElfW(Word) flags) {
int prot = 0;
if (flags & PF_R) {
prot |= PROT_READ;
}
if (flags & PF_W) {
prot |= PROT_WRITE;
}
if (flags & PF_X) {
prot |= PROT_EXEC;
}
return prot;
}
int ProtectROSegments(struct dl_phdr_info* info, size_t info_size, void* data) {
if (!strcmp(info->dlpi_name, "linux-vdso.so.1")) {
return 0;
}
for (int i = 0; i < info->dlpi_phnum; i++) {
const ElfW(Phdr)* phdr = &info->dlpi_phdr[i];
if (phdr->p_type != PT_LOAD && phdr->p_type != PT_GNU_RELRO) {
continue;
}
if (phdr->p_flags & PF_W) {
continue;
}
uintptr_t start = info->dlpi_addr + phdr->p_vaddr;
uintptr_t end = start + phdr->p_memsz;
uintptr_t start_page = RoundDownToSystemPage(start);
uintptr_t end_page = RoundUpToSystemPage(end);
uintptr_t size = end_page - start_page;
PA_PCHECK(PkeyMprotect(reinterpret_cast<void*>(start_page), size,
ProtFromSegmentFlags(phdr->p_flags),
isolated_globals.pkey) == 0);
}
return 0;
}
class PkeyTest : public testing::Test {
protected:
static void PkeyProtectMemory() {
PA_PCHECK(dl_iterate_phdr(ProtectROSegments, nullptr) == 0);
PA_PCHECK(PkeyMprotect(&isolated_globals, sizeof(isolated_globals),
PROT_READ | PROT_WRITE, isolated_globals.pkey) == 0);
PA_PCHECK(PkeyMprotect(isolated_globals.stack, kIsolatedThreadStackSize,
PROT_READ | PROT_WRITE, isolated_globals.pkey) == 0);
}
static void InitializeIsolatedThread() {
isolated_globals.stack =
mmap(nullptr, kIsolatedThreadStackSize, PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE | MAP_STACK, -1, 0);
PA_PCHECK(isolated_globals.stack != MAP_FAILED);
PkeyProtectMemory();
}
void SetUp() override {
// SetUp only once, but we can't do it in SetUpTestSuite since that runs
// before other PartitionAlloc initialization happened.
if (isolated_globals.pkey != kInvalidPkey) {
return;
}
int pkey = PkeyAlloc(0);
if (pkey == -1) {
return;
}
isolated_globals.pkey = pkey;
isolated_globals.allocator->init([]() {
partition_alloc::PartitionOptions opts;
opts.aligned_alloc = PartitionOptions::kAllowed;
opts.thread_isolation = ThreadIsolationOption(isolated_globals.pkey);
return opts;
}());
InitializeIsolatedThread();
Wrpkru(kPKRUAllowAccessNoWrite);
}
static void TearDownTestSuite() {
if (isolated_globals.pkey == kInvalidPkey) {
return;
}
PA_PCHECK(PkeyMprotect(&isolated_globals, sizeof(isolated_globals),
PROT_READ | PROT_WRITE, kDefaultPkey) == 0);
isolated_globals.pkey = kDefaultPkey;
InitializeIsolatedThread();
PkeyFree(isolated_globals.pkey);
}
};
// This code will run with access limited to pkey 1, no default pkey access.
// Note that we're stricter than required for debugging purposes.
// In the final use, we'll likely allow at least read access to the default
// pkey.
ISOLATED_FUNCTION uint64_t IsolatedAllocFree(void* arg) {
char* buf = (char*)isolated_globals.allocator->root()
->Alloc<partition_alloc::AllocFlags::kNoHooks>(1024);
if (!buf) {
return 0xffffffffffffffffllu;
}
isolated_globals.allocator->root()->Free<FreeFlags::kNoHooks>(buf);
return kTestReturnValue;
}
// This test is a bit compliated. We want to ensure that the code
// allocating/freeing from the pkey pool doesn't *unexpectedly* access memory
// tagged with the default pkey (pkey 0). This could be a security issue since
// in our CFI threat model that memory might be attacker controlled.
// To test for this, we run alloc/free without access to the default pkey. In
// order to do this, we need to tag all global read-only memory with our pkey as
// well as switch to a pkey-tagged stack.
TEST_F(PkeyTest, AllocWithoutDefaultPkey) {
if (isolated_globals.pkey == kInvalidPkey) {
return;
}
uint64_t ret;
uint32_t pkru_value = 0;
for (int pkey = 0; pkey < kNumPkey; pkey++) {
if (pkey != isolated_globals.pkey) {
pkru_value |= (PKEY_DISABLE_ACCESS | PKEY_DISABLE_WRITE) << (2 * pkey);
}
}
// Switch to the safe stack with inline assembly.
//
// The simple solution would be to use one asm statement as a prologue to
// switch to the protected stack and a second one to switch it back. However,
// that doesn't work since inline assembly doesn't support a clobbered stack
// register. So instead, we switch the stack, perform a function call
// to the
// actual code and switch back afterwards.
//
// The inline asm docs mention that special care must be taken
// when calling a function in inline assembly. I.e. we will
// need to make sure that we follow the ABI of the platform.
// In this example, we use the System-V ABI.
//
// == Caller-saved registers ==
// We had two ideas for handling caller-saved registers. Option 1 was chosen,
// but I'll describe both to show why option 2 didn't work out:
// * Option 1) mark all caller-saved registers as clobbered. This should be
// in line with how the compiler would create the function call.
// Problem: future additions to caller-saved registers can break
// this.
// * Option 2) use attribute no_caller_saved_registers. This prohibits use of
// sse/mmx/x87. We can disable sse/mmx with a "target" attribute,
// but I couldn't find a way to disable x87.
// The docs tell you to use -mgeneral-regs-only. Maybe we
// could move the isolated code to a separate file and then
// use that flag for compiling that file only.
// !!! This doesn't work: the inner function can call out to code
// that uses caller-saved registers and won't save
// them itself.
//
// == stack alignment ==
// The ABI requires us to have a 16 byte aligned rsp on function
// entry. We push one qword onto the stack so we need to subtract
// an additional 8 bytes from the stack pointer.
//
// == additional clobbering ==
// As described above, we need to clobber everything besides
// callee-saved registers. The ABI requires all x87 registers to
// be set to empty on fn entry / return,
// so we should tell the compiler that this is the case. As I understand the
// docs, this is done by marking them as clobbered. Worst case, we'll notice
// any issues quickly and can fix them if it turned out to be false>
//
// == direction flag ==
// Theoretically, the DF flag could be set to 1 at asm entry. If this
// leads to problems, we might have to zero it before the fn call and
// restore it afterwards. I would'ave assumed that marking flags as
// clobbered would require the compiler to reset the DF before the next fn
// call, but that doesn't seem to be the case.
asm volatile(
// Set pkru to only allow access to pkey 1 memory.
".byte 0x0f,0x01,0xef\n" // wrpkru
// Move to the isolated stack and store the old value
"xchg %4, %%rsp\n"
"push %4\n"
"call IsolatedAllocFree\n"
// We need rax below, so move the return value to the stack
"push %%rax\n"
// Set pkru to only allow access to pkey 0 memory.
"mov $0b10101010101010101010101010101000, %%rax\n"
"xor %%rcx, %%rcx\n"
"xor %%rdx, %%rdx\n"
".byte 0x0f,0x01,0xef\n" // wrpkru
// Pop the return value
"pop %0\n"
// Restore the original stack
"pop %%rsp\n"
: "=r"(ret)
: "a"(pkru_value), "c"(0), "d"(0),
"r"(reinterpret_cast<uintptr_t>(isolated_globals.stack) +
kIsolatedThreadStackSize - 8)
: "memory", "cc", "r8", "r9", "r10", "r11", "xmm0", "xmm1", "xmm2",
"xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "xmm8", "xmm9", "xmm10",
"xmm11", "xmm12", "xmm13", "xmm14", "xmm15", "flags", "fpsr", "st",
"st(1)", "st(2)", "st(3)", "st(4)", "st(5)", "st(6)", "st(7)");
ASSERT_EQ(ret, kTestReturnValue);
}
class MockAddressSpaceStatsDumper : public AddressSpaceStatsDumper {
public:
MockAddressSpaceStatsDumper() = default;
void DumpStats(const AddressSpaceStats* address_space_stats) override {}
};
TEST_F(PkeyTest, DumpPkeyPoolStats) {
if (isolated_globals.pkey == kInvalidPkey) {
return;
}
MockAddressSpaceStatsDumper mock_stats_dumper;
partition_alloc::internal::AddressPoolManager::GetInstance().DumpStats(
&mock_stats_dumper);
}
} // namespace partition_alloc::internal
#endif // BUILDFLAG(ENABLE_THREAD_ISOLATION)