kernel: validate typed handles and retire exhausted slots
2fa6ad333439a5b01ae9e54e5f82483df5a24b0933e34140242513860d2ba28c
Verified: make -C kernel check with the machine-capable emulator; all pass.
1 parent
71bd6f97
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 core/frames.rad |
|
| 6 | + | MODULES := core/fdt.rad core/platform.rad core/frames.rad core/abi.rad core/handles.rad |
|
| 7 | 7 | CORE := -pkg core -mod core.rad $(addprefix -mod ,$(MODULES)) |
|
| 8 | - | CHECK_MODULES := check/boot.rad check/fixture.rad check/frames.rad |
|
| 8 | + | CHECK_MODULES := check/boot.rad check/fixture.rad check/frames.rad check/handles.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
+15 -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 3 of the 22-step plan. |
|
| 5 | + | record the contracts established through step 4 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. |
| 41 | 41 | - Allocation clears frames before a new domain receives access. Kernel/image, |
|
| 42 | 42 | FDT, boot data, firmware reservations, and MMIO regions are excluded. |
|
| 43 | 43 | - Contiguous allocations validate complete ranges and fail without partial |
|
| 44 | 44 | ownership when memory or metadata capacity is unavailable. |
|
| 45 | 45 | ||
| 46 | + | ## Handles and rights |
|
| 47 | + | ||
| 48 | + | - A handle packs kind [63:60], generation [59:28], zero reserved bits [27:24], |
|
| 49 | + | and slot [23:0]. It is domain-relative authority, never a kernel pointer. |
|
| 50 | + | This build has 512 handle slots per domain and up to 512 domains. |
|
| 51 | + | - Object identities include kind, index, and nonzero incarnation. Slot generation |
|
| 52 | + | and object incarnation are distinct stale-name defenses. Exhausted generations |
|
| 53 | + | and incarnations are retired rather than wrapped. |
|
| 54 | + | - Rights are Read=1, Write=2, Execute=4, Grant=8, Transfer=16, Create=32, |
|
| 55 | + | Allocate=64, Wake=128, Destroy=256; higher bits are reserved. New Page rights |
|
| 56 | + | are Read|Write|Grant|Transfer. New Domain rights are |
|
| 57 | + | Destroy|Execute|Grant|Transfer|Wake; root additionally has Create|Allocate. |
|
| 58 | + | ||
| 46 | 59 | ## Validation |
|
| 47 | 60 | ||
| 48 | 61 | Use the current machine-capable sibling emulator. Set `RAD_EMULATOR`, pass |
|
| 49 | 62 | `EMU` to the kernel Make invocation, or put `emulator` on PATH. The kernel build |
|
| 50 | 63 | checks compiler dependencies. From the repository root, run: |
|
| 51 | 64 | ||
| 52 | 65 | ```sh |
|
| 53 | 66 | make -C kernel check |
|
| 54 | 67 | ``` |
|
| 55 | 68 | ||
| 56 | - | Exercise contiguous allocation, clearing, reservation exclusion, exhaustion, and exact physical boundaries. |
|
| 69 | + | Exercise invented and stale names, reserved bits, wrong kinds, rights checks, and generation retirement. |
|
| 57 | 70 | ||
| 58 | 71 | The entry probe uses explicit M-mode success/fault finish writes; secondary |
|
| 59 | 72 | harts idle. This checks machine entry, not user-domain execution. Finish writes |
|
| 60 | 73 | 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 | 5 | mod frames; |
|
| 6 | + | mod handles; |
|
| 6 | 7 | ||
| 7 | 8 | /// Run the available kernel mechanism checks. |
|
| 8 | 9 | @default fn main() -> u32 { |
|
| 9 | 10 | frames::run(); |
|
| 10 | 11 | boot::run(); |
|
| 12 | + | handles::run(); |
|
| 11 | 13 | return 0; |
|
| 12 | 14 | } |
kernel/check/handles.rad
added
+51 -0
| 1 | + | //! Capability lookup authority, rights, and stale-generation checks. |
|
| 2 | + | ||
| 3 | + | use core::abi; |
|
| 4 | + | use core::handles; |
|
| 5 | + | ||
| 6 | + | /// Require a handle lookup to fail with the specified error. |
|
| 7 | + | fn reject(table: *handles::Table, handle: abi::Handle, kind: abi::Kind, rights: u16, expected: abi::Error) { |
|
| 8 | + | let _entry = try handles::require(table, handle, kind, rights) catch error { |
|
| 9 | + | assert error == expected; |
|
| 10 | + | return; |
|
| 11 | + | }; |
|
| 12 | + | panic "reject: invalid authority accepted"; |
|
| 13 | + | } |
|
| 14 | + | ||
| 15 | + | /// Exercise packed handles, rights checks, reuse, and generation exhaustion. |
|
| 16 | + | export fn run() { |
|
| 17 | + | let mut table: handles::Table = undefined; |
|
| 18 | + | let mut other: handles::Table = undefined; |
|
| 19 | + | handles::init(&mut table); |
|
| 20 | + | handles::init(&mut other); |
|
| 21 | + | let page = abi::Object { kind: abi::Kind::Page, index: 9, epoch: 1 }; |
|
| 22 | + | let original = handles::install(&mut table, 0, page, abi::READ); |
|
| 23 | + | let entry = try! handles::require(&table, original, abi::Kind::Page, abi::READ); |
|
| 24 | + | assert entry.object.index == 9 and entry.object.epoch == 1; |
|
| 25 | + | reject(&table, original, abi::Kind::Page, abi::WRITE, abi::Error::Denied); |
|
| 26 | + | reject(&table, original, abi::Kind::Domain, 0, abi::Error::BadHandle); |
|
| 27 | + | reject(&other, original, abi::Kind::Page, 0, abi::Error::BadHandle); |
|
| 28 | + | reject(&table, abi::Handle { bits: 0 }, abi::Kind::Page, 0, abi::Error::BadHandle); |
|
| 29 | + | reject(&table, abi::Handle { bits: original.bits | 0x1000000 }, abi::Kind::Page, 0, abi::Error::BadHandle); |
|
| 30 | + | reject(&table, abi::Handle { bits: original.bits | 0xffffff }, abi::Kind::Page, 0, abi::Error::BadHandle); |
|
| 31 | + | reject(&table, abi::Handle { bits: original.bits ^ (3 as u64 << 60) }, abi::Kind::Page, 0, abi::Error::BadHandle); |
|
| 32 | + | let _removed = handles::remove(&mut table, 0); |
|
| 33 | + | let replacement = handles::install(&mut table, 0, page, abi::READ | abi::WRITE); |
|
| 34 | + | reject(&table, original, abi::Kind::Page, 0, abi::Error::BadHandle); |
|
| 35 | + | let _valid = try! handles::require(&table, replacement, abi::Kind::Page, abi::WRITE); |
|
| 36 | + | let _again = handles::remove(&mut table, 0); |
|
| 37 | + | set table.entries[0].generation = 0xffffffff; |
|
| 38 | + | let last = handles::install(&mut table, 0, page, abi::READ); |
|
| 39 | + | let _last = handles::remove(&mut table, 0); |
|
| 40 | + | reject(&table, last, abi::Kind::Page, 0, abi::Error::BadHandle); |
|
| 41 | + | let next = try! handles::vacant(&table); |
|
| 42 | + | assert next == 1; |
|
| 43 | + | for i in 1..abi::MAX_HANDLES { |
|
| 44 | + | let _handle = handles::install(&mut table, i, page, abi::READ); |
|
| 45 | + | } |
|
| 46 | + | let _slot = try handles::vacant(&table) catch error { |
|
| 47 | + | assert error == abi::Error::Exhausted; |
|
| 48 | + | return; |
|
| 49 | + | }; |
|
| 50 | + | panic "run: full capability table accepted an entry"; |
|
| 51 | + | } |
kernel/core.rad
+2 -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 | 5 | export mod frames; |
|
| 6 | + | export mod abi; |
|
| 7 | + | export mod handles; |
kernel/core/abi.rad
added
+91 -0
| 1 | + | //! Stable object kinds, rights, handles, and direct-call errors. |
|
| 2 | + | ||
| 3 | + | /// Per-domain capability slots in this build. |
|
| 4 | + | export constant MAX_HANDLES: u32 = 512; |
|
| 5 | + | /// Maximum live domains in this build. |
|
| 6 | + | export constant MAX_DOMAINS: u32 = 512; |
|
| 7 | + | /// A domain identifier that cannot name a live domain. |
|
| 8 | + | export constant INVALID_DOMAIN: u32 = 0xffff; |
|
| 9 | + | ||
| 10 | + | /// Kernel object kinds in packed-handle order. |
|
| 11 | + | export union Kind: Copy { |
|
| 12 | + | /// An unoccupied capability slot. |
|
| 13 | + | Empty, |
|
| 14 | + | /// Contiguous physical memory. |
|
| 15 | + | Page, |
|
| 16 | + | /// One external interrupt source. |
|
| 17 | + | Interrupt, |
|
| 18 | + | /// One device register region. |
|
| 19 | + | Device, |
|
| 20 | + | /// A domain's notification queue. |
|
| 21 | + | Events, |
|
| 22 | + | /// A protection domain. |
|
| 23 | + | Domain, |
|
| 24 | + | /// Finite execution authority on one hart. |
|
| 25 | + | Budget, |
|
| 26 | + | } |
|
| 27 | + | ||
| 28 | + | /// Direct-call error codes in ABI order. |
|
| 29 | + | export union Error: Copy { |
|
| 30 | + | /// The operation succeeded. |
|
| 31 | + | Ok, |
|
| 32 | + | /// The handle is absent, stale, malformed, or of the wrong kind. |
|
| 33 | + | BadHandle, |
|
| 34 | + | /// Required capability rights are absent. |
|
| 35 | + | Denied, |
|
| 36 | + | /// No suitable physical memory remains. |
|
| 37 | + | OutOfMemory, |
|
| 38 | + | /// An argument is invalid for this operation. |
|
| 39 | + | InvalidArg, |
|
| 40 | + | /// The resource or a bounded request queue is busy. |
|
| 41 | + | Busy, |
|
| 42 | + | /// Image verification failed. |
|
| 43 | + | VerifyFailed, |
|
| 44 | + | /// The target is not pending activation. |
|
| 45 | + | NotPending, |
|
| 46 | + | /// A bounded kernel resource is fully committed. |
|
| 47 | + | Exhausted, |
|
| 48 | + | } |
|
| 49 | + | ||
| 50 | + | /// Read or derive read-only resource access. |
|
| 51 | + | export constant READ: u16 = 1; |
|
| 52 | + | /// Write or derive writable resource access. |
|
| 53 | + | export constant WRITE: u16 = 2; |
|
| 54 | + | /// Activate an execution context. |
|
| 55 | + | export constant EXECUTE: u16 = 4; |
|
| 56 | + | /// Copy authority to another domain. |
|
| 57 | + | export constant GRANT: u16 = 8; |
|
| 58 | + | /// Move authority to another domain. |
|
| 59 | + | export constant TRANSFER: u16 = 16; |
|
| 60 | + | /// Create protection domains. |
|
| 61 | + | export constant CREATE: u16 = 32; |
|
| 62 | + | /// Allocate from the physical resource pool. |
|
| 63 | + | export constant ALLOCATE: u16 = 64; |
|
| 64 | + | /// Append a wakeup event to a domain. |
|
| 65 | + | export constant WAKE: u16 = 128; |
|
| 66 | + | /// Destroy a protection domain. |
|
| 67 | + | export constant DESTROY: u16 = 256; |
|
| 68 | + | /// All defined rights; high bits are reserved. |
|
| 69 | + | export constant RIGHTS: u16 = 511; |
|
| 70 | + | /// Rights on a new Page allocation. |
|
| 71 | + | export constant PAGE_RIGHTS: u16 = READ | WRITE | GRANT | TRANSFER; |
|
| 72 | + | /// Rights on a newly created Domain handle. |
|
| 73 | + | export constant DOMAIN_RIGHTS: u16 = DESTROY | EXECUTE | GRANT | TRANSFER | WAKE; |
|
| 74 | + | /// Initial root authority on its own Domain. |
|
| 75 | + | export constant ROOT_RIGHTS: u16 = DOMAIN_RIGHTS | CREATE | ALLOCATE; |
|
| 76 | + | ||
| 77 | + | /// A domain-relative packed capability identifier, not a kernel pointer. |
|
| 78 | + | export record Handle: Copy { |
|
| 79 | + | /// Kind [63:60], generation [59:28], reserved [27:24], slot [23:0]. |
|
| 80 | + | bits: u64, |
|
| 81 | + | } |
|
| 82 | + | ||
| 83 | + | /// Identity of a kernel object across slot reuse. |
|
| 84 | + | export record Object: Copy { |
|
| 85 | + | /// Resource table selected by this identity. |
|
| 86 | + | kind: Kind, |
|
| 87 | + | /// Index in that resource table. |
|
| 88 | + | index: u32, |
|
| 89 | + | /// Incarnation of the object slot; zero is never live. |
|
| 90 | + | epoch: u32, |
|
| 91 | + | } |
kernel/core/handles.rad
added
+102 -0
| 1 | + | //! Domain-local capability lookup and slot generation management. |
|
| 2 | + | ||
| 3 | + | use core::abi; |
|
| 4 | + | ||
| 5 | + | /// One authoritative capability-table entry. |
|
| 6 | + | export record Entry: Copy { |
|
| 7 | + | /// Named object and its incarnation. |
|
| 8 | + | object: abi::Object, |
|
| 9 | + | /// Rights held through this entry. |
|
| 10 | + | rights: u16, |
|
| 11 | + | /// Slot generation; zero marks permanent retirement. |
|
| 12 | + | generation: u32, |
|
| 13 | + | } |
|
| 14 | + | ||
| 15 | + | /// Fixed storage for a domain's capabilities. |
|
| 16 | + | export record Table: Copy { |
|
| 17 | + | /// Live and empty slots; slot positions do not move. |
|
| 18 | + | entries: [Entry; abi::MAX_HANDLES], |
|
| 19 | + | } |
|
| 20 | + | ||
| 21 | + | /// Initialize a table once, before its first domain incarnation. |
|
| 22 | + | export fn init(table: *mut Table) { |
|
| 23 | + | for i in 0..abi::MAX_HANDLES { |
|
| 24 | + | set table.entries[i] = Entry { |
|
| 25 | + | object: abi::Object { kind: abi::Kind::Empty, index: 0, epoch: 0 }, |
|
| 26 | + | rights: 0, generation: 1, |
|
| 27 | + | }; |
|
| 28 | + | } |
|
| 29 | + | } |
|
| 30 | + | ||
| 31 | + | /// Find an empty slot whose generation has not been retired. |
|
| 32 | + | export fn vacant(table: *Table) -> u32 throws (abi::Error) { |
|
| 33 | + | for i in 0..abi::MAX_HANDLES { |
|
| 34 | + | let entry = &table.entries[i]; |
|
| 35 | + | if entry.object.kind == abi::Kind::Empty and entry.generation <> 0 { return i; } |
|
| 36 | + | } |
|
| 37 | + | throw abi::Error::Exhausted; |
|
| 38 | + | } |
|
| 39 | + | ||
| 40 | + | /// Install authority into a known empty slot and return its packed handle. |
|
| 41 | + | export fn install(table: *mut Table, slot: u32, object: abi::Object, rights: u16) -> abi::Handle { |
|
| 42 | + | assert slot < abi::MAX_HANDLES and object.kind <> abi::Kind::Empty and object.epoch <> 0; |
|
| 43 | + | let entry = &mut table.entries[slot]; |
|
| 44 | + | assert entry.object.kind == abi::Kind::Empty and entry.generation <> 0; |
|
| 45 | + | set entry.object = object; |
|
| 46 | + | set entry.rights = rights & abi::RIGHTS; |
|
| 47 | + | return abi::Handle { |
|
| 48 | + | bits: (object.kind as u64 << 60) | (entry.generation as u64 << 28) | slot as u64, |
|
| 49 | + | }; |
|
| 50 | + | } |
|
| 51 | + | ||
| 52 | + | /// Validate the packed handle and return its authoritative table index. |
|
| 53 | + | export fn slot(table: *Table, handle: abi::Handle) -> u32 throws (abi::Error) { |
|
| 54 | + | let bits = handle.bits; |
|
| 55 | + | let index = (bits & 0xffffff) as u32; |
|
| 56 | + | if bits == 0 or (bits & 0xf000000) <> 0 or index >= abi::MAX_HANDLES { |
|
| 57 | + | throw abi::Error::BadHandle; |
|
| 58 | + | } |
|
| 59 | + | let entry = &table.entries[index]; |
|
| 60 | + | if entry.object.kind == abi::Kind::Empty or (bits >> 60) <> entry.object.kind as u64 |
|
| 61 | + | or ((bits >> 28) & 0xffffffff) <> entry.generation as u64 { |
|
| 62 | + | throw abi::Error::BadHandle; |
|
| 63 | + | } |
|
| 64 | + | return index; |
|
| 65 | + | } |
|
| 66 | + | ||
| 67 | + | /// Require a live handle of the given kind with all requested rights. |
|
| 68 | + | /// The object owner must also check the object's epoch and lifecycle. |
|
| 69 | + | export fn require(table: *Table, handle: abi::Handle, kind: abi::Kind, rights: u16) -> Entry throws (abi::Error) { |
|
| 70 | + | let index = try slot(table, handle); |
|
| 71 | + | let entry = table.entries[index]; |
|
| 72 | + | if entry.object.kind <> kind { throw abi::Error::BadHandle; } |
|
| 73 | + | if entry.rights & rights <> rights { throw abi::Error::Denied; } |
|
| 74 | + | return entry; |
|
| 75 | + | } |
|
| 76 | + | ||
| 77 | + | /// Invalidate a slot before reuse and return its former authority. |
|
| 78 | + | /// Generation exhaustion permanently retires the slot. |
|
| 79 | + | export fn remove(table: *mut Table, index: u32) -> Entry { |
|
| 80 | + | assert index < abi::MAX_HANDLES; |
|
| 81 | + | let previous = table.entries[index]; |
|
| 82 | + | assert previous.object.kind <> abi::Kind::Empty; |
|
| 83 | + | let entry = &mut table.entries[index]; |
|
| 84 | + | set entry.object = abi::Object { kind: abi::Kind::Empty, index: 0, epoch: 0 }; |
|
| 85 | + | set entry.rights = 0; |
|
| 86 | + | if previous.generation == 0xffffffff { |
|
| 87 | + | set entry.generation = 0; |
|
| 88 | + | } else { |
|
| 89 | + | set entry.generation = previous.generation + 1; |
|
| 90 | + | } |
|
| 91 | + | return previous; |
|
| 92 | + | } |
|
| 93 | + | ||
| 94 | + | /// Read a live slot by kind without constructing a handle from an index. |
|
| 95 | + | export fn get(table: *Table, index: u32, kind: abi::Kind) -> abi::Handle throws (abi::Error) { |
|
| 96 | + | if index >= abi::MAX_HANDLES or kind == abi::Kind::Empty { throw abi::Error::BadHandle; } |
|
| 97 | + | let entry = &table.entries[index]; |
|
| 98 | + | if entry.object.kind <> kind { throw abi::Error::BadHandle; } |
|
| 99 | + | return abi::Handle { |
|
| 100 | + | bits: (kind as u64 << 60) | (entry.generation as u64 << 28) | index as u64, |
|
| 101 | + | }; |
|
| 102 | + | } |