Compare commits

..
23 Commits
Author SHA1 Message Date
Raito Bezarius 3a8c42c1bf release: 2.91.3 "Dragon's Breath"
Release produced with releng/create_release.xsh

Change-Id: I68ab3b251290a9ca8b98866d4de816787a551496
2025-06-30 00:03:53 +02:00
Raito Bezarius 1bb22839d3 release: release notes for 2.91.3
Release created with releng/create_release.xsh

Change-Id: I934446f3119a091f27350d4f47a3f0c8ef84c97b
2025-06-30 00:03:47 +02:00
Raito Bezarius 008f44beff version: 2.91.2 -> 2.91.3
Resolves critical correctness bugs as per fj#883.

These bugs were introduced in the previous release for the CVE bugfixes.

Change-Id: I4a9f82e1fbf87403d67450e79478a711a587f07d
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-29 21:44:57 +02:00
EmilyandRaito Bezarius 49c27760c1 libstore: fix Unix sockets in the build directory on sandboxed macOS
We’re already allowing `/tmp` anyway, so this should be harmless,
and it fixes a regression in the default configuration caused by
moving the build directories out of `temp-dir`. (For instance, that
broke the Lix `guessOrInventPath.sockets` test.)

Note that removing `/tmp` breaks quite a few builds, so although it may
be a good idea in general it would require work on the Nixpkgs side.

Fixes: 749afbbe99
Change-Id: I6a6a69645f429bc50d4cb24283feda3d3091f534
(cherry picked from commit d1db3e5fa3)
2025-06-29 21:44:57 +02:00
Raito Bezarius 9638c4cd92 libstore: fallback on creating a safe space in the default tempdir
If `settings.buildDir` cannot be written to, because we are in a chroot
store, unprivileged or anything.

We can and should always gracefully fallback to a *secure* location
inside of /tmp, i.e. `/tmp/<a directory under 0700>/<our temporary
directory for build under 0700>/...`.

This does not reintroduce CVE-2025-52991 because we are creating a
directory in-between compared to creating only ONE level of directory.

Under macOS, the first level of directory has actually mode 0755 instead
of 0700 as macOS often do not possess the right primitives to chroot
inside of these directories, leading to
https://github.com/NixOS/nix/pull/11031.

Thanks to Emily for the heads-up on this type of matter.

Fixes #876.

Change-Id: Ie521202923f763225e1901ab1b9b6c6132aaf548
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-29 21:44:57 +02:00
eldritch horrorsandRaito Bezarius 92cc6193e0 Revert "libstore/build: automatic clean up of unsuccessfully built scratch outputs"
This reverts commit a2189bcb2e as this is the root cause of the critical correctness bug.

Change-Id: Ic85d3d670dc9488d49f03988fe42feb0cf0e7084
2025-06-29 21:44:57 +02:00
eldritch horrorsandRaito Bezarius d227ad324e Revert "libstore: fix scratch output cleanup"
This reverts commit 779e795732 as this is an insufficient fix for the critical correctness bug.

Change-Id: I5681c9f25537e01892e4c9e7feff2cee8b6edb25
2025-06-29 19:05:14 +00:00
eldritch horrorsandRaito Bezarius 2310539e66 Revert "libstore: don't delete already valid outputs after build"
This reverts commit 4ef56601b5 as this is an insufficient fix for the critical correctness bug.

Change-Id: I7885c437ce4df25002d92654312b3b1bae53bfa3
2025-06-29 19:04:43 +00:00
Raito Bezariusandeldritch horrors 3d446ea37e releng: move to a non-official release
This is required for releng to cut a new release.

This is suboptimal releng as the script should probably do it itself and
merge the releng branch.

As we are in a hurry, we will skip this.

Change-Id: I54fccb11938adefc8685d04247e16401135dad8d
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-29 20:39:29 +02:00
eldritch horrors 4ef56601b5 libstore: don't delete already valid outputs after build
eagerly consider outputs as not needing deletion during output
registration rather than only doing so after registration. not
waiting for registration to succeed may keep store paths alive
in the file system if registration fails for some reason; that
seem preferrable to the possibility of having another instance
of this bug. since we only leave *good* outputs around there's
not much to worry about except maybe bit of wasted disk space.

fixes #883

Change-Id: I8c22c92e39b9e203f1061278f86cde19dc4474a4
2025-06-27 16:37:30 +02:00
eldritch horrors 779e795732 libstore: fix scratch output cleanup
the daemon must use real store paths, not virtual store paths. using
virtual paths may inadvertently delete paths in the system nix store
when a build was run on a redirected store as root, which isn't good

