Repository navigation
Native AOT: the degraded IList<T> lane is probed, and it is only reached when the interface's indexer metadata is rooted - #3393
Merged
Conversation
…ached when the interface's indexer metadata is rooted `ArrayOperations.IndexWrappedOperations` is, after sebastienros#3356, reachable in one situation: a wrapped CLR collection with an index to read but no `IList` to read it through, which is the Native AOT degradation of an `IList<T>` that is not a non-generic `IList`. Nothing exercised it, so the two latent bugs repaired in passing while fixing sebastienros#3302 - the reflection fallback handing `PropertyInfo.GetValue` the *wrapper* rather than the CLR target, and `SetLength` throwing a bare `NotSupportedException` where the wrapper's read-only `length` owes script a `TypeError` - had nothing to fail against. Two probes in `Jint.AotExample/Program.cs` now pin it: a reference-type sibling that keeps the typed `GenericListWrapper<string>` on every runtime, and the value-type row that degrades. The degraded one asserts the reads that go through the reflection fallback (`join`, `indexOf`, `filter`), the direct `host[1]` that never used the lane and is why nothing caught the receiver bug, and `Array.prototype.splice.call(host, 0, 0)` - the shortest generic that reaches `SetLength` and nothing else, and therefore the one whose refusal is the `TypeError` rather than the CLR exception. Reaching the lane at all takes a root, and that is the part worth writing down. The lane needs the `PropertyInfo` for `IList<int>.Item`, which Jint obtains by reflecting over an interface it discovered at run time; no signature preserves it, so ILC trims it. Measured on the published binary: `typeof(IList<int>).GetProperties()` returned an empty array, `TypeDescriptor` reported no integer index, and the collection was served by `ObjectOperations` instead - the same answers from a different lane, with no diagnostic anywhere. The probe carries a `[DynamicDependency]` for it and asserts the precondition, so a lost root fails the run instead of quietly moving the probe off the lane. Native run: 25 probes / 5 gaps before, 27 probes / 5 gaps after. Verified by publishing with the two repairs reverted one at a time: the receiver bug fails the probe as `System.Reflection.TargetException: Object type System.Collections.Generic.IList`1[System.Int32] does not match target type Jint.Runtime.Interop.ObjectWrapper`, and the `SetLength` bug as `System.NotSupportedException`. Fixes sebastienros#3362. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S
lahma
added a commit
to lahma/jint
that referenced
this pull request
Aug 27, 2026
…ic that must grow refuses instead of leaking Fixes sebastienros#3394. `ArrayOperations.IndexWrappedOperations` was believed to be reachable only under Native AOT — its own comment said so, sebastienros#3362 said so, and the probe sebastienros#3393 added pins it there. It is entered under a plain JIT whenever a host object is **exposed** as `IList<T>`: engine.SetValue("host", ObjectWrapper.Create(engine, hostList, typeof(IList<int>))); `ObjectWrapper.Create(Engine, object, Type?)` is public API and the exposed-type parameter is exactly what a `WrapObjectDelegate` supplies. `ResolveArrayLikeWrapperFactoryType` looks for `IList<>` among the exposed type's `GetInterfaces()`, and an interface is not among its own, so no typed wrapper is built; the target is not a non-generic `IList`, so there is no `ListWrapper` to fall back to; and the plain `ObjectWrapper` that results still reports an integer index, which is what admits it to the lane. `Jint.Tests.PublicInterface/HostExposedCollectionTypeTests.cs` covers that route on every leg and every target framework, with no `[DynamicDependency]` — the AOT probe needs one because ILC trims `IList<int>.Item`'s metadata, which is its own finding. It pins the two repairs sebastienros#3381 made blind: the reflection fallback handing `PropertyInfo.GetValue` the wrapper instead of the CLR target, and a `SetLength` that threw a bare `NotSupportedException` where a read-only `length` owes script a `TypeError`. Writing that coverage found a third defect on the same lane, live on both runtimes: `Array.prototype.push.call(host, 4)` and `unshift` raised the collection's own `ArgumentOutOfRangeException` out of `Evaluate`. `IndexWrappedOperations.Set` grew through `_list` when it had one and otherwise wrote through the reflected indexer, which takes the out-of-range index straight to the collection. A position the view cannot hold is now refused the way its read-only `length` already is — the `TypeError` a `Set(O, k, v, true)` raises on a false `[[Set]]` — and the collection is left untouched. The two comments that call the lane AOT-only are corrected, in `ArrayOperations.cs` and in `Jint.AotExample/Program.cs`; the probe keeps its own reason to exist, which is the value-type degradation only a published native binary produces. `docs/v5-migration.md` §4.32 carries the embedder-facing form. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S
lahma
added a commit
to lahma/jint
that referenced
this pull request
Aug 27, 2026
…ic that must grow refuses instead of leaking Fixes sebastienros#3394. `ArrayOperations.IndexWrappedOperations` was believed to be reachable only under Native AOT — its own comment said so, sebastienros#3362 said so, and the probe sebastienros#3393 added pins it there. It is entered under a plain JIT whenever a host object is **exposed** as `IList<T>`: engine.SetValue("host", ObjectWrapper.Create(engine, hostList, typeof(IList<int>))); `ObjectWrapper.Create(Engine, object, Type?)` is public API and the exposed-type parameter is exactly what a `WrapObjectDelegate` supplies. `ResolveArrayLikeWrapperFactoryType` looks for `IList<>` among the exposed type's `GetInterfaces()`, and an interface is not among its own, so no typed wrapper is built; the target is not a non-generic `IList`, so there is no `ListWrapper` to fall back to; and the plain `ObjectWrapper` that results still reports an integer index, which is what admits it to the lane. `Jint.Tests.PublicInterface/HostExposedCollectionTypeTests.cs` covers that route on every leg and every target framework, with no `[DynamicDependency]` — the AOT probe needs one because ILC trims `IList<int>.Item`'s metadata, which is its own finding. It pins the two repairs sebastienros#3381 made blind: the reflection fallback handing `PropertyInfo.GetValue` the wrapper instead of the CLR target, and a `SetLength` that threw a bare `NotSupportedException` where a read-only `length` owes script a `TypeError`. Writing that coverage found a third defect on the same lane, live on both runtimes: `Array.prototype.push.call(host, 4)` and `unshift` raised the collection's own `ArgumentOutOfRangeException` out of `Evaluate`. `IndexWrappedOperations.Set` grew through `_list` when it had one and otherwise wrote through the reflected indexer, which takes the out-of-range index straight to the collection. A position the view cannot hold is now refused the way its read-only `length` already is — the `TypeError` a `Set(O, k, v, true)` raises on a false `[[Set]]` — and the collection is left untouched. The two comments that call the lane AOT-only are corrected, in `ArrayOperations.cs` and in `Jint.AotExample/Program.cs`; the probe keeps its own reason to exist, which is the value-type degradation only a published native binary produces. `docs/v5-migration.md` §4.32 carries the embedder-facing form. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S
lahma
added a commit
to lahma/jint
that referenced
this pull request
Aug 27, 2026
…ic that must grow refuses instead of leaking Fixes sebastienros#3394. `ArrayOperations.IndexWrappedOperations` was believed to be reachable only under Native AOT — its own comment said so, sebastienros#3362 said so, and the probe sebastienros#3393 added pins it there. It is entered under a plain JIT whenever a host object is **exposed** as `IList<T>`: engine.SetValue("host", ObjectWrapper.Create(engine, hostList, typeof(IList<int>))); `ObjectWrapper.Create(Engine, object, Type?)` is public API and the exposed-type parameter is exactly what a `WrapObjectDelegate` supplies. `ResolveArrayLikeWrapperFactoryType` looks for `IList<>` among the exposed type's `GetInterfaces()`, and an interface is not among its own, so no typed wrapper is built; the target is not a non-generic `IList`, so there is no `ListWrapper` to fall back to; and the plain `ObjectWrapper` that results still reports an integer index, which is what admits it to the lane. `Jint.Tests.PublicInterface/HostExposedCollectionTypeTests.cs` covers that route on every leg and every target framework, with no `[DynamicDependency]` — the AOT probe needs one because ILC trims `IList<int>.Item`'s metadata, which is its own finding. It pins the two repairs sebastienros#3381 made blind: the reflection fallback handing `PropertyInfo.GetValue` the wrapper instead of the CLR target, and a `SetLength` that threw a bare `NotSupportedException` where a read-only `length` owes script a `TypeError`. Writing that coverage found a third defect on the same lane, live on both runtimes: `Array.prototype.push.call(host, 4)` and `unshift` raised the collection's own `ArgumentOutOfRangeException` out of `Evaluate`. `IndexWrappedOperations.Set` grew through `_list` when it had one and otherwise wrote through the reflected indexer, which takes the out-of-range index straight to the collection. A position the view cannot hold is now refused the way its read-only `length` already is — the `TypeError` a `Set(O, k, v, true)` raises on a false `[[Set]]` — and the collection is left untouched. The two comments that call the lane AOT-only are corrected, in `ArrayOperations.cs` and in `Jint.AotExample/Program.cs`; the probe keeps its own reason to exist, which is the value-type degradation only a published native binary produces. `docs/v5-migration.md` §4.32 carries the embedder-facing form. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S
lahma
added a commit
that referenced
this pull request
Aug 27, 2026
…ic that must grow refuses instead of leaking (#3425) Fixes #3394. `ArrayOperations.IndexWrappedOperations` was believed to be reachable only under Native AOT — its own comment said so, #3362 said so, and the probe #3393 added pins it there. It is entered under a plain JIT whenever a host object is **exposed** as `IList<T>`: engine.SetValue("host", ObjectWrapper.Create(engine, hostList, typeof(IList<int>))); `ObjectWrapper.Create(Engine, object, Type?)` is public API and the exposed-type parameter is exactly what a `WrapObjectDelegate` supplies. `ResolveArrayLikeWrapperFactoryType` looks for `IList<>` among the exposed type's `GetInterfaces()`, and an interface is not among its own, so no typed wrapper is built; the target is not a non-generic `IList`, so there is no `ListWrapper` to fall back to; and the plain `ObjectWrapper` that results still reports an integer index, which is what admits it to the lane. `Jint.Tests.PublicInterface/HostExposedCollectionTypeTests.cs` covers that route on every leg and every target framework, with no `[DynamicDependency]` — the AOT probe needs one because ILC trims `IList<int>.Item`'s metadata, which is its own finding. It pins the two repairs #3381 made blind: the reflection fallback handing `PropertyInfo.GetValue` the wrapper instead of the CLR target, and a `SetLength` that threw a bare `NotSupportedException` where a read-only `length` owes script a `TypeError`. Writing that coverage found a third defect on the same lane, live on both runtimes: `Array.prototype.push.call(host, 4)` and `unshift` raised the collection's own `ArgumentOutOfRangeException` out of `Evaluate`. `IndexWrappedOperations.Set` grew through `_list` when it had one and otherwise wrote through the reflected indexer, which takes the out-of-range index straight to the collection. A position the view cannot hold is now refused the way its read-only `length` already is — the `TypeError` a `Set(O, k, v, true)` raises on a false `[[Set]]` — and the collection is left untouched. The two comments that call the lane AOT-only are corrected, in `ArrayOperations.cs` and in `Jint.AotExample/Program.cs`; the probe keeps its own reason to exist, which is the value-type degradation only a published native binary produces. `docs/v5-migration.md` §4.32 carries the embedder-facing form. Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S Co-authored-by: Claude Opus 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.
Fixes #3362.
ArrayOperations.IndexWrappedOperationsis, after #3356, reachable in one situation: a wrapped CLRcollection with an index to read but no
IListto read it through — the Native AOT degradation of anIList<T>that is not a non-genericIList. Nothing exercised it, so the two latent bugs repaired inpassing while fixing #3302 had nothing to fail against. They do now.
What the probes assert
Two rows in
Jint.AotExample/Program.cs, a pair the way every generic site in that file is a pair.IList<string> without IList— the reference-type sibling.GenericListWrapperFactory<string>shares canonical code, so this host keeps the typed wrapper on every runtime and its
lengthstayswritable. It is what makes the row below a boundary rather than an assertion about one type.
IList<int> without IList— the value-type row, which degrades to a plainObjectWrapperunderNative AOT and is served by
IndexWrappedOperations. It asserts:Array.prototype.join,indexOfandfilter,every element of which arrives via
PropertyInfo.GetValueoverIList<int>.Itemwith the CLRcollection as the receiver (the first repaired bug passed the wrapper);
host[1], the contrast: it resolves the type's own indexer, never used that lane, and worked allalong — which is exactly why nothing ever caught the receiver bug;
Array.prototype.splice.call(host, 0, 0), the shortest generic that reachesSetLengthandnothing else (a
deleteCountof 0 with no items performs no element write and no delete, justSet(O, "length", len, true)). The degraded wrapper has noIListto resize, so that write meetsthe wrapper's read-only
lengthand becomes aTypeErrorscript can catch — where the secondrepaired bug threw a bare
NotSupportedExceptionout ofEvaluate.The host type carries a non-generic
ICollectionas well asIList<T>, and that is load-bearingrather than decorative:
ArrayOperations.Forreads the count offICollection, so anIList<T>without it falls through to
ObjectOperationsand never reaches the lane.How I established the lane is genuinely reached
Not from the code alone — the first version of this probe passed with both bugs put back, which is
how the real obstacle turned up.
I instrumented
ArrayOperations.ForandTypeDescriptor.Analyze(temporarily, reverted) andpublished + ran the native binary. It printed
IndexedItems`1 hasIdx=False arrayLike=True intIdx=False idxProp=<null>andIList`1 … item=<null> props=0:typeof(IList<int>).GetProperties()returns an empty array inthe published binary. The lane needs the
PropertyInfoforIList<int>.Item, which Jint obtainsby reflecting over an interface it discovered at run time, so no signature preserves it and ILC
trims it.
TypeDescriptortherefore reported no integer index,HasIndexedElementswas false, andthe collection was served by
ObjectOperations— the same answers, from a different lane, with nodiagnostic anywhere. That is the same failure mode this project already roots its own assembly to
avoid (the
CompanyExtensions.Shoutnote in the csproj).Adding
[DynamicDependency(PublicProperties, typeof(IList<int>))]to the host type flipped it:hasIdx=True intIdx=True idxProp=IList`1and[lane] -> IndexWrappedOperationson everyArray.prototypecall in the probe.The instrumentation is gone; two things keep the probe honest in its place — an assertion that the
wrapper actually is the degraded plain
ObjectWrapperunder AOT and the typed one under a JIT, andan assertion that
typeof(IList<int>).GetProperty("Item")is non-null, so a lost root fails the runinstead of quietly moving the probe onto
ObjectOperations.Finally, the decisive check: published and ran the native binary with each repair reverted in
turn.
FAIL … System.Reflection.TargetException: Object type System.Collections.Generic.IList`1[System.Int32] does not match target type Jint.Runtime.Interop.ObjectWrapper.SetLengthbug →FAIL … System.NotSupportedException: Specified method is not supported.Both restored, both green.
Probe counts
ALL PROBES PASSEDand exit 0 in both directions, and the same 27/5 under a JIT (dotnet run).Verification
dotnet publish Jint.AotExample -c Release, binary run nativelyALL PROBES PASSED (27 probes, 5 known AOT gaps)dotnet build -c ReleaseMSB3277is the pre-existingSystem.Memoryconflict inJint.Tests.CommonScriptsonnet472Jint.TestsJint.Tests.PublicInterfaceJint.Tests.Test262Jint.AotExample/Program.csonlyVerifysnapshot and noUndocumentedPublicApi.txtmovementNo
docs/v5-migration.mdrow: nothing an embedder observes changed, and #3385 is editing that file.Nothing under
Jint/Runtime/Interop/AGENTS.mdeither, since that file is over budget and being fixedseparately. Nothing here contradicts #3385 — it does not touch
IndexWrappedOperations, and the hostcollection this probe uses is growable and not read-only, so its
CanWrite/IsReadOnlychange doesnot reach it.
Deferred
The lane is not actually AOT-only, and that is worth a test that runs on every leg. While proving
the above I found that
IndexWrappedOperationsis entered under a plain JIT whenever the target isexposed as
IList<T>—ObjectWrapper.Create(engine, target, typeof(IList<int>)), which is publicAPI and what a
WrapObjectDelegatedoes — becausetypeof(IList<int>).GetInterfaces()does notinclude
IList<>itself, soResolveArrayLikeWrapperFactoryTypefinds no typed factory and the targetis not an
IListto fall back on. Verified:[lane] -> IndexWrappedOperations,Array.prototype.join.call(host, '-') === "1-2-3", wrapperObjectWrapper. AJint.Tests.PublicInterfacetest for that shape would pin both repaired bugs in every test leg ratherthan only in the AOT one, and would need no
[DynamicDependency]. Filed as #3394 rather than foldedin, so this PR stays the probe #3362 asks for; #3394 also carries the two comments that call the lane
AOT-only and should be corrected with it.
🤖 Generated with Claude Code
https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S