kernel: Manage additional execution contexts

52449bef848b31f41da152b9bf5fb2f82a63f378164573e3321f41ecd4badd43
Assisted-by: Codex:gpt-6
Alexis Sellier committed ago 1 parent a6324524
kernel/kernel/abi.rad +30 -0
55 55
    codeBytes: u64,
56 56
    /// Number of private state bytes per instance.
57 57
    dataBytes: u64,
58 58
}
59 59
60 +
/// Validated entry, stack, and arguments for an additional context.
61 +
export record ContextStart: Copy {
62 +
    /// Exported native entry in the target domain's package graph.
63 +
    entry: u64,
64 +
    /// Aligned top of writable user stack storage.
65 +
    stack: u64,
66 +
    /// Readable startup argument address passed in a0.
67 +
    args: u64,
68 +
    /// Startup argument byte count passed in a1.
69 +
    size: u64,
70 +
}
71 +
72 +
/// Identity, owner, and user stack bounds of a live context.
73 +
export record ContextInfo: Copy {
74 +
    /// Generation-bearing context identity.
75 +
    context: u64,
76 +
    /// Owning domain identity.
77 +
    domain: u64,
78 +
    /// First byte of the reserved user stack.
79 +
    stackBase: u64,
80 +
    /// Exclusive upper bound of the reserved user stack.
81 +
    stackTop: u64,
82 +
    /// Ready or Stopped encoded as zero or one.
83 +
    state: u64,
84 +
    /// Executing hart, or all ones while no hart owns execution.
85 +
    hart: u64,
86 +
}
87 +
60 88
/// Identity and CPU authority of the calling execution context.
61 89
export record CurrentContextInfo: Copy {
62 90
    /// Generation-bearing context identity.
63 91
    context: u64,
64 92
    /// Physical hart executing this call.
196 224
    ContextDestroy = 61,
197 225
    /// Query an execution context.
198 226
    QueryContext = 62,
199 227
    /// Query the current context and execution window.
200 228
    CurrentContext = 63,
229 +
    /// Report normal return from the calling execution context.
230 +
    ContextReturn = 64,
201 231
    /// Partition a CPU time window.
202 232
    BudgetSplit = 70,
203 233
    /// Combine adjacent CPU time windows.
204 234
    BudgetMerge = 71,
205 235
    /// Query a CPU time window.
kernel/kernel/budgets.rad +1 -0
211 211
    let permission = try capability::lookup(table, request.domain, abi::Kind::Domain, abi::Rights(abi::WAKE));
212 212
    let mut target = try domains::get(domainStore, permission.object);
213 213
    if target.state == domains::Lifecycle::Dead { throw abi::Error::BadHandle; }
214 214
    if not slots::matches(&domainStore.contextSlots[..], request.context, slots::State::Live)
215 215
        or domainStore.contexts[request.context.index].owner <> permission.object { throw abi::Error::BadHandle; }
216 +
    if domainStore.contexts[request.context.index].state == domains::ContextState::Stopped { throw abi::Error::InvalidArg; }
216 217
    if not available(store, object, candidate, request.context) { throw abi::Error::Busy; }
217 218
    let mut result = request.budget;
218 219
    if permission.object <> table.owner {
219 220
        let entry = try! capability::get(table, request.budget);
220 221
        set result = try transfer(store, table, &mut target.memory.table, request.budget, *entry.rights as u64, now);
kernel/kernel/calls.rad +34 -6
64 64
            let removed = try! capability::invalidate(table, handle);
65 65
        },
66 66
    }
67 67
}
68 68
69 -
/// Map a checked, aligned result address to writable words.
69 +
/// Map a checked, aligned caller record address to words.
70 70
fn memory(address: u64) -> *mut u64;
71 71
72 72
/// Test whether one nonwrapping extent contains the complete result range.
73 73
fn contains(base: u64, size: u64, address: u64, length: u64) -> bool {
74 74
    return address >= base and address - base <= size and length <= size - (address - base);
75 75
}
76 76
77 -
/// Validate writable Page or private package-data storage for a result record.
78 -
/// Package-state tables and kernel metadata are not writable result storage.
79 -
export unsafe fn resultBuffer(domain: &domains::Domain, address: u64, size: u64) -> bool {
77 +
/// Validate aligned Page or private package-data storage with the given rights.
78 +
/// Package-state tables and kernel metadata cannot hold caller-supplied records.
79 +
unsafe fn buffer(domain: &domains::Domain, address: u64, size: u64, rights: abi::Rights) -> bool {
80 80
    if (address & 7) <> 0 or size > 0xffffffffffffffff - address { return false; }
81 -
    if pages::accessible(&pages::STORE, &domain.memory.table, address, size, abi::Rights(abi::WRITE)) { return true; }
81 +
    if pages::accessible(&pages::STORE, &domain.memory.table, address, size, rights) { return true; }
82 82
    for i in 0..limits::PACKAGES {
83 83
        if registry::STORE.slots[i].state <> slots::State::Live { continue; }
84 84
        let base = domain.graph.table[i];
85 85
        if base == 0 { continue; }
86 86
        let package = try! registry::get(&registry::STORE, abi::Ref { index: i, generation: registry::STORE.slots[i].generation });
90 90
}
91 91
92 92
/// Copy a fixed query record only after validating its exact buffer size.
93 93
unsafe fn reply(domain: &domains::Domain, arguments: &[u64], words: &[u64]) throws (abi::Error) {
94 94
    let size = words.len as u64 * 8;
95 -
    if arguments[2] <> size or arguments[3] <> 0 or not resultBuffer(domain, arguments[1], size) {
95 +
    if arguments[2] <> size or arguments[3] <> 0 or not buffer(domain, arguments[1], size, abi::Rights(abi::WRITE)) {
96 96
        throw abi::Error::InvalidArg;
97 97
    }
98 98
    let output = @sliceOf(memory(arguments[1]), words.len);
99 99
    for i in 0..words.len { set output[i] = words[i]; }
100 100
}
144 144
            let mut entry: u64 = 0;
145 145
            if let address = package.entry { set entry = address; }
146 146
            try reply(&domain, arguments, &[abi::id(object), entry, package.codeAddress, package.code.len as u64 * 4, package.memory as u64]);
147 147
            return 0;
148 148
        },
149 +
        case 60 => {
150 +
            if arguments[2] <> @sizeOf(abi::ContextStart) as u64 or arguments[3] <> 0
151 +
                or not buffer(&domain, arguments[1], arguments[2], abi::Rights(abi::READ)) {
152 +
                throw abi::Error::InvalidArg;
153 +
            }
154 +
            let words = @sliceOf(memory(arguments[1]), 4);
155 +
            let start = abi::ContextStart { entry: words[0], stack: words[1], args: words[2], size: words[3] };
156 +
            return abi::id(try domains::contextCreate(&mut domains::STORE, &mut pages::STORE, &registry::STORE,
157 +
                &domain.memory.table, handle, start));
158 +
        },
159 +
        case 61 => {
160 +
            if arguments[2] <> 0 or arguments[3] <> 0 { throw abi::Error::InvalidArg; }
161 +
            try domains::contextDestroy(&mut domains::STORE, &mut pages::STORE, &domain.memory.table,
162 +
                handle, abi::reference(arguments[1]));
163 +
            return 0;
164 +
        },
165 +
        case 62 => {
166 +
            let permission = try capability::authority(&domain.memory.table, handle, abi::Rights(0));
167 +
            let target = try domains::get(&domains::STORE, permission.object);
168 +
            let mut object = target.initial;
169 +
            if arguments[1] <> 0 { set object = abi::reference(arguments[1]); }
170 +
            let context = try domains::context(&domains::STORE, permission.object, object);
171 +
            let mut hart: u64 = 0xffffffffffffffff;
172 +
            if let id = context.hart { set hart = id as u64; }
173 +
            try reply(&domain, &[0, arguments[2], arguments[3], 0],
174 +
                &[abi::id(object), abi::id(permission.object), context.userStack.start, context.userStack.end, context.state as u64, hart]);
175 +
            return 0;
176 +
        },
149 177
        case 63 => {
150 178
            if arguments[0] <> 0 { throw abi::Error::InvalidArg; }
151 179
            let current = dispatch::current();
152 180
            try reply(&domain, arguments, &[current.context, current.hart, current.budget]);
153 181
            return 0;
kernel/kernel/calls.ras +2 -2
1 -
//! Typed access to a checked user result buffer.
1 +
//! Typed access to a checked user record buffer.
2 2
.text;
3 3
.export @kernel::calls::memory;
4 4
5 -
// The caller checked alignment, extent, and writable domain ownership.
5 +
// The caller checked alignment, extent, and the required domain access rights.
6 6
@kernel::calls::memory
7 7
    ret;
kernel/kernel/dispatch.rad +10 -2
86 86
export unsafe fn call(frame: &mut trap::Frame, anchor: &mut trap::Hart) {
87 87
    let id = hart();
88 88
    assert id < limits::HARTS;
89 89
    let context = HARTS[id].state.current else { trap::halt(); return; };
90 90
    assert slots::matches(&domains::STORE.contextSlots[..], context, slots::State::Live);
91 -
    calls::handle(domains::STORE.contexts[context.index].owner, frame, now(HARTS[id].timer));
91 +
    let owner = domains::STORE.contexts[context.index].owner;
92 +
    if frame.registers[17] == abi::Operation::ContextReturn as u64 {
93 +
        assert trap::advanceCall(frame);
94 +
        try! domains::returned(&mut domains::STORE, owner, context);
95 +
    } else {
96 +
        calls::handle(owner, frame, now(HARTS[id].timer));
97 +
    }
92 98
    interrupt(frame, anchor);
93 99
}
94 100
95 101
/// Enter the first authorized context or the hart's retained idle frame.
96 102
export unsafe fn start(anchor: &mut trap::Hart) -> ! {
143 149
/// Test whether a live context belongs to the active domain owning its budget.
144 150
fn runnable(store: &domains::Store, owner: abi::Ref, context: abi::Ref) -> bool {
145 151
    if not slots::matches(&store.slots[..], owner, slots::State::Live) { return false; }
146 152
    if store.records[owner.index].state <> domains::Lifecycle::Active { return false; }
147 153
    if not slots::matches(&store.contextSlots[..], context, slots::State::Live) { return false; }
148 -
    return store.contexts[context.index].owner == owner;
154 +
    return store.contexts[context.index].owner == owner and store.contexts[context.index].state == domains::ContextState::Ready;
149 155
}
150 156
151 157
/// Select the unique runnable window and the earliest relevant timer boundary.
152 158
/// The caller serializes budget and domain metadata while taking this snapshot.
153 159
export fn select(windows: &budgets::Store, contexts: &domains::Store, hart: u32, now: u64)
180 186
{
181 187
    let choice = try select(windows, contexts, state.hart, now);
182 188
    if let current = state.current {
183 189
        assert slots::matches(&contexts.contextSlots[..], current, slots::State::Live);
184 190
        set contexts.contexts[current.index].frame = *frame;
191 +
        set contexts.contexts[current.index].hart = nil;
185 192
    }
186 193
    if let next = choice.context {
187 194
        let context = contexts.contexts[next.index];
188 195
        assert context.kernelStack.start < context.kernelStack.end;
189 196
        set *frame = context.frame;
190 197
        set anchor.stackBottom = context.kernelStack.start;
191 198
        set anchor.stackTop = context.kernelStack.end;
199 +
        set contexts.contexts[next.index].hart = state.hart;
192 200
    } else {
193 201
        set *frame = state.idle;
194 202
        set anchor.stackBottom = state.stack.start;
195 203
        set anchor.stackTop = state.stack.end;
196 204
    }
kernel/kernel/domains.rad +134 -5
10 10
use super::instances;
11 11
use super::events;
12 12
use super::trap;
13 13
use super::range;
14 14
use super::pages;
15 +
use std::arch::rv64::shared;
15 16
16 17
/// Retained kernel stack capacity for one suspended execution context.
17 18
export constant KERNEL_STACK_PAGES: u32 = 64;
18 19
19 20
/// Management rights returned to a domain's creator.
37 38
    ring: events::Ring,
38 39
    /// Startup values exposed read-only to this domain.
39 40
    env: abi::Env,
40 41
}
41 42
43 +
/// Eligibility to resume a live execution context.
44 +
export union ContextState: Copy {
45 +
    /// The context can execute with CPU authority in an active domain.
46 +
    Ready,
47 +
    /// The context has returned and cannot execute again.
48 +
    Stopped,
49 +
}
50 +
42 51
/// Saved integer state owned by one execution context.
43 52
export record Context: Copy {
44 53
    /// Domain that owns this context.
45 54
    owner: abi::Ref,
55 +
    /// Whether this context can resume.
56 +
    state: ContextState,
57 +
    /// Hart retaining this context as its current execution owner.
58 +
    hart: ?u32,
46 59
    /// Physical frames retained until this context stops.
47 60
    kernelFrames: frames::Run,
48 61
    /// Exclusive stack bounds used by trap entry and suspended M-mode calls.
49 62
    kernelStack: range::Range,
63 +
    /// Exclusive user stack extent; empty until initial activation.
64 +
    userStack: range::Range,
50 65
    /// Architectural state used when this context resumes.
51 66
    frame: trap::Frame,
52 67
}
53 68
54 69
/// Published domain resources and lifecycle metadata.
92 107
93 108
/// View an allocated physical extent that contains a complete Memory record.
94 109
fn memory(address: u64) -> *unsafe mut Memory;
95 110
/// Clear whole mapped physical frames before installing domain metadata.
96 111
fn zero(address: u64, count: u32);
112 +
/// Address of the U-mode normal-return call sequence.
113 +
fn returnAddress() -> u64;
97 114
98 115
/// Initialize fresh metadata before boot-domain publication.
99 116
export fn initialize(store: &mut Store) {
100 117
    slots::initialize(&mut store.slots[..]);
101 118
    slots::initialize(&mut store.contextSlots[..]);
106 123
export fn get(store: &Store, object: abi::Ref) -> Domain throws (abi::Error) {
107 124
    if not slots::matches(&store.slots[..], object, slots::State::Live) { throw abi::Error::BadHandle; }
108 125
    return store.records[object.index];
109 126
}
110 127
128 +
/// Read a live context after checking its generation and owning domain.
129 +
export fn context(store: &Store, owner: abi::Ref, object: abi::Ref) -> Context throws (abi::Error) {
130 +
    let domain = try get(store, owner);
131 +
    if not slots::matches(&store.contextSlots[..], object, slots::State::Live)
132 +
        or store.contexts[object.index].owner <> owner { throw abi::Error::BadHandle; }
133 +
    return store.contexts[object.index];
134 +
}
135 +
136 +
/// Stop a returned context; initial return ends execution of its whole domain.
137 +
export fn returned(store: &mut Store, owner: abi::Ref, object: abi::Ref) throws (abi::Error) {
138 +
    let current = try context(store, owner, object);
139 +
    if store.records[owner.index].initial == object {
140 +
        set store.records[owner.index].state = Lifecycle::Dead;
141 +
        for i in 0..limits::CONTEXTS {
142 +
            if store.contextSlots[i].state == slots::State::Live and store.contexts[i].owner == owner {
143 +
                set store.contexts[i].state = ContextState::Stopped;
144 +
            }
145 +
        }
146 +
    } else {
147 +
        set store.contexts[object.index].state = ContextState::Stopped;
148 +
    }
149 +
}
150 +
151 +
/// Reclaim an additional context after all harts have released execution ownership.
152 +
export fn contextDestroy(store: &mut Store, memory: &mut pages::Store, table: &capability::Table,
153 +
    authority: abi::Handle, object: abi::Ref) throws (abi::Error)
154 +
{
155 +
    let permit = try capability::authority(table, authority, abi::Rights(abi::DESTROY));
156 +
    let target = try context(store, permit.object, object);
157 +
    if store.records[permit.object.index].initial == object { throw abi::Error::Denied; }
158 +
    if target.hart <> nil { throw abi::Error::Busy; }
159 +
    try! frames::release(&mut memory.backings.pool, target.kernelFrames);
160 +
    try! slots::release(&mut store.contextSlots[..], object);
161 +
}
162 +
163 +
/// Derive a descending stack extent from one writable Page capability.
164 +
fn stackRange(memory: &pages::Store, table: &capability::Table, top: u64) -> range::Range throws (abi::Error) {
165 +
    if top < 16 or (top & 15) <> 0 { throw abi::Error::InvalidArg; }
166 +
    for i in 0..limits::HANDLES {
167 +
        if table.slots[i].state <> slots::State::Live { continue; }
168 +
        let entry = table.entries[i];
169 +
        if entry.kind <> abi::Kind::Page or not abi::permits(entry.rights, abi::Rights(abi::WRITE)) { continue; }
170 +
        let page = try pages::get(memory, entry.object) catch { continue; };
171 +
        if page.base <= top - 16 and top - page.base <= page.count as u64 * limits::FRAME_SIZE {
172 +
            return range::Range { start: page.base, end: top };
173 +
        }
174 +
    }
175 +
    throw abi::Error::InvalidArg;
176 +
}
177 +
178 +
/// Reject physical overlap with any live context's reserved user stack.
179 +
fn stackAvailable(store: &Store, candidate: range::Range) throws (abi::Error) {
180 +
    for i in 0..limits::CONTEXTS {
181 +
        if store.contextSlots[i].state <> slots::State::Live { continue; }
182 +
        let used = store.contexts[i].userStack;
183 +
        if used.start < used.end and candidate.start < used.end and used.start < candidate.end {
184 +
            throw abi::Error::Busy;
185 +
        }
186 +
    }
187 +
}
188 +
111 189
/// Activate a pending domain after checking its stack and argument authority.
112 190
/// The caller serializes metadata until the initial frame and state are published.
113 191
export unsafe fn activate(store: &mut Store, memory: &pages::Store, table: &capability::Table,
114 192
    handle: abi::Handle, stack: u64, args: u64, size: u64) throws (abi::Error)
115 193
{
116 194
    let entry = try capability::lookup(table, handle, abi::Kind::Domain, abi::Rights(abi::EXECUTE));
117 195
    let mut domain = try get(store, entry.object);
118 196
    if domain.state <> Lifecycle::Pending { throw abi::Error::NotPending; }
119 -
    if (stack & 15) <> 0 or stack < 16
120 -
        or not pages::accessible(memory, &domain.memory.table, stack - 16, 16, abi::Rights(abi::WRITE)) {
121 -
        throw abi::Error::InvalidArg;
122 -
    }
197 +
    let userStack = try stackRange(memory, &domain.memory.table, stack);
123 198
    if size <> 0 and not pages::accessible(memory, &domain.memory.table, args, size, abi::Rights(abi::READ)) {
124 199
        throw abi::Error::InvalidArg;
125 200
    }
126 201
    assert slots::matches(&store.contextSlots[..], domain.initial, slots::State::Live);
202 +
    try stackAvailable(store, userStack);
127 203
    set domain.memory.env = abi::Env {
128 204
        argsPointer: args, argsSize: size, eventsHandle: *domain.events,
129 205
        eventsPointer: (&domain.memory.ring) as u64,
130 206
    };
131 207
    set store.contexts[domain.initial.index].frame.registers[2] = stack;
132 208
    set store.contexts[domain.initial.index].frame.registers[10] = (&domain.memory.env) as u64;
209 +
    set store.contexts[domain.initial.index].userStack = userStack;
133 210
    set store.records[entry.object.index].state = Lifecycle::Active;
134 211
}
135 212
213 +
/// Check an entry against the target's resident package graph exports.
214 +
unsafe fn exportedEntry(domain: &Domain, packages: &registry::Store, address: u64) -> bool {
215 +
    for i in 0..limits::PACKAGES {
216 +
        if domain.graph.table[i] == 0 or packages.slots[i].state <> slots::State::Live { continue; }
217 +
        let package = try! registry::get(packages, abi::Ref { index: i, generation: packages.slots[i].generation });
218 +
        if package.entry == address { return true; }
219 +
        for symbol in package.exports {
220 +
            match symbol.target {
221 +
                case shared::Target::Function(entry) => { if entry == address { return true; } },
222 +
                else => {},
223 +
            }
224 +
        }
225 +
    }
226 +
    return false;
227 +
}
228 +
229 +
/// Create an additional context with private kernel storage and shared domain state.
230 +
/// The caller serializes domain, capability, and physical allocation metadata.
231 +
export unsafe fn contextCreate(store: &mut Store, memory: &mut pages::Store, packages: &registry::Store,
232 +
    table: &capability::Table, authority: abi::Handle, start: abi::ContextStart) -> abi::Ref throws (abi::Error)
233 +
{
234 +
    let permission = try capability::authority(table, authority, abi::Rights(abi::EXECUTE));
235 +
    let domain = try get(store, permission.object);
236 +
    if domain.state == Lifecycle::Dead { throw abi::Error::BadHandle; }
237 +
    if start.entry == 0 or not exportedEntry(&domain, packages, start.entry)
238 +
        or (start.size <> 0 and not pages::accessible(memory, &domain.memory.table, start.args, start.size, abi::Rights(abi::READ))) {
239 +
        throw abi::Error::InvalidArg;
240 +
    }
241 +
    let userStack = try stackRange(memory, &domain.memory.table, start.stack);
242 +
    try stackAvailable(store, userStack);
243 +
    let slot = try slots::reserve(&mut store.contextSlots[..]);
244 +
    let pending = try frames::reserve(&mut memory.backings.pool, KERNEL_STACK_PAGES) catch error {
245 +
        try! slots::cancel(&mut store.contextSlots[..], slot); throw error;
246 +
    };
247 +
    let kernelFrames = frames::commit(pending);
248 +
    let kernelStack = try! frames::extent(&memory.backings.pool, kernelFrames);
249 +
    zero(kernelStack.start, kernelFrames.count);
250 +
    let mut frame = trap::Frame { registers: [0; 32], pc: start.entry, status: 0x80, cause: 0, value: 0 };
251 +
    set frame.registers[1] = returnAddress();
252 +
    set frame.registers[2] = start.stack;
253 +
    set frame.registers[3] = domain.graph.table.ptr as u64;
254 +
    set frame.registers[10] = start.args;
255 +
    set frame.registers[11] = start.size;
256 +
    let object = slots::reference(&slot);
257 +
    set store.contexts[object.index] = Context {
258 +
        owner: permission.object, state: ContextState::Ready, hart: nil, kernelFrames, kernelStack, userStack, frame,
259 +
    };
260 +
    return try! slots::commit(&mut store.contextSlots[..], slot);
261 +
}
262 +
136 263
/// Memory allocated before the domain and its context become observable.
137 264
record Prepared: Copy {
138 265
    /// Frames for capabilities and the event ring.
139 266
    allocation: frames::Run,
140 267
    /// Mapped metadata at the allocation's start.
217 344
        kind: abi::Kind::Events, object, rights: abi::Rights(abi::READ | abi::WRITE),
218 345
    });
219 346
    let initial = slots::reference(&contextSlot);
220 347
    // MPIE enables interrupts after the first return into U-mode.
221 348
    let mut frame = trap::Frame { registers: [0; 32], pc: entry, status: 0x80, cause: 0, value: 0 };
349 +
    set frame.registers[1] = returnAddress();
222 350
    set frame.registers[3] = prepared.graph.table.ptr as u64;
223 351
    set store.contexts[initial.index] = Context {
224 -
        owner: object, kernelFrames: prepared.kernelFrames, kernelStack: prepared.kernelStack, frame,
352 +
        owner: object, state: ContextState::Ready, hart: nil, kernelFrames: prepared.kernelFrames, kernelStack: prepared.kernelStack,
353 +
        userStack: range::Range { start: 0, end: 0 }, frame,
225 354
    };
226 355
    set store.records[object.index] = Domain {
227 356
        state: Lifecycle::Pending, creator: table.owner, parent: table.owner, image: root, initial,
228 357
        allocation: prepared.allocation, memory: prepared.memory, graph: prepared.graph, events: queue,
229 358
    };
kernel/kernel/domains.ras +10 -0
1 1
//! Physical domain metadata views and complete frame clearing.
2 2
.text;
3 3
.export @kernel::domains::memory;
4 4
.export @kernel::domains::zero;
5 +
.export @kernel::domains::returnAddress;
6 +
7 +
// Native entries return to a gp-independent user call sequence.
8 +
@kernel::domains::returnAddress
9 +
    la %a0 @contextReturn;
10 +
    ret;
11 +
@contextReturn
12 +
    li %a7 64;
13 +
    ecall;
14 +
    ebreak;
5 15
6 16
// The allocated frame extent contains one complete aligned Memory record.
7 17
@kernel::domains::memory
8 18
    ret;
9 19
kernel/kernel/sys.rad +21 -0
120 120
        try! result(ecall(abi::Operation::QueryEvents as u32, *handle as i64, (&mut info) as i64, @sizeOf(abi::EventsInfo) as i64, 0));
121 121
        return info;
122 122
    }
123 123
}
124 124
125 +
/// Create an additional context with Execute authority over its domain.
126 +
export fn contextCreate(domain: abi::Handle, start: abi::ContextStart) -> u64 throws (abi::Error) {
127 +
    return try result(ecall(abi::Operation::ContextCreate as u32, *domain as i64,
128 +
        (&start) as i64, @sizeOf(abi::ContextStart) as i64, 0));
129 +
}
130 +
131 +
/// Reclaim an additional context that no hart is executing.
132 +
export fn contextDestroy(domain: abi::Handle, context: u64) throws (abi::Error) {
133 +
    try result(ecall(abi::Operation::ContextDestroy as u32, *domain as i64, context as i64, 0, 0));
134 +
}
135 +
136 +
/// Read a context's owner, stack, and state. Zero selects the initial context.
137 +
export fn queryContext(domain: abi::Handle, context: u64) -> abi::ContextInfo throws (abi::Error) {
138 +
    unsafe {
139 +
        let mut info: abi::ContextInfo = undefined;
140 +
        try result(ecall(abi::Operation::QueryContext as u32, *domain as i64, context as i64,
141 +
            (&mut info) as i64, @sizeOf(abi::ContextInfo) as i64));
142 +
        return info;
143 +
    }
144 +
}
145 +
125 146
/// Read the current extent and binding of a held CPU window.
126 147
export fn queryBudget(handle: abi::Handle) -> abi::BudgetInfo throws (abi::Error) {
127 148
    unsafe {
128 149
        let mut info: abi::BudgetInfo = undefined;
129 150
        try result(ecall(abi::Operation::QueryBudget as u32, *handle as i64, (&mut info) as i64, @sizeOf(abi::BudgetInfo) as i64, 0));
kernel/kernel/tests/budgets.rad +3 -1
126 126
    let child = slots::reference(&childSlot);
127 127
    capability::initialize(&mut MEMORY.table, child);
128 128
    let contextSlot = try! slots::reserve(&mut DOMAINS.contextSlots[..]);
129 129
    let context = slots::reference(&contextSlot);
130 130
    set DOMAINS.contexts[context.index] = domains::Context {
131 -
        owner: child, kernelFrames: frames::Run { first: 0, count: 0 }, kernelStack: range::Range { start: 0, end: 0 },
131 +
        owner: child, state: domains::ContextState::Ready, hart: nil,
132 +
        kernelFrames: frames::Run { first: 0, count: 0 }, kernelStack: range::Range { start: 0, end: 0 },
133 +
        userStack: range::Range { start: 0, end: 0 },
132 134
        frame: trap::Frame { registers: [0; 32], pc: 4, status: 0x80, cause: 0, value: 0 },
133 135
    };
134 136
    set DOMAINS.records[child.index] = domains::Domain {
135 137
        state: domains::Lifecycle::Pending, creator: root, parent: root,
136 138
        image: abi::Ref { index: 0, generation: 1 }, initial: context,
kernel/kernel/tests/calls.rad +20 -0
139 139
        try testing::expect(error == abi::Error::InvalidArg); set rejected += 1;
140 140
    };
141 141
    try testing::expect(rejected == 6 and RAM[OFFSET + 2] == 0x12345678);
142 142
}
143 143
144 +
/// Context calls validate descriptor storage and preserve capacity on failure.
145 +
@test unsafe fn contextBuffers() throws (testing::TestError) {
146 +
    let owner = initialize();
147 +
    let page = try! calls::invoke(owner, 30, &[0, 1, 0, 0], 0);
148 +
    let base = (&RAM[OFFSET]) as u64;
149 +
    let mut rejected: u32 = 0;
150 +
    for args in &[[0 as u64, 0, 32, 0], [0, base, 24, 0], [0, base, 32, 1], [0, base + 1, 32, 0]] {
151 +
        try calls::invoke(owner, 60, &args[..], 0) catch error {
152 +
            try testing::expect(error == abi::Error::InvalidArg); set rejected += 1;
153 +
        };
154 +
    }
155 +
    try calls::invoke(owner, 60, &[0, base, 32, 0], 0) catch error {
156 +
        try testing::expect(error == abi::Error::Denied); set rejected += 1;
157 +
    };
158 +
    try calls::invoke(owner, 62, &[0, 0, base, 32], 0) catch error {
159 +
        try testing::expect(error == abi::Error::BadHandle); set rejected += 1;
160 +
    };
161 +
    try testing::expect(rejected == 6 and domains::STORE.contextSlots[0].state == slots::State::Free);
162 +
}
163 +
144 164
/// An unknown user operation advances PC and returns only a negative a0 error.
145 165
@test unsafe fn trapReply() throws (testing::TestError) {
146 166
    let owner = initialize();
147 167
    let mut frame = trap::Frame { registers: [123; 32], pc: 0x80000000, status: 0x80, cause: 8, value: 0 };
148 168
    set frame.registers[17] = 0xffffffffffffffff;
kernel/kernel/tests/dispatch.rad +10 -0
29 29
    capability::initialize(&mut TABLE, owner);
30 30
    set DOMAINS.slots[0] = slots::Slot { generation: 1, state: slots::State::Live };
31 31
    set DOMAINS.records[0].state = domains::Lifecycle::Active;
32 32
    set DOMAINS.contextSlots[0] = slots::Slot { generation: 1, state: slots::State::Live };
33 33
    set DOMAINS.contexts[0].owner = owner;
34 +
    set DOMAINS.contexts[0].state = domains::ContextState::Ready;
35 +
    set DOMAINS.contexts[0].hart = nil;
34 36
    let first = try! budgets::seed(&mut BUDGETS, &mut TABLE, 0, 10, 40);
35 37
    let gap = try! budgets::split(&mut BUDGETS, &mut TABLE, first, 20, 0);
36 38
    let last = try! budgets::split(&mut BUDGETS, &mut TABLE, gap, 30, 0);
37 39
    let a = try! capability::get(&TABLE, first);
38 40
    let b = try! capability::get(&TABLE, last);
65 67
    try testing::expect(pending.context == nil);
66 68
    set DOMAINS.records[0].state = domains::Lifecycle::Active;
67 69
    set DOMAINS.contextSlots[0].generation = 2;
68 70
    let stale = try! dispatch::select(&BUDGETS, &DOMAINS, 0, 10);
69 71
    try testing::expect(stale.context == nil);
72 +
    set DOMAINS.contextSlots[0].generation = 1;
73 +
    set DOMAINS.contexts[0].state = domains::ContextState::Stopped;
74 +
    let stopped = try! dispatch::select(&BUDGETS, &DOMAINS, 0, 10);
75 +
    try testing::expect(stopped.context == nil);
70 76
}
71 77
72 78
/// Each online hart has a separate comparison word and shares one clock register.
73 79
@test unsafe fn timerRegisters() throws (testing::TestError) {
74 80
    set PLATFORM.harts = 0x81;
100 106
    let mut frame = trap::Frame { registers: [0; 32], pc: 0x40, status: 0x80, cause: 0, value: 0 };
101 107
    set DOMAINS.contexts[0].frame = frame;
102 108
    set DOMAINS.contexts[0].kernelStack = range::Range { start: 0x8000, end: 0xc000 };
103 109
    set DOMAINS.contextSlots[1] = slots::Slot { generation: 1, state: slots::State::Live };
104 110
    set DOMAINS.contexts[1].owner = TABLE.owner;
111 +
    set DOMAINS.contexts[1].state = domains::ContextState::Ready;
112 +
    set DOMAINS.contexts[1].hart = nil;
105 113
    set DOMAINS.contexts[1].frame = frame;
106 114
    set DOMAINS.contexts[1].kernelStack = range::Range { start: 0xc000, end: 0x10000 };
107 115
    set BUDGETS.windows[1].context = abi::Ref { index: 1, generation: 1 };
108 116
    let mut idle = frame;
109 117
    set idle.pc = 0x100;
117 125
        stackTop: 0x4000, stackBottom: 0x2000, kernelGp: 0x20000, handler,
118 126
        savedT0: 0, savedT1: 0, savedSp: 0,
119 127
    };
120 128
    let first = try! dispatch::exchange(&mut state, &BUDGETS, &mut DOMAINS, &mut frame, &mut anchor, 10);
121 129
    try testing::expect(frame.pc == 0x40 and anchor.stackTop == 0xc000);
130 +
    try testing::expect(DOMAINS.contexts[0].hart == 0);
122 131
    set frame.pc = 0x88;
123 132
    set frame.status = 0x1880;
124 133
    set frame.registers[2] = 0xb000;
125 134
    set frame.registers[5] = 43;
126 135
    let second = try! dispatch::exchange(&mut state, &BUDGETS, &mut DOMAINS, &mut frame, &mut anchor, 20);
127 136
    try testing::expect(anchor.stackBottom == 0xc000 and anchor.stackTop == 0x10000);
137 +
    try testing::expect(DOMAINS.contexts[0].hart == nil and DOMAINS.contexts[1].hart == 0);
128 138
    set frame.pc = 0x77;
129 139
    let resumed = try! dispatch::exchange(&mut state, &BUDGETS, &mut DOMAINS, &mut frame, &mut anchor, 30);
130 140
    try testing::expect(frame.pc == 0x88 and frame.status == 0x1880);
131 141
    try testing::expect(frame.registers[2] == 0xb000 and frame.registers[5] == 43);
132 142
    try testing::expect(DOMAINS.contexts[1].frame.pc == 0x77 and anchor.stackTop == 0xc000);
kernel/kernel/tests/domains.rad +105 -0
101 101
        try testing::expect(err == abi::Error::NotPending); set failures += 1;
102 102
    };
103 103
    try testing::expect(failures == 5);
104 104
}
105 105
106 +
/// Additional contexts share private package state and reserve distinct kernel stacks.
107 +
@test unsafe fn additionalContext() throws (testing::TestError) {
108 +
    initialize(abi::CREATE);
109 +
    slots::initialize(&mut PAGES.slots[..]);
110 +
    let handle = try! domains::create(&mut DOMAINS, &mut BACKINGS, &PACKAGES, &mut TABLE, abi::Handle(0), IMAGE);
111 +
    let authority = try! capability::get(&TABLE, handle);
112 +
    let mut child = try! domains::get(&DOMAINS, authority.object);
113 +
    // Move the fixture's physical allocation state into its Page store.
114 +
    set PAGES.backings = BACKINGS;
115 +
    let pending = try! slots::reserve(&mut PAGES.slots[..]);
116 +
    let object = slots::reference(&pending);
117 +
    set PAGES.records[object.index] = pages::Page {
118 +
        backing: object, base: 0x90000000, count: 2, origin: authority.object, handles: 1,
119 +
    };
120 +
    let published = try! slots::commit(&mut PAGES.slots[..], pending);
121 +
    let memory = try! capability::install(&mut child.memory.table, capability::Entry {
122 +
        kind: abi::Kind::Page, object: published, rights: abi::Rights(abi::READ | abi::WRITE),
123 +
    });
124 +
    let start = abi::ContextStart { entry: 0x80000000, stack: 0x90002000, args: 0x90000000, size: 16 };
125 +
    let count = PAGES.backings.pool.count;
126 +
    set PAGES.backings.pool.count = 0;
127 +
    let mut exhausted = false;
128 +
    try domains::contextCreate(&mut DOMAINS, &mut PAGES, &PACKAGES, &TABLE, handle, start) catch error {
129 +
        try testing::expect(error == abi::Error::OutOfMemory); set exhausted = true;
130 +
    };
131 +
    try testing::expect(exhausted and DOMAINS.contextSlots[child.initial.index + 1].state == slots::State::Free);
132 +
    set PAGES.backings.pool.count = count;
133 +
    let context = try! domains::contextCreate(&mut DOMAINS, &mut PAGES, &PACKAGES, &TABLE, handle, start);
134 +
    let created = try! domains::context(&DOMAINS, authority.object, context);
135 +
    let initial = DOMAINS.contexts[child.initial.index];
136 +
    try testing::expect(context <> child.initial and created.owner == authority.object);
137 +
    try testing::expect(created.frame.registers[3] == initial.frame.registers[3]);
138 +
    try testing::expect(created.kernelStack.start >= initial.kernelStack.end or created.kernelStack.end <= initial.kernelStack.start);
139 +
    try testing::expect(created.frame.pc == start.entry and created.frame.registers[2] == start.stack);
140 +
    try testing::expect(created.frame.registers[10] == start.args and created.frame.registers[11] == start.size);
141 +
    try testing::expect(created.userStack.start == 0x90000000 and created.userStack.end == start.stack);
142 +
    let mut invalid = start;
143 +
    set invalid.entry += 4;
144 +
    let mut rejected: u32 = 0;
145 +
    try domains::contextCreate(&mut DOMAINS, &mut PAGES, &PACKAGES, &TABLE, handle, invalid) catch error {
146 +
        try testing::expect(error == abi::Error::InvalidArg); set rejected += 1;
147 +
    };
148 +
    set invalid = start;
149 +
    set invalid.stack -= 1;
150 +
    try domains::contextCreate(&mut DOMAINS, &mut PAGES, &PACKAGES, &TABLE, handle, invalid) catch error {
151 +
        try testing::expect(error == abi::Error::InvalidArg); set rejected += 1;
152 +
    };
153 +
    set invalid = start;
154 +
    set invalid.size = 0xffffffffffffffff;
155 +
    try domains::contextCreate(&mut DOMAINS, &mut PAGES, &PACKAGES, &TABLE, handle, invalid) catch error {
156 +
        try testing::expect(error == abi::Error::InvalidArg); set rejected += 1;
157 +
    };
158 +
    try testing::expect(rejected == 3 and DOMAINS.contextSlots[context.index + 1].state == slots::State::Free);
159 +
    try domains::contextCreate(&mut DOMAINS, &mut PAGES, &PACKAGES, &TABLE, handle, start) catch error {
160 +
        try testing::expect(error == abi::Error::Busy); set rejected += 1;
161 +
    };
162 +
    try testing::expect(rejected == 4 and DOMAINS.contextSlots[context.index + 1].state == slots::State::Free);
163 +
    try domains::activate(&mut DOMAINS, &PAGES, &TABLE, handle, start.stack, 0, 0) catch error {
164 +
        try testing::expect(error == abi::Error::Busy); set rejected += 1;
165 +
    };
166 +
    try testing::expect(rejected == 5 and DOMAINS.records[authority.object.index].state == domains::Lifecycle::Pending);
167 +
    let other = try! domains::create(&mut DOMAINS, &mut PAGES.backings, &PACKAGES, &mut TABLE, abi::Handle(0), IMAGE);
168 +
    let otherEntry = try! capability::get(&TABLE, other);
169 +
    let mut receiver = try! domains::get(&DOMAINS, otherEntry.object);
170 +
    let shared = try! capability::install(&mut receiver.memory.table, capability::Entry {
171 +
        kind: abi::Kind::Page, object: published, rights: abi::Rights(abi::WRITE),
172 +
    });
173 +
    try domains::activate(&mut DOMAINS, &PAGES, &TABLE, other, start.stack, 0, 0) catch error {
174 +
        try testing::expect(error == abi::Error::Busy); set rejected += 1;
175 +
    };
176 +
    try testing::expect(rejected == 6 and DOMAINS.records[otherEntry.object.index].state == domains::Lifecycle::Pending);
177 +
    try domains::context(&DOMAINS, otherEntry.object, context) catch error {
178 +
        try testing::expect(error == abi::Error::BadHandle); set rejected += 1;
179 +
    };
180 +
    try domains::context(&DOMAINS, authority.object, abi::Ref { index: context.index, generation: context.generation + 1 }) catch error {
181 +
        try testing::expect(error == abi::Error::BadHandle); set rejected += 1;
182 +
    };
183 +
    try testing::expect(rejected == 8);
184 +
    try domains::contextDestroy(&mut DOMAINS, &mut PAGES, &TABLE, handle, child.initial) catch error {
185 +
        try testing::expect(error == abi::Error::Denied); set rejected += 1;
186 +
    };
187 +
    set DOMAINS.contexts[context.index].hart = 0;
188 +
    try domains::contextDestroy(&mut DOMAINS, &mut PAGES, &TABLE, handle, context) catch error {
189 +
        try testing::expect(error == abi::Error::Busy); set rejected += 1;
190 +
    };
191 +
    try! domains::returned(&mut DOMAINS, authority.object, context);
192 +
    try testing::expect(DOMAINS.contexts[context.index].state == domains::ContextState::Stopped);
193 +
    try testing::expect(DOMAINS.records[authority.object.index].state == domains::Lifecycle::Pending);
194 +
    set DOMAINS.contexts[context.index].hart = nil;
195 +
    try! domains::contextDestroy(&mut DOMAINS, &mut PAGES, &TABLE, handle, context);
196 +
    for i in created.kernelFrames.first..created.kernelFrames.first + created.kernelFrames.count {
197 +
        try testing::expect(PAGES.backings.pool.free[i]);
198 +
    }
199 +
    try domains::context(&DOMAINS, authority.object, context) catch error {
200 +
        try testing::expect(error == abi::Error::BadHandle); set rejected += 1;
201 +
    };
202 +
    let next = try! domains::contextCreate(&mut DOMAINS, &mut PAGES, &PACKAGES, &TABLE, handle, start);
203 +
    try testing::expect(next.index == context.index and next.generation <> context.generation);
204 +
    try! domains::returned(&mut DOMAINS, authority.object, child.initial);
205 +
    try testing::expect(DOMAINS.records[authority.object.index].state == domains::Lifecycle::Dead);
206 +
    try testing::expect(DOMAINS.contexts[next.index].state == domains::ContextState::Stopped);
207 +
    try! domains::contextDestroy(&mut DOMAINS, &mut PAGES, &TABLE, handle, next);
208 +
    try testing::expect(rejected == 11);
209 +
}
210 +
106 211
/// Creation installs only Events in the child and preserves selected authorizing rights.
107 212
@test unsafe fn pending() throws (testing::TestError) {
108 213
    initialize(abi::CREATE | abi::ALLOCATE);
109 214
    let handle = try! domains::create(&mut DOMAINS, &mut BACKINGS, &PACKAGES, &mut TABLE, abi::Handle(0), IMAGE);
110 215
    let authority = try! capability::get(&TABLE, handle);
test/dispatch/kernel/dispatchcheck.rad +17 -4
60 60
        }
61 61
    }
