Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
600eb26
feat(fork): implement classic Unix fork with Copy-on-Write
stlankes Apr 8, 2026
d8bed1a
create independent fd tables for each process
stlankes Apr 8, 2026
78a75ce
fix(clippy): resolve all warnings in common-os build
stlankes Apr 8, 2026
8ac099e
mm/x86_64: fix COW page leak by reference-counting shared frames
stlankes Apr 9, 2026
0b723bd
x86_64: track page fault count in IRQ statistics
stlankes Apr 9, 2026
d812e59
syscall: add spawn_process syscall (number 16)
stlankes Apr 9, 2026
8279739
kernel: scope idle task OBJECT_MAP to unikernel builds
stlankes Apr 10, 2026
3ed1b15
fix(fork): stop leaking stack frames in copy_kernel_stack_to
stlankes Apr 11, 2026
6fd282e
register remaining syscalls in dispatch table
stlankes Apr 11, 2026
66867e8
Add user-space thread spawning with per-thread TLS for common-os
stlankes Apr 12, 2026
29aa1da
Add sys_exec syscall and clear_user_space for process image replacement
stlankes Apr 12, 2026
934079c
x86_64: clear registers before jumping into the user-space
stlankes May 3, 2026
db4b2ad
fix(aarch64): add support od macOS HVF on Apple Silicon
stlankes May 3, 2026
062a182
aarch64/mm: add common-os page-table and address-space primitives
stlankes May 3, 2026
ce06b1c
aarch64: implement load_application and jump_to_user_land
stlankes May 3, 2026
c28ce37
aarch64: SVC syscall dispatcher and EL0 vector handlers
stlankes May 3, 2026
083d39c
aarch64: fork()/exec() with copy-on-write page faults
stlankes May 3, 2026
4f61c06
remove compiler warnings
stlankes May 3, 2026
a1e4ee5
x86_64: avoid unneeded TLB flushes
stlankes May 5, 2026
c3b18ce
remove user-mode heap demand-paging
stlankes May 5, 2026
84bd6ee
aarch64: support user-space thread spawning for common-os
stlankes May 5, 2026
2effd74
introduce feature fork to enable the support of the system call fork
stlankes May 5, 2026
a81799f
fix fork refcount for non-COW user pages
stlankes May 6, 2026
faacc9c
arch: take argv by owned Vec in jump_to_user_land and drop before eret
stlankes May 6, 2026
e28af25
mm: introduce per-task VMA list and sys_mmap
stlankes May 13, 2026
3937c6a
arch: register VMAs for loaded image and per-thread TLS
stlankes May 13, 2026
336798b
arch: lazy-map user pages via VMA lookup in the fault handler
stlankes May 13, 2026
b9c7aa3
x86_64: convert VirtAddr at last/user_stack_pointer assignments
stlankes May 13, 2026
f9e3ff1
mm/vma: move user heap into L0[USER_L0_INDEX] for proper fork isolation
stlankes May 14, 2026
b12c2ac
arch: zero and refcount lazily-mapped user pages
stlankes May 14, 2026
43bb804
arch: tighten user stack layout under common-os
stlankes May 14, 2026
c7a99c1
scheduler: track a process id
stlankes May 18, 2026
5e4dcf8
x86_64/syscall: disable interrupts before swapgs on syscall return
stlankes May 18, 2026
ee42424
timer: swapgs
mkroening May 21, 2026
331be35
remove mfence
mkroening May 21, 2026
ec9d1c1
x86_64/fork: keep user RSP per-task across syscalls
stlankes May 29, 2026
1f6a2f2
cleanup: dedupe stdio bootstrap and tighten imports
stlankes Jun 1, 2026
8800a1e
arch: canonicalize module paths and enable common-os on aarch64
stlankes Jun 22, 2026
379818d
style: apply rustfmt and tidy up imports across arch modules
stlankes Jul 12, 2026
d8bef4b
remove some typos
stlankes Jul 12, 2026
974ae43
remove clippy and compiler warnings
stlankes Jul 12, 2026
4ca68ae
pass argument vector and environment to user processes
stlankes Jul 12, 2026
ab37371
add riscv64 support for spawn_process and exec (common-os)
stlankes Jul 13, 2026
2575d7e
add fork with copy-on-write for riscv64
stlankes Jul 13, 2026
44e7cab
enable the virtual timer interrupt on application processors
stlankes Jul 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ default = [
## Note that this feature is not complete yet.
common-os = []

## Enable the support of classical fork
fork = ["common-os"]

## Enables support for memory management system calls.
##
## This feature enables functions similar to [sys/mman.h].
Expand Down
244 changes: 215 additions & 29 deletions src/arch/aarch64/kernel/interrupts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ const PPI_START: u8 = 16;
const SPI_START: u8 = 32;
/// Software-generated interrupt for rescheduling
pub(crate) const SGI_RESCHED: u8 = 1;
/// Synthetic IRQ slot used for page-fault accounting. The number does not
/// correspond to any GIC interrupt — it is purely a bookkeeping ID for
/// `IrqStatistics` so the page-fault count shows up in `print_statistics`
/// alongside the real interrupts. Picked at 14 to mirror the x86_64
/// CPU exception vector for #PF, which keeps the two architectures
/// consistent in the diagnostic output.
#[cfg(feature = "common-os")]
pub(crate) const PAGE_FAULT_IRQ: u8 = 14;

/// Number of the timer interrupt
static mut TIMER_INTERRUPT: u32 = 0;
Expand Down Expand Up @@ -170,43 +178,138 @@ pub(crate) extern "C" fn do_irq(_state: &State) -> *mut usize {
}

#[unsafe(no_mangle)]
pub(crate) extern "C" fn do_sync(state: &State) {
pub(crate) extern "C" fn do_sync(state: &mut State) {
#[cfg(all(feature = "common-os", feature = "fork"))]
use crate::arch::mm::paging::do_cow_fault;

let esr = ESR_EL1.get();
let ec_raw = ESR_EL1.read(ESR_EL1::EC);
let ec: ESR_EL1::EC::Value = ESR_EL1.read_as_enum(ESR_EL1::EC).unwrap();
let iss = ESR_EL1.read(ESR_EL1::ISS);
let pc = ELR_EL1.get();

/* data abort from lower or current level */
if (ec == ESR_EL1::EC::Value::DataAbortCurrentEL)
|| (ec == ESR_EL1::EC::Value::DataAbortLowerEL)
{
/* check if value in far_el1 is valid */
if (iss & (1 << 10)) == 0 {
/* read far_el1 register, which holds the faulting virtual address */
let far = FAR_EL1.get();

// add page fault handler

error!("Current stack pointer {state:p}");
error!("Unable to handle page fault at {far:#x}");
error!("Exception return address {:#x}", ELR_EL1.get());
error!("Thread ID register {:#x}", TPIDR_EL0.get());
error!("Table Base Register {:#x}", TTBR0_EL1.get());
error!("Exception Syndrome Register {esr:#x}");

if let Some(irqid) =
GicCpuInterface::get_and_acknowledge_interrupt(InterruptGroup::Group1)
// SVC64 — user-space syscall (ESR_EL1.EC = 0x15). Dispatched here
// before any of the other branches so we can return early without
// touching debug/breakpoint/FPU paths.
#[cfg(feature = "common-os")]
if ec_raw == 0x15 {
dispatch_svc64(state);
return;
}

// User-mode instruction-fetch fault at PC=0: the entry wrapper of a
// freshly spawned user thread (`std::sys::thread::hermit::Thread::
// new_with_coreid::thread_start`) returns with `ret`, popping LR
// from the user stack. The trap frame we crafted in
// `Task::create_user_stack_frame` zeroed every register, so LR=0
// and the implicit branch lands at PC 0 — there is no code there.
// Mirror the x86_64 page-fault handler and treat this as a clean
// thread exit instead of crashing the whole process.
#[cfg(feature = "common-os")]
if ec == ESR_EL1::EC::Value::InstrAbortLowerEL && ELR_EL1.get() == 0 {
use crate::scheduler::PerCoreSchedulerExt;
debug!(
"User thread {} returned from entry; exiting cleanly.",
core_scheduler().get_current_task_id()
);
core_scheduler().exit(0);
}

/* Data Abort from current or lower EL — the primary path is a COW
* write fault from EL0 (EC=0x25). EC=0x24 covers a kernel write to a
* COW-marked page, which can happen e.g. when the kernel writes
* argv/envp into the freshly-mapped user page during the loader path.
*/
if ec == ESR_EL1::EC::Value::DataAbortCurrentEL || ec == ESR_EL1::EC::Value::DataAbortLowerEL {
#[cfg(feature = "common-os")]
increment_irq_counter(PAGE_FAULT_IRQ);

let far = FAR_EL1.get();
// ESR_EL1.ISS layout for Data Abort (ARM ARM D24.2.45):
// bits 5:0 = DFSC (Data Fault Status Code)
// bit 6 = WnR (1 = write, 0 = read)
// Permission fault DFSC values are 0b001100..0b001111 (level 0..3).
let dfsc = iss & 0b11_1111;
let is_write = (iss & (1 << 6)) != 0;
#[cfg(all(feature = "common-os", feature = "fork"))]
let is_permission_fault = (0b00_1100..=0b00_1111).contains(&dfsc);

#[cfg(all(feature = "common-os", feature = "fork"))]
if is_write && is_permission_fault && do_cow_fault(VirtAddr::new(far)) {
// Faulting instruction is retried on `eret` from the trap.
return;
}

#[cfg(feature = "common-os")]
{
use core::ops::Bound;

use align_address::Align;

use crate::mm::FrameAlloc;
use crate::mm::vma::VirtualMemoryAreaProt;

let addr = VirtAddr::new(far).align_down(BasePageSize::SIZE);
let current_task = core_scheduler().get_current_task();
let current_task_borrowed = current_task.borrow();
let guard = current_task_borrowed.vmas.read();

if let Some((_, vma)) = guard
.range((Bound::Unbounded, Bound::Included(addr)))
.next_back() && addr >= vma.start
&& addr < vma.end
{
GicCpuInterface::end_interrupt(irqid, InterruptGroup::Group1);
} else {
error!("Unable to acknowledge interrupt!");
let layout = PageLayout::from_size_align(
BasePageSize::SIZE as usize,
BasePageSize::SIZE as usize,
)
.unwrap();
let frame_range = FrameAlloc::allocate(layout).unwrap();
let physaddr = PhysAddr::from(frame_range.start());
let mut flags = PageTableEntryFlags::empty();
flags.normal().user();
if vma.prot.contains(VirtualMemoryAreaProt::WRITE) {
flags.writable();
}
if vma.prot.contains(VirtualMemoryAreaProt::EXECUTE) {
flags.execute_disable();
}

paging::map::<BasePageSize>(addr, physaddr, 1, flags);

// clear page
let slice = unsafe {
core::slice::from_raw_parts_mut(
addr.as_mut_ptr::<u8>().cast::<u8>(),
BasePageSize::SIZE as usize,
)
};
slice.fill(0);

#[cfg(feature = "fork")]
crate::mm::frame_ref_inc(physaddr);

return;
}
}

let kind = dfsc_kind(dfsc);
let access = if is_write { "write" } else { "read" };
error!("Current stack pointer {state:p}");
error!("Unhandled data abort: {kind} on {access} of {far:#x} (DFSC={dfsc:#x})");
error!("Exception return address {:#x}", ELR_EL1.get());
error!("Thread ID register {:#x}", TPIDR_EL0.get());
error!("Table Base Register {:#x}", TTBR0_EL1.get());
error!("Exception Syndrome Register {esr:#x}");

scheduler::abort()
if let Some(irqid) = GicCpuInterface::get_and_acknowledge_interrupt(InterruptGroup::Group1)
{
GicCpuInterface::end_interrupt(irqid, InterruptGroup::Group1);
} else {
error!("Unknown exception");
error!("Unable to acknowledge interrupt!");
}

scheduler::abort()
} else if ec == ESR_EL1::EC::Value::Brk64 {
error!("Trap to debugger, PC={pc:#x}");
loop {
Expand All @@ -232,6 +335,64 @@ pub(crate) extern "C" fn do_sync(state: &State) {
}
}

/// Convert the 6-bit DFSC (Data Fault Status Code) of `ESR_EL1.ISS` into
/// a short human-readable label for diagnostics. Mapping per ARM ARM
/// D24.2.45 (ESR_EL1, Data Abort).
fn dfsc_kind(dfsc: u64) -> &'static str {
match dfsc {
0b00_0000..=0b00_0011 => "address size fault",
0b00_0100..=0b00_0111 => "translation fault",
0b00_1000..=0b00_1011 => "access flag fault",
0b00_1100..=0b00_1111 => "permission fault",
0b01_0000 => "synchronous external abort",
0b01_0001 => "synchronous tag check fail",
0b01_0100..=0b01_0111 => "external abort on translation table walk",
0b01_1000 => "synchronous parity/ECC error",
0b01_1100..=0b01_1111 => "parity/ECC error on translation table walk",
0b10_0001 => "alignment fault",
0b11_0000 => "TLB conflict abort",
0b11_0001 => "unsupported atomic hardware-update fault",
_ => "unknown data fault",
}
}

/// Dispatch an EL0-issued AArch64 syscall (`svc #0`).
///
/// Hermit's user-space follows the Linux-like AArch64 convention:
/// - syscall number in `x8`
/// - up to 6 arguments in `x0`..`x5`
/// - return value in `x0`
///
/// The handler entries in `SYSHANDLER_TABLE` are typed for the SystemV
/// x86_64 ABI but are reachable just as well via the AAPCS64 ABI: in both
/// cases the first six 64-bit args land in the first six argument
/// registers and the return value goes into the first return register.
/// Registers in `state` were saved by `trap_entry` and are restored by
/// `trap_exit` — writing back `state.x0` is what propagates the return
/// value to user-space.
#[cfg(feature = "common-os")]
fn dispatch_svc64(state: &mut State) {
use crate::errno::Errno;
use crate::syscalls::table::{NO_SYSCALLS, SYSHANDLER_TABLE, invalid_syscall, sys_invalid};

let nr = state.x8 as usize;

if nr >= NO_SYSCALLS {
error!("Invalid syscall number {nr}");
state.x0 = u64::from((-i32::from(Errno::Nosys)) as u32);
return;
}

let handler_ptr = SYSHANDLER_TABLE.handler(nr);
if handler_ptr == sys_invalid as *const usize {
invalid_syscall(state.x8);
}

let f: extern "C" fn(u64, u64, u64, u64, u64, u64) -> u64 =
unsafe { core::mem::transmute(handler_ptr) };
state.x0 = f(state.x0, state.x1, state.x2, state.x3, state.x4, state.x5);
}

#[unsafe(no_mangle)]
pub(crate) extern "C" fn do_bad_mode(_state: &State, reason: u32) -> ! {
error!("Receive unhandled exception: {reason}");
Expand Down Expand Up @@ -335,11 +496,26 @@ pub(crate) fn init() {
if let Some(timer_node) = fdt.find_compatible(&["arm,armv8-timer", "arm,armv7-timer"]) {
let irq_slice = timer_node.property("interrupts").unwrap().value;

/* Secure Phys IRQ */
// The "arm,armv8-timer" interrupts property lists four (type, irq,
// flags) triplets in this exact order:
// 1. Secure Phys
// 2. Non-secure Phys
// 3. Virtual ← we want this one
// 4. Hypervisor Phys
// We program the Virtual Timer (CNTV_*) instead of the Physical
// Timer because virtualised guests (e.g. macOS HVF) hide the
// physical timer from EL1, and CNTV_* works identically on bare
// metal where CNTVOFF_EL2 defaults to 0.

/* Secure Phys IRQ — skip */
let (_irqtype, irq_slice) = irq_slice.split_at(size_of::<u32>());
let (_irq, irq_slice) = irq_slice.split_at(size_of::<u32>());
let (_irqflags, irq_slice) = irq_slice.split_at(size_of::<u32>());
/* Non-secure Phys IRQ */
/* Non-secure Phys IRQ — skip */
let (_irqtype, irq_slice) = irq_slice.split_at(size_of::<u32>());
let (_irq, irq_slice) = irq_slice.split_at(size_of::<u32>());
let (_irqflags, irq_slice) = irq_slice.split_at(size_of::<u32>());
/* Virtual Timer IRQ */
let (irqtype, irq_slice) = irq_slice.split_at(size_of::<u32>());
let (irq, irq_slice) = irq_slice.split_at(size_of::<u32>());
let (irqflags, _irq_slice) = irq_slice.split_at(size_of::<u32>());
Expand Down Expand Up @@ -426,6 +602,8 @@ pub(crate) fn init() {
.unwrap();
gic.enable_interrupt(reschedid, Some(cpu_id), true).unwrap();
IRQ_NAMES.lock().insert(SGI_RESCHED, "Reschedule");
#[cfg(feature = "common-os")]
IRQ_NAMES.lock().insert(PAGE_FAULT_IRQ, "Page Fault");

*GIC.lock() = Some(gic);
}
Expand All @@ -449,7 +627,15 @@ pub fn init_cpu() {

if let Some(timer_node) = fdt.find_compatible(&["arm,armv8-timer", "arm,armv7-timer"]) {
let irq_slice = timer_node.property("interrupts").unwrap().value;
/* Secure Phys IRQ */
// Select the Virtual Timer triplet, matching `init()`: the kernel
// programs the virtual timer (CNTV_*), and timer PPIs are per-core,
// so every application processor has to enable the *same* interrupt
// in its redistributor that the boot core selected.
/* Secure Phys IRQ — skip */
let (_irqtype, irq_slice) = irq_slice.split_at(size_of::<u32>());
let (_irq, irq_slice) = irq_slice.split_at(size_of::<u32>());
let (_irqflags, irq_slice) = irq_slice.split_at(size_of::<u32>());
/* Non-secure Phys IRQ — skip */
let (_irqtype, irq_slice) = irq_slice.split_at(size_of::<u32>());
let (_irq, irq_slice) = irq_slice.split_at(size_of::<u32>());
let (_irqflags, irq_slice) = irq_slice.split_at(size_of::<u32>());
Expand Down
Loading
Loading