Cap UDT expansion depth in arg_value_name to prevent stack overflow on recursive specs - #2679

Open
Galmanus wants to merge 1 commit into
stellar:mainfrom
Galmanus:fix/2445-recursive-spec-value-name
Open

Cap UDT expansion depth in arg_value_name to prevent stack overflow on recursive specs#2679
Galmanus wants to merge 1 commit into
stellar:mainfrom
Galmanus:fix/2445-recursive-spec-value-name

Conversation

@Galmanus

Copy link
Copy Markdown

Fixes#2445.

Spec::arg_value_name threads a depth counter but never checks it. A contract whose spec contains a recursive type (e.g. struct TreeNode { children: Vec<TreeNode> } — accepted by the SDK and the network) sends the resolver into unbounded recursion TreeNode → Vec<TreeNode> → TreeNode → …, overflowing the stack on any contract invoke against the contract, including -- --help. Since anyone can deploy such a contract, this is a client-side DoS on anyone who tries to interact with it.

Fix

UDTs are the only types resolved by name (self.find(...)), so any cycle must pass through the ScType::Udt arm. This caps the expansion depth there and renders the type's name past the cap — the same approach example_udts already uses for examples (depth > 2None), except returning the name keeps the outer value name intact: a None propagates through the ? in every caller and would erase the entire help string.

For the reproduction from the issue, --help now renders the argument as:

{ children: Array<{ children: Array<{ children: Array<TreeNode>, value: u32 }>, value: u32 }>, value: u32 }

Tests

  • arg_value_name_terminates_on_self_recursive_struct — the issue's TreeNode shape; aborted with fatal runtime error: stack overflow before the fix.
  • arg_value_name_terminates_on_mutually_recursive_structsA ↔ B cycle.
  • arg_value_name_expands_non_recursive_nested_structs — output unchanged for non-recursive specs.
  • build_custom_cmd_terminates_on_recursive_spec_type (soroban-cli) — end-to-end guard at the clap-command level, the surface the issue's repro crashes.

Related

#2668 capped XDR decode depth for deeply-nested specs. A cycle through UDT name references never nests deeply in the encoded XDR, so it needs this separate guard.

