Low firefox Race 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactlow
DescriptionRace condition in the JavaScript Engine component
ComponentSpiderMonkey
Bug ClassRace
Tracker2053670
Fix commitaafbada78b13 (firefox) +20/-11
CISA KEVNot listed
CreditedAmy Burnett of OpenAI
Disclosed2026-08-18

Background

`ScriptSource`
SpiderMonkey object holding a script’s source text, whose payload lives in a tagged variant data that can be Compressed, Uncompressed, Retrievable, or Missing.
`SourceRetrievable`
flag on a source variant indicating whether the source text can be re-fetched later via a sourceHook on the main thread.
`isTwoByteString`
property reporting whether the underlying source Unit is char16_t (two-byte) rather than Latin1/char (one-byte).
`GeckoProfilerRuntime`
profiler machinery that walks script sources (potentially off the main thread) to record ProfilerJSSources metadata.

Root Cause Analysis

The off-main-thread accessor ScriptSource::getSourceProperties reported only hasSourceText and retrievable, so any caller that also needed the source’s character encoding had to inspect the source payload separately rather than deriving it from the single data.match dispatch. Because getSourceProperties exists specifically to be called outside the main thread (avoiding JSContext and sourceHook), a caller resolving encoding through a second, unsynchronized read of the source could race with main-thread mutation of the data variant.

The fix threads a third out-parameter isTwoByteString through SourcePropertiesGetter and each of its operator() overloads, computing it as std::is_same_v<Unit, char16_t> at the same instant the variant tag is matched. This makes the encoding determination part of the one atomic classification of data, so callers no longer need a separate, racy inspection of the source. The invariant restored is that all encoding/availability properties of a ScriptSource are read together from one variant dispatch.

Key insight
The core mistake was exposing source availability from the safe off-main-thread accessor while forcing callers to learn the character encoding elsewhere, opening a data-race window; the fix folds the encoding (isTwoByteString) into the same data.match classification so it is derived atomically with the other properties.

Attack Path

  1. Trigger off-thread source inspection Cause profiling or another background consumer to query script-source properties for a source while it may be concurrently mutated on the main thread.
  2. Race the variant read The consumer determines encoding via a separate read of the source payload that is not synchronized with the property dispatch, creating a window where data changes underneath it.
  3. Observe inconsistent state The mismatched availability/encoding view yields a torn or stale read of the ScriptSource variant.

Impact Assessment

An attacker gains a low-severity race in the JavaScript engine’s script-source handling, exercised from background/profiler code paths that read ScriptSource off the main thread. Realizing it requires concurrent access to a source that is being mutated on the main thread, and the metadata classifies it as low impact with no assigned CVSS or exploit primitive in the diff.

Changed Functions

FunctionChangeNotes
ScriptSource
js/src/vm/JSScript.cpp
modified
isTwoByteString_
js/src/vm/JSScript.cpp
modified

Files Changed

  • js/src/vm/GeckoProfiler.cpp
  • js/src/vm/JSScript.cpp
  • js/src/vm/JSScript.h

Audit Directions

  • Split property reads on tagged variants
    Flag any code that classifies a variant like ScriptSource::data once but then reads a related property (encoding, unit type) through a separate access; fold both into the single match dispatch.
  • Off-main-thread source access
    Review other callers of getSourceProperties and loadSource for encoding or availability assumptions resolved outside the atomic classification, especially in profiler and background threads.
  • Out-parameter signature changes
    Audit all call sites when an accessor gains an out-parameter to ensure no caller silently discards information that a security-relevant path actually needs.
diff --git a/js/src/vm/GeckoProfiler.cpp b/js/src/vm/GeckoProfiler.cpp
index 03958d36d5e..15a55dd34fa 100644
--- a/js/src/vm/GeckoProfiler.cpp
+++ b/js/src/vm/GeckoProfiler.cpp
@@ -453,8 +453,9 @@ js::ProfilerJSSources GeckoProfilerRuntime::getProfilerScriptSources(
 
     bool hasSourceText;
     bool retrievableSource;
+    bool unused;
     ScriptSource::getSourceProperties(scriptSource, &hasSourceText,
-                                      &retrievableSource);
+                                      &retrievableSource, &unused);
 
     uint32_t sourceId = scriptSource->id();
 
