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:
Tooru Fujisawa 2021-08-10 16:04:21 +00:00
Родитель 418e9b4700
Коммит ceba89bc46
8 изменённых файлов: 377 добавлений и 14 удалений

Просмотреть файл

@ -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