diff options
Diffstat (limited to 'js/src/vm/Compartment.cpp')
-rw-r--r-- | js/src/vm/Compartment.cpp | 569 |
1 files changed, 569 insertions, 0 deletions
diff --git a/js/src/vm/Compartment.cpp b/js/src/vm/Compartment.cpp new file mode 100644 index 0000000000..59f2112746 --- /dev/null +++ b/js/src/vm/Compartment.cpp @@ -0,0 +1,569 @@ +/* -*- 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 "vm/Compartment-inl.h" + +#include "mozilla/MemoryReporting.h" + +#include <stddef.h> + +#include "jsfriendapi.h" + +#include "debugger/DebugAPI.h" +#include "gc/Policy.h" +#include "gc/PublicIterators.h" +#include "gc/Zone.h" +#include "js/Date.h" +#include "js/friend/StackLimits.h" // js::CheckSystemRecursionLimit +#include "js/friend/WindowProxy.h" // js::IsWindow, js::IsWindowProxy, js::ToWindowProxyIfWindow +#include "js/Proxy.h" +#include "js/RootingAPI.h" +#include "js/StableStringChars.h" +#include "js/Wrapper.h" +#include "proxy/DeadObjectProxy.h" +#include "proxy/DOMProxy.h" +#include "vm/Iteration.h" +#include "vm/JSContext.h" +#include "vm/WrapperObject.h" + +#include "gc/GC-inl.h" +#include "gc/Marking-inl.h" +#include "gc/WeakMap-inl.h" +#include "vm/JSAtom-inl.h" +#include "vm/JSFunction-inl.h" +#include "vm/JSObject-inl.h" +#include "vm/JSScript-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +using JS::AutoStableStringChars; + +Compartment::Compartment(Zone* zone, bool invisibleToDebugger) + : zone_(zone), + runtime_(zone->runtimeFromAnyThread()), + invisibleToDebugger_(invisibleToDebugger), + crossCompartmentObjectWrappers(zone, 0), + realms_(zone) {} + +#ifdef JSGC_HASH_TABLE_CHECKS + +void Compartment::checkObjectWrappersAfterMovingGC() { + for (ObjectWrapperEnum e(this); !e.empty(); e.popFront()) { + // Assert that the postbarriers have worked and that nothing is left in the + // wrapper map that points into the nursery, and that the hash table entries + // are discoverable. + auto key = e.front().key(); + CheckGCThingAfterMovingGC(key); + + auto ptr = crossCompartmentObjectWrappers.lookup(key); + MOZ_RELEASE_ASSERT(ptr.found() && &*ptr == &e.front()); + } +} + +#endif // JSGC_HASH_TABLE_CHECKS + +bool Compartment::putWrapper(JSContext* cx, JSObject* wrapped, + JSObject* wrapper) { + MOZ_ASSERT(!js::IsProxy(wrapper) || js::GetProxyHandler(wrapper)->family() != + js::GetDOMRemoteProxyHandlerFamily()); + + if (!crossCompartmentObjectWrappers.put(wrapped, wrapper)) { + ReportOutOfMemory(cx); + return false; + } + + return true; +} + +bool Compartment::putWrapper(JSContext* cx, JSString* wrapped, + JSString* wrapper) { + if (!zone()->crossZoneStringWrappers().put(wrapped, wrapper)) { + ReportOutOfMemory(cx); + return false; + } + + return true; +} + +void Compartment::removeWrapper(js::ObjectWrapperMap::Ptr p) { + JSObject* key = p->key(); + JSObject* value = p->value().unbarrieredGet(); + if (js::gc::detail::GetDelegate(value) == key) { + key->zone()->beforeClearDelegate(value, key); + } + + crossCompartmentObjectWrappers.remove(p); +} + +static JSString* CopyStringPure(JSContext* cx, JSString* str) { + /* + * Directly allocate the copy in the destination compartment, rather than + * first flattening it (and possibly allocating in source compartment), + * because we don't know whether the flattening will pay off later. + */ + + size_t len = str->length(); + JSString* copy; + if (str->isLinear()) { + /* Only use AutoStableStringChars if the NoGC allocation fails. */ + if (str->hasLatin1Chars()) { + JS::AutoCheckCannotGC nogc; + copy = NewStringCopyN<NoGC>(cx, str->asLinear().latin1Chars(nogc), len); + } else { + JS::AutoCheckCannotGC nogc; + copy = NewStringCopyNDontDeflate<NoGC>( + cx, str->asLinear().twoByteChars(nogc), len); + } + if (copy) { + return copy; + } + + AutoStableStringChars chars(cx); + if (!chars.init(cx, str)) { + return nullptr; + } + + return chars.isLatin1() ? NewStringCopyN<CanGC>( + cx, chars.latin1Range().begin().get(), len) + : NewStringCopyNDontDeflate<CanGC>( + cx, chars.twoByteRange().begin().get(), len); + } + + if (str->hasLatin1Chars()) { + UniquePtr<Latin1Char[], JS::FreePolicy> copiedChars = + str->asRope().copyLatin1Chars(cx, js::StringBufferArena); + if (!copiedChars) { + return nullptr; + } + + return NewString<CanGC>(cx, std::move(copiedChars), len); + } + + UniqueTwoByteChars copiedChars = + str->asRope().copyTwoByteChars(cx, js::StringBufferArena); + if (!copiedChars) { + return nullptr; + } + + return NewStringDontDeflate<CanGC>(cx, std::move(copiedChars), len); +} + +bool Compartment::wrap(JSContext* cx, MutableHandleString strp) { + MOZ_ASSERT(cx->compartment() == this); + + /* If the string is already in this compartment, we are done. */ + JSString* str = strp; + if (str->zoneFromAnyThread() == zone()) { + return true; + } + + /* + * If the string is an atom, we don't have to copy, but we do need to mark + * the atom as being in use by the new zone. + */ + if (str->isAtom()) { + cx->markAtom(&str->asAtom()); + return true; + } + + /* Check the cache. */ + if (StringWrapperMap::Ptr p = lookupWrapper(str)) { + strp.set(p->value().get()); + return true; + } + + /* No dice. Make a copy, and cache it. */ + JSString* copy = CopyStringPure(cx, str); + if (!copy) { + return false; + } + if (!putWrapper(cx, strp, copy)) { + return false; + } + + strp.set(copy); + return true; +} + +bool Compartment::wrap(JSContext* cx, MutableHandleBigInt bi) { + MOZ_ASSERT(cx->compartment() == this); + + if (bi->zone() == cx->zone()) { + return true; + } + + BigInt* copy = BigInt::copy(cx, bi); + if (!copy) { + return false; + } + bi.set(copy); + return true; +} + +bool Compartment::getNonWrapperObjectForCurrentCompartment( + JSContext* cx, HandleObject origObj, MutableHandleObject obj) { + // Ensure that we have entered a realm. + MOZ_ASSERT(cx->global()); + + // If we have a cross-compartment wrapper, make sure that the cx isn't + // associated with the self-hosting zone. We don't want to create + // wrappers for objects in other runtimes, which may be the case for the + // self-hosting zone. + MOZ_ASSERT(!cx->zone()->isSelfHostingZone()); + MOZ_ASSERT(!obj->zone()->isSelfHostingZone()); + + // The object is already in the right compartment. Normally same- + // compartment returns the object itself, however, windows are always + // wrapped by a proxy, so we have to check for that case here manually. + if (obj->compartment() == this) { + obj.set(ToWindowProxyIfWindow(obj)); + return true; + } + + // Note that if the object is same-compartment, but has been wrapped into a + // different compartment, we need to unwrap it and return the bare same- + // compartment object. Note again that windows are always wrapped by a + // WindowProxy even when same-compartment so take care not to strip this + // particular wrapper. + RootedObject objectPassedToWrap(cx, obj); + obj.set(UncheckedUnwrap(obj, /* stopAtWindowProxy = */ true)); + if (obj->compartment() == this) { + MOZ_ASSERT(!IsWindow(obj)); + return true; + } + + // Disallow creating new wrappers if we nuked the object's realm or the + // current compartment. + if (!AllowNewWrapper(this, obj)) { + obj.set(NewDeadProxyObject(cx, IsCallableFlag(obj->isCallable()), + IsConstructorFlag(obj->isConstructor()))); + return !!obj; + } + + // Use the WindowProxy instead of the Window here, so that we don't have to + // deal with this in the rest of the wrapping code. + if (IsWindow(obj)) { + obj.set(ToWindowProxyIfWindow(obj)); + + // ToWindowProxyIfWindow can return a CCW if |obj| was a navigated-away-from + // Window. Strip any CCWs. + obj.set(UncheckedUnwrap(obj)); + + if (JS_IsDeadWrapper(obj)) { + obj.set(NewDeadProxyObject(cx, obj)); + return !!obj; + } + + MOZ_ASSERT(IsWindowProxy(obj) || IsDOMRemoteProxyObject(obj)); + + // We crossed a compartment boundary there, so may now have a gray object. + // This function is not allowed to return gray objects, so don't do that. + ExposeObjectToActiveJS(obj); + } + + // If the object is a dead wrapper, return a new dead wrapper rather than + // trying to wrap it for a different compartment. + if (JS_IsDeadWrapper(obj)) { + obj.set(NewDeadProxyObject(cx, obj)); + return !!obj; + } + + // Invoke the prewrap callback. The prewrap callback is responsible for + // doing similar reification as above, but can account for any additional + // embedder requirements. + // + // We're a bit worried about infinite recursion here, so we do a check - + // see bug 809295. + auto preWrap = cx->runtime()->wrapObjectCallbacks->preWrap; + if (!CheckSystemRecursionLimit(cx)) { + return false; + } + if (preWrap) { + preWrap(cx, cx->global(), origObj, obj, objectPassedToWrap, obj); + if (!obj) { + return false; + } + } + MOZ_ASSERT(!IsWindow(obj)); + + return true; +} + +bool Compartment::getOrCreateWrapper(JSContext* cx, HandleObject existing, + MutableHandleObject obj) { + // If we already have a wrapper for this value, use it. + if (ObjectWrapperMap::Ptr p = lookupWrapper(obj)) { + obj.set(p->value().get()); + MOZ_ASSERT(obj->is<CrossCompartmentWrapperObject>()); + return true; + } + + // Ensure that the wrappee is exposed in case we are creating a new wrapper + // for a gray object. + ExposeObjectToActiveJS(obj); + + // Create a new wrapper for the object. + auto wrap = cx->runtime()->wrapObjectCallbacks->wrap; + RootedObject wrapper(cx, wrap(cx, existing, obj)); + if (!wrapper) { + return false; + } + + // We maintain the invariant that the key in the cross-compartment wrapper + // map is always directly wrapped by the value. + MOZ_ASSERT(Wrapper::wrappedObject(wrapper) == obj); + + if (!putWrapper(cx, obj, wrapper)) { + // Enforce the invariant that all cross-compartment wrapper object are + // in the map by nuking the wrapper if we couldn't add it. + // Unfortunately it's possible for the wrapper to still be marked if we + // took this path, for example if the object metadata callback stashes a + // reference to it. + if (wrapper->is<CrossCompartmentWrapperObject>()) { + NukeCrossCompartmentWrapper(cx, wrapper); + } + return false; + } + + obj.set(wrapper); + return true; +} + +bool Compartment::wrap(JSContext* cx, MutableHandleObject obj) { + MOZ_ASSERT(cx->compartment() == this); + + if (!obj) { + return true; + } + + AutoDisableProxyCheck adpc; + + // Anything we're wrapping has already escaped into script, so must have + // been unmarked-gray at some point in the past. + JS::AssertObjectIsNotGray(obj); + + // The passed object may already be wrapped, or may fit a number of special + // cases that we need to check for and manually correct. + if (!getNonWrapperObjectForCurrentCompartment(cx, /* origObj = */ nullptr, + obj)) { + return false; + } + + // If the reification above did not result in a same-compartment object, + // get or create a new wrapper object in this compartment for it. + if (obj->compartment() != this) { + if (!getOrCreateWrapper(cx, nullptr, obj)) { + return false; + } + } + + // Ensure that the wrapper is also exposed. + ExposeObjectToActiveJS(obj); + return true; +} + +bool Compartment::rewrap(JSContext* cx, MutableHandleObject obj, + HandleObject existingArg) { + MOZ_ASSERT(cx->compartment() == this); + MOZ_ASSERT(obj); + MOZ_ASSERT(existingArg); + MOZ_ASSERT(existingArg->compartment() == cx->compartment()); + MOZ_ASSERT(IsDeadProxyObject(existingArg)); + + AutoDisableProxyCheck adpc; + + // It may not be possible to re-use existing; if so, clear it so that we + // are forced to create a new wrapper. Note that this cannot call out to + // |wrap| because of the different gray unmarking semantics. + RootedObject existing(cx, existingArg); + if (existing->hasStaticPrototype() || + // Note: Class asserted above, so all that's left to check is callability + existing->isCallable() || obj->isCallable()) { + existing.set(nullptr); + } + + // The passed object may already be wrapped, or may fit a number of special + // cases that we need to check for and manually correct. We pass in + // |existingArg| instead of |existing|, because the purpose is to get the + // address of the object we are transplanting onto, not to find a wrapper + // to reuse. + if (!getNonWrapperObjectForCurrentCompartment(cx, existingArg, obj)) { + return false; + } + + // If the reification above resulted in a same-compartment object, we do + // not need to create or return an existing wrapper. + if (obj->compartment() == this) { + return true; + } + + return getOrCreateWrapper(cx, existing, obj); +} + +bool Compartment::wrap(JSContext* cx, + MutableHandle<JS::PropertyDescriptor> desc) { + if (!wrap(cx, desc.object())) { + return false; + } + + if (desc.hasGetterObject()) { + if (!wrap(cx, desc.getterObject())) { + return false; + } + } + if (desc.hasSetterObject()) { + if (!wrap(cx, desc.setterObject())) { + return false; + } + } + + return wrap(cx, desc.value()); +} + +bool Compartment::wrap(JSContext* cx, MutableHandle<GCVector<Value>> vec) { + for (size_t i = 0; i < vec.length(); ++i) { + if (!wrap(cx, vec[i])) { + return false; + } + } + return true; +} + +static inline bool ShouldTraceWrapper(JSObject* wrapper, + Compartment::EdgeSelector whichEdges) { + if (whichEdges == Compartment::AllEdges) { + return true; + } + + bool isGray = wrapper->isMarkedGray(); + return (whichEdges == Compartment::NonGrayEdges && !isGray) || + (whichEdges == Compartment::GrayEdges && isGray); +} + +void Compartment::traceWrapperTargetsInCollectedZones(JSTracer* trc, + EdgeSelector whichEdges) { + // Trace cross compartment wrapper private pointers into collected zones to + // either mark or update them. Wrapped object pointers are updated by + // sweepCrossCompartmentObjectWrappers(). + + MOZ_ASSERT(JS::RuntimeHeapIsMajorCollecting()); + MOZ_ASSERT(!zone()->isCollectingFromAnyThread() || + trc->runtime()->gc.isHeapCompacting()); + + for (WrappedObjectCompartmentEnum c(this); !c.empty(); c.popFront()) { + Zone* zone = c.front()->zone(); + if (!zone->isCollectingFromAnyThread()) { + continue; + } + + for (ObjectWrapperEnum e(this, c); !e.empty(); e.popFront()) { + JSObject* obj = e.front().value().unbarrieredGet(); + ProxyObject* wrapper = &obj->as<ProxyObject>(); + if (ShouldTraceWrapper(wrapper, whichEdges)) { + ProxyObject::traceEdgeToTarget(trc, wrapper); + } + } + } +} + +/* static */ +void Compartment::traceIncomingCrossCompartmentEdgesForZoneGC( + JSTracer* trc, EdgeSelector whichEdges) { + MOZ_ASSERT(JS::RuntimeHeapIsMajorCollecting()); + + for (ZonesIter zone(trc->runtime(), SkipAtoms); !zone.done(); zone.next()) { + if (zone->isCollectingFromAnyThread()) { + continue; + } + + for (CompartmentsInZoneIter c(zone); !c.done(); c.next()) { + c->traceWrapperTargetsInCollectedZones(trc, whichEdges); + } + } + + // Currently we trace all debugger edges as black. + if (whichEdges != GrayEdges) { + DebugAPI::traceCrossCompartmentEdges(trc); + } +} + +void Compartment::sweepAfterMinorGC(JSTracer* trc) { + crossCompartmentObjectWrappers.sweepAfterMinorGC(trc); + + for (RealmsInCompartmentIter r(this); !r.done(); r.next()) { + r->sweepAfterMinorGC(); + } +} + +// Remove dead wrappers from the table or update pointers to moved objects. +void Compartment::sweepCrossCompartmentObjectWrappers() { + crossCompartmentObjectWrappers.sweep(); +} + +void Compartment::fixupCrossCompartmentObjectWrappersAfterMovingGC( + JSTracer* trc) { + MOZ_ASSERT(trc->runtime()->gc.isHeapCompacting()); + + // Sweep the wrapper map to update keys (wrapped values) in other + // compartments that may have been moved. + sweepCrossCompartmentObjectWrappers(); + + // Trace the wrappers in the map to update their cross-compartment edges + // to wrapped values in other compartments that may have been moved. + traceWrapperTargetsInCollectedZones(trc, AllEdges); +} + +void Compartment::fixupAfterMovingGC(JSTracer* trc) { + MOZ_ASSERT(zone()->isGCCompacting()); + + for (RealmsInCompartmentIter r(this); !r.done(); r.next()) { + r->fixupAfterMovingGC(trc); + } + + // Sweep the wrapper map to update values (wrapper objects) in this + // compartment that may have been moved. + sweepCrossCompartmentObjectWrappers(); +} + +void Compartment::addSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf, + size_t* compartmentObjects, + size_t* crossCompartmentWrappersTables, + size_t* compartmentsPrivateData) { + *compartmentObjects += mallocSizeOf(this); + *crossCompartmentWrappersTables += + crossCompartmentObjectWrappers.sizeOfExcludingThis(mallocSizeOf); + + if (auto callback = runtime_->sizeOfIncludingThisCompartmentCallback) { + *compartmentsPrivateData += callback(mallocSizeOf, this); + } +} + +GlobalObject& Compartment::firstGlobal() const { + for (Realm* realm : realms_) { + if (!realm->hasLiveGlobal()) { + continue; + } + GlobalObject* global = realm->maybeGlobal(); + ExposeObjectToActiveJS(global); + return *global; + } + MOZ_CRASH("If all our globals are dead, why is someone expecting a global?"); +} + +JS_FRIEND_API JSObject* js::GetFirstGlobalInCompartment(JS::Compartment* comp) { + return &comp->firstGlobal(); +} + +JS_FRIEND_API bool js::CompartmentHasLiveGlobal(JS::Compartment* comp) { + MOZ_ASSERT(comp); + for (Realm* r : comp->realms()) { + if (r->hasLiveGlobal()) { + return true; + } + } + return false; +} |