kernel: Load trusted binary RIL

5400cd48daabedb5cbc482265b727b02378879b1a153607cf7f3717c0a03c4dc
Snapshot readable Page input, compile through the shared RV64 backend,
retain native code and catalog metadata, and publish Image handles
with rollback on failure. Verify hosted and runtime-loaded execution
after compiler workspace release.

Align small aggregate return loads exposed by native loader execution
while preserving the return ABI.

Assisted-by: Codex:gpt-6
Alexis Sellier committed ago 1 parent 3d8abc9f
kernel/kernel.rad +1 -0
15 15
export mod events;
16 16
export mod registry;
17 17
export mod frames;
18 18
export mod backing;
19 19
export mod pages;
20 +
export mod loader;
20 21
export mod boot;
21 22
@test export mod tests;
kernel/kernel/loader.rad added +299 -0
1 +
//! Bounded trusted-RIL loading and unpublished physical-memory ownership.
2 +
3 +
use std::mem;
4 +
use std::lang::alloc;
5 +
use std::lang::gen::data;
6 +
use std::lang::il::binary;
7 +
use std::lang::il::binary::program;
8 +
use std::arch::rv64;
9 +
use std::arch::rv64::shared;
10 +
use std::arch::rv64::shared::catalog;
11 +
use super::abi;
12 +
use super::limits;
13 +
use super::slots;
14 +
use super::frames;
15 +
use super::pages;
16 +
use super::backing;
17 +
use super::capability;
18 +
use super::registry;
19 +
use super::sync;
20 +
21 +
/// Maximum snapshotted binary RIL size.
22 +
export constant INPUT_BYTES: u32 = 8388608;
23 +
/// Maximum native output extent per load.
24 +
constant CODE_BYTES: u32 = 8388608;
25 +
/// Maximum persistent source and metadata extent per load.
26 +
constant METADATA_BYTES: u32 = 16777216;
27 +
28 +
/// Temporary compiler storage owned by one load.
29 +
export record Workspace: Copy {
30 +
    /// Stable input bytes used throughout decoding and compilation.
31 +
    input: [u8; INPUT_BYTES],
32 +
    /// Decoded binary program and names.
33 +
    decoded: [u8; 67108864],
34 +
    /// Native emitter state and generated instructions.
35 +
    code: [u8; 16777216],
36 +
    /// Reusable per-function storage.
37 +
    scratch: [u8; 16777216],
38 +
    /// Native data layout workspace.
39 +
    data: [data::DataSym; 4096],
40 +
    /// Local package symbols.
41 +
    symbols: [shared::Symbol; 4096],
42 +
    /// Public package symbols.
43 +
    exports: [shared::Symbol; 4096],
44 +
    /// Admitted dependency symbols.
45 +
    imports: [shared::Symbol; 8192],
46 +
    /// Initialized private-state template.
47 +
    template: [u8; 1048576],
48 +
    /// Private-state pointer relocations.
49 +
    relocations: [shared::Relocation; 4096],
50 +
}
51 +
52 +
/// Physical storage retained by one runtime-loaded package.
53 +
export record Resident: Copy {
54 +
    /// Executable frames, including final-page padding.
55 +
    code: frames::Run,
56 +
    /// Packed catalog entry and retained source bytes.
57 +
    metadata: frames::Run,
58 +
}
59 +
60 +
/// Single-loader ownership and persistent frame accounting.
61 +
export record State: Copy {
62 +
    /// Workspace occupancy protected by the caller's metadata serialization.
63 +
    busy: bool,
64 +
    /// Domain generation holding the workspace while busy is true.
65 +
    owner: abi::Ref,
66 +
    /// True for package slots whose frames are owned by this loader.
67 +
    live: [bool; limits::PACKAGES],
68 +
    /// Physical extents for live runtime-loaded packages.
69 +
    resident: [Resident; limits::PACKAGES],
70 +
}
71 +
72 +
/// Arguments supplied to a trusted image load.
73 +
export record Request: Copy {
74 +
    /// Self-Domain allocation authority, including the zero sentinel.
75 +
    authority: abi::Handle,
76 +
    /// Readable page containing the binary RIL bytes.
77 +
    source: abi::Handle,
78 +
    /// Byte offset within the source page.
79 +
    offset: u64,
80 +
    /// Number of source bytes to snapshot.
81 +
    length: u64,
82 +
}
83 +
84 +
/// Workspace capacity that must be returned to the same frame pool.
85 +
export union Lease: Once {
86 +
    /// Private temporary frames owned by the active load.
87 +
    Held(frames::Run),
88 +
}
89 +
90 +
/// Global runtime-loader ownership.
91 +
export unsafe static STATE: State = State { busy: false, owner: abi::Ref { index: 0, generation: 0 },
92 +
    live: [false; limits::PACKAGES], resident: undefined };