62 62
    let cap = try! capability::get(&TABLE, HANDLES[0]);
63 63
    let mut user = try! domains::get(&domains::STORE, cap.object);
64 64
    let selfAuthority = try! capability::install(&mut user.memory.table, capability::Entry {
65 -
        kind: abi::Kind::Domain, object: cap.object, rights: abi::Rights(abi::CREATE | abi::ALLOCATE),
65 +
        kind: abi::Kind::Domain, object: cap.object, rights: abi::Rights(abi::CREATE | abi::ALLOCATE | abi::EXECUTE | abi::WAKE | abi::DESTROY),
66 66
    });
67 67
    let root = user.image;
68 68
    let executable = try! registry::install(&registry::STORE, &mut user.memory.table, root);
69 69
    let stack = try! pages::allocate(&mut pages::STORE, &mut TABLE, authority, 1);
70 70
    let granted = try! pages::grant(&mut pages::STORE, &TABLE, &mut user.memory.table, stack, (abi::READ | abi::WRITE) as u64);
71 71
    let stackCap = try! capability::get(&TABLE, stack);
72 72
    let stackPage = try! pages::get(&pages::STORE, stackCap.object);
73 -
    let args = @sliceOf(memory(stackPage.base), 24);
73 +
    let args = @sliceOf(memory(stackPage.base), 32);
