3i
3i (Three-I) provides a general mechanism for cages (and grates) to make system calls and intercept system calls via a programmable system call table. The goal is to enable complex functionality without modifying the microvisor or increasing the microvisor’s trusted computing base.
Motivation
In traditional Linux systems, a process issues a system call and control transfers to a single, shared kernel implementation. All policy and system services are centralized inside the kernel. Extending or intercepting system calls typically requires kernel changes or privileged mechanisms such as ptrace, seccomp, or eBPF.
Lind runs multiple isolated cages inside a single process, so system calls are the main boundary between them. If every system call went directly to the microvisor, control and policy would again become centralized in trusted code. 3i avoids this by giving each cage its own programmable system call table, allowing routing and interposition to happen in user space without expanding the microvisor’s trusted computing base.
Design Goals
3i is designed as a runtime-agnostic interposition layer that can operate on top of a wide range of isolation backends (for example, software sandboxes or hardware-assisted memory protection) to execute arbitrary code. Its core goal is to provide a uniform mechanism for inter-cage call routing, including system call interception, syscall customization, and cross-cage RPC, without being tied to any specific runtime.
To achieve this, 3i allows runtimes to register C-ABI function pointers that implement the dispatch and execution logic for system calls. These function pointers allow each backend to integrate its own cage-management requirements (such as switching execution contexts, updating thread-local state, or preparing runtime metadata) into the call path.
Because 3i itself never assumes the presence of a particular runtime structure or object model, backend-specific behavior remains fully encapsulated in the corresponding adapter layer, leaving 3i’s core logic small, generic, and portable.
High-level Concepts
[todo] - add general Lind architecture figure
In traditional operating systems, a process makes a system call which traps into the kernel. Every process which makes a system call ends up trapping into the same kernel routine. In essence, there is one system call table which is shared by every process.
In contrast, Lind provides a per-cage system call table. Each cage may register a handler for a specific system call using register_handler.
When a cage invokes a system call via make_syscall, 3i looks up the handler registered for that system call in the calling cage’s table and transfers control to that handler. The handler may execute in the same cage or in a different cage, depending on how the table is configured.
Because each cage has its own table, different cages may register different handlers for the same system call without affecting one another.
Specifically, 3i supports the following scenarios:
-
Per-call routing within a single cage
Different system calls issued by the same cage can be handled by different grates (or RawPOSIX) by registering distinct handlers for each system call. -
Shared handlers across multiple cages
Multiple cages may register the same handler function (provided by a grate) by invokingregister_handlerwith differentcageids, enabling controlled sharing of system call implementations across cages.
As a term of convenience, a cage which processes system calls is called a grate. This conveys the mental model of a cage calling down toward the microvisor or kernel and having a grate filter, transform, or handle those system calls.
A grate is simply a cage; there is no special handling code or permission for it in 3i or the rest of the system. A grate may tend to make different system calls from a normal application, but it is still a cage, much like strace is a normal Linux process that happens to use otherwise uncommon system calls.
Cross-cage memory access
One important feature needed by a grate is the ability to read and write the memory of a cage whose system call it intercepts. For example, to handle a write system call, the grate must be able to read data from the calling cage’s buffer.
3i provides the function copy_data_between_cages to enable this safely.
Consider a grate that wishes to count how many times a specific cage invokes the write system call. The grate must increment its counter and then re-issue the write call on behalf of the originating cage. Copying the user buffer into the grate’s own address space would be wasteful when the grate merely wants to observe and forward the call.
To support this use case, make_syscall allows each argument of the system call to be annotated with a source cageid. The grate can therefore perform the forwarded write using its own system call table while specifying that the buffer pointer resides in the calling cage’s address space. This mechanism allows grates to safely observe and forward data without unnecessary copying.
Acting on behalf of another cage
Another important feature of make_syscall is the ability for a grate to perform a system call as though another cage had issued it.
Consider an interposed fork system call. If a cage invokes fork and the grate handling that call were to directly invoke the underlying RawPOSIX fork implementation on itself, the grate — not the originating cage — would be duplicated.
To prevent this, each make_syscall invocation explicitly specifies the target cage whose state and identity the system call should operate on, ensuring that process-like operations apply to the correct execution context.
Interposition on 3i itself
3i functions such as register_handler and copy_data_between_cages are themselves treated as system calls within 3i. They are 3i-specific APIs that participate in the same interception and dispatch framework.
This enables grates to interpose on operations performed by other grates and is a key mechanism used to enforce security and namespacing between cages, including mediation of calls to 3i itself.
3i Function Calls
[todo] - short example of writing a grate using these functions
| Caller | Callee | Function | Interposable | Remarks |
|---|---|---|---|---|
| grate | 3i | register_handler |
Yes | Register a handler for a syscall |
| grate | 3i | copy_data_between_cages |
Yes | Copy memory across cages |
| grate | 3i | copy_handler_table_to_cage |
Yes | Overwrite the syscall handler table of a cage |
| grate | 3i | make_syscall |
No | Invoke the registered handler |
| WASM / NaCl / RawPOSIX | 3i | trigger_harsh_cage_exit |
No | Initiate unclean cage termination |
| 3i / grate | grate / RawPOSIX | harsh_cage_exit |
Yes | Notify of cage termination |
Interposable indicates whether the call is made via the system call table and may itself be intercepted.
register_handler
Registers an interposition rule mapping a syscall number from a source cage to a handler function in a destination cage or grate.
copy_data_between_cages
Copies memory between cages. The source and destination cages may differ from the calling cage.
copy_handler_table_to_cage
Copies the syscall handler table of one cage to another. This is commonly used for fork-like behavior.
make_syscall
Performs a 3i call and routes it to the appropriate handler. This function is not interposable, as it is the base mechanism used to implement interposition.
trigger_harsh_cage_exit and harsh_cage_exit
These calls support cleanup when a cage exits abruptly, such as due to a signal. They notify grates and the microvisor that the cage’s memory and control flow can no longer be trusted.
These calls are not interposable during teardown to ensure system-level invariants are preserved and to prevent interference during cleanup.