Open Bug 2078744 Opened 1 month ago Updated 1 month ago

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)

defect

Tracking

()

UNCONFIRMED

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

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

  1. Start the JS shell with --emit-interpreter-entry so interpreter and baseline-interpreter calls go through the BaselineInterpreterEntry trampoline.
  2. 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.
  3. Inside the resumed generator body call assertJitStackInvariants(); AssertJitStackInvariants walks the JIT stack and reaches the resumed frame whose caller type is BaselineInterpreterEntry.
  4. The checker computes expectedFrameSize without the resume args, so expectedFrameSize + JitStackAlignment > frameSize is false and MOZ_RELEASE_ASSERT aborts the shell.

Steps to Reproduce

  1. Build the SpiderMonkey debug shell from an unmodified tree with /app/mozconfigs/mozconfig.js.linux.debug.
  2. 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(); }
  3. Observe Assertion failure: expectedFrameSize + JitStackAlignment > frameSize (The frame size is optimal), at js/src/jit/JitFrames.cpp:2871.
  4. 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
Attached patch fix.patch — — Splinter Review
Attached file crash_stack.txt —
Attached file testcase.js —
Attached file README.md —
Attached file flags.txt —

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.

Group: core-security → javascript-core-security
Blocks: sm-security
Severity: -- → S3
Priority: -- → P3
No longer blocks: sm-security
Group: javascript-core-security
Keywords: sec-other
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: