feat: add Rust userspace support; fix TLS layout for p_align < 16; kill whole process on sibling-thread faults

This commit is contained in:
daniel committed 2026-10-14 07:16:42 +02:00
1 parent a81298fb24
commit f8b8f9610f
31 files changed
+1076 -38

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
[package]
name = "montauk-rt"
version = "0.1.0"
edition = "2024"
description = "Minimal no_std runtime for Rust programs on MontaukOS"
build = "build.rs"
[lib]
path = "src/lib.rs"
+65
View File
@@ -0,0 +1,65 @@
// Generate syscall numbers and status codes from the C++ headers so Rust
// never becomes another hand-maintained copy of the syscall table.
//
// programs/include/Api/Syscall.hpp -> pub const SYS_*: u64
// kernel/src/Common/StatusCodes.hpp -> pub enum Status (positive values,
// matching montauk/statuscodes.h)
use std::env;
use std::fs;
use std::path::PathBuf;
fn main() {
let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let repo = manifest.join("../../..");
let syscall_hpp = repo.join("programs/include/Api/Syscall.hpp");
let status_hpp = repo.join("kernel/src/Common/StatusCodes.hpp");
println!("cargo:rerun-if-changed={}", syscall_hpp.display());
println!("cargo:rerun-if-changed={}", status_hpp.display());
let mut out = String::from("// @generated by montauk-rt/build.rs - do not edit.\n\n");
// static constexpr uint64_t SYS_NAME = 123; (decimal literals only)
let syscalls = fs::read_to_string(&syscall_hpp).expect("read Syscall.hpp");
let mut count = 0;
for line in syscalls.lines() {
let line = line.trim();
let Some(rest) = line.strip_prefix("static constexpr uint64_t SYS_") else { continue };
let Some((name, value)) = rest.split_once('=') else { continue };
let value = value.split(';').next().unwrap_or("").trim();
if value.is_empty() || !value.bytes().all(|b| b.is_ascii_digit()) {
continue;
}
out.push_str(&format!("pub const SYS_{}: u64 = {};\n", name.trim(), value));
count += 1;
}
assert!(count > 100, "parsed only {count} syscalls from {}", syscall_hpp.display());
// enum StatusCodes { Success = 0, GenericFailure = -1, ... };
let status = fs::read_to_string(&status_hpp).expect("read StatusCodes.hpp");
let body = status
.split_once("enum StatusCodes")
.and_then(|(_, rest)| rest.split_once('{'))
.and_then(|(_, rest)| rest.split_once('}'))
.map(|(body, _)| body)
.expect("enum StatusCodes not found");
out.push_str("\n/// Kernel status codes, stored positive like montauk/statuscodes.h.\n");
out.push_str("/// The kernel returns them negated; see [`Status::from_result`].\n");
out.push_str("#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(i32)]\npub enum Status {\n");
let mut variants = Vec::new();
for entry in body.split(',') {
let entry = entry.split("//").next().unwrap_or("").trim();
let Some((name, value)) = entry.split_once('=') else { continue };
let value: i32 = value.trim().parse().expect("status value");
out.push_str(&format!(" {} = {},\n", name.trim(), -value));
variants.push((name.trim().to_string(), -value));
}
out.push_str("}\n\nimpl Status {\n pub const fn from_code(code: i32) -> Status {\n match code {\n");
for (name, value) in &variants {
out.push_str(&format!(" {} => Status::{},\n", value, name));
}
out.push_str(" _ => Status::GenericFailure,\n }\n }\n}\n");
let dest = PathBuf::from(env::var("OUT_DIR").unwrap()).join("abi.rs");
fs::write(dest, out).expect("write abi.rs");
}
+48
View File
@@ -0,0 +1,48 @@
//! Command-line arguments, captured from crt1.o's argv by [`crate::entry!`].
use core::ffi::CStr;
use core::sync::atomic::{AtomicPtr, AtomicUsize, Ordering};
static ARGC: AtomicUsize = AtomicUsize::new(0);
static ARGV: AtomicPtr<*const u8> = AtomicPtr::new(core::ptr::null_mut());
/// # Safety
/// `argv` must point to `argc` NUL-terminated strings that live forever.
#[doc(hidden)]
pub unsafe fn init(argc: i32, argv: *const *const u8) {
ARGV.store(argv as *mut *const u8, Ordering::Relaxed);
ARGC.store(argc.max(0) as usize, Ordering::Release);
}
/// Iterator over the program arguments; argv[0] is the executable path.
/// Arguments that are not valid UTF-8 are yielded lossily as `"?"`.
pub fn args() -> Args {
Args { next: 0 }
}
pub struct Args {
next: usize,
}
impl Iterator for Args {
type Item = &'static str;
fn next(&mut self) -> Option<&'static str> {
let argc = ARGC.load(Ordering::Acquire);
if self.next >= argc {
return None;
}
let argv = ARGV.load(Ordering::Relaxed);
// SAFETY: init() guarantees argc valid, immortal C strings.
let s = unsafe { CStr::from_ptr(*argv.add(self.next) as *const core::ffi::c_char) };
self.next += 1;
Some(s.to_str().unwrap_or("?"))
}
fn size_hint(&self) -> (usize, Option<usize>) {
let left = ARGC.load(Ordering::Acquire).saturating_sub(self.next);
(left, Some(left))
}
}
impl ExactSizeIterator for Args {}
+66
View File
@@ -0,0 +1,66 @@
//! Global allocator over the Montauk libc heap (process-wide locked, so
//! safe across threads). libc malloc returns 16-byte aligned blocks; larger
//! alignments over-allocate and stash the original pointer just below the
//! aligned block.
use core::alloc::{GlobalAlloc, Layout};
use core::ptr;
unsafe extern "C" {
fn malloc(size: usize) -> *mut u8;
fn free(ptr: *mut u8);
fn realloc(ptr: *mut u8, size: usize) -> *mut u8;
}
const MALLOC_ALIGN: usize = 16;
pub struct LibcAllocator;
unsafe impl GlobalAlloc for LibcAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
unsafe {
if layout.align() <= MALLOC_ALIGN {
return malloc(layout.size().max(1));
}
let Some(total) = layout.size().checked_add(layout.align() + size_of::<usize>()) else {
return ptr::null_mut();
};
let raw = malloc(total);
if raw.is_null() {
return raw;
}
let start = raw as usize + size_of::<usize>();
let aligned = (start + layout.align() - 1) & !(layout.align() - 1);
*((aligned - size_of::<usize>()) as *mut usize) = raw as usize;
aligned as *mut u8
}
}
unsafe fn dealloc(&self, p: *mut u8, layout: Layout) {
unsafe {
if layout.align() <= MALLOC_ALIGN {
free(p);
} else {
free(*((p as usize - size_of::<usize>()) as *const usize) as *mut u8);
}
}
}
unsafe fn realloc(&self, p: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
unsafe {
if layout.align() <= MALLOC_ALIGN {
return realloc(p, new_size.max(1));
}
let new_layout = Layout::from_size_align_unchecked(new_size, layout.align());
let q = self.alloc(new_layout);
if !q.is_null() {
ptr::copy_nonoverlapping(p, q, layout.size().min(new_size));
self.dealloc(p, layout);
}
q
}
}
}
#[global_allocator]
static ALLOCATOR: LibcAllocator = LibcAllocator;
+82
View File
@@ -0,0 +1,82 @@
//! Terminal output. `print!`/`println!` format into a stack buffer and hand
//! it to SYS_PRINT in NUL-terminated chunks, so one call is one (or a few)
//! syscalls rather than libc write()'s one SYS_PUTCHAR per byte.
use core::fmt::{self, Write};
use crate::sys;
const CHUNK: usize = 256;
/// Buffered writer to the process's terminal (stdout).
pub struct Stdout {
buf: [u8; CHUNK],
len: usize,
}
impl Stdout {
pub const fn new() -> Self {
Stdout { buf: [0; CHUNK], len: 0 }
}
pub fn write_bytes(&mut self, bytes: &[u8]) {
for &b in bytes {
if b == 0 {
// SYS_PRINT stops at NUL; send it on its own.
self.flush();
unsafe { sys::syscall1(sys::SYS_PUTCHAR, 0) };
continue;
}
if self.len == CHUNK - 1 {
self.flush();
}
self.buf[self.len] = b;
self.len += 1;
}
}
pub fn flush(&mut self) {
if self.len == 0 {
return;
}
self.buf[self.len] = 0;
unsafe { sys::syscall1(sys::SYS_PRINT, self.buf.as_ptr() as u64) };
self.len = 0;
}
}
impl Default for Stdout {
fn default() -> Self {
Self::new()
}
}
impl Write for Stdout {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.write_bytes(s.as_bytes());
Ok(())
}
}
impl Drop for Stdout {
fn drop(&mut self) {
self.flush();
}
}
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
let mut out = Stdout::new();
let _ = out.write_fmt(args);
}
#[macro_export]
macro_rules! print {
($($arg:tt)*) => { $crate::io::_print(format_args!($($arg)*)) };
}
#[macro_export]
macro_rules! println {
() => { $crate::print!("\n") };
($($arg:tt)*) => { $crate::io::_print(format_args!("{}\n", format_args!($($arg)*))) };
}
+49
View File
@@ -0,0 +1,49 @@
//! Minimal `no_std` runtime for Rust programs on MontaukOS.
//!
//! Phase 1 of Rust support: `core` + `alloc` on a stock nightly with a
//! custom target spec. Programs link against the Montauk libc through the
//! x86_64-montauk GCC driver, so crt1.o owns `_start` (argv, environment,
//! .init_array) and calls the `main` that [`entry!`] defines.
//!
//! ```ignore
//! #![no_std]
//! #![no_main]
//! montauk_rt::entry!(main);
//! fn main() -> i32 { montauk_rt::println!("hi"); 0 }
//! ```
#![no_std]
extern crate alloc;
pub mod sys;
pub mod io;
pub mod heap;
pub mod env;
pub mod process;
pub mod sync;
pub mod thread;
pub mod time;
mod panic;
pub use sys::Status;
/// Define the C `main` that crt1.o calls, forwarding to `fn() -> i32`.
#[macro_export]
macro_rules! entry {
($main:path) => {
const _: () = {
// Exported as the C symbol `main`; the Rust name stays free so
// the program can call its own entry function `main`.
#[unsafe(export_name = "main")]
extern "C" fn __montauk_rt_main(argc: i32, argv: *const *const u8) -> i32 {
// SAFETY: crt1.o passes its static argv table, which lives
// for the whole program and is never modified after startup.
unsafe { $crate::env::init(argc, argv) };
let f: fn() -> i32 = $main;
f()
}
};
};
}
+14
View File
@@ -0,0 +1,14 @@
use core::fmt::Write;
use core::panic::PanicInfo;
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
let mut out = crate::io::Stdout::new();
let _ = write!(out, "\nthread {} panicked", crate::thread::current_id());
if let Some(loc) = info.location() {
let _ = write!(out, " at {}:{}:{}", loc.file(), loc.line(), loc.column());
}
let _ = writeln!(out, ":\n{}", info.message());
out.flush();
crate::process::abort()
}
+35
View File
@@ -0,0 +1,35 @@
//! Process control.
use crate::sys;
unsafe extern "C" {
// libc exit(): runs atexit handlers (and .fini_array), then SYS_EXIT.
#[link_name = "exit"]
fn libc_exit(code: i32) -> !;
}
/// Exit the process. From a sibling thread this ends only that thread
/// (kernel semantics); use [`abort`] to take the whole process down.
pub fn exit(code: i32) -> ! {
unsafe { libc_exit(code) }
}
pub fn id() -> u32 {
unsafe { sys::syscall0(sys::SYS_GETPID) as u32 }
}
/// Terminate the whole process from any thread.
pub fn abort() -> ! {
unsafe {
if crate::thread::current_id() == id() {
sys::syscall1(sys::SYS_EXIT, 134);
} else {
// Sibling: SYS_EXIT would end only this thread. Self-kill flags
// every thread; park here until the kernel reaps us.
sys::syscall1(sys::SYS_KILL, id() as u64);
}
loop {
sys::syscall0(sys::SYS_YIELD);
}
}
}
+125
View File
@@ -0,0 +1,125 @@
//! Futex-backed synchronization (SYS_FUTEX_WAIT / SYS_FUTEX_WAKE).
use core::cell::UnsafeCell;
use core::ops::{Deref, DerefMut};
use core::sync::atomic::{AtomicU32, Ordering};
use core::time::Duration;
use crate::sys::{self, Status};
/// Block while `*word == expected`. `Ok(())` means woken by [`futex_wake`];
/// `Err(WouldBlock)` means the word had already changed; `Err(TimedOut)`
/// means the deadline passed. Always re-check your condition afterwards.
pub fn futex_wait(word: &AtomicU32, expected: u32, timeout: Option<Duration>) -> Result<(), Status> {
let ns = match timeout {
None => sys::FUTEX_WAIT_FOREVER,
Some(d) => d.as_nanos().min((sys::FUTEX_WAIT_FOREVER - 1) as u128) as u64,
};
let rc = unsafe {
sys::syscall3(sys::SYS_FUTEX_WAIT, word.as_ptr() as u64, expected as u64, ns)
};
Status::from_result(rc).map(|_| ())
}
/// Wake up to `count` waiters on `word`, longest-waiting first.
/// Returns how many were woken.
pub fn futex_wake(word: &AtomicU32, count: u32) -> u32 {
let rc = unsafe { sys::syscall2(sys::SYS_FUTEX_WAKE, word.as_ptr() as u64, count as u64) };
Status::from_result(rc).unwrap_or(0) as u32
}
// ====================================================================
// Mutex: Drepper's three-state futex mutex ("Futexes Are Tricky", #3).
// 0 = unlocked, 1 = locked, 2 = locked with possible waiters.
// ====================================================================
pub struct Mutex<T: ?Sized> {
state: AtomicU32,
data: UnsafeCell<T>,
}
unsafe impl<T: ?Sized + Send> Send for Mutex<T> {}
unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}
impl<T> Mutex<T> {
pub const fn new(value: T) -> Self {
Mutex { state: AtomicU32::new(0), data: UnsafeCell::new(value) }
}
pub fn into_inner(self) -> T {
self.data.into_inner()
}
}
impl<T: ?Sized> Mutex<T> {
pub fn lock(&self) -> MutexGuard<'_, T> {
if self.state.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed).is_err() {
self.lock_contended();
}
MutexGuard { mutex: self }
}
pub fn try_lock(&self) -> Option<MutexGuard<'_, T>> {
self.state
.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
.ok()
.map(|_| MutexGuard { mutex: self })
}
#[cold]
fn lock_contended(&self) {
// Brief spin first: most critical sections are short.
for _ in 0..100 {
if self.state.load(Ordering::Relaxed) == 0
&& self.state.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed).is_ok()
{
return;
}
core::hint::spin_loop();
}
while self.state.swap(2, Ordering::Acquire) != 0 {
let _ = futex_wait(&self.state, 2, None);
}
}
fn unlock(&self) {
if self.state.swap(0, Ordering::Release) == 2 {
futex_wake(&self.state, 1);
}
}
pub fn get_mut(&mut self) -> &mut T {
self.data.get_mut()
}
}
impl<T: Default> Default for Mutex<T> {
fn default() -> Self {
Mutex::new(T::default())
}
}
pub struct MutexGuard<'a, T: ?Sized> {
mutex: &'a Mutex<T>,
}
unsafe impl<T: ?Sized + Sync> Sync for MutexGuard<'_, T> {}
impl<T: ?Sized> Deref for MutexGuard<'_, T> {
type Target = T;
fn deref(&self) -> &T {
unsafe { &*self.mutex.data.get() }
}
}
impl<T: ?Sized> DerefMut for MutexGuard<'_, T> {
fn deref_mut(&mut self) -> &mut T {
unsafe { &mut *self.mutex.data.get() }
}
}
impl<T: ?Sized> Drop for MutexGuard<'_, T> {
fn drop(&mut self) {
self.mutex.unlock();
}
}
+75
View File
@@ -0,0 +1,75 @@
//! Raw syscall interface. Numbers and status codes are generated from
//! programs/include/Api/Syscall.hpp and kernel/src/Common/StatusCodes.hpp.
use core::arch::asm;
include!(concat!(env!("OUT_DIR"), "/abi.rs"));
/// `SYS_FUTEX_WAIT` timeout meaning "no deadline".
pub const FUTEX_WAIT_FOREVER: u64 = !0;
/// `SYS_FUTEX_WAKE` count meaning "every waiter".
pub const FUTEX_WAKE_ALL: u32 = u32::MAX;
impl Status {
/// Convert a raw syscall result: negative values are negated status
/// codes, everything else (handles, counts, sizes) is success.
pub fn from_result(rc: i64) -> Result<u64, Status> {
if rc >= 0 {
Ok(rc as u64)
} else if rc < i32::MIN as i64 {
Err(Status::GenericFailure)
} else {
Err(Status::from_code(-(rc as i32)))
}
}
}
// The kernel's SYSCALL path clobbers rcx/r11 (return RIP/RFLAGS) and, like
// the C++ wrappers in montauk/syscall.h, is treated as clobbering every
// argument register.
#[inline(always)]
pub unsafe fn syscall0(nr: u64) -> i64 {
let ret: i64;
unsafe {
asm!("syscall", inlateout("rax") nr as i64 => ret,
out("rcx") _, out("r11") _, out("rdi") _, out("rsi") _, out("rdx") _,
out("r8") _, out("r9") _, out("r10") _, options(nostack));
}
ret
}
#[inline(always)]
pub unsafe fn syscall1(nr: u64, a1: u64) -> i64 {
let ret: i64;
unsafe {
asm!("syscall", inlateout("rax") nr as i64 => ret, inlateout("rdi") a1 => _,
out("rcx") _, out("r11") _, out("rsi") _, out("rdx") _,
out("r8") _, out("r9") _, out("r10") _, options(nostack));
}
ret
}
#[inline(always)]
pub unsafe fn syscall2(nr: u64, a1: u64, a2: u64) -> i64 {
let ret: i64;
unsafe {
asm!("syscall", inlateout("rax") nr as i64 => ret, inlateout("rdi") a1 => _,
inlateout("rsi") a2 => _,
out("rcx") _, out("r11") _, out("rdx") _,
out("r8") _, out("r9") _, out("r10") _, options(nostack));
}
ret
}
#[inline(always)]
pub unsafe fn syscall3(nr: u64, a1: u64, a2: u64, a3: u64) -> i64 {
let ret: i64;
unsafe {
asm!("syscall", inlateout("rax") nr as i64 => ret, inlateout("rdi") a1 => _,
inlateout("rsi") a2 => _, inlateout("rdx") a3 => _,
out("rcx") _, out("r11") _,
out("r8") _, out("r9") _, out("r10") _, options(nostack));
}
ret
}
+93
View File
@@ -0,0 +1,93 @@
//! Threads (SYS_THREAD_SPAWN / SYS_THREAD_JOIN).
//!
//! The kernel uses the caller's stack verbatim and delivers one argument in
//! %rdi. A thread cannot free the stack it runs on, so the [`JoinHandle`]
//! owns it and frees it after a successful join; dropping an unjoined
//! handle leaks the stack (the thread may still be running on it).
use alloc::alloc::{alloc, dealloc, Layout};
use alloc::boxed::Box;
use crate::sys::{self, Status};
pub const DEFAULT_STACK_SIZE: usize = 256 * 1024;
const STACK_ALIGN: usize = 16;
pub struct JoinHandle {
tid: u32,
stack: *mut u8,
stack_layout: Layout,
}
unsafe impl Send for JoinHandle {}
impl JoinHandle {
pub fn id(&self) -> u32 {
self.tid
}
/// Wait for the thread to finish and return its exit code.
pub fn join(self) -> Result<i32, Status> {
let mut code: i32 = 0;
let rc = unsafe {
sys::syscall2(sys::SYS_THREAD_JOIN, self.tid as u64, &mut code as *mut i32 as u64)
};
// The kernel reports a bad tid as -1 (GenericFailure).
Status::from_result(rc)?;
unsafe { dealloc(self.stack, self.stack_layout) };
Ok(code)
}
}
/// Spawn a thread running `f`; its return value becomes the exit code.
pub fn spawn<F>(f: F) -> Result<JoinHandle, Status>
where
F: FnOnce() -> i32 + Send + 'static,
{
spawn_with_stack(DEFAULT_STACK_SIZE, f)
}
pub fn spawn_with_stack<F>(stack_size: usize, f: F) -> Result<JoinHandle, Status>
where
F: FnOnce() -> i32 + Send + 'static,
{
let stack_layout = Layout::from_size_align(stack_size.max(4096), STACK_ALIGN)
.map_err(|_| Status::InvalidArgument)?;
let stack = unsafe { alloc(stack_layout) };
if stack.is_null() {
return Err(Status::OutOfMemory);
}
let closure = Box::into_raw(Box::new(f));
let top = (stack as usize + stack_layout.size()) & !(STACK_ALIGN - 1);
let rc = unsafe {
sys::syscall3(sys::SYS_THREAD_SPAWN, trampoline::<F> as *const () as u64, closure as u64, top as u64)
};
match Status::from_result(rc) {
Ok(tid) => Ok(JoinHandle { tid: tid as u32, stack, stack_layout }),
Err(e) => unsafe {
drop(Box::from_raw(closure));
dealloc(stack, stack_layout);
Err(e)
},
}
}
extern "C" fn trampoline<F>(arg: *mut F) -> !
where
F: FnOnce() -> i32,
{
let f = unsafe { Box::from_raw(arg) };
let code = f();
unsafe { sys::syscall1(sys::SYS_THREAD_EXIT, code as u32 as u64) };
unreachable!()
}
/// The calling thread's id (equals the process id on the main thread).
pub fn current_id() -> u32 {
unsafe { sys::syscall0(sys::SYS_THREAD_SELF) as u32 }
}
pub fn yield_now() {
unsafe { sys::syscall0(sys::SYS_YIELD) };
}
+18
View File
@@ -0,0 +1,18 @@
//! Time and sleeping.
use core::time::Duration;
use crate::sys;
/// Milliseconds since boot (monotonic).
pub fn uptime_ms() -> u64 {
unsafe { sys::syscall0(sys::SYS_GETMILLISECONDS) as u64 }
}
/// Sleep for at least `d`, rounded up to the 1 ms scheduler tick.
pub fn sleep(d: Duration) {
let ms = d.as_millis().min(u64::MAX as u128) as u64 + u64::from(d.subsec_nanos() % 1_000_000 != 0);
if ms > 0 {
unsafe { sys::syscall1(sys::SYS_SLEEP_MS, ms) };
}
}