bug 601803 - Support adopting a node cross-compartment. r=gal/jst, a=blocker
authorBlake Kaplan <mrbkap@gmail.com>
Mon, 15 Nov 2010 17:21:25 -0800
changeset 58534 a4813c8be814ca7dd0faaedb9dc6d30791f34de8
parent 58533 065c920fb3723dc8ec32c2b1bc544d8b7e5be81e
child 58535 9280c6d82204bdea2901e5e24726186370f86624
push id1
push usershaver@mozilla.com
push dateTue, 04 Jan 2011 17:58:04 +0000
reviewersgal, jst, blocker
bugs601803
milestone2.0b8pre
bug 601803 - Support adopting a node cross-compartment. r=gal/jst, a=blocker
content/base/src/nsNodeUtils.cpp
content/base/test/test_bug601803.html
dom/base/nsGlobalWindow.cpp
js/src/js.msg
js/src/jsapi-tests/testBug604087.cpp
js/src/jsapi.cpp
js/src/jsapi.h
js/src/jsobj.cpp
js/src/jsobj.h
js/src/jsobjinlines.h
js/src/jswrapper.cpp
js/src/jswrapper.h
js/src/xpconnect/src/nsXPConnect.cpp
js/src/xpconnect/src/xpcpublic.h
js/src/xpconnect/src/xpcwrappednative.cpp
js/src/xpconnect/tests/chrome/Makefile.in
js/src/xpconnect/tests/chrome/test_bug601803.xul
js/src/xpconnect/wrappers/WrapperFactory.h
--- a/content/base/src/nsNodeUtils.cpp
+++ b/content/base/src/nsNodeUtils.cpp
@@ -1,9 +1,10 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sw=2 et tw=99: */
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  *
  * The contents of this file are subject to the Mozilla Public License Version
  * 1.1 (the "License"); you may not use this file except in compliance with
  * the License. You may obtain a copy of the License at
  * http://www.mozilla.org/MPL/
  *
@@ -59,16 +60,17 @@
 #include "nsBindingManager.h"
 #include "nsGenericHTMLElement.h"
 #ifdef MOZ_MEDIA
 #include "nsHTMLMediaElement.h"
 #endif // MOZ_MEDIA
 #include "nsImageLoadingContent.h"
 #include "jsobj.h"
 #include "jsgc.h"
+#include "xpcpublic.h"
 
 using namespace mozilla::dom;
 
 // This macro expects the ownerDocument of content_ to be in scope as
 // |nsIDocument* doc|
 // NOTE: AttributeChildRemoved doesn't use this macro but has a very similar use.
 // If you change how this macro behave please update AttributeChildRemoved.
 #define IMPL_MUTATION_NOTIFICATION(func_, content_, params_)      \
