kernel: wait for events with bounded one-shot timeouts

e7e57c61bb88a4c566b9c9b6f82d47b9ab22041ed130b1b4ed7fb3c4012c9e24
Verified: make -C kernel check with the machine-capable emulator; all pass.
Alexis Sellier committed ago 1 parent 8c203f66
kernel/Makefile +5 -4
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 8
	core/memory.rad core/state.rad core/pages.rad core/atomic.rad core/cpu.rad \
9 -
	core/budgets.rad core/contexts.rad core/clock.rad core/budget_caps.rad
9 +
	core/budgets.rad core/contexts.rad core/clock.rad core/budget_caps.rad \
10 +
	core/timers.rad core/notifications.rad
10 11
CORE_ASM := arch/atomic.ras arch/context.ras arch/clock.ras
11 12
CORE := -pkg core -mod core.rad $(addprefix -mod ,$(MODULES) $(CORE_ASM))
12 13
CHECK_MODULES := check/boot.rad check/fixture.rad check/frames.rad check/handles.rad \
13 14
	check/domains.rad check/capabilities.rad check/pages.rad check/events.rad \
14 -
	check/budgets.rad check/budget_caps.rad
15 +
	check/budgets.rad check/budget_caps.rad check/timers.rad check/notifications.rad
15 16
16 17
.PHONY: all check clean compiler-check
17 18
all: kernel.rv64
18 19
19 20
compiler-check:
25 26
	$(COMPILE) $(CORE) -pkg kernel -start arch/entry.ras -mod main.rad -entry kernel -o $@
26 27
27 28
check.rv64: check.rad core.rad $(MODULES) $(CORE_ASM) $(CHECK_MODULES) $(COMPILER)
28 29
	$(COMPILE) $(CORE) -pkg check -mod check.rad $(addprefix -mod ,$(CHECK_MODULES)) -entry check -o $@
29 30
30 -
context.rv64: context.rad check/context.ras arch/entry.ras core.rad $(MODULES) $(CORE_ASM) $(COMPILER)
31 -
	$(COMPILE) $(CORE) -pkg context -start arch/entry.ras -mod context.rad -mod check/context.ras -entry context -o $@
31 +
context.rv64: context.rad context/wait.rad check/context.ras arch/entry.ras core.rad $(MODULES) $(CORE_ASM) $(COMPILER)
32 +
	$(COMPILE) $(CORE) -pkg context -start arch/entry.ras -mod context.rad -mod context/wait.rad -mod check/context.ras -entry context -o $@
32 33
33 34
check: all check.rv64 context.rv64
34 35
	$(HOST_EMU) -run check.rv64
35 36
	$(EMU) -machine -no-guard-stack -max-steps=100000000 -count-instructions -run kernel.rv64
36 37
	$(EMU) -machine -no-guard-stack -max-steps=1000000 -count-instructions -run context.rv64
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 12 of the 22-step plan.
5 +
record the contracts established through step 13 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.
160 160
  charges the monotonic interval once, including transition overhead, saturating
161 161
  at zero. Empty, wrong-hart, incorrectly bound, or non-ready contexts cannot run.
162 162
- Creating a context or publishing an event supplies no ticks. Checks exercise
163 163
  conservation, failed operations, retained bindings, and actual CLINT preemption.
164 164
165 +
## Waits and one-shot timeouts
166 +
167 +
- Each hart owns 64 timeout slots. An accepted timeout remains allocated until
168 +
  publication succeeds or its exact owner incarnation dies. Busy cannot erase
169 +
  accepted work. Rotating delivery prevents low-slot reuse from starving it.
170 +
- Program the earlier of the current budget deadline and a future timeout.
171 +
  Already-due backpressured timeouts do not cause interrupt spin; kernel entry
172 +
  retries publication.
173 +
- Wait validates consumer progress before blocking. Resumption requires an
174 +
  unread event and positive retained budget. Wakeup requires an explicit Wake
175 +
  handle and records the sender identity. Idle waiting does not consume ticks
176 +
  from the suspended context's budget.
177 +
165 178
## Validation
166 179
167 180
Use the current machine-capable sibling emulator. Set `RAD_EMULATOR`, pass
168 181
`EMU` to the kernel Make invocation, or put `emulator` on PATH. The kernel build
169 182
checks compiler dependencies. From the repository root, run:
170 183
171 184
```sh
172 185
make -C kernel check
173 186
```
174 187
175 -
Exercise budget conservation and failed split/bind operations; preempt finite computation through CLINT and reject execution after exhaustion.
188 +
Exercise Timeout, Wait, machine idle, timer expiry, event consumption, retained notifications, and unchanged budget during idle.
176 189
177 190
Run the context reservation probe with an emulator that retains LR/SC
178 191
reservations across traps. This checks the kernel's reservation invalidation.
kernel/check.rad +4 -0
8 8
mod capabilities;
9 9
mod pages;
10 10
mod events;
11 11
mod budgets;
12 12
mod budget_caps;
13 +
mod timers;
14 +
mod notifications;
13 15
14 16
/// Run the available kernel mechanism checks.
15 17
@default fn main() -> u32 {
16 18
    frames::run();
17 19
    boot::run();
20 22
    capabilities::run();
21 23
    pages::run();
22 24
    events::run();
23 25
    budgets::run();
24 26
    budget_caps::run();
27 +
    timers::run();
28 +
    notifications::run();
25 29
    return 0;
26 30
}
kernel/check/context.ras +39 -0
95 95
	li     %t0    1;
