Skip to content

Make the encoder stack-depth tests hold on a reused thread stack - #4722

Merged
marcschier merged 2 commits into
masterfrom
fix/encoder-stack-depth-json-flake
Oct 9, 2026
Merged

marcschier merged 2 commits into
masterfrom
fix/encoder-stack-depth-json-flake

Conversation

@marcschier

Copy link
Copy Markdown
Collaborator

Description

EncoderStackDepthTests.DecodeWithUnboundedNestingLimitFailsBeforeTheStackOverflows(Json, VariantArray) and (Json, DataValue) fail intermittently on Linux CI, for example in run 37921301898, attempt 1, test-linux-net10.0 (12/15). The 450-level JSON input decodes without tripping the stack check, so the expected BadEncodingLimitsExceeded is missing. A re-run passes.

Cause: a thread that asks for a 256 KB stack can run on more. glibc reuses the cached stack of an exited thread when it is at most four times the requested size, and the fixture's own 1 MB threads leave such stacks behind. RuntimeHelpers.EnsureSufficientExecutionStack measures the stack the thread actually got, and 450 JSON levels fit a 1 MB stack. The product's stack check is correct; the test relied on the requested size.

Deepest nesting that fits before the check fires (net10.0, Release):

  • On a 256 KB thread: about 100 JSON levels on Linux and 70 on Windows.
  • On a 1 MB thread, Linux: 548 levels for a JSON Variant array, and up to 2477 for a JSON EncodeableTree. Encoding a Binary EncodeableTree fits 1899 levels, so the 2000 levels of the other codecs had no margin either.

Fix:

  • The unbounded-limit decode and encode tests run on a 1 MB and a 256 KB stack: the largest stack glibc can hand a 256 KB request, and the requested size.
  • Every codec nests 5000 levels, twice the deepest nesting that fits a 1 MB stack.
  • The JsonEncoder cannot nest deeper than 1000 JSON levels, so the JSON input is written by hand. NestedJsonMatchesTheJsonEncoder checks it against the encoder's output for every shape.

Validation

Command Result
Original test logic in the mcr.microsoft.com/dotnet/sdk:10.0 container, each decode right after a 1 MB thread exits, 20 times per shape Decode succeeded 20 of 20 times for VariantArray and DataValue, the CI failure; 0 of 20 without the 1 MB thread, and 0 of 20 on Windows either way
dotnet test tests/Opc.Ua.Types.Tests -f net10.0 --filter FullyQualifiedName~EncoderStackDepthTests, Release and Debug on Windows, 2 runs each 80 passed
Same in the Linux container (Release), 3 runs 80 passed
Same with -f net48 80 passed
Linux, depth temporarily set back to 2000 EncodeWithUnboundedNestingLimitFailsBeforeTheStackOverflows(Binary,EncodeableTree) fails on the 1 MB stack
Linux, EncodingLimits.EnsureSufficientStack temporarily disabled The test host crashes with a stack overflow, so the tests still catch a missing check

Related Issues

Checklist

  • I have signed the CLA and read the CONTRIBUTING doc.
  • I have added tests that prove my fix is effective or that my feature works and increased code coverage.
  • I have added all necessary documentation.
  • I have verified that my changes do not introduce (new) build or analyzer warnings.
  • I ran all tests locally using the UA.slnx solution against at least .net framework and .net 10, and all passed.
  • I fixed all failing and flaky tests in the CI pipelines and all CodeQL warnings.
  • I have addressed all PR feedback received.

DecodeWithUnboundedNestingLimitFailsBeforeTheStackOverflows(Json, VariantArray)
and (Json, DataValue) failed intermittently on Linux CI: the 450-level JSON
input decoded without tripping the stack check.

A thread that asks for a 256 KB stack can run on more. glibc reuses the cached
stack of an exited thread when it is at most four times the requested size,
and this fixture's own 1 MB threads leave such stacks behind. The stack check
measures the stack the thread got, and 450 JSON levels fit a 1 MB stack.
Running a 1 MB thread right before the decode reproduces the failure on
Linux 20 times out of 20; Windows does not reuse stacks this way.

The unbounded-limit tests now run on the 1 MB and the 256 KB stack and nest
5000 levels, twice the deepest nesting of any codec and shape that fits a
1 MB stack (2477 levels: JSON EncodeableTree, Linux, Release). The 2000
levels of the other codecs were too few as well: on Linux, encoding a Binary
EncodeableTree of 2000 levels fits a 1 MB stack. The JSON input is written
by hand because the JsonEncoder cannot nest deeper than 1000 levels, and
NestedJsonMatchesTheJsonEncoder keeps it equal to the encoder's output.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@marcschier
marcschier marked this pull request as ready for review October 9, 2026 14:50
Copilot AI balanced review requested due to automatic review settings October 9, 2026 14:50

Copilot AI 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.

🟢 Approval recommended

The focused test changes correctly address reused thread stacks and validate the handcrafted JSON inputs.

0 open findings

What changed in this PR

Stabilizes encoder stack-depth tests against Linux thread-stack reuse behavior.

Changes:

  • Tests both 256 KB and 1 MB thread stacks at 5,000 nesting levels.
  • Generates deeply nested JSON manually and validates it against JsonEncoder output.
File Description
tests/​Opc.Ua.Types.Tests/​Encoders/​EncoderStackDepthTests.cs Strengthens stack-overflow regression coverage across codecs and shapes.

🧠 Review effort: Balanced


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

@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Code coverage

✅ Coverage gate passed.

Check Result Threshold
✅ Project line rate 88.60% (306508/345943 lines) >= 70.00%
✅ Project branch rate 79.04% >= 60.00%
ℹ️ Patch coverage no changed C# files -
ℹ️ Baseline delta (advisory) +15.00 pp 73.60% recorded

Coverage is above the recorded baseline - consider ratcheting coverage-thresholds.json.

Thresholds live in coverage-thresholds.json. Whole report before exclusions: line 87.74%, branch 78.25%.

@codecov

codecov Bot commented Oct 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.83%. Comparing base (60dc5ab) to head (d3d2517).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #4722      +/-   ##
==========================================
- Coverage   82.84%   82.83%   -0.02%     
==========================================
  Files        2267     2267              
  Lines      345943   345943              
  Branches    60303    60303              
==========================================
- Hits       286609   286568      -41     
- Misses      39238    39269      +31     
- Partials    20096    20106      +10     
Flag Coverage Δ
actions 82.83% <ø> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.
see 30 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants