diff --git a/dom/bindings/Exceptions.cpp b/dom/bindings/Exceptions.cpp index 99da0a23624e..5dee9623ca2c 100644 --- a/dom/bindings/Exceptions.cpp +++ b/dom/bindings/Exceptions.cpp @@ -673,7 +673,10 @@ CreateStack(JSContext* aCx, int32_t aMaxDepth) } JS::Rooted stack(aCx); - if (!JS::CaptureCurrentStack(aCx, &stack, aMaxDepth)) { + JS::StackCapture capture = aMaxDepth == 0 + ? JS::StackCapture(JS::AllFrames()) + : JS::StackCapture(JS::MaxFrames(aMaxDepth)); + if (!JS::CaptureCurrentStack(aCx, &stack, mozilla::Move(capture))) { return nullptr; } diff --git a/js/src/builtin/Promise.cpp b/js/src/builtin/Promise.cpp index 2062a4873b9c..558fd7ba72d6 100644 --- a/js/src/builtin/Promise.cpp +++ b/js/src/builtin/Promise.cpp @@ -135,7 +135,7 @@ PromiseObject::create(JSContext* cx, HandleObject executor, HandleObject proto / // but oh well. RootedObject stack(cx); if (cx->options().asyncStack() || cx->compartment()->isDebuggee()) { - if (!JS::CaptureCurrentStack(cx, &stack, 0)) + if (!JS::CaptureCurrentStack(cx, &stack, JS::StackCapture(JS::AllFrames()))) return nullptr; } promise->setFixedSlot(PROMISE_ALLOCATION_SITE_SLOT, ObjectOrNullValue(stack)); @@ -425,7 +425,7 @@ PromiseObject::onSettled(JSContext* cx) Rooted promise(cx, this); RootedObject stack(cx); if (cx->options().asyncStack() || cx->compartment()->isDebuggee()) { - if (!JS::CaptureCurrentStack(cx, &stack, 0)) { + if (!JS::CaptureCurrentStack(cx, &stack, JS::StackCapture(JS::AllFrames()))) { cx->clearPendingException(); return; } diff --git a/js/src/builtin/TestingFunctions.cpp b/js/src/builtin/TestingFunctions.cpp index 8c6b826ba532..371d4949d6c8 100644 --- a/js/src/builtin/TestingFunctions.cpp +++ b/js/src/builtin/TestingFunctions.cpp @@ -1103,7 +1103,7 @@ SaveStack(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); - unsigned maxFrameCount = 0; + JS::StackCapture capture((JS::AllFrames())); if (args.length() >= 1) { double d; if (!ToNumber(cx, args[0], &d)) @@ -1114,7 +1114,8 @@ SaveStack(JSContext* cx, unsigned argc, Value* vp) "not a valid maximum frame count", NULL); return false; } - maxFrameCount = d; + if (d > 0) + capture = JS::StackCapture(JS::MaxFrames(d)); } JSCompartment* targetCompartment = cx->compartment(); @@ -1134,7 +1135,7 @@ SaveStack(JSContext* cx, unsigned argc, Value* vp) RootedObject stack(cx); { AutoCompartment ac(cx, targetCompartment); - if (!JS::CaptureCurrentStack(cx, &stack, maxFrameCount)) + if (!JS::CaptureCurrentStack(cx, &stack, mozilla::Move(capture))) return false; } @@ -1145,6 +1146,37 @@ SaveStack(JSContext* cx, unsigned argc, Value* vp) return true; } +static bool +CaptureFirstSubsumedFrame(JSContext* cx, unsigned argc, JS::Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "captureFirstSubsumedFrame", 1)) + return false; + + if (!args[0].isObject()) { + JS_ReportError(cx, "The argument must be an object"); + return false; + } + + RootedObject obj(cx, &args[0].toObject()); + obj = CheckedUnwrap(obj); + if (!obj) { + JS_ReportError(cx, "Denied permission to object."); + return false; + } + + JS::StackCapture capture(JS::FirstSubsumedFrame(cx, obj->compartment()->principals())); + if (args.length() > 1) + capture.as().ignoreSelfHosted = JS::ToBoolean(args[1]); + + JS::RootedObject capturedStack(cx); + if (!JS::CaptureCurrentStack(cx, &capturedStack, mozilla::Move(capture))) + return false; + + args.rval().setObjectOrNull(capturedStack); + return true; +} + static bool CallFunctionFromNativeFrame(JSContext* cx, unsigned argc, Value* vp) { @@ -3603,6 +3635,12 @@ static const JSFunctionSpecWithHelp TestingFunctions[] = { " of frames. If 'compartment' is given, allocate the js::SavedFrame instances\n" " with the given object's compartment."), + JS_FN_HELP("captureFirstSubsumedFrame", CaptureFirstSubsumedFrame, 1, 0, +"saveStack(object [, shouldIgnoreSelfHosted = true]])", +" Capture a stack back to the first frame whose principals are subsumed by the\n" +" object's compartment's principals. If 'shouldIgnoreSelfHosted' is given,\n" +" control whether self-hosted frames are considered when checking principals."), + JS_FN_HELP("callFunctionFromNativeFrame", CallFunctionFromNativeFrame, 1, 0, "callFunctionFromNativeFrame(function)", " Call 'function' with a (C++-)native frame on stack.\n" diff --git a/js/src/jit-test/tests/saved-stacks/capture-first-frame-with-principals.js b/js/src/jit-test/tests/saved-stacks/capture-first-frame-with-principals.js new file mode 100644 index 000000000000..6b126cb09866 --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/capture-first-frame-with-principals.js @@ -0,0 +1,92 @@ +// Create two different globals whose compartments have two different +// principals. Test getting the first frame on the stack with some given +// principals in various configurations of JS stack and of wanting self-hosted +// frames or not. + +const g1 = newGlobal({ + principal: 0xffff +}); + +const g2 = newGlobal({ + principal: 0xff +}); + +// Introduce everyone to themselves and each other. +g1.g2 = g2.g2 = g2; +g1.g1 = g2.g1 = g1; + +g1.g2obj = g2.eval("new Object"); + +g1.evaluate(` + const global = this; + + // Capture the stack back to the first frame in the g2 global. + function capture(shouldIgnoreSelfHosted = true) { + return captureFirstSubsumedFrame(global.g2obj, shouldIgnoreSelfHosted); + } +`, { + fileName: "script1.js" +}); + +g2.evaluate(` + const capture = g1.capture; + + // Use our Function.prototype.bind, not capture.bind (which is === + // g1.Function.prototype.bind) so that the generated bound function is in our + // compartment and has our principals. + const boundTrue = Function.prototype.bind.call(capture, null, true); + const boundFalse = Function.prototype.bind.call(capture, null, false); + + function getOldestFrame(stack) { + while (stack.parent) { + stack = stack.parent; + } + return stack; + } + + function dumpStack(name, stack) { + print("Stack " + name + " ="); + while (stack) { + print(" " + stack.functionDisplayName + " @ " + stack.source); + stack = stack.parent; + } + print(); + } + + // When the youngest frame is not self-hosted, it doesn't matter whether or not + // we specify that we should ignore self hosted frames when capturing the first + // frame with the given principals. + // + // Stack: iife1 (g2) <- capture (g1) + + (function iife1() { + const captureTrueStack = capture(true); + dumpStack("captureTrueStack", captureTrueStack); + assertEq(getOldestFrame(captureTrueStack).functionDisplayName, "iife1"); + assertEq(getOldestFrame(captureTrueStack).source, "script2.js"); + + const captureFalseStack = capture(false); + dumpStack("captureFalseStack", captureFalseStack); + assertEq(getOldestFrame(captureFalseStack).functionDisplayName, "iife1"); + assertEq(getOldestFrame(captureFalseStack).source, "script2.js"); + }()); + + // When the youngest frame is a self hosted frame, we get two different + // captured stacks depending on whether or not we ignore self-hosted frames. + // + // Stack: iife2 (g2) <- bound function (g2) <- capture (g1) + + (function iife2() { + const boundTrueStack = boundTrue(); + dumpStack("boundTrueStack", boundTrueStack); + assertEq(getOldestFrame(boundTrueStack).functionDisplayName, "iife2"); + assertEq(getOldestFrame(boundTrueStack).source, "script2.js"); + + const boundFalseStack = boundFalse(); + dumpStack("boundFalseStack", boundFalseStack); + assertEq(getOldestFrame(boundFalseStack).functionDisplayName !== "iife2", true); + assertEq(getOldestFrame(boundFalseStack).source, "self-hosted"); + }()); +`, { + fileName: "script2.js" +}); diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp index 75f3f4ca0b59..3cb2b52f2523 100644 --- a/js/src/jsapi.cpp +++ b/js/src/jsapi.cpp @@ -6536,8 +6536,14 @@ JS::SetOutOfMemoryCallback(JSContext* cx, OutOfMemoryCallback cb, void* data) cx->oomCallbackData = data; } +JS::FirstSubsumedFrame::FirstSubsumedFrame(JSContext* cx, + bool ignoreSelfHostedFrames /* = true */) + : JS::FirstSubsumedFrame(cx, cx->compartment()->principals(), ignoreSelfHostedFrames) +{ } + JS_PUBLIC_API(bool) -JS::CaptureCurrentStack(JSContext* cx, JS::MutableHandleObject stackp, unsigned maxFrameCount) +JS::CaptureCurrentStack(JSContext* cx, JS::MutableHandleObject stackp, + JS::StackCapture&& capture /* = JS::StackCapture(JS::AllFrames()) */) { AssertHeapIsIdle(cx); CHECK_REQUEST(cx); @@ -6545,7 +6551,7 @@ JS::CaptureCurrentStack(JSContext* cx, JS::MutableHandleObject stackp, unsigned JSCompartment* compartment = cx->compartment(); Rooted frame(cx); - if (!compartment->savedStacks().saveCurrentStack(cx, &frame, maxFrameCount)) + if (!compartment->savedStacks().saveCurrentStack(cx, &frame, mozilla::Move(capture))) return false; stackp.set(frame.get()); return true; diff --git a/js/src/jsapi.h b/js/src/jsapi.h index 8082759c71c7..6f39b0727fb9 100644 --- a/js/src/jsapi.h +++ b/js/src/jsapi.h @@ -5896,14 +5896,96 @@ typedef void extern JS_PUBLIC_API(void) SetOutOfMemoryCallback(JSContext* cx, OutOfMemoryCallback cb, void* data); +/** + * Capture all frames. + */ +struct AllFrames { }; + +/** + * Capture at most this many frames. + */ +struct MaxFrames +{ + unsigned maxFrames; + + explicit MaxFrames(unsigned max) + : maxFrames(max) + { + MOZ_ASSERT(max > 0); + } +}; + +/** + * Capture the first frame with the given principals. By default, do not + * consider self-hosted frames with the given principals as satisfying the stack + * capture. + */ +struct FirstSubsumedFrame +{ + JSContext* cx; + JSPrincipals* principals; + bool ignoreSelfHosted; + + /** + * Use the cx's current compartment's principals. + */ + explicit FirstSubsumedFrame(JSContext* cx, bool ignoreSelfHostedFrames = true); + + explicit FirstSubsumedFrame(JSContext* ctx, JSPrincipals* p, bool ignoreSelfHostedFrames = true) + : cx(ctx) + , principals(p) + , ignoreSelfHosted(ignoreSelfHostedFrames) + { + JS_HoldPrincipals(principals); + } + + // No copying because we want to avoid holding and dropping principals + // unnecessarily. + FirstSubsumedFrame(const FirstSubsumedFrame&) = delete; + FirstSubsumedFrame& operator=(const FirstSubsumedFrame&) = delete; + + FirstSubsumedFrame(FirstSubsumedFrame&& rhs) + : principals(rhs.principals) + , ignoreSelfHosted(rhs.ignoreSelfHosted) + { + MOZ_ASSERT(this != &rhs, "self move disallowed"); + rhs.principals = nullptr; + } + + FirstSubsumedFrame& operator=(FirstSubsumedFrame&& rhs) { + new (this) FirstSubsumedFrame(mozilla::Move(rhs)); + return *this; + } + + ~FirstSubsumedFrame() { + if (principals) + JS_DropPrincipals(cx, principals); + } +}; + +using StackCapture = mozilla::Variant; + /** * Capture the current call stack as a chain of SavedFrame JSObjects, and set * |stackp| to the SavedFrame for the youngest stack frame, or nullptr if there - * are no JS frames on the stack. If |maxFrameCount| is non-zero, capture at - * most the youngest |maxFrameCount| frames. + * are no JS frames on the stack. + * + * The |capture| parameter describes the portion of the JS stack to capture: + * + * * |JS::AllFrames|: Capture all frames on the stack. + * + * * |JS::MaxFrames|: Capture no more than |JS::MaxFrames::maxFrames| from the + * stack. + * + * * |JS::FirstSubsumedFrame|: Capture the first frame whose principals are + * subsumed by |JS::FirstSubsumedFrame::principals|. By default, do not + * consider self-hosted frames; this can be controlled via the + * |JS::FirstSubsumedFrame::ignoreSelfHosted| flag. Do not capture any async + * stack. */ extern JS_PUBLIC_API(bool) -CaptureCurrentStack(JSContext* cx, MutableHandleObject stackp, unsigned maxFrameCount = 0); +CaptureCurrentStack(JSContext* cx, MutableHandleObject stackp, + StackCapture&& capture = StackCapture(AllFrames())); /* * This is a utility function for preparing an async stack to be used diff --git a/js/src/jsexn.cpp b/js/src/jsexn.cpp index fe6177e3cb1e..97f1b206a938 100644 --- a/js/src/jsexn.cpp +++ b/js/src/jsexn.cpp @@ -266,7 +266,8 @@ static const size_t MAX_REPORTED_STACK_DEPTH = 1u << 7; static bool CaptureStack(JSContext* cx, MutableHandleObject stack) { - return CaptureCurrentStack(cx, stack, MAX_REPORTED_STACK_DEPTH); + return CaptureCurrentStack(cx, stack, + JS::StackCapture(JS::MaxFrames(MAX_REPORTED_STACK_DEPTH))); } JSString* diff --git a/js/src/vm/SavedStacks.cpp b/js/src/vm/SavedStacks.cpp index 7fb9fc8f48e8..71da3b69fd16 100644 --- a/js/src/vm/SavedStacks.cpp +++ b/js/src/vm/SavedStacks.cpp @@ -1068,7 +1068,8 @@ SavedStacks::init() } bool -SavedStacks::saveCurrentStack(JSContext* cx, MutableHandleSavedFrame frame, unsigned maxFrameCount) +SavedStacks::saveCurrentStack(JSContext* cx, MutableHandleSavedFrame frame, + JS::StackCapture&& capture /* = JS::StackCapture(JS::AllFrames()) */) { MOZ_ASSERT(initialized()); MOZ_RELEASE_ASSERT(cx->compartment()); @@ -1085,7 +1086,7 @@ SavedStacks::saveCurrentStack(JSContext* cx, MutableHandleSavedFrame frame, unsi AutoSPSEntry psuedoFrame(cx->runtime(), "js::SavedStacks::saveCurrentStack"); FrameIter iter(cx); - return insertFrames(cx, iter, frame, maxFrameCount); + return insertFrames(cx, iter, frame, mozilla::Move(capture)); } bool @@ -1137,9 +1138,49 @@ SavedStacks::sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) pcLocationMap.sizeOfExcludingThis(mallocSizeOf); } +// Given that we have captured a stqck frame with the given principals and +// source, return true if the requested `StackCapture` has been satisfied and +// stack walking can halt. Return false otherwise (and stack walking and frame +// capturing should continue). +static inline bool +captureIsSatisfied(JSContext* cx, JSPrincipals* principals, const JSAtom* source, + JS::StackCapture& capture) +{ + class Matcher + { + JSContext* cx_; + JSPrincipals* framePrincipals_; + const JSAtom* frameSource_; + + public: + Matcher(JSContext* cx, JSPrincipals* principals, const JSAtom* source) + : cx_(cx) + , framePrincipals_(principals) + , frameSource_(source) + { } + + bool match(JS::FirstSubsumedFrame& target) { + auto subsumes = cx_->runtime()->securityCallbacks->subsumes; + return (!subsumes || subsumes(target.principals, framePrincipals_)) && + (!target.ignoreSelfHosted || frameSource_ != cx_->names().selfHosted); + } + + bool match(JS::MaxFrames& target) { + return target.maxFrames == 1; + } + + bool match(JS::AllFrames&) { + return false; + } + }; + + Matcher m(cx, principals, source); + return capture.match(m); +} + bool SavedStacks::insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFrame frame, - unsigned maxFrameCount) + JS::StackCapture&& capture) { // In order to lookup a cached SavedFrame object, we need to have its parent // SavedFrame, which means we need to walk the stack from oldest frame to @@ -1216,9 +1257,10 @@ SavedStacks::insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFram // The bit set means that the next older parent (frame, pc) pair *must* // be in the cache. - if (maxFrameCount == 0) + if (capture.is()) parentIsInCache = iter.hasCachedSavedFrame(); + auto principals = iter.compartment()->principals(); auto displayAtom = iter.isFunctionFrame() ? iter.functionDisplayAtom() : nullptr; if (!stackChain->emplaceBack(location.source(), location.line(), @@ -1226,7 +1268,7 @@ SavedStacks::insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFram displayAtom, nullptr, nullptr, - iter.compartment()->principals(), + principals, LiveSavedFrameCache::getFramePtr(iter), iter.pc(), &activation)) @@ -1235,15 +1277,15 @@ SavedStacks::insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFram return false; } - ++iter; - - if (maxFrameCount == 1) { + if (captureIsSatisfied(cx, principals, location.source(), capture)) { // The frame we just saved was the last one we were asked to save. // If we had an async stack, ensure we don't use any of its frames. asyncStack.set(nullptr); break; } + ++iter; + if (parentIsInCache && !iter.done() && iter.hasCachedSavedFrame()) @@ -1256,19 +1298,21 @@ SavedStacks::insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFram break; } - // If maxFrameCount is zero there's no limit on the number of frames. - if (maxFrameCount == 0) - continue; - - maxFrameCount--; + if (capture.is()) + capture.as().maxFrames--; } // Limit the depth of the async stack, if any, and ensure that the // SavedFrame instances we use are stored in the same compartment as the // rest of the synchronous stack chain. RootedSavedFrame parentFrame(cx, cachedFrame); - if (asyncStack && !adoptAsyncStack(cx, asyncStack, asyncCause, &parentFrame, maxFrameCount)) - return false; + if (asyncStack && !capture.is()) { + unsigned maxAsyncFrames = capture.is() + ? capture.as().maxFrames + : ASYNC_STACK_MAX_FRAME_COUNT; + if (!adoptAsyncStack(cx, asyncStack, asyncCause, &parentFrame, maxAsyncFrames)) + return false; + } // Iterate through |stackChain| in reverse order and get or create the // actual SavedFrame instances. @@ -1279,7 +1323,7 @@ SavedStacks::insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFram if (!parentFrame) return false; - if (maxFrameCount == 0 && lookup->framePtr && parentFrame != cachedFrame) { + if (capture.is() && lookup->framePtr && parentFrame != cachedFrame) { auto* cache = lookup->activation->getLiveSavedFrameCache(cx); if (!cache || !cache->insert(cx, *lookup->framePtr, lookup->pc, parentFrame)) return false; diff --git a/js/src/vm/SavedStacks.h b/js/src/vm/SavedStacks.h index 0bb8cfe4834b..310b7e5428d7 100644 --- a/js/src/vm/SavedStacks.h +++ b/js/src/vm/SavedStacks.h @@ -165,7 +165,7 @@ class SavedStacks { MOZ_MUST_USE bool init(); bool initialized() const { return frames.initialized(); } MOZ_MUST_USE bool saveCurrentStack(JSContext* cx, MutableHandleSavedFrame frame, - unsigned maxFrameCount = 0); + JS::StackCapture&& capture = JS::StackCapture(JS::AllFrames())); MOZ_MUST_USE bool copyAsyncStack(JSContext* cx, HandleObject asyncStack, HandleString asyncCause, MutableHandleSavedFrame adoptedStack, @@ -221,7 +221,7 @@ class SavedStacks { MOZ_MUST_USE bool insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFrame frame, - unsigned maxFrameCount = 0); + JS::StackCapture&& capture); MOZ_MUST_USE bool adoptAsyncStack(JSContext* cx, HandleSavedFrame asyncStack, HandleString asyncCause, MutableHandleSavedFrame adoptedStack,