kernel: publish ordered events with reserved lifecycle capacity

d84aebbccc0a1bc31157fffcf54cf07a805002e796bffa665bb39461eebff80b
Verified: make -C kernel check with the machine-capable emulator; all pass.
Alexis Sellier committed ago 1 parent 53c5af21
kernel/Makefile +6 -5
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 7
	core/events.rad core/domains.rad core/resources.rad core/capabilities.rad \
8 -
	core/memory.rad core/state.rad core/pages.rad
9 -
CORE := -pkg core -mod core.rad $(addprefix -mod ,$(MODULES))
8 +
	core/memory.rad core/state.rad core/pages.rad core/atomic.rad
9 +
CORE_ASM := arch/atomic.ras
10 +
CORE := -pkg core -mod core.rad $(addprefix -mod ,$(MODULES) $(CORE_ASM))
10 11
CHECK_MODULES := check/boot.rad check/fixture.rad check/frames.rad check/handles.rad \
11 -
	check/domains.rad check/capabilities.rad check/pages.rad
12 +
	check/domains.rad check/capabilities.rad check/pages.rad check/events.rad
12 13
13 14
.PHONY: all check clean compiler-check
14 15
all: kernel.rv64
15 16
16 17
compiler-check:
17 18
18 19
$(COMPILER): compiler-check
19 20
	$(MAKE) -C .. RAD_EMULATOR=$(abspath $(shell command -v $(HOST_EMU)))
20 21
21 -
kernel.rv64: main.rad arch/entry.ras core.rad $(MODULES) $(COMPILER)
22 +
kernel.rv64: main.rad arch/entry.ras core.rad $(MODULES) $(CORE_ASM) $(COMPILER)
22 23
	$(COMPILE) $(CORE) -pkg kernel -start arch/entry.ras -mod main.rad -entry kernel -o $@
23 24
24 -
check.rv64: check.rad core.rad $(MODULES) $(CHECK_MODULES) $(COMPILER)
25 +
check.rv64: check.rad core.rad $(MODULES) $(CORE_ASM) $(CHECK_MODULES) $(COMPILER)
25 26
	$(COMPILE) $(CORE) -pkg check -mod check.rad $(addprefix -mod ,$(CHECK_MODULES)) -entry check -o $@
26 27
27 28
check: all check.rv64
28 29
	$(HOST_EMU) -run check.rv64
29 30
	$(EMU) -machine -no-guard-stack -max-steps=100000000 -count-instructions -run kernel.rv64
kernel/NOTES.md +18 -3
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 8 of the 22-step plan.
5 +
record the contracts established through step 9 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.
63 63
  Active domain identities.
64 64
- Zero as a self-sentinel identifies the caller but supplies no resource right.
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 -
  outstanding in a 256-entry Events queue.
68 +
  outstanding in a 256-entry Events queue. Ordinary capacity is limited to
69 +
  128 entries.
69 70
70 71
## Capability transactions
71 72
72 73
- Grant intersects requested rights with source rights. A grant to another
73 74
  domain requires Grant; a self-grant may attenuate without that right.
99 100
  disappear. Clear a quiescent domain's bitmap before reusing its identity.
100 101
- Allocation preflights capability/object slots before taking RAM. PageSplit
101 102
  checks exclusivity and both destination slots before changing the source.
102 103
  Readable coverage may span adjacent grants; a gap never authorizes access.
103 104
105 +
## Event publication and consumption
106 +
107 +
- The 4112-byte shared ring contains 256 complete Event records followed by u32
108 +
  head, tail, mask, and consumer lock at offsets 4096, 4100, 4104, and 4108.
109 +
  Kernel producers do not acquire the user consumer lock.
110 +
- Write data before release-publishing tail. Consumers acquire tail, copy an
111 +
  entry, then release-publish head. Private positions and slot marks validate
112 +
  progress and remain authoritative despite user-writable event contents.
113 +
- Ordinary delivery returns Busy at its capacity limit. Terminal delivery uses
114 +
  relationship credits; consumption releases the credit. At most one entry per
