/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * 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/. */ #include "builtin/FinalizationRegistryObject.h" #include "gc/GC.h" #include "gc/PublicIterators.h" #include "js/friend/WindowProxy.h" // js::IsWindow, js::IsWindowProxy #include "js/Wrapper.h" #include "proxy/DeadObjectProxy.h" #include "proxy/DOMProxy.h" #include "vm/Iteration.h" #include "vm/Runtime.h" #include "vm/WrapperObject.h" #include "gc/Nursery-inl.h" #include "vm/Compartment-inl.h" #include "vm/JSObject-inl.h" #include "vm/Realm-inl.h" using namespace js; #define PIERCE(cx, wrapper, pre, op, post) \ JS_BEGIN_MACRO \ bool ok; \ { \ AutoRealm call(cx, wrappedObject(wrapper)); \ ok = (pre) && (op); \ } \ return ok && (post); \ JS_END_MACRO #define NOTHING (true) static bool MarkAtoms(JSContext* cx, jsid id) { cx->markId(id); return true; } static bool MarkAtoms(JSContext* cx, HandleIdVector ids) { for (size_t i = 0; i < ids.length(); i++) { cx->markId(ids[i]); } return true; } bool CrossCompartmentWrapper::getOwnPropertyDescriptor( JSContext* cx, HandleObject wrapper, HandleId id, MutableHandle> desc) const { PIERCE(cx, wrapper, MarkAtoms(cx, id), Wrapper::getOwnPropertyDescriptor(cx, wrapper, id, desc), cx->compartment()->wrap(cx, desc)); } bool CrossCompartmentWrapper::defineProperty(JSContext* cx, HandleObject wrapper, HandleId id, Handle desc, ObjectOpResult& result) const { Rooted desc2(cx, desc); PIERCE(cx, wrapper, MarkAtoms(cx, id) && cx->compartment()->wrap(cx, &desc2), Wrapper::defineProperty(cx, wrapper, id, desc2, result), NOTHING); } bool CrossCompartmentWrapper::ownPropertyKeys( JSContext* cx, HandleObject wrapper, MutableHandleIdVector props) const { PIERCE(cx, wrapper, NOTHING, Wrapper::ownPropertyKeys(cx, wrapper, props), MarkAtoms(cx, props)); } bool CrossCompartmentWrapper::delete_(JSContext* cx, HandleObject wrapper, HandleId id, ObjectOpResult& result) const { PIERCE(cx, wrapper, MarkAtoms(cx, id), Wrapper::delete_(cx, wrapper, id, result), NOTHING); } bool CrossCompartmentWrapper::getPrototype(JSContext* cx, HandleObject wrapper, MutableHandleObject protop) const { { RootedObject wrapped(cx, wrappedObject(wrapper)); AutoRealm call(cx, wrapped); if (!GetPrototype(cx, wrapped, protop)) { return false; } } return cx->compartment()->wrap(cx, protop); } bool CrossCompartmentWrapper::setPrototype(JSContext* cx, HandleObject wrapper, HandleObject proto, ObjectOpResult& result) const { RootedObject protoCopy(cx, proto); PIERCE(cx, wrapper, cx->compartment()->wrap(cx, &protoCopy), Wrapper::setPrototype(cx, wrapper, protoCopy, result), NOTHING); } bool CrossCompartmentWrapper::getPrototypeIfOrdinary( JSContext* cx, HandleObject wrapper, bool* isOrdinary, MutableHandleObject protop) const { { RootedObject wrapped(cx, wrappedObject(wrapper)); AutoRealm call(cx, wrapped); if (!GetPrototypeIfOrdinary(cx, wrapped, isOrdinary, protop)) { return false; } if (!*isOrdinary) { return true; } } return cx->compartment()->wrap(cx, protop); } bool CrossCompartmentWrapper::setImmutablePrototype(JSContext* cx, HandleObject wrapper, bool* succeeded) const { PIERCE(cx, wrapper, NOTHING, Wrapper::setImmutablePrototype(cx, wrapper, succeeded), NOTHING); } bool CrossCompartmentWrapper::preventExtensions(JSContext* cx, HandleObject wrapper, ObjectOpResult& result) const { PIERCE(cx, wrapper, NOTHING, Wrapper::preventExtensions(cx, wrapper, result), NOTHING); } bool CrossCompartmentWrapper::isExtensible(JSContext* cx, HandleObject wrapper, bool* extensible) const { PIERCE(cx, wrapper, NOTHING, Wrapper::isExtensible(cx, wrapper, extensible), NOTHING); } bool CrossCompartmentWrapper::has(JSContext* cx, HandleObject wrapper, HandleId id, bool* bp) const { PIERCE(cx, wrapper, MarkAtoms(cx, id), Wrapper::has(cx, wrapper, id, bp), NOTHING); } bool CrossCompartmentWrapper::hasOwn(JSContext* cx, HandleObject wrapper, HandleId id, bool* bp) const { PIERCE(cx, wrapper, MarkAtoms(cx, id), Wrapper::hasOwn(cx, wrapper, id, bp), NOTHING); } static bool WrapReceiver(JSContext* cx, HandleObject wrapper, MutableHandleValue receiver) { // Usually the receiver is the wrapper and we can just unwrap it. If the // wrapped object is also a wrapper, things are more complicated and we // fall back to the slow path (it calls UncheckedUnwrap to unwrap all // wrappers). if (ObjectValue(*wrapper) == receiver) { JSObject* wrapped = Wrapper::wrappedObject(wrapper); if (!IsWrapper(wrapped)) { MOZ_ASSERT(wrapped->compartment() == cx->compartment()); MOZ_ASSERT(!IsWindow(wrapped)); receiver.setObject(*wrapped); return true; } } return cx->compartment()->wrap(cx, receiver); } bool CrossCompartmentWrapper::get(JSContext* cx, HandleObject wrapper, HandleValue receiver, HandleId id, MutableHandleValue vp) const { RootedValue receiverCopy(cx, receiver); { AutoRealm call(cx, wrappedObject(wrapper)); if (!MarkAtoms(cx, id) || !WrapReceiver(cx, wrapper, &receiverCopy)) { return false; } if (!Wrapper::get(cx, wrapper, receiverCopy, id, vp)) { return false; } } return cx->compartment()->wrap(cx, vp); } bool CrossCompartmentWrapper::set(JSContext* cx, HandleObject wrapper, HandleId id, HandleValue v, HandleValue receiver, ObjectOpResult& result) const { RootedValue valCopy(cx, v); RootedValue receiverCopy(cx, receiver); PIERCE(cx, wrapper, MarkAtoms(cx, id) && cx->compartment()->wrap(cx, &valCopy) && WrapReceiver(cx, wrapper, &receiverCopy), Wrapper::set(cx, wrapper, id, valCopy, receiverCopy, result), NOTHING); } bool CrossCompartmentWrapper::getOwnEnumerablePropertyKeys( JSContext* cx, HandleObject wrapper, MutableHandleIdVector props) const { PIERCE(cx, wrapper, NOTHING, Wrapper::getOwnEnumerablePropertyKeys(cx, wrapper, props), MarkAtoms(cx, props)); } bool CrossCompartmentWrapper::enumerate(JSContext* cx, HandleObject wrapper, MutableHandleIdVector props) const { PIERCE(cx, wrapper, NOTHING, Wrapper::enumerate(cx, wrapper, props), MarkAtoms(cx, props)); } bool CrossCompartmentWrapper::call(JSContext* cx, HandleObject wrapper, const CallArgs& args) const { RootedObject wrapped(cx, wrappedObject(wrapper)); { AutoRealm call(cx, wrapped); args.setCallee(ObjectValue(*wrapped)); if (!cx->compartment()->wrap(cx, args.mutableThisv())) { return false; } for (size_t n = 0; n < args.length(); ++n) { if (!cx->compartment()->wrap(cx, args[n])) { return false; } } if (!Wrapper::call(cx, wrapper, args)) { return false; } } return cx->compartment()->wrap(cx, args.rval()); } bool CrossCompartmentWrapper::construct(JSContext* cx, HandleObject wrapper, const CallArgs& args) const { RootedObject wrapped(cx, wrappedObject(wrapper)); { AutoRealm call(cx, wrapped); for (size_t n = 0; n < args.length(); ++n) { if (!cx->compartment()->wrap(cx, args[n])) { return false; } } if (!cx->compartment()->wrap(cx, args.newTarget())) { return false; } if (!Wrapper::construct(cx, wrapper, args)) { return false; } } return cx->compartment()->wrap(cx, args.rval()); } bool CrossCompartmentWrapper::nativeCall(JSContext* cx, IsAcceptableThis test, NativeImpl impl, const CallArgs& srcArgs) const { RootedObject wrapper(cx, &srcArgs.thisv().toObject()); MOZ_ASSERT(srcArgs.thisv().isMagic(JS_IS_CONSTRUCTING) || !UncheckedUnwrap(wrapper)->is()); RootedObject wrapped(cx, wrappedObject(wrapper)); { AutoRealm call(cx, wrapped); InvokeArgs dstArgs(cx); if (!dstArgs.init(cx, srcArgs.length())) { return false; } Value* src = srcArgs.base(); Value* srcend = srcArgs.array() + srcArgs.length(); Value* dst = dstArgs.base(); RootedValue source(cx); for (; src < srcend; ++src, ++dst) { source = *src; if (!cx->compartment()->wrap(cx, &source)) { return false; } *dst = source.get(); // Handle |this| specially. When we rewrap on the other side of the // membrane, we might apply a same-compartment security wrapper that // will stymie this whole process. If that happens, unwrap the wrapper. // This logic can go away when same-compartment security wrappers go away. if ((src == srcArgs.base() + 1) && dst->isObject()) { RootedObject thisObj(cx, &dst->toObject()); if (thisObj->is() && Wrapper::wrapperHandler(thisObj)->hasSecurityPolicy()) { MOZ_ASSERT(!thisObj->is()); *dst = ObjectValue(*Wrapper::wrappedObject(thisObj)); } } } if (!CallNonGenericMethod(cx, test, impl, dstArgs)) { return false; } srcArgs.rval().set(dstArgs.rval()); } return cx->compartment()->wrap(cx, srcArgs.rval()); } const char* CrossCompartmentWrapper::className(JSContext* cx, HandleObject wrapper) const { AutoRealm call(cx, wrappedObject(wrapper)); return Wrapper::className(cx, wrapper); } JSString* CrossCompartmentWrapper::fun_toString(JSContext* cx, HandleObject wrapper, bool isToSource) const { RootedString str(cx); { AutoRealm call(cx, wrappedObject(wrapper)); str = Wrapper::fun_toString(cx, wrapper, isToSource); if (!str) { return nullptr; } } if (!cx->compartment()->wrap(cx, &str)) { return nullptr; } return str; } RegExpShared* CrossCompartmentWrapper::regexp_toShared( JSContext* cx, HandleObject wrapper) const { RootedRegExpShared re(cx); { AutoRealm call(cx, wrappedObject(wrapper)); re = Wrapper::regexp_toShared(cx, wrapper); if (!re) { return nullptr; } } // Get an equivalent RegExpShared associated with the current compartment. Rooted source(cx, re->getSource()); cx->markAtom(source); return cx->zone()->regExps().get(cx, source, re->getFlags()); } bool CrossCompartmentWrapper::boxedValue_unbox(JSContext* cx, HandleObject wrapper, MutableHandleValue vp) const { PIERCE(cx, wrapper, NOTHING, Wrapper::boxedValue_unbox(cx, wrapper, vp), cx->compartment()->wrap(cx, vp)); } const CrossCompartmentWrapper CrossCompartmentWrapper::singleton(0u); JS_PUBLIC_API void js::NukeCrossCompartmentWrapper(JSContext* cx, JSObject* wrapper) { JS::Compartment* comp = wrapper->compartment(); auto ptr = comp->lookupWrapper(Wrapper::wrappedObject(wrapper)); if (ptr) { comp->removeWrapper(ptr); } NukeRemovedCrossCompartmentWrapper(cx, wrapper); } JS_PUBLIC_API void js::NukeCrossCompartmentWrapperIfExists( JSContext* cx, JS::Compartment* source, JSObject* target) { MOZ_ASSERT(source != target->compartment()); MOZ_ASSERT(!target->is()); auto ptr = source->lookupWrapper(target); if (ptr) { JSObject* wrapper = ptr->value().get(); NukeCrossCompartmentWrapper(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; } /* * NukeCrossCompartmentWrappers invalidates all cross-compartment wrappers * that point to objects in the |target| realm. * * There is some complexity in targeting to preserve semantics which requires * the filtering and behavioural options: * * - |sourceFilter| limits the compartments searched for source pointers * - |nukeReferencesToWindow| will, if set to DontNukeWindowReferences skip * wrappers whose target is the window proxy of the target realm. * - |nukeReferencesFromTarget| will, when set to NukeAllReferences, disallow * the creation of new wrappers to the target realm. This option can also * allow more wrappers to be cleaned up transitively. */ JS_PUBLIC_API bool js::NukeCrossCompartmentWrappers( 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 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->compartment() == c.get() && NukedAllRealms(c.get())); // Iterate only the wrappers that have target compartment matched unless // |nukeAll| is true. Use Maybe to avoid copying from conditionally // initializing ObjectWrapperEnum. mozilla::Maybe e; if (MOZ_LIKELY(!nukeAll)) { e.emplace(c, target->compartment()); } else { e.emplace(c); c.get()->nukedOutgoingWrappers = true; } for (; !e->empty(); e->popFront()) { JSObject* key = e->front().key(); AutoWrapperRooter wobj(cx, WrapperValue(*e)); // Unwrap from the wrapped object in key instead of the wrapper, this // could save us a bit of time. JSObject* wrapped = UncheckedUnwrap(key); // 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 only skip nuking window references that point to a target // compartment, not the ones that belong to it. if (nukeReferencesToWindow == DontNukeWindowReferences && MOZ_LIKELY(!nukeAll) && IsWindowProxy(wrapped)) { continue; } // Now this is the wrapper we want to nuke. e->removeFront(); NukeRemovedCrossCompartmentWrapper(cx, wobj); } } return true; } JS_PUBLIC_API bool js::AllowNewWrapper(JS::Compartment* target, JSObject* obj) { // Disallow creating new wrappers if we nuked the object realm or target // compartment. MOZ_ASSERT(obj->compartment() != target); if (target->nukedOutgoingWrappers || obj->nonCCWRealm()->nukedIncomingWrappers) { return false; } return true; } JS_PUBLIC_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. // This operation crashes on failure rather than leaving the heap in an // inconsistent state. void js::RemapWrapper(JSContext* cx, JSObject* wobjArg, JSObject* newTargetArg) { RootedObject wobj(cx, wobjArg); RootedObject newTarget(cx, newTargetArg); MOZ_ASSERT(wobj->is()); MOZ_ASSERT(!newTarget->is()); JSObject* origTarget = Wrapper::wrappedObject(wobj); MOZ_ASSERT(origTarget); JS::Compartment* wcompartment = wobj->compartment(); MOZ_ASSERT(wcompartment != newTarget->compartment()); AutoDisableProxyCheck adpc; // If we're mapping to a different target (as opposed to just recomputing // for the same target), we must not have an existing wrapper for the new // target, otherwise this will break. MOZ_ASSERT_IF(origTarget != newTarget, !wcompartment->lookupWrapper(newTarget)); // The old value should still be in the cross-compartment wrapper map, and // the lookup should return wobj. ObjectWrapperMap::Ptr p = wcompartment->lookupWrapper(origTarget); MOZ_ASSERT(*p->value().unsafeGet() == wobj); wcompartment->removeWrapper(p); // When we remove origv from the wrapper map, its wrapper, wobj, must // immediately cease to be a cross-compartment wrapper. Nuke it. NukeCrossCompartmentWrapper(cx, wobj); // If the target is a dead wrapper, and we're just fixing wrappers for // it, then we're done now that the CCW is a dead wrapper. if (JS_IsDeadWrapper(origTarget)) { MOZ_RELEASE_ASSERT(origTarget == newTarget); return; } js::RemapDeadWrapper(cx, wobj, newTarget); } // Given a dead proxy object |wobj|, turn it into a cross-compartment wrapper // pointing at |newTarget|. // This operation crashes on failure rather than leaving the heap in an // inconsistent state. void js::RemapDeadWrapper(JSContext* cx, HandleObject wobj, HandleObject newTarget) { MOZ_ASSERT(IsDeadProxyObject(wobj)); MOZ_ASSERT(!newTarget->is()); // These are not exposed. Doing this would require updating the // FinalizationObservers data structures. MOZ_ASSERT(!newTarget->is()); AutoDisableProxyCheck adpc; // wobj is not a cross-compartment wrapper, so we can use nonCCWRealm. Realm* wrealm = wobj->nonCCWRealm(); // First, we wrap it in the new compartment. We try to use the existing // wrapper, |wobj|, since it's been nuked anyway. The rewrap() function has // the choice to reuse |wobj| or not. RootedObject tobj(cx, newTarget); AutoRealmUnchecked ar(cx, wrealm); AutoEnterOOMUnsafeRegion oomUnsafe; JS::Compartment* wcompartment = wobj->compartment(); if (!wcompartment->rewrap(cx, &tobj, wobj)) { oomUnsafe.crash("js::RemapWrapper"); } // If rewrap() reused |wobj|, it will have overwritten it and returned with // |tobj == wobj|. Otherwise, |tobj| will point to a new wrapper and |wobj| // will still be nuked. In the latter case, we replace |wobj| with the // contents of the new wrapper in |tobj|. if (tobj != wobj) { // Now, because we need to maintain object identity, we do a brain // transplant on the old object so that it contains the contents of the // new one. JSObject::swap(cx, wobj, tobj, oomUnsafe); } if (!wobj->is()) { MOZ_ASSERT(js::IsDOMRemoteProxyObject(wobj) || IsDeadProxyObject(wobj)); return; } // Before swapping, this wrapper came out of rewrap(), which enforces the // invariant that the wrapper in the map points directly to the key. MOZ_ASSERT(Wrapper::wrappedObject(wobj) == newTarget); // Update the entry in the compartment's wrapper map to point to the old // wrapper, which has now been updated (via reuse or swap). if (!wcompartment->putWrapper(cx, newTarget, wobj)) { oomUnsafe.crash("js::RemapWrapper"); } } // Remap all cross-compartment wrappers pointing to |oldTarget| to point to // |newTarget|. All wrappers are recomputed. JS_PUBLIC_API bool js::RemapAllWrappersForObject(JSContext* cx, HandleObject oldTarget, HandleObject newTarget) { AutoWrapperVector toTransplant(cx); for (CompartmentsIter c(cx->runtime()); !c.done(); c.next()) { if (ObjectWrapperMap::Ptr wp = c->lookupWrapper(oldTarget)) { // We found a wrapper. Remember and root it. if (!toTransplant.append(WrapperValue(wp))) { return false; } } } for (const WrapperValue& v : toTransplant) { RemapWrapper(cx, v, newTarget); } return true; } JS_PUBLIC_API bool js::RecomputeWrappers( JSContext* cx, const CompartmentFilter& sourceFilter, const CompartmentFilter& targetFilter) { bool evictedNursery = false; AutoWrapperVector toRecompute(cx); for (CompartmentsIter c(cx->runtime()); !c.done(); c.next()) { // Filter by source compartment. if (!sourceFilter.match(c)) { continue; } if (!evictedNursery && c->hasNurseryAllocatedObjectWrapperEntries(targetFilter)) { cx->runtime()->gc.evictNursery(); evictedNursery = true; } // Iterate over object wrappers, filtering appropriately. for (Compartment::ObjectWrapperEnum e(c, targetFilter); !e.empty(); e.popFront()) { // Don't remap wrappers to finalization record objects. These are used // internally and are not exposed. JSObject* wrapper = *e.front().value().unsafeGet(); if (Wrapper::wrappedObject(wrapper)->is()) { continue; } // Add the wrapper to the list. if (!toRecompute.append(WrapperValue(e))) { return false; } } } // Recompute all the wrappers in the list. for (const WrapperValue& wrapper : toRecompute) { JSObject* wrapped = Wrapper::wrappedObject(wrapper); RemapWrapper(cx, wrapper, wrapped); } return true; }