Backport #3877 to 4.x: Guard native recursion and forwarding paths - #4007
Merged
Merged
Conversation
Backport of PR sebastienros#3877 (commit 044f44a) from main. `StackGuard`'s backstop sat on `ScriptFunction`'s four entry points, so it covered recursion that goes back through interpreted code and nothing else. A recursion that stays inside native frames -- `Array.prototype.flat`/`join`/`toLocaleString` walking a nested array, `JSON.stringify` walking a nested graph, a chain of `bind`s or `Proxy` wrappers forwarding a single call, a host `ClrFunction` or delegate that calls back into the engine -- never reached it, and ended the process with a native stack overflow that no constraint and no `catch` can see. `EnsureNativeStackHeadroom` is the same probe placed on those paths. It is gated on `Options.Constraints.StackOverflowGuard` alone (`_graphGuardEnabled` renamed `_nativeBackstopEnabled` for what it now covers), because these paths hold ref structs, pooled arrays and identity-bearing state and so cannot take `MaxExecutionStackCount`'s stack-hopping lane. `BindFunction` also returns its pooled argument buffer in a `finally`, so an exceptional exit no longer leaks it. Adapted for 4.x: - `Jint/Native/HostFunction.cs` does not exist on this branch -- it is a v5 public API -- so that hunk is dropped. The host-callable surfaces 4.x does have, `ClrFunction` and `DelegateWrapper`, both take the probe as on main. - `JsonSerializer` has no `EnterContainer` here: main's result-limit accounting (`_limits`, `_depth`, `CountProperties`) is part of the security stack 4.x does not carry. The probe goes to the three `_stack.Enter(value)` sites instead, which is where `EnterContainer` is called from on main. - One context conflict in `Engine.cs`: `SignalError` is still `SignalError` here (sebastienros#3845 is a separate backport), so `ConstructNativeFunction` is added above it unchanged. - Tests transcribed from NUnit to xUnit, which is what 4.x's test projects still are, and every engine in them asks for `StackOverflowGuard` explicitly: the guard is opt-in on this branch where it defaults on upstream, so a default 4.x engine is deliberately still unprotected. The `HostFunction` half of the host-callable test goes with the type; the `ClrFunction` and `Constructor` halves stay. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SLCujwvKtTvtWD9f6RTyiF
lahma
force-pushed
the
backport/4x-3877
branch
from
September 9, 2026 08:25
9db2629 to
f8cf40d
Compare
…k on every frame size On Linux 4.x's BindFunction and Proxy hops are light enough that ten thousand of them fit in the test thread's 1 MiB stack, so the chain returned and the guard had nothing to catch; the CI leg reported "none" where a RangeError was expected. Fifty thousand layers exhaust that stack for any plausible frame size, so the probe fires before the process would die, on every platform. The traversal routes are unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qq1kWgfU9v38r7yDcjxK48
…any frame size On Linux x64 ten thousand of this branch's bound-call or proxy hops fit in the test thread's 1 MiB stack, so the chain returned and the CI leg saw "none"; a five-fold deeper chain then died on ARM64 before the probe could answer. A 256 KiB thread runs out at a depth the probe sees first on every platform, with main's original depth. The traversal routes are unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qq1kWgfU9v38r7yDcjxK48
On Linux x64 the bound-call chain answered "none" even on a 256 KiB thread: the System V JIT turns this branch's bound-call forward into a tail call, so the hop consumes no stack and the chain cannot overflow, exactly as the .NET Framework JIT already did for the proxy forward. The guard promises a catchable error whenever the stack does run out, not that it must, so both forwarding routes accept completion; the traversal and construct routes stay strict. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qq1kWgfU9v38r7yDcjxK48
This was referenced Sep 9, 2026
PatrickSt1991
pushed a commit
to Apps2Samsung/Apps2Samsung
that referenced
this pull request
Sep 14, 2026
Updated [Avalonia](https://github.com/AvaloniaUI/Avalonia/) from 11.3.20 to 11.3.22. <details> <summary>Release notes</summary> _Sourced from [Avalonia's releases](https://github.com/AvaloniaUI/Avalonia//releases)._ No release notes found for this version range. Commits viewable in [compare view](https://github.com/AvaloniaUI/Avalonia//commits). </details> Updated [Avalonia.Desktop](https://github.com/AvaloniaUI/Avalonia/) from 11.3.20 to 11.3.22. <details> <summary>Release notes</summary> _Sourced from [Avalonia.Desktop's releases](https://github.com/AvaloniaUI/Avalonia//releases)._ No release notes found for this version range. Commits viewable in [compare view](https://github.com/AvaloniaUI/Avalonia//commits). </details> Updated [Avalonia.Diagnostics](https://github.com/AvaloniaUI/Avalonia/) from 11.3.20 to 11.3.22. <details> <summary>Release notes</summary> _Sourced from [Avalonia.Diagnostics's releases](https://github.com/AvaloniaUI/Avalonia//releases)._ No release notes found for this version range. Commits viewable in [compare view](https://github.com/AvaloniaUI/Avalonia//commits). </details> Updated [Avalonia.Fonts.Inter](https://github.com/AvaloniaUI/Avalonia/) from 11.3.20 to 11.3.22. <details> <summary>Release notes</summary> _Sourced from [Avalonia.Fonts.Inter's releases](https://github.com/AvaloniaUI/Avalonia//releases)._ No release notes found for this version range. Commits viewable in [compare view](https://github.com/AvaloniaUI/Avalonia//commits). </details> Updated [Avalonia.Themes.Fluent](https://github.com/AvaloniaUI/Avalonia/) from 11.3.20 to 11.3.22. <details> <summary>Release notes</summary> _Sourced from [Avalonia.Themes.Fluent's releases](https://github.com/AvaloniaUI/Avalonia//releases)._ No release notes found for this version range. Commits viewable in [compare view](https://github.com/AvaloniaUI/Avalonia//commits). </details> Updated [Jint](https://github.com/sebastienros/jint) from 4.16.1 to 4.16.2. <details> <summary>Release notes</summary> _Sourced from [Jint's releases](https://github.com/sebastienros/jint/releases)._ ## 4.16.2 Jint 4.16.2 is a maintenance release from the `4.x` branch: **correctness and conformance fixes backported from `main`, and nothing that changes an existing API or an existing default.** If you are on 4.16.1 it is a drop-in update — every public signature is the one 4.16.0 shipped, on all five target frameworks, and the per-framework snapshots in `Jint.Tests.PublicInterface/Verify/` are unchanged. `main` remains 5.0.0 development; what is coming there is recorded as it lands in [`docs/v5-migration.md`](https://github.com/sebastienros/jint/blob/main/docs/v5-migration.md). ### Highlights **Failures that used to end the process, or never end.** A native error raised while a call's arguments are being evaluated is propagated instead of leaving an empty value behind, which on 4.16.1 could recurse until the process died — `decodeURIComponent` on a malformed sequence was enough (#4009). Native recursion and the forwarding paths through bound functions and proxies are guarded so a deep native chain raises a catchable error (#4007). A module graph too deep to link raises an error the host can catch instead of overflowing the stack (#3548). Temporal and Intl parsing cannot throw an uncatchable `RegexMatchTimeoutException` because the machine was busy (#3543), a Temporal difference past a calendar's range raises `RangeError` instead of spinning forever (#3555), and the process-wide Intl culture cache and Temporal zone cache are read-only and bounded, with a rejected zone no longer remembered — closing a script-driven unbounded growth (#3546). **Generators and built-ins, step by step.** A `yield*` delegation reached again by a loop both re-delegates and keeps its place: `countdown(3)` in a loop no longer hangs, and a delegating generator no longer returns the memoized first result (#3545). `Array.prototype.map` and `slice` hand a `@@species` constructor the length `ToLength` produced, and a non-callable `map` argument is a `TypeError` (#3547). A trailing NUL pads neither a numeric string nor an array index (#3552). A removed property slot is a tombstone rather than a free slot to reuse, so enumeration order survives a delete-and-readd (#3318), and `LengthOfArrayLike` no longer clamps through a `uint` overload (#3328). **Interop that answers for the right engine.** Two engines in one process no longer decide each other's conversions and operators (#3559), a host type converter's answer stays with the engine whose converter gave it (#3563), and a value the host registers on a `ShadowRealm` — and the members its wrapper builds eagerly — belong to that realm (#3557). Realm construction state is restored after nesting or a failure (#4008). Overload selection is by the arguments in hand: an operator overload is chosen that way (#3611), a `params` overload is chosen by the array's element type with a failing element declining rather than throwing (#3782), an overload the argument cannot bind to is not a match, and a host operator that throws reports what it threw (#3554). An index on a wrapped host collection is one property however it is spelled, and a member filter that hides the indexer hides it (#3562); a read-only host collection refuses a write with a JavaScript `TypeError` rather than the CLR's `NotSupportedException` (#3556). **Internationalization and Temporal.** The Persian calendar extends into proleptic years on its 33-year cycle, so the ends of Temporal's range land in the right Persian year (#4006); a calendar that counts Gregorian months writes their names (#3612); and a `-u-` extension carrying more than one key is read whole (#3613). **Errors.** Only a string-valued `stack` counts as a pre-existing stack when a `JavaScriptException` is built, so an accessor or non-string `stack` on a thrown object no longer breaks error reporting (#3677, reported by @jeske). Every change was verified failing-first against the unfixed branch on both .NET Framework and .NET 10, and the release was gated on a paired SunSpider and Dromaeo comparison against 4.16.1 on an idle machine: no row regressed outside run-to-run noise, most run 1–4 % faster. ## What's Changed * Backport: a removed property slot is a tombstone, not a free slot to reuse (#3273) by @lahma in sebastienros/jint#3318 * Backport: LengthOfArrayLike, delete the uint overload rather than clamp it (#3248) by @lahma in sebastienros/jint#3328 * Temporal and Intl parsing cannot fail because the machine was busy (#3486) by @lahma in sebastienros/jint#3543 * Backport: the process-wide Intl culture cache and Temporal zone cache are read-only and bounded, and a rejected zone is not remembered by @lahma in sebastienros/jint#3546 * Array: map and slice hand a @@species constructor the length ToLength produced (#3510) by @lahma in sebastienros/jint#3547 * Generators: a yield* delegation both re-delegates and keeps its place (backport of #3506 and #3518) by @lahma in sebastienros/jint#3545 * A module graph too deep to link raises an error the host can catch, instead of ending the process (#3415) by @lahma in sebastienros/jint#3548 * String to number: a trailing NUL pads neither a number string nor an array index (backport of #3544) by @lahma in sebastienros/jint#3552 * Interop: a host operator reports what it threw, and an overload the argument cannot bind to is not a match by @lahma in sebastienros/jint#3554 * Temporal: a difference past a calendar's range raises RangeError instead of spinning (#3452) by @lahma in sebastienros/jint#3555 * Interop: a read-only host collection refuses script with a JavaScript error, not the CLR's own (backport of #3385) by @lahma in sebastienros/jint#3556 * ShadowRealm: a value the host registers, and the members its wrapper builds eagerly, belong to that realm by @lahma in sebastienros/jint#3557 * Interop: two engines in one process do not decide each other's conversions and operators (backport of #3521 and #3526) by @lahma in sebastienros/jint#3559 * Interop: an index on a wrapped host collection is one property, and a filter that hides the indexer hides it by @lahma in sebastienros/jint#3562 * Interop: a host type converter's answer stays with the engine whose converter gave it by @lahma in sebastienros/jint#3563 * Interop: an operator overload is chosen by the arguments in hand (backport of #3578) by @lahma in sebastienros/jint#3611 * Intl: a calendar counting Gregorian months writes their names (backport of #3589) by @lahma in sebastienros/jint#3612 * Intl: a `-u-` extension carrying more than one key is read whole (backport of #3594) by @lahma in sebastienros/jint#3613 * JavaScriptException: only a string "stack" counts as a pre-existing stack (#3607 backport) by @lahma in sebastienros/jint#3677 * Interop: a params overload is chosen by the array's element type, and a failing element declines instead of throwing (#3764) by @lahma in sebastienros/jint#3782 * Backport #3751 to 4.x: Temporal: the persian calendar extends into proleptic years on the 33-year cycle by @lahma in sebastienros/jint#4006 * Backport #3922 to 4.x: Restore realm construction state after nesting or failure by @lahma in sebastienros/jint#4008 * Backport #3845 to 4.x: Propagate native errors during call argument evaluation by @lahma in sebastienros/jint#4009 * Backport #3877 to 4.x: Guard native recursion and forwarding paths by @lahma in sebastienros/jint#4007 **Full Changelog**: sebastienros/jint@v4.16.1...v4.16.2 Commits viewable in [compare view](sebastienros/jint@v4.16.1...v4.16.2). </details> Updated [Microsoft.AspNetCore](https://github.com/dotnet/aspnetcore) from 2.3.12 to 2.3.13. <details> <summary>Release notes</summary> _Sourced from [Microsoft.AspNetCore's releases](https://github.com/dotnet/aspnetcore/releases)._ No release notes found for this version range. Commits viewable in [compare view](https://github.com/dotnet/aspnetcore/commits). </details> Updated [Microsoft.AspNetCore.Server.Kestrel.Core](https://github.com/dotnet/aspnetcore) from 2.3.12 to 2.3.13. <details> <summary>Release notes</summary> _Sourced from [Microsoft.AspNetCore.Server.Kestrel.Core's releases](https://github.com/dotnet/aspnetcore/releases)._ No release notes found for this version range. Commits viewable in [compare view](https://github.com/dotnet/aspnetcore/commits). </details> Updated [System.Security.Cryptography.Xml](https://github.com/dotnet/dotnet) from 10.0.11 to 10.0.12. <details> <summary>Release notes</summary> _Sourced from [System.Security.Cryptography.Xml's releases](https://github.com/dotnet/dotnet/releases)._ No release notes found for this version range. Commits viewable in [compare view](https://github.com/dotnet/dotnet/commits). </details> Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This was referenced Sep 14, 2026
8 tasks
legrab
added a commit
to legrab/pocok
that referenced
this pull request
Sep 15, 2026
Updated [Jint](https://github.com/sebastienros/jint) from 4.16.1 to 4.16.2. <details> <summary>Release notes</summary> _Sourced from [Jint's releases](https://github.com/sebastienros/jint/releases)._ ## 4.16.2 Jint 4.16.2 is a maintenance release from the `4.x` branch: **correctness and conformance fixes backported from `main`, and nothing that changes an existing API or an existing default.** If you are on 4.16.1 it is a drop-in update — every public signature is the one 4.16.0 shipped, on all five target frameworks, and the per-framework snapshots in `Jint.Tests.PublicInterface/Verify/` are unchanged. `main` remains 5.0.0 development; what is coming there is recorded as it lands in [`docs/v5-migration.md`](https://github.com/sebastienros/jint/blob/main/docs/v5-migration.md). ### Highlights **Failures that used to end the process, or never end.** A native error raised while a call's arguments are being evaluated is propagated instead of leaving an empty value behind, which on 4.16.1 could recurse until the process died — `decodeURIComponent` on a malformed sequence was enough (#4009). Native recursion and the forwarding paths through bound functions and proxies are guarded so a deep native chain raises a catchable error (#4007). A module graph too deep to link raises an error the host can catch instead of overflowing the stack (#3548). Temporal and Intl parsing cannot throw an uncatchable `RegexMatchTimeoutException` because the machine was busy (#3543), a Temporal difference past a calendar's range raises `RangeError` instead of spinning forever (#3555), and the process-wide Intl culture cache and Temporal zone cache are read-only and bounded, with a rejected zone no longer remembered — closing a script-driven unbounded growth (#3546). **Generators and built-ins, step by step.** A `yield*` delegation reached again by a loop both re-delegates and keeps its place: `countdown(3)` in a loop no longer hangs, and a delegating generator no longer returns the memoized first result (#3545). `Array.prototype.map` and `slice` hand a `@@species` constructor the length `ToLength` produced, and a non-callable `map` argument is a `TypeError` (#3547). A trailing NUL pads neither a numeric string nor an array index (#3552). A removed property slot is a tombstone rather than a free slot to reuse, so enumeration order survives a delete-and-readd (#3318), and `LengthOfArrayLike` no longer clamps through a `uint` overload (#3328). **Interop that answers for the right engine.** Two engines in one process no longer decide each other's conversions and operators (#3559), a host type converter's answer stays with the engine whose converter gave it (#3563), and a value the host registers on a `ShadowRealm` — and the members its wrapper builds eagerly — belong to that realm (#3557). Realm construction state is restored after nesting or a failure (#4008). Overload selection is by the arguments in hand: an operator overload is chosen that way (#3611), a `params` overload is chosen by the array's element type with a failing element declining rather than throwing (#3782), an overload the argument cannot bind to is not a match, and a host operator that throws reports what it threw (#3554). An index on a wrapped host collection is one property however it is spelled, and a member filter that hides the indexer hides it (#3562); a read-only host collection refuses a write with a JavaScript `TypeError` rather than the CLR's `NotSupportedException` (#3556). **Internationalization and Temporal.** The Persian calendar extends into proleptic years on its 33-year cycle, so the ends of Temporal's range land in the right Persian year (#4006); a calendar that counts Gregorian months writes their names (#3612); and a `-u-` extension carrying more than one key is read whole (#3613). **Errors.** Only a string-valued `stack` counts as a pre-existing stack when a `JavaScriptException` is built, so an accessor or non-string `stack` on a thrown object no longer breaks error reporting (#3677, reported by @jeske). Every change was verified failing-first against the unfixed branch on both .NET Framework and .NET 10, and the release was gated on a paired SunSpider and Dromaeo comparison against 4.16.1 on an idle machine: no row regressed outside run-to-run noise, most run 1–4 % faster. ## What's Changed * Backport: a removed property slot is a tombstone, not a free slot to reuse (#3273) by @lahma in sebastienros/jint#3318 * Backport: LengthOfArrayLike, delete the uint overload rather than clamp it (#3248) by @lahma in sebastienros/jint#3328 * Temporal and Intl parsing cannot fail because the machine was busy (#3486) by @lahma in sebastienros/jint#3543 * Backport: the process-wide Intl culture cache and Temporal zone cache are read-only and bounded, and a rejected zone is not remembered by @lahma in sebastienros/jint#3546 * Array: map and slice hand a @@species constructor the length ToLength produced (#3510) by @lahma in sebastienros/jint#3547 * Generators: a yield* delegation both re-delegates and keeps its place (backport of #3506 and #3518) by @lahma in sebastienros/jint#3545 * A module graph too deep to link raises an error the host can catch, instead of ending the process (#3415) by @lahma in sebastienros/jint#3548 * String to number: a trailing NUL pads neither a number string nor an array index (backport of #3544) by @lahma in sebastienros/jint#3552 * Interop: a host operator reports what it threw, and an overload the argument cannot bind to is not a match by @lahma in sebastienros/jint#3554 * Temporal: a difference past a calendar's range raises RangeError instead of spinning (#3452) by @lahma in sebastienros/jint#3555 * Interop: a read-only host collection refuses script with a JavaScript error, not the CLR's own (backport of #3385) by @lahma in sebastienros/jint#3556 * ShadowRealm: a value the host registers, and the members its wrapper builds eagerly, belong to that realm by @lahma in sebastienros/jint#3557 * Interop: two engines in one process do not decide each other's conversions and operators (backport of #3521 and #3526) by @lahma in sebastienros/jint#3559 * Interop: an index on a wrapped host collection is one property, and a filter that hides the indexer hides it by @lahma in sebastienros/jint#3562 * Interop: a host type converter's answer stays with the engine whose converter gave it by @lahma in sebastienros/jint#3563 * Interop: an operator overload is chosen by the arguments in hand (backport of #3578) by @lahma in sebastienros/jint#3611 * Intl: a calendar counting Gregorian months writes their names (backport of #3589) by @lahma in sebastienros/jint#3612 * Intl: a `-u-` extension carrying more than one key is read whole (backport of #3594) by @lahma in sebastienros/jint#3613 * JavaScriptException: only a string "stack" counts as a pre-existing stack (#3607 backport) by @lahma in sebastienros/jint#3677 * Interop: a params overload is chosen by the array's element type, and a failing element declines instead of throwing (#3764) by @lahma in sebastienros/jint#3782 * Backport #3751 to 4.x: Temporal: the persian calendar extends into proleptic years on the 33-year cycle by @lahma in sebastienros/jint#4006 * Backport #3922 to 4.x: Restore realm construction state after nesting or failure by @lahma in sebastienros/jint#4008 * Backport #3845 to 4.x: Propagate native errors during call argument evaluation by @lahma in sebastienros/jint#4009 * Backport #3877 to 4.x: Guard native recursion and forwarding paths by @lahma in sebastienros/jint#4007 **Full Changelog**: sebastienros/jint@v4.16.1...v4.16.2 Commits viewable in [compare view](sebastienros/jint@v4.16.1...v4.16.2). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details>
This was referenced Sep 16, 2026
This was referenced Sep 21, 2026
lahma
added a commit
that referenced
this pull request
Sep 24, 2026
…d GetFunctionRealm instead of recursing (#4169) Backport of PR #4165 (commit 8a1aad7) from main. BindFunction.IsConstructor asked BoundTargetFunction.IsConstructor, and GetFunctionRealm called itself for every [[BoundTargetFunction]] and [[ProxyTarget]] link - one native frame per link with no stack probe - so on net8.0/net10.0, where tier-0 code is not tail-called, a chain of ~16,000 (IsConstructor) or ~12,800 (GetFunctionRealm) links on a 1 MB thread ended the process through `new f()`, `Reflect.construct`, `class extends`, `super()`, `new Proxy(f, h)`, an array species constructor or a ShadowRealm call, before any probed [[Call]]/[[Construct]] ran. Neither question observes anything on the way, so both are loops now. The StackOverflowGuard probes #4007 brought to 4.x sit in BindFunction's and JsProxy's [[Call]] and [[Construct]], not on these two walks, so an engine that opted into the guard was not protected either. Adapted for 4.x: - Jint/Native/Function/BindFunction.cs: main's walk, verbatim. The doc comment drops main's "(#4130 made building one linear)", which is not true on 4.x. - Jint/Native/Function/Function.cs: the same loop, keeping 4.x's test order (Function, then BindFunction, then JsProxy). Main tests BindFunction first because #3658 made BindFunction derive from Function there; on 4.x BindFunction and JsProxy both derive from ObjectInstance, so the order is free and main's "Step 2 before step 3" comment is replaced by one that says so. - Jint.Tests/Runtime/FunctionTests.cs: the three cases, [Test] -> [Fact]. TheRealmOfAChainOfBoundFunctionsAndProxiesIsItsInnermostTargets creates the other realm with engine._host.CreateRealm(), the pattern this file already uses, in place of main's Test262Object/$262.createRealm(), which Jint.Tests on 4.x does not have. - Jint.Tests.PublicInterface/BoundFunctionChainWalkTests.cs: new, [Test] -> [Fact]; routes, depths (200,000 bound links, 100,000 proxy+bind pairs), the 1 MB thread and the expected outcomes are main's. Each chain step also runs `delete f.name`: 4.x lacks #4130, so bind writes "bound " + the target's name eagerly and an undeleted chain retains names quadratic in its depth (measured: 370 MB of managed heap at 8,000 links; the ~16,000 links a 1 MB thread needs is past a 1 GB heap). With no own name the next bind reads Function.prototype's empty one, so the build is linear (200,000 links in ~0.5 s, ~50 MB) and the chain is the same chain of BindFunctions. Main's test has no NETFRAMEWORK carve-out and none is needed: on .NET Framework x64 the JIT tail-calls both recursions, so the cases pass there before and after the fix. Evidence (-c Release, freshly built): - Unfixed 4.x, net10.0: each BoundFunctionChainWalkTests case, run alone, kills the test host - "Stack overflow." with BindFunction.get_IsConstructor() repeated 15,998 / 16,003 times (the guarded and unguarded bound-chain cases) and Function.GetFunctionRealm repeated 12,807 times (the mixed chain); under dotnet test that surfaces as "[FATAL ERROR] Xunit.Sdk.TestPipelineException" and no results. net472: 3/3 pass (tail-called). The three FunctionTests cases pass on both TFMs before the fix (72/72 FunctionTests): they pin that the walk kept the answer. - Unfixed 4.x is reachable without an expensive chain: with StackOverflowGuard on, a script that recurses until the guard's RangeError and, in the catch at that depth, evaluates `new Proxy(f, {})` over a plain 2,000-link bind chain (24 MB, built in ~0.25 s) ends the process (get_IsConstructor repeated ~1,800 times); a plain 20,000-link proxy chain (4 MB) as an array's constructor ends it in GetFunctionRealm (12,794 frames). With the fix both answer. - With the fix: BoundFunctionChainWalkTests 3/3 and FunctionTests 72/72 on net10.0 and net472. - Full solution dotnet test -c Release: 0 failures. Jint.Tests 7616/0/4 (net10.0), 7531/0/4 (net472); Jint.Tests.PublicInterface 1877/0/9 (net10.0), 1869/0/9 (net472), public-API Verify snapshots unchanged; Jint.Tests.CommonScripts 28/0/0 on both; Jint.Tests.SourceGenerators 52/0/0. (passed/failed/skipped) - test262 (4.x pin), from that same run: 102,501 passed / 0 failed / 183 skipped, identical to the 4.x control; no load flakes in this run. - JINT_HOST_CONTRACT_VERIFICATION=1: Jint.Tests 7616/0/4 (net10.0), 7531/0/4 (net472); Jint.Tests.PublicInterface 1881/0/5 (net10.0), 1873/0/5 (net472). Claude-Session: https://claude.ai/code/session_01PanPJbBD7pQC9fRiTpHxvs Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
lahma
added a commit
that referenced
this pull request
Sep 25, 2026
…a wrapper's forward (#4170) * Backport #4078 to 4.x: Walk the prototype chain in a loop instead of one native frame per link Backport of PR #4078 (head b506e62, not yet merged on main) from main. A prototype chain is built by script, so its depth is an input. ObjectInstance's [[Get]], [[Set]] and [[HasProperty]] each resolved it by calling the same method on Prototype, one native frame per link, so let x = {}; for (let i = 0; i < 20000; i++) x = { __proto__: x }; x.missing ended the process with a native stack overflow no catch could see (#4076). The write side was reached through `x.missing = 1`, the existence side through `'missing' in x`, and both through every identifier resolved inside `with (x) { ... }`. A chain of trapless proxies had the same defect one level up: `return target.Get(property, receiver)` with nothing in between. All four walks are loops now (GetFromPrototypeChain, the private receiver-threading TryGetValue, SetOnPrototypeChain, HasProperty inline), so an ordinary chain of any depth resolves. A walk hands the rest of the algorithm to the first link it may not walk -- on the read side a link carrying InternalTypes.ExoticGet | OwnValueHook, on the write and existence sides any link without the positive InternalTypes.PlainObject claim -- and probes the native stack at that hand-over, off the ordinary path. A trapless JsProxy forwards through ForwardToTarget(target), which probes; a trapped proxy's trap already probes as a callee. SharedShapeObject (what every JsObjectShape.Instantiate returns) takes PlainObject, which main's paired gate required after shaped host prototypes declined the walk. PrototypeChainWalkTests pins the flag's claim over every reachable object, in both directions. Adapted for 4.x: - JsProxy.cs, three conflicts, all context: 4.x's [[IsArray]] hook is IsArray() where main's is IsSpecArray(), and its [[IsExtensible]] is the virtual Extensible getter where main's is IsExtensible(). ForwardToTarget goes on the same forwards. The probe set otherwise matches main's: the 24 trapless forwards (11 internal methods x trapless arm and CLR-declined arm, plus IsArray and ToObject) take ForwardToTarget, and [[Call]]/[[Construct]] keep the entry probes #4007 gave them, byte-identical. Their forwards (callable.Call, constructor.Construct) do not go through ForwardToTarget, so no route probes twice and none lost a probe. - ObjectInstance.cs: 4.x's SetUnlikely still inlines OrdinarySetWithOwnDescriptor, which main extracted in #3944 (not on 4.x). SetOnPrototypeChain resolves a found link with that algorithm, so the same extraction is made here, private and with its body unchanged; SetUnlikely delegates to it as on main. - StackOverflowGuard is opt-in on 4.x, so every engine in the new depth cases asks for it (Guarded()), as #4007's did. A default 4.x engine gets the loops -- an ordinary chain resolves at any depth either way -- but not the hand-over probes, which are gated on the guard. - Tests transcribed from NUnit to xUnit v3. Main's b506e62 hunk on the existing forwarding-chain rows is not taken: #4007's backport already settled those rows for 4.x (256 KiB stack, proxy and bound call accepting either answer), and this change does not touch those routes. - The census allowlist is main's, unchanged: emptied on 4.x, the converse names exactly the same 19 types. Both directions were broken on purpose on 4.x (flag dropped from SharedShapeObject; flag added to ArrayInstance) and each named its offender. - Jint.Benchmark/PrototypeChainReadBenchmark.cs did not exist on 4.x (main added it with #4048, which 4.x does not carry); it is added whole, rows unchanged, with its prose saying that on this branch the member cache only serves a direct-prototype holder. Not run. - Co-located AGENTS.md edits dropped (the files do not exist on 4.x); the two doc comments citing Jint/Constraints/AGENTS.md say it is main's. Evidence (Windows x64, Release): - Unfixed (the engine files as on 4.x, the ported tests), each depth row run in its own process: on net10.0, 17 of 23 rows end the test host (exit 0xC00000FD, "Stack overflow.", ObjectInstance.Get x3058 for the plain chain, x3047 shaped, JsProxy.Get x7119 for the proxy chain): all 9 plain rows, 5 shaped (read miss, write, in miss, with hit, with miss) and the 3 trapless proxy rows. The 6 that pass are the shaped hits (every level declares the name, so the first link answers) and the 3 trapped-proxy rows (the trap's callee already probed). On net472 ("Process is terminated due to StackOverflowException.") 10 rows die (plain read hit, read miss, inherited getter, write, inherited setter, with hit; shaped read miss, write, with hit; trapless write) and trapless has fails its assertion ("false" for the RangeError): the .NET Framework JIT turns the unfixed HasProperty and trapless-read forwards into tail calls, so plain in hit/in miss/with miss, shaped in miss/with miss and trapless read complete there even unfixed. The converse census fails naming SharedShapeObject on both. - Fixed: HostNativeRecursionGuardTests 34/34 and PrototypeChainWalkTests 5/5 on net10.0 and net472. - Depth: on a 1 MB thread the 10,000-proxy rows probe from ~6,330 hops (read, has) and ~2,720 (write) on net10.0, ~5,370 (has) and ~1,890 (write) on net472, where the read completes -- the carve-out main already has. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PanPJbBD7pQC9fRiTpHxvs * Backport #4125 to 4.x: Stop a chain of adjacent host object wrappers from killing the process on a member miss Backport of PR #4125 (head 02bed3e, not yet merged on main) from main. ObjectWrapper.Get answers a member miss by forwarding the read to its prototype and then inspecting the result for Options.Interop.ThrowOnUnresolvedMember. That post-check keeps the forward out of tail position, so the link cannot join the loop ObjectInstance now walks a chain with: a hop to an ordinary link re-enters that loop, but a hop to another wrapper is a native frame with no probe between the two. A wrapper does not override SetPrototypeOf, so script builds such a chain itself (`Object.setPrototypeOf(w[i - 1], w[i])`) and its depth is an input; twenty thousand links ended the process (#4087). The forward now probes the native stack first, as every other hand-over does, which turns that into a catchable RangeError. It is the only site of that shape: Set and HasProperty end in base.<op>, which is the loop; GetOwnProperty and RemoveOwnProperty walk nothing; TypeReference and NamespaceReference forward nothing to a prototype. Adapted for 4.x: - The comment on the probe drops main's reference to the IL pin StackOverflowGuardTests.ExactlyTheInteropAndForwardingFunctionsProbeTheNativeStack, which 4.x does not have; the depth cases are what hold the probe in place. - The new tests are xUnit v3 (TheoryData/MemberData, Fact), at the top of the class as on main, and ask for StackOverflowGuard, which is opt-in here: on a default 4.x engine the probe is inert, as every #4007 probe is. - Jint/Runtime/Interop/AGENTS.md does not exist on 4.x; that edit is dropped. Evidence (Windows x64, Release): - With #4078 applied and ObjectWrapper.cs as on 4.x, both rows of AChainOfAdjacentHostWrappersRaisesACatchableErrorAndTheEngineRecovers end the test host, each run alone, on net10.0 ("Stack overflow.", ObjectWrapper.Get x2781, exit 0xC00000FD) and on net472 ("Process is terminated due to StackOverflowException."). The three-link AShortChainOfAdjacentWrappersAnswersExactlyAsItDid passes unfixed, which is what says the probe did not change an answer. - Fixed: HostNativeRecursionGuardTests 37/37 on net10.0 and net472. - Both commits, `dotnet test -c Release`: Jint.Tests 7655 + 7570, Jint.Tests.PublicInterface 1903 + 1895 (net10.0 + net472), CommonScripts 28 + 28, SourceGenerators 52, test262 102,509 passed / 0 failed / 175 skipped; 0 failures anywhere. JINT_HOST_CONTRACT_VERIFICATION=1: Jint.Tests 7655 + 7570, Jint.Tests.PublicInterface 1907 + 1899, 0 failures. Public API snapshots unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PanPJbBD7pQC9fRiTpHxvs --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Backport of #3877 (
044f44ae9) to4.x.What the main PR fixed
StackGuard's opt-in backstop sat onScriptFunction's four entry points, so it covered recursion that goes back through interpreted code and nothing else. Recursion that stays inside native frames never reached it:Array.prototype.flat/join/toLocaleStringwalking a nested array,JSON.stringifywalking a nested graph, a chain ofbinds orProxywrappers forwarding one call, a hostClrFunctionor delegate that calls back into the engine. Those end the process with a native stack overflow that nocatchand no constraint can see.EnsureNativeStackHeadroomis the same probe placed on those paths. It is gated onOptions.Constraints.StackOverflowGuardalone --_graphGuardEnabledrenamed_nativeBackstopEnabledfor what it now covers -- because these paths hold ref structs, pooled arrays and identity-bearing state, soMaxExecutionStackCount's stack-hopping lane cannot take them.BindFunctionalso returns its pooled argument buffer in afinally, so an exceptional exit no longer leaks it.Why a 4.x user hits it
4.x has
StackGuardwithEnsureStackHeadroomandHasGraphRecursionHeadroomand noEnsureNativeStackHeadroom, so every one of those shapes kills the process -- and no constraint covers a script stack overflow except this guard. Measured on this branch before the fix, each route on its own, both target frameworks:flat(dense)Process is terminated due to StackOverflowException.Stack overflow.flat(@@species)jointoLocaleStringJSON.stringify(array)JSON.stringify(object)Proxyconstruct chainClrFunctionrecursionConstructorrecursionProxycall chainbindchainThe
bindchain is honest to call a regression guard rather than a fix on this branch: the chain bottoms out in an interpreted function whose entry already probed, so the existing backstop caught it. Nine of the eleven cases end the process on .NET 10 and eight of eleven on .NET Framework.The method is additive and internal; no API and no default changes.
What was adapted
Jint/Native/HostFunction.csdoes not exist on 4.x -- it is a v5 public API for host-defined callables -- so that hunk is dropped. The host-callable surfaces 4.x does have,ClrFunctionandDelegateWrapper, take the probe exactly as on main.JsonSerializerhas noEnterContainerhere. Main's result-limit accounting (_limits,_depth,CountProperties) is part of a security stack 4.x does not carry, so the probe goes to the three_stack.Enter(value)sites instead -- which is whereEnterContaineris called from on main.Engine.cs:SignalErroris stillSignalErroron this branch (Propagate native errors during call argument evaluation #3845 is a separate backport), soConstructNativeFunctionis added above it unchanged.BindFunctionisObjectInstance, IConstructor, ICallablehere, not aFunction, so the probe lands in its explicitICallable.Call/IConstructor.Construct. That is also why the outer call goes throughEngine.Call's non-FunctionICallablearm, which the PR guards too.StackOverflowGuardexplicitly: the guard is opt-in on 4.x where it defaults on upstream, so a default 4.x engine is deliberately still unprotected -- the same call this branch made when A module graph too deep to link now raises an error the host can catch, instead of ending the process #3415 was backported (A module graph too deep to link raises an error the host can catch, instead of ending the process (#3415) #3548). TheHostFunctionhalf of the host-callable test goes with the type; itsClrFunctionandConstructorhalves stay.JsonPoolingInternalsTestsassertion relaxation rides along, sinceBindFunctionnow returns its buffer on the exceptional path too.Verification
Everything below on net472 and net10.0.
HostNativeRecursionGuardTests(the new file)[FATAL ERROR] Xunit.Sdk.TestPipelineException. Per route, the table aboveJint.Tests(whole suite)Jint.Tests.PublicInterface(whole suite)The per-route numbers come from running the built test assemblies directly with a per-case VSTest filter, because a route that overflows takes the test host with it and nothing gets reported through
dotnet test.test262: 102,498 passed / 1 failed / 185 skipped of 102,684, the single failure being
intl402/supportedLocalesOf-unicode-extensions-ignored.jsat 34 s under the loaded run -- the load flake thisbranch already knows, 2 s and green in isolation. Effective 102,499 / 0 / 185, the 4.x control exactly.
dotnet build -c Releaseis clean (the one warning is 4.x's pre-existing MSB3277 inJint.Tests.CommonScripts).🤖 Generated with Claude Code
https://claude.ai/code/session_01SLCujwvKtTvtWD9f6RTyiF