Patching Vulnerable Derivations With Nix Overlays
Overlays patch vulnerabilities across your entire dependency graph, not just one package.

A Nix overlay patches a vulnerable derivation at the point where nixpkgs assembles its entire package graph, so the fix reaches every dependent package automatically, not just the one the engineer happened to touch. A one-off overrideAttrs call cannot do this: it produces a new derivation local to the caller, while the rest of nixpkgs keeps resolving to the unpatched original. This piece works through the mechanics of that distinction, from the fixed-point model that makes overlays global, to a working patch pattern, to a documented incident where the overlay path was the fastest route to remediation.
Security remediation beyond a one-off overrideAttrs call
A Nix derivation is the atomic unit of the build system: a pure, deterministic description of how to produce a specific store path from a specific set of inputs. Every package that depends on another package does so by referring to that package's derivation, and the whole of nixpkgs is a graph of these references. When a vulnerability is found in one derivation, the fix has to reach every other derivation that consumes it, not just the single build an engineer happens to be looking at. That is where a direct overrideAttrs call falls short: it returns a modified derivation for use at the call site, but it has no effect on the shared pkgs instance that every other package in the tree reads from. Patch the vulnerable library at the call site and the thousand other packages that depend on that same library keep building against the original, unpatched derivation.
The nixpkgs manual lists four primitives for modifying a single package: override, overrideAttrs, overrideDerivation, and lib.makeOverridable. Each one changes exactly one package at exactly one point in the dependency graph, and each one leaves every other consumer of that same input untouched. These tools are correct for local experimentation, version pinning on a single package, or a one-off build tweak. None of them is built to close a vulnerability that might be reachable through dozens of transitive paths. Overlays exist as the nixpkgs mechanism for combining overridden packages across the entire set, precisely because single-package overrides cannot be composed to cover a whole graph. The gap between a scoped fix and a graph-wide fix is the reason overlays, not overrideAttrs, are the correct tool when the question is "did we patch everything that could be affected."
The nixpkgs fixed point and graph-wide overlays
Nix treats nixpkgs as a fixed point: pkgs is defined in terms of itself, so any package that depends on another package inside that set refers to the same shared instance of pkgs, not to an independent copy. A useful comparison is a spreadsheet where one cell is defined as a formula referencing another cell that is itself defined by a formula referencing the first: change the value feeding into that self-referential chain, and every cell downstream recalculates from the new value automatically, with no need to touch the downstream cells individually.
An overlay is a function with the signature final: prev: { ... }. The prev argument gives access to the package set as it existed before this particular overlay was applied, possibly already containing the results of earlier overlays in the chain. The final argument gives access to the fully composed package set after every overlay has been applied, including this one. Because every package in nixpkgs that depends on the overlaid derivation reaches it through final, substituting a patched derivation at that layer reaches every consumer automatically. No downstream package definition has to change. The packages that depend on the patched library were already written to ask final for that library, so they get the new one the moment the overlay is in place.
The default rule: reach for prev unless the attribute being defined needs to reference some other package from the same fully composed set, in which case final is required. Scoped package sets, such as a set of related libraries grouped under one attribute, need overrideScope rather than a plain overlay, because the fixed point has to be re-tied inside that nested attribute set as well as at the top level. The mechanism is the same fixed-point logic, applied one level deeper.
Writing a patch overlay: the fetchpatch pattern
With the fixed-point mechanism established, writing a security-patch overlay is a matter of following a fixed pattern. A minimal version looks like this:
final: prev: {
vulnerable-lib = prev.vulnerable-lib.overrideAttrs (old: {
patches = (old.patches or []) ++ [
(prev.fetchpatch {
url = ";
hash = "sha256-AAAA...";
})
];
});
}
Inside it, prev.vulnerable-lib.overrideAttrs reaches back into the package set as it stood before this overlay, which is deliberate, since calling final.vulnerable-lib here would mean asking the overlay for the very definition it is in the middle of producing, an infinite loop. The old.patches or [] The old.patches or [] guard exists because not every package ships a patches attribute by default, and appending to a missing attribute would throw a type error. fetchpatch retrieves the patch from the given URL and checks it against the declared hash at evaluation time, which extends Nix's tamper-resistance guarantee to the patch file itself and not only to the original package source. Whether that hash is written in one format or another is a formatting detail, not a decision that changes what the overlay does. Once applied, the patched package gets a new store path derived from its new inputs, so the pre-patch and post-patch derivations coexist side by side in the store, and the change is visible and auditable.
Grafting and correct use of prev vs. final
Getting the prev/final distinction right matters for more than avoiding an infinite loop. It governs whether Nix's grafting optimization can do its job, which lets a patched derivation reach its dependents by rewriting the dynamic library references those dependents already embed, rather than recompiling every package that links against the updated library from scratch. For a library consumed by hundreds of packages, that governs whether a fix lands in minutes or ties up a build farm for hours.
The failure mode is specific: if an overlay writes final.vulnerable-lib in the body of the function that is defining vulnerable-lib itself, it creates a self-referential loop. Nix can no longer treat the patched package as a simple substitution at one point in the graph; it has to recompute the entire closure, which defeats the grafting mechanism outright and forces a full rebuild of every package that depends on the patched library, directly or transitively. For a widely-consumed library such as OpenSSL or zlib, a correctly written overlay propagates through binary cache hits in minutes. An overlay that references final where it should reference prev can queue hours of recompilation across an entire system closure, for a fix that should have cost nothing beyond fetching one patch file. The rule is the same one introduced with the fixed-point: prev is the layer before this overlay, final is the fully composed result, and asking final for the package currently being defined is circular by construction, not merely inefficient.
Wiring an overlay into a NixOS configuration and a flake
An overlay does nothing sitting on its own. It has to be registered in the nixpkgs instance that a system or a flake actually evaluates against, and the registration point depends on which path a team is building from.
In a flake, overlays are supplied when the pkgs instance is constructed:
pkgs = import nixpkgs {
inherit system;
overlays = [ mySecurityOverlay ];
};
That pkgs value is then threaded into nixosConfigurations, devShells, or whatever other output consumes packages, so every output built from it sees the patched package set.
On a NixOS system configured through configuration.nix rather than a flake, the equivalent is the nixpkgs.overlays option:
nixpkgs.overlays = [ mySecurityOverlay ];
Overlays compose in list order, and a later overlay can see and further modify what an earlier one defined. nix-darwin nix-darwin and other configuration systems register overlays through the same mechanism, so the pattern described here applies the same way when the target is a different kind of host.
A real CVE response traced through the overlay path
The overlay pattern is already part of how the Nix community has responded to real vulnerabilities under time pressure, not a theoretical convenience. In April 2026, a critical privilege-escalation bug was found in the Nix daemon, tracked as CVE-2026-39860. The flaw was a regression introduced by the fix for an earlier vulnerability, CVE-2024-27297: during registration of a fixed-output derivation's output, the Nix process running in the host's mount namespace followed a symlink that the derivation's builder had placed inside the build chroot, which allowed arbitrary overwrites of files writable by the Nix daemon. In multi-user installations the daemon runs as root, so any user permitted to submit builds, governed by the allowed-users setting, which defaults to all users, could exploit the bug to gain root on the affected system. Sandboxed macOS builds were unaffected; sandboxed Linux builds were the primary exposure.
Teams that could not wait for an operating-system-level package update pinned their flake to a patched nixpkgs revision and applied the fix as an overlay directly against the nixpkgs source tree. That path compressed the response window from waiting on a distribution release to merging a one-file change and rebuilding.
A comparable episode played out in a coordinated disclosure covering both Nix and Lix, reported by the Snyk Security Labs team. It affected Nix branches 2.24, 2.26, 2.28, and 2.29, and Lix branches 2.91, 2.92, and 2.93. The community guidance that followed was specific: hold off on running nix flake update until the affected nixpkgs branch carried the fix, then update nixpkgs first before touching any other flake input. That sequencing only makes sense if the nixpkgs pin is treated as the authoritative source of the remediation, which is exactly the assumption the overlay pattern depends on. The Lix post-disclosure write-up traced the underlying cause to time-of-check/time-of-use races in path-based filesystem APIs, and confirmed five CVEs: CVE-2025-46415, CVE-2025-46416, CVE-2025-52991, CVE-2025-52992, and CVE-2025-52993. The remediation moved build staging out of the shared /tmp directory and into /nix/var/nix/builds, a location under the daemon's exclusive control. In both episodes, the fix was something a team could pin to and rebuild against immediately, well before any package manager's default channel caught up.
Global propagation, supply-chain auditability, and SBOM requirements
The same fixed-point substitution that makes an overlay propagate everywhere also makes the resulting system mechanically auditable. Every artifact in a closure that touches the patched package carries a store path whose hash is derived from the patched inputs, so confirming that no consumer of the vulnerable version survived the patch is a matter of comparing hashes.
Nix requires a SHA-256 hash of a package's source at definition time, checks it again at fetch and build time, and builds inside a sandbox with no network access, apart from fixed-output derivations, which are permitted network access specifically to fetch their declared sources. A source that has been altered without authorization cannot pass that check, because its hash no longer matches what was declared. An overlay-patched build produces a new store path whose hash reflects the patch as one of its inputs. Any downstream artifact still carrying the old hash is, by construction, still running the unpatched code, and that fact is visible from the hash alone.
That property lines up with where supply-chain regulation is heading. The EU Cyber Resilience Act requires, starting in December 2027, a machine-readable software bill of materials covering at least the top-level dependencies of products with digital elements. A closure built through an overlay can generate that SBOM from the same inputs that produced the build itself, with no separate reconstruction step required. Nix's hermetic, sandboxed build model also supports the isolated, ephemeral build environments called for at Level 3 of the SLSA supply-chain framework, and aligns with, though is not required by, that framework's broader recommendations around hermetic builds.
No major vulnerability database currently indexes against Nix package URLs the way npm, PyPI, and Go ecosystems are indexed through package URLs. A vulnerability scanner built for those ecosystems cannot confirm, on its own, that a Nix-native package has actually been patched. The hash-level verification an overlay provides fills that specific gap: it does not depend on a database recognizing the package at all, only on comparing the store path hash the closure contains against the hash the patch is known to produce.
Applying the same overlay discipline to declarative environment definitions
Everything described above depends on the environment being defined declaratively, evaluated against a pinned, version-controlled package set, rather than assembled at install time against whatever a package manager happens to find on a given machine. That dependency is what makes the overlay pattern extend cleanly from a single system configuration to an entire fleet of environments.
An environment described in a human-readable manifest alongside the application's own code can carry a security overlay as a versioned, reviewable change: one atomic edit, one new hash, one diff that a reviewer can read and approve, replacing the slower cycle of building, scanning, tagging, and pushing a new image by hand. Rollback is equally atomic. Reverting the manifest to its previous generation restores the exact prior closure, including whatever state existed before the patch, which matters if a given patch turns out to introduce its own regression. That same generation-based rollback discipline makes Nix systems safe to update aggressively.
Teams that distribute environments by name and version across a fleet, from a developer's laptop through continuous integration to production, propagate a patched overlay to every one of those consumers in a single operation, instead of coordinating separate package upgrades across machines that may not even share an operating system. The same overlay definition that patches a library on a developer's laptop patches it identically in CI and in a minimal OCI image built for deployment, because all three consume the same nixpkgs instance with the same overlay stack applied. That removes the specific failure mode where CI gets patched and production does not, simply because the two were updated through different mechanisms at different times. An AI coding agent operating inside a Nix-defined environment inherits the same patched closure without any special handling, because the guarantee comes from the environment's hash, not from whatever the agent does or does not know about a given vulnerability.


