Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
628de5e
feat(network): support packet capture file creation
cagatay-y Feb 25, 2026
1893ac5
feat(network): support packet capture file creation
cagatay-y Feb 25, 2026
958971d
feat(network): support packet capture file creation
cagatay-y Feb 25, 2026
e28ad60
feat(network): support packet capture file creation
cagatay-y Feb 25, 2026
fdc858d
feat(fork): implement classic Unix fork with Copy-on-Write
stlankes Apr 8, 2026
b53a442
create independent fd tables for each process
stlankes Apr 8, 2026
96004fe
fix(clippy): resolve all warnings in common-os build
stlankes Apr 8, 2026
ffbb20e
mm/x86_64: fix COW page leak by reference-counting shared frames
stlankes Apr 9, 2026
54baf55
x86_64: track page fault count in IRQ statistics
stlankes Apr 9, 2026
4265c7d
syscall: add spawn_process syscall (number 16)
stlankes Apr 9, 2026
ee2db1c
kernel: scope idle task OBJECT_MAP to unikernel builds
stlankes Apr 10, 2026
327967c
fix(fork): stop leaking stack frames in copy_kernel_stack_to
stlankes Apr 11, 2026
f30d53b
register remaining syscalls in dispatch table
stlankes Apr 11, 2026
1ab5b80
Add user-space thread spawning with per-thread TLS for common-os
stlankes Apr 12, 2026
562c663
Add sys_exec syscall and clear_user_space for process image replacement
stlankes Apr 12, 2026
2d02650
x86_64: clear registers before jumping into the user-space
stlankes May 3, 2026
2352627
fix(aarch64): add support od macOS HVF on Apple Silicon
stlankes May 3, 2026
d72964f
aarch64/mm: add common-os page-table and address-space primitives
stlankes May 3, 2026
4ca63a6
aarch64: implement load_application and jump_to_user_land
stlankes May 3, 2026
13c4340
aarch64: SVC syscall dispatcher and EL0 vector handlers
stlankes May 3, 2026
d0eb0dd
aarch64: fork()/exec() with copy-on-write page faults
stlankes May 3, 2026
a436c95
remove compiler warnings
stlankes May 3, 2026
b86a65f
x86_64: avoid unneeded TLB flushes
stlankes May 5, 2026
e1e3ea1
remove user-mode heap demand-paging
stlankes May 5, 2026
230df90
aarch64: support user-space thread spawning for common-os
stlankes May 5, 2026
26c8ed7
introduce feature fork to enable the support of the system call fork
stlankes May 5, 2026
df2d1ca
fix fork refcount for non-COW user pages
stlankes May 6, 2026
5131976
arch: take argv by owned Vec in jump_to_user_land and drop before eret
stlankes May 6, 2026
3f21c24
mm: introduce per-task VMA list and sys_mmap
stlankes May 13, 2026
af34090
arch: register VMAs for loaded image and per-thread TLS
stlankes May 13, 2026
5fdf449
arch: lazy-map user pages via VMA lookup in the fault handler
stlankes May 13, 2026
2320383
x86_64: convert VirtAddr at last/user_stack_pointer assignments
stlankes May 13, 2026
ef24432
mm/vma: move user heap into L0[USER_L0_INDEX] for proper fork isolation
stlankes May 14, 2026
84bcb24
arch: zero and refcount lazily-mapped user pages
stlankes May 14, 2026
facb6ce
arch: tighten user stack layout under common-os
stlankes May 14, 2026
f1c0701
scheduler: track a process id
stlankes May 18, 2026
310ace4
x86_64/syscall: disable interrupts before swapgs on syscall return
stlankes May 18, 2026
db73fd1
timer: swapgs
mkroening May 21, 2026
1d65ce8
remove mfence
mkroening May 21, 2026
c1d1ea0
x86_64/fork: keep user RSP per-task across syscalls
stlankes May 29, 2026
7df086c
cleanup: dedupe stdio bootstrap and tighten imports
stlankes Jun 1, 2026
c0dadd7
arch: canonicalize module paths and enable common-os on aarch64
stlankes Jun 22, 2026
d658dca
style: apply rustfmt and tidy up imports across arch modules
stlankes Jul 12, 2026
1a461d9
remove some typos
stlankes Jul 12, 2026
bc8fb70
remove clippy and compiler warnings
stlankes Jul 12, 2026
511c632
pass argument vector and environment to user processes
stlankes Jul 12, 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
234 changes: 206 additions & 28 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 Down
Loading
Loading