diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /js/src/proxy/CrossCompartmentWrapper.cpp | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'js/src/proxy/CrossCompartmentWrapper.cpp')
-rw-r--r-- | js/src/proxy/CrossCompartmentWrapper.cpp | 685 |
1 files changed, 685 insertions, 0 deletions
diff --git a/js/src/proxy/CrossCompartmentWrapper.cpp b/js/src/proxy/CrossCompartmentWrapper.cpp new file mode 100644 index 000000000..2a6cb5400 --- /dev/null +++ b/js/src/proxy/CrossCompartmentWrapper.cpp @@ -0,0 +1,685 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * 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 "jsiter.h" +#include "jswrapper.h" + +#include "proxy/DeadObjectProxy.h" +#include "vm/WrapperObject.h" + +#include "jscompartmentinlines.h" +#include "jsobjinlines.h" + +using namespace js; + +#define PIERCE(cx, wrapper, pre, op, post) \ + JS_BEGIN_MACRO \ + bool ok; \ + { \ + AutoCompartment call(cx, wrappedObject(wrapper)); \ + ok = (pre) && (op); \ + } \ + return ok && (post); \ + JS_END_MACRO + +#define NOTHING (true) + +bool +CrossCompartmentWrapper::getPropertyDescriptor(JSContext* cx, HandleObject wrapper, HandleId id, + MutableHandle<PropertyDescriptor> desc) const +{ + PIERCE(cx, wrapper, + NOTHING, + Wrapper::getPropertyDescriptor(cx, wrapper, id, desc), + cx->compartment()->wrap(cx, desc)); +} + +bool +CrossCompartmentWrapper::getOwnPropertyDescriptor(JSContext* cx, HandleObject wrapper, HandleId id, + MutableHandle<PropertyDescriptor> desc) const +{ + PIERCE(cx, wrapper, + NOTHING, + Wrapper::getOwnPropertyDescriptor(cx, wrapper, id, desc), + cx->compartment()->wrap(cx, desc)); +} + +bool +CrossCompartmentWrapper::defineProperty(JSContext* cx, HandleObject wrapper, HandleId id, + Handle<PropertyDescriptor> desc, + ObjectOpResult& result) const +{ + Rooted<PropertyDescriptor> desc2(cx, desc); + PIERCE(cx, wrapper, + cx->compartment()->wrap(cx, &desc2), + Wrapper::defineProperty(cx, wrapper, id, desc2, result), + NOTHING); +} + +bool +CrossCompartmentWrapper::ownPropertyKeys(JSContext* cx, HandleObject wrapper, + AutoIdVector& props) const +{ + PIERCE(cx, wrapper, + NOTHING, + Wrapper::ownPropertyKeys(cx, wrapper, props), + NOTHING); +} + +bool +CrossCompartmentWrapper::delete_(JSContext* cx, HandleObject wrapper, HandleId id, + ObjectOpResult& result) const +{ + PIERCE(cx, wrapper, + NOTHING, + Wrapper::delete_(cx, wrapper, id, result), + NOTHING); +} + +bool +CrossCompartmentWrapper::getPrototype(JSContext* cx, HandleObject wrapper, + MutableHandleObject protop) const +{ + { + RootedObject wrapped(cx, wrappedObject(wrapper)); + AutoCompartment call(cx, wrapped); + if (!GetPrototype(cx, wrapped, protop)) + return false; + if (protop) { + if (!protop->setDelegate(cx)) + 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)); + AutoCompartment call(cx, wrapped); + if (!GetPrototypeIfOrdinary(cx, wrapped, isOrdinary, protop)) + return false; + + if (!*isOrdinary) + return true; + + if (protop) { + if (!protop->setDelegate(cx)) + return false; + } + } + + 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, + NOTHING, + Wrapper::has(cx, wrapper, id, bp), + NOTHING); +} + +bool +CrossCompartmentWrapper::hasOwn(JSContext* cx, HandleObject wrapper, HandleId id, bool* bp) const +{ + PIERCE(cx, wrapper, + NOTHING, + 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); + { + AutoCompartment call(cx, wrappedObject(wrapper)); + if (!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, + 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, + AutoIdVector& props) const +{ + PIERCE(cx, wrapper, + NOTHING, + Wrapper::getOwnEnumerablePropertyKeys(cx, wrapper, props), + NOTHING); +} + +/* + * We can reify non-escaping iterator objects instead of having to wrap them. This + * allows fast iteration over objects across a compartment boundary. + */ +static bool +CanReify(HandleObject obj) +{ + return obj->is<PropertyIteratorObject>() && + (obj->as<PropertyIteratorObject>().getNativeIterator()->flags & JSITER_ENUMERATE); +} + +struct AutoCloseIterator +{ + AutoCloseIterator(JSContext* cx, JSObject* obj) : cx(cx), obj(cx, obj) {} + + ~AutoCloseIterator() { if (obj) CloseIterator(cx, obj); } + + void clear() { obj = nullptr; } + + private: + JSContext* cx; + RootedObject obj; +}; + +static bool +Reify(JSContext* cx, JSCompartment* origin, MutableHandleObject objp) +{ + Rooted<PropertyIteratorObject*> iterObj(cx, &objp->as<PropertyIteratorObject>()); + NativeIterator* ni = iterObj->getNativeIterator(); + + AutoCloseIterator close(cx, iterObj); + + /* Wrap the iteratee. */ + RootedObject obj(cx, ni->obj); + if (!origin->wrap(cx, &obj)) + return false; + + /* + * Wrap the elements in the iterator's snapshot. + * N.B. the order of closing/creating iterators is important due to the + * implicit cx->enumerators state. + */ + size_t length = ni->numKeys(); + AutoIdVector keys(cx); + if (length > 0) { + if (!keys.reserve(length)) + return false; + for (size_t i = 0; i < length; ++i) { + RootedId id(cx); + RootedValue v(cx, StringValue(ni->begin()[i])); + if (!ValueToId<CanGC>(cx, v, &id)) + return false; + keys.infallibleAppend(id); + } + } + + close.clear(); + if (!CloseIterator(cx, iterObj)) + return false; + + return EnumeratedIdVectorToIterator(cx, obj, ni->flags, keys, objp); +} + +bool +CrossCompartmentWrapper::enumerate(JSContext* cx, HandleObject wrapper, + MutableHandleObject objp) const +{ + { + AutoCompartment call(cx, wrappedObject(wrapper)); + if (!Wrapper::enumerate(cx, wrapper, objp)) + return false; + } + + if (CanReify(objp)) + return Reify(cx, cx->compartment(), objp); + return cx->compartment()->wrap(cx, objp); +} + +bool +CrossCompartmentWrapper::call(JSContext* cx, HandleObject wrapper, const CallArgs& args) const +{ + RootedObject wrapped(cx, wrappedObject(wrapper)); + + { + AutoCompartment 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)); + { + AutoCompartment 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<CrossCompartmentWrapperObject>()); + + RootedObject wrapped(cx, wrappedObject(wrapper)); + { + AutoCompartment 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<WrapperObject>() && + Wrapper::wrapperHandler(thisObj)->hasSecurityPolicy()) + { + MOZ_ASSERT(!thisObj->is<CrossCompartmentWrapperObject>()); + *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()); +} + +bool +CrossCompartmentWrapper::hasInstance(JSContext* cx, HandleObject wrapper, MutableHandleValue v, + bool* bp) const +{ + AutoCompartment call(cx, wrappedObject(wrapper)); + if (!cx->compartment()->wrap(cx, v)) + return false; + return Wrapper::hasInstance(cx, wrapper, v, bp); +} + +const char* +CrossCompartmentWrapper::className(JSContext* cx, HandleObject wrapper) const +{ + AutoCompartment call(cx, wrappedObject(wrapper)); + return Wrapper::className(cx, wrapper); +} + +JSString* +CrossCompartmentWrapper::fun_toString(JSContext* cx, HandleObject wrapper, unsigned indent) const +{ + RootedString str(cx); + { + AutoCompartment call(cx, wrappedObject(wrapper)); + str = Wrapper::fun_toString(cx, wrapper, indent); + if (!str) + return nullptr; + } + if (!cx->compartment()->wrap(cx, &str)) + return nullptr; + return str; +} + +bool +CrossCompartmentWrapper::regexp_toShared(JSContext* cx, HandleObject wrapper, RegExpGuard* g) const +{ + RegExpGuard wrapperGuard(cx); + { + AutoCompartment call(cx, wrappedObject(wrapper)); + if (!Wrapper::regexp_toShared(cx, wrapper, &wrapperGuard)) + return false; + } + + // Get an equivalent RegExpShared associated with the current compartment. + RegExpShared* re = wrapperGuard.re(); + return cx->compartment()->regExps.get(cx, re->getSource(), re->getFlags(), g); +} + +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); + +bool +js::IsCrossCompartmentWrapper(JSObject* obj) +{ + return IsWrapper(obj) && + !!(Wrapper::wrapperHandler(obj)->flags() & Wrapper::CROSS_COMPARTMENT); +} + +static void +NukeRemovedCrossCompartmentWrapper(JSContext* cx, JSObject* wrapper) +{ + MOZ_ASSERT(wrapper->is<CrossCompartmentWrapperObject>()); + + NotifyGCNukeWrapper(wrapper); + + wrapper->as<ProxyObject>().nuke(); + + MOZ_ASSERT(IsDeadProxyObject(wrapper)); +} + +JS_FRIEND_API(void) +js::NukeCrossCompartmentWrapper(JSContext* cx, JSObject* wrapper) +{ + JSCompartment* comp = wrapper->compartment(); + auto ptr = comp->lookupWrapper(ObjectValue(*Wrapper::wrappedObject(wrapper))); + if (ptr) + comp->removeWrapper(ptr); + NukeRemovedCrossCompartmentWrapper(cx, wrapper); +} + +/* + * 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. + */ +JS_FRIEND_API(bool) +js::NukeCrossCompartmentWrappers(JSContext* cx, + const CompartmentFilter& sourceFilter, + const CompartmentFilter& targetFilter, + js::NukeReferencesToWindow nukeReferencesToWindow) +{ + CHECK_REQUEST(cx); + JSRuntime* rt = cx->runtime(); + + rt->gc.evictNursery(JS::gcreason::EVICT_NURSERY); + + // Iterate through scopes looking for system cross compartment wrappers + // that point to an object that shares a global with obj. + + for (CompartmentsIter c(rt, SkipAtoms); !c.done(); c.next()) { + if (!sourceFilter.match(c)) + continue; + + // Iterate the wrappers looking for anything interesting. + for (JSCompartment::WrapperEnum e(c); !e.empty(); e.popFront()) { + // Some cross-compartment wrappers are for strings. We're not + // interested in those. + const CrossCompartmentKey& k = e.front().key(); + if (!k.is<JSObject*>()) + continue; + + AutoWrapperRooter wobj(cx, WrapperValue(e)); + JSObject* wrapped = UncheckedUnwrap(wobj); + + if (nukeReferencesToWindow == DontNukeWindowReferences && + IsWindowProxy(wrapped)) + { + continue; + } + + if (targetFilter.match(wrapped->compartment())) { + // We found a wrapper to nuke. + e.removeFront(); + NukeRemovedCrossCompartmentWrapper(cx, wobj); + } + } + } + + return true; +} + +// 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) +{ + MOZ_ASSERT(!IsInsideNursery(wobjArg)); + MOZ_ASSERT(!IsInsideNursery(newTargetArg)); + + RootedObject wobj(cx, wobjArg); + RootedObject newTarget(cx, newTargetArg); + MOZ_ASSERT(wobj->is<CrossCompartmentWrapperObject>()); + MOZ_ASSERT(!newTarget->is<CrossCompartmentWrapperObject>()); + JSObject* origTarget = Wrapper::wrappedObject(wobj); + MOZ_ASSERT(origTarget); + Value origv = ObjectValue(*origTarget); + JSCompartment* wcompartment = wobj->compartment(); + + AutoDisableProxyCheck adpc(cx->runtime()); + + // 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(ObjectValue(*newTarget))); + + // The old value should still be in the cross-compartment wrapper map, and + // the lookup should return wobj. + WrapperMap::Ptr p = wcompartment->lookupWrapper(origv); + MOZ_ASSERT(&p->value().unsafeGet()->toObject() == 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); + + // First, we wrap it in the new compartment. We try to use the existing + // wrapper, |wobj|, since it's been nuked anyway. The wrap() function has + // the choice to reuse |wobj| or not. + RootedObject tobj(cx, newTarget); + AutoCompartment ac(cx, wobj); + if (!wcompartment->rewrap(cx, &tobj, wobj)) + MOZ_CRASH(); + + // If wrap() 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. + if (!JSObject::swap(cx, wobj, tobj)) + MOZ_CRASH(); + } + + // Before swapping, this wrapper came out of wrap(), 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). + MOZ_ASSERT(wobj->is<WrapperObject>()); + if (!wcompartment->putWrapper(cx, CrossCompartmentKey(newTarget), ObjectValue(*wobj))) + MOZ_CRASH(); +} + +// Remap all cross-compartment wrappers pointing to |oldTarget| to point to +// |newTarget|. All wrappers are recomputed. +JS_FRIEND_API(bool) +js::RemapAllWrappersForObject(JSContext* cx, JSObject* oldTargetArg, + JSObject* newTargetArg) +{ + MOZ_ASSERT(!IsInsideNursery(oldTargetArg)); + MOZ_ASSERT(!IsInsideNursery(newTargetArg)); + + RootedValue origv(cx, ObjectValue(*oldTargetArg)); + RootedObject newTarget(cx, newTargetArg); + + AutoWrapperVector toTransplant(cx); + if (!toTransplant.reserve(cx->runtime()->numCompartments)) + return false; + + for (CompartmentsIter c(cx->runtime(), SkipAtoms); !c.done(); c.next()) { + if (WrapperMap::Ptr wp = c->lookupWrapper(origv)) { + // We found a wrapper. Remember and root it. + toTransplant.infallibleAppend(WrapperValue(wp)); + } + } + + for (const WrapperValue& v : toTransplant) + RemapWrapper(cx, &v.toObject(), newTarget); + + return true; +} + +JS_FRIEND_API(bool) +js::RecomputeWrappers(JSContext* cx, const CompartmentFilter& sourceFilter, + const CompartmentFilter& targetFilter) +{ + // Drop any nursery-allocated wrappers. + cx->runtime()->gc.evictNursery(JS::gcreason::EVICT_NURSERY); + + AutoWrapperVector toRecompute(cx); + for (CompartmentsIter c(cx->runtime(), SkipAtoms); !c.done(); c.next()) { + // Filter by source compartment. + if (!sourceFilter.match(c)) + continue; + + // Iterate over the wrappers, filtering appropriately. + for (JSCompartment::WrapperEnum e(c); !e.empty(); e.popFront()) { + // Filter out non-objects. + CrossCompartmentKey& k = e.front().mutableKey(); + if (!k.is<JSObject*>()) + continue; + + // Filter by target compartment. + if (!targetFilter.match(k.compartment())) + continue; + + // Add it to the list. + if (!toRecompute.append(WrapperValue(e))) + return false; + } + } + + // Recompute all the wrappers in the list. + for (const WrapperValue& v : toRecompute) { + JSObject* wrapper = &v.toObject(); + JSObject* wrapped = Wrapper::wrappedObject(wrapper); + RemapWrapper(cx, wrapper, wrapped); + } + + return true; +} |