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}")