kernel: grant and transfer authority transactionally

5b8f59d1be6fcccbdd95f4d7c22ce5d967c239fab7a12f2aefb758513915cf9f
Verified: make -C kernel check with the machine-capable emulator; all pass.
Alexis Sellier committed ago 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 +
}