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

VM

A VM userdata wraps one guest. You get one from lab:vm("name", "profile") (or the dot-sugar lab.<name> after creation), and every operation against the guest dispatches through it.

VM handles are cheap to clone — passing them around or storing them in tables doesn't duplicate state.

Constructing

Source Returns
lab:vm("name", "profile") New VM in lab, in the Created state.
lab:vm("name", "profile", opts) Same, with boot opts staged for the next :boot(). See boot opts.
lab:vm("name") Lookup the VM previously declared with that name. Errors if absent.
lab.name Same lookup as lab:vm("name"), returns nil on miss.
provium:vm_fixture("path") Restored from a cached fixture; already booted.

State machine

Created ──:boot()──> Booted ──:pause()──> Paused
                       │                    │
                       ├─:reset()→Booted    ├─:resume()→Booted
                       ├─:power_button()──> Shutdown
                       └─:shutdown()─────> Shutdown

vm:state() returns "created", "booted", "paused", "shutdown", or "dead". Operations against a VM in the wrong state error cleanly with the offending state in the message.

Boot opts

Pass as the third arg to lab:vm(name, profile, opts) (creation-time defaults) or as the only arg to vm:boot(opts) (per-boot overrides). Per-boot opts merge with creation-time opts.

Key Type Description
memory int (bytes) or string "512M"/"2G" VM memory cap.
cpus int vCPU count.
kernel_cmdline string Replaces the profile's cmdline.
rng_seed int (u64) Seeds the guest's virtio-rng. Use for determinism.
initial_time int or float (seconds since epoch) Sets the guest's wall clock at boot.
files array of {path=string, content=string} Files to inject into the guest's filesystem before init runs.

Example:

local vm = provium:vm("v", "peios", {
    memory = "2G",
    cpus = 4,
    rng_seed = 0xdeadbeef,
    initial_time = 1700000000,
    files = {
        {path = "/etc/hostname", content = "v"},
    },
})
vm:boot()

Lifecycle methods

vm:boot(opts?)

Boots the guest. Returns self so calls chain (provium:vm("a", "peios"):boot()). Optional opts table merges into the boot opts staged at creation time. Errors if already booted.

Emits a vm_spawned event after the guest is up and the agent has handshaken. The event payload carries {file, vm_name, profile, memory_bytes, cid}.

vm:pause()

Pauses the guest's vCPUs. Returns nothing. Errors if not in Booted.

vm:resume()

Resumes a paused guest. Returns nothing. Errors if not in Paused.

vm:shutdown()

Tears down the guest. Idempotent in the sense that Created, Booted, Paused, and Dead all transition to Shutdown. Emits a vm_shutdown event with {file, vm_name, duration_ns}.

vm:reset()

Warm reboot — guest re-runs its boot path. Errors if not in Booted. The post-reset state is still Booted.

vm:power_button()

Sends ACPI power-button to the guest. Triggers a graceful shutdown sequence. Errors if not in Booted. Post-call state is Shutdown.

vm:close()

Auto-close hook used by the resource walker. Idempotent. Calls :shutdown() if not already in Shutdown. Test code rarely calls this directly — the harness fires it for every VM at file end (or per-test if provium.reset_between_tests = true).

Snapshot and restore

vm:snapshot(path?)

Take a snapshot. With an explicit path, writes there; without, writes to a tempfile. Returns a Snapshot userdata wrapping the path.

local s = vm:snapshot()
local s2 = vm:snapshot("/tmp/before-mutation.snap")

vm:restore(snap_or_path)

Restore from a Snapshot userdata (preferred) or a bare path string. Errors if not in Created or Shutdown.

vm:shutdown()
vm:restore(s)

Layer-1 ops

Layer-1 ops are the high-level "do it like SSH" primitives. They dispatch through the agent's exec / file / stat handlers.

vm:run(cmd_or_args, opts?)