96 96
	sd     %t0    0(%a0);
97 97
	lr.d   %a1    0(%a0);
98 98
	ecall;
99 99
100 +
.export @"context::wait::entry";
101 +
@"context::wait::entry"
102 +
	la     %a0    @wait_entry;
103 +
	ret;
104 +
@wait_entry
105 +
	li     %a0    10000;
106 +
	li     %a1    7;
107 +
	li     %a7    41;
108 +
	ecall;
109 +
	li     %a7    42;
110 +
	ecall;
111 +
	ld     %t0    24(%tp);
112 +
	li     %t2    4096;
113 +
	add    %t2    %t0    %t2;
114 +
	lwu    %t1    0(%t2);
115 +
	lwu    %t3    4(%t2);
116 +
	fence;
117 +
	beq    %t1    %t3    @wait_failed;
118 +
	andi   %t4    %t1    255;
119 +
	slli   %t4    %t4    4;
120 +
	add    %t4    %t0    %t4;
121 +
	lhu    %t5    0(%t4);
122 +
	li     %t6    2;
123 +
	bne    %t5    %t6    @wait_failed;
124 +
	lwu    %t5    4(%t4);
125 +
	li     %t6    7;
126 +
	bne    %t5    %t6    @wait_failed;
127 +
	ld     %t5    8(%t4);
128 +
	bnez   %t5    @wait_failed;
129 +
	addi   %t1    %t1    1;
130 +
	fence;
131 +
	sw     %t1    0(%t2);
132 +
	li     %a0    42;
133 +
	li     %a7    49;
134 +
	ecall;
135 +
@wait_failed
136 +
	li     %a0    100;
137 +
	li     %a7    49;
138 +
	ecall;
