October 13, 2026·7 min read·engineeringnixsecuritydebuggingrpc

Lix repairs daemon trust and debugger behavior

Lix 2.96 fixes daemon security problems while making evaluation errors and debugger sessions easier to understand. Its protocol and Rust changes show how the implementation is evolving.

A debugger that changes the behavior of the expression under inspection makes an already awkward language harder to understand. Lix had that problem with builtins.break. Its 2.96 release, announced on October 13, fixes the evaluation behavior alongside changes to error traces and stack navigation. The same release repairs security problems in the daemon. Both parts concern the reliability of the machinery underneath a declarative configuration, although their consequences differ considerably.

The Bingsu announcement also describes continuing work on Rust integration and a replacement RPC protocol. These changes are at different stages. The security fixes and debugger improvements ship now. The replacement protocol still requires explicit experimental configuration and additional system setup. Treating the whole release as a completed architectural migration would misrepresent what the project has delivered.

I use NixOS and Nix for the desired state of my personal infrastructure. That makes the implementation of evaluation and store operations particularly relevant to me. Declaring a system in a repository remains useful only when the software interpreting that declaration and installing its results behaves predictably.

Repair permission belongs to the daemon

The release notes describe a privilege escalation problem in three daemon operations. AddToStore, AddToStoreNar, and AddMultipleToStore accepted a client request to repair a path even when the client was untrusted. Repair could replace existing contents without an atomic transition, while the store database continued to describe the path as valid.

That combination matters because permission to submit work to a daemon does not imply permission to replace arbitrary existing store contents. A valid database entry cannot protect a path whose contents an untrusted client can change underneath it. The fix rejects repair requests from untrusted clients, consistent with restrictions already applied to other operations.

This is a useful distinction within the declarative model. A configuration describes intended contents, but the daemon mediates which users may alter the realized store. Reproducible evaluation does not itself enforce that authorization rule. The repair fix addresses the authority of the caller at the operation that changes contents.

An archive length became a memory write

A separate fix addresses the NAR parser. The parser patch explains the arithmetic error. String data shared a buffer with its binary length field, and buffer size calculations included that field. An excessive declared length could wrap the calculation. The resulting buffer handling permitted writes outside the intended allocation.

The patch makes a small, concrete change to the parser limit. Its unrestricted string reader previously used the largest representable size_t as its limit. The replacement caps that string reader at 1,048,576 bytes. The commit explains that these strings represent archive tags and symbolic link targets. Tags are small, and link targets already have operating system limits. The bound follows the meaning of the field being parsed.

This does not mean that every file in an archive must fit within one mebibyte. The commit concerns this string reader and its metadata uses. Confusing metadata length with file payload length would make the fix sound far more disruptive than its implementation indicates.

The release notes describe possible code execution, including privilege escalation when the affected process is the system daemon. They also describe substantial exploitation constraints involving weakened address randomization and repeated attempts. Those qualifications belong beside the severity claim. The defect crosses a memory boundary in a privileged parser, but the published account does not describe a universally reliable remote exploit.

The implementation lesson is specific. A length encoded in incoming data must satisfy the needs of its field before it participates in buffer arithmetic. Using the largest machine integer as an apparent upper bound did not provide that semantic restriction. Here the parser already knew enough about the data to impose a much smaller limit.

Breaking into evaluation without changing its result

The debugger fix has an unusually clear implementation history. In change 5422, blokyk explains that builtins.break did not force its argument. The wrapped value could therefore remain opaque to builtins that expected to inspect it, unless another operation forced it first. The fix adds a call to forceValue inside the breakpoint implementation.

That explains why a breakpoint could interfere with a program even though it was intended to help inspect it. Evaluation state had become observable through the wrapper. The change makes the value available to the operation receiving it after the debugger interaction.

The patch also changed stack output in existing tests. Its commit message records updates to the expected traces because forcing the value changes the call stack. That is a meaningful distinction when assessing a debugger repair. An expected trace can change because the implementation evaluates at a different point, while the test still checks the same intended behavior.

I find this more relevant than a general claim that the debugger is friendlier. Nix already requires care about when values are evaluated. A breakpoint should help explain that behavior without adding an accidental difference in the value seen by another builtin.

Error context survives trace truncation

The error reporting work addresses a different source of confusion. Code can use builtins.addErrorContext to attach an explanation to a failing evaluation. The Nixpkgs module system uses such context to identify relevant options and definitions. Previously those explanations could disappear in abbreviated traces.

Blokyk describes the problem in change 5847. Context added outside the first few frames could be truncated. Requesting a full trace recovered it, but buried the explanation among many implementation frames. The module author had supplied useful information that the default display often discarded.

The patch introduces a trace category and marks user context separately. The printer continues looking for those frames after it starts skipping ordinary frames. It also removes frames corresponding merely to the call to addErrorContext, since those calls add little explanatory value.

This changes which information survives abbreviated output. Printing every frame by default would leave the reader to locate the relevant explanation. Preserving author supplied context lets the short trace retain information about the expression or option being evaluated. The category in the implementation also makes the selection explicit instead of relying only on frame position.

Change 5411 extends the debugger stack command to accept relative offsets. An unsigned index still identifies a frame directly. A leading plus or minus requests movement relative to the current frame.

:st +3
:st -4

These commands advance three frame positions and move back four, respectively. The implementation proposal also describes boundary handling. When a relative movement would exceed the available frames, the debugger clamps the result to the nearest boundary and prints a warning.

This reduces bookkeeping during an investigation. A user following neighboring calls does not need to remember the current absolute index and calculate a destination for each move. The distinction between absolute and relative syntax also retains direct access to a known frame.

The examples are debugger commands, not Nix expressions or shell commands. Their signed arguments describe movement through the current stack. That distinction matters because the same punctuation has other meanings in those surrounding languages.

Rust integration reaches the REPL

The rustyline integration replaces the REPL input backend and supplies an example of the mixed language implementation. Its binding declaration exposes a Rust line editor to C++ while retaining a C++ completion interface. The binding includes operations for asking for a line and writing history, with explicit end of input and interruption cases.

The Zngur project provides the C++ and Rust interoperability mechanism. Its purpose is to expose Rust types and functions while preserving their semantics across the language boundary. In Lix, the binding demonstrates a bounded integration point. Completion can remain in existing C++ code while line editing uses a Rust library.

The announcement reports Unicode support and improvements to multiline input. It also identifies remote builder configuration and hash conversion among the areas already using Rust. That is incremental replacement of implementation components. It does not establish that the evaluator or daemon has been rewritten in Rust.

My interest in this direction comes from the operational model I already value in Nix. Better implementation tools are useful when they make that model easier to inspect and maintain. The REPL change has a visible user interface consequence as well as a language choice behind it.

Remote operation remains a transition

The move toward new daemon communication also includes work on an existing protocol. Change 6416 stabilizes ssh-ng, noting that it uses the same protocol as local daemon communication. Change 6415 supplies build option forwarding through a capability check on the remote daemon.

That check matters for mixed installations. A new client sends the extended options only when the remote advertises support. The implementation forwards build timeout, maximum silent time, and failed build retention settings. Existing connectivity therefore does not imply that every pair of versions supports the same controls.

The announcement separately deprecates ssh:// store URIs and states an intention to remove them in the next major release. It retains them in 2.96 without warnings. Meanwhile, the Cap'n Proto replacement remains behind rpc-sockets and needs system configuration beyond enabling the flag. Administrators have a stable ssh-ng option while that separate experimental protocol continues development.