libexpr: fix stack overflow in printAmbiguous on deeply nested structures

printAmbiguous (used by nix-instantiate --eval and nix-env) had a depth
parameter, but all callers passed INT_MAX, effectively disabling the
limit. The function relied on the C++ stack to eventually overflow,
which could cause uncontrolled SIGSEGV crashes on deeply nested
pre-forced structures.

Now printAmbiguous checks depth against max-call-depth (default 10000)
and throws StackOverflowError with a proper trace, consistent with
other recursive value traversal functions.

The function signature is updated to take EvalState& to access the
settings and throw proper errors. The depth parameter now counts up
from 0 instead of down from INT_MAX.
This commit is contained in:
Robert Hensing
2025-11-22 23:29:31 +01:00
parent c2d2a0fe2d
commit fd1ecfbfc8
5 changed files with 23 additions and 14 deletions

View File

@@ -5,6 +5,8 @@
namespace nix {
class EvalState;
/**
* Print a value in the deprecated format used by `nix-instantiate --eval` and
* `nix-env` (for manifests).
@@ -15,7 +17,6 @@ namespace nix {
*
* See: https://github.com/NixOS/nix/issues/9730
*/
void printAmbiguous(
Value & v, const SymbolTable & symbols, std::ostream & str, std::set<const void *> * seen, int depth);
void printAmbiguous(EvalState & state, Value & v, std::ostream & str, std::set<const void *> * seen, size_t depth = 0);
} // namespace nix

View File

@@ -2,19 +2,17 @@
#include "nix/expr/print.hh"
#include "nix/util/signals.hh"
#include "nix/expr/eval.hh"
#include "nix/expr/eval-error.hh"
namespace nix {
// See: https://github.com/NixOS/nix/issues/9730
void printAmbiguous(
Value & v, const SymbolTable & symbols, std::ostream & str, std::set<const void *> * seen, int depth)
void printAmbiguous(EvalState & state, Value & v, std::ostream & str, std::set<const void *> * seen, size_t depth)
{
checkInterrupt();
if (depth <= 0) {
str << "«too deep»";
return;
}
if (depth > state.settings.maxCallDepth)
state.error<StackOverflowError>().atPos(v.determinePos(noPos)).debugThrow();
switch (v.type()) {
case nInt:
str << v.integer();
@@ -36,9 +34,9 @@ void printAmbiguous(
str << "«repeated»";
else {
str << "{ ";
for (auto & i : v.attrs()->lexicographicOrder(symbols)) {
str << symbols[i->name] << " = ";
printAmbiguous(*i->value, symbols, str, seen, depth - 1);
for (auto & i : v.attrs()->lexicographicOrder(state.symbols)) {
str << state.symbols[i->name] << " = ";
printAmbiguous(state, *i->value, str, seen, depth + 1);
str << "; ";
}
str << "}";
@@ -54,7 +52,7 @@ void printAmbiguous(
str << "[ ";
for (auto v2 : v.listView()) {
if (v2)
printAmbiguous(*v2, symbols, str, seen, depth - 1);
printAmbiguous(state, *v2, str, seen, depth + 1);
else
str << "(nullptr)";
str << " ";

View File

@@ -108,7 +108,7 @@ bool createUserEnv(
environment. */
auto manifestFile = ({
std::ostringstream str;
printAmbiguous(manifest, state.symbols, str, nullptr, std::numeric_limits<int>::max());
printAmbiguous(state, manifest, str, nullptr);
StringSource source{str.view()};
state.store->addToStoreFromDump(
source,

View File

@@ -68,7 +68,7 @@ void processExpr(
if (strict)
state.forceValueDeep(vRes);
std::set<const void *> seen;
printAmbiguous(vRes, state.symbols, std::cout, &seen, std::numeric_limits<int>::max());
printAmbiguous(state, vRes, std::cout, &seen);
std::cout << std::endl;
}
} else {

View File

@@ -46,3 +46,13 @@ expectStderr 1 nix-instantiate --expr 'let x = { recurseForDerivations = true; m
echo 'let f = n: { type = "derivation"; name = "test"; system = "x86_64-linux"; meta.nested = f (n + 1); }; in { pkg = f 0; }' > "$TEST_ROOT/deep-meta.nix"
expectStderr 1 nix-env -qa -f "$TEST_ROOT/deep-meta.nix" --json --meta \
| grepQuiet "stack overflow; max-call-depth exceeded"
# Test that nix-instantiate --eval on a pre-forced deep structure (built with
# foldl' to avoid thunks) produces a controlled stack overflow error rather than
# a segfault when printAmbiguous traverses the structure.
# Note: Without the fix, this test may pass if the system stack is large enough.
# The fix ensures we get a controlled error at max-call-depth (default 10000)
# rather than relying on the system stack limit.
# shellcheck disable=SC2016
expectStderr 1 nix-instantiate --eval --expr 'builtins.foldl'\'' (acc: _: { inner = acc; }) null (builtins.genList (x: x) 20000)' \
| grepQuiet "stack overflow; max-call-depth exceeded"