115 +
  IRQ source is outstanding, and validated consumption permits its next event.
116 +
- Boundary checks cover full capacity, u32 wraparound, malformed head progress,
117 +
  lifecycle credit integrity, and coalescing through IRQ source 127.
118 +
104 119
## Validation
105 120
106 121
Use the current machine-capable sibling emulator. Set `RAD_EMULATOR`, pass
107 122
`EMU` to the kernel Make invocation, or put `emulator` on PATH. The kernel build
108 123
checks compiler dependencies. From the repository root, run:
109 124
110 125
```sh
111 126
make -C kernel check
112 127
```
113 128
114 -
Exercise split exclusivity, failed allocation/split transactions, pointers retained after handle drop, and reclamation after the final recipient lifetime.
129 +
Exercise all ring slots, ordinary backpressure, reserved delivery, IRQ coalescing, wraparound, and invalid consumer progress.
kernel/arch/atomic.ras added +12 -0
1 +
// Ordered, naturally aligned shared-memory accesses.
2 +
.text;
3 +
.export @"core::atomic::load";
4 +
@"core::atomic::load"
5 +
	lwu    %a0    0(%a0);
6 +
	fence;
7 +
	ret;
8 +
.export @"core::atomic::store";
9 +
@"core::atomic::store"
10 +
	fence;
11 +
	sw     %a1    0(%a0);
12 +
	ret;
