Skip to content

Harden LiveView array wrappers and interop wrapper caches for 4.14#2735

Merged
lahma merged 1 commit into
sebastienros:mainfrom
lahma:fix/liveview-wrapper-hardening
Jul 22, 2026
Merged

Harden LiveView array wrappers and interop wrapper caches for 4.14#2735
lahma merged 1 commit into
sebastienros:mainfrom
lahma:fix/liveview-wrapper-hardening

Conversation

@lahma

@lahma lahma commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Pre-release review hardening for the 4.14 default flips (Interop.ArrayConversion = LiveView, Interop.CacheRecentObjectWrappers = true). All issues below were verified with concrete repros before fixing.

Correctness fixes

  • Read-only declared types produced writable live views. TryConvert routed any runtime array through the LiveView lane keyed off value.GetType(), dropping the declared member type — so IReadOnlyList<int> backed by int[] came out as a writable ArrayWrapper<int> and h.Data[0] = 42 silently mutated host state through a read-only API. Arrays crossing under a non-array declared type now flow through the regular wrapper lane and honor the declared contract (read-only view for IReadOnlyList<T>), the same way other collections already do. Copy mode keeps its snapshot behavior for such members.
  • Static type-mapper poisoning (pre-existing, crash). The _typeMappers memoization registered ConvertArray under the declared type; with IEnumerable<int> first carrying int[] and later List<int>, the memoized mapper cast the list to ArrayInvalidCastException — and since the map is static, one engine's conversion poisoned every engine in the process. Memoization now only happens when the declared type itself is an array type.
  • Identity-map ArgumentException on exposed-type change. ConvertArray's CWT inserts lacked the Remove-before-Add the general lane got with the type-guarded probe; a type-guard miss followed by Add threw. Last view wins now, matching the general lane.
  • Engine.Dispose leaked the recent-wrapper ring. It early-returned when the opt-in identity map was never created — the common configuration since the cache default flip — leaving up to 8 host objects strongly rooted by a disposed engine.
  • Object.keys / for-in / spread on host arrays yielded reflected CLR member names (Length,LongLength,Rank,...) instead of the 0,1,2 the 4.13 Copy default produced. Array-like wrappers now enumerate index keys like JS arrays; members stay accessible, they just don't enumerate. Dictionary-shaped hybrids (Newtonsoft JObject is both IDictionary<string,_> and IList<_>) keep dictionary key enumeration. Note this also applies to wrapped lists, which previously enumerated Capacity,Count.
  • -1 in hostArray was true. The fall-through consulted the reflected indexer, which reports presence for any parseable index. Numeric membership of an array-like view is now exactly [0, length), including string-form and non-canonical keys ('-1', '08').
  • Out-of-range reads returned null instead of undefined, breaking while (a[i] !== undefined) idioms; they now read like JS array holes (matching ArrayLikeOperations.Get, which already did this).

Perf

  • Array.prototype methods (map/join/filter/forEach/indexOf) on array-like wrappers routed element reads through the boxing path (GetAt + FromObject), missing the Convert primitive array-like wrapper elements without boxing #2731 boxing-free lane entirely; ArrayLikeOperations.ReadValue now uses GetJsValueAt.
  • The per-element host-boundary constraint probe in the indexed Get lane is skipped for plain memory-backed targets (T[], List<T>) where element access cannot execute user CLR code; custom IList<T>/IReadOnlyList<T> implementations keep the post-access probe.

Docs

ArrayConversionMode.LiveView and TrackObjectWrapperIdentity XML docs updated: declared-type behavior, index-key enumeration, undefined for out-of-range reads, instanceof Array vs Array.isArray, and the typed-array-like partial-write behavior of shift/splice on fixed-size views.

Full suite green on net10.0 + net472 (Jint.Tests, PublicInterface, CommonScripts) and Test262.

🤖 Generated with Claude Code

https://claude.ai/code/session_0115tQFNyyQqc1HQGPLUgZND

Fixes a set of issues found in a pre-release review of the new LiveView
default and the wrapper identity caches:

- An array crossing under a non-array declared type now honors that
  contract like other collections do: a member typed IReadOnlyList<T>
  produces a read-only view instead of a writable view keyed off the
  runtime type (which silently mutated host state through a read-only
  API). Copy mode keeps its snapshot behavior for such members.
- The static type-mapper memoization no longer registers the array lane
  under non-array declared types; a later non-array value of the same
  declared type (e.g. IEnumerable<int> first carrying int[], then
  List<int>) crashed with InvalidCastException and poisoned every
  engine in the process.
- ConvertArray's identity-map inserts now replace an entry for a
  different exposed view of the same array (last view wins) instead of
  throwing ArgumentException on ConditionalWeakTable.Add.
- Engine.Dispose releases the recent-wrapper ring (on by default since
  4.14); it previously early-returned when the opt-in identity map was
  never created, leaving up to 8 host objects strongly rooted.
- Array-like wrappers enumerate like JS arrays: Object.keys / for-in /
  spread yield index keys instead of reflected CLR member names
  (members like Length stay accessible, they just don't enumerate).
  Dictionary-shaped targets such as Newtonsoft's JObject keep key
  enumeration.
- The in operator matches JS array semantics on array-like views:
  "-1 in view" and out-of-range indices are false (the reflected
  indexer reported presence for any parseable index).
- Out-of-range and negative element reads yield undefined (JS array
  hole semantics) instead of null.
- Array.prototype methods (map/join/filter/forEach) route element reads
  through the boxing-free GetJsValueAt lane instead of boxing through
  GetAt/FromObject.
- The per-element host-boundary constraint probe is skipped for plain
  memory-backed targets (T[], List<T>) where element access cannot run
  user CLR code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115tQFNyyQqc1HQGPLUgZND
@lahma
lahma merged commit d02fc32 into sebastienros:main Jul 22, 2026
4 checks passed
@lahma
lahma deleted the fix/liveview-wrapper-hardening branch July 22, 2026 10:10
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.

1 participant