• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2022 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "partition_alloc/address_pool_manager.h"
6 #include "partition_alloc/partition_alloc_buildflags.h"
7 #include "partition_alloc/partition_alloc_constants.h"
8 #include "partition_alloc/partition_root.h"
9 #include "partition_alloc/thread_isolation/thread_isolation.h"
10 
11 #if BUILDFLAG(ENABLE_PKEYS)
12 
13 #include <link.h>
14 #include <sys/mman.h>
15 #include <sys/syscall.h>
16 
17 #include "partition_alloc/address_space_stats.h"
18 #include "partition_alloc/page_allocator.h"
19 #include "partition_alloc/page_allocator_constants.h"
20 #include "partition_alloc/partition_alloc.h"
21 #include "partition_alloc/partition_alloc_base/no_destructor.h"
22 #include "partition_alloc/partition_alloc_forward.h"
23 #include "partition_alloc/thread_isolation/pkey.h"
24 #include "testing/gtest/include/gtest/gtest.h"
25 
26 #define ISOLATED_FUNCTION extern "C" __attribute__((used))
27 constexpr size_t kIsolatedThreadStackSize = 64 * 1024;
28 constexpr int kNumPkey = 16;
29 constexpr size_t kTestReturnValue = 0x8765432187654321llu;
30 constexpr uint32_t kPKRUAllowAccessNoWrite = 0b10101010101010101010101010101000;
31 
32 namespace partition_alloc::internal {
33 
34 struct PA_THREAD_ISOLATED_ALIGN IsolatedGlobals {
35   int pkey = kInvalidPkey;
36   void* stack;
37   partition_alloc::internal::base::NoDestructor<
38       partition_alloc::PartitionAllocator>
39       allocator{};
40 } isolated_globals;
41 
ProtFromSegmentFlags(ElfW (Word)flags)42 int ProtFromSegmentFlags(ElfW(Word) flags) {
43   int prot = 0;
44   if (flags & PF_R) {
45     prot |= PROT_READ;
46   }
47   if (flags & PF_W) {
48     prot |= PROT_WRITE;
49   }
50   if (flags & PF_X) {
51     prot |= PROT_EXEC;
52   }
53   return prot;
54 }
55 
ProtectROSegments(struct dl_phdr_info * info,size_t info_size,void * data)56 int ProtectROSegments(struct dl_phdr_info* info, size_t info_size, void* data) {
57   if (!strcmp(info->dlpi_name, "linux-vdso.so.1")) {
58     return 0;
59   }
60   for (int i = 0; i < info->dlpi_phnum; i++) {
61     const ElfW(Phdr)* phdr = &info->dlpi_phdr[i];
62     if (phdr->p_type != PT_LOAD && phdr->p_type != PT_GNU_RELRO) {
63       continue;
64     }
65     if (phdr->p_flags & PF_W) {
66       continue;
67     }
68     uintptr_t start = info->dlpi_addr + phdr->p_vaddr;
69     uintptr_t end = start + phdr->p_memsz;
70     uintptr_t start_page = RoundDownToSystemPage(start);
71     uintptr_t end_page = RoundUpToSystemPage(end);
72     uintptr_t size = end_page - start_page;
73     PA_PCHECK(PkeyMprotect(reinterpret_cast<void*>(start_page), size,
74                            ProtFromSegmentFlags(phdr->p_flags),
75                            isolated_globals.pkey) == 0);
76   }
77   return 0;
78 }
79 
80 class PkeyTest : public testing::Test {
81  protected:
PkeyProtectMemory()82   static void PkeyProtectMemory() {
83     PA_PCHECK(dl_iterate_phdr(ProtectROSegments, nullptr) == 0);
84 
85     PA_PCHECK(PkeyMprotect(&isolated_globals, sizeof(isolated_globals),
86                            PROT_READ | PROT_WRITE, isolated_globals.pkey) == 0);
87 
88     PA_PCHECK(PkeyMprotect(isolated_globals.stack, kIsolatedThreadStackSize,
89                            PROT_READ | PROT_WRITE, isolated_globals.pkey) == 0);
90   }
91 
InitializeIsolatedThread()92   static void InitializeIsolatedThread() {
93     isolated_globals.stack =
94         mmap(nullptr, kIsolatedThreadStackSize, PROT_READ | PROT_WRITE,
95              MAP_ANONYMOUS | MAP_PRIVATE | MAP_STACK, -1, 0);
96     PA_PCHECK(isolated_globals.stack != MAP_FAILED);
97 
98     PkeyProtectMemory();
99   }
100 
SetUp()101   void SetUp() override {
102     // SetUp only once, but we can't do it in SetUpTestSuite since that runs
103     // before other PartitionAlloc initialization happened.
104     if (isolated_globals.pkey != kInvalidPkey) {
105       return;
106     }
107 
108     int pkey = PkeyAlloc(0);
109     if (pkey == -1) {
110       return;
111     }
112     isolated_globals.pkey = pkey;
113 
114     isolated_globals.allocator->init([]() {
115       partition_alloc::PartitionOptions opts;
116       opts.aligned_alloc = PartitionOptions::kAllowed;
117       opts.thread_isolation = ThreadIsolationOption(isolated_globals.pkey);
118       return opts;
119     }());
120 
121     InitializeIsolatedThread();
122 
123     Wrpkru(kPKRUAllowAccessNoWrite);
124   }
125 
TearDownTestSuite()126   static void TearDownTestSuite() {
127     if (isolated_globals.pkey == kInvalidPkey) {
128       return;
129     }
130     PA_PCHECK(PkeyMprotect(&isolated_globals, sizeof(isolated_globals),
131                            PROT_READ | PROT_WRITE, kDefaultPkey) == 0);
132     isolated_globals.pkey = kDefaultPkey;
133     InitializeIsolatedThread();
134     PkeyFree(isolated_globals.pkey);
135   }
136 };
137 
138 // This code will run with access limited to pkey 1, no default pkey access.
139 // Note that we're stricter than required for debugging purposes.
140 // In the final use, we'll likely allow at least read access to the default
141 // pkey.
IsolatedAllocFree(void * arg)142 ISOLATED_FUNCTION uint64_t IsolatedAllocFree(void* arg) {
143   char* buf = (char*)isolated_globals.allocator->root()
144                   ->Alloc<partition_alloc::AllocFlags::kNoHooks>(1024);
145   if (!buf) {
146     return 0xffffffffffffffffllu;
147   }
148   isolated_globals.allocator->root()->Free<FreeFlags::kNoHooks>(buf);
149 
150   return kTestReturnValue;
151 }
152 
153 // This test is a bit compliated. We want to ensure that the code
154 // allocating/freeing from the pkey pool doesn't *unexpectedly* access memory
155 // tagged with the default pkey (pkey 0). This could be a security issue since
156 // in our CFI threat model that memory might be attacker controlled.
157 // To test for this, we run alloc/free without access to the default pkey. In
158 // order to do this, we need to tag all global read-only memory with our pkey as
159 // well as switch to a pkey-tagged stack.
TEST_F(PkeyTest,AllocWithoutDefaultPkey)160 TEST_F(PkeyTest, AllocWithoutDefaultPkey) {
161   if (isolated_globals.pkey == kInvalidPkey) {
162     return;
163   }
164 
165   uint64_t ret;
166   uint32_t pkru_value = 0;
167   for (int pkey = 0; pkey < kNumPkey; pkey++) {
168     if (pkey != isolated_globals.pkey) {
169       pkru_value |= (PKEY_DISABLE_ACCESS | PKEY_DISABLE_WRITE) << (2 * pkey);
170     }
171   }
172 
173   // Switch to the safe stack with inline assembly.
174   //
175   // The simple solution would be to use one asm statement as a prologue to
176   // switch to the protected stack and a second one to switch it back. However,
177   // that doesn't work since inline assembly doesn't support a clobbered stack
178   // register. So instead, we switch the stack, perform a function call
179   // to the
180   // actual code and switch back afterwards.
181   //
182   // The inline asm docs mention that special care must be taken
183   // when calling a function in inline assembly. I.e. we will
184   // need to make sure that we follow the ABI of the platform.
185   // In this example, we use the System-V ABI.
186   //
187   // == Caller-saved registers ==
188   // We had two ideas for handling caller-saved registers. Option 1 was chosen,
189   // but I'll describe both to show why option 2 didn't work out:
190   // * Option 1) mark all caller-saved registers as clobbered. This should be
191   //             in line with how the compiler would create the function call.
192   //             Problem: future additions to caller-saved registers can break
193   //             this.
194   // * Option 2) use attribute no_caller_saved_registers. This prohibits use of
195   //             sse/mmx/x87. We can disable sse/mmx with a "target" attribute,
196   //             but I couldn't find a way to disable x87.
197   //             The docs tell you to use -mgeneral-regs-only. Maybe we
198   //             could move the isolated code to a separate file and then
199   //             use that flag for compiling that file only.
200   //             !!! This doesn't work: the inner function can call out to code
201   //             that uses caller-saved registers and won't save
202   //             them itself.
203   //
204   // == stack alignment ==
205   // The ABI requires us to have a 16 byte aligned rsp on function
206   // entry. We push one qword onto the stack so we need to subtract
207   // an additional 8 bytes from the stack pointer.
208   //
209   // == additional clobbering ==
210   // As described above, we need to clobber everything besides
211   // callee-saved registers. The ABI requires all x87 registers to
212   // be set to empty on fn entry / return,
213   // so we should tell the compiler that this is the case. As I understand the
214   // docs, this is done by marking them as clobbered. Worst case, we'll notice
215   // any issues quickly and can fix them if it turned out to be false>
216   //
217   // == direction flag ==
218   // Theoretically, the DF flag could be set to 1 at asm entry. If this
219   // leads to problems, we might have to zero it before the fn call and
220   // restore it afterwards. I would'ave assumed that marking flags as
221   // clobbered would require the compiler to reset the DF before the next fn
222   // call, but that doesn't seem to be the case.
223   asm volatile(
224       // Set pkru to only allow access to pkey 1 memory.
225       ".byte 0x0f,0x01,0xef\n"  // wrpkru
226 
227       // Move to the isolated stack and store the old value
228       "xchg %4, %%rsp\n"
229       "push %4\n"
230       "call IsolatedAllocFree\n"
231       // We need rax below, so move the return value to the stack
232       "push %%rax\n"
233 
234       // Set pkru to only allow access to pkey 0 memory.
235       "mov $0b10101010101010101010101010101000, %%rax\n"
236       "xor %%rcx, %%rcx\n"
237       "xor %%rdx, %%rdx\n"
238       ".byte 0x0f,0x01,0xef\n"  // wrpkru
239 
240       // Pop the return value
241       "pop %0\n"
242       // Restore the original stack
243       "pop %%rsp\n"
244 
245       : "=r"(ret)
246       : "a"(pkru_value), "c"(0), "d"(0),
247         "r"(reinterpret_cast<uintptr_t>(isolated_globals.stack) +
248             kIsolatedThreadStackSize - 8)
249       : "memory", "cc", "r8", "r9", "r10", "r11", "xmm0", "xmm1", "xmm2",
250         "xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "xmm8", "xmm9", "xmm10",
251         "xmm11", "xmm12", "xmm13", "xmm14", "xmm15", "flags", "fpsr", "st",
252         "st(1)", "st(2)", "st(3)", "st(4)", "st(5)", "st(6)", "st(7)");
253 
254   ASSERT_EQ(ret, kTestReturnValue);
255 }
256 
257 class MockAddressSpaceStatsDumper : public AddressSpaceStatsDumper {
258  public:
259   MockAddressSpaceStatsDumper() = default;
DumpStats(const AddressSpaceStats * address_space_stats)260   void DumpStats(const AddressSpaceStats* address_space_stats) override {}
261 };
262 
TEST_F(PkeyTest,DumpPkeyPoolStats)263 TEST_F(PkeyTest, DumpPkeyPoolStats) {
264   if (isolated_globals.pkey == kInvalidPkey) {
265     return;
266   }
267 
268   MockAddressSpaceStatsDumper mock_stats_dumper;
269   partition_alloc::internal::AddressPoolManager::GetInstance().DumpStats(
270       &mock_stats_dumper);
271 }
272 
273 }  // namespace partition_alloc::internal
274 
275 #endif  // BUILDFLAG(ENABLE_THREAD_ISOLATION)
276