RawPOSIX
RawPOSIX is the microvisor of Lind-Wasm: a small, trusted Rust component that implements system calls on behalf of cages. It is a library crate at src/rawposix, linked into the lind-boot host process. When a cage issues a system call and no grate intercepts it, the call is dispatched by 3i to a RawPOSIX handler, which validates and converts the arguments and then performs the operation, in most cases by issuing a real Linux system call through libc.
The "raw" in the name describes the implementation strategy. Rather than reimplementing OS subsystems, RawPOSIX forwards work to the host kernel wherever possible and keeps only the per-cage state needed to give each cage its own coherent view: virtual file descriptor tables, memory maps, working directory, parent and child relationships, and signal state. This is what lets many isolated cages run inside a single Linux process while each behaves like a separate POSIX process.
On compatibility: RawPOSIX aims to preserve POSIX behavior for the system calls it implements. It is not a complete POSIX implementation, and no strict compliance guarantee is intended. The authoritative list of supported calls is the dispatch table in src/rawposix/src/syscall_table.rs, and behavior can differ from native Linux where the WebAssembly runtime or the cage model requires it (for example, the 32-bit guest address space, virtual file descriptors, and cage IDs in place of kernel PIDs).
What RawPOSIX is responsible for
- Implementing supported system calls. Each entry in
syscall_table.rsmaps a Linux syscall number to a handler function in one of three modules:fs_calls.rs(files, directories,mmap/brk, shared memory),net_calls.rs(sockets,poll/select/epoll), andsys_calls.rs(fork,exec,exit,waitpid, IDs, signal-related calls). - Per-cage resource isolation. Translating each cage's virtual file descriptors to kernel file descriptors (via the
fdtablescrate), maintaining per-cage memory maps (vmmap), and tracking process relationships between cages. - Registering itself as the default syscall handler. At startup,
register_rawposix_syscallininit.rswalks the syscall table and registers every handler with 3i for the initial cage, so that uninterposed syscalls reach RawPOSIX. - Runtime lifecycle.
rawposix_startbootstraps the environment (cage table, file descriptor tables, the init cage with cage ID 1, standard I/O descriptors), andrawposix_shutdownexits any remaining cages.
What it is not responsible for
- Executing WebAssembly. Wasmtime (driven by
lind-boot) runs cage code and owns the linear memory. - Syscall routing and interposition policy. 3i owns the per-cage handler tables; grates implement interception logic. RawPOSIX is simply the handler that uninterposed calls land on.
- The C library that applications see. lind-glibc implements the userspace side and decides how a libc call becomes a Lind syscall.
- Filesystem namespace isolation.
lind-bootchroots into thelindfsdirectory before starting RawPOSIX, so path-based syscalls are already confined by the time RawPOSIX forwards them to the kernel. - Scheduling. Cage threads are ordinary host threads scheduled by the Linux kernel; preemption for signals and cage termination is handled by Wasmtime's epoch mechanism.
Life of a syscall
The path of a read(fd, buf, count) call from a cage down to the host kernel:
cage (WebAssembly) host process (native)
read() in lind-glibc
└─ MAKE_SYSCALL macro
└─ make_threei_call
└─ import lind::make-syscall ──► host func (lind-common, Wasmtime)
└─ 3i make_syscall
└─ per-cage handler table lookup
└─ read_syscall (RawPOSIX fs_calls.rs)
└─ libc::read ──► Linux kernel
- lind-glibc. The guest's
read()reaches aMAKE_SYSCALLsite (seesrc/glibc/sysdeps/unix/syscall-template.h), which callsmake_threei_callinsrc/glibc/lind_syscall/lind_syscall.c. Pointer arguments are translated from guest linear-memory offsets to host virtual addresses here (TRANSLATE_ARG_TO_HOSTinaddr_translation.h). - Crossing the sandbox boundary.
make_threei_callinvokes__lind_make_syscall_trampoline, which is a WebAssembly import with modulelindand namemake-syscall. - Wasmtime. The runtime supplies that import:
add_syscall_to_linkerinsrc/wasmtime/crates/lind-common/src/lib.rsregisters a host function forlind::make-syscall. After handling Asyncify replay cases (relevant forfork/exec/exit), it forwards the call to 3i'smake_syscall. - 3i dispatch. 3i looks up the handler registered for this cage and syscall number in the cage's handler table. For a cage with no interposition, that handler is the RawPOSIX implementation registered at startup. A grate may be registered instead, in which case the grate decides whether and how to forward the call.
- RawPOSIX handler.
read_syscallinsrc/rawposix/src/fs_calls.rsconverts the virtual fd to a kernel fd (convert_fd_to_host), converts the buffer pointer and count (sc_convert_buf,sc_convert_sysarg_to_usizefrom thetypemapcrate), verifies that unused argument slots are actually unused, and callslibc::read. - Return path. Handlers return an
i32, with errors encoded as-errno. Back in glibc,make_threei_calltranslates a negative return intoerrnoplus a-1return value, following the usual POSIX convention. A few call sites (such as futex operations) opt out of this translation and consume the raw value.
Per-cage state
The Cage struct lives in the sibling crate src/cage, and RawPOSIX manipulates it through a global cage table keyed by cage ID (cagetable_getref). A cage holds, among other fields, its working directory, parent cage ID, vmmap (the per-cage memory map, tracked in page units; see Memory), signal handler and pending-signal state, and zombie children for waitpid. Virtual file descriptors are kept outside the Cage struct, in per-cage tables owned by the fdtables crate; every fd-taking syscall translates its virtual fd before touching the kernel.
Process-like semantics are built from this state: fork creates a new cage that inherits copies of the parent's fd table and memory mappings, exec replaces a cage's contents, and exit/waitpid update the parent's zombie list. Details are covered in Multi-Processing.
The handler ABI
Every RawPOSIX handler has the same C-ABI signature, RawCallFunc in src/rawposix/src/init.rs: a target_cageid followed by six argument pairs, where each pair is a raw u64 value and the ID of the cage that value belongs to.
Arguments carry their own cage IDs because the caller is not always the cage the syscall operates on. A grate forwarding a write on behalf of another cage passes a buffer pointer that refers to that cage's memory, and the per-argument cage ID tells RawPOSIX where to resolve it (see cross-cage memory access in the 3i docs).
Two conventions matter when reading or writing handlers:
- Convert arguments early. All values arrive as
u64. A guest-1arrives as18446744073709551615, so handlers use thetypemapconversion helpers (sc_convert_sysarg_to_i32,sc_convert_buf, and friends) at the top of the function before any logic runs. - Check unused slots. Handlers assert that unused argument slots hold the expected sentinel via
sc_unusedargand panic on mismatch, treating unexpected values as a security violation.
Source layout
src/rawposix/src/
├── lib.rs crate root, module declarations
├── syscall_table.rs syscall number → handler dispatch table
├── init.rs rawposix_start/rawposix_shutdown, handler registration, RawCallFunc
├── fs_calls.rs file, directory, memory, and shared-memory syscalls
├── net_calls.rs socket and I/O-multiplexing syscalls
└── sys_calls.rs process, identity, and signal syscalls
RawPOSIX depends on several sibling crates that used to be part of a single codebase and are now separate:
| Crate | Role |
|---|---|
src/cage |
The Cage struct, cage table, vmmap, signal state |
src/fdtables |
Per-cage virtual fd to kernel fd translation |
src/threei |
The 3i dispatcher that routes syscalls to RawPOSIX handlers |
src/typemap |
Argument conversion and validation helpers |
src/sysdefs |
Shared constants (syscall numbers, errnos, platform constants) |
Testing
RawPOSIX has no in-crate unit test suite. It is exercised end to end: C test programs in tests/unit-tests/ are compiled to WebAssembly and run under lind-boot, so every syscall they make flows through the full glibc → Wasmtime → 3i → RawPOSIX path described above. The harness is scripts/test/harnesses/wasmtestreport.py, which compiles and runs each test and compares output against native execution or an expected/ directory. See Testing for usage.
Logic that lives in the sibling crates is unit-tested there; for example, the vmmap implementation has Rust tests runnable with cargo test in src/cage.