1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
base / allocator / partition_allocator / src / partition_alloc / thread_isolation / pkey_unittest.cc [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/buildflags.h"
#include "partition_alloc/partition_alloc_constants.h"
#include "partition_alloc/partition_root.h"
#include "partition_alloc/thread_isolation/thread_isolation.h"
#if PA_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.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 // PA_BUILDFLAG(ENABLE_THREAD_ISOLATION)