Skip to content

Backport #3877 to 4.x: Guard native recursion and forwarding paths - #4007

Merged
lahma merged 4 commits into
sebastienros:4.xfrom
lahma:backport/4x-3877
Sep 9, 2026
Merged

lahma merged 4 commits into
sebastienros:4.xfrom
lahma:backport/4x-3877

Conversation

@lahma

@lahma lahma commented Sep 9, 2026 •

Copy link
Copy Markdown
Collaborator

Backport of #3877 (044f44ae9) to 4.x.

What the main PR fixed

StackGuard's opt-in backstop sat on ScriptFunction'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/toLocaleString walking a nested array, JSON.stringify walking a nested graph, a chain of binds or Proxy wrappers forwarding one call, a host ClrFunction or delegate that calls back into the engine. Those end the process with a native stack overflow that no catch and no constraint 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, so MaxExecutionStackCount's stack-hopping lane cannot take them. BindFunction also returns its pooled argument buffer in a finally, so an exceptional exit no longer leaks it.

Why a 4.x user hits it

4.x has StackGuard with EnsureStackHeadroom and HasGraphRecursionHeadroom and no EnsureNativeStackHeadroom, 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:

route net472 net10.0
flat (dense) Process is terminated due to StackOverflowException. Stack overflow.
flat (@@species) terminated stack overflow
join terminated stack overflow
toLocaleString terminated stack overflow
JSON.stringify (array) terminated stack overflow
JSON.stringify (object) terminated stack overflow
Proxy construct chain terminated stack overflow
host ClrFunction recursion terminated stack overflow
host Constructor recursion terminated stack overflow
Proxy call chain already caught (the .NET Framework JIT tail-calls the empty forward) stack overflow
bind chain already caught already caught

The bind chain 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.cs does 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, ClrFunction and DelegateWrapper, take the probe exactly as on main.
  • JsonSerializer has no EnterContainer here. 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 where EnterContainer is called from on main.
  • One context conflict in Engine.cs: SignalError is still SignalError on this branch (Propagate native errors during call argument evaluation #3845 is a separate backport), so ConstructNativeFunction is added above it unchanged.
  • BindFunction is ObjectInstance, IConstructor, ICallable here, not a Function, so the probe lands in its explicit ICallable.Call/IConstructor.Construct. That is also why the outer call goes through Engine.Call's non-Function ICallable arm, which the PR guards too.
  • Tests transcribed from NUnit to xUnit, and every engine in them asks for StackOverflowGuard explicitly: 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). The HostFunction half of the host-callable test goes with the type; its ClrFunction and Constructor halves stay.
  • The JsonPoolingInternalsTests assertion relaxation rides along, since BindFunction now returns its buffer on the exceptional path too.

Verification

Everything below on net472 and net10.0.

unfixed 4.x with the fix
HostNativeRecursionGuardTests (the new file) the whole run dies: net10.0 reports nothing at all, net472 gets 2 of 11 out before [FATAL ERROR] Xunit.Sdk.TestPipelineException. Per route, the table above 11 of 11 pass on both legs
Jint.Tests (whole suite) -- 7102/7106 net10.0, 7017/7021 net472, 0 failed
Jint.Tests.PublicInterface (whole suite) -- 1847/1856 net10.0, 1839/1848 net472, 0 failed

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.js at 34 s under the loaded run -- the load flake this
branch already knows, 2 s and green in isolation. Effective 102,499 / 0 / 185, the 4.x control exactly.

dotnet build -c Release is clean (the one warning is 4.x's pre-existing MSB3277 in Jint.Tests.CommonScripts).

🤖 Generated with Claude Code

https://claude.ai/code/session_01SLCujwvKtTvtWD9f6RTyiF

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 and others added 3 commits September 9, 2026 11:56
…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
@lahma
lahma merged commit 730db51 into sebastienros:4.x Sep 9, 2026
5 checks passed
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>
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>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=Jint&package-manager=nuget&previous-version=4.16.1&new-version=4.16.2)](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>
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>
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.

2 participants