m³OS docs GitHub

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.

Complete

Learning goal

Understand what it actually means to "run a program" at the kernel level.

Category

userspace

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_LOAD segment at the right virtual address.
  • Allocate and map a userspace stack; push argc/argv/envp in the System V ABI layout.
  • Wire execve to load a new image into the calling process’s address space.
  • Wire fork to copy the parent’s page tables and kernel state into a new process entry.
  • Wire exit to mark the process as zombie and wake any waiting parent.
  • Wire waitpid to 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
  • clone with shared address spaces (threads)
  • ptrace and debugging support

Source materials

Important code

Code spotlight cards

Code spotlight

ELF64 header and program header parser

kernel/src/mm/elf.rs L132-L155

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

kernel/src/mm/user_space.rs L24-L47

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

kernel/src/process/mod.rs L85-L108

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