← Documents Documentation/arch/x86/pti.rst GitHub 원문 ↗

Linux 6.18.37 · Architecture

Page Table Isolation (PTI)

PTI의 user/kernel page table 분리, 비용, 시험과 crash signature를 설명합니다.

Source pathDocumentation/arch/x86/pti.rst
Source versionLinux v6.18.37
TranslationDUJINLABS 전문 번역 + 해설

요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.

1. 요약·해설

원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.

요약과 해설

pti.rst:1-193

PTI는 userspace 실행 중 최소 kernel entry data만 보이는 별도 page table을 사용하고 syscall·interrupt·exception에서 full kernel CR3로 전환해 Meltdown 계열 paging side channel을 완화합니다.

대가는 process별 추가 PGD, 2MB `cpu_entry_area`, entry/exit CR3 write와 TLB 관리입니다. PCID는 전체 TLB flush를 줄이지만 user·kernel PCID의 일관된 invalidation이 필요합니다.

2. 영어 원문 전체

번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.

원문 전체 펼치기
1 .. SPDX-License-Identifier: GPL-2.0
2
3 ==========================
4 Page Table Isolation (PTI)
5 ==========================
6
7 Overview
8 ========
9
10 Page Table Isolation (pti, previously known as KAISER [1]_) is a
11 countermeasure against attacks on the shared user/kernel address
12 space such as the "Meltdown" approach [2]_.
13
14 To mitigate this class of attacks, we create an independent set of
15 page tables for use only when running userspace applications. When
16 the kernel is entered via syscalls, interrupts or exceptions, the
17 page tables are switched to the full "kernel" copy. When the system
18 switches back to user mode, the user copy is used again.
19
20 The userspace page tables contain only a minimal amount of kernel
21 data: only what is needed to enter/exit the kernel such as the
22 entry/exit functions themselves and the interrupt descriptor table
23 (IDT). There are a few strictly unnecessary things that get mapped
24 such as the first C function when entering an interrupt (see
25 comments in pti.c).
26
27 This approach helps to ensure that side-channel attacks leveraging
28 the paging structures do not function when PTI is enabled. It can be
29 enabled by setting CONFIG_MITIGATION_PAGE_TABLE_ISOLATION=y at compile
30 time. Once enabled at compile-time, it can be disabled at boot with
31 the 'nopti' or 'pti=' kernel parameters (see kernel-parameters.txt).
32
33 Page Table Management
34 =====================
35
36 When PTI is enabled, the kernel manages two sets of page tables.
37 The first set is very similar to the single set which is present in
38 kernels without PTI. This includes a complete mapping of userspace
39 that the kernel can use for things like copy_to_user().
40
41 Although _complete_, the user portion of the kernel page tables is
42 crippled by setting the NX bit in the top level. This ensures
43 that any missed kernel->user CR3 switch will immediately crash
44 userspace upon executing its first instruction.
45
46 The userspace page tables map only the kernel data needed to enter
47 and exit the kernel. This data is entirely contained in the 'struct
48 cpu_entry_area' structure which is placed in the fixmap which gives
49 each CPU's copy of the area a compile-time-fixed virtual address.
50
51 For new userspace mappings, the kernel makes the entries in its
52 page tables like normal. The only difference is when the kernel
53 makes entries in the top (PGD) level. In addition to setting the
54 entry in the main kernel PGD, a copy of the entry is made in the
55 userspace page tables' PGD.
56
57 This sharing at the PGD level also inherently shares all the lower
58 layers of the page tables. This leaves a single, shared set of
59 userspace page tables to manage. One PTE to lock, one set of
60 accessed bits, dirty bits, etc...
61
62 Overhead
63 ========
64
65 Protection against side-channel attacks is important. But,
66 this protection comes at a cost:
67
68 1. Increased Memory Use
69
70 a. Each process now needs an order-1 PGD instead of order-0.
71 (Consumes an additional 4k per process).
72 b. The 'cpu_entry_area' structure must be 2MB in size and 2MB
73 aligned so that it can be mapped by setting a single PMD
74 entry. This consumes nearly 2MB of RAM once the kernel
75 is decompressed, but no space in the kernel image itself.
76
77 2. Runtime Cost
78
79 a. CR3 manipulation to switch between the page table copies
80 must be done at interrupt, syscall, and exception entry
81 and exit (it can be skipped when the kernel is interrupted,
82 though.) Moves to CR3 are on the order of a hundred
83 cycles, and are required at every entry and exit.
84 b. Percpu TSS is mapped into the user page tables to allow SYSCALL64 path
85 to work under PTI. This doesn't have a direct runtime cost but it can
86 be argued it opens certain timing attack scenarios.
87 c. Global pages are disabled for all kernel structures not
88 mapped into both kernel and userspace page tables. This
89 feature of the MMU allows different processes to share TLB
90 entries mapping the kernel. Losing the feature means more
91 TLB misses after a context switch. The actual loss of
92 performance is very small, however, never exceeding 1%.
93 d. Process Context IDentifiers (PCID) is a CPU feature that
94 allows us to skip flushing the entire TLB when switching page
95 tables by setting a special bit in CR3 when the page tables
96 are changed. This makes switching the page tables (at context
97 switch, or kernel entry/exit) cheaper. But, on systems with
98 PCID support, the context switch code must flush both the user
99 and kernel entries out of the TLB. The user PCID TLB flush is
100 deferred until the exit to userspace, minimizing the cost.
101 See intel.com/sdm for the gory PCID/INVPCID details.
102 e. The userspace page tables must be populated for each new
103 process. Even without PTI, the shared kernel mappings
104 are created by copying top-level (PGD) entries into each
105 new process. But, with PTI, there are now *two* kernel
106 mappings: one in the kernel page tables that maps everything
107 and one for the entry/exit structures. At fork(), we need to
108 copy both.
109 f. In addition to the fork()-time copying, there must also
110 be an update to the userspace PGD any time a set_pgd() is done
111 on a PGD used to map userspace. This ensures that the kernel
112 and userspace copies always map the same userspace
113 memory.
114 g. On systems without PCID support, each CR3 write flushes
115 the entire TLB. That means that each syscall, interrupt
116 or exception flushes the TLB.
117 h. INVPCID is a TLB-flushing instruction which allows flushing
118 of TLB entries for non-current PCIDs. Some systems support
119 PCIDs, but do not support INVPCID. On these systems, addresses
120 can only be flushed from the TLB for the current PCID. When
121 flushing a kernel address, we need to flush all PCIDs, so a
122 single kernel address flush will require a TLB-flushing CR3
123 write upon the next use of every PCID.
124
125 Possible Future Work
126 ====================
127 1. We can be more careful about not actually writing to CR3
128 unless its value is actually changed.
129 2. Allow PTI to be enabled/disabled at runtime in addition to the
130 boot-time switching.
131
132 Testing
133 ========
134
135 To test stability of PTI, the following test procedure is recommended,
136 ideally doing all of these in parallel:
137
138 1. Set CONFIG_DEBUG_ENTRY=y
139 2. Run several copies of all of the tools/testing/selftests/x86/ tests
140 (excluding MPX and protection_keys) in a loop on multiple CPUs for
141 several minutes. These tests frequently uncover corner cases in the
142 kernel entry code. In general, old kernels might cause these tests
143 themselves to crash, but they should never crash the kernel.
144 3. Run the 'perf' tool in a mode (top or record) that generates many
145 frequent performance monitoring non-maskable interrupts (see "NMI"
146 in /proc/interrupts). This exercises the NMI entry/exit code which
147 is known to trigger bugs in code paths that did not expect to be
148 interrupted, including nested NMIs. Using "-c" boosts the rate of
149 NMIs, and using two -c with separate counters encourages nested NMIs
150 and less deterministic behavior.
151 ::
152
153 while true; do perf record -c 10000 -e instructions,cycles -a sleep 10; done
154
155 4. Launch a KVM virtual machine.
156 5. Run 32-bit binaries on systems supporting the SYSCALL instruction.
157 This has been a lightly-tested code path and needs extra scrutiny.
158
159 Debugging
160 =========
161
162 Bugs in PTI cause a few different signatures of crashes
163 that are worth noting here.
164
165 * Failures of the selftests/x86 code. Usually a bug in one of the
166 more obscure corners of entry_64.S
167 * Crashes in early boot, especially around CPU bringup. Bugs
168 in the mappings cause these.
169 * Crashes at the first interrupt. Caused by bugs in entry_64.S,
170 like screwing up a page table switch. Also caused by
171 incorrectly mapping the IRQ handler entry code.
172 * Crashes at the first NMI. The NMI code is separate from main
173 interrupt handlers and can have bugs that do not affect
174 normal interrupts. Also caused by incorrectly mapping NMI
175 code. NMIs that interrupt the entry code must be very
176 careful and can be the cause of crashes that show up when
177 running perf.
178 * Kernel crashes at the first exit to userspace. entry_64.S
179 bugs, or failing to map some of the exit code.
180 * Crashes at first interrupt that interrupts userspace. The paths
181 in entry_64.S that return to userspace are sometimes separate
182 from the ones that return to the kernel.
183 * Double faults: overflowing the kernel stack because of page
184 faults upon page faults. Caused by touching non-pti-mapped
185 data in the entry code, or forgetting to switch to kernel
186 CR3 before calling into C functions which are not pti-mapped.
187 * Userspace segfaults early in boot, sometimes manifesting
188 as mount(8) failing to mount the rootfs. These have
189 tended to be TLB invalidation issues. Usually invalidating
190 the wrong PCID, or otherwise missing an invalidation.
191
192 .. [1] https://gruss.cc/files/kaiser.pdf
193 .. [2] https://meltdownattack.com/meltdown.pdf
194

3. 한국어 전문 번역

영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.

Page Table Isolation 개요

1-32

이 문서는 `SPDX-License-Identifier: GPL-2.0`으로 배포됩니다. Page Table Isolation(PTI, 이전 이름 KAISER `[1]`)은 Meltdown 방식 `[2]`처럼 공유 user/kernel address space를 노리는 attack에 대한 countermeasure입니다.

이 공격 계열을 완화하기 위해 userspace application을 실행할 때만 쓰는 독립 page table 집합을 만듭니다. syscall, interrupt, exception으로 kernel에 진입하면 full kernel copy로 전환하고 user mode로 돌아갈 때 user copy를 다시 사용합니다.

userspace page table에는 kernel 진입·종료 function과 interrupt descriptor table(IDT)처럼 kernel을 드나드는 데 필요한 최소한의 kernel data만 포함됩니다. `pti.c` comment에 설명된 interrupt 진입 시 첫 C function처럼 엄격히는 불필요하지만 mapping되는 항목도 조금 있습니다.

PTI가 활성화되면 paging structure를 활용하는 side-channel attack이 동작하지 않게 하는 데 도움이 됩니다. compile time에 `CONFIG_MITIGATION_PAGE_TABLE_ISOLATION=y`로 활성화하며, build에서 켠 뒤에도 boot 시 `nopti` 또는 `pti=` kernel parameter로 비활성화할 수 있습니다. parameter는 `kernel-parameters.txt`를 참고하십시오.

두 page table 집합의 관리

33-61

PTI가 활성화되면 kernel은 page table 두 집합을 관리합니다. 첫 집합은 PTI가 없는 kernel의 단일 집합과 거의 같으며, `copy_to_user()` 같은 작업에 kernel이 사용하는 완전한 userspace mapping을 포함합니다.

완전한 mapping이지만 kernel page table의 user portion은 top level에 NX bit를 설정해 실행할 수 없게 만듭니다. kernel에서 user로 CR3를 전환하는 작업을 놓치면 userspace가 첫 instruction을 실행하자마자 즉시 crash하도록 보장합니다.

userspace page table은 kernel 진입과 종료에 필요한 data만 mapping합니다. 이 data는 각 CPU copy가 compile-time-fixed virtual address를 갖도록 fixmap에 놓인 `struct cpu_entry_area` 안에 모두 들어 있습니다.

새 userspace mapping은 평소처럼 kernel page table에 entry를 만듭니다. top-level PGD entry를 만들 때만 차이가 있어 main kernel PGD에 설정하는 동시에 userspace page table PGD에도 entry copy를 만듭니다.

PGD level에서 공유하면 하위 page-table layer도 본질적으로 모두 공유됩니다. 따라서 관리할 userspace page table은 단일 공유 집합이며, lock할 PTE와 accessed bit·dirty bit 집합도 하나뿐입니다.

PTI의 memory 및 runtime 비용

62-124

side-channel 방어는 중요하지만 다음 비용이 발생합니다.

