Assertion failure: blFrame->numValueSlots() <= script->nslots(), at js/src/jit/BaselineBailouts.cpp:1011 with Debugger and OOM

VERIFIED FIXED in Firefox 58

Status

()

defect
P1
critical
VERIFIED FIXED
2 years ago
Last year

People

(Reporter: decoder, Assigned: nbp)

Tracking

(Blocks 2 bugs, 4 keywords)

Trunk
mozilla59
x86_64
Linux
Points:
---
Dependency tree / graph

Firefox Tracking Flags

(firefox-esr52 unaffected, firefox56 unaffected, firefox57 wontfix, firefox58+ verified, firefox59+ verified)

Details

(Whiteboard: [jsbugmon:update][adv-main58+])

Attachments

(4 attachments)

The following testcase crashes on mozilla-central revision c16bc8097c10 (build with --enable-posix-nspr-emulation --enable-valgrind --enable-gczeal --disable-tests --enable-stdcxx-compat --disable-profiling --enable-debug --enable-optimize, run with --fuzzing-safe):

g = newGlobal();
g.parent = this;
g.eval("new Debugger(parent).onExceptionUnwind = function () {}");
function pushLimits(limit, offset) {
    function pusher() {
        Array.prototype.push.apply(arr, arguments)
    }
    var arr = [0, 1, 2, 3, 4, 5, 6, 7];
    oomAtAllocation(limit);
    for (var i = 0; i < 100; i++) pusher(0, 1, 2, 3, 4, 5, 6, 7);
}
var limit = 1024 * 1024 * 1024;
while (true) {
    var res = pushLimits(limit, 0);
    limit = (limit / 1.5) | 0;
}


Backtrace:

received signal SIGSEGV, Segmentation fault.
#0  0x0000000000efacf5 in InitFromBailout (caller=..., ionScript=<optimized out>, excInfo=0x7fffffffc0e0, callPC=<synthetic pointer>, nextCallee=..., startFrameFormals=..., builder=..., invalidate=true, iter=..., script=..., fun=..., callerPC=<optimized out>, cx=0x7ffff6948000) at js/src/jit/BaselineBailouts.cpp:1011
#1  js::jit::BailoutIonToBaseline (cx=cx@entry=0x7ffff6948000, activation=0x7fffffffc930, iter=..., invalidate=invalidate@entry=true, bailoutInfo=bailoutInfo@entry=0x7fffffffbbe8, excInfo=excInfo@entry=0x7fffffffc0e0) at js/src/jit/BaselineBailouts.cpp:1656
#2  0x0000000000efcb51 in js::jit::ExceptionHandlerBailout (cx=cx@entry=0x7ffff6948000, frame=..., rfe=rfe@entry=0x7fffffffc62c, excInfo=..., overrecursed=overrecursed@entry=0x7fffffffbf6f) at js/src/jit/Bailouts.cpp:218
#3  0x000000000077e4e1 in js::jit::HandleExceptionIon (overrecursed=0x7fffffffbf6f, rfe=0x7fffffffc62c, frame=..., cx=0x7ffff6948000) at js/src/jit/JitFrames.cpp:204
#4  js::jit::HandleException (rfe=<optimized out>) at js/src/jit/JitFrames.cpp:673
#5  0x000010cc1f39f676 in ?? ()
#6  0x0000000000000000 in ?? ()
rax	0x0	0
rbx	0x90	144
rcx	0x7ffff6c28a2d	140737333332525
rdx	0x0	0
rsi	0x7ffff6ef7770	140737336276848
rdi	0x7ffff6ef6540	140737336272192
rbp	0x7fffffffbb90	140737488337808
rsp	0x7fffffffb690	140737488336528
r8	0x7ffff6ef7770	140737336276848
r9	0x7ffff7fe4740	140737354024768
r10	0x58	88
r11	0x7ffff6b9f750	140737332770640
r12	0xa	10
r13	0x7fffffffb890	140737488337040
r14	0x7fffffffc0e0	140737488339168
r15	0x7ffff6946180	140737330307456
rip	0xefacf5 <js::jit::BailoutIonToBaseline(JSContext*, js::jit::JitActivation*, js::jit::JSJitFrameIter const&, bool, js::jit::BaselineBailoutInfo**, js::jit::ExceptionBailoutInfo const*)+11861>
=> 0xefacf5 <js::jit::BailoutIonToBaseline(JSContext*, js::jit::JitActivation*, js::jit::JSJitFrameIter const&, bool, js::jit::BaselineBailoutInfo**, js::jit::ExceptionBailoutInfo const*)+11861>:	movl   $0x0,0x0
   0xefad00 <js::jit::BailoutIonToBaseline(JSContext*, js::jit::JitActivation*, js::jit::JSJitFrameIter const&, bool, js::jit::BaselineBailoutInfo**, js::jit::ExceptionBailoutInfo const*)+11872>:	ud2