93 +
94 +
/// Map a validated physical workspace extent to its compiler storage.
95 +
/// The caller must hold the exclusive workspace lease until the last access.
96 +
unsafe fn workspace(address: u64) -> *mut Workspace;
97 +
/// Map a validated physical byte extent.
98 +
fn memory(address: u64) -> *unsafe mut u8;
99 +
/// Read the native descriptor at a successfully packed metadata extent.
100 +
fn descriptor(address: u64) -> *unsafe catalog::Entry;
101 +
102 +
/// Reset loader metadata before any workspace or resident allocation exists.
103 +
export fn initialize(state: &mut State) {
104 +
    set state.busy = false;
105 +
    set state.owner = abi::Ref { index: 0, generation: 0 };
106 +
    for i in 0..limits::PACKAGES {
107 +
        set state.live[i] = false;
108 +
    }
109 +
}
110 +
111 +
/// Acquire the sole compiler workspace and reserve its private physical storage.
112 +
/// The caller serializes workspace ownership and frame-pool mutations.
113 +
export fn acquire(state: &mut State, pool: &mut frames::Pool, owner: abi::Ref) -> Lease throws (abi::Error) {
114 +
    if owner.index >= limits::DOMAINS or owner.generation == 0 {
115 +
        throw abi::Error::BadHandle;
116 +
    }
117 +
    if state.busy {
118 +
        throw abi::Error::Busy;
119 +
    }
120 +
    let count = (@sizeOf(Workspace) + 4095) / 4096;
121 +
    let reservation = try frames::reserve(pool, count);
122 +
    set state.busy = true;
123 +
    set state.owner = owner;
124 +
    return Lease::Held(frames::commit(reservation));
125 +
}
126 +
127 +
/// Return temporary frames and release the compiler workspace under metadata serialization.
128 +
export fn release(state: &mut State, pool: &mut frames::Pool, lease: Lease) {
129 +
    match lease {
130 +
        case Lease::Held(run) => {
131 +
            try! frames::release(pool, run);
132 +
        },
133 +
    }
134 +
    set state.owner = abi::Ref { index: 0, generation: 0 };
135 +
    set state.busy = false;
136 +
}
137 +
138 +
/// Resolve the private workspace while its lease remains held.
139 +
unsafe fn storage(pool: &frames::Pool, lease: &Lease) -> *mut Workspace {
140 +
    match lease {
141 +
        case Lease::Held(run) => {
142 +
            let extent = try! frames::extent(pool, *run);
143 +
            assert extent.end - extent.start >= @sizeOf(Workspace) as u64;
144 +
            return workspace(extent.start);
145 +
        },
146 +
    }
147 +
}
148 +
149 +
/// Validate allocation authority and a readable source range before taking resources.
150 +
fn source(store: &pages::Store, table: &capability::Table, request: Request) -> u64 throws (abi::Error) {
151 +
    let authority = try capability::authority(table, request.authority, abi::Rights(abi::ALLOCATE));
152 +
    if authority.object <> table.owner {
153 +
        throw abi::Error::Denied;
154 +
    }
155 +
    if not backing::domainLive(&store.backings, table.owner) {
156 +
        throw abi::Error::BadHandle;
157 +
    }
158 +
    let entry = try capability::lookup(table, request.source, abi::Kind::Page, abi::Rights(abi::READ));
159 +
    let page = try pages::get(store, entry.object);
160 +
    let bytes = page.count as u64 * limits::FRAME_SIZE;
161 +
    if request.length == 0 or request.offset > bytes or request.length > bytes - request.offset {
162 +
        throw abi::Error::InvalidArg;
163 +
    }
164 +
    if request.length > INPUT_BYTES as u64 {
165 +
        throw abi::Error::Exhausted;
166 +
    }
167 +
    return page.base + request.offset;
168 +
}
169 +
170 +
/// Translate bounded backend storage failures without validating trusted IL semantics.
171 +
fn backend(error: shared::Error) -> abi::Error {
172 +
    match error {
173 +
        case shared::Error::Capacity => return abi::Error::Exhausted,
174 +
        case shared::Error::Codegen(inner) => {
175 +
            if inner == rv64::Error::Allocation or inner == rv64::Error::Capacity {
176 +
                return abi::Error::Exhausted;
177 +
            }
178 +
            return abi::Error::VerifyFailed;
179 +
        },
180 +
        else => return abi::Error::VerifyFailed,
181 +
    }
182 +
}
183 +
184 +
/// Used page counts for unpublished native output.
185 +
record OutputSize: Copy {
186 +
    /// Pages occupied by generated code.
187 +
    code: u32,
188 +
    /// Pages occupied by the packed native catalog.
189 +
    metadata: u32,
190 +
}
191 +
192 +
/// Compile and pack one candidate into caller-owned physical output extents.
193 +
unsafe fn generate(work: *mut Workspace, input: &binary::Package, packages: &registry::Store,
194 +
    slot: u32, code: u64, metadata: u64, length: u32) -> OutputSize throws (abi::Error)
