Isolation

A Lambda C actor runs at the lowest privilege the chip offers and reaches the kernel through exactly one syscall gate. And it cannot reach another actor's memory.

TargetPrivilege dropGateActor-vs-actor confinement
x86_64iretq to ring 3int 0x80an address space per actor
riscv64sret to U-modeecallan address space per actor (Sv39)
aarch64eret to EL0svcan address space per actor
RV32, MMU-lessto U-modeecalla PMP region per actor
ARMv6-M, MMU-lessto unprivileged Thread modesvcan MPU region per actor ¹

¹ On the Cortex-M0+, in the images that carry confined actors — both the node image and the flight-control image.

One address space per actor (the MMU targets)

On x86_64, riscv64 and aarch64 each actor gets its own page tables. The kernel's tables are cloned per actor; the user bit is cleared on every other actor's VM context and user stack; and the scheduler loads that actor's root (CR3 / satp / TTBR0) on every context switch.

A sibling's pages stay present and writable to privileged code, which is what keeps the kernel working under any address space — the timer trap, the syscall gate, and the FFI that delivers a message into another actor's mailbox all still reach that memory. Only the unprivileged side loses the ability to touch a sibling.

A probe at boot proves it. An actor stores into a sibling's context and the hardware refuses — a page fault on x86 (#PF err=0x7: present | write | user), a U-mode store-access fault on riscv64, an EL0 fault on aarch64. Remove the confinement and the identical store lands.

The VM's own per-core state is confined the same way. With the VM running on every core, the array of per-core run-state blocks is a sibling's memory: an actor could write the block of a core it does not run on and corrupt another actor's in-flight VM. An actor is pinned to one core, so its address space grants the unprivileged side exactly that core's block and denies the rest. Each block is padded to a full page, because otherwise several blocks share one page and the user bit cannot tell them apart. This costs no syscall — the VM writes that block on every instruction, so a gate there would be absurd.

The actor layer's shared bookkeeping now lives in kernel memory on all three MMU targets — the name registry, the pid→context table, the return address for a remote reply, and the lock words. The FFI reaches it from ring 0; the unprivileged side cannot. x86 was the last of the three to move it there.

Isolation without an MMU

On a cheap microcontroller there is no MMU. The address space is flat and physical (VA == PA). Isolation is enforced by region-based hardware protection instead — PMP on RISC-V, the MPU on Cortex-M — and it reaches the same place: an actor cannot touch a sibling.

Bare RV32. The actors are spawned unprivileged (U-mode), and each is confined by PMP to its own VM context, its own stack, and the shared interpreter's code and data. The mailbox lives in kernel memory; the FFI reaches it, the timer and the console only through the ecall gate. PMP is reprogrammed on every context switch. A boot self-test stores from one actor into another's context and takes a store-access fault; with the confinement removed, the identical store lands.

Cortex-M0+, on real silicon. The actors are dropped into unprivileged Thread mode on their own stacks, each confined by an MPU region; the mailbox, the microsecond timer and the console are reached only through the SVC gate. A boot self-test stores from one actor into another's context and takes a HardFault (ARMv6-M has no MemManage fault); with the confinement removed, the identical store lands. Verified on real silicon.

The limits of the MMU-less confinement

On the real-silicon MMU-less parts, the region count is small — the RP2350's PMP is NAPOT-only, the RP2040's MPU has a handful of regions. There the confinement grants broadly and then denies the sibling, rather than granting only that actor's own region and denying by default.

On those parts an actor cannot reach a sibling's memory, but it can still reach the kernel's bookkeeping. (The bare-RV32 QEMU build's PMP uses TOR ranges and is deny-by-default: it grants the actor's own four spans and denies everything else, the kernel-owned mailbox included.)

The ARM MPU forces two further compromises, both from power-of-two region sizing: the actor region is one region carved into subregions (the sibling's subregion is switched off), and it is granted read-write rather than split read-execute — a W^X compromise on the shared interpreter, which does not weaken the per-actor separation.

Both MMU-less chips, in one image

A Raspberry Pi Pico W (RP2040, Cortex-M0+, 264 KB of SRAM) and a Pico 2 W (RP2350, RISC-V, 520 KB). Both run isolation, the radio and distribution in one image, on real silicon.

The network stack runs as a privileged task beside unprivileged, hardware-confined actors.

It fits in 264 KB: measured at 208 KB of 256 KB, with 48 KB to spare. The kernel ends at 107 KB, under the 128 KB the MPU split needs. The radio's 224 KB firmware streams from flash straight into the chip, so it never sits in the host's SRAM at all.

Verified on a Pico W: a cross-actor store takes a HardFault, the radio joins WPA2, DHCP gives it an address, DNS resolves, and it exchanges messages with an x86 Lambda OS.

The control loop and confined actors, in the same image

On the Cortex-M0+, the cascade-PID control loop and the safety envelope run native and privileged, while the mission — the flight plan — runs as one unprivileged, MPU-confined actor beside them.

On real silicon, over Wi-Fi, against a hardware-in-the-loop simulator, it flew the whole mission: base → cruise → island at 90 m → cargo drop → return → land. A bug in the mission cannot reach the safety envelope or the control loop: they are on the other side of the kernel boundary, not merely another actor's memory — stronger than actor-vs-actor isolation, and free, because that boundary already exists.

The boundary is proven before the mission is armed

Before the RP2350 flies, a pre-flight check attempts an unprivileged store into kernel memory, and the hardware must refuse it. If the store lands, the board does not spawn the mission at all. Only after the refusal is observed does the mission run, unprivileged and confined — so the isolation here is demonstrated, not assumed, on the exact build that flies.

The control loop runs on one of the chip's two cores, the radio and the HIL link on the other. The mission, the interpreter and its context pool share the 128 KB the MPU can grant, and the whole build fits 254 KB in the chip's 256 KB. Keeping the safety envelope native — rather than confining it as bytecode — is both the stronger isolation and what leaves that budget for the mission.

The full demo — the graded fault response, the pre-flight sequence, the HIL rig — is on the Flight Control page.

Where the FFI handler runs, by architecture

Where the FFI handler runs differs by how each architecture treats user pages from privileged mode.

The FP register that must not leak (the XMM invariant)

SSE is on for the VM's C objects, because the SysV ABI returns doubles in XMM registers. Floating-point state has to be handled correctly at two boundaries, and a third must be provably clean.

  1. Context switch. Each task has a 512-byte, 16-aligned FXSAVE area. The outgoing task's FP state is saved and the incoming task's restored.
  2. Synchronous syscall / FFI. The int 0x80 inline assembly declares the XMM registers clobbered, so the compiler spills any live XMM around the syscall.
  3. The asynchronous window. From trap entry to the FP save, no kernel code may touch XMM, or it clobbers the preempted task's live FP before it is saved.

The third boundary is enforced by construction: the Zig kernel is built with SSE and MMX off (general-registers-only). The kernel is entirely integer, so this costs nothing, and the compiler cannot emit an XMM instruction in the preemption window. A future float, or an auto-vectorized struct copy that slipped in, becomes a build error.

A build-time guard asserts zero XMM instructions in the (symbol-bearing, un-stripped) kernel object and in the SSE-on C helpers that run just after the FP restore — verified on artifacts that carry their symbols, so a missing symbol fails loudly rather than passing vacuously.

On the hardware-FP RV32 build the same window is closed the direct way: the per-task trap saves and restores f0..f31 across a preempt. On a soft-float, FPU-less chip there are no float registers at all — the trap is FP-presence-adaptive at compile time and the FP block vanishes.

On riscv64 and aarch64 no FP context is saved across a preempt today. The save and restore hooks are stubs. That is safe only for as long as no task holds live floating-point across a preemption, which is the case now. It is a constraint, not a design.

What isolation is, and is not

Isolation in Lambda OS is a robustness boundary: it contains a bug's blast radius to one actor.

It is not a multi-tenancy boundary. There is no instruction-level verifier for hostile bytecode, because all bytecode comes from the operator's own compiler over a trusted channel. The target is a dedicated appliance running the operator's own software. The mechanism is hardware isolation; the threat model is a fault, not an adversary.