I'm marking this s-s until investigated because the assertion sounds quite dangerous. The test uses Debugger, so it is at most sec-moderate if the use of the Debugger is required. But I could imagine that there might be other ways to cause this bailout, so I'll leave this to the JS devs to decide.
Whiteboard: [jsbugmon:update,bisect] → [jsbugmon:update]
JSBugMon: Bisection requested, result:
autoBisect shows this is probably related to the following changeset:

The first bad revision is:
changeset:   https://hg.mozilla.org/mozilla-central/rev/8b1881ead0b6
user:        Nicolas B. Pierron
date:        Thu Sep 07 13:01:13 2017 +0000
summary:     Bug 966743 - Inline Array.prototype.push with more than one argument. r=jandem

This iteration took 267.413 seconds to run.
Flags: needinfo?(nicolas.b.pierron)
Priority: -- → P1
Calling it sec-high out of caution because probable regressor had nothing to do with the Debugger code, and jorendorff was worried enough to make this a P1.
Blocks: 966743
Keywords: sec-high
I am able to reproduce this issue, I will investigate it more on Monday.
Flags: needinfo?(nicolas.b.pierron)
(In reply to Christian Holler (:decoder) from comment #0)
>     function pusher() {
>         Array.prototype.push.apply(arr, arguments)
>     }

The problem is here, and suppose that we OOM or freeze of the array "arr", which cause the array.push to fail and bailout, then we would be restoring twice the set of arguments, as part of the bailout frame.

I am not completely sure of the security implication of this modification, but I am not confident that the Debugger is mandatory for creating a similar issue.
This patch would be good if we need to backport a fix, and if I cannot find
a better solution in the mean time.

This restores the behaviour that we had before Bug 966743.
Attachment #8926865 - Flags: review?(jdemooij)
Assignee: nobody → nicolas.b.pierron
Attachment #8926865 - Flags: review?(jdemooij) → review+
It's too late for 57, wontfix.
This adds the fuzzer's test case in addition to a copy of the existing
fun.apply test case for fun.call, just to ensure that we can bailout
correctly as well with fun.call.
Attachment #8928198 - Flags: review?(jdemooij)
This patch rename pushFormals to pushCallStack and pushPriorCallStack.

 - pushCallStack is used for filling the content of the Outer resume
   points. This content is expected to explicitly list of the arguments,
   such that the call-frame can be emulated in Baseline.

 - pushPriorCallStack is used for filling the content of the stack for At
   resume points. This content is expected to be used for resuming at the
   call instruction, with enough content to resume the execution properly.

The problem seen here happens when we reconstruct the baseline frame under
HandleException (when resizing the array?). From this point of view I would
not think that this issue is restricted to the Debugger.

The problem seen here, is that the resume point contained the full set of
arguments instead of the state of the stack prior JSOP_FUNAPPLY.  Thus, the
frame recovery which attempted to reconstruct the stack certainly gets it
wrong.

This patch adds machinery to capture the stack, if needed, such that it can
be restored properly as-is. It defaults to restoring the set of call-stack
definition if no state were captured.  Thus JSOP_FUNCALL get one argument less
when restored (could that cause Debugger issues?).

This should solve the issue, as we restore a proper stack where the
execution can be resumed, and satisfies the bailout assertion raised under
HandleException.
Attachment #8928206 - Flags: review?(jdemooij)
Comment on attachment 8928198 [details] [diff] [review]
Update test cases to cover all code paths.

Review of attachment 8928198 [details] [diff] [review]:
-----------------------------------------------------------------

We will land the tests once this is in release, right?
Attachment #8928198 - Flags: review?(jdemooij) → review+
Comment on attachment 8928206 [details] [diff] [review]
Distinguish call stack used for outer resume points than call stacks used for resuming at one instruction.

Review of attachment 8928206 [details] [diff] [review]:
-----------------------------------------------------------------

Sorry for the delay; I thought this was more complicated but the patch makes sense.

::: js/src/jit/IonBuilder.h
@@ +1255,5 @@
>      }
>  
> +    // Before doing any pop to the stack, capture whatever flows into the
> +    // instruction, such that we can restore it later.
> +    AbortReasonOr<Ok> savePriorCallStack(MIRGenerator* mir, MBasicBlock* current, size_t peekDepth)

