Disks and fault injection
Provium's disk support has three goals: put a block device in front of the guest, give the test direct sector-level access to the backing image, and inject faults that exercise the guest's error-handling paths.
The exhaustive method reference is on Disk.
Attaching a disk #
A disk the guest can see is declared before the boot, because the guest's firmware and initramfs look for block devices long before there is an agent to call anything.
For one test — a blank disk to install onto, a filesystem you have damaged on purpose:
-- `scratch` asks Provium for a blank disk of that size. It creates the
-- file, owns it, and removes it with the VM.
local vm = provium::
local disk = vm:
To start from contents you have prepared — a seeded filesystem, a
registry hive captured from an earlier boot — copy a template. Each VM
gets its own copy, and the template itself is never written, so a
fixture survives the test that used it:
local vm = provium::
path is the third form and means something different: a file you
own, taken verbatim and never created. A path that does not exist fails
the launch naming the disk and the path, so a typo stays an error
instead of quietly becoming a blank disk.
local vm = provium::
For every test in a suite — a boot medium, an image that is the system under test — put it in the profile instead, beside kernel and initrd:
[]
= [{ = "{out}/peios.iso", = "medium", = true }]
Either way the guest gets a virtio-blk-pci device, named /dev/vda, /dev/vdb, … in attachment order, with the profile's disks first. Both forms are documented in full under disks and Boot disks.
Reading and writing sectors #
Sectors are 512 bytes throughout. Offsets and counts are in sectors, not bytes.
-- Read sector 0 (the first 512 bytes).
local sec0 = disk:
-- Read 4 sectors starting at sector 100 (bytes 51200..53247).
local block = disk:
assert
-- Write at sector 50.
disk:
Without a backing image, both ops error with a no backing image — disk:with_image required message.
Fault injection #
Three modes:
| Mode | Effect |
|---|---|
eio_read | Every read_sectors short-circuits to EIO. |
eio_write | Every write_sectors short-circuits to EIO. |
slow | Every read_sectors / write_sectors sleeps 50 ms before doing the I/O. |
Modes are activated with disk:fault_inject(mode) and cleared with disk:clear_faults(). Multiple modes can be active simultaneously — with slow + eio_read both set, the EIO check wins: the read errors immediately, without the 50 ms delay.
Inject EIO #
test
Inject slow I/O #
slow delays each sector op but doesn't change its outcome. Assert both halves: the op took the hit, and it still worked. The guest Clock gives you a sub-millisecond time source (os.time() only has 1-second resolution):
test
The 50 ms delay is fixed per call; it is not currently configurable from test code.
Combine modes #
test
Concurrent injection during I/O #
The harness re-checks the fault set after the slow-fault sleep AND after the actual I/O completes, so an EIO fault that lands mid-call still takes effect. There is currently no way to exercise this from a test, though: test code runs on a single thread, and workers run guest processes — they can't call disk:fault_inject. Inject faults up-front, act, then clear; the mid-call recheck is defensive insurance in the harness, not a pattern you can drive.
Inspecting state #
local active = disk: -- {"eio_read", "slow"}
disk: -- false
Clearing #
disk:
local r = disk: -- succeeds
Cutting the power #
Everything above is host-side: fault_inject changes what read_sectors and write_sectors do, which is the test's view of the image rather than the guest's. For the question a write-ahead log actually poses — did what the guest fsynced survive, and did what it did not fsync correctly vanish? — the disk has to be mediated:
local vm = provium::
-- … the guest writes and flushes …
vm:: -- drop everything it never flushed
vm: -- and let the reboot clear its caches
Provium serves a mediated disk over an NBD server of its own and holds each write until the guest flushes it, which is what makes the cut exact. Killing the VM is not a substitute: unflushed data written through an ordinary drive sits in the host's page cache and outlives the process, so the test would find everything intact having proven nothing.
power_cut on a disk that was not booted mediated fails, naming the disk — a power cut that silently did nothing is exactly the failure this is here to prevent.
Making the guest's own I/O fail #
A power cut is one fault a mediated disk can serve. The rest make the guest's reads and writes fail or stall while it is running, which is how you exercise the error paths rather than the recovery path.
local vm = provium::
local disk = vm:
test
Every verb is listed on the Disk reference. Two of them are worth keeping straight, because they model genuinely different hardware failures:
fail_writes()refuses new writes but still honours flushes — a disk that will not take data.fail_after(n)counts flushes too, so it can fail anfsync: the guest is told that data it was promised was durable is gone. That is the fault a write-ahead log has to survive, and the onefail_writesdeliberately cannot produce.
The fail_after counter starts from the call that arms it, not from boot. By the time a test runs, the guest has already issued hundreds of commands mounting its filesystem, so a count from boot would be unusable.
These are the guest's faults. fault_inject above is the test's, and the two sets are cleared separately — disk:clear_policy() for these, disk:clear_faults() for those.
Detaching a disk #
disk:
local ok = pcall
assert -- "disk is detached"
disk:detach() issues a best-effort QMP device_del against the parent VM and marks the local handle detached; after that, sector ops error. This also addresses disks declared at launch. Guest removal is asynchronous, so wait for the device to disappear if the next assertion depends on its absence. The exact behaviour for disks that were never QMP-added is in the Disk reference.
Common patterns #
"Does the guest retry after a transient EIO?" #
test
"Does the filesystem remount read-only after EIO?" #
test
"Does the guest panic on EIO at boot?" #
test
Multiple disks per VM #
local vm = provium::
-- Inject EIO on data only; logs is unaffected.
vm::
Attachment order is device order in the guest, so data is /dev/vda and logs is /dev/vdb — or /dev/vdb and /dev/vdc if the profile already attached one of its own.
Use vm:disk(id) to look a disk up again anywhere later in the test:
vm::
Caveats #
- Sector size is fixed at 512 bytes. Tests that need 4 KiB sectors should expect their guest to layer that on top.
read_sectorsandwrite_sectorsgo directly to the host file, not through QEMU's block backend. This means a test that exercises QEMU's block translation (sparse holes, compression, etc.) will not see those layers — the disk userdata is a direct view of the underlying image bytes.disk:size()reports the live image file size when an image is attached. Test code that resizes the underlying file (truncate,fallocate) sees the new size, not the modelledsizefromattach_disk.
See also #
- Disk reference — every method, every error message.
- VM reference —
vm:attach_disk,vm:disk.