CVE-2025-1012
Overview
Background
- Delazification
- the process by which SpiderMonkey compiles a function’s bytecode on demand from its retained source text, which can now run concurrently on a helper thread.
- `ScriptSource`
- the object that owns a script’s source text (
units_) and can hold it compressed, uncompressed, or retrievable, freeing or transforming the backing buffer over its lifetime. - `PinnedUnits`
- an RAII guard that borrows a pointer into a
ScriptSource’s source units and must keep that buffer alive for the duration of the read. - Reader count
- a refcount managed by
addReader/removeReaderthat tells the source how manyPinnedUnitsare currently reading, blocking any concurrent free or compression of the units while it is non-zero.
Root Cause Analysis
The ScriptSource::PinnedUnits and ScriptSource::PinnedUnitsIfUncompressed constructors fetched the raw source pointer via source->units<Unit>(...) (or uncompressedUnits<Unit>(...)) before calling addReader() to register their interest in the buffer. This ordering violates the invariant that the reader count must be incremented before any pointer into the source is materialized, because a concurrent delazification task on another thread could observe a zero reader count in that window and free or recompress the buffer, leaving the just-obtained units_ dangling.
The fix hoists addReader() to the very top of each constructor so the guard is registered before units_ is computed, and if the fetch subsequently returns null it calls removeReader<Unit>() to balance the count. This closes the race by ensuring the buffer is protected across the entire fetch, rather than only after a pointer to it already exists.
addReader) after acquiring the source pointer instead of before, creating a check-to-use gap in which another thread could free the units; the fix reorders the registration to precede the fetch and rolls it back with removeReader only on the null-result path.Attack Path
- Trigger concurrent delazification
Cause a lazily-compiled function to be delazified while another thread is also touching the same
ScriptSource, so two agents race over the source units. - Enter the unguarded window
On one thread, a
PinnedUnitsconstructor computesunits_from the source beforeaddReader()has run, so the reader count is still zero at the moment the pointer is taken. - Free the buffer The other thread, seeing no readers, frees or recompresses the source units, invalidating the pointer captured in step 2.
- Use after free
The first thread proceeds to read through the now-dangling
units_, dereferencing freed memory during tokenizing/parsing of the source. - Escalate By controlling reallocation of the freed region, the attacker turns the stale read (or subsequent write via the reader) into a controlled corruption primitive within the JS engine.
Impact Assessment
PinnedUnits reader and a concurrent delazification or source-freeing operation, making it non-deterministic and rated moderate.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifjs/src/vm/JSScript.cpp |
modified |
Files Changed
js/src/vm/JSScript.cpp
Audit Directions
- Check-then-register orderingAudit every RAII guard or refcounted borrow where a raw pointer or resource is acquired before the guard’s count is incremented; the increment must strictly precede the acquisition.
- Concurrent source lifetimeReview all readers of
ScriptSourcebuffers (units,uncompressedUnits, source retrieval and compression paths) for correctaddReader/removeReaderbalancing under helper-thread delazification. - Error-path refcount balanceLook for constructors and acquire functions that increment a reference or reader count but fail to decrement it on early-return/null/exception paths, which can mask or invert lifetime races.
Patch
diff --git a/js/src/vm/JSScript.cpp b/js/src/vm/JSScript.cpp
index 84e1f6298ab..75551c72a92 100644
--- a/js/src/vm/JSScript.cpp
+++ b/js/src/vm/JSScript.cpp
@@ -1255,9 +1255,11 @@ ScriptSource::PinnedUnits<Unit>::PinnedUnits(
: PinnedUnitsBase(source) {
MOZ_ASSERT(source->hasSourceType<Unit>(), "must pin units of source's type");
+ addReader();
+
units_ = source->units<Unit>(cx, holder, begin, len);
- if (units_) {
- addReader();
+ if (!units_) {
+ removeReader<Unit>();
}
}
@@ -1270,9 +1272,11 @@ ScriptSource::PinnedUnitsIfUncompressed<Unit>::PinnedUnitsIfUncompressed(
: PinnedUnitsBase(source) {
MOZ_ASSERT(source->hasSourceType<Unit>(), "must pin units of source's type");
+ addReader();
+
units_ = source->uncompressedUnits<Unit>(begin, len);
- if (units_) {
- addReader();
+ if (!units_) {
+ removeReader<Unit>();
}
}