74 74
    let words = [*granted, *executable, *selfAuthority];
75 75
    for word in 0..3 {
76 76
        for byte in 0..8 { set args[word * 8 + byte] = (words[word] >> (byte as u64 * 8)) as u8; }
77 77
    }
78 -
    try! domains::activate(&mut domains::STORE, &pages::STORE, &TABLE, HANDLES[0], stackPage.base + 4096, stackPage.base, 24);
78 +
    try! domains::activate(&mut domains::STORE, &pages::STORE, &TABLE, HANDLES[0], stackPage.base + 4096, stackPage.base, 32);
79 79
    let imageRef = try! registry::image(&registry::STORE, &TABLE, image, abi::Rights(abi::EXECUTE));
80 80
    let target = try! registry::exported(&registry::STORE, imageRef, &"spin::count"[..]);
81 81
    let case shared::Target::Data(data) = target else panic "counter";
82 82
    let timer = try! dispatch::timer(&boot::PLATFORM, 0);
83 83
    let clock = dispatch::now(timer);
84 84
    let start = clock + 1000000;
85 -
    let span: u64 = 1000000;
85 +
    let span: u64 = 4000000;
86 86
    set domains::STORE.contexts[CONTEXTS[1].index].frame.pc = worker();
87 87
    set domains::STORE.contexts[CONTEXTS[1].index].frame.registers[10] = user.graph.table[data.slot] + data.offset as u64;