195 +
{
196 +
    let count = try registry::imports(packages, &input.dependencies[..], &mut work.imports[..]);
197 +
    let mut arena = alloc::new(&mut work.code[..]);
198 +
    let mut scratch = alloc::new(&mut work.scratch[..]);
199 +
    let package = try shared::compile(&*input, slot, code, &work.imports[..count],
200 +
        shared::Storage {
201 +
            data: &mut work.data[..], symbols: &mut work.symbols[..], exports: &mut work.exports[..],
202 +
            template: &mut work.template[..], relocations: &mut work.relocations[..],
203 +
        }, &mut arena, &mut scratch) catch error {
204 +
            throw backend(error);
205 +
        };
206 +
    if package.code.len > CODE_BYTES / 4 {
207 +
        throw abi::Error::Exhausted;
208 +
    }
209 +
    let codeBytes = package.code.len * 4;
210 +
    let codeMemory = @sliceOf(memory(code), CODE_BYTES);
211 +
    try! mem::copy(&mut codeMemory[..codeBytes], @sliceOf(package.code.ptr as *u8, codeBytes));
212 +
    let metadataMemory = @sliceOf(memory(metadata), METADATA_BYTES);
213 +
    let used = try catalog::pack(&[catalog::Entry { source: &work.input[..length], package }], metadata, metadataMemory)
214 +
        catch error {
215 +
            throw backend(error);
216 +
        };
217 +
    let mut codePages = (codeBytes + 4095) / 4096;
218 +
    if codePages == 0 {
219 +
        set codePages = 1;
220 +
    }
221 +
    return OutputSize { code: codePages, metadata: (used + 4095) / 4096 };
222 +
}
223 +
224 +
/// Return unused output frames while preserving at least one frame for each extent.
225 +
fn trim(pool: &mut frames::Pool, run: frames::Run, count: u32) -> frames::Run {
226 +
    assert count > 0 and count <= run.count;
227 +
    if count < run.count {
228 +
        try! frames::release(pool, frames::Run { first: run.first + count, count: run.count - count });
229 +
    }
230 +
    return frames::Run { first: run.first, count };
231 +
}
232 +
233 +
/// Snapshot, decode, compile, and publish a package while its caller remains live.
234 +
unsafe fn produce(state: &mut State, pool: &mut frames::Pool, packages: &mut registry::Store,
235 +
    work: *mut Workspace, address: u64, length: u32) -> abi::Ref throws (abi::Error)
236 +
{
237 +
    try! mem::copy(&mut work.input[..length], @sliceOf(memory(address), length));
238 +
    let mut decoder = alloc::new(&mut work.decoded[..]);
239 +
    let input = try program::decode(&work.input[..length], &mut decoder, binary::Limits { registers: 8192, blocks: 4096 }) catch err {
240 +
        if err == binary::Error::Storage {
241 +
            throw abi::Error::Exhausted;
242 +
        }
243 +
        throw abi::Error::VerifyFailed;
244 +
    };
245 +
    let existing = try registry::identify(packages, &input.name[..], &work.input[..length]);
246 +
    if let object = existing {
247 +
        return object;
248 +
    }
249 +
    let slot = try registry::reserve(packages);
250 +
    let object = slots::reference(&slot);
251 +
    let codeReservation = try frames::reserve(pool, CODE_BYTES / 4096) catch err {
252 +
        registry::cancel(packages, slot);
253 +
        throw err;
254 +
    };
255 +
    let code = frames::commit(codeReservation);
256 +
    let metadataReservation = try frames::reserve(pool, METADATA_BYTES / 4096) catch err {
257 +
        try! frames::release(pool, code); registry::cancel(packages, slot); throw err;
258 +
    };
259 +
    let metadata = frames::commit(metadataReservation);
260 +
    let codeRange = try! frames::extent(pool, code);
261 +
    let metadataRange = try! frames::extent(pool, metadata);
262 +
    let sizes = try generate(work, &input, packages, object.index, codeRange.start, metadataRange.start, length) catch err {
263 +
        try! frames::release(pool, metadata); try! frames::release(pool, code);
264 +
        registry::cancel(packages, slot); throw err;
265 +
    };
266 +
    let retainedCode = trim(pool, code, sizes.code);
267 +
    let retainedMetadata = trim(pool, metadata, sizes.metadata);
268 +
    let native = descriptor(metadataRange.start);
269 +
    sync::syncInstructions();
270 +
    let result = try registry::publish(packages, slot, native.source, native.package) catch err {
271 +
        try! frames::release(pool, retainedMetadata); try! frames::release(pool, retainedCode); throw err;
272 +
    };
273 +
    assert result == object;
274 +
    set state.resident[result.index] = Resident { code: retainedCode, metadata: retainedMetadata };
275 +
    set state.live[result.index] = true;
276 +
    return result;
277 +
}
278 +
279 +
/// Load trusted RIL and install an Image capability after complete native publication.
280 +
/// The caller keeps its domain alive and serializes shared metadata transactions.
281 +
export unsafe fn load(state: &mut State, store: &mut pages::Store, packages: &mut registry::Store,
282 +
    table: &mut capability::Table, request: Request) -> abi::Handle throws (abi::Error)
