These docs are under active development and cover the v0.20 Kobicha security model.
On this page
Concept 2 min read

Raw Syscalls

Provium's most powerful feature is raw syscall access. From Lua, you can call any syscall — standard Linux syscalls, custom kernel module syscalls, anything — passing integer arguments, buffer pointers, and receiving results.

Simple syscalls

vm:syscall(nr, arg1, arg2, ...) executes a syscall with up to 6 integer arguments and returns the raw return value:

-- getpid (syscall 39)
local pid = vm:syscall(39)
assert(pid > 0)

-- Custom syscall: kacs_open_self_token(flags=0, access=TOKEN_QUERY)
local TOKEN_QUERY = 0x0008
local fd = vm:syscall(1000, 0, TOKEN_QUERY)
assert(fd >= 0, "open_self_token failed: " .. fd)

Arguments are passed as integers (Lua numbers). The return value is the raw int64 result — for syscalls that return error codes, this will be a negative errno value on failure.

Syscalls with one buffer

Many syscalls take a pointer to a buffer — a struct, a path, a data block. vm:syscall_buf(nr, buf_arg_index, buf_data, arg1, ...) replaces one argument with a pointer to buffer data:

-- syscall(nr, arg0, arg1, arg2, ...)
-- buf_arg_index=0 means arg0 gets the buffer pointer

local spec = provium.pack("u32 u32", 1, 42)
local fd = vm:syscall_buf(1003, 0, spec, 0, #spec)
assert(fd >= 0)

The buf_arg_index is the position (0-indexed) of the argument that should be replaced with a pointer. The agent allocates memory in the guest, copies the buffer data there, patches the pointer into the argument, and executes the syscall.

Syscalls with multiple buffers

vm:syscall_bufs(nr, bufs_table, arg1, ...) handles syscalls that need multiple buffer arguments:

-- bufs_table maps argument indices to buffer data
local path_bytes = "/tmp/test\0"
local sd_data = build_sd()

local ret = vm:syscall_bufs(1022,
    {[1] = path_bytes, [3] = sd_data},   -- buffers
    -100, 0, 0x07, 0, #sd_data, 0)       -- remaining args

Each key in the bufs_table is an argument index (0-based). The corresponding argument is replaced with a pointer to the buffer data. Non-buffer arguments are passed as-is.

Output buffers

syscall_bufs also returns output buffer contents — the buffer data after the syscall completes. This is essential for syscalls that write into caller-provided buffers:

-- Allocate an output buffer (filled with zeros)
local out_buf = string.rep("\0", 256)

local ret, path_out, data_out = vm:syscall_bufs(1021,
    {[1] = path_bytes, [3] = out_buf},
    -100, 0, 0x04, 0, 256, 0)

-- data_out contains what the kernel wrote into the buffer
if ret > 0 then
    local sd = data_out:sub(1, ret)
end

Output buffers are returned as additional return values in argument-index order. If you have buffers at indices 1 and 3, you get two extra return values: the buffer at index 1 first, then the buffer at index 3.

Error handling

Syscall return values follow Linux conventions:

  • Success: Non-negative value (often a file descriptor or 0)
  • Failure: Negative errno value (-1 = EPERM, -13 = EACCES, -22 = EINVAL, etc.)
local fd = vm:syscall(1000, 0, 0x0008)
if fd < 0 then
    error("syscall failed with errno: " .. (-fd))
end

For tests that verify error cases, assert the expected negative value:

test("rejects invalid flags", function()
    local ret = vm:syscall(1000, 0xFF, 0)
    assert_eq(ret, -22, "expected EINVAL")  -- -22 = EINVAL
end)

Worked example

Testing a custom kacs_open_self_token syscall:

local vm = provium.create("minimal")
vm:boot()

-- Syscall 1000 = kacs_open_self_token(flags, access_mask)
local TOKEN_QUERY = 0x0008
local fd = vm:syscall(1000, 0, TOKEN_QUERY)
assert(fd >= 0, "kacs_open_self_token failed: " .. fd)

-- The fd should be a valid file descriptor (3+)
assert(fd >= 3, "unexpected fd: " .. fd)

vm:shutdown()

For more complex examples involving binary struct construction, see Binary Packing. For syscalls with nested pointer structures, see Pointer Patching.