Skip to content

Default CLR array projection to isolated copies - #3056

Merged
lahma merged 1 commit into
sebros/disable-clr-writes-defaultfrom
sebros/default-clr-array-copy-498
Aug 21, 2026
Merged

lahma merged 1 commit into
sebros/disable-clr-writes-defaultfrom
sebros/default-clr-array-copy-498

Conversation

@sebastienros

Copy link
Copy Markdown
Owner

Summary

  • make ArrayConversionMode.Copy the breaking security default while retaining explicit LiveView compatibility
  • keep copied arrays as mutable engine-owned JS snapshots while requiring separate AllowClrWrite() authority for LiveView write-through
  • preserve cyclic/deep graph identity with transactional cache publication, rollback, resource accounting, and weak converter-aware provenance
  • document the migration and add public/core coverage plus a trustworthy cold-projection benchmark

Security stack

#3054 -> #3052 -> #3051 -> #3046 -> #3045 -> #3037 -> #3036 -> #3035 -> #3030

Validation

  • Release Jint multi-target build (net462, netstandard2.0, netstandard2.1, net8.0, net10.0)
  • PublicInterface: 1,745 tests default; 1,745 with host-contract verification
  • Core under UTC: 5,800 tests
  • Focused host-verification interop/memory/result/GC/concurrency: 439 tests
  • Benchmark project Release build and git diff --check
  • clean specialist follow-up review

Benchmark

Default BenchmarkDotNet job, ClrArrayProjectionBenchmark (cold first crossing):

Length Copy LiveView Copy allocated LiveView allocated
0 445.98 ns 53.29 ns 458 B 192 B
10 574.63 ns 53.50 ns 774 B 192 B
100 2.104 us 52.60 ns 3,661 B 192 B
1,000 16.764 us 53.58 ns 32,465 B 192 B

@lahma lahma left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed as the incremental diff against sebros/disable-clr-writes-default. Approving.

