CVE-2026-117910
Overview
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
.prototypeoff the constructor viaGetProperty, 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.
.prototype, handing attackers a script-reentrancy callback during generator construction; self-hosted builtins must bypass that lookup entirely.Attack Path
- ENTRY
From content JS, take the prototype of a generator or async-generator function (e.g.
Object.getPrototypeOf(function*(){})). - CONTROLLED INPUT
Object.defineProperty(thatProto, "prototype", { get() { /* attacker code */ } }). - TRIGGER
Invoke an iterator helper that builds a self-hosted generator, e.g.
[].values().take(1)or the async form. - MISSING CHECK
create()reads.prototypeeven for the self-hosted builtin, calling the attacker getter mid-construction (unexpected reentrancy). - 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifjs/src/vm/AsyncIteration.cpp |
modified | |
getjs/src/jit-test/tests/iterator/bug2079156-async.js |
modified | |
getjs/src/jit-test/tests/iterator/bug2079156.js |
modified |
Files Changed
js/src/jit-test/tests/iterator/bug2079156-async.jsjs/src/jit-test/tests/iterator/bug2079156.jsjs/src/vm/AsyncIteration.cppjs/src/vm/GeneratorObject.cpp
Audit Directions
- Self-hosted construction pathsGrep for
GetProperty(..., names().prototype, ...)and similar in object-creation code and confirm each is skipped forisSelfHostedBuiltin(). - Getter reentrancyAudit engine-internal operations that read user-reachable properties for attacker-triggered callbacks during half-initialized object states.
Patch
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);
Regression Test / PoC
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);