kernel: validate typed handles and retire exhausted slots

2fa6ad333439a5b01ae9e54e5f82483df5a24b0933e34140242513860d2ba28c
Verified: make -C kernel check with the machine-capable emulator; all pass.
Alexis Sellier committed ago 1 parent 71bd6f97
kernel/Makefile +2 -2
1 1
# Freestanding kernel and hosted mechanism checks.
2 2
EMU ?= $(or $(RAD_EMULATOR),emulator)
3 3
HOST_EMU ?= $(EMU)
4 4
COMPILER := ../bin/radiance.rv64.dev
5 5
COMPILE := $(HOST_EMU) -memory-size=385024 -data-size=348160 -stack-size=512 -run $(COMPILER)
6 -
MODULES := core/fdt.rad core/platform.rad core/frames.rad
6 +
MODULES := core/fdt.rad core/platform.rad core/frames.rad core/abi.rad core/handles.rad
7 7
CORE := -pkg core -mod core.rad $(addprefix -mod ,$(MODULES))
8 -
CHECK_MODULES := check/boot.rad check/fixture.rad check/frames.rad
8 +
CHECK_MODULES := check/boot.rad check/fixture.rad check/frames.rad check/handles.rad
9 9
10 10
.PHONY: all check clean compiler-check
11 11
all: kernel.rv64
12 12
13 13
compiler-check:
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 3 of the 22-step plan.
5 +
record the contracts established through step 4 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.
41 41
- Allocation clears frames before a new domain receives access. Kernel/image,
42 42
  FDT, boot data, firmware reservations, and MMIO regions are excluded.
43 43
- Contiguous allocations validate complete ranges and fail without partial
44 44
  ownership when memory or metadata capacity is unavailable.
45 45
46 +
## Handles and rights
47 +
48 +
- A handle packs kind [63:60], generation [59:28], zero reserved bits [27:24],
49 +
  and slot [23:0]. It is domain-relative authority, never a kernel pointer.
50 +
  This build has 512 handle slots per domain and up to 512 domains.
51 +
- Object identities include kind, index, and nonzero incarnation. Slot generation
52 +
  and object incarnation are distinct stale-name defenses. Exhausted generations
53 +
  and incarnations are retired rather than wrapped.
54 +
- Rights are Read=1, Write=2, Execute=4, Grant=8, Transfer=16, Create=32,
55 +
  Allocate=64, Wake=128, Destroy=256; higher bits are reserved. New Page rights
56 +
  are Read|Write|Grant|Transfer. New Domain rights are
57 +
  Destroy|Execute|Grant|Transfer|Wake; root additionally has Create|Allocate.
