ReviewOS

zig-utils/zig-js

Intl.Locale: publish native immutable locale state

#590
Closed chris opened this 9 days ago ·
9 days ago

Problem

Intl.Locale currently exposes its specification-internal [[Locale]] state as a forgeable \0locale ordinary own property. The constructor, CanonicalizeLocaleList, every prototype accessor, and every prototype method recover the brand through generic named-property lookup; most consumers then rescan the same canonical BCP 47 tag on every call.

This is both a security boundary defect (a forged NUL-named property passes the brand check and bypasses ordinary conversion) and unnecessary steady-state work (property synchronization plus repeated subtag/Unicode-extension parsing).

Parent work: #475, #460. Related internal-state migrations: #528, #589. Worst-case/bounded-latency context: #493.

Scope

  • Freeze a representative Locale reflection workload with construction outside the timed boundary and checksum parity with JavaScriptCore.
  • Publish Intl.Locale only after all constructor coercions and option observations complete, preserving the required order: language, script, region, variants, calendar, collation, hourCycle, caseFirst, numeric, numberingSystem, firstDayOfWeek.
  • Replace the forgeable ordinary property with object-owned immutable native state containing the one canonical GC-managed locale string plus precomputed byte ranges/closed values for the base-name components and supported Unicode extension keys.
  • Add explicit strong tracing and moving-GC relocation for the retained string; finalize the native record exactly once through object backing ownership.
  • Route construction from an existing Locale, CanonicalizeLocaleList, all accessors, toString, maximize/minimize, and Locale info methods through the unforgeable native brand.
  • Preserve Locale subclass behavior: internal-slot consumers must recognize branded subclass instances without observing an overridden toString.

Acceptance

  • A plain object with a forged \0locale property is rejected by Locale prototype accessors/methods and is treated as an ordinary object by locale-list/constructor conversion.
  • Intl.Locale instances publish no NUL-prefixed own property.
  • Repeated reflection performs no generic own-property lookup and does not reparse the canonical tag.
  • Constructor failure/OOM publishes no partial native state; installed state survives moving GC and shared/no-GIL access without races or leaks.
  • The frozen workload has the exact same checksum on zig-js and JavaScriptCore.
  • test/intl402/Locale remains at least 152/152, the full Intl402 subtree has no regressions, focused normal/TSan tests pass, and the full sharded unit suite remains green.
  • Exact-parent timing and allocation/locking telemetry are reported without a throughput claim unless the sample window is stable.
9 days ago

Boundary and implementation update:

  • Frozen reflection boundary landed in 71387a9e: construction stays outside the timed region; the row consumes the canonical tag, all base-name fields, all supported Unicode-keyword accessors, sparse/undefined fields, and numeric.
  • Exact 10-job checksum is 967094315 in both zig-js and JavaScriptCore. The 100-job attribution checksum is 670943087 in both exact-parent and candidate builds.
  • Pre-change Locale conformance was 152/152.

The candidate now stores one canonical GC-managed string plus precomputed immutable byte ranges in an object-owned native record. Constructor-from-Locale, CanonicalizeLocaleList, every accessor, toString, maximize/minimize, and every Locale info method require the native brand. The retained string has an insertion barrier, explicit strong tracing, moving-GC relocation, and exact-once object-backing finalization.

Validation so far:

  • test/intl402/Locale: 152/152
  • full test/intl402: 3341/3341
  • focused normal unit tests: 5/5, 0 leaks
  • focused TSan unit tests: 5/5, 0 leaks
  • the tests include failure-atomic OOM publication, forged-NUL rejection, option getter order, branded subclass conversion without toString, bounded-heap reflection, actual nursery relocation of both the Locale object and canonical StringCell, and shared-realm concurrent reads

Exact-parent 100-job attribution (parent 71387a9e → candidate):

  • property-lock acquisitions: 5,068,738 → 4,569,506 (-499,232 / -9.85%)
  • named-snapshot acquisitions: 774,075 → 774,075 (unchanged)
  • other property-lock acquisitions: 4,293,063 → 3,793,831 (-499,232)
  • backing allocations: 2,871,702 → 2,871,686 (-16)
  • GC-cell allocations: 1,601,825 → 1,601,825 (unchanged)
  • collections: 52 → 52 (unchanged)
  • current backing: 101,768,255 → 101,768,716 (+461 bytes)
  • peak backing: 107,641,940 → 107,642,398 (+458 bytes)

Two order-balanced seven-pair timing windows showed small same-direction medians (793.192 → 786.484 ms, -0.85%; 810.931 → 806.648 ms, -0.53%; candidate won 6/7 pairs in each). I am treating those as focused diagnostic evidence, not a general throughput claim.

The remaining landing gate is the full sharded unit suite after the final insertion-barrier review adjustment.

9 days ago

Completed by 28165f51, following the frozen boundary in 71387a9e.

Final result:

  • Intl.Locale publishes an unforgeable object-owned native record, not a \0locale ordinary property.
  • One canonical GC-managed StringCell is retained with an insertion barrier, strong tracing, moving relocation, and exact backing finalization.
  • Base name, language, script, region, variants, ca, co, fw, hc, kf, nu, and kn are parsed once into immutable ranges/closed state.
  • Constructor-from-Locale, CanonicalizeLocaleList (including branded subclasses), every accessor, toString, maximize/minimize, and all Locale info methods use the native brand.
  • Constructor option observation remains ordered and failure-atomic; forged NUL properties no longer bypass conversion or prototype brand checks.

Final validation:

  • Locale: 152/152 (before and after; 0 flips)
  • Intl402: 3341/3341
  • focused normal: 5/5, 0 leaks
  • focused TSan: 5/5, 0 leaks
  • full sharded units: 1704 pass, 1 skip, 0 fail, 0 leaks, 162.4 s
  • benchmark checksum parity: 967094315 at 10 jobs on zig-js/JSC; exact-parent/candidate 670943087 at 100 jobs
  • exact-parent property locks: 5,068,738 → 4,569,506 (-499,232 / -9.85%)
  • backing allocations: 2,871,702 → 2,871,686 (-16); GC cells and collections unchanged
  • current/peak backing deltas: +461/+458 bytes
  • two balanced timing windows: -0.85% and -0.53% median; diagnostic only, not a general throughput claim

Audit note: the first full-suite attempt reported the older locale-list bounded test failing because a duplicate-literal test-only edit shortened its loop from 128 to 32 rounds without changing its frozen checksum. The runtime returned the exact 32-round checksum (2240). The old test was restored, the reduction was scoped to the new Locale test, both focused cases passed, and the complete suite then passed cleanly.

Sign in to comment on this issue.