diff --git a/js/src/vm/JSScript.cpp b/js/src/vm/JSScript.cpp
index 9a13d49a713..6b6334ef6ec 100644
--- a/js/src/vm/JSScript.cpp
+++ b/js/src/vm/JSScript.cpp
@@ -899,21 +899,27 @@ bool ScriptSource::loadSource(JSContext* cx, ScriptSource* ss, bool* loaded) {
 class ScriptSource::SourcePropertiesGetter {
   bool* const hasSourceText_;
   bool* const retrievable_;
+  bool* const isTwoByteString_;
 
  public:
-  explicit SourcePropertiesGetter(bool* hasSourceText, bool* retrievable)
-      : hasSourceText_(hasSourceText), retrievable_(retrievable) {}
+  explicit SourcePropertiesGetter(bool* hasSourceText, bool* retrievable,
+                                  bool* isTwoByteString)
+      : hasSourceText_(hasSourceText),
+        retrievable_(retrievable),
+        isTwoByteString_(isTwoByteString) {}
 
   template <typename Unit, SourceRetrievable CanRetrieve>
   void operator()(const Compressed<Unit, CanRetrieve>&) const {
     *hasSourceText_ = true;
     *retrievable_ = false;
+    *isTwoByteString_ = std::is_same_v<Unit, char16_t>;
   }
 
   template <typename Unit, SourceRetrievable CanRetrieve>
   void operator()(const Uncompressed<Unit, CanRetrieve>&) const {
     *hasSourceText_ = true;
     *retrievable_ = false;
+    *isTwoByteString_ = std::is_same_v<Unit, char16_t>;
   }
 
   template <typename Unit>
@@ -921,17 +927,21 @@ class ScriptSource::SourcePropertiesGetter {
     // Retrievable requires the main thread. Do not attempt to retrieve it.
     *hasSourceText_ = false;
     *retrievable_ = true;
+    *isTwoByteString_ = std::is_same_v<Unit, char16_t>;
   }
 
   void operator()(const Missing&) const {
     *hasSourceText_ = false;
     *retrievable_ = false;
+    *isTwoByteString_ = false;
   }
 };
 
 void ScriptSource::getSourceProperties(ScriptSource* ss, bool* hasSourceText,
-                                       bool* retrievable) {
-  ss->data.match(SourcePropertiesGetter(hasSourceText, retrievable));
+                                       bool* retrievable,
+                                       bool* isTwoByteString) {
+  ss->data.match(
+      SourcePropertiesGetter(hasSourceText, retrievable, isTwoByteString));
 }
 
 /* static */
diff --git a/js/src/vm/JSScript.h b/js/src/vm/JSScript.h
index eb272d790da..7e8220e3968 100644
--- a/js/src/vm/JSScript.h
+++ b/js/src/vm/JSScript.h
@@ -696,15 +696,13 @@ class ScriptSource {
   // return false.
   static bool loadSource(JSContext* cx, ScriptSource* ss, bool* loaded);
 
-  // This is similar to loadSource, but it is designed to be used outside of the
-  // main thread. This is done by removing the need of JSContext for the
-  // Retrievable sources that require sourceHook. For retrievable cases, it
-  // sets retrievable to true and sets the isUT16 depending on the encoding.
+  // Returns the source data properties of the script source.
   //
   // *loaded indicates whether source text is available, *retrievable indicates
-  // whether the source can be retrieved later via source hook.
+  // whether the source can be retrieved later via source hook, and
+  // *isTwoByteString indicates if the underlying source data is char16_t-typed.
   static void getSourceProperties(ScriptSource* ss, bool* hasSourceText,
-                                  bool* retrievable);
+                                  bool* retrievable, bool* isTwoByteString);
 
   // Assign source data from |srcBuf| to this recently-created |ScriptSource|.
   template <typename Unit>
Loading diff…