FluxDisk: fluxvm-blockd
fluxvm-blockd is a small NBD block server with copy-on-write overlays. On macOS the daemon starts one per VM for
apple.extra_disks of kind: overlay and attaches each export to the guest through Virtualization.framework's NBD client as an
ordinary nbd disk (see macos.md). The binary also works standalone for branching, rebasing and growing
overlay files.
Status. The NBD protocol, overlay format, encryption and lazy fetch are covered by in-tree tests against an in-tree client.
Virtualization.framework's NBD client has been run against it once on an Apple M4 (macOS 27.2, Debian 13 guest): connect,
read, write, persistence across restart, and an immutable base all held. A second run on an Apple M4 (macOS 27, Debian 13 guest, release build) served three exports to one guest through
Virtualization.framework's NBD client: a plain overlay, an encrypted overlay (encrypt, a key file) and a lazy overlay on an
HTTP base (sha256=). All three read back the base byte-for-byte (the guest's sha256sum of each disk equalled the base's); a
900 KB marker written with O_DIRECT and fsync read back identical after drop_caches on all three; the plain and lazy
overlay files held the marker in clear and the encrypted overlay file held none; the base file was unchanged. The lazy export
fetched 6 MiB (in 1 MiB ranges, from a Range-capable server) before the guest read the disk, the rest on demand, and a second
full read caused no new requests. Offline rebase and resize against a VZ guest's overlay were verified on an M4
(scripts/vz-blockd-rebase-resize-live-test.sh). Live rebase/resize/branch while the overlay is open take an exclusive
flock and refuse with "in use" (shared flock held by serve / open). Virtualization.framework's NBD client does
not negotiate structured replies (server log on an M4: structured replies: false, scripts/vz-blockd-structured-reply-live-test.sh),
so VZ always gets simple replies; the structured path is covered only by the in-tree client. An encrypted overlay's branch was verified on an M4
(scripts/vz-blockd-encrypted-branch-live-test.sh): a branch while served was refused, a wrong key was refused, the encrypted
child read the parent's marker and took its own writes, neither file held a marker in clear, the base was unchanged and the
frozen parent had none of the child's data. An overlay that has children is frozen and is served read-only.
Overlays
An overlay is a sparse file (magic FLXOVL01) that sits on a parent: a raw image, another overlay, or an http(s) URL.
Reads of blocks the overlay has not written fall through to the parent; writes land in the overlay, which allocates in 64 KiB
blocks. The parent is never modified.
Crash behaviour: data is written before its bitmap bit, so a process crash can lose a write but never exposes unwritten data.
Durability is promised only after a flush (NBD_CMD_FLUSH, or FUA). After power loss a set bit could in theory point at unwritten
data, as with a disk that lies about flushes; the guest must flush for durability.
CLI
# serve (one or more exports; stops when --watch-pid / --watch-pid-file process exits)
fluxvm-blockd serve --export data=/images/base.raw,overlay=/images/vm1.ovl --listen 127.0.0.1:10809
fluxvm-blockd branch --from vm1.ovl --to vm1-branch.ovl # new overlay on top of vm1.ovl; the parent is frozen
fluxvm-blockd rebase --overlay vm1.ovl --parent /images/base-v2.raw [--sha256 HEX]
fluxvm-blockd resize --overlay vm1.ovl --size 40G # offline grow; K, M, G, T suffixes
serve export spec: name=base[,overlay=path][,ro][,iops=N][,bps=N][,encrypt][,keyfile=PATH][,sha256=HEX][,parallel=N] [,timeout=SECS][,cache=PATH].
- serve. An
overlay=that does not exist is created on top ofbase. Ifbaseis itself an overlay file it becomes the parent (and is frozen). Withoutoverlay=, an overlaybaseis opened directly and a plain image is served raw (rois honoured). Exports are named byname; that is the NBD export name. - branch. Creates a new overlay whose parent is the source overlay and marks the source frozen: no more writes through it. Use it for a snapshot you can return to: keep serving the branch, keep the frozen parent as the checkpoint. Chains are limited to 64 layers.
- rebase. Repoints an overlay at a different parent without touching its data (
qemu-img rebase -usemantics: you assert the new parent has the same content). The parent path is stored relative to the overlay when possible;--sha256sets the expected hash of a lazy (URL) parent. - resize. Offline grow only. In place when the reserved bitmap space suffices, otherwise the file is rewritten (allocated
blocks copied, zero blocks skipped) to a temporary file and renamed over the original, upgrading the header to version 2.
Refuses with "in use" while the overlay is served or otherwise open (exclusive
flock).
Encryption and keys
encrypt makes a new overlay AES-256-XTS encrypted per 64 KiB block (tweak = block index). The key is 32 or 64 raw bytes, or
64/128 hex digits, from keyfile= / --key-file or the hex FLUXVM_BLOCKD_KEY environment variable. A salted key-check value
in the header makes a wrong key fail at open instead of decrypting to garbage. A child of an encrypted parent is created
encrypted with the same key. Keys are never printed or logged.
Limits, inherent to XTS: no authentication (someone with write access to the overlay can flip or replace blocks undetected), and the same plaintext at the same block index under the same key produces the same ciphertext, so a branch and its parent share keys and tweaks. There is no key rotation or per-overlay key derivation. Only the overlay is encrypted; the base image and the NBD connection (plaintext TCP) are not.
Lazy base (http/https)
A base that is an http(s) URL is read through range requests in 1 MiB chunks, cached in a sparse file plus a persisted bitmap
(<overlay>.lazy.data / .map next to an overlay, or cache=PATH for a direct, read-only export). A chunk's bit is set only after
its data is written and synced, so a restart keeps the cache. Requests have a timeout (default 30 s) and 2 retries; a failed
chunk surfaces as an I/O error (EIO) to the guest. With sha256= the whole image is checked once every chunk is cached; a
mismatch makes the source permanently failed. That is a completeness check, not per-read authentication: bytes read before the
cache completed were not verified. The remote image must be immutable and the server must answer ranges with 206 (a 200
is rejected). parallel= (default 4) bounds concurrent chunk fetches.
NBD export and URL
The server speaks newstyle fixed-handshake NBD over TCP: options EXPORT_NAME, GO, INFO, LIST, ABORT,
STRUCTURED_REPLY; commands READ, WRITE (+FUA), FLUSH, TRIM, WRITE_ZEROES, DISC. No TLS (STARTTLS is unsupported)
and no authentication: it listens where you tell it to, default 127.0.0.1:10809. The URL for export data on port P is
nbd://127.0.0.1:P/data. The daemon's own servers use an ephemeral port on 127.0.0.1 and export each disk under its disk name
(an unnamed disk is disk<position>).
Limits
- 64 KiB overlay block; a layer chain of at most 64 overlays.
- Up to 16 requests in flight per connection, executed in parallel; requests up to 32 MiB;
FLUSHis a full barrier. iops=/bps=are token buckets with a quarter-second burst; a large request is delayed, never rejected.- The daemon passes only
overlay,ro,iopsandbpsto its servers. Encryption, lazy bases, branch, rebase and resize are CLI features: they are not reachable throughapple.extra_disks(a disk whose overlay was prepared with the CLI, including an encrypted one, cannot be unlocked by the daemon: the key is not passed). Use the CLI on the files, or runfluxvm-blockd serveyourself and attach the export as akind: nbddisk. - An overlay file belongs to one VM: FluxVM refuses to attach or start a VM whose overlay is another record's overlay, base, or disk (see macos.md). Nothing stops a second process from opening the same file.
- Not a replacement for backups: the overlay depends on its parent staying put and unchanged (a frozen parent is only frozen by convention of this tool).