Nix: bump to latest stable releases (#507699)

This commit is contained in:
Jörg Thalheim
2026-04-07 20:47:39 +00:00
committed by GitHub
11 changed files with 24 additions and 1381 deletions
@@ -1,8 +1,8 @@
{
x86_64-linux = "/nix/store/mxn3vxpvk2b42kgd08aw4bn27qhf434w-nix-2.31.3";
i686-linux = "/nix/store/b7d35ifww9683l74ydkb0qhrq6lqcisi-nix-2.31.3";
aarch64-linux = "/nix/store/ysz7dwmy4zd1zm3wzx5dh9g999xv7pbm-nix-2.31.3";
riscv64-linux = "/nix/store/avfv2lqbnphj2ap8y5ihg1l0sqhpjll7-nix-riscv64-unknown-linux-gnu-2.31.3";
x86_64-darwin = "/nix/store/s8lcl6nrd5ia7nr4zx9mg720i9f8qm37-nix-2.31.3";
aarch64-darwin = "/nix/store/vfjzbcl3kf9jjwh0g2x03cvz2x5hg8py-nix-2.31.3";
x86_64-linux = "/nix/store/vals1fs2rl6yn5f8gbqj9mvly4r27shs-nix-2.31.4";
i686-linux = "/nix/store/fyrlz8cdzvf5csdh5885wifpxc8ywdii-nix-2.31.4";
aarch64-linux = "/nix/store/19p3nc892m7idfg2ngd1614660xqbhnm-nix-2.31.4";
riscv64-linux = "/nix/store/x1isvq0xnyrg0l29qk2xlp929cgjsmqy-nix-riscv64-unknown-linux-gnu-2.31.4";
x86_64-darwin = "/nix/store/4gqxzd5zkxcq271wi5saml4zd92rdkws-nix-2.31.4";
aarch64-darwin = "/nix/store/r3gz609kdqchxcmil7dhbravbq8kwm93-nix-2.31.4";
}
+15 -48
View File
@@ -155,108 +155,75 @@ lib.makeExtensible (
(
{
nix_2_28 = commonMeson {
version = "2.28.5";
hash = "sha256-oIfAHxO+BCtHXJXLHBnsKkGl1Pw+Uuq1PwNxl+lZ+Oc=";
version = "2.28.6";
hash = "sha256-jg2YDTFt8CY4kMg4ha3UK5C+mQY+Zg67nwNy+CmTk5w=";
self_attribute_name = "nix_2_28";
patches = patches_common ++ [
(fetchpatch2 {
name = "nix-2.28-14764-mdbook-0.5-support.patch";
url = "https://github.com/NixOS/nix/commit/5a64138e862fe364e751c5c286e8db8c466aaee7.patch?full_index=1";
hash = "sha256-vFv/D08x9urtoIE9wiC7Lln4Eq3sgNBwU7TBE1iyrfI=";
})
lowdown30PatchOld
./patches/ghsa-g3g9-5vj6-r3gj-2.28.patch
];
};
nixComponents_2_30 =
(nixDependencies.callPackage ./modular/packages.nix rec {
version = "2.30.3";
version = "2.30.4";
inherit teams;
otherSplices = generateSplicesForNixComponents "nixComponents_2_30";
src = fetchFromGitHub {
owner = "NixOS";
repo = "nix";
tag = version;
hash = "sha256-kBuwzMgIE9Tmve0Rpp+q+YCsE2mw9d62M/950ViWeJ0=";
hash = "sha256-cJ96IBZCYoX0Tdlo5Q7qDSAKfL6QcUq/4Kr1UplH50E=";
};
}).appendPatches
(
patches_common
++ [
(fetchpatch2 {
name = "nix-2.30-14695-mdbook-0.5-support.patch";
url = "https://github.com/NixOS/nix/commit/5cbd7856de0a9c13351f98e32a1e26d0854d87fd.patch?full_index=1";
hash = "sha256-r2ZF1zBZDKMvyX6X4VsaTMrg0zdjn59Jf6Hqg56r29E=";
})
lowdown30PatchOld
./patches/ghsa-g3g9-5vj6-r3gj-2.30.patch
]
);
(patches_common ++ [ lowdown30PatchOld ]);
nix_2_30 = addTests "nix_2_30" self.nixComponents_2_30.nix-everything;
nixComponents_2_31 =
(nixDependencies.callPackage ./modular/packages.nix rec {
version = "2.31.3";
version = "2.31.4";
inherit teams;
otherSplices = generateSplicesForNixComponents "nixComponents_2_31";
src = fetchFromGitHub {
owner = "NixOS";
repo = "nix";
tag = version;
hash = "sha256-oe0YWe8f+pwQH4aYD2XXLW5iEHyXNUddurqJ5CUVCIk=";
hash = "sha256-f/haYfcI+9IiYVH+g6cjhF8cK7QWHAFfcPtF+57ujZ0=";
};
}).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;
nixComponents_2_34 =
(nixDependencies.callPackage ./modular/packages.nix rec {
version = "2.34.4";
version = "2.34.5";
inherit teams;
otherSplices = generateSplicesForNixComponents "nixComponents_2_34";
src = fetchFromGitHub {
owner = "NixOS";
repo = "nix";
tag = version;
hash = "sha256-WPuGqMQGepXoRYjtRudMAMHEoLsIObw2x4sVfho5feA=";
hash = "sha256-/S2bnz+TbRFGmNyR31Hfa70uFvJoMM9wYDjpyEw8I+U=";
};
}).appendPatches
(
patches_common
++ [
./patches/ghsa-g3g9-5vj6-r3gj-2.34.patch
./patches/landlock-abstract-socket-hardening-2.34.patch
]
);
patches_common;
nix_2_34 = addTests "nix_2_34" self.nixComponents_2_34.nix-everything;
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 32b09e0bfeb33434866610994d89f48da8a2bf41 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 716dba6692c42e301a1e769e5eac02a4d6e63150
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 a3215e7c5c260fab5f2cb034c4df01dfa3b284e5
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 +--
.../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<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(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<AutoDelete> 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,
@@ -1,126 +0,0 @@
From 4d0a078f1dae9a07d04e1a72e7e62fbf2ca249e0 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 a8e5b27728e3b18e63c2503a83afd04031994623
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 49ff4d6779ec20c5339b237dec9b240c3eabf535
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 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<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(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<AutoDelete> 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,
@@ -1,126 +0,0 @@
From df0153a9eca42c4ed5ac784657c8c5d0664c9e0e 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 bf1d95ae399e47daeafa9e0dfac967f415c52447
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 ca5986fa353d402738acf961bcaa017c61019809
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 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<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(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<AutoDelete> 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,
@@ -1,126 +0,0 @@
From dfda33d358f4522450737c40407ba8d8403a86a1 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 d1f2de7d683bef1e2a164f23f33172c79c7725ea
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 2f13ee6dc4ade1d919c14757891d5d8a1001eee8
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 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<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);
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,
@@ -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,232 +0,0 @@
From b743f908b10aa56b88fb490de8a0d893bd2d83d5 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
(cherry picked from commit 44017ca497c8b44d5dac179f5afc63e91fe45ed6)
Signed-off-by: Jörg Thalheim <joerg@thalheim.io>
---
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 <algorithm>
+# include <string_view>
+# include <cstdint>
+
# include <sys/ioctl.h>
# include <net/if.h>
# include <netinet/ip.h>
@@ -13,11 +19,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 {
@@ -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}")
@@ -1,234 +0,0 @@
From 23c7a6490a829a978af1acd56d76cc2dee7f4ad6 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
(cherry picked from commit 44017ca497c8b44d5dac179f5afc63e91fe45ed6)
Signed-off-by: Jörg Thalheim <joerg@thalheim.io>
---
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 <algorithm>
+# include <string_view>
+# include <cstdint>
+
# include <sys/ioctl.h>
# include <net/if.h>
# include <netinet/ip.h>
@@ -17,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 {
@@ -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}")
@@ -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}")