Release assertion failure in [@ js::jit::AssertJitStackInvariants] due to argument overapplication through BaselineInterpreterEntry
Categories
(Core :: JavaScript Engine: JIT, defect, P2)
Tracking
()
People
(Reporter: bugmon, Assigned: iain)
References
(Blocks 1 open bug)
Details
(4 keywords, Whiteboard: [adv-main152-])
Attachments
(3 files, 1 external file)
|
188 bytes,
text/plain
|
Details | |
|
1.18 KB,
patch
|
Details | Diff | Splinter Review | |
|
1.35 KB,
text/plain
|
Details |
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
- Branch: main
- Revision: 07e27bfce5acf193bdb89c7ba2aa73451a3e43b4
- Timestamp: 2026-04-07T18:06:29+00:00
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
- Run the JS shell with --emit-interpreter-entry --blinterp-eager so baseline-interpreter entry trampolines are used for scripted calls.
- Define a function target(a) that takes 1 formal argument and have it call the testing builtin assertJitStackInvariants().
- 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.
- 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.
- 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
| Reporter | ||
Comment 1•7 months ago
|
||
| Reporter | ||
Comment 2•7 months ago
|
||
| Reporter | ||
Comment 3•7 months ago
|
||
Updated•7 months ago
|
Updated•7 months ago
|
| Assignee | ||
Comment 5•6 months ago
|
||
Updated•6 months ago
|
Comment 7•6 months ago
|
||
Updated•6 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Updated•1 month ago
|
Description
•