Next Generation WASM Microkernel Operating System
1// Copyright 2025 Jonas Kruckenberg
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// http://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8#![no_std]
9#![no_main]
10#![feature(used_with_arg)]
11#![feature(naked_functions)]
12#![feature(thread_local, never_type)]
13#![feature(new_range_api)]
14#![feature(debug_closure_helpers)]
15#![expect(internal_features, reason = "panic internals")]
16#![feature(std_internals, panic_can_unwind, formatting_options)]
17#![feature(step_trait)]
18#![feature(box_into_inner)]
19#![feature(let_chains)]
20#![feature(array_chunks)]
21#![feature(iter_array_chunks)]
22#![feature(iter_next_chunk)]
23#![feature(if_let_guard)]
24#![feature(allocator_api)]
25#![expect(dead_code, reason = "TODO")] // TODO remove
26#![feature(asm_unwind)]
27
28extern crate alloc;
29extern crate panic_unwind2;
30
31mod allocator;
32mod arch;
33mod backtrace;
34mod bootargs;
35mod device_tree;
36mod irq;
37mod mem;
38mod metrics;
39mod shell;
40mod state;
41#[cfg(test)]
42mod tests;
43mod tracing;
44mod util;
45mod wasm;
46
47use crate::backtrace::Backtrace;
48use crate::device_tree::DeviceTree;
49use crate::mem::bootstrap_alloc::BootstrapAllocator;
50use crate::state::{CpuLocal, Global};
51use abort::abort;
52use arrayvec::ArrayVec;
53use cfg_if::cfg_if;
54use core::range::Range;
55use core::slice;
56use core::time::Duration;
57use fastrand::FastRand;
58use kasync::executor::{Executor, Worker};
59use kasync::time::{Instant, Ticks, Timer};
60use loader_api::{BootInfo, LoaderConfig, MemoryRegionKind};
61use mem::PhysicalAddress;
62use mem::frame_alloc;
63use rand::{RngCore, SeedableRng};
64use rand_chacha::ChaCha20Rng;
65
66/// The size of the stack in pages
67pub const STACK_SIZE_PAGES: u32 = 256; // TODO find a lower more appropriate value
68/// The size of the trap handler stack in pages
69pub const TRAP_STACK_SIZE_PAGES: usize = 64; // TODO find a lower more appropriate value
70/// The initial size of the kernel heap in pages.
71///
72/// This initial size should be small enough so the loaders less sophisticated allocator can
73/// doesn't cause startup slowdown & inefficient mapping, but large enough so we can bootstrap
74/// our own virtual memory subsystem. At that point we are no longer reliant on this initial heap
75/// size and can dynamically grow the heap as needed.
76pub const INITIAL_HEAP_SIZE_PAGES: usize = 4096 * 2; // 32 MiB
77
78pub type Result<T> = anyhow::Result<T>;
79
80#[used(linker)]
81#[unsafe(link_section = ".loader_config")]
82static LOADER_CONFIG: LoaderConfig = {
83 let mut cfg = LoaderConfig::new_default();
84 cfg.kernel_stack_size_pages = STACK_SIZE_PAGES;
85 cfg
86};
87
88#[unsafe(no_mangle)]
89fn _start(cpuid: usize, boot_info: &'static BootInfo, boot_ticks: u64) -> ! {
90 panic_unwind2::set_hook(|info| {
91 tracing::error!("CPU {info}");
92
93 // FIXME 32 seems adequate for unoptimized builds where the callstack can get quite deep
94 // but (at least at the moment) is absolute overkill for optimized builds. Sadly there
95 // is no good way to do conditional compilation based on the opt-level.
96 const MAX_BACKTRACE_FRAMES: usize = 32;
97
98 let backtrace = backtrace::__rust_end_short_backtrace(|| {
99 Backtrace::<MAX_BACKTRACE_FRAMES>::capture().unwrap()
100 });
101 tracing::error!("{backtrace}");
102
103 if backtrace.frames_omitted {
104 tracing::warn!("Stack trace was larger than backtrace buffer, omitted some frames.");
105 }
106 });
107
108 // Unwinding expects at least one landing pad in the callstack, but capturing all unwinds that
109 // bubble up to this point is also a good idea since we can perform some last cleanup and
110 // print an error message.
111 let res = panic_unwind2::catch_unwind(|| {
112 backtrace::__rust_begin_short_backtrace(|| kmain(cpuid, boot_info, boot_ticks));
113 });
114
115 match res {
116 Ok(_) => arch::exit(0),
117 // If the panic propagates up to this catch here there is nothing we can do, this is a terminal
118 // failure.
119 Err(_) => {
120 tracing::error!("unrecoverable kernel panic");
121 abort()
122 }
123 }
124}
125
126fn kmain(cpuid: usize, boot_info: &'static BootInfo, boot_ticks: u64) {
127 // perform EARLY per-cpu, architecture-specific initialization
128 // (e.g. resetting the FPU)
129 arch::per_cpu_init_early();
130 tracing::per_cpu_init_early(cpuid);
131
132 let (fdt, fdt_region_phys) = locate_device_tree(boot_info);
133 let mut rng = ChaCha20Rng::from_seed(boot_info.rng_seed);
134
135 let global = state::try_init_global(|| {
136 // set up the basic functionality of the tracing subsystem as early as possible
137 tracing::init_early();
138
139 // initialize a simple bump allocator for allocating memory before our virtual memory subsystem
140 // is available
141 let allocatable_memories = allocatable_memory_regions(boot_info);
142 tracing::info!("allocatable memories: {:?}", allocatable_memories);
143 let mut boot_alloc = BootstrapAllocator::new(&allocatable_memories);
144
145 // initializing the global allocator
146 allocator::init(&mut boot_alloc, boot_info);
147
148 let device_tree = DeviceTree::parse(fdt)?;
149 tracing::debug!("{device_tree:?}");
150
151 let bootargs = bootargs::parse(&device_tree)?;
152
153 // initialize the backtracing subsystem after the allocator has been set up
154 // since setting up the symbolization context requires allocation
155 backtrace::init(boot_info, bootargs.backtrace);
156
157 // fully initialize the tracing subsystem now that we can allocate
158 tracing::init(bootargs.log);
159
160 // perform global, architecture-specific initialization
161 let arch = arch::init();
162
163 // initialize the global frame allocator
164 // at this point we have parsed and processed the flattened device tree, so we pass it to the
165 // frame allocator for reuse
166 let frame_alloc = frame_alloc::init(boot_alloc, fdt_region_phys);
167
168 // initialize the virtual memory subsystem
169 mem::init(boot_info, &mut rng, frame_alloc).unwrap();
170
171 // perform LATE per-cpu, architecture-specific initialization
172 // (e.g. setting the trap vector and enabling interrupts)
173 let cpu = arch::device::cpu::Cpu::new(&device_tree, cpuid)?;
174
175 let executor = Executor::with_capacity(boot_info.cpu_mask.count_ones() as usize);
176 let timer = Timer::new(Duration::from_millis(1), cpu.clock);
177
178 Ok(Global {
179 time_origin: Instant::from_ticks(&timer, Ticks(boot_ticks)),
180 timer,
181 executor,
182 device_tree,
183 boot_info,
184 arch,
185 })
186 })
187 .unwrap();
188
189 // perform LATE per-cpu, architecture-specific initialization
190 // (e.g. setting the trap vector and enabling interrupts)
191 let arch_state = arch::per_cpu_init_late(&global.device_tree, cpuid).unwrap();
192
193 state::init_cpu_local(CpuLocal {
194 id: cpuid,
195 arch: arch_state,
196 });
197
198 tracing::info!(
199 "Booted in ~{:?} ({:?} in k23)",
200 Instant::now(&global.timer).duration_since(Instant::ZERO),
201 Instant::from_ticks(&global.timer, Ticks(boot_ticks)).elapsed(&global.timer)
202 );
203
204 let mut worker2 = Worker::new(&global.executor, FastRand::from_seed(rng.next_u64()));
205
206 cfg_if! {
207 if #[cfg(test)] {
208 if cpuid == 0 {
209 arch::block_on(worker2.run(tests::run_tests(global))).unwrap().exit_if_failed();
210 } else {
211 arch::block_on(worker2.run(futures::future::pending::<()>())).unwrap_err(); // the only way `run` can return is when the executor is closed
212 }
213 } else {
214 shell::init(
215 &global.device_tree,
216 &global.executor,
217 boot_info.cpu_mask.count_ones() as usize,
218 );
219 arch::block_on(worker2.run(futures::future::pending::<()>())).unwrap_err(); // the only way `run` can return is when the executor is closed
220 }
221 }
222}
223
224/// Builds a list of memory regions from the boot info that are usable for allocation.
225///
226/// The regions passed by the loader are guaranteed to be non-overlapping, but might not be
227/// sorted and might not be optimally "packed". This function will both sort regions and
228/// attempt to compact the list by merging adjacent regions.
229fn allocatable_memory_regions(boot_info: &BootInfo) -> ArrayVec<Range<PhysicalAddress>, 16> {
230 let temp: ArrayVec<Range<PhysicalAddress>, 16> = boot_info
231 .memory_regions
232 .iter()
233 .filter_map(|region| {
234 let range = Range::from(
235 PhysicalAddress::new(region.range.start)..PhysicalAddress::new(region.range.end),
236 );
237
238 region.kind.is_usable().then_some(range)
239 })
240 .collect();
241
242 // merge adjacent regions
243 let mut out: ArrayVec<Range<PhysicalAddress>, 16> = ArrayVec::new();
244 'outer: for region in temp {
245 for other in &mut out {
246 if region.start == other.end {
247 other.end = region.end;
248 continue 'outer;
249 }
250 if region.end == other.start {
251 other.start = region.start;
252 continue 'outer;
253 }
254 }
255
256 out.push(region);
257 }
258
259 out
260}
261
262fn locate_device_tree(boot_info: &BootInfo) -> (&'static [u8], Range<PhysicalAddress>) {
263 let fdt = boot_info
264 .memory_regions
265 .iter()
266 .find(|region| region.kind == MemoryRegionKind::FDT)
267 .expect("no FDT region");
268
269 let base = boot_info
270 .physical_address_offset
271 .checked_add(fdt.range.start)
272 .unwrap() as *const u8;
273
274 // Safety: we need to trust the bootinfo data is correct
275 let slice =
276 unsafe { slice::from_raw_parts(base, fdt.range.end.checked_sub(fdt.range.start).unwrap()) };
277 (
278 slice,
279 Range::from(PhysicalAddress::new(fdt.range.start)..PhysicalAddress::new(fdt.range.end)),
280 )
281}