Skip to content
/ rfcs Public
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions rfcs/0199-sandbox-nix-evaluator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---
feature: sandbox-nix-evaluator
start-date: 2026-03-03
author: bgl gwyng
co-authors:
shepherd-team:
shepherd-leader:
related-issues:
- https://github.com/NixOS/rfcs/pull/68
- https://github.com/NixOS/rfcs/pull/8
- https://github.com/NixOS/rfcs/pull/17
- https://github.com/NixOS/nix/issues/12607
---

# Summary
[summary]: #summary

Run the Nix evaluator — and any alternative frontend — inside the same sandbox used for builds, with a mounted Nix daemon socket as the sole interface to the store. This inverts the current relationship: instead of an unsandboxed evaluator invoking sandboxed builds, an evaluator runs inside a sandbox and requests store operations through the daemon.

# Motivation
[motivation]: #motivation

Today, when Nix builds a derivation, it creates a sandbox (Linux namespaces, macOS sandbox-exec), places the builder inside, and mounts a daemon socket so the build can request store operations. The evaluator, by contrast, runs **outside any sandbox**. In multi-user mode it already communicates with the store through the daemon socket — but the evaluator process itself is unconstrained: it has full network access, can read the entire filesystem, and is not isolated in any way.

This asymmetry gives the Nix language a privileged position:

- `nix build`, `nix develop`, `nix flake` — all CLI entry points invoke the Nix evaluator. There is no sanctioned path to the store that does not go through the Nix language.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. nix-store --add on a valid derivation works.
  2. nix derivation add takes JSON and creates derivations.
  3. Guix was in fact sharing the daemon and the store just fine for some time, until they decided to rewrite it.