kernel/check/notifications.rad added +105 -0
1 +
//! Wait predicates, explicit Wake authority, and budget-neutral notification.
2 +
3 +
use core::abi;
4 +
use core::atomic;
5 +
use core::budget_caps;
6 +
use core::budgets;
7 +
use core::capabilities;
8 +
use core::contexts;
9 +
use core::domains;
10 +
use core::events;
11 +
use core::frames;
12 +
use core::handles;
13 +
use core::memory;
14 +
use core::notifications;
15 +
use core::platform;
16 +
use core::resources;
17 +
use core::state;
18 +
19 +
/// Independent sender and receiver domain storage.
20 +
static DOMAINS: [domains::Domain; 2] = undefined;
21 +
/// Reserve and execution-loan resource slots.
22 +
static OBJECTS: [resources::Slot; 3] = undefined;
23 +
/// Waiting receiver context.
24 +
static CONTEXTS: [contexts::Context; 1] = undefined;
25 +
/// Empty, valid frame metadata for this notification-only fixture.
26 +
static POOL: frames::Pool = undefined;
27 +
/// Empty-pool pin counts.
28 +
static PINS: [u16; 1] = undefined;
29 +
/// Empty-pool claims.
30 +
static ASSIGNED: [bool; 1] = undefined;
31 +
/// Empty-pool grants.
32 +
static GRANTS: [u64; 2] = undefined;
33 +
/// Kernel mechanism storage.
34 +
static KERNEL: state::State = undefined;
35 +
36 +
/// Consume the receiver's next Wakeup with acquire/release progress.
37 +
fn consume(token: u32, sender: u32) {
38 +
    let queue = &mut DOMAINS[1].events;
39 +
    let head = atomic::load(&queue.ring.head);
40 +
    assert head <> atomic::load(&queue.ring.tail);
41 +
    let event = queue.ring.data[head & (events::CAPACITY - 1)];
42 +
    assert event.kind == 5 and event.code == token and event.value == sender as u64;
43 +
    atomic::store(&mut queue.ring.head, head + 1);
44 +
}
45 +
46 +
/// Reject the identity-only sentinel where an explicit Wake capability is required.
47 +
fn noAuthority() {
48 +
    try notifications::wakeup(&mut KERNEL, 0, abi::Handle { bits: 0 }, 99) catch error {
49 +
        assert error == abi::Error::BadHandle;
50 +
        return;
51 +
    };
52 +
    panic "noAuthority: sentinel authorized a wakeup";
53 +
}
54 +
55 +
/// A retained event cannot wake an exhausted context until authority is assigned.
56 +
export fn run() {
57 +
    domains::init(&mut DOMAINS[..]);
58 +
    contexts::init(&mut CONTEXTS[0], 0);
59 +
    resources::init(&mut OBJECTS[..]);
60 +
    let mut machine: platform::Platform = undefined;
61 +
    set machine.memoryCount = 0;
62 +
    set machine.reservedCount = 0;
63 +
    try! frames::init(&mut POOL, &machine);
64 +
    let mut ram: memory::Memory = undefined;
65 +
    memory::init(&mut ram, &mut POOL, &mut PINS[..], &mut ASSIGNED[..], &mut GRANTS[..], 2);
66 +
    set KERNEL = state::State { domains: &mut DOMAINS[..], resources: &mut OBJECTS[..], contexts: &mut CONTEXTS[..], memory: ram };
67 +
    let rootHandle = domains::root(&mut DOMAINS[..], 0);
68 +
    let root = try! domains::resolve(&DOMAINS[..], 0, rootHandle, abi::CREATE);
69 +
    let child = try! domains::create(&mut DOMAINS[..], root, 0);
70 +
    let target = handles::install(&mut DOMAINS[0].handles, 2, child, abi::DOMAIN_RIGHTS);
71 +
    set DOMAINS[0].state = domains::Lifecycle::Active;
72 +
    set DOMAINS[1].state = domains::Lifecycle::Active;
73 +
    try! contexts::prepare(&mut CONTEXTS[0], child, 0, 0, DOMAINS[1].env);
74 +
    let id = contexts::identity(&CONTEXTS[0]);
75 +
    let object = try! resources::create(&mut OBJECTS[..], resources::Value::Budget(try! budgets::init(0, 2000)));
76 +
    resources::retain(&mut OBJECTS[..], object);
77 +
    let reserve = handles::install(&mut DOMAINS[0].handles, 3, object, abi::READ | abi::WRITE | abi::EXECUTE);
78 +
    let loan = try! budget_caps::split(&mut KERNEL, 0, reserve, 1000);
79 +
    try! budget_caps::bind(&mut KERNEL, 0, loan, target, id);
80 +
    let own = try! capabilities::grant(&mut KERNEL, 0, target, target, abi::WAKE as u64);
81 +
    noAuthority();
82 +
    try! notifications::wait(&mut KERNEL, 0);
83 +
    assert CONTEXTS[0].status == contexts::Status::Waiting;
84 +
    try! notifications::wakeup(&mut KERNEL, 1, own, 77);
85 +
    assert notifications::poll(&mut KERNEL) == nil;
86 +
    assert CONTEXTS[0].status == contexts::Status::Ready;
87 +
    consume(77, 1);
88 +
    let budget = budget_caps::value(&mut KERNEL, CONTEXTS[0].budget);
89 +
    let running = try! contexts::begin(&mut CONTEXTS[0], budget, 1, 0);
90 +
    assert contexts::finish(&mut CONTEXTS[0], budget, running, 1001) == 0;
91 +
    try! notifications::wait(&mut KERNEL, 0);
92 +
    try! notifications::wakeup(&mut KERNEL, 0, target, 42);
93 +
    assert notifications::poll(&mut KERNEL) == nil;
94 +
    assert CONTEXTS[0].status == contexts::Status::Waiting and budget.remaining == 0;
95 +
    let more = try! budget_caps::split(&mut KERNEL, 0, reserve, 500);
96 +
    try! budget_caps::bind(&mut KERNEL, 0, more, target, id);
97 +
    assert notifications::poll(&mut KERNEL) == nil;
98 +
    try! notifications::wait(&mut KERNEL, 0);
99 +
    assert CONTEXTS[0].status == contexts::Status::Ready;
100 +
    consume(42, 0);
101 +
    try! notifications::wait(&mut KERNEL, 0);
102 +
    atomic::store(&mut DOMAINS[1].events.ring.head, 3);
103 +
    let bad = notifications::poll(&mut KERNEL) else { panic "run: corrupt wait queue was accepted"; };
104 +
    assert bad.index == child.index and bad.epoch == child.epoch;
105 +
}
kernel/check/timers.rad added +232 -0
1 +
//! One-shot timeout delivery, bounded retention, and incarnation isolation.
2 +
3 +
use core::abi;
4 +
use core::atomic;
5 +
use core::domains;
6 +
use core::events;
7 +
use core::handles;
8 +
use core::timers;
9 +
10 +
/// Real domain storage kept off the boot check stack.
11 +
static DOMAINS: [domains::Domain; 2] = undefined;
12 +
13 +
/// Reset private stores and return root's live domain identity.
14 +
fn setup(store: *mut timers::Store) -> abi::Object {
15 +
    timers::init(store);
16 +
    domains::init(&mut DOMAINS[..]);
17 +
    let root = domains::root(&mut DOMAINS[..], 0);
18 +
    return try! domains::resolve(&DOMAINS[..], 0, root, abi::CREATE);
19 +
}
20 +
21 +
/// Acquire a complete notification and release the consumer's new position.
22 +
fn pop(queue: *mut events::Queue) -> events::Event {
23 +
    let head = atomic::load(&queue.ring.head);
24 +
    assert head <> atomic::load(&queue.ring.tail);
25 +
    let event = queue.ring.data[head & queue.ring.mask];
26 +
    atomic::store(&mut queue.ring.head, ((head as u64 + 1) & 0xffffffff) as u32);
27 +
    return event;
28 +
}
29 +
30 +
/// Observe published producer progress without touching private queue counters.
31 +
fn empty(queue: *events::Queue) -> bool {
32 +
    return atomic::load(&queue.ring.head) == atomic::load(&queue.ring.tail);
33 +
}
34 +
35 +
/// Require the complete public Timeout payload, including its reserved fields.
36 +
fn timeout(queue: *mut events::Queue, token: u32) {
37 +
    let event = pop(queue);
38 +
    assert event.kind == 2 and event.reserved == 0;
39 +
    assert event.code == token and event.value == 0;
40 +
}
41 +
42 +
/// Require rejection instead of accepting an additional timeout.
43 +
fn rejected(store: *mut timers::Store, owner: abi::Object, now: u64, delay: u64, expected: abi::Error) {
44 +
    try timers::arm(store, owner, now, delay, 0xffffffff) catch error {
45 +
        assert error == expected;
46 +
        return;
47 +
    };
48 +
    panic "rejected: invalid or full timer store accepted a timeout";
49 +
}
50 +
51 +
/// Future deadlines do not publish early, and late entries still publish once.
52 +
fn timing() {
53 +
    let mut store: timers::Store = undefined;
54 +
    let root = setup(&mut store);
55 +
    let queue = &mut DOMAINS[0].events;
56 +
    assert timers::next(&store, 100) == 0xffffffffffffffff;
57 +
    try! timers::arm(&mut store, root, 100, 10, 0xfedcba98);
58 +
    try! timers::arm(&mut store, root, 100, 5, 0);
59 +
    assert timers::next(&store, 100) == 105;
60 +
    timers::deliver(&mut store, 104, &mut DOMAINS[..]);
61 +
    assert empty(queue);
62 +
    timers::deliver(&mut store, 105, &mut DOMAINS[..]);
63 +
    timeout(queue, 0);
64 +
    assert empty(queue) and timers::next(&store, 105) == 110;
65 +
    timers::deliver(&mut store, 109, &mut DOMAINS[..]);
66 +
    assert empty(queue);
67 +
    timers::deliver(&mut store, 120, &mut DOMAINS[..]);
68 +
    timeout(queue, 0xfedcba98);
69 +
    timers::deliver(&mut store, 121, &mut DOMAINS[..]);
70 +
    assert empty(queue) and timers::next(&store, 121) == 0xffffffffffffffff;
71 +
}
72 +
73 +
/// Full-store rejection leaves every accepted token intact and frees no slot.
74 +
fn capacity() {
75 +
    let mut store: timers::Store = undefined;
76 +
    let root = setup(&mut store);
77 +
    let queue = &mut DOMAINS[0].events;
78 +
    for i in 0..timers::CAPACITY { try! timers::arm(&mut store, root, 0, 10, i); }
79 +
    rejected(&mut store, root, 0, 1, abi::Error::Busy);
80 +
    assert timers::next(&store, 0) == 10;
81 +
    timers::deliver(&mut store, 10, &mut DOMAINS[..]);
82 +
    for i in 0..timers::CAPACITY { timeout(queue, i); }
83 +
    assert empty(queue);
84 +
    try! timers::arm(&mut store, root, 10, 1, 64);
85 +
    timers::deliver(&mut store, 11, &mut DOMAINS[..]);
86 +
    timeout(queue, 64);
87 +
    timers::deliver(&mut store, 12, &mut DOMAINS[..]);
88 +
    assert empty(queue);
89 +
}
90 +
91 +
/// A completely full real ring retains an expired timer until consumption.
92 +
fn backpressure() {
93 +
    let mut store: timers::Store = undefined;
94 +
    let root = setup(&mut store);
95 +
    let queue = &mut DOMAINS[0].events;
96 +
    for i in 0..events::CAPACITY - events::CRITICAL {
97 +
        try! events::push(queue, events::Event { kind: 5, reserved: 0, code: i, value: i as u64 }, events::Class::Ordinary);
98 +
    }
99 +
    for i in 0..events::CRITICAL {
100 +
        try! events::reserve(queue);
101 +
        try! events::push(queue, events::Event { kind: 4, reserved: 0, code: i, value: i as u64 }, events::Class::Critical);
102 +
    }
103 +
    try! timers::arm(&mut store, root, 0, 5, 0x12345678);
104 +
    try! timers::arm(&mut store, root, 0, 20, 20);
105 +
    timers::deliver(&mut store, 5, &mut DOMAINS[..]);
106 +
    timers::deliver(&mut store, 6, &mut DOMAINS[..]);
107 +
    assert atomic::load(&queue.ring.tail) == events::CAPACITY;
108 +
    assert timers::next(&store, 6) == 20;
109 +
    assert pop(queue).kind == 5;
110 +
    timers::deliver(&mut store, 7, &mut DOMAINS[..]);
111 +
    timers::deliver(&mut store, 8, &mut DOMAINS[..]);
112 +
    assert atomic::load(&queue.ring.tail) == events::CAPACITY + 1;
113 +
    for i in 1..events::CAPACITY - events::CRITICAL {
114 +
        let event = pop(queue);
115 +
        assert event.kind == 5 and event.code == i and event.value == i as u64;
116 +
    }
117 +
    for i in 0..events::CRITICAL {
118 +
        let event = pop(queue);
119 +
        assert event.kind == 4 and event.code == i and event.value == i as u64;
120 +
    }
121 +
    timeout(queue, 0x12345678);
122 +
    assert empty(queue);
123 +
    timers::deliver(&mut store, 20, &mut DOMAINS[..]);
124 +
    timeout(queue, 20);
125 +
    timers::deliver(&mut store, 21, &mut DOMAINS[..]);
126 +
    assert empty(queue) and timers::next(&store, 21) == 0xffffffffffffffff;
127 +
}
128 +
129 +
/// A reused low slot cannot steal the next free event position from a survivor.
130 +
fn rotation() {
131 +
    let mut store: timers::Store = undefined;
132 +
    let root = setup(&mut store);
133 +
    let queue = &mut DOMAINS[0].events;
134 +
    for i in 0..events::CAPACITY - events::CRITICAL - 1 {
135 +
        try! events::push(queue, events::Event { kind: 5, reserved: 0, code: i, value: 0 }, events::Class::Ordinary);
136 +
    }
137 +
    try! timers::arm(&mut store, root, 0, 1, 10);
138 +
    try! timers::arm(&mut store, root, 0, 1, 11);
139 +
    timers::deliver(&mut store, 1, &mut DOMAINS[..]);
140 +
    assert timers::next(&store, 1) == 0xffffffffffffffff;
141 +
    assert pop(queue).kind == 5;
142 +
    try! timers::arm(&mut store, root, 1, 1, 12);
143 +
    timers::deliver(&mut store, 2, &mut DOMAINS[..]);
144 +
    for i in 1..events::CAPACITY - events::CRITICAL - 1 { assert pop(queue).code == i; }
145 +
    timeout(queue, 10);
146 +
    timeout(queue, 11);
147 +
    assert empty(queue);
148 +
    timers::deliver(&mut store, 3, &mut DOMAINS[..]);
149 +
    timeout(queue, 12);
150 +
    timers::deliver(&mut store, 4, &mut DOMAINS[..]);
151 +
    assert empty(queue);
152 +
}
153 +
154 +
/// Delay arithmetic rejects wraparound but accepts the last representable tick.
155 +
fn deadlines() {
156 +
    let mut store: timers::Store = undefined;
157 +
    let root = setup(&mut store);
158 +
    let queue = &mut DOMAINS[0].events;
159 +
    rejected(&mut store, root, 0, 0, abi::Error::InvalidArg);
160 +
    rejected(&mut store, root, 0xffffffffffffffff, 1, abi::Error::InvalidArg);
161 +
    rejected(&mut store, root, 0xfffffffffffffffe, 2, abi::Error::InvalidArg);
162 +
    try! timers::arm(&mut store, root, 0xfffffffffffffffe, 1, 99);
163 +
    timers::deliver(&mut store, 0xfffffffffffffffe, &mut DOMAINS[..]);
164 +
    assert empty(queue);
165 +
    timers::deliver(&mut store, 0xffffffffffffffff, &mut DOMAINS[..]);
166 +
    timeout(queue, 99);
167 +
    timers::deliver(&mut store, 0xffffffffffffffff, &mut DOMAINS[..]);
168 +
    assert empty(queue);
169 +
}
170 +
171 +
/// Cancellation compares kind, index, and epoch, never merely a domain slot.
172 +
fn cancellation() {
173 +
    let mut store: timers::Store = undefined;
174 +
    let root = setup(&mut store);
175 +
    let child = try! domains::create(&mut DOMAINS[..], root, 1);
176 +
    try! timers::arm(&mut store, root, 0, 5, 1);
177 +
    try! timers::arm(&mut store, child, 0, 5, 2);
178 +
    timers::cancel(&mut store, root);
179 +
    timers::cancel(&mut store, abi::Object { kind: abi::Kind::Events, index: child.index, epoch: child.epoch });
180 +
    timers::cancel(&mut store, abi::Object { kind: child.kind, index: child.index, epoch: child.epoch + 1 });
181 +
    timers::deliver(&mut store, 5, &mut DOMAINS[..]);
182 +
    assert empty(&DOMAINS[root.index].events);
183 +
    timeout(&mut DOMAINS[child.index].events, 2);
184 +
    try! timers::arm(&mut store, child, 5, 1, 3);
185 +
    try! timers::arm(&mut store, child, 5, 2, 4);
186 +
    timers::cancel(&mut store, child);
187 +
    timers::deliver(&mut store, 7, &mut DOMAINS[..]);
188 +
    assert empty(&DOMAINS[child.index].events);
189 +
    assert timers::next(&store, 7) == 0xffffffffffffffff;
190 +
}
191 +
192 +
/// Reallocated domain slots cannot receive old timeouts or old cancellation.
193 +
fn incarnations() {
194 +
    let mut store: timers::Store = undefined;
195 +
    let root = setup(&mut store);
196 +
    let old = try! domains::create(&mut DOMAINS[..], root, 1);
197 +
    try! timers::arm(&mut store, old, 0, 5, 1);
198 +
    set DOMAINS[old.index].state = domains::Lifecycle::Dead;
199 +
    handles::init(&mut DOMAINS[old.index].handles);
200 +
    let current = try! domains::create(&mut DOMAINS[..], root, 2);
201 +
    assert current.index == old.index and current.epoch <> old.epoch;
202 +
    try! timers::arm(&mut store, current, 0, 5, 2);
203 +
    timers::deliver(&mut store, 5, &mut DOMAINS[..]);
204 +
    timeout(&mut DOMAINS[current.index].events, 2);
205 +
    assert empty(&DOMAINS[current.index].events);
206 +
    try! timers::arm(&mut store, current, 5, 5, 3);
207 +
    timers::cancel(&mut store, old);
208 +
    timers::deliver(&mut store, 10, &mut DOMAINS[..]);
209 +
    timeout(&mut DOMAINS[current.index].events, 3);
210 +
    try! timers::arm(&mut store, current, 10, 10, 4);
211 +
    set DOMAINS[current.index].state = domains::Lifecycle::Dying;
212 +
    timers::deliver(&mut store, 11, &mut DOMAINS[..]);
213 +
    assert timers::next(&store, 11) == 0xffffffffffffffff;
214 +
    set DOMAINS[current.index].state = domains::Lifecycle::Active;
215 +
    timers::deliver(&mut store, 20, &mut DOMAINS[..]);
216 +
    assert empty(&DOMAINS[current.index].events);
217 +
    try! timers::arm(&mut store, abi::Object { kind: abi::Kind::Domain, index: abi::MAX_DOMAINS - 1, epoch: 1 }, 20, 1, 5);
218 +
    timers::deliver(&mut store, 21, &mut DOMAINS[..]);
219 +
    assert timers::next(&store, 21) == 0xffffffffffffffff;
220 +
    rejected(&mut store, domains::none(), 21, 1, abi::Error::BadHandle);
221 +
}
222 +
223 +
/// Exercise timeout acceptance and publication using real domain event stores.
224 +
export fn run() {
225 +
    timing();
226 +
    capacity();
227 +
    backpressure();
228 +
    rotation();
229 +
    deadlines();
230 +
    cancellation();
231 +
    incarnations();
232 +
}
kernel/context.rad +3 -0
5 5
use core::cpu;
6 6
use core::budgets;
7 7
use core::clock;
8 8
use core::contexts;
9 9
10 +
mod wait;
11 +
10 12
/// Address of the fixed user register and fault probe.
11 13
fn entry() -> u64;
12 14
/// Instruction address of the first user ecall.
13 15
fn checkpoint() -> u64;
14 16
/// Arm hart zero's machine timer for the idle-path check.
115 117
    clock::arm(0x02000000, 0, 0xffffffffffffffff);
