From 59a566db130611dfb508a89af862ac9b9eeb0e08 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Fri, 21 Nov 2025 23:35:13 +0100 Subject: [PATCH 1/3] 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" +) From a812b6c6e628f878c485a1b78e3fc7ee90352c2c Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Fri, 21 Nov 2025 23:51:07 +0100 Subject: [PATCH 2/3] libexpr: add list index to deepSeq error traces When deepSeq encounters an error while evaluating a list element, the error trace now includes the list index, making it easier to locate the problematic element. --- src/libexpr/eval.cc | 9 ++++++- .../lang/eval-fail-deepseq-list-attr.err.exp | 25 +++++++++++++++++++ .../lang/eval-fail-deepseq-list-attr.nix | 10 ++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 tests/functional/lang/eval-fail-deepseq-list-attr.err.exp create mode 100644 tests/functional/lang/eval-fail-deepseq-list-attr.nix diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 837e674cb..0a6b199bf 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -2216,8 +2216,15 @@ void EvalState::forceValueDeep(Value & v) } else if (v.isList()) { + size_t index = 0; for (auto v2 : v.listView()) - recurse(*v2); + try { + recurse(*v2); + index++; + } catch (Error & e) { + state.addErrorTrace(e, "while evaluating list element at index %1%", index); + throw; + } } }(v); } diff --git a/tests/functional/lang/eval-fail-deepseq-list-attr.err.exp b/tests/functional/lang/eval-fail-deepseq-list-attr.err.exp new file mode 100644 index 000000000..9abd937ba --- /dev/null +++ b/tests/functional/lang/eval-fail-deepseq-list-attr.err.exp @@ -0,0 +1,25 @@ +error: + … while calling the 'deepSeq' builtin + at /pwd/lang/eval-fail-deepseq-list-attr.nix:3:1: + 2| + 3| builtins.deepSeq [ + | ^ + 4| 1 + + … while evaluating list element at index 1 + + … while evaluating the attribute 'b' + at /pwd/lang/eval-fail-deepseq-list-attr.nix:7:5: + 6| a = 2; + 7| b = throw "error in attr in list element"; + | ^ + 8| } + + … while calling the 'throw' builtin + at /pwd/lang/eval-fail-deepseq-list-attr.nix:7:9: + 6| a = 2; + 7| b = throw "error in attr in list element"; + | ^ + 8| } + + error: error in attr in list element diff --git a/tests/functional/lang/eval-fail-deepseq-list-attr.nix b/tests/functional/lang/eval-fail-deepseq-list-attr.nix new file mode 100644 index 000000000..5ffd8c196 --- /dev/null +++ b/tests/functional/lang/eval-fail-deepseq-list-attr.nix @@ -0,0 +1,10 @@ +# Test that deepSeq reports list index and attribute name in error traces. + +builtins.deepSeq [ + 1 + { + a = 2; + b = throw "error in attr in list element"; + } + 3 +] "unexpected success" From c7e1c612ebe44bb531367be86f8faf813c162681 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sat, 22 Nov 2025 00:09:45 +0100 Subject: [PATCH 3/3] libexpr: fix stack overflow in toJSON on deeply nested structures Similar to the deepSeq fix, toJSON on deeply nested structures caused an uncontrolled OS-level stack overflow. Fix by adding call depth tracking to printValueAsJSON. --- src/libexpr/value-to-json.cc | 2 + ...ion-structuredAttrs-stack-overflow.err.exp | 54 +++++++++++++++++++ ...ivation-structuredAttrs-stack-overflow.nix | 15 ++++++ .../eval-fail-toJSON-stack-overflow.err.exp | 30 +++++++++++ .../lang/eval-fail-toJSON-stack-overflow.nix | 8 +++ 5 files changed, 109 insertions(+) create mode 100644 tests/functional/lang/eval-fail-derivation-structuredAttrs-stack-overflow.err.exp create mode 100644 tests/functional/lang/eval-fail-derivation-structuredAttrs-stack-overflow.nix create mode 100644 tests/functional/lang/eval-fail-toJSON-stack-overflow.err.exp create mode 100644 tests/functional/lang/eval-fail-toJSON-stack-overflow.nix diff --git a/src/libexpr/value-to-json.cc b/src/libexpr/value-to-json.cc index 03b14b83c..b2cc482c6 100644 --- a/src/libexpr/value-to-json.cc +++ b/src/libexpr/value-to-json.cc @@ -16,6 +16,8 @@ json printValueAsJSON( { checkInterrupt(); + auto _level = state.addCallDepth(pos); + if (strict) state.forceValue(v, pos); diff --git a/tests/functional/lang/eval-fail-derivation-structuredAttrs-stack-overflow.err.exp b/tests/functional/lang/eval-fail-derivation-structuredAttrs-stack-overflow.err.exp new file mode 100644 index 000000000..c61eab0aa --- /dev/null +++ b/tests/functional/lang/eval-fail-derivation-structuredAttrs-stack-overflow.err.exp @@ -0,0 +1,54 @@ +error: + … while evaluating the attribute 'outPath' + at «nix-internal»/derivation-internal.nix:50:7: + 49| value = commonAttrs // { + 50| outPath = builtins.getAttr outputName strict; + | ^ + 51| drvPath = strict.drvPath; + + … while calling the 'getAttr' builtin + at «nix-internal»/derivation-internal.nix:50:17: + 49| value = commonAttrs // { + 50| outPath = builtins.getAttr outputName strict; + | ^ + 51| drvPath = strict.drvPath; + + … while calling the 'derivationStrict' builtin + at «nix-internal»/derivation-internal.nix:37:12: + 36| + 37| strict = derivationStrict drvAttrs; + | ^ + 38| + + … while evaluating derivation 'test' + whose name attribute is located at /pwd/lang/eval-fail-derivation-structuredAttrs-stack-overflow.nix:5:3 + + … while evaluating attribute 'nested' of derivation 'test' + at /pwd/lang/eval-fail-derivation-structuredAttrs-stack-overflow.nix:9:3: + 8| __structuredAttrs = true; + 9| nested = + | ^ + 10| let + + … while evaluating attribute 'tail' + at /pwd/lang/eval-fail-derivation-structuredAttrs-stack-overflow.nix:12:71: + 11| long = builtins.genList (x: x) 100000; + 12| reverseLinkedList = builtins.foldl' (tail: head: { inherit head tail; }) null long; + | ^ + 13| in + + (9994 duplicate frames omitted) + + … while evaluating attribute 'head' + at /pwd/lang/eval-fail-derivation-structuredAttrs-stack-overflow.nix:12:66: + 11| long = builtins.genList (x: x) 100000; + 12| reverseLinkedList = builtins.foldl' (tail: head: { inherit head tail; }) null long; + | ^ + 13| in + + error: stack overflow; max-call-depth exceeded + at /pwd/lang/eval-fail-derivation-structuredAttrs-stack-overflow.nix:12:66: + 11| long = builtins.genList (x: x) 100000; + 12| reverseLinkedList = builtins.foldl' (tail: head: { inherit head tail; }) null long; + | ^ + 13| in diff --git a/tests/functional/lang/eval-fail-derivation-structuredAttrs-stack-overflow.nix b/tests/functional/lang/eval-fail-derivation-structuredAttrs-stack-overflow.nix new file mode 100644 index 000000000..c80950f1e --- /dev/null +++ b/tests/functional/lang/eval-fail-derivation-structuredAttrs-stack-overflow.nix @@ -0,0 +1,15 @@ +# Test that derivations with __structuredAttrs and deeply nested structures +# produce a controlled stack overflow error rather than a segfault. + +derivation { + name = "test"; + system = "x86_64-linux"; + builder = "/bin/sh"; + __structuredAttrs = true; + nested = + let + long = builtins.genList (x: x) 100000; + reverseLinkedList = builtins.foldl' (tail: head: { inherit head tail; }) null long; + in + reverseLinkedList; +} diff --git a/tests/functional/lang/eval-fail-toJSON-stack-overflow.err.exp b/tests/functional/lang/eval-fail-toJSON-stack-overflow.err.exp new file mode 100644 index 000000000..cda77331d --- /dev/null +++ b/tests/functional/lang/eval-fail-toJSON-stack-overflow.err.exp @@ -0,0 +1,30 @@ +error: + … while calling the 'toJSON' builtin + at /pwd/lang/eval-fail-toJSON-stack-overflow.nix:8:1: + 7| in + 8| builtins.toJSON reverseLinkedList + | ^ + 9| + + … while evaluating attribute 'tail' + at /pwd/lang/eval-fail-toJSON-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 attribute 'head' + at /pwd/lang/eval-fail-toJSON-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-toJSON-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 diff --git a/tests/functional/lang/eval-fail-toJSON-stack-overflow.nix b/tests/functional/lang/eval-fail-toJSON-stack-overflow.nix new file mode 100644 index 000000000..135ed0a17 --- /dev/null +++ b/tests/functional/lang/eval-fail-toJSON-stack-overflow.nix @@ -0,0 +1,8 @@ +# Test that toJSON 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.toJSON reverseLinkedList