Phase 11
Process Model
Load and execute arbitrary userspace binaries from disk as isolated ring-3 processes, with a proper lifecycle: spawn, run, wait, exit.
Learning goal
Understand what it actually means to "run a program" at the kernel level.
Category
userspace
Components touched
Builds on
Unlocked by this phase
Why this phase matters
Load and execute arbitrary userspace binaries from disk as isolated ring-3 processes, with a proper lifecycle: spawn, run, wait, exit.
How it works in m3OS
The roadmap entry defines the learning scope for this phase; a deeper implementation doc has not been matched yet.
How it builds on earlier phases
This phase builds on Secure Boot and turns that foundation into Process Model.
Implementation outline
- Write an ELF64 parser that reads a file from the VFS and validates the header.
- Allocate a fresh page table root and map each
PT_LOADsegment at the right virtual address. - Allocate and map a userspace stack; push
argc/argv/envpin the System V ABI layout. - Wire
execveto load a new image into the calling process’s address space. - Wire
forkto copy the parent’s page tables and kernel state into a new process entry. - Wire
exitto mark the process as zombie and wake any waiting parent. - Wire
waitpidto block until a matching child transitions to zombie, then reap it.
Task progress snapshot
41 of 41 tracked tasks are complete.
Documentation targets
- Documentation deliverables have not been listed yet.
Deferred follow-on work
- copy-on-write page faults
- dynamic linking and
ld.so - process groups and sessions
clonewith shared address spaces (threads)ptraceand debugging support
Source materials
Important code
Code spotlight cards
Code spotlight
ELF64 header and program header parser
ELF64 header and program header parser. Why it matters: Correct parsing and validation of the ELF header prevents loading corrupt or incompatible binaries. Focus on `parse_ehdr` and `Ehdr` in `kernel/src/mm/elf.rs` while you trace the snippet.
fn parse_ehdr(data: &[u8]) -> Result<Ehdr, ElfError> {
if data.len() < EHDR_SIZE {
return Err(ElfError::TruncatedHeader);
}
if data[EI_MAG0..EI_MAG0 + 4] != ELFMAG {
return Err(ElfError::InvalidMagic);
}
if data[EI_CLASS] != ELFCLASS64 {
return Err(ElfError::Not64Bit);
}
if data[EI_DATA] != ELFDATA2LSB {
return Err(ElfError::NotLittleEndian);
}
let machine = read_u16_le(data, EH_MACHINE).ok_or(ElfError::TruncatedHeader)?;
if machine != EM_X86_64 {
return Err(ElfError::NotX86_64);
}
let entry = read_u64_le(data, EH_ENTRY).ok_or(ElfError::TruncatedHeader)?;
let phoff = read_u64_le(data, EH_PHOFF).ok_or(ElfError::TruncatedHeader)?;
let phentsize = read_u16_le(data, EH_PHENTSIZE).ok_or(ElfError::TruncatedHeader)?;
let phnum = read_u16_le(data, EH_PHNUM).ok_or(ElfError::TruncatedHeader)?; Code spotlight
Allocate userspace stack with guard page
Allocate userspace stack with guard page. Why it matters: A guard page below the stack catches stack overflows with a page fault instead of silent corruption. Focus on `USER_STACK_TOP` and `USER_STACK_PAGES` in `kernel/src/mm/user_space.rs` while you trace the snippet.
/// Virtual address of userspace stack top.
pub const USER_STACK_TOP: u64 = 0x0000_0000_8000_0000; // 2 GiB
/// Number of pages for userspace stack.
pub const USER_STACK_PAGES: u64 = 4; // 16 KiB
/// Map `n` pages of physical memory at `virt_base` with user-accessible flags.
///
/// Allocates fresh physical frames for each page.
///
/// # Safety
/// `mapper` must be the currently-active page table and `virt_base` must not
/// already be mapped. `virt_base` must be 4 KiB-aligned; misaligned bases
/// cause `Page::containing_address` to round down and map the wrong page.
///
/// # Error handling
/// If `map_to` fails after a frame has been allocated, that frame is leaked
/// (the frame allocator does not support deallocation in Phase 5). A mapping
/// failure at boot is unrecoverable regardless.
pub unsafe fn map_user_pages(
mapper: &mut OffsetPageTable,
virt_base: u64,
n: u64,
flags: PageTableFlags, Code spotlight
Define the Process struct
Define the Process struct. Why it matters: The Process struct is the kernel's authoritative record of each running program's state. Focus on `Process` and `ProcessState` in `kernel/src/process/mod.rs` while you trace the snippet.
/// Descriptor for a single userspace process.
pub struct Process {
/// This process's unique identifier.
pub pid: Pid,
/// Parent PID. 0 means no parent (init or an orphan).
pub ppid: Pid,
/// Current lifecycle state.
pub state: ProcessState,
/// Root physical address of this process's page table.
///
/// `None` means the process has not yet been assigned a dedicated
/// address space and shares the kernel's mappings (pre-Phase 12).
pub page_table_root: Option<x86_64::PhysAddr>,
/// Top of this process's kernel-mode stack (virtual address).
pub kernel_stack_top: u64,
/// Userspace entry-point virtual address.
pub entry_point: u64,
/// Initial userspace RSP: the virtual address of `argc` on the ABI stack,
/// as returned by `setup_abi_stack`. This is **not** the raw top of the
/// stack allocation — it points into the stack, below the argv/envp data.
pub user_stack_top: u64,
/// Exit code written when the process transitions to [`ProcessState::Zombie`].
pub exit_code: Option<i32>,
} Key files
- kernel/src/process/mod.rs — Referenced in the implementation notes.
How real OSes differ
- Production kernels implement copy-on-write `fork` so parent and child share physical pages until one writes. They also support dynamic linking (the ELF interpreter field points at `ld.so`), `execve` argument size limits, and process groups. The toy implementation uses eager copying and static linking only, which is much simpler to reason about.
Success criteria
- A statically linked ELF binary (written in Rust or C with no libc) boots from disk, runs in ring 3, and exits cleanly.
- The kernel correctly records and reports the exit code to the waiting parent.
- Two processes run concurrently without corrupting each other's address spaces.
- `init` can spawn a child, wait for it, and spawn another.
Related phases
Keep exploring the roadmap
Phase 10
CompleteSecure Boot
Sign the kernel's EFI binary so it can boot on real hardware with UEFI Secure Boot enabled, without needing to disable the firmware security check.
Phase 12
CompletePOSIX Compat
Implement enough Linux-compatible syscalls and bundle musl libc so that ordinary C programs compiled on the host can run unmodified inside the OS.