Chrome · V8
CVE-2026-17979
Race in V8
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/common/synchronization-point-support.cc |
modified | |
whilesrc/common/synchronization-point-support.cc |
modified | |
ifsrc/runtime/runtime-test.cc |
modified |
Files Changed
src/common/synchronization-point-support.ccsrc/common/synchronization-point-support.hsrc/runtime/runtime-test.cctest/mjsunit/compiler/regress-519664497.jstest/mjsunit/regress/regress-511263221.js
Patch
From d1273475a38b49b9d8281435e8567475b18f448b Mon Sep 17 00:00:00 2001
From: Maksim Ivanov <emaxx@chromium.org>
Date: Thu, 16 Jul 2026 14:51:14 +0000
Subject: [PATCH] [testing] Do not throw errors from sync point APIs
Just print an stderr message and return false instead of throwing an
error from %WaitUntilBlocked() or %Resume(). This allows regression
tests with highly specific scenarios to not break CQ/Waterfall when the
actual sequence of events differs from the expected one or the execution
is slower than expected.
This also fixes the flakiness of regress-519664497 on debug builds.
Bug: 519664497, 514998642, 532293195
Change-Id: I0fcf7f3015cb22b3236295379b041b502af5e2a4
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8091127
Reviewed-by: Marja Hölttä <marja@chromium.org>
Reviewed-by: Arash Kazemi <arashk@chromium.org>
Commit-Queue: Maksim Ivanov <emaxx@google.com>
Cr-Commit-Position: refs/heads/main@{#108758}
---
diff --git a/src/common/synchronization-point-support.cc b/src/common/synchronization-point-support.cc
index 34dcaae..89bcc47 100644
--- a/src/common/synchronization-point-support.cc
+++ b/src/common/synchronization-point-support.cc
@@ -45,32 +45,45 @@
std::string_view synchronization_point) {
base::MutexGuard lock(&mutex_);
auto it = states_.find(synchronization_point);
- if (it == states_.end()) return false;
- SyncPointState* state = it->second.get();
- if (!state->block_requested) return false;
- state->block_requested = false;
- state->cv.NotifyAll();
- return true;
+ if (it != states_.end()) {
+ SyncPointState* state = it->second.get();
+ if (state->block_requested) {
+ state->block_requested = false;
+ state->cv.NotifyAll();
+ return true;
+ }
+ }
+ base::OS::PrintError("Synchronization point not armed\n");
+ return false;
}
bool SynchronizationPointSupport::WaitUntilBlocked(
- std::string_view synchronization_point, base::TimeDelta timeout,
- bool& timed_out) {
- bool success = false;
- auto wait_loop = [this, synchronization_point, timeout, &timed_out,
- &success]() {
+ std::string_view synchronization_point, base::TimeDelta timeout) {
+ // Safe: map elements are never removed, so the pointer is stable.
+ SyncPointState* state;
+ {
base::MutexGuard lock(&mutex_);
auto it = states_.find(synchronization_point);
- if (it == states_.end()) return;
- SyncPointState* state = it->second.get();
- if (!state->block_requested && state->blocked_threads == 0) return;
+ if (it == states_.end()) {
+ base::OS::PrintError("Synchronization point not armed\n");
+ return false;
+ }
+ state = it->second.get();
+ }
+ bool success = false;
+ auto wait_loop = [this, state, timeout, &success]() {
+ base::MutexGuard lock(&mutex_);
const base::TimeTicks start = base::TimeTicks::Now();
while (state->blocked_threads == 0) {
+ if (!state->block_requested) {
+ base::OS::PrintError("Synchronization point not armed\n");
+ return;
+ }
const base::TimeDelta remaining =
start + timeout - base::TimeTicks::Now();
if (remaining <= base::TimeDelta()) {
- timed_out = true;
+ base::OS::PrintError("Synchronization point wait timed out\n");
return;
}
std::ignore = state->cv.WaitFor(&mutex_, remaining);
diff --git a/src/common/synchronization-point-support.h b/src/common/synchronization-point-support.h
index 4ebd39d..c9830ef 100644
--- a/src/common/synchronization-point-support.h
+++ b/src/common/synchronization-point-support.h
@@ -44,11 +44,10 @@
bool Resume(std::string_view synchronization_point);
// Waits until the given synchronization point is reached by some thread.
- // Returns false if a block wasn't requested or on timeout (in which case
- // `timed_out` is set to true as well). Note: this does not request a block;
- // it must be requested first.
+ // Returns false if a block wasn't requested or on timeout. Note: this does
+ // not request a block; it must be requested first.
bool WaitUntilBlocked(std::string_view synchronization_point,
- base::TimeDelta timeout, bool& timed_out);
+ base::TimeDelta timeout);
// Called when the synchronization point is reached; blocks if requested.
V8_INLINE void BlockIfRequested(std::string_view synchronization_point) {
diff --git a/src/runtime/runtime-test.cc b/src/runtime/runtime-test.cc
index 72a208a..b6884c2 100644
--- a/src/runtime/runtime-test.cc
+++ b/src/runtime/runtime-test.cc
@@ -2903,13 +2903,9 @@
DCHECK_EQ(1, args.length());
CHECK_UNLESS_FUZZING(IsString(args[0]));
DirectHandle<String> phase_name = args.at<String>(0);
- bool resumed =
+ bool success =
SynchronizationPointSupport::Get()->Resume(phase_name->ToStdString());
- if (!resumed) {
- return isolate->Throw(*isolate->factory()->NewStringFromAsciiChecked(
- "No thread is currently blocked at this synchronization point"));
- }
- return ReadOnlyRoots(isolate).undefined_value();
+ return isolate->heap()->ToBoolean(success);
}
// Waits until the given synchronization point is reached. Throws an exception
@@ -2926,15 +2922,9 @@
base::TimeDelta timeout =
base::TimeDelta::FromMilliseconds(args.smi_value_at(1));
- bool timed_out = false;
- bool blocked = SynchronizationPointSupport::Get()->WaitUntilBlocked(
- phase_name->ToStdString(), timeout, timed_out);
- if (!blocked) {
- return isolate->Throw(*isolate->factory()->NewStringFromAsciiChecked(
- timed_out ? "Synchronization point wait timed out"
- : "Synchronization point not found or not armed"));
- }
- return ReadOnlyRoots(isolate).undefined_value();
+ bool success = SynchronizationPointSupport::Get()->WaitUntilBlocked(
+ phase_name->ToStdString(), timeout);
+ return isolate->heap()->ToBoolean(success);
}
} // namespace internal
diff --git a/test/mjsunit/compiler/regress-519664497.js b/test/mjsunit/compiler/regress-519664497.js
index 566a322..57a7321 100644
--- a/test/mjsunit/compiler/regress-519664497.js
+++ b/test/mjsunit/compiler/regress-519664497.js
@@ -26,13 +26,13 @@
%OptimizeFunctionOnNextCall(f, "concurrent");
assertEquals(expected, f());
-%WaitUntilBlocked('NewConsString', 1000);
+assertTrue(%WaitUntilBlocked('NewConsString', 1000));
// Internalize the string. This will mutate the string on the main thread
// and turn it into a ThinString, right while the background thread is
// stopped at NewConsString.
%InternalizeString(left);
-%Resume('NewConsString');
+assertTrue(%Resume('NewConsString'));
%WaitForBackgroundOptimization();
assertEquals(expected, f());
diff --git a/test/mjsunit/regress/regress-511263221.js b/test/mjsunit/regress/regress-511263221.js
index 18d63d6..85fad54 100644
--- a/test/mjsunit/regress/regress-511263221.js
+++ b/test/mjsunit/regress/regress-511263221.js
@@ -8,34 +8,28 @@
onmessage = function(msg) {
let sab = new Int32Array(msg.data);
- try {
- %BlockAt("ExternalizeStringExtensionMakeExternalOneByte", 10000);
- %BlockAt("ConcurrentConcatenateStrings", 10000);
- postMessage("ready");
+ %BlockAt("ExternalizeStringExtensionMakeExternalOneByte", 10000);
+ %BlockAt("ConcurrentConcatenateStrings", 10000);
+ postMessage("ready");
- %WaitUntilBlocked("ConcurrentConcatenateStrings", 10000);
- %WaitUntilBlocked("ExternalizeStringExtensionMakeExternalOneByte", 10000);
+ %WaitUntilBlocked("ConcurrentConcatenateStrings", 10000);
+ %WaitUntilBlocked("ExternalizeStringExtensionMakeExternalOneByte", 10000);
- %BlockAt("StringWriteToFlatConsString", 10000);
- %Resume("ConcurrentConcatenateStrings");
+ %BlockAt("StringWriteToFlatConsString", 10000);
+ %Resume("ConcurrentConcatenateStrings");
- %WaitUntilBlocked("StringWriteToFlatConsString", 10000);
+ %WaitUntilBlocked("StringWriteToFlatConsString", 10000);
- %Resume("ExternalizeStringExtensionMakeExternalOneByte");
+ %Resume("ExternalizeStringExtensionMakeExternalOneByte");
- Atomics.wait(sab, 0, 0);
+ Atomics.wait(sab, 0, 0);
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/test/mjsunit/compiler/regress-519664497.js b/test/mjsunit/compiler/regress-519664497.js
index 566a322..57a7321 100644
--- a/test/mjsunit/compiler/regress-519664497.js
+++ b/test/mjsunit/compiler/regress-519664497.js
@@ -26,13 +26,13 @@
%OptimizeFunctionOnNextCall(f, "concurrent");
assertEquals(expected, f());
-%WaitUntilBlocked('NewConsString', 1000);
+assertTrue(%WaitUntilBlocked('NewConsString', 1000));
// Internalize the string. This will mutate the string on the main thread
// and turn it into a ThinString, right while the background thread is
// stopped at NewConsString.
%InternalizeString(left);
-%Resume('NewConsString');
+assertTrue(%Resume('NewConsString'));
%WaitForBackgroundOptimization();
assertEquals(expected, f());
diff --git a/test/mjsunit/regress/regress-511263221.js b/test/mjsunit/regress/regress-511263221.js
index 18d63d6..85fad54 100644
--- a/test/mjsunit/regress/regress-511263221.js
+++ b/test/mjsunit/regress/regress-511263221.js
@@ -8,34 +8,28 @@
onmessage = function(msg) {
let sab = new Int32Array(msg.data);
- try {
- %BlockAt("ExternalizeStringExtensionMakeExternalOneByte", 10000);
- %BlockAt("ConcurrentConcatenateStrings", 10000);
- postMessage("ready");
+ %BlockAt("ExternalizeStringExtensionMakeExternalOneByte", 10000);
+ %BlockAt("ConcurrentConcatenateStrings", 10000);
+ postMessage("ready");
- %WaitUntilBlocked("ConcurrentConcatenateStrings", 10000);
- %WaitUntilBlocked("ExternalizeStringExtensionMakeExternalOneByte", 10000);
+ %WaitUntilBlocked("ConcurrentConcatenateStrings", 10000);
+ %WaitUntilBlocked("ExternalizeStringExtensionMakeExternalOneByte", 10000);
- %BlockAt("StringWriteToFlatConsString", 10000);
- %Resume("ConcurrentConcatenateStrings");
+ %BlockAt("StringWriteToFlatConsString", 10000);
+ %Resume("ConcurrentConcatenateStrings");
- %WaitUntilBlocked("StringWriteToFlatConsString", 10000);
+ %WaitUntilBlocked("StringWriteToFlatConsString", 10000);
- %Resume("ExternalizeStringExtensionMakeExternalOneByte");
+ %Resume("ExternalizeStringExtensionMakeExternalOneByte");
- Atomics.wait(sab, 0, 0);
+ Atomics.wait(sab, 0, 0);
- %Resume("StringWriteToFlatConsString");
+ %Resume("StringWriteToFlatConsString");
- } catch(e) {
- } finally {
- try {
- %Resume("ExternalizeStringExtensionMakeExternalOneByte");
- } catch(e) {}
- try { %Resume("ConcurrentConcatenateStrings"); } catch(e) {}
- try { %Resume("StringWriteToFlatConsString"); } catch(e) {}
- close();
- }
+ %Resume("ExternalizeStringExtensionMakeExternalOneByte");
+ %Resume("ConcurrentConcatenateStrings");
+ %Resume("StringWriteToFlatConsString");
+ close();
}
}
diff --git a/test/mjsunit/sandbox/regress/regress-502997649.js b/test/mjsunit/sandbox/regress/regress-502997649.js
index cbf6978..9b4529f 100644
--- a/test/mjsunit/sandbox/regress/regress-502997649.js
+++ b/test/mjsunit/sandbox/regress/regress-502997649.js
@@ -30,7 +30,7 @@
%OptimizeFunctionOnNextCall(g, 'concurrent');
g();
-%WaitUntilBlocked('TurbofanEarlyGraphTrimming', 10000);
+assertTrue(%WaitUntilBlocked('TurbofanEarlyGraphTrimming', 10000));
const aMod = Sandbox.getAddressOf(mod);
const aInst = Sandbox.getAddressOf(inst);
@@ -55,6 +55,6 @@
gc(); gc(); gc();
-%Resume('TurbofanEarlyGraphTrimming');
+assertTrue(%Resume('TurbofanEarlyGraphTrimming'));
%WaitForBackgroundOptimization();
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page