# Disk

_Provium / Reference_

> The Disk userdata — read and write sectors directly, inject EIO and slow-I/O faults, and detach the disk from the live guest.

A Disk wraps one virtio-blk disk attached to a VM. It exposes block-level operations and a fault-injection surface useful for exercising I/O error paths in the guest.

## Constructing

| Source | Returns |
|---|---|
| `vm:attach_disk({id="vda", size=…, image="…"})` | New disk attached to `vm`. |
| `vm:disk("id")` | Lookup of an already-attached disk. Errors if absent. |

`vm:attach_disk` opts:

| Field | Type | Default | Description |
|---|---|---|---|
| `id` | string | `"attached-<vm_name>"` | Disk identifier within the VM. |
| `size` | int (bytes) | `4 GiB` | Modelled disk size. Used for `:size()` when no image is attached. |
| `image` | string (path) | none | Backing file. Sector ops require an image. |

Disks are 512-byte sectors throughout. The `read_sectors` and `write_sectors` ops express offsets and counts in sectors.

## Methods

### `disk:size()`

Returns the disk's size in bytes. When a backing image is attached, returns the live `stat()` size of the image file (so a test that resized the underlying file with `truncate` / `fallocate` reads honest output). Without an image, returns the modelled size from `attach_disk`.

### `disk:read_sectors(offset, n)`

Read `n` sectors starting at sector `offset`. Returns the bytes as a Lua string. Errors if:

- The disk is detached (`disk:read_sectors: disk is detached`).
- No backing image (`disk:read_sectors: no backing image — disk:with_image required`).
- An `eio_read` fault is active (`disk:read_sectors: EIO (fault_inject)`).
- The host-side `read` fails (passes the underlying error through).

Honours active faults:

- `eio_read` — short-circuits to EIO before any I/O.
- `slow` — sleeps 50 ms per call. After the sleep, re-checks for `eio_read` so a concurrent fault injection during the sleep still takes effect. After the actual I/O, checks one more time for the same reason.

### `disk:write_sectors(offset, data)`

Write `data` (a Lua string) starting at sector `offset`. The data does not have to be a multiple of 512 bytes; it is written verbatim from the offset. Returns nothing.

Errors and fault handling mirror `read_sectors`:

- Detached → error.
- No backing image → error.
- `eio_write` active before the I/O → EIO.
- `slow` → 50 ms sleep, with `eio_write` re-checks after the sleep and after the I/O.

### `disk:fault_inject(mode)`

Activate a fault. Valid modes: `"eio_read"`, `"eio_write"`, `"slow"`. Unknown modes error with the valid list (`disk:fault_inject: unknown mode \`X\` (valid: eio_read, eio_write, slow)`).

Multiple modes can be active simultaneously. When `slow` and the matching `eio_*` are both active at call time, the EIO check runs first — the call errors immediately, without the 50 ms delay. (The post-sleep re-checks only matter for an EIO fault set after the call started.)

### `disk:clear_faults()`

Clear every active fault. Subsequent reads / writes succeed normally.

### `disk:active_faults()`

Returns a Lua array of the currently-active fault mode names. Useful for tests that need to assert the harness state.

### `disk:detach()`

Mark the disk as detached and issue a best-effort QMP `device_del` against the parent VM. Subsequent `read_sectors` / `write_sectors` error. The QMP call is best-effort: a disk that was never QMP-added (host-bookkeeping-only attach) silently surfaces "Device not found" but the local detached flag is still set.

### `disk:is_detached()` / `disk:id()`

Accessors. `:id()` returns the disk identifier as given to `attach_disk`.

## Example: full fault-injection pattern

```lua
test("guest sees EIO and recovers after clear", function(t)
    local img = "/tmp/test-image"
    -- Pre-create the backing file.
    local f = io.open(img, "w"); f:write(string.rep("\0", 512 * 1024)); f:close()

    local vm = provium:vm("v", "peios"):boot()
    local disk = vm:attach_disk({id = "vda", size = 512 * 1024, image = img})

    -- Confirm baseline read works.
    local body = disk:read_sectors(0, 1)
    t:assert_eq(#body, 512)

    -- Inject EIO.
    disk:fault_inject("eio_read")
    local ok, err = pcall(function() disk:read_sectors(0, 1) end)
    t:assert(not ok)
    t:assert(tostring(err):find("EIO"))

    -- Clear and confirm reads recover.
    disk:clear_faults()
    body = disk:read_sectors(0, 1)
    t:assert_eq(#body, 512)
end)
```

## See also

- [VM](/provium/reference/vm.md) — disks come from `vm:attach_disk` / `vm:disk`.
- [Disks and fault injection](/provium/writing-tests/disks-and-fault-injection.md) — patterns for exercising I/O error paths.

Related content:

- [Disks and fault injection](/provium/writing-tests/disks-and-fault-injection.md)
- [VM](/provium/reference/vm.md)
