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