Differential behavior: Ion-inlined WeakMap.prototype.get misses incremental read barrier for atoms-zone values (emitWeapMapBarrierFastPath consults compile-realm zone, not cell zone)
Categories
(Core :: JavaScript Engine: JIT, defect, P1)
Tracking
()
| Tracking | Status | |
|---|---|---|
| firefox-esr115 | --- | unaffected |
| firefox-esr140 | --- | unaffected |
| firefox151 | --- | wontfix |
| firefox152 | + | fixed |
| firefox153 | + | fixed |
People
(Reporter: decoder, Assigned: jandem)
References
(Blocks 1 open bug, Regression)
Details
(4 keywords, Whiteboard: [adv-main152+r])
Attachments
(3 files, 3 external files)
Summary
The Ion fast path for the inlined WeakMap.prototype.get decides whether the value read barrier is needed based on the compilation realm's zone, while the VM implementation (WeakMap::valueReadBarrier → JS::ExposeValueToActiveJS → ExposeGCThingToActiveJS) bases the decision on the zone of the cell being returned.
WeakMap values that are atomized strings live in the atoms zone, not the script's zone. During an incremental GC whose zone set includes the atoms zone but not the script's zone, the atoms zone has needsMarkingBarrier() == true while the script's zone does not. In that window, Ion-compiled wm.get(k) hands an unmarked (white) atoms-zone cell to the running script without ever calling PerformIncrementalReadBarrier, violating the engine's read-barrier contract:
js/public/HeapAPI.h:883—ExposeGCThingToActiveJSis documented as having to be "kept in sync" withjs::jit::WeakMapValueReadBarrier; it uses the cell's zone for theneedsMarkingBarrier()check.js/src/gc/AtomMarking.cpp:356(AtomMarkingRuntime::markAtom) — the read barrier on the atom "is necessary if the atom is being marked because a reference to it was obtained from another zone which is not being collected by the incremental GC."
A missed incremental read barrier is the classic GC use-after-free bug class: the mutator obtains and stores a reference to a cell the collector considers unreachable-so-far. This is the same bug class that bug 2034837 / bug 2042465 closed for symbols; the non-symbol atoms-zone case (atom strings) remains broken because the wrong zone's needsMarkingBarrier flag is consulted.
Root cause
Introduced by commit 783dd8752bd2 (bug 2034837, "Call MarkSymbolForWeakMapReadBarrier in JIT path") together with follow-up 1a134015e8ae (bug 2042465). The series replaced the cell-zone-driven js::jit::ReadBarrier(gc::Cell*) with compile-realm-zone wiring:
js/src/jit/CompileWrappers.cpp:170—CompileZone::addressOfZone()exposes the compilation realm's zone pointer;js/src/jit/CodeGenerator.cpp:22505bakes it in as an immediate (masm.movePtr(ImmPtr(mirGen().realm->zone()->addressOfZone()), ...)) and passes it tojs::jit::WeakMapValueReadBarrier(cell, mapZone).js/src/jit/MacroAssembler.cpp:8355—MacroAssembler::emitWeapMapBarrierFastPathis the inline filter deciding whether to call the out-of-line barrier. For a white cell it only calls the barrier ifbranchTestNeedsMarkingBarrierAnyZone(...)fires (js/src/jit/MacroAssembler.cpp:8415).js/src/jit/MacroAssembler-inl.h:788— despite its name,branchTestNeedsMarkingBarrierAnyZonetests (when compiling Ion code) only the compile realm zone's barrier word, viaCompileZone::addressOfNeedsMarkingBarrier()(js/src/jit/CompileWrappers.cpp:143).
The VM path checks the cell's zone instead (js/src/gc/WeakMap.h:528 valueReadBarrier → ExposeGCThingToActiveJS, js/public/HeapAPI.h:883).
Divergent configuration:
- value cell zone = atoms zone (an atomized string; symbols are special-cased, objects/bigints/non-atom strings are always in the script's zone),
- incremental GC marking a zone set that includes the atoms zone but not the script's zone,
wm.get(k)executed in Ion.
Then needsMarkingBarrier(atoms) == true but needsMarkingBarrier(scriptZone) == false, so the Ion fast path falls through to "no barrier" and returns the white cell. Baseline/interpreter (CacheIRCompiler::emitWeakMapGetObjectResult → WeakMapObject::getObject → barriered WeakMap::lookup) marks the cell black.
Note: the out-of-line helper js::jit::WeakMapValueReadBarrier (js/src/jit/VMFunctions.cpp:3448) is itself correct (it re-reads cell->zone()); the bug is purely that the inline fast path filters out the call using the wrong zone's flag.
Consequence
At sweep start, GCRuntime::updateAtomsBitmap (js/src/gc/Sweeping.cpp:1393) first refines every collected zone's atom marking bitmap from the chunk mark bits and only afterwards ORs in the bitmaps of uncollected zones. Because Ion skipped the read barrier, the atom is still white at refinement time, so a collected zone that legitimately had the atom in its marking bitmap has it wrongly removed. The mutator runs with a reference to a white atoms-zone cell for the whole remainder of the marking phase; the cell's survival hinges solely on the redundant snapshot rescue of uncollected-zone bitmaps. Any state where that redundancy does not hold turns this into a use-after-free of an atoms-zone cell (dangling JSString* reachable from live JS).
Reproduction
Testcase poc.js (attached). It observes the divergence deterministically via the --fuzzing-safe testing functions getAtomMarkIndex/getAtomMarkColor (testing functions are used only to observe mark state and make the zonal incremental GC deterministic — the missed barrier itself needs no testing functions):
$ js --fuzzing-safe poc.js # Ion enabled (default)
atom color in zone A after GC: white
atom color in this zone after GC: black
poc.js:82:9 Error: Assertion failed: got "white", expected "black"
(exit code 3)
$ js --fuzzing-safe --no-ion poc.js
atom color in zone A after GC: black
atom color in this zone after GC: black
TEST PASSED
(exit code 0)
Reproduces identically on the debug ASan shell (m-c-asan-debug) and the opt ASan fuzzing shell (m-c-fuzzing-asan-opt); also with --ion-eager --ion-offthread-compile=off vs --no-ion. Stable 10/10 runs.
Shell version: product_version = 20260603-5cffec5992d3 (mozilla-central, Linux x86_64 ASan builds).
The differential proves the Ion path skips PerformIncrementalReadBarrier on a weakmap value whose zone has needsMarkingBarrier() set — a missing GC read barrier in JIT code, not merely a bookkeeping difference: with --no-ion the same execution leaves the cell black-marked during marking.
Suggested fix
In MacroAssembler::emitWeapMapBarrierFastPath, the white-cell fallback must consult the cell's zone (load the zone from the cell, as ExposeGCThingToActiveJS does), or conservatively also branch to the out-of-line barrier when the value is a (non-permanent) atoms-zone string — mirroring the existing branchTestSymbol special case — or test a runtime-wide "any zone needs marking barrier" flag instead of the compile-realm zone's flag. The out-of-line WeakMapValueReadBarrier already handles all cases correctly.
Files
poc.js— the testcase (run with--fuzzing-safe; compare default vs--no-ion).probe1.js,probe2.js— exploratory probes used during analysis.
| Reporter | ||
Comment 1•5 months ago
|
||
| Reporter | ||
Comment 2•5 months ago
|
||
| Reporter | ||
Comment 3•5 months ago
|
||
| Assignee | ||
Updated•5 months ago
|
Updated•5 months ago
|
| Assignee | ||
Updated•5 months ago
|
Comment 4•5 months ago
|
||
Set release status flags based on info from the regressing bug 2000328
| Assignee | ||
Comment 5•5 months ago
|
||
| Assignee | ||
Comment 6•5 months ago
|
||
Updated•5 months ago
|
| Assignee | ||
Comment 7•5 months ago
|
||
Comment on attachment 9595417 [details]
(secure)
Security Approval Request
- How easily could an exploit be constructed based on the patch?: Pretty difficult to set up the right state but maybe possible now with AI?
- Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: No
- Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: beta, release
- If not all supported branches, which bug introduced the flaw?: Bug 2000328
- Do you have backports for the affected branches?: Yes
- If not, how different, hard to create, and risky will they be?: Should apply or easy to backport.
- How likely is this patch to cause regressions; how much testing does it need?: Pretty unlikely, it's a small change.
- Is the patch ready to land after security approval is given?: Yes
- Is Android affected?: Yes
Updated•5 months ago
|
| Assignee | ||
Comment 9•5 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D305465
Updated•5 months ago
|
Comment 10•5 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined/Reason for urgency: Potential crashes or security bugs.
- Code covered by automated testing?: yes
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Small and local fix.
- String changes made/needed?: N/A
- Is Android affected?: yes
Updated•5 months ago
|
Updated•5 months ago
|
Comment 11•5 months ago
|
||
| uplift | ||
Comment 12•5 months ago
|
||
Backed out from Beta for bustage. Jan, it appears this depends on bug 2004011 which had to be backed out from Beta. Can you please post a reworked patch?
https://treeherder.mozilla.org/logviewer?job_id=571778675&repo=mozilla-beta&task=e0z8fQmBQr-l8zvRlh6NDw.0&lineNumber=9236
Comment 13•5 months ago
|
||
| uplift | ||
| Assignee | ||
Comment 14•5 months ago
|
||
(In reply to Ryan VanderMeulen [:RyanVM] from comment #12)
Can you please post a reworked patch?
I've updated the patch.
Comment 15•5 months ago
|
||
Updated•5 months ago
|
Comment 16•5 months ago
|
||
| uplift | ||
Updated•5 months ago
|
Updated•5 months ago
|
Comment 17•2 months ago
|
||
Comment 18•2 months ago
|
||
| bugherder | ||
Updated•1 month ago
|
Description
•