분류비용
memory: process PGDprocess마다 order-0 대신 order-1 PGD가 필요해 process당 4k를 추가 소비합니다.
memory: `cpu_entry_area`PMD entry 하나로 mapping하도록 2MB 크기와 2MB alignment가 필요합니다. kernel decompress 뒤 RAM 약 2MB를 쓰지만 kernel image 공간은 쓰지 않습니다.
runtime: CR3interrupt·syscall·exception 진입과 종료마다 page table copy 사이를 전환합니다. kernel이 interrupt된 경우는 생략할 수 있습니다. CR3 move는 약 100 cycle입니다.
runtime: percpu TSSPTI 아래에서 `SYSCALL64` path가 동작하도록 user page table에 mapping합니다. 직접 runtime cost는 없지만 timing attack 가능성을 열 수 있습니다.
runtime: global pagekernel과 user page table 양쪽에 mapping되지 않은 kernel structure는 global page를 쓰지 못합니다. context switch 뒤 TLB miss가 늘지만 실제 손실은 1%를 넘지 않습니다.
runtime: PCIDCR3의 special bit로 page-table switch 때 전체 TLB flush를 피합니다. context switch는 user와 kernel TLB entry를 모두 flush해야 하며 user PCID flush는 userspace 복귀까지 미뤄 비용을 줄입니다.
runtime: process 생성`fork()`에서 전체 kernel mapping과 entry/exit structure mapping 두 가지를 복사합니다.
runtime: `set_pgd()`userspace mapping용 PGD를 바꿀 때 user PGD도 update해 kernel과 user copy가 같은 userspace memory를 mapping하도록 합니다.
runtime: PCID 없음CR3 write마다 전체 TLB가 flush되므로 syscall·interrupt·exception마다 TLB를 flush합니다.
runtime: INVPCID 없음PCID는 있지만 INVPCID가 없으면 current PCID의 address만 flush할 수 있습니다. kernel address 하나를 flush하려면 모든 PCID의 다음 사용 시 TLB-flushing CR3 write가 필요합니다.

PCID와 INVPCID의 자세한 동작은 `intel.com/sdm`을 참고하십시오. `INVPCID`는 current가 아닌 PCID의 TLB entry도 flush할 수 있는 instruction입니다.

향후 개선 가능성

125-131
  • CR3 값이 실제로 달라질 때만 write하도록 더 엄격히 판단할 수 있습니다.
  • boot-time switch뿐 아니라 runtime에도 PTI를 활성화하거나 비활성화할 수 있게 할 수 있습니다.

PTI 안정성 시험

132-158

PTI 안정성을 시험하려면 가능하면 다음 작업을 모두 병렬로 수행합니다.

  • `CONFIG_DEBUG_ENTRY=y`를 설정합니다.
  • `tools/testing/selftests/x86/`의 test에서 MPX와 `protection_keys`를 제외하고 여러 copy를 여러 CPU에서 몇 분간 loop합니다. 오래된 kernel에서는 test 자체가 crash할 수 있지만 kernel은 절대 crash하면 안 됩니다.
  • `perf top` 또는 `perf record`로 performance-monitoring NMI를 자주 발생시킵니다. `/proc/interrupts`의 `NMI`를 확인하며 `-c`는 NMI 비율을 높이고 서로 다른 counter에 두 개의 `-c`를 사용하면 nested NMI와 비결정적 동작을 촉진합니다.
  • KVM virtual machine을 시작합니다.
  • `SYSCALL` instruction을 지원하는 system에서 32-bit binary를 실행합니다. 시험이 적었던 path이므로 추가 검토가 필요합니다.

NMI entry/exit path를 압박하는 loop 예시는 다음과 같습니다.

while true; do perf record -c 10000 -e instructions,cycles -a sleep 10; done

PTI crash signature와 참고 자료

159-193

PTI bug에서 관찰할 수 있는 crash signature는 다음과 같습니다.

  • `selftests/x86` 실패: 대개 `entry_64.S`의 드문 corner에 있는 bug입니다.
  • early boot, 특히 CPU bringup 부근의 crash: mapping bug가 원인입니다.
  • 첫 interrupt의 crash: `entry_64.S` page-table switch bug 또는 IRQ handler entry code의 잘못된 mapping이 원인입니다.
  • 첫 NMI의 crash: 별도 NMI code의 bug 또는 잘못된 NMI code mapping이 원인입니다. entry code를 interrupt하는 NMI는 특히 조심해야 하며 `perf` 실행 중 crash로 나타날 수 있습니다.
  • 첫 userspace 복귀 시 kernel crash: `entry_64.S` bug 또는 exit code 일부를 mapping하지 않은 경우입니다.
  • userspace를 interrupt하는 첫 interrupt의 crash: `entry_64.S`의 userspace 복귀 path가 kernel 복귀 path와 분리된 경우가 있습니다.
  • double fault: page fault 처리 중 다시 page fault가 발생해 kernel stack이 overflow합니다. entry code에서 non-PTI-mapped data를 건드리거나 PTI mapping이 없는 C function 호출 전에 kernel CR3로 전환하지 않은 것이 원인입니다.
  • early boot userspace segfault, 때로는 `mount(8)`의 rootfs mount 실패: 대개 잘못된 PCID를 invalidate하거나 invalidation을 놓친 TLB invalidation 문제입니다.

참고 자료: `[1] https://gruss.cc/files/kaiser.pdf`, `[2] https://meltdownattack.com/meltdown.pdf`.