88 88
    set domains::STORE.contexts[CONTEXTS[1].index].frame.registers[11] = timer.clock;
89 89
    set domains::STORE.contexts[CONTEXTS[1].index].frame.registers[12] = start + 3 * span;
90 90
    set domains::STORE.contexts[CONTEXTS[1].index].frame.registers[13] = start + 4 * span;
99 99
    for i in 0..6 {
100 100
        if i == 1 { continue; }
101 101
        let owner = owners[i];
102 102
        let handle = try! budgets::bind(&mut budgets::STORE, &domains::STORE, &mut TABLE,
103 103
            budgets::Binding { budget: windows[i], domain: HANDLES[owner], context: CONTEXTS[owner] }, clock);
104 +
        if i == 3 {
105 +
            for byte in 0..8 { set args[24 + byte] = (*handle >> (byte as u64 * 8)) as u8; }
106 +
        }
104 107
    }
105 108
}
106 109
107 110
/// Check state captured by production timer handling after both resumptions.
108 111
export unsafe fn verify() {
109 112
    let user = domains::STORE.contexts[CONTEXTS[0].index].frame;
110 113
    let machine = domains::STORE.contexts[CONTEXTS[1].index].frame;
111 114
    assert (user.status & 0x1800) == 0 and (machine.status & 0x1800) == 0x1800;
112 115
    assert machine.registers[8] > 0 and machine.registers[9] == 1 and machine.registers[18] > 0;
116 +
    let domain = try! domains::get(&domains::STORE, domains::STORE.contexts[CONTEXTS[0].index].owner);
117 +
    let target = try! registry::exported(&registry::STORE, domain.image, &"spin::extraCount"[..]);
118 +
    let case shared::Target::Data(data) = target else panic "extra counter";
119 +
    let bytes = @sliceOf(memory(domain.graph.table[data.slot] + data.offset as u64), 8);
120 +
    let mut progress: u64 = 0;
121 +
    for byte in 0..8 { set progress |= bytes[byte] as u64 << (byte as u64 * 8); }
122 +
    assert progress > 0;
123 +
    let checked = try! registry::exported(&registry::STORE, domain.image, &"spin::destroyed"[..]);
124 +
    let case shared::Target::Data(flag) = checked else panic "destruction flag";
125 +
    assert *memory(domain.graph.table[flag.slot] + flag.offset as u64) == 1;
113 126
    finish();
114 127
}
test/dispatch/machine.ras +21 -5
2 2
.text;
3 3
    call @kernel::boot::initialize;
