Phase 06
IPC Core
Turn the kernel into a real microkernel by making userspace services communicate through explicit message passing and capabilities.
Learning goal
Understand why microkernels move services out of ring 0.
Category
foundations
Components touched
Builds on
Unlocked by this phase
Why this phase matters
Turn the kernel into a real microkernel by making userspace services communicate through explicit message passing and capabilities.
How it works in m3OS
Phase 6 turns m³OS into a real microkernel by wiring up the IPC subsystem that all future userspace servers depend on. By the end of this phase the kernel: Everything from Phase 7 onward — the shell, console server, VFS — sits on top of this foundation. If the IPC contract is clean, those higher layers are straightforward. If it is muddled, nothing above it is trustworthy.
How it builds on earlier phases
This phase builds on Userspace Entry and turns that foundation into IPC Core.
Implementation outline
- Define kernel IPC objects and task wait states.
- Create a per-process capability table with explicit validation.
- Implement synchronous message transfer first.
- Add reply-and-wait server flow once the basic path is stable.
- Add notification objects for interrupts and one-way signals.
Task progress snapshot
26 of 26 tracked tasks are complete.
Documentation targets
- Documentation deliverables have not been listed yet.
Deferred follow-on work
- Large page-grant transfers
- IPC timeouts and cancellation
- Advanced scheduling policies around IPC
Source materials
Important code
Code spotlight cards
Code spotlight
Define kernel IPC objects for endpoints and notifications
Define kernel IPC objects for endpoints and notifications. Why it matters: These are the kernel objects through which all inter-process communication flows — endpoints for synchronous rendezvous, notifications for async signals. Focus on `Message` and `Endpoint` in `kernel/src/ipc/message.rs` while you trace the snippet.
/// A small, register-sized IPC message.
///
/// `label` identifies the operation (similar to a method selector in Mach or
/// a message tag in seL4). `data` carries up to four 64-bit words of inline
/// payload — enough for most control operations.
///
/// Capability grants are deferred to Phase 7+. For now, if a server needs to
/// share memory with a client it must use a pre-arranged shared address.
#[derive(Debug, Clone, Copy, Default)]
pub struct Message {
/// Operation identifier, chosen by convention between sender and receiver.
pub label: u64,
/// Inline data payload — up to 4 machine words.
pub data: [u64; 4],
} Code spotlight
Add a per-process capability table with validation
Add a per-process capability table with validation. Why it matters: Every IPC operation must go through capability validation — a process can only use handles it legitimately holds, preventing capability forgery. Focus on `CapabilityTable` and `CapHandle` in `kernel/src/ipc/capability.rs` while you trace the snippet.
/// Fixed-size per-process capability table (64 slots).
///
/// `None` means the slot is empty. Slots are filled on [`insert`][Self::insert]
/// and cleared on [`remove`][Self::remove].
pub struct CapabilityTable {
slots: [Option<Capability>; Self::SIZE],
} Code spotlight
Implement blocking recv and send primitives
Implement blocking recv and send primitives. Why it matters: Synchronous IPC requires the ability to block the calling task until a partner arrives on the endpoint, then transfer the message and wake both sides. Focus on `recv_msg` and `send_msg` in `kernel/src/ipc/endpoint.rs` while you trace the snippet.
/// Receive a message from an endpoint.
///
/// If a sender is already waiting, dequeue it, wake it (if it used `send`
/// rather than `call`), copy its message, and return the message label.
/// If the endpoint is for a `call`, insert a reply capability into the
/// server's table instead of waking the sender immediately.
///
/// If no sender is waiting, the calling task blocks until one arrives.
///
/// Returns the message label on success, or `u64::MAX` on error.
pub fn recv(receiver: TaskId, ep_id: EndpointId) -> u64 {
let action = {
let mut reg = ENDPOINTS.lock();
let ep = match reg.get_mut(ep_id) {
Some(e) => e,
None => return u64::MAX,
};
if let Some(pending) = ep.senders.pop_front() {
Some(pending)
} else {
// No sender yet — enqueue self and block.
ep.receivers.push_back(receiver);
None
} Key files
- kernel/src/ipc/endpoint.rs — cross-table move — see transfer_cap in kernel/src/ipc/endpoint.rs and
- kernel/src/arch/x86_64/syscall.rs — authoritative mapping lives in kernel/src/arch/x86_64/syscall.rs, and
- userspace/syscall-lib — userspace uses it via userspace/syscall-lib. Treat the table above as
- kernel/src/mm/user_mem.rs — kernel/src/mm/user_mem.rs) to transfer bytes through the caller's page
- kernel-core/src/ipc/buffer.rs — validate_user_buffer(addr, len) (defined in kernel-core/src/ipc/buffer.rs),
- kernel/src/ipc/mod.rs — kernel/src/ipc/mod.rs — module overview and syscall dispatcher
- kernel/src/ipc/notification.rs — kernel/src/ipc/notification.rs — async notification objects
- kernel/src/ipc/capability.rs — kernel/src/ipc/capability.rs — per-task capability table
- kernel/src/task/scheduler.rs — kernel/src/task/scheduler.rs — IPC scheduler primitives
How real OSes differ
- Real microkernels often include more message registers, stronger formal models, priority-aware scheduling interactions, and carefully tuned fast paths.
- Production systems like seL4 use formal verification to prove IPC correctness.
- This project keeps the IPC contract small and explicit so the reader can trace every state change during a message exchange.
Success criteria
- A client can call a server and receive a reply.
- Invalid capabilities are rejected safely.
- Server loops use the intended `reply_recv` pattern.
- IRQ-driven notifications can wake a userspace task.
Related phases
Keep exploring the roadmap
Phase 05
CompleteUserspace Entry
Run the first ring 3 program and prove that privilege separation, page permissions, and basic syscalls work.
Phase 07
CompleteCore Servers
Use IPC to assemble the first useful userspace services and prove that the system can be structured as cooperating servers rather than one large kernel.