зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1723124 - Part 1: Use GetFunctionRealm when creating promise reaction/thenable jobs. r=jandem
Also changed the unwrap performed in EnqueuePromiseReactionJob from unchecked to checked, given we don't rely on the job's realm there, and fallbacking to the current realm works. Differential Revision: https://phabricator.services.mozilla.com/D121816
This commit is contained in:
Родитель
418e9b4700
Коммит
ceba89bc46
|
@ -11,3 +11,5 @@ skip-if = debug == false
|
|||
[test_promise_retval_xrays.html]
|
||||
support-files = file_promise_xrays.html file_promise_retval_tests.js
|
||||
skip-if = debug == false
|
||||
[test_promise_job_with_bind_from_discarded_iframe.html]
|
||||
support-files = file_promise_job_with_bind_from_discarded_iframe.html
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>iframe in http</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="result"></div>
|
||||
<script type="text/javascript">
|
||||
if (typeof SpecialPowers !== "undefined") {
|
||||
document.getElementById("result").textContent = "ok";
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,63 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id=1723124.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test for Bug 1723124.</title>
|
||||
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1723124.">Mozilla Bug 1723124.</a>
|
||||
|
||||
<iframe id="frame" src="http://example.org/chrome/dom/promise/tests/file_promise_job_with_bind_from_discarded_iframe.html"></iframe>
|
||||
|
||||
<pre id="test">
|
||||
<script type="text/javascript">
|
||||
/** Test for Bug 1723124. **/
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
var frame = document.getElementById("frame");
|
||||
|
||||
SimpleTest.waitForCondition(() => {
|
||||
var result = frame.contentDocument.getElementById("result");
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
// Wait for the iframe's script to check if it has no access to SpecialPowers.
|
||||
return result.textContent == "ok";
|
||||
}, () => {
|
||||
var iframe_bind = frame.contentWindow.Function.prototype.bind;
|
||||
// Removing iframe from the tree discards the browsing context,
|
||||
// and promise jobs in the iframe global stops working.
|
||||
frame.remove();
|
||||
|
||||
Promise.resolve(10)
|
||||
.then(function (v) {
|
||||
// Handler in top-level realm, without bind.
|
||||
//
|
||||
// This job is created with the top-level realm, and should be called.
|
||||
is(v, 10, "normal function should get the value from promise");
|
||||
return 20;
|
||||
}, function () {
|
||||
ok(false, "unexpectedly rejected");
|
||||
})
|
||||
.then(iframe_bind.call(function (bound_arg, v) {
|
||||
// Handler in top-level realm, with bind in discarded iframe.
|
||||
//
|
||||
// This job is also created with the top-level realm, and should be
|
||||
// called.
|
||||
is(v, 20, "bound function should get the value from promise");
|
||||
is(bound_arg, 30, "bound function should get the arguments from bind");
|
||||
SimpleTest.finish();
|
||||
}, this, 30), function () {
|
||||
ok(false, "unexpectedly rejected");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,223 @@
|
|||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
function createSandbox() {
|
||||
const uri = Services.io.newURI("https://example.com");
|
||||
const principal = Services.scriptSecurityManager.createContentPrincipal(
|
||||
uri,
|
||||
{}
|
||||
);
|
||||
return new Cu.Sandbox(principal, {});
|
||||
}
|
||||
|
||||
add_task(async function testReactionJob() {
|
||||
const sandbox = createSandbox();
|
||||
|
||||
sandbox.eval(`
|
||||
var testPromise = Promise.resolve(10);
|
||||
`);
|
||||
|
||||
// Calling `Promise.prototype.then` from sandbox performs GetFunctionRealm
|
||||
// on wrapped `resolve` in sandbox realm, and it fails to unwrap the security
|
||||
// wrapper. The reaction job should be created with sandbox realm.
|
||||
const p = new Promise(resolve => {
|
||||
sandbox.resolve = resolve;
|
||||
|
||||
sandbox.eval(`
|
||||
testPromise.then(resolve);
|
||||
`);
|
||||
});
|
||||
|
||||
const result = await p;
|
||||
|
||||
equal(result, 10);
|
||||
});
|
||||
|
||||
add_task(async function testReactionJobNuked() {
|
||||
const sandbox = createSandbox();
|
||||
|
||||
sandbox.eval(`
|
||||
var testPromise = Promise.resolve(10);
|
||||
`);
|
||||
|
||||
// Calling `Promise.prototype.then` from sandbox performs GetFunctionRealm
|
||||
// on wrapped `resolve` in sandbox realm, and it fails to unwrap the security
|
||||
// wrapper. The reaction job should be created with sandbox realm.
|
||||
const p1 = new Promise(resolve => {
|
||||
sandbox.resolve = resolve;
|
||||
|
||||
sandbox.eval(`
|
||||
testPromise.then(resolve);
|
||||
`);
|
||||
|
||||
// Given the reaction job is created with the sandbox realm, nuking the
|
||||
// sandbox prevents the job gets executed.
|
||||
Cu.nukeSandbox(sandbox);
|
||||
});
|
||||
|
||||
const p2 = Promise.resolve(11);
|
||||
|
||||
// Given the p1 doesn't get resolved, p2 should win.
|
||||
const result = await Promise.race([p1, p2]);
|
||||
|
||||
equal(result, 11);
|
||||
});
|
||||
|
||||
add_task(async function testReactionJobWithXray() {
|
||||
const sandbox = createSandbox();
|
||||
|
||||
sandbox.eval(`
|
||||
var testPromise = Promise.resolve(10);
|
||||
`);
|
||||
|
||||
// Calling `Promise.prototype.then` from privileged realm via Xray uses
|
||||
// privileged `Promise.prototype.then` function, and GetFunctionRealm
|
||||
// performed there successfully gets top-level realm. The reaction job
|
||||
// should be created with top-level realm.
|
||||
const result = await new Promise(resolve => {
|
||||
sandbox.testPromise.then(resolve);
|
||||
|
||||
// Given the reaction job is created with the top-level realm, nuking the
|
||||
// sandbox doesn't affect the reaction job.
|
||||
Cu.nukeSandbox(sandbox);
|
||||
});
|
||||
|
||||
equal(result, 10);
|
||||
});
|
||||
|
||||
add_task(async function testBoundReactionJob() {
|
||||
const sandbox = createSandbox();
|
||||
|
||||
sandbox.eval(`
|
||||
var resolve = undefined;
|
||||
var callbackPromise = new Promise(r => { resolve = r; });
|
||||
var callback = function (v) { resolve(v + 1); };
|
||||
`);
|
||||
|
||||
// Create a bound function where its realm is privileged realm, and
|
||||
// its target is from sandbox realm.
|
||||
sandbox.bound_callback = Function.prototype.bind.call(
|
||||
sandbox.callback,
|
||||
sandbox
|
||||
);
|
||||
|
||||
// Calling `Promise.prototype.then` from sandbox performs GetFunctionRealm
|
||||
// and it fails. The reaction job should be created with sandbox realm.
|
||||
sandbox.eval(`
|
||||
Promise.resolve(10).then(bound_callback);
|
||||
`);
|
||||
|
||||
const result = await sandbox.callbackPromise;
|
||||
equal(result, 11);
|
||||
});
|
||||
|
||||
add_task(async function testThenableJob() {
|
||||
const sandbox = createSandbox();
|
||||
|
||||
const p = new Promise(resolve => {
|
||||
// Create a bound function where its realm is privileged realm, and
|
||||
// its target is from sandbox realm.
|
||||
sandbox.then = function(onFulfilled, onRejected) {
|
||||
resolve(10);
|
||||
};
|
||||
});
|
||||
|
||||
// Creating a promise thenable job in the following `Promise.resolve` performs
|
||||
// GetFunctionRealm on the bound thenable.then and fails. The reaction job
|
||||
// should be created with sandbox realm.
|
||||
sandbox.eval(`
|
||||
var thenable = {
|
||||
then: then,
|
||||
};
|
||||
|
||||
Promise.resolve(thenable);
|
||||
`);
|
||||
|
||||
const result = await p;
|
||||
equal(result, 10);
|
||||
});
|
||||
|
||||
add_task(async function testThenableJobNuked() {
|
||||
const sandbox = createSandbox();
|
||||
|
||||
let called = false;
|
||||
sandbox.then = function(onFulfilled, onRejected) {
|
||||
called = true;
|
||||
};
|
||||
|
||||
// Creating a promise thenable job in the following `Promise.resolve` performs
|
||||
// GetFunctionRealm on the bound thenable.then and fails. The reaction job
|
||||
// should be created with sandbox realm.
|
||||
sandbox.eval(`
|
||||
var thenable = {
|
||||
then: then,
|
||||
};
|
||||
|
||||
Promise.resolve(thenable);
|
||||
`);
|
||||
|
||||
Cu.nukeSandbox(sandbox);
|
||||
|
||||
// Drain the job queue, to make sure we hit dead object error inside the
|
||||
// thenable job.
|
||||
await Promise.resolve(10);
|
||||
|
||||
equal(
|
||||
Services.console.getMessageArray().find(x => {
|
||||
return x.toString().includes("can't access dead object");
|
||||
}) !== undefined,
|
||||
true
|
||||
);
|
||||
equal(called, false);
|
||||
});
|
||||
|
||||
add_task(async function testThenableJobAccessError() {
|
||||
const sandbox = createSandbox();
|
||||
|
||||
let accessed = false;
|
||||
sandbox.thenable = {
|
||||
get then() {
|
||||
accessed = true;
|
||||
},
|
||||
};
|
||||
|
||||
// The following operation silently fails when accessing `then` property.
|
||||
sandbox.eval(`
|
||||
var x = typeof thenable.then;
|
||||
|
||||
Promise.resolve(thenable);
|
||||
`);
|
||||
|
||||
equal(accessed, false);
|
||||
});
|
||||
|
||||
add_task(async function testBoundThenableJob() {
|
||||
const sandbox = createSandbox();
|
||||
|
||||
sandbox.eval(`
|
||||
var resolve = undefined;
|
||||
var callbackPromise = new Promise(r => { resolve = r; });
|
||||
var callback = function (v) { resolve(v + 1); };
|
||||
|
||||
var then = function(onFulfilled, onRejected) {
|
||||
onFulfilled(10);
|
||||
};
|
||||
`);
|
||||
|
||||
// Create a bound function where its realm is privileged realm, and
|
||||
// its target is from sandbox realm.
|
||||
sandbox.bound_then = Function.prototype.bind.call(sandbox.then, sandbox);
|
||||
|
||||
// Creating a promise thenable job in the following `Promise.resolve` performs
|
||||
// GetFunctionRealm on the bound thenable.then and fails. The reaction job
|
||||
// should be created with sandbox realm.
|
||||
sandbox.eval(`
|
||||
var thenable = {
|
||||
then: bound_then,
|
||||
};
|
||||
|
||||
Promise.resolve(thenable).then(callback);
|
||||
`);
|
||||
|
||||
const result = await sandbox.callbackPromise;
|
||||
equal(result, 11);
|
||||
});
|
|
@ -3,3 +3,4 @@ head =
|
|||
|
||||
[test_monitor_uncaught.js]
|
||||
[test_promise_unhandled_rejection.js]
|
||||
[test_promise_job_across_sandbox.js]
|
||||
|
|
|
@ -1175,19 +1175,35 @@ static bool PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp);
|
|||
RootedValue reactionVal(cx, ObjectValue(*reaction));
|
||||
RootedValue handler(cx, reaction->handler());
|
||||
|
||||
// If we have a handler callback, we enter that handler's compartment so
|
||||
// that the promise reaction job function is created in that compartment.
|
||||
// If we have a handler callback, we enter the realm returned by
|
||||
// GetFunctionRealm(handler) so that the promise reaction job function is
|
||||
// created in that compartment.
|
||||
// That guarantees that the embedding ends up with the right entry global.
|
||||
// This is relevant for some html APIs like fetch that derive information
|
||||
// from said global.
|
||||
mozilla::Maybe<AutoRealm> ar2;
|
||||
//
|
||||
// GetFunctionRealm performed inside AutoFunctionOrCurrentRealm uses checked
|
||||
// unwrap and it can hit permission error if there's a security wrapper, and
|
||||
// in that case the reaction job is created in the current realm, instead of
|
||||
// the target function's realm.
|
||||
//
|
||||
// If this reaction crosses chrome/content boundary, and the security
|
||||
// wrapper would allow "call" operation, it still works inside the
|
||||
// reaction job.
|
||||
//
|
||||
// This behavior is observable only when the job belonging to the content
|
||||
// realm stops working (*1, *2), and it won't matter in practice.
|
||||
//
|
||||
// *1: "we can run script" performed inside HostEnqueuePromiseJob
|
||||
// in HTML spec
|
||||
// https://html.spec.whatwg.org/#hostenqueuepromisejob
|
||||
// https://html.spec.whatwg.org/#check-if-we-can-run-script
|
||||
// https://html.spec.whatwg.org/#fully-active
|
||||
// *2: nsIGlobalObject::IsDying performed inside PromiseJobRunnable::Run
|
||||
// in our implementation
|
||||
mozilla::Maybe<AutoFunctionOrCurrentRealm> ar2;
|
||||
if (handler.isObject()) {
|
||||
// The unwrapping has to be unchecked because we specifically want to
|
||||
// be able to use handlers with wrappers that would only allow calls.
|
||||
// E.g., it's ok to have a handler from a chrome compartment in a
|
||||
// reaction to a content compartment's Promise instance.
|
||||
JSObject* handlerObj = UncheckedUnwrap(&handler.toObject());
|
||||
MOZ_ASSERT(handlerObj);
|
||||
RootedObject handlerObj(cx, &handler.toObject());
|
||||
ar2.emplace(cx, handlerObj);
|
||||
|
||||
// We need to wrap the reaction to store it on the job function.
|
||||
|
@ -1949,7 +1965,6 @@ static bool PromiseResolveThenableJob(JSContext* cx, unsigned argc, Value* vp) {
|
|||
RootedFunction job(cx, &args.callee().as<JSFunction>());
|
||||
RootedValue then(cx, job->getExtendedSlot(ThenableJobSlot_Handler));
|
||||
MOZ_ASSERT(then.isObject());
|
||||
MOZ_ASSERT(!IsWrapper(&then.toObject()));
|
||||
RootedNativeObject jobArgs(cx, &job->getExtendedSlot(ThenableJobSlot_JobData)
|
||||
.toObject()
|
||||
.as<NativeObject>());
|
||||
|
@ -2061,13 +2076,27 @@ static bool PromiseResolveBuiltinThenableJob(JSContext* cx, unsigned argc,
|
|||
RootedValue promiseToResolve(cx, promiseToResolve_);
|
||||
RootedValue thenable(cx, thenable_);
|
||||
|
||||
// We enter the `then` callable's compartment so that the job function is
|
||||
// created in that compartment.
|
||||
// We enter the realm returned by GetFunctionRealm(then) so that the job
|
||||
// function is created in the right realm.
|
||||
// That guarantees that the embedding ends up with the right entry global.
|
||||
// This is relevant for some html APIs like fetch that derive information
|
||||
// from said global.
|
||||
RootedObject then(cx, CheckedUnwrapStatic(&thenVal.toObject()));
|
||||
AutoRealm ar(cx, then);
|
||||
//
|
||||
// GetFunctionRealm performed inside AutoFunctionOrCurrentRealm uses checked
|
||||
// unwrap and this is fine given the behavior difference (see the comment
|
||||
// around AutoFunctionOrCurrentRealm usage in EnqueuePromiseReactionJob for
|
||||
// more details) is observable only when the `thenable` is from content realm
|
||||
// and `then` is from chrome realm, that shouldn't happen in practice.
|
||||
//
|
||||
// NOTE: If `thenable` is also from chrome realm, accessing `then` silently
|
||||
// fails and it returns `undefined`, and that case doesn't reach here.
|
||||
RootedObject then(cx, &thenVal.toObject());
|
||||
AutoFunctionOrCurrentRealm ar(cx, then);
|
||||
if (then->maybeCCWRealm() != cx->realm()) {
|
||||
if (!cx->compartment()->wrap(cx, &then)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap the `promiseToResolve` and `thenable` arguments.
|
||||
if (!cx->compartment()->wrap(cx, &promiseToResolve)) {
|
||||
|
|
|
@ -65,6 +65,18 @@ js::AutoRealm::AutoRealm(JSContext* cx, JS::Realm* target)
|
|||
|
||||
js::AutoRealm::~AutoRealm() { cx_->leaveRealm(origin_); }
|
||||
|
||||
js::AutoFunctionOrCurrentRealm::AutoFunctionOrCurrentRealm(JSContext* cx,
|
||||
HandleObject fun) {
|
||||
JS::Realm* realm = JS::GetFunctionRealm(cx, fun);
|
||||
if (!realm) {
|
||||
cx->clearPendingException();
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter the function's realm.
|
||||
ar_.emplace(cx, realm);
|
||||
}
|
||||
|
||||
js::AutoAllocInAtomsZone::AutoAllocInAtomsZone(JSContext* cx)
|
||||
: cx_(cx), origin_(cx->realm()) {
|
||||
cx_->enterAtomsZone();
|
||||
|
|
|
@ -869,6 +869,25 @@ class AutoRealmUnchecked : protected AutoRealm {
|
|||
inline AutoRealmUnchecked(JSContext* cx, JS::Realm* target);
|
||||
};
|
||||
|
||||
// Similar to AutoRealm, but this uses GetFunctionRealm in the spec, and
|
||||
// handles both bound functions and proxies.
|
||||
//
|
||||
// If GetFunctionRealm fails for the following reasons, this does nothing:
|
||||
// * `fun` is revoked proxy
|
||||
// * unwrapping failed because of a security wrapper
|
||||
class AutoFunctionOrCurrentRealm {
|
||||
mozilla::Maybe<AutoRealmUnchecked> ar_;
|
||||
|
||||
public:
|
||||
inline AutoFunctionOrCurrentRealm(JSContext* cx, js::HandleObject fun);
|
||||
~AutoFunctionOrCurrentRealm() = default;
|
||||
|
||||
private:
|
||||
AutoFunctionOrCurrentRealm(const AutoFunctionOrCurrentRealm&) = delete;
|
||||
AutoFunctionOrCurrentRealm& operator=(const AutoFunctionOrCurrentRealm&) =
|
||||
delete;
|
||||
};
|
||||
|
||||
/*
|
||||
* Use this to change the behavior of an AutoRealm slightly on error. If
|
||||
* the exception happens to be an Error object, copy it to the origin
|
||||
|
|
Загрузка…
Ссылка в новой задаче