283 +
{
284 +
    let address = try source(store, table, request);
285 +
    let handle = try slots::reserve(&mut table.slots[..]);
286 +
    let lease = try acquire(state, &mut store.backings.pool, table.owner) catch err {
287 +
        try! slots::cancel(&mut table.slots[..], handle); throw err;
288 +
    };
289 +
    let work = storage(&store.backings.pool, &lease);
290 +
    let object = try produce(state, &mut store.backings.pool, packages, work, address, request.length as u32) catch err {
291 +
        release(state, &mut store.backings.pool, lease);
292 +
        try! slots::cancel(&mut table.slots[..], handle); throw err;
293 +
    };
294 +
    let result = capability::publish(table, handle, capability::Entry {
295 +
        kind: abi::Kind::Image, object, rights: abi::Rights(registry::IMAGE_RIGHTS),
296 +
    });
297 +
    release(state, &mut store.backings.pool, lease);
298 +
    return result;
299 +
}
kernel/kernel/loader.ras added +17 -0
1 +
//! Physical views of validated loader-owned extents.
2 +
.text;
3 +
.export @kernel::loader::workspace;
4 +
.export @kernel::loader::memory;
5 +
.export @kernel::loader::descriptor;
6 +
7 +
// A validated workspace run contains one complete aligned Workspace record.
8 +
@kernel::loader::workspace
9 +
    ret;
10 +
11 +
// A validated physical extent supplies the caller's mapped byte view.
12 +
@kernel::loader::memory
13 +
    ret;
14 +
15 +
// Successful packing initializes one native catalog entry at this address.
16 +
@kernel::loader::descriptor
17 +
    ret;