58 +
46 59
## Validation
47 60
48 61
Use the current machine-capable sibling emulator. Set `RAD_EMULATOR`, pass
49 62
`EMU` to the kernel Make invocation, or put `emulator` on PATH. The kernel build
50 63
checks compiler dependencies. From the repository root, run:
51 64
52 65
```sh
53 66
make -C kernel check
54 67
```
55 68
56 -
Exercise contiguous allocation, clearing, reservation exclusion, exhaustion, and exact physical boundaries.
69 +
Exercise invented and stale names, reserved bits, wrong kinds, rights checks, and generation retirement.
57 70
58 71
The entry probe uses explicit M-mode success/fault finish writes; secondary
59 72
harts idle. This checks machine entry, not user-domain execution. Finish writes
60 73
are a check protocol, not a domain-exit operation.
kernel/check.rad +2 -0
1 1
//! Hosted entry for kernel mechanism checks.
2 2
3 3
mod boot;
4 4
export mod fixture;
5 5
mod frames;
6 +
mod handles;
6 7
7 8
/// Run the available kernel mechanism checks.
8 9
@default fn main() -> u32 {
9 10
    frames::run();
10 11
    boot::run();
12 +
    handles::run();
11 13
    return 0;
12 14
}
kernel/check/handles.rad added +51 -0
1 +
//! Capability lookup authority, rights, and stale-generation checks.
2 +
3 +
use core::abi;
4 +
use core::handles;
5 +
6 +
/// Require a handle lookup to fail with the specified error.
7 +
fn reject(table: *handles::Table, handle: abi::Handle, kind: abi::Kind, rights: u16, expected: abi::Error) {
8 +
    let _entry = try handles::require(table, handle, kind, rights) catch error {
9 +
        assert error == expected;
10 +
        return;
11 +
    };
12 +
    panic "reject: invalid authority accepted";
13 +
}
14 +
15 +
/// Exercise packed handles, rights checks, reuse, and generation exhaustion.
16 +
export fn run() {
17 +
    let mut table: handles::Table = undefined;
18 +
    let mut other: handles::Table = undefined;
19 +
    handles::init(&mut table);
20 +
    handles::init(&mut other);
21 +
    let page = abi::Object { kind: abi::Kind::Page, index: 9, epoch: 1 };
22 +
    let original = handles::install(&mut table, 0, page, abi::READ);
23 +
    let entry = try! handles::require(&table, original, abi::Kind::Page, abi::READ);
24 +
    assert entry.object.index == 9 and entry.object.epoch == 1;
25 +
    reject(&table, original, abi::Kind::Page, abi::WRITE, abi::Error::Denied);
26 +
    reject(&table, original, abi::Kind::Domain, 0, abi::Error::BadHandle);
27 +
    reject(&other, original, abi::Kind::Page, 0, abi::Error::BadHandle);
28 +
    reject(&table, abi::Handle { bits: 0 }, abi::Kind::Page, 0, abi::Error::BadHandle);
29 +
    reject(&table, abi::Handle { bits: original.bits | 0x1000000 }, abi::Kind::Page, 0, abi::Error::BadHandle);
30 +
    reject(&table, abi::Handle { bits: original.bits | 0xffffff }, abi::Kind::Page, 0, abi::Error::BadHandle);
31 +
    reject(&table, abi::Handle { bits: original.bits ^ (3 as u64 << 60) }, abi::Kind::Page, 0, abi::Error::BadHandle);
32 +
    let _removed = handles::remove(&mut table, 0);
33 +
    let replacement = handles::install(&mut table, 0, page, abi::READ | abi::WRITE);
34 +
    reject(&table, original, abi::Kind::Page, 0, abi::Error::BadHandle);
35 +
    let _valid = try! handles::require(&table, replacement, abi::Kind::Page, abi::WRITE);
36 +
    let _again = handles::remove(&mut table, 0);
37 +
    set table.entries[0].generation = 0xffffffff;
38 +
    let last = handles::install(&mut table, 0, page, abi::READ);
39 +
    let _last = handles::remove(&mut table, 0);
40 +
    reject(&table, last, abi::Kind::Page, 0, abi::Error::BadHandle);
41 +
    let next = try! handles::vacant(&table);
42 +
    assert next == 1;
43 +
    for i in 1..abi::MAX_HANDLES {
44 +
        let _handle = handles::install(&mut table, i, page, abi::READ);
45 +
    }
46 +
    let _slot = try handles::vacant(&table) catch error {
47 +
        assert error == abi::Error::Exhausted;
48 +
        return;
49 +
    };
50 +
    panic "run: full capability table accepted an entry";
51 +
}
kernel/core.rad +2 -0
1 1
//! Safe kernel mechanisms shared by machine execution and hosted checks.
2 2
3 3
export mod fdt;
4 4
export mod platform;
5 5
export mod frames;
6 +
export mod abi;
7 +
export mod handles;
kernel/core/abi.rad added +91 -0
1 +
//! Stable object kinds, rights, handles, and direct-call errors.
2 +
3 +
/// Per-domain capability slots in this build.
4 +
export constant MAX_HANDLES: u32 = 512;
5 +
/// Maximum live domains in this build.
6 +
export constant MAX_DOMAINS: u32 = 512;
7 +
/// A domain identifier that cannot name a live domain.
8 +
export constant INVALID_DOMAIN: u32 = 0xffff;
9 +
10 +
/// Kernel object kinds in packed-handle order.
11 +
export union Kind: Copy {
12 +
    /// An unoccupied capability slot.
13 +
    Empty,
14 +
    /// Contiguous physical memory.
15 +
    Page,
16 +
    /// One external interrupt source.
17 +
    Interrupt,
18 +
    /// One device register region.
19 +
    Device,
20 +
    /// A domain's notification queue.
21 +
    Events,
22 +
    /// A protection domain.
23 +
    Domain,
24 +
    /// Finite execution authority on one hart.
25 +
    Budget,
26 +
}
27 +
28 +
/// Direct-call error codes in ABI order.
29 +
export union Error: Copy {
30 +
    /// The operation succeeded.
31 +
    Ok,
32 +
    /// The handle is absent, stale, malformed, or of the wrong kind.
33 +
    BadHandle,
34 +
    /// Required capability rights are absent.
35 +
    Denied,
36 +
    /// No suitable physical memory remains.
37 +
    OutOfMemory,
38 +
    /// An argument is invalid for this operation.
39 +
    InvalidArg,
40 +
    /// The resource or a bounded request queue is busy.
41 +
    Busy,
42 +
    /// Image verification failed.
43 +
    VerifyFailed,
44 +
    /// The target is not pending activation.
45 +
    NotPending,
46 +
    /// A bounded kernel resource is fully committed.
47 +
    Exhausted,
48 +
}
49 +
50 +
/// Read or derive read-only resource access.
51 +
export constant READ: u16 = 1;
52 +
/// Write or derive writable resource access.
53 +
export constant WRITE: u16 = 2;
54 +
/// Activate an execution context.
55 +
export constant EXECUTE: u16 = 4;
56 +
/// Copy authority to another domain.
57 +
export constant GRANT: u16 = 8;
58 +
/// Move authority to another domain.
59 +
export constant TRANSFER: u16 = 16;
60 +
/// Create protection domains.
61 +
export constant CREATE: u16 = 32;
62 +
/// Allocate from the physical resource pool.
63 +
export constant ALLOCATE: u16 = 64;
64 +
/// Append a wakeup event to a domain.
65 +
export constant WAKE: u16 = 128;
66 +
/// Destroy a protection domain.
67 +
export constant DESTROY: u16 = 256;
68 +
/// All defined rights; high bits are reserved.
69 +
export constant RIGHTS: u16 = 511;
70 +
/// Rights on a new Page allocation.
71 +
export constant PAGE_RIGHTS: u16 = READ | WRITE | GRANT | TRANSFER;
72 +
/// Rights on a newly created Domain handle.
73 +
export constant DOMAIN_RIGHTS: u16 = DESTROY | EXECUTE | GRANT | TRANSFER | WAKE;
74 +
/// Initial root authority on its own Domain.
75 +
export constant ROOT_RIGHTS: u16 = DOMAIN_RIGHTS | CREATE | ALLOCATE;
76 +
77 +
/// A domain-relative packed capability identifier, not a kernel pointer.
78 +
export record Handle: Copy {
79 +
    /// Kind [63:60], generation [59:28], reserved [27:24], slot [23:0].
80 +
    bits: u64,
81 +
}
82 +
83 +
/// Identity of a kernel object across slot reuse.
84 +
export record Object: Copy {
85 +
    /// Resource table selected by this identity.
86 +
    kind: Kind,
87 +
    /// Index in that resource table.
88 +
    index: u32,
89 +
    /// Incarnation of the object slot; zero is never live.
90 +
    epoch: u32,
91 +
}
kernel/core/handles.rad added +102 -0
1 +
//! Domain-local capability lookup and slot generation management.
2 +
3 +
use core::abi;
4 +
5 +
/// One authoritative capability-table entry.
6 +
export record Entry: Copy {
7 +
    /// Named object and its incarnation.
8 +
    object: abi::Object,
9 +
    /// Rights held through this entry.
10 +
    rights: u16,
11 +
    /// Slot generation; zero marks permanent retirement.
12 +
    generation: u32,
13 +
}
14 +
15 +
/// Fixed storage for a domain's capabilities.
16 +
export record Table: Copy {
17 +
    /// Live and empty slots; slot positions do not move.
18 +
    entries: [Entry; abi::MAX_HANDLES],
19 +
}
20 +
21 +
/// Initialize a table once, before its first domain incarnation.
22 +
export fn init(table: *mut Table) {
23 +
    for i in 0..abi::MAX_HANDLES {
24 +
        set table.entries[i] = Entry {
25 +
            object: abi::Object { kind: abi::Kind::Empty, index: 0, epoch: 0 },
26 +
            rights: 0, generation: 1,
27 +
        };
28 +
    }
29 +
}
30 +
31 +
/// Find an empty slot whose generation has not been retired.
32 +
export fn vacant(table: *Table) -> u32 throws (abi::Error) {
33 +
    for i in 0..abi::MAX_HANDLES {
34 +
        let entry = &table.entries[i];
35 +
        if entry.object.kind == abi::Kind::Empty and entry.generation <> 0 { return i; }
36 +
    }
37 +
    throw abi::Error::Exhausted;
38 +
}
39 +
40 +
/// Install authority into a known empty slot and return its packed handle.
41 +
export fn install(table: *mut Table, slot: u32, object: abi::Object, rights: u16) -> abi::Handle {
42 +
    assert slot < abi::MAX_HANDLES and object.kind <> abi::Kind::Empty and object.epoch <> 0;
43 +
    let entry = &mut table.entries[slot];
44 +
    assert entry.object.kind == abi::Kind::Empty and entry.generation <> 0;
45 +
    set entry.object = object;
46 +
    set entry.rights = rights & abi::RIGHTS;
47 +
    return abi::Handle {
48 +
        bits: (object.kind as u64 << 60) | (entry.generation as u64 << 28) | slot as u64,
49 +
    };
50 +
}
51 +
52 +
/// Validate the packed handle and return its authoritative table index.
53 +
export fn slot(table: *Table, handle: abi::Handle) -> u32 throws (abi::Error) {
54 +
    let bits = handle.bits;
55 +
    let index = (bits & 0xffffff) as u32;
56 +
    if bits == 0 or (bits & 0xf000000) <> 0 or index >= abi::MAX_HANDLES {
57 +
        throw abi::Error::BadHandle;
58 +
    }
59 +
    let entry = &table.entries[index];
60 +
    if entry.object.kind == abi::Kind::Empty or (bits >> 60) <> entry.object.kind as u64
61 +
        or ((bits >> 28) & 0xffffffff) <> entry.generation as u64 {
62 +
        throw abi::Error::BadHandle;
63 +
    }
64 +
    return index;
65 +
}
66 +
67 +
/// Require a live handle of the given kind with all requested rights.
68 +
/// The object owner must also check the object's epoch and lifecycle.
69 +
export fn require(table: *Table, handle: abi::Handle, kind: abi::Kind, rights: u16) -> Entry throws (abi::Error) {
70 +
    let index = try slot(table, handle);
71 +
    let entry = table.entries[index];
72 +
    if entry.object.kind <> kind { throw abi::Error::BadHandle; }
73 +
    if entry.rights & rights <> rights { throw abi::Error::Denied; }
74 +
    return entry;
75 +
}
76 +
77 +
/// Invalidate a slot before reuse and return its former authority.
78 +
/// Generation exhaustion permanently retires the slot.
79 +
export fn remove(table: *mut Table, index: u32) -> Entry {
80 +
    assert index < abi::MAX_HANDLES;
81 +
    let previous = table.entries[index];
82 +
    assert previous.object.kind <> abi::Kind::Empty;
83 +
    let entry = &mut table.entries[index];
84 +
    set entry.object = abi::Object { kind: abi::Kind::Empty, index: 0, epoch: 0 };
85 +
    set entry.rights = 0;
86 +
    if previous.generation == 0xffffffff {
87 +
        set entry.generation = 0;
88 +
    } else {
89 +
        set entry.generation = previous.generation + 1;
90 +
    }
91 +
    return previous;
92 +
}
93 +
94 +
/// Read a live slot by kind without constructing a handle from an index.
95 +
export fn get(table: *Table, index: u32, kind: abi::Kind) -> abi::Handle throws (abi::Error) {
96 +
    if index >= abi::MAX_HANDLES or kind == abi::Kind::Empty { throw abi::Error::BadHandle; }
97 +
    let entry = &table.entries[index];
98 +
    if entry.object.kind <> kind { throw abi::Error::BadHandle; }
99 +
    return abi::Handle {
100 +
        bits: (kind as u64 << 60) | (entry.generation as u64 << 28) | index as u64,
101 +
    };
102 +
}