Commit d22a1823 authored by Jörg Thalheim's avatar Jörg Thalheim
Browse files

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.
parent 36341693
Loading
Loading
Loading
Loading
+4 −10
Original line number Diff line number Diff line
@@ -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;

+3 −0
Original line number Diff line number Diff line
@@ -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 = [
+0 −126
Original line number Diff line number Diff line
From 0af4c1f88d0646bda0a90a37105360cab466a550 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= <joerg@thalheim.io>
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 <sergei@zimmerman.foo>
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 <sergei@zimmerman.foo>
Date:   Fri Apr 3 00:21:21 2026 +0300

    libstore: Make temporary in-store directory not world-readable

Signed-off-by: Jörg Thalheim <joerg@thalheim.io>
---
 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<std::filesystem::path, AutoCloseFD> 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,
+0 −231
Original line number Diff line number Diff line
From 44017ca497c8b44d5dac179f5afc63e91fe45ed6 Mon Sep 17 00:00:00 2001
From: Sergei Zimmerman <sergei@zimmerman.foo>
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 <joerg@thalheim.io>
---
 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 <algorithm>
 #  include <string_view>
+#  include <cstdint>
+
 #  include <sys/ioctl.h>
 #  include <net/if.h>
 #  include <netinet/ip.h>
@@ -19,11 +23,16 @@
 #  include <sys/param.h>
 #  include <sys/mount.h>
 #  include <sys/syscall.h>
+#  include <sys/prctl.h>
 
 #  if HAVE_SECCOMP
 #    include <seccomp.h>
 #  endif
 
+#  if HAVE_LANDLOCK
+#    include <linux/landlock.h>
+#  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}")