- The evaluator's purity is a language-level property, not an OS-enforced one. Even in multi-user mode where store operations go through the daemon, nothing prevents the evaluator from performing arbitrary I/O during evaluation. We *trust* the evaluator not to do this; we don't *enforce* it.
- Alternative frontends (GNU Guix's Guile, or any hypothetical Python/Rust/Go-based package tool) cannot participate as equals. Guix had to rebuild its entire package set from scratch. There is no "just speak the store protocol" option.

The solution is straightforward: apply the same sandboxing we already use for builds to evaluation. The daemon socket mechanism already exists. The sandbox infrastructure already exists. We simply need to put the evaluator on the same side of the sandbox boundary as the builder.

# Detailed design
[design]: #detailed-design

## The inversion

Currently (multi-user mode):

```
Nix Evaluator (unsandboxed)
|
|-- talks to daemon for store operations
|-- but: has full network access
|-- but: can read entire filesystem
|-- but: purity enforced only by language semantics
|
\--> daemon creates sandbox for build
\-- builder runs inside sandbox
\-- daemon socket mounted for store access
```

Proposed:

```
Nix Daemon (manages store, creates sandboxes)
|
\--> creates sandbox for evaluation
|-- evaluator runs inside sandbox
|-- daemon socket mounted (same mechanism as builds)
|-- no network access
|-- filesystem restricted to source inputs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really the main problem of this proposal and will need to be elaborated on.

It is completely impossible to determine the “source inputs” of an evaluation without performing said evaluation.

For that purpose, it is actually better that the evaluator has full file system access (at least to read) since performing those reads over the daemon protocol would mean that we would need to add an interface for performing arbitrary filesystem reads to the daemon which runs as reads. This would create the potential for new exploits.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it's mentioned above that the relevant information comes from the CLI, I'm guessing "source inputs" just means "initial source files to be evaluated" e.g. a flake. And that evaluation would determine if anything needs to be fetched. This is further supported by what is said in the examples. So, if that's what it means, it isn't impossible but we'll need a round trip once we find anything that needs to be fetched before further evaluation, yes?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strictly speaking, the initial input you learn from the CLI is a single file. While evaluating it, you will learn about more files you need to read in order to properly evaluate it (in contrast to a less dynamic language with a module system or similar where you would be able to find this information statically).

With flakes and/or pure eval, you would be able to determine a maximum set of available files for evaluation and make them available to the evaluator in the sandbox. The slight problem being, of course, that neither is a stable feature of Nix in the first place. Also, in order for the proposal to be properly implemented based on those feature, flakes and pure eval would not only need to be stabilized, but also the only evaluation mode available which is not a desirable goal in the first place.

|-- purity enforced by OS, not by language
|
\--> requests build through daemon socket
\-- daemon creates nested sandbox for build
```

The key insight: Nix already sandboxes builds and provides daemon socket access inside the sandbox. This RFC applies the same pattern to evaluation. The evaluator becomes just another sandboxed process that talks to the daemon, identical in privilege level to a builder.

## What changes for the evaluator

In multi-user mode, the evaluator already uses the daemon protocol for store operations. What changes is the **execution environment**: the evaluator process is now confined to a sandbox with no network access and restricted filesystem access. `builtins.fetchurl` and `builtins.path` continue to go through the daemon — but now this is the *only* way to perform I/O, enforced by the sandbox rather than by language semantics.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

builtins.fetchurl and builtins.path […] now this is the only way[s] to perform I/O”

For builtins.path, the evaluator process performs the actual file read or at least needs to open the file and pass the fd to the daemon. Otherwise, as I've touched on above, we would read everything as root.

Also note that forcing something akin to builtins.path (i.e. copying to store) for readFile and import would likely create an unreasonable overhead.


## What changes for alternative frontends

Nothing special. Any program that can speak the daemon protocol through a Unix socket can produce derivations. A Python script, a Rust binary, a Guile program — all run inside the same sandbox with the same daemon socket. The Nix language becomes one frontend among many, distinguished only by the size of its ecosystem (nixpkgs), not by architectural privilege.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also do not fully understand this angle. This proposal is not strictly necessary for alternative frontends to be feasible.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also do not fully understand this angle. This proposal is not strictly necessary for alternative frontends to be feasible.


## What happens to IFD and Recursive Nix

They stop being special cases. IFD is currently "evaluation pauses, triggers a build, resumes with the result" — a special code path. Under this proposal, the evaluator simply sends a build request through the daemon socket and reads back the output path. This is an ordinary protocol interaction, the same thing builders already do with Recursive Nix. The distinction between "evaluation-time store access" and "build-time store access" disappears because both go through the same socket in the same kind of sandbox.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not believe you are describing this correctly. Evaluation pauses in the IFD case not because of some “special case", but because the evaluator has to wait for the file it wants to read to become available (and can't do anything else because it isn't multithreaded), so nothing would change, but we would also not gain anything.


## Trust model

Currently, the evaluator's purity is trusted but not enforced. Even in multi-user mode, the daemon constrains *what the evaluator can do to the store*, but not *what the evaluator can do outside the store* (read files, access the network, observe system state). Under this proposal, the sandbox constrains all of the evaluator's capabilities. The kernel enforces isolation; the daemon validates store operations. The combination is a strictly stronger guarantee than language-level purity alone.

## Migration path

1. **Stabilize the daemon protocol.** Document and version it as a public interface. (Nix 2.32 has begun this work with the derivation JSON format.)
2. **Decouple the evaluator from libstore.** The Nix project is already exploring this ([NixOS/nix#12607](https://github.com/NixOS/nix/issues/12607)): separating the "language proper" from store-dependent primops, so the evaluator can operate purely through the daemon protocol.
3. **Sandbox the evaluator.** Run the evaluator in a sandbox with only a daemon socket and source inputs. Validate against nixpkgs.
4. **Publish client libraries.** With a stable protocol, provide libraries in Rust, Python, etc. for building alternative frontends.

# Examples and Interactions
[examples-and-interactions]: #examples-and-interactions

A user runs `nix build nixpkgs#hello`. The CLI asks the daemon to spawn a sandboxed evaluation. The evaluator starts inside the sandbox, reads the flake, evaluates the Nix expression, and sends derivation registrations through the daemon socket. When the evaluator finishes, the daemon builds the resulting derivations in their own sandboxes as usual. From the user's perspective, nothing changes.

A developer writes a Python-based package generator. It runs in the same kind of sandbox, sends derivations as JSON through the daemon socket, and requests builds. It has exactly the same store access as the Nix evaluator — no more, no less.

# Drawbacks
[drawbacks]: #drawbacks

- **Performance.** Sandboxing the evaluator adds overhead for sandbox creation and teardown. In single-user mode, where the evaluator currently accesses the store directly (bypassing the daemon), switching to daemon-mediated access would add per-call latency. In multi-user mode the daemon path is already used, so the additional cost is only the sandbox itself.
- **Bootstrapping.** The evaluator binary may reside in the store. The daemon must make it available inside the sandbox before evaluation starts, similar to how it provisions builders.
- **Protocol commitment.** A stable daemon protocol is a long-term maintenance burden.
- **Community.** This deliberately levels the Nix language's special status, which may face resistance.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this lose the current fetch-without-checksum-and-cache functionality?


# Alternatives
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you just run the evaluator inside a user space sandbox of some description to get the same guarantees?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you just run the evaluator inside a user space sandbox of some description to get the same guarantees?

[alternatives]: #alternatives

- **Do nothing.** The Nix language retains its privileged position. Alternative frontends remain second-class.
- **Stable derivation API without sandboxing.** Expose a JSON-based derivation submission endpoint but don't sandbox frontends. This enables alternative languages but doesn't simplify the trust model — each frontend must provide its own purity guarantees.
- **Guix approach.** Build a parallel ecosystem with a different frontend language. This has been done; it works but duplicates enormous effort and fragments the package base.

# Prior art
[prior-art]: #prior-art

- [**RFC #68 — Minimal Daemon**](https://github.com/NixOS/rfcs/pull/68): Proposed modularizing the daemon for better separation. The author mentioned sharing libnixstore with Guix. Closed for lack of interest, but the motivation overlaps with this RFC.
- [**RFC #8 — Readonly Recursive Nix**](https://github.com/NixOS/rfcs/pull/8): Proposed mounting a daemon socket inside build sandboxes — the exact mechanism this RFC generalizes to evaluation. Scoped to builds only.
- [**RFC #17 — Intensional Store**](https://github.com/NixOS/rfcs/pull/17): Proposed restructuring the store itself. Orthogonal but compatible with this proposal.
- [**NixOS/nix#12607**](https://github.com/NixOS/nix/issues/12607): Proposes decoupling the evaluator from libstore, separating the "language proper" from store-dependent primops. A complementary effort that would make sandboxed evaluation easier to implement.
- **Nix 2.32** (October 2025): Began stabilizing the derivation JSON format, removing store directory dependencies from serialized paths. A prerequisite step toward language-agnostic derivation handling.
- **GNU Guix**: Demonstrated that the store concept is language-portable but the ecosystem is not. Had to rebuild its entire package set because there was no language-agnostic store interface to reuse.

# Unresolved questions
[unresolved]: #unresolved-questions

- What is the stable wire format for the daemon protocol? The current protocol is under-documented and has no stability guarantees.
- What is the quantitative performance impact of sandboxing the evaluator, particularly on large evaluations like nixpkgs?
- How should the daemon handle fetch operations (URLs, git repos) on behalf of sandboxed frontends?
- How should source inputs (flake sources, local paths) be provisioned inside the evaluation sandbox?

# Future work
[future-work]: #future-work

- Official daemon protocol client libraries in multiple languages.
- A formal, versioned derivation interchange schema (building on Nix 2.32's JSON work).
- Mechanisms for cross-frontend package dependencies (e.g., a Python-defined package depending on a Nix-defined one).