kernel/check.rad +2 -0
5 5
mod frames;
6 6
mod handles;
7 7
mod domains;
8 8
mod capabilities;
9 9
mod pages;
10 +
mod events;
10 11
11 12
/// Run the available kernel mechanism checks.
12 13
@default fn main() -> u32 {
13 14
    frames::run();
14 15
    boot::run();
15 16
    handles::run();
16 17
    domains::run();
17 18
    capabilities::run();
18 19
    pages::run();
20 +
    events::run();
19 21
    return 0;
20 22
}
kernel/check/events.rad added +103 -0
1 +
//! Notification ordering, reserved delivery, and untrusted consumer progress.
2 +
3 +
use core::abi;
4 +
use core::atomic;
5 +
use core::events;
6 +
7 +
/// Read one complete entry and publish consumer progress with release ordering.
8 +
fn pop(queue: *mut events::Queue) -> events::Event {
9 +
    let head = atomic::load(&queue.ring.head);
10 +
    assert head <> atomic::load(&queue.ring.tail);
11 +
    let event = queue.ring.data[head & queue.ring.mask];
12 +
    atomic::store(&mut queue.ring.head, ((head as u64 + 1) & 0xffffffff) as u32);
13 +
    return event;
14 +
}
15 +
16 +
/// Reject an ordinary notification when its bounded capacity is occupied.
17 +
fn busy(queue: *mut events::Queue, event: events::Event) {
18 +
    try events::push(queue, event, events::Class::Ordinary) catch error {
19 +
        assert error == abi::Error::Busy;
20 +
        return;
21 +
    };
22 +
    panic "busy: full ordinary ring accepted a notification";
23 +
}
24 +
25 +
/// Reject a relationship when all lifecycle credits are occupied.
26 +
fn exhausted(queue: *mut events::Queue) {
27 +
    try events::reserve(queue) catch error {
28 +
        assert error == abi::Error::Exhausted;
29 +
        return;
30 +
    };
31 +
    panic "exhausted: full lifecycle reservations accepted a child";
32 +
}
33 +
34 +
/// Reject backward or out-of-range consumer positions.
35 +
fn invalid(queue: *mut events::Queue) {
36 +
    try events::refresh(queue) catch error {
37 +
        assert error == abi::Error::InvalidArg;
38 +
        return;
39 +
    };
40 +
    panic "invalid: corrupt consumer position was accepted";
41 +
}
42 +
43 +
/// Exercise delivery through saturation, wraparound, coalescing, and corruption.
44 +
export fn run() {
45 +
    let mut queue: events::Queue = undefined;
46 +
    events::init(&mut queue);
47 +
    let ordinary = events::Event { kind: 5, reserved: 0, code: 0, value: 0 };
48 +
    for i in 0..events::CRITICAL { try! events::reserve(&mut queue); }
49 +
    exhausted(&mut queue);
50 +
    for i in 0..events::CAPACITY - events::CRITICAL {
51 +
        try! events::push(&mut queue, events::Event { kind: 5, reserved: 0, code: i, value: i as u64 }, events::Class::Ordinary);
52 +
    }
53 +
    busy(&mut queue, ordinary);
54 +
    for i in 0..events::CRITICAL {
55 +
        try! events::push(&mut queue, events::Event { kind: 4, reserved: 0, code: i, value: i as u64 + 128 }, events::Class::Critical);
56 +
    }
57 +
    for i in 0..events::CAPACITY {
58 +
        let event = pop(&mut queue);
59 +
        assert event.value == i as u64;
60 +
        if i < 128 { assert event.kind == 5; }
61 +
        else { assert event.kind == 4; }
62 +
    }
63 +
    try! events::refresh(&mut queue);
64 +
    assert not events::available(&queue);
65 +
    for i in 0..events::CRITICAL { try! events::reserve(&mut queue); }
66 +
    exhausted(&mut queue);
67 +
68 +
    events::init(&mut queue);
69 +
    let interrupt = events::Event { kind: 1, reserved: 0, code: 127, value: 0 };
70 +
    try! events::push(&mut queue, interrupt, events::Class::Interrupt(127));
71 +
    try! events::push(&mut queue, interrupt, events::Class::Interrupt(127));
72 +
    assert pop(&mut queue).code == 127;
73 +
    try! events::refresh(&mut queue);
74 +
    assert not events::available(&queue);
75 +
    try! events::push(&mut queue, interrupt, events::Class::Interrupt(127));
76 +
    assert pop(&mut queue).code == 127;
77 +
    try! events::refresh(&mut queue);
78 +
79 +
    set queue.head = 0xfffffff0;
80 +
    set queue.tail = 0xfffffff0;
81 +
    atomic::store(&mut queue.ring.head, 0xfffffff0);
82 +
    atomic::store(&mut queue.ring.tail, 0xfffffff0);
83 +
    for i in 0..32 {
84 +
        try! events::push(&mut queue, events::Event { kind: 5, reserved: 0, code: i, value: i as u64 }, events::Class::Ordinary);
85 +
    }
86 +
    for i in 0..32 { assert pop(&mut queue).code == i; }
87 +
    try! events::refresh(&mut queue);
88 +
    assert not events::available(&queue);
89 +
    atomic::store(&mut queue.ring.head, 17);
90 +
    invalid(&mut queue);
91 +
    atomic::store(&mut queue.ring.head, 15);
92 +
    invalid(&mut queue);
93 +
    atomic::store(&mut queue.ring.head, 16);
94 +
    try! events::refresh(&mut queue);
95 +
96 +
    try! events::reserve(&mut queue);
97 +
    try! events::push(&mut queue, events::Event { kind: 4, reserved: 0, code: 0, value: 1 }, events::Class::Critical);
98 +
    set queue.ring.data[16].kind = 5;
99 +
    let _event = pop(&mut queue);
100 +
    try! events::refresh(&mut queue);
101 +
    for i in 0..events::CRITICAL { try! events::reserve(&mut queue); }
102 +
    exhausted(&mut queue);
103 +
}
kernel/core.rad +1 -0
10 10
export mod resources;
11 11
export mod capabilities;
12 12
export mod memory;
13 13
export mod state;
14 14
export mod pages;
15 +
export mod atomic;
kernel/core/atomic.rad added +7 -0
1 +
//! Acquire and release accesses used by shared notification memory.
2 +
3 +
/// Load one naturally aligned u32 with acquire ordering.
4 +
export fn load(pointer: *u32) -> u32;
5 +
6 +
/// Store one naturally aligned u32 with release ordering.
7 +
export fn store(pointer: *mut u32, value: u32);
kernel/core/domains.rad +1 -2
145 145
146 146
/// Create a pending domain after the caller has authorized image admission.
147 147
/// Reserve terminal event capacity before accepting the parent relationship.
148 148
export fn create(domains: *mut [Domain], creator: abi::Object, image: u32) -> abi::Object throws (abi::Error) {
149 149
    if not live(domains, creator) { throw abi::Error::BadHandle; }
150 -
    if domains[creator.index].events.reserved == events::CRITICAL { throw abi::Error::Exhausted; }
151 150
    for index in 0..domains.len {
152 151
        let domain = &domains[index];
153 152
        if domain.state == Lifecycle::Dead and domain.epoch < 0xffffffff
154 153
            and domain.handles.entries[1].generation <> 0 {
154 +
            try events::reserve(&mut domains[creator.index].events);
155 155
            let result = prepare(domains, index, creator, image);
156 -
            set domains[creator.index].events.reserved += 1;
157 156
            return result;
158 157
        }
159 158
    }
160 159
    throw abi::Error::Exhausted;
161 160
}
kernel/core/events.rad +100 -3
1 1
//! Fixed per-domain notification memory and producer state.
2 2
3 +
use core::abi;
4 +
use core::atomic;
5 +
3 6
/// Shared notification ring depth; a power of two.
4 7
export constant CAPACITY: u32 = 256;
5 8
/// Notification slots reserved for terminal lifecycle events.
6 9
export constant CRITICAL: u32 = 128;
10 +
/// Largest interrupt source that can have an outstanding ring entry.
11 +
export constant MAX_IRQ: u32 = 127;
12 +
/// Private marker for a lifecycle entry.
13 +
constant LIFECYCLE: u16 = 256;
14 +
15 +
/// Notification capacity and coalescing policy.
16 +
export union Class: Copy {
17 +
    /// Ordinary notification subject to backpressure.
18 +
    Ordinary,
19 +
    /// One outstanding entry for a physical interrupt source.
20 +
    Interrupt(u32),
21 +
    /// Terminal lifecycle notification with a prior reservation.
22 +
    Critical,
23 +
}
7 24
8 25
/// A kernel-to-domain asynchronous notification.
9 26
export record Event: Copy {
10 27
    /// Interrupt=1, Timeout=2, Fault=3, ChildExit=4, Wakeup=5.
11 28
    kind: u16,
25 42
    head: u32,
26 43
    /// Producer progress, published by the kernel.
27 44
    tail: u32,
28 45
    /// CAPACITY - 1, supplied by the kernel.
29 46
    mask: u32,
47 +
    /// User-space consumer lock; zero means unlocked.
48 +
    consumer: u32,
30 49
}
31 50
32 51
/// Private kernel state for a domain's ring.
33 52
export record Queue: Copy {
34 53
    /// Shared ABI storage exposed to this domain.
39 58
    tail: u32,
40 59
    /// Number of outstanding ordinary notifications.
41 60
    ordinary: u32,
42 61
    /// Live-child and queued-lifecycle delivery reservations.
43 62
    reserved: u32,
44 -
    /// Slots that release a lifecycle reservation when consumed.
45 -
    critical: [bool; CAPACITY],
63 +
    /// Zero for ordinary entries, source for IRQs, LIFECYCLE for terminal events.
64 +
    marks: [u16; CAPACITY],
65 +
    /// Interrupt sources with outstanding notifications.
66 +
    pending: [u64; 2],
46 67
}
47 68
48 69
/// Clear event memory before it is exposed to a domain incarnation.
49 70
export fn init(queue: *mut Queue) {
50 71
    for i in 0..CAPACITY {
51 72
        set queue.ring.data[i] = Event { kind: 0, reserved: 0, code: 0, value: 0 };
52 -
        set queue.critical[i] = false;
73 +
        set queue.marks[i] = 0;
53 74
    }
54 75
    set queue.ring.head = 0;
55 76
    set queue.ring.tail = 0;
56 77
    set queue.ring.mask = CAPACITY - 1;
78 +
    set queue.ring.consumer = 0;
57 79
    set queue.head = 0;
58 80
    set queue.tail = 0;
59 81
    set queue.ordinary = 0;
60 82
    set queue.reserved = 0;
83 +
    set queue.pending = [0; 2];
84 +
}
85 +
86 +
/// Difference between wrapping u32 positions, evaluated without overflow.
87 +
fn distance(a: u32, b: u32) -> u32 {
88 +
    return ((a as u64 + 0x100000000 - b as u64) & 0xffffffff) as u32;
89 +
}
90 +
91 +
/// Validate consumer progress and reclaim consumed capacity.
92 +
export fn refresh(queue: *mut Queue) throws (abi::Error) {
93 +
    let head = atomic::load(&queue.ring.head);
94 +
    let count = distance(head, queue.head);
95 +
    if count > distance(queue.tail, queue.head) { throw abi::Error::InvalidArg; }
96 +
    for n in 0..count {
97 +
        let i = ((queue.head as u64 + n as u64) & (CAPACITY - 1) as u64) as u32;
98 +
        let mark = queue.marks[i];
99 +
        if mark == LIFECYCLE { set queue.reserved -= 1; }
100 +
        else {
101 +
            set queue.ordinary -= 1;
102 +
            if mark > 0 {
103 +
                let source = mark as u32;
104 +
                set queue.pending[source / 64] &= ~(1 << (source as u64 % 64));
105 +
            }
106 +
        }
107 +
        set queue.marks[i] = 0;
108 +
    }
109 +
    set queue.head = head;
110 +
}
111 +
112 +
/// Reserve a delivery credit before installing a parent relationship.
113 +
export fn reserve(queue: *mut Queue) throws (abi::Error) {
114 +
    try refresh(queue);
115 +
    if queue.reserved == CRITICAL { throw abi::Error::Exhausted; }
116 +
    set queue.reserved += 1;
117 +
}
118 +
119 +
/// Publish complete event data before the tail; coalesce outstanding IRQs.
120 +
/// The kernel serializes calls that change private producer state.
121 +
export fn push(queue: *mut Queue, event: Event, class: Class) throws (abi::Error) {
122 +
    try refresh(queue);
123 +
    let count = distance(queue.tail, queue.head);
124 +
    let mut mark: u16 = 0;
125 +
    match class {
126 +
        case Class::Ordinary => {},
127 +
        case Class::Interrupt(source) => {
128 +
            if source == 0 or source > MAX_IRQ { throw abi::Error::InvalidArg; }
129 +
            if (queue.pending[source / 64] & (1 << (source as u64 % 64))) <> 0 { return; }
130 +
            set mark = source as u16;
131 +
        },
132 +
        case Class::Critical => {
133 +
            assert queue.reserved > count - queue.ordinary;
134 +
            set mark = LIFECYCLE;
135 +
        },
136 +
    }
137 +
    if mark <> LIFECYCLE and queue.ordinary == CAPACITY - CRITICAL {
138 +
        throw abi::Error::Busy;
139 +
    }
140 +
    assert count < CAPACITY and event.reserved == 0;
141 +
    let i = queue.tail & (CAPACITY - 1);
142 +
    set queue.ring.data[i] = event;
143 +
    set queue.marks[i] = mark;
144 +
    if mark <> LIFECYCLE {
145 +
        set queue.ordinary += 1;
146 +
        if mark > 0 {
147 +
            let source = mark as u32;
148 +
            set queue.pending[source / 64] |= 1 << (source as u64 % 64);
149 +
        }
150 +
    }
151 +
    set queue.tail = ((queue.tail as u64 + 1) & 0xffffffff) as u32;
152 +
    atomic::store(&mut queue.ring.tail, queue.tail);
153 +
}
154 +
155 +
/// Test the authoritative queue after consumer progress has been validated.
156 +
export fn available(queue: *Queue) -> bool {
157 +
    return queue.head <> queue.tail;
61 158
}