Bug 1441333: Part 1 - Add helper to retrieve closest stack frame with a given principal.
Most WebExtension APIs are async, and have fairly complicated error reporting
semantics. As a result, when we report an error, the current JS stack has very
little to do with the JS caller that triggered the error, which makes it
difficult to diagnose.
In order to improve the situation, we need to store the location of the caller
at the start of an async operation, so we can tie the error to some marginally
useful location. We don't have a reasonable way to do that now other than
proactively creating an error object when the API is called, or creating a
promise with a full async stack, both of which are too expensive.
This helper instead returns a single SavedStack frame with a given principal,
which should be considerably cheaper, and likely good enough to give a
starting point for debugging cryptic errors.
MozReview-Commit-ID: BTxhpZK9Fdz
--- a/dom/base/ChromeUtils.cpp
+++ b/dom/base/ChromeUtils.cpp
@@ -648,10 +648,36 @@ ChromeUtils::ClearRecentJSDevError(Globa
{
auto runtime = CycleCollectedJSRuntime::Get();
MOZ_ASSERT(runtime);
runtime->ClearRecentDevError();
}
#endif // NIGHTLY_BUILD
+constexpr auto kSkipSelfHosted = JS::SavedFrameSelfHosted::Exclude;
+
+/* static */ void
+ChromeUtils::GetCallerLocation(const GlobalObject& aGlobal, nsIPrincipal* aPrincipal,
+ JS::MutableHandle<JSObject*> aRetval)
+{
+ JSContext* cx = aGlobal.Context();
+
+ auto* principals = nsJSPrincipals::get(aPrincipal);
+
+ JS::StackCapture captureMode(JS::FirstSubsumedFrame(cx, principals));
+
+ JS::RootedObject frame(cx);
+ if (!JS::CaptureCurrentStack(cx, &frame, mozilla::Move(captureMode))) {
+ JS_ClearPendingException(cx);
+ aRetval.set(nullptr);
+ return;
+ }
+
+ // FirstSubsumedFrame gets us a stack which stops at the first principal which
+ // is subsumed by the given principal. That means that we may have a lot of
+ // privileged frames that we don't care about at the top of the stack, though.
+ // We need to filter those out to get the frame we actually want.
+ aRetval.set(js::GetFirstSubsumedSavedFrame(cx, principals, frame, kSkipSelfHosted));
+}
+
} // namespace dom
} // namespace mozilla
--- a/dom/base/ChromeUtils.h
+++ b/dom/base/ChromeUtils.h
@@ -161,14 +161,18 @@ public:
JS::MutableHandle<JSObject*> aRetval,
ErrorResult& aRv);
static void DefineModuleGetter(const GlobalObject& global,
JS::Handle<JSObject*> target,
const nsAString& id,
const nsAString& resourceURI,
ErrorResult& aRv);
+
+ static void
+ GetCallerLocation(const GlobalObject& global, nsIPrincipal* principal,
+ JS::MutableHandle<JSObject*> aRetval);
};
} // namespace dom
} // namespace mozilla
#endif // mozilla_dom_ChromeUtils__
--- a/dom/webidl/ChromeUtils.webidl
+++ b/dom/webidl/ChromeUtils.webidl
@@ -289,16 +289,23 @@ partial namespace ChromeUtils {
* @param target The target object on which to define the property.
* @param id The name of the property to define, and of the symbol to
* import.
* @param resourceURI The resource URI of the module, as passed to
* ChromeUtils.import.
*/
[Throws]
void defineModuleGetter(object target, DOMString id, DOMString resourceURI);
+
+ /**
+ * Returns the scripted location of the first ancestor stack frame with a
+ * principal which is subsumed by the given principal. If no such frame
+ * exists on the call stack, returns null.
+ */
+ object? getCallerLocation(Principal principal);
};
/**
* Used by principals and the script security manager to represent origin
* attributes. The first dictionary is designed to contain the full set of
* OriginAttributes, the second is used for pattern-matching (i.e. does this
* OriginAttributesDictionary match the non-empty attributes in this pattern).
*
--- a/js/src/jsfriendapi.h
+++ b/js/src/jsfriendapi.h
@@ -2984,16 +2984,25 @@ SetJitExceptionHandler(JitExceptionHandl
*
* Do NOT pass a non-SavedFrame object here.
*
* The savedFrame and cx do not need to be in the same compartment.
*/
extern JS_FRIEND_API(JSObject*)
GetFirstSubsumedSavedFrame(JSContext* cx, JS::HandleObject savedFrame, JS::SavedFrameSelfHosted selfHosted);
+/**
+ * Get the first SavedFrame object in this SavedFrame stack whose principals are
+ * subsumed by the given |principals|. If there is no such frame, return nullptr.
+ *
+ * Do NOT pass a non-SavedFrame object here.
+ */
+extern JS_FRIEND_API(JSObject*)
+GetFirstSubsumedSavedFrame(JSContext* cx, JSPrincipals* principals, JS::HandleObject savedFrame, JS::SavedFrameSelfHosted selfHosted);
+
extern JS_FRIEND_API(bool)
ReportIsNotFunction(JSContext* cx, JS::HandleValue v);
extern JS_FRIEND_API(JSObject*)
ConvertArgsToArray(JSContext* cx, const JS::CallArgs& args);
/**
* Window and WindowProxy
--- a/js/src/vm/SavedStacks.cpp
+++ b/js/src/vm/SavedStacks.cpp
@@ -567,55 +567,89 @@ SavedFrameSubsumedByCaller(JSContext* cx
return cx->runningWithTrustedPrincipals();
if (framePrincipals == &ReconstructedSavedFramePrincipals::IsNotSystem)
return true;
return subsumes(currentCompartmentPrincipals, framePrincipals);
}
// Return the first SavedFrame in the chain that starts with |frame| whose
-// principals are subsumed by |principals|, according to |subsumes|. If there is
-// no such frame, return nullptr. |skippedAsync| is set to true if any of the
-// skipped frames had the |asyncCause| property set, otherwise it is explicitly
-// set to false.
+// for which the given match function returns true. If there is no such frame,
+// return nullptr. |skippedAsync| is set to true if any of the skipped frames
+// had the |asyncCause| property set, otherwise it is explicitly set to false.
+template<typename Matcher>
static SavedFrame*
-GetFirstSubsumedFrame(JSContext* cx, HandleSavedFrame frame, JS::SavedFrameSelfHosted selfHosted,
- bool& skippedAsync)
+GetFirstMatchedFrame(JSContext* cx, Matcher& matches,
+ HandleSavedFrame frame, JS::SavedFrameSelfHosted selfHosted,
+ bool& skippedAsync)
{
skippedAsync = false;
RootedSavedFrame rootedFrame(cx, frame);
while (rootedFrame) {
if ((selfHosted == JS::SavedFrameSelfHosted::Include ||
!rootedFrame->isSelfHosted(cx)) &&
- SavedFrameSubsumedByCaller(cx, rootedFrame))
+ matches(cx, rootedFrame))
{
return rootedFrame;
}
if (rootedFrame->getAsyncCause())
skippedAsync = true;
rootedFrame = rootedFrame->getParent();
}
return nullptr;
}
+// Return the first SavedFrame in the chain that starts with |frame| whose
+// principals are subsumed by the principals of the context's current
+// compartment, according to |subsumes|. If there is no such frame, return
+// nullptr. |skippedAsync| is set to true if any of the skipped frames had the
+// |asyncCause| property set, otherwise it is explicitly set to false.
+static SavedFrame*
+GetFirstSubsumedFrame(JSContext* cx, HandleSavedFrame frame, JS::SavedFrameSelfHosted selfHosted,
+ bool& skippedAsync)
+{
+ return GetFirstMatchedFrame(cx, SavedFrameSubsumedByCaller, frame, selfHosted, skippedAsync);
+}
+
JS_FRIEND_API(JSObject*)
GetFirstSubsumedSavedFrame(JSContext* cx, HandleObject savedFrame,
JS::SavedFrameSelfHosted selfHosted)
{
if (!savedFrame)
return nullptr;
bool skippedAsync;
RootedSavedFrame frame(cx, &savedFrame->as<SavedFrame>());
return GetFirstSubsumedFrame(cx, frame, selfHosted, skippedAsync);
}
+JS_FRIEND_API(JSObject*)
+GetFirstSubsumedSavedFrame(JSContext* cx, JSPrincipals* principals,
+ HandleObject savedFrame,
+ JS::SavedFrameSelfHosted selfHosted)
+{
+ if (!savedFrame)
+ return nullptr;
+
+ auto subsumes = cx->runtime()->securityCallbacks->subsumes;
+ if (!subsumes)
+ return nullptr;
+
+ auto matcher = [&](JSContext* cx, HandleSavedFrame frame) -> bool {
+ return subsumes(principals, frame->getPrincipals());
+ };
+
+ bool skippedAsync;
+ RootedSavedFrame frame(cx, &savedFrame->as<SavedFrame>());
+ return GetFirstMatchedFrame(cx, matcher, frame, selfHosted, skippedAsync);
+}
+
static MOZ_MUST_USE bool
SavedFrame_checkThis(JSContext* cx, CallArgs& args, const char* fnName,
MutableHandleObject frame)
{
const Value& thisValue = args.thisv();
if (!thisValue.isObject()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_NONNULL_OBJECT,
new file mode 100644
--- /dev/null
+++ b/js/xpconnect/tests/unit/test_getCallerLocation.js
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+Cu.importGlobalProperties(["ChromeUtils"]);
+
+add_task(async function() {
+ const sandbox = Cu.Sandbox("http://example.com/");
+
+ function foo() {
+ return bar();
+ }
+
+ function bar() {
+ return baz();
+ }
+
+ function baz() {
+ return ChromeUtils.getCallerLocation(Cu.getObjectPrincipal(sandbox));
+ }
+
+ Cu.evalInSandbox(`
+ function it() {
+ // Use map() to throw a self-hosted frame on the stack, which we
+ // should filter out.
+ return [0].map(foo)[0];
+ }
+ function thing() {
+ return it();
+ }
+ `, sandbox, undefined, "thing.js");
+
+ Cu.exportFunction(foo, sandbox, {defineAs: "foo"});
+
+ let frame = sandbox.thing();
+
+ equal(frame.source, "thing.js", "Frame source");
+ equal(frame.line, 5, "Frame line");
+ equal(frame.column, 14, "Frame column");
+ equal(frame.functionDisplayName, "it", "Frame function name");
+ equal(frame.parent, null, "Frame parent");
+
+ equal(String(frame), "it@thing.js:5:14\n", "Stringified frame");
+});
--- a/js/xpconnect/tests/unit/xpcshell.ini
+++ b/js/xpconnect/tests/unit/xpcshell.ini
@@ -67,16 +67,17 @@ support-files =
[test_classesByID_instanceof.js]
[test_compileScript.js]
[test_deepFreezeClone.js]
[test_defineModuleGetter.js]
[test_file.js]
[test_blob.js]
[test_blob2.js]
[test_file2.js]
+[test_getCallerLocation.js]
[test_import.js]
[test_import_fail.js]
[test_interposition.js]
[test_isModuleLoaded.js]
[test_js_weak_references.js]
[test_onGarbageCollection-01.js]
head = head_ongc.js
[test_onGarbageCollection-02.js]