High firefox UAF 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionInvalid pointer in the JavaScript Engine component
ComponentSpiderMonkey
Bug ClassUAF
Tracker2079156
Fix commite1d135f3efc5 (firefox) +38/-6
CISA KEVNot listed
CreditedMozilla
Disclosed2026-10-27

Background

Iterator helpers
ECMAScript methods such as Iterator.prototype.take; SpiderMonkey implements them with self-hosted generators that the spec defines as abstract closures, not real JS functions.
`OrdinaryCreateFromConstructor`
the step that derives a new object’s prototype by reading .prototype off the constructor via GetProperty, which can invoke a user-defined getter.
`isSelfHostedBuiltin()`
predicate identifying engine-internal functions; such functions must use the default prototype, never a user-visible .prototype.

Root Cause Analysis

When creating the generator object that backs an iterator helper, GeneratorObject::create (and the async form OrdinaryCreateFromConstructorAsyncGen) unconditionally called GetProperty(cx, fun, fun, cx->names().prototype, …) to obtain the instance prototype. Because iterator-helper generators are self-hosted abstract closures, their prototype is engine-internal and should never be read from the function, but the lookup still ran — and an attacker can install a getter on the relevant %GeneratorFunction.prototype%/%AsyncGeneratorFunction.prototype% so that this internal creation step calls back into script at a point the engine does not expect reentrancy. That attacker-controlled getter runs mid-construction (it can allocate, trigger GC, or re-enter), which the advisory records as an “invalid pointer” in the engine.

The fix skips the lookup for self-hosted builtins (!fun->isSelfHostedBuiltin() && !GetProperty(...)), so the default prototype is used and no user getter is invoked; the added tests assert the getter is never called.

Key insight
A privileged, internal object-creation step read a user-controllable .prototype, handing attackers a script-reentrancy callback during generator construction; self-hosted builtins must bypass that lookup entirely.

Attack Path

  1. ENTRY From content JS, take the prototype of a generator or async-generator function (e.g. Object.getPrototypeOf(function*(){})).
  2. CONTROLLED INPUT Object.defineProperty(thatProto, "prototype", { get() { /* attacker code */ } }).
  3. TRIGGER Invoke an iterator helper that builds a self-hosted generator, e.g. [].values().take(1) or the async form.
  4. MISSING CHECK create() reads .prototype even for the self-hosted builtin, calling the attacker getter mid-construction (unexpected reentrancy).
  5. IMPACT Script runs while a generator object is half-constructed, leading to an invalid-pointer condition in the JS engine. (The concrete corrupted pointer/UAF sink is not shown by the patch — inferred from the advisory; the patch proves only the unexpected getter invocation.)

Impact Assessment

Untrusted content JavaScript gains attacker-controlled reentrancy into an internal engine operation, which the advisory attributes to an invalid pointer — a memory-safety fault usable toward corruption in the content process (primitive/escalation inferred). The only precondition is the ability to define a getter on a built-in generator function prototype, which any script can do. Rated high; no ITW noted.

Changed Functions

FunctionChangeNotes
if
js/src/vm/AsyncIteration.cpp
modified
get
js/src/jit-test/tests/iterator/bug2079156-async.js
modified
get
js/src/jit-test/tests/iterator/bug2079156.js
modified

Files Changed

  • js/src/jit-test/tests/iterator/bug2079156-async.js
  • js/src/jit-test/tests/iterator/bug2079156.js
  • js/src/vm/AsyncIteration.cpp
  • js/src/vm/GeneratorObject.cpp

Audit Directions

  • Self-hosted construction paths
    Grep for GetProperty(..., names().prototype, ...) and similar in object-creation code and confirm each is skipped for isSelfHostedBuiltin().
  • Getter reentrancy
    Audit engine-internal operations that read user-reachable properties for attacker-triggered callbacks during half-initialized object states.
