CVE-2026-117937
Overview
Background
- `JS::FirstSubsumedFrame`
- a stack-capture descriptor carrying a
JSContext* cx, aJSPrincipals* principals, and anignoreSelfHostedflag; used when capturing aSavedFramestack. - 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.
cx, leaving an uninitialized pointer that was later dereferenced; initializing cx from the source fixes it.Attack Path
- ENTRY
Reach a code path that move-constructs a
JS::FirstSubsumedFrame(e.g. building aStackCapturefor a saved stack). - MISSING CHECK
The move constructor leaves
cxuninitialized in the destination object. - PRIMITIVE
A subsequent stack capture dereferences the indeterminate
cxpointer — an invalid-pointer / uninitialized-use fault. - 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
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
| Function | Change | Notes |
|---|---|---|
ignoreSelfHostedjs/public/Stack.h |
modified | |
BEGIN_TESTjs/src/jsapi-tests/testSavedStacks.cpp |
modified |
Files Changed
js/public/Stack.hjs/src/jsapi-tests/testSavedStacks.cpp
Audit Directions
- Partial move/copy constructorsAudit hand-written move/copy constructors for members omitted from the initializer list, especially raw pointers like
cx.
Patch
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);