kernel: create bounded domains with private event state
f126a2b75baefc37668be7668848af8d87d469702419a9a0b05a4c2dcffc8e2c
Verified: make -C kernel check with the machine-capable emulator; all pass.
1 parent
2fa6ad33
kernel/Makefile
+3 -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 core/abi.rad core/handles.rad |
|
| 6 | + | MODULES := core/fdt.rad core/platform.rad core/frames.rad core/abi.rad core/handles.rad \ |
|
| 7 | + | core/events.rad core/domains.rad |
|
| 7 | 8 | CORE := -pkg core -mod core.rad $(addprefix -mod ,$(MODULES)) |
|
| 8 | - | CHECK_MODULES := check/boot.rad check/fixture.rad check/frames.rad check/handles.rad |
|
| 9 | + | CHECK_MODULES := check/boot.rad check/fixture.rad check/frames.rad check/handles.rad check/domains.rad |
|
| 9 | 10 | ||
| 10 | 11 | .PHONY: all check clean compiler-check |
|
| 11 | 12 | all: kernel.rv64 |
|
| 12 | 13 | ||
| 13 | 14 | compiler-check: |
kernel/NOTES.md
+13 -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 4 of the 22-step plan. |
|
| 5 | + | record the contracts established through step 5 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. |
| 54 | 54 | - Rights are Read=1, Write=2, Execute=4, Grant=8, Transfer=16, Create=32, |
|
| 55 | 55 | Allocate=64, Wake=128, Destroy=256; higher bits are reserved. New Page rights |
|
| 56 | 56 | are Read|Write|Grant|Transfer. New Domain rights are |
|
| 57 | 57 | Destroy|Execute|Grant|Transfer|Wake; root additionally has Create|Allocate. |
|
| 58 | 58 | ||
| 59 | + | ## Domain storage and initial authority |
|
| 60 | + | ||
| 61 | + | - Mechanisms borrow bounded caller-owned storage. Each domain has private |
|
| 62 | + | capability and event-producer state. Lookup accepts only live Pending or |
|
| 63 | + | Active domain identities. |
|
| 64 | + | - Zero as a self-sentinel identifies the caller but supplies no resource right. |
|
| 65 | + | Allocate and Create require a live capability to that caller with the right. |
|
| 66 | + | A newly created domain has only its installed Read|Write Events handle. |
|
| 67 | + | - Accepting a child reserves one terminal event. At most 128 credits can be |
|
| 68 | + | outstanding in a 256-entry Events queue. |
|
| 69 | + | ||
| 59 | 70 | ## Validation |
|
| 60 | 71 | ||
| 61 | 72 | Use the current machine-capable sibling emulator. Set `RAD_EMULATOR`, pass |
|
| 62 | 73 | `EMU` to the kernel Make invocation, or put `emulator` on PATH. The kernel build |
|
| 63 | 74 | checks compiler dependencies. From the repository root, run: |
|
| 64 | 75 | ||
| 65 | 76 | ```sh |
|
| 66 | 77 | make -C kernel check |
|
| 67 | 78 | ``` |
|
| 68 | 79 | ||
| 69 | - | Exercise invented and stale names, reserved bits, wrong kinds, rights checks, and generation retirement. |
|
| 80 | + | Exercise empty startup authority, self-sentinel checks, bounded domain storage, relationship credits, and identity reuse. |
|
| 70 | 81 | ||
| 71 | 82 | The entry probe uses explicit M-mode success/fault finish writes; secondary |
|
| 72 | 83 | harts idle. This checks machine entry, not user-domain execution. Finish writes |
|
| 73 | 84 | are a check protocol, not a domain-exit operation. |
kernel/check.rad
+2 -0
| 2 | 2 | ||
| 3 | 3 | mod boot; |
|
| 4 | 4 | export mod fixture; |
|
| 5 | 5 | mod frames; |
|
| 6 | 6 | mod handles; |
|
| 7 | + | mod domains; |
|
| 7 | 8 | ||
| 8 | 9 | /// Run the available kernel mechanism checks. |
|
| 9 | 10 | @default fn main() -> u32 { |
|
| 10 | 11 | frames::run(); |
|
| 11 | 12 | boot::run(); |
|
| 12 | 13 | handles::run(); |
|
| 14 | + | domains::run(); |
|
| 13 | 15 | return 0; |
|
| 14 | 16 | } |
kernel/check/domains.rad
added
+40 -0
| 1 | + | //! Pending-domain authority and bounded creation checks. |
|
| 2 | + | ||
| 3 | + | use core::abi; |
|
| 4 | + | use core::domains; |
|
| 5 | + | use core::handles; |
|
| 6 | + | ||
| 7 | + | /// Small domain pool used to exercise resource exhaustion. |
|
| 8 | + | static DOMAINS: [domains::Domain; 4] = undefined; |
|
| 9 | + | ||
| 10 | + | /// Require the self sentinel to fail without explicit resource authority. |
|
| 11 | + | fn denied(caller: u32) { |
|
| 12 | + | let _object = try domains::authority(&DOMAINS[..], caller, abi::Handle { bits: 0 }, abi::ALLOCATE) catch error { |
|
| 13 | + | assert error == abi::Error::Denied; |
|
| 14 | + | return; |
|
| 15 | + | }; |
|
| 16 | + | panic "denied: self sentinel created authority"; |
|
| 17 | + | } |
|
| 18 | + | ||
| 19 | + | /// Check initial root rights and the empty authority of pending children. |
|
| 20 | + | export fn run() { |
|
| 21 | + | domains::init(&mut DOMAINS[..]); |
|
| 22 | + | let rootHandle = domains::root(&mut DOMAINS[..], 0); |
|
| 23 | + | let root = try! domains::resolve(&DOMAINS[..], 0, rootHandle, abi::CREATE | abi::ALLOCATE); |
|
| 24 | + | let child = try! domains::create(&mut DOMAINS[..], root, 1); |
|
| 25 | + | assert domains::live(&DOMAINS[..], child); |
|
| 26 | + | denied(child.index); |
|
| 27 | + | let queue = try! handles::get(&DOMAINS[child.index].handles, 1, abi::Kind::Events); |
|
| 28 | + | let _events = try! handles::require(&DOMAINS[child.index].handles, queue, abi::Kind::Events, abi::READ | abi::WRITE); |
|
| 29 | + | let _invalid = try handles::get(&DOMAINS[child.index].handles, 0, abi::Kind::Domain) catch error { |
|
| 30 | + | assert error == abi::Error::BadHandle; |
|
| 31 | + | let _second = try! domains::create(&mut DOMAINS[..], root, 1); |
|
| 32 | + | let _third = try! domains::create(&mut DOMAINS[..], root, 1); |
|
| 33 | + | let _fourth = try domains::create(&mut DOMAINS[..], root, 1) catch full { |
|
| 34 | + | assert full == abi::Error::Exhausted; |
|
| 35 | + | return; |
|
| 36 | + | }; |
|
| 37 | + | panic "run: exhausted domain pool accepted a child"; |
|
| 38 | + | }; |
|
| 39 | + | panic "run: child received ambient Domain authority"; |
|
| 40 | + | } |
kernel/core.rad
+2 -0
| 3 | 3 | export mod fdt; |
|
| 4 | 4 | export mod platform; |
|
| 5 | 5 | export mod frames; |
|
| 6 | 6 | export mod abi; |
|
| 7 | 7 | export mod handles; |
|
| 8 | + | export mod events; |
|
| 9 | + | export mod domains; |
kernel/core/domains.rad
added
+161 -0
| 1 | + | //! Domain identities, initial authority, and private startup state. |
|
| 2 | + | ||
| 3 | + | use core::abi; |
|
| 4 | + | use core::events; |
|
| 5 | + | use core::handles; |
|
| 6 | + | ||
| 7 | + | /// Domain state; Dying is an internal quiescence phase. |
|
| 8 | + | export union Lifecycle: Copy { |
|
| 9 | + | /// Startup capabilities can be installed before activation. |
|
| 10 | + | Pending, |
|
| 11 | + | /// Authorized execution contexts can run. |
|
| 12 | + | Active, |
|
| 13 | + | /// No execution or handle lookup is permitted. |
|
| 14 | + | Dead, |
|
| 15 | + | /// Contexts must stop before resources can be reclaimed. |
|
| 16 | + | Dying, |
|
| 17 | + | } |
|
| 18 | + | ||
| 19 | + | /// Fixed startup environment; each field occupies eight bytes. |
|
| 20 | + | export record Env: Copy { |
|
| 21 | + | /// Readable startup argument range base. |
|
| 22 | + | argsPointer: u64, |
|
| 23 | + | /// Number of argument bytes. |
|
| 24 | + | argsSize: u64, |
|
| 25 | + | /// Packed Events handle in this domain's table. |
|
| 26 | + | eventsHandle: u64, |
|
| 27 | + | /// Physical address of this domain's shared event ring. |
|
| 28 | + | eventsPointer: u64, |
|
| 29 | + | /// Physical base of this image instance's mutable data. |
|
| 30 | + | stateBase: u64, |
|
| 31 | + | /// Lowest address available to this context's stack frames. |
|
| 32 | + | stackBase: u64, |
|
| 33 | + | /// Exclusive upper bound of this context's stack. |
|
| 34 | + | stackTop: u64, |
|
| 35 | + | /// Kernel execution-context identity. |
|
| 36 | + | context: u64, |
|
| 37 | + | } |
|
| 38 | + | ||
| 39 | + | /// One protection domain and its private authority boundary. |
|
| 40 | + | export record Domain: Copy { |
|
| 41 | + | /// Domain-relative capability names. |
|
| 42 | + | handles: handles::Table, |
|
| 43 | + | /// Notification storage and private producer state. |
|
| 44 | + | events: events::Queue, |
|
| 45 | + | /// Live incarnation, or zero before the first allocation. |
|
| 46 | + | epoch: u32, |
|
| 47 | + | /// Protection-domain lifecycle. |
|
| 48 | + | state: Lifecycle, |
|
| 49 | + | /// Historical creator identity; independent of the current parent. |
|
| 50 | + | creator: abi::Object, |
|
| 51 | + | /// Current receiver of terminal lifecycle notifications. |
|
| 52 | + | parent: abi::Object, |
|
| 53 | + | /// Trusted immutable image registry index. |
|
| 54 | + | image: u32, |
|
| 55 | + | /// Initial execution environment. |
|
| 56 | + | env: Env, |
|
| 57 | + | } |
|
| 58 | + | ||
| 59 | + | /// Return the identity used for a missing parent or creator. |
|
| 60 | + | export fn none() -> abi::Object { |
|
| 61 | + | return abi::Object { kind: abi::Kind::Empty, index: abi::INVALID_DOMAIN, epoch: 0 }; |
|
| 62 | + | } |
|
| 63 | + | ||
| 64 | + | /// Initialize caller-owned domain storage once at boot. |
|
| 65 | + | export fn init(domains: *mut [Domain]) { |
|
| 66 | + | assert domains.len > 0 and domains.len <= abi::MAX_DOMAINS; |
|
| 67 | + | for i in 0..domains.len { |
|
| 68 | + | let domain = &mut domains[i]; |
|
| 69 | + | handles::init(&mut domain.handles); |
|
| 70 | + | events::init(&mut domain.events); |
|
| 71 | + | set domain.epoch = 0; |
|
| 72 | + | set domain.state = Lifecycle::Dead; |
|
| 73 | + | set domain.creator = none(); |
|
| 74 | + | set domain.parent = none(); |
|
| 75 | + | set domain.image = 0; |
|
| 76 | + | set domain.env = Env { |
|
| 77 | + | argsPointer: 0, argsSize: 0, eventsHandle: 0, eventsPointer: 0, |
|
| 78 | + | stateBase: 0, stackBase: 0, stackTop: 0, context: 0, |
|
| 79 | + | }; |
|
| 80 | + | } |
|
| 81 | + | } |
|
| 82 | + | ||
| 83 | + | /// Check that an object identifies a live, non-dying domain incarnation. |
|
| 84 | + | export fn live(domains: *[Domain], object: abi::Object) -> bool { |
|
| 85 | + | if object.kind <> abi::Kind::Domain or object.index >= domains.len or object.epoch == 0 { |
|
| 86 | + | return false; |
|
| 87 | + | } |
|
| 88 | + | let domain = &domains[object.index]; |
|
| 89 | + | return domain.epoch == object.epoch |
|
| 90 | + | and (domain.state == Lifecycle::Pending or domain.state == Lifecycle::Active); |
|
| 91 | + | } |
|
| 92 | + | ||
| 93 | + | /// Get a live domain identity from a capability with all required rights. |
|
| 94 | + | export fn resolve(domains: *[Domain], caller: u32, handle: abi::Handle, rights: u16) -> abi::Object throws (abi::Error) { |
|
| 95 | + | assert caller < domains.len; |
|
| 96 | + | let entry = try handles::require(&domains[caller].handles, handle, abi::Kind::Domain, rights); |
|
| 97 | + | if not live(domains, entry.object) { throw abi::Error::BadHandle; } |
|
| 98 | + | return entry.object; |
|
| 99 | + | } |
|
| 100 | + | ||
| 101 | + | /// Resolve self authority by finding a live capability to the caller. |
|
| 102 | + | /// The zero sentinel does not supply Allocate or Create rights itself. |
|
| 103 | + | export fn authority(domains: *[Domain], caller: u32, handle: abi::Handle, rights: u16) -> abi::Object throws (abi::Error) { |
|
| 104 | + | if handle.bits <> 0 { return try resolve(domains, caller, handle, rights); } |
|
| 105 | + | let domain = &domains[caller]; |
|
| 106 | + | for i in 0..abi::MAX_HANDLES { |
|
| 107 | + | let entry = &domain.handles.entries[i]; |
|
| 108 | + | if entry.object.kind == abi::Kind::Domain and entry.object.index == caller |
|
| 109 | + | and entry.object.epoch == domain.epoch and entry.rights & rights == rights { |
|
| 110 | + | return entry.object; |
|
| 111 | + | } |
|
| 112 | + | } |
|
| 113 | + | throw abi::Error::Denied; |
|
| 114 | + | } |
|
| 115 | + | ||
| 116 | + | /// Prepare a free slot with an Events handle and no ambient authority. |
|
| 117 | + | fn prepare(domains: *mut [Domain], index: u32, creator: abi::Object, image: u32) -> abi::Object { |
|
| 118 | + | let domain = &mut domains[index]; |
|
| 119 | + | assert domain.state == Lifecycle::Dead and domain.epoch < 0xffffffff; |
|
| 120 | + | for i in 0..abi::MAX_HANDLES { |
|
| 121 | + | assert domain.handles.entries[i].object.kind == abi::Kind::Empty; |
|
| 122 | + | } |
|
| 123 | + | set domain.epoch += 1; |
|
| 124 | + | set domain.creator = creator; |
|
| 125 | + | set domain.parent = creator; |
|
| 126 | + | set domain.image = image; |
|
| 127 | + | events::init(&mut domain.events); |
|
| 128 | + | let object = abi::Object { kind: abi::Kind::Domain, index, epoch: domain.epoch }; |
|
| 129 | + | let eventObject = abi::Object { kind: abi::Kind::Events, index, epoch: domain.epoch }; |
|
| 130 | + | let eventHandle = handles::install(&mut domain.handles, 1, eventObject, abi::READ | abi::WRITE); |
|
| 131 | + | set domain.env = Env { |
|
| 132 | + | argsPointer: 0, argsSize: 0, eventsHandle: eventHandle.bits, eventsPointer: 0, |
|
| 133 | + | stateBase: 0, stackBase: 0, stackTop: 0, context: 0, |
|
| 134 | + | }; |
|
| 135 | + | set domain.state = Lifecycle::Pending; |
|
| 136 | + | return object; |
|
| 137 | + | } |
|
| 138 | + | ||
| 139 | + | /// Install root's initial Domain and Events authority into slot zero. |
|
| 140 | + | export fn root(domains: *mut [Domain], image: u32) -> abi::Handle { |
|
| 141 | + | assert domains[0].epoch == 0; |
|
| 142 | + | let object = prepare(domains, 0, none(), image); |
|
| 143 | + | return handles::install(&mut domains[0].handles, 0, object, abi::ROOT_RIGHTS); |
|
| 144 | + | } |
|
| 145 | + | ||
| 146 | + | /// Create a pending domain after the caller has authorized image admission. |
|
| 147 | + | /// Reserve terminal event capacity before accepting the parent relationship. |
|
| 148 | + | export fn create(domains: *mut [Domain], creator: abi::Object, image: u32) -> abi::Object throws (abi::Error) { |
|
| 149 | + | if not live(domains, creator) { throw abi::Error::BadHandle; } |
|
| 150 | + | if domains[creator.index].events.reserved == events::CRITICAL { throw abi::Error::Exhausted; } |
|
| 151 | + | for index in 0..domains.len { |
|
| 152 | + | let domain = &domains[index]; |
|
| 153 | + | if domain.state == Lifecycle::Dead and domain.epoch < 0xffffffff |
|
| 154 | + | and domain.handles.entries[1].generation <> 0 { |
|
| 155 | + | let result = prepare(domains, index, creator, image); |
|
| 156 | + | set domains[creator.index].events.reserved += 1; |
|
| 157 | + | return result; |
|
| 158 | + | } |
|
| 159 | + | } |
|
| 160 | + | throw abi::Error::Exhausted; |
|
| 161 | + | } |
kernel/core/events.rad
added
+61 -0
| 1 | + | //! Fixed per-domain notification memory and producer state. |
|
| 2 | + | ||
| 3 | + | /// Shared notification ring depth; a power of two. |
|
| 4 | + | export constant CAPACITY: u32 = 256; |
|
| 5 | + | /// Notification slots reserved for terminal lifecycle events. |
|
| 6 | + | export constant CRITICAL: u32 = 128; |
|
| 7 | + | ||
| 8 | + | /// A kernel-to-domain asynchronous notification. |
|
| 9 | + | export record Event: Copy { |
|
| 10 | + | /// Interrupt=1, Timeout=2, Fault=3, ChildExit=4, Wakeup=5. |
|
| 11 | + | kind: u16, |
|
| 12 | + | /// Reserved ABI field, always zero. |
|
| 13 | + | reserved: u16, |
|
| 14 | + | /// Event-specific token, source, status, or fault code. |
|
| 15 | + | code: u32, |
|
| 16 | + | /// Event-specific payload or sender identifier. |
|
| 17 | + | value: u64, |
|
| 18 | + | } |
|
| 19 | + | ||
| 20 | + | /// Shared ring layout; head and tail require acquire/release access. |
|
| 21 | + | export record Ring: Copy { |
|
| 22 | + | /// Notifications indexed by the low bits of head or tail. |
|
| 23 | + | data: [Event; CAPACITY], |
|
| 24 | + | /// Consumer progress, published by serialized user consumers. |
|
| 25 | + | head: u32, |
|
| 26 | + | /// Producer progress, published by the kernel. |
|
| 27 | + | tail: u32, |
|
| 28 | + | /// CAPACITY - 1, supplied by the kernel. |
|
| 29 | + | mask: u32, |
|
| 30 | + | } |
|
| 31 | + | ||
| 32 | + | /// Private kernel state for a domain's ring. |
|
| 33 | + | export record Queue: Copy { |
|
| 34 | + | /// Shared ABI storage exposed to this domain. |
|
| 35 | + | ring: Ring, |
|
| 36 | + | /// Last validated consumer progress. |
|
| 37 | + | head: u32, |
|
| 38 | + | /// Authoritative producer progress. |
|
| 39 | + | tail: u32, |
|
| 40 | + | /// Number of outstanding ordinary notifications. |
|
| 41 | + | ordinary: u32, |
|
| 42 | + | /// Live-child and queued-lifecycle delivery reservations. |
|
| 43 | + | reserved: u32, |
|
| 44 | + | /// Slots that release a lifecycle reservation when consumed. |
|
| 45 | + | critical: [bool; CAPACITY], |
|
| 46 | + | } |
|
| 47 | + | ||
| 48 | + | /// Clear event memory before it is exposed to a domain incarnation. |
|
| 49 | + | export fn init(queue: *mut Queue) { |
|
| 50 | + | for i in 0..CAPACITY { |
|
| 51 | + | set queue.ring.data[i] = Event { kind: 0, reserved: 0, code: 0, value: 0 }; |
|
| 52 | + | set queue.critical[i] = false; |
|
| 53 | + | } |
|
| 54 | + | set queue.ring.head = 0; |
|
| 55 | + | set queue.ring.tail = 0; |
|
| 56 | + | set queue.ring.mask = CAPACITY - 1; |
|
| 57 | + | set queue.head = 0; |
|
| 58 | + | set queue.tail = 0; |
|
| 59 | + | set queue.ordinary = 0; |
|
| 60 | + | set queue.reserved = 0; |
|
| 61 | + | } |