@@ -456,16 +458,21 @@ nsNodeUtils::CloneAndAdopt(nsINode *aNod
 
   *aResult = nsnull;
 
   // First deal with aNode and walk its attributes (and their children). Then,
   // if aDeep is PR_TRUE, deal with aNode's children (and recurse into their
   // attributes and children).
 
   nsresult rv;
+  if (aCx) {
+      rv = xpc_MorphSlimWrapper(aCx, aNode);
+      NS_ENSURE_SUCCESS(rv, rv);
+  }
+
   nsNodeInfoManager *nodeInfoManager = aNewNodeInfoManager;
 
   // aNode.
   nsINodeInfo *nodeInfo = aNode->mNodeInfo;
   nsCOMPtr<nsINodeInfo> newNodeInfo;
   if (nodeInfoManager) {
 
     // Don't allow importing/adopting nodes from non-privileged "scriptable"
@@ -511,21 +518,16 @@ nsNodeUtils::CloneAndAdopt(nsINode *aNod
       isDeepDocumentClone = PR_TRUE;
       // After cloning the document itself, we want to clone the children into
       // the cloned document (somewhat like cloning and importing them into the
       // cloned document).
       nodeInfoManager = clone->mNodeInfo->NodeInfoManager();
     }
   }
   else if (nodeInfoManager) {
-    // FIXME Bug 601803 Need to support adopting a node cross-compartment
-    if (aCx && aOldScope->compartment() != aNewScope->compartment()) {
-      return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
-    }
-
     nsIDocument* oldDoc = aNode->GetOwnerDoc();
     PRBool wasRegistered = PR_FALSE;
     if (oldDoc && aNode->IsElement()) {
       Element* element = aNode->AsElement();
       oldDoc->ClearBoxObjectFor(element);
       wasRegistered = oldDoc->UnregisterFreezableElement(element);
     }
 
@@ -578,19 +580,38 @@ nsNodeUtils::CloneAndAdopt(nsINode *aNod
 
     if (elem) {
       elem->RecompileScriptEventListeners();
     }
 
     if (aCx) {
       nsIXPConnect *xpc = nsContentUtils::XPConnect();
       if (xpc) {
+        nsWrapperCache *cache;
+        CallQueryInterface(aNode, &cache);
+        JSObject *preservedWrapper = nsnull;
+
+        // If reparenting moves us to a new compartment, preserving causes
+        // problems. In that case, we release ourselves and re-preserve after
+        // reparenting so we're sure to have the right JS object preserved.
+        // We use a JSObject stack copy of the wrapper to protect it from GC
+        // under ReparentWrappedNativeIfFound.
+        if (cache && cache->PreservingWrapper()) {
+          preservedWrapper = cache->GetWrapper();
+          nsContentUtils::ReleaseWrapper(aNode, cache);
+        }
+
         nsCOMPtr<nsIXPConnectJSObjectHolder> oldWrapper;
         rv = xpc->ReparentWrappedNativeIfFound(aCx, aOldScope, aNewScope, aNode,
                                                getter_AddRefs(oldWrapper));
+
+        if (preservedWrapper) {
+          nsContentUtils::PreserveWrapper(aNode, cache);
+        }
+
         if (NS_FAILED(rv)) {
           aNode->mNodeInfo.swap(nodeInfo);
 
           return rv;
         }
       }
     }
   }
--- a/content/base/test/test_bug601803.html
+++ b/content/base/test/test_bug601803.html
@@ -17,17 +17,17 @@ https://bugzilla.mozilla.org/show_bug.cg
 </div>
 <pre id="test">
 <script type="application/javascript">
 
 /** Test for Bug 601803 **/
 SimpleTest.waitForExplicitFinish();
 
 window.onmessage = function (event) {
-    todo(event.data == "false", "Shouldn't throw when adopting a node cross-compartment");
+    is(event.data, "false", "Shouldn't throw when adopting a node cross-compartment");
     SimpleTest.finish();
 }
 
 document.getElementById("frame").src = "http://example.org/tests/content/base/test/file_bug601803a.html";
 
 
 
 </script>
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -2045,17 +2045,17 @@ nsGlobalWindow::SetNewDocument(nsIDocume
     } else {
       JSObject *outerObject =
         NS_NewOuterWindowProxy(cx, newInnerWindow->mJSObject);
       if (!outerObject) {
         NS_ERROR("out of memory");
         return NS_ERROR_FAILURE;
       }
 
-      outerObject = JS_TransplantWrapper(cx, mJSObject, outerObject);
+      outerObject = JS_TransplantObject(cx, mJSObject, outerObject);
       if (!outerObject) {
         NS_ERROR("unable to transplant wrappers, probably OOM");
         return NS_ERROR_FAILURE;
       }
 
       mJSObject = outerObject;
       SetWrapper(mJSObject);
 
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -339,8 +339,9 @@ MSG_DEF(JSMSG_CALLER_IS_STRICT,       25
 MSG_DEF(JSMSG_NEED_DEBUG_MODE,        257, 0, JSEXN_ERR, "function can be called only in debug mode")
 MSG_DEF(JSMSG_STRICT_CODE_LET_EXPR_STMT, 258, 0, JSEXN_ERR, "strict mode code may not contain unparenthesized let expression statements")
 MSG_DEF(JSMSG_CANT_CHANGE_EXTENSIBILITY, 259, 0, JSEXN_TYPEERR, "can't change object's extensibility")
 MSG_DEF(JSMSG_SC_BAD_SERIALIZED_DATA, 260, 1, JSEXN_INTERNALERR, "bad serialized structured data ({0})")
 MSG_DEF(JSMSG_SC_UNSUPPORTED_TYPE,    261, 0, JSEXN_TYPEERR, "unsupported type for structured data")
 MSG_DEF(JSMSG_SC_RECURSION,           262, 0, JSEXN_INTERNALERR, "recursive object")
 MSG_DEF(JSMSG_CANT_WRAP_XML_OBJECT,   263, 0, JSEXN_TYPEERR, "can't wrap XML objects")
 MSG_DEF(JSMSG_BAD_CLONE_VERSION,      264, 0, JSEXN_ERR, "unsupported structured clone version")
+MSG_DEF(JSMSG_CANT_CLONE_OBJECT,      265, 0, JSEXN_TYPEERR, "can't clone object")
--- a/js/src/jsapi-tests/testBug604087.cpp
+++ b/js/src/jsapi-tests/testBug604087.cpp
@@ -1,12 +1,12 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
  * vim: set ts=8 sw=4 et tw=99:
  *
- * Tests JS_TransplantWrappers
+ * Tests JS_TransplantObject
  */
 
 #include "tests.h"
 #include "jswrapper.h"
 
 struct OuterWrapper : JSWrapper
 {
     OuterWrapper() : JSWrapper(0) {}
@@ -73,12 +73,12 @@ BEGIN_TEST(testBug604087)
         JSAutoEnterCompartment ac;
         CHECK(ac.enter(cx, compartment2));
         next = JSWrapper::New(cx, compartment2, compartment2->getProto(), compartment2,
                               &OuterWrapper::singleton);
         CHECK(next);
     }
 
     JS_SetWrapObjectCallbacks(JS_GetRuntime(cx), Wrap, PreWrap);
-    CHECK(JS_TransplantWrapper(cx, outerObj, next));
+    CHECK(JS_TransplantObject(cx, outerObj, next));
     return true;
 }
 END_TEST(testBug604087)
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -1212,41 +1212,39 @@ JS_WrapObject(JSContext *cx, JSObject **
 JS_PUBLIC_API(JSBool)
 JS_WrapValue(JSContext *cx, jsval *vp)
 {
     CHECK_REQUEST(cx);
     return cx->compartment->wrap(cx, Valueify(vp));
 }
 
 JS_PUBLIC_API(JSObject *)
-JS_TransplantWrapper(JSContext *cx, JSObject *wrapper, JSObject *target)
-{
-    JS_ASSERT(wrapper->isWrapper());
-
-    /*
-     * This function is called when a window is navigating. In that case, we
-     * need to "move" the window from wrapper's compartment to target's
-     * compartment.
-     */
+JS_TransplantObject(JSContext *cx, JSObject *origobj, JSObject *target)
+{
+     // This function is called when an object moves between two different
+     // compartments. In that case, we need to "move" the window from origobj's
+     // compartment to target's compartment.
     JSCompartment *destination = target->getCompartment();
-    if (wrapper->getCompartment() == destination) {
-        // If the wrapper is in the same compartment as the destination, then
-        // we know that we won't find wrapper in the destination's cross
-        // compartment map and that the same object will continue to work.
-        if (!wrapper->swap(cx, target))
+    if (origobj->getCompartment() == destination) {
+        // If the original object is in the same compartment as the
+        // destination, then we know that we won't find wrapper in the
+        // destination's cross compartment map and that the same object
+        // will continue to work.
+        if (!origobj->swap(cx, target))
             return NULL;
-        return wrapper;
+        return origobj;
     }
 
     JSObject *obj;
     WrapperMap &map = destination->crossCompartmentWrappers;
-    Value wrapperv = ObjectValue(*wrapper);
-
-    // There might already be a wrapper for the window in the new compartment.
-    if (WrapperMap::Ptr p = map.lookup(wrapperv)) {
+    Value origv = ObjectValue(*origobj);
+
+    // There might already be a wrapper for the original object in the new
+    // compartment.
+    if (WrapperMap::Ptr p = map.lookup(origv)) {
         // If there is, make it the primary outer window proxy around the
         // inner (accomplished by swapping target's innards with the old,
         // possibly security wrapper, innards).
         obj = &p->value.toObject();
         map.remove(p);
         if (!obj->swap(cx, target))
             return NULL;
     } else {
@@ -1261,28 +1259,28 @@ JS_TransplantWrapper(JSContext *cx, JSOb
     // based on whether the new compartment is same origin with them.
     Value targetv = ObjectValue(*obj);
     WrapperVector &vector = cx->runtime->compartments;
     AutoValueVector toTransplant(cx);
     toTransplant.reserve(vector.length());
 
     for (JSCompartment **p = vector.begin(), **end = vector.end(); p != end; ++p) {
         WrapperMap &pmap = (*p)->crossCompartmentWrappers;
-        if (WrapperMap::Ptr wp = pmap.lookup(wrapperv)) {
+        if (WrapperMap::Ptr wp = pmap.lookup(origv)) {
             // We found a wrapper. Remember and root it.
             toTransplant.append(wp->value);
         }
     }
 
     for (Value *begin = toTransplant.begin(), *end = toTransplant.end(); begin != end; ++begin) {
         JSObject *wobj = &begin->toObject();
         JSCompartment *wcompartment = wobj->compartment();
         WrapperMap &pmap = wcompartment->crossCompartmentWrappers;
-        JS_ASSERT(pmap.lookup(wrapperv));
-        pmap.remove(wrapperv);
+        JS_ASSERT(pmap.lookup(origv));
+        pmap.remove(origv);
 
         // First, we wrap it in the new compartment. This will return a
         // new wrapper.
         AutoCompartment ac(cx, wobj);
         JSObject *tobj = obj;
         if (!ac.enter() || !wcompartment->wrap(cx, &tobj))
             return NULL;
 
@@ -1291,25 +1289,25 @@ JS_TransplantWrapper(JSContext *cx, JSOb
         // entry in the compartment's wrapper map to point to the old
         // wrapper.
         JS_ASSERT(tobj != wobj);
         if (!wobj->swap(cx, tobj))
             return NULL;
         pmap.put(targetv, ObjectValue(*wobj));
     }
 
-    // Lastly, update the old outer window proxy to point to the new one.
+    // Lastly, update the original object to point to the new one.
     {
-        AutoCompartment ac(cx, wrapper);
+        AutoCompartment ac(cx, origobj);
         JSObject *tobj = obj;
         if (!ac.enter() || !JS_WrapObject(cx, &tobj))
             return NULL;
-        if (!wrapper->swap(cx, tobj))
+        if (!origobj->swap(cx, tobj))
             return NULL;
-        wrapper->getCompartment()->crossCompartmentWrappers.put(targetv, wrapperv);
+        origobj->getCompartment()->crossCompartmentWrappers.put(targetv, origv);
     }
 
     return obj;
 }
 
 JS_PUBLIC_API(JSObject *)
 JS_GetGlobalObject(JSContext *cx)
 {
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -960,17 +960,17 @@ JS_GetCompartmentPrivate(JSContext *cx, 
 
 extern JS_PUBLIC_API(JSBool)
 JS_WrapObject(JSContext *cx, JSObject **objp);
 
 extern JS_PUBLIC_API(JSBool)
 JS_WrapValue(JSContext *cx, jsval *vp);
 
 extern JS_PUBLIC_API(JSObject *)
-JS_TransplantWrapper(JSContext *cx, JSObject *wrapper, JSObject *target);
+JS_TransplantObject(JSContext *cx, JSObject *origobj, JSObject *target);
 
 #ifdef __cplusplus
 JS_END_EXTERN_C
 
 class JS_PUBLIC_API(JSAutoEnterCompartment)
 {
     JSCrossCompartmentCall *call;
 
--- a/js/src/jsobj.cpp
+++ b/js/src/jsobj.cpp
@@ -72,16 +72,17 @@
 #include "jsscope.h"
 #include "jsscript.h"
 #include "jsstaticcheck.h"
 #include "jsstdint.h"
 #include "jsstr.h"
 #include "jstracer.h"
 #include "jsdbgapi.h"
 #include "json.h"
+#include "jswrapper.h"
 
 #include "jsinterpinlines.h"
 #include "jsscopeinlines.h"
 #include "jsscriptinlines.h"
 #include "jsobjinlines.h"
 
 #if JS_HAS_GENERATORS
 #include "jsiter.h"
@@ -3291,64 +3292,214 @@ JSObject::defineBlockVariable(JSContext 
 static size_t
 GetObjectSize(JSObject *obj)
 {
     return (obj->isFunction() && !obj->getPrivate())
            ? sizeof(JSFunction)
            : sizeof(JSObject) + sizeof(js::Value) * obj->numFixedSlots();
 }
 
+bool
+JSObject::copyPropertiesFrom(JSContext *cx, JSObject *obj)
+{
+    // If we're not native, then we cannot copy properties.
+    JS_ASSERT(isNative() == obj->isNative());
+    if (!isNative())
+        return true;
+
+    Vector<const Shape *> shapes(cx);
+    for (Shape::Range r(obj->lastProperty()); !r.empty(); r.popFront()) {
+        if (!shapes.append(&r.front()))
+            return false;
+    }
+
+    size_t n = shapes.length();
+    while (n > 0) {
+        const Shape *shape = shapes[--n];
+        uintN attrs = shape->attributes();
+        PropertyOp getter = shape->getter();
+        if ((attrs & JSPROP_GETTER) && !cx->compartment->wrap(cx, &getter))
+            return false;
+        PropertyOp setter = shape->setter();
+        if ((attrs & JSPROP_SETTER) && !cx->compartment->wrap(cx, &setter))
+            return false;
+        Value v = shape->hasSlot() ? obj->getSlot(shape->slot) : UndefinedValue();
+        if (!cx->compartment->wrap(cx, &v))
+            return false;
+        if (!defineProperty(cx, shape->id, v, getter, setter, attrs))
+            return false;
+    }
+    return true;
+}
+
+static bool
+CopySlots(JSContext *cx, JSObject *from, JSObject *to)
+{
+    JS_ASSERT(!from->isNative() && !to->isNative());
+    size_t nslots = from->numSlots();
+    if (to->ensureSlots(cx, nslots))
+        return false;
+
+    size_t n = 0;
+    if (to->isWrapper() &&
+        (JSWrapper::wrapperHandler(to)->flags() & JSWrapper::CROSS_COMPARTMENT)) {
+        to->slots[0] = from->slots[0];
+        to->slots[1] = from->slots[1];
+        n = 2;
+    }
+
+    for (; n < nslots; ++n) {
+        Value v = from->slots[n];
+        if (!cx->compartment->wrap(cx, &v))
+            return false;
+        to->slots[n] = v;
+    }
+    return true;
+}
+
+JSObject *
+JSObject::clone(JSContext *cx, JSObject *proto, JSObject *parent)
+{
+    /*
+     * We can only clone native objects and proxies. Dense arrays are slowified if
+     * we try to clone them.
+     */
+    if (!isNative()) {
+        if (isDenseArray()) {
+            if (!makeDenseArraySlow(cx))
+                return NULL;
+        } else if (!isProxy()) {
+            JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL,
+                                 JSMSG_CANT_CLONE_OBJECT);
+            return NULL;
+        }
+    }
+    JSObject *clone = NewObject<WithProto::Given>(cx, getClass(),
+                                                  proto, parent,
+                                                  gc::FinalizeKind(finalizeKind()));
+    if (!clone)
+        return NULL;
+    if (isNative()) {
+        if (clone->isFunction() && (compartment() != clone->compartment())) {
+            JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL,
+                                 JSMSG_CANT_CLONE_OBJECT);
+            return NULL;
+        }
+
+        if (getClass()->flags & JSCLASS_HAS_PRIVATE)
+            clone->setPrivate(getPrivate());
+    } else {
+        JS_ASSERT(isProxy());
+        if (!CopySlots(cx, this, clone))
+            return NULL;
+    }
+    return clone;
+}
+
+static void
+TradeGuts(JSObject *a, JSObject *b)
+{
+    JS_ASSERT(a->compartment() == b->compartment());
+    JS_ASSERT(a->isFunction() == b->isFunction());
+
+    bool aInline = !a->hasSlotsArray();
+    bool bInline = !b->hasSlotsArray();
+
+    /* Trade the guts of the objects. */
+    const size_t size = GetObjectSize(a);
+    if (size == GetObjectSize(b)) {
+        /*
+         * If the objects are the same size, then we make no assumptions about
+         * whether they have dynamically allocated slots and instead just copy
+         * them over wholesale.
+         */
+        char tmp[tl::Max<sizeof(JSFunction), sizeof(JSObject_Slots16)>::result];
+        JS_ASSERT(size <= sizeof(tmp));
+
+        memcpy(tmp, a, size);
+        memcpy(a, b, size);
+        memcpy(b, tmp, size);
+
+        /* Fixup pointers for inline slots on the objects. */
+        if (aInline)
+            b->slots = b->fixedSlots();
+        if (bInline)
+            a->slots = a->fixedSlots();
+    } else {
+        /*
+         * If the objects are of differing sizes, then we only copy over the
+         * JSObject portion (things like class, etc.) and leave it to
+         * JSObject::clone to copy over the dynamic slots for us.
+         */
+        if (a->isFunction()) {
+            JSFunction tmp;
+            memcpy(&tmp, a, sizeof tmp);
+            memcpy(a, b, sizeof tmp);
+            memcpy(b, &tmp, sizeof tmp);
+        } else {
+            JSObject tmp;
+            memcpy(&tmp, a, sizeof tmp);
+            memcpy(a, b, sizeof tmp);
+            memcpy(b, &tmp, sizeof tmp);
+        }
+
+        JS_ASSERT(!aInline);
+        JS_ASSERT(!bInline);
+    }
+}
+
 /*
  * Use this method with extreme caution. It trades the guts of two objects and updates
  * scope ownership. This operation is not thread-safe, just as fast array to slow array
  * transitions are inherently not thread-safe. Don't perform a swap operation on objects
  * shared across threads or, or bad things will happen. You have been warned.
  */
 bool
 JSObject::swap(JSContext *cx, JSObject *other)
 {
-    size_t size = GetObjectSize(this);
-
-    if (size != GetObjectSize(other)) {
-        /*
-         * Objects with different numbers of fixed slots can be swapped only if they
-         * are both shapeless non-natives, to preserve the invariant that objects with the
-         * same shape have the same number of fixed slots.  Use a dynamic array for both.
-         */
-        JS_ASSERT(!isNative());
-        JS_ASSERT(!other->isNative());
-        size = sizeof(JSObject);
+    /*
+     * If we are swapping objects with a different number of builtin slots, force
+     * both to not use their inline slots.
+     */
+    if (GetObjectSize(this) != GetObjectSize(other)) {
         if (!hasSlotsArray()) {
             if (!allocSlots(cx, numSlots()))
                 return false;
         }
         if (!other->hasSlotsArray()) {
             if (!other->allocSlots(cx, other->numSlots()))
                 return false;
         }
     }
 
-    bool thisInline = !hasSlotsArray();
-    bool otherInline = !other->hasSlotsArray();
-
-    JS_STATIC_ASSERT(FINALIZE_OBJECT_LAST == FINALIZE_OBJECT16);
-
-    char tmp[tl::Max<sizeof(JSFunction), sizeof(JSObject_Slots16)>::result];
-    JS_ASSERT(size <= sizeof(tmp));
-
-    /* Trade the guts of the objects. */
-    memcpy(tmp, this, size);
-    memcpy(this, other, size);
-    memcpy(other, tmp, size);
-
-    /* Fixup pointers for inline slots on the objects. */
-    if (thisInline)
-        other->slots = other->fixedSlots();
-    if (otherInline)
-        this->slots = this->fixedSlots();
+    if (this->compartment() == other->compartment()) {
+        TradeGuts(this, other);
+        return true;
+    }
+
+    JSObject *thisClone;
+    JSObject *otherClone;
+    {
+        AutoCompartment ac(cx, other);
+        if (!ac.enter())
+            return false;
+        thisClone = this->clone(cx, other->getProto(), other->getParent());
+        if (!thisClone || !thisClone->copyPropertiesFrom(cx, this))
+            return false;
+    }
+    {
+        AutoCompartment ac(cx, this);
+        if (!ac.enter())
+            return false;
+        otherClone = other->clone(cx, other->getProto(), other->getParent());
+        if (!otherClone || !otherClone->copyPropertiesFrom(cx, other))
+            return false;
+    }
+    TradeGuts(this, otherClone);
+    TradeGuts(other, thisClone);
 
     return true;
 }
 
 #if JS_HAS_XDR
 
 #define NO_PARENT_INDEX ((uint32)-1)
 
--- a/js/src/jsobj.h
+++ b/js/src/jsobj.h
@@ -547,16 +547,18 @@ struct JSObject : js::gc::Cell {
      * a doubly-linked list.
      */
     inline bool inDictionaryMode() const;
 
     inline uint32 propertyCount() const;
 
     inline bool hasPropertyTable() const;
 
+    /* gc::FinalizeKind */ unsigned finalizeKind() const;
+
     uint32 numSlots() const { return capacity; }
 
     size_t slotsAndStructSize(uint32 nslots) const;
     size_t slotsAndStructSize() const { return slotsAndStructSize(numSlots()); }
 
     inline js::Value* fixedSlots() const;
     inline size_t numFixedSlots() const;
 
@@ -1126,17 +1128,19 @@ struct JSObject : js::gc::Cell {
     }
 
     static bool thisObject(JSContext *cx, const js::Value &v, js::Value *vp);
 
     inline JSCompartment *getCompartment() const;
 
     inline JSObject *getThrowTypeError() const;
 
-    bool swap(JSContext *cx, JSObject *obj);
+    JS_FRIEND_API(JSObject *) clone(JSContext *cx, JSObject *proto, JSObject *parent);
+    JS_FRIEND_API(bool) copyPropertiesFrom(JSContext *cx, JSObject *obj);
+    bool swap(JSContext *cx, JSObject *other);
 
     const js::Shape *defineBlockVariable(JSContext *cx, jsid id, intN index);
 
     inline bool canHaveMethodBarrier() const;
 
     inline bool isArguments() const;
     inline bool isNormalArguments() const;
     inline bool isStrictArguments() const;
--- a/js/src/jsobjinlines.h
+++ b/js/src/jsobjinlines.h
@@ -247,30 +247,30 @@ JSObject::getPrimitiveThis() const
 
 inline void
 JSObject::setPrimitiveThis(const js::Value &pthis)
 {
     JS_ASSERT(isPrimitive());
     setSlot(JSSLOT_PRIMITIVE_THIS, pthis);
 }
 
-inline js::gc::FinalizeKind
-GetObjectFinalizeKind(const JSObject *obj)
+inline /* gc::FinalizeKind */ unsigned
+JSObject::finalizeKind() const
 {
-    return js::gc::FinalizeKind(obj->arena()->header()->thingKind);
+    return js::gc::FinalizeKind(arena()->header()->thingKind);
 }
 
 inline size_t
 JSObject::numFixedSlots() const
 {
     if (isFunction())
         return JSObject::FUN_CLASS_RESERVED_SLOTS;
     if (!hasSlotsArray())
         return capacity;
-    return js::gc::GetGCKindSlots(GetObjectFinalizeKind(this));
+    return js::gc::GetGCKindSlots(js::gc::FinalizeKind(finalizeKind()));
 }
 
 inline size_t
 JSObject::slotsAndStructSize(uint32 nslots) const
 {
     bool isFun = isFunction() && this == (JSObject*) getPrivate();
 
     int ndslots = hasSlotsArray() ? nslots : 0;
@@ -1065,17 +1065,17 @@ NewObjectGCKind(JSContext *cx, js::Class
 
 /* Make an object with pregenerated shape from a NEWOBJECT bytecode. */
 static inline JSObject *
 CopyInitializerObject(JSContext *cx, JSObject *baseobj)
 {
     JS_ASSERT(baseobj->getClass() == &js_ObjectClass);
     JS_ASSERT(!baseobj->inDictionaryMode());
 
-    gc::FinalizeKind kind = GetObjectFinalizeKind(baseobj);
+    gc::FinalizeKind kind = gc::FinalizeKind(baseobj->finalizeKind());
     JSObject *obj = NewBuiltinClassInstance(cx, &js_ObjectClass, kind);
 
     if (!obj || !obj->ensureSlots(cx, baseobj->numSlots()))
         return NULL;
 
     obj->flags = baseobj->flags;
     obj->lastProp = baseobj->lastProp;
     obj->objShape = baseobj->objShape;
--- a/js/src/jswrapper.cpp
+++ b/js/src/jswrapper.cpp
@@ -378,17 +378,18 @@ AutoCompartment::leave()
         JS_ASSERT_IF(wasSane && context->hasfp(), context->compartment == origin);
         context->compartment->wrapException(context);
     }
     entered = false;
 }
 
 /* Cross compartment wrappers. */
 
-JSCrossCompartmentWrapper::JSCrossCompartmentWrapper(uintN flags) : JSWrapper(flags)
+JSCrossCompartmentWrapper::JSCrossCompartmentWrapper(uintN flags)
+  : JSWrapper(CROSS_COMPARTMENT | flags)
 {
 }
 
 JSCrossCompartmentWrapper::~JSCrossCompartmentWrapper()
 {
 }
 
 bool
--- a/js/src/jswrapper.h
+++ b/js/src/jswrapper.h
@@ -98,16 +98,24 @@ class JS_FRIEND_API(JSWrapper) : public 
     static JSWrapper singleton;
 
     static JSObject *New(JSContext *cx, JSObject *obj, JSObject *proto, JSObject *parent,
                          JSWrapper *handler);
 
     static inline JSObject *wrappedObject(const JSObject *wrapper) {
         return wrapper->getProxyPrivate().toObjectOrNull();
     }
+    static inline JSWrapper *wrapperHandler(const JSObject *wrapper) {
+        return static_cast<JSWrapper *>(wrapper->getProxyHandler());
+    }
+
+    enum {
+        CROSS_COMPARTMENT = 1 << 0,
+        LAST_USED_FLAG = CROSS_COMPARTMENT
+    };
 
     static void *getWrapperFamily();
 };
 
 /* Base class for all cross compartment wrapper handlers. */
 class JS_FRIEND_API(JSCrossCompartmentWrapper) : public JSWrapper {
   public:
     JSCrossCompartmentWrapper(uintN flags);
--- a/js/src/xpconnect/src/nsXPConnect.cpp
+++ b/js/src/xpconnect/src/nsXPConnect.cpp
@@ -1177,16 +1177,30 @@ nsXPConnect::InitClassesWithNewWrappedGl
         }
     }
 
     NS_ADDREF(*_retval = holder);
 
     return NS_OK;
 }
 
+nsresult
+xpc_MorphSlimWrapper(JSContext *cx, nsISupports *tomorph)
+{
+    nsWrapperCache *cache;
+    CallQueryInterface(tomorph, &cache);
+    if(!cache)
+        return NS_OK;
+
+    JSObject *obj = cache->GetWrapper();
+    if(!obj || !IS_SLIM_WRAPPER(obj))
+        return NS_OK;
+    return MorphSlimWrapper(cx, obj);
+}
+
 static nsresult
 NativeInterface2JSObject(XPCLazyCallContext & lccx,
                          JSObject * aScope,
                          nsISupports *aCOMObj,
                          nsWrapperCache *aCache,
                          const nsIID * aIID,
                          PRBool aAllowWrapping,
                          jsval *aVal,
@@ -1478,20 +1492,24 @@ nsXPConnect::ReparentWrappedNativeIfFoun
         ReparentWrapperIfFound(ccx, scope, scope2, aNewParent, aCOMObj,
                                (XPCWrappedNative**) _retval);
 }
 
 static JSDHashOperator
 MoveableWrapperFinder(JSDHashTable *table, JSDHashEntryHdr *hdr,
                       uint32 number, void *arg)
 {
-    // Every element counts.
     nsTArray<nsRefPtr<XPCWrappedNative> > *array =
         static_cast<nsTArray<nsRefPtr<XPCWrappedNative> > *>(arg);
-    array->AppendElement(((Native2WrappedNativeMap::Entry*)hdr)->value);
+    XPCWrappedNative *wn = ((Native2WrappedNativeMap::Entry*)hdr)->value;
+
+    // If a wrapper is expired, then there are no references to it from JS, so
+    // we don't have to move it.
+    if(!wn->IsWrapperExpired())
+        array->AppendElement(wn);
     return JS_DHASH_NEXT;
 }
 
 /* void moveWrappers(in JSContextPtr aJSContext, in JSObjectPtr  aOldScope, in JSObjectPtr  aNewScope); */
 NS_IMETHODIMP
 nsXPConnect::MoveWrappers(JSContext *aJSContext,
                           JSObject *aOldScope,
                           JSObject *aNewScope)
--- a/js/src/xpconnect/src/xpcpublic.h
+++ b/js/src/xpconnect/src/xpcpublic.h
@@ -36,16 +36,17 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 #ifndef xpcpublic_h
 #define xpcpublic_h
 
 #include "jsapi.h"
+#include "nsISupports.h"
 #include "jsobj.h"
 #include "nsAString.h"
 #include "nsIPrincipal.h"
 #include "nsWrapperCache.h"
 
 class nsIPrincipal;
 
 nsresult
@@ -54,16 +55,19 @@ xpc_CreateGlobalObject(JSContext *cx, JS
                        bool wantXrays, JSObject **global,
                        JSCompartment **compartment);
 
 nsresult
 xpc_CreateMTGlobalObject(JSContext *cx, JSClass *clasp,
                          nsISupports *ptr, JSObject **global,
                          JSCompartment **compartment);
 
+nsresult
+xpc_MorphSlimWrapper(JSContext *cx, nsISupports *tomorph);
+
 extern JSBool
 XPC_WN_Equality(JSContext *cx, JSObject *obj, const jsval *v, JSBool *bp);
 
 #define IS_WRAPPER_CLASS(clazz)                                               \
     (clazz->ext.equality == js::Valueify(XPC_WN_Equality))
 
 inline JSBool
 DebugCheckWrapperClass(JSObject* obj)
--- a/js/src/xpconnect/src/xpcwrappednative.cpp
+++ b/js/src/xpconnect/src/xpcwrappednative.cpp
@@ -514,18 +514,18 @@ XPCWrappedNative::GetNewOrUsed(XPCCallCo
             return NS_ERROR_FAILURE;
 
         nsISupports *Object = helper.Object();
         if(nsXPCWrappedJSClass::IsWrappedJS(Object))
         {
             nsCOMPtr<nsIXPConnectWrappedJS> wrappedjs(do_QueryInterface(Object));
             JSObject *obj;
             wrappedjs->GetJSObject(&obj);
-            if(xpc::AccessCheck::isChrome(obj->getCompartment()) &&
-               !xpc::AccessCheck::isChrome(Scope->GetGlobalJSObject()->getCompartment()))
+            if(xpc::AccessCheck::isChrome(obj->compartment()) &&
+               !xpc::AccessCheck::isChrome(Scope->GetGlobalJSObject()->compartment()))
             {
                 needsCOW = JS_TRUE;
             }
         }
     }
 
     AutoMarkingWrappedNativeProtoPtr proto(ccx);
 
@@ -1503,27 +1503,41 @@ XPCWrappedNative::ReparentWrapperIfFound
     }
 
     if(!flat)
     {
         *aWrapper = nsnull;
         return NS_OK;
     }
 
+    bool crosscompartment = aOldScope->GetGlobalJSObject()->compartment() !=
+                            aNewScope->GetGlobalJSObject()->compartment();
+#ifdef DEBUG
+    if(crosscompartment)
+    {
+        NS_ASSERTION(aNewParent, "won't be able to find the new parent");
+        NS_ASSERTION(wrapper, "can't transplant slim wrappers");
+    }
+#endif
+
     // ReparentWrapperIfFound is really only meant to be called from DOM code
     // which must happen only on the main thread. Bail if we're on some other
     // thread or have a non-main-thread-only wrapper.
     if (!XPCPerThreadData::IsMainThread(ccx) ||
         (wrapper &&
          wrapper->GetProto() &&
          !wrapper->GetProto()->ClassIsMainThreadOnly()))
     {
         return NS_ERROR_FAILURE;
     }
 
+    JSAutoEnterCompartment ac;
+    if(!ac.enter(ccx, aNewScope->GetGlobalJSObject()))
+        return NS_ERROR_FAILURE;
+
     if(aOldScope != aNewScope)
     {
         // Oh, so now we need to move the wrapper to a different scope.
         AutoMarkingWrappedNativeProtoPtr oldProto(ccx);
         AutoMarkingWrappedNativeProtoPtr newProto(ccx);
 
         if(!wrapper)
             oldProto = GetSlimWrapperProto(flat);
@@ -1585,30 +1599,56 @@ XPCWrappedNative::ReparentWrapperIfFound
                              "wrapper already in new scope!");
 
                 (void) newMap->Add(wrapper);
             }
 
             // We only try to fixup the __proto__ JSObject if the wrapper
             // is directly using that of its XPCWrappedNativeProto.
 
-            if(wrapper->HasProto() &&
-               flat->getProto() == oldProto->GetJSProtoObject())
+            if(crosscompartment)
             {
-                if(!JS_SetPrototype(ccx, flat, newProto->GetJSProtoObject()))
-                {
-                    // this is bad, very bad
-                    NS_ERROR("JS_SetPrototype failed");
+                JSObject *newobj = flat->clone(ccx, newProto->GetJSProtoObject(),
+                                               aNewParent);
+                if(!newobj)
                     return NS_ERROR_FAILURE;
-                }
+
+                JS_SetPrivate(ccx, flat, nsnull);
+
+                JSObject *propertyHolder =
+                    JS_NewObjectWithGivenProto(ccx, NULL, NULL, aNewParent);
+                if(!propertyHolder || !propertyHolder->copyPropertiesFrom(ccx, flat))
+                    return NS_ERROR_OUT_OF_MEMORY;
+
+                flat = JS_TransplantObject(ccx, flat, newobj);
+                if(!flat)
+                    return NS_ERROR_FAILURE;
+                wrapper->mFlatJSObject = flat;
+                if(cache)
+                    cache->SetWrapper(flat);
+                if (!flat->copyPropertiesFrom(ccx, propertyHolder))
+                    return NS_ERROR_FAILURE;
             }
             else
             {
-                NS_WARNING("Moving XPConnect wrappedNative to new scope, "
-                           "but can't fixup __proto__");
+                if(wrapper->HasProto() &&
+                   flat->getProto() == oldProto->GetJSProtoObject())
+                {
+                    if(!JS_SetPrototype(ccx, flat, newProto->GetJSProtoObject()))
+                    {
+                        // this is bad, very bad
+                        NS_ERROR("JS_SetPrototype failed");
+                        return NS_ERROR_FAILURE;
+                    }
+                }
+                else
+                {
+                    NS_WARNING("Moving XPConnect wrappedNative to new scope, "
+                               "but can't fixup __proto__");
+                }
             }
         }
         else
         {
             if(!JS_SetReservedSlot(ccx, flat, 0,
                                    PRIVATE_TO_JSVAL(newProto.get())) ||
                !JS_SetPrototype(ccx, flat, newProto->GetJSProtoObject()))
             {
--- a/js/src/xpconnect/tests/chrome/Makefile.in
+++ b/js/src/xpconnect/tests/chrome/Makefile.in
@@ -52,16 +52,17 @@ include $(topsrcdir)/config/rules.mk
 		test_doublewrappedcompartments.xul \
 		test_evalInSandbox.xul \
 		test_sandboxImport.xul \
 		test_wrappers.xul \
 		test_bug484459.xul \
 		test_cows.xul \
 		test_bug517163.xul \
 		test_bug571849.xul \
+		test_bug601803.xul \
 		$(NULL)
 
 # Disabled until this test gets updated to test the new proxy based
 # wrappers.
 #		test_wrappers-2.xul \
 
 libs:: $(_CHROME_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/chrome/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/js/src/xpconnect/tests/chrome/test_bug601803.xul
@@ -0,0 +1,40 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+                 type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=500931
+-->
+<window title="Mozilla Bug 601803"
+  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript"
+          src="chrome://mochikit/content/MochiKit/packed.js"></script>
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+  <!-- test results are displayed in the html:body -->
+  <body xmlns="http://www.w3.org/1999/xhtml">
+  <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=601803"
+     target="_blank">Mozilla Bug 601803</a>
+
+  <!-- test code goes here -->
+  <script type="application/javascript"><![CDATA[
+
+  /** Test for Bug 601803 **/
+
+  function go() {
+    var ifr = document.getElementById('ifr');
+    var elem = document.createElementNS("http://www.w3.org/1999/xhtml","html:div");
+    elem.appendChild(document.createTextNode("hello, world"));
+    elem.expando = 42;
+    ifr.contentDocument.body.appendChild(elem);
+    is(elem.wrappedJSObject.expando, 42, "expando is preserved");
+    SimpleTest.finish();
+  }
+
+  SimpleTest.waitForExplicitFinish();
+
+  ]]></script>
+  <iframe type="content" src="about:blank" onload="go()" id="ifr" />
+  </body>
+</window>
--- a/js/src/xpconnect/wrappers/WrapperFactory.h
+++ b/js/src/xpconnect/wrappers/WrapperFactory.h
@@ -39,21 +39,21 @@
 
 #include "jsapi.h"
 #include "jswrapper.h"
 
 namespace xpc {
 
 class WrapperFactory {
   public:
-    enum { WAIVE_XRAY_WRAPPER_FLAG = (1<<0),
-           IS_XRAY_WRAPPER_FLAG = (1<<1),
-           SCRIPT_ACCESS_ONLY_FLAG = (1<<2),
-           PARTIALLY_TRANSPARENT = (1<<3),
-           SOW_FLAG = (1<<4) };
+    enum { WAIVE_XRAY_WRAPPER_FLAG = JSWrapper::LAST_USED_FLAG << 1,
+           IS_XRAY_WRAPPER_FLAG    = WAIVE_XRAY_WRAPPER_FLAG << 1,
+           SCRIPT_ACCESS_ONLY_FLAG = IS_XRAY_WRAPPER_FLAG << 1,
+           PARTIALLY_TRANSPARENT   = SCRIPT_ACCESS_ONLY_FLAG << 1,
+           SOW_FLAG                = PARTIALLY_TRANSPARENT << 1 };
 
     // Return true if any of any of the nested wrappers have the flag set.
     static bool HasWrapperFlag(JSObject *wrapper, uintN flag) {
         uintN flags = 0;
         wrapper->unwrap(&flags);
         return !!(flags & flag);
     }