Syscall ABI
This page is the reference for the 28 syscalls defined in lib/trona/uapi/consts/kernel.rs.
Every number, label, and register mapping documented here is consumed verbatim by substrate/syscall.rs and by the kernel handler in kernite/src/syscall/mod.rs — any change to this contract requires matching edits on both sides.
The source-of-truth file is:
lib/trona/uapi/consts/kernel.rs
which is include!’d into substrate via `substrate/consts.rs, so every substrate call site sees the same numeric constants at compile time.
Syscall numbers
| # | Name | Description |
|---|---|---|
0 |
|
Blocking send to an endpoint. Transfers the message registers and any staged capabilities. Blocks until a receiver accepts the message. |
1 |
|
Blocking receive from an endpoint. Writes the incoming message and sender badge into the caller’s IPC buffer. |
2 |
|
Client RPC: send + blocking receive. The server replies using |
3 |
|
Server event loop primitive: reply to the current caller and immediately block waiting for the next request on the same endpoint. Fastpath. |
4 |
|
Non-blocking send. Fails with |
5 |
|
Signal a notification object. Wakes any TCB bound or waiting on the notification with the supplied bitmask OR-merged into the pending bits. |
6 |
|
Wait on a notification object. Blocks until at least one bit is pending, then returns the pending bitmask and clears it. |
7 |
|
Non-blocking notification poll. Returns the current pending bitmask and clears it. Never blocks. |
8 |
|
Voluntarily yield the remaining scheduling budget back to the scheduler. |
9 |
|
Invoke a capability. The universal entry point for every typed operation ( |
10 |
|
Write a single character to the kernel debug serial port. Debug-only; gated by the kernel’s debug console control. |
11 |
|
Dump CPU register state to the debug serial port. Used by the userspace panic handler. |
12 |
|
Read the kernel clock. Supports |
13 |
|
Sleep for a requested duration in nanoseconds. |
14 |
|
Write a NUL-terminated string to the debug serial port. |
15 |
|
Write a sized buffer to the debug serial port. |
16 |
|
Enable/disable the kernel debug console output. Used by the console server to mute kernel logs after userland takes over. |
17 |
|
Set the source/destination CNode resolution depth for subsequent invocations on the calling thread. |
18 |
|
Userspace futex operation. Sub-op in |
19 |
|
Read a single |
20 |
|
Power off the machine via ACPI (x86_64) or PSCI (aarch64). Does not return. |
21 |
|
Blocking send with a nanosecond timeout. Returns |
22 |
|
Blocking receive with a timeout. |
23 |
|
Receive from any endpoint in a caller-supplied badged source array. The kernel picks the first endpoint with a pending sender; the source index is reported in the return value. |
24 |
|
Reply to the current caller and then receive from any endpoint in the source array — server event loop across multiple endpoints. |
25 |
|
|
26 |
|
|
27 |
|
Return from a kernel-injected notification frame. The notification dispatcher jumps back through this syscall to restore the pre-notification TCB state. |
The value u64::MAX is reserved as IPC_RECV_SOURCE_NOTIFICATION, used in SYS_RECV_ANY return values to indicate the wake came from a notification rather than a regular endpoint.
Register calling convention — x86_64
On x86_64, SaltyOS uses the AMD64 syscall instruction (not int 0x80).
The convention follows the AMD64 SysV layout but with different clobber semantics.
| Register | Role | Notes |
|---|---|---|
|
Syscall number in → error code out |
Overwritten by the |
|
|
First argument. |
|
|
Second argument. |
|
|
Input is the third argument; on return holds |
|
|
Fourth argument. Note this uses |
|
|
Fifth argument. |
|
|
Sixth argument. |
|
— |
Clobbered by |
|
— |
Clobbered by |
The substrate implementation in substrate/syscall.rs encodes this directly:
#[cfg(target_arch = "x86_64")]
unsafe {
core::arch::asm!(
"syscall",
inlateout("rax") num => error,
in("rdi") a0,
in("rsi") a1,
inlateout("rdx") a2 => value,
in("r10") a3,
in("r8") a4,
in("r9") a5,
lateout("rcx") _,
lateout("r11") _,
options(nostack),
);
}
The nostack option tells the compiler that the syscall does not touch the red zone below %rsp, which lets the caller keep the frame intact across the transition.
Register calling convention — aarch64
On aarch64, SaltyOS uses svc #0 and the ARMv8-A AAPCS64 register layout, with one non-standard twist: the syscall number is passed in x8 rather than being encoded in the svc immediate.
| Register | Role | Notes |
|---|---|---|
|
Syscall number in |
Matches the Linux aarch64 convention so that existing toolchains do not need special handling. |
|
|
First argument, replaced on return with |
|
|
Second argument, replaced on return with |
|
|
Third argument. |
|
|
Fourth argument. |
|
|
Fifth argument. |
|
|
Sixth argument. |
The inline assembly:
#[cfg(target_arch = "aarch64")]
unsafe {
core::arch::asm!(
"svc #0",
in("x8") num,
inlateout("x0") a0 => error,
inlateout("x1") a1 => value,
in("x2") a2,
in("x3") a3,
in("x4") a4,
in("x5") a5,
options(nostack),
);
}
Note that on aarch64 the result value lives in x1, not x2, because the ARM64 syscall ABI already uses x0/x1 as the natural return pair.
This is the one meaningful difference from x86_64: callers that want to read both the error and the value must extract them from different registers on the two architectures.
The substrate wrapper hides this difference behind a single TronaResult { error, value } struct.
The TronaResult return contract
Every syscall that returns to userspace produces a TronaResult:
#[repr(C)]
pub struct TronaResult {
pub error: u64, // 0 = TRONA_OK, non-zero = error code
pub value: u64, // result on success; undefined on error
}
Callers should treat any non-zero error as "the value field is not meaningful".
Error codes are listed on Error Codes; the common ones are TRONA_INVALID_CAPABILITY, TRONA_INVALID_ARGUMENT, TRONA_WOULD_BLOCK, TRONA_TIMEOUT (encoded as TRONA_RESTART in some paths), and TRONA_NOT_FOUND.
For the two syscalls that return pure error (SYS_SIGNAL, SYS_YIELD, SYS_DEBUG_*), the value field is left at zero and callers ignore it.
For syscalls that return a real value (SYS_GETRANDOM, SYS_WAIT, SYS_POLL, SYS_CLOCK_GETTIME), the result lives entirely in value and error is TRONA_OK on success.
Message registers for IPC
SYS_SEND, SYS_RECV, SYS_CALL, and SYS_REPLY_RECV take a 64-bit message info word as their first argument and 32 additional message registers from the TronaMsg struct.
The IPC buffer page (mapped at a fixed virtual address per thread) holds the overflow: message registers beyond what fits in the syscall register window are read and written through the per-thread IPC buffer.
The seL4-style msginfo encoding packs three fields into a single u64:
| Bits | Field | Meaning |
|---|---|---|
6:0 |
|
0–127: number of message registers carrying payload data. |
11:7 |
|
0–31: number of capability slots staged in the IPC buffer’s cap window for this transfer. |
51:12 |
|
40-bit operation identifier. This is where the IPC protocol label ( |
substrate/ipc.rs provides msginfo(label, length, caps) → u64 and three accessors (msginfo_label, msginfo_length, msginfo_extracaps) so call sites never manipulate the bit layout by hand.
Futex operation codes
SYS_FUTEX is the only syscall whose arg1 further splits into a sub-operation:
| Code | Name | Semantics |
|---|---|---|
0 |
|
Atomically check that |
1 |
|
Wake up to |
2 |
|
Like |
This is what the Mutex, Condvar, RWLock, and Once primitives in Synchronization Primitives sit on top of.
Clock IDs
SYS_CLOCK_GETTIME takes a clock identifier as its first argument:
| ID | Constant | Semantics |
|---|---|---|
0 |
|
Wall-clock time since the UNIX epoch. Monotonicity is not guaranteed if the system clock is adjusted. |
1 |
|
Monotonic time since boot. Guaranteed non-decreasing; preferred for measuring intervals. |
These are the only two clock IDs currently defined.
POSIX clocks beyond these (CLOCK_PROCESS_CPUTIME_ID, CLOCK_THREAD_CPUTIME_ID) are not implemented by the kernel.
Auxv tags consumed by trona
The spawner (init or procmgr) hands the child process an auxv vector that includes the standard ELF tags (AT_PHDR, AT_PHENT, AT_PHNUM, AT_ENTRY, AT_BASE) plus a small set of SaltyOS-specific tags defined in uapi/consts/kernel.rs:
| Value | Tag | Role |
|---|---|---|
|
|
Pointer to a |
|
|
Notification capability slot the slot allocator signals when it runs out of slots and needs a CSpace expansion. |
|
|
Virtual address of the thread’s IPC buffer page. |
|
|
SchedContext capability slot for the main thread. |
|
|
Pointer to the |
For PE processes spawned by the Win32 subsystem, the PE rtld also consumes a separate range:
| Value | Tag | Role |
|---|---|---|
|
|
Pre-mapped base address of the PE image. |
|
|
Size of the PE image mapping. |
|
|
Endpoint capability for |
|
|
Pre-mapped base of |
|
|
Size of the |
The ELF rtld (ld-trona.so) consumes the first block; the PE rtld (ld-trona-pe.so) consumes the second.
See ELF Dynamic Linker and PE Dynamic Linker for how each tag is acted on.
Related pages
-
Invoke Labels — the labels consumed by
SYS_INVOKE. -
IPC Protocol Labels — the labels consumed by
SYS_CALL/SYS_SENDon behalf of each server. -
Error Codes — the
TRONA_*error space. -
Syscall Wrappers — the substrate module that turns these numbers into Rust function calls.
-
docs/spec/syscalls.mdin the saltyos tree — the prose specification of each syscall’s behavior.