CopilotAI balanced review requested due to automatic review settings August 14, 2026 23:01
@github-project-automationgithub-project-automationBot moved this to Backlog (Not Ready) in DevXAug 14, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds safeguards and regression coverage to prevent stack overflows when rendering CLI argument value names for contracts with recursive UDTs in their spec (issue #2445).

Changes:

  • Cap UDT expansion depth in Spec::arg_value_name to prevent unbounded recursion on recursive type graphs.
  • Add spec-tools regression tests for self-recursive and mutually-recursive structs, and ensure non-recursive nesting still expands fully.
  • Add a soroban-cli regression test ensuring build_custom_cmd(...).render_long_help() succeeds for a recursive spec type.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
cmd/soroban-cli/src/commands/contract/arg_parsing.rsAdds a regression test to ensure clap help generation doesn’t crash when a contract spec includes recursive types.
cmd/crates/soroban-spec-tools/src/lib.rsAdds a maximum recursion/expansion depth for UDT value-name rendering and tests for recursive and non-recursive cases.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

// UDTs are the only types resolved by name, so any cycle in a
// recursive spec passes through here; cap the expansion depth
// and fall back to the type's name (#2445).
if depth > Self::MAX_UDT_VALUE_NAME_DEPTH {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The > check already matches the doc comment, so I'll keep it. The comment defines the constant as "the maximum nesting depth at which user-defined types are still expanded" (L1155-1156) and says "past this depth the type's name is rendered" (L1161) — "past" meaning strictly greater than the max. So expansion is intended to still happen at depth == MAX_UDT_VALUE_NAME_DEPTH and stop only beyond it, which is exactly what > does (depth 4 expands, depth 5 renders the name).

Switching to >= would stop expansion at depth 4, making the true maximum expansion depth 3 — that contradicts both the constant's name and the "still expanded" wording. In other words >= would introduce the off-by-one rather than remove it.

Fair point that the current tests don't actually pin this boundary — they only assert termination and that the outer value name survives, so they'd pass under either operator. I can add an assertion that fixes the expansion depth to the documented value to make the intended cutoff unambiguous.

…n recursive specs
`Spec::arg_value_name` threads a `depth` counter but never checks it. A
contract whose spec contains a recursive type — e.g.
`struct TreeNode { children: Vec<TreeNode> }`, which the SDK and the
network accept — sent the resolver into unbounded recursion, overflowing
the stack on every `contract invoke` against the contract, including
`-- --help`. Anyone can deploy such a contract, so this crashed the CLI
of anyone who tried to interact with it.
UDTs are the only types resolved by name, so any cycle must pass through
the `ScType::Udt` arm; cap the expansion depth there and render the
type's name past the cap. Returning `None` instead would erase the
whole value name, since `None` propagates through the `?` in every
caller. `example_udts` already bounds example rendering the same way.
Complements stellar#2668, which capped XDR decode depth for deeply-nested
specs; a cycle through UDT name references never nests deeply in the
encoded XDR, so it needs this separate guard.
Fixesstellar#2445
CopilotAI review requested due to automatic review settings September 1, 2026 23:05
@Galmanus
Galmanusforce-pushed the fix/2445-recursive-spec-value-name branch from af4e67f to 3a1c8fbCompareSeptember 1, 2026 23:05

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines +1248 to +1249
if depth > Self::MAX_UDT_VALUE_NAME_DEPTH {
return Some(name.to_utf8_string_lossy());
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog (Not Ready)

Development

Successfully merging this pull request may close these issues.

Stack overflow crash on contracts with recursive types in spec

2 participants

@Galmanus
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Cap UDT expansion depth in arg_value_name to prevent stack overflow on recursive specs - #2679

Open
Galmanus wants to merge 1 commit into
stellar:mainfrom
Galmanus:fix/2445-recursive-spec-value-name
Open

Cap UDT expansion depth in arg_value_name to prevent stack overflow on recursive specs#2679
Galmanus wants to merge 1 commit into
stellar:mainfrom
Galmanus:fix/2445-recursive-spec-value-name

Conversation

@Galmanus

Copy link
Copy Markdown

Fixes#2445.

Spec::arg_value_name threads a depth counter but never checks it. A contract whose spec contains a recursive type (e.g. struct TreeNode { children: Vec<TreeNode> } — accepted by the SDK and the network) sends the resolver into unbounded recursion TreeNode → Vec<TreeNode> → TreeNode → …, overflowing the stack on any contract invoke against the contract, including -- --help. Since anyone can deploy such a contract, this is a client-side DoS on anyone who tries to interact with it.

Fix

UDTs are the only types resolved by name (self.find(...)), so any cycle must pass through the ScType::Udt arm. This caps the expansion depth there and renders the type's name past the cap — the same approach example_udts already uses for examples (depth > 2None), except returning the name keeps the outer value name intact: a None propagates through the ? in every caller and would erase the entire help string.

For the reproduction from the issue, --help now renders the argument as:

{ children: Array<{ children: Array<{ children: Array<TreeNode>, value: u32 }>, value: u32 }>, value: u32 }

Tests

  • arg_value_name_terminates_on_self_recursive_struct — the issue's TreeNode shape; aborted with fatal runtime error: stack overflow before the fix.
  • arg_value_name_terminates_on_mutually_recursive_structsA ↔ B cycle.
  • arg_value_name_expands_non_recursive_nested_structs — output unchanged for non-recursive specs.
  • build_custom_cmd_terminates_on_recursive_spec_type (soroban-cli) — end-to-end guard at the clap-command level, the surface the issue's repro crashes.

Related

#2668 capped XDR decode depth for deeply-nested specs. A cycle through UDT name references never nests deeply in the encoded XDR, so it needs this separate guard.

CopilotAI balanced review requested due to automatic review settings August 14, 2026 23:01
@github-project-automationgithub-project-automationBot moved this to Backlog (Not Ready) in DevXAug 14, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds safeguards and regression coverage to prevent stack overflows when rendering CLI argument value names for contracts with recursive UDTs in their spec (issue #2445).

Changes:

  • Cap UDT expansion depth in Spec::arg_value_name to prevent unbounded recursion on recursive type graphs.
  • Add spec-tools regression tests for self-recursive and mutually-recursive structs, and ensure non-recursive nesting still expands fully.
  • Add a soroban-cli regression test ensuring build_custom_cmd(...).render_long_help() succeeds for a recursive spec type.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
cmd/soroban-cli/src/commands/contract/arg_parsing.rsAdds a regression test to ensure clap help generation doesn’t crash when a contract spec includes recursive types.
cmd/crates/soroban-spec-tools/src/lib.rsAdds a maximum recursion/expansion depth for UDT value-name rendering and tests for recursive and non-recursive cases.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

// UDTs are the only types resolved by name, so any cycle in a
// recursive spec passes through here; cap the expansion depth
// and fall back to the type's name (#2445).
if depth > Self::MAX_UDT_VALUE_NAME_DEPTH {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The > check already matches the doc comment, so I'll keep it. The comment defines the constant as "the maximum nesting depth at which user-defined types are still expanded" (L1155-1156) and says "past this depth the type's name is rendered" (L1161) — "past" meaning strictly greater than the max. So expansion is intended to still happen at depth == MAX_UDT_VALUE_NAME_DEPTH and stop only beyond it, which is exactly what > does (depth 4 expands, depth 5 renders the name).

Switching to >= would stop expansion at depth 4, making the true maximum expansion depth 3 — that contradicts both the constant's name and the "still expanded" wording. In other words >= would introduce the off-by-one rather than remove it.

Fair point that the current tests don't actually pin this boundary — they only assert termination and that the outer value name survives, so they'd pass under either operator. I can add an assertion that fixes the expansion depth to the documented value to make the intended cutoff unambiguous.

…n recursive specs
`Spec::arg_value_name` threads a `depth` counter but never checks it. A
contract whose spec contains a recursive type — e.g.
`struct TreeNode { children: Vec<TreeNode> }`, which the SDK and the
network accept — sent the resolver into unbounded recursion, overflowing
the stack on every `contract invoke` against the contract, including
`-- --help`. Anyone can deploy such a contract, so this crashed the CLI
of anyone who tried to interact with it.
UDTs are the only types resolved by name, so any cycle must pass through
the `ScType::Udt` arm; cap the expansion depth there and render the
type's name past the cap. Returning `None` instead would erase the
whole value name, since `None` propagates through the `?` in every
caller. `example_udts` already bounds example rendering the same way.
Complements stellar#2668, which capped XDR decode depth for deeply-nested
specs; a cycle through UDT name references never nests deeply in the
encoded XDR, so it needs this separate guard.
Fixesstellar#2445
CopilotAI review requested due to automatic review settings September 1, 2026 23:05
@Galmanus
Galmanusforce-pushed the fix/2445-recursive-spec-value-name branch from af4e67f to 3a1c8fbCompareSeptember 1, 2026 23:05

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines +1248 to +1249
if depth > Self::MAX_UDT_VALUE_NAME_DEPTH {
return Some(name.to_utf8_string_lossy());
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog (Not Ready)

Development

Successfully merging this pull request may close these issues.

Stack overflow crash on contracts with recursive types in spec

2 participants

@Galmanus
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Cap UDT expansion depth in arg_value_name to prevent stack overflow on recursive specs - #2679

Open
Galmanus wants to merge 1 commit into
stellar:mainfrom
Galmanus:fix/2445-recursive-spec-value-name
Open

Cap UDT expansion depth in arg_value_name to prevent stack overflow on recursive specs#2679
Galmanus wants to merge 1 commit into
stellar:mainfrom
Galmanus:fix/2445-recursive-spec-value-name

Conversation

@Galmanus

Copy link
Copy Markdown

Fixes#2445.

Spec::arg_value_name threads a depth counter but never checks it. A contract whose spec contains a recursive type (e.g. struct TreeNode { children: Vec<TreeNode> } — accepted by the SDK and the network) sends the resolver into unbounded recursion TreeNode → Vec<TreeNode> → TreeNode → …, overflowing the stack on any contract invoke against the contract, including -- --help. Since anyone can deploy such a contract, this is a client-side DoS on anyone who tries to interact with it.

Fix

UDTs are the only types resolved by name (self.find(...)), so any cycle must pass through the ScType::Udt arm. This caps the expansion depth there and renders the type's name past the cap — the same approach example_udts already uses for examples (depth > 2None), except returning the name keeps the outer value name intact: a None propagates through the ? in every caller and would erase the entire help string.

For the reproduction from the issue, --help now renders the argument as:

{ children: Array<{ children: Array<{ children: Array<TreeNode>, value: u32 }>, value: u32 }>, value: u32 }

Tests

  • arg_value_name_terminates_on_self_recursive_struct — the issue's TreeNode shape; aborted with fatal runtime error: stack overflow before the fix.
  • arg_value_name_terminates_on_mutually_recursive_structsA ↔ B cycle.
  • arg_value_name_expands_non_recursive_nested_structs — output unchanged for non-recursive specs.
  • build_custom_cmd_terminates_on_recursive_spec_type (soroban-cli) — end-to-end guard at the clap-command level, the surface the issue's repro crashes.

Related

#2668 capped XDR decode depth for deeply-nested specs. A cycle through UDT name references never nests deeply in the encoded XDR, so it needs this separate guard.

CopilotAI balanced review requested due to automatic review settings August 14, 2026 23:01
@github-project-automationgithub-project-automationBot moved this to Backlog (Not Ready) in DevXAug 14, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds safeguards and regression coverage to prevent stack overflows when rendering CLI argument value names for contracts with recursive UDTs in their spec (issue #2445).

Changes:

  • Cap UDT expansion depth in Spec::arg_value_name to prevent unbounded recursion on recursive type graphs.
  • Add spec-tools regression tests for self-recursive and mutually-recursive structs, and ensure non-recursive nesting still expands fully.
  • Add a soroban-cli regression test ensuring build_custom_cmd(...).render_long_help() succeeds for a recursive spec type.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
cmd/soroban-cli/src/commands/contract/arg_parsing.rsAdds a regression test to ensure clap help generation doesn’t crash when a contract spec includes recursive types.
cmd/crates/soroban-spec-tools/src/lib.rsAdds a maximum recursion/expansion depth for UDT value-name rendering and tests for recursive and non-recursive cases.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

// UDTs are the only types resolved by name, so any cycle in a
// recursive spec passes through here; cap the expansion depth
// and fall back to the type's name (#2445).
if depth > Self::MAX_UDT_VALUE_NAME_DEPTH {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The > check already matches the doc comment, so I'll keep it. The comment defines the constant as "the maximum nesting depth at which user-defined types are still expanded" (L1155-1156) and says "past this depth the type's name is rendered" (L1161) — "past" meaning strictly greater than the max. So expansion is intended to still happen at depth == MAX_UDT_VALUE_NAME_DEPTH and stop only beyond it, which is exactly what > does (depth 4 expands, depth 5 renders the name).

Switching to >= would stop expansion at depth 4, making the true maximum expansion depth 3 — that contradicts both the constant's name and the "still expanded" wording. In other words >= would introduce the off-by-one rather than remove it.

Fair point that the current tests don't actually pin this boundary — they only assert termination and that the outer value name survives, so they'd pass under either operator. I can add an assertion that fixes the expansion depth to the documented value to make the intended cutoff unambiguous.

…n recursive specs
`Spec::arg_value_name` threads a `depth` counter but never checks it. A
contract whose spec contains a recursive type — e.g.
`struct TreeNode { children: Vec<TreeNode> }`, which the SDK and the
network accept — sent the resolver into unbounded recursion, overflowing
the stack on every `contract invoke` against the contract, including
`-- --help`. Anyone can deploy such a contract, so this crashed the CLI
of anyone who tried to interact with it.
UDTs are the only types resolved by name, so any cycle must pass through
the `ScType::Udt` arm; cap the expansion depth there and render the
type's name past the cap. Returning `None` instead would erase the
whole value name, since `None` propagates through the `?` in every
caller. `example_udts` already bounds example rendering the same way.
Complements stellar#2668, which capped XDR decode depth for deeply-nested
specs; a cycle through UDT name references never nests deeply in the
encoded XDR, so it needs this separate guard.
Fixesstellar#2445
CopilotAI review requested due to automatic review settings September 1, 2026 23:05
@Galmanus
Galmanusforce-pushed the fix/2445-recursive-spec-value-name branch from af4e67f to 3a1c8fbCompareSeptember 1, 2026 23:05

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines +1248 to +1249
if depth > Self::MAX_UDT_VALUE_NAME_DEPTH {
return Some(name.to_utf8_string_lossy());
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog (Not Ready)

Development

Successfully merging this pull request may close these issues.

Stack overflow crash on contracts with recursive types in spec

2 participants

@Galmanus
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Cap UDT expansion depth in arg_value_name to prevent stack overflow on recursive specs - #2679

Open
Galmanus wants to merge 1 commit into
stellar:mainfrom
Galmanus:fix/2445-recursive-spec-value-name
Open

Cap UDT expansion depth in arg_value_name to prevent stack overflow on recursive specs#2679
Galmanus wants to merge 1 commit into
stellar:mainfrom
Galmanus:fix/2445-recursive-spec-value-name

Conversation

@Galmanus

Copy link
Copy Markdown

Fixes#2445.

Spec::arg_value_name threads a depth counter but never checks it. A contract whose spec contains a recursive type (e.g. struct TreeNode { children: Vec<TreeNode> } — accepted by the SDK and the network) sends the resolver into unbounded recursion TreeNode → Vec<TreeNode> → TreeNode → …, overflowing the stack on any contract invoke against the contract, including -- --help. Since anyone can deploy such a contract, this is a client-side DoS on anyone who tries to interact with it.

Fix

UDTs are the only types resolved by name (self.find(...)), so any cycle must pass through the ScType::Udt arm. This caps the expansion depth there and renders the type's name past the cap — the same approach example_udts already uses for examples (depth > 2None), except returning the name keeps the outer value name intact: a None propagates through the ? in every caller and would erase the entire help string.

For the reproduction from the issue, --help now renders the argument as:

{ children: Array<{ children: Array<{ children: Array<TreeNode>, value: u32 }>, value: u32 }>, value: u32 }

Tests

  • arg_value_name_terminates_on_self_recursive_struct — the issue's TreeNode shape; aborted with fatal runtime error: stack overflow before the fix.
  • arg_value_name_terminates_on_mutually_recursive_structsA ↔ B cycle.
  • arg_value_name_expands_non_recursive_nested_structs — output unchanged for non-recursive specs.
  • build_custom_cmd_terminates_on_recursive_spec_type (soroban-cli) — end-to-end guard at the clap-command level, the surface the issue's repro crashes.

Related

#2668 capped XDR decode depth for deeply-nested specs. A cycle through UDT name references never nests deeply in the encoded XDR, so it needs this separate guard.

CopilotAI balanced review requested due to automatic review settings August 14, 2026 23:01
@github-project-automationgithub-project-automationBot moved this to Backlog (Not Ready) in DevXAug 14, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds safeguards and regression coverage to prevent stack overflows when rendering CLI argument value names for contracts with recursive UDTs in their spec (issue #2445).

Changes:

  • Cap UDT expansion depth in Spec::arg_value_name to prevent unbounded recursion on recursive type graphs.
  • Add spec-tools regression tests for self-recursive and mutually-recursive structs, and ensure non-recursive nesting still expands fully.
  • Add a soroban-cli regression test ensuring build_custom_cmd(...).render_long_help() succeeds for a recursive spec type.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
cmd/soroban-cli/src/commands/contract/arg_parsing.rsAdds a regression test to ensure clap help generation doesn’t crash when a contract spec includes recursive types.
cmd/crates/soroban-spec-tools/src/lib.rsAdds a maximum recursion/expansion depth for UDT value-name rendering and tests for recursive and non-recursive cases.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

// UDTs are the only types resolved by name, so any cycle in a
// recursive spec passes through here; cap the expansion depth
// and fall back to the type's name (#2445).
if depth > Self::MAX_UDT_VALUE_NAME_DEPTH {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The > check already matches the doc comment, so I'll keep it. The comment defines the constant as "the maximum nesting depth at which user-defined types are still expanded" (L1155-1156) and says "past this depth the type's name is rendered" (L1161) — "past" meaning strictly greater than the max. So expansion is intended to still happen at depth == MAX_UDT_VALUE_NAME_DEPTH and stop only beyond it, which is exactly what > does (depth 4 expands, depth 5 renders the name).

Switching to >= would stop expansion at depth 4, making the true maximum expansion depth 3 — that contradicts both the constant's name and the "still expanded" wording. In other words >= would introduce the off-by-one rather than remove it.

Fair point that the current tests don't actually pin this boundary — they only assert termination and that the outer value name survives, so they'd pass under either operator. I can add an assertion that fixes the expansion depth to the documented value to make the intended cutoff unambiguous.

…n recursive specs
`Spec::arg_value_name` threads a `depth` counter but never checks it. A
contract whose spec contains a recursive type — e.g.
`struct TreeNode { children: Vec<TreeNode> }`, which the SDK and the
network accept — sent the resolver into unbounded recursion, overflowing
the stack on every `contract invoke` against the contract, including
`-- --help`. Anyone can deploy such a contract, so this crashed the CLI
of anyone who tried to interact with it.
UDTs are the only types resolved by name, so any cycle must pass through
the `ScType::Udt` arm; cap the expansion depth there and render the
type's name past the cap. Returning `None` instead would erase the
whole value name, since `None` propagates through the `?` in every
caller. `example_udts` already bounds example rendering the same way.
Complements stellar#2668, which capped XDR decode depth for deeply-nested
specs; a cycle through UDT name references never nests deeply in the
encoded XDR, so it needs this separate guard.
Fixesstellar#2445
CopilotAI review requested due to automatic review settings September 1, 2026 23:05
@Galmanus
Galmanusforce-pushed the fix/2445-recursive-spec-value-name branch from af4e67f to 3a1c8fbCompareSeptember 1, 2026 23:05

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines +1248 to +1249
if depth > Self::MAX_UDT_VALUE_NAME_DEPTH {
return Some(name.to_utf8_string_lossy());
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog (Not Ready)

Development

Successfully merging this pull request may close these issues.

Stack overflow crash on contracts with recursive types in spec

2 participants

@Galmanus
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Cap UDT expansion depth in arg_value_name to prevent stack overflow on recursive specs - #2679

Open
Galmanus wants to merge 1 commit into
stellar:mainfrom
Galmanus:fix/2445-recursive-spec-value-name
Open

Cap UDT expansion depth in arg_value_name to prevent stack overflow on recursive specs#2679
Galmanus wants to merge 1 commit into
stellar:mainfrom
Galmanus:fix/2445-recursive-spec-value-name

Conversation

@Galmanus

Copy link
Copy Markdown

Fixes#2445.

Spec::arg_value_name threads a depth counter but never checks it. A contract whose spec contains a recursive type (e.g. struct TreeNode { children: Vec<TreeNode> } — accepted by the SDK and the network) sends the resolver into unbounded recursion TreeNode → Vec<TreeNode> → TreeNode → …, overflowing the stack on any contract invoke against the contract, including -- --help. Since anyone can deploy such a contract, this is a client-side DoS on anyone who tries to interact with it.

Fix

UDTs are the only types resolved by name (self.find(...)), so any cycle must pass through the ScType::Udt arm. This caps the expansion depth there and renders the type's name past the cap — the same approach example_udts already uses for examples (depth > 2None), except returning the name keeps the outer value name intact: a None propagates through the ? in every caller and would erase the entire help string.

For the reproduction from the issue, --help now renders the argument as:

{ children: Array<{ children: Array<{ children: Array<TreeNode>, value: u32 }>, value: u32 }>, value: u32 }

Tests

  • arg_value_name_terminates_on_self_recursive_struct — the issue's TreeNode shape; aborted with fatal runtime error: stack overflow before the fix.
  • arg_value_name_terminates_on_mutually_recursive_structsA ↔ B cycle.
  • arg_value_name_expands_non_recursive_nested_structs — output unchanged for non-recursive specs.
  • build_custom_cmd_terminates_on_recursive_spec_type (soroban-cli) — end-to-end guard at the clap-command level, the surface the issue's repro crashes.

Related

#2668 capped XDR decode depth for deeply-nested specs. A cycle through UDT name references never nests deeply in the encoded XDR, so it needs this separate guard.

CopilotAI balanced review requested due to automatic review settings August 14, 2026 23:01
@github-project-automationgithub-project-automationBot moved this to Backlog (Not Ready) in DevXAug 14, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds safeguards and regression coverage to prevent stack overflows when rendering CLI argument value names for contracts with recursive UDTs in their spec (issue #2445).

Changes:

  • Cap UDT expansion depth in Spec::arg_value_name to prevent unbounded recursion on recursive type graphs.
  • Add spec-tools regression tests for self-recursive and mutually-recursive structs, and ensure non-recursive nesting still expands fully.
  • Add a soroban-cli regression test ensuring build_custom_cmd(...).render_long_help() succeeds for a recursive spec type.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
cmd/soroban-cli/src/commands/contract/arg_parsing.rsAdds a regression test to ensure clap help generation doesn’t crash when a contract spec includes recursive types.
cmd/crates/soroban-spec-tools/src/lib.rsAdds a maximum recursion/expansion depth for UDT value-name rendering and tests for recursive and non-recursive cases.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

// UDTs are the only types resolved by name, so any cycle in a
// recursive spec passes through here; cap the expansion depth
// and fall back to the type's name (#2445).
if depth > Self::MAX_UDT_VALUE_NAME_DEPTH {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The > check already matches the doc comment, so I'll keep it. The comment defines the constant as "the maximum nesting depth at which user-defined types are still expanded" (L1155-1156) and says "past this depth the type's name is rendered" (L1161) — "past" meaning strictly greater than the max. So expansion is intended to still happen at depth == MAX_UDT_VALUE_NAME_DEPTH and stop only beyond it, which is exactly what > does (depth 4 expands, depth 5 renders the name).

Switching to >= would stop expansion at depth 4, making the true maximum expansion depth 3 — that contradicts both the constant's name and the "still expanded" wording. In other words >= would introduce the off-by-one rather than remove it.

Fair point that the current tests don't actually pin this boundary — they only assert termination and that the outer value name survives, so they'd pass under either operator. I can add an assertion that fixes the expansion depth to the documented value to make the intended cutoff unambiguous.

…n recursive specs
`Spec::arg_value_name` threads a `depth` counter but never checks it. A
contract whose spec contains a recursive type — e.g.
`struct TreeNode { children: Vec<TreeNode> }`, which the SDK and the
network accept — sent the resolver into unbounded recursion, overflowing
the stack on every `contract invoke` against the contract, including
`-- --help`. Anyone can deploy such a contract, so this crashed the CLI
of anyone who tried to interact with it.
UDTs are the only types resolved by name, so any cycle must pass through
the `ScType::Udt` arm; cap the expansion depth there and render the
type's name past the cap. Returning `None` instead would erase the
whole value name, since `None` propagates through the `?` in every
caller. `example_udts` already bounds example rendering the same way.
Complements stellar#2668, which capped XDR decode depth for deeply-nested
specs; a cycle through UDT name references never nests deeply in the
encoded XDR, so it needs this separate guard.
Fixesstellar#2445
CopilotAI review requested due to automatic review settings September 1, 2026 23:05
@Galmanus
Galmanusforce-pushed the fix/2445-recursive-spec-value-name branch from af4e67f to 3a1c8fbCompareSeptember 1, 2026 23:05

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines +1248 to +1249
if depth > Self::MAX_UDT_VALUE_NAME_DEPTH {
return Some(name.to_utf8_string_lossy());
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog (Not Ready)

Development

Successfully merging this pull request may close these issues.

Stack overflow crash on contracts with recursive types in spec

2 participants

@Galmanus
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Cap UDT expansion depth in arg_value_name to prevent stack overflow on recursive specs - #2679

Open
Galmanus wants to merge 1 commit into
stellar:mainfrom
Galmanus:fix/2445-recursive-spec-value-name
Open

Cap UDT expansion depth in arg_value_name to prevent stack overflow on recursive specs#2679
Galmanus wants to merge 1 commit into
stellar:mainfrom
Galmanus:fix/2445-recursive-spec-value-name

Conversation

@Galmanus

Copy link
Copy Markdown

Fixes#2445.

Spec::arg_value_name threads a depth counter but never checks it. A contract whose spec contains a recursive type (e.g. struct TreeNode { children: Vec<TreeNode> } — accepted by the SDK and the network) sends the resolver into unbounded recursion TreeNode → Vec<TreeNode> → TreeNode → …, overflowing the stack on any contract invoke against the contract, including -- --help. Since anyone can deploy such a contract, this is a client-side DoS on anyone who tries to interact with it.

Fix

UDTs are the only types resolved by name (self.find(...)), so any cycle must pass through the ScType::Udt arm. This caps the expansion depth there and renders the type's name past the cap — the same approach example_udts already uses for examples (depth > 2None), except returning the name keeps the outer value name intact: a None propagates through the ? in every caller and would erase the entire help string.

For the reproduction from the issue, --help now renders the argument as:

{ children: Array<{ children: Array<{ children: Array<TreeNode>, value: u32 }>, value: u32 }>, value: u32 }

Tests

  • arg_value_name_terminates_on_self_recursive_struct — the issue's TreeNode shape; aborted with fatal runtime error: stack overflow before the fix.
  • arg_value_name_terminates_on_mutually_recursive_structsA ↔ B cycle.
  • arg_value_name_expands_non_recursive_nested_structs — output unchanged for non-recursive specs.
  • build_custom_cmd_terminates_on_recursive_spec_type (soroban-cli) — end-to-end guard at the clap-command level, the surface the issue's repro crashes.

Related

#2668 capped XDR decode depth for deeply-nested specs. A cycle through UDT name references never nests deeply in the encoded XDR, so it needs this separate guard.

CopilotAI balanced review requested due to automatic review settings August 14, 2026 23:01
@github-project-automationgithub-project-automationBot moved this to Backlog (Not Ready) in DevXAug 14, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds safeguards and regression coverage to prevent stack overflows when rendering CLI argument value names for contracts with recursive UDTs in their spec (issue #2445).

Changes:

  • Cap UDT expansion depth in Spec::arg_value_name to prevent unbounded recursion on recursive type graphs.
  • Add spec-tools regression tests for self-recursive and mutually-recursive structs, and ensure non-recursive nesting still expands fully.
  • Add a soroban-cli regression test ensuring build_custom_cmd(...).render_long_help() succeeds for a recursive spec type.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
cmd/soroban-cli/src/commands/contract/arg_parsing.rsAdds a regression test to ensure clap help generation doesn’t crash when a contract spec includes recursive types.
cmd/crates/soroban-spec-tools/src/lib.rsAdds a maximum recursion/expansion depth for UDT value-name rendering and tests for recursive and non-recursive cases.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

// UDTs are the only types resolved by name, so any cycle in a
// recursive spec passes through here; cap the expansion depth
// and fall back to the type's name (#2445).
if depth > Self::MAX_UDT_VALUE_NAME_DEPTH {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The > check already matches the doc comment, so I'll keep it. The comment defines the constant as "the maximum nesting depth at which user-defined types are still expanded" (L1155-1156) and says "past this depth the type's name is rendered" (L1161) — "past" meaning strictly greater than the max. So expansion is intended to still happen at depth == MAX_UDT_VALUE_NAME_DEPTH and stop only beyond it, which is exactly what > does (depth 4 expands, depth 5 renders the name).

Switching to >= would stop expansion at depth 4, making the true maximum expansion depth 3 — that contradicts both the constant's name and the "still expanded" wording. In other words >= would introduce the off-by-one rather than remove it.

Fair point that the current tests don't actually pin this boundary — they only assert termination and that the outer value name survives, so they'd pass under either operator. I can add an assertion that fixes the expansion depth to the documented value to make the intended cutoff unambiguous.

…n recursive specs
`Spec::arg_value_name` threads a `depth` counter but never checks it. A
contract whose spec contains a recursive type — e.g.
`struct TreeNode { children: Vec<TreeNode> }`, which the SDK and the
network accept — sent the resolver into unbounded recursion, overflowing
the stack on every `contract invoke` against the contract, including
`-- --help`. Anyone can deploy such a contract, so this crashed the CLI
of anyone who tried to interact with it.
UDTs are the only types resolved by name, so any cycle must pass through
the `ScType::Udt` arm; cap the expansion depth there and render the
type's name past the cap. Returning `None` instead would erase the
whole value name, since `None` propagates through the `?` in every
caller. `example_udts` already bounds example rendering the same way.
Complements stellar#2668, which capped XDR decode depth for deeply-nested
specs; a cycle through UDT name references never nests deeply in the
encoded XDR, so it needs this separate guard.
Fixesstellar#2445
CopilotAI review requested due to automatic review settings September 1, 2026 23:05
@Galmanus
Galmanusforce-pushed the fix/2445-recursive-spec-value-name branch from af4e67f to 3a1c8fbCompareSeptember 1, 2026 23:05

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines +1248 to +1249
if depth > Self::MAX_UDT_VALUE_NAME_DEPTH {
return Some(name.to_utf8_string_lossy());
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog (Not Ready)

Development

Successfully merging this pull request may close these issues.

Stack overflow crash on contracts with recursive types in spec

2 participants

@Galmanus
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Cap UDT expansion depth in arg_value_name to prevent stack overflow on recursive specs - #2679

Open
Galmanus wants to merge 1 commit into
stellar:mainfrom
Galmanus:fix/2445-recursive-spec-value-name
Open

Cap UDT expansion depth in arg_value_name to prevent stack overflow on recursive specs#2679
Galmanus wants to merge 1 commit into
stellar:mainfrom
Galmanus:fix/2445-recursive-spec-value-name

Conversation

@Galmanus

Copy link
Copy Markdown

Fixes#2445.

Spec::arg_value_name threads a depth counter but never checks it. A contract whose spec contains a recursive type (e.g. struct TreeNode { children: Vec<TreeNode> } — accepted by the SDK and the network) sends the resolver into unbounded recursion TreeNode → Vec<TreeNode> → TreeNode → …, overflowing the stack on any contract invoke against the contract, including -- --help. Since anyone can deploy such a contract, this is a client-side DoS on anyone who tries to interact with it.

Fix

UDTs are the only types resolved by name (self.find(...)), so any cycle must pass through the ScType::Udt arm. This caps the expansion depth there and renders the type's name past the cap — the same approach example_udts already uses for examples (depth > 2None), except returning the name keeps the outer value name intact: a None propagates through the ? in every caller and would erase the entire help string.

For the reproduction from the issue, --help now renders the argument as:

{ children: Array<{ children: Array<{ children: Array<TreeNode>, value: u32 }>, value: u32 }>, value: u32 }

Tests

  • arg_value_name_terminates_on_self_recursive_struct — the issue's TreeNode shape; aborted with fatal runtime error: stack overflow before the fix.
  • arg_value_name_terminates_on_mutually_recursive_structsA ↔ B cycle.
  • arg_value_name_expands_non_recursive_nested_structs — output unchanged for non-recursive specs.
  • build_custom_cmd_terminates_on_recursive_spec_type (soroban-cli) — end-to-end guard at the clap-command level, the surface the issue's repro crashes.

Related

#2668 capped XDR decode depth for deeply-nested specs. A cycle through UDT name references never nests deeply in the encoded XDR, so it needs this separate guard.

CopilotAI balanced review requested due to automatic review settings August 14, 2026 23:01
@github-project-automationgithub-project-automationBot moved this to Backlog (Not Ready) in DevXAug 14, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds safeguards and regression coverage to prevent stack overflows when rendering CLI argument value names for contracts with recursive UDTs in their spec (issue #2445).

Changes:

  • Cap UDT expansion depth in Spec::arg_value_name to prevent unbounded recursion on recursive type graphs.
  • Add spec-tools regression tests for self-recursive and mutually-recursive structs, and ensure non-recursive nesting still expands fully.
  • Add a soroban-cli regression test ensuring build_custom_cmd(...).render_long_help() succeeds for a recursive spec type.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
cmd/soroban-cli/src/commands/contract/arg_parsing.rsAdds a regression test to ensure clap help generation doesn’t crash when a contract spec includes recursive types.
cmd/crates/soroban-spec-tools/src/lib.rsAdds a maximum recursion/expansion depth for UDT value-name rendering and tests for recursive and non-recursive cases.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

// UDTs are the only types resolved by name, so any cycle in a
// recursive spec passes through here; cap the expansion depth
// and fall back to the type's name (#2445).
if depth > Self::MAX_UDT_VALUE_NAME_DEPTH {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The > check already matches the doc comment, so I'll keep it. The comment defines the constant as "the maximum nesting depth at which user-defined types are still expanded" (L1155-1156) and says "past this depth the type's name is rendered" (L1161) — "past" meaning strictly greater than the max. So expansion is intended to still happen at depth == MAX_UDT_VALUE_NAME_DEPTH and stop only beyond it, which is exactly what > does (depth 4 expands, depth 5 renders the name).

Switching to >= would stop expansion at depth 4, making the true maximum expansion depth 3 — that contradicts both the constant's name and the "still expanded" wording. In other words >= would introduce the off-by-one rather than remove it.

Fair point that the current tests don't actually pin this boundary — they only assert termination and that the outer value name survives, so they'd pass under either operator. I can add an assertion that fixes the expansion depth to the documented value to make the intended cutoff unambiguous.

…n recursive specs
`Spec::arg_value_name` threads a `depth` counter but never checks it. A
contract whose spec contains a recursive type — e.g.
`struct TreeNode { children: Vec<TreeNode> }`, which the SDK and the
network accept — sent the resolver into unbounded recursion, overflowing
the stack on every `contract invoke` against the contract, including
`-- --help`. Anyone can deploy such a contract, so this crashed the CLI
of anyone who tried to interact with it.
UDTs are the only types resolved by name, so any cycle must pass through
the `ScType::Udt` arm; cap the expansion depth there and render the
type's name past the cap. Returning `None` instead would erase the
whole value name, since `None` propagates through the `?` in every
caller. `example_udts` already bounds example rendering the same way.
Complements stellar#2668, which capped XDR decode depth for deeply-nested
specs; a cycle through UDT name references never nests deeply in the
encoded XDR, so it needs this separate guard.
Fixesstellar#2445
CopilotAI review requested due to automatic review settings September 1, 2026 23:05
@Galmanus
Galmanusforce-pushed the fix/2445-recursive-spec-value-name branch from af4e67f to 3a1c8fbCompareSeptember 1, 2026 23:05

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines +1248 to +1249
if depth > Self::MAX_UDT_VALUE_NAME_DEPTH {
return Some(name.to_utf8_string_lossy());
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog (Not Ready)

Development

Successfully merging this pull request may close these issues.

Stack overflow crash on contracts with recursive types in spec

2 participants

@Galmanus
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Cap UDT expansion depth in arg_value_name to prevent stack overflow on recursive specs - #2679

Open
Galmanus wants to merge 1 commit into
stellar:mainfrom
Galmanus:fix/2445-recursive-spec-value-name
Open

Cap UDT expansion depth in arg_value_name to prevent stack overflow on recursive specs#2679
Galmanus wants to merge 1 commit into
stellar:mainfrom
Galmanus:fix/2445-recursive-spec-value-name

Conversation

@Galmanus

Copy link
Copy Markdown

Fixes#2445.

Spec::arg_value_name threads a depth counter but never checks it. A contract whose spec contains a recursive type (e.g. struct TreeNode { children: Vec<TreeNode> } — accepted by the SDK and the network) sends the resolver into unbounded recursion TreeNode → Vec<TreeNode> → TreeNode → …, overflowing the stack on any contract invoke against the contract, including -- --help. Since anyone can deploy such a contract, this is a client-side DoS on anyone who tries to interact with it.

Fix

UDTs are the only types resolved by name (self.find(...)), so any cycle must pass through the ScType::Udt arm. This caps the expansion depth there and renders the type's name past the cap — the same approach example_udts already uses for examples (depth > 2None), except returning the name keeps the outer value name intact: a None propagates through the ? in every caller and would erase the entire help string.

For the reproduction from the issue, --help now renders the argument as:

{ children: Array<{ children: Array<{ children: Array<TreeNode>, value: u32 }>, value: u32 }>, value: u32 }

Tests

  • arg_value_name_terminates_on_self_recursive_struct — the issue's TreeNode shape; aborted with fatal runtime error: stack overflow before the fix.
  • arg_value_name_terminates_on_mutually_recursive_structsA ↔ B cycle.
  • arg_value_name_expands_non_recursive_nested_structs — output unchanged for non-recursive specs.
  • build_custom_cmd_terminates_on_recursive_spec_type (soroban-cli) — end-to-end guard at the clap-command level, the surface the issue's repro crashes.

Related

#2668 capped XDR decode depth for deeply-nested specs. A cycle through UDT name references never nests deeply in the encoded XDR, so it needs this separate guard.

CopilotAI balanced review requested due to automatic review settings August 14, 2026 23:01
@github-project-automationgithub-project-automationBot moved this to Backlog (Not Ready) in DevXAug 14, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds safeguards and regression coverage to prevent stack overflows when rendering CLI argument value names for contracts with recursive UDTs in their spec (issue #2445).

Changes:

  • Cap UDT expansion depth in Spec::arg_value_name to prevent unbounded recursion on recursive type graphs.
  • Add spec-tools regression tests for self-recursive and mutually-recursive structs, and ensure non-recursive nesting still expands fully.
  • Add a soroban-cli regression test ensuring build_custom_cmd(...).render_long_help() succeeds for a recursive spec type.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
cmd/soroban-cli/src/commands/contract/arg_parsing.rsAdds a regression test to ensure clap help generation doesn’t crash when a contract spec includes recursive types.
cmd/crates/soroban-spec-tools/src/lib.rsAdds a maximum recursion/expansion depth for UDT value-name rendering and tests for recursive and non-recursive cases.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

// UDTs are the only types resolved by name, so any cycle in a
// recursive spec passes through here; cap the expansion depth
// and fall back to the type's name (#2445).
if depth > Self::MAX_UDT_VALUE_NAME_DEPTH {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The > check already matches the doc comment, so I'll keep it. The comment defines the constant as "the maximum nesting depth at which user-defined types are still expanded" (L1155-1156) and says "past this depth the type's name is rendered" (L1161) — "past" meaning strictly greater than the max. So expansion is intended to still happen at depth == MAX_UDT_VALUE_NAME_DEPTH and stop only beyond it, which is exactly what > does (depth 4 expands, depth 5 renders the name).

Switching to >= would stop expansion at depth 4, making the true maximum expansion depth 3 — that contradicts both the constant's name and the "still expanded" wording. In other words >= would introduce the off-by-one rather than remove it.

Fair point that the current tests don't actually pin this boundary — they only assert termination and that the outer value name survives, so they'd pass under either operator. I can add an assertion that fixes the expansion depth to the documented value to make the intended cutoff unambiguous.

…n recursive specs
`Spec::arg_value_name` threads a `depth` counter but never checks it. A
contract whose spec contains a recursive type — e.g.
`struct TreeNode { children: Vec<TreeNode> }`, which the SDK and the
network accept — sent the resolver into unbounded recursion, overflowing
the stack on every `contract invoke` against the contract, including
`-- --help`. Anyone can deploy such a contract, so this crashed the CLI
of anyone who tried to interact with it.
UDTs are the only types resolved by name, so any cycle must pass through
the `ScType::Udt` arm; cap the expansion depth there and render the
type's name past the cap. Returning `None` instead would erase the
whole value name, since `None` propagates through the `?` in every
caller. `example_udts` already bounds example rendering the same way.
Complements stellar#2668, which capped XDR decode depth for deeply-nested
specs; a cycle through UDT name references never nests deeply in the
encoded XDR, so it needs this separate guard.
Fixesstellar#2445
CopilotAI review requested due to automatic review settings September 1, 2026 23:05
@Galmanus
Galmanusforce-pushed the fix/2445-recursive-spec-value-name branch from af4e67f to 3a1c8fbCompareSeptember 1, 2026 23:05

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines +1248 to +1249
if depth > Self::MAX_UDT_VALUE_NAME_DEPTH {
return Some(name.to_utf8_string_lossy());
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog (Not Ready)

Development

Successfully merging this pull request may close these issues.

Stack overflow crash on contracts with recursive types in spec

2 participants

@Galmanus