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.
This commit is contained in:
Jörg Thalheim
2026-04-07 21:30:07 +02:00
parent 36341693fa
commit d22a182395
4 changed files with 7 additions and 367 deletions
+4 -10
View File
@@ -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;
@@ -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 = [
@@ -1,126 +0,0 @@
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,
@@ -1,231 +0,0 @@
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}")