116 118
    assert contexts::finish(&mut context, &mut budget, running, stopped) == 0;
117 119
    assert context.frame.cause == 0x8000000000000007;
118 120
    assert context.frame.registers[10] > 0 and context.frame.registers[10] <= 500;
119 121
    exhausted(&mut context, &mut budget, stopped);
122 +
    wait::run();
120 123
    return 0;
121 124
}
kernel/context/wait.rad added +112 -0
1 +
//! Real Timeout/Wait transitions with a retained finite budget and an idle hart.
2 +
3 +
use core::abi;
4 +
use core::atomic;
5 +
use core::budget_caps;
6 +
use core::budgets;
7 +
use core::clock;
8 +
use core::contexts;
9 +
use core::cpu;
10 +
use core::domains;
11 +
use core::frames;
12 +
use core::handles;
13 +
use core::memory;
14 +
use core::notifications;
15 +
use core::platform;
16 +
use core::resources;
17 +
use core::state;
18 +
use core::timers;
19 +
20 +
/// Trusted test program that requests a timeout and consumes its event after Wait.
21 +
fn entry() -> u64;
22 +
/// One complete domain and event queue.
23 +
static DOMAINS: [domains::Domain; 1] = undefined;
24 +
/// One retained execution budget.
25 +
static OBJECTS: [resources::Slot; 1] = undefined;
26 +
/// The user context suspended across Wait.
27 +
static CONTEXTS: [contexts::Context; 1] = undefined;
28 +
/// Valid empty RAM accounting for this machine-only control-plane check.
29 +
static POOL: frames::Pool = undefined;
30 +
/// Empty-pool pins.
31 +
static PINS: [u16; 1] = undefined;
32 +
/// Empty-pool claims.
33 +
static ASSIGNED: [bool; 1] = undefined;
34 +
/// Empty-pool grants.
35 +
static GRANTS: [u64; 1] = undefined;
36 +
/// Private one-shot timer storage.
37 +
static TIMERS: timers::Store = undefined;
38 +
/// Shared kernel mechanisms.
39 +
static KERNEL: state::State = undefined;
40 +
41 +
/// Resume one authorized user interval until a real trap and charge it once.
42 +
unsafe fn step() -> u64 {
43 +
    let context = &mut CONTEXTS[0];
44 +
    let budget = budget_caps::value(&mut KERNEL, context.budget);
45 +
    let start = clock::read(0x02000000);
46 +
    let running = try! contexts::begin(context, budget, start, 0);
47 +
    let timeout = timers::next(&TIMERS, start);
48 +
    let mut deadline = context.deadline;
49 +
    if timeout < deadline { set deadline = timeout; }
50 +
    clock::arm(0x02000000, 0, deadline);
51 +
    clock::interrupts(128);
52 +
    cpu::enter(&mut context.frame);
53 +
    let now = clock::read(0x02000000);
54 +
    clock::interrupts(0);
55 +
    assert contexts::finish(context, budget, running, now) > 0;
56 +
    return now;
57 +
}
58 +
59 +
/// An idle interval neither consumes nor replenishes the waiting context's budget.
60 +
export unsafe fn run() {
61 +
    domains::init(&mut DOMAINS[..]);
62 +
    contexts::init(&mut CONTEXTS[0], 0);
63 +
    resources::init(&mut OBJECTS[..]);
64 +
    timers::init(&mut TIMERS);
65 +
    let mut machine: platform::Platform = undefined;
66 +
    set machine.memoryCount = 0;
67 +
    set machine.reservedCount = 0;
68 +
    try! frames::init(&mut POOL, &machine);
69 +
    let mut ram: memory::Memory = undefined;
70 +
    memory::init(&mut ram, &mut POOL, &mut PINS[..], &mut ASSIGNED[..], &mut GRANTS[..], 1);
71 +
    set KERNEL = state::State { domains: &mut DOMAINS[..], resources: &mut OBJECTS[..], contexts: &mut CONTEXTS[..], memory: ram };
72 +
    let handle = domains::root(&mut DOMAINS[..], 0);
73 +
    let root = try! domains::resolve(&DOMAINS[..], 0, handle, abi::EXECUTE);
74 +
    set DOMAINS[0].state = domains::Lifecycle::Active;
75 +
    set DOMAINS[0].env.eventsPointer = &DOMAINS[0].events.ring as u64;
76 +
    try! contexts::prepare(&mut CONTEXTS[0], root, entry(), 0, DOMAINS[0].env);
77 +
    let object = try! resources::create(&mut OBJECTS[..], resources::Value::Budget(try! budgets::init(0, 100000)));
78 +
    resources::retain(&mut OBJECTS[..], object);
79 +
    let authority = handles::install(&mut DOMAINS[0].handles, 2, object, abi::EXECUTE);
80 +
    try! budget_caps::bind(&mut KERNEL, 0, authority, handle, contexts::identity(&CONTEXTS[0]));
81 +
82 +
    let now = step();
83 +
    let frame = &mut CONTEXTS[0].frame;
84 +
    assert frame.cause == 8 and frame.registers[17] == 41;
85 +
    try! timers::arm(&mut TIMERS, root, now, frame.registers[10], frame.registers[11] as u32);
86 +
    cpu::reply(frame, abi::Error::Ok, 0, 0, 0, 0);
87 +
    let blocked = step();
88 +
    assert frame.cause == 8 and frame.registers[17] == 42;
89 +
    cpu::reply(frame, abi::Error::Ok, 0, 0, 0, 0);
90 +
    try! notifications::wait(&mut KERNEL, 0);
91 +
    assert CONTEXTS[0].status == contexts::Status::Waiting;
92 +
    let remaining = budget_caps::value(&mut KERNEL, object).remaining;
93 +
    let deadline = timers::next(&TIMERS, blocked);
94 +
    clock::arm(0x02000000, 0, deadline);
95 +
    clock::interrupts(128);
96 +
    let mut idle: cpu::Frame = undefined;
97 +
    cpu::init(&mut idle, 0, 0, 0, 0);
98 +
    cpu::idle(&mut idle);
99 +
    let expired = clock::read(0x02000000);
100 +
    clock::interrupts(0);
101 +
    clock::arm(0x02000000, 0, 0xffffffffffffffff);
102 +
    assert idle.cause == 0x8000000000000007 and expired >= deadline;
103 +
    assert budget_caps::value(&mut KERNEL, object).remaining == remaining;
104 +
    timers::deliver(&mut TIMERS, expired, &mut DOMAINS[..]);
105 +
    assert notifications::poll(&mut KERNEL) == nil;
106 +
    assert CONTEXTS[0].status == contexts::Status::Ready;
107 +
    let _finished = step();
108 +
    clock::arm(0x02000000, 0, 0xffffffffffffffff);
109 +
    assert frame.cause == 8 and frame.registers[17] == 49 and frame.registers[10] == 42;
110 +
    timers::deliver(&mut TIMERS, expired + 1, &mut DOMAINS[..]);
111 +
    assert atomic::load(&DOMAINS[0].events.ring.head) == atomic::load(&DOMAINS[0].events.ring.tail);
112 +
}
kernel/core.rad +2 -0
16 16
export mod cpu;
17 17
export mod budgets;
18 18
export mod contexts;
19 19
export mod clock;
20 20
export mod budget_caps;
21 +
export mod timers;
22 +
export mod notifications;
kernel/core/notifications.rad added +41 -0
1 +
//! Event-driven context waits; notification never supplies execution authority.
2 +
3 +
use core::abi;
4 +
use core::budget_caps;
5 +
use core::contexts;
6 +
use core::domains;
7 +
use core::events;
8 +
use core::state;
9 +
10 +
/// Block a quiescent current context only after validating the unread-event predicate.
11 +
export fn wait(kernel: *mut state::State, index: u32) throws (abi::Error) {
12 +
    let context = &mut kernel.contexts[index];
13 +
    if context.status <> contexts::Status::Ready { throw abi::Error::Busy; }
14 +
    if not domains::live(kernel.domains, context.owner) { throw abi::Error::BadHandle; }
15 +
    let queue = &mut kernel.domains[context.owner.index].events;
16 +
    try events::refresh(queue);
17 +
    if not events::available(queue) { set context.status = contexts::Status::Waiting; }
18 +
}
19 +
20 +
/// Make waiting contexts ready only while both an unread event and retained budget exist.
21 +
/// Return a malformed queue owner so the control path can apply fault termination.
22 +
export fn poll(kernel: *mut state::State) -> ?abi::Object {
23 +
    for i in 0..kernel.contexts.len {
24 +
        let context = &mut kernel.contexts[i];
25 +
        if context.status <> contexts::Status::Waiting { continue; }
26 +
        if not domains::live(kernel.domains, context.owner) { continue; }
27 +
        let queue = &mut kernel.domains[context.owner.index].events;
28 +
        try events::refresh(queue) catch { return context.owner; };
29 +
        if context.budget.kind <> abi::Kind::Budget or not events::available(queue) { continue; }
30 +
        let budget = budget_caps::value(kernel, context.budget);
31 +
        if budget.remaining > 0 { set context.status = contexts::Status::Ready; }
32 +
    }
33 +
    return nil;
34 +
}
35 +
36 +
/// Publish a checked Wakeup notification without donating or replenishing budget.
37 +
export fn wakeup(kernel: *mut state::State, caller: u32, target: abi::Handle, token: u32) throws (abi::Error) {
38 +
    let domain = try domains::resolve(kernel.domains, caller, target, abi::WAKE);
39 +
    try events::push(&mut kernel.domains[domain.index].events,
40 +
        events::Event { kind: 5, reserved: 0, code: token, value: caller as u64 }, events::Class::Ordinary);
41 +
}
kernel/core/timers.rad added +100 -0
1 +
//! Bounded one-shot timeouts owned and serialized by one hardware hart.
2 +
3 +
use core::abi;
4 +
use core::domains;
5 +
use core::events;
6 +
7 +
/// Maximum accepted timeouts, including expired notifications under backpressure.
8 +
export constant CAPACITY: u32 = 64;
9 +
10 +
/// One accepted notification; a zero owner epoch marks an unused slot.
11 +
export record Timer: Copy {
12 +
    /// Complete destination domain incarnation, never a capability-table slot.
13 +
    owner: abi::Object,
14 +
    /// Absolute monotonic CLINT deadline, private to the kernel.
15 +
    deadline: u64,
16 +
    /// Caller-supplied Timeout notification code.
17 +
    token: u32,
18 +
}
19 +
20 +
/// Fixed private timer storage; all accesses must be serialized by its owning hart.
21 +
export record Store: Copy {
22 +
    /// Armed and expired-but-unpublished one-shot notifications.
23 +
    entries: [Timer; CAPACITY],
24 +
    /// First slot examined by the next bounded delivery pass.
25 +
    cursor: u32,
26 +
}
27 +
28 +
/// Initialize unused per-hart storage before accepting any timeouts.
29 +
export fn init(store: *mut Store) {
30 +
    for i in 0..CAPACITY {
31 +
        set store.entries[i] = Timer { owner: domains::none(), deadline: 0, token: 0 };
32 +
    }
33 +
    set store.cursor = 0;
34 +
}
35 +
36 +
/// Accept one positive, non-overflowing delay in monotonic CLINT ticks.
37 +
/// The caller authorizes the live owner; malformed identities return BadHandle.
38 +
/// InvalidArg and Busy leave all accepted timeouts and delivery order unchanged.
39 +
export fn arm(store: *mut Store, owner: abi::Object, now: u64, delay: u64, token: u32) throws (abi::Error) {
40 +
    if delay == 0 or now > 0xffffffffffffffff - delay { throw abi::Error::InvalidArg; }
41 +
    if owner.kind <> abi::Kind::Domain or owner.index >= abi::MAX_DOMAINS or owner.epoch == 0 {
42 +
        throw abi::Error::BadHandle;
43 +
    }
44 +
    for i in 0..CAPACITY {
45 +
        if store.entries[i].owner.epoch == 0 {
46 +
            set store.entries[i] = Timer { owner, deadline: now + delay, token };
47 +
            return;
48 +
        }
49 +
    }
50 +
    throw abi::Error::Busy;
51 +
}
52 +
53 +
/// Attempt each accepted timeout at most once, retaining failed publications.
54 +
/// Timeout entries carry kind=2, code=token, value=0, and reserved=0.
55 +
/// Dead, dying, and stale owners are discarded before any destination access.
56 +
/// Busy or invalid consumer progress retains the entry until a later kernel entry.
57 +
export fn deliver(store: *mut Store, now: u64, table: *mut [domains::Domain]) {
58 +
    let start = store.cursor;
59 +
    // Advance the starting point even when every publication is backpressured.
60 +
    // Repeated reuse of an early free slot cannot monopolize newly freed capacity.
61 +
    set store.cursor = (start + 1) % CAPACITY;
62 +
    for offset in 0..CAPACITY {
63 +
        let timer = &mut store.entries[(start + offset) % CAPACITY];
64 +
        if timer.owner.epoch == 0 { continue; }
65 +
        if not domains::live(table, timer.owner) {
66 +
            set timer.owner = domains::none();
67 +
            continue;
68 +
        }
69 +
        if timer.deadline > now { continue; }
70 +
        let event = events::Event { kind: 2, reserved: 0, code: timer.token, value: 0 };
71 +
        try events::push(&mut table[timer.owner.index].events, event, events::Class::Ordinary) catch {
72 +
            continue;
73 +
        };
74 +
        set timer.owner = domains::none();
75 +
    }
76 +
}
77 +
78 +
/// Return the nearest strictly future deadline, or u64::MAX if none exists.
79 +
/// Ignore due retained notifications so backpressure cannot cause interrupt spin.
80 +
export fn next(store: *Store, now: u64) -> u64 {
81 +
    let mut deadline: u64 = 0xffffffffffffffff;
82 +
    for i in 0..CAPACITY {
83 +
        let timer = &store.entries[i];
84 +
        if timer.owner.epoch <> 0 and timer.deadline > now and timer.deadline < deadline {
85 +
            set deadline = timer.deadline;
86 +
        }
87 +
    }
88 +
    return deadline;
89 +
}
90 +
91 +
/// Cancel every unpublished timeout belonging to the exact domain incarnation.
92 +
/// Already published events remain in the destination's notification ring.
93 +
export fn cancel(store: *mut Store, owner: abi::Object) {
94 +
    for i in 0..CAPACITY {
95 +
        let timer = &mut store.entries[i];
96 +
        if timer.owner.kind == owner.kind and timer.owner.index == owner.index and timer.owner.epoch == owner.epoch {
97 +
            set timer.owner = domains::none();
98 +
        }
99 +
    }
100 +
}