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

+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once
#define MONTAUK_BUILD_NUMBER 302
#define MONTAUK_BUILD_NUMBER 306
+4
View File
@@ -221,6 +221,10 @@ namespace montauk::abi {
}
static int Sys_Kill(int pid) {
// A process may always kill itself: a sibling thread has no other
// way to take the whole process down (SYS_EXIT from a sibling ends
// only that thread), which abort()/panic=abort need.
if (pid == Sched::GetCurrentPid()) return Sched::KillProcess(pid);
if (!Sched::HasCapability(CAP_PROCESS_ADMIN)) {
int ancestor = pid;
bool descendant = false;
+11 -1
View File
@@ -177,7 +177,17 @@ namespace Hal {
if (i == 0x00) sig = 8; /* #DE -> SIGFPE */
else if (i == 0x06) sig = 4; /* #UD -> SIGILL */
else if (i == 0x10 || i == 0x13) sig = 8; /* x87/SIMD FP */
Sched::SetProcessExitCode(proc->pid, 256 + sig);
// ExitProcess from a sibling thread ends only that thread,
// which would leave the process running with the crash
// swallowed. Flag the whole process (main thread and every
// sibling) first, carrying the crash code rather than
// KillProcess's default SIGKILL, so waiters see the fault.
if (Sched::GetCurrentThreadPtr() != proc) {
Sched::KillProcess(proc->pid, 256 + sig);
} else {
Sched::SetProcessExitCode(proc->pid, 256 + sig);
}
}
Sched::ExitProcess();
+7 -10
View File
@@ -536,16 +536,13 @@ namespace Sched {
}
// Build the main thread's TLS block (x86-64 variant II): the block
// lives below the thread pointer, TP = base + S where
// S = align_up(memsz, align), template data at [base, base+filesz),
// .tbss zeroed, and [TP] holds the ABI-required self-pointer.
// lives below the thread pointer (see ComputeTlsLayout), template
// data first, .tbss zeroed, and [TP] holds the ABI self-pointer.
if (tlsPhdr != nullptr && outTls != nullptr) {
uint64_t align = tlsPhdr->p_align < 16 ? 16 : tlsPhdr->p_align;
uint64_t S = (tlsPhdr->p_memsz + align - 1) & ~(align - 1);
TlsLayout layout = ComputeTlsLayout(tlsPhdr->p_memsz, tlsPhdr->p_align);
uint64_t base = (maxVaddrEnd + 0x1FFFULL) & ~0xFFFULL; // page-aligned, 1 guard page gap
uint64_t total = S + 16; // + minimal TCB
uint64_t numPages = (total + 0xFFF) / 0x1000;
uint64_t tp = base + S;
uint64_t numPages = (layout.totalSize + 0xFFF) / 0x1000;
uint64_t tp = base + layout.tpOffset;
bool ok = true;
for (uint64_t p = 0; p < numPages && ok; p++) {
@@ -562,7 +559,7 @@ namespace Sched {
}
// Copy the .tdata template bytes that land in this page.
if (!copyPageFromFile(physAddr, virtAddr, base,
if (!copyPageFromFile(physAddr, virtAddr, base + layout.templateOffset,
tlsPhdr->p_offset, tlsPhdr->p_filesz)) {
ok = false;
break;
@@ -579,7 +576,7 @@ namespace Sched {
outTls->templateVaddr = tlsPhdr->p_vaddr;
outTls->fileSize = tlsPhdr->p_filesz;
outTls->memSize = tlsPhdr->p_memsz;
outTls->align = align;
outTls->align = tlsPhdr->p_align;
} else {
Kt::KernelLogStream(Kt::ERROR, "ELF")
<< "PT_TLS setup failed (out of pages?), "
+22 -1
View File
@@ -60,9 +60,30 @@ namespace Sched {
uint64_t templateVaddr; // source of .tdata in the loaded image
uint64_t fileSize; // initialized bytes
uint64_t memSize; // total bytes (incl. .tbss)
uint64_t align;
uint64_t align; // PT_TLS p_align, NOT rounded up
};
// Geometry of one variant II TLS block, as offsets from its
// page-aligned base. The linker addresses TLS variables at
// TP - align_up(memSize, p_align) + offset, so the template must start
// exactly that far below TP; rounding p_align up first (as an older
// version did) shifts every variable away from its initial value.
// TP itself is kept at least 16-byte aligned, which the gap between
// base and the template absorbs.
struct TlsLayout {
uint64_t templateOffset; // where .tdata/.tbss start
uint64_t tpOffset; // thread pointer; [TP] holds the self-pointer
uint64_t totalSize; // bytes to map, incl. the 16-byte TCB
};
inline TlsLayout ComputeTlsLayout(uint64_t memSize, uint64_t pAlign) {
uint64_t align = pAlign ? pAlign : 1;
uint64_t block = (memSize + align - 1) & ~(align - 1);
uint64_t tpAlign = align < 16 ? 16 : align;
uint64_t tp = (block + tpAlign - 1) & ~(tpAlign - 1);
return {tp - block, tp, tp + 16};
}
uint64_t ElfLoad(const char* vfsPath, uint64_t pml4Phys, TlsInfo* outTls);
// Load a shared library into a fixed address range.
+16 -16
View File
@@ -720,10 +720,8 @@ namespace Sched {
uint64_t threadTlsPages = 0;
Process& primary = processTable[primarySlot_];
if (primary.tlsMemSize > 0) {
uint64_t align = primary.tlsAlign < 16 ? 16 : primary.tlsAlign;
uint64_t S = (primary.tlsMemSize + align - 1) & ~(align - 1);
uint64_t total = S + 16;
uint64_t numPages = (total + 0xFFF) / 0x1000;
TlsLayout layout = ComputeTlsLayout(primary.tlsMemSize, primary.tlsAlign);
uint64_t numPages = (layout.totalSize + 0xFFF) / 0x1000;
uint64_t base = 0;
if (!ReserveUserHeapRange(primarySlot_, numPages * 0x1000, base)) {
@@ -757,9 +755,9 @@ namespace Sched {
return -1;
}
uint64_t tp = base + S;
memcpy((void*)base, (const void*)primary.tlsTemplateVaddr,
primary.tlsFileSize);
uint64_t tp = base + layout.tpOffset;
memcpy((void*)(base + layout.templateOffset),
(const void*)primary.tlsTemplateVaddr, primary.tlsFileSize);
*(uint64_t*)tp = tp; /* ABI self-pointer */
threadFsBase = tp;
threadTlsBase = base;
@@ -945,14 +943,13 @@ namespace Sched {
return;
}
uint64_t align = primary.tlsAlign < 16 ? 16 : primary.tlsAlign;
uint64_t blockSize = (primary.tlsMemSize + align - 1) & ~(align - 1);
if (blockSize > thr.fsBase || blockSize > UINT64_MAX - 16) {
TlsLayout layout = ComputeTlsLayout(primary.tlsMemSize, primary.tlsAlign);
if (layout.tpOffset > thr.fsBase || layout.totalSize < layout.tpOffset) {
Panic("Invalid sibling TLS metadata during thread teardown", nullptr);
}
uint64_t base = thr.fsBase - blockSize;
uint64_t pages = (blockSize + 16 + 0xFFF) / 0x1000;
uint64_t base = thr.fsBase - layout.tpOffset;
uint64_t pages = (layout.totalSize + 0xFFF) / 0x1000;
thr.fsBase = 0;
Memory::UnmapAndFreeUserRange(primary.pml4Phys, base, pages);
ReleaseUserHeapRange(primarySlot_, base, pages * 0x1000ULL);
@@ -1791,10 +1788,13 @@ namespace Sched {
}
}
int KillProcess(int pid) {
// Refuse to kill PID 0 (init) or caller's own process
int KillProcess(int pid, int exitCode) {
// Refuse to kill PID 0 (init). Killing the caller's own process is
// allowed: a faulting/aborting sibling thread shares the pid and has
// no other way to take the process down. The caller's own slot gets
// killPending below and exits at syscall return (ExitIfKilled) or
// via the ExitProcess the fault handler calls next.
if (pid == 0) return -1;
if (pid == GetCurrentPid()) return -1;
schedLock.Acquire();
@@ -1817,7 +1817,7 @@ namespace Sched {
return -1;
}
processTable[primarySlot_].exitCode = 256 + 9; /* killed (SIGKILL) */
processTable[primarySlot_].exitCode = exitCode;
// Flag the main thread so its next tick (or scheduler dispatch)
// routes through ExitProcess, which sweeps every sibling thread
+3 -2
View File
@@ -302,9 +302,10 @@ namespace Sched {
int FutexWake(uint64_t uaddr, uint32_t count);
// Kill a process by PID. If the process is running on another CPU,
// sets a kill-pending flag checked on the next timer tick.
// sets a kill-pending flag checked on the next timer tick. exitCode is
// what a waiter sees (256+sig; SIGKILL by default).
// Returns 0 on success, -1 on failure.
int KillProcess(int pid);
int KillProcess(int pid, int exitCode = 256 + 9);
// Start a new process session for the caller, or terminate every process
// belonging to a session. KillSession returns the number of live members
+23 -3
View File
@@ -96,9 +96,9 @@ WPDIR := data/wallpapers
WPSRC := $(wildcard $(WPDIR)/*.jpg)
WPDST := $(patsubst $(WPDIR)/%,$(BINDIR)/os/wallpapers/%,$(WPSRC))
.PHONY: all clean 2048 fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities keyboard sshserver sshd terminal syslog procmgr powermgr calculator charmap login desktop shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs icons fonts configs osdata bearssl libc tls libjpeg libjpegwrite install-apps libloader crashpad rtlsdr sdr check-syscalls gen-syscalls
.PHONY: all clean 2048 fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities keyboard sshserver sshd terminal syslog procmgr powermgr calculator charmap login desktop shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs icons fonts configs osdata bearssl libc tls libjpeg libjpegwrite install-apps libloader crashpad rtlsdr sdr check-syscalls gen-syscalls rust
all: bearssl libc libjpeg libjpegwrite tls libloader rtlsdr sdr devkit $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities sshserver terminal syslog procmgr powermgr calculator charmap 2048 paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs login desktop shell sshd icons fonts install-apps crashpad $(MANDST) $(WWWDST) $(CA_CERTS) $(CONFIGDST) $(OSDATADST) $(FWDST) $(LICDST) $(WPDST)
all: bearssl libc libjpeg libjpegwrite tls libloader rtlsdr sdr devkit $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities sshserver terminal syslog procmgr powermgr calculator charmap 2048 paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs login desktop shell sshd icons fonts install-apps crashpad rust $(MANDST) $(WWWDST) $(CA_CERTS) $(CONFIGDST) $(OSDATADST) $(FWDST) $(LICDST) $(WPDST)
# Build BearSSL static library (cross-compiled for freestanding x86_64).
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
@@ -320,6 +320,26 @@ mandelbrot: libc
screenshot: libc libjpegwrite libloader dialogs
$(MAKE) -C src/screenshot
# Rust programs (programs/rust, Phase 1: no_std + alloc via -Zbuild-std).
# Optional: skipped with a notice when no rustup-managed cargo with the
# pinned nightly is installed, so the default build never requires Rust.
# RUSTUP_AUTO_INSTALL=0 keeps 'make' from downloading a toolchain itself.
# Rust links the toolchain sysroot libc (like ports), not lib/libc directly:
# re-run build-montauk-toolchain.sh after libc changes.
CARGO ?= $(firstword $(wildcard $(HOME)/.cargo/bin/cargo) $(shell command -v cargo 2>/dev/null))
RUST_DIR := rust
RUST_OUT := $(RUST_DIR)/target/x86_64-unknown-montauk/release
RUST_PROGRAMS := rusthello
rust:
@if [ -z "$(CARGO)" ] || ! (cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 $(CARGO) --version >/dev/null 2>&1); then \
echo "rust: no nightly cargo found - skipping Rust programs (see programs/rust/README.md)"; \
else \
(cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 $(CARGO) build --release) || exit 1; \
mkdir -p $(BINDIR)/os; \
for p in $(RUST_PROGRAMS); do cp $(RUST_OUT)/$$p $(BINDIR)/os/$$p.elf || exit 1; done; \
fi
# Verify the TCC C API header (libc/montauk.h) exposes every kernel syscall with
# matching numbers. Fails the build on drift; 'make gen-syscalls' refreshes them.
check-syscalls:
@@ -486,7 +506,7 @@ else
endif
clean:
rm -rf $(BINDIR) obj
rm -rf $(BINDIR) obj $(RUST_DIR)/target
$(MAKE) -C lib/bearssl clean
$(MAKE) -C lib/libc clean
$(MAKE) -C lib/libjpeg clean
+6 -3
View File
@@ -32,6 +32,9 @@ typedef enum {
WouldBlock = 16
} StatusCode;
// Highest defined StatusCode; update when adding a code above.
enum { MaxStatusCode = WouldBlock };
inline const char* GetStatusText(StatusCode statusCode) {
const char* statusText[] = {
"Success", // 0
@@ -53,7 +56,7 @@ inline const char* GetStatusText(StatusCode statusCode) {
"Operation would block", // 16
};
if (statusCode > 0 && statusCode <= 16) {
if (statusCode > 0 && statusCode <= MaxStatusCode) {
return statusText[statusCode];
} else {
return "Unknown error";
@@ -63,7 +66,7 @@ inline const char* GetStatusText(StatusCode statusCode) {
// Map a raw syscall result to a StatusCode.
inline StatusCode StatusFromResult(long long rc) {
if (rc >= 0) return Success;
if (rc < -(long long)WouldBlock) return GenericFailure;
if (rc < -(long long)MaxStatusCode) return GenericFailure;
return (StatusCode)(-rc);
}
@@ -89,7 +92,7 @@ inline const char* GetStatusExplanationText(StatusCode statusCode) {
0, // 16
};
if (statusCode > 0 && statusCode <= 16) {
if (statusCode > 0 && statusCode <= MaxStatusCode) {
if (explanationText[statusCode] != 0) {
return explanationText[statusCode];
}
+15
View File
@@ -0,0 +1,15 @@
# Phase 1 Rust support: no_std + alloc on a stock nightly, built with
# -Zbuild-std against the custom target spec next to this directory.
# Paths below are relative to programs/rust/ (the parent of .cargo/).
[build]
target = "x86_64-unknown-montauk.json"
[unstable]
build-std = ["core", "alloc"]
json-target-spec = true
[target.x86_64-unknown-montauk]
# The montauk GCC driver supplies crt1/crti/crtbegin, -lc and libgcc, and
# links the static non-PIE ET_EXEC layout the kernel ELF loader expects.
linker = "../../toolchain/local/bin/x86_64-montauk-gcc"
+1
View File
@@ -0,0 +1 @@
target/
+14
View File
@@ -0,0 +1,14 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "montauk-rt"
version = "0.1.0"
[[package]]
name = "rusthello"
version = "0.1.0"
dependencies = [
"montauk-rt",
]
+13
View File
@@ -0,0 +1,13 @@
[workspace]
resolver = "2"
members = ["montauk-rt", "rusthello"]
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
opt-level = "s"
lto = true
codegen-units = 1
debug = false
+70
View File
@@ -0,0 +1,70 @@
# Rust on MontaukOS
Phase 1: `no_std` + `alloc` programs, cross-compiled from Linux with a
stock nightly toolchain and a custom target spec. No compiler fork.
## Setup (once)
```bash
curl --proto '=https' -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain none
~/.cargo/bin/rustup toolchain install nightly --component rust-src
```
`rust-toolchain.toml` selects nightly + `rust-src` for this directory.
`make programs` builds the Rust programs when that toolchain is present
and prints a notice and skips them otherwise. It never downloads a toolchain
(`RUSTUP_AUTO_INSTALL=0`).
## Building
```bash
make -C programs rust # cargo build --release, copies to bin/os/*.elf
cd programs/rust && cargo build --release
```
Output: `target/x86_64-unknown-montauk/release/<name>`, installed as
`programs/bin/os/<name>.elf`. Add new binaries to `RUST_PROGRAMS` in
`programs/GNUmakefile` and to the workspace `members`.
## Layout
| Path | Purpose |
|------|---------|
| `x86_64-unknown-montauk.json` | Target spec: static non-PIE ET_EXEC, no red zone, local-exec TLS, panic=abort |
| `.cargo/config.toml` | `-Zbuild-std=core,alloc`, linker = `x86_64-montauk-gcc` |
| `montauk-rt/` | Runtime: syscalls, allocator, `print!`, argv, threads, futex `Mutex`, panic handler |
| `rusthello/` | Smoke test (`rusthello`, `rusthello panic`, `rusthello panic-thread`) |
The GCC driver links crt1/crti/crtbegin, the libc and libgcc from
`toolchain/sysroot`, the same as ports. After changing `programs/lib/libc`,
re-run `toolchain/build-montauk-toolchain.sh` so Rust programs pick it up.
Syscall numbers and status codes are generated at build time by
`montauk-rt/build.rs` from `programs/include/Api/Syscall.hpp` and
`kernel/src/Common/StatusCodes.hpp`, so there is nothing to keep in sync by hand.
## Writing a program
```rust
#![no_std]
#![no_main]
extern crate alloc;
montauk_rt::entry!(main);
fn main() -> i32 {
montauk_rt::println!("hello from Rust");
0
}
```
## Limits (Phase 1)
- No `std`. No `std::fs`, `std::net`, `std::process` or `std::thread`; use
`montauk_rt` equivalents where they exist.
- `panic = "abort"`. A panic on any thread prints the message and kills the
whole process.
- Thread stacks are owned by the `JoinHandle`. An unjoined thread leaks its stack.
Phase 2 (a forked rustc at `~/dev/mtk/rust` with a native `target_os =
"montauk"` std) replaces the JSON spec with a built-in target.
+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) };
}
}
+3
View File
@@ -0,0 +1,3 @@
[toolchain]
channel = "nightly"
components = ["rust-src"]
+7
View File
@@ -0,0 +1,7 @@
[package]
name = "rusthello"
version = "0.1.0"
edition = "2024"
[dependencies]
montauk-rt = { path = "../montauk-rt" }
+148
View File
@@ -0,0 +1,148 @@
//! rusthello - Phase 1 Rust smoke test for MontaukOS.
//!
//! Exercises alloc collections, formatting, argv, a #[thread_local]
//! (PT_TLS + per-thread %fs blocks), threads, and the futex mutex.
//! `rusthello panic` / `rusthello panic-thread` check that a panic on
//! either thread takes the whole process down; `rusthello fault-thread`
//! checks the same for a page fault on a worker thread.
#![no_std]
#![no_main]
#![feature(thread_local)]
extern crate alloc;
use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::cell::Cell;
use core::time::Duration;
use montauk_rt::sync::Mutex;
use montauk_rt::{entry, println, thread, time};
entry!(main);
static mut FAILURES: u32 = 0;
fn check(ok: bool, name: &str) {
println!("{} {}", if ok { "PASS" } else { "FAIL" }, name);
if !ok {
unsafe { FAILURES += 1 };
}
}
#[thread_local]
static TLS_COUNTER: Cell<u64> = Cell::new(7);
const THREADS: usize = 4;
const ITERS: u64 = 20_000;
fn main() -> i32 {
println!("rusthello: Rust on MontaukOS (no_std + alloc)");
let args: Vec<&str> = montauk_rt::env::args().collect();
println!("argv = {:?}", args);
match args.get(1).copied() {
Some("panic") => panic!("requested panic on the main thread"),
Some("fault-thread") => {
// A CPU fault (not a panic) on a worker must still kill the
// whole process, with the SIGSEGV exit code (267).
let h = thread::spawn(|| {
unsafe { core::ptr::write_volatile(8 as *mut u64, 0xdead) };
0
})
.unwrap();
let _ = h.join();
thread::yield_now();
println!("FAIL process survived a worker-thread fault");
return 1;
}
Some("panic-thread") => {
let h = thread::spawn(|| panic!("requested panic on a worker thread")).unwrap();
let _ = h.join();
println!("FAIL process survived a worker-thread panic");
return 1;
}
_ => {}
}
// --- alloc ---
let mut v: Vec<u64> = (1..=100).collect();
v.retain(|x| x % 3 == 0);
check(v.len() == 33 && v.iter().sum::<u64>() == 1683, "Vec collect/retain/sum");
let mut s = String::new();
for word in ["Montauk", "speaks", "Rust"] {
if !s.is_empty() {
s.push(' ');
}
s.push_str(word);
}
check(s == "Montauk speaks Rust", "String building");
check(format!("{:>6.2}|{:#x}|{:?}", 3.14159f64, 255, Some("x")) == " 3.14|0xff|Some(\"x\")",
"format! with floats, hex, Debug");
let mut map = BTreeMap::new();
for (i, w) in s.split(' ').enumerate() {
map.insert(w, i);
}
check(map.get("Rust") == Some(&2) && map.len() == 3, "BTreeMap");
let big: Vec<u8> = alloc::vec![0xA5; 1 << 20];
check(big.iter().all(|&b| b == 0xA5), "1 MiB allocation");
#[repr(align(4096))]
struct PageAligned([u8; 64]);
let page = alloc::boxed::Box::new(PageAligned([1; 64]));
check((&*page as *const PageAligned as usize) % 4096 == 0 && page.0[63] == 1, "over-aligned Box (4096)");
// --- thread-local storage ---
check(TLS_COUNTER.get() == 7, "#[thread_local] initial value on main thread");
TLS_COUNTER.set(1000);
let tls_seen = thread::spawn(|| {
let initial = TLS_COUNTER.get();
TLS_COUNTER.set(55);
initial as i32
})
.unwrap()
.join();
check(tls_seen == Ok(7), "worker thread gets a fresh TLS block");
check(TLS_COUNTER.get() == 1000, "main thread TLS untouched by worker");
// --- threads + futex mutex ---
let counter = Arc::new(Mutex::new(0u64));
let mut handles = Vec::new();
for t in 0..THREADS {
let counter = counter.clone();
handles.push(
thread::spawn(move || {
for i in 0..ITERS {
let mut g = counter.lock();
let v = *g;
if i % 512 == 0 {
thread::yield_now(); // widen the race window under the lock
}
*g = v + 1;
}
t as i32 + 10
})
.unwrap(),
);
}
let codes: Vec<_> = handles.into_iter().map(|h| h.join()).collect();
check(codes.iter().enumerate().all(|(t, c)| *c == Ok(t as i32 + 10)), "thread exit codes via join");
check(*counter.lock() == THREADS as u64 * ITERS, "futex Mutex keeps the counter exact");
// --- time ---
let t0 = time::uptime_ms();
time::sleep(Duration::from_millis(25));
let dt = time::uptime_ms() - t0;
check((25..1000).contains(&dt), "sleep(25 ms)");
let failures = unsafe { FAILURES };
println!("rusthello: {}", if failures == 0 { "all passed" } else { "FAILURES" });
if failures == 0 { 0 } else { 1 }
}
+32
View File
@@ -0,0 +1,32 @@
{
"arch": "x86_64",
"cpu": "x86-64",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"llvm-target": "x86_64-unknown-none-elf",
"os": "montauk",
"vendor": "unknown",
"target-pointer-width": 64,
"max-atomic-width": 64,
"executables": true,
"linker-flavor": "gnu-cc",
"linker": "x86_64-montauk-gcc",
"no-default-libraries": false,
"crt-objects-fallback": "false",
"relocation-model": "static",
"code-model": "small",
"position-independent-executables": false,
"static-position-independent-executables": false,
"relro-level": "off",
"disable-redzone": true,
"has-thread-local": true,
"tls-model": "local-exec",
"panic-strategy": "abort",
"plt-by-default": false,
"stack-probes": { "kind": "inline" },
"metadata": {
"description": "MontaukOS userspace (static ET_EXEC, Montauk libc)",
"host_tools": false,
"std": false,
"tier": 3
}
}
+1 -1
View File
@@ -39,7 +39,7 @@ static void print_exit_code(const int exitCode) {
montauk::print(c);
}
if (exitCode > 0 && exitCode <= 14) { // 0-14 - current range of status codes within StatusCodes enum
if (exitCode > 0 && exitCode <= montauk::MaxStatusCode) {
const char* statusText = montauk::GetStatusText((montauk::StatusCode)exitCode);
if (statusText != nullptr) {