4 4
    call @kernel::dispatchcheck::setup;
5 5
    call @kernel::boot::run;
6 6
@fail
7 +
    li %a0 1;
8 +
    j @failure;
9 +
@late
10 +
    li %a0 2;
11 +
    j @failure;
12 +
@noProgress
13 +
    li %a0 3;
14 +
    j @failure;
15 +
@noResumption
16 +
    li %a0 4;
17 +
    j @failure;
18 +
@stackCorruption
19 +
    li %a0 5;
20 +
@failure
7 21
    li %t0 0x10001000;
8 -
    li %t1 0x13333;
22 +
    slli %t1 %a0 16;
23 +
    li %t2 0x3333;
24 +
    or %t1 %t1 %t2;
9 25
    sw %t1 0(%t0);
10 26
.export @kernel::dispatchcheck::memory;
11 27
.export @kernel::dispatchcheck::kernelGp;
12 28
.export @kernel::dispatchcheck::worker;
13 29
.export @kernel::dispatchcheck::completion;
23 39
@kernel::dispatchcheck::completion
24 40
    la %a0 @complete;
25 41
    ret;
26 42
@machineWork
27 43
    ld %t0 0(%a1);
28 -
    bgeu %t0 %a2 @fail;
44 +
    bgeu %t0 %a2 @late;