Change-Id: Id048b236bda0e0ab1f3be6ccba0ddc1de2a3e941
2025-06-27 16:37:08 +02:00
Raito Bezarius 151cb75236 release: 2.91.2 "Dragon's Breath"
Release produced with releng/create_release.xsh

Change-Id: Iaae75be51d51a553c29720e00f98565d314783bb
2025-06-24 13:12:01 +00:00
Raito Bezarius 17df7e0e35 release: release notes for 2.91.2
Release created with releng/create_release.xsh

Change-Id: Idf78ea5315a8e6c3f44085605f2b2884754783ea
2025-06-24 13:12:01 +00:00
Raito Bezarius f67a99bdd3 version: 2.91.1 -> 2.91.2
Fixes CVE-2025-46415, CVE-2025-46416, CVE-2025-52991, CVE-2025-52992,
and CVE-2025-52993.

Change-Id: Ie1d97a4e1569bf3b601753665b20cdeb5427e5ca
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-24 13:12:01 +00:00
Raito Bezarius d33062fd47 flake: bump nix2container
skopeo had a patch that doesn't apply anymore.

That's unfortunate. We force bump nix2container to resume releng
building.

Change-Id: Ic04d962bd444fd5619de6e0842d3535f8bd1b296
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-24 13:12:01 +00:00
Raito Bezarius a2189bcb2e libstore/build: automatic clean up of unsuccessfully built scratch outputs
When a build fails, its scratch output paths are not cleaned up.

Until recently, this was deemed not a problem but as part of the effort
to harden the Nix builds and protect these paths against being part of a
staged attack (race conditions, etc.), we automatically cleanup after
failed builds.

Fixes CVE-2025-52992.

Change-Id: I58481b1cc83826298b9d80d37fecf81f117ccb09
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-24 10:50:21 +00:00
eldritch horrorsandRaito Bezarius 91f526e739 libstore: don't default build-dir to temp-dir
if a build directory is accessible to other users it is possible to
smuggle data in and out of build directories. usually this ins only
a build purity problem, but in combination with other issues it can
be used to break out of a build sandbox. to prevent this we default
to using a subdirectory of nixStateDir (which is more restrictive).

Fixes CVE-2025-52991.

Change-Id: Iacfc9b50534de158618c815f9fb99d7dae1be4d0
2025-06-24 10:50:21 +00:00
9c2d05997d libstore: use pasta for FODs if available
This allows using a userspace program, pasta, to handle comms between
the build sandbox, and the outside world; allowing for full isolation
including the network namespace, closing the "fixed-output derivation
talks to the host over an abstract domain socket" hole for good.

Fixes CVE-2025-46416.

Co-Authored-By: Puck Meerburg <puck@puckipedia.com>
Change-Id: Ifd499b7dbb3784600a6e842fede65fc031ff9f15
2025-06-24 10:50:21 +00:00
eldritch horrorsandRaito Bezarius 83bba3f12c libutil: add capability support to runProgram2
launching pasta to not run as root will ambient require capabilities.

Change-Id: I1dd2506a1fa3944a9d9062123ef8a74903c597ea
2025-06-24 10:50:21 +00:00
eldritch horrorsandRaito Bezarius de6629754d libutil: add generic redirections runProgram2
explicit stderr redirection makes mergeStderrToStdout unnecessary also.

Change-Id: I63de929e6dc53f6c5ceb2d43c2ce288bfc04d872
2025-06-24 10:50:21 +00:00
eldritch horrorsandRaito Bezarius faa50e15c8 libutil: make RunningProgram more useful
make it moveable, make it killable, and add a stdout fd accessor.

Change-Id: I2387cbe8ac67b899a322cd6c7d306ef9ea7abcd0
2025-06-24 10:50:21 +00:00
Raito Bezarius 18a50a1734 libutil: ensure that _deletePath does NOT use absolute paths with dirfds
When calling `_deletePath` with a parent file descriptor, `openat` is
made effective by using relative paths to the directory file descriptor.

To avoid the problem, the signature is changed to resist misuse with an
assert in the prologue of the function.

Fixes CVE-2025-46415.

Change-Id: I6b3fc766bad2afe54dc27d47d1df3873e188de96
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-24 10:50:21 +00:00
Raito Bezarius d27c247715 libstore: ensure that passAsFile is created in the original temp dir
This ensures that `passAsFile` data is created inside the expected
temporary build directory by `openat()` from the parent directory file
descriptor.

Fixes CVE-2025-52993.

Change-Id: Ie5273446c4a19403088d0389ae8e3f473af8879a
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-06-24 10:46:21 +00:00
9 changed files with 116 additions and 98 deletions
+55
View File
@@ -1,4 +1,59 @@
# Lix 2.91 "Dragon's Breath" (2024-08-12)
# Lix 2.91.3 (2025-06-30)
## Fixes
- Revert CVE-2025-52992 failed mitigation [fj#883](https://git.lix.systems/lix-project/lix/issues/883) [fj#887](https://git.lix.systems/lix-project/lix/issues/887) [cl/3420](https://gerrit.lix.systems/c/lix/+/3420) [cl/3524](https://gerrit.lix.systems/c/lix/+/3524) [cl/3523](https://gerrit.lix.systems/c/lix/+/3523) [cl/3522](https://gerrit.lix.systems/c/lix/+/3522)
Following the initial mitigation of **CVE-2025-52992** in `cl/3420`, we
received reports of **unexpected deletion of in-use store paths**.
Upon investigation, we found that the patch did **not correctly cancel all
automatic deleters**, resulting in potentially critical path loss during normal
operation.
Given the severity and time-sensitive nature of the situation ([see incident
report](https://lix.systems/blog/2025-06-27-lix-critical-bug/)), we evaluated
possible options to repair the behavior safely. However, we concluded that a
rushed fix would either:
* **Overdelete**, i.e. breaking running systems, or,
* **Underdelete**, effectively **reopening CVE-2025-52992** while leaving
orphaned paths behind.
As **CVE-2025-52992 has no known exploit vector**, and correctness is critical
in the Lix project, we have **fully reverted the previous mitigations**.
Moving forward, the Lix team will rework this code path in a **long-term,
correctness-first fix** on the main branch. We will explore backporting it to
stable channels once its safety is assured.
We are deeply sorry for the stability incident and the Lix team remain
available for assisting you in recovering your systems.
Many thanks to [Raito Bezarius](https://git.lix.systems/raito) and [eldritch horrors](https://git.lix.systems/pennae) for this.
- Fallback to safe temp dir when build-dir is unwritable [fj#876](https://git.lix.systems/lix-project/lix/issues/876) [cl/3502](https://gerrit.lix.systems/c/lix/+/3502)
Non-daemon builds started failing with a permission error after introducing the `build-dir` option:
```
$ nix build --store ~/scratch nixpkgs#hello --rebuild
error: creating directory '/nix/var/nix/builds/nix-build-hello-2.12.2.drv-0': Permission denied
```
This happens because:
1. These builds are not run via the daemon, which owns `/nix/var/nix/builds`.
2. The user lacks permissions for that path.
We considered making `build-dir` a store-level option and defaulting it to `<chroot-root>/nix/var/nix/builds` for chroot stores, but opted instead for a fallback: if the default fails, Nix now creates a safe build directory under `/tmp`.
To avoid CVE-2025-52991, the fallback uses an extra path component between `/tmp` and the build dir.
**Note**: this fallback clutters `/tmp` with build directories that are not cleaned up. To prevent this, explicitly set `build-dir` to a path managed by Lix, even for local workloads.
Many thanks to [Raito Bezarius](https://git.lix.systems/raito) and [eldritch horrors](https://git.lix.systems/pennae) for this.
# Lix 2.91.2 (2025-06-23)
## Breaking Changes
+48 -45
View File
@@ -363,13 +363,9 @@ void LocalDerivationGoal::cleanupPostOutputsRegisteredModeCheck()
void LocalDerivationGoal::cleanupPostOutputsRegisteredModeNonCheck()
{
/* In the past, redirected outputs were manually tracked for deletion.
* Now that we have the scratch outputs cleaner which are a superset of
* redirected outputs, we just fire all uncancelled automatic deleters now.
*
* This should clean up any paths that IS NOT registered in the database.
*/
scratchOutputsCleaner.clear();
/* Delete unused redirected outputs (when doing hash rewriting). */
for (auto & i : redirectedOutputs)
deletePath(worker.store.Store::toRealPath(i.second));
/* Delete the chroot (if we were using one). */
autoDelChroot.reset(); /* this runs the destructor */
@@ -479,17 +475,47 @@ void LocalDerivationGoal::startBuilder()
settings.thisSystem,
concatStringsSep<StringSet>(", ", worker.store.systemFeatures));
createDirs(settings.buildDir.get());
try {
auto buildDir = worker.buildDirOverride.value_or(settings.buildDir.get());
/* Create a temporary directory where the build will take
place. */
tmpDir = createTempDir(
settings.buildDir.get(),
"nix-build-" + std::string(drvPath.name()),
false,
false,
0700
);
createDirs(buildDir);
/* Create a temporary directory where the build will take
place. */
tmpDir =
createTempDir(buildDir, "nix-build-" + std::string(drvPath.name()), false, false, 0700);
} catch (SysError & e) {
/*
* Fallback to the global tmpdir and create a safe space there
* only if it's a permission error.
*/
if (e.errNo != EACCES) {
throw;
}
auto globalTmp = defaultTempDir();
createDirs(globalTmp);
#if __APPLE__
/* macOS filesystem namespacing does not exist, to avoid breaking builds, we need to weaken
* the mode bits on the top-level directory. This avoids issues like
* https://github.com/NixOS/nix/pull/11031. */
constexpr int toplevelDirMode = 0755;
#else
constexpr int toplevelDirMode = 0700;
#endif
auto nixBuildsTmp =
createTempDir(globalTmp, fmt("nix-builds-%s", geteuid()), false, false, toplevelDirMode);
warn(
"Failed to use the system-wide build directory '%s', falling back to a temporary "
"directory inside '%s'",
settings.buildDir.get(),
nixBuildsTmp
);
worker.buildDirOverride = nixBuildsTmp;
tmpDir = createTempDir(
nixBuildsTmp, "nix-build-" + std::string(drvPath.name()), false, false, 0700
);
}
/* The TOCTOU between the previous mkdir call and this open call is unavoidable due to
* POSIX semantics.*/
tmpDirFd = AutoCloseFD{open(tmpDir.c_str(), O_RDONLY | O_NOFOLLOW | O_DIRECTORY)};
@@ -527,10 +553,6 @@ void LocalDerivationGoal::startBuilder()
to use a temporary path */
makeFallbackPath(status.known->path);
scratchOutputs.insert_or_assign(outputName, scratchPath);
/* Schedule this scratch output path for automatic deletion
* if we do not cancel it, e.g. when registering the outputs.
*/
scratchOutputsCleaner.insert_or_assign(outputName, worker.store.printStorePath(scratchPath));
/* Substitute output placeholders with the scratch output paths.
We'll use during the build. */
@@ -553,6 +575,8 @@ void LocalDerivationGoal::startBuilder()
std::string h2 { scratchPath.hashPart() };
inputRewrites[h1] = h2;
}
redirectedOutputs.insert_or_assign(std::move(fixedFinalPath), std::move(scratchPath));
}
/* Construct the environment passed to the builder. */
@@ -1846,6 +1870,8 @@ void LocalDerivationGoal::runChild()
if (getEnv("_NIX_TEST_NO_SANDBOX") != "1") {
Strings sandboxArgs;
sandboxArgs.push_back("_NIX_BUILD_TOP");
sandboxArgs.push_back(tmpDir);
sandboxArgs.push_back("_GLOBAL_TMP_DIR");
sandboxArgs.push_back(globalTmpDir);
if (allowLocalNetworking) {
@@ -2382,10 +2408,6 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs()
localStore.registerValidPaths({{oldInfo.path, oldInfo}});
}
/* Don't register anything, since we already have the
previous versions which we're comparing.
NOTE: this means that the `.check` path will be automatically deleted.
*/
continue;
}
@@ -2409,13 +2431,8 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs()
/* If it's a CA path, register it right away. This is necessary if it
isn't statically known so that we can safely unlock the path before
the next iteration */
if (newInfo.ca) {
if (newInfo.ca)
localStore.registerValidPaths({{newInfo.path, newInfo}});
/* Cancel automatic deletion of that output if it was a scratch output. */
if (auto cleaner = scratchOutputsCleaner.extract(outputName)) {
cleaner.mapped().cancel();
}
}
infos.emplace(outputName, std::move(newInfo));
}
@@ -2443,13 +2460,6 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs()
infos2.insert_or_assign(newInfo.path, newInfo);
}
localStore.registerValidPaths(infos2);
/* Cancel automatic deletion of that output if it was a scratch output that we just registered. */
for (auto & [outputName, _ ] : infos) {
if (auto cleaner = scratchOutputsCleaner.extract(outputName)) {
cleaner.mapped().cancel();
}
}
}
/* In case of a fixed-output derivation hash mismatch, throw an
@@ -2483,13 +2493,6 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs()
builtOutputs.emplace(outputName, thisRealisation);
}
/* NOTE: At this point, all outputs MAY NOT have been registered.
* Therefore, there may remains auto-deleters pending in the cleaner list (`scratchOutputsCleaner`).
*
* They will be finally deleted but we have no way to assert they all have been, e.g.
* `assert(scratchOutputsCleaner.size() == 0)` cannot be written.
*/
return builtOutputs;
}
+2 -13
View File
@@ -107,6 +107,8 @@ struct LocalDerivationGoal : public DerivationGoal
* Hash rewriting.
*/
StringMap inputRewrites, outputRewrites;
typedef map<StorePath, StorePath> RedirectedOutputs;
RedirectedOutputs redirectedOutputs;
/**
* The outputs paths used during the build.
@@ -123,19 +125,6 @@ struct LocalDerivationGoal : public DerivationGoal
* self-references.
*/
OutputPathMap scratchOutputs;
/**
* Output paths used during the build are scheduled for
* automatic cleanup unless they have been successfully built.
*
* `registerOutputs` take care of cancelling the cleanups
* and clearing this vector.
*
* `startBuilder` take care of filling this vector
* as `scratchOutputs` gets filled.
*
* This is a map from output names to automatic delete handles.
*/
std::map<std::string, AutoDelete> scratchOutputsCleaner;
/**
* Path registration info from the previous round, if we're
+4 -2
View File
@@ -26,12 +26,14 @@ R""(
; Allow getpwuid.
(allow mach-lookup (global-name "com.apple.system.opendirectoryd.libinfo"))
; Access to /tmp.
; Access to /tmp and the build directory.
; The network-outbound/network-inbound ones are for unix domain sockets, which
; we allow access to in TMPDIR (but if we allow them more broadly, you could in
; theory escape the sandbox)
(allow file* process-exec network-outbound network-inbound
(literal "/tmp") (subpath TMPDIR))
(literal "/tmp")
(subpath TMPDIR)
(subpath (param "_NIX_BUILD_TOP")))
; Some packages like to read the system version.
(allow file-read*
+1
View File
@@ -161,6 +161,7 @@ public:
Store & store;
Store & evalStore;
std::optional<Path> buildDirOverride;
struct HookState {
std::unique_ptr<HookInstance> instance;
+5
View File
@@ -170,3 +170,8 @@ test "$(<<<"$out" grep -E '^error:' | wc -l)" = 3
<<<"$out" grepQuiet -E "error: 2 dependencies of derivation '.*-x4\\.drv' failed to build"
<<<"$out" grepQuiet -vE "hash mismatch in fixed-output derivation '.*-x3\\.drv'"
<<<"$out" grepQuiet -vE "hash mismatch in fixed-output derivation '.*-x2\\.drv'"
# Ensure when if the system build dir is inaccessible, we can still build things
BUILD_DIR=$(mktemp -d)
chmod 0000 "$BUILD_DIR"
nix --build-dir "$BUILD_DIR" build -E 'with import ./config.nix; mkDerivation { name = "test"; buildCommand = "echo rawr > $out"; }' --impure --no-link
-3
View File
@@ -146,9 +146,6 @@ in
symlinkResolvconf = runNixOSTestFor "x86_64-linux" ./symlink-resolvconf.nix;
# Use this test to test things that cannot easily be tested under chroot Nix stores in functional test suite.
non-chroot-misc = runNixOSTestFor "x86_64-linux" ./non-chroot-misc;
noNewPrivilegesInSandbox = runNixOSTestFor "x86_64-linux" ./no-new-privileges/sandbox.nix;
noNewPrivilegesOutsideSandbox = runNixOSTestFor "x86_64-linux" ./no-new-privileges/no-sandbox.nix;
-34
View File
@@ -1,34 +0,0 @@
{ ... }:
# Misc things we want to test inside of a non redirected, non chroot Nix store.
let
nonAutoCleaningFailingDerivationCode = ''
derivation {
name = "scratch-failing";
system = builtins.currentSystem;
builder = "/bin/sh";
args = [ (builtins.toFile "builder.sh" "echo bonjour > $out; echo out: $out; false") ];
}
'';
in
{
name = "non-chroot-sandbox-misc";
nodes.machine = {
};
testScript = { nodes }: ''
import re
start_all()
# You might ask yourself why write such a convoluted thing?
# The condition for fooling Nix into NOT cleaning up the output path are non trivial and unclear.
# This is one of those: create a derivation, mkdir or touch the $out path, communicate it back.
# Even with a sandboxed Lix, you will observe leftovers before 2.93.0. After this version, this test passes.
result = machine.fail("""nix-build --substituters "" -E '${nonAutoCleaningFailingDerivationCode}' 2>&1""")
match = re.search(r'out: (\S+)', result)
assert match is not None, "Did not find Nix store path in the result of the failing build"
outpath = match.group(1).strip()
print(f"Found Nix store path: {outpath}")
machine.fail(f'stat {outpath}')
'';
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"version": "2.91.2",
"version": "2.91.3",
"official_release": true,
"release_name": "Dragon's Breath"
}