kernel: grant and transfer authority transactionally
5b8f59d1be6fcccbdd95f4d7c22ce5d967c239fab7a12f2aefb758513915cf9f
Verified: make -C kernel check with the machine-capable emulator; all pass.
1 parent
f126a2b7
kernel/Makefile
+3 -2
| 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 | 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 | + | core/events.rad core/domains.rad core/resources.rad core/capabilities.rad |
|
| 8 | 8 | CORE := -pkg core -mod core.rad $(addprefix -mod ,$(MODULES)) |
|
| 9 | - | CHECK_MODULES := check/boot.rad check/fixture.rad check/frames.rad check/handles.rad check/domains.rad |
|
| 9 | + | CHECK_MODULES := check/boot.rad check/fixture.rad check/frames.rad check/handles.rad \ |
|
| 10 | + | check/domains.rad check/capabilities.rad |
|
| 10 | 11 | ||
| 11 | 12 | .PHONY: all check clean compiler-check |
|
| 12 | 13 | all: kernel.rv64 |
|
| 13 | 14 | ||
| 14 | 15 | compiler-check: |
kernel/NOTES.md
+14 -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 5 of the 22-step plan. |
|
| 5 | + | record the contracts established through step 6 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. |
| 65 | 65 | Allocate and Create require a live capability to that caller with the right. |
|
| 66 | 66 | A newly created domain has only its installed Read|Write Events handle. |
|
| 67 | 67 | - Accepting a child reserves one terminal event. At most 128 credits can be |
|
| 68 | 68 | outstanding in a 256-entry Events queue. |
|
| 69 | 69 | ||
| 70 | + | ## Capability transactions |
|
| 71 | + | ||
| 72 | + | - Grant intersects requested rights with source rights. A grant to another |
|
| 73 | + | domain requires Grant; a self-grant may attenuate without that right. |
|
| 74 | + | - Transfer requires Transfer and a different target domain. Check the source, |
|
| 75 | + | live target, and destination capacity before moving authority. Failure leaves |
|
| 76 | + | the source usable. Object liveness is checked independently of the handle. |
|
| 77 | + | - Events cannot cross domains; the installed Events handle cannot be dropped. |
|
| 78 | + | Interrupt authority cannot be granted and moves exclusively by transfer. |
|
| 79 | + | - Dropping a handle removes authority to invoke operations through that name |
|
| 80 | + | and releases its object reference. |
|
| 81 | + | ||
| 70 | 82 | ## Validation |
|
| 71 | 83 | ||
| 72 | 84 | Use the current machine-capable sibling emulator. Set `RAD_EMULATOR`, pass |
|
| 73 | 85 | `EMU` to the kernel Make invocation, or put `emulator` on PATH. The kernel build |
|
| 74 | 86 | checks compiler dependencies. From the repository root, run: |
|
| 75 | 87 | ||
| 76 | 88 | ```sh |
|
| 77 | 89 | make -C kernel check |
|
| 78 | 90 | ``` |
|
| 79 | 91 | ||
| 80 | - | Exercise empty startup authority, self-sentinel checks, bounded domain storage, relationship credits, and identity reuse. |
|
| 92 | + | Exercise attenuation, self-grants, failed transfers with an intact source, Events locality, Interrupt exclusivity, and stale aliases. |
|
| 81 | 93 | ||
| 82 | 94 | The entry probe uses explicit M-mode success/fault finish writes; secondary |
|
| 83 | 95 | harts idle. This checks machine entry, not user-domain execution. Finish writes |
|
| 84 | 96 | are a check protocol, not a domain-exit operation. |
kernel/check.rad
+2 -0
| 3 | 3 | mod boot; |
|
| 4 | 4 | export mod fixture; |
|
| 5 | 5 | mod frames; |
|
| 6 | 6 | mod handles; |
|
| 7 | 7 | mod domains; |
|
| 8 | + | mod capabilities; |
|
| 8 | 9 | ||
| 9 | 10 | /// Run the available kernel mechanism checks. |
|
| 10 | 11 | @default fn main() -> u32 { |
|
| 11 | 12 | frames::run(); |
|
| 12 | 13 | boot::run(); |
|
| 13 | 14 | handles::run(); |
|
| 14 | 15 | domains::run(); |
|
| 16 | + | capabilities::run(); |
|
| 15 | 17 | return 0; |
|
| 16 | 18 | } |
kernel/check/capabilities.rad
added
+73 -0
| 1 | + | //! Capability mutation contracts across independent domain tables. |
|
| 2 | + | ||
| 3 | + | use core::abi; |
|
| 4 | + | use core::capabilities; |
|
| 5 | + | use core::domains; |
|
| 6 | + | use core::fdt; |
|
| 7 | + | use core::handles; |
|
| 8 | + | use core::resources; |
|
| 9 | + | ||
| 10 | + | /// Domain storage for capability transactions. |
|
| 11 | + | static DOMAINS: [domains::Domain; 3] = undefined; |
|
| 12 | + | /// Physical resources used by the transactions. |
|
| 13 | + | static OBJECTS: [resources::Slot; 4] = undefined; |
|
| 14 | + | ||
| 15 | + | /// Return a grant error as a test value. |
|
| 16 | + | fn grantError(source: abi::Handle, target: abi::Handle) -> abi::Error { |
|
| 17 | + | let _handle = try capabilities::grant(&mut DOMAINS[..], &mut OBJECTS[..], 0, source, target, abi::RIGHTS as u64) catch error { |
|
| 18 | + | return error; |
|
| 19 | + | }; |
|
| 20 | + | return abi::Error::Ok; |
|
| 21 | + | } |
|
| 22 | + | ||
| 23 | + | /// Check attenuation, non-delegable queues, unique IRQs, and failed moves. |
|
| 24 | + | export fn run() { |
|
| 25 | + | domains::init(&mut DOMAINS[..]); |
|
| 26 | + | resources::init(&mut OBJECTS[..]); |
|
| 27 | + | let rootHandle = domains::root(&mut DOMAINS[..], 0); |
|
| 28 | + | let root = try! domains::resolve(&DOMAINS[..], 0, rootHandle, abi::CREATE); |
|
| 29 | + | let child = try! domains::create(&mut DOMAINS[..], root, 1); |
|
| 30 | + | let childHandle = handles::install(&mut DOMAINS[0].handles, 2, child, abi::DOMAIN_RIGHTS); |
|
| 31 | + | let device = try! resources::create(&mut OBJECTS[..], resources::Value::Device(fdt::Range { base: 0x10000000, size: 256 })); |
|
| 32 | + | let source = handles::install(&mut DOMAINS[0].handles, 3, device, abi::READ | abi::GRANT | abi::TRANSFER); |
|
| 33 | + | resources::retain(&mut OBJECTS[..], device); |
|
| 34 | + | let shared = try! capabilities::grant(&mut DOMAINS[..], &mut OBJECTS[..], 0, source, childHandle, (abi::READ | abi::WRITE) as u64); |
|
| 35 | + | let authority = try! capabilities::lookup(&DOMAINS[..], &OBJECTS[..], child.index, shared); |
|
| 36 | + | assert authority.rights == abi::READ; |
|
| 37 | + | let _source = try! capabilities::lookup(&DOMAINS[..], &OBJECTS[..], 0, source); |
|
| 38 | + | let narrowed = try! capabilities::grant(&mut DOMAINS[..], &mut OBJECTS[..], child.index, shared, abi::Handle { bits: 0 }, abi::READ as u64); |
|
| 39 | + | let _local = try! capabilities::lookup(&DOMAINS[..], &OBJECTS[..], child.index, narrowed); |
|
| 40 | + | ||
| 41 | + | let queue = try! handles::get(&DOMAINS[0].handles, 1, abi::Kind::Events); |
|
| 42 | + | assert grantError(queue, childHandle) == abi::Error::Denied; |
|
| 43 | + | let readQueue = try! capabilities::grant(&mut DOMAINS[..], &mut OBJECTS[..], 0, queue, abi::Handle { bits: 0 }, abi::READ as u64); |
|
| 44 | + | try! capabilities::drop(&mut DOMAINS[..], &mut OBJECTS[..], 0, readQueue); |
|
| 45 | + | try capabilities::drop(&mut DOMAINS[..], &mut OBJECTS[..], 0, queue) catch error { |
|
| 46 | + | assert error == abi::Error::Denied; |
|
| 47 | + | }; |
|
| 48 | + | let _queue = try! capabilities::lookup(&DOMAINS[..], &OBJECTS[..], 0, queue); |
|
| 49 | + | ||
| 50 | + | let irq = try! resources::create(&mut OBJECTS[..], resources::Value::Interrupt(resources::Interrupt { number: 10, target: root })); |
|
| 51 | + | let irqSlot = try! handles::vacant(&DOMAINS[0].handles); |
|
| 52 | + | let interrupt = handles::install(&mut DOMAINS[0].handles, irqSlot, irq, abi::TRANSFER); |
|
| 53 | + | resources::retain(&mut OBJECTS[..], irq); |
|
| 54 | + | assert grantError(interrupt, childHandle) == abi::Error::Denied; |
|
| 55 | + | assert grantError(interrupt, abi::Handle { bits: 0 }) == abi::Error::Denied; |
|
| 56 | + | let moved = try! capabilities::transfer(&mut DOMAINS[..], &mut OBJECTS[..], 0, interrupt, childHandle, abi::TRANSFER as u64); |
|
| 57 | + | let _receiver = try! capabilities::lookup(&DOMAINS[..], &OBJECTS[..], child.index, moved); |
|
| 58 | + | let _old = try capabilities::lookup(&DOMAINS[..], &OBJECTS[..], 0, interrupt) catch error { |
|
| 59 | + | assert error == abi::Error::BadHandle; |
|
| 60 | + | for i in 0..abi::MAX_HANDLES - 4 { |
|
| 61 | + | let _copy = try! capabilities::grant(&mut DOMAINS[..], &mut OBJECTS[..], 0, source, childHandle, abi::READ as u64); |
|
| 62 | + | } |
|
| 63 | + | let _failed = try capabilities::transfer(&mut DOMAINS[..], &mut OBJECTS[..], 0, source, childHandle, abi::READ as u64) catch full { |
|
| 64 | + | assert full == abi::Error::InvalidArg; |
|
| 65 | + | let _stillLive = try! capabilities::lookup(&DOMAINS[..], &OBJECTS[..], 0, source); |
|
| 66 | + | try! capabilities::drop(&mut DOMAINS[..], &mut OBJECTS[..], 0, childHandle); |
|
| 67 | + | assert domains::live(&DOMAINS[..], child); |
|
| 68 | + | return; |
|
| 69 | + | }; |
|
| 70 | + | panic "run: transfer into a full table succeeded"; |
|
| 71 | + | }; |
|
| 72 | + | panic "run: transferred source remained live"; |
|
| 73 | + | } |
kernel/core.rad
+2 -0
| 5 | 5 | export mod frames; |
|
| 6 | 6 | export mod abi; |
|
| 7 | 7 | export mod handles; |
|
| 8 | 8 | export mod events; |
|
| 9 | 9 | export mod domains; |
|
| 10 | + | export mod resources; |
|
| 11 | + | export mod capabilities; |
kernel/core/capabilities.rad
added
+55 -0
| 1 | + | //! Validated capability grants, transfers, and drops under the kernel lock. |
|
| 2 | + | ||
| 3 | + | use core::abi; |
|
| 4 | + | use core::domains; |
|
| 5 | + | use core::handles; |
|
| 6 | + | use core::resources; |
|
| 7 | + | ||
| 8 | + | /// Resolve source authority and check the named object's incarnation. |
|
| 9 | + | export fn lookup(store: *[domains::Domain], objects: *[resources::Slot], caller: u32, handle: abi::Handle) -> handles::Entry throws (abi::Error) { |
|
| 10 | + | let slot = try handles::slot(&store[caller].handles, handle); |
|
| 11 | + | let entry = store[caller].handles.entries[slot]; |
|
| 12 | + | if not resources::live(objects, store, entry.object) { throw abi::Error::BadHandle; } |
|
| 13 | + | return entry; |
|
| 14 | + | } |
|
| 15 | + | ||
| 16 | + | /// Copy authority with attenuation after all failure checks complete. |
|
| 17 | + | export fn grant(store: *mut [domains::Domain], objects: *mut [resources::Slot], caller: u32, source: abi::Handle, target: abi::Handle, rights: u64) -> abi::Handle throws (abi::Error) { |
|
| 18 | + | let entry = try lookup(store, objects, caller, source); |
|
| 19 | + | let mut recipient = abi::Object { kind: abi::Kind::Domain, index: caller, epoch: store[caller].epoch }; |
|
| 20 | + | if target.bits <> 0 { set recipient = try domains::resolve(store, caller, target, 0); } |
|
| 21 | + | if entry.object.kind == abi::Kind::Interrupt { throw abi::Error::Denied; } |
|
| 22 | + | if recipient.index <> caller { |
|
| 23 | + | if entry.object.kind == abi::Kind::Events or entry.rights & abi::GRANT == 0 { |
|
| 24 | + | throw abi::Error::Denied; |
|
| 25 | + | } |
|
| 26 | + | } |
|
| 27 | + | let slot = try handles::vacant(&store[recipient.index].handles) catch { throw abi::Error::InvalidArg; }; |
|
| 28 | + | resources::retain(objects, entry.object); |
|
| 29 | + | return handles::install(&mut store[recipient.index].handles, slot, entry.object, entry.rights & rights as u16); |
|
| 30 | + | } |
|
| 31 | + | ||
| 32 | + | /// Move authority as one transaction; failure leaves the source live. |
|
| 33 | + | export fn transfer(store: *mut [domains::Domain], objects: *mut [resources::Slot], caller: u32, source: abi::Handle, target: abi::Handle, rights: u64) -> abi::Handle throws (abi::Error) { |
|
| 34 | + | let entry = try lookup(store, objects, caller, source); |
|
| 35 | + | let recipient = try domains::resolve(store, caller, target, 0); |
|
| 36 | + | if recipient.index == caller { throw abi::Error::InvalidArg; } |
|
| 37 | + | if entry.object.kind == abi::Kind::Events or entry.rights & abi::TRANSFER == 0 { |
|
| 38 | + | throw abi::Error::Denied; |
|
| 39 | + | } |
|
| 40 | + | let slot = try handles::vacant(&store[recipient.index].handles) catch { throw abi::Error::InvalidArg; }; |
|
| 41 | + | let sourceSlot = try handles::slot(&store[caller].handles, source); |
|
| 42 | + | resources::retarget(objects, entry.object, recipient); |
|
| 43 | + | let result = handles::install(&mut store[recipient.index].handles, slot, entry.object, entry.rights & rights as u16); |
|
| 44 | + | let _previous = handles::remove(&mut store[caller].handles, sourceSlot); |
|
| 45 | + | return result; |
|
| 46 | + | } |
|
| 47 | + | ||
| 48 | + | /// Drop a live local capability without revoking persistent pointer access. |
|
| 49 | + | export fn drop(store: *mut [domains::Domain], objects: *mut [resources::Slot], caller: u32, handle: abi::Handle) throws (abi::Error) { |
|
| 50 | + | let entry = try lookup(store, objects, caller, handle); |
|
| 51 | + | let slot = try handles::slot(&store[caller].handles, handle); |
|
| 52 | + | if entry.object.kind == abi::Kind::Events and slot == 1 { throw abi::Error::Denied; } |
|
| 53 | + | let _previous = handles::remove(&mut store[caller].handles, slot); |
|
| 54 | + | resources::release(objects, entry.object); |
|
| 55 | + | } |
kernel/core/resources.rad
added
+108 -0
| 1 | + | //! Physical object identities shared by capability entries. |
|
| 2 | + | ||
| 3 | + | use core::abi; |
|
| 4 | + | use core::domains; |
|
| 5 | + | use core::fdt; |
|
| 6 | + | ||
| 7 | + | /// An exclusive external interrupt source. |
|
| 8 | + | export record Interrupt: Copy { |
|
| 9 | + | /// PLIC source number. |
|
| 10 | + | number: u32, |
|
| 11 | + | /// Current receiving domain, or the empty identity. |
|
| 12 | + | target: abi::Object, |
|
| 13 | + | } |
|
| 14 | + | ||
| 15 | + | /// Resource data that determines the authoritative object kind. |
|
| 16 | + | export union Value: Copy { |
|
| 17 | + | /// Unoccupied object slot. |
|
| 18 | + | Free, |
|
| 19 | + | /// User-visible MMIO register range. |
|
| 20 | + | Device(fdt::Range), |
|
| 21 | + | /// Exclusively held external interrupt source. |
|
| 22 | + | Interrupt(Interrupt), |
|
| 23 | + | } |
|
| 24 | + | ||
| 25 | + | /// One resource slot and its live handle count. |
|
| 26 | + | export record Slot: Copy { |
|
| 27 | + | /// Type-specific physical resource state. |
|
| 28 | + | value: Value, |
|
| 29 | + | /// Incarnation of the slot; exhausted epochs are never reused. |
|
| 30 | + | epoch: u32, |
|
| 31 | + | /// Number of live capability entries that name this resource. |
|
| 32 | + | references: u32, |
|
| 33 | + | } |
|
| 34 | + | ||
| 35 | + | /// Get the kind associated with a resource value. |
|
| 36 | + | export fn kind(value: Value) -> abi::Kind { |
|
| 37 | + | match value { |
|
| 38 | + | case Value::Free => return abi::Kind::Empty, |
|
| 39 | + | case Value::Device(_) => return abi::Kind::Device, |
|
| 40 | + | case Value::Interrupt(_) => return abi::Kind::Interrupt, |
|
| 41 | + | } |
|
| 42 | + | } |
|
| 43 | + | ||
| 44 | + | /// Initialize caller-owned physical resource storage once at boot. |
|
| 45 | + | export fn init(slots: *mut [Slot]) { |
|
| 46 | + | for i in 0..slots.len { |
|
| 47 | + | set slots[i] = Slot { value: Value::Free, epoch: 0, references: 0 }; |
|
| 48 | + | } |
|
| 49 | + | } |
|
| 50 | + | ||
| 51 | + | /// Install a physical resource before its first capability is published. |
|
| 52 | + | export fn create(slots: *mut [Slot], value: Value) -> abi::Object throws (abi::Error) { |
|
| 53 | + | assert kind(value) <> abi::Kind::Empty; |
|
| 54 | + | for i in 0..slots.len { |
|
| 55 | + | let slot = &mut slots[i]; |
|
| 56 | + | if kind(slot.value) == abi::Kind::Empty and slot.epoch < 0xffffffff { |
|
| 57 | + | set slot.epoch += 1; |
|
| 58 | + | set slot.value = value; |
|
| 59 | + | set slot.references = 0; |
|
| 60 | + | return abi::Object { kind: kind(value), index: i, epoch: slot.epoch }; |
|
| 61 | + | } |
|
| 62 | + | } |
|
| 63 | + | throw abi::Error::Exhausted; |
|
| 64 | + | } |
|
| 65 | + | ||
| 66 | + | /// Check a physical-resource incarnation or a live domain-owned object. |
|
| 67 | + | export fn live(slots: *[Slot], domainStore: *[domains::Domain], object: abi::Object) -> bool { |
|
| 68 | + | if object.kind == abi::Kind::Domain { return domains::live(domainStore, object); } |
|
| 69 | + | if object.kind == abi::Kind::Events { |
|
| 70 | + | return domains::live(domainStore, abi::Object { |
|
| 71 | + | kind: abi::Kind::Domain, index: object.index, epoch: object.epoch, |
|
| 72 | + | }); |
|
| 73 | + | } |
|
| 74 | + | if object.kind == abi::Kind::Empty or object.index >= slots.len { return false; } |
|
| 75 | + | let slot = &slots[object.index]; |
|
| 76 | + | return slot.epoch == object.epoch and object.epoch <> 0 and kind(slot.value) == object.kind; |
|
| 77 | + | } |
|
| 78 | + | ||
| 79 | + | /// Account for one newly installed physical-resource handle. |
|
| 80 | + | export fn retain(slots: *mut [Slot], object: abi::Object) { |
|
| 81 | + | if object.kind == abi::Kind::Domain or object.kind == abi::Kind::Events { return; } |
|
| 82 | + | let slot = &mut slots[object.index]; |
|
| 83 | + | assert slot.epoch == object.epoch and kind(slot.value) == object.kind; |
|
| 84 | + | assert slot.references < abi::MAX_DOMAINS * abi::MAX_HANDLES; |
|
| 85 | + | set slot.references += 1; |
|
| 86 | + | } |
|
| 87 | + | ||
| 88 | + | /// Account for one removed physical-resource handle. |
|
| 89 | + | export fn release(slots: *mut [Slot], object: abi::Object) { |
|
| 90 | + | if object.kind == abi::Kind::Domain or object.kind == abi::Kind::Events { return; } |
|
| 91 | + | let slot = &mut slots[object.index]; |
|
| 92 | + | assert slot.epoch == object.epoch and kind(slot.value) == object.kind and slot.references > 0; |
|
| 93 | + | set slot.references -= 1; |
|
| 94 | + | if let case Value::Interrupt(irq) = &mut slot.value { |
|
| 95 | + | assert slot.references == 0; |
|
| 96 | + | set irq.target = domains::none(); |
|
| 97 | + | } |
|
| 98 | + | } |
|
| 99 | + | ||
| 100 | + | /// Change the exclusive receiver before an Interrupt transfer becomes visible. |
|
| 101 | + | export fn retarget(slots: *mut [Slot], object: abi::Object, target: abi::Object) { |
|
| 102 | + | if object.kind <> abi::Kind::Interrupt { return; } |
|
| 103 | + | let slot = &mut slots[object.index]; |
|
| 104 | + | match &mut slot.value { |
|
| 105 | + | case Value::Interrupt(irq) => set irq.target = target, |
|
| 106 | + | else => panic "retarget: object kind mismatch", |
|
| 107 | + | } |
|
| 108 | + | } |