CVE-2026-74984
Overview
Background
- `ScriptSource`
- SpiderMonkey object holding a script’s source text, whose payload lives in a tagged variant
datathat can beCompressed,Uncompressed,Retrievable, orMissing. - `SourceRetrievable`
- flag on a source variant indicating whether the source text can be re-fetched later via a
sourceHookon the main thread. - `isTwoByteString`
- property reporting whether the underlying source
Unitischar16_t(two-byte) rather thanLatin1/char(one-byte). - `GeckoProfilerRuntime`
- profiler machinery that walks script sources (potentially off the main thread) to record
ProfilerJSSourcesmetadata.
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.
isTwoByteString) into the same data.match classification so it is derived atomically with the other properties.Attack Path
- 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.
- 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
datachanges underneath it. - Observe inconsistent state
The mismatched availability/encoding view yields a torn or stale read of the
ScriptSourcevariant.
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
ScriptSourcejs/src/vm/JSScript.cpp |
modified | |
isTwoByteString_js/src/vm/JSScript.cpp |
modified |
Files Changed
js/src/vm/GeckoProfiler.cppjs/src/vm/JSScript.cppjs/src/vm/JSScript.h
Audit Directions
- Split property reads on tagged variantsFlag any code that classifies a variant like
ScriptSource::dataonce but then reads a related property (encoding, unit type) through a separate access; fold both into the singlematchdispatch. - Off-main-thread source accessReview other callers of
getSourcePropertiesandloadSourcefor encoding or availability assumptions resolved outside the atomic classification, especially in profiler and background threads. - Out-parameter signature changesAudit all call sites when an accessor gains an out-parameter to ensure no caller silently discards information that a security-relevant path actually needs.
Patch
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>