diff --git a/pkgs/tools/package-management/nix/default.nix b/pkgs/tools/package-management/nix/default.nix index bbb45c9d419d..4460014ad689 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 ]; }; @@ -189,6 +190,7 @@ lib.makeExtensible ( hash = "sha256-r2ZF1zBZDKMvyX6X4VsaTMrg0zdjn59Jf6Hqg56r29E="; }) lowdown30PatchOld + ./patches/ghsa-g3g9-5vj6-r3gj-2.30.patch ] ); @@ -208,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; @@ -224,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; @@ -240,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-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, 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, 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/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/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-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}") 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}") 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}")