From 59a566db130611dfb508a89af862ac9b9eeb0e08 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Fri, 21 Nov 2025 23:35:13 +0100 Subject: [PATCH] libexpr: fix stack overflow in deepSeq on deeply nested structures builtins.deepSeq on deeply nested structures (e.g., a linked list with 100,000 elements) caused an uncontrolled OS-level stack overflow with no Nix stack trace. Fix by adding call depth tracking to forceValueDeep, integrating with Nix's existing max-call-depth mechanism. Now produces a controlled "stack overflow; max-call-depth exceeded" error with a proper stack trace. Closes: https://github.com/NixOS/nix/issues/7816 --- src/libexpr/eval.cc | 2 ++ .../eval-fail-deepseq-stack-overflow.err.exp | 30 +++++++++++++++++++ .../lang/eval-fail-deepseq-stack-overflow.nix | 10 +++++++ 3 files changed, 42 insertions(+) create mode 100644 tests/functional/lang/eval-fail-deepseq-stack-overflow.err.exp create mode 100644 tests/functional/lang/eval-fail-deepseq-stack-overflow.nix diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 385d4c05f..837e674cb 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -2188,6 +2188,8 @@ void EvalState::forceValueDeep(Value & v) std::set seen; [&, &state(*this)](this const auto & recurse, Value & v) { + auto _level = state.addCallDepth(v.determinePos(noPos)); + if (!seen.insert(&v).second) return; diff --git a/tests/functional/lang/eval-fail-deepseq-stack-overflow.err.exp b/tests/functional/lang/eval-fail-deepseq-stack-overflow.err.exp new file mode 100644 index 000000000..4cc43ca09 --- /dev/null +++ b/tests/functional/lang/eval-fail-deepseq-stack-overflow.err.exp @@ -0,0 +1,30 @@ +error: + … while calling the 'deepSeq' builtin + at /pwd/lang/eval-fail-deepseq-stack-overflow.nix:8:1: + 7| in + 8| builtins.deepSeq reverseLinkedList ( + | ^ + 9| throw "unexpected success; expected a controlled stack overflow instead" + + … while evaluating the attribute 'tail' + at /pwd/lang/eval-fail-deepseq-stack-overflow.nix:6:67: + 5| long = builtins.genList (x: x) 100000; + 6| reverseLinkedList = builtins.foldl' (tail: head: { inherit head tail; }) null long; + | ^ + 7| in + + (9997 duplicate frames omitted) + + … while evaluating the attribute 'head' + at /pwd/lang/eval-fail-deepseq-stack-overflow.nix:6:62: + 5| long = builtins.genList (x: x) 100000; + 6| reverseLinkedList = builtins.foldl' (tail: head: { inherit head tail; }) null long; + | ^ + 7| in + + error: stack overflow; max-call-depth exceeded + at /pwd/lang/eval-fail-deepseq-stack-overflow.nix:5:28: + 4| let + 5| long = builtins.genList (x: x) 100000; + | ^ + 6| reverseLinkedList = builtins.foldl' (tail: head: { inherit head tail; }) null long; diff --git a/tests/functional/lang/eval-fail-deepseq-stack-overflow.nix b/tests/functional/lang/eval-fail-deepseq-stack-overflow.nix new file mode 100644 index 000000000..08c0fe4e8 --- /dev/null +++ b/tests/functional/lang/eval-fail-deepseq-stack-overflow.nix @@ -0,0 +1,10 @@ +# Test that deepSeq on a deeply nested structure produces a controlled +# stack overflow error rather than a segfault. + +let + long = builtins.genList (x: x) 100000; + reverseLinkedList = builtins.foldl' (tail: head: { inherit head tail; }) null long; +in +builtins.deepSeq reverseLinkedList ( + throw "unexpected success; expected a controlled stack overflow instead" +)