# VM

_Provium / Reference_

> The VM userdata — lifecycle, run and run_async, file I/O, syscalls and ioctls, disks, NICs, console, clock, workers, batch, snapshots, and RunResult.

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 sizing opts (`memory`, `cpus`). See [boot opts](#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

Boot opts split between the two call sites. The sizing keys go in the third arg to `lab:vm(name, profile, opts)` — the scheduler and QEMU need them when the VM is declared. The boot-shaping keys go to `vm:boot(opts)` and merge per field into the VM's pending boot options.

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

Example:

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

## Lifecycle methods

### `vm:boot(opts?)`

Boots the guest. Returns `self` so calls chain (`provium:vm("a", "peios"):boot()`). The optional `opts` table (`kernel_cmdline`, `rng_seed`, `initial_time`, `files`) merges per field into the VM's pending boot options. 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](/provium/reference/snapshot.md) userdata wrapping the path.

```lua
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`.

```lua
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 a recognised opts key (`env`, `env_clear`, `cwd`, `stdin`, `timeout`, `timeout_ms`, `args`); a table with none of them is treated as the legacy array. Mixed array entries with `args=` are not allowed; use one form or the other.

Returns a [RunResult](#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](/provium/reference/process.md) 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](/provium/writing-tests/files-and-handles.md#vmpush-filehost-path-guest-path-opts) and [fixtures and dependencies](/provium/running-tests/fixtures-and-dependencies.md#external-host-file-deps).

### `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](/provium/reference/file-handle.md) 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](/provium/reference/streams.md) userdata. Opts: `{start = "beginning" | "end" | <offset>}` (`"start"` is accepted as an alias for `"beginning"`). 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](/provium/reference/streams.md).

## 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}, nested={…}})`. 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.

**Nested pointers.** `nested` is a list of `{parent=N, child=M, offset=K}` entries (`parent`/`child` are **1-based** indices into `bufs`). Before the arg-slot `ptrs` are applied, the agent writes `bufs[M]`'s address into `bufs[N]` at byte `offset` (an 8-byte native-endian pointer). This is how you pass a **struct argument that itself contains a pointer** — a buffer pointing at another buffer. Use it for ioctls invoked via raw `syscall(SYS_ioctl, fd, cmd, &struct)` whose struct has an inner output-buffer pointer (a query/info ioctl), or for syscalls whose argument struct embeds pointers (`kacs_access_check`-style). Each pointed-at buffer is returned post-call in `out_bufs` at its own index.

```lua
-- ioctl(fd, QUERY, &args) where struct kacs_query_args { u32 class; u32 buf_len; u64 buf_ptr; }
-- buf_ptr (offset 8) must point at the output buffer.
local args = string.pack("<I4I4I8", PRIVS_CLASS, 4096, 0)  -- buf_ptr filled in by nested
local out  = string.rep("\0", 4096)
local r = vm:syscall(SYS_ioctl, {
    args   = { fd, QUERY_CMD, 0 },     -- arg slot 2 receives the struct pointer
    bufs   = { args, out },            -- buf 1 = struct, buf 2 = output
    ptrs   = { 2 },                    -- buf 1 → arg slot 2
    nested = { { parent = 1, child = 2, offset = 8 } },  -- buf 2's addr → buf 1 @ offset 8
})
local data = r.out_bufs[2]             -- the QUERY output buffer
```

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` (including nested-pointer target buffers). Useful for read-style syscalls. |

The same `bufs` / `ptrs` / `nested` options are available on [`worker:syscall`](/provium/reference/worker.md#workersyscallnr-).

### `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](/provium/reference/nic.md) 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](/provium/reference/disk.md) 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](/provium/reference/disk.md) 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](/provium/reference/console.md) userdata.

### `vm:clock()`

Returns a [Clock](/provium/reference/clock.md) 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](/provium/reference/worker.md) 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.

```lua
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](/provium/reference/lab.md) — what creates and contains VMs.
- [Process](/provium/reference/process.md) — what `vm:run_async` returns.
- [Worker](/provium/reference/worker.md) — what `vm:spawn_worker` returns.
- [Disk](/provium/reference/disk.md), [Nic](/provium/reference/nic.md) — hypervisor-side handles.
- [Console](/provium/reference/console.md), [Clock](/provium/reference/clock.md) — guest-side accessors.
- [Streams](/provium/reference/streams.md) — what `vm:tail_file` and `vm:fd_stream` return.

Related content:

- [VMs and profiles](/provium/writing-tests/vms-and-profiles.md)
- [Running commands inside the guest](/provium/writing-tests/running-commands.md)
- [Lab](/provium/reference/lab.md)
- [Worker](/provium/reference/worker.md)
