Assertion failure: expectedFrameSize + JitStackAlignment > frameSize (The frame size is optimal) in [@ js::jit::AssertJitStackInvariants] for resumed generator frames with --emit-interpreter-entry
Categories
(Core :: JavaScript Engine: JIT, defect, P3)
Tracking
()
People
(Reporter: bugmon, Unassigned)
Details
(Keywords: assertion, testcase)
Attachments
(5 files)
js::jit::AssertJitStackInvariants computes the expected size of a scripted frame whose caller is a BaselineInterpreterEntry frame from numActualArgs, the callee's formal count, |this| and new.target only. It does not account for the four ResumeFrameArgs Values that the baseline interpreter entry trampoline copies above the formals when the frame descriptor has IsResumingGenerator set. For a resumed generator the real frame is therefore at least 32 bytes larger than the computed size, and the release assertion "The frame size is optimal" fails even though the frame matches the layout documented in JitFrames.h.
A JS shell user or fuzzer can abort the shell process by calling the assertJitStackInvariants() testing function from a resumed generator frame while running with --emit-interpreter-entry. The demonstrated effect is a deterministic assertion abort in a stack-invariant checker; no memory corruption was observed, and the frame built by the trampoline is consistent with the documented resume layout. Reaching it requires both the non-default interpreter-entry trampoline option and a shell testing function.
Build Info
- Branch: main
- Revision: bf9757d91e2e53b8ab836f2a08bf4c0275fdcd23
- Timestamp: 2026-10-02T21:18:24+00:00
Affected Code
File: js/src/jit/JitFrames.cpp, line 2853-2872
if (frames.isScripted() &&
frames.prevType() == FrameType::BaselineInterpreterEntry) {
MOZ_RELEASE_ASSERT(
frameSize % JitStackAlignment == 0,
"The blinterp entry frame should keep the alignment");
size_t numArgs = frames.numActualArgs();
if (frames.isFunctionFrame()) {
// The caller pushes `undefined` for missing formals.
numArgs = std::max(numArgs, frames.callee()->nargs());
}
size_t expectedFrameSize =
sizeof(Value) * (numArgs + 1 /* |this| argument */ +
frames.isConstructing() /* new.target */) +
sizeof(JitFrameLayout);
MOZ_RELEASE_ASSERT(frameSize >= expectedFrameSize,
"The frame is large enough to hold all arguments");
MOZ_RELEASE_ASSERT(expectedFrameSize + JitStackAlignment > frameSize,
"The frame size is optimal");
}
File: js/src/jit/InterpreterEntryTrampoline.cpp, line 95-116
masm.bind(&resuming);
{
#ifdef DEBUG
Label argcOk;
masm.branchTest32(Assembler::Zero, nargs, nargs, &argcOk);
masm.assumeUnreachable("Resume frames have numActualArgs == 0");
masm.bind(&argcOk);
#endif
masm.branchTestPtr(Assembler::NonZero, callee, Imm32(CalleeTokenScriptBit),
&moduleResume);
// The callee is a generator or async function: the resume args were pushed
// above the formals.
masm.andPtr(Imm32(uint32_t(CalleeTokenMask)), callee, scratch);
masm.loadFunctionArgCount(scratch, nargs);
masm.addPtr(Imm32(ResumeFrameArgs::NumSlots), nargs);
}
masm.bind(&argsCounted);
// Align stack
masm.alignJitStackBasedOnNArgs(nargs, /*countIncludesThis = */ false);
The checker (first block) derives expectedFrameSize without consulting the IsResumingGenerator descriptor bit. The trampoline (second block) adds ResumeFrameArgs::NumSlots (4, js/src/vm/Stack.h:276-283) to the number of Values it copies when that bit is set, as documented at js/src/jit/JitFrames.h:127-137. For function* g() with zero formals the checker expects 8*1 + sizeof(JitFrameLayout); the trampoline pushes |this| plus four resume Values plus alignment padding. The register dump at the abort (r13=0x50, r14=0x38) is consistent with frameSize=80 and expectedFrameSize+JitStackAlignment=56. The option is off by default unless the IONPERF environment variable is set (js/src/jit/JitOptions.cpp:156-157) and is turned on in the shell by --emit-interpreter-entry (js/src/shell/js.cpp:14439-14440). The checker is only called from the assertJitStackInvariants testing function (js/src/builtin/TestingFunctions.cpp:5713-5717).
Exploit Chain
- Start the JS shell with --emit-interpreter-entry so interpreter and baseline-interpreter calls go through the BaselineInterpreterEntry trampoline.
- Create a generator, run it to its first yield, then resume it with a second next() call; the trampoline copies |this|, the formals and the four ResumeFrameArgs Values into the new frame.
- Inside the resumed generator body call assertJitStackInvariants(); AssertJitStackInvariants walks the JIT stack and reaches the resumed frame whose caller type is BaselineInterpreterEntry.
- The checker computes expectedFrameSize without the resume args, so
expectedFrameSize + JitStackAlignment > frameSizeis false and MOZ_RELEASE_ASSERT aborts the shell.
Steps to Reproduce
- Build the SpiderMonkey debug shell from an unmodified tree with /app/mozconfigs/mozconfig.js.linux.debug.
- Run testcase.js with js_shell_evaluator using js_binary=/firefox/obj-js-debug/dist/bin/js and flags=["--emit-interpreter-entry"] (the evaluator adds --fuzzing-safe). Testcase: function* g() { yield 1; assertJitStackInvariants(); } for (var i = 0; i < 50; i++) { var it = g(); it.next(); it.next(); }
- Observe
Assertion failure: expectedFrameSize + JitStackAlignment > frameSize (The frame size is optimal), at js/src/jit/JitFrames.cpp:2871. - Control: the same testcase without --emit-interpreter-entry exits 0.
Security Impact
- Severity: Low
- Attacker capability: Deterministic release-assertion abort of the JS shell. No out-of-bounds access, use-after-free or other memory corruption was demonstrated; the evidence indicates the checker's expected-size formula is stale rather than the frame being malformed.
- Preconditions: The shell must be run with the non-default --emit-interpreter-entry option (default is off unless IONPERF is set, js/src/jit/JitOptions.cpp:156-157), and the script must call the assertJitStackInvariants() shell testing function from a resumed generator frame.
ASAN Report
[2814] Assertion failure: expectedFrameSize + JitStackAlignment > frameSize (The frame size is optimal), at js/src/jit/JitFrames.cpp:2871
#01: ???[/firefox/obj-js-debug/dist/bin/js +0x3b7020e]
#02: ???[/firefox/obj-js-debug/dist/bin/js +0x2add1bd]
#03: ???[/firefox/obj-js-debug/dist/bin/js +0x3d2bc95]
#04: ???[/firefox/obj-js-debug/dist/bin/js +0x3d086a8]
#05: ???[/firefox/obj-js-debug/dist/bin/js +0x324c8da]
#06: ??? (???:???)
UndefinedBehaviorSanitizer:DEADLYSIGNAL
==2814==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x000000000000 (pc 0x5599f3919226 bp 0x7ee20ded15f0 sp 0x7ee20ded1500 T2814)
==2814==The signal is caused by a READ memory access.
==2814==Hint: address points to the zero page.
#0 0x5599f3919226 in MOZ_CrashSequence(void*, long) /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:261:3
#1 0x5599f3919226 in js::jit::AssertJitStackInvariants(JSContext*) /firefox/obj-js-debug/js/src/jit/./../../../../js/src/jit/JitFrames.cpp:2870:11
#2 0x5599f28861bc in TestingFunc_assertJitStackInvariants(JSContext*, unsigned int, JS::Value*) /firefox/obj-js-debug/js/src/builtin/./../../../../js/src/builtin/TestingFunctions.cpp:5717:3
#3 0x5599f3ad4c94 in CallJSNative(JSContext*, bool (*)(JSContext*, unsigned int, JS::Value*), js::CallReason, JS::CallArgs const&) /firefox/js/src/vm/Interpreter.cpp:528:13
#4 0x5599f3ab16a7 in js::InternalCallOrConstruct(JSContext*, JS::CallArgs const&, js::MaybeConstruct, js::CallReason) /firefox/js/src/vm/Interpreter.cpp:624:12
#5 0x5599f2ff58d9 in js::jit::DoCallFallback(JSContext*, js::jit::BaselineFrame*, js::jit::ICFallbackStub*, unsigned int, JS::Value*, JS::MutableHandle<JS::Value>) /firefox/obj-js-debug/js/src/jit/./../../../../js/src/jit/BaselineIC.cpp:1717:10
#6 0x0edb39b6500e ([anon:js-executable-memory]+0x1c00e)
==2814==Register values:
rax = 0x0000000000000000 rbx = 0x00007ee20ded1b40 rcx = 0x0000000000000b37 rdx = 0x00007eef43e10563
rdi = 0x00007eef43e11700 rsi = 0x0000000000000000 rbp = 0x00007ee20ded15f0 rsp = 0x00007ee20ded1500
r8 = 0x0000000000000000 r9 = 0x0000000000000003 r10 = 0x0000000000000002 r11 = 0x0000000000000000
r12 = 0x00007ee20ded1b40 r13 = 0x0000000000000050 r14 = 0x0000000000000038 r15 = 0x0000000000000000
UndefinedBehaviorSanitizer can not provide additional info.
SUMMARY: UndefinedBehaviorSanitizer: SEGV /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:261:3 in MOZ_CrashSequence(void*, long)
==2814==ABORTING
| Reporter | ||
Comment 1•1 month ago
|
||
| Reporter | ||
Comment 2•1 month ago
|
||
| Reporter | ||
Comment 3•1 month ago
|
||
| Reporter | ||
Comment 4•1 month ago
|
||
| Reporter | ||
Comment 5•1 month ago
|
||
| Reporter | ||
Comment 6•1 month ago
|
||
This bug was filed automatically by an AI-powered security testing tool. The analysis, testcase, and any suggested patches were generated by an AI model and have NOT been verified by a human.
Please review all details carefully before triaging. The issue was confirmed by an independent verifier, but for non-ASAN issues, the verifier has been made less strict. Additionally, the root cause analysis and suggested fix may contain errors.
Updated•1 month ago
|
Updated•1 month ago
|
Updated•1 month ago
|
Description
•