Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace condition in V8
DescriptionRace condition in V8
ComponentV8
Sub-componentCompiler
Bug ClassRace
Tracker559780376
Fix commitec136c5038d3 (v8/v8) +11/-6
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Files Changed

  • src/compiler/js-heap-broker.cc
From ec136c5038d337396e362db4c9a4198ffda0d1ed Mon Sep 17 00:00:00 2001
From: Jakob Linke <jgruber@chromium.org>
Date: Thu, 17 Sep 2026 13:49:15 +0200
Subject: [PATCH] [compiler] Don't assume a memory fence for generated-code feedback

Feedback slots written by generated code are published with a plain
store, so background reads of such values have to keep the
IsPendingAllocation check.

These incorrectly used MakeRefAssumeMemoryFence:

ReadFeedbackForInstanceOf ic-callable.tq
ReadFeedbackForArrayOrObjectLiteral CreateEmptyArrayLiteral, in CSA
ReadFeedbackForTemplateObject internal.tq GetTemplateObject

Use plain TryMakeRef instead.

Fixed: 559780376
Change-Id: I727e94cda8f858cc84a33309bab6962b9572a8c6
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8425682
Commit-Queue: Marja Hölttä <marja@chromium.org>
Reviewed-by: Marja Hölttä <marja@chromium.org>
Auto-Submit: Jakob Linke <jgruber@chromium.org>
Cr-Commit-Position: refs/heads/main@{#109982}
---
diff --git a/src/compiler/js-heap-broker.cc b/src/compiler/js-heap-broker.cc
index 710fa28..141ba73 100644
--- a/src/compiler/js-heap-broker.cc
+++ b/src/compiler/js-heap-broker.cc
@@ -731,7 +731,8 @@
         nexus.GetConstructorFeedback();
     DirectHandle<JSObject> constructor;
     if (maybe_constructor.ToHandle(&constructor)) {
-      optional_constructor = MakeRefAssumeMemoryFence(this, *constructor);
+      // Written by generated code so this needs TryMakeRef.
+      optional_constructor = TryMakeRef(this, *constructor);
     }
   }
   return *zone()->New<InstanceOfFeedback>(optional_constructor, nexus.kind());
@@ -756,9 +757,11 @@
     return NewInsufficientFeedback(nexus.kind());
   }
 
-  AllocationSiteRef site =
-      MakeRefAssumeMemoryFence(this, Cast<AllocationSite>(object));
-  return *zone()->New<LiteralFeedback>(site, nexus.kind());
+  // Written by generated code so this needs TryMakeRef.
+  OptionalAllocationSiteRef site =
+      TryMakeRef(this, Cast<AllocationSite>(object));
+  if (!site.has_value()) return NewInsufficientFeedback(nexus.kind());
+  return *zone()->New<LiteralFeedback>(*site, nexus.kind());
 }
 
 ProcessedFeedback const& JSHeapBroker::ReadFeedbackForRegExpLiteral(
@@ -786,8 +789,10 @@
     return NewInsufficientFeedback(nexus.kind());
   }
 
-  JSArrayRef array = MakeRefAssumeMemoryFence(this, Cast<JSArray>(object));
-  return *zone()->New<TemplateObjectFeedback>(array, nexus.kind());
+  // Written by generated code so this needs TryMakeRef.
+  OptionalJSArrayRef array = TryMakeRef(this, Cast<JSArray>(object));
+  if (!array.has_value()) return NewInsufficientFeedback(nexus.kind());
+  return *zone()->New<TemplateObjectFeedback>(*array, nexus.kind());
 }
 
 ProcessedFeedback const& JSHeapBroker::ReadFeedbackForCall(
Loading diff…

Original Bug Report

reported by vm...@google.com

Publication race in ReadFeedbackForInstanceOf due to fence over-claim on ARM64

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: V8’s instanceof feedback slot is written by generated code with a plain, non-release store, but the background compiler consumes the slot’s constructor via MakeRefAssumeMemoryFence, which waives the IsPendingAllocation bail — the only protection for objects allocated mid-compilation. From plain web JavaScript with default flags, a concurrent TurboFan or Maglev compile can therefore consume a freshly-allocated, unpublished constructor on a worker thread; this consumption was demonstrated on real ARM64 Android hardware and from web content. On ARM64 the missing release/acquire pair architecturally permits the compiler to then read pre-allocation residue as the constructor’s map, yielding type-confused compiler refs, miscompiled instanceof, and a tagged-write path at dependency install.

Affected files:

  • v8/src/compiler/js-heap-broker.cc:720-735 — ReadFeedbackForInstanceOf wraps the slot value with MakeRefAssumeMemoryFence (:731)
  • v8/src/compiler/heap-refs.cc:1205-1210 — kAssumeMemoryFence skips the ObjectMayBeUninitialized bail; :1219 then dereferences the map word
  • v8/src/builtins/ic-callable.tq:22-43 — TryInitializeAsMonomorphic publishes the constructor with a plain store (:41)
  • v8/src/codegen/code-stub-assembler.cc:15335-15342,4392-4418 — StoreWeakReferenceInFeedbackVector → StoreFeedbackVectorSlot → plain Store (:4416)
  • v8/src/compiler/code-assembler.cc:1046-1051 — CodeAssembler::Store is a plain tagged store (write barrier only, no ordering)
  • v8/src/objects/dependent-code.cc:65-100 — escalation: dependency install reads/writes through the residue-derived map

Estimated timestamp from git blame: 2021-08-04

Summary

V8’s concurrency contract for feedback slots requires release publication for anything a background compiler thread may read: C++ writers use FeedbackVector::SynchronizedSet → Release_Store (v8/src/objects/feedback-vector-inl.h:356-363,518-523) and background reads use Acquire_Load (:348-354). The Torque instanceof-feedback writer is outside this protocol — its slot store and the just-allocated constructor’s initializing stores are all plain. The background reader nevertheless asserts fence-based publication via kAssumeMemoryFence, whose documented contract (v8/src/compiler/js-heap-broker.h:85-93) assumes acquire-release publication.

Executed evidence (observation-only instrumentation that re-inserts the exact bypassed IsPendingAllocation check at the read site, changing no behavior):

  • Background compile threads consumed a pending-allocation constructor through the fence-assuming path deterministically: 10/10 zero-flag pure-JS runs on x86-64 d8, 5/5 on a physical ARM64 Android device (crash thread V8 DefaultWorke, tid != pid), and from web content over HTTP in a Chrome renderer.
  • Both consumers were captured with symbolized worker-thread stacks: Maglev (MaglevCompilationJob::ExecuteJobImpl → VisitTestInstanceOf → ... → ReadFeedbackForInstanceOf) and TurboFan (PipelineCompilationJob::ExecuteJobImpl → CreateGraph → InliningPhase → ReduceJSInstanceOf).
  • The race window was entered on ~14% of all attempts in the pure-JS shape; pending-allocation status persists until the next PublishMainThreadPendingAllocations, which runs only at compile-job prepare (v8/src/compiler/pipeline.cc:749-807).
  • The final corruption step — the background load actually observing the slot before the constructor’s map word — was not captured in ~2.9M stock-build racing executions on the ARM64 device. It remains architecturally permitted (Arm ARM, below) but is a nanosecond-scale stochastic event; an attacker can retry indefinitely at no cost.

All relevant flags (--maglev, --concurrent-recompilation, --concurrent-osr) are default-on (v8/src/flags/flag-definitions.h:522,1280,1539); the zero-flag reproductions confirm this empirically. x86-64 is unaffected in consequence (TSO forbids the store reorder); the corruption outcome is specific to weakly-ordered targets — Android, Apple Silicon, Windows-on-ARM.


Root Cause Analysis

  1. Writer publishes the slot with a plain store: v8/src/builtins/ic-callable.tq:22-43 — TryInitializeAsMonomorphic (shared by call and instanceof feedback) calls StoreWeakReferenceInFeedbackVector (:41) → StoreFeedbackVectorSlot with UPDATE_WRITE_BARRIER (v8/src/codegen/code-stub-assembler.cc:15335-15342) → Store(feedback_vector, offset, value) (:4416):
    // v8/src/compiler/code-assembler.cc:1046-1051
    void CodeAssembler::Store(Node* base, Node* offset, Node* value) {
      ...
      raw_assembler()->Store(MachineRepresentation::kTagged, base, offset, value,
                             kFullWriteBarrier);   // <-- plain STR; barrier affects GC, not ordering
    }
    
  2. The stored constructor is itself unpublished: the RHS closure is allocated moments earlier on the same thread with plain stores only — FastNewClosure is Allocate + StoreMapNoWriteBarrier + plain field stores (v8/src/builtins/builtins-constructor-gen.cc:211-286); TestInstanceOf collects feedback before evaluating (v8/src/interpreter/interpreter-generator.cc:1958-1972, via CollectInstanceOfFeedback, ic-callable.tq:170-201). No release operation separates constructor initialization from the slot store.
  3. Background reader over-claims a fence: v8/src/compiler/js-heap-broker.cc:720-735:
    // v8/src/compiler/js-heap-broker.cc:730-731
    if (maybe_constructor.ToHandle(&constructor)) {
      optional_constructor = MakeRefAssumeMemoryFence(this, *constructor);  // <-- asserts release publication that does not exist
    }
    
    MakeRefAssumeMemoryFence = TryMakeRef(..., kAssumeMemoryFence | kCrashOnError) (v8/src/compiler/js-heap-broker.h:649-654).
  4. The flag waives the only fresh-object protection: v8/src/compiler/heap-refs.cc:1205-1210 — with kAssumeMemoryFence set, ObjectMayBeUninitialized (!IsMainThread() && heap()->IsPendingAllocation(object), v8/src/compiler/js-heap-broker.cc:168-170) is never consulted, and :1219 immediately reads heap_object->map()->instance_type(). In-tree contrast: ReadFeedbackForCall consumes values written by the same Torque macro via plain TryMakeRef (v8/src/compiler/js-heap-broker.cc:807), correctly keeping the bail.
  5. ARM64 architecturally permits the reorder: Arm ARM DDI 0487, §K11.2.1.1-2 (message-passing litmus): the data→flag pattern is ordered only if the flag store is Store-Release (STLR); without it, “different observers might observe the accesses in a different order”, even with an address dependency on the reader side. Both the constructor-init stores and the slot store here are plain STRs.
  6. Consequences on a reorder hit: GetConstructorFeedback performs a DCHECK-only Cast<JSObject> (v8/src/objects/feedback-vector.cc:1493-1501); the broker builds HeapObjectData/MapData from the residue map word (v8/src/compiler/heap-refs.cc:876-891,992-1029), dispatching CREATE_DATA on the residue-derived instance type (:1221-1233) — an in-cage wild read at a heap-groomable address, then a type-confused JSObjectRef cached for the whole compilation (v8/src/compiler/js-heap-broker.cc:920-926). TurboFan ReduceJSInstanceOf (v8/src/compiler/js-native-context-specialization.cc:604-735) and Maglev (v8/src/maglev/maglev-reducer-inl.h:2739-2886) walk the poisoned ref (map, descriptors, @@hasInstance constant-folding); at finalize, DependentCode::InstallDependency → WeakArrayList::AddToEnd (v8/src/objects/dependent-code.cc:65-100) performs tagged writes through the residue-derived map — with groomed residue presenting a plausible list header, a tagged-word write at an attacker-influenced in-cage address.

Reproduction

  1. On an ARM64 device with default flags, run a hot function containing a cold branch whose first execution allocates a fresh constructor and immediately does instanceof with it (class definition or function expression as RHS — allocation and feedback store are adjacent bytecodes).
  2. Keep a concurrent Maglev or TurboFan compile of that function in flight (hot loop → concurrent OSR, or deopt/reopt cycling) so the background graph build reads the instanceof feedback slot while the constructor is still a pending allocation.
  3. Expected: the background compiler must either observe the slot only via a release/acquire pair, or bail on pending-allocation objects as ReadFeedbackForCall does.

Observed: the background compile thread consumes the pending-allocation constructor through the fence-assuming path — demonstrated deterministically (verified via an observation-only build re-inserting the bypassed check: 10/10 zero-flag d8 runs on x86-64, 5/5 on a physical ARM64 Android device with symbolized Maglev worker-thread stacks, plus a renderer hit from web content in Chrome; window entered on ~14% of pure-JS attempts; both TurboFan and Maglev consumers captured on platform worker threads). The subsequent step — the consuming load actually observing pre-map-word residue — is architecturally permitted per the Arm ARM but stochastic at nanosecond scale; ~2.9M stock-build attempts on the device produced no corruption crash, so the corruption endgame, while retryable at scale, was not empirically captured.

Severity

medium — memory corruption in a sandboxed renderer would ordinarily rate high, but this meets the extreme-trigger-complexity demotion: corruption requires (1) a background compile reading the exact slot while the constructor is pending (cheap — entered ~14% of attempts), (2) the architecturally-permitted but nanosecond-scale ARM64 store-visibility reorder landing on that read (not observed in ~2.9M hardware attempts), and (3) groomed allocation residue at the constructor’s map-word offset to convert crash into corruption. It is not low: everything is default-on, the attack is pure web JavaScript, attempts are free and independently retryable (new instanceof sites, GC-cleared weak slots re-arming, recompiles), and the outcome on a hit is genuine corruption (type-confused compiler refs plus a tagged in-cage write at dependency install), not merely a crash.

Suggested Fix Direction

Minimal reader-side fix: consume the slot via TryMakeRef (mirroring ReadFeedbackForCall, v8/src/compiler/js-heap-broker.cc:807), restoring the IsPendingAllocation bail — dynamically verified: the race window is still entered but every pending-allocation constructor is rejected, with no functional regression (compilation proceeds as with insufficient feedback and re-reads on later recompiles). The more complete fix is writer-side — make StoreWeakReferenceInFeedbackVector/StoreFeedbackVectorSlot emit a release store — since ReadFeedbackForCall itself performs unprotected pre-reads on the raw slot value before its bail (v8/src/compiler/js-heap-broker.cc:799-806); note a Torque-level synchronization annotation alone would not suffice, as the CSA backend’s reference load/store emission carries no synchronization (v8/src/torque/csa-generator.cc:909-946).


POC: V8: InstanceOf feedback constructor published via plain Torque store but consumed with MakeRefAssumeMemoryFence in background ReadFeedbackForInstanceOf (ARM64 publication race)

Strategy: web-content (also reproduced in d8, natives-assisted d8, and on a real ARM64 Android device) Bug Type: data race (CWE-362) — unsynchronized publication of a freshly-allocated constructor to a background compiler thread; memory-corruption consequence on weakly-ordered hardware (ARM64)

What this POC proves, and how

The defect: the instanceof feedback slot is written by generated code with a plain, non-release store (TryInitializeAsMonomorphic → StoreWeakReferenceInFeedbackVector, v8/src/builtins/ic-callable.tq), but the background compiler consumes the slot value via MakeRefAssumeMemoryFence (v8/src/compiler/js-heap-broker.cc, ReadFeedbackForInstanceOf). The kAssumeMemoryFence flag waives the ObjectMayBeUninitialized bail (!IsMainThread() && heap()->IsPendingAllocation(object), the only protection for objects allocated mid-compilation) even though no fence exists on this writer path. Contrast ReadFeedbackForCall, which consumes values written by the same Torque macro through plain TryMakeRef.

Because the final corruption step depends on an architecturally-permitted but nanosecond-scale ARM64 store-visibility reorder, this POC proves the bug with a pure observation oracle: a 1-hunk patch (race-oracle.patch) that re-inserts — as an observer, changing no behavior — the exact ObjectMayBeUninitialized check that kAssumeMemoryFence bypasses, at the instanceof read site, and FATALs if it would have fired. An oracle hit means, verbatim: a background compile thread consumed a pending-allocation constructor through the fence-assuming path — the bypassed safety net was load-bearing at that moment, and nothing else orders the constructor’s initialization (the writer store is plain; no C++ release-store writer for kInstanceOf slots exists).

The oracle is sound for this slot kind specifically because instanceof feedback constructors are only written by the plain Torque store: a pending-allocation constructor seen at this site is by construction an unsynchronized publication. (For release-published values, e.g. AllocationSites, pending-ness would be harmless — which is why the oracle sits only in ReadFeedbackForInstanceOf.)

Results

Trigger Flags Platform Result
poc.html via HTTP (web content) none (env-only flags) x86-64 Chrome, oracle build renderer hits oracle (log-oracle-webcontent-x86_64.txt)
poc.js none x86-64 d8, oracle build 10/10 runs hit in < 1 s (log-oracle-purejs-x86_64.txt)
poc.js none real ARM64 Android device, oracle build 5/5 runs hit (attempt 0, ~5 ms); tombstone: thread V8 DefaultWorke, symbolized stack through MaglevCompilationJob::ExecuteJobImpl → VisitTestInstanceOf → GetFeedbackForInstanceOf → ReadFeedbackForInstanceOf (log-oracle-purejs-arm64-device.txt)
poc-natives.js -- maglev --allow-natives-syntax x86-64 + ARM64 device hit within seconds; worker-thread stack WorkerThread::Run → DefaultJobWorker::Run → MaglevConcurrentDispatcher::JobTask::Run; device hit pid=22519 tid=22525
poc-natives.js -- tf --allow-natives-syntax x86-64 + ARM64 device hit within seconds; worker-thread stack WorkerThread::Run → OptimizingCompileTaskExecutor::CompileTask::Run → PipelineCompilationJob::ExecuteJobImpl → CreateGraph → InliningPhase → ReduceJSInstanceOf; device hit pid=23056 tid=23062

Both claimed consumers (TurboFan JSNativeContextSpecialization::ReduceJSInstanceOf and Maglev TryBuildFastInstanceOfWithFeedback) were caught red-handed with symbolized stacks, on default-on tiers, from plain JavaScript.

Thread-identity rigor. Every shipped log shows the consumption on a platform worker thread: x86-64 stacks bottom out in DefaultWorkerThreadsTaskRunner::WorkerThread::Run, and the device logcat/tombstones show tid != pid (pure-JS tombstone: thread name V8 DefaultWorke). The natives variant deliberately busy-spins instead of calling %WaitForBackgroundOptimization after the trigger call: joining the job queue would let the main thread execute the compile job itself (V8 still classifies that broker as background via the job’s LocalIsolate, but it would be weaker evidence of a cross-thread race, so the POC avoids it).

Interpretation. The race window is entered essentially every time a fresh instanceof site executes while a concurrent compile of its function is in flight — pending-allocation status persists until the next PublishMainThreadPendingAllocations (only at compile-job prepare / incremental-marking start), so the window is micro- to milliseconds wide, and the pure-JS trigger lands in it reliably. On x86-64, TSO hides the consequence. On ARM64, each oracle hit is an iteration of the real memory-corruption race: the consumed constructor’s initializing stores are separated from the observed slot store by no release/acquire pair, so the compile thread’s subsequent map-word read (heap-refs.cc, TryGetOrCreateData: heap_object->map()->instance_type()) is architecturally permitted to observe pre-allocation residue (Arm ARM DDI 0487, §K11.2.1.1–.2, verified against the mirrored text: message passing requires writer-side Store-Release for the flag, and without it “observers might observe the accesses in a different order … even when the observers are using an address dependency”; both writer stores here are plain STRs). The corruption itself is stochastic (store-buffer-drain-scale window); hunt.js is a stock-build lottery for it (see below), but the synchronization defect and its reachability are proven deterministically above.

Stock-build reorder lottery (honest negative result). hunt.js ran on the leased ARM64 device against an unpatched release d8 for ~22 minutes: 152 batches × 400 fresh functions × 48 instanceof race sites ≈ 2.9 M racing site-executions, 0 crashes. The reorder needs the background slot-read to land within the store-buffer-drain window (tens of ns) of the plain slot store — the interleaving is entered constantly (oracle data above) but that final coincidence is a lottery at this attempt rate, consistent with the “extreme trigger complexity” severity rationale. An attacker retries at scale for as long as they like; the defect itself is proven without winning the lottery.

Prerequisites

  • Chromium checkout; builds used: out/Default (release x86-64) and out/android-arm64 (release arm64) — any equivalent works.
  • For the oracle demonstration: apply race-oracle.patch (observation only) to v8/ and build d8 / chrome.
cd /chromium_src/src/v8 && git apply /poc-artifacts/race-oracle.patch
cd /chromium_src/src && autoninja -C out/Default d8 chrome

Reproduction

d8, pure JS, zero flags (10/10 in under a second):

/chromium_src/src/out/Default/d8 /poc-artifacts/poc.js
# => Fatal error: instanceof-feedback race window HIT: background compile
#    thread consumed pending-allocation constructor ... via MakeRefAssumeMemoryFence

d8, per-tier (hits within seconds):

/chromium_src/src/out/Default/d8 --allow-natives-syntax /poc-artifacts/poc-natives.js -- maglev
/chromium_src/src/out/Default/d8 --allow-natives-syntax /poc-artifacts/poc-natives.js -- tf

Web content (Chrome, oracle build):

cd /poc-artifacts && python3 server.py &
/chromium_src/src/out/Default/chrome \
  --no-first-run --no-default-browser-check --ignore-gpu-blocklist \
  --no-proxy-server --enable-logging=stderr \
  --user-data-dir=/tmp/chrome-poc-profile http://localhost:8080/poc.html
# => renderer process FATALs with the oracle message (see log)

Real ARM64 Android device (oracle arm64 d8):

adb -s $SERIAL push out/android-arm64/d8 out/android-arm64/snapshot_blob.bin \
    out/android-arm64/icudtl.dat poc.js /data/local/tmp/instpoc/
adb -s $SERIAL shell "cd /data/local/tmp/instpoc && ./d8 poc.js"
# => SIGTRAP; logcat has the oracle FATAL on a non-main tid; tombstone thread
#    name "V8 DefaultWorke" with the full Maglev background-compile stack

Stock-build reorder hunt (bonus, stochastic):

adb -s $SERIAL shell "cd /data/local/tmp/stockpoc && while true; do ./d8 hunt.js; done"
# watch /data/tombstones for compile-thread crashes in v8::internal::compiler/maglev

Expected Result

On oracle builds: Fatal error ... instanceof-feedback race window HIT ... from a background compile thread (d8: process aborts; Chrome: renderer crashes). On stock builds: silent on x86-64 (TSO); on ARM64 each window entry is a real corruption attempt whose success requires the ns-scale reorder (crash in the compile thread on a hit).

Output

x86-64 d8, poc.js, zero flags (excerpt of log-oracle-purejs-x86_64.txt):

attempt 0 (trig~507, 10ms)
#
# Fatal error
# instanceof-feedback race window HIT: background compile thread consumed
# pending-allocation constructor 0x10a101253315 (map word currently
# 0x10a10101f9e1) via MakeRefAssumeMemoryFence; kAssumeMemoryFence bypassed the
# IsPendingAllocation bail, and the writer-side slot store is plain, so this
# object's contents are unsynchronized
...
v8::internal::maglev::MaglevGraphBuilder::VisitTestInstanceOf()  maglev-graph-builder.cc:13438
v8::internal::maglev::MaglevCompilationJob::ExecuteJobImpl(...)

Real ARM64 device tombstone (excerpt of log-oracle-purejs-arm64-device.txt):

Cmdline: ./d8 poc.js
pid: 19677, ppid: 19676, tid: 19682, name: V8 DefaultWorke  >>> ./d8 <<<
signal 5 (SIGTRAP), code 1 (TRAP_BRKPT)
...
V8_Fatal(char const*, ...)
v8::internal::compiler::JSHeapBroker::ReadFeedbackForInstanceOf(...) js-heap-broker.cc:743
v8::internal::compiler::JSHeapBroker::GetFeedbackForInstanceOf(...)  js-heap-broker.cc:945
v8::internal::maglev::MaglevReducer<...>::TryBuildFastInstanceOfWithFeedback(...) maglev-reducer-inl.h:2863
v8::internal::maglev::MaglevGraphBuilder::VisitTestInstanceOf()      maglev-graph-builder.cc:13438
...
v8::internal::maglev::MaglevCompilationJob::ExecuteJobImpl(...)      maglev-concurrent-dispatcher.cc:152

Files

  • poc.js — pure-JS zero-flag trigger (d8) — also the payload of poc.html
  • poc.html, server.py — web-content variant
  • poc-natives.js — per-tier variant (-- tf / -- maglev)
  • hunt.js — stock-build ARM64 reorder lottery (function-expression RHS: CreateClosure and TestInstanceOf are adjacent bytecodes, minimizing the init-store → slot-store gap the reorder must cover)
  • race-oracle.patch — the observation oracle (no behavior change)
  • bandaid-fix.patch — minimal fix: consume the slot via TryMakeRef (mirroring ReadFeedbackForCall), restoring the IsPendingAllocation bail
  • log-*.txt — captured runs (see table above)

Bandaid fix

bandaid-fix.patch replaces MakeRefAssumeMemoryFence(this, *constructor) with TryMakeRef(this, *constructor) in ReadFeedbackForInstanceOf. On bail the compiler simply proceeds with constructor-less InstanceOfFeedback (same as an uninitialized slot) — no functional regression, the value is re-read on later recompiles. Verified dynamically: with the fix applied plus a temporary observation log, the same POCs still enter the race window (same interleaving) but the pending-allocation constructor is now rejected by the restored IsPendingAllocation bail instead of being consumed (see log-bandaid-verify.txt): in a single full poc.js run the window was entered 412 times out of 3000 attempts (~14%) and rejected every time; the run then completed cleanly. Rebuilt with the final (log-free) patch, all POCs run to completion with exit 0.

Window-entry dynamics, for interpretation: the pure-JS shape enters the window on ~14% of all attempts because its window is iterations-wide (the store lands hundreds of loop iterations after the concurrent OSR job is created, while the background build is still underway). The natives shape queues the job at the trigger call’s entry and busy-spins ~3 ms so a platform worker picks the job up while the constructor is still pending; hits arrive within the first attempts. (The bandaid-verify natives numbers in log-bandaid-verify.txt were captured with an earlier variant of poc-natives.js that waited via %WaitForBackgroundOptimization; the pure-JS numbers are unaffected.)

Note (from the report, concurs with our reading): the writer-side fix — making StoreWeakReferenceInFeedbackVector emit a release store — is the more complete option, since ReadFeedbackForCall also performs unprotected pre-reads on the raw slot value; but the reader-side TryMakeRef change above is the minimal correct bandaid for this consumer.


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker
Links in the report