From c7e1c612ebe44bb531367be86f8faf813c162681 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sat, 22 Nov 2025 00:09:45 +0100 Subject: [PATCH] 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