29 45
    ld %s2 0(%a0);
30 -
    beqz %s2 @fail;
46 +
    beqz %s2 @noProgress;
31 47
    addi %sp %sp -16;
32 48
    li %s3 1234567;
33 49
    sd %s3 0(%sp);
34 50
    li %s0 0;
35 51
    li %s1 0;
36 52
@workLoop
37 53
    addi %s0 %s0 1;
38 54
    ld %t0 0(%sp);
39 -
    bne %t0 %s3 @fail;
55 +
    bne %t0 %s3 @stackCorruption;
40 56
    ld %t0 0(%a1);
41 57
    bltu %t0 %a3 @workLoop;
42 58
    ld %t0 0(%a0);
43 -
    bgeu %s2 %t0 @fail;
59 +
    bgeu %s2 %t0 @noResumption;
44 60
    li %s1 1;
45 61
    j @workLoop;
46 62
@complete
47 63
    call @kernel::dispatchcheck::verify;
48 64
    j @fail;
test/dispatch/run +1 -1
20 20
printf '];\n' >> "$work/kernel/dispatchinput.rad"
21 21
sh test/acceptance/compile "$emulator" "$work"
22 22
cat test/dispatch/machine.ras kernel/kernel/*.ras > "$work/dispatch.ras"
23 23
"$emulator" -memory-size=385024 -data-size=348160 -stack-size=512 -run bin/kernel.build.rv64 \
24 24
    -- "$work/std.ril" "$work/kernel.ril" "$work/dispatch.ras" "$work/dispatch.rv64"
25 -
"$emulator" -machine -memory-size=262144 -max-steps=80000000 -run "$work/dispatch.rv64"
25 +
"$emulator" -machine -memory-size=262144 -max-steps=200000000 -run "$work/dispatch.rv64"
26 26
printf 'dispatch: user calls, preemption, idle gap, and retained machine continuation passed\n'
test/dispatch/spin.rad +42 -1
9 9
    stack: u64,
10 10
    /// Local capability for the resident user package.
11 11
    image: u64,
12 12
    /// Local self-Domain capability with allocation authority.
13 13
    domain: u64,
14 +
    /// Future window that the user can partition between its contexts.
15 +
    future: u64,
14 16
}
15 17
16 18
/// Startup environment supplied by the kernel.
17 19
record Env: Copy {
18 20
    /// Readable startup capabilities.
26 28
}
27 29
/// Private query output used to check writable package-state authority.
28 30
static INFO: [u64; 2] = [0; 2];
29 31
/// Private progress counter observed by the machine fixture.
30 32
export static count: u64 = 0;
33 +
/// Additional-context progress in the same private package state.
34 +
export static extraCount: u64 = 0;
35 +
/// Successful user-side return and destruction checks.
36 +
export static destroyed: u64 = 0;
37 +
38 +
/// Update shared private state and return from an additional context.
39 +
export fn worker(args: *opaque, size: u64) {
40 +
    assert size == 0;
41 +
    set extraCount += 1;
42 +
    set count += 1;
43 +
}
31 44
/// Check direct calls, then consume each window without yielding.
32 45
@default unsafe fn main(env: *Env) {
33 -
    assert env.argsSize == 24;
46 +
    if env.argsSize == 0 { return; }
47 +
    assert env.argsSize == 32;
34 48
    let handle = env.argsPointer.stack as i64;
35 49
    let info = sys::queryPage(abi::Handle(handle as u64));
36 50
    assert info.base <> 0 and info.count == 1;
37 51
    let mut rejected = false;
38 52
    try sys::pageAllocate(abi::Handle(0), 0) catch error {
70 84
    assert rejected;
71 85
    let current = sys::currentContext();
72 86
    assert current.context <> 0 and current.hart == 0 and current.budget <> 0;
73 87
    let window = try! sys::queryBudget(abi::Handle(current.budget));
74 88
    assert window.context == current.context and window.hart == current.hart and window.start < window.end;
89 +
    let extraStack = try! sys::pageAllocate(abi::Handle(0), 1);
90 +
    let extraMemory = sys::queryPage(extraStack);
91 +
    let extra = try! sys::contextCreate(abi::Handle(env.argsPointer.domain), abi::ContextStart {
92 +
        entry: worker as u64, stack: extraMemory.base + 4096, args: 0, size: 0,
93 +
    });
94 +
    let context = try! sys::queryContext(abi::Handle(env.argsPointer.domain), extra);
95 +
    assert context.context == extra and context.domain == domain.id;
96 +
    assert context.stackBase == extraMemory.base and context.stackTop == extraMemory.base + 4096;
97 +
    let future = abi::Handle(env.argsPointer.future);
98 +
    let later = try! sys::queryBudget(future);
99 +
    let tail = try! sys::budgetSplit(future, later.start + (later.end - later.start) / 2);
100 +
    let childWindow = try! sys::budgetSplit(tail, later.start + (later.end - later.start) * 3 / 4);
101 +
    let resumption = try! sys::budgetSplit(childWindow, later.start + (later.end - later.start) * 7 / 8);
102 +
    let bound = try! sys::budgetBind(tail, abi::Handle(env.argsPointer.domain), extra);
103 +
    let initial = try! sys::queryContext(child, 0);
104 +
    let childBound = try! sys::budgetBind(childWindow, child, initial.context);
75 105
    let mut local: [u64; 2] = [0; 2];
76 106
    assert ecall(44, handle, (&mut local[0]) as i64, 16, 0) == 0;
77 107
    assert local[0] <> 0 and local[1] == 1;
78 108
    assert ecall(44, handle, (&mut INFO[0]) as i64, 16, 0) == 0;
79 109
    assert INFO[0] == local[0] and INFO[1] == local[1];
80 110
    assert ecall(44, handle, 0, 16, 0) == -4;
81 111
    assert ecall(0xffffffff, 0, 0, 0, 0) == -4;
112 +
    while extraCount == 0 { set count += 1; }
113 +
    let stopped = try! sys::queryContext(abi::Handle(env.argsPointer.domain), extra);
114 +
    assert stopped.state == 1 and stopped.hart == 0xffffffffffffffff;
115 +
    try! sys::contextDestroy(abi::Handle(env.argsPointer.domain), extra);
116 +
    set rejected = false;
117 +
    try sys::queryContext(abi::Handle(env.argsPointer.domain), extra) catch error {
118 +
        assert error == abi::Error::BadHandle;
119 +
        set rejected = true;
120 +
    };
121 +
    assert rejected and sys::queryDomain(child).state == 2;
122 +
    set destroyed = 1;
82 123
    while true { set count += 1; }
83 124
}