diff options
| author | Richard Qin <richard.qin.x@gmail.com> | 2026-05-03 01:47:15 +0800 |
|---|---|---|
| committer | Richard Qin <richard.qin.x@gmail.com> | 2026-05-03 01:47:15 +0800 |
| commit | 2b237b24c4db5513f8046e15d24501257e7feeaa (patch) | |
| tree | 7b5fcbe3e00d30056064801594b899eb3db5c88b /na | |
| parent | 4b1b9912caf41fa52818e5d84130016ce00888cc (diff) | |
| download | lume-2b237b24c4db5513f8046e15d24501257e7feeaa.tar.gz lume-2b237b24c4db5513f8046e15d24501257e7feeaa.tar.xz | |
feat: implement KASLR support and runtime address mapping for RISC-V architecture
Co-authored-by: Copilot <copilot@github.com>
Diffstat (limited to 'na')
| -rw-r--r-- | na/arch/riscv/config.cc | 59 | ||||
| -rw-r--r-- | na/arch/riscv/include/arch/config.h | 40 | ||||
| -rw-r--r-- | na/arch/riscv/mm/pmap.cc | 12 | ||||
| -rw-r--r-- | na/drivers/uart.cc | 17 | ||||
| -rw-r--r-- | na/fdt/fdt.cc | 9 | ||||
| -rw-r--r-- | na/include/lume/addr.h | 17 | ||||
| -rw-r--r-- | na/include/lume/config.h | 57 | ||||
| -rw-r--r-- | na/include/lume/console.h | 7 | ||||
| -rw-r--r-- | na/include/lume/fdt.h | 1 | ||||
| -rw-r--r-- | na/include/lume/pmm.h | 3 | ||||
| -rw-r--r-- | na/mm/pmm.cc | 32 | ||||
| -rw-r--r-- | na/mm/vmm.cc | 40 |
12 files changed, 258 insertions, 36 deletions
diff --git a/na/arch/riscv/config.cc b/na/arch/riscv/config.cc index 139d932..9b4cc6e 100644 --- a/na/arch/riscv/config.cc +++ b/na/arch/riscv/config.cc @@ -1,10 +1,69 @@ /* SPDX-License-Identifier: GPL-2.0-or-later */ /* Copyright (C) 2026 Richard Qin */ +/* + * RISC-V Architecture Configuration — runtime state and KASLR + * + * Provides the runtime (post-KASLR) values of the higher-half + * virtual address bases. kaslr_init() is called once during + * early boot, before vmm_init(), to slide these bases by a + * random, 1GB-aligned offset. + * + * KASLR slide budget: + * The default Direct Map starts at 0xFFFFFFC0_0000_0000 (VPN[2]=256). + * The default Vmemmap starts at 0xFFFFFFD0_0000_0000 (VPN[2]=320). + * A 1GB superpage occupies one VPN[2] slot. We allow a slide of + * 0..63 slots (0..63 GB) downward, which keeps both regions inside + * the upper canonical half and avoids colliding with the kernel + * text region (linked at VPN[2] = 258 + 2 = ~0xFFFFFFC0_8020_0000). + * + * In practice, with 128MB RAM the kernel+direct map only touches + * VPN[2] 258-259, so a slide of up to 63GB is safe. + */ + #include <arch/config.h> +#include <lume/random.h> +#include <lume/kprintf.h> namespace arch { +/* Runtime bases — default to the compile-time constants. */ uint64 g_direct_map_base = kDirectMapBaseDefault; +uint64 g_vmemmap_base = kVmemmapBaseDefault; + +void kaslr_init() +{ +#ifdef CONFIG_KASLR + /* + * Generate a random 1GB-aligned slide. + * + * Strategy (matching Linux's approach): + * 1. Pull 64 random bits from the early PRNG. + * 2. Reduce to a slot number in [0, kMaxSlots). + * 3. Convert to a byte offset (slot * 1GB). + * 4. Add to both bases (slide upward in VA space). + * + * Why 1GB alignment? + * SV39 L2 (root) PTEs map 1GB superpages. Keeping the slide + * 1GB-aligned means the direct-map can still use gigapages, + * avoiding TLB pressure. This matches Linux's KASLR on RISC-V. + */ + constexpr uint64 k1GB = 1ULL << 30; + constexpr uint64 kMaxSlots = 64; /* 0..63 GB slide range */ + + uint64 raw = get_random_u64(); + uint64 slot = raw % kMaxSlots; + uint64 slide = slot * k1GB; + + g_direct_map_base = kDirectMapBaseDefault + slide; + g_vmemmap_base = kVmemmapBaseDefault + slide; + + kprintf("[kaslr] slide = %llu GB (slot %llu/64)\n", slide >> 30, slot); + kprintf("[kaslr] direct_map_base = 0x%llx\n", g_direct_map_base); + kprintf("[kaslr] vmemmap_base = 0x%llx\n", g_vmemmap_base); +#else + kprintf("[kaslr] CONFIG_KASLR disabled, using fixed layout\n"); +#endif +} } // namespace arch diff --git a/na/arch/riscv/include/arch/config.h b/na/arch/riscv/include/arch/config.h index c407b25..e7b1027 100644 --- a/na/arch/riscv/include/arch/config.h +++ b/na/arch/riscv/include/arch/config.h @@ -8,21 +8,49 @@ * * These constants define the architecture-specific memory layout. * Each arch/ implementation must provide this header with the same symbols. + * + * KASLR: At boot, kaslr_init() randomises g_direct_map_base and + * g_vmemmap_base. All MI code must read these runtime variables + * (via pa_to_va / vmm helpers) rather than the static defaults. */ #include <lume/types.h> namespace arch { -/* Direct-map virtual base (higher-half base). - * SV39: top of 39-bit VA space. VA = PA + direct_map_base. */ +/* ================================================================ + * Default (compile-time) virtual addresses + * + * SV39 gives a 512GB virtual address space. + * Higher-half layout (defaults before KASLR): + * + * 0xFFFFFFC0_00000000 (256) Direct map (PA+offset) + * 0xFFFFFFD0_00000000 (320) Vmemmap (Frame array) + * + * The 64GB gap between them is the KASLR slide range. + * ================================================================ */ inline constexpr uint64 kDirectMapBaseDefault = 0xFFFFFFC000000000ULL; -extern uint64 g_direct_map_base; - -/* Dedicated VA base for the Frame descriptor array (vmemmap). */ -inline constexpr uint64 kVmemmapBase = 0xFFFFFFD000000000ULL; +inline constexpr uint64 kVmemmapBaseDefault = 0xFFFFFFD000000000ULL; /* Physical memory base (where RAM starts on RISC-V virt platform) */ inline constexpr uint64 kPhysBase = 0x80000000ULL; +/* ================================================================ + * Runtime (post-KASLR) virtual addresses + * + * These start equal to the defaults above. If CONFIG_KASLR is + * enabled, kaslr_init() adds a random, 1GB-aligned offset + * to each, sliding both regions upward in VA space. + * ================================================================ */ +extern uint64 g_direct_map_base; +extern uint64 g_vmemmap_base; + +/* ================================================================ + * KASLR entry point + * + * Must be called AFTER random_early_init() and BEFORE vmm_init(). + * When CONFIG_KASLR is disabled this is a no-op. + * ================================================================ */ +void kaslr_init(); + } // namespace arch diff --git a/na/arch/riscv/mm/pmap.cc b/na/arch/riscv/mm/pmap.cc index 4ed3bca..dafbeaf 100644 --- a/na/arch/riscv/mm/pmap.cc +++ b/na/arch/riscv/mm/pmap.cc @@ -24,6 +24,12 @@ namespace pmap { +/* + * Before the first pmap::activate(), CPU runs on entry.S bootstrap page table + * (fixed direct-map base). After activation, runtime (KASLR) base is valid. + */ +static bool g_runtime_mapping_ready = false; + /* PTE bit definitions */ inline constexpr uint64 PTE_V = 1ULL << 0; inline constexpr uint64 PTE_R = 1ULL << 1; @@ -59,7 +65,10 @@ static inline uint64 pa_to_pte(uint64 pa, uint64 flags) /* Convert PA to kernel VA for accessing page table contents */ static inline uint64 *pa_to_ptr(uint64 pa) { - return reinterpret_cast<uint64 *>(pa_to_va(phys_addr(pa)).raw); + uint64 va = g_runtime_mapping_ready + ? pa_to_va(phys_addr(pa)).raw + : boot_pa_to_va(phys_addr(pa)).raw; + return reinterpret_cast<uint64 *>(va); } /* Allocate a zeroed page for a page table level. @@ -203,6 +212,7 @@ void activate(uint64 root_pa) { arch::mmu::set_page_table(root_pa); arch::mmu::flush_tlb_all(); + g_runtime_mapping_ready = true; } uint64 vm_perm_to_pte(uint64 vm_perm) diff --git a/na/drivers/uart.cc b/na/drivers/uart.cc index 6a35588..2e553d6 100644 --- a/na/drivers/uart.cc +++ b/na/drivers/uart.cc @@ -15,10 +15,21 @@ static uint64 g_uart_pa = 0; static uint64 g_uart_size = 0; +static bool g_runtime_mapping_ready = false; extern "C" void early_putc(char c) { if (!g_uart_pa) return; - volatile auto* uart = reinterpret_cast<volatile uint8*>(pa_to_va(phys_addr(g_uart_pa)).raw); + + /* + * Before vmm_init() activates the final page table, UART MMIO is only + * reachable via the bootstrap fixed direct map from entry.S. + * After activation, switch to the runtime (possibly KASLR-slid) base. + */ + uint64 uart_va = g_runtime_mapping_ready + ? pa_to_va(phys_addr(g_uart_pa)).raw + : boot_pa_to_va(phys_addr(g_uart_pa)).raw; + + volatile auto* uart = reinterpret_cast<volatile uint8*>(uart_va); *uart = static_cast<uint8>(c); } @@ -45,3 +56,7 @@ void console_init() { vmm_map_kernel_mmio(g_uart_pa, g_uart_size); } } + +void console_use_runtime_mapping() { + g_runtime_mapping_ready = true; +} diff --git a/na/fdt/fdt.cc b/na/fdt/fdt.cc index ef614e4..fbec245 100644 --- a/na/fdt/fdt.cc +++ b/na/fdt/fdt.cc @@ -126,7 +126,7 @@ static inline uint64 read_cells(const uint8 *data, uint32 cells) { /* ------------------------------------------------------------------ */ FdtManager::FdtManager(uint64 fdt_paddr) - : fdt_paddr_(pa_to_va(phys_addr(fdt_paddr)).raw) {} + : fdt_paddr_(fdt_paddr) {} /* ------------------------------------------------------------------ */ /* Phase 1: Early scan — memory layout */ @@ -352,14 +352,15 @@ void FdtManager::early_scan_uart(uint64 fdt_paddr, uint64 *uart_addr, /* ------------------------------------------------------------------ */ void FdtManager::unflatten() { - const fdt_header *hdr = reinterpret_cast<const fdt_header *>(fdt_paddr_); + const uint64 fdt_va = pa_to_va(phys_addr(fdt_paddr_)).raw; + const fdt_header *hdr = reinterpret_cast<const fdt_header *>(fdt_va); if (bswap32(hdr->magic) != FDT_MAGIC) kernel_panic("fdt unflatten: invalid magic", nullptr); const uint8 *dt_struct = - reinterpret_cast<const uint8 *>(fdt_paddr_) + bswap32(hdr->off_dt_struct); + reinterpret_cast<const uint8 *>(fdt_va) + bswap32(hdr->off_dt_struct); const char *dt_strings = - reinterpret_cast<const char *>(fdt_paddr_) + bswap32(hdr->off_dt_strings); + reinterpret_cast<const char *>(fdt_va) + bswap32(hdr->off_dt_strings); uint32 offset = 0; diff --git a/na/include/lume/addr.h b/na/include/lume/addr.h index 9c20d66..572b6fe 100644 --- a/na/include/lume/addr.h +++ b/na/include/lume/addr.h @@ -29,6 +29,23 @@ inline PhysAddr va_to_pa(VirtAddr va) return PhysAddr{va.raw - arch::g_direct_map_base}; } +/* + * Early-boot conversions pinned to the bootstrap direct-map base. + * + * Use these only before vmm_init() activates the final kernel page table. + * They are required when KASLR has already randomized g_direct_map_base, + * but the CPU is still running on the bootstrap mapping built in entry.S. + */ +inline VirtAddr boot_pa_to_va(PhysAddr pa) +{ + return VirtAddr{pa.raw + arch::kDirectMapBaseDefault}; +} + +inline PhysAddr boot_va_to_pa(VirtAddr va) +{ + return PhysAddr{va.raw - arch::kDirectMapBaseDefault}; +} + /* Align up to page boundary */ inline uint64 page_align_up(uint64 addr) { diff --git a/na/include/lume/config.h b/na/include/lume/config.h index acd76ef..40787cc 100644 --- a/na/include/lume/config.h +++ b/na/include/lume/config.h @@ -12,12 +12,59 @@ * This header is safe to include from both C++ and assembly files. * Assembly-visible constants use #define; C++ code gets typed constexpr. * + * Values are injected by the build system via -DCONFIG_xxx flags + * (see scripts/Kconfig.mk). The #ifndef guards below provide + * safe defaults so the header is self-contained for IDE analysis. + * * Reference: docs/specs/00_conventions.md §1.3 */ -/* These macros are shared with assembly (entry.S) */ -#define LUME_MAX_CPUS 8 -#define LUME_PAGE_SIZE 4096 +/* ================================================================ + * Defaults — only used when CONFIG_xxx is NOT supplied by -D flag. + * In a normal build, scripts/Kconfig.mk always provides them. + * ================================================================ */ + +#ifndef CONFIG_NR_CPUS +#define CONFIG_NR_CPUS 8 +#endif + +#ifndef CONFIG_PAGE_SIZE +#define CONFIG_PAGE_SIZE 4096 +#endif + +#ifndef CONFIG_MAX_ORDER +#define CONFIG_MAX_ORDER 11 +#endif + +#ifndef CONFIG_KERNEL_STACK_SIZE +#define CONFIG_KERNEL_STACK_SIZE 8192 +#endif + +#ifndef CONFIG_HZ +#define CONFIG_HZ 100 +#endif + +#ifndef CONFIG_NR_PRIORITIES +#define CONFIG_NR_PRIORITIES 32 +#endif + +#ifndef CONFIG_DEFAULT_PRIORITY +#define CONFIG_DEFAULT_PRIORITY (CONFIG_NR_PRIORITIES / 2) +#endif + +#ifndef CONFIG_IDLE_PRIORITY +#define CONFIG_IDLE_PRIORITY (CONFIG_NR_PRIORITIES - 1) +#endif + +#ifndef CONFIG_DEFAULT_TIMESLICE +#define CONFIG_DEFAULT_TIMESLICE (CONFIG_HZ / 10) +#endif + +/* ================================================================ + * Legacy compatibility aliases (shared with assembly) + * ================================================================ */ +#define LUME_MAX_CPUS CONFIG_NR_CPUS +#define LUME_PAGE_SIZE CONFIG_PAGE_SIZE #ifndef __ASSEMBLER__ @@ -26,9 +73,9 @@ /* Maximum number of CPUs supported by the kernel. * Compile-time limit for static arrays (boot stacks, per-cpu data). * Actual CPU count is detected at runtime via FDT. */ -inline constexpr int kMaxCpus = LUME_MAX_CPUS; +inline constexpr int kMaxCpus = CONFIG_NR_CPUS; /* Virtual memory page size (4KB, matching SV39 base page). */ -inline constexpr uint64 kPageSize = LUME_PAGE_SIZE; +inline constexpr uint64 kPageSize = CONFIG_PAGE_SIZE; #endif /* __ASSEMBLER__ */
\ No newline at end of file diff --git a/na/include/lume/console.h b/na/include/lume/console.h index ff9f019..df129cf 100644 --- a/na/include/lume/console.h +++ b/na/include/lume/console.h @@ -13,3 +13,10 @@ void console_early_init(uint64 fdt_paddr); /* Full console initialization. Maps MMIO. */ void console_init(); + +/* + * Notify console that the runtime kernel page table is active. + * After this point early_putc() must use runtime direct-map base + * instead of the bootstrap fixed base. + */ +void console_use_runtime_mapping(); diff --git a/na/include/lume/fdt.h b/na/include/lume/fdt.h index 63d185b..626cc48 100644 --- a/na/include/lume/fdt.h +++ b/na/include/lume/fdt.h @@ -82,3 +82,4 @@ DeviceNode *fdt_get_node_by_path(const char *path); // Early scan wrappers (delegate to FdtManager static methods). void fdt_early_get_mem_info(uint64 *base, uint64 *size); void fdt_early_get_uart_info(uint64 *uart_addr, uint64 *uart_size); +void fdt_early_get_kaslr_seed(uint64 *seed); diff --git a/na/include/lume/pmm.h b/na/include/lume/pmm.h index a9249ac..266a83c 100644 --- a/na/include/lume/pmm.h +++ b/na/include/lume/pmm.h @@ -22,9 +22,10 @@ #include <lume/types.h> #include <lume/frame.h> #include <lume/addr_types.h> +#include <lume/config.h> /* Buddy system constants */ -inline constexpr int kMaxOrder = 11; // Orders 0..10 → 4KB..4MB +inline constexpr int kMaxOrder = CONFIG_MAX_ORDER; // Orders 0..(N-1) /* Per-CPU cache dynamic parameters (calculated in pmm_init) */ extern uint32 g_pcp_high_watermark; diff --git a/na/mm/pmm.cc b/na/mm/pmm.cc index 1400448..633bd2f 100644 --- a/na/mm/pmm.cc +++ b/na/mm/pmm.cc @@ -7,11 +7,9 @@ * Buddy allocator with Per-CPU single-page caches (PCP). * * Initialization sequence (called by BSP in single-core period): - * 1. Query FDT for physical memory base and size. - * 2. Carve out space for the frame_map array at the start of free memory - * (immediately after _kernel_end, page-aligned). - * 3. Initialize all Frame descriptors as Free. - * 4. Feed free frames into the buddy system at the highest possible order. + * 1. memblock + vmm_init allocate and map the frame_map at VMEMMAP. + * 2. Zero and mark all Frame descriptors reserved by default. + * 3. Carve free regions from memblock into the buddy system. * * The frame_map is a flat array indexed by physical page frame number (PFN). * PFN = (pa - mem_base) / PAGE_SIZE. This keeps address conversion O(1). @@ -68,17 +66,26 @@ uint32 g_pcp_batch_size = 0; * PFN = (pa - g_mem_base) / kPageSize */ static inline uint64 pa_to_frame_index(uint64 pa) { + if (pa < g_mem_base || pa >= g_mem_base + g_num_frames * kPageSize) + kernel_panic("pa_to_frame: physical address outside managed range"); return (pa - g_mem_base) / kPageSize; } static inline uint64 frame_index_to_pa(uint64 idx) { + if (idx >= g_num_frames) + kernel_panic("frame_to_pa: frame index outside managed range"); return g_mem_base + idx * kPageSize; } PhysAddr frame_to_pa(const Frame *frame) { - uint64 idx = static_cast<uint64>(frame - g_frame_map); - if (frame < g_frame_map || idx >= g_num_frames) + uintptr base = reinterpret_cast<uintptr>(g_frame_map); + uintptr addr = reinterpret_cast<uintptr>(frame); + uintptr end = base + g_num_frames * sizeof(Frame); + + if (addr < base || addr >= end) kernel_panic("frame_to_pa: frame outside frame_map bounds"); + + uint64 idx = (addr - base) / sizeof(Frame); return phys_addr(frame_index_to_pa(idx)); } @@ -391,11 +398,20 @@ void pmm_init() { * No relocation needed — frame_map starts life at the right VA. */ g_frame_map_pa = vmm_get_frame_map_pa(); g_frame_map_bytes = vmm_get_frame_map_size(); - g_frame_map = reinterpret_cast<Frame *>(arch::kVmemmapBase); + g_frame_map = reinterpret_cast<Frame *>(arch::g_vmemmap_base); /* 4. Zero out the entire frame_map */ zero_range(g_frame_map, g_frame_map_bytes); + /* 4b. Default every frame descriptor to reserved/non-free. */ + for (uint64 i = 0; i < g_num_frames; i++) { + Frame *f = &g_frame_map[i]; + f->refcount.store(0, __ATOMIC_RELAXED); + f->state = FrameState::Allocated; + f->order = 0; + f->free.free_link.init(); + } + /* 5. Initialize buddy free list heads */ g_pmm_lock.init("pmm"); for (int i = 0; i < kMaxOrder; i++) { diff --git a/na/mm/vmm.cc b/na/mm/vmm.cc index 041413d..c1436d1 100644 --- a/na/mm/vmm.cc +++ b/na/mm/vmm.cc @@ -50,7 +50,7 @@ uint64 vmm_get_num_frames() { return g_vmemmap_num_frames; } /* Convert a linker symbol to PA (kernel is executing in higher-half VA). */ static inline uint64 sym_to_pa(const void *sym) { - return va_to_pa(virt_addr(reinterpret_cast<uint64>(sym))).raw; + return boot_va_to_pa(virt_addr(reinterpret_cast<uint64>(sym))).raw; } /* Map a PA range into the kernel page table via the direct map. */ @@ -65,6 +65,18 @@ static void map_range(uint64 root, uint64 pa_start, uint64 pa_end, } } +/* Map a PA range into the kernel page table at the fixed linked VA. */ +static void map_range_linked(uint64 root, uint64 pa_start, uint64 pa_end, + uint64 perm) { + for (uint64 pa = pa_start; pa < pa_end; pa += kPageSize) { + uint64 va = boot_pa_to_va(phys_addr(pa)).raw; + int ret = pmap::map(root, va, pa, perm | pmap::PTE_G); + if (ret != 0) { + kernel_panic("vmm_init: pmap::map (linked) failed"); + } + } +} + /* Map a PA range to an explicit VA base, using 2MB pages where possible. */ static void map_range_at_huge(uint64 root, uint64 va_base, uint64 pa_start, uint64 pa_end, uint64 perm) { @@ -117,14 +129,19 @@ void vmm_init() { uint64 mem_size = memblock_get_mem_size(); uint64 mem_end = mem_base + mem_size; - /* 3. Map kernel .text (RX) */ - map_range(root, text_start, text_end, pmap::PTE_R | pmap::PTE_X); + /* 3. Map kernel image at FIXED linked VA. + * + * Kernel text/data are linked at the bootstrap higher-half addresses + * (kDirectMapBaseDefault + PA). They must remain executable/readable at + * those addresses until full kernel-image relocation is implemented. + */ + map_range_linked(root, text_start, text_end, pmap::PTE_R | pmap::PTE_X); - /* 4. Map .rodata (R) */ - map_range(root, ro_start, ro_end, pmap::PTE_R); + /* 4. Map .rodata (R) at fixed linked VA */ + map_range_linked(root, ro_start, ro_end, pmap::PTE_R); - /* 5. Map .data + .bss (RW) */ - map_range(root, data_start, bss_end, pmap::PTE_R | pmap::PTE_W); + /* 5. Map .data + .bss (RW) at fixed linked VA */ + map_range_linked(root, data_start, bss_end, pmap::PTE_R | pmap::PTE_W); /* 6. Map remaining physical memory after kernel (RW) */ uint64 after_kernel = page_align_up(sym_to_pa(_kernel_end)); @@ -150,8 +167,8 @@ void vmm_init() { kprintf("[vmm] vmemmap: %llu frames, %llu KB at PA 0x%llx\n", num_frames, frame_map_span / 1024, frame_map_pa); - /* Map frame_map at kVmemmapBase, using 2MB pages where possible */ - map_range_at_huge(root, arch::kVmemmapBase, frame_map_pa, + /* Map frame_map at g_vmemmap_base (randomised by KASLR), using 2MB pages where possible */ + map_range_at_huge(root, arch::g_vmemmap_base, frame_map_pa, frame_map_pa + frame_map_span, pmap::PTE_R | pmap::PTE_W); /* Export for pmm_init() */ @@ -166,7 +183,10 @@ void vmm_init() { /* 9. Activate the new page table */ pmap::activate(root); - kprintf("[vmm] vmm_init complete (vmemmap at 0x%llx)\n", arch::kVmemmapBase); + /* Console MMIO access must now use runtime (KASLR) direct-map base. */ + console_use_runtime_mapping(); + + kprintf("[vmm] vmm_init complete (vmemmap at 0x%llx)\n", arch::g_vmemmap_base); } void vmm_init_ap() { |
