nixos/qemu-vm: 9p -> virtiofs (#552774)

This commit is contained in:
lassulus
2026-09-08 19:09:35 +00:00
committed by GitHub
5 changed files with 124 additions and 153 deletions

View File

@@ -324,6 +324,24 @@ let
''
)}
echo "Starting virtiofs daemons..."
NIX_VIRTIOFS_DIR=$(mktemp -d)
${lib.concatLines (
lib.mapAttrsToList (tag: share: ''
${lib.getExe hostPkgs.virtiofsd} \
--socket-path="$NIX_VIRTIOFS_DIR"/"${tag}" \
--shared-dir="${share.source}" \
${if share.writable then "--writeback" else "--readonly"} \
--sandbox=none \
--seccomp=none \
--cache=always \
--no-announce-submounts \
--translate-uid=host:65534:0:1 \
--translate-gid=host:65534:0:1 \
&
'') cfg.sharedDirectories
)}
# Start QEMU.
exec ${
qemu-common.qemuBinaryWith {
@@ -336,14 +354,6 @@ let
-smp ${toString config.virtualisation.cores} \
-device virtio-rng-pci \
${concatStringsSep " " config.virtualisation.qemu.networkingOptions} \
${
concatStringsSep " \\\n " (
mapAttrsToList (
tag: share:
"-virtfs local,path=${share.source},security_model=${share.securityModel},mount_tag=${tag}"
) config.virtualisation.sharedDirectories
)
} \
${drivesCmdLine config.virtualisation.qemu.drives} \
${concatStringsSep " \\\n " config.virtualisation.qemu.options} \
$QEMU_OPTS \
@@ -424,6 +434,14 @@ in
"virtualisation"
"useSecureBoot"
] "The default OVMF now always supports Secure Boot.")
(mkRemovedOptionModule [
"virtualisation"
"msize"
] "9p was replaced with virtiofs and thus this option is obsolete.")
(mkRemovedOptionModule [
"virtualisation"
"nixStore9pCache"
] "9p was replaced with virtiofs and thus this option is obsolete.")
];
options = {
@@ -438,16 +456,6 @@ in
'';
};
virtualisation.msize = mkOption {
type = types.ints.positive;
default = 16384;
description = ''
The msize (maximum packet size) option passed to 9p file systems, in
bytes. Increasing this should increase performance significantly,
at the cost of higher RAM usage.
'';
};
virtualisation.diskImage = mkOption {
type = types.nullOr types.str;
default = "./${config.system.name}.qcow2";
@@ -570,22 +578,8 @@ in
type = types.path;
description = "The mount point of the directory inside the virtual machine";
};
options.securityModel = mkOption {
type = types.enum [
"passthrough"
"mapped-xattr"
"mapped-file"
"none"
];
default = "mapped-xattr";
description = ''
The security model to use for this share:
- `passthrough`: files are stored using the same credentials as they are created on the guest (this requires QEMU to run as root)
- `mapped-xattr`: some of the file attributes like uid, gid, mode bits and link target are stored as file attributes
- `mapped-file`: the attributes are stored in the hidden .virtfs_metadata directory. Directories exported by this security model cannot interact with other unix tools
- `none`: same as "passthrough" except the sever won't report failures if it fails to set file attributes like ownership
'';
options.writable = lib.mkEnableOption "" // {
description = "Whether the directory is writable on the host and guest.";
};
}
);
@@ -610,11 +604,10 @@ in
A list of paths whose closure should be made available to
the VM.
When 9p is used, the closure is registered in the Nix
database in the VM. All other paths in the host Nix store
appear in the guest Nix store as well, but are considered
garbage (because they are not registered in the Nix
database of the guest).
When the Nix store is mounted from the host, the closure is registered
in the Nix database in the VM. All other paths in the host Nix store
appear in the guest Nix store as well, but are considered garbage
(because they are not registered in the Nix database of the guest).
When {option}`virtualisation.useNixStoreImage` is
set, the closure is copied to the Nix store image.
@@ -867,7 +860,7 @@ in
default = false;
description = ''
Build and use a disk image for the Nix store, instead of
accessing the host's one through 9p.
accessing the host's one.
For applications which do a lot of reads from the store,
this can drastically improve performance, but at the cost of
@@ -889,24 +882,7 @@ in
default = !cfg.useNixStoreImage && !cfg.useBootLoader;
defaultText = literalExpression "!cfg.useNixStoreImage && !cfg.useBootLoader";
description = ''
Mount the host Nix store as a 9p mount.
'';
};
virtualisation.nixStore9pCache = mkOption {
type = types.enum [
"loose"
"none"
"fscache"
];
default = "loose";
description = ''
Type of 9p cache to use when mounting host nix store. "none" provides
no caching. "loose" enables Linux's local VFS cache. "fscache" uses Linux's
fscache subsystem.
This option is only respected when {option}`virtualisation.mountHostNixStore`
is enabled.
Mount the host Nix store via a virtual filesystem.
'';
};
@@ -1242,22 +1218,20 @@ in
# Always mount this to /nix/.ro-store because we never want to actually
# write to the host Nix Store.
target = "/nix/.ro-store";
securityModel = "none";
};
xchg = {
source = ''"$TMPDIR"/xchg'';
securityModel = "none";
target = "/tmp/xchg";
writable = true;
};
shared = {
source = ''"''${SHARED_DIR:-$TMPDIR/xchg}"'';
target = "/tmp/shared";
securityModel = "none";
writable = true;
};
certs = mkIf cfg.useHostCerts {
source = ''"$TMPDIR"/certs'';
target = "/etc/ssl/certs";
securityModel = "none";
};
};
@@ -1303,6 +1277,12 @@ in
"-object memory-backend-memfd,id=mem0,size=${toString config.virtualisation.memorySize}M,share=on"
"-machine memory-backend=mem0"
])
(lib.flatten (
lib.mapAttrsToList (tag: share: [
"-chardev socket,id=${tag},path=$NIX_VIRTIOFS_DIR/${tag}"
"-device vhost-user-fs-pci,chardev=${tag},tag=${tag}"
]) cfg.sharedDirectories
))
(
let
alphaNumericChars = lowerChars ++ upperChars ++ (map toString (range 0 9));
@@ -1388,86 +1368,78 @@ in
virtualisation.diskSizeAutoSupported = false;
virtualisation.fileSystems =
let
mkSharedDir = tag: share: {
name = share.target;
value.device = tag;
value.fsType = "9p";
value.neededForBoot = true;
value.options = [
"trans=virtio"
"version=9p2000.L"
"msize=${toString cfg.msize}"
"x-systemd.requires=modprobe@9pnet_virtio.service"
]
++ lib.optional (tag == "nix-store") "cache=${cfg.nixStore9pCache}";
virtualisation.fileSystems = lib.mkMerge [
(lib.mapAttrs' (tag: share: {
name = share.target;
value = {
device = tag;
fsType = "virtiofs";
neededForBoot = true;
options = lib.mkIf (!share.writable) [ "ro" ];
};
in
lib.mkMerge [
(lib.mapAttrs' mkSharedDir cfg.sharedDirectories)
{
"/" = lib.mkIf cfg.useDefaultFilesystems (
if cfg.diskImage == null then
{
device = "tmpfs";
fsType = "tmpfs";
options = [ "mode=755" ];
}
else
{
device = cfg.rootDevice;
fsType = "ext4";
}
);
"/tmp" = lib.mkIf config.boot.tmp.useTmpfs {
device = "tmpfs";
fsType = "tmpfs";
neededForBoot = true;
# Sync with systemd's tmp.mount;
options = [
"mode=1777"
"strictatime"
"nosuid"
"nodev"
"size=${toString config.boot.tmp.tmpfsSize}"
];
};
"/nix/store" = lib.mkIf (cfg.useNixStoreImage || cfg.mountHostNixStore) (
if cfg.writableStore then
{
overlay = {
lowerdir = [ "/nix/.ro-store" ];
upperdir = "/nix/.rw-store/upper";
workdir = "/nix/.rw-store/work";
};
}
else
{
device = "/nix/.ro-store";
fsType = "none";
options = [ "bind" ];
}
);
"/nix/.ro-store" = lib.mkIf cfg.useNixStoreImage {
device = "/dev/disk/by-label/${nixStoreFilesystemLabel}";
fsType = "erofs";
neededForBoot = true;
options = [ "ro" ];
};
"/nix/.rw-store" = lib.mkIf (cfg.writableStore && cfg.writableStoreUseTmpfs) {
fsType = "tmpfs";
options = [ "mode=0755" ];
neededForBoot = true;
};
"${config.boot.loader.efi.efiSysMountPoint}" =
lib.mkIf (cfg.useBootLoader && cfg.bootPartition != null)
{
device = cfg.bootPartition;
fsType = "vfat";
}) cfg.sharedDirectories)
{
"/" = lib.mkIf cfg.useDefaultFilesystems (
if cfg.diskImage == null then
{
device = "tmpfs";
fsType = "tmpfs";
options = [ "mode=755" ];
}
else
{
device = cfg.rootDevice;
fsType = "ext4";
}
);
"/tmp" = lib.mkIf config.boot.tmp.useTmpfs {
device = "tmpfs";
fsType = "tmpfs";
neededForBoot = true;
# Sync with systemd's tmp.mount;
options = [
"mode=1777"
"strictatime"
"nosuid"
"nodev"
"size=${toString config.boot.tmp.tmpfsSize}"
];
};
"/nix/store" = lib.mkIf (cfg.useNixStoreImage || cfg.mountHostNixStore) (
if cfg.writableStore then
{
overlay = {
lowerdir = [ "/nix/.ro-store" ];
upperdir = "/nix/.rw-store/upper";
workdir = "/nix/.rw-store/work";
};
}
];
}
else
{
device = "/nix/.ro-store";
fsType = "none";
options = [ "bind" ];
}
);
"/nix/.ro-store" = lib.mkIf cfg.useNixStoreImage {
device = "/dev/disk/by-label/${nixStoreFilesystemLabel}";
fsType = "erofs";
neededForBoot = true;
options = [ "ro" ];
};
"/nix/.rw-store" = lib.mkIf (cfg.writableStore && cfg.writableStoreUseTmpfs) {
fsType = "tmpfs";
options = [ "mode=0755" ];
neededForBoot = true;
};
"${config.boot.loader.efi.efiSysMountPoint}" =
lib.mkIf (cfg.useBootLoader && cfg.bootPartition != null)
{
device = cfg.bootPartition;
fsType = "vfat";
};
}
];
swapDevices = (if cfg.useDefaultFilesystems then mkVMOverride else mkDefault) [ ];
boot.initrd.luks.devices = (if cfg.useDefaultFilesystems then mkVMOverride else mkDefault) { };

View File

@@ -840,9 +840,7 @@ in
healthchecks = runTest ./web-apps/healthchecks.nix;
hedgedoc = runTest ./hedgedoc.nix;
herbstluftwm = runTest ./herbstluftwm.nix;
# 9pnet_virtio used to mount /nix partition doesn't support
# hibernation. This test happens to work on x86_64-linux but
# not on other platforms.
# This test happens to work on x86_64-linux but not on other platforms.
hibernate = handleTestOn [ "x86_64-linux" ] ./hibernate.nix {
systemdStage1 = false;
};

View File

@@ -29,7 +29,9 @@ makeTest {
powerManagement.resumeCommands = "systemctl --no-block restart backdoor.service";
virtualisation.emptyDiskImages = [ (2 * config.virtualisation.memorySize) ];
# virtiofs doesn't support hibernation
virtualisation.useNixStoreImage = true;
virtualisation.sharedDirectories = lib.mkForce { };
swapDevices = lib.mkOverride 0 [
{

View File

@@ -55,17 +55,16 @@
sharedReadOnly.fail(build_derivation)
imageReadOnly.fail(build_derivation)
# Checking whether the fs type is 9P is just a proxy to test whether the
# Nix Store is shared. If we switch to a different technology (e.g.
# virtiofs) for sharing, we need to adjust these tests.
# Checking whether the fs type is virtiofs is just a proxy to test whether the
# Nix Store is shared.
with subtest("Nix store is shared from the host via 9P"):
sharedWritable.succeed("findmnt --kernel --type 9P /nix/.ro-store")
sharedReadOnly.succeed("findmnt --kernel --type 9P /nix/.ro-store")
with subtest("Nix store is shared from the host via virtiofs"):
sharedWritable.succeed("findmnt --kernel --type virtiofs /nix/.ro-store")
sharedReadOnly.succeed("findmnt --kernel --type virtiofs /nix/.ro-store")
with subtest("Nix store is not shared via 9P"):
imageWritable.fail("findmnt --kernel --type 9P /nix/.ro-store")
imageReadOnly.fail("findmnt --kernel --type 9P /nix/.ro-store")
with subtest("Nix store is not shared via virtiofs"):
imageWritable.fail("findmnt --kernel --type virtiofs /nix/.ro-store")
imageReadOnly.fail("findmnt --kernel --type virtiofs /nix/.ro-store")
with subtest("Nix store is not mounted separately"):
rootDevice = fullDisk.succeed("stat -c %d /")

View File

@@ -50,7 +50,7 @@
)
}
mkdir -p /tmp/shared
mount -t 9p shared -o trans=virtio,version=9p2000.L /tmp/shared
mount -t virtiofs shared /tmp/shared
touch /tmp/shared/shutdown-test
umount /tmp/shared
'';