Run a command. Two forms:

  • vm:run("echo hi") — runs through /bin/sh -c "echo hi". Shell metacharacters work.
  • vm:run("echo", {"hi"}) — direct exec. No shell. The second arg is a positional-arg array.
  • vm:run("echo", {args={"hi"}, env={K="v"}, cwd="/tmp", stdin="…", env_clear=true, timeout="5s"}) — opts form. Auto-detected by the presence of any non-array key. Mixed array entries with args= are not allowed; use one form or the other.

Returns a RunResult userdata.

Opts key Type Description
args array of strings Direct positional args (only used with the table-as-opts form).
env string→string map Environment variables.
env_clear bool When true, the guest sees only env; otherwise env merges with the agent's environment.
cwd string Working directory inside the guest.
stdin string Bytes piped to the process's stdin.
timeout_ms int Hard wall-clock timeout in milliseconds.
timeout int / float / string Same, expressed as seconds or "5s" / "500ms" / "2m" / "1h". Negative or NaN errors.

vm:run_async(cmd, opts?)

Like :run, but returns a Process userdata immediately. The agent does not auto-kill; you control the lifetime via proc:kill() / proc:wait(). Passing timeout here is rejected — use proc:wait(timeout) instead.

vm:read_file(path)

Returns the file contents as a Lua string. Errors on agent-side read failure (ENOENT, EACCES, etc.).

vm:write_file(path, data)

Replaces the file's contents with data. Creates the file if absent. Returns nothing.

vm:push_file(host_path, guest_path, opts?)

Read a file from the host filesystem and write its bytes to guest_path in the guest. Inside a fixture, the host file is folded into the fixture's cache key automatically — rebuilding the host file (typical case: a binary under test) invalidates the snapshot. Relative host_path is resolved against the directory of the file containing the call. Pass {auto_dep = false} to skip the auto-fold for a single call. See files and handles and fixtures and dependencies.

vm:stat(path)

Returns a table:

Field Type Description
size int (bytes) File size.
mtime float (seconds since epoch) Modification time.
mtime_ns int (ns since epoch) Same, full precision.
perm int POSIX mode bits (≤ 4095, i.e. 0o7777).
entry_type string One of "file", "directory", "symlink", "fifo", "socket", "block_device", "char_device", "other".

vm:listdir(path)

Returns an array of {name=string, entry_type=string} tables.

vm:mkdir(path, opts?)

Create a directory. Opts: {parents=bool, perm=int}. With parents=true, intermediate directories are created (mkdir -p). perm is the POSIX mode for the new directory.

vm:unlink(path)

Remove a file or empty directory.

vm:rename(from, to)

Atomic rename within the guest filesystem.

vm:open_file(path, mode_table)

Returns a File userdata.

The mode table accepts:

Key Type Effect
read bool Open for reading.
write bool Open for writing.
create bool Create if absent.
truncate bool Truncate to zero bytes on open.
append bool Append-only writes.
exclusive bool Combine with create=true to require the file not already exist (O_EXCL).
perm int POSIX mode for newly-created files.

At least one of read, write, or append must be true; an empty mode table errors at open time with a pointer.

vm:tail_file(path, opts?)

Open a streaming subscription to a file. Returns a Tail userdata. Opts: {start = "beginning" | "end" | <offset>}. Default is "end" — only bytes appended after the call are streamed. <offset> may be a non-negative integer (absolute byte offset), a negative integer (N bytes before EOF — provium stats and resolves), or a finite float (truncated toward zero, same sign semantics).

vm:fd_stream(fd_or_file)

Open a streaming subscription to an existing file handle. Accepts either an integer handle id (from file:fd()) or a File userdata directly. Returns a Tail.

Layer-0 ops

Layer-0 ops are direct kernel-level primitives — syscall(2) and ioctl(2). Use these to exercise driver paths or to test syscall semantics directly.

vm:syscall(nr, ...)

Two call shapes:

  • Integer-only: vm:syscall(nr, a1, a2, a3, a4, a5, a6) — up to 6 integer arguments.
  • Table form: vm:syscall(nr, {args={a1, a2, …}, bufs={"…", …}, ptrs={1, 3}}). Buffers are byte arrays; ptrs[i] says which arg slot (0-indexed) bufs[i] should be spliced into. The buffer's address is filled in for the kernel.

Returns a table:

Field Type Description
ret int Syscall return value.
result int Same as ret (alias for clarity).
errno int Errno if the syscall returned negative; 0 otherwise.
out_bufs array of strings Post-syscall contents of the supplied bufs. Useful for read-style syscalls.

vm:ioctl(fd, cmd, data?, opts?)

Direct ioctl(2). fd and cmd are integers. data is an optional byte string passed as the third arg's pointer. opts.bufs and opts.ptr_offsets work like :syscall's bufs / ptrs.

Returns {ret, result, out_data, out_bufs}. out_data is the post-call value of data (for ioctls that write back through the same buffer).

Hypervisor resources

vm:nic(name)

Returns a Nic userdata for the named bridge attachment, or for a guest-style interface name (eth0, enp0s3, ens3). Guest-name lookup maps to the Nth attached bridge sorted by bridge name.

vm:disk(id)

Returns a Disk userdata for an already-attached disk. Errors if no disk with that id.

vm:attach_disk(opts?)

Attaches a new disk and returns a Disk userdata. Opts: {id=string, size=int, image=string}. Defaults: id = attached-<vm_name>, size = 4 GiB, image = no backing file.

Console and clock

vm:console()

Returns a Console userdata.

vm:clock()

Returns a Clock userdata.

Workers

vm:spawn_worker(opts?)

Spawns a sub-agent connection so test code can dispatch concurrent ops against the same guest. Returns a Worker userdata. Opts are reserved for future use; passing {thread=true} errors with a future-work pointer.

Batch operations

vm:batch(fn)

Collect multiple ops inside fn(b) and dispatch them as one wire round-trip. b exposes:

  • b:run(...) (with the same shape as vm:run)
  • b:read_file(path)
  • b:write_file(path, data)
  • b:stat(path)
  • b:listdir(path)
  • b:mkdir(path, opts)
  • b:unlink(path)
  • b:rename(from, to)
  • b:syscall(nr, a, b, …) — integer-args form only; the table form is rejected here.

Returns a Lua array of {ok=value} or {err=msg} entries, one per op. A failure on op N does not short-circuit the rest of the batch.

local results = vm:batch(function(b)
    b:write_file("/tmp/a", "1")
    b:write_file("/tmp/b", "2")
    b:read_file("/tmp/a")
    b:stat("/tmp/missing")
end)
-- results = {{ok=nil}, {ok=nil}, {ok="1"}, {err="No such file or directory"}}

Accessors

vm:name() — Returns the VM's name as given to lab:vm.

vm:profile() — Returns the profile name.

vm:state() — Returns "created", "booted", "paused", "shutdown", or "dead".

vm:cid() — Returns the assigned vsock CID, or nil before boot.

vm:is_quiescent()true if no in-flight ops, no open files or streams.

vm:open_file_count() — Number of files currently open through the agent.

vm:open_stream_count() — Number of streams currently active (tails, fd-streams, console-reads, captures).

RunResult

The return value of vm:run(...) (and worker:run(...), proc:wait()).

Fields:

Field Type Description
exit_code int Exit code. -1 for signalled, -2 for timed_out. Use signal and timed_out to disambiguate.
stdout string Captured stdout.
stderr string Captured stderr.
status string "exited", "signalled", "timed_out".
timed_out bool true if the timeout fired.
signal int or nil Signal number when status is signalled, else nil.

Methods:

  • result:ok() — convenience for result.exit_code == 0.
  • result:assert_ok() — raises if not OK; the error message includes status, stdout, and stderr (with signal name when signalled).

See also

  • Lab — what creates and contains VMs.
  • Process — what vm:run_async returns.
  • Worker — what vm:spawn_worker returns.
  • Disk, Nic — hypervisor-side handles.
  • Console, Clock — guest-side accessors.
  • Streams — what vm:tail_file and vm:fd_stream return.