summaryrefslogtreecommitdiffstats
path: root/rsnix/src/paging.rs
diff options
context:
space:
mode:
Diffstat (limited to 'rsnix/src/paging.rs')
-rw-r--r--rsnix/src/paging.rs356
1 files changed, 356 insertions, 0 deletions
diff --git a/rsnix/src/paging.rs b/rsnix/src/paging.rs
new file mode 100644
index 0000000..766bde3
--- /dev/null
+++ b/rsnix/src/paging.rs
@@ -0,0 +1,356 @@
1// ---------------------------------------------------------------------------
2// Identity page-table helpers — 4-level paging (x86_64).
3//
4// The kernel keeps an identity-mapped 4-level tree (no higher half). At
5// boot it runs on UEFI's tables; `adopt_tables()` rebuilds that map into
6// our own tables (UEFI maps its own table pages read-only, so the firmware
7// tree cannot be modified in place) and switches CR3. After that the
8// helpers walk our tree to map / unmap / query individual 4 KB pages 1:1.
9// Page-table pages come from the physical page allocator. Firmware may
10// have used 2 MB / 1 GB large pages for RAM, so the walk splits large pages
11// into 4 KB tables on demand. Split tables stay allocated afterwards (no
12// refcounting) — fine for a kernel that owns all memory.
13// ---------------------------------------------------------------------------
14
15use crate::memory::{alloc_pages, free_pages, PAGE_SIZE};
16
17pub const PAGE_PRESENT: u64 = 1 << 0;
18pub const PAGE_WRITABLE: u64 = 1 << 1;
19pub const PAGE_USER: u64 = 1 << 2;
20pub const PAGE_PS: u64 = 1 << 7; // large page (2 MB PDE, 1 GB PDPTE)
21pub const PAGE_NX: u64 = 1 << 63;
22
23/// Physical address bits in a page-table entry.
24const PHYS_ADDR_MASK: u64 = 0x000F_FFFF_FFFF_F000;
25
26const PAGE_2M: usize = 1 << 21;
27const PAGE_1G: usize = 1 << 30;
28
29/// Current PML4 root (physical address, identity-mapped).
30pub fn cr3() -> usize {
31 let val: usize;
32 unsafe {
33 core::arch::asm!("mov {0}, cr3", out(reg) val, options(nostack, preserves_flags));
34 }
35 val
36}
37
38/// Full TLB flush by reloading CR3 (slow but bulletproof; map/unmap are
39/// rare operations in this kernel).
40fn flush_tlb_all() {
41 unsafe {
42 let cr3: usize;
43 core::arch::asm!("mov {0}, cr3", out(reg) cr3, options(nostack, preserves_flags));
44 core::arch::asm!("mov cr3, {0}", in(reg) cr3, options(nostack, preserves_flags));
45 }
46}
47
48/// Rebuild the identity map into our own page tables and switch CR3.
49///
50/// UEFI maps its own page-table pages read-only (R/W clear), so we cannot
51/// modify the firmware tree in place: map/unmap would #PF. Instead, walk
52/// the current tables, replicate every present leaf (4 KB / 2 MB / 1 GB)
53/// into fresh tables from the page allocator, force R/W so the tree stays
54/// editable, then load the new PML4 into CR3.
55pub fn adopt_tables() -> bool {
56 unsafe {
57 let old_pml4 = cr3() as *const u64;
58 let Some(new_pml4) = alloc_pages(1) else {
59 return false;
60 };
61 core::ptr::write_bytes(new_pml4 as *mut u8, 0, PAGE_SIZE);
62 let new_pml4 = new_pml4 as *mut u64;
63
64 for l4 in 0..512 {
65 let l4e = old_pml4.add(l4).read_volatile();
66 if l4e & PAGE_PRESENT == 0 {
67 continue;
68 }
69 // Replicate this PDPT into a fresh one.
70 let old_pdpt = (l4e & PHYS_ADDR_MASK) as *const u64;
71 let Some(new_pdpt) = alloc_pages(1) else {
72 return false;
73 };
74 core::ptr::write_bytes(new_pdpt as *mut u8, 0, PAGE_SIZE);
75 let new_pdpt = new_pdpt as *mut u64;
76 *new_pml4.add(l4) =
77 (new_pdpt as u64) | PAGE_PRESENT | PAGE_WRITABLE | (l4e & PAGE_USER);
78
79 for l3 in 0..512 {
80 let pdpte = old_pdpt.add(l3).read_volatile();
81 if pdpte & PAGE_PRESENT == 0 {
82 continue;
83 }
84 if pdpte & PAGE_PS != 0 {
85 // 1 GB large page — copy the leaf, force writable.
86 *new_pdpt.add(l3) = pdpte | PAGE_WRITABLE;
87 continue;
88 }
89 let old_pd = (pdpte & PHYS_ADDR_MASK) as *const u64;
90 let Some(new_pd) = alloc_pages(1) else {
91 return false;
92 };
93 core::ptr::write_bytes(new_pd as *mut u8, 0, PAGE_SIZE);
94 let new_pd = new_pd as *mut u64;
95 *new_pdpt.add(l3) =
96 (new_pd as u64) | PAGE_PRESENT | PAGE_WRITABLE | (pdpte & PAGE_USER);
97
98 for l2 in 0..512 {
99 let pde = old_pd.add(l2).read_volatile();
100 if pde & PAGE_PRESENT == 0 {
101 continue;
102 }
103 if pde & PAGE_PS != 0 {
104 // 2 MB large page — copy the leaf, force writable.
105 *new_pd.add(l2) = pde | PAGE_WRITABLE;
106 continue;
107 }
108 let old_pt = (pde & PHYS_ADDR_MASK) as *const u64;
109 let Some(new_pt) = alloc_pages(1) else {
110 return false;
111 };
112 core::ptr::write_bytes(new_pt as *mut u8, 0, PAGE_SIZE);
113 let new_pt = new_pt as *mut u64;
114 *new_pd.add(l2) =
115 (new_pt as u64) | PAGE_PRESENT | PAGE_WRITABLE | (pde & PAGE_USER);
116
117 for l1 in 0..512 {
118 let pte = old_pt.add(l1).read_volatile();
119 if pte & PAGE_PRESENT != 0 {
120 *new_pt.add(l1) = pte | PAGE_WRITABLE;
121 }
122 }
123 }
124 }
125 }
126
127 // Switch to the new tables (CR3 reload flushes the TLB).
128 let new_cr3 = new_pml4 as usize;
129 core::arch::asm!("mov cr3, {0}", in(reg) new_cr3, options(nostack, preserves_flags));
130 true
131 }
132}
133
134/// Ensure the PDPT entry points at a page-directory table, splitting a
135/// 1 GB large page at `base_1g` if present. Returns the PD pointer.
136unsafe fn ensure_pd(pdpte: &mut u64, base_1g: usize) -> Option<*mut u64> {
137 unsafe {
138 if *pdpte & PAGE_PRESENT == 0 {
139 let page = alloc_pages(1)? as *mut u64;
140 core::ptr::write_bytes(page as *mut u8, 0, PAGE_SIZE);
141 *pdpte = (page as u64) | PAGE_PRESENT | PAGE_WRITABLE;
142 return Some(page);
143 }
144 if *pdpte & PAGE_PS != 0 {
145 // Split the 1 GB page into 512 × 2 MB pages.
146 let pd = alloc_pages(1)? as *mut u64;
147 core::ptr::write_bytes(pd as *mut u8, 0, PAGE_SIZE);
148 let inherit = *pdpte & (PAGE_USER | PAGE_NX);
149 for i in 0..512 {
150 let p = base_1g + i * PAGE_2M;
151 *pd.add(i) = (p as u64) | PAGE_PRESENT | PAGE_WRITABLE | PAGE_PS | inherit;
152 }
153 *pdpte = (pd as u64) | PAGE_PRESENT | PAGE_WRITABLE | (*pdpte & PAGE_USER);
154 return Some(pd);
155 }
156 Some(((*pdpte & PHYS_ADDR_MASK) as usize) as *mut u64)
157 }
158}
159
160/// Ensure the PD entry points at a page table, splitting a 2 MB large page
161/// at `base_2m` if present. Returns the PT pointer.
162unsafe fn ensure_pt(pde: &mut u64, base_2m: usize) -> Option<*mut u64> {
163 unsafe {
164 if *pde & PAGE_PRESENT == 0 {
165 let page = alloc_pages(1)? as *mut u64;
166 core::ptr::write_bytes(page as *mut u8, 0, PAGE_SIZE);
167 *pde = (page as u64) | PAGE_PRESENT | PAGE_WRITABLE;
168 return Some(page);
169 }
170 if *pde & PAGE_PS != 0 {
171 // Split the 2 MB page into 512 × 4 KB pages.
172 let pt = alloc_pages(1)? as *mut u64;
173 core::ptr::write_bytes(pt as *mut u8, 0, PAGE_SIZE);
174 let inherit = *pde & (PAGE_USER | PAGE_NX);
175 for i in 0..512 {
176 let p = base_2m + i * PAGE_SIZE;
177 *pt.add(i) = (p as u64) | PAGE_PRESENT | PAGE_WRITABLE | inherit;
178 }
179 *pde = (pt as u64) | PAGE_PRESENT | PAGE_WRITABLE | (*pde & PAGE_USER);
180 return Some(pt);
181 }
182 Some(((*pde & PHYS_ADDR_MASK) as usize) as *mut u64)
183 }
184}
185
186/// Identity-map the physical page at `phys` (must be 4 KB aligned).
187/// `flags` adds to PRESENT (e.g. PAGE_WRITABLE, PAGE_USER).
188pub fn map_page(phys: usize, flags: u64) -> bool {
189 if !phys.is_multiple_of(PAGE_SIZE) {
190 return false;
191 }
192 unsafe {
193 let pml4 = cr3() as *mut u64;
194 let l4 = (phys >> 39) & 0x1FF;
195 let l3 = (phys >> 30) & 0x1FF;
196 let l2 = (phys >> 21) & 0x1FF;
197 let l1 = (phys >> 12) & 0x1FF;
198
199 let pml4e = &mut *pml4.add(l4);
200 if *pml4e & PAGE_PRESENT == 0 {
201 let Some(page) = alloc_pages(1) else {
202 return false;
203 };
204 core::ptr::write_bytes(page as *mut u8, 0, PAGE_SIZE);
205 *pml4e = (page as u64) | PAGE_PRESENT | PAGE_WRITABLE;
206 }
207 // PML4 entries are never large pages.
208
209 let base_1g = phys & !(PAGE_1G - 1);
210 let pdpte = &mut *(((*pml4e & PHYS_ADDR_MASK) as usize) as *mut u64).add(l3);
211 let Some(pd) = ensure_pd(pdpte, base_1g) else {
212 return false;
213 };
214
215 let base_2m = phys & !(PAGE_2M - 1);
216 let pde = &mut *pd.add(l2);
217 let Some(pt) = ensure_pt(pde, base_2m) else {
218 return false;
219 };
220
221 let pte = &mut *pt.add(l1);
222 *pte = (phys as u64) | flags | PAGE_PRESENT;
223 flush_tlb_all();
224 true
225 }
226}
227
228/// Identity-map `count` consecutive pages starting at `phys`.
229pub fn map_range(phys: usize, count: usize, flags: u64) -> bool {
230 for i in 0..count {
231 if !map_page(phys + i * PAGE_SIZE, flags) {
232 return false;
233 }
234 }
235 true
236}
237
238/// Unmap the 4 KB page at `phys`. Splits large pages as needed so only
239/// this one page is removed. Returns false if it wasn't mapped.
240pub fn unmap_page(phys: usize) -> bool {
241 if !phys.is_multiple_of(PAGE_SIZE) {
242 return false;
243 }
244 unsafe {
245 let pml4 = cr3() as *mut u64;
246 let l4 = (phys >> 39) & 0x1FF;
247 let l3 = (phys >> 30) & 0x1FF;
248 let l2 = (phys >> 21) & 0x1FF;
249 let l1 = (phys >> 12) & 0x1FF;
250
251 let pml4e = &mut *pml4.add(l4);
252 if *pml4e & PAGE_PRESENT == 0 {
253 return false;
254 }
255 let base_1g = phys & !(PAGE_1G - 1);
256 let pdpte = &mut *(((*pml4e & PHYS_ADDR_MASK) as usize) as *mut u64).add(l3);
257 if *pdpte & PAGE_PRESENT == 0 {
258 return false;
259 }
260 let Some(pd) = ensure_pd(pdpte, base_1g) else {
261 return false;
262 };
263 let base_2m = phys & !(PAGE_2M - 1);
264 let pde = &mut *pd.add(l2);
265 if *pde & PAGE_PRESENT == 0 {
266 return false;
267 }
268 let Some(pt) = ensure_pt(pde, base_2m) else {
269 return false;
270 };
271 let pte = &mut *pt.add(l1);
272 if *pte & PAGE_PRESENT == 0 {
273 return false;
274 }
275 *pte = 0;
276 flush_tlb_all();
277 true
278 }
279}
280
281/// Is the page at `phys` mapped (present at any level, incl. large pages)?
282pub fn is_mapped(phys: usize) -> bool {
283 unsafe {
284 let pml4 = cr3() as *const u64;
285 let l4 = (phys >> 39) & 0x1FF;
286 let l3 = (phys >> 30) & 0x1FF;
287 let l2 = (phys >> 21) & 0x1FF;
288 let l1 = (phys >> 12) & 0x1FF;
289
290 let pml4e = *pml4.add(l4);
291 if pml4e & PAGE_PRESENT == 0 {
292 return false;
293 }
294 let pdpte = *((pml4e & PHYS_ADDR_MASK) as *const u64).add(l3);
295 if pdpte & PAGE_PRESENT == 0 {
296 return false;
297 }
298 if pdpte & PAGE_PS != 0 {
299 return true; // 1 GB page covers phys
300 }
301 let pde = *((pdpte & PHYS_ADDR_MASK) as *const u64).add(l2);
302 if pde & PAGE_PRESENT == 0 {
303 return false;
304 }
305 if pde & PAGE_PS != 0 {
306 return true; // 2 MB page covers phys
307 }
308 let pte = *((pde & PHYS_ADDR_MASK) as *const u64).add(l1);
309 pte & PAGE_PRESENT != 0
310 }
311}
312
313/// Smoke test: allocate two physical pages, unmap one, verify the mapping
314/// state, remap it, verify data still works, and free both.
315pub fn self_test() -> bool {
316 let Some(p1) = alloc_pages(1) else {
317 return false;
318 };
319 let Some(p2) = alloc_pages(1) else {
320 return false;
321 };
322
323 // Both pages must already be identity-mapped (they're RAM).
324 if !is_mapped(p1) || !is_mapped(p2) {
325 return false;
326 }
327
328 // Unmap p1 — only p1 should disappear.
329 if !unmap_page(p1) {
330 return false;
331 }
332 if is_mapped(p1) {
333 return false;
334 }
335 if !is_mapped(p2) {
336 return false;
337 }
338
339 // Remap and verify we can write through the mapping.
340 if !map_page(p1, PAGE_WRITABLE) {
341 return false;
342 }
343 if !is_mapped(p1) {
344 return false;
345 }
346 unsafe {
347 core::ptr::write_volatile(p1 as *mut u8, 0xCD);
348 if core::ptr::read_volatile(p1 as *const u8) != 0xCD {
349 return false;
350 }
351 }
352
353 free_pages(p1, 1);
354 free_pages(p2, 1);
355 true
356}