Limits: the bounds are named properties, and a preset is something you adjust - #3459
Merged
Merged
Conversation
lahma
force-pushed
the
v5-c6-limits-are-properties
branch
2 times, most recently
from
August 27, 2026 03:23
fefd211 to
e6897d7
Compare
lahma
force-pushed
the
v5-c6-limits-are-properties
branch
3 times, most recently
from
August 27, 2026 04:13
88b8fe4 to
cf49b71
Compare
…u adjust `UntrustedCodeLimits` took fifteen constructor parameters, eight of them required and positional, four of those adjacent `TimeSpan`s. Every call site was a column of unlabelled values in an order nobody remembers, and `regexTimeout` and `promiseTimeout` could be swapped without a diagnostic. The repository's own integrator suite had written a wrapper with eight nullable parameters to avoid it. `ResultLimits` had the same shape with five. Both are now records with `init` properties, which is the rule sebastienros#3310 established for `Options` applied to the two bags that still ignored it. The eight that were required are still required, as C# `required` members: omitting one is CS9035 rather than a weaker limit nobody notices. The seven that had defaults keep byte-identical defaults. `UntrustedCodeLimits.Default` is new API, and a preset composes: `Default with { MaxStatements = 5_000 }` satisfies the required members and keeps every dimension it does not name. Validation moved from the constructor to the property, so it also runs for a dimension `with` changes, and names the property rather than the parameter. `JsonSerializer` takes its limits on the constructor, the way `JsonParser` already takes its depth. That deletes `SerializeWithLimits` and the three `Serialize` overloads whose trailing argument had to be repeated at every call site, collapsing eight serialization entry points to four. Two wrappers that existed only to avoid a positional constructor are deleted and replaced by a preset plus `with`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S
sebastienros#3453 landed OptionsCloneReadOnlyTests while this branch was open, and it constructs UntrustedCodeLimits through the positional constructor this change replaces: error CS1739: The best overload for 'UntrustedCodeLimits' does not have a parameter named 'timeoutInterval' Converted to the object initializer, with the same eight values. This is the migration the guide's 3.16 row describes, applied to the one call site that arrived after the rewrite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S
lahma
force-pushed
the
v5-c6-limits-are-properties
branch
from
August 27, 2026 04:32
cd6c179 to
392008e
Compare
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.
Item C6 of the v5 API campaign: optional configuration is
initproperties, never a positional constructor — 3.3's rule applied to the three places that still ignored it.Nothing became looser
Stated first because a wrong default here has no compile-time hint. No dimension a host used to state acquired a default, and no default moved.
requiredmembers. Omitting one isCS9035at the call site, not a weaker limit. That is the strongest available protection during the rewrite this breaking change forces: a host cannot dropTimeSpan.FromSeconds(5)and silently inherit a10.Jint.Tests.PublicInterfacenow pins those seven numbers, because nothing else would notice a weaker one: the value is in no signature a compiler checks, and a looser limit fails no test by failing to fire.UntrustedCodeLimits.Defaultis new API, so nothing can regress through it.UntrustedCodeLimitsFifteen constructor parameters, eight of them required and positional, four of those adjacent
TimeSpans. Every call site was a column of unlabelled values in an order nobody remembers, andregexTimeoutandpromiseTimeoutcould be swapped without a diagnostic. This repository's own integrator suite had written a wrapper with eight nullable parameters and eight??fallbacks to avoid it.Which are
required, and whyrequiredshould mean a value the host genuinely must decide. The call sites decide the answer, and they are unanimous: across the README example, the threat model, the benchmark and four test suites, every one of the eight varies —timeoutIntervalis 5 s / 1 s / 2 s / 750 ms,maxStatementsis 100,000 / 500 / 50,000 / 12,345,maxOperationDurationis 10 s / 2 s / 3 s / 5 s. The seven with defaults are overridden only where a test is deliberately proving that they can be. So the existing split is where the evidence puts it, and it stays.TimeoutIntervalMaxStatementsMemoryLimitMaxRecursionDepthMaxArraySizeRegexTimeoutPromiseTimeoutMaxOperationDurationMaxSourceLength1_000_000MaxNodeCount250_000MaxModuleCount100MaxTotalModuleSourceBytes10_000_000MaxModuleGraphDepth32MaxModuleResolutionHops1_000ResultLimitsResultLimits.Conservativenullalready meant this)UntrustedCodeLimits.Defaultis the one place Jint picks the eight itself, so it takes the stricter value of the two deployment examples Jint documents, dimension by dimension:TimeoutInterval1 s (README 1 s, threat model 2 s),MaxStatements50,000 (100,000 / 50,000),MemoryLimit16,000,000 (both),MaxRecursionDepth64 (both),MaxArraySize10,000 (10,000 / 100,000),RegexTimeout250 ms (both),PromiseTimeout500 ms (1 s / 500 ms),MaxOperationDuration2 s (2 s / 3 s). The other seven are the property defaults, soDefaultis exactly "name only the eight, with Jint's own budget filled in". Its documentation says what it is: Jint guessing on the host's behalf, which is what therequiredmembers exist to prevent.ResultLimitsSame treatment; five optional parameters become five
initproperties with the same unlimited defaults,UnlimitedandConservativestay, andConservative with { MaxStringLength = 4_096 }is now the natural way to adjust one dimension.JsonSerializertakes its limits on the constructorThe way
JsonParseralready takes its depth. The real overload count, since the item's estimate was close but not exact: there were seven methods namedSerializeplusSerializeWithLimits— eight serialization entry points. Four usedOptions.ResultLimits; three took a trailingResultLimitsthat had to be repeated at every call site, where forgetting it silently fell back to the engine's limits, which default to unlimited;SerializeWithLimitswas the three-argument overload with its replacer and space omitted. Eight → four.new JsonSerializer(engine)is unchanged and still takesOptions.ResultLimits— now read once at construction rather than once per call, which is not observable because an engine'sOptionsare frozen before anything can serialize through it.Presets compose on every target framework
Default with { … }needs the record copy constructor to carry[SetsRequiredMembers], andrequired/initneedRequiredMemberAttribute/IsExternalInit, which PolySharp already generates (Jint.csprojconfigures it). Verified two ways: thenet472andnetstandard2.0legs ofJint/Jint.csprojbuild the presets, and a separate cross-assemblynet472probe confirmed a consumer can also writeDefault with { … }against Jint's metadata rather than only Jint itself.Jint.Tests.PublicInterface— the only project withoutInternalsVisibleTo— runs the composition tests onnet472as well asnet8.0andnet10.0.Tests
In
Jint.Tests.PublicInterface:TheDefaultPresetIsAdjustedWithoutRestatingTheRest—withsatisfies the required members, keeps all fourteen dimensions it does not name, and leaves the preset alone.TheDimensionsThatCarryDefaultsCarryTheOnesTheyAlwaysCarried— the seven defaults, by value.ThePresetProfileStillBoundsWhatTheProfileBounds— behaviour, not values: an engine built fromDefault with { MaxStatements = 500 }still overridesAllowClr, still refuseseval, still trips the statement budget, and still reports a clean security configuration.ASerializerConstructedWithLimitsAppliesThemToEveryOverload— all four overloads, plus proof that the engine's own unlimitedResultLimitsis not merged in.ASerializerWithoutExplicitLimitsTakesTheEnginesOwn,AResultLimitsPresetSurvivesBeingAdjusted,AnAdjustedPresetIsStillValidated.The two positional-avoidance wrappers are deleted:
UntrustedCodeProfileTests.CreateLimits(eight nullable parameters, eight??) andHostResultLimitsTests.Limits(five). Both become a preset pluswith, which is the readable proof —Fixture with { MaxArraySize = 2 }at the call site, and the sixteen validation rows now readFixture with { TimeoutInterval = TimeSpan.Zero }againstnameof(UntrustedCodeLimits.TimeoutInterval).Also in this PR
UndocumentedPublicApi.txtshrinks by two (both new constructors ship documented).docs/v5-migration.md§2.6 (the serializer's limits are its own) and §3.16 (the two limit bags are named properties), plus four rows in the §2 removal table..github/THREAT_MODEL.md's hardened baseline rewritten to the new shape.Verification
dotnet build -c Release(solution) anddotnet test -c Releasegreen, including thenet472legs ofJint.Tests(7,414),Jint.Tests.PublicInterface(2,522) andJint.Tests.CommonScripts.Jint.Tests.PublicInterfacealso run once withJINT_HOST_CONTRACT_VERIFICATION=1, all three frameworks green. test262: 102,495 passed / 0 failed / 189 skipped.🤖 Generated with Claude Code
https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S