kernel/kernel/tests.rad +1 -0
11 11
export mod backing;
12 12
export mod pages;
13 13
export mod transactions;
14 14
export mod events;
15 15
export mod registry;
16 +
export mod loader;
kernel/kernel/tests/loader.rad added +213 -0
1 +
//! Trusted binary loading, resident snapshots, and resource rollback.
2 +
3 +
use std::testing;
4 +
use std::arch::rv64::shared;
5 +
use std::lang::il;
6 +
use std::lang::il::binary;
7 +
use std::lang::il::binary::program;
8 +
use kernel::abi;
9 +
use kernel::limits;
10 +
use kernel::slots;
11 +
use kernel::pages;
12 +
use kernel::frames;
13 +
use kernel::backing;
14 +
use kernel::capability;
15 +
use kernel::registry;
16 +
use kernel::loader;
17 +
18 +
/// Mapped frames for temporary compiler storage and persistent native output.
19 +
static RAM: [u64; 18874368] = [0; 18874368];
20 +
/// Physical frame and page metadata.
21 +
unsafe static PAGES: pages::Store = undefined;
22 +
/// Resident package registry.
23 +
unsafe static PACKAGES: registry::Store = undefined;
24 +
/// Loader ownership and resident-frame accounting.
25 +
unsafe static LOADER: loader::State = undefined;
26 +
/// Calling domain's capability table.
27 +
unsafe static TABLE: capability::Table = undefined;
28 +
/// Source page allocated by the calling domain.
29 +
static SOURCE: abi::Handle = abi::Handle(0);
30 +
/// Byte offset of the first aligned test frame.
31 +
static OFFSET: u32 = 0;
32 +
33 +
/// Initialize a live domain and one source page over actual mapped memory.
34 +
unsafe fn initialize() {
35 +
    slots::initialize(&mut PAGES.slots[..]);
36 +
    backing::initialize(&mut PAGES.backings);
37 +
    registry::initialize(&mut PACKAGES);
38 +
    loader::initialize(&mut LOADER);
39 +
    let pointer = &RAM[0];
40 +
    let base = pointer as u64;
41 +
    let aligned = (base + 4095) & ~4095;
42 +
    set OFFSET = (aligned - base) as u32;
43 +
    set PAGES.backings.pool.count = (@sizeOf([u64; 18874368]) - OFFSET) / 4096;
44 +
    for i in 0..PAGES.backings.pool.count {
45 +
        set PAGES.backings.pool.addresses[i] = aligned + i as u64 * 4096;
46 +
        set PAGES.backings.pool.free[i] = true;
47 +
    }
48 +
    let owner = abi::Ref { index: 0, generation: 1 };
49 +
    capability::initialize(&mut TABLE, owner);
50 +
    try! backing::registerDomain(&mut PAGES.backings, owner);
51 +
    let authority = try! capability::install(&mut TABLE, capability::Entry {
52 +
        kind: abi::Kind::Domain, object: owner, rights: abi::Rights(abi::ALLOCATE),
53 +
    });
54 +
    set SOURCE = try! pages::allocate(&mut PAGES, &mut TABLE, abi::Handle(0), 1);
55 +
}
56 +
57 +
/// Encode one small function into the source page, optionally naming an absent export.
58 +
unsafe fn input(value: i64, badExport: bool) -> loader::Request {
59 +
    return inputWithDependencies(value, badExport, &[]);
60 +
}
61 +
62 +
/// Encode declared dependencies with a constant-return entry function.
63 +
unsafe fn inputWithDependencies(value: i64, badExport: bool, dependencies: *[*[u8]]) -> loader::Request {
64 +
    let mut instructions = [il::Instr::Ret { val: il::Val::Imm(value) }];
65 +
    let function = il::Fn {
66 +
        name: "loaded::main", params: &[], returnType: il::Type::W64, isExtern: false, isLeaf: true,
67 +
        blocks: &[il::Block { label: "entry", params: &[], instrs: &mut instructions[..], locs: &[], preds: &[], loopDepth: 0 }],
68 +
    };
69 +
    let mut name: *[u8] = "loaded::main";
70 +
    if badExport {
71 +
        set name = "missing";
72 +
    }
73 +
    let package = binary::Package {
74 +
        symbols: &["loaded", "loaded::main", "entry", "missing"], name: "loaded", dependencies: (&dependencies[..]) as *unsafe [*[u8]],
75 +
        exports: &[binary::Export { name, kind: binary::ExportKind::Function }], entry: "loaded::main",
76 +
        program: il::Program { data: &[], fns: &[&function] },
77 +
    };
78 +
    let bytes = @sliceOf(&mut RAM[0] as *mut u8, @sizeOf([u64; 18874368]));
79 +
    let length = try! program::encode(&mut bytes[OFFSET..OFFSET + 4096], &package);
80 +
    return loader::Request { authority: abi::Handle(0), source: SOURCE, offset: 0, length: length as u64 };
81 +
}
82 +
83 +
/// Count available frames to detect leaked temporary or unpublished output storage.
84 +
unsafe fn free() -> u32 {
85 +
    let mut count: u32 = 0;
86 +
    for i in 0..PAGES.backings.pool.count {
87 +
        if PAGES.backings.pool.free[i] {
88 +
            set count += 1;
89 +
        }
90 +
    }
91 +
    return count;
92 +
}
93 +
94 +
/// Resident source bytes survive mutation of the source page and workspace release.
95 +
@test unsafe fn residentSnapshot() throws (testing::TestError) {
96 +
    initialize();
97 +
    let request = input(43, false);
98 +
    let before = free();
99 +
    let handle = try! loader::load(&mut LOADER, &mut PAGES, &mut PACKAGES, &mut TABLE, request);
100 +
    let object = try! registry::image(&PACKAGES, &TABLE, handle, abi::Rights(abi::EXECUTE));
101 +
    let package = try! registry::get(&PACKAGES, object);
102 +
    try testing::expect(package.code.len > 0 and package.entry <> nil);
103 +
    let retained = LOADER.resident[object.index];
104 +
    try testing::expect(free() + retained.code.count + retained.metadata.count == before);
105 +
    try testing::expect(not LOADER.busy and LOADER.owner.generation == 0);
106 +
    let bytes = @sliceOf(&mut RAM[0] as *mut u8, @sizeOf([u64; 18874368]));
107 +
    let magic = bytes[OFFSET];
108 +
    set bytes[OFFSET] = 0;
109 +
    try testing::expect(PACKAGES.entries[object.index].source[0] == magic);
110 +
    set bytes[OFFSET] = magic;
111 +
    let duplicate = try! loader::load(&mut LOADER, &mut PAGES, &mut PACKAGES, &mut TABLE, request);
112 +
    try testing::expect(duplicate <> handle);
113 +
    try testing::expect(try! registry::image(&PACKAGES, &TABLE, duplicate, abi::Rights(0)) == object);
114 +
    try testing::expect(free() + retained.code.count + retained.metadata.count == before);
115 +
}
116 +
117 +
/// Decode and linking failures return all unpublished frames and capability capacity.
118 +
@test unsafe fn rollback() throws (testing::TestError) {
119 +
    initialize();
120 +
    let mut request = input(43, false);
121 +
    let before = free();
122 +
    set request.length = 3;
123 +
    let mut failures: u32 = 0;
124 +
    try loader::load(&mut LOADER, &mut PAGES, &mut PACKAGES, &mut TABLE, request) catch err {
125 +
        try testing::expect(err == abi::Error::VerifyFailed); set failures += 1;
126 +
    };
127 +
    set request = input(43, true);
128 +
    try loader::load(&mut LOADER, &mut PAGES, &mut PACKAGES, &mut TABLE, request) catch err {
129 +
        try testing::expect(err == abi::Error::VerifyFailed); set failures += 1;
130 +
    };
131 +
    try testing::expect(failures == 2 and free() == before and not LOADER.busy);
132 +
    try testing::expect(TABLE.slots[2].state == slots::State::Free and PACKAGES.slots[0].state == slots::State::Free);
133 +
    let valid = input(43, false);
134 +
    let handle = try! loader::load(&mut LOADER, &mut PAGES, &mut PACKAGES, &mut TABLE, valid);
135 +
    let object = try! registry::image(&PACKAGES, &TABLE, handle, abi::Rights(0));
136 +
    try testing::expect(object.index == 0);
137 +
}
138 +
139 +
/// Contention and workspace allocation failure preserve source authority and clear workspace occupancy.
140 +
@test unsafe fn workspaceOwnership() throws (testing::TestError) {
141 +
    initialize();
142 +
    let request = input(43, false);
143 +
    let before = free();
144 +
    let lease = try! loader::acquire(&mut LOADER, &mut PAGES.backings.pool, TABLE.owner);
145 +
    let mut busy = false;
146 +
    try loader::load(&mut LOADER, &mut PAGES, &mut PACKAGES, &mut TABLE, request) catch err {
147 +
        try testing::expect(err == abi::Error::Busy); set busy = true;
148 +
    };
149 +
    try testing::expect(busy and LOADER.busy and LOADER.owner == TABLE.owner);
150 +
    loader::release(&mut LOADER, &mut PAGES.backings.pool, lease);
151 +
    try testing::expect(free() == before);
152 +
    let count = PAGES.backings.pool.count;
153 +
    set PAGES.backings.pool.count = 32;
154 +
    let mut exhausted = false;
155 +
    try loader::load(&mut LOADER, &mut PAGES, &mut PACKAGES, &mut TABLE, request) catch err {
156 +
        try testing::expect(err == abi::Error::OutOfMemory); set exhausted = true;
157 +
    };
158 +
    set PAGES.backings.pool.count = count;
159 +
    try testing::expect(exhausted and free() == before and not LOADER.busy);
160 +
    try testing::expect(TABLE.slots[2].state == slots::State::Free);
161 +
}
162 +
163 +
/// Output allocation failures cancel registry admission and return compiler frames.
164 +
@test unsafe fn outputAllocationRollback() throws (testing::TestError) {
165 +
    initialize();
166 +
    let request = input(43, false);
167 +
    let before = free();
168 +
    let workspacePages = (@sizeOf(loader::Workspace) + 4095) / 4096;
169 +
    for extra in &[0 as u32, 2048] {
170 +
        let reservation = try! frames::reserve(&mut PAGES.backings.pool, before - workspacePages - extra);
171 +
        let held = frames::commit(reservation);
172 +
        let available = free();
173 +
        let mut failed = false;
174 +
        try loader::load(&mut LOADER, &mut PAGES, &mut PACKAGES, &mut TABLE, request) catch err {
175 +
            try testing::expect(err == abi::Error::OutOfMemory); set failed = true;
176 +
        };
177 +
        try testing::expect(failed and free() == available and not LOADER.busy);
178 +
        try testing::expect(TABLE.slots[2].state == slots::State::Free);
179 +
        try testing::expect(PACKAGES.slots[0].state == slots::State::Free);
180 +
        try! frames::release(&mut PAGES.backings.pool, held);
181 +
        try testing::expect(free() == before);
182 +
    }
183 +
    let handle = try! loader::load(&mut LOADER, &mut PAGES, &mut PACKAGES, &mut TABLE, request);
184 +
    let object = try! registry::image(&PACKAGES, &TABLE, handle, abi::Rights(0));
185 +
    try testing::expect(object.index == 0);
186 +
}
187 +
188 +
/// Admission rejects duplicate dependencies after compilation and returns native output.
189 +
@test unsafe fn publicationRollback() throws (testing::TestError) {
190 +
    initialize();
191 +
    let pending = try! registry::reserve(&mut PACKAGES);
192 +
    let reference = slots::reference(&pending);
193 +
    let dependency = shared::Package {
194 +
        name: "missing", dependencies: &[], slot: reference.index, codeAddress: 0,
195 +
        code: &[], exports: &[], entry: nil, template: &[], memory: 0,
196 +
        alignment: 8, relocations: &[],
197 +
    };
198 +
    let resident = try! registry::publish(&mut PACKAGES, pending, "dependency", dependency);
199 +
    let request = inputWithDependencies(43, false, &["missing", "missing"]);
200 +
    let before = free();
201 +
    let mut failed = false;
202 +
    try loader::load(&mut LOADER, &mut PAGES, &mut PACKAGES, &mut TABLE, request) catch err {
203 +
        try testing::expect(err == abi::Error::VerifyFailed); set failed = true;
204 +
    };
205 +
    try testing::expect(failed and free() == before and not LOADER.busy);
206 +
    try testing::expect(TABLE.slots[2].state == slots::State::Free);
207 +
    try testing::expect(PACKAGES.slots[1].state == slots::State::Free and not LOADER.live[1]);
208 +
    try testing::expect(slots::matches(&PACKAGES.slots[..], resident, slots::State::Live));
209 +
    let valid = inputWithDependencies(43, false, &["missing"]);
210 +
    let handle = try! loader::load(&mut LOADER, &mut PAGES, &mut PACKAGES, &mut TABLE, valid);
211 +
    let object = try! registry::image(&PACKAGES, &TABLE, handle, abi::Rights(0));
212 +
    try testing::expect(object.index == 1 and PACKAGES.counts[1] == 1);
213 +
}
test/acceptance/compile added +15 -0
1 +
#!/bin/sh
2 +
# Compile std and a staged kernel fixture from the repository root.
3 +
# Arguments: emulator path, fixture directory.
4 +
set -eu
5 +
emulator=$1
6 +
work=$2
7 +
set -- -pkg std
8 +
while IFS= read -r source; do set -- "$@" -mod "$source"; done < std.lib
9 +
set -- "$@" -pkg kernel -mod "$work/kernel.rad"
10 +
for source in "$work"/kernel/*.rad; do
11 +
    case "$source" in */tests.rad) continue ;; esac
