Medium CVSS 7.5 firefox UAF 🔧 Commit mapped

Overview

Medium
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionA race during concurrent delazification could have led to a use-after-free.
ComponentSpiderMonkey
Bug ClassUAF
Tracker1939710
Fix commitf2115f8f412c (firefox) +8/-4
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedNils Bars
Disclosed2025-02-04

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/removeReader that tells the source how many PinnedUnits are 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.

Key insight
The single mistake was registering the reader (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

  1. 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.
  2. Enter the unguarded window On one thread, a PinnedUnits constructor computes units_ from the source before addReader() has run, so the reader count is still zero at the moment the pointer is taken.
  3. Free the buffer The other thread, seeing no readers, frees or recompresses the source units, invalidating the pointer captured in step 2.
  4. Use after free The first thread proceeds to read through the now-dangling units_, dereferencing freed memory during tokenizing/parsing of the source.
  5. 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

An attacker who can run JavaScript in the content process gains a use-after-free on the source-units buffer, giving a read (and potentially write) through freed memory that can be shaped into memory disclosure or corruption inside SpiderMonkey. Exploitation is in the content/renderer process and requires winning a timing race between a PinnedUnits reader and a concurrent delazification or source-freeing operation, making it non-deterministic and rated moderate.

Changed Functions

FunctionChangeNotes
if
js/src/vm/JSScript.cpp
modified

Files Changed

  • js/src/vm/JSScript.cpp

Audit Directions

  • Check-then-register ordering
    Audit 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 lifetime
    Review all readers of ScriptSource buffers (units, uncompressedUnits, source retrieval and compression paths) for correct addReader/removeReader balancing under helper-thread delazification.
  • Error-path refcount balance
    Look 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.
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>();
   }
 }
 
Loading diff…