eval: Don't expose partially constructed sets

This turns several instances of unsoundness into runtime infinite
recursion errors

Fixes https://github.com/NixOS/nix/issues/7012,
https://github.com/NixOS/nix/issues/3241

Change-Id: Id8d352c5a78ef08d8913f07fe83f55c875684714
This commit is contained in:
piegames
2026-01-31 15:32:27 +01:00
parent 7bd3202175
commit f289462c59
5 changed files with 93 additions and 5 deletions
+14 -5
View File
@@ -158,13 +158,22 @@ void ExprSet::eval(EvalState & state, Env & env, Value & v)
/* Dynamic attrs apply *after* rec and __overrides. */
for (auto & i : dynamicAttrs) {
/* Before evaluating dynamic attrs, we blackhole the output attrset and only restore it after the operation.
* This is to avoid exposing the partially constructed set as a value, see
* http://github.com/NixOS/nix/issues/7012. Any accesses to the output attrset will thus infrec.
*/
Value vBackup = v;
Value nameVal;
i.nameExpr->eval(state, *dynamicEnv, nameVal);
state.forceValue(nameVal, i.pos);
if (nameVal.type() == nNull) {
continue;
{
KJ_DEFER(v = vBackup);
v = Value{NewValueAs::blackhole};
i.nameExpr->eval(state, *dynamicEnv, nameVal);
state.forceValue(nameVal, i.pos);
if (nameVal.type() == nNull) {
continue;
}
state.forceStringNoCtx(nameVal, i.pos, "while evaluating the name of a dynamic attribute");
}
state.forceStringNoCtx(nameVal, i.pos, "while evaluating the name of a dynamic attribute");
auto nameSym = state.ctx.symbols.create(nameVal.str());
auto j = v.attrs()->get(nameSym);
if (j) {
@@ -0,0 +1,20 @@
error:
… while evaluating a
at /pwd/in.nix:1:84:
1| let b = builtins.toJSON a; a = { foo = "bar"; "${builtins.seq b "baz"}" = b; }; in a
| ^
2|
… while calling the 'seq' builtin
at /pwd/in.nix:1:50:
1| let b = builtins.toJSON a; a = { foo = "bar"; "${builtins.seq b "baz"}" = b; }; in a
| ^
2|
… while calling the 'toJSON' builtin
at /pwd/in.nix:1:9:
1| let b = builtins.toJSON a; a = { foo = "bar"; "${builtins.seq b "baz"}" = b; }; in a
| ^
2|
error: infinite recursion encountered
@@ -0,0 +1,50 @@
error:
… from call site
at /pwd/in.nix:4:11:
3| b = { ${lenName a} = null; };
4| a = { ${lenName b} = null; };
| ^
5|
… while calling 'lenName'
at /pwd/in.nix:2:13:
1| let
2| lenName = x: toString (builtins.length (builtins.attrNames x));
| ^
3| b = { ${lenName a} = null; };
… while calling the 'toString' builtin
at /pwd/in.nix:2:16:
1| let
2| lenName = x: toString (builtins.length (builtins.attrNames x));
| ^
3| b = { ${lenName a} = null; };
… while calling the 'length' builtin
at /pwd/in.nix:2:26:
1| let
2| lenName = x: toString (builtins.length (builtins.attrNames x));
| ^
3| b = { ${lenName a} = null; };
… while evaluating the first argument passed to builtins.length
… while calling the 'attrNames' builtin
at /pwd/in.nix:2:43:
1| let
2| lenName = x: toString (builtins.length (builtins.attrNames x));
| ^
3| b = { ${lenName a} = null; };
… while evaluating the argument passed to builtins.attrNames
… from call site
at /pwd/in.nix:3:11:
2| lenName = x: toString (builtins.length (builtins.attrNames x));
3| b = { ${lenName a} = null; };
| ^
4| a = { ${lenName b} = null; };
(6 duplicate frames omitted)
error: infinite recursion encountered
@@ -0,0 +1 @@
let b = builtins.toJSON a; a = { foo = "bar"; "${builtins.seq b "baz"}" = b; }; in a
@@ -0,0 +1,8 @@
let
lenName = x: toString (builtins.length (builtins.attrNames x));
b = { ${lenName a} = null; };
a = { ${lenName b} = null; };
in
[ a b ]