12 +
    set -- "$@" -mod "$source"
13 +
done
14 +
"$emulator" -memory-size=385024 -data-size=348160 -stack-size=1024 \
15 +
    -run bin/radiance.rv64.dev "$@" -entry kernel -ril "$work"
test/boot/run +2 -2
2 2
# Boot the kernel through its production initialization on each supported hart count.
3 3
set -eu
4 4
emulator=${RAD_EMULATOR:-emulator}
5 5
work=$(mktemp -d)
6 6
trap 'rm -rf "$work"' EXIT HUP INT TERM
7 -
cat test/boot/machine.ras kernel/kernel/boot.ras kernel/kernel/sync.ras kernel/kernel/trap.ras kernel/kernel/pages.ras > "$work/boot.ras"
7 +
cat test/boot/machine.ras kernel/kernel/boot.ras kernel/kernel/sync.ras kernel/kernel/trap.ras kernel/kernel/pages.ras kernel/kernel/loader.ras > "$work/boot.ras"
8 8
"$emulator" -memory-size=385024 -data-size=348160 -stack-size=512 -run bin/kernel.build.rv64 -- bin/std.ril bin/kernel.ril "$work/boot.ras" "$work/boot.rv64"
9 9
for harts in 1 2 8; do
10 10
    # The deterministic emulator shares its tick budget across all harts.
11 11
    steps=$((10000000 * harts))
12 12
    if ! "$emulator" -machine -max-steps="$steps" -harts="$harts" -run "$work/boot.rv64" > "$work/log" 2>&1; then
28 28
if [ "$status" -ne 2 ] || [ "$(grep -c 'wfi=1 mcause=0x0' "$work/log")" -ne 8 ]; then
29 29
    cat "$work/log" >&2
30 30
    exit 1
31 31
fi
32 32
printf 'kernel startup: all eight harts reached machine idle without traps\n'
33 -
cat test/boot/trap.ras kernel/kernel/boot.ras kernel/kernel/sync.ras kernel/kernel/trap.ras kernel/kernel/pages.ras > "$work/trap.ras"
33 +
cat test/boot/trap.ras kernel/kernel/boot.ras kernel/kernel/sync.ras kernel/kernel/trap.ras kernel/kernel/pages.ras kernel/kernel/loader.ras > "$work/trap.ras"
34 34
"$emulator" -memory-size=385024 -data-size=348160 -stack-size=512 -run bin/kernel.build.rv64 -- bin/std.ril bin/kernel.ril "$work/trap.ras" "$work/trap.rv64"
35 35
status=0
36 36
"$emulator" -machine -harts=2 -max-steps=20000000 -run "$work/trap.rv64" > "$work/log" 2>&1 || status=$?
37 37
if [ "$status" -ne 2 ] || [ "$(grep -c '^kernel: unexpected trap$' "$work/log")" -ne 1 ] \
38 38
    || [ "$(grep -c 'wfi=1' "$work/log")" -ne 2 ]; then
test/loader/kernel/loadcheck.rad added +44 -0
1 +
//! Execute runtime-loaded native code after releasing its compiler workspace.
2 +
3 +
use std::mem;
4 +
use kernel::abi;
5 +
use kernel::backing;
6 +
use kernel::capability;
7 +
use kernel::pages;
8 +
use kernel::registry;
9 +
use kernel::loader;
10 +
use kernel::loadinput;
11 +
12 +
/// Capability storage for the loading domain.
13 +
unsafe static TABLE: capability::Table = undefined;
14 +
15 +
/// View a validated source Page as mapped bytes.
16 +
fn memory(address: u64) -> *mut u8;
17 +
18 +
/// Call a published entry that does not access private package state.
19 +
fn invoke(address: u64) -> i64;
20 +
21 +
/// Load the hosted comparison package and execute its retained native instructions.
22 +
export unsafe fn run() {
23 +
    let owner = abi::Ref { index: 0, generation: 1 };
24 +
    capability::initialize(&mut TABLE, owner);
25 +
    try! backing::registerDomain(&mut pages::STORE.backings, owner);
26 +
    let authority = try! capability::install(&mut TABLE, capability::Entry {
27 +
        kind: abi::Kind::Domain, object: owner, rights: abi::Rights(abi::ALLOCATE),
28 +
    });
29 +
    let source = try! pages::allocate(&mut pages::STORE, &mut TABLE, authority, 1);
30 +
    let cap = try! capability::lookup(&TABLE, source, abi::Kind::Page, abi::Rights(abi::READ));
31 +
    let page = try! pages::get(&pages::STORE, cap.object);
32 +
    let bytes = @sliceOf(memory(page.base), 4096);
33 +
    let length = try! mem::copy(&mut bytes[..], &loadinput::INPUT[..]);
34 +
    let handle = try! loader::load(&mut loader::STATE, &mut pages::STORE, &mut registry::STORE, &mut TABLE,
35 +
        loader::Request { authority, source, offset: 0, length: length as u64 });
36 +
    assert not loader::STATE.busy;
37 +
    let object = try! registry::image(&registry::STORE, &TABLE, handle, abi::Rights(abi::EXECUTE));
38 +
    let package = try! registry::get(&registry::STORE, object);
39 +
    let entry = package.entry else panic "loaded entry";
40 +
    for i in 0..length {
41 +
        set bytes[i] = 0;
42 +
    }
43 +
    assert invoke(entry) == 43;
44 +
}
test/loader/loaded.rad added +7 -0
1 +
//! A package executed by both hosted compilation and the kernel loader.
2 +
3 +
/// Compute a result through a package-local call.
4 +
fn calculate(value: i64) -> i64 { return value * 6 + 1; }
5 +
6 +
/// Return the result checked by both execution paths.
7 +
@default fn main() -> i32 { return calculate(7) as i32; }
test/loader/machine.ras added +22 -0
1 +
//! Run the loader fixture after all harts complete production initialization.
2 +
.text;
3 +
    call @kernel::boot::initialize;
4 +
    beqz %a0 @wait;
5 +
    call @kernel::loadcheck::run;
6 +
    li %t0 0x10001000;
7 +
    li %t1 0x5555;
8 +
    sw %t1 0(%t0);
9 +
@wait
10 +
    wfi;
11 +
    j @wait;
12 +
13 +
.export @kernel::loadcheck::memory;
14 +
.export @kernel::loadcheck::invoke;
15 +
16 +
// The caller supplies an address within its validated source Page.
17 +
@kernel::loadcheck::memory
18 +
    ret;
19 +
20 +
// Tail-call published code with no private-state accesses.
21 +
@kernel::loadcheck::invoke
22 +
    jalr %zero %a0 0;
test/loader/run added +29 -0
1 +
#!/bin/sh
2 +
# Build a kernel fixture and call code produced by its runtime loader.
3 +
set -eu
4 +
emulator=${RAD_EMULATOR:-emulator}
5 +
work=$(mktemp -d)
6 +
trap 'rm -rf "$work"' EXIT HUP INT TERM
7 +
"$emulator" -memory-size=385024 -data-size=348160 -stack-size=512 \
8 +
    -run bin/radiance.rv64.dev -pkg loaded -mod test/loader/loaded.rad -entry loaded -ril "$work"
9 +
"$emulator" -memory-size=385024 -data-size=348160 -stack-size=512 \
10 +
    -run bin/radiance.rv64.dev -pkg loaded -mod test/loader/loaded.rad -entry loaded -o "$work/hosted.rv64"
11 +
status=0
12 +
"$emulator" -run "$work/hosted.rv64" || status=$?
13 +
test "$status" -eq 43
14 +
cat kernel/kernel.rad > "$work/kernel.rad"
15 +
printf '\nexport mod loadinput;\nexport mod loadcheck;\n' >> "$work/kernel.rad"
16 +
mkdir "$work/kernel"
17 +
cp kernel/kernel/*.rad "$work/kernel/"
18 +
cp test/loader/kernel/loadcheck.rad "$work/kernel/"
19 +
length=$(wc -c < "$work/loaded.ril")
20 +
printf '//! Binary package used by both execution paths.\n/// Complete trusted binary RIL input.\nexport static INPUT: [u8; %s] = [\n' "$length" > "$work/kernel/loadinput.rad"
21 +
od -An -v -tu1 "$work/loaded.ril" | awk '{ for (i = 1; i <= NF; i++) printf "%s,", $i; print "" }' >> "$work/kernel/loadinput.rad"
22 +
printf '];\n' >> "$work/kernel/loadinput.rad"
23 +
sh test/acceptance/compile "$emulator" "$work"
24 +
cat test/loader/machine.ras kernel/kernel/boot.ras kernel/kernel/sync.ras \
25 +
    kernel/kernel/trap.ras kernel/kernel/pages.ras kernel/kernel/loader.ras > "$work/loader.ras"
26 +
"$emulator" -memory-size=385024 -data-size=348160 -stack-size=512 \
27 +
    -run bin/kernel.build.rv64 -- "$work/std.ril" "$work/kernel.ril" "$work/loader.ras" "$work/loader.rv64"
28 +
"$emulator" -machine -memory-size=262144 -max-steps=200000000 -run "$work/loader.rv64"
29 +
printf 'runtime loader: hosted and loaded entries agree after workspace release\n'