Firefox · SpiderMonkey
CVE-2026-117891
Logic Error in SpiderMonkey
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifjs/src/jit/CodeGenerator.cpp |
modified | |
CallNativeExitFrameLayoutjs/src/jit/JitFrames.h |
modified | |
ifjs/src/vm/PortableBaselineInterpret.cpp |
modified | |
forjs/src/jit-test/tests/ion/bug2078837.js |
modified | |
whilejs/src/jit-test/tests/ion/bug2078837.js |
modified |
Files Changed
js/src/builtin/TestingFunctions.cppjs/src/jit-test/tests/ion/bug2078837.jsjs/src/jit/BaselineCacheIRCompiler.cppjs/src/jit/CodeGenerator.cppjs/src/jit/IonCacheIRCompiler.cppjs/src/jit/JitFrames.cppjs/src/jit/JitFrames.hjs/src/vm/PortableBaselineInterpret.cpp
Patch
diff --git a/js/src/builtin/TestingFunctions.cpp b/js/src/builtin/TestingFunctions.cpp
index 163f580fc13..d69dddd6f1f 100644
--- a/js/src/builtin/TestingFunctions.cpp
+++ b/js/src/builtin/TestingFunctions.cpp
@@ -3647,6 +3647,13 @@ static bool CaptureFirstSubsumedFrame(JSContext* cx, unsigned argc,
return true;
}
+static bool CallAfterSettingRval(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ args.rval().setUndefined();
+ return JS::Call(cx, UndefinedHandleValue, args.get(0),
+ JS::HandleValueArray::empty(), args.rval());
+}
+
static bool CallFunctionFromNativeFrame(JSContext* cx, unsigned argc,
Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
@@ -10542,6 +10549,11 @@ static const JSFunctionSpecWithHelp TestingFunctions[] = {
" object's compartment's principals. If 'shouldIgnoreSelfHosted' is given,\n"
" control whether self-hosted frames are considered when checking principals."),
+ JS_FN_HELP("callAfterSettingRval", CallAfterSettingRval, 1, 0,
+"callAfterSettingRval(callback)",
+" Set the return value to undefined, then call callback with no arguments\n"
+" and return its result."),
+
JS_FN_HELP("callFunctionFromNativeFrame", CallFunctionFromNativeFrame, 1, 0,
"callFunctionFromNativeFrame(function)",
" Call 'function' with a (C++-)native frame on stack.\n"
diff --git a/js/src/jit/BaselineCacheIRCompiler.cpp b/js/src/jit/BaselineCacheIRCompiler.cpp
index 47651df52df..bd11ae2028d 100644
--- a/js/src/jit/BaselineCacheIRCompiler.cpp
+++ b/js/src/jit/BaselineCacheIRCompiler.cpp
@@ -2985,6 +2985,7 @@ bool BaselineCacheIRCompiler::emitCallNativeShared(
masm.moveStackPtrTo(scratch2.get());
// Construct a native exit frame.
+ masm.push(calleeReg);
masm.push(argcReg);
masm.push(FrameDescriptor(FrameType::BaselineStub));
diff --git a/js/src/jit/CodeGenerator.cpp b/js/src/jit/CodeGenerator.cpp
index edb5b891922..26d71433062 100644
--- a/js/src/jit/CodeGenerator.cpp
+++ b/js/src/jit/CodeGenerator.cpp
@@ -6280,6 +6280,13 @@ void CodeGenerator::emitCallNative(LCallIns* call, JSNative native,
masm.loadJSContext(argContextReg);
masm.moveStackPtrTo(argVpReg);
+ // Initialize |NativeExitFrameLayout::savedCallee_|.
+ if constexpr (std::is_same_v<LCallIns, LCallClassHook>) {
+ masm.Push(ToRegister(call->getCallee()));
+ } else {
+ masm.Push(ImmGCPtr(call->mir()->getSingleTarget()->rawNativeJSFunction()));
+ }
+
// Initialize |NativeExitFrameLayout::argc_|.
masm.Push(argUintNReg);
@@ -6449,6 +6456,11 @@ void CodeGenerator::visitCallDOMNative(LCallDOMNative* call) {
MOZ_ASSERT(target->hasJitInfo());
MOZ_ASSERT(call->mir()->isCallDOMNative());
+ // CanAttachDOMCall only optimizes DOM calls for same-realm targets. This
+ // means that IonDOMMethod native exit frames don't need a copy of the callee.
+ // See NativeExitFrameLayout::savedCallee_.
+ MOZ_RELEASE_ASSERT(!call->mir()->maybeCrossRealm());
+
int unusedStack = UnusedStackBytesForCall(call->mir()->paddedNumStackArgs());
// Registers used for callWithABI() argument-passing.
@@ -6507,12 +6519,6 @@ void CodeGenerator::visitCallDOMNative(LCallDOMNative* call) {
masm.Push(argObj);
masm.moveStackPtrTo(argObj);
- if (call->mir()->maybeCrossRealm()) {
- // We use argJSContext as scratch register here.
- masm.movePtr(ImmGCPtr(target->rawNativeJSFunction()), argJSContext);
- masm.switchToObjectRealm(argJSContext, argJSContext);
- }
-
bool preTenureWrapperAllocation =
call->mir()->to<MCallDOMNative>()->initialHeap() == gc::Heap::Tenured;
if (preTenureWrapperAllocation) {
@@ -6563,12 +6569,6 @@ void CodeGenerator::visitCallDOMNative(LCallDOMNative* call) {
static_assert(!JSReturnOperand.aliases(ReturnReg),
"Clobbering ReturnReg should not affect the return value");
- // Switch back to the current realm if needed. Note: if the DOM method threw
- // an exception, the exception handler will do this.
- if (call->mir()->maybeCrossRealm()) {
- masm.switchToRealm(gen->realm->realmPtr(), ReturnReg);
- }
-
// Wipe out the preTenuring bit from the local alloc site
// On exception we handle this in C++
if (preTenureWrapperAllocation) {
@@ -7051,6 +7051,12 @@ void JitRuntime::generateIonGenericCallNativeFunction(MacroAssembler& masm,
// Push a value containing the callee, which will become argv[0].
masm.pushValue(JSVAL_TYPE_OBJECT, calleeReg);
+ // Load argv into scratch2.
+ masm.moveStackPtrTo(scratch2);
+
+ // Initialize |NativeExitFrameLayout::savedCallee_|.
+ masm.push(calleeReg);
+
// Load the callee address into calleeReg.
#ifdef JS_SIMULATOR
masm.movePtr(ImmPtr(RedirectedCallAnyNative()), calleeReg);
@@ -7059,9 +7065,6 @@ void JitRuntime::generateIonGenericCallNativeFunction(MacroAssembler& masm,
calleeReg);
#endif
- // Load argv into scratch2.
- masm.moveStackPtrTo(scratch2);
-
// Push argc.
masm.push(argcReg);
diff --git a/js/src/jit/IonCacheIRCompiler.cpp b/js/src/jit/IonCacheIRCompiler.cpp
index 950181be43c..2e70eb2b5bf 100644
--- a/js/src/jit/IonCacheIRCompiler.cpp
+++ b/js/src/jit/IonCacheIRCompiler.cpp
@@ -1211,6 +1211,7 @@ bool IonCacheIRCompiler::emitCallNativeGetterResult(
masm.moveStackPtrTo(argVp.get());
// Push marking data for later use.
+ masm.Push(ImmGCPtr(target));
masm.Push(argUintN);
pushStubCodePointer();
@@ -1725,6 +1726,7 @@ bool IonCacheIRCompiler::emitCallNativeSetter(ObjOperandId receiverId,
masm.move32(Imm32(1), argUintN);
// Push marking data for later use.
+ masm.Push(ImmGCPtr(target));
masm.Push(argUintN);
pushStubCodePointer();
diff --git a/js/src/jit/JitFrames.cpp b/js/src/jit/JitFrames.cpp
index e37d76b1f37..fe84b525d90 100644
--- a/js/src/jit/JitFrames.cpp
+++ b/js/src/jit/JitFrames.cpp
@@ -1308,6 +1308,7 @@ static void TraceJitExitFrame(JSTracer* trc, const JSJitFrameIter& frame) {
size_t len = native->argc() + 2;
Value* vp = native->vp();
TraceRootRange(trc, len, vp, "ion-native-args");
+ TraceRoot(trc, native->savedCallee(), "ion-native-saved-callee");
if (frame.isExitFrameLayout<ConstructNativeExitFrameLayout>()) {
TraceRoot(trc, vp + len, "ion-native-new-target");
}
@@ -1319,6 +1320,7 @@ static void TraceJitExitFrame(JSTracer* trc, const JSJitFrameIter& frame) {
frame.exitFrame()->as<IonOOLNativeExitFrameLayout>();
TraceRoot(trc, oolnative->stubCode(), "ion-ool-native-code");
TraceRoot(trc, oolnative->vp(), "iol-ool-native-vp");
+ TraceRoot(trc, oolnative->savedCallee(), "ion-ool-native-saved-callee");
size_t len = oolnative->argc() + 1;
TraceRootRange(trc, len, oolnative->thisp(), "ion-ool-native-thisargs");
return;
diff --git a/js/src/jit/JitFrames.h b/js/src/jit/JitFrames.h
index 7f4b5f6bf31..1373380d563 100644
--- a/js/src/jit/JitFrames.h
+++ b/js/src/jit/JitFrames.h
@@ -684,6 +684,11 @@ class NativeExitFrameLayout {
ExitFrameLayout exit_;
uintptr_t argc_;
+ // The callee. Unlike the Value below, this isn't overwritten by the return
+ // value. This ensures the callee's realm is kept alive on GC.
+ // See bug 2078837.
+ JSObject* savedCallee_;
+
// We need to split the Value into 2 fields of 32 bits, otherwise the C++
// compiler may add some padding between the fields.
uint32_t loCalleeResult_;
@@ -699,6 +704,7 @@ class NativeExitFrameLayout {
return reinterpret_cast<JS::Value*>(&loCalleeResult_);
}
inline uintptr_t argc() const { return argc_; }
+ inline JSObject** savedCallee() { return &savedCallee_; }
};
class CallNativeExitFrameLayout : public NativeExitFrameLayout {
@@ -727,6 +733,9 @@ class IonOOLNativeExitFrameLayout {
uintptr_t argc_;
+ // See NativeExitFrameLayout::savedCallee_.
+ JSObject* savedCallee_;
+
// We need to split the Value into 2 fields of 32 bits, otherwise the C++
// compiler may add some padding between the fields.
uint32_t loCalleeResult_;
@@ -755,6 +764,7 @@ class IonOOLNativeExitFrameLayout {
}
inline JS::Value* thisp() { return reinterpret_cast<JS::Value*>(&loThis_); }
inline uintptr_t argc() const { return argc_; }
+ inline JSObject** savedCallee() { return &savedCallee_; }
};
// ProxyGetProperty(JSContext* cx, HandleObject proxy, HandleId id,
diff --git a/js/src/vm/PortableBaselineInterpret.cpp b/js/src/vm/PortableBaselineInterpret.cpp
index 0bdda223452..ec80deec911 100644
--- a/js/src/vm/PortableBaselineInterpret.cpp
+++ b/js/src/vm/PortableBaselineInterpret.cpp
@@ -2304,7 +2304,7 @@ uint64_t ICInterpretOps(uint64_t arg0, uint64_t arg1, ICStub* stub,
{
PUSH_IC_FRAME();
- if (!ctx.stack.check(sp, sizeof(StackVal) * (totalArgs + 6))) {
+ if (!ctx.stack.check(sp, sizeof(StackVal) * (totalArgs + 7))) {
ReportOverRecursed(ctx.frameMgr.cxForLocalUseOnly());
ctx.error = PBIResult::Error;
return IC_ERROR_SENTINEL();
@@ -2374,6 +2374,7 @@ uint64_t ICInterpretOps(uint64_t arg0, uint64_t arg1, ICStub* stub,
Value* args = reinterpret_cast<Value*>(sp);
if (isNative) {
+ PUSHNATIVE(StackValNative(callee));
PUSHNATIVE(StackValNative(argc));
PUSHNATIVE(
StackValNative(MakeFrameDescriptor(FrameType::BaselineStub)));
diff --git a/js/src/jit-test/tests/ion/bug2078837.js b/js/src/jit-test/tests/ion/bug2078837.js
new file mode 100644
index 00000000000..531d58660af
--- /dev/null
+++ b/js/src/jit-test/tests/ion/bug2078837.js
@@ -0,0 +1,15 @@
+// |jit-test| --no-threads; --fast-warmup
+var fns = [];
+for (var i = 0; i < 5; i++) {
+ fns.push(newGlobal({sameCompartmentAs: this}).callAfterSettingRval);
+}
+for (var i = 0; i < 50; i++) {
+ fns.push(Math.abs, Math.sin, Math.cos, Math.tan,
+ Math.exp, Math.log, Math.sqrt, Math.atan);
+}
+function test() {
+ while (fns.length > 0) {
+ fns.pop()(gc);
+ }
+}
+test();
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/js/src/jit-test/tests/ion/bug2078837.js b/js/src/jit-test/tests/ion/bug2078837.js
new file mode 100644
index 00000000000..531d58660af
--- /dev/null
+++ b/js/src/jit-test/tests/ion/bug2078837.js
@@ -0,0 +1,15 @@
+// |jit-test| --no-threads; --fast-warmup
+var fns = [];
+for (var i = 0; i < 5; i++) {
+ fns.push(newGlobal({sameCompartmentAs: this}).callAfterSettingRval);
+}
+for (var i = 0; i < 50; i++) {
+ fns.push(Math.abs, Math.sin, Math.cos, Math.tan,
+ Math.exp, Math.log, Math.sqrt, Math.atan);
+}
+function test() {
+ while (fns.length > 0) {
+ fns.pop()(gc);
+ }
+}
+test();
Loading diff…
References
On This Page