Nit: we should define this in IonBuilder.cpp since it's pretty big and inlining it doesn't matter (and the caller is in IonBuilder.cpp too).

@@ +1261,5 @@
> +        MOZ_ASSERT(priorArgs_.empty());
> +        if (!priorArgs_.reserve(peekDepth))
> +            return mir->abort(AbortReason::Alloc);
> +        while (peekDepth) {
> +            priorArgs_.infallibleAppend(current->peek(0 - peekDepth));

Nit: int32_t(peekDepth) to make the unsigned-to-signed conversion explicit and (maybe) to keep some compilers from complaining.
Attachment #8928206 - Flags: review?(jdemooij) → review+
Comment on attachment 8928206 [details] [diff] [review]
Distinguish call stack used for outer resume points than call stacks used for resuming at one instruction.

> [Security approval request comment]
> How easily could an exploit be constructed based on the patch?

I tried to do it without the Debugger, and failed to make something crash.  Still, I think this is possible, as this MArrayPush is made to bailout, and resume its execution in Baseline, thus resuming with a corrupted JS stack, and failing again.

> Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?

Yes, based on the patch it is obvious that the way the stack is recovered on a bailout is different in case of MArrayPush with multiple arguments.

> Which older supported branches are affected by this flaw?

Firefox 57, and newer.

> If not all supported branches, which bug introduced the flaw?

Bug 966743

> Do you have backports for the affected branches? If not, how different, hard to create, and risky will they be?

The same patch should apply cleanly.

> How likely is this patch to cause regressions; how much testing does it need?

Not likely. The test added in the previous patch, and the existing test should be enough.
Attachment #8928206 - Flags: sec-approval?
Comment on attachment 8928206 [details] [diff] [review]
Distinguish call stack used for outer resume points than call stacks used for resuming at one instruction.

sec-approval+ for trunk. If you nominate it for beta, I can approve it as well so we fix it everywhere affected.
Attachment #8928206 - Flags: sec-approval? → sec-approval+
Comment on attachment 8928206 [details] [diff] [review]
Distinguish call stack used for outer resume points than call stacks used for resuming at one instruction.

Landed, with fixup patch:
https://hg.mozilla.org/integration/mozilla-inbound/rev/6596b8c168f5
https://hg.mozilla.org/integration/mozilla-inbound/rev/43ccb7de1afe
Attachment #8928206 - Flags: checkin+
Status: RESOLVED → VERIFIED
JSBugMon: This bug has been automatically verified fixed.
This needs rebasing for Beta uplift. Please post a rebased patch and nominate it for approval when you get a chance.
Flags: needinfo?(nicolas.b.pierron)
Comment on attachment 8936854 [details] [diff] [review]
Distinguish between call stack used for outer resume points from call stacks used for resuming at one instruction.

Approval Request Comment
[Feature/Bug causing the regression]: Bug 966743
[User impact if declined]: Potential crash, and misuse of pointers.
[Is this code covered by automated tests?]: (new & old test cases works)
[Has the fix been verified in Nightly?]: Yes.
[Needs manual test from QE? If yes, steps to reproduce]: no.
[List of other uplifts needed for the feature/fix]: none.
[Is the change risky?]: no.
[Why is the change risky/not risky?]: this patch mostly rename a function, and changes the behaviour of one to match the expected resume state.
[String changes made/needed]: none.
Flags: needinfo?(nicolas.b.pierron)
Attachment #8936854 - Flags: approval-mozilla-beta?
Group: javascript-core-security → core-security-release
Comment on attachment 8936854 [details] [diff] [review]
Distinguish between call stack used for outer resume points from call stacks used for resuming at one instruction.

Sec-high, Beta58+
Attachment #8936854 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
Whiteboard: [jsbugmon:update] → [jsbugmon:update][adv-main58+]
JSBugMon: This bug has been automatically verified fixed on Fx58
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.