From 601566155bc2d4689bd97d7ca1fe30489fcb6ee9 Mon Sep 17 00:00:00 2001 From: "R. Ryantm" Date: Mon, 16 Mar 2026 17:40:05 +0000 Subject: [PATCH 01/20] dhcpcd: 10.3.0 -> 10.3.1 --- pkgs/by-name/dh/dhcpcd/package.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/by-name/dh/dhcpcd/package.nix b/pkgs/by-name/dh/dhcpcd/package.nix index 87cc09b8c36f..fd0119918706 100644 --- a/pkgs/by-name/dh/dhcpcd/package.nix +++ b/pkgs/by-name/dh/dhcpcd/package.nix @@ -15,13 +15,13 @@ stdenv.mkDerivation (finalAttrs: { pname = "dhcpcd"; - version = "10.3.0"; + version = "10.3.1"; src = fetchFromGitHub { owner = "NetworkConfiguration"; repo = "dhcpcd"; rev = "v${finalAttrs.version}"; - sha256 = "sha256-XbXZkws1eHvN7OEq7clq2kziwwdk04lNrWbJ9RdHExU="; + sha256 = "sha256-L2rR6/qMHWVth2GR3VAoBZmhA6lmCLddbi0VvEG5r70="; }; nativeBuildInputs = [ pkg-config ]; From 7508a0338e13e9068e70df45b31f104ef9fdfb6a Mon Sep 17 00:00:00 2001 From: r-vdp Date: Wed, 4 Mar 2026 23:01:43 +0100 Subject: [PATCH 02/20] nixos/qemu-vm: replace postBootCommands by a systemd service Reapply the systemd service approach for registering nix store paths in QEMU VMs that was introduced in #490967, fixing the ordering issue that caused it to be reverted in #496623. The service previously ran in multi-user.target with After=nix-daemon, racing with the test backdoor and other services. nix-store --load-db writes directly to the SQLite DB without needing the daemon, so move the service to sysinit.target with DefaultDependencies=false. This ensures the store DB is populated before any regular service starts. Requires the accompanying channel.nix change to also convert the channel init from boot.postBootCommands to a systemd service, since postBootCommands runs before systemd and can no longer rely on the store DB being loaded at that point. --- nixos/modules/virtualisation/qemu-vm.nix | 37 ++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix index 5f001e05aa47..f0ff6fb1d475 100644 --- a/nixos/modules/virtualisation/qemu-vm.nix +++ b/nixos/modules/virtualisation/qemu-vm.nix @@ -1236,11 +1236,38 @@ in # allow `system.build.toplevel' to be included. (If we had a direct # reference to ${regInfo} here, then we would get a cyclic # dependency.) - boot.postBootCommands = lib.mkIf config.nix.enable '' - if [[ "$(cat /proc/cmdline)" =~ regInfo=([^ ]*) ]]; then - ${config.nix.package.out}/bin/nix-store --load-db < ''${BASH_REMATCH[1]} - fi - ''; + systemd.services.register-nix-paths = lib.mkIf config.nix.enable { + # Run early during boot so the nix store DB is populated before any + # service (or test backdoor) tries to use nix commands. + # nix-store --load-db writes to the SQLite DB directly, so it does not + # need the nix-daemon. + unitConfig.DefaultDependencies = false; + wantedBy = [ + "sysinit.target" + ]; + before = [ + "sysinit.target" + "shutdown.target" + "nix-daemon.socket" + "nix-daemon.service" + ]; + after = [ + "local-fs.target" + ]; + conflicts = [ + "shutdown.target" + ]; + restartIfChanged = false; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if [[ "$(cat /proc/cmdline)" =~ regInfo=([^ ]*) ]]; then + ${lib.getExe' config.nix.package.out "nix-store"} --load-db < "''${BASH_REMATCH[1]}" + fi + ''; + }; boot.initrd.availableKernelModules = optional (cfg.qemu.diskInterface == "scsi") "sym53c8xx" ++ optional (cfg.tpm.enable) "tpm_tis"; From d292d475265fa54d56a69408a23b6f27535f9af1 Mon Sep 17 00:00:00 2001 From: r-vdp Date: Wed, 4 Mar 2026 23:01:52 +0100 Subject: [PATCH 03/20] nixos/channel: convert postBootCommands to a systemd service Replace boot.postBootCommands with a nix-channel-init systemd service. This is needed because the nix store DB registration in QEMU VMs now happens in a systemd service (register-nix-paths) rather than in postBootCommands, so the channel init must also be a systemd service to express the ordering dependency. The service runs in sysinit.target, after local-fs.target and (when present) register-nix-paths.service, but before nix-daemon.socket. Since nix-env is invoked as root before the daemon socket exists, it accesses the store directly. --- nixos/modules/installer/cd-dvd/channel.nix | 58 ++++++++++++++++------ 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/nixos/modules/installer/cd-dvd/channel.nix b/nixos/modules/installer/cd-dvd/channel.nix index e5fa5db420be..ef8606bfc977 100644 --- a/nixos/modules/installer/cd-dvd/channel.nix +++ b/nixos/modules/installer/cd-dvd/channel.nix @@ -54,19 +54,49 @@ in }; # Provide the NixOS/Nixpkgs sources in /etc/nixos. This is required - # for nixos-install. - boot.postBootCommands = lib.mkAfter '' - if ! [ -e /var/lib/nixos/did-channel-init ]; then - echo "unpacking the NixOS/Nixpkgs sources..." - mkdir -p /nix/var/nix/profiles/per-user/root - ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/per-user/root/channels \ - -i ${channelSources} --quiet --option build-use-substitutes false \ - ${lib.optionalString config.boot.initrd.systemd.enable "--option sandbox false"} # There's an issue with pivot_root - mkdir -m 0700 -p /root/.nix-defexpr - ln -s /nix/var/nix/profiles/per-user/root/channels /root/.nix-defexpr/channels - mkdir -m 0755 -p /var/lib/nixos - touch /var/lib/nixos/did-channel-init - fi - ''; + # for nixos-install. We use a systemd service rather than + # boot.postBootCommands so that ordering relative to other + # early-boot services (e.g. register-nix-paths in QEMU VMs) is + # explicit. + systemd.services.nix-channel-init = { + description = "Initialize NixOS Channel"; + # Run early so the channel is available before regular services. + # nix-env is invoked before nix-daemon.socket is up, so it + # accesses the store directly (we are root). + unitConfig.DefaultDependencies = false; + wantedBy = [ "sysinit.target" ]; + before = [ + "sysinit.target" + "shutdown.target" + "nix-daemon.socket" + "nix-daemon.service" + ]; + after = [ + "local-fs.target" + # In QEMU VMs the store DB is populated by register-nix-paths. + # On real hardware this unit does not exist and the dependency + # is silently ignored by systemd. + "register-nix-paths.service" + ]; + conflicts = [ "shutdown.target" ]; + restartIfChanged = false; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if ! [ -e /var/lib/nixos/did-channel-init ]; then + echo "unpacking the NixOS/Nixpkgs sources..." + mkdir -p /nix/var/nix/profiles/per-user/root + ${lib.getExe' config.nix.package.out "nix-env"} -p /nix/var/nix/profiles/per-user/root/channels \ + -i ${channelSources} --quiet --option build-use-substitutes false \ + ${lib.optionalString config.boot.initrd.systemd.enable "--option sandbox false"} # There's an issue with pivot_root + mkdir -m 0700 -p /root/.nix-defexpr + ln -s /nix/var/nix/profiles/per-user/root/channels /root/.nix-defexpr/channels + mkdir -m 0755 -p /var/lib/nixos + touch /var/lib/nixos/did-channel-init + fi + ''; + }; }; } From 6ab12a513a4debf1443a583fb5b2a3a072d561a7 Mon Sep 17 00:00:00 2001 From: r-vdp Date: Wed, 4 Mar 2026 23:11:08 +0100 Subject: [PATCH 04/20] nixos: convert remaining postBootCommands that load the nix DB to systemd services Convert all remaining boot.postBootCommands that call nix-store --load-db into register-nix-paths systemd services for consistency with qemu-vm.nix and to make the ordering dependency with nix-channel-init explicit. All services use the same unit name (register-nix-paths) and the same sysinit.target ordering, so nix-channel-init's after=register-nix-paths.service dependency works uniformly across all platforms. Platform-specific logic (netboot password setup, sd-image partition expansion) is kept in the service script or in boot.postBootCommands as appropriate. --- nixos/modules/installer/cd-dvd/iso-image.nix | 35 ++++++-- nixos/modules/installer/netboot/netboot.nix | 41 ++++++--- nixos/modules/installer/sd-card/sd-image.nix | 85 +++++++++++++------ nixos/modules/profiles/docker-container.nix | 42 ++++++--- .../modules/virtualisation/lxc-container.nix | 35 ++++++-- nixos/modules/virtualisation/proxmox-lxc.nix | 41 ++++++--- 6 files changed, 202 insertions(+), 77 deletions(-) diff --git a/nixos/modules/installer/cd-dvd/iso-image.nix b/nixos/modules/installer/cd-dvd/iso-image.nix index cf96b1abe880..0e519feca76a 100644 --- a/nixos/modules/installer/cd-dvd/iso-image.nix +++ b/nixos/modules/installer/cd-dvd/iso-image.nix @@ -1054,16 +1054,33 @@ in } ); - boot.postBootCommands = '' - # After booting, register the contents of the Nix store on the - # CD in the Nix database in the tmpfs. - ${config.nix.package.out}/bin/nix-store --load-db < /nix/store/nix-path-registration + systemd.services.register-nix-paths = { + description = "Register Nix Store Paths"; + unitConfig.DefaultDependencies = false; + wantedBy = [ "sysinit.target" ]; + before = [ + "sysinit.target" + "shutdown.target" + "nix-daemon.socket" + "nix-daemon.service" + ]; + after = [ "local-fs.target" ]; + conflicts = [ "shutdown.target" ]; + restartIfChanged = false; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + # After booting, register the contents of the Nix store on the + # CD in the Nix database in the tmpfs. + ${lib.getExe' config.nix.package.out "nix-store"} --load-db < /nix/store/nix-path-registration - # nixos-rebuild also requires a "system" profile and an - # /etc/NIXOS tag. - touch /etc/NIXOS - ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system - ''; + # nixos-rebuild also requires a "system" profile and an /etc/NIXOS tag. + touch /etc/NIXOS + ${lib.getExe' config.nix.package.out "nix-env"} -p /nix/var/nix/profiles/system --set /run/current-system + ''; + }; # Add vfat support to the initrd to enable people to copy the # contents of the CD to a bootable USB stick. diff --git a/nixos/modules/installer/netboot/netboot.nix b/nixos/modules/installer/netboot/netboot.nix index a4d9664a5531..4b5593d37d88 100644 --- a/nixos/modules/installer/netboot/netboot.nix +++ b/nixos/modules/installer/netboot/netboot.nix @@ -166,27 +166,46 @@ with lib; boot.loader.timeout = 10; + systemd.services.register-nix-paths = { + description = "Register Nix Store Paths"; + unitConfig.DefaultDependencies = false; + wantedBy = [ "sysinit.target" ]; + before = [ + "sysinit.target" + "shutdown.target" + "nix-daemon.socket" + "nix-daemon.service" + ]; + after = [ "local-fs.target" ]; + conflicts = [ "shutdown.target" ]; + restartIfChanged = false; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + # After booting, register the contents of the Nix store + # in the Nix database in the tmpfs. + ${lib.getExe' config.nix.package "nix-store"} --load-db < /nix/store/nix-path-registration + + # nixos-rebuild also requires a "system" profile and an /etc/NIXOS tag. + touch /etc/NIXOS + ${lib.getExe' config.nix.package "nix-env"} -p /nix/var/nix/profiles/system --set /run/current-system + ''; + }; + boot.postBootCommands = '' - # After booting, register the contents of the Nix store - # in the Nix database in the tmpfs. - ${config.nix.package}/bin/nix-store --load-db < /nix/store/nix-path-registration - - # nixos-rebuild also requires a "system" profile and an - # /etc/NIXOS tag. - touch /etc/NIXOS - ${config.nix.package}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system - # Set password for user nixos if specified on cmdline # Allows using nixos-anywhere in headless environments for o in $( Date: Mon, 30 Mar 2026 19:41:34 +0000 Subject: [PATCH 05/20] libblake3: 1.8.3 -> 1.8.4 --- pkgs/by-name/li/libblake3/package.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/by-name/li/libblake3/package.nix b/pkgs/by-name/li/libblake3/package.nix index da12d4c73181..d2d392071d73 100644 --- a/pkgs/by-name/li/libblake3/package.nix +++ b/pkgs/by-name/li/libblake3/package.nix @@ -10,7 +10,7 @@ stdenv.mkDerivation (finalAttrs: { pname = "libblake3"; - version = "1.8.3"; + version = "1.8.4"; outputs = [ "out" @@ -21,7 +21,7 @@ stdenv.mkDerivation (finalAttrs: { owner = "BLAKE3-team"; repo = "BLAKE3"; tag = finalAttrs.version; - hash = "sha256-aj+fru2saqWxDDiV3mNCZZeZIGTxSgta/X50R87hoko="; + hash = "sha256-Xz0LH0YpUjDishvXsW6VNK8msFlPXg08wFoSfbgws0g="; }; sourceRoot = finalAttrs.src.name + "/c"; From f655b835a08bd4d7534aef86c17820fa7fb2868c Mon Sep 17 00:00:00 2001 From: "R. Ryantm" Date: Sun, 5 Apr 2026 01:42:04 +0000 Subject: [PATCH 06/20] bind: 9.20.21 -> 9.20.22 --- pkgs/by-name/bi/bind/package.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/by-name/bi/bind/package.nix b/pkgs/by-name/bi/bind/package.nix index 0c4a7dd86461..aa435fcb2feb 100644 --- a/pkgs/by-name/bi/bind/package.nix +++ b/pkgs/by-name/bi/bind/package.nix @@ -29,11 +29,11 @@ stdenv.mkDerivation (finalAttrs: { pname = "bind"; - version = "9.20.21"; + version = "9.20.22"; src = fetchurl { url = "https://downloads.isc.org/isc/bind9/${finalAttrs.version}/bind-${finalAttrs.version}.tar.xz"; - hash = "sha256-FeG1oifSiQ98ToI6bqAY3nDuLzoOhZy/89gqrYWQ3gM="; + hash = "sha256-y6kv9jG5SWVfR1/ktUKQ9oYP0AcNOZ8iefZDfA04PsY="; }; outputs = [ From 5513c368ad2d1420cbb65e8a36c77bb172569ec2 Mon Sep 17 00:00:00 2001 From: Will Fancher Date: Sun, 5 Apr 2026 17:22:48 -0400 Subject: [PATCH 07/20] nixos/power-management: Handle resume correctly with `sleep.target` Fixes `nixos.tests.hibernate` nondeterministic failure. The documentation in `systemd.special(7)` recommends using this `StopWhenUnneeded` method, rather than using custom unit logic post-resume. This method is sufficiently general, and matches systemd's typical model of using `ExecStart` and `ExecStop` as duals that mirror each others processes across symmetric events in the system's life cycle, e.g. bootup / shutdown. This involved removing `post-resume.target`. This was introduced in d5604f0b22daf8a0ec341c70607f014dfc9bf207 as a way to notify services of resume events, but as discussed, this is not the typical model. See: https://github.com/NixOS/nixpkgs/pull/488429/changes#r3038005884 --- .../manual/release-notes/rl-2605.section.md | 2 + nixos/modules/config/power-management.nix | 57 +++++-------------- nixos/modules/services/hardware/undervolt.nix | 22 +++++-- 3 files changed, 33 insertions(+), 48 deletions(-) diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index 5220d49e71e2..1d50b7b8d257 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -137,6 +137,8 @@ - `services.uptime` has been removed because the package it relies on does not exist anymore in nixpkgs. +- `post-resume.target` has been removed. See {manpage}`systemd.special(7)` about `sleep.target` for instructions on ordering a process after resume with `ExecStop=`. + - `services.kubernetes.addons.dns.coredns` has been renamed to `services.kubernetes.addons.dns.corednsImage` and now expects a package instead of attrs. Now, by default, nixpkgs.coredns in conjunction with dockerTools.buildImage is used, instead of pulling the upstream container image from Docker Hub. If you want the old behavior, you can set: diff --git a/nixos/modules/config/power-management.nix b/nixos/modules/config/power-management.nix index 202ecdb7cffb..9d015c021794 100644 --- a/nixos/modules/config/power-management.nix +++ b/nixos/modules/config/power-management.nix @@ -90,68 +90,35 @@ in https://www.freedesktop.org/software/systemd/man/latest/systemd.special.html#sleep.target ''; - systemd.targets.post-resume = { - description = "Post-Resume Actions"; - requires = [ "post-resume.service" ]; - after = [ "post-resume.service" ]; - wantedBy = [ "sleep.target" ]; - unitConfig.StopWhenUnneeded = true; - }; - systemd.services = { # Service executed before suspending/hibernating. - pre-sleep = { - description = "Pre-Sleep Actions"; + sleep-actions = { + description = "Sleep Actions"; wantedBy = [ "sleep.target" ]; before = [ "sleep.target" ]; + unitConfig.StopWhenUnneeded = true; script = '' # NixOS pre-sleep script # config.powerManagement.powerDownCommands ${cfg.powerDownCommands} ''; - serviceConfig.Type = "oneshot"; - }; - - # Service executed after resuming from suspend/hibernate - post-resume = { - description = "Post-Resume Actions"; - # Pulled in by post-resume.service above - after = [ "sleep.target" ]; - script = '' + preStop = '' # NixOS pre-resume script - /run/current-system/systemd/bin/systemctl try-restart --no-block post-resume.target - # config.powerManagement.resumeCommands ${cfg.resumeCommands} # config.powerManagement.powerUpCommands ${cfg.powerUpCommands} ''; - serviceConfig.Type = "oneshot"; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; }; - # Service executed before shutdown - pre-shutdown = { - description = "Pre-Shutdown Actions"; - wantedBy = [ - "shutdown.target" - ]; - before = [ - "shutdown.target" - ]; - script = '' - # NixOS pre-shutdown script - - # config.powerManagement.powerDownCommands - ${cfg.powerDownCommands} - ''; - serviceConfig.Type = "oneshot"; - unitConfig.DefaultDependencies = false; - }; - - # Service executed after boot + # Service executed after boot, and stopped during shutdown post-boot = { description = "Post-Boot Actions"; # It's not well defined at what point in the bootup sequence this should run @@ -167,6 +134,12 @@ in # config.powerManagement.powerUpCommands ${cfg.powerUpCommands} ''; + preStop = '' + # NixOS pre-shutdown script + + # config.powerManagement.powerDownCommands + ${cfg.powerDownCommands} + ''; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; diff --git a/nixos/modules/services/hardware/undervolt.nix b/nixos/modules/services/hardware/undervolt.nix index a2c16f9bd8de..585364e24e34 100644 --- a/nixos/modules/services/hardware/undervolt.nix +++ b/nixos/modules/services/hardware/undervolt.nix @@ -194,12 +194,7 @@ in systemd.services.undervolt = { description = "Intel Undervolting Service"; - # Apply undervolt on boot, nixos generation switch and resume - wantedBy = [ - "multi-user.target" - "post-resume.target" - ]; - after = [ "post-resume.target" ]; # Not sure why but it won't work without this + wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "oneshot"; @@ -208,6 +203,21 @@ in }; }; + systemd.services.undervolt-sleep = { + description = "Preserve Intel Undervolting After Sleep"; + + wantedBy = [ "sleep.target" ]; + before = [ "sleep.target" ]; + + unitConfig.StopWhenUnneeded = true; + serviceConfig = { + Type = "oneshot"; + Restart = "no"; + RemainAfterExit = true; + ExecStop = "${cfg.package}/bin/undervolt ${toString cliArgs}"; + }; + }; + systemd.timers.undervolt = lib.mkIf cfg.useTimer { description = "Undervolt timer to ensure voltage settings are always applied"; partOf = [ "undervolt.service" ]; From e6fbe2b798b3711e4fedbd7c3c6293aa6aac1270 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Tue, 7 Apr 2026 01:35:26 +0200 Subject: [PATCH 08/20] nixos/modules: import intel-npu module This was missed twice during review. --- nixos/modules/module-list.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 10d04094dd74..ee38f638dfcf 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -61,6 +61,7 @@ ./hardware/cpu/amd-ryzen-smu.nix ./hardware/cpu/amd-sev.nix ./hardware/cpu/intel-microcode.nix + ./hardware/cpu/intel-npu.nix ./hardware/cpu/intel-sgx.nix ./hardware/cpu/x86-msr.nix ./hardware/decklink.nix From 44cdcdf7aa52a4a71eaaae0bc215a71a4f297e18 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Tue, 7 Apr 2026 01:36:04 +0200 Subject: [PATCH 09/20] nixos/nixos-generate-config: drop intel-npu import Loading modules at config gen time is wrong. This should only happen for profiles and the options should be available all the time instead. --- nixos/modules/installer/tools/nixos-generate-config.pl | 1 - 1 file changed, 1 deletion(-) diff --git a/nixos/modules/installer/tools/nixos-generate-config.pl b/nixos/modules/installer/tools/nixos-generate-config.pl index 1586a09aee5f..3d0f4a9f7d86 100644 --- a/nixos/modules/installer/tools/nixos-generate-config.pl +++ b/nixos/modules/installer/tools/nixos-generate-config.pl @@ -211,7 +211,6 @@ sub pciCheck { ($device eq "0xfd3e" || $device eq "0x7d1d" || $device eq "0xad1d" || $device eq "0x643e" || $device eq "0xb03e")) { - push @imports, "(modulesPath + \"/hardware/cpu/intel-npu.nix\")"; push @attrs, "hardware.cpu.intel.npu.enable = true;"; } From 128dad70c66d00c248c9821fc3e45222818a0e3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Mon, 6 Apr 2026 16:49:40 +0200 Subject: [PATCH 10/20] nix_2_28: add patch for GHSA-g3g9-5vj6-r3gj This addresses GHSA-g3g9-5vj6-r3gj, a vulnerability where std::filesystem::copy_file follows symlinks when copying FOD outputs, allowing a malicious builder to overwrite files outside the build sandbox. The patch puts FOD output copies in a temporary directory inside the store (instead of the chroot) and tightens permissions on the in-store temporary directory. --- pkgs/tools/package-management/nix/default.nix | 1 + .../patches/ghsa-g3g9-5vj6-r3gj-2.28.patch | 126 ++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.28.patch diff --git a/pkgs/tools/package-management/nix/default.nix b/pkgs/tools/package-management/nix/default.nix index bbb45c9d419d..70533da7574b 100644 --- a/pkgs/tools/package-management/nix/default.nix +++ b/pkgs/tools/package-management/nix/default.nix @@ -165,6 +165,7 @@ lib.makeExtensible ( hash = "sha256-vFv/D08x9urtoIE9wiC7Lln4Eq3sgNBwU7TBE1iyrfI="; }) lowdown30PatchOld + ./patches/ghsa-g3g9-5vj6-r3gj-2.28.patch ]; }; diff --git a/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.28.patch b/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.28.patch new file mode 100644 index 000000000000..ae50b007a30b --- /dev/null +++ b/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.28.patch @@ -0,0 +1,126 @@ +From 32b09e0bfeb33434866610994d89f48da8a2bf41 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= +Date: Mon, 6 Apr 2026 16:49:13 +0200 +Subject: [PATCH] Fixes for GHSA-g3g9-5vj6-r3gj +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Squashed commit of the following: + +commit 716dba6692c42e301a1e769e5eac02a4d6e63150 +Author: Sergei Zimmerman +Date: Fri Apr 3 00:21:31 2026 +0300 + + derivation-builder: Don't use copyFile for FOD output copying, put the output in a temporary directory in the store + +commit a3215e7c5c260fab5f2cb034c4df01dfa3b284e5 +Author: Sergei Zimmerman +Date: Fri Apr 3 00:21:21 2026 +0300 + + libstore: Make temporary in-store directory not world-readable + +Signed-off-by: Jörg Thalheim +--- + src/libstore/include/nix/store/local-store.hh | 2 ++ + src/libstore/local-store.cc | 5 +-- + .../unix/build/local-derivation-goal.cc | 36 ++++++++++++++----- + 3 files changed, 33 insertions(+), 10 deletions(-) + +diff --git a/src/libstore/include/nix/store/local-store.hh b/src/libstore/include/nix/store/local-store.hh +index 5893b7d8b..c9266f6b4 100644 +--- a/src/libstore/include/nix/store/local-store.hh ++++ b/src/libstore/include/nix/store/local-store.hh +@@ -408,6 +408,8 @@ private: + friend struct PathSubstitutionGoal; + friend struct SubstitutionGoal; + friend struct DerivationGoal; ++ /* Only used for createTempDirInStore. */ ++ friend class DerivationBuilderImpl; + }; + + } // namespace nix +diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc +index 63108fab4..ab3a0d034 100644 +--- a/src/libstore/local-store.cc ++++ b/src/libstore/local-store.cc +@@ -1311,8 +1311,9 @@ std::pair LocalStore::createTempDirInStore() + do { + /* There is a slight possibility that `tmpDir' gets deleted by + the GC between createTempDir() and when we acquire a lock on it. +- We'll repeat until 'tmpDir' exists and we've locked it. */ +- tmpDirFn = createTempDir(realStoreDir, "tmp"); ++ We'll repeat until 'tmpDir' exists and we've locked it. ++ Make the directory accessible only to the current user.*/ ++ tmpDirFn = createTempDir(realStoreDir, "tmp", /*mode=*/0700); + tmpDirFd = openDirectory(tmpDirFn); + if (!tmpDirFd) { + continue; +diff --git a/src/libstore/unix/build/local-derivation-goal.cc b/src/libstore/unix/build/local-derivation-goal.cc +index 88c82e063..3e4c3017d 100644 +--- a/src/libstore/unix/build/local-derivation-goal.cc ++++ b/src/libstore/unix/build/local-derivation-goal.cc +@@ -2547,6 +2547,13 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() + assert(output && scratchPath); + auto actualPath = toRealPathChroot(worker.store.printStorePath(*scratchPath)); + ++ /* An optional file descriptor of a directory used for intermediate ++ operations. */ ++ AutoCloseFD tempDirFd; ++ /* RAII cleanup of a temporary directory inside the store that is used ++ for intermediate operations. */ ++ std::optional delTempDir; ++ + auto finish = [&](StorePath finalStorePath) { + /* Store the final path */ + finalOutputs.insert_or_assign(outputName, finalStorePath); +@@ -2681,6 +2688,25 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() + return newInfo0; + }; + ++ auto moveOutputToTempDir = [&]() -> void { ++ std::filesystem::path tempDir; ++ std::tie(tempDir, tempDirFd) = getLocalStore().createTempDirInStore(); ++ delTempDir.emplace(tempDir); ++ ++ auto tmpOutput = tempDir / "x"; ++ ++ /* Serialise and create a fresh copy of the output to break ++ any stale writable file descriptors. Copy through the ++ serialisation/deserialisation. TODO: Use copyRecursive here and ++ make use of reflinking. */ ++ auto source = sinkToSource([&](Sink & nextSink) { dumpPath(actualPath, nextSink); }); ++ restorePath(tmpOutput, *source, settings.fsyncStorePaths); ++ /* This makes it slightly harder to make sense of the control flow. The rule ++ of thumb is that actualPath points to the current location of the stuff ++ that we'll end up registering. */ ++ actualPath = std::move(tmpOutput); ++ }; ++ + ValidPathInfo newInfo = std::visit( + overloaded{ + +@@ -2708,14 +2734,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() + + [&](const DerivationOutput::CAFixed & dof) { + auto & wanted = dof.ca.hash; +- +- // Replace the output by a fresh copy of itself to make sure +- // that there's no stale file descriptor pointing to it +- Path tmpOutput = actualPath + ".tmp"; +- copyFile(std::filesystem::path(actualPath), std::filesystem::path(tmpOutput), true); +- +- std::filesystem::rename(tmpOutput, actualPath); +- ++ moveOutputToTempDir(); + auto newInfo0 = newInfoFromCA( + DerivationOutput::CAFloating{ + .method = dof.ca.method, +@@ -2756,6 +2775,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() + }, + + [&](const DerivationOutput::Impure & doi) { ++ moveOutputToTempDir(); + return newInfoFromCA( + DerivationOutput::CAFloating{ + .method = doi.method, From 69e8fef8129d2a7377f212f8fbd0a1ceb1f38ca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Mon, 6 Apr 2026 16:49:40 +0200 Subject: [PATCH 11/20] nixComponents_2_30: add patch for GHSA-g3g9-5vj6-r3gj This addresses GHSA-g3g9-5vj6-r3gj, a vulnerability where std::filesystem::copy_file follows symlinks when copying FOD outputs, allowing a malicious builder to overwrite files outside the build sandbox. The patch puts FOD output copies in a temporary directory inside the store (instead of the chroot) and tightens permissions on the in-store temporary directory. --- pkgs/tools/package-management/nix/default.nix | 1 + .../patches/ghsa-g3g9-5vj6-r3gj-2.30.patch | 126 ++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.30.patch diff --git a/pkgs/tools/package-management/nix/default.nix b/pkgs/tools/package-management/nix/default.nix index 70533da7574b..66cb2fc91636 100644 --- a/pkgs/tools/package-management/nix/default.nix +++ b/pkgs/tools/package-management/nix/default.nix @@ -190,6 +190,7 @@ lib.makeExtensible ( hash = "sha256-r2ZF1zBZDKMvyX6X4VsaTMrg0zdjn59Jf6Hqg56r29E="; }) lowdown30PatchOld + ./patches/ghsa-g3g9-5vj6-r3gj-2.30.patch ] ); diff --git a/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.30.patch b/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.30.patch new file mode 100644 index 000000000000..d7514f864afa --- /dev/null +++ b/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.30.patch @@ -0,0 +1,126 @@ +From 4d0a078f1dae9a07d04e1a72e7e62fbf2ca249e0 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= +Date: Mon, 6 Apr 2026 16:49:13 +0200 +Subject: [PATCH] Fixes for GHSA-g3g9-5vj6-r3gj +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Squashed commit of the following: + +commit a8e5b27728e3b18e63c2503a83afd04031994623 +Author: Sergei Zimmerman +Date: Fri Apr 3 00:21:31 2026 +0300 + + derivation-builder: Don't use copyFile for FOD output copying, put the output in a temporary directory in the store + +commit 49ff4d6779ec20c5339b237dec9b240c3eabf535 +Author: Sergei Zimmerman +Date: Fri Apr 3 00:21:21 2026 +0300 + + libstore: Make temporary in-store directory not world-readable + +Signed-off-by: Jörg Thalheim +--- + src/libstore/include/nix/store/local-store.hh | 2 ++ + src/libstore/local-store.cc | 5 +-- + src/libstore/unix/build/derivation-builder.cc | 36 ++++++++++++++----- + 3 files changed, 33 insertions(+), 10 deletions(-) + +diff --git a/src/libstore/include/nix/store/local-store.hh b/src/libstore/include/nix/store/local-store.hh +index 5f3a249f8..74fa1c2e8 100644 +--- a/src/libstore/include/nix/store/local-store.hh ++++ b/src/libstore/include/nix/store/local-store.hh +@@ -446,6 +446,8 @@ private: + friend struct PathSubstitutionGoal; + friend struct SubstitutionGoal; + friend struct DerivationGoal; ++ /* Only used for createTempDirInStore. */ ++ friend class DerivationBuilderImpl; + }; + + } // namespace nix +diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc +index 49c499e3f..5f897856f 100644 +--- a/src/libstore/local-store.cc ++++ b/src/libstore/local-store.cc +@@ -1316,8 +1316,9 @@ std::pair LocalStore::createTempDirInStore() + do { + /* There is a slight possibility that `tmpDir' gets deleted by + the GC between createTempDir() and when we acquire a lock on it. +- We'll repeat until 'tmpDir' exists and we've locked it. */ +- tmpDirFn = createTempDir(config->realStoreDir, "tmp"); ++ We'll repeat until 'tmpDir' exists and we've locked it. ++ Make the directory accessible only to the current user.*/ ++ tmpDirFn = createTempDir(config->realStoreDir, "tmp", /*mode=*/0700); + tmpDirFd = openDirectory(tmpDirFn); + if (!tmpDirFd) { + continue; +diff --git a/src/libstore/unix/build/derivation-builder.cc b/src/libstore/unix/build/derivation-builder.cc +index 5f9dafb57..609d88a79 100644 +--- a/src/libstore/unix/build/derivation-builder.cc ++++ b/src/libstore/unix/build/derivation-builder.cc +@@ -1581,6 +1581,13 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() + assert(output && scratchPath); + auto actualPath = realPathInSandbox(store.printStorePath(*scratchPath)); + ++ /* An optional file descriptor of a directory used for intermediate ++ operations. */ ++ AutoCloseFD tempDirFd; ++ /* RAII cleanup of a temporary directory inside the store that is used ++ for intermediate operations. */ ++ std::optional delTempDir; ++ + auto finish = [&](StorePath finalStorePath) { + /* Store the final path */ + finalOutputs.insert_or_assign(outputName, finalStorePath); +@@ -1715,6 +1722,25 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() + return newInfo0; + }; + ++ auto moveOutputToTempDir = [&]() -> void { ++ std::filesystem::path tempDir; ++ std::tie(tempDir, tempDirFd) = getLocalStore(store).createTempDirInStore(); ++ delTempDir.emplace(tempDir); ++ ++ auto tmpOutput = tempDir / "x"; ++ ++ /* Serialise and create a fresh copy of the output to break ++ any stale writable file descriptors. Copy through the ++ serialisation/deserialisation. TODO: Use copyRecursive here and ++ make use of reflinking. */ ++ auto source = sinkToSource([&](Sink & nextSink) { dumpPath(actualPath, nextSink); }); ++ restorePath(tmpOutput, *source, settings.fsyncStorePaths); ++ /* This makes it slightly harder to make sense of the control flow. The rule ++ of thumb is that actualPath points to the current location of the stuff ++ that we'll end up registering. */ ++ actualPath = std::move(tmpOutput); ++ }; ++ + ValidPathInfo newInfo = std::visit( + overloaded{ + +@@ -1742,14 +1768,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() + + [&](const DerivationOutput::CAFixed & dof) { + auto & wanted = dof.ca.hash; +- +- // Replace the output by a fresh copy of itself to make sure +- // that there's no stale file descriptor pointing to it +- Path tmpOutput = actualPath + ".tmp"; +- copyFile(std::filesystem::path(actualPath), std::filesystem::path(tmpOutput), true); +- +- std::filesystem::rename(tmpOutput, actualPath); +- ++ moveOutputToTempDir(); + auto newInfo0 = newInfoFromCA( + DerivationOutput::CAFloating{ + .method = dof.ca.method, +@@ -1790,6 +1809,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() + }, + + [&](const DerivationOutput::Impure & doi) { ++ moveOutputToTempDir(); + return newInfoFromCA( + DerivationOutput::CAFloating{ + .method = doi.method, From be231299383aa4a41ed83ee422f07f316f0fa63c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Mon, 6 Apr 2026 16:49:40 +0200 Subject: [PATCH 12/20] nixComponents_2_31: add patches for GHSA-g3g9-5vj6-r3gj This addresses GHSA-g3g9-5vj6-r3gj, a vulnerability where std::filesystem::copy_file follows symlinks when copying FOD outputs, allowing a malicious builder to overwrite files outside the build sandbox. The patch puts FOD output copies in a temporary directory inside the store (instead of the chroot) and tightens permissions on the in-store temporary directory. The second patch partially fixes the issue with cooperating processes being able to communicate via abstract sockets. The fix is partial, because processes outside the landlock domain of the sandboxed process can still connect to a socket created by the FOD. There's no equivalent way of restricting inbound connections. This closes the gap when there's no cooperating process on the host (i.e. 2 separate FODs). The patch applies landlock LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET on kernels >= 6.12 (default on NixOS 25.11+) to deny abstract socket connect from inside the build sandbox. --- pkgs/tools/package-management/nix/default.nix | 2 + .../patches/ghsa-g3g9-5vj6-r3gj-2.31.patch | 126 ++++++++++ ...dlock-abstract-socket-hardening-2.31.patch | 232 ++++++++++++++++++ 3 files changed, 360 insertions(+) create mode 100644 pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.31.patch create mode 100644 pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-2.31.patch diff --git a/pkgs/tools/package-management/nix/default.nix b/pkgs/tools/package-management/nix/default.nix index 66cb2fc91636..a0e28a02f9f1 100644 --- a/pkgs/tools/package-management/nix/default.nix +++ b/pkgs/tools/package-management/nix/default.nix @@ -210,6 +210,8 @@ lib.makeExtensible ( }).appendPatches [ lowdown30Patch + ./patches/ghsa-g3g9-5vj6-r3gj-2.31.patch + ./patches/landlock-abstract-socket-hardening-2.31.patch ]; nix_2_31 = addTests "nix_2_31" self.nixComponents_2_31.nix-everything; diff --git a/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.31.patch b/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.31.patch new file mode 100644 index 000000000000..4d08316d67a7 --- /dev/null +++ b/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.31.patch @@ -0,0 +1,126 @@ +From df0153a9eca42c4ed5ac784657c8c5d0664c9e0e Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= +Date: Mon, 6 Apr 2026 16:49:13 +0200 +Subject: [PATCH] Fixes for GHSA-g3g9-5vj6-r3gj +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Squashed commit of the following: + +commit bf1d95ae399e47daeafa9e0dfac967f415c52447 +Author: Sergei Zimmerman +Date: Fri Apr 3 00:21:31 2026 +0300 + + derivation-builder: Don't use copyFile for FOD output copying, put the output in a temporary directory in the store + +commit ca5986fa353d402738acf961bcaa017c61019809 +Author: Sergei Zimmerman +Date: Fri Apr 3 00:21:21 2026 +0300 + + libstore: Make temporary in-store directory not world-readable + +Signed-off-by: Jörg Thalheim +--- + src/libstore/include/nix/store/local-store.hh | 2 ++ + src/libstore/local-store.cc | 5 +-- + src/libstore/unix/build/derivation-builder.cc | 36 ++++++++++++++----- + 3 files changed, 33 insertions(+), 10 deletions(-) + +diff --git a/src/libstore/include/nix/store/local-store.hh b/src/libstore/include/nix/store/local-store.hh +index ec88ffb4f..91df9cd77 100644 +--- a/src/libstore/include/nix/store/local-store.hh ++++ b/src/libstore/include/nix/store/local-store.hh +@@ -455,6 +455,8 @@ private: + + friend struct PathSubstitutionGoal; + friend struct DerivationGoal; ++ /* Only used for createTempDirInStore. */ ++ friend class DerivationBuilderImpl; + }; + + } // namespace nix +diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc +index d24bc415c..524a9fdda 100644 +--- a/src/libstore/local-store.cc ++++ b/src/libstore/local-store.cc +@@ -1342,8 +1342,9 @@ std::pair LocalStore::createTempDirInStore() + do { + /* There is a slight possibility that `tmpDir' gets deleted by + the GC between createTempDir() and when we acquire a lock on it. +- We'll repeat until 'tmpDir' exists and we've locked it. */ +- tmpDirFn = createTempDir(config->realStoreDir, "tmp"); ++ We'll repeat until 'tmpDir' exists and we've locked it. ++ Make the directory accessible only to the current user.*/ ++ tmpDirFn = createTempDir(config->realStoreDir, "tmp", /*mode=*/0700); + tmpDirFd = openDirectory(tmpDirFn); + if (!tmpDirFd) { + continue; +diff --git a/src/libstore/unix/build/derivation-builder.cc b/src/libstore/unix/build/derivation-builder.cc +index 73bb026a2..d11f80159 100644 +--- a/src/libstore/unix/build/derivation-builder.cc ++++ b/src/libstore/unix/build/derivation-builder.cc +@@ -1473,6 +1473,13 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() + assert(output && scratchPath); + auto actualPath = realPathInSandbox(store.printStorePath(*scratchPath)); + ++ /* An optional file descriptor of a directory used for intermediate ++ operations. */ ++ AutoCloseFD tempDirFd; ++ /* RAII cleanup of a temporary directory inside the store that is used ++ for intermediate operations. */ ++ std::optional delTempDir; ++ + auto finish = [&](StorePath finalStorePath) { + /* Store the final path */ + finalOutputs.insert_or_assign(outputName, finalStorePath); +@@ -1607,6 +1614,25 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() + return newInfo0; + }; + ++ auto moveOutputToTempDir = [&]() -> void { ++ std::filesystem::path tempDir; ++ std::tie(tempDir, tempDirFd) = store.createTempDirInStore(); ++ delTempDir.emplace(tempDir); ++ ++ auto tmpOutput = tempDir / "x"; ++ ++ /* Serialise and create a fresh copy of the output to break ++ any stale writable file descriptors. Copy through the ++ serialisation/deserialisation. TODO: Use copyRecursive here and ++ make use of reflinking. */ ++ auto source = sinkToSource([&](Sink & nextSink) { dumpPath(actualPath, nextSink); }); ++ restorePath(tmpOutput, *source, settings.fsyncStorePaths); ++ /* This makes it slightly harder to make sense of the control flow. The rule ++ of thumb is that actualPath points to the current location of the stuff ++ that we'll end up registering. */ ++ actualPath = std::move(tmpOutput); ++ }; ++ + ValidPathInfo newInfo = std::visit( + overloaded{ + +@@ -1634,14 +1660,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() + + [&](const DerivationOutput::CAFixed & dof) { + auto & wanted = dof.ca.hash; +- +- // Replace the output by a fresh copy of itself to make sure +- // that there's no stale file descriptor pointing to it +- Path tmpOutput = actualPath + ".tmp"; +- copyFile(std::filesystem::path(actualPath), std::filesystem::path(tmpOutput), true); +- +- std::filesystem::rename(tmpOutput, actualPath); +- ++ moveOutputToTempDir(); + auto newInfo0 = newInfoFromCA( + DerivationOutput::CAFloating{ + .method = dof.ca.method, +@@ -1682,6 +1701,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() + }, + + [&](const DerivationOutput::Impure & doi) { ++ moveOutputToTempDir(); + return newInfoFromCA( + DerivationOutput::CAFloating{ + .method = doi.method, diff --git a/pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-2.31.patch b/pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-2.31.patch new file mode 100644 index 000000000000..7584a608db7f --- /dev/null +++ b/pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-2.31.patch @@ -0,0 +1,232 @@ +From b743f908b10aa56b88fb490de8a0d893bd2d83d5 Mon Sep 17 00:00:00 2001 +From: Sergei Zimmerman +Date: Sun, 5 Apr 2026 16:39:58 +0300 +Subject: [PATCH] libstore: Use landlock with + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET for new enough kernels +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This partially fixes the issue with cooperating processes being able +to communicate via abstract sockets. The fix is partial, because processes +outside the landlock domain of the sandboxed process can still connect to +a socket created by the FOD. There's no equivalent way of restricting inbound +connections. This closes the gap when there's no cooperating process on the host +(i.e. 2 separate FODs). + +>= 6.12 kernel is widespread enough (NixOS 25.11 ships it by +default) that we have no reason not to apply this hardening, even though +it's incomplete. + +ca-fd-leak test exercises this exact code path and now the smuggling +process fails with (on new enough kernels that have landlock support enabled): + +vm-test-run-ca-fd-leak> machine # sandbox setup: applied landlock sandboxing +vm-test-run-ca-fd-leak> machine # building '/nix/store/s7brgi6pdr5f3n8yqlgmdlz8blb89njc-smuggled.drv'... +vm-test-run-ca-fd-leak> machine # building derivation '/nix/store/s7brgi6pdr5f3n8yqlgmdlz8blb89njc-smuggled.drv': woken up +vm-test-run-ca-fd-leak> machine # connect: Operation not permitted +vm-test-run-ca-fd-leak> machine # sendmsg: Socket not connected + +(cherry picked from commit 44017ca497c8b44d5dac179f5afc63e91fe45ed6) +Signed-off-by: Jörg Thalheim +--- + src/libstore/meson.build | 5 + + .../unix/build/linux-derivation-builder.cc | 103 ++++++++++++++++++ + tests/nixos/ca-fd-leak/default.nix | 8 +- + 3 files changed, 114 insertions(+), 2 deletions(-) + +diff --git a/src/libstore/meson.build b/src/libstore/meson.build +index a275f4edc..dc8a7a940 100644 +--- a/src/libstore/meson.build ++++ b/src/libstore/meson.build +@@ -77,6 +77,11 @@ foreach funcspec : check_funcs + configdata_priv.set(define_name, define_value) + endforeach + ++if host_machine.system() == 'linux' ++ has_landlock = cxx.has_header('linux/landlock.h') ++ configdata_priv.set('HAVE_LANDLOCK', has_landlock.to_int()) ++endif ++ + has_acl_support = cxx.has_header('sys/xattr.h') \ + and cxx.has_function('llistxattr') \ + and cxx.has_function('lremovexattr') +diff --git a/src/libstore/unix/build/linux-derivation-builder.cc b/src/libstore/unix/build/linux-derivation-builder.cc +index eaac2bc28..dc5abf808 100644 +--- a/src/libstore/unix/build/linux-derivation-builder.cc ++++ b/src/libstore/unix/build/linux-derivation-builder.cc +@@ -1,10 +1,16 @@ + #ifdef __linux__ + ++# include "store-config-private.hh" ++ + # include "nix/store/personality.hh" + # include "nix/util/cgroup.hh" + # include "nix/util/linux-namespaces.hh" + # include "linux/fchmodat2-compat.hh" + ++# include ++# include ++# include ++ + # include + # include + # include +@@ -13,11 +19,16 @@ + # include + # include + # include ++# include + + # if HAVE_SECCOMP + # include + # endif + ++# if HAVE_LANDLOCK ++# include ++# endif ++ + # define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root, put_old)) + + namespace nix { +@@ -121,6 +132,77 @@ static void setupSeccomp() + # endif + } + ++# if HAVE_LANDLOCK && defined(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET) ++ ++# define DO_LANDLOCK 1 ++ ++/* We are using LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET on best-effort basis. There are no glibc wrappers for now. */ ++ ++static int landlockCreateRuleset(const ::landlock_ruleset_attr * attr, std::size_t size, std::uint32_t flags) ++{ ++ return ::syscall(__NR_landlock_create_ruleset, attr, size, flags); ++} ++ ++static int landlockRestrictSelf(Descriptor rulesetFd, std::uint32_t flags) ++{ ++ return ::syscall(__NR_landlock_restrict_self, rulesetFd, flags); ++} ++ ++static int getLandlockAbiVersion() ++{ ++ int abiVersion = landlockCreateRuleset(nullptr, 0, LANDLOCK_CREATE_RULESET_VERSION); ++ return abiVersion; ++} ++ ++static void setupLandlock() ++{ ++ bool landlockSupportsScopeAbstractUnixSocket = []() { ++ int abiVersion = getLandlockAbiVersion(); ++ if (abiVersion >= 6) ++ /* All good, we can use LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET. See ++ https://docs.kernel.org/userspace-api/landlock.html#abstract-unix-socket-abi-6 */ ++ return true; ++ ++ if (abiVersion == -1) { ++ debug("landlock is not available"); ++ return false; ++ } ++ ++ debug("landlock version %d does not support LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", abiVersion); ++ return false; ++ }(); ++ ++ /* Bail out early if landlock is not enabled or LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET wouldn't work. ++ TODO: Consider adding more landlock rules for filesystem access as defense-in-depth on top. */ ++ if (!landlockSupportsScopeAbstractUnixSocket) ++ return; ++ ++ ::landlock_ruleset_attr attr = { ++ /* This prevents multiple FODs from communicating with each other ++ via abstract sockets. Note that cooperating processes outside the ++ sandbox can still connect to an abstract socket created by the FOD. To ++ mitigate that issue entirely we'd still need network namespaces. */ ++ .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, ++ }; ++ ++ /* This better not fail - if the kernel reports a new enough ABI version we ++ should treat any errors as fatal from now on. */ ++ AutoCloseFD rulesetFd = landlockCreateRuleset(&attr, sizeof(attr), 0); ++ if (!rulesetFd) ++ throw SysError("failed to create a landlock ruleset"); ++ ++ if (landlockRestrictSelf(rulesetFd.get(), 0) == -1) ++ throw SysError("failed to apply landlock"); ++ ++ debug("applied landlock sandboxing"); ++} ++ ++# else ++ ++# define DO_LANDLOCK 0 ++ ++# endif ++ + static void doBind(const Path & source, const Path & target, bool optional = false) + { + debug("bind mounting '%1%' to '%2%'", source, target); +@@ -159,8 +241,27 @@ struct LinuxDerivationBuilder : virtual DerivationBuilderImpl + + void enterChroot() override + { ++ /* Set the NO_NEW_PRIVS before doing seccomp/landlock setup. ++ landlock_restrict_self requires either NO_NEW_PRIVS or CAP_SYS_ADMIN. ++ With user namespaces we do get CAP_SYS_ADMIN. */ ++ if (!settings.allowNewPrivileges) ++ if (::prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) ++ throw SysError("failed to set PR_SET_NO_NEW_PRIVS"); ++ + setupSeccomp(); + ++# if DO_LANDLOCK ++ try { ++ setupLandlock(); ++ } catch (SysError & e) { ++ if (e.errNo != EPERM) ++ throw; ++ /* If allowNewPrivileges is true and we don't have CAP_SYS_ADMIN ++ this code path might be hit. */ ++ warn("setting up landlock: %s", e.message()); ++ } ++# endif ++ + linux::setPersonality(drv.platform); + } + }; +@@ -709,4 +810,6 @@ struct ChrootLinuxDerivationBuilder : ChrootDerivationBuilder, LinuxDerivationBu + + } // namespace nix + ++# undef DO_LANDLOCK ++ + #endif +diff --git a/tests/nixos/ca-fd-leak/default.nix b/tests/nixos/ca-fd-leak/default.nix +index 902aacdc6..dc944290f 100644 +--- a/tests/nixos/ca-fd-leak/default.nix ++++ b/tests/nixos/ca-fd-leak/default.nix +@@ -78,7 +78,7 @@ in + + # Build the smuggled derivation. + # This will connect to the smuggler server and send it the file descriptor +- machine.succeed(r""" ++ sender_output = machine.succeed(r""" + nix-build -E ' + builtins.derivation { + name = "smuggled"; +@@ -89,9 +89,13 @@ in + outputHash = builtins.hashString "sha256" "hello, world\n"; + builder = "${pkgs.busybox-sandbox-shell}/bin/sh"; + args = [ "-c" "echo \"hello, world\" > $out; ''${${sender}} ${socketName}" ]; +- }' ++ }' 2>&1 + """.strip()) + ++ # Landlock's LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET prevents a sandboxed process ++ # from connecting to an abstract socket created in an unrelated landlock domain. ++ # There's no such flag for preventing inbound connections. ++ assert "connect: Operation not permitted" in sender_output + + # Tell the smuggler server that we're done + machine.execute("echo done | ${pkgs.socat}/bin/socat - ABSTRACT-CONNECT:${socketName}") From 7ca89c4ab60606c7ecdb10c57ef91553f31c10a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Mon, 6 Apr 2026 16:49:41 +0200 Subject: [PATCH 13/20] nixComponents_2_34: add patches for GHSA-g3g9-5vj6-r3gj This addresses GHSA-g3g9-5vj6-r3gj, a vulnerability where std::filesystem::copy_file follows symlinks when copying FOD outputs, allowing a malicious builder to overwrite files outside the build sandbox. The patch puts FOD output copies in a temporary directory inside the store (instead of the chroot) and tightens permissions on the in-store temporary directory. The second patch partially fixes the issue with cooperating processes being able to communicate via abstract sockets. The fix is partial, because processes outside the landlock domain of the sandboxed process can still connect to a socket created by the FOD. There's no equivalent way of restricting inbound connections. This closes the gap when there's no cooperating process on the host (i.e. 2 separate FODs). The patch applies landlock LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET on kernels >= 6.12 (default on NixOS 25.11+) to deny abstract socket connect from inside the build sandbox. --- pkgs/tools/package-management/nix/default.nix | 8 +- .../patches/ghsa-g3g9-5vj6-r3gj-2.34.patch | 126 ++++++++++ ...dlock-abstract-socket-hardening-2.34.patch | 234 ++++++++++++++++++ 3 files changed, 367 insertions(+), 1 deletion(-) create mode 100644 pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.34.patch create mode 100644 pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-2.34.patch diff --git a/pkgs/tools/package-management/nix/default.nix b/pkgs/tools/package-management/nix/default.nix index a0e28a02f9f1..f8e997482863 100644 --- a/pkgs/tools/package-management/nix/default.nix +++ b/pkgs/tools/package-management/nix/default.nix @@ -228,7 +228,13 @@ lib.makeExtensible ( hash = "sha256-WPuGqMQGepXoRYjtRudMAMHEoLsIObw2x4sVfho5feA="; }; }).appendPatches - patches_common; + ( + patches_common + ++ [ + ./patches/ghsa-g3g9-5vj6-r3gj-2.34.patch + ./patches/landlock-abstract-socket-hardening-2.34.patch + ] + ); nix_2_34 = addTests "nix_2_34" self.nixComponents_2_34.nix-everything; diff --git a/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.34.patch b/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.34.patch new file mode 100644 index 000000000000..ea140e3fb149 --- /dev/null +++ b/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.34.patch @@ -0,0 +1,126 @@ +From dfda33d358f4522450737c40407ba8d8403a86a1 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= +Date: Mon, 6 Apr 2026 16:49:13 +0200 +Subject: [PATCH] Fixes for GHSA-g3g9-5vj6-r3gj +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Squashed commit of the following: + +commit d1f2de7d683bef1e2a164f23f33172c79c7725ea +Author: Sergei Zimmerman +Date: Fri Apr 3 00:21:31 2026 +0300 + + derivation-builder: Don't use copyFile for FOD output copying, put the output in a temporary directory in the store + +commit 2f13ee6dc4ade1d919c14757891d5d8a1001eee8 +Author: Sergei Zimmerman +Date: Fri Apr 3 00:21:21 2026 +0300 + + libstore: Make temporary in-store directory not world-readable + +Signed-off-by: Jörg Thalheim +--- + src/libstore/include/nix/store/local-store.hh | 2 ++ + src/libstore/local-store.cc | 5 +-- + src/libstore/unix/build/derivation-builder.cc | 36 ++++++++++++++----- + 3 files changed, 33 insertions(+), 10 deletions(-) + +diff --git a/src/libstore/include/nix/store/local-store.hh b/src/libstore/include/nix/store/local-store.hh +index 512c198b0..0b1e13b51 100644 +--- a/src/libstore/include/nix/store/local-store.hh ++++ b/src/libstore/include/nix/store/local-store.hh +@@ -505,6 +505,8 @@ private: + + friend struct PathSubstitutionGoal; + friend struct DerivationGoal; ++ /* Only used for createTempDirInStore. */ ++ friend class DerivationBuilderImpl; + }; + + } // namespace nix +diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc +index 649ad70dd..686e9988e 100644 +--- a/src/libstore/local-store.cc ++++ b/src/libstore/local-store.cc +@@ -1283,8 +1283,9 @@ std::pair LocalStore::createTempDirInStore() + do { + /* There is a slight possibility that `tmpDir' gets deleted by + the GC between createTempDir() and when we acquire a lock on it. +- We'll repeat until 'tmpDir' exists and we've locked it. */ +- tmpDirFn = createTempDir(std::filesystem::path{config->realStoreDir.get()}, "tmp"); ++ We'll repeat until 'tmpDir' exists and we've locked it. ++ Make the directory accessible only to the current user. */ ++ tmpDirFn = createTempDir(std::filesystem::path{config->realStoreDir.get()}, "tmp", /*mode=*/0700); + tmpDirFd = openDirectory(tmpDirFn); + if (!tmpDirFd) { + continue; +diff --git a/src/libstore/unix/build/derivation-builder.cc b/src/libstore/unix/build/derivation-builder.cc +index da225dc0d..fc877dd56 100644 +--- a/src/libstore/unix/build/derivation-builder.cc ++++ b/src/libstore/unix/build/derivation-builder.cc +@@ -1595,6 +1595,13 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() + assert(output && scratchPath); + auto actualPath = realPathInHost(store.printStorePath(*scratchPath)); + ++ /* An optional file descriptor of a directory used for intermediate ++ operations. */ ++ AutoCloseFD tempDirFd; ++ /* RAII cleanup of a temporary directory inside the store that is used ++ for intermediate operations. */ ++ AutoDelete delTempDir; ++ + auto finish = [&](StorePath finalStorePath) { + /* Store the final path */ + finalOutputs.insert_or_assign(outputName, finalStorePath); +@@ -1742,6 +1749,25 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() + return newInfo0; + }; + ++ auto moveOutputToTempDir = [&]() -> void { ++ std::filesystem::path tempDir; ++ std::tie(tempDir, tempDirFd) = store.createTempDirInStore(); ++ delTempDir = AutoDelete(tempDir); ++ ++ auto tmpOutput = tempDir / "x"; ++ ++ /* Serialise and create a fresh copy of the output to break ++ any stale writable file descriptors. Copy through the ++ serialisation/deserialisation. TODO: Use copyRecursive here and ++ make use of reflinking. */ ++ auto source = sinkToSource([&](Sink & nextSink) { dumpPath(actualPath, nextSink); }); ++ restorePath(tmpOutput, *source, store.config->getLocalSettings().fsyncStorePaths); ++ /* This makes it slightly harder to make sense of the control flow. The rule ++ of thumb is that actualPath points to the current location of the stuff ++ that we'll end up registering. */ ++ actualPath = std::move(tmpOutput); ++ }; ++ + ValidPathInfo newInfo = std::visit( + overloaded{ + +@@ -1769,14 +1795,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() + + [&](const DerivationOutput::CAFixed & dof) { + auto & wanted = dof.ca.hash; +- +- // Replace the output by a fresh copy of itself to make sure +- // that there's no stale file descriptor pointing to it +- std::filesystem::path tmpOutput = actualPath.native() + ".tmp"; +- copyFile(actualPath, tmpOutput, true); +- +- std::filesystem::rename(tmpOutput, actualPath); +- ++ moveOutputToTempDir(); + return newInfoFromCA( + DerivationOutput::CAFloating{ + .method = dof.ca.method, +@@ -1793,6 +1812,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() + }, + + [&](const DerivationOutput::Impure & doi) { ++ moveOutputToTempDir(); + return newInfoFromCA( + DerivationOutput::CAFloating{ + .method = doi.method, diff --git a/pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-2.34.patch b/pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-2.34.patch new file mode 100644 index 000000000000..1ca40ee24b17 --- /dev/null +++ b/pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-2.34.patch @@ -0,0 +1,234 @@ +From 23c7a6490a829a978af1acd56d76cc2dee7f4ad6 Mon Sep 17 00:00:00 2001 +From: Sergei Zimmerman +Date: Sun, 5 Apr 2026 16:39:58 +0300 +Subject: [PATCH] libstore: Use landlock with + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET for new enough kernels +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This partially fixes the issue with cooperating processes being able +to communicate via abstract sockets. The fix is partial, because processes +outside the landlock domain of the sandboxed process can still connect to +a socket created by the FOD. There's no equivalent way of restricting inbound +connections. This closes the gap when there's no cooperating process on the host +(i.e. 2 separate FODs). + +>= 6.12 kernel is widespread enough (NixOS 25.11 ships it by +default) that we have no reason not to apply this hardening, even though +it's incomplete. + +ca-fd-leak test exercises this exact code path and now the smuggling +process fails with (on new enough kernels that have landlock support enabled): + +vm-test-run-ca-fd-leak> machine # sandbox setup: applied landlock sandboxing +vm-test-run-ca-fd-leak> machine # building '/nix/store/s7brgi6pdr5f3n8yqlgmdlz8blb89njc-smuggled.drv'... +vm-test-run-ca-fd-leak> machine # building derivation '/nix/store/s7brgi6pdr5f3n8yqlgmdlz8blb89njc-smuggled.drv': woken up +vm-test-run-ca-fd-leak> machine # connect: Operation not permitted +vm-test-run-ca-fd-leak> machine # sendmsg: Socket not connected + +(cherry picked from commit 44017ca497c8b44d5dac179f5afc63e91fe45ed6) +Signed-off-by: Jörg Thalheim +--- + src/libstore/meson.build | 5 + + .../unix/build/linux-derivation-builder.cc | 103 ++++++++++++++++++ + tests/nixos/ca-fd-leak/default.nix | 8 +- + 3 files changed, 114 insertions(+), 2 deletions(-) + +diff --git a/src/libstore/meson.build b/src/libstore/meson.build +index 9e52517d4..edc476ae7 100644 +--- a/src/libstore/meson.build ++++ b/src/libstore/meson.build +@@ -79,6 +79,11 @@ foreach funcspec : check_funcs + configdata_priv.set(define_name, define_value) + endforeach + ++if host_machine.system() == 'linux' ++ has_landlock = cxx.has_header('linux/landlock.h') ++ configdata_priv.set('HAVE_LANDLOCK', has_landlock.to_int()) ++endif ++ + has_acl_support = cxx.has_header('sys/xattr.h') \ + and cxx.has_function('llistxattr') \ + and cxx.has_function('lremovexattr') +diff --git a/src/libstore/unix/build/linux-derivation-builder.cc b/src/libstore/unix/build/linux-derivation-builder.cc +index 555a3b6da..476baabe2 100644 +--- a/src/libstore/unix/build/linux-derivation-builder.cc ++++ b/src/libstore/unix/build/linux-derivation-builder.cc +@@ -1,5 +1,7 @@ + #ifdef __linux__ + ++# include "store-config-private.hh" ++ + # include "nix/store/globals.hh" + # include "nix/store/personality.hh" + # include "nix/store/filetransfer.hh" +@@ -9,6 +11,10 @@ + # include "nix/util/serialise.hh" + # include "linux/fchmodat2-compat.hh" + ++# include ++# include ++# include ++ + # include + # include + # include +@@ -17,11 +23,16 @@ + # include + # include + # include ++# include + + # if HAVE_SECCOMP + # include + # endif + ++# if HAVE_LANDLOCK ++# include ++# endif ++ + # define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root, put_old)) + + namespace nix { +@@ -125,6 +136,77 @@ static void setupSeccomp(const LocalSettings & localSettings) + # endif + } + ++# if HAVE_LANDLOCK && defined(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET) ++ ++# define DO_LANDLOCK 1 ++ ++/* We are using LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET on best-effort basis. There are no glibc wrappers for now. */ ++ ++static int landlockCreateRuleset(const ::landlock_ruleset_attr * attr, std::size_t size, std::uint32_t flags) ++{ ++ return ::syscall(__NR_landlock_create_ruleset, attr, size, flags); ++} ++ ++static int landlockRestrictSelf(Descriptor rulesetFd, std::uint32_t flags) ++{ ++ return ::syscall(__NR_landlock_restrict_self, rulesetFd, flags); ++} ++ ++static int getLandlockAbiVersion() ++{ ++ int abiVersion = landlockCreateRuleset(nullptr, 0, LANDLOCK_CREATE_RULESET_VERSION); ++ return abiVersion; ++} ++ ++static void setupLandlock() ++{ ++ bool landlockSupportsScopeAbstractUnixSocket = []() { ++ int abiVersion = getLandlockAbiVersion(); ++ if (abiVersion >= 6) ++ /* All good, we can use LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET. See ++ https://docs.kernel.org/userspace-api/landlock.html#abstract-unix-socket-abi-6 */ ++ return true; ++ ++ if (abiVersion == -1) { ++ debug("landlock is not available"); ++ return false; ++ } ++ ++ debug("landlock version %d does not support LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", abiVersion); ++ return false; ++ }(); ++ ++ /* Bail out early if landlock is not enabled or LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET wouldn't work. ++ TODO: Consider adding more landlock rules for filesystem access as defense-in-depth on top. */ ++ if (!landlockSupportsScopeAbstractUnixSocket) ++ return; ++ ++ ::landlock_ruleset_attr attr = { ++ /* This prevents multiple FODs from communicating with each other ++ via abstract sockets. Note that cooperating processes outside the ++ sandbox can still connect to an abstract socket created by the FOD. To ++ mitigate that issue entirely we'd still need network namespaces. */ ++ .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, ++ }; ++ ++ /* This better not fail - if the kernel reports a new enough ABI version we ++ should treat any errors as fatal from now on. */ ++ AutoCloseFD rulesetFd = landlockCreateRuleset(&attr, sizeof(attr), 0); ++ if (!rulesetFd) ++ throw SysError("failed to create a landlock ruleset"); ++ ++ if (landlockRestrictSelf(rulesetFd.get(), 0) == -1) ++ throw SysError("failed to apply landlock"); ++ ++ debug("applied landlock sandboxing"); ++} ++ ++# else ++ ++# define DO_LANDLOCK 0 ++ ++# endif ++ + static void doBind(const std::filesystem::path & source, const std::filesystem::path & target, bool optional = false) + { + debug("bind mounting %1% to %2%", PathFmt(source), PathFmt(target)); +@@ -165,8 +247,27 @@ struct LinuxDerivationBuilder : virtual DerivationBuilderImpl + { + auto & localSettings = store.config->getLocalSettings(); + ++ /* Set the NO_NEW_PRIVS before doing seccomp/landlock setup. ++ landlock_restrict_self requires either NO_NEW_PRIVS or CAP_SYS_ADMIN. ++ With user namespaces we do get CAP_SYS_ADMIN. */ ++ if (!localSettings.allowNewPrivileges) ++ if (::prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) ++ throw SysError("failed to set PR_SET_NO_NEW_PRIVS"); ++ + setupSeccomp(localSettings); + ++# if DO_LANDLOCK ++ try { ++ setupLandlock(); ++ } catch (SysError & e) { ++ if (e.errNo != EPERM) ++ throw; ++ /* If allowNewPrivileges is true and we don't have CAP_SYS_ADMIN ++ this code path might be hit. */ ++ warn("setting up landlock: %s", e.message()); ++ } ++# endif ++ + linux::setPersonality({ + .system = drv.platform, + .impersonateLinux26 = localSettings.impersonateLinux26, +@@ -760,4 +861,6 @@ struct ChrootLinuxDerivationBuilder : ChrootDerivationBuilder, LinuxDerivationBu + + } // namespace nix + ++# undef DO_LANDLOCK ++ + #endif +diff --git a/tests/nixos/ca-fd-leak/default.nix b/tests/nixos/ca-fd-leak/default.nix +index 902aacdc6..dc944290f 100644 +--- a/tests/nixos/ca-fd-leak/default.nix ++++ b/tests/nixos/ca-fd-leak/default.nix +@@ -78,7 +78,7 @@ in + + # Build the smuggled derivation. + # This will connect to the smuggler server and send it the file descriptor +- machine.succeed(r""" ++ sender_output = machine.succeed(r""" + nix-build -E ' + builtins.derivation { + name = "smuggled"; +@@ -89,9 +89,13 @@ in + outputHash = builtins.hashString "sha256" "hello, world\n"; + builder = "${pkgs.busybox-sandbox-shell}/bin/sh"; + args = [ "-c" "echo \"hello, world\" > $out; ''${${sender}} ${socketName}" ]; +- }' ++ }' 2>&1 + """.strip()) + ++ # Landlock's LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET prevents a sandboxed process ++ # from connecting to an abstract socket created in an unrelated landlock domain. ++ # There's no such flag for preventing inbound connections. ++ assert "connect: Operation not permitted" in sender_output + + # Tell the smuggler server that we're done + machine.execute("echo done | ${pkgs.socat}/bin/socat - ABSTRACT-CONNECT:${socketName}") From 50eec35e48a03435730b68b851bd27213ea1c802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Mon, 6 Apr 2026 16:49:41 +0200 Subject: [PATCH 14/20] nixComponents_git: add patches for GHSA-g3g9-5vj6-r3gj This addresses GHSA-g3g9-5vj6-r3gj, a vulnerability where std::filesystem::copy_file follows symlinks when copying FOD outputs, allowing a malicious builder to overwrite files outside the build sandbox. The patch puts FOD output copies in a temporary directory inside the store (instead of the chroot) and tightens permissions on the in-store temporary directory. The second patch partially fixes the issue with cooperating processes being able to communicate via abstract sockets. The fix is partial, because processes outside the landlock domain of the sandboxed process can still connect to a socket created by the FOD. There's no equivalent way of restricting inbound connections. This closes the gap when there's no cooperating process on the host (i.e. 2 separate FODs). The patch applies landlock LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET on kernels >= 6.12 (default on NixOS 25.11+) to deny abstract socket connect from inside the build sandbox. --- pkgs/tools/package-management/nix/default.nix | 8 +- .../nix/patches/ghsa-g3g9-5vj6-r3gj-git.patch | 126 ++++++++++ ...ndlock-abstract-socket-hardening-git.patch | 231 ++++++++++++++++++ 3 files changed, 364 insertions(+), 1 deletion(-) create mode 100644 pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-git.patch create mode 100644 pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-git.patch diff --git a/pkgs/tools/package-management/nix/default.nix b/pkgs/tools/package-management/nix/default.nix index f8e997482863..4460014ad689 100644 --- a/pkgs/tools/package-management/nix/default.nix +++ b/pkgs/tools/package-management/nix/default.nix @@ -250,7 +250,13 @@ lib.makeExtensible ( hash = "sha256-fybp46IQmRN7lEUTChc3MTqxmRutmDO4RNSPEQfJQsQ="; }; }).appendPatches - patches_common; + ( + patches_common + ++ [ + ./patches/ghsa-g3g9-5vj6-r3gj-git.patch + ./patches/landlock-abstract-socket-hardening-git.patch + ] + ); git = addTests "git" self.nixComponents_git.nix-everything; diff --git a/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-git.patch b/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-git.patch new file mode 100644 index 000000000000..05fb52902018 --- /dev/null +++ b/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-git.patch @@ -0,0 +1,126 @@ +From 0af4c1f88d0646bda0a90a37105360cab466a550 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= +Date: Mon, 6 Apr 2026 16:49:13 +0200 +Subject: [PATCH] Fixes for GHSA-g3g9-5vj6-r3gj +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Squashed commit of the following: + +commit a760af86b3a42aa5ac9d9002929107fe357bf128 +Author: Sergei Zimmerman +Date: Fri Apr 3 00:21:31 2026 +0300 + + derivation-builder: Don't use copyFile for FOD output copying, put the output in a temporary directory in the store + +commit 0e3412a93f43a017342c267000c152e5c45327e7 +Author: Sergei Zimmerman +Date: Fri Apr 3 00:21:21 2026 +0300 + + libstore: Make temporary in-store directory not world-readable + +Signed-off-by: Jörg Thalheim +--- + src/libstore/include/nix/store/local-store.hh | 2 ++ + src/libstore/local-store.cc | 5 +-- + src/libstore/unix/build/derivation-builder.cc | 36 ++++++++++++++----- + 3 files changed, 33 insertions(+), 10 deletions(-) + +diff --git a/src/libstore/include/nix/store/local-store.hh b/src/libstore/include/nix/store/local-store.hh +index 63a1da67d..bf3437e95 100644 +--- a/src/libstore/include/nix/store/local-store.hh ++++ b/src/libstore/include/nix/store/local-store.hh +@@ -512,6 +512,8 @@ private: + + friend struct PathSubstitutionGoal; + friend struct DerivationGoal; ++ /* Only used for createTempDirInStore. */ ++ friend class DerivationBuilderImpl; + }; + + } // namespace nix +diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc +index e9eb48bfe..d5e457731 100644 +--- a/src/libstore/local-store.cc ++++ b/src/libstore/local-store.cc +@@ -1304,8 +1304,9 @@ std::pair LocalStore::createTempDirInStore() + do { + /* There is a slight possibility that `tmpDir' gets deleted by + the GC between createTempDir() and when we acquire a lock on it. +- We'll repeat until 'tmpDir' exists and we've locked it. */ +- tmpDirFn = createTempDir(std::filesystem::path{config->realStoreDir.get()}, "tmp"); ++ We'll repeat until 'tmpDir' exists and we've locked it. ++ Make the directory accessible only to the current user. */ ++ tmpDirFn = createTempDir(std::filesystem::path{config->realStoreDir.get()}, "tmp", /*mode=*/0700); + tmpDirFd = openDirectory(tmpDirFn, FinalSymlink::DontFollow); + if (!tmpDirFd) { + continue; +diff --git a/src/libstore/unix/build/derivation-builder.cc b/src/libstore/unix/build/derivation-builder.cc +index 8f6343e0f..8288a4a31 100644 +--- a/src/libstore/unix/build/derivation-builder.cc ++++ b/src/libstore/unix/build/derivation-builder.cc +@@ -1597,6 +1597,13 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() + assert(output && scratchPath); + auto actualPath = realPathInHost(store.printStorePath(*scratchPath)); + ++ /* An optional file descriptor of a directory used for intermediate ++ operations. */ ++ AutoCloseFD tempDirFd; ++ /* RAII cleanup of a temporary directory inside the store that is used ++ for intermediate operations. */ ++ AutoDelete delTempDir; ++ + auto finish = [&](StorePath finalStorePath) { + /* Store the final path */ + finalOutputs.insert_or_assign(outputName, finalStorePath); +@@ -1744,6 +1751,25 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() + return newInfo0; + }; + ++ auto moveOutputToTempDir = [&]() -> void { ++ std::filesystem::path tempDir; ++ std::tie(tempDir, tempDirFd) = store.createTempDirInStore(); ++ delTempDir = AutoDelete(tempDir); ++ ++ auto tmpOutput = tempDir / "x"; ++ ++ /* Serialise and create a fresh copy of the output to break ++ any stale writable file descriptors. Copy through the ++ serialisation/deserialisation. TODO: Use copyRecursive here and ++ make use of reflinking. */ ++ auto source = sinkToSource([&](Sink & nextSink) { dumpPath(actualPath, nextSink); }); ++ restorePath(tmpOutput, *source, store.config->getLocalSettings().fsyncStorePaths); ++ /* This makes it slightly harder to make sense of the control flow. The rule ++ of thumb is that actualPath points to the current location of the stuff ++ that we'll end up registering. */ ++ actualPath = std::move(tmpOutput); ++ }; ++ + ValidPathInfo newInfo = std::visit( + overloaded{ + +@@ -1771,14 +1797,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() + + [&](const DerivationOutput::CAFixed & dof) { + auto & wanted = dof.ca.hash; +- +- // Replace the output by a fresh copy of itself to make sure +- // that there's no stale file descriptor pointing to it +- std::filesystem::path tmpOutput = actualPath.native() + ".tmp"; +- copyFile(actualPath, tmpOutput, true); +- +- std::filesystem::rename(tmpOutput, actualPath); +- ++ moveOutputToTempDir(); + return newInfoFromCA( + DerivationOutput::CAFloating{ + .method = dof.ca.method, +@@ -1795,6 +1814,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() + }, + + [&](const DerivationOutput::Impure & doi) { ++ moveOutputToTempDir(); + return newInfoFromCA( + DerivationOutput::CAFloating{ + .method = doi.method, diff --git a/pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-git.patch b/pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-git.patch new file mode 100644 index 000000000000..5b0fbdb46536 --- /dev/null +++ b/pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-git.patch @@ -0,0 +1,231 @@ +From 44017ca497c8b44d5dac179f5afc63e91fe45ed6 Mon Sep 17 00:00:00 2001 +From: Sergei Zimmerman +Date: Sun, 5 Apr 2026 16:39:58 +0300 +Subject: [PATCH] libstore: Use landlock with + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET for new enough kernels +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This partially fixes the issue with cooperating processes being able +to communicate via abstract sockets. The fix is partial, because processes +outside the landlock domain of the sandboxed process can still connect to +a socket created by the FOD. There's no equivalent way of restricting inbound +connections. This closes the gap when there's no cooperating process on the host +(i.e. 2 separate FODs). + +>= 6.12 kernel is widespread enough (NixOS 25.11 ships it by +default) that we have no reason not to apply this hardening, even though +it's incomplete. + +ca-fd-leak test exercises this exact code path and now the smuggling +process fails with (on new enough kernels that have landlock support enabled): + +vm-test-run-ca-fd-leak> machine # sandbox setup: applied landlock sandboxing +vm-test-run-ca-fd-leak> machine # building '/nix/store/s7brgi6pdr5f3n8yqlgmdlz8blb89njc-smuggled.drv'... +vm-test-run-ca-fd-leak> machine # building derivation '/nix/store/s7brgi6pdr5f3n8yqlgmdlz8blb89njc-smuggled.drv': woken up +vm-test-run-ca-fd-leak> machine # connect: Operation not permitted +vm-test-run-ca-fd-leak> machine # sendmsg: Socket not connected + +Signed-off-by: Jörg Thalheim +--- + src/libstore/meson.build | 5 + + .../unix/build/linux-derivation-builder.cc | 101 ++++++++++++++++++ + tests/nixos/ca-fd-leak/default.nix | 8 +- + 3 files changed, 112 insertions(+), 2 deletions(-) + +diff --git a/src/libstore/meson.build b/src/libstore/meson.build +index 445798544..753c78687 100644 +--- a/src/libstore/meson.build ++++ b/src/libstore/meson.build +@@ -79,6 +79,11 @@ foreach funcspec : check_funcs + configdata_priv.set(define_name, define_value) + endforeach + ++if host_machine.system() == 'linux' ++ has_landlock = cxx.has_header('linux/landlock.h') ++ configdata_priv.set('HAVE_LANDLOCK', has_landlock.to_int()) ++endif ++ + has_acl_support = cxx.has_header('sys/xattr.h') \ + and cxx.has_function('llistxattr') \ + and cxx.has_function('lremovexattr') +diff --git a/src/libstore/unix/build/linux-derivation-builder.cc b/src/libstore/unix/build/linux-derivation-builder.cc +index 9cfd3cbcd..c71d23e15 100644 +--- a/src/libstore/unix/build/linux-derivation-builder.cc ++++ b/src/libstore/unix/build/linux-derivation-builder.cc +@@ -1,5 +1,7 @@ + #ifdef __linux__ + ++# include "store-config-private.hh" ++ + # include "nix/store/globals.hh" + # include "nix/store/personality.hh" + # include "nix/store/filetransfer.hh" +@@ -11,6 +13,8 @@ + + # include + # include ++# include ++ + # include + # include + # include +@@ -19,11 +23,16 @@ + # include + # include + # include ++# include + + # if HAVE_SECCOMP + # include + # endif + ++# if HAVE_LANDLOCK ++# include ++# endif ++ + # define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root, put_old)) + + namespace nix { +@@ -129,6 +138,77 @@ static void setupSeccomp(const LocalSettings & localSettings) + # endif + } + ++# if HAVE_LANDLOCK && defined(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET) ++ ++# define DO_LANDLOCK 1 ++ ++/* We are using LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET on best-effort basis. There are no glibc wrappers for now. */ ++ ++static int landlockCreateRuleset(const ::landlock_ruleset_attr * attr, std::size_t size, std::uint32_t flags) ++{ ++ return ::syscall(__NR_landlock_create_ruleset, attr, size, flags); ++} ++ ++static int landlockRestrictSelf(Descriptor rulesetFd, std::uint32_t flags) ++{ ++ return ::syscall(__NR_landlock_restrict_self, rulesetFd, flags); ++} ++ ++static int getLandlockAbiVersion() ++{ ++ int abiVersion = landlockCreateRuleset(nullptr, 0, LANDLOCK_CREATE_RULESET_VERSION); ++ return abiVersion; ++} ++ ++static void setupLandlock() ++{ ++ bool landlockSupportsScopeAbstractUnixSocket = []() { ++ int abiVersion = getLandlockAbiVersion(); ++ if (abiVersion >= 6) ++ /* All good, we can use LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET. See ++ https://docs.kernel.org/userspace-api/landlock.html#abstract-unix-socket-abi-6 */ ++ return true; ++ ++ if (abiVersion == -1) { ++ debug("landlock is not available"); ++ return false; ++ } ++ ++ debug("landlock version %d does not support LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", abiVersion); ++ return false; ++ }(); ++ ++ /* Bail out early if landlock is not enabled or LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET wouldn't work. ++ TODO: Consider adding more landlock rules for filesystem access as defense-in-depth on top. */ ++ if (!landlockSupportsScopeAbstractUnixSocket) ++ return; ++ ++ ::landlock_ruleset_attr attr = { ++ /* This prevents multiple FODs from communicating with each other ++ via abstract sockets. Note that cooperating processes outside the ++ sandbox can still connect to an abstract socket created by the FOD. To ++ mitigate that issue entirely we'd still need network namespaces. */ ++ .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, ++ }; ++ ++ /* This better not fail - if the kernel reports a new enough ABI version we ++ should treat any errors as fatal from now on. */ ++ AutoCloseFD rulesetFd = landlockCreateRuleset(&attr, sizeof(attr), 0); ++ if (!rulesetFd) ++ throw SysError("failed to create a landlock ruleset"); ++ ++ if (landlockRestrictSelf(rulesetFd.get(), 0) == -1) ++ throw SysError("failed to apply landlock"); ++ ++ debug("applied landlock sandboxing"); ++} ++ ++# else ++ ++# define DO_LANDLOCK 0 ++ ++# endif ++ + static void doBind(const std::filesystem::path & source, const std::filesystem::path & target, bool optional = false) + { + debug("bind mounting %1% to %2%", PathFmt(source), PathFmt(target)); +@@ -169,8 +249,27 @@ struct LinuxDerivationBuilder : virtual DerivationBuilderImpl + { + auto & localSettings = store.config->getLocalSettings(); + ++ /* Set the NO_NEW_PRIVS before doing seccomp/landlock setup. ++ landlock_restrict_self requires either NO_NEW_PRIVS or CAP_SYS_ADMIN. ++ With user namespaces we do get CAP_SYS_ADMIN. */ ++ if (!localSettings.allowNewPrivileges) ++ if (::prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) ++ throw SysError("failed to set PR_SET_NO_NEW_PRIVS"); ++ + setupSeccomp(localSettings); + ++# if DO_LANDLOCK ++ try { ++ setupLandlock(); ++ } catch (SysError & e) { ++ if (e.errNo != EPERM) ++ throw; ++ /* If allowNewPrivileges is true and we don't have CAP_SYS_ADMIN ++ this code path might be hit. */ ++ warn("setting up landlock: %s", e.message()); ++ } ++# endif ++ + linux::setPersonality({ + .system = drv.platform, + .impersonateLinux26 = localSettings.impersonateLinux26, +@@ -765,4 +864,6 @@ struct ChrootLinuxDerivationBuilder : ChrootDerivationBuilder, LinuxDerivationBu + + } // namespace nix + ++# undef DO_LANDLOCK ++ + #endif +diff --git a/tests/nixos/ca-fd-leak/default.nix b/tests/nixos/ca-fd-leak/default.nix +index 902aacdc6..dc944290f 100644 +--- a/tests/nixos/ca-fd-leak/default.nix ++++ b/tests/nixos/ca-fd-leak/default.nix +@@ -78,7 +78,7 @@ in + + # Build the smuggled derivation. + # This will connect to the smuggler server and send it the file descriptor +- machine.succeed(r""" ++ sender_output = machine.succeed(r""" + nix-build -E ' + builtins.derivation { + name = "smuggled"; +@@ -89,9 +89,13 @@ in + outputHash = builtins.hashString "sha256" "hello, world\n"; + builder = "${pkgs.busybox-sandbox-shell}/bin/sh"; + args = [ "-c" "echo \"hello, world\" > $out; ''${${sender}} ${socketName}" ]; +- }' ++ }' 2>&1 + """.strip()) + ++ # Landlock's LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET prevents a sandboxed process ++ # from connecting to an abstract socket created in an unrelated landlock domain. ++ # There's no such flag for preventing inbound connections. ++ assert "connect: Operation not permitted" in sender_output + + # Tell the smuggler server that we're done + machine.execute("echo done | ${pkgs.socat}/bin/socat - ABSTRACT-CONNECT:${socketName}") From b368cceed7f6ff4aa8154507da051055e886132b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 7 Apr 2026 21:29:21 +0200 Subject: [PATCH 15/20] nixVersions.nix_2_30: 2.30.3 -> 2.30.4 The 2.30.4 tarball already contains the mdbook 0.5, lowdown 3.0 and GHSA-g3g9-5vj6-r3gj fixes that were previously backported downstream, so drop the now-redundant patches. --- pkgs/tools/package-management/nix/default.nix | 17 +-- .../patches/ghsa-g3g9-5vj6-r3gj-2.30.patch | 126 ------------------ 2 files changed, 3 insertions(+), 140 deletions(-) delete mode 100644 pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.30.patch diff --git a/pkgs/tools/package-management/nix/default.nix b/pkgs/tools/package-management/nix/default.nix index 4460014ad689..edadd2782e83 100644 --- a/pkgs/tools/package-management/nix/default.nix +++ b/pkgs/tools/package-management/nix/default.nix @@ -171,28 +171,17 @@ lib.makeExtensible ( nixComponents_2_30 = (nixDependencies.callPackage ./modular/packages.nix rec { - version = "2.30.3"; + version = "2.30.4"; inherit teams; otherSplices = generateSplicesForNixComponents "nixComponents_2_30"; src = fetchFromGitHub { owner = "NixOS"; repo = "nix"; tag = version; - hash = "sha256-kBuwzMgIE9Tmve0Rpp+q+YCsE2mw9d62M/950ViWeJ0="; + hash = "sha256-cJ96IBZCYoX0Tdlo5Q7qDSAKfL6QcUq/4Kr1UplH50E="; }; }).appendPatches - ( - patches_common - ++ [ - (fetchpatch2 { - name = "nix-2.30-14695-mdbook-0.5-support.patch"; - url = "https://github.com/NixOS/nix/commit/5cbd7856de0a9c13351f98e32a1e26d0854d87fd.patch?full_index=1"; - hash = "sha256-r2ZF1zBZDKMvyX6X4VsaTMrg0zdjn59Jf6Hqg56r29E="; - }) - lowdown30PatchOld - ./patches/ghsa-g3g9-5vj6-r3gj-2.30.patch - ] - ); + (patches_common ++ [ lowdown30PatchOld ]); nix_2_30 = addTests "nix_2_30" self.nixComponents_2_30.nix-everything; diff --git a/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.30.patch b/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.30.patch deleted file mode 100644 index d7514f864afa..000000000000 --- a/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.30.patch +++ /dev/null @@ -1,126 +0,0 @@ -From 4d0a078f1dae9a07d04e1a72e7e62fbf2ca249e0 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= -Date: Mon, 6 Apr 2026 16:49:13 +0200 -Subject: [PATCH] Fixes for GHSA-g3g9-5vj6-r3gj -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -Squashed commit of the following: - -commit a8e5b27728e3b18e63c2503a83afd04031994623 -Author: Sergei Zimmerman -Date: Fri Apr 3 00:21:31 2026 +0300 - - derivation-builder: Don't use copyFile for FOD output copying, put the output in a temporary directory in the store - -commit 49ff4d6779ec20c5339b237dec9b240c3eabf535 -Author: Sergei Zimmerman -Date: Fri Apr 3 00:21:21 2026 +0300 - - libstore: Make temporary in-store directory not world-readable - -Signed-off-by: Jörg Thalheim ---- - src/libstore/include/nix/store/local-store.hh | 2 ++ - src/libstore/local-store.cc | 5 +-- - src/libstore/unix/build/derivation-builder.cc | 36 ++++++++++++++----- - 3 files changed, 33 insertions(+), 10 deletions(-) - -diff --git a/src/libstore/include/nix/store/local-store.hh b/src/libstore/include/nix/store/local-store.hh -index 5f3a249f8..74fa1c2e8 100644 ---- a/src/libstore/include/nix/store/local-store.hh -+++ b/src/libstore/include/nix/store/local-store.hh -@@ -446,6 +446,8 @@ private: - friend struct PathSubstitutionGoal; - friend struct SubstitutionGoal; - friend struct DerivationGoal; -+ /* Only used for createTempDirInStore. */ -+ friend class DerivationBuilderImpl; - }; - - } // namespace nix -diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc -index 49c499e3f..5f897856f 100644 ---- a/src/libstore/local-store.cc -+++ b/src/libstore/local-store.cc -@@ -1316,8 +1316,9 @@ std::pair LocalStore::createTempDirInStore() - do { - /* There is a slight possibility that `tmpDir' gets deleted by - the GC between createTempDir() and when we acquire a lock on it. -- We'll repeat until 'tmpDir' exists and we've locked it. */ -- tmpDirFn = createTempDir(config->realStoreDir, "tmp"); -+ We'll repeat until 'tmpDir' exists and we've locked it. -+ Make the directory accessible only to the current user.*/ -+ tmpDirFn = createTempDir(config->realStoreDir, "tmp", /*mode=*/0700); - tmpDirFd = openDirectory(tmpDirFn); - if (!tmpDirFd) { - continue; -diff --git a/src/libstore/unix/build/derivation-builder.cc b/src/libstore/unix/build/derivation-builder.cc -index 5f9dafb57..609d88a79 100644 ---- a/src/libstore/unix/build/derivation-builder.cc -+++ b/src/libstore/unix/build/derivation-builder.cc -@@ -1581,6 +1581,13 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() - assert(output && scratchPath); - auto actualPath = realPathInSandbox(store.printStorePath(*scratchPath)); - -+ /* An optional file descriptor of a directory used for intermediate -+ operations. */ -+ AutoCloseFD tempDirFd; -+ /* RAII cleanup of a temporary directory inside the store that is used -+ for intermediate operations. */ -+ std::optional delTempDir; -+ - auto finish = [&](StorePath finalStorePath) { - /* Store the final path */ - finalOutputs.insert_or_assign(outputName, finalStorePath); -@@ -1715,6 +1722,25 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() - return newInfo0; - }; - -+ auto moveOutputToTempDir = [&]() -> void { -+ std::filesystem::path tempDir; -+ std::tie(tempDir, tempDirFd) = getLocalStore(store).createTempDirInStore(); -+ delTempDir.emplace(tempDir); -+ -+ auto tmpOutput = tempDir / "x"; -+ -+ /* Serialise and create a fresh copy of the output to break -+ any stale writable file descriptors. Copy through the -+ serialisation/deserialisation. TODO: Use copyRecursive here and -+ make use of reflinking. */ -+ auto source = sinkToSource([&](Sink & nextSink) { dumpPath(actualPath, nextSink); }); -+ restorePath(tmpOutput, *source, settings.fsyncStorePaths); -+ /* This makes it slightly harder to make sense of the control flow. The rule -+ of thumb is that actualPath points to the current location of the stuff -+ that we'll end up registering. */ -+ actualPath = std::move(tmpOutput); -+ }; -+ - ValidPathInfo newInfo = std::visit( - overloaded{ - -@@ -1742,14 +1768,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() - - [&](const DerivationOutput::CAFixed & dof) { - auto & wanted = dof.ca.hash; -- -- // Replace the output by a fresh copy of itself to make sure -- // that there's no stale file descriptor pointing to it -- Path tmpOutput = actualPath + ".tmp"; -- copyFile(std::filesystem::path(actualPath), std::filesystem::path(tmpOutput), true); -- -- std::filesystem::rename(tmpOutput, actualPath); -- -+ moveOutputToTempDir(); - auto newInfo0 = newInfoFromCA( - DerivationOutput::CAFloating{ - .method = dof.ca.method, -@@ -1790,6 +1809,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() - }, - - [&](const DerivationOutput::Impure & doi) { -+ moveOutputToTempDir(); - return newInfoFromCA( - DerivationOutput::CAFloating{ - .method = doi.method, From 83250692a8ffc491590b6f490137d12db1c77853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 7 Apr 2026 21:29:38 +0200 Subject: [PATCH 16/20] nixVersions.nix_2_31: 2.31.3 -> 2.31.4 The 2.31.4 tarball already contains the lowdown 3.0 compatibility fix, the GHSA-g3g9-5vj6-r3gj sandbox escape fix and the landlock abstract socket hardening, so drop the now-redundant downstream backports. --- pkgs/tools/package-management/nix/default.nix | 10 +- .../patches/ghsa-g3g9-5vj6-r3gj-2.31.patch | 126 ---------- ...dlock-abstract-socket-hardening-2.31.patch | 232 ------------------ 3 files changed, 3 insertions(+), 365 deletions(-) delete mode 100644 pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.31.patch delete mode 100644 pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-2.31.patch diff --git a/pkgs/tools/package-management/nix/default.nix b/pkgs/tools/package-management/nix/default.nix index edadd2782e83..26e77581f121 100644 --- a/pkgs/tools/package-management/nix/default.nix +++ b/pkgs/tools/package-management/nix/default.nix @@ -187,21 +187,17 @@ lib.makeExtensible ( nixComponents_2_31 = (nixDependencies.callPackage ./modular/packages.nix rec { - version = "2.31.3"; + version = "2.31.4"; inherit teams; otherSplices = generateSplicesForNixComponents "nixComponents_2_31"; src = fetchFromGitHub { owner = "NixOS"; repo = "nix"; tag = version; - hash = "sha256-oe0YWe8f+pwQH4aYD2XXLW5iEHyXNUddurqJ5CUVCIk="; + hash = "sha256-f/haYfcI+9IiYVH+g6cjhF8cK7QWHAFfcPtF+57ujZ0="; }; }).appendPatches - [ - lowdown30Patch - ./patches/ghsa-g3g9-5vj6-r3gj-2.31.patch - ./patches/landlock-abstract-socket-hardening-2.31.patch - ]; + [ ]; nix_2_31 = addTests "nix_2_31" self.nixComponents_2_31.nix-everything; diff --git a/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.31.patch b/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.31.patch deleted file mode 100644 index 4d08316d67a7..000000000000 --- a/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.31.patch +++ /dev/null @@ -1,126 +0,0 @@ -From df0153a9eca42c4ed5ac784657c8c5d0664c9e0e Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= -Date: Mon, 6 Apr 2026 16:49:13 +0200 -Subject: [PATCH] Fixes for GHSA-g3g9-5vj6-r3gj -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -Squashed commit of the following: - -commit bf1d95ae399e47daeafa9e0dfac967f415c52447 -Author: Sergei Zimmerman -Date: Fri Apr 3 00:21:31 2026 +0300 - - derivation-builder: Don't use copyFile for FOD output copying, put the output in a temporary directory in the store - -commit ca5986fa353d402738acf961bcaa017c61019809 -Author: Sergei Zimmerman -Date: Fri Apr 3 00:21:21 2026 +0300 - - libstore: Make temporary in-store directory not world-readable - -Signed-off-by: Jörg Thalheim ---- - src/libstore/include/nix/store/local-store.hh | 2 ++ - src/libstore/local-store.cc | 5 +-- - src/libstore/unix/build/derivation-builder.cc | 36 ++++++++++++++----- - 3 files changed, 33 insertions(+), 10 deletions(-) - -diff --git a/src/libstore/include/nix/store/local-store.hh b/src/libstore/include/nix/store/local-store.hh -index ec88ffb4f..91df9cd77 100644 ---- a/src/libstore/include/nix/store/local-store.hh -+++ b/src/libstore/include/nix/store/local-store.hh -@@ -455,6 +455,8 @@ private: - - friend struct PathSubstitutionGoal; - friend struct DerivationGoal; -+ /* Only used for createTempDirInStore. */ -+ friend class DerivationBuilderImpl; - }; - - } // namespace nix -diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc -index d24bc415c..524a9fdda 100644 ---- a/src/libstore/local-store.cc -+++ b/src/libstore/local-store.cc -@@ -1342,8 +1342,9 @@ std::pair LocalStore::createTempDirInStore() - do { - /* There is a slight possibility that `tmpDir' gets deleted by - the GC between createTempDir() and when we acquire a lock on it. -- We'll repeat until 'tmpDir' exists and we've locked it. */ -- tmpDirFn = createTempDir(config->realStoreDir, "tmp"); -+ We'll repeat until 'tmpDir' exists and we've locked it. -+ Make the directory accessible only to the current user.*/ -+ tmpDirFn = createTempDir(config->realStoreDir, "tmp", /*mode=*/0700); - tmpDirFd = openDirectory(tmpDirFn); - if (!tmpDirFd) { - continue; -diff --git a/src/libstore/unix/build/derivation-builder.cc b/src/libstore/unix/build/derivation-builder.cc -index 73bb026a2..d11f80159 100644 ---- a/src/libstore/unix/build/derivation-builder.cc -+++ b/src/libstore/unix/build/derivation-builder.cc -@@ -1473,6 +1473,13 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() - assert(output && scratchPath); - auto actualPath = realPathInSandbox(store.printStorePath(*scratchPath)); - -+ /* An optional file descriptor of a directory used for intermediate -+ operations. */ -+ AutoCloseFD tempDirFd; -+ /* RAII cleanup of a temporary directory inside the store that is used -+ for intermediate operations. */ -+ std::optional delTempDir; -+ - auto finish = [&](StorePath finalStorePath) { - /* Store the final path */ - finalOutputs.insert_or_assign(outputName, finalStorePath); -@@ -1607,6 +1614,25 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() - return newInfo0; - }; - -+ auto moveOutputToTempDir = [&]() -> void { -+ std::filesystem::path tempDir; -+ std::tie(tempDir, tempDirFd) = store.createTempDirInStore(); -+ delTempDir.emplace(tempDir); -+ -+ auto tmpOutput = tempDir / "x"; -+ -+ /* Serialise and create a fresh copy of the output to break -+ any stale writable file descriptors. Copy through the -+ serialisation/deserialisation. TODO: Use copyRecursive here and -+ make use of reflinking. */ -+ auto source = sinkToSource([&](Sink & nextSink) { dumpPath(actualPath, nextSink); }); -+ restorePath(tmpOutput, *source, settings.fsyncStorePaths); -+ /* This makes it slightly harder to make sense of the control flow. The rule -+ of thumb is that actualPath points to the current location of the stuff -+ that we'll end up registering. */ -+ actualPath = std::move(tmpOutput); -+ }; -+ - ValidPathInfo newInfo = std::visit( - overloaded{ - -@@ -1634,14 +1660,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() - - [&](const DerivationOutput::CAFixed & dof) { - auto & wanted = dof.ca.hash; -- -- // Replace the output by a fresh copy of itself to make sure -- // that there's no stale file descriptor pointing to it -- Path tmpOutput = actualPath + ".tmp"; -- copyFile(std::filesystem::path(actualPath), std::filesystem::path(tmpOutput), true); -- -- std::filesystem::rename(tmpOutput, actualPath); -- -+ moveOutputToTempDir(); - auto newInfo0 = newInfoFromCA( - DerivationOutput::CAFloating{ - .method = dof.ca.method, -@@ -1682,6 +1701,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() - }, - - [&](const DerivationOutput::Impure & doi) { -+ moveOutputToTempDir(); - return newInfoFromCA( - DerivationOutput::CAFloating{ - .method = doi.method, diff --git a/pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-2.31.patch b/pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-2.31.patch deleted file mode 100644 index 7584a608db7f..000000000000 --- a/pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-2.31.patch +++ /dev/null @@ -1,232 +0,0 @@ -From b743f908b10aa56b88fb490de8a0d893bd2d83d5 Mon Sep 17 00:00:00 2001 -From: Sergei Zimmerman -Date: Sun, 5 Apr 2026 16:39:58 +0300 -Subject: [PATCH] libstore: Use landlock with - LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET for new enough kernels -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -This partially fixes the issue with cooperating processes being able -to communicate via abstract sockets. The fix is partial, because processes -outside the landlock domain of the sandboxed process can still connect to -a socket created by the FOD. There's no equivalent way of restricting inbound -connections. This closes the gap when there's no cooperating process on the host -(i.e. 2 separate FODs). - ->= 6.12 kernel is widespread enough (NixOS 25.11 ships it by -default) that we have no reason not to apply this hardening, even though -it's incomplete. - -ca-fd-leak test exercises this exact code path and now the smuggling -process fails with (on new enough kernels that have landlock support enabled): - -vm-test-run-ca-fd-leak> machine # sandbox setup: applied landlock sandboxing -vm-test-run-ca-fd-leak> machine # building '/nix/store/s7brgi6pdr5f3n8yqlgmdlz8blb89njc-smuggled.drv'... -vm-test-run-ca-fd-leak> machine # building derivation '/nix/store/s7brgi6pdr5f3n8yqlgmdlz8blb89njc-smuggled.drv': woken up -vm-test-run-ca-fd-leak> machine # connect: Operation not permitted -vm-test-run-ca-fd-leak> machine # sendmsg: Socket not connected - -(cherry picked from commit 44017ca497c8b44d5dac179f5afc63e91fe45ed6) -Signed-off-by: Jörg Thalheim ---- - src/libstore/meson.build | 5 + - .../unix/build/linux-derivation-builder.cc | 103 ++++++++++++++++++ - tests/nixos/ca-fd-leak/default.nix | 8 +- - 3 files changed, 114 insertions(+), 2 deletions(-) - -diff --git a/src/libstore/meson.build b/src/libstore/meson.build -index a275f4edc..dc8a7a940 100644 ---- a/src/libstore/meson.build -+++ b/src/libstore/meson.build -@@ -77,6 +77,11 @@ foreach funcspec : check_funcs - configdata_priv.set(define_name, define_value) - endforeach - -+if host_machine.system() == 'linux' -+ has_landlock = cxx.has_header('linux/landlock.h') -+ configdata_priv.set('HAVE_LANDLOCK', has_landlock.to_int()) -+endif -+ - has_acl_support = cxx.has_header('sys/xattr.h') \ - and cxx.has_function('llistxattr') \ - and cxx.has_function('lremovexattr') -diff --git a/src/libstore/unix/build/linux-derivation-builder.cc b/src/libstore/unix/build/linux-derivation-builder.cc -index eaac2bc28..dc5abf808 100644 ---- a/src/libstore/unix/build/linux-derivation-builder.cc -+++ b/src/libstore/unix/build/linux-derivation-builder.cc -@@ -1,10 +1,16 @@ - #ifdef __linux__ - -+# include "store-config-private.hh" -+ - # include "nix/store/personality.hh" - # include "nix/util/cgroup.hh" - # include "nix/util/linux-namespaces.hh" - # include "linux/fchmodat2-compat.hh" - -+# include -+# include -+# include -+ - # include - # include - # include -@@ -13,11 +19,16 @@ - # include - # include - # include -+# include - - # if HAVE_SECCOMP - # include - # endif - -+# if HAVE_LANDLOCK -+# include -+# endif -+ - # define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root, put_old)) - - namespace nix { -@@ -121,6 +132,77 @@ static void setupSeccomp() - # endif - } - -+# if HAVE_LANDLOCK && defined(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET) -+ -+# define DO_LANDLOCK 1 -+ -+/* We are using LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET on best-effort basis. There are no glibc wrappers for now. */ -+ -+static int landlockCreateRuleset(const ::landlock_ruleset_attr * attr, std::size_t size, std::uint32_t flags) -+{ -+ return ::syscall(__NR_landlock_create_ruleset, attr, size, flags); -+} -+ -+static int landlockRestrictSelf(Descriptor rulesetFd, std::uint32_t flags) -+{ -+ return ::syscall(__NR_landlock_restrict_self, rulesetFd, flags); -+} -+ -+static int getLandlockAbiVersion() -+{ -+ int abiVersion = landlockCreateRuleset(nullptr, 0, LANDLOCK_CREATE_RULESET_VERSION); -+ return abiVersion; -+} -+ -+static void setupLandlock() -+{ -+ bool landlockSupportsScopeAbstractUnixSocket = []() { -+ int abiVersion = getLandlockAbiVersion(); -+ if (abiVersion >= 6) -+ /* All good, we can use LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET. See -+ https://docs.kernel.org/userspace-api/landlock.html#abstract-unix-socket-abi-6 */ -+ return true; -+ -+ if (abiVersion == -1) { -+ debug("landlock is not available"); -+ return false; -+ } -+ -+ debug("landlock version %d does not support LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", abiVersion); -+ return false; -+ }(); -+ -+ /* Bail out early if landlock is not enabled or LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET wouldn't work. -+ TODO: Consider adding more landlock rules for filesystem access as defense-in-depth on top. */ -+ if (!landlockSupportsScopeAbstractUnixSocket) -+ return; -+ -+ ::landlock_ruleset_attr attr = { -+ /* This prevents multiple FODs from communicating with each other -+ via abstract sockets. Note that cooperating processes outside the -+ sandbox can still connect to an abstract socket created by the FOD. To -+ mitigate that issue entirely we'd still need network namespaces. */ -+ .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, -+ }; -+ -+ /* This better not fail - if the kernel reports a new enough ABI version we -+ should treat any errors as fatal from now on. */ -+ AutoCloseFD rulesetFd = landlockCreateRuleset(&attr, sizeof(attr), 0); -+ if (!rulesetFd) -+ throw SysError("failed to create a landlock ruleset"); -+ -+ if (landlockRestrictSelf(rulesetFd.get(), 0) == -1) -+ throw SysError("failed to apply landlock"); -+ -+ debug("applied landlock sandboxing"); -+} -+ -+# else -+ -+# define DO_LANDLOCK 0 -+ -+# endif -+ - static void doBind(const Path & source, const Path & target, bool optional = false) - { - debug("bind mounting '%1%' to '%2%'", source, target); -@@ -159,8 +241,27 @@ struct LinuxDerivationBuilder : virtual DerivationBuilderImpl - - void enterChroot() override - { -+ /* Set the NO_NEW_PRIVS before doing seccomp/landlock setup. -+ landlock_restrict_self requires either NO_NEW_PRIVS or CAP_SYS_ADMIN. -+ With user namespaces we do get CAP_SYS_ADMIN. */ -+ if (!settings.allowNewPrivileges) -+ if (::prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) -+ throw SysError("failed to set PR_SET_NO_NEW_PRIVS"); -+ - setupSeccomp(); - -+# if DO_LANDLOCK -+ try { -+ setupLandlock(); -+ } catch (SysError & e) { -+ if (e.errNo != EPERM) -+ throw; -+ /* If allowNewPrivileges is true and we don't have CAP_SYS_ADMIN -+ this code path might be hit. */ -+ warn("setting up landlock: %s", e.message()); -+ } -+# endif -+ - linux::setPersonality(drv.platform); - } - }; -@@ -709,4 +810,6 @@ struct ChrootLinuxDerivationBuilder : ChrootDerivationBuilder, LinuxDerivationBu - - } // namespace nix - -+# undef DO_LANDLOCK -+ - #endif -diff --git a/tests/nixos/ca-fd-leak/default.nix b/tests/nixos/ca-fd-leak/default.nix -index 902aacdc6..dc944290f 100644 ---- a/tests/nixos/ca-fd-leak/default.nix -+++ b/tests/nixos/ca-fd-leak/default.nix -@@ -78,7 +78,7 @@ in - - # Build the smuggled derivation. - # This will connect to the smuggler server and send it the file descriptor -- machine.succeed(r""" -+ sender_output = machine.succeed(r""" - nix-build -E ' - builtins.derivation { - name = "smuggled"; -@@ -89,9 +89,13 @@ in - outputHash = builtins.hashString "sha256" "hello, world\n"; - builder = "${pkgs.busybox-sandbox-shell}/bin/sh"; - args = [ "-c" "echo \"hello, world\" > $out; ''${${sender}} ${socketName}" ]; -- }' -+ }' 2>&1 - """.strip()) - -+ # Landlock's LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET prevents a sandboxed process -+ # from connecting to an abstract socket created in an unrelated landlock domain. -+ # There's no such flag for preventing inbound connections. -+ assert "connect: Operation not permitted" in sender_output - - # Tell the smuggler server that we're done - machine.execute("echo done | ${pkgs.socat}/bin/socat - ABSTRACT-CONNECT:${socketName}") From 123da37985273eed01b34db436538c45e05c5cbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 7 Apr 2026 21:29:50 +0200 Subject: [PATCH 17/20] nixVersions.nix_2_34: 2.34.4 -> 2.34.5 The 2.34.5 tarball already contains the GHSA-g3g9-5vj6-r3gj sandbox escape fix and the landlock abstract socket hardening, so drop the now-redundant downstream backports. --- pkgs/tools/package-management/nix/default.nix | 12 +- .../patches/ghsa-g3g9-5vj6-r3gj-2.34.patch | 126 ---------- ...dlock-abstract-socket-hardening-2.34.patch | 234 ------------------ 3 files changed, 3 insertions(+), 369 deletions(-) delete mode 100644 pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.34.patch delete mode 100644 pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-2.34.patch diff --git a/pkgs/tools/package-management/nix/default.nix b/pkgs/tools/package-management/nix/default.nix index 26e77581f121..13a8a74e7a41 100644 --- a/pkgs/tools/package-management/nix/default.nix +++ b/pkgs/tools/package-management/nix/default.nix @@ -203,23 +203,17 @@ lib.makeExtensible ( nixComponents_2_34 = (nixDependencies.callPackage ./modular/packages.nix rec { - version = "2.34.4"; + version = "2.34.5"; inherit teams; otherSplices = generateSplicesForNixComponents "nixComponents_2_34"; src = fetchFromGitHub { owner = "NixOS"; repo = "nix"; tag = version; - hash = "sha256-WPuGqMQGepXoRYjtRudMAMHEoLsIObw2x4sVfho5feA="; + hash = "sha256-/S2bnz+TbRFGmNyR31Hfa70uFvJoMM9wYDjpyEw8I+U="; }; }).appendPatches - ( - patches_common - ++ [ - ./patches/ghsa-g3g9-5vj6-r3gj-2.34.patch - ./patches/landlock-abstract-socket-hardening-2.34.patch - ] - ); + patches_common; nix_2_34 = addTests "nix_2_34" self.nixComponents_2_34.nix-everything; diff --git a/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.34.patch b/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.34.patch deleted file mode 100644 index ea140e3fb149..000000000000 --- a/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.34.patch +++ /dev/null @@ -1,126 +0,0 @@ -From dfda33d358f4522450737c40407ba8d8403a86a1 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= -Date: Mon, 6 Apr 2026 16:49:13 +0200 -Subject: [PATCH] Fixes for GHSA-g3g9-5vj6-r3gj -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -Squashed commit of the following: - -commit d1f2de7d683bef1e2a164f23f33172c79c7725ea -Author: Sergei Zimmerman -Date: Fri Apr 3 00:21:31 2026 +0300 - - derivation-builder: Don't use copyFile for FOD output copying, put the output in a temporary directory in the store - -commit 2f13ee6dc4ade1d919c14757891d5d8a1001eee8 -Author: Sergei Zimmerman -Date: Fri Apr 3 00:21:21 2026 +0300 - - libstore: Make temporary in-store directory not world-readable - -Signed-off-by: Jörg Thalheim ---- - src/libstore/include/nix/store/local-store.hh | 2 ++ - src/libstore/local-store.cc | 5 +-- - src/libstore/unix/build/derivation-builder.cc | 36 ++++++++++++++----- - 3 files changed, 33 insertions(+), 10 deletions(-) - -diff --git a/src/libstore/include/nix/store/local-store.hh b/src/libstore/include/nix/store/local-store.hh -index 512c198b0..0b1e13b51 100644 ---- a/src/libstore/include/nix/store/local-store.hh -+++ b/src/libstore/include/nix/store/local-store.hh -@@ -505,6 +505,8 @@ private: - - friend struct PathSubstitutionGoal; - friend struct DerivationGoal; -+ /* Only used for createTempDirInStore. */ -+ friend class DerivationBuilderImpl; - }; - - } // namespace nix -diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc -index 649ad70dd..686e9988e 100644 ---- a/src/libstore/local-store.cc -+++ b/src/libstore/local-store.cc -@@ -1283,8 +1283,9 @@ std::pair LocalStore::createTempDirInStore() - do { - /* There is a slight possibility that `tmpDir' gets deleted by - the GC between createTempDir() and when we acquire a lock on it. -- We'll repeat until 'tmpDir' exists and we've locked it. */ -- tmpDirFn = createTempDir(std::filesystem::path{config->realStoreDir.get()}, "tmp"); -+ We'll repeat until 'tmpDir' exists and we've locked it. -+ Make the directory accessible only to the current user. */ -+ tmpDirFn = createTempDir(std::filesystem::path{config->realStoreDir.get()}, "tmp", /*mode=*/0700); - tmpDirFd = openDirectory(tmpDirFn); - if (!tmpDirFd) { - continue; -diff --git a/src/libstore/unix/build/derivation-builder.cc b/src/libstore/unix/build/derivation-builder.cc -index da225dc0d..fc877dd56 100644 ---- a/src/libstore/unix/build/derivation-builder.cc -+++ b/src/libstore/unix/build/derivation-builder.cc -@@ -1595,6 +1595,13 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() - assert(output && scratchPath); - auto actualPath = realPathInHost(store.printStorePath(*scratchPath)); - -+ /* An optional file descriptor of a directory used for intermediate -+ operations. */ -+ AutoCloseFD tempDirFd; -+ /* RAII cleanup of a temporary directory inside the store that is used -+ for intermediate operations. */ -+ AutoDelete delTempDir; -+ - auto finish = [&](StorePath finalStorePath) { - /* Store the final path */ - finalOutputs.insert_or_assign(outputName, finalStorePath); -@@ -1742,6 +1749,25 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() - return newInfo0; - }; - -+ auto moveOutputToTempDir = [&]() -> void { -+ std::filesystem::path tempDir; -+ std::tie(tempDir, tempDirFd) = store.createTempDirInStore(); -+ delTempDir = AutoDelete(tempDir); -+ -+ auto tmpOutput = tempDir / "x"; -+ -+ /* Serialise and create a fresh copy of the output to break -+ any stale writable file descriptors. Copy through the -+ serialisation/deserialisation. TODO: Use copyRecursive here and -+ make use of reflinking. */ -+ auto source = sinkToSource([&](Sink & nextSink) { dumpPath(actualPath, nextSink); }); -+ restorePath(tmpOutput, *source, store.config->getLocalSettings().fsyncStorePaths); -+ /* This makes it slightly harder to make sense of the control flow. The rule -+ of thumb is that actualPath points to the current location of the stuff -+ that we'll end up registering. */ -+ actualPath = std::move(tmpOutput); -+ }; -+ - ValidPathInfo newInfo = std::visit( - overloaded{ - -@@ -1769,14 +1795,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() - - [&](const DerivationOutput::CAFixed & dof) { - auto & wanted = dof.ca.hash; -- -- // Replace the output by a fresh copy of itself to make sure -- // that there's no stale file descriptor pointing to it -- std::filesystem::path tmpOutput = actualPath.native() + ".tmp"; -- copyFile(actualPath, tmpOutput, true); -- -- std::filesystem::rename(tmpOutput, actualPath); -- -+ moveOutputToTempDir(); - return newInfoFromCA( - DerivationOutput::CAFloating{ - .method = dof.ca.method, -@@ -1793,6 +1812,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() - }, - - [&](const DerivationOutput::Impure & doi) { -+ moveOutputToTempDir(); - return newInfoFromCA( - DerivationOutput::CAFloating{ - .method = doi.method, diff --git a/pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-2.34.patch b/pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-2.34.patch deleted file mode 100644 index 1ca40ee24b17..000000000000 --- a/pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-2.34.patch +++ /dev/null @@ -1,234 +0,0 @@ -From 23c7a6490a829a978af1acd56d76cc2dee7f4ad6 Mon Sep 17 00:00:00 2001 -From: Sergei Zimmerman -Date: Sun, 5 Apr 2026 16:39:58 +0300 -Subject: [PATCH] libstore: Use landlock with - LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET for new enough kernels -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -This partially fixes the issue with cooperating processes being able -to communicate via abstract sockets. The fix is partial, because processes -outside the landlock domain of the sandboxed process can still connect to -a socket created by the FOD. There's no equivalent way of restricting inbound -connections. This closes the gap when there's no cooperating process on the host -(i.e. 2 separate FODs). - ->= 6.12 kernel is widespread enough (NixOS 25.11 ships it by -default) that we have no reason not to apply this hardening, even though -it's incomplete. - -ca-fd-leak test exercises this exact code path and now the smuggling -process fails with (on new enough kernels that have landlock support enabled): - -vm-test-run-ca-fd-leak> machine # sandbox setup: applied landlock sandboxing -vm-test-run-ca-fd-leak> machine # building '/nix/store/s7brgi6pdr5f3n8yqlgmdlz8blb89njc-smuggled.drv'... -vm-test-run-ca-fd-leak> machine # building derivation '/nix/store/s7brgi6pdr5f3n8yqlgmdlz8blb89njc-smuggled.drv': woken up -vm-test-run-ca-fd-leak> machine # connect: Operation not permitted -vm-test-run-ca-fd-leak> machine # sendmsg: Socket not connected - -(cherry picked from commit 44017ca497c8b44d5dac179f5afc63e91fe45ed6) -Signed-off-by: Jörg Thalheim ---- - src/libstore/meson.build | 5 + - .../unix/build/linux-derivation-builder.cc | 103 ++++++++++++++++++ - tests/nixos/ca-fd-leak/default.nix | 8 +- - 3 files changed, 114 insertions(+), 2 deletions(-) - -diff --git a/src/libstore/meson.build b/src/libstore/meson.build -index 9e52517d4..edc476ae7 100644 ---- a/src/libstore/meson.build -+++ b/src/libstore/meson.build -@@ -79,6 +79,11 @@ foreach funcspec : check_funcs - configdata_priv.set(define_name, define_value) - endforeach - -+if host_machine.system() == 'linux' -+ has_landlock = cxx.has_header('linux/landlock.h') -+ configdata_priv.set('HAVE_LANDLOCK', has_landlock.to_int()) -+endif -+ - has_acl_support = cxx.has_header('sys/xattr.h') \ - and cxx.has_function('llistxattr') \ - and cxx.has_function('lremovexattr') -diff --git a/src/libstore/unix/build/linux-derivation-builder.cc b/src/libstore/unix/build/linux-derivation-builder.cc -index 555a3b6da..476baabe2 100644 ---- a/src/libstore/unix/build/linux-derivation-builder.cc -+++ b/src/libstore/unix/build/linux-derivation-builder.cc -@@ -1,5 +1,7 @@ - #ifdef __linux__ - -+# include "store-config-private.hh" -+ - # include "nix/store/globals.hh" - # include "nix/store/personality.hh" - # include "nix/store/filetransfer.hh" -@@ -9,6 +11,10 @@ - # include "nix/util/serialise.hh" - # include "linux/fchmodat2-compat.hh" - -+# include -+# include -+# include -+ - # include - # include - # include -@@ -17,11 +23,16 @@ - # include - # include - # include -+# include - - # if HAVE_SECCOMP - # include - # endif - -+# if HAVE_LANDLOCK -+# include -+# endif -+ - # define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root, put_old)) - - namespace nix { -@@ -125,6 +136,77 @@ static void setupSeccomp(const LocalSettings & localSettings) - # endif - } - -+# if HAVE_LANDLOCK && defined(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET) -+ -+# define DO_LANDLOCK 1 -+ -+/* We are using LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET on best-effort basis. There are no glibc wrappers for now. */ -+ -+static int landlockCreateRuleset(const ::landlock_ruleset_attr * attr, std::size_t size, std::uint32_t flags) -+{ -+ return ::syscall(__NR_landlock_create_ruleset, attr, size, flags); -+} -+ -+static int landlockRestrictSelf(Descriptor rulesetFd, std::uint32_t flags) -+{ -+ return ::syscall(__NR_landlock_restrict_self, rulesetFd, flags); -+} -+ -+static int getLandlockAbiVersion() -+{ -+ int abiVersion = landlockCreateRuleset(nullptr, 0, LANDLOCK_CREATE_RULESET_VERSION); -+ return abiVersion; -+} -+ -+static void setupLandlock() -+{ -+ bool landlockSupportsScopeAbstractUnixSocket = []() { -+ int abiVersion = getLandlockAbiVersion(); -+ if (abiVersion >= 6) -+ /* All good, we can use LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET. See -+ https://docs.kernel.org/userspace-api/landlock.html#abstract-unix-socket-abi-6 */ -+ return true; -+ -+ if (abiVersion == -1) { -+ debug("landlock is not available"); -+ return false; -+ } -+ -+ debug("landlock version %d does not support LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", abiVersion); -+ return false; -+ }(); -+ -+ /* Bail out early if landlock is not enabled or LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET wouldn't work. -+ TODO: Consider adding more landlock rules for filesystem access as defense-in-depth on top. */ -+ if (!landlockSupportsScopeAbstractUnixSocket) -+ return; -+ -+ ::landlock_ruleset_attr attr = { -+ /* This prevents multiple FODs from communicating with each other -+ via abstract sockets. Note that cooperating processes outside the -+ sandbox can still connect to an abstract socket created by the FOD. To -+ mitigate that issue entirely we'd still need network namespaces. */ -+ .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, -+ }; -+ -+ /* This better not fail - if the kernel reports a new enough ABI version we -+ should treat any errors as fatal from now on. */ -+ AutoCloseFD rulesetFd = landlockCreateRuleset(&attr, sizeof(attr), 0); -+ if (!rulesetFd) -+ throw SysError("failed to create a landlock ruleset"); -+ -+ if (landlockRestrictSelf(rulesetFd.get(), 0) == -1) -+ throw SysError("failed to apply landlock"); -+ -+ debug("applied landlock sandboxing"); -+} -+ -+# else -+ -+# define DO_LANDLOCK 0 -+ -+# endif -+ - static void doBind(const std::filesystem::path & source, const std::filesystem::path & target, bool optional = false) - { - debug("bind mounting %1% to %2%", PathFmt(source), PathFmt(target)); -@@ -165,8 +247,27 @@ struct LinuxDerivationBuilder : virtual DerivationBuilderImpl - { - auto & localSettings = store.config->getLocalSettings(); - -+ /* Set the NO_NEW_PRIVS before doing seccomp/landlock setup. -+ landlock_restrict_self requires either NO_NEW_PRIVS or CAP_SYS_ADMIN. -+ With user namespaces we do get CAP_SYS_ADMIN. */ -+ if (!localSettings.allowNewPrivileges) -+ if (::prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) -+ throw SysError("failed to set PR_SET_NO_NEW_PRIVS"); -+ - setupSeccomp(localSettings); - -+# if DO_LANDLOCK -+ try { -+ setupLandlock(); -+ } catch (SysError & e) { -+ if (e.errNo != EPERM) -+ throw; -+ /* If allowNewPrivileges is true and we don't have CAP_SYS_ADMIN -+ this code path might be hit. */ -+ warn("setting up landlock: %s", e.message()); -+ } -+# endif -+ - linux::setPersonality({ - .system = drv.platform, - .impersonateLinux26 = localSettings.impersonateLinux26, -@@ -760,4 +861,6 @@ struct ChrootLinuxDerivationBuilder : ChrootDerivationBuilder, LinuxDerivationBu - - } // namespace nix - -+# undef DO_LANDLOCK -+ - #endif -diff --git a/tests/nixos/ca-fd-leak/default.nix b/tests/nixos/ca-fd-leak/default.nix -index 902aacdc6..dc944290f 100644 ---- a/tests/nixos/ca-fd-leak/default.nix -+++ b/tests/nixos/ca-fd-leak/default.nix -@@ -78,7 +78,7 @@ in - - # Build the smuggled derivation. - # This will connect to the smuggler server and send it the file descriptor -- machine.succeed(r""" -+ sender_output = machine.succeed(r""" - nix-build -E ' - builtins.derivation { - name = "smuggled"; -@@ -89,9 +89,13 @@ in - outputHash = builtins.hashString "sha256" "hello, world\n"; - builder = "${pkgs.busybox-sandbox-shell}/bin/sh"; - args = [ "-c" "echo \"hello, world\" > $out; ''${${sender}} ${socketName}" ]; -- }' -+ }' 2>&1 - """.strip()) - -+ # Landlock's LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET prevents a sandboxed process -+ # from connecting to an abstract socket created in an unrelated landlock domain. -+ # There's no such flag for preventing inbound connections. -+ assert "connect: Operation not permitted" in sender_output - - # Tell the smuggler server that we're done - machine.execute("echo done | ${pkgs.socat}/bin/socat - ABSTRACT-CONNECT:${socketName}") From cc8d42cd8132b12a6484f5add58471480e59d3d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 7 Apr 2026 21:33:41 +0200 Subject: [PATCH 18/20] nix: update nix-fallback-paths to 2.31.4 --- nixos/modules/installer/tools/nix-fallback-paths.nix | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nixos/modules/installer/tools/nix-fallback-paths.nix b/nixos/modules/installer/tools/nix-fallback-paths.nix index 68f6494510cb..98bd3cec0de2 100644 --- a/nixos/modules/installer/tools/nix-fallback-paths.nix +++ b/nixos/modules/installer/tools/nix-fallback-paths.nix @@ -1,8 +1,8 @@ { - x86_64-linux = "/nix/store/mxn3vxpvk2b42kgd08aw4bn27qhf434w-nix-2.31.3"; - i686-linux = "/nix/store/b7d35ifww9683l74ydkb0qhrq6lqcisi-nix-2.31.3"; - aarch64-linux = "/nix/store/ysz7dwmy4zd1zm3wzx5dh9g999xv7pbm-nix-2.31.3"; - riscv64-linux = "/nix/store/avfv2lqbnphj2ap8y5ihg1l0sqhpjll7-nix-riscv64-unknown-linux-gnu-2.31.3"; - x86_64-darwin = "/nix/store/s8lcl6nrd5ia7nr4zx9mg720i9f8qm37-nix-2.31.3"; - aarch64-darwin = "/nix/store/vfjzbcl3kf9jjwh0g2x03cvz2x5hg8py-nix-2.31.3"; + x86_64-linux = "/nix/store/vals1fs2rl6yn5f8gbqj9mvly4r27shs-nix-2.31.4"; + i686-linux = "/nix/store/fyrlz8cdzvf5csdh5885wifpxc8ywdii-nix-2.31.4"; + aarch64-linux = "/nix/store/19p3nc892m7idfg2ngd1614660xqbhnm-nix-2.31.4"; + riscv64-linux = "/nix/store/x1isvq0xnyrg0l29qk2xlp929cgjsmqy-nix-riscv64-unknown-linux-gnu-2.31.4"; + x86_64-darwin = "/nix/store/4gqxzd5zkxcq271wi5saml4zd92rdkws-nix-2.31.4"; + aarch64-darwin = "/nix/store/r3gz609kdqchxcmil7dhbravbq8kwm93-nix-2.31.4"; } From 36341693fab5ac961292c88cc4d8a03166ccc19a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 7 Apr 2026 21:56:16 +0200 Subject: [PATCH 19/20] nixVersions.nix_2_28: 2.28.5 -> 2.28.6 The 2.28.6 tarball already contains the mdbook 0.5 compatibility fix and the GHSA-g3g9-5vj6-r3gj sandbox escape fix, so drop the now-redundant downstream backports. --- pkgs/tools/package-management/nix/default.nix | 10 +- .../patches/ghsa-g3g9-5vj6-r3gj-2.28.patch | 126 ------------------ 2 files changed, 2 insertions(+), 134 deletions(-) delete mode 100644 pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.28.patch diff --git a/pkgs/tools/package-management/nix/default.nix b/pkgs/tools/package-management/nix/default.nix index 13a8a74e7a41..8da4c6b494fb 100644 --- a/pkgs/tools/package-management/nix/default.nix +++ b/pkgs/tools/package-management/nix/default.nix @@ -155,17 +155,11 @@ lib.makeExtensible ( ( { nix_2_28 = commonMeson { - version = "2.28.5"; - hash = "sha256-oIfAHxO+BCtHXJXLHBnsKkGl1Pw+Uuq1PwNxl+lZ+Oc="; + version = "2.28.6"; + hash = "sha256-jg2YDTFt8CY4kMg4ha3UK5C+mQY+Zg67nwNy+CmTk5w="; self_attribute_name = "nix_2_28"; patches = patches_common ++ [ - (fetchpatch2 { - name = "nix-2.28-14764-mdbook-0.5-support.patch"; - url = "https://github.com/NixOS/nix/commit/5a64138e862fe364e751c5c286e8db8c466aaee7.patch?full_index=1"; - hash = "sha256-vFv/D08x9urtoIE9wiC7Lln4Eq3sgNBwU7TBE1iyrfI="; - }) lowdown30PatchOld - ./patches/ghsa-g3g9-5vj6-r3gj-2.28.patch ]; }; diff --git a/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.28.patch b/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.28.patch deleted file mode 100644 index ae50b007a30b..000000000000 --- a/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-2.28.patch +++ /dev/null @@ -1,126 +0,0 @@ -From 32b09e0bfeb33434866610994d89f48da8a2bf41 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= -Date: Mon, 6 Apr 2026 16:49:13 +0200 -Subject: [PATCH] Fixes for GHSA-g3g9-5vj6-r3gj -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -Squashed commit of the following: - -commit 716dba6692c42e301a1e769e5eac02a4d6e63150 -Author: Sergei Zimmerman -Date: Fri Apr 3 00:21:31 2026 +0300 - - derivation-builder: Don't use copyFile for FOD output copying, put the output in a temporary directory in the store - -commit a3215e7c5c260fab5f2cb034c4df01dfa3b284e5 -Author: Sergei Zimmerman -Date: Fri Apr 3 00:21:21 2026 +0300 - - libstore: Make temporary in-store directory not world-readable - -Signed-off-by: Jörg Thalheim ---- - src/libstore/include/nix/store/local-store.hh | 2 ++ - src/libstore/local-store.cc | 5 +-- - .../unix/build/local-derivation-goal.cc | 36 ++++++++++++++----- - 3 files changed, 33 insertions(+), 10 deletions(-) - -diff --git a/src/libstore/include/nix/store/local-store.hh b/src/libstore/include/nix/store/local-store.hh -index 5893b7d8b..c9266f6b4 100644 ---- a/src/libstore/include/nix/store/local-store.hh -+++ b/src/libstore/include/nix/store/local-store.hh -@@ -408,6 +408,8 @@ private: - friend struct PathSubstitutionGoal; - friend struct SubstitutionGoal; - friend struct DerivationGoal; -+ /* Only used for createTempDirInStore. */ -+ friend class DerivationBuilderImpl; - }; - - } // namespace nix -diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc -index 63108fab4..ab3a0d034 100644 ---- a/src/libstore/local-store.cc -+++ b/src/libstore/local-store.cc -@@ -1311,8 +1311,9 @@ std::pair LocalStore::createTempDirInStore() - do { - /* There is a slight possibility that `tmpDir' gets deleted by - the GC between createTempDir() and when we acquire a lock on it. -- We'll repeat until 'tmpDir' exists and we've locked it. */ -- tmpDirFn = createTempDir(realStoreDir, "tmp"); -+ We'll repeat until 'tmpDir' exists and we've locked it. -+ Make the directory accessible only to the current user.*/ -+ tmpDirFn = createTempDir(realStoreDir, "tmp", /*mode=*/0700); - tmpDirFd = openDirectory(tmpDirFn); - if (!tmpDirFd) { - continue; -diff --git a/src/libstore/unix/build/local-derivation-goal.cc b/src/libstore/unix/build/local-derivation-goal.cc -index 88c82e063..3e4c3017d 100644 ---- a/src/libstore/unix/build/local-derivation-goal.cc -+++ b/src/libstore/unix/build/local-derivation-goal.cc -@@ -2547,6 +2547,13 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() - assert(output && scratchPath); - auto actualPath = toRealPathChroot(worker.store.printStorePath(*scratchPath)); - -+ /* An optional file descriptor of a directory used for intermediate -+ operations. */ -+ AutoCloseFD tempDirFd; -+ /* RAII cleanup of a temporary directory inside the store that is used -+ for intermediate operations. */ -+ std::optional delTempDir; -+ - auto finish = [&](StorePath finalStorePath) { - /* Store the final path */ - finalOutputs.insert_or_assign(outputName, finalStorePath); -@@ -2681,6 +2688,25 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() - return newInfo0; - }; - -+ auto moveOutputToTempDir = [&]() -> void { -+ std::filesystem::path tempDir; -+ std::tie(tempDir, tempDirFd) = getLocalStore().createTempDirInStore(); -+ delTempDir.emplace(tempDir); -+ -+ auto tmpOutput = tempDir / "x"; -+ -+ /* Serialise and create a fresh copy of the output to break -+ any stale writable file descriptors. Copy through the -+ serialisation/deserialisation. TODO: Use copyRecursive here and -+ make use of reflinking. */ -+ auto source = sinkToSource([&](Sink & nextSink) { dumpPath(actualPath, nextSink); }); -+ restorePath(tmpOutput, *source, settings.fsyncStorePaths); -+ /* This makes it slightly harder to make sense of the control flow. The rule -+ of thumb is that actualPath points to the current location of the stuff -+ that we'll end up registering. */ -+ actualPath = std::move(tmpOutput); -+ }; -+ - ValidPathInfo newInfo = std::visit( - overloaded{ - -@@ -2708,14 +2734,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() - - [&](const DerivationOutput::CAFixed & dof) { - auto & wanted = dof.ca.hash; -- -- // Replace the output by a fresh copy of itself to make sure -- // that there's no stale file descriptor pointing to it -- Path tmpOutput = actualPath + ".tmp"; -- copyFile(std::filesystem::path(actualPath), std::filesystem::path(tmpOutput), true); -- -- std::filesystem::rename(tmpOutput, actualPath); -- -+ moveOutputToTempDir(); - auto newInfo0 = newInfoFromCA( - DerivationOutput::CAFloating{ - .method = dof.ca.method, -@@ -2756,6 +2775,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() - }, - - [&](const DerivationOutput::Impure & doi) { -+ moveOutputToTempDir(); - return newInfoFromCA( - DerivationOutput::CAFloating{ - .method = doi.method, From d22a182395ccfc06d84b1b13a6ea660b0aeddb47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 7 Apr 2026 21:30:07 +0200 Subject: [PATCH 20/20] nixVersions.git: 2026-03-28 -> 2026-04-07 Master already contains the GHSA-g3g9-5vj6-r3gj sandbox escape fix and the landlock abstract socket hardening, so drop the now-redundant downstream backports. --- pkgs/tools/package-management/nix/default.nix | 14 +- .../nix/modular/src/nix/package.nix | 3 + .../nix/patches/ghsa-g3g9-5vj6-r3gj-git.patch | 126 ---------- ...ndlock-abstract-socket-hardening-git.patch | 231 ------------------ 4 files changed, 7 insertions(+), 367 deletions(-) delete mode 100644 pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-git.patch delete mode 100644 pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-git.patch diff --git a/pkgs/tools/package-management/nix/default.nix b/pkgs/tools/package-management/nix/default.nix index 8da4c6b494fb..0be4bdb9205a 100644 --- a/pkgs/tools/package-management/nix/default.nix +++ b/pkgs/tools/package-management/nix/default.nix @@ -213,23 +213,17 @@ lib.makeExtensible ( nixComponents_git = (nixDependencies.callPackage ./modular/packages.nix rec { - version = "2.35pre20260328_${lib.substring 0 8 src.rev}"; + version = "2.35pre20260407_${lib.substring 0 8 src.rev}"; inherit teams; otherSplices = generateSplicesForNixComponents "nixComponents_git"; src = fetchFromGitHub { owner = "NixOS"; repo = "nix"; - rev = "7edcd0a24dc71abb7caa600527833ef540c1bc86"; - hash = "sha256-fybp46IQmRN7lEUTChc3MTqxmRutmDO4RNSPEQfJQsQ="; + rev = "a37db9d249afd61a81ae26368696f60e065d6f61"; + hash = "sha256-RpfExg4DcWZ/SanVuwVbdijqPylsjvtMrHTQHemE+t8="; }; }).appendPatches - ( - patches_common - ++ [ - ./patches/ghsa-g3g9-5vj6-r3gj-git.patch - ./patches/landlock-abstract-socket-hardening-git.patch - ] - ); + patches_common; git = addTests "git" self.nixComponents_git.nix-everything; diff --git a/pkgs/tools/package-management/nix/modular/src/nix/package.nix b/pkgs/tools/package-management/nix/modular/src/nix/package.nix index d96823a991c7..deb49c9e7b4d 100644 --- a/pkgs/tools/package-management/nix/modular/src/nix/package.nix +++ b/pkgs/tools/package-management/nix/modular/src/nix/package.nix @@ -7,6 +7,8 @@ nix-main, nix-cmd, + mimalloc, + # Configuration Options version, @@ -23,6 +25,7 @@ mkMesonExecutable (finalAttrs: { nix-expr nix-main nix-cmd + mimalloc ]; mesonFlags = [ diff --git a/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-git.patch b/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-git.patch deleted file mode 100644 index 05fb52902018..000000000000 --- a/pkgs/tools/package-management/nix/patches/ghsa-g3g9-5vj6-r3gj-git.patch +++ /dev/null @@ -1,126 +0,0 @@ -From 0af4c1f88d0646bda0a90a37105360cab466a550 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= -Date: Mon, 6 Apr 2026 16:49:13 +0200 -Subject: [PATCH] Fixes for GHSA-g3g9-5vj6-r3gj -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -Squashed commit of the following: - -commit a760af86b3a42aa5ac9d9002929107fe357bf128 -Author: Sergei Zimmerman -Date: Fri Apr 3 00:21:31 2026 +0300 - - derivation-builder: Don't use copyFile for FOD output copying, put the output in a temporary directory in the store - -commit 0e3412a93f43a017342c267000c152e5c45327e7 -Author: Sergei Zimmerman -Date: Fri Apr 3 00:21:21 2026 +0300 - - libstore: Make temporary in-store directory not world-readable - -Signed-off-by: Jörg Thalheim ---- - src/libstore/include/nix/store/local-store.hh | 2 ++ - src/libstore/local-store.cc | 5 +-- - src/libstore/unix/build/derivation-builder.cc | 36 ++++++++++++++----- - 3 files changed, 33 insertions(+), 10 deletions(-) - -diff --git a/src/libstore/include/nix/store/local-store.hh b/src/libstore/include/nix/store/local-store.hh -index 63a1da67d..bf3437e95 100644 ---- a/src/libstore/include/nix/store/local-store.hh -+++ b/src/libstore/include/nix/store/local-store.hh -@@ -512,6 +512,8 @@ private: - - friend struct PathSubstitutionGoal; - friend struct DerivationGoal; -+ /* Only used for createTempDirInStore. */ -+ friend class DerivationBuilderImpl; - }; - - } // namespace nix -diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc -index e9eb48bfe..d5e457731 100644 ---- a/src/libstore/local-store.cc -+++ b/src/libstore/local-store.cc -@@ -1304,8 +1304,9 @@ std::pair LocalStore::createTempDirInStore() - do { - /* There is a slight possibility that `tmpDir' gets deleted by - the GC between createTempDir() and when we acquire a lock on it. -- We'll repeat until 'tmpDir' exists and we've locked it. */ -- tmpDirFn = createTempDir(std::filesystem::path{config->realStoreDir.get()}, "tmp"); -+ We'll repeat until 'tmpDir' exists and we've locked it. -+ Make the directory accessible only to the current user. */ -+ tmpDirFn = createTempDir(std::filesystem::path{config->realStoreDir.get()}, "tmp", /*mode=*/0700); - tmpDirFd = openDirectory(tmpDirFn, FinalSymlink::DontFollow); - if (!tmpDirFd) { - continue; -diff --git a/src/libstore/unix/build/derivation-builder.cc b/src/libstore/unix/build/derivation-builder.cc -index 8f6343e0f..8288a4a31 100644 ---- a/src/libstore/unix/build/derivation-builder.cc -+++ b/src/libstore/unix/build/derivation-builder.cc -@@ -1597,6 +1597,13 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() - assert(output && scratchPath); - auto actualPath = realPathInHost(store.printStorePath(*scratchPath)); - -+ /* An optional file descriptor of a directory used for intermediate -+ operations. */ -+ AutoCloseFD tempDirFd; -+ /* RAII cleanup of a temporary directory inside the store that is used -+ for intermediate operations. */ -+ AutoDelete delTempDir; -+ - auto finish = [&](StorePath finalStorePath) { - /* Store the final path */ - finalOutputs.insert_or_assign(outputName, finalStorePath); -@@ -1744,6 +1751,25 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() - return newInfo0; - }; - -+ auto moveOutputToTempDir = [&]() -> void { -+ std::filesystem::path tempDir; -+ std::tie(tempDir, tempDirFd) = store.createTempDirInStore(); -+ delTempDir = AutoDelete(tempDir); -+ -+ auto tmpOutput = tempDir / "x"; -+ -+ /* Serialise and create a fresh copy of the output to break -+ any stale writable file descriptors. Copy through the -+ serialisation/deserialisation. TODO: Use copyRecursive here and -+ make use of reflinking. */ -+ auto source = sinkToSource([&](Sink & nextSink) { dumpPath(actualPath, nextSink); }); -+ restorePath(tmpOutput, *source, store.config->getLocalSettings().fsyncStorePaths); -+ /* This makes it slightly harder to make sense of the control flow. The rule -+ of thumb is that actualPath points to the current location of the stuff -+ that we'll end up registering. */ -+ actualPath = std::move(tmpOutput); -+ }; -+ - ValidPathInfo newInfo = std::visit( - overloaded{ - -@@ -1771,14 +1797,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() - - [&](const DerivationOutput::CAFixed & dof) { - auto & wanted = dof.ca.hash; -- -- // Replace the output by a fresh copy of itself to make sure -- // that there's no stale file descriptor pointing to it -- std::filesystem::path tmpOutput = actualPath.native() + ".tmp"; -- copyFile(actualPath, tmpOutput, true); -- -- std::filesystem::rename(tmpOutput, actualPath); -- -+ moveOutputToTempDir(); - return newInfoFromCA( - DerivationOutput::CAFloating{ - .method = dof.ca.method, -@@ -1795,6 +1814,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() - }, - - [&](const DerivationOutput::Impure & doi) { -+ moveOutputToTempDir(); - return newInfoFromCA( - DerivationOutput::CAFloating{ - .method = doi.method, diff --git a/pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-git.patch b/pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-git.patch deleted file mode 100644 index 5b0fbdb46536..000000000000 --- a/pkgs/tools/package-management/nix/patches/landlock-abstract-socket-hardening-git.patch +++ /dev/null @@ -1,231 +0,0 @@ -From 44017ca497c8b44d5dac179f5afc63e91fe45ed6 Mon Sep 17 00:00:00 2001 -From: Sergei Zimmerman -Date: Sun, 5 Apr 2026 16:39:58 +0300 -Subject: [PATCH] libstore: Use landlock with - LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET for new enough kernels -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -This partially fixes the issue with cooperating processes being able -to communicate via abstract sockets. The fix is partial, because processes -outside the landlock domain of the sandboxed process can still connect to -a socket created by the FOD. There's no equivalent way of restricting inbound -connections. This closes the gap when there's no cooperating process on the host -(i.e. 2 separate FODs). - ->= 6.12 kernel is widespread enough (NixOS 25.11 ships it by -default) that we have no reason not to apply this hardening, even though -it's incomplete. - -ca-fd-leak test exercises this exact code path and now the smuggling -process fails with (on new enough kernels that have landlock support enabled): - -vm-test-run-ca-fd-leak> machine # sandbox setup: applied landlock sandboxing -vm-test-run-ca-fd-leak> machine # building '/nix/store/s7brgi6pdr5f3n8yqlgmdlz8blb89njc-smuggled.drv'... -vm-test-run-ca-fd-leak> machine # building derivation '/nix/store/s7brgi6pdr5f3n8yqlgmdlz8blb89njc-smuggled.drv': woken up -vm-test-run-ca-fd-leak> machine # connect: Operation not permitted -vm-test-run-ca-fd-leak> machine # sendmsg: Socket not connected - -Signed-off-by: Jörg Thalheim ---- - src/libstore/meson.build | 5 + - .../unix/build/linux-derivation-builder.cc | 101 ++++++++++++++++++ - tests/nixos/ca-fd-leak/default.nix | 8 +- - 3 files changed, 112 insertions(+), 2 deletions(-) - -diff --git a/src/libstore/meson.build b/src/libstore/meson.build -index 445798544..753c78687 100644 ---- a/src/libstore/meson.build -+++ b/src/libstore/meson.build -@@ -79,6 +79,11 @@ foreach funcspec : check_funcs - configdata_priv.set(define_name, define_value) - endforeach - -+if host_machine.system() == 'linux' -+ has_landlock = cxx.has_header('linux/landlock.h') -+ configdata_priv.set('HAVE_LANDLOCK', has_landlock.to_int()) -+endif -+ - has_acl_support = cxx.has_header('sys/xattr.h') \ - and cxx.has_function('llistxattr') \ - and cxx.has_function('lremovexattr') -diff --git a/src/libstore/unix/build/linux-derivation-builder.cc b/src/libstore/unix/build/linux-derivation-builder.cc -index 9cfd3cbcd..c71d23e15 100644 ---- a/src/libstore/unix/build/linux-derivation-builder.cc -+++ b/src/libstore/unix/build/linux-derivation-builder.cc -@@ -1,5 +1,7 @@ - #ifdef __linux__ - -+# include "store-config-private.hh" -+ - # include "nix/store/globals.hh" - # include "nix/store/personality.hh" - # include "nix/store/filetransfer.hh" -@@ -11,6 +13,8 @@ - - # include - # include -+# include -+ - # include - # include - # include -@@ -19,11 +23,16 @@ - # include - # include - # include -+# include - - # if HAVE_SECCOMP - # include - # endif - -+# if HAVE_LANDLOCK -+# include -+# endif -+ - # define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root, put_old)) - - namespace nix { -@@ -129,6 +138,77 @@ static void setupSeccomp(const LocalSettings & localSettings) - # endif - } - -+# if HAVE_LANDLOCK && defined(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET) -+ -+# define DO_LANDLOCK 1 -+ -+/* We are using LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET on best-effort basis. There are no glibc wrappers for now. */ -+ -+static int landlockCreateRuleset(const ::landlock_ruleset_attr * attr, std::size_t size, std::uint32_t flags) -+{ -+ return ::syscall(__NR_landlock_create_ruleset, attr, size, flags); -+} -+ -+static int landlockRestrictSelf(Descriptor rulesetFd, std::uint32_t flags) -+{ -+ return ::syscall(__NR_landlock_restrict_self, rulesetFd, flags); -+} -+ -+static int getLandlockAbiVersion() -+{ -+ int abiVersion = landlockCreateRuleset(nullptr, 0, LANDLOCK_CREATE_RULESET_VERSION); -+ return abiVersion; -+} -+ -+static void setupLandlock() -+{ -+ bool landlockSupportsScopeAbstractUnixSocket = []() { -+ int abiVersion = getLandlockAbiVersion(); -+ if (abiVersion >= 6) -+ /* All good, we can use LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET. See -+ https://docs.kernel.org/userspace-api/landlock.html#abstract-unix-socket-abi-6 */ -+ return true; -+ -+ if (abiVersion == -1) { -+ debug("landlock is not available"); -+ return false; -+ } -+ -+ debug("landlock version %d does not support LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", abiVersion); -+ return false; -+ }(); -+ -+ /* Bail out early if landlock is not enabled or LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET wouldn't work. -+ TODO: Consider adding more landlock rules for filesystem access as defense-in-depth on top. */ -+ if (!landlockSupportsScopeAbstractUnixSocket) -+ return; -+ -+ ::landlock_ruleset_attr attr = { -+ /* This prevents multiple FODs from communicating with each other -+ via abstract sockets. Note that cooperating processes outside the -+ sandbox can still connect to an abstract socket created by the FOD. To -+ mitigate that issue entirely we'd still need network namespaces. */ -+ .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, -+ }; -+ -+ /* This better not fail - if the kernel reports a new enough ABI version we -+ should treat any errors as fatal from now on. */ -+ AutoCloseFD rulesetFd = landlockCreateRuleset(&attr, sizeof(attr), 0); -+ if (!rulesetFd) -+ throw SysError("failed to create a landlock ruleset"); -+ -+ if (landlockRestrictSelf(rulesetFd.get(), 0) == -1) -+ throw SysError("failed to apply landlock"); -+ -+ debug("applied landlock sandboxing"); -+} -+ -+# else -+ -+# define DO_LANDLOCK 0 -+ -+# endif -+ - static void doBind(const std::filesystem::path & source, const std::filesystem::path & target, bool optional = false) - { - debug("bind mounting %1% to %2%", PathFmt(source), PathFmt(target)); -@@ -169,8 +249,27 @@ struct LinuxDerivationBuilder : virtual DerivationBuilderImpl - { - auto & localSettings = store.config->getLocalSettings(); - -+ /* Set the NO_NEW_PRIVS before doing seccomp/landlock setup. -+ landlock_restrict_self requires either NO_NEW_PRIVS or CAP_SYS_ADMIN. -+ With user namespaces we do get CAP_SYS_ADMIN. */ -+ if (!localSettings.allowNewPrivileges) -+ if (::prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) -+ throw SysError("failed to set PR_SET_NO_NEW_PRIVS"); -+ - setupSeccomp(localSettings); - -+# if DO_LANDLOCK -+ try { -+ setupLandlock(); -+ } catch (SysError & e) { -+ if (e.errNo != EPERM) -+ throw; -+ /* If allowNewPrivileges is true and we don't have CAP_SYS_ADMIN -+ this code path might be hit. */ -+ warn("setting up landlock: %s", e.message()); -+ } -+# endif -+ - linux::setPersonality({ - .system = drv.platform, - .impersonateLinux26 = localSettings.impersonateLinux26, -@@ -765,4 +864,6 @@ struct ChrootLinuxDerivationBuilder : ChrootDerivationBuilder, LinuxDerivationBu - - } // namespace nix - -+# undef DO_LANDLOCK -+ - #endif -diff --git a/tests/nixos/ca-fd-leak/default.nix b/tests/nixos/ca-fd-leak/default.nix -index 902aacdc6..dc944290f 100644 ---- a/tests/nixos/ca-fd-leak/default.nix -+++ b/tests/nixos/ca-fd-leak/default.nix -@@ -78,7 +78,7 @@ in - - # Build the smuggled derivation. - # This will connect to the smuggler server and send it the file descriptor -- machine.succeed(r""" -+ sender_output = machine.succeed(r""" - nix-build -E ' - builtins.derivation { - name = "smuggled"; -@@ -89,9 +89,13 @@ in - outputHash = builtins.hashString "sha256" "hello, world\n"; - builder = "${pkgs.busybox-sandbox-shell}/bin/sh"; - args = [ "-c" "echo \"hello, world\" > $out; ''${${sender}} ${socketName}" ]; -- }' -+ }' 2>&1 - """.strip()) - -+ # Landlock's LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET prevents a sandboxed process -+ # from connecting to an abstract socket created in an unrelated landlock domain. -+ # There's no such flag for preventing inbound connections. -+ assert "connect: Operation not permitted" in sender_output - - # Tell the smuggler server that we're done - machine.execute("echo done | ${pkgs.socat}/bin/socat - ABSTRACT-CONNECT:${socketName}")