Bug 1512260 - Make wrapper nuking work with a target realm instead of target compartment. r=kmag

For *incoming* wrappers this preserves behavior. We nuke *outgoing* wrappers
when all realms in the compartment have been nuked. To implement this I moved
the wasNuked flag from XPConnect to JS::Compartment as nukedOutgoingWrappers and
to JS::Realm as nukedIncomingWrappers.

The code to create a dead wrapper in the nuked compartment/realm case was also
moved into the JS engine. I added a shell test for it.

Differential Revision: https://phabricator.services.mozilla.com/D14149

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Jan de Mooij 2018-12-12 08:02:30 +00:00
parent fa80837513
commit b8a316aca4
16 changed files with 172 additions and 89 deletions

View File

@ -109,21 +109,19 @@ WindowDestroyedEvent::Run() {
JS::Rooted<JSObject*> obj(cx, currentInner->FastGetGlobalJSObject());
if (obj && !js::IsSystemRealm(js::GetNonCCWObjectRealm(obj))) {
JS::Realm* realm = js::GetNonCCWObjectRealm(obj);
JS::Compartment* cpt = JS::GetCompartmentForRealm(realm);
nsCOMPtr<nsIPrincipal> pc =
nsJSPrincipals::get(JS::GetRealmPrincipals(realm));
if (BasePrincipal::Cast(pc)->AddonPolicy()) {
// We want to nuke all references to the add-on compartment.
xpc::NukeAllWrappersForCompartment(
cx, cpt,
mIsInnerWindow ? js::DontNukeWindowReferences
: js::NukeWindowReferences);
// We want to nuke all references to the add-on realm.
xpc::NukeAllWrappersForRealm(cx, realm,
mIsInnerWindow
? js::DontNukeWindowReferences
: js::NukeWindowReferences);
} else {
// We only want to nuke wrappers for the chrome->content case
js::NukeCrossCompartmentWrappers(
cx, BrowserCompartmentMatcher(), cpt,
cx, BrowserCompartmentMatcher(), realm,
mIsInnerWindow ? js::DontNukeWindowReferences
: js::NukeWindowReferences,
js::NukeIncomingReferences);

View File

@ -58,8 +58,8 @@ NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(CallbackObject)
if (MOZ_UNLIKELY(!callback)) {
return true;
}
auto pvt = xpc::CompartmentPrivate::Get(callback);
if (MOZ_LIKELY(tmp->mIncumbentGlobal && pvt) && MOZ_UNLIKELY(pvt->wasNuked)) {
if (MOZ_LIKELY(tmp->mIncumbentGlobal) &&
MOZ_UNLIKELY(js::NukedObjectRealm(tmp->CallbackGlobalPreserveColor()))) {
// It's not safe to release our global reference or drop our JS objects at
// this point, so defer their finalization until CC is finished.
AddForDeferredFinalization(new JSObjectsDropper(tmp));

View File

@ -127,6 +127,10 @@ class CallbackObject : public nsISupports {
// because the value of mCallback cannot change after if has been set.
return JS::Handle<JSObject*>::fromMarkedLocation(mCallback.address());
}
JS::Handle<JSObject*> CallbackGlobalPreserveColor() const {
// The comment in CallbackPreserveColor applies here as well.
return JS::Handle<JSObject*>::fromMarkedLocation(mCallbackGlobal.address());
}
/*
* If the callback is known to be non-gray, then this method can be

View File

@ -0,0 +1,49 @@
// Ensure nuking happens on a single target realm instead of compartment.
var g1 = newGlobal();
var g2 = newGlobal({sameCompartmentAs: g1});
g2.other = g1;
var o1 = g1.Math;
var o2 = g2.Math;
g1.nukeAllCCWs();
// o1 is now dead.
ex = null;
try {
assertEq(o1.abs(1), 1);
} catch (e) {
ex = e;
}
assertEq(ex.toString().includes("dead object"), true);
// o2 still works.
assertEq(o2.abs(1), 1);
// g2 can still access g1 because they're same-compartment.
assertEq(g2.evaluate("other.Math.abs(-2)"), 2);
// Attempting to create a new wrapper targeting nuked realm g1 should return a
// dead wrapper now. Note that we can't use g1 directly because that's now a
// dead object, so we try to get to g1 via g2.
ex = null;
try {
g2.other.toString();
} catch (e) {
ex = e;
}
assertEq(ex.toString().includes("dead object"), true);
// Nuke g2 too. We have nuked all realms in its compartment so we should now
// throw if we try to create a new outgoing wrapper.
g2.evaluate("(" + function() {
nukeAllCCWs();
var ex = null;
try {
newGlobal().Array();
} catch (e) {
ex = e;
}
assertEq(ex.toString().includes('dead object'), true);
} + ")()");

View File

@ -525,10 +525,6 @@ JS_FRIEND_API JSObject* JS_NewDeadWrapper(JSContext* cx, JSObject* origObj) {
return NewDeadProxyObject(cx, origObj);
}
JS_FRIEND_API bool JS_IsScriptSourceObject(JSObject* obj) {
return obj->is<ScriptSourceObject>();
}
void js::TraceWeakMaps(WeakMapTracer* trc) {
WeakMapBase::traceAllMappings(trc);
}

View File

@ -106,11 +106,6 @@ extern JS_FRIEND_API bool JS_IsDeadWrapper(JSObject* obj);
extern JS_FRIEND_API JSObject* JS_NewDeadWrapper(
JSContext* cx, JSObject* origObject = nullptr);
/**
* Determine whether the given object is a ScriptSourceObject.
*/
extern JS_FRIEND_API bool JS_IsScriptSourceObject(JSObject* obj);
/*
* Used by the cycle collector to trace through a shape or object group and
* all cycle-participating data it reaches, using bounded stack space.
@ -1184,10 +1179,15 @@ struct CompartmentsWithPrincipals : public CompartmentFilter {
};
extern JS_FRIEND_API bool NukeCrossCompartmentWrappers(
JSContext* cx, const CompartmentFilter& sourceFilter,
JS::Compartment* target, NukeReferencesToWindow nukeReferencesToWindow,
JSContext* cx, const CompartmentFilter& sourceFilter, JS::Realm* target,
NukeReferencesToWindow nukeReferencesToWindow,
NukeReferencesFromTarget nukeReferencesFromTarget);
extern JS_FRIEND_API bool AllowNewWrapper(JS::Compartment* target,
JSObject* obj);
extern JS_FRIEND_API bool NukedObjectRealm(JSObject* obj);
/* Specify information about DOMProxy proxies in the DOM, for use by ICs. */
/*

View File

@ -460,30 +460,47 @@ JS_FRIEND_API void js::NukeCrossCompartmentWrapper(JSContext* cx,
NukeRemovedCrossCompartmentWrapper(cx, wrapper);
}
// Returns true iff all realms in the compartment have been nuked.
static bool NukedAllRealms(JS::Compartment* comp) {
for (RealmsInCompartmentIter realm(comp); !realm.done(); realm.next()) {
if (!realm->nukedIncomingWrappers) {
return false;
}
}
return true;
}
/*
* NukeChromeCrossCompartmentWrappersForGlobal reaches into chrome and cuts
* all of the cross-compartment wrappers that point to objects parented to
* obj's global. The snag here is that we need to avoid cutting wrappers that
* point to the window object on page navigation (inner window destruction)
* and only do that on tab close (outer window destruction). Thus the
* option of how to handle the global object.
* all of the cross-compartment wrappers that point to an object in the |target|
* realm. The snag here is that we need to avoid cutting wrappers that point to
* the window object on page navigation (inner window destruction) and only do
* that on tab close (outer window destruction). Thus the option of how to
* handle the global object.
*/
JS_FRIEND_API bool js::NukeCrossCompartmentWrappers(
JSContext* cx, const CompartmentFilter& sourceFilter,
JS::Compartment* target, js::NukeReferencesToWindow nukeReferencesToWindow,
JSContext* cx, const CompartmentFilter& sourceFilter, JS::Realm* target,
js::NukeReferencesToWindow nukeReferencesToWindow,
js::NukeReferencesFromTarget nukeReferencesFromTarget) {
CHECK_THREAD(cx);
JSRuntime* rt = cx->runtime();
// If we're nuking all wrappers into the target realm, prevent us from
// creating new wrappers for it in the future.
if (nukeReferencesFromTarget == NukeAllReferences) {
target->nukedIncomingWrappers = true;
}
for (CompartmentsIter c(rt); !c.done(); c.next()) {
if (!sourceFilter.match(c)) {
continue;
}
// If the compartment matches both the source and target filter, we may
// want to cut both incoming and outgoing wrappers.
// If the realm matches both the source and target filter, we may want to
// cut outgoing wrappers too, if we nuked all realms in the compartment.
bool nukeAll =
(nukeReferencesFromTarget == NukeAllReferences && target == c.get());
(nukeReferencesFromTarget == NukeAllReferences &&
target->compartment() == c.get() && NukedAllRealms(c.get()));
// Iterate only the wrappers that have target compartment matched unless
// |nukeAll| is true. The string wrappers that we're not interested in
@ -492,9 +509,10 @@ JS_FRIEND_API bool js::NukeCrossCompartmentWrappers(
// initializing NonStringWrapperEnum.
mozilla::Maybe<Compartment::NonStringWrapperEnum> e;
if (MOZ_LIKELY(!nukeAll)) {
e.emplace(c, target);
e.emplace(c, target->compartment());
} else {
e.emplace(c);
c.get()->nukedOutgoingWrappers = true;
}
for (; !e->empty(); e->popFront()) {
// Skip debugger references because NukeCrossCompartmentWrapper()
@ -511,6 +529,13 @@ JS_FRIEND_API bool js::NukeCrossCompartmentWrappers(
// the wrapper, this could save us a bit of time.
JSObject* wrapped = UncheckedUnwrap(k.as<JSObject*>());
// Don't nuke wrappers for objects in other realms in the target
// compartment unless nukeAll is set because in that case we want to nuke
// all outgoing wrappers for the current compartment.
if (!nukeAll && wrapped->nonCCWRealm() != target) {
continue;
}
// We never nuke script source objects, since only ever used internally by
// the JS engine, and are expected to remain valid throughout a scripts
// lifetime.
@ -534,6 +559,31 @@ JS_FRIEND_API bool js::NukeCrossCompartmentWrappers(
return true;
}
JS_FRIEND_API bool js::AllowNewWrapper(JS::Compartment* target, JSObject* obj) {
// Disallow creating new wrappers if we nuked the object realm or target
// compartment. However, we always need to provide live wrappers for
// ScriptSourceObjects, since they're used for cross-compartment cloned
// scripts, and need to remain accessible even after the original realm has
// been nuked.
MOZ_ASSERT(obj->compartment() != target);
if (obj->is<ScriptSourceObject>()) {
return true;
}
if (target->nukedOutgoingWrappers ||
obj->nonCCWRealm()->nukedIncomingWrappers) {
return false;
}
return true;
}
JS_FRIEND_API bool js::NukedObjectRealm(JSObject* obj) {
return obj->nonCCWRealm()->nukedIncomingWrappers;
}
// Given a cross-compartment wrapper |wobj|, update it to point to
// |newTarget|. This recomputes the wrapper with JS_WrapValue, and thus can be
// useful even if wrapper already points to newTarget.

View File

@ -6229,7 +6229,7 @@ static bool NukeAllCCWs(JSContext* cx, unsigned argc, Value* vp) {
return false;
}
NukeCrossCompartmentWrappers(cx, AllCompartments(), cx->compartment(),
NukeCrossCompartmentWrappers(cx, AllCompartments(), cx->realm(),
NukeWindowReferences, NukeAllReferences);
args.rval().setUndefined();
return true;
@ -8490,7 +8490,7 @@ JS_FN_HELP("parseBin", BinParse, 1, 0,
JS_FN_HELP("nukeAllCCWs", NukeAllCCWs, 0, 0,
"nukeAllCCWs()",
" Like nukeCCW, but for all CrossCompartmentWrappers targeting the current compartment."),
" Like nukeCCW, but for all CrossCompartmentWrappers targeting the current realm."),
JS_FN_HELP("recomputeWrappers", RecomputeWrappers, 2, 0,
"recomputeWrappers([src, [target]])",

View File

@ -224,6 +224,17 @@ bool Compartment::getNonWrapperObjectForCurrentCompartment(
return true;
}
// Disallow creating new wrappers if we nuked the object's realm or the
// current compartment.
if (!AllowNewWrapper(this, obj)) {
JSObject* res = NewDeadProxyObject(cx);
if (!res) {
return false;
}
obj.set(res);
return true;
}
// Invoke the prewrap callback. The prewrap callback is responsible for
// doing similar reification as above, but can account for any additional
// embedder requirements.

View File

@ -446,6 +446,12 @@ class JS::Compartment {
bool hasEnteredRealm = false;
} gcState;
// True if all outgoing wrappers have been nuked. This happens when all realms
// have been nuked and NukeCrossCompartmentWrappers is called with the
// NukeAllReferences option. This prevents us from creating new wrappers for
// the compartment.
bool nukedOutgoingWrappers = false;
JS::Zone* zone() { return zone_; }
const JS::Zone* zone() const { return zone_; }

View File

@ -441,6 +441,11 @@ class JS::Realm : public JS::shadow::Realm {
bool firedOnNewGlobalObject = false;
#endif
// True if all incoming wrappers have been nuked. This happens when
// NukeCrossCompartmentWrappers is called with the NukeAllReferences option.
// This prevents us from creating new wrappers for the compartment.
bool nukedIncomingWrappers = false;
private:
void updateDebuggerObservesFlag(unsigned flag);

View File

@ -2077,7 +2077,7 @@ nsXPCComponents_Utils::NukeSandbox(HandleValue obj, JSContext* cx) {
RootedObject sb(cx, UncheckedUnwrap(wrapper));
NS_ENSURE_TRUE(IsSandbox(sb), NS_ERROR_INVALID_ARG);
xpc::NukeAllWrappersForCompartment(cx, GetObjectCompartment(sb));
xpc::NukeAllWrappersForRealm(cx, GetNonCCWObjectRealm(sb));
return NS_OK;
}

View File

@ -195,7 +195,6 @@ CompartmentPrivate::CompartmentPrivate(JS::Compartment* c,
isSandboxCompartment(false),
universalXPConnectEnabled(false),
forcePermissiveCOWs(false),
wasNuked(false),
mWrappedJSMap(JSObject2WrappedJSMap::newMap(XPC_JS_MAP_LENGTH)) {
MOZ_COUNT_CTOR(xpc::CompartmentPrivate);
mozilla::PodArrayZero(wrapperDenialWarnings);
@ -590,36 +589,26 @@ nsGlobalWindowInner* CurrentWindowOrNull(JSContext* cx) {
return glob ? WindowOrNull(glob) : nullptr;
}
// Nukes all wrappers into or out of the given compartment, and prevents new
// wrappers from being created. Additionally marks the compartment as
// Nukes all wrappers into or out of the given realm, and prevents new
// wrappers from being created. Additionally marks the realm as
// unscriptable after wrappers have been nuked.
//
// Note: This should *only* be called for browser or extension compartments.
// Note: This should *only* be called for browser or extension realms.
// Wrappers between web compartments must never be cut in web-observable
// ways.
void NukeAllWrappersForCompartment(
JSContext* cx, JS::Compartment* compartment,
void NukeAllWrappersForRealm(
JSContext* cx, JS::Realm* realm,
js::NukeReferencesToWindow nukeReferencesToWindow) {
// First, nuke all wrappers into or out of the target compartment. Once
// the compartment is marked as nuked, WrapperFactory will refuse to
// create new live wrappers for it, in either direction. This means that
// we need to be sure that we don't have any existing cross-compartment
// wrappers which may be replaced with dead wrappers during unrelated
// wrapper recomputation *before* we set that bit.
js::NukeCrossCompartmentWrappers(cx, js::AllCompartments(), compartment,
// We do the following:
// * Nuke all wrappers into the realm.
// * Nuke all wrappers out of the realm's compartment, once we have nuked all
// realms in it.
js::NukeCrossCompartmentWrappers(cx, js::AllCompartments(), realm,
nukeReferencesToWindow,
js::NukeAllReferences);
// At this point, we should cross-compartment wrappers for the nuked
// compartment. Set the wasNuked bit so WrapperFactory will return a
// DeadObjectProxy when asked to create a new wrapper for it, and mark as
// unscriptable.
xpc::CompartmentPrivate::Get(compartment)->wasNuked = true;
auto blockScriptability = [](JSContext*, void*, Handle<Realm*> realm) {
xpc::RealmPrivate::Get(realm)->scriptability.Block();
};
JS::IterateRealmsInCompartment(cx, compartment, nullptr, blockScriptability);
// Mark the realm as unscriptable.
xpc::RealmPrivate::Get(realm)->scriptability.Block();
}
} // namespace xpc

View File

@ -2807,10 +2807,6 @@ class CompartmentPrivate {
// Using it in production is inherently unsafe.
bool forcePermissiveCOWs;
// True if this compartment has been nuked. If true, any wrappers into or
// out of it should be considered invalid.
bool wasNuked;
// Whether we've emitted a warning about a property that was filtered out
// by a security wrapper. See XrayWrapper.cpp.
bool wrapperDenialWarnings[WrapperDenialTypeCount];

View File

@ -409,10 +409,9 @@ bool StringToJsval(JSContext* cx, mozilla::dom::DOMString& str,
nsIPrincipal* GetCompartmentPrincipal(JS::Compartment* compartment);
nsIPrincipal* GetRealmPrincipal(JS::Realm* realm);
void NukeAllWrappersForCompartment(
JSContext* cx, JS::Compartment* compartment,
js::NukeReferencesToWindow nukeReferencesToWindow =
js::NukeWindowReferences);
void NukeAllWrappersForRealm(JSContext* cx, JS::Realm* realm,
js::NukeReferencesToWindow nukeReferencesToWindow =
js::NukeWindowReferences);
void SetLocationForGlobal(JSObject* global, const nsACString& location);
void SetLocationForGlobal(JSObject* global, nsIURI* locationURI);

View File

@ -180,23 +180,6 @@ void WrapperFactory::PrepareForWrapping(JSContext* cx, HandleObject scope,
return;
}
// If we've somehow gotten to this point after either the source or target
// compartment has been nuked, return a DeadObjectProxy to prevent further
// access.
// However, we always need to provide live wrappers for ScriptSourceObjects,
// since they're used for cross-compartment cloned scripts, and need to
// remain accessible even after the original compartment has been nuked.
JS::Compartment* origin = js::GetObjectCompartment(obj);
JS::Compartment* target = js::GetObjectCompartment(scope);
if (!JS_IsScriptSourceObject(obj) &&
(CompartmentPrivate::Get(origin)->wasNuked ||
CompartmentPrivate::Get(target)->wasNuked)) {
NS_WARNING("Trying to create a wrapper into or out of a nuked compartment");
retObj.set(JS_NewDeadWrapper(cx));
return;
}
// If we've got a WindowProxy, there's nothing special that needs to be
// done here, and we can move on to the next phase of wrapping. We handle
// this case first to allow us to assert against wrappers below.
@ -348,12 +331,9 @@ static void DEBUG_CheckUnwrapSafety(HandleObject obj,
const js::Wrapper* handler,
JS::Compartment* origin,
JS::Compartment* target) {
if (!JS_IsScriptSourceObject(obj) &&
(CompartmentPrivate::Get(origin)->wasNuked ||
CompartmentPrivate::Get(target)->wasNuked)) {
// If either compartment has already been nuked, we should have returned
// a dead wrapper from our prewrap callback, and this function should
// not be called.
if (!js::AllowNewWrapper(target, obj)) {
// The JS engine should have returned a dead wrapper in this case and we
// shouldn't even get here.
MOZ_ASSERT_UNREACHABLE("CheckUnwrapSafety called for a dead wrapper");
} else if (AccessCheck::isChrome(target) ||
xpc::IsUniversalXPConnectEnabled(target)) {