Skip to content

Add regression tests for JsonSerializer.Deserialize<JsonNode> MaxDepth enforcement - #3

Draft
steveisok with Copilot wants to merge 2 commits into
cca-eval-treatmentfrom
copilot/fix-jsonserializer-maxdepth
Draft

Add regression tests for JsonSerializer.Deserialize<JsonNode> MaxDepth enforcement#3
steveisok with Copilot wants to merge 2 commits into
cca-eval-treatmentfrom
copilot/fix-jsonserializer-maxdepth

Conversation

Copilot AI commented May 27, 2026

Copy link
Copy Markdown

JsonSerializer.Deserialize<JsonNode> was reported as not honoring JsonSerializerOptions.MaxDepth for deeply nested arrays. Investigation confirmed the Utf8JsonReader depth checks (StartArray/StartObject) already enforce the limit correctly — no production code fix was needed. This PR adds the missing regression test coverage.

Tests added

  • DomTests.csDeserializeToNode_RespectsMaxDepth_Arrays and DeserializeToNode_RespectsMaxDepth_Objects: symmetric counterparts to the existing SerializeToNode_RespectsMaxDepth, covering JsonNode, JsonArray, and JsonObject deserialization targets across depths 5, 32, and 70 (above default 64).

  • JsonNodeTests.csDeserialize_JsonNode_RespectsMaxDepth_DeeplyNestedArrays: focused test in the JsonNode-specific class covering the exact repro scenario (e.g. 200 nested arrays with MaxDepth = 32).

Each test verifies that JSON nested exactly at MaxDepth deserializes successfully, while JSON nested one level deeper throws JsonException.

…h enforcement

Adds tests verifying that JsonSerializer.Deserialize<JsonNode/JsonArray/JsonObject>
correctly throws JsonException when MaxDepth is exceeded for deeply nested arrays and
objects, as reported in the issue.

- DomTests.cs: DeserializeToNode_RespectsMaxDepth_Arrays/_Objects (parallel to
  existing SerializeToNode_RespectsMaxDepth)
- JsonNodeTests.cs: Deserialize_JsonNode_RespectsMaxDepth_DeeplyNestedArrays

Co-authored-by: steveisok <471438+steveisok@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix MaxDepth handling in JsonSerializer.Deserialize<JsonNode> Add regression tests for JsonSerializer.Deserialize<JsonNode> MaxDepth enforcement May 27, 2026
Copilot AI requested a review from steveisok May 27, 2026 22:29
agocke pushed a commit that referenced this pull request Jul 30, 2026
…dotnet#131279)

[wasm] Restrict enclosing-try throw-helper attach to the same funclet

## Problem

crossgen2 emits an invalid WASM (wasm32) R2R module for methods with
certain try/catch shapes: the terminal `end` opcode (0x0b) is dropped
for an EH funclet, so `wasm-tools validate` (and V8) reject the module
with *"function body must end with end opcode"*. A related symptom is a
miscomputed branch target depth in the same funclet (tracked as
dotnet#131252).

## Root cause

The defect is in wasm block layout, in `FgWasm::VisitWasmSuccs`
(`src/coreclr/jit/fgwasm.h`).

dotnet#130945 added an "enclosing try" relaxation: when a block is a try
side-entry and the throw-helper ACD key is `KD_TRY`, the helper is also
attached as a successor if

```cpp
comp->bbInTryRegions(key.RegionIndex(), block)
```

is true. `bbInTryRegions` is a purely **lexical** try-nesting test — it
does not check that the throw helper and the side-entry live in the same
**function region (funclet)**.

So a throw helper for an outer try region that belongs to the **main
method** can be attached to a catch-resumption side-entry
(`BBF_CATCH_RESUMPTION`) that merely nests inside that try but
physically lives in a **handler funclet**. RPO layout then lays the
main-method helper *inside* the funclet, interleaving it with funclet
blocks. Because a funclet is a distinct wasm function body, that
interleaving drops the funclet's terminal `end` and corrupts its branch
target depths.

Concrete trace from `System.Data.DataColumn:set_Expression`
(instrumented `VisitWasmSuccs`, deduped):

```
sideEntry BB50 (tryIdx=4 hndIdx=3) preds[async=0 catch=1 other=2] pulls dst BB76 (hndIdx=0) crossFunclet=1
sideEntry BB73 (tryIdx=4 hndIdx=3) preds[async=0 catch=1 other=0] pulls dst BB76 (hndIdx=0) crossFunclet=1
```

BB76 is the throw helper for root try region #4 (`KD_TRY`, no handler
index → main method); BB50/BB73 are catch-resumption side-entries in
handler funclet #3 (which lexically nests in try #4), so the
enclosing-try rule pulls the main-method helper into funclet #3.

This is an ordinary catch-resumption EH shape (`async=0`); it is
independent of runtime-async, and `fgwasm.h` is byte-identical to
`main`. It is latent on `main` only because R2R-wasm codegen isn't
enabled there yet.

## Fix

Gate the enclosing-try relaxation on same-function-region ownership. A
new `funcRegionOf` lambda returns the funclet index that physically
contains an arbitrary block (0 == main method); it mirrors
`funGetFuncIdx` but works for non-entry blocks and distinguishes a
filter funclet from its filter-handler. The helper is attached only when
it shares the side-entry's function region:

```cpp
if (!viaEnclosingTry || (funcRegionOf(block) == funcRegionOf(acd->acdDstBlk)))
{
    RETURN_ON_ABORT(func(acd->acdDstBlk));
}
```

The exact-match path (a helper keyed to the block's own region) is
unchanged, and dotnet#130945's intended case (an inner-try side-entry pulling
its enclosing try's helper *within the same funclet*) still matches — so
this only removes the cross-funclet edge.

## Validation

Standalone `System.Data.Common` R2R wasm crossgen, release JIT,
`wasm-tools validate` (only `fgwasm.h` differs between runs):

| | `wasm-tools validate` |
|---|---|
| baseline (`origin/main` layout) | `func 597 failed to validate` ❌ |
| with this fix | passes ✅ |

## Notes

- Supersedes dotnet#131251, which hardened the terminal-`end` emission (the
*effect*); this fixes the *cause* in layout. Closing dotnet#131251 in favor of
this.
- Related: dotnet#129335 (incomplete predecessor for this defect class, do not
reopen); dotnet#130945 (introduced the lexical enclosing-try match); dotnet#131252
(miscomputed branch target depth — same corrupted layout, very likely
subsumed by this fix).
- Follow-up idea (not in this PR): a JIT-time assert that a funclet's
blocks form a contiguous RPO range, so any future mis-layout traps at
compile time instead of surfacing as an invalid module.

> [!NOTE]
> This change was authored with the assistance of GitHub Copilot.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2e627b94-2658-41ce-a880-d9c27b7febd9
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.

JsonSerializer.Deserialize<JsonNode> does not honor MaxDepth for deeply nested arrays

2 participants