Phase 05
Userspace Entry
Run the first ring 3 program and prove that privilege separation, page permissions, and basic syscalls work.
Learning goal
Understand the boundary between ring 0 and ring 3.
Category
userspace
Components touched
Builds on
Unlocked by this phase
Why this phase matters
Run the first ring 3 program and prove that privilege separation, page permissions, and basic syscalls work.
How it works in m3OS
Phase 5 delivers the first ring-3 program running on m³OS. By the end of this phase the kernel: This is the foundational step for everything in the userspace roadmap. All later phases (IPC, servers, shell) build on the ring-3 execution model established here.
How it builds on earlier phases
This phase builds on Tasking and turns that foundation into Userspace Entry.
Implementation outline
- Create a process abstraction separate from kernel threads.
- Build a minimal userspace address space with code and stack mappings.
- Enter ring 3 through a carefully documented transition path.
- Install the syscall entry point and argument convention.
- Add one tiny userspace binary to exercise the path.
Task progress snapshot
19 of 19 tracked tasks are complete.
Documentation targets
- Documentation deliverables have not been listed yet.
Deferred follow-on work
- dynamic ELF loading
- shared libraries
- full process lifecycle management
Source materials
Important code
Code spotlight cards
Code spotlight
Define a minimal process abstraction
Define a minimal process abstraction. Why it matters: Processes need their own page tables, file descriptors, and lifecycle state — separate from the kernel task that executes them. Focus on `Process` and `ProcessState` in `kernel/src/process/mod.rs` while you trace the snippet.
/// A minimal userspace process descriptor.
pub struct Process {
/// Virtual address of the process entry point.
pub entry: u64,
/// Virtual address of the process stack top.
pub stack_top: u64,
} Code spotlight
Build a user address space
Build a user address space. Why it matters: Userspace code must execute in its own virtual address space with code, stack, and kernel-protected mappings to enforce isolation. Focus on `copy_to_user` in `kernel/src/mm/user_space.rs` while you trace the snippet.
/// Copy `src` bytes into the user code region at `virt_base`.
///
/// The region must already be mapped (e.g. via `map_user_pages`).
/// The bounds check is sized to the **code** region (`USER_CODE_PAGES * 4096`);
/// do not use this helper to write to the stack or any other user region.
pub fn copy_to_user(virt_base: u64, src: &[u8]) -> Result<(), &'static str> {
let max_bytes = (USER_CODE_PAGES * 4096) as usize;
if src.len() > max_bytes {
return Err("copy_to_user: src exceeds mapped user code region");
}
// Safety: caller guarantees virt_base is mapped and we've checked the length.
let dst = unsafe { core::slice::from_raw_parts_mut(virt_base as *mut u8, src.len()) };
dst.copy_from_slice(src);
Ok(())
} Code spotlight
Implement the transition into ring 3
Implement the transition into ring 3. Why it matters: The ring transition is the security boundary — it drops privilege from ring 0 to ring 3 using iretq with the correct segment selectors. Focus on `enter_userspace` and `iretq` in `kernel/src/arch/x86_64/mod.rs` while you trace the snippet.
/// Transfer execution to ring 3 (userspace).
///
/// Uses `iretq` to atomically switch to user code segment, user stack, and
/// the given entry point with interrupts enabled (RFLAGS.IF = 1).
///
/// # Safety
///
/// * `entry` must be a valid, mapped, executable userspace virtual address.
/// * `user_stack_top` must be a valid, mapped, writable userspace stack
/// address (highest address; stack grows downward).
/// * Must be called after `init()` so that GDT user segments are loaded.
pub unsafe fn enter_userspace(entry: u64, user_stack_top: u64) -> ! {
use core::arch::asm;
// Push the five values that `iretq` pops in order:
// SS, RSP, RFLAGS, CS, RIP
// RFLAGS: bit 9 (IF) set → interrupts enabled in userspace.
asm!(
"push {ss}",
"push {rsp}",
"push {rflags}",
"push {cs}",
"push {rip}",
"iretq",
ss = in(reg) u64::from(gdt::user_data_selector().0), Key files
How real OSes differ
- Production kernels have richer executable loading, memory permissions, security policies, and per-thread user contexts. This milestone should focus on a tiny, single-purpose userspace program so the privilege transition remains easy to follow.
Success criteria
- A userspace binary runs and returns control cleanly.
- Kernel pages are not writable from ring 3.
- The syscall path is documented and observable through logging.
- Faulting userspace code is reported without taking down the kernel silently.
Related phases
Keep exploring the roadmap
Phase 04
CompleteTasking
Introduce multiple concurrent execution contexts in the kernel and make timer-driven preemption visible and understandable.
Phase 06
CompleteIPC Core
Turn the kernel into a real microkernel by making userspace services communicate through explicit message passing and capabilities.