A NixOS module that turns declarative machine and PCI definitions into host VFIO policy, libvirt configuration, generated domains, storage, device preparation and QEMU lifecycle hooks.
The same Nix data decides how each host function reaches vfio-pci, where it
appears in the guest PCI tree, whether its option ROM is dumped, whether a
resizable BAR resource is rewritten, and how the device is returned after
shutdown.
flowchart TD
A["Nix machine definitions"] --> B["Boot and modprobe policy"]
A --> C["libvirtd pre-start preparation"]
A --> D["Generated libvirt XML"]
A --> E["Per-VM QEMU hook"]
For every entry in virtualisation.virtualMachines.machines, the module can
produce:
- host kernel parameters and
vfio,vfio_iommu_type1, andvfio_pciinitrd modules; - modprobe rules built from the declared vendor IDs, host drivers, and selected binding strategy;
- a qcow2 disk created on first libvirtd start;
- a normal domain and a separate
-setupinstallation domain; - a libvirt directory storage pool pointing at the configured ISO directory;
- PCI
<hostdev>XML with an explicit guest bus, slot, and function; - optional VGA option-ROM extraction and XML attachment;
- optional resizable-BAR writes through
resourceN_resize; - a QEMU hook containing the matching unbind, bind, rescan, display-manager, and Looking Glass operations.
Enabling the module currently applies the following host-wide configuration:
intel_iommu=on,amd_iommu=on, andiommu=pt;kvm_amd.npt=1,kvm_amd.avic=1, andvideo=efifb:off;vfio_iommu_type1.allow_unsafe_interrupts=1andkvm.ignore_msrs=1;- libvirt with
qemu_kvm, root-run QEMU, swtpm, and OVMF built with Secure Boot and TPM support; virt-manager,looking-glass-client,rofi-vm, and themacos-dlhelper inenvironment.systemPackages.
These are module-wide choices in the current source, not per-machine options.
The three booleans under functions[].blacklist select how a PCI function is
prepared. If all three are false, the module uses the dynamic QEMU-hook path.
| Configuration | Generated behavior | Release behavior |
|---|---|---|
driver = true |
Adds options <driver> modeset=0 and blacklist <driver> for non-disk devices |
No explicit manual bind/unbind path is generated for this strategy |
vfioPriority = true |
Adds the vendor ID to vfio-pci ids=... and emits softdep <driver> pre: vfio-pci |
Device remains assigned according to the boot-time policy |
startUnload = true |
At libvirtd.preStart, unbinds the host driver and writes the ID to vfio-pci/new_id |
No QEMU-release path is emitted for this strategy |
| All false | On QEMU prepare, unbinds the current driver and writes new_id |
On release, removes the ID, removes the PCI device, rescans the bus, and optionally restarts the display manager |
The schema does not enforce mutual exclusion between these flags. Combinations therefore combine the generated snippets; choose them deliberately.
passthrough.pcies models a physical slot and its individual functions:
passthrough = {
enable = true;
restartDm = true;
pcies = [
{
disk = false;
lines = {
bus = "0b";
slot = "00";
vmBus = "09";
functions = [
{
function = "0";
vendor = "1002:0000"; # replace with the real vendor:device ID
drivers = [ "amdgpu" ];
blacklist = {
driver = false;
vfioPriority = true;
startUnload = false;
};
fix = {
rom = true;
rebar.enable = false;
rebar.resources = [];
};
}
];
};
}
];
};The host address is assembled as
0000:<bus>:<slot>.<function>. The generated guest address uses vmBus while
retaining the declared slot and function.
When disk = false, each function becomes a managed libvirt PCI host device.
If fix.rom is enabled and the build-time lspci probe identifies the
function as VGA, the module:
- reads its ROM from sysfs once;
- stores it under
/var/lib/libvirt/roms/pcie-<BDF>.rom; - adds
<rom bar="on" file="..."/>to that function's hostdev XML.
When disk = true, the generated hostdev receives <boot order="1"/>. This
path is also inserted into the generic installation domain, allowing an entire
controller or physical disk to participate in guest installation.
For every declared fix.rebar.resources entry, libvirtd pre-start writes the
configured integer to:
/sys/bus/pci/devices/0000:<BDF>/resource<resource>_resize
The function is temporarily unbound from vfio-pci before the writes and bound
again afterward. Resource indices and resize values are intentionally supplied
by the machine configuration because they are device-specific.
The generic normal domain is generated with:
- Q35
pc-q35-8.1and OVMF pflash/NVRAM; - host-passthrough CPU topology with
topoext; - shared
memfdmemory, required by Looking Glass; - Hyper-V enlightenments, a hidden KVM state, local-time clock and TPM 2.0;
- a virtio network adapter, virtio inputs, virtio-serial, virtio-scsi, SATA, SPICE audio, watchdog and balloon device;
- fifteen PCIe root ports plus a PCIe-to-PCI bridge, giving passed-through functions explicit guest attachment points;
- a qcow2 system disk using
cache='directsync'anddiscard='unmap'; - QXL/SPICE when passthrough is disabled, or no emulated video adapter when it is enabled;
- an optional 128 MiB
ivshmem-plainLooking Glass device at guest bus0x10.
The corresponding -setup domain adds the configured installation ISO. For
os = "win11", it also attaches virtio-win.iso. The setup domain keeps a
QXL/SPICE console and only injects PCI entries marked as disks; the normal
domain receives the non-disk functions and Looking Glass definition.
For os = "linux", the generated libosinfo identifier changes to the Linux
profile. Any other non-macos value currently falls back to the Windows 11
libosinfo identifier; only the exact win11 value adds the VirtIO driver ISO.
os = "macos" selects separate normal and setup XML templates containing:
- OpenCore and BaseSystem disks in addition to the guest disk;
- custom OVMF code and variable files under
/var/lib/libvirt/firmware/macos; - Apple SMC command-line data and an explicit Penryn/GenuineIntel CPU model;
- a
vmxnet3network device, ICH9 USB controllers, serial console and guest agent channel; - no memory-balloon device;
- normal-domain injection of non-disk PCI functions.
The installed macos-dl helper downloads the recovery media, OpenCore image,
and firmware files used by those templates from kholia/OSX-KVM.
When hardware.disk.enable = true, libvirtd pre-start creates the qcow2 file if
it does not already exist. hardware.disk.size is interpreted in GiB.
hardware.disk.ssdEmulation emits a QEMU override with rotation_rate = 1:
- for the generic template, on the
scsi0-0-0-0device; - for macOS, on the three SATA aliases used by OpenCore, the guest disk, and BaseSystem.
Existing disks are never recreated or resized by the module.
During libvirtd.service pre-start, each machine definition materializes these
paths:
| Path | Source |
|---|---|
<hardware.disk.path>/<name>.qcow2 |
Created with qemu-img when absent |
/var/lib/libvirt/roms/pcie-<BDF>.rom |
Dumped from sysfs for detected VGA functions when absent |
/var/lib/libvirt/hooks/qemu.d/<name> |
Store-backed generated QEMU hook |
/var/lib/libvirt/storage/ISO-<name>.xml |
Store-backed ISO pool XML |
/var/lib/libvirt/qemu/<name>.xml |
Store-backed normal-domain XML |
/var/lib/libvirt/qemu/<name>-setup.xml |
Store-backed installation-domain XML |
The XML and hook files are symlinks into the Nix store. Editing them directly does not change their source; edit the Nix module or templates and rebuild.
With lookingGlass = true, the normal domain receives a fixed 128 MiB shared
memory device. On the QEMU started operation, the generated hook changes
/dev/shm/looking-glass ownership to <username>:libvirtd.
The module installs the client but does not configure the guest-side Looking Glass host or IVSHMEM driver.
Add the module as a flake input:
inputs.revolunix-vms = {
url = "github:RevolunixOS/module-virtual-machines";
inputs.nixpkgs.follows = "nixpkgs";
};Then import and configure it in the target NixOS system:
{
imports = [ inputs.revolunix-vms.nixosModules.default ];
virtualisation.virtualMachines = {
enable = true;
username = "your-user";
vmFolderPath = "/home/your-user/VM";
isoFolderPath = "/home/your-user/VM/ISO";
machines = [
{
name = "win11";
os = "win11";
isoName = "Windows.iso";
uuid = "";
uuidSetup = "";
lookingGlass = true;
hardware = {
cores = 6;
threads = 2;
memory = 16;
disk = {
enable = true;
size = 256;
path = "/home/your-user/VM/DISK";
ssdEmulation = true;
};
};
passthrough = {
enable = false;
restartDm = false;
pcies = [];
};
}
];
};
}The module expects pkgs.rofi-vm to exist. The original RevolunixOS consumer
passes the custom revolunixpkgs package set as pkgs; a vanilla Nixpkgs
consumer must provide rofi-vm through an overlay or remove that package from
the module.
| Option | Type | Default | Effect in the current implementation |
|---|---|---|---|
enable |
boolean | false |
Enables all host, libvirt and generation logic |
username |
string | required | Builds default paths, Samba ownership and Looking Glass ownership |
sambaAccess.enable |
boolean | false |
Exposes the user's home and /run/media/<user> as writable Samba shares |
vmFolderPath |
string | /home/<user>/VM |
Base for default disk and ISO paths |
isoFolderPath |
string | <vmFolderPath>/ISO |
Source directory of the generated ISO pool |
machines[].name |
string | win11 |
Domain name, disk filename and generated state paths |
machines[].os |
string | win11 |
Selects generic or macOS XML and OS-specific media |
machines[].uuid |
string | empty | Generic normal-domain UUID; empty invokes uuidgen during the Nix build |
machines[].uuidSetup |
string | empty | Generic setup-domain UUID; empty invokes uuidgen during the Nix build |
machines[].isoName |
string | win11.iso |
Installation ISO filename |
machines[].lookingGlass |
boolean | true |
Adds IVSHMEM and the ownership hook |
hardware.cores |
integer | 2 |
CPU cores in the generated topology |
hardware.threads |
integer | 2 |
Threads per core; vCPU count is cores × threads |
hardware.memory |
integer | 8 |
Guest memory in GiB |
hardware.disk.enable |
boolean | true |
Generates and attaches the qcow2 disk |
hardware.disk.size |
integer | 128 |
Initial qcow2 size in GiB |
hardware.disk.path |
string | <vmFolderPath>/DISK |
Disk directory |
hardware.disk.ssdEmulation |
boolean | true |
Emits the rotation-rate override |
passthrough.enable |
boolean | false |
Enables hostdev XML and device-management logic |
passthrough.restartDm |
boolean | false |
Restarts display-manager.service during the QEMU prepare and release hook operations |
These are concrete properties of the current source and matter when adapting it to a new host:
src/qemuHook.shcurrently filters lifecycle actions withOBJECT == "win11". Domains with another name receive generated files, but the prepare/started/release body does not run until that condition is made name-aware.- The generic XML contains fixed SMBIOS strings and a fixed MAC address. The
macOS templates also contain fixed MAC addresses and fixed UUIDs; configured
uuidvalues are not substituted into the macOS XML. - The ISO pool XML has a fixed pool name and UUID even though one symlink is generated per machine.
- VGA detection executes
lspcithrough a Nix build-time derivation. ROM injection therefore depends on the build environment seeing the host PCI device. - Empty UUID options call
uuidgenfrom a build-time derivation. Set explicit UUIDs when stable identity across rebuilds and caches is required. - The binding flags are independent booleans and have no assertions preventing contradictory combinations or incomplete PCI declarations.
- The macOS templates insert normal non-disk passthrough functions but do not
contain the
pciesDiskXmlplaceholder used by the generic templates. - The flake pins NixOS 24.05, while the XML pins the Q35 8.1 machine type and
several
/run/libvirt/nix-*paths. - QEMU runs as root and unsafe VFIO interrupts are enabled globally.
These constraints are documented so downstream configurations can decide which values to preserve, parameterize or remove.
flake.nix exports nixosModules.virtual-machine and .default
default.nix option schema and all generated host/guest configuration
src/template.xml generic normal domain
src/template-setup.xml generic installation domain
src/macOS.xml macOS normal domain
src/macOS-setup.xml macOS installation domain
src/ISO.xml libvirt directory-pool template
src/qemuHook.sh lifecycle-hook template
See LICENSE.