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:
@@ -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,
|
||||
-231
@@ -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}")
|
||||
Reference in New Issue
Block a user