On 13 October 2026, the Nix and Lix projects published a joint security release fixing five vulnerabilities. CVE IDs followed on 14 October. The most serious, CVE-2026-110450, lets any user who is allowed to talk to the Nix daemon overwrite any path in /nix/store, including the nix binary that the root-owned daemon runs. On macOS, a second bug lets a build process itself reach the daemon, so building a malicious derivation is enough to start the chain.
This post is for anyone running multi-user Nix or Lix: shared build servers, CI runners, NixOS machines with more than one account, and Macs installed with the official installer. It explains the repair-flag bug from the fix commits, how the macOS issue widens it, the three smaller bugs in the same release, a script that checks a host, and the upgrade traps people hit on the first day.
The short answer
Upgrade Nix to 2.35.3 (or 2.34.9, 2.31.6, 2.30.6 or 2.28.8 on older branches), or Lix to 2.95.4, 2.94.3 or 2.96.0. Nix 2.29, 2.32 and 2.33 are no longer maintained and won't get fixes, and Lix 2.93 and older are in the same position. The fixes landed in nixpkgs on 13 October. Until a host is upgraded, any local account matched by the daemon's allowed-users setting, which is every account by default, should be treated as able to become root.
| CVE | Advisory | What it is | Who is exposed |
|---|---|---|---|
| CVE-2026-110450 | GHSA-jwvx-xq5j-gc56 | Untrusted daemon clients can overwrite any store path using the repair flag, leading to root | Multi-user installs, Linux and macOS; Nix and Lix |
| CVE-2026-110453 | GHSA-92x5-h528-wrmw | On macOS and FreeBSD, the daemon doesn't reject connections from build users | Unsandboxed builds, the macOS default; Nix only |
| CVE-2026-110451 | GHSA-m4vq-qf6v-82pj | macOS Seatbelt sandbox escape when the generated profile crosses a size threshold | macOS with sandbox = true or relaxed; Nix and Lix |
| CVE-2026-110449 | GHSA-25vp-6h23-hj3g | Integer overflow in builtins.genList list allocation, out-of-bounds access during evaluation | Anything that evaluates untrusted Nix code; Nix and Lix |
| CVE-2026-110452 | GHSA-hf6w-2rg9-v84g | The multi-user installer left store contents owned by the installing user, not root | Fresh installs with nixos.org/nix/install since Nix 2.4; not NixOS |
The advisories credit Guthrie McAfee Armstrong and joaoviictorti with reporting the repair-flag escalation, Armstrong with the build-user connection bug, and xokdvium with the installer bug. The Seatbelt advisory says that bug was identified using an AI model (it names GPT-6.1 Sol) and reported by a third party. The joint announcement says Guix is not affected.
How the repair flag became a root exploit
On a multi-user install, users don't write to /nix/store directly. They ask nix-daemon, which runs as root, to do it. The daemon sorts its clients into two groups. Trusted users (trusted-users, by default only root) can do almost anything, including adding unsigned paths. Everyone else allowed to connect (allowed-users, by default *) is untrusted. They can add content only when the content proves itself, by being content-addressed or signed by a trusted key.
Several daemon operations (AddToStore, AddToStoreNar and AddMultipleToStore) accept a "repair" flag, which tells the daemon to rewrite a path even if it already exists. In vulnerable versions, untrusted clients could set it. On its own that would only matter if the daemon wrote unverified bytes into place, and that turned out to be exactly what it did. The fix commit explains it directly: the incoming archive (a NAR) "is eagerly unpacked in its expected store path, while NAR size and hash are checked only after the full NAR is unpacked to that location". When the check failed, the advisory says, "the malicious content is left on disk in the store."
Put together, an untrusted user could:
- pick any existing store path, such as the one holding the
nixbinary that the root daemon runs, - send an
AddToStoreNarrequest for that path with the repair flag set and an archive containing their own files, - let the integrity check fail, which is expected, since the content doesn't match,
- and wait for something privileged to execute the replaced files.
The advisory dates the problem to Nix 2.1, when NAR streaming was added to those operations. The commit message calls the result "trivial LPE".
The release fixes this twice over. First, the daemon now refuses repair from untrusted clients:
/* Repairing is not atomic, so disallowed for "untrusted"
clients. */
if (repair && !trusted)
throw Error("you are not privileged to repair store paths");
Second, LocalStore::addToStore no longer unpacks into the final path. It restores the archive into a temporary directory "accessible only to us first", checks the hash, size and content address there, and only then moves it into place. That second change matters beyond repair. It enforces the rule that untrusted users can only ever get verified content into the store, whichever request they send.
The commit is candid that repair still isn't atomic: the old contents are deleted before the new ones are moved in. The developers note it could be made atomic with RENAME_EXCHANGE on Linux, or its macOS equivalent, and treat that now as a reliability issue rather than a security one, since only trusted users can repair.
Why macOS makes it worse
On Linux, the daemon reads a client's identity with SO_PEERCRED and refuses connections from the build users group, the nixbld accounts that builds run under. On macOS and FreeBSD it uses LOCAL_PEERCRED, and according to the advisory for CVE-2026-110453, it ignored the group ID that call returns. So the daemon accepted connections from build users.
That matters because sandboxing is off by default on macOS. An unsandboxed build process runs as a nixbld user with access to the daemon socket. Combined with the repair bug, the joint announcement's summary is that the issues "can be combined to achieve root privilege escalation", and that "all systems that build untrusted derivations are impacted". The attacker doesn't need a login on the Mac. They need someone on it to build their derivation.
The other three fixes
Seatbelt sandbox escape (CVE-2026-110451). On macOS, Nix builds sandboxes by generating a Seatbelt profile, which it splits into chunks to work around limits in the profile interpreter. The joint announcement says that when the profile crossed an internal size threshold at a missing optional path, an unqualified (allow ...) rule could be generated, leaving the build unconfined. It applies only where the sandbox was turned on, since it's off by default on macOS. The announcement notes it can be triggered deliberately and, in some configurations, by accident.
builtins.genList overflow (CVE-2026-110449). A large argument to genList overflows the size calculation, so the allocated list is smaller than Nix believes. The advisory says GCC 15 builds, the ones nixpkgs and the official installers ship, don't show the out-of-bounds write because of how the compiler handles the arithmetic, but out-of-bounds reads remain reliably triggerable. The Nix team knows of no exploit. Its broader advice is worth repeating: don't rely only on evaluator correctness when evaluating untrusted Nix code, particularly in CI jobs that can see secrets.
Installer store ownership (CVE-2026-110452). Since Nix 2.4, the multi-user scripted installer, when run as a normal user as the download page recommends, left the contents of the store owned by that user rather than root. That user could later replace the nix binary that launchd or systemd starts as root. The advisory notes the bad ownership only affects the initial bootstrap closure, so hosts that have upgraded Nix and garbage-collected the original generation are likely clean. NixOS installs aren't affected. The advisory includes a short find and chown snippet to repair ownership.
Check a host
This script reads the installed version, compares it with the fixed release for its branch, reads nix.conf to show who can reach the daemon, and scans the store for objects not owned by root. It only reads, and it stops scanning after ten bad entries. Run it as root so the store scan can see everything.
#!/usr/bin/env python3
"""Check a Nix or Lix host against the October 2026 advisories
(CVE-2026-110449 to CVE-2026-110453). Read-only; run as root for the store scan.
Usage: python3 nix_oct2026_check.py [--root /] [--version "nix (Nix) 2.35.2"] [--skip-store]"""
import argparse, os, platform, re, subprocess
ap = argparse.ArgumentParser()
ap.add_argument("--root", default="/") # for testing against a copy of a host
ap.add_argument("--version") # override `nix --version` output
ap.add_argument("--skip-store", action="store_true")
a = ap.parse_args()
R = lambda p: os.path.join(a.root, p.lstrip("/"))
# Fixed patch release per maintained branch, from the joint advisory (13 Oct 2026)
NIX_FIXED = {(2, 35): 3, (2, 34): 9, (2, 31): 6, (2, 30): 6, (2, 28): 8}
NIX_DROPPED = {(2, 29), (2, 32), (2, 33)} # no fix will be released
LIX_FIXED = {(2, 94): 3, (2, 95): 4} # 2.96.0 and later are fixed
problems = []
ver = a.version or subprocess.run(["nix", "--version"], capture_output=True, text=True).stdout
m = re.search(r"\((Lix|Nix)[^)]*\)\s+(\d+)\.(\d+)\.(\d+)", ver)
if not m:
problems.append(f"could not parse version from {ver.strip()!r}")
else:
impl, maj, mnr, pat = m[1], int(m[2]), int(m[3]), int(m[4])
br = (maj, mnr)
print(f"{impl} {maj}.{mnr}.{pat}")
if impl == "Nix":
if br in NIX_FIXED and pat >= NIX_FIXED[br] or br > (2, 35):
print(" version: fixed")
elif br in NIX_FIXED:
problems.append(f"Nix {maj}.{mnr}.{pat} is vulnerable: upgrade to {maj}.{mnr}.{NIX_FIXED[br]}")
elif br in NIX_DROPPED:
problems.append(f"Nix {maj}.{mnr} is no longer maintained and gets no fix: move to 2.35.3")
else:
problems.append(f"Nix {maj}.{mnr} predates the fixed branches: assume vulnerable, move to 2.35.3")
else:
if br in LIX_FIXED and pat >= LIX_FIXED[br] or br >= (2, 96):
print(" version: fixed")
elif br in LIX_FIXED:
problems.append(f"Lix {maj}.{mnr}.{pat} is vulnerable: upgrade to {maj}.{mnr}.{LIX_FIXED[br]}")
else:
problems.append(f"Lix {maj}.{mnr} is unmaintained: move to 2.95.4 or 2.96.0")
conf = {}
for f in ("/etc/nix/nix.conf", "/etc/nix/nix.custom.conf"):
if os.path.exists(R(f)):
for line in open(R(f)):
k, _, v = line.split("#")[0].partition("=")
if v:
conf[k.strip()] = v.strip()
daemon = os.path.exists(R("/nix/var/nix/daemon-socket/socket"))
allowed = conf.get("allowed-users", "*")
print(f" multi-user daemon: {'yes' if daemon else 'no'}; allowed-users = {allowed}; "
f"trusted-users = {conf.get('trusted-users', 'root')}")
if daemon and problems:
problems.append(f"repair-flag privilege escalation (CVE-2026-110450) is reachable by allowed-users = {allowed}"
+ (" <- every local account" if allowed.strip() == "*" else ""))
macos = platform.system() == "Darwin" or os.path.exists(R("/Library/LaunchDaemons/org.nixos.nix-daemon.plist"))
if macos and conf.get("sandbox", "false") in ("true", "relaxed") and problems:
problems.append("macOS with sandbox enabled: Seatbelt escape (CVE-2026-110451) applies until upgraded")
if not a.skip_store and os.path.isdir(R("/nix/store")):
bad = []
for top in os.scandir(R("/nix/store")): # stops after the first 10 hits
paths = [top.path]
if top.is_dir(follow_symlinks=False):
for dirpath, dirs, files in os.walk(top.path):
paths += [os.path.join(dirpath, x) for x in dirs + files]
bad += [p for p in paths if os.lstat(p).st_uid != 0]
if len(bad) >= 10:
break
if bad:
problems.append(f"store objects not owned by root (installer bug, CVE-2026-110452), e.g. {bad[0]}")
else:
print(" store ownership: all checked objects owned by root")
print("\n" + ("\n".join("[!] " + p for p in problems) if problems else "no issues found"))
raise SystemExit(1 if problems else 0)
Against a test root set up like a multi-user Nix 2.35.2 host (default allowed-users, daemon socket present, and store files owned by a normal user, as the installer bug leaves them), it reports:
$ python3 nix_oct2026_check.py --root fx1 --version "nix (Nix) 2.35.2"
Nix 2.35.2
multi-user daemon: yes; allowed-users = *; trusted-users = root
[!] Nix 2.35.2 is vulnerable: upgrade to 2.35.3
[!] repair-flag privilege escalation (CVE-2026-110450) is reachable by allowed-users = * <- every local account
[!] store objects not owned by root (installer bug, CVE-2026-110452), e.g. fx1/nix/store/abc-nix-2.35.2
We tested the version logic against fixed and unfixed releases of both Nix and Lix, including an unmaintained Nix 2.33 branch, which it flags as needing a move to 2.35.3. It doesn't connect to the daemon, so it can't detect the bugs directly. It tells you whether the conditions for them are present.
What to do
- Upgrade. For installs from nixos.org/nix/install, the announcement gives
nix upgrade-nix --upgrade-nix-store-path-url https://releases.nixos.org/nix/nix-<version>/fallback-paths.nix, using the fixed patch release of the minor version you already have. - On NixOS, confirm the fix has reached your channel. The nixpkgs updates for unstable merged on 13 October. The NixOS 26.05 Nix update merged into a staging branch that same day, so check
nix --versionafter your next rebuild rather than assuming. - On Lix, read the upgrade note first. A user in the announcement thread reported that a plain
nix upgrade-nixon an overlay install of Lix 2.95 uninstalled the old version before installing the new one, then failed because no daemon was left to finish the job. The Lix project provides an upgrade script for 2.95.4, and the announcement says 2.94.3 can usenix upgrade-nix. - Narrow
allowed-users. The default*lets every local account talk to the daemon. On shared machines, set it to a group such as@wheelor a dedicated@nix-users. That was good practice before this release, and it shrinks the next daemon bug too. - Fix installer ownership on Macs and Linux hosts installed with the scripted multi-user installer, using the snippet in GHSA-hf6w-2rg9-v84g, or check with the script above.
- Sandbox untrusted evaluation. If CI evaluates or builds Nix code from pull requests, run it in a disposable VM or container without access to secrets, and on macOS turn the Nix sandbox on once you're on a fixed version.
The pattern behind it
The core bug is a familiar one: verify after you write, and a failed check leaves the attacker's bytes in place. The fix is the textbook answer of unpacking to a private temporary location, verifying there, and moving into place. It's worth checking anything you maintain that accepts uploads, artifacts or archives for the same ordering. A related point applies too. "Repair" sounds like a maintenance operation, so it was easy to leave open to everyone, but any operation that rewrites existing trusted content belongs behind the strongest check you have.
To be fair to Nix, every one of these bugs is local. The attacker needs an account on the machine or a build running on it, and a single-user laptop that only builds your own code is at much lower risk. The projects coordinated the disclosure across two implementations and several release branches and published concrete remediation steps on day one. For shared build infrastructure, though, "local" covers everyone who can submit a job. For more on why Nix is worth this attention in supply-chain terms, see our earlier post on Nix and reproducible builds.