The two defaults compose coherently: with AllowWrite now false (#3054), a LiveView array would refuse push/element writes, so making Copy the default actually restores script-side array mutability for the common case — the script gets a snapshot it can freely mutate, and the host graph is isolated. Good sequencing.

The transactional ArrayCopyContext earns its complexity: cyclic and mutually-referential CLR arrays reproduce their cycles in the JS snapshot, partially-built snapshots can never leak into _objectWrapperCache/the recent-wrapper ring when a constraint aborts mid-copy (publication is deferred to root success, with a real savepoint/rollback on the recent cache), and the generation-splicing hazard — a cached nested snapshot pointing back into an older copy of the same cycle — is explicitly guarded by CanReuseCachedArray. The iterative worklist keeps deep graphs off the native stack, and CheckInteropProjectionConstraints is a thoughtful touch: cancellation and an armed OperationDeadlineConstraint still bound a host-side projection that has no execution entry to charge.

Notes:

  1. Third default flip in the stack; same versioning point as #3054/#3051. This one has an extra wrinkle: the LiveView default only shipped in 4.14, so hosts have now been asked to adapt twice in one year. The release notes should present #3054 + #3056 together as one migration story ("writes need AllowClrWrite(), live views need ArrayConversionMode.LiveView"), because the pair reads much more sensibly than either alone.
  2. The benchmark table quotes Copy vs LiveView, but the interesting regression check is new-Copy (with the context bookkeeping) vs the old simple copy loop for the dominant flat, acyclic case — the CWT Begin/Complete per array isn't free. Non-blocking, but if you have the pre-context numbers handy, put them in the PR body; if flat-array copies got measurably slower, a fast path that skips the context when the element type can't contain an Array (primitives, strings, sealed non-array element types) would recover it cheaply.
  3. Doc nit: the enum doc now says non-empty multidimensional arrays "throw during conversion" — worth stating the exception type so hosts can catch it deliberately.

Stack merge still gated on the #3035 fix below.

@lahma
lahma force-pushed the sebros/default-clr-array-copy-498 branch from 99f0904 to ea98a9b Compare August 21, 2026 19:07
@lahma
lahma merged commit 5da1196 into main Aug 21, 2026
5 checks passed
@lahma
lahma deleted the sebros/default-clr-array-copy-498 branch August 21, 2026 19:59
lahma added a commit to lahma/jint that referenced this pull request Aug 23, 2026
…lly has

Main has moved 181 commits past v4.16.0 and nothing records what an embedder
has to react to. Most of it is invisible to a compiler: every entry below still
compiles exactly as it did in 4.16 and behaves differently at run time, and six
of them flip a default.

`docs/v5-migration.md` is the artefact those entries go in. It is deliberately
not a second README: a table per change, before/after code where it helps, and
the rationale left in the pull request it cites.

Seeded from git history, every claim checked against the tree rather than
against the pull request title:

  * sebastienros#3054 `Interop.AllowWrite` true -> false. A projected CLR write is now
    silently ignored in sloppy mode and a TypeError in strict mode.
  * sebastienros#3056 `Interop.ArrayConversion` LiveView -> Copy. `Array.isArray` flips to
    true, `push`/`length` stop throwing, and CLR-side mutations after the
    crossing stop being visible.
  * sebastienros#3057 `Constraints.StackOverflowGuard` false -> true. RangeError instead of
    a process that ends with no exception in the log.
  * sebastienros#3058 `AgentCanSuspend` true -> false. `Atomics.wait` is a TypeError before
    a waiter is registered; `waitAsync` is untouched.
  * sebastienros#3052 namespace type discovery loses its implicit fallback to
    `Assembly.GetCallingAssembly()` / `GetExecutingAssembly()` /
    `Type.GetType(name)` and becomes the `AllowedAssemblies` allow-list.
  * sebastienros#3051 host exception, module-load and CLR-resolution messages are redacted
    from script; `ExposeDetailedErrors()` restores all three.
  * sebastienros#3035 concurrent `Engine` use fails fast, and an engine stays reserved for
    the lifetime of a returned async Task.
  * sebastienros#3036 `LimitMemory` charges allocations across async continuations, so a
    budget can now trip where one synchronous segment never reached it.
  * sebastienros#3252 every `*Async` entry reports the operation's failures through the
    task and only a usage error out of the call.
  * sebastienros#3248 an array-like `length` above 2^32-1 stops answering differently per
    target framework.
  * sebastienros#3037, sebastienros#3045, sebastienros#3046 add parser, module-graph and result bounds that all
    default to unlimited, so they change nothing until configured; sebastienros#3059 and
    sebastienros#3060 add the diagnostics and the hardened profile on top.

Two entries the prompt for this work had slightly differently, both checked and
written as the tree has them: the stack-overflow guard raises a catchable
`RangeError`, not a `RecursionDepthOverflowException`, and `ArrayOperations` is
internal, so sebastienros#3248 removes no public member — its break is what a script sees.

Sections 2, 3 and 6 (removed API, renamed API, AOT) are explicit empty
placeholders. Nothing public has been removed or renamed since v4.16.0, and a
parallel task is measuring the AOT state.

The target-framework section is written and marked pending: `Jint.csproj` still
lists net462, and the net472 change is not on main yet.

`Jint/AGENTS.md` gains the rule that makes the guide stay current — a change to
anything in its public-contract table is a row in the guide, in the same pull
request, including a change that breaks nothing at compile time. The root
`AGENTS.md` is untouched; it is at 23 KB against a 24 KiB budget.

README's "Branches and releases" described `main` alone and did not mention the
`3.x` branch at all. It now has a row per live branch, in the same wording
sebastienros#3291 gives the 4.x branch's own copy, plus a pointer to the guide.

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 23, 2026
…lly has (#3294)

Main has moved 181 commits past v4.16.0 and nothing records what an embedder
has to react to. Most of it is invisible to a compiler: every entry below still
compiles exactly as it did in 4.16 and behaves differently at run time, and six
of them flip a default.

`docs/v5-migration.md` is the artefact those entries go in. It is deliberately
not a second README: a table per change, before/after code where it helps, and
the rationale left in the pull request it cites.

Seeded from git history, every claim checked against the tree rather than
against the pull request title:

  * #3054 `Interop.AllowWrite` true -> false. A projected CLR write is now
    silently ignored in sloppy mode and a TypeError in strict mode.
  * #3056 `Interop.ArrayConversion` LiveView -> Copy. `Array.isArray` flips to
    true, `push`/`length` stop throwing, and CLR-side mutations after the
    crossing stop being visible.
  * #3057 `Constraints.StackOverflowGuard` false -> true. RangeError instead of
    a process that ends with no exception in the log.
  * #3058 `AgentCanSuspend` true -> false. `Atomics.wait` is a TypeError before
    a waiter is registered; `waitAsync` is untouched.
  * #3052 namespace type discovery loses its implicit fallback to
    `Assembly.GetCallingAssembly()` / `GetExecutingAssembly()` /
    `Type.GetType(name)` and becomes the `AllowedAssemblies` allow-list.
  * #3051 host exception, module-load and CLR-resolution messages are redacted
    from script; `ExposeDetailedErrors()` restores all three.
  * #3035 concurrent `Engine` use fails fast, and an engine stays reserved for
    the lifetime of a returned async Task.
  * #3036 `LimitMemory` charges allocations across async continuations, so a
    budget can now trip where one synchronous segment never reached it.
  * #3252 every `*Async` entry reports the operation's failures through the
    task and only a usage error out of the call.
  * #3248 an array-like `length` above 2^32-1 stops answering differently per
    target framework.
  * #3037, #3045, #3046 add parser, module-graph and result bounds that all
    default to unlimited, so they change nothing until configured; #3059 and
    #3060 add the diagnostics and the hardened profile on top.

Two entries the prompt for this work had slightly differently, both checked and
written as the tree has them: the stack-overflow guard raises a catchable
`RangeError`, not a `RecursionDepthOverflowException`, and `ArrayOperations` is
internal, so #3248 removes no public member — its break is what a script sees.

Sections 2, 3 and 6 (removed API, renamed API, AOT) are explicit empty
placeholders. Nothing public has been removed or renamed since v4.16.0, and a
parallel task is measuring the AOT state.

The target-framework section is written and marked pending: `Jint.csproj` still
lists net462, and the net472 change is not on main yet.

`Jint/AGENTS.md` gains the rule that makes the guide stay current — a change to
anything in its public-contract table is a row in the guide, in the same pull
request, including a change that breaks nothing at compile time. The root
`AGENTS.md` is untouched; it is at 23 KB against a 24 KiB budget.

README's "Branches and releases" described `main` alone and did not mention the
`3.x` branch at all. It now has a row per live branch, in the same wording
#3291 gives the 4.x branch's own copy, plus a pointer to the guide.


Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S

Co-authored-by: Claude Opus 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