kernel: own and clear contiguous physical allocations
71bd6f97ea1d457327d38dae8d7293c13285b60e5185a4ca6747a3795fa09268
Verified: make -C kernel check with the machine-capable emulator; all pass.
1 parent
d88ae1a1
kernel/Makefile
+2 -2
| 1 | 1 | # Freestanding kernel and hosted mechanism checks. |
|
| 2 | 2 | EMU ?= $(or $(RAD_EMULATOR),emulator) |
|
| 3 | 3 | HOST_EMU ?= $(EMU) |
|
| 4 | 4 | COMPILER := ../bin/radiance.rv64.dev |
|
| 5 | 5 | COMPILE := $(HOST_EMU) -memory-size=385024 -data-size=348160 -stack-size=512 -run $(COMPILER) |
|
| 6 | - | MODULES := core/fdt.rad core/platform.rad |
|
| 6 | + | MODULES := core/fdt.rad core/platform.rad core/frames.rad |
|
| 7 | 7 | CORE := -pkg core -mod core.rad $(addprefix -mod ,$(MODULES)) |
|
| 8 | - | CHECK_MODULES := check/boot.rad check/fixture.rad |
|
| 8 | + | CHECK_MODULES := check/boot.rad check/fixture.rad check/frames.rad |
|
| 9 | 9 | ||
| 10 | 10 | .PHONY: all check clean compiler-check |
|
| 11 | 11 | all: kernel.rv64 |
|
| 12 | 12 | ||
| 13 | 13 | compiler-check: |
kernel/NOTES.md
+12 -2
| 1 | 1 | # Kernel implementation decisions |
|
| 2 | 2 | ||
| 3 | 3 | The specification at https://radiant.computer/system/kernel takes precedence |
|
| 4 | 4 | for fixed call numbers, handle layout, rights, and object behavior. These notes |
|
| 5 | - | record the contracts established through step 2 of the 22-step plan. |
|
| 5 | + | record the contracts established through step 3 of the 22-step plan. |
|
| 6 | 6 | ||
| 7 | 7 | ## Source and trust boundary |
|
| 8 | 8 | ||
| 9 | 9 | - Kernel mechanisms use freestanding Radiance; RAS owns machine entry, register |
|
| 10 | 10 | state, atomics, and MMIO. Hosted checks exercise the same mechanism modules. |
| 31 | 31 | - Decode firmware reservations and /reserved-memory. MMIO is not allocatable RAM. |
|
| 32 | 32 | CLINT, PLIC, and radiant,finish are machine resources, not user Device authority. |
|
| 33 | 33 | - Hardware hart identifiers fit the eight-entry build limit. CLINT banks and |
|
| 34 | 34 | PLIC contexts come from interrupts-extended, not the hart identifier itself. |
|
| 35 | 35 | ||
| 36 | + | ## Physical frame ownership |
|
| 37 | + | ||
| 38 | + | - Frame indexes and counts use u32: MAX_PAGES = 65536 is not representable in u16. |
|
| 39 | + | - Frame indices address the first 256 MiB of physical memory. Discovered RAM |
|
| 40 | + | must fit in this build's physical address range. |
|
| 41 | + | - Allocation clears frames before a new domain receives access. Kernel/image, |
|
| 42 | + | FDT, boot data, firmware reservations, and MMIO regions are excluded. |
|
| 43 | + | - Contiguous allocations validate complete ranges and fail without partial |
|
| 44 | + | ownership when memory or metadata capacity is unavailable. |
|
| 45 | + | ||
| 36 | 46 | ## Validation |
|
| 37 | 47 | ||
| 38 | 48 | Use the current machine-capable sibling emulator. Set `RAD_EMULATOR`, pass |
|
| 39 | 49 | `EMU` to the kernel Make invocation, or put `emulator` on PATH. The kernel build |
|
| 40 | 50 | checks compiler dependencies. From the repository root, run: |
|
| 41 | 51 | ||
| 42 | 52 | ```sh |
|
| 43 | 53 | make -C kernel check |
|
| 44 | 54 | ``` |
|
| 45 | 55 | ||
| 46 | - | Exercise bounded FDT topology discovery, malformed sections, every truncated prefix, reservations, and disabled CPU nodes. |
|
| 56 | + | Exercise contiguous allocation, clearing, reservation exclusion, exhaustion, and exact physical boundaries. |
|
| 47 | 57 | ||
| 48 | 58 | The entry probe uses explicit M-mode success/fault finish writes; secondary |
|
| 49 | 59 | harts idle. This checks machine entry, not user-domain execution. Finish writes |
|
| 50 | 60 | are a check protocol, not a domain-exit operation. |
kernel/check.rad
+2 -0
| 1 | 1 | //! Hosted entry for kernel mechanism checks. |
|
| 2 | 2 | ||
| 3 | 3 | mod boot; |
|
| 4 | 4 | export mod fixture; |
|
| 5 | + | mod frames; |
|
| 5 | 6 | ||
| 6 | 7 | /// Run the available kernel mechanism checks. |
|
| 7 | 8 | @default fn main() -> u32 { |
|
| 9 | + | frames::run(); |
|
| 8 | 10 | boot::run(); |
|
| 9 | 11 | return 0; |
|
| 10 | 12 | } |
kernel/check/frames.rad
added
+73 -0
| 1 | + | //! Physical allocator bounds, fragmentation, and data-isolation checks. |
|
| 2 | + | ||
| 3 | + | use core::fdt; |
|
| 4 | + | use core::frames; |
|
| 5 | + | use core::platform; |
|
| 6 | + | ||
| 7 | + | /// Test-owned physical memory spanning more than one bitmap word. |
|
| 8 | + | static MEMORY: [u8; 68 * frames::PAGE_SIZE] = undefined; |
|
| 9 | + | ||
| 10 | + | /// Clear only the physical range supplied by the allocator. |
|
| 11 | + | fn clear(base: u64, size: u32) { |
|
| 12 | + | let start = base as u32; |
|
| 13 | + | assert base < MEMORY.len as u64; |
|
| 14 | + | assert size <= MEMORY.len - start; |
|
| 15 | + | for i in start..start + size { set MEMORY[i] = 0; } |
|
| 16 | + | } |
|
| 17 | + | ||
| 18 | + | /// Require a contiguous allocation request to fail without taking frames. |
|
| 19 | + | fn exhausted(pool: *mut frames::Pool, count: u32) { |
|
| 20 | + | let available = pool.available; |
|
| 21 | + | let allocation = try frames::allocate(pool, count, clear) catch { |
|
| 22 | + | assert pool.available == available; |
|
| 23 | + | return; |
|
| 24 | + | }; |
|
| 25 | + | frames::discard(pool, allocation); |
|
| 26 | + | panic "exhausted: allocation unexpectedly succeeded"; |
|
| 27 | + | } |
|
| 28 | + | ||
| 29 | + | /// Check unaligned RAM, reserved overlaps, bitmap boundaries, and zeroing. |
|
| 30 | + | export fn run() { |
|
| 31 | + | for i in 0..MEMORY.len { set MEMORY[i] = 0xa5; } |
|
| 32 | + | let mut machine: platform::Platform = undefined; |
|
| 33 | + | set machine.memoryCount = 1; |
|
| 34 | + | set machine.memory[0] = fdt::Range { |
|
| 35 | + | base: frames::PAGE_SIZE as u64 + 1, |
|
| 36 | + | size: frames::PAGE_SIZE as u64 * 66 - 1, |
|
| 37 | + | }; |
|
| 38 | + | set machine.reservedCount = 1; |
|
| 39 | + | set machine.reserved[0] = fdt::Range { |
|
| 40 | + | base: frames::PAGE_SIZE as u64 * 3 + 1, |
|
| 41 | + | size: frames::PAGE_SIZE as u64, |
|
| 42 | + | }; |
|
| 43 | + | let mut pool: frames::Pool = undefined; |
|
| 44 | + | try! frames::init(&mut pool, &machine); |
|
| 45 | + | assert pool.available == 63; |
|
| 46 | + | let allocation = try! frames::allocate(&mut pool, 2, clear); |
|
| 47 | + | let run = frames::install(allocation); |
|
| 48 | + | assert run.first == 5 and run.count == 2; |
|
| 49 | + | for i in 5 * frames::PAGE_SIZE..7 * frames::PAGE_SIZE { |
|
| 50 | + | assert MEMORY[i] == 0; |
|
| 51 | + | } |
|
| 52 | + | assert MEMORY[5 * frames::PAGE_SIZE - 1] == 0xa5; |
|
| 53 | + | assert MEMORY[7 * frames::PAGE_SIZE] == 0xa5; |
|
| 54 | + | exhausted(&mut pool, 61); |
|
| 55 | + | let rest = try! frames::allocate(&mut pool, 60, clear); |
|
| 56 | + | let last = try! frames::allocate(&mut pool, 1, clear); |
|
| 57 | + | assert pool.available == 0; |
|
| 58 | + | exhausted(&mut pool, 1); |
|
| 59 | + | frames::discard(&mut pool, last); |
|
| 60 | + | frames::discard(&mut pool, rest); |
|
| 61 | + | frames::reclaim(&mut pool, run); |
|
| 62 | + | assert pool.available == 63; |
|
| 63 | + | ||
| 64 | + | set machine.memory[0] = fdt::Range { base: 0, size: MEMORY.len as u64 }; |
|
| 65 | + | set machine.reservedCount = 0; |
|
| 66 | + | try! frames::init(&mut pool, &machine); |
|
| 67 | + | let whole = try! frames::allocate(&mut pool, 68, clear); |
|
| 68 | + | assert pool.available == 0; |
|
| 69 | + | frames::discard(&mut pool, whole); |
|
| 70 | + | assert pool.available == 68; |
|
| 71 | + | exhausted(&mut pool, 0); |
|
| 72 | + | exhausted(&mut pool, frames::MAX_FRAMES + 1); |
|
| 73 | + | } |
kernel/core.rad
+1 -0
| 1 | 1 | //! Safe kernel mechanisms shared by machine execution and hosted checks. |
|
| 2 | 2 | ||
| 3 | 3 | export mod fdt; |
|
| 4 | 4 | export mod platform; |
|
| 5 | + | export mod frames; |
kernel/core/frames.rad
added
+144 -0
| 1 | + | //! Contiguous physical frames with explicit allocation ownership. |
|
| 2 | + | ||
| 3 | + | use core::fdt; |
|
| 4 | + | use core::platform; |
|
| 5 | + | ||
| 6 | + | /// Physical allocation unit in bytes. |
|
| 7 | + | export constant PAGE_SIZE: u32 = 4096; |
|
| 8 | + | /// Number of physical frames supported by this build. |
|
| 9 | + | export constant MAX_FRAMES: u32 = 65536; |
|
| 10 | + | /// Words in the frame availability bitmap. |
|
| 11 | + | constant BITMAP_WORDS: u32 = MAX_FRAMES / 64; |
|
| 12 | + | ||
| 13 | + | /// Invalid allocation input or insufficient contiguous frames. |
|
| 14 | + | export union Error: Copy { |
|
| 15 | + | /// The request has zero frames or exceeds the supported physical range. |
|
| 16 | + | Invalid, |
|
| 17 | + | /// No contiguous free range satisfies the request. |
|
| 18 | + | OutOfMemory, |
|
| 19 | + | } |
|
| 20 | + | ||
| 21 | + | /// Physical range metadata held by kernel page objects. |
|
| 22 | + | export record Run: Copy { |
|
| 23 | + | /// Index of the first physical frame. |
|
| 24 | + | first: u32, |
|
| 25 | + | /// Number of contiguous frames. |
|
| 26 | + | count: u32, |
|
| 27 | + | } |
|
| 28 | + | ||
| 29 | + | /// An allocation that must be installed into a kernel object or returned. |
|
| 30 | + | export union Allocation: Once { |
|
| 31 | + | /// Cleared frames exclusively held by this obligation. |
|
| 32 | + | Frames(Run), |
|
| 33 | + | } |
|
| 34 | + | ||
| 35 | + | /// A bitmap of available physical frames. |
|
| 36 | + | export record Pool: Copy { |
|
| 37 | + | /// A set bit identifies a complete, unreserved, unallocated RAM frame. |
|
| 38 | + | free: [u64; BITMAP_WORDS], |
|
| 39 | + | /// Exclusive high frame index of discovered RAM. |
|
| 40 | + | limit: u32, |
|
| 41 | + | /// Number of currently available frames. |
|
| 42 | + | available: u32, |
|
| 43 | + | } |
|
| 44 | + | ||
| 45 | + | /// Initialize available frames from physical RAM and boot reservations. |
|
| 46 | + | export fn init(pool: *mut Pool, machine: *platform::Platform) throws (Error) { |
|
| 47 | + | for i in 0..BITMAP_WORDS { set pool.free[i] = 0; } |
|
| 48 | + | set pool.limit = 0; |
|
| 49 | + | set pool.available = 0; |
|
| 50 | + | let physicalLimit = MAX_FRAMES as u64 * PAGE_SIZE as u64; |
|
| 51 | + | for i in 0..machine.memoryCount { |
|
| 52 | + | let range = machine.memory[i]; |
|
| 53 | + | if range.base > physicalLimit or range.size > physicalLimit - range.base { |
|
| 54 | + | throw Error::Invalid; |
|
| 55 | + | } |
|
| 56 | + | let first = ((range.base + PAGE_SIZE as u64 - 1) / PAGE_SIZE as u64) as u32; |
|
| 57 | + | let end = ((range.base + range.size) / PAGE_SIZE as u64) as u32; |
|
| 58 | + | for frame in first..end { |
|
| 59 | + | let bit = 1 as u64 << (frame as u64 & 63); |
|
| 60 | + | if pool.free[frame / 64] & bit == 0 { |
|
| 61 | + | set pool.free[frame / 64] |= bit; |
|
| 62 | + | set pool.available += 1; |
|
| 63 | + | } |
|
| 64 | + | } |
|
| 65 | + | if end > pool.limit { set pool.limit = end; } |
|
| 66 | + | } |
|
| 67 | + | for i in 0..machine.reservedCount { |
|
| 68 | + | let range = machine.reserved[i]; |
|
| 69 | + | if range.size == 0 or range.base > 0xffffffffffffffff - range.size { |
|
| 70 | + | throw Error::Invalid; |
|
| 71 | + | } |
|
| 72 | + | let first = range.base / PAGE_SIZE as u64; |
|
| 73 | + | let endByte = range.base + range.size; |
|
| 74 | + | let mut end = endByte / PAGE_SIZE as u64; |
|
| 75 | + | if endByte % PAGE_SIZE as u64 <> 0 { set end += 1; } |
|
| 76 | + | if first >= pool.limit as u64 { continue; } |
|
| 77 | + | if end > pool.limit as u64 { set end = pool.limit as u64; } |
|
| 78 | + | for frame in first as u32..end as u32 { |
|
| 79 | + | let bit = 1 as u64 << (frame as u64 & 63); |
|
| 80 | + | if pool.free[frame / 64] & bit <> 0 { |
|
| 81 | + | set pool.free[frame / 64] &= ~bit; |
|
| 82 | + | set pool.available -= 1; |
|
| 83 | + | } |
|
| 84 | + | } |
|
| 85 | + | } |
|
| 86 | + | } |
|
| 87 | + | ||
| 88 | + | /// Take contiguous frames and clear them before returning their ownership. |
|
| 89 | + | /// The clear function must write exactly the supplied physical byte range. |
|
| 90 | + | export fn allocate(pool: *mut Pool, count: u32, clear: fn(u64, u32)) -> Allocation throws (Error) { |
|
| 91 | + | if count == 0 or count > MAX_FRAMES { throw Error::Invalid; } |
|
| 92 | + | if count > pool.available { throw Error::OutOfMemory; } |
|
| 93 | + | let mut length: u32 = 0; |
|
| 94 | + | let mut frame: u32 = 0; |
|
| 95 | + | while frame < pool.limit { |
|
| 96 | + | let word = pool.free[frame / 64]; |
|
| 97 | + | if word == 0 { |
|
| 98 | + | set length = 0; |
|
| 99 | + | set frame = (frame + 64) & ~63; |
|
| 100 | + | continue; |
|
| 101 | + | } |
|
| 102 | + | if word & (1 as u64 << (frame as u64 & 63)) <> 0 { |
|
| 103 | + | set length += 1; |
|
| 104 | + | if length == count { |
|
| 105 | + | let first = frame + 1 - count; |
|
| 106 | + | for current in first..frame + 1 { |
|
| 107 | + | set pool.free[current / 64] &= ~(1 as u64 << (current as u64 & 63)); |
|
| 108 | + | } |
|
| 109 | + | set pool.available -= count; |
|
| 110 | + | clear(first as u64 * PAGE_SIZE as u64, count * PAGE_SIZE); |
|
| 111 | + | return Allocation::Frames(Run { first, count }); |
|
| 112 | + | } |
|
| 113 | + | } else { |
|
| 114 | + | set length = 0; |
|
| 115 | + | } |
|
| 116 | + | set frame += 1; |
|
| 117 | + | } |
|
| 118 | + | throw Error::OutOfMemory; |
|
| 119 | + | } |
|
| 120 | + | ||
| 121 | + | /// Consume allocation ownership when a page object assumes its lifetime. |
|
| 122 | + | export fn install(allocation: Allocation) -> Run { |
|
| 123 | + | match allocation { |
|
| 124 | + | case Allocation::Frames(run) => return run, |
|
| 125 | + | } |
|
| 126 | + | } |
|
| 127 | + | ||
| 128 | + | /// Return frames after all object and persistent-grant lifetimes have ended. |
|
| 129 | + | export fn reclaim(pool: *mut Pool, run: Run) { |
|
| 130 | + | assert run.count > 0 and run.first <= pool.limit; |
|
| 131 | + | assert run.count <= pool.limit - run.first; |
|
| 132 | + | for frame in run.first..run.first + run.count { |
|
| 133 | + | let bit = 1 as u64 << (frame as u64 & 63); |
|
| 134 | + | assert pool.free[frame / 64] & bit == 0; |
|
| 135 | + | set pool.free[frame / 64] |= bit; |
|
| 136 | + | } |
|
| 137 | + | set pool.available += run.count; |
|
| 138 | + | } |
|
| 139 | + | ||
| 140 | + | /// Return an allocation that was not installed into a page object. |
|
| 141 | + | export fn discard(pool: *mut Pool, allocation: Allocation) { |
|
| 142 | + | let run = install(allocation); |
|
| 143 | + | reclaim(pool, run); |
|
| 144 | + | } |