From 2038dd3aa0a58f3084689b7fb1c7edc0591c6d41 Mon Sep 17 00:00:00 2001 From: nikstur Date: Fri, 14 Aug 2026 12:53:39 +0200 Subject: [PATCH 1/4] nixos/tests/systemd: 9p -> virtiofs --- nixos/tests/systemd.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/tests/systemd.nix b/nixos/tests/systemd.nix index b04b288e8939..24c045ceb082 100644 --- a/nixos/tests/systemd.nix +++ b/nixos/tests/systemd.nix @@ -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 ''; From c04500f019853aebcac32e4d5270fca678a90a66 Mon Sep 17 00:00:00 2001 From: nikstur Date: Fri, 14 Aug 2026 14:11:14 +0200 Subject: [PATCH 2/4] nixos/tests/hibernate: disable all shared directories --- nixos/tests/all-tests.nix | 4 +--- nixos/tests/hibernate.nix | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 647f548ba3d3..e8736824eceb 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -785,9 +785,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; }; diff --git a/nixos/tests/hibernate.nix b/nixos/tests/hibernate.nix index 53daec48d6c0..24e0c91e84df 100644 --- a/nixos/tests/hibernate.nix +++ b/nixos/tests/hibernate.nix @@ -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 [ { From 97ec03a35edd482b9ef688811e6fea1f6878cb29 Mon Sep 17 00:00:00 2001 From: nikstur Date: Fri, 14 Aug 2026 21:54:27 +0200 Subject: [PATCH 3/4] nixos/tests/qemu-vm-store: check for virtiofs instead of 9p --- nixos/tests/qemu-vm-store.nix | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/nixos/tests/qemu-vm-store.nix b/nixos/tests/qemu-vm-store.nix index 1824ef559159..00301b6185be 100644 --- a/nixos/tests/qemu-vm-store.nix +++ b/nixos/tests/qemu-vm-store.nix @@ -51,17 +51,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 /") From de829fca8c2ee9b47e20f4e1459853a53a281c8b Mon Sep 17 00:00:00 2001 From: nikstur Date: Fri, 14 Aug 2026 02:29:37 +0200 Subject: [PATCH 4/4] nixos/qemu-vm: 9p -> virtiofs --- nixos/modules/virtualisation/qemu-vm.nix | 252 ++++++++++------------- 1 file changed, 112 insertions(+), 140 deletions(-) diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix index a99dd8f2ec5e..adeb840787e6 100644 --- a/nixos/modules/virtualisation/qemu-vm.nix +++ b/nixos/modules/virtualisation/qemu-vm.nix @@ -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) { };