CVE-2026-106255
Overview
Files Changed
src/compiler/js-heap-broker.cc
Patch
From ec136c5038d337396e362db4c9a4198ffda0d1ed Mon Sep 17 00:00:00 2001 From: Jakob Linke <[email protected]> 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ä <[email protected]> Reviewed-by: Marja Hölttä <[email protected]> Auto-Submit: Jakob Linke <[email protected]> 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(
Original Bug Report
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—ReadFeedbackForInstanceOfwraps the slot value withMakeRefAssumeMemoryFence(:731)v8/src/compiler/heap-refs.cc:1205-1210—kAssumeMemoryFenceskips theObjectMayBeUninitializedbail;:1219then dereferences the map wordv8/src/builtins/ic-callable.tq:22-43—TryInitializeAsMonomorphicpublishes the constructor with a plain store (:41)v8/src/codegen/code-stub-assembler.cc:15335-15342,4392-4418—StoreWeakReferenceInFeedbackVector→StoreFeedbackVectorSlot→ plainStore(:4416)v8/src/compiler/code-assembler.cc:1046-1051—CodeAssembler::Storeis 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
- Writer publishes the slot with a plain store:
v8/src/builtins/ic-callable.tq:22-43—TryInitializeAsMonomorphic(shared by call and instanceof feedback) callsStoreWeakReferenceInFeedbackVector(:41) →StoreFeedbackVectorSlotwithUPDATE_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 } - The stored constructor is itself unpublished: the RHS closure is allocated moments earlier on the same thread with plain stores only —
FastNewClosureisAllocate+StoreMapNoWriteBarrier+ plain field stores (v8/src/builtins/builtins-constructor-gen.cc:211-286);TestInstanceOfcollects feedback before evaluating (v8/src/interpreter/interpreter-generator.cc:1958-1972, viaCollectInstanceOfFeedback,ic-callable.tq:170-201). No release operation separates constructor initialization from the slot store. - 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). - The flag waives the only fresh-object protection:
v8/src/compiler/heap-refs.cc:1205-1210— withkAssumeMemoryFenceset,ObjectMayBeUninitialized(!IsMainThread() && heap()->IsPendingAllocation(object),v8/src/compiler/js-heap-broker.cc:168-170) is never consulted, and:1219immediately readsheap_object->map()->instance_type(). In-tree contrast:ReadFeedbackForCallconsumes values written by the same Torque macro via plainTryMakeRef(v8/src/compiler/js-heap-broker.cc:807), correctly keeping the bail. - 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 plainSTRs. - Consequences on a reorder hit:
GetConstructorFeedbackperforms a DCHECK-onlyCast<JSObject>(v8/src/objects/feedback-vector.cc:1493-1501); the broker buildsHeapObjectData/MapDatafrom the residue map word (v8/src/compiler/heap-refs.cc:876-891,992-1029), dispatchingCREATE_DATAon the residue-derived instance type (:1221-1233) — an in-cage wild read at a heap-groomable address, then a type-confusedJSObjectRefcached for the whole compilation (v8/src/compiler/js-heap-broker.cc:920-926). TurboFanReduceJSInstanceOf(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
- 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
instanceofwith it (class definition or function expression as RHS — allocation and feedback store are adjacent bytecodes). - 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.
- Expected: the background compiler must either observe the slot only via a release/acquire pair, or bail on pending-allocation objects as
ReadFeedbackForCalldoes.
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) andout/android-arm64(release arm64) — any equivalent works. - For the oracle demonstration: apply
race-oracle.patch(observation only) tov8/and buildd8/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 ofpoc.htmlpoc.html,server.py— web-content variantpoc-natives.js— per-tier variant (-- tf/-- maglev)hunt.js— stock-build ARM64 reorder lottery (function-expression RHS:CreateClosureandTestInstanceOfare 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 viaTryMakeRef(mirroringReadFeedbackForCall), restoring theIsPendingAllocationbaillog-*.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.