Closed Bug 2030562 Opened 7 months ago Closed 6 months ago

Release assertion failure in [@ js::jit::AssertJitStackInvariants] due to argument overapplication through BaselineInterpreterEntry

Categories

(Core :: JavaScript Engine: JIT, defect, P2)

defect

Tracking

()

RESOLVED FIXED
152 Branch
Tracking Status
firefox-esr115 --- wontfix
firefox-esr140 --- wontfix
firefox150 --- wontfix
firefox151 --- wontfix
firefox152 --- fixed

People

(Reporter: bugmon, Assigned: iain)

References

(Blocks 1 open bug)

Details

(4 keywords, Whiteboard: [adv-main152-])

Attachments

(3 files, 1 external file)

js::jit::AssertJitStackInvariants in js/src/jit/JitFrames.cpp contains a MOZ_RELEASE_ASSERT that claims a scripted frame reached via a BaselineInterpreterEntry trampoline must have a size within one JitStackAlignment unit of an 'expected' size computed from the callee's formal argument count (nargs()). This formula is incorrect: when a caller overapplies arguments (passes more actuals than the callee's declared formals), the pushed frame legitimately contains numActualArgs slots rather than nargs slots, so the real frame size exceeds expectedFrameSize + JitStackAlignment and the release assertion fires. The invariant checker walks the stack via JSJitFrameIter which correctly reports the oversized frame; the bug is in the checker's formula, not in the stack layout. Because the assertion is MOZ_RELEASE_ASSERT, it aborts non-debug builds as well, turning a testable invariant violation into a crash whenever AssertJitStackInvariants runs over such a frame.

Build Info

Affected Code

File: js/src/jit/JitFrames.cpp, line 2722-2737

if (frames.isScripted() &&
    frames.prevType() == FrameType::BaselineInterpreterEntry) {
  MOZ_RELEASE_ASSERT(
      frameSize % JitStackAlignment == 0,
      "The blinterp entry frame should keep the alignment");

  size_t expectedFrameSize =
      sizeof(Value) *
          (frames.callee()->nargs() + 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");
}

expectedFrameSize uses the formal parameter count (nargs()) without considering numActualArgs(). For an overapplied call (e.g. target(a) called with 12 actuals), the real frame contains all 12 actual argument Values plus |this| plus JitFrameLayout, exceeding expectedFrameSize + JitStackAlignment and tripping the third MOZ_RELEASE_ASSERT.

Exploit Chain

  1. Run the JS shell with --emit-interpreter-entry --blinterp-eager so baseline-interpreter entry trampolines are used for scripted calls.
  2. Define a function target(a) that takes 1 formal argument and have it call the testing builtin assertJitStackInvariants().
  3. Call target() from another scripted function passing many more actuals than formals (e.g. 12 arguments), so the pushed frame contains 12 Value slots for args rather than 1.
  4. Loop enough times for baseline interpreter entry to be used, so that at the time of assertJitStackInvariants() the live JSJitFrameIter sees a scripted frame whose prevType() == FrameType::BaselineInterpreterEntry.
  5. AssertJitStackInvariants computes expectedFrameSize from callee()->nargs() (= 1) but the actual frame holds 12 argument slots, so frameSize > expectedFrameSize + JitStackAlignment and the MOZ_RELEASE_ASSERT at JitFrames.cpp:2736 fires, aborting the process.

Security Impact

  • Severity: Low
  • Attacker capability: Triggers an engine-invariant release assertion (process abort) when AssertJitStackInvariants runs over a baseline-interpreter-entry scripted frame produced by an overapplied call. In the shipping browser the entry point (assertJitStackInvariants) is a testing-only builtin not exposed to web content, so direct reachability is limited to the JS shell and fuzzing; however the invariant formula itself is demonstrably wrong, indicating a latent mismatch between the checker's assumptions and how argument overapplication materializes frames through BaselineInterpreterEntry.
  • Preconditions: JS shell (or any consumer that exposes assertJitStackInvariants) with --emit-interpreter-entry --blinterp-eager; a scripted call that overapplies arguments so numActualArgs() > callee()->nargs(); the call must reach baseline through a BaselineInterpreterEntry trampoline.

ASAN Report

Assertion failure: expectedFrameSize + JitStackAlignment > frameSize (The frame size is optimal), at /firefox/js/src/jit/JitFrames.cpp:2736
#0 MOZ_CrashSequence
#1 js::jit::AssertJitStackInvariants(JSContext*) at JitFrames.cpp:2735
#2 TestingFunc_assertJitStackInvariants at TestingFunctions.cpp:5582
#3 CallJSNative at Interpreter.cpp:488
#4 js::InternalCallOrConstruct at Interpreter.cpp:584
#5 js::jit::DoCallFallback at BaselineIC.cpp:1699
Attached file tmps3g44ov9.js —
Attached patch fix.patch — — Splinter Review
Attached file crash_stack.txt —
Group: core-security → javascript-core-security
Blocks: sm-security
Severity: -- → S3
Priority: -- → P2
Attached file (secure) —
Assignee: nobody → iireland
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true
Group: javascript-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 6 months ago
Resolution: --- → FIXED
Target Milestone: --- → 152 Branch
QA Whiteboard: [sec] [qa-triage-done-c153/b152]
Whiteboard: [adv-main-152-]
Whiteboard: [adv-main-152-] → [adv-main152-]
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: