Security researchers have found five security issues in Lix. These issues were assigned CVE numbers:
We have release updates to Lix 2.91, 2.92, and 2.93 that contain fixes for these issues. All Lix users are advised to update immediately.
A big thanks to Rory McNamara from the Snyk Security Labs team for this report, the help and the guidance.
Context
While Nix builds are isolated by design, they are not immune to local privilege escalation.
The Nix daemon, typically running as nix-daemon.service
on systemd-based systems, operates with root
privileges. Build processes, however, are executed under restricted users such as nixbld*
(or dynamically assigned UIDs when using auto-allocate-uids
). These builds are orchestrated by the Nix daemon, which retains full root access.
Importantly, the user initiating the build:
- should not be able to impersonate any of the
nixbld*
users, - and should not be able to replace build directories in
$TMPDIR
Recently, researchers demonstrated that Linux’s abstract UNIX domain sockets, a mechanism for socket communication not bound to the filesystem, can be exploited during Nix builds. Specifically, if a build user can predict a temporary directory path and use multiple collaborating malicious derivations that smuggle out file descriptors, it becomes a potential attack vector.
File descriptor smuggling was the basis for CVE-2024-27297, further detailed in this write-up by Jade, a Lix core developer.
Unfortunately, the vulnerability potential of CVE-2024-27297 was not fully realized. We’ll briefly outline the nature of the bug chain here. A comprehensive analysis will be published in a future blog post by the original reporters (link to be updated here once available).
The bugs
A detailed breakdown of the vulnerabilities will be provided in the upcoming publication from the Snyk research team.
For now, here are a few insights into key elements of the attack chain.
Handling temporary directories as paths rather than handles
Unix systems support directory file descriptors (dirfds) which allow preventing directories being swapped out under one’s nose. We did not use them extensively and used absolute paths in some places, which allowed doing a switcheroo of the temporary directory assigned to a build.
Broken recursive delete
CVE-2025-46415
We had a years-old buggy recursive deletion function which was intended to use directory file descriptors to ensure that it didn’t fall victim to the directories it is deleting being swapped out by another process.
Because of a mixing up of absolute paths and relative paths due to misleading variable names, we were calling unlinkat(dirfd_tmp_foo, "/tmp/foo/.../filename")
instead of unlinkat(dirfd_for_tmp_foo, "filename")
, allowing it to fall victim to /tmp/foo
being swapped out under it.
This was accidentally discovered several months ago by Jade who found the broken path handling as a correctness bug while doing something else that involved passing a relative filename to it, fixed the bug, then forgot to split the fix out and apply it to mainline when she didn’t finish that other thing.
Our response
Closing all attack code paths
Most of the attack vectors in Lix stem from a classic “time-of-check vs. time-of-use” issue in how filesystem nodes are accessed.
Much of the codebase relies heavily on path-based APIs. As a result, patterns like the following are everywhere:
// Time 1: decide to use a file at path P
Path myLocation = P;
// ...some unrelated work happens...
// Time 2: actually use the file at path P
doThingWith(open(myLocation))
The core problem is that by the time we open the path at time 2, it might no longer point to the file we expected – especially if an attacker has managed to intervene in between.
To avoid this, Linux (and BSDs) provide safer alternatives like openat
, which lets us navigate the filesystem using file descriptors instead of string paths. File descriptors refer to actual objects, not just names, suppressing the risk of race conditions or symlink attacks.
But how does an attacker get that window of opportunity? Historically, Nix builds were staged in a shared, world-writeable temporary directory – typically /tmp
. That’s problematic: any user or process with access to /tmp
can create directories, and even replace directories Lix created for itself with their own if Lix (or another privileged process) can be tricked into removing the existing, correct directory. Lix, assuming it’s using its own directories, might then operate on attacker-owned data.
The main fix here was to stop relying on paths and instead use file descriptors more consistently, especially when passing around files and directories. We also added checks to validate that many incoming file descriptors during a build are what Lix expects them to be.
Hardening the build directory
As mentioned earlier, Lix used to rely on a shared temp directory to stage builds before copying them to the Nix store.
A better approach is to move the build directory into a location that’s under the exclusive control of the Nix daemon.
That’s what we’ve done: Lix now stages builds in /nix/var/nix/builds
, a secure area controlled by the daemon.
This change has some side effects. Previously, many users preferred staging builds in /tmp
because it’s usually a tmpfs
, meaning builds happened in RAM – faster and with less disk wear.
However, large builds often didn’t fit in memory, forcing users to fall back to disk-backed /tmp
, or redirect builds elsewhere using TMPDIR
.
Now, by default, Lix uses /nix/var/nix/builds
for builds. This path is on the same filesystem as /nix/store
or /nix/var
, depending on your setup. If you were relying on RAM for performance or disk longevity, you may want to revisit how your build directories are mounted and configured.
If you plan to use a tmpfs, we strongly recommend to set mode=0755
as a mount option for that tmpfs
, otherwise you are effectively reverting this mitigation.
Isolating the network stack
Linux has a concept of network namespaces and is also the only platform which allows abstract UNIX domain sockets that are not bound to a filesystem path, but accessible to all processes in the same network namespace.
Since abstract Unix sockets are separated by network namespace, unlike file-based Unix sockets which are separated by chroot
making the outside filesystem invisible, it’s necessary to put builds in their own network namespace to prevent them from connecting UNIX sockets to each other and sending each other file descriptors.
It is already the case before these patches that input-addressed derivations (that is, any build where you don’t specify outputHash
) are in their own network namespace as they only need to have connectivity to localhost and don’t need connectivity to the outside world.
The problem case is output-addressed derivations (“fixed-output derivations”) which have outputHash
given and which need network access to do their job of fetching sources or similar; these were, to date, in the host network namespace as network namespaces are isolated by default and require doing something to route their traffic to the outside world.
There are multiple methods for implementing this with their own tradeoffs, which you can see on the Docker network drivers page: what we were doing before was analogous to host
for output-addressed derivations and none
for input-addressed derivations.
Lix builds now optionally use Pasta to create an isolated network namespace per build of fixed-output derivations (most typically, fetching sources).
This is doing the same thing as rootless docker/podman that uses pasta
: we don’t touch the networking configuration of the host and we route the traffic through a process where the build’s traffic will appear to come from on the host.
Input-addressed derivations (i.e. most application code builds) remain the same as today and continue to run in an isolated network namespace.
Pasta is disabled by default except on NixOS unstable channels.
Alternate mitigation: banning UNIX abstract domain sockets
As in CVE-2024-27297, it was considered to ban abstract UNIX domain sockets on Linux with a Linux Security Module, but it never landed because it is really hard to scope such a ban correctly to only affect fixed-output derivations and not affect input-addressed derivations that don’t need the ban.
We did prototype it.
The technical reason that it wasn’t possible to land is because per-build cgroup
isolation support in Lix is an active work in progress to bring up to systemd standards compliance, and Linux only allows either scoping Linux Security Modules by cgroup
or having them host-wide (the latter would break DBus among other legitimate users of abstract Unix sockets!).
Our view is that such a ban should probably become a systemd hardening option as systemd is the perfect place to put in a generic service-scoped mitigation as they use cgroups
extensively.
The prototype is available in https://gerrit.lix.systems/c/lix/+/3468 and we plan to integrate it better inside of Lix.
Nonetheless, it makes more sense for older Lix or Nixes, where cgroups could not be used. Lix can be used here to obtain the deabstract-ur-sockets
binary and apply it to your old workloads as extra mitigation.
What of macOS or edge cases?
macOS is an interesting situation, as is the whole concept of the defense-in-depth mitigations used here. The Lix sandbox is not intended as a security boundary, in a similar fashion to User Account Control on Windows.
That being said, we do not want random Nix derivations to be able to root your computer, and those are security bugs! The Lix daemon is not supposed to unintentionally become sudo (*unless you are trusted-user), and that is a vulnerability.
Let’s talk about edge cases:
macOS has full isolation of input-addressed derivations from the network by default just like Linux (assuming no
__darwinAllowLocalNetworking = true
or__noChroot = true
), but does not have filesystem isolation by default, unlike Linux.Filesystem isolation can be enabled by the
sandbox = true
/sandbox = relaxed
setting, but this breaks some things including especially-huge derivations as the sandbox policy has a length limit in the kernel/userspace where it will fail to create the sandbox configuration.This means that any kind of problems with Unix sockets in the parts of the Lix daemon that run as root will, on macOS, be equivalent to “output-addressed derivations in host network namespace” on Linux with respect to being able to open random Unix sockets with each other and send file descriptors as they can just use normal Unix sockets for that!
Setting
sandbox = relaxed
orsandbox = false
on Linux will allow__noChroot = true
(or in the latter case, all) input-addressed derivations to open random Unix sockets to each other by default just like macOS since it disables filesystem isolation.
For this reason, we want the Lix daemon to be resilient against derivations playing funny games with their file handles even after their termination, and this should not allow compromising security or correctness properties where possible. Sometimes we fall short of the mark, like in this case where we had a broken delete directory function and in the case of CVE-2024-27297 where a time-of-check versus time-of-use bug allowed tampering with fixed-output derivation outputs after they had their hash verified.
What to expect?
Functional regressions
With Pasta mitigation enabled, some fixed-output derivations may behave unexpectedly – especially if they rely on networking features beyond standard TCP or UDP.
Advanced functionality in TCP or UDP might also work differently, as noted in Pasta’s feature list. For example, fetchBittorrent
or other UDP-based tools may run into problems especially if they expect incoming connections to work.
Protocols like SCTP will break entirely.
If you run into issues, please open a bug in the Lix issue tracker. As a workaround, you can disable the Pasta mitigation by setting an empty pasta-path
in /etc/nix/nix.conf
:
experimental-features = ...
pasta-path =
Lix will only enable the mitigation if it finds a valid pasta
binary, so clearing the path disables it.
Stability and quality assurance
While these security patches were tested thoroughly on Linux, they have not seen the broader real-world exposure that comes from being merged into Lix HEAD and used across diverse setups.
We know reliability matters. That’s why Lix structures the mitigations in layers:
- Base fixes – default-on changes that close the vulnerability itself
- Pasta mitigation – prevents access to abstract UNIX domain sockets for any build
- LSM mitigation – blocks abstract UNIX sockets for a given build
In stable NixOS releases, the Pasta mitigation is disabled by default. On NixOS unstable channels, it is enabled by default.
Lix 2.90 backport status
We originally considered backporting these fixes to Lix 2.90. However, that version has diverged too much since 2.91, making a clean backport impractical.
Lix 2.90 will be removed in upcoming Nixpkgs and NixOS releases. Going forward:
- Lix 2.91.2 becomes the “old stable”
- Lix 2.93.1 becomes the new stable
If you still depend on Lix 2.90 and need the fixes backported, reach out to the Lix team.
Protecting yourselves
On NixOS
On NixOS, you have two scenarios.
I’m using lix-module
Just upgrade your Lix pin and rebuild Lix as always.
I’m using Lix from Nixpkgs
First of all, Nixpkgs moves slower when you are subscribing to one of its channel (unstable, 25.05, etc.).
The fix will not be built by Hydra instantly.
Multiple tools are available for you:
- https://nixpk.gs/pr-tracker.html — this tells you when a PR lands in the channel you might be interested
- https://status.nixos.org/ — this tells when is the last time a channel was updated
If you cannot wait, you have the option to apply the patch:
- apply the nixpkgs PR as a patch to your nixpkgs source tree → will cause a Lix rebuild
- apply the Lix patches as a patch to Lix as an overlay → will cause a Lix rebuild
- adopt temporarily lix-module → will cause a Lix rebuild
Either case, if you are using Lix this way, you cannot escape from a rebuild.
- unstable: https://github.com/NixOS/nixpkgs/pull/419582
- 25.05: https://github.com/NixOS/nixpkgs/pull/419583
- 24.11: https://github.com/NixOS/nixpkgs/pull/419585
On non-NixOS Linux
I have used the Lix’s installer (or Determinate Systems’ installer)
On non-NixOS Linux, you may have installed Lix via our installer or Determinate Systems’ installer with a Lix’s URL.
You can use nix upgrade-nix
to upgrade and download the new binaries.
I have used my Linux distribution’s package manager
To the best of our knowledge, Lix is only packaged in Nixpkgs and Pacstall (without maintainer).
This means you have a package in a distribution we do not have knowledge of, please contact your distribution’s security team.
On macOS
I have used the Lix’s installer (or Determinate Systems’ installer)
On macOS, you may have installed Lix via our installer or Determinate Systems’ installer with a Lix’s URL.
You can use nix upgrade-nix
to upgrade and download the new binaries.
I am using nix-darwin
On macOS, you may be using nix-darwin
and therefore you are depending on Nixpkgs or lix-module
, thus, the section on On NixOS
applies to you as well.
I have used a macOS package manager
To the best of our knowledge, Lix is only packaged in Nixpkgs and Pacstall (without maintainer).
This means you have a package in a distribution we do not have knowledge of, please contact your distribution’s security team.
Additional guidance
Am I fully protected if Pasta or the LSM mitigation is not used?
As per the CVE bulletin, CVE-2025-46416 is mitigated only under Pasta or the LSM mitigation.
If you are on a stable release of NixOS, Pasta will be disabled by default, this is an intentional choice of ours.
The stability risk run by enabled Pasta on a large (stable) userbase exceed the risk caused by the user to nixbld escalation which we deem unimportant in most cases.
Users who are running stable workloads and believe they might be affected by this attack vector should enable Pasta and run their own stability tests.
The Lix project remains available to help anyfew who would run into stability issues with Pasta, but we give you the options to control the balance between risk and reliability.
I have additional sandbox-paths
Additional sandbox paths can be liable to similar attacks as the one we described in section on build directories.
It’s the responsibility of the system operator to ensure that additional sandbox paths are protected adequately.
Timeline
- 2025-03-26: Bug reported to Lix team.
- 2025-03-26: Lix team setupped the private repo and started fixing bugs.
- 2025-03-31: Lix team finished the mandatory mitigations.
- 2025-04-10 - 2025-04-21: Lix team on holidays.
- 2025-04-25: Lix team finished the optional mitigations.
- 2025-04-30: QA started.
- 2025-05-08: QA ended. Lix team made final patches available.
- 2025-06-24: Disclosure deadline.
Credits
This was a team work, we are blessed with amazing collaborators, so thanks to you all!
Credits to the Nixpkgs security team for coordinating and dealing with the MITRE paperwork.
Credits to the Guix team for sharing thoughts on their approach to the problem.
Credits to Rory McNamara for reporting this issue and providing guidance for the fixes.
Credits to Jade Lovelace for the Linux Security Module defense-in-depth option, the general coordination and co-authoring this blog post.
Credits to Puck Meerburg for the initial Pasta prototype.
Credits to eldritch horrors for the build directory, Pasta fixes and enduring Raito’s worries and proofreading the final mitigation process on the D-day.
Credits to Raito Bezarius for the file descriptors-related fixes and QA on Linux.