diff --git a/js/src/vm/AsyncIteration.cpp b/js/src/vm/AsyncIteration.cpp
index eefcf78239d..42e13ae703e 100644
--- a/js/src/vm/AsyncIteration.cpp
+++ b/js/src/vm/AsyncIteration.cpp
@@ -43,7 +43,7 @@ const JSClassOps AsyncGeneratorObject::classOps_ = {
 // https://tc39.es/ecma262/#sec-ordinarycreatefromconstructor
 //
 // specialized for AsyncGeneratorObjects.
-static AsyncGeneratorObject* OrdinaryCreateFromConstructorAsynGen(
+static AsyncGeneratorObject* OrdinaryCreateFromConstructorAsyncGen(
     JSContext* cx, HandleFunction constructor) {
   // Step 1. Assert: intrinsicDefaultProto is this specification's name of an
   //         intrinsic object. The corresponding object must be an intrinsic
@@ -53,8 +53,13 @@ static AsyncGeneratorObject* OrdinaryCreateFromConstructorAsynGen(
 
   // Step 2. Let proto be
   //         ? GetPrototypeFromConstructor(constructor, intrinsicDefaultProto).
+  // Note: this step is skipped for async iterator helpers, which are specified
+  // as abstract closures, not JS functions, so they always use the default
+  // prototype instead of reading it from the constructor:
+  // https://arai-a.github.io/ecma262-compare/?rev=5f4be6e41ce9f6e1997f5e1e42af253145c6c885&id=sec-createasynciteratorfromclosure
   RootedValue protoVal(cx);
-  if (!GetProperty(cx, constructor, constructor, cx->names().prototype,
+  if (!constructor->isSelfHostedBuiltin() &&
+      !GetProperty(cx, constructor, constructor, cx->names().prototype,
                    &protoVal)) {
     return nullptr;
   }
@@ -91,7 +96,7 @@ AsyncGeneratorObject* AsyncGeneratorObject::create(JSContext* cx,
   MOZ_ASSERT(asyncGen->isAsync() && asyncGen->isGenerator());
 
   AsyncGeneratorObject* generator =
-      OrdinaryCreateFromConstructorAsynGen(cx, asyncGen);
+      OrdinaryCreateFromConstructorAsyncGen(cx, asyncGen);
   if (!generator) {
     return nullptr;
   }
diff --git a/js/src/vm/GeneratorObject.cpp b/js/src/vm/GeneratorObject.cpp
index 42818dcc8de..70d3365a722 100644
--- a/js/src/vm/GeneratorObject.cpp
+++ b/js/src/vm/GeneratorObject.cpp
@@ -305,11 +305,16 @@ bool js::ResumeGenerator(JSContext* cx, Handle<AbstractGeneratorObject*> genObj,
 GeneratorObject* GeneratorObject::create(JSContext* cx, HandleFunction fun) {
   MOZ_ASSERT(fun->isGenerator() && !fun->isAsync());
 
-  // FIXME: This would be faster if we could avoid doing a lookup to get
-  // the prototype for the instance.  Bug 906600.
+  // In general, we have to look up the prototype for the instance here
+  // (although it would be nice if we could avoid it somehow; see bug 906600.)
+  // Self-hosted generators are only used to implement iterator helpers, which
+  // are specified as abstract closures. They always use the default prototype
+  // instead of reading it from the constructor.
+  // See https://tc39.es/ecma262/#sec-createiteratorfromclosure.
   RootedTuple<Value, JSObject*> roots(cx);
   RootedField<Value, 0> pval(roots);
-  if (!GetProperty(cx, fun, fun, cx->names().prototype, &pval)) {
+  if (!fun->isSelfHostedBuiltin() &&
+      !GetProperty(cx, fun, fun, cx->names().prototype, &pval)) {
     return nullptr;
   }
   RootedField<JSObject*, 1> proto(roots,
diff --git a/js/src/jit-test/tests/iterator/bug2079156-async.js b/js/src/jit-test/tests/iterator/bug2079156-async.js
new file mode 100644
index 00000000000..4c3f2aadd76
--- /dev/null
+++ b/js/src/jit-test/tests/iterator/bug2079156-async.js
@@ -0,0 +1,12 @@
+// |jit-test| --enable-async-iterator-helpers
+
+var getterCalled = false;
+
+var AGFP = Object.getPrototypeOf(async function* () {});
+Object.defineProperty(AGFP, "prototype", {
+  get() {
+    getterCalled = true;
+  },
+});
+(async function* () {})().take(1);
+assertEq(getterCalled, false);
diff --git a/js/src/jit-test/tests/iterator/bug2079156.js b/js/src/jit-test/tests/iterator/bug2079156.js
new file mode 100644
index 00000000000..a2f65d167ed
--- /dev/null
+++ b/js/src/jit-test/tests/iterator/bug2079156.js
@@ -0,0 +1,10 @@
+var getterCalled = false;
+
+var GFP = Object.getPrototypeOf(function* () {});
+Object.defineProperty(GFP, "prototype", {
+  get() {
+    getterCalled = true;
+  },
+});
+[].values().take(1);
+assertEq(getterCalled, false);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/js/src/jit-test/tests/iterator/bug2079156-async.js b/js/src/jit-test/tests/iterator/bug2079156-async.js
new file mode 100644
index 00000000000..4c3f2aadd76
--- /dev/null
+++ b/js/src/jit-test/tests/iterator/bug2079156-async.js
@@ -0,0 +1,12 @@
+// |jit-test| --enable-async-iterator-helpers
+
+var getterCalled = false;
+
+var AGFP = Object.getPrototypeOf(async function* () {});
+Object.defineProperty(AGFP, "prototype", {
+  get() {
+    getterCalled = true;
+  },
+});
+(async function* () {})().take(1);
+assertEq(getterCalled, false);
diff --git a/js/src/jit-test/tests/iterator/bug2079156.js b/js/src/jit-test/tests/iterator/bug2079156.js
new file mode 100644
index 00000000000..a2f65d167ed
--- /dev/null
+++ b/js/src/jit-test/tests/iterator/bug2079156.js
@@ -0,0 +1,10 @@
+var getterCalled = false;
+
+var GFP = Object.getPrototypeOf(function* () {});
+Object.defineProperty(GFP, "prototype", {
+  get() {
+    getterCalled = true;
+  },
+});
+[].values().take(1);
+assertEq(getterCalled, false);
Loading diff…