kernel: Add ABI types and bounded object storage

1873ef7c9d924b8588c54be9c171cc659592bceee55f9965a7ad39914730076d
Assisted-by: Codex:gpt-6
Alexis Sellier committed ago 1 parent fc84a1d3
kernel/kernel.rad +3 -0
1 1
//! Kernel resource management and machine execution.
2 2
3 3
use std::testing;
4 4
5 +
export mod abi;
6 +
export mod limits;
7 +
export mod slots;
5 8
export mod range;
6 9
export mod sync;
7 10
@test export mod tests;
kernel/kernel/abi.rad added +201 -0
1 +
//! Kernel handle representation, rights, errors, and operation numbers.
2 +
3 +
/// Domain-relative capability value. Zero is the self sentinel where allowed.
4 +
export record Handle: Copy(u64);
5 +
/// Rights granted by a live capability-table entry.
6 +
export record Rights: Copy(u16);
7 +
8 +
/// Read object contents or metadata.
9 +
export constant READ: u16 = 1;
10 +
/// Modify object contents.
11 +
export constant WRITE: u16 = 2;
12 +
/// Execute an image or entry.
13 +
export constant EXECUTE: u16 = 4;
14 +
/// Grant an attenuated capability.
15 +
export constant GRANT: u16 = 8;
16 +
/// Transfer a capability or CPU authority.
17 +
export constant TRANSFER: u16 = 16;
18 +
/// Create a domain.
19 +
export constant CREATE: u16 = 32;
20 +
/// Allocate physical memory.
21 +
export constant ALLOCATE: u16 = 64;
22 +
/// Wake or manage the parent relationship of a domain.
23 +
export constant WAKE: u16 = 128;
24 +
/// Destroy a domain.
25 +
export constant DESTROY: u16 = 256;
26 +
/// All defined right bits.
27 +
export constant RIGHTS_MASK: u16 = 511;
28 +
/// Cascade destruction through creation ancestry.
29 +
export constant CASCADE: u64 = 1;
30 +
31 +
/// Object kind stored in the high four handle bits.
32 +
export union Kind: Copy {
33 +
    /// Unoccupied table entry.
34 +
    Empty = 0,
35 +
    /// Contiguous physical memory.
36 +
    Page = 1,
37 +
    /// Interrupt source ownership.
38 +
    Interrupt = 2,
39 +
    /// Device memory region.
40 +
    Device = 3,
41 +
    /// Domain notification queue.
42 +
    Events = 4,
43 +
    /// Domain management authority.
44 +
    Domain = 5,
45 +
    /// Exclusive CPU time window.
46 +
    Budget = 6,
47 +
    /// Resident executable package graph entry.
48 +
    Image = 7,
49 +
}
50 +
51 +
/// Scalar error codes; the syscall ABI returns their negatives.
52 +
export union Error: Copy {
53 +
    /// Successful operation.
54 +
    Ok = 0,
55 +
    /// Missing, stale, or incorrectly typed handle.
56 +
    BadHandle = 1,
57 +
    /// Required rights are absent.
58 +
    Denied = 2,
59 +
    /// Physical memory is exhausted.
60 +
    OutOfMemory = 3,
61 +
    /// Invalid argument, alignment, or reserved bits.
62 +
    InvalidArg = 4,
63 +
    /// The operation cannot proceed in the current state.
64 +
    Busy = 5,
65 +
    /// The supplied executable image is invalid.
66 +
    VerifyFailed = 6,
67 +
    /// The domain is not pending activation.
68 +
    NotPending = 7,
69 +
    /// Fixed kernel storage is exhausted.
70 +
    Exhausted = 8,
71 +
}
72 +
73 +
/// Operation identifier passed in a7.
74 +
export union Operation: Copy {
75 +
    /// Grant capability rights.
76 +
    CapabilityGrant = 10,
77 +
    /// Transfer a capability.
78 +
    CapabilityTransfer = 11,
79 +
    /// Drop a capability.
80 +
    CapabilityDrop = 12,
81 +
    /// Create a pending domain from an image.
82 +
    DomainCreate = 20,
83 +
    /// Activate a pending domain.
84 +
    DomainActivate = 21,
85 +
    /// Destroy a domain.
86 +
    DomainDestroy = 22,
87 +
    /// Select a new lifecycle-event parent.
88 +
    DomainReparent = 23,
89 +
    /// Allocate physical frames.
90 +
    PageAllocate = 30,
91 +
    /// Split an exclusive page object.
92 +
    PageSplit = 31,
93 +
    /// Transfer the remaining active window to a target domain.
94 +
    Yield = 40,
95 +
    /// Request a timeout event.
96 +
    Timeout = 41,
97 +
    /// Wait for an event.
98 +
    Wait = 42,
99 +
    /// Terminate with an abort report.
100 +
    Abort = 43,
101 +
    /// Query page metadata.
102 +
    QueryPage = 44,
103 +
    /// Query device metadata.
104 +
    QueryDevice = 45,
105 +
    /// Query interrupt metadata.
106 +
    QueryInterrupt = 46,
107 +
    /// Query domain metadata.
108 +
    QueryDomain = 47,
109 +
    /// Query event queue metadata.
110 +
    QueryEvents = 48,
111 +
    /// Terminate normally.
112 +
    Exit = 49,
113 +
    /// Deliver a wake token.
114 +
    Wakeup = 50,
115 +
    /// Load a trusted binary RIL package.
116 +
    ImageLoad = 51,
117 +
    /// Query a resident image.
118 +
    QueryImage = 52,
119 +
    /// Create an additional execution context.
120 +
    ContextCreate = 60,
121 +
    /// Destroy an additional execution context.
122 +
    ContextDestroy = 61,
123 +
    /// Query an execution context.
124 +
    QueryContext = 62,
125 +
    /// Query the current context and execution window.
126 +
    CurrentContext = 63,
127 +
    /// Partition a CPU time window.
128 +
    BudgetSplit = 70,
129 +
    /// Combine adjacent CPU time windows.
130 +
    BudgetMerge = 71,
131 +
    /// Query a CPU time window.
132 +
    QueryBudget = 72,
133 +
    /// Bind and transfer a CPU time window to a context.
134 +
    BudgetBind = 73,
135 +
}
136 +
137 +
/// Generation-bearing reference within one object table.
138 +
export record Ref: Copy {
139 +
    /// Table index.
140 +
    index: u32,
141 +
    /// Generation of the indexed slot.
142 +
    generation: u32,
143 +
}
144 +
145 +
/// Structurally valid handle fields. Lookup must also check the live table entry.
146 +
export record Decoded: Copy {
147 +
    /// Encoded object kind.
148 +
    kind: Kind,
149 +
    /// Table slot and generation.
150 +
    object: Ref,
151 +
}
152 +
153 +
/// Pack kind, 32-bit generation, and 24-bit index with zero reserved bits.
154 +
export fn handle(kind: Kind, object: Ref) -> Handle throws (Error) {
155 +
    if kind == Kind::Empty or object.index > 0xffffff {
156 +
        throw Error::InvalidArg;
157 +
    }
158 +
    return Handle((kind as u64 << 60) | (object.generation as u64 << 28) | object.index as u64);
159 +
}
160 +
161 +
/// Decode a nonzero handle and reject reserved bits or unsupported kinds.
162 +
export fn decode(value: Handle) -> Decoded throws (Error) {
163 +
    let bits = *value;
164 +
    if bits == 0 or (bits & 0x0f000000) <> 0 {
165 +
        throw Error::BadHandle;
166 +
    }
167 +
    let mut kind = Kind::Empty;
168 +
    match bits >> 60 {
169 +
        case 1 => set kind = Kind::Page,
170 +
        case 2 => set kind = Kind::Interrupt,
171 +
        case 3 => set kind = Kind::Device,
172 +
        case 4 => set kind = Kind::Events,
173 +
        case 5 => set kind = Kind::Domain,
174 +
        case 6 => set kind = Kind::Budget,
175 +
        case 7 => set kind = Kind::Image,
176 +
        else => throw Error::BadHandle,
177 +
    }
178 +
    return Decoded { kind, object: Ref { index: (bits & 0xffffff) as u32, generation: (bits >> 28) as u32 } };
179 +
}
180 +
181 +
/// Validate a caller-supplied rights mask.
182 +
export fn rights(bits: u64) -> Rights throws (Error) {
183 +
    if (bits & ~(RIGHTS_MASK as u64)) <> 0 {
184 +
        throw Error::InvalidArg;
185 +
    }
186 +
    return Rights(bits as u16);
187 +
}
188 +
189 +
/// Check that all requested rights are present.
190 +
export fn permits(granted: Rights, requested: Rights) -> bool {
191 +
    return (*granted & *requested) == *requested;
192 +
}
193 +
194 +
/// Encode a generation-bearing internal ID without reserved bits.
195 +
export fn id(object: Ref) -> u64 {
196 +
    return (object.generation as u64 << 32) | object.index as u64;
197 +
}
198 +
/// Decode an internal ID. Object lookup validates its scope and liveness.
199 +
export fn reference(value: u64) -> Ref {
200 +
    return Ref { index: value as u32, generation: (value >> 32) as u32 };
201 +
}
kernel/kernel/limits.rad added +28 -0
1 +
//! Fixed kernel storage profile.
2 +
3 +
/// Maximum resident domains, including retained tombstones.
4 +
export constant DOMAINS: u32 = 512;
5 +
/// Per-domain capability table capacity.
6 +
export constant HANDLES: u32 = 512;
7 +
/// Maximum physical frames tracked by this build.
8 +
export constant FRAMES: u32 = 65536;
9 +
/// Physical frame size in bytes.
10 +
export constant FRAME_SIZE: u64 = 4096;
11 +
/// Maximum page objects, including split allocation segments.
12 +
export constant PAGES: u32 = 65536;
13 +
/// Maximum backing allocation records.
14 +
export constant ALLOCATIONS: u32 = 65536;
15 +
/// Maximum interrupt objects.
16 +
export constant INTERRUPTS: u32 = 128;
17 +
/// Maximum device memory regions.
18 +
export constant DEVICES: u32 = 128;
19 +
/// Maximum budget objects.
20 +
export constant BUDGETS: u32 = 512;
21 +
/// Maximum execution contexts across all domains.
22 +
export constant CONTEXTS: u32 = 2048;
23 +
/// Maximum hardware threads.
24 +
export constant HARTS: u32 = 8;
25 +
/// Maximum resident package records and gp table entries.
26 +
export constant PACKAGES: u32 = 256;
27 +
/// Generation values never wrap.
28 +
export constant LAST_GENERATION: u32 = 0xffffffff;
kernel/kernel/slots.rad added +103 -0
1 +
//! Bounded slot lifetimes and linear reservations.
2 +
3 +
use super::abi;
4 +
use super::limits;
5 +
6 +
/// Slot lifetime state.
7 +
export union State: Copy {
8 +
    /// Available for reservation.
9 +
    Free,
10 +
    /// Owned by an uncommitted reservation.
11 +
    Reserved,
12 +
    /// Published object.
13 +
    Live,
14 +
    /// Generation space is exhausted; the slot cannot be reused.
15 +
    Retired,
16 +
}
17 +
18 +
/// Metadata stored beside an object table's payload array.
19 +
export record Slot: Copy {
20 +
    /// Current nonzero generation.
21 +
    generation: u32,
22 +
    /// Current lifetime state.
23 +
    state: State,
24 +
}
25 +
26 +
/// A reserved slot that must be committed or cancelled in the same table.
27 +
export union Reservation: Once {
28 +
    /// Unpublished object slot.
29 +
    Held(abi::Ref),
30 +
}
31 +
32 +
/// Initialize fresh metadata. No live references may name this table.
33 +
export fn initialize(slots: &mut [Slot]) {
34 +
    for i in 0..slots.len {
35 +
        set slots[i] = Slot { generation: 1, state: State::Free };
36 +
    }
37 +
}
38 +
39 +
/// Reserve the first available slot with a bounded scan.
40 +
export fn reserve(slots: &mut [Slot]) -> Reservation throws (abi::Error) {
41 +
    for i in 0..slots.len {
42 +
        if slots[i].state == State::Free {
43 +
            set slots[i].state = State::Reserved;
44 +
            return Reservation::Held(abi::Ref { index: i, generation: slots[i].generation });
45 +
        }
46 +
    }
47 +
    throw abi::Error::Exhausted;
48 +
}
49 +
50 +
/// Check an internal reference against its table and required lifetime state.
51 +
export fn matches(slots: &[Slot], object: abi::Ref, state: State) -> bool {
52 +
    return object.index < slots.len and slots[object.index].generation == object.generation
53 +
        and slots[object.index].state == state;
54 +
}
55 +
56 +
/// Inspect a reservation's unpublished reference without consuming it.
57 +
export fn reference(reservation: &Reservation) -> abi::Ref {
58 +
    match reservation { case Reservation::Held(object) => return *object, }
59 +
}
60 +
61 +
/// Publish a reserved slot. The caller must initialize its payload first.
62 +
export fn commit(slots: &mut [Slot], reservation: Reservation) -> abi::Ref throws (abi::Error) {
63 +
    match reservation {
64 +
        case Reservation::Held(object) => {
65 +
            if not matches(slots, object, State::Reserved) {
66 +
                throw abi::Error::BadHandle;
67 +
            }
68 +
            set slots[object.index].state = State::Live;
69 +
            return object;
70 +
        },
71 +
    }
72 +
}
73 +
74 +
/// Advance the slot generation or retire it permanently.
75 +
fn advance(slot: &mut Slot) {
76 +
    if slot.generation == limits::LAST_GENERATION {
77 +
        set slot.state = State::Retired;
78 +
    }
79 +
    else {
80 +
        set slot.generation += 1;
81 +
        set slot.state = State::Free;
82 +
    }
83 +
}
84 +
85 +
/// Cancel a reservation and invalidate its generation.
86 +
export fn cancel(slots: &mut [Slot], reservation: Reservation) throws (abi::Error) {
87 +
    match reservation {
88 +
        case Reservation::Held(object) => {
89 +
            if not matches(slots, object, State::Reserved) {
90 +
                throw abi::Error::BadHandle;
91 +
            }
92 +
            advance(&mut slots[object.index]);
93 +
        },
94 +
    }
95 +
}
96 +
97 +
/// Invalidate a live object after all payload-specific teardown is complete.
98 +
export fn release(slots: &mut [Slot], object: abi::Ref) throws (abi::Error) {
99 +
    if not matches(slots, object, State::Live) {
100 +
        throw abi::Error::BadHandle;
101 +
    }
102 +
    advance(&mut slots[object.index]);
103 +
}
kernel/kernel/tests.rad +2 -0
1 1
//! Kernel unit tests. Machine execution tests have separate entry points.
2 2
3 3
export mod range;
4 +
export mod abi;
5 +
export mod slots;
kernel/kernel/tests/abi.rad added +60 -0
1 +
//! Handle bit boundaries and stable ABI values.
2 +
3 +
use std::testing;
4 +
use kernel::abi;
5 +
6 +
/// Check every kind at the index and generation boundaries.
7 +
@test fn handles() throws (testing::TestError) {
8 +
    for kind in &[abi::Kind::Page, abi::Kind::Interrupt, abi::Kind::Device,
9 +
        abi::Kind::Events, abi::Kind::Domain, abi::Kind::Budget, abi::Kind::Image]
10 +
    {
11 +
        for index in &[0 as u32, 0xffffff as u32] {
12 +
            for generation in &[0 as u32, 1 as u32, 0xffffffff as u32] {
13 +
                let object = abi::Ref { index, generation };
14 +
                let value = try! abi::handle(kind, object);
15 +
                let decoded = try! abi::decode(value);
16 +
                try testing::expect(decoded.kind == kind and decoded.object == object);
17 +
                try testing::expect((*value & 0x0f000000) == 0);
18 +
                try testing::expect(abi::reference(abi::id(object)) == object);
19 +
            }
20 +
        }
21 +
    }
22 +
    let maximum = try! abi::handle(abi::Kind::Image, abi::Ref { index: 0xffffff, generation: 0xffffffff });
23 +
    try testing::expect(*maximum == 0x7ffffffff0ffffff);
24 +
}
25 +
26 +
/// Reject self, empty/unsupported kinds, reserved bits, and oversized indices.
27 +
@test fn invalidHandles() throws (testing::TestError) {
28 +
    let mut failed: u32 = 0;
29 +
    for bits in &[0 as u64, 1 as u64, 0x8000000000000000, 0xf000000000000000,
30 +
        0x1000000001000000, 0x1000000002000000, 0x1000000004000000, 0x1000000008000000]
31 +
    {
32 +
        try abi::decode(abi::Handle(bits)) catch err {
33 +
            try testing::expect(err == abi::Error::BadHandle); set failed += 1;
34 +
        };
35 +
    }
36 +
    try testing::expect(failed == 8);
37 +
    try abi::handle(abi::Kind::Page, abi::Ref { index: 0x1000000, generation: 1 }) catch err {
38 +
        try testing::expect(err == abi::Error::InvalidArg); set failed += 1;
39 +
    };
40 +
    try testing::expect(failed == 9);
41 +
}
42 +
43 +
/// Check rights validation and the specified error/operation numbers.
44 +
@test fn numbersAndRights() throws (testing::TestError) {
45 +
    let all = try! abi::rights(511);
46 +
    let read = try! abi::rights(abi::READ as u64);
47 +
    try testing::expect(abi::permits(all, read) and not abi::permits(read, all));
48 +
    let mut failed = false;
49 +
    try abi::rights(512) catch err {
50 +
        try testing::expect(err == abi::Error::InvalidArg);
51 +
        set failed = true;
52 +
    };
53 +
    try testing::expect(failed);
54 +
    try testing::expect(abi::Operation::CapabilityGrant as u32 == 10);
55 +
    try testing::expect(abi::Operation::DomainReparent as u32 == 23);
56 +
    try testing::expect(abi::Operation::PageAllocate as u32 == 30);
57 +
    try testing::expect(abi::Operation::Yield as u32 == 40);
58 +
    try testing::expect(abi::Operation::Wakeup as u32 == 50);
59 +
    try testing::expect(abi::Error::BadHandle as u32 == 1 and abi::Error::Exhausted as u32 == 8);
60 +
}
kernel/kernel/tests/slots.rad added +63 -0
1 +
//! Slot reservation, publication, cancellation, and generation retirement.
2 +
3 +
use std::testing;
4 +
use kernel::abi;
5 +
use kernel::slots;
6 +
7 +
/// Reservations occupy capacity before publication and must be consumed once.
8 +
@test unsafe fn lifetime() throws (testing::TestError) {
9 +
    let mut table: [slots::Slot; 2] = undefined;
10 +
    slots::initialize(&mut table[..]);
11 +
    let first = try! slots::reserve(&mut table[..]);
12 +
    let pending = slots::reference(&first);
13 +
    let reserved = slots::matches(&table[..], pending, slots::State::Reserved);
14 +
    let second = try! slots::reserve(&mut table[..]);
15 +
    let left = try! slots::commit(&mut table[..], first);
16 +
    let right = try! slots::commit(&mut table[..], second);
17 +
    try testing::expect(reserved and left.index == 0 and right.index == 1);
18 +
    let mut exhausted = false;
19 +
    try reserveAndCancel(&mut table[..]) catch err {
20 +
        try testing::expect(err == abi::Error::Exhausted); set exhausted = true;
21 +
    };
22 +
    try testing::expect(exhausted);
23 +
    try! slots::release(&mut table[..], left);
24 +
    try testing::expect(not slots::matches(&table[..], left, slots::State::Live));
25 +
    let replacement = try! slots::reserve(&mut table[..]);
26 +
    let next = try! slots::commit(&mut table[..], replacement);
27 +
    try testing::expect(next.index == left.index and next.generation == left.generation + 1);
28 +
    let mut stale = false;
29 +
    try slots::release(&mut table[..], left) catch err {
30 +
        try testing::expect(err == abi::Error::BadHandle); set stale = true;
31 +
    };
32 +
    try testing::expect(stale and slots::matches(&table[..], next, slots::State::Live));
33 +
}
34 +
35 +
/// Cancellation invalidates references and exhausted generations never wrap.
36 +
@test unsafe fn cancellationAndRetirement() throws (testing::TestError) {
37 +
    let mut table: [slots::Slot; 1] = undefined;
38 +
    slots::initialize(&mut table[..]);
39 +
    let token = try! slots::reserve(&mut table[..]);
40 +
    let old = slots::reference(&token);
41 +
    try! slots::cancel(&mut table[..], token);
42 +
    try testing::expect(table[0].state == slots::State::Free and table[0].generation == old.generation + 1);
43 +
    set table[0].generation = 0xffffffff;
44 +
    let last = try! slots::reserve(&mut table[..]);
45 +
    let object = try! slots::commit(&mut table[..], last);
46 +
    try! slots::release(&mut table[..], object);
47 +
    try testing::expect(table[0].state == slots::State::Retired and table[0].generation == 0xffffffff);
48 +
    let mut failed = false;
49 +
    try reserveAndCancel(&mut table[..]) catch err {
50 +
        try testing::expect(err == abi::Error::Exhausted); set failed = true;
51 +
    };
52 +
    try testing::expect(failed);
53 +
    set table[0].state = slots::State::Free;
54 +
    let cancelled = try! slots::reserve(&mut table[..]);
55 +
    try! slots::cancel(&mut table[..], cancelled);
56 +
    try testing::expect(table[0].state == slots::State::Retired);
57 +
}
58 +
59 +
/// Consume a reservation when a capacity check unexpectedly succeeds.
60 +
fn reserveAndCancel(table: &mut [slots::Slot]) throws (abi::Error) {
61 +
    let token = try slots::reserve(table);
62 +
    try slots::cancel(table, token);
63 +
}
test/slots/abandon.rad added +11 -0
1 +
//! Compile-failure fixture for an unconsumed reservation.
2 +
3 +
use std::testing;
4 +
use kernel::slots;
5 +
6 +
/// A reservation must be consumed before its scope ends.
7 +
@test unsafe fn abandon() throws (testing::TestError) {
8 +
    let mut table: [slots::Slot; 1] = undefined;
9 +
    slots::initialize(&mut table[..]);
10 +
    let token = try! slots::reserve(&mut table[..]);
11 +
}
test/slots/duplicate.rad added +13 -0
1 +
//! Compile-failure fixture for repeated reservation consumption.
2 +
3 +
use std::testing;
4 +
use kernel::slots;
5 +
6 +
/// A reservation cannot be cancelled twice.
7 +
@test unsafe fn duplicate() throws (testing::TestError) {
8 +
    let mut table: [slots::Slot; 1] = undefined;
9 +
    slots::initialize(&mut table[..]);
10 +
    let token = try! slots::reserve(&mut table[..]);
11 +
    try! slots::cancel(&mut table[..], token);
12 +
    try! slots::cancel(&mut table[..], token);
13 +
}
test/slots/run added +25 -0
1 +
#!/bin/sh
2 +
# Check that reservation misuse fails during compilation.
3 +
set -eu
4 +
emulator=$1
5 +
shift
6 +
work=$(mktemp -d)
7 +
trap 'rm -rf "$work"' EXIT HUP INT TERM
8 +
for name in duplicate abandon; do
9 +
    case "$name" in
10 +
        duplicate) diagnostic='linear value used after consumption' ;;
11 +
        abandon) diagnostic='linear value is not consumed' ;;
12 +
    esac
13 +
    if "$emulator" -memory-size=385024 -data-size=348160 -stack-size=512 \
14 +
        -run bin/radiance.rv64.dev "$@" -test \
15 +
        -pkg "$name" -mod "test/slots/$name.rad" -entry "$name" \
16 +
        -o "$work/test.rv64" > "$work/log" 2>&1; then
17 +
        printf 'reservation misuse compiled: %s\n' "$name" >&2
18 +
        exit 1
19 +
    fi
20 +
    if ! grep -q "$diagnostic" "$work/log"; then
21 +
        cat "$work/log" >&2
22 +
        exit 1
23 +
    fi
24 +
    printf 'reservation rejection: %s passed\n' "$name"
25 +
done