High firefox Uninitialized Memory 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionInvalid pointer in the JavaScript Engine component
ComponentSpiderMonkey
Bug ClassUninitialized Memory
Tracker2082005
Fix commit42d81ac17e74 (firefox) +43/-1
CISA KEVNot listed
CreditedOpenAI Codex Security (kylebot)
Disclosed2026-10-27

Background

`JS::FirstSubsumedFrame`
a stack-capture descriptor carrying a JSContext* cx, a JSPrincipals* principals, and an ignoreSelfHosted flag; used when capturing a SavedFrame stack.
Move constructor
C++ constructor that transfers ownership from an rvalue; it must initialize every member of the new object.

Root Cause Analysis

FirstSubsumedFrame’s move constructor initialized only principals and ignoreSelfHosted, leaving the cx member uninitialized (indeterminate) in the moved-to object. When that object was later used to capture a saved stack, the garbage cx was dereferenced — the “invalid pointer” the advisory describes.

The fix adds cx(rhs.cx) to the member-initializer list so the context is carried through the move; the added testSavedStacks_FirstSubsumedFrameMove test asserts target.cx == cx across chained moves.

Key insight
The move constructor forgot to copy cx, leaving an uninitialized pointer that was later dereferenced; initializing cx from the source fixes it.

Attack Path

  1. ENTRY Reach a code path that move-constructs a JS::FirstSubsumedFrame (e.g. building a StackCapture for a saved stack).
  2. MISSING CHECK The move constructor leaves cx uninitialized in the destination object.
  3. PRIMITIVE A subsequent stack capture dereferences the indeterminate cx pointer — an invalid-pointer / uninitialized-use fault.
  4. IMPACT Memory-safety fault in the JS engine. (Whether the garbage value is attacker-influenceable is not shown by the patch; classified from the uninitialized-field root cause.)

Impact Assessment

An uninitialized JSContext* is dereferenced during saved-stack capture, a memory-safety defect in the engine; exploitability depends on whether the stale stack slot is attacker-controllable and on cx having no default member initializer (garbage vs null) — neither is shown by the patch (inferred/uncertain). Found by OpenAI Codex Security; advisory severity high; no ITW stated.

Changed Functions

FunctionChangeNotes
ignoreSelfHosted
js/public/Stack.h
modified
BEGIN_TEST
js/src/jsapi-tests/testSavedStacks.cpp
modified

Files Changed

  • js/public/Stack.h
  • js/src/jsapi-tests/testSavedStacks.cpp

Audit Directions

  • Partial move/copy constructors
    Audit hand-written move/copy constructors for members omitted from the initializer list, especially raw pointers like cx.
diff --git a/js/public/Stack.h b/js/public/Stack.h
index 7ab880b9baf..9e7d8a6c739 100644
--- a/js/public/Stack.h
+++ b/js/public/Stack.h
@@ -106,7 +106,9 @@ struct JS_PUBLIC_API FirstSubsumedFrame {
   FirstSubsumedFrame& operator=(FirstSubsumedFrame&&) = delete;
 
   FirstSubsumedFrame(FirstSubsumedFrame&& rhs)
-      : principals(rhs.principals), ignoreSelfHosted(rhs.ignoreSelfHosted) {
+      : cx(rhs.cx),
+        principals(rhs.principals),
+        ignoreSelfHosted(rhs.ignoreSelfHosted) {
     MOZ_ASSERT(this != &rhs, "self move disallowed");
     rhs.principals = nullptr;
   }
diff --git a/js/src/jsapi-tests/testSavedStacks.cpp b/js/src/jsapi-tests/testSavedStacks.cpp
index b2a0b64c868..1fecc79211a 100644
--- a/js/src/jsapi-tests/testSavedStacks.cpp
+++ b/js/src/jsapi-tests/testSavedStacks.cpp
@@ -6,6 +6,8 @@
 
 #include "mozilla/Utf8.h"  // mozilla::Utf8Unit
 
+#include <utility>  // std::move
+
 #include "builtin/TestingFunctions.h"
 #include "js/ColumnNumber.h"  // JS::LimitedColumnNumberOneOrigin, JS::TaggedColumnNumberOneOrigin
 #include "js/CompilationAndEvaluation.h"  // JS::Evaluate
@@ -27,6 +29,44 @@ BEGIN_TEST(testSavedStacks_withNoStack) {
 }
 END_TEST(testSavedStacks_withNoStack)
 
+BEGIN_TEST(testSavedStacks_FirstSubsumedFrameMove) {
+  struct Principals final : public TestJSPrincipals {
+    size_t destroyCalls = 0;
+
+    static void destroy(JSPrincipals* p) {
+      static_cast<Principals*>(p)->destroyCalls++;
+    }
+  };
+
+  JS_InitDestroyPrincipalsCallback(cx, Principals::destroy);
+  Principals principals;
+
+  {
+    JS::FirstSubsumedFrame frame(cx, &principals, false);
+    CHECK(principals.refcount == 1);
+    {
+      JS::StackCapture capture(std::move(frame));
+      CHECK(frame.principals == nullptr);
+
+      JS::StackCapture moved(std::move(capture));
+      CHECK(capture.as<JS::FirstSubsumedFrame>().principals == nullptr);
+      const auto& target = moved.as<JS::FirstSubsumedFrame>();
+      CHECK(target.cx == cx);
+      CHECK(target.principals == &principals);
+      CHECK(!target.ignoreSelfHosted);
+      CHECK(principals.refcount == 1);
+      CHECK(principals.destroyCalls == 0);
+    }
+    CHECK(principals.refcount == 0);
+    CHECK(principals.destroyCalls == 1);
+  }
+
+  CHECK(principals.refcount == 0);
+  CHECK(principals.destroyCalls == 1);
+  return true;
+}
+END_TEST(testSavedStacks_FirstSubsumedFrameMove)
+
 BEGIN_TEST(testSavedStacks_ApiDefaultValues) {
   JS::Rooted<js::SavedFrame*> savedFrame(cx, nullptr);
 
Loading diff…