Bug 877673 - Part2: Export helpers for sandbox. r=bholley
authorGabor Krizsanits <gkrizsanits@mozilla.com>
Mon, 19 Aug 2013 11:46:36 +0200
changeset 143051 9b4c4e56f4bb76d04bd2119bd08d05cd03316823
parent 143050 ad0a766f2f6bb0593477784ce9035ca2b826894b
child 143052 d608711909f43b57154ce4ba11a45c656c95b826
child 143105 5e29096a0d1243482ec16d8c1fbf3d7afe97218c
child 143129 04149d2dace694bf8c9a0013bd21567ffe495be9
child 155715 156f07313f85232b60d559b088e93246544bf1ca
push id32609
push usergkrizsanits@mozilla.com
push dateMon, 19 Aug 2013 09:49:40 +0000
treeherdermozilla-inbound@9b4c4e56f4bb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbholley
bugs877673
milestone26.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 877673 - Part2: Export helpers for sandbox. r=bholley
js/xpconnect/src/XPCComponents.cpp
js/xpconnect/src/xpcprivate.h
js/xpconnect/src/xpcpublic.h
js/xpconnect/tests/chrome/Makefile.in
js/xpconnect/tests/chrome/test_evalInWindow.xul
js/xpconnect/tests/unit/test_exportFunction.js
js/xpconnect/tests/unit/xpcshell.ini
--- a/js/xpconnect/src/XPCComponents.cpp
+++ b/js/xpconnect/src/XPCComponents.cpp
@@ -32,16 +32,18 @@
 #include "nsPrincipal.h"
 #include "mozilla/Attributes.h"
 #include "nsIScriptContext.h"
 #include "nsJSEnvironment.h"
 #include "nsXMLHttpRequest.h"
 #include "mozilla/Telemetry.h"
 #include "mozilla/XPTInterfaceInfoManager.h"
 #include "nsDOMClassInfoID.h"
+#include "nsGlobalWindow.h"
+
 
 using namespace mozilla;
 using namespace js;
 using namespace xpc;
 
 using mozilla::dom::DestroyProtoAndIfaceCache;
 
 /***************************************************************************/
@@ -2930,16 +2932,363 @@ CreateXMLHttpRequest(JSContext *cx, unsi
 
     rv = nsContentUtils::WrapNative(cx, global, xhr, vp);
     if (NS_FAILED(rv))
         return false;
 
     return true;
 }
 
+bool
+NewFunctionForwarder(JSContext *cx, HandleId id, HandleObject callable,
+                     bool doclone, MutableHandleValue vp);
+
+/*
+ * Instead of simply wrapping a function into another compartment,
+ * this helper function creates a native function in the target
+ * compartment and forwards the call to the original function.
+ * That call will be different than a regular JS function call in
+ * that, the |this| is left unbound, and all the non-native JS
+ * object arguments will be cloned using the structured clone
+ * algorithm.
+ * The return value is the new forwarder function, wrapped into
+ * the caller's compartment.
+ * The 3rd argument is the name of the property that will
+ * be set on the target scope, with the forwarder function as
+ * the value.
+ * The principal of the caller must subsume that of the target.
+ *
+ * Expected type of the arguments and the return value:
+ * function exportFunction(function funToExport,
+ *                         object targetScope,
+ *                         string name)
+ */
+static bool
+ExportFunction(JSContext *cx, unsigned argc, jsval *vp)
+{
+    MOZ_ASSERT(cx);
+    if (argc < 3) {
+        JS_ReportError(cx, "Function requires at least 3 arguments");
+        return false;
+    }
+
+    CallArgs args = CallArgsFromVp(argc, vp);
+    if (!args[0].isObject() || !args[1].isObject() || !args[2].isString()) {
+        JS_ReportError(cx, "Invalid argument");
+        return false;
+    }
+
+    RootedObject funObj(cx, &args[0].toObject());
+    RootedObject targetScope(cx, &args[1].toObject());
+    RootedString funName(cx, args[2].toString());
+
+    // We can only export functions to scopes those are transparent for us,
+    // so if there is a security wrapper around targetScope we must throw.
+    targetScope = CheckedUnwrap(targetScope);
+    if (!targetScope) {
+        JS_ReportError(cx, "Permission denied to export function into scope");
+        return false;
+    }
+
+    if (JS_GetStringLength(funName) == 0) {
+        JS_ReportError(cx, "3rd argument should be a non-empty string");
+        return false;
+    }
+
+    {
+        // We need to operate in the target scope from here on, let's enter
+        // its compartment.
+        JSAutoCompartment ac(cx, targetScope);
+
+        // Unwrapping to see if we have a callable.
+        funObj = UncheckedUnwrap(funObj);
+        if (!JS_ObjectIsCallable(cx, funObj)) {
+            JS_ReportError(cx, "First argument must be a function");
+            return false;
+        }
+
+        // The function forwarder will live in the target compartment. Since
+        // this function will be referenced from its private slot, to avoid a
+        // GC hazard, we must wrap it to the same compartment.
+        if (!JS_WrapObject(cx, funObj.address()))
+            return false;
+
+        RootedId id(cx);
+        if (!JS_ValueToId(cx, args[2], id.address()))
+            return false;
+
+        // And now, let's create the forwarder function in the target compartment
+        // for the function the be exported.
+        if (!NewFunctionForwarder(cx, id, funObj, /* doclone = */ true, args.rval())) {
+            JS_ReportError(cx, "Exporting function failed");
+            return false;
+        }
+
+        // We have the forwarder function in the target compartment, now
+        // we have to add it to the target scope as a property.
+        if (!JS_DefinePropertyById(cx, targetScope, id, args.rval(),
+                                   JS_PropertyStub, JS_StrictPropertyStub,
+                                   JSPROP_ENUMERATE))
+            return false;
+    }
+
+    // Finally we have to re-wrap the exported function back to the caller compartment.
+    if (!JS_WrapValue(cx, args.rval().address()))
+        return false;
+
+    return true;
+}
+
+static bool
+GetFilenameAndLineNumber(JSContext *cx, nsACString &filename, unsigned &lineno)
+{
+    JSScript *script;
+    if (JS_DescribeScriptedCaller(cx, &script, &lineno)) {
+        if (const char *cfilename = JS_GetScriptFilename(cx, script)) {
+            filename.Assign(nsDependentCString(cfilename));
+            return true;
+        }
+    }
+    return false;
+}
+
+namespace xpc {
+bool
+IsReflector(JSObject *obj)
+{
+    return IS_WN_REFLECTOR(obj) || dom::IsDOMObject(obj);
+}
+} /* namespace xpc */
+
+enum ForwarderCloneTags {
+    SCTAG_BASE = JS_SCTAG_USER_MIN,
+    SCTAG_REFLECTOR
+};
+
+static JSObject *
+CloneNonReflectorsRead(JSContext *cx, JSStructuredCloneReader *reader, uint32_t tag,
+                       uint32_t data, void *closure)
+{
+    MOZ_ASSERT(closure, "Null pointer!");
+    AutoObjectVector *reflectors = static_cast<AutoObjectVector *>(closure);
+    if (tag == SCTAG_REFLECTOR) {
+        MOZ_ASSERT(!data);
+
+        size_t idx;
+        if (JS_ReadBytes(reader, &idx, sizeof(size_t))) {
+            RootedObject reflector(cx, reflectors->handleAt(idx));
+            MOZ_ASSERT(reflector, "No object pointer?");
+            MOZ_ASSERT(IsReflector(reflector), "Object pointer must be a reflector!");
+
+            JS_WrapObject(cx, reflector.address());
+            JS_ASSERT(WrapperFactory::IsXrayWrapper(reflector) ||
+                      IsReflector(reflector));
+
+            return reflector;
+        }
+    }
+
+    JS_ReportError(cx, "CloneNonReflectorsRead error");
+    return nullptr;
+}
+
+static bool
+CloneNonReflectorsWrite(JSContext *cx, JSStructuredCloneWriter *writer,
+                        Handle<JSObject *> obj, void *closure)
+{
+    MOZ_ASSERT(closure, "Null pointer!");
+
+    // We need to maintain a list of reflectors to make sure all these objects
+    // are properly rooter. Only their indices will be serialized.
+    AutoObjectVector *reflectors = static_cast<AutoObjectVector *>(closure);
+    if (IsReflector(obj)) {
+        if (!reflectors->append(obj))
+            return false;
+
+        size_t idx = reflectors->length()-1;
+        if (JS_WriteUint32Pair(writer, SCTAG_REFLECTOR, 0) &&
+            JS_WriteBytes(writer, &idx, sizeof(size_t))) {
+            return true;
+        }
+    }
+
+    JS_ReportError(cx, "CloneNonReflectorsWrite error");
+    return false;
+}
+
+JSStructuredCloneCallbacks gForwarderStructuredCloneCallbacks = {
+    CloneNonReflectorsRead,
+    CloneNonReflectorsWrite,
+    nullptr
+};
+
+/*
+ * This is a special structured cloning, that clones only non-reflectors.
+ * The function assumes the cx is already entered the compartment we want
+ * to clone to, and that if val is an object is from the compartment we
+ * clone from.
+ */
+bool
+CloneNonReflectors(JSContext *cx, MutableHandleValue val)
+{
+    JSAutoStructuredCloneBuffer buffer;
+    AutoObjectVector rootedReflectors(cx);
+    {
+        // For parsing val we have to enter its compartment.
+        // (unless it's a primitive)
+        Maybe<JSAutoCompartment> ac;
+        if (val.isObject()) {
+            ac.construct(cx, &val.toObject());
+        }
+
+        if (!buffer.write(cx, val,
+            &gForwarderStructuredCloneCallbacks,
+            &rootedReflectors))
+        {
+            return false;
+        }
+    }
+
+    // Now recreate the clones in the target compartment.
+    RootedValue rval(cx);
+    if (!buffer.read(cx, val.address(),
+        &gForwarderStructuredCloneCallbacks,
+        &rootedReflectors))
+    {
+        return false;
+    }
+
+    return true;
+}
+
+/*
+ * Similar to evalInSandbox except this one is used to eval a script in the
+ * scope of a window. Also note, that the return value and the possible exceptions
+ * in the script are structured cloned, unless they are natives (then they are just
+ * wrapped).
+ * Principal of the caller must subsume the target's.
+ *
+ * Expected type of the arguments:
+ * value evalInWindow(string script,
+ *                    object window)
+ */
+static bool
+EvalInWindow(JSContext *cx, unsigned argc, jsval *vp)
+{
+    MOZ_ASSERT(cx);
+    if (argc < 2) {
+        JS_ReportError(cx, "Function requires two arguments");
+        return false;
+    }
+
+    CallArgs args = CallArgsFromVp(argc, vp);
+    if (!args[0].isString() || !args[1].isObject()) {
+        JS_ReportError(cx, "Invalid arguments");
+        return false;
+    }
+
+    RootedString srcString(cx, args[0].toString());
+    RootedObject targetScope(cx, &args[1].toObject());
+
+    // If we cannot unwrap we must not eval in it.
+    targetScope = CheckedUnwrap(targetScope);
+    if (!targetScope) {
+        JS_ReportError(cx, "Permission denied to eval in target scope");
+        return false;
+    }
+
+    // Make sure that we have a window object.
+    RootedObject inner(cx, CheckedUnwrap(targetScope, /* stopAtOuter = */ false));
+    nsCOMPtr<nsIGlobalObject> global;
+    nsCOMPtr<nsPIDOMWindow> window;
+    if (!JS_IsGlobalObject(inner) ||
+        !(global = GetNativeForGlobal(inner)) ||
+        !(window = do_QueryInterface(global)))
+    {
+        JS_ReportError(cx, "Second argument must be a window");
+        return false;
+    }
+
+    nsCOMPtr<nsIScriptContext> context =
+        (static_cast<nsGlobalWindow*>(window.get()))->GetScriptContext();
+    if (!context) {
+        JS_ReportError(cx, "Script context needed");
+        return false;
+    }
+
+    if (!context->GetScriptsEnabled()) {
+        JS_ReportError(cx, "Scripts are disabled in this window");
+        return false;
+    }
+
+    nsCString filename;
+    unsigned lineNo;
+    if (!GetFilenameAndLineNumber(cx, filename, lineNo)) {
+        // Default values for non-scripted callers.
+        filename.Assign("Unknown");
+        lineNo = 0;
+    }
+
+    nsDependentJSString srcDepString;
+    srcDepString.init(cx, srcString);
+
+    {
+        // CompileOptions must be created from the context
+        // we will execute this script in.
+        JSContext *wndCx = context->GetNativeContext();
+        AutoCxPusher pusher(wndCx);
+        JS::CompileOptions compileOptions(wndCx);
+        compileOptions.setFileAndLine(filename.get(), lineNo);
+
+        // We don't want the JS engine to automatically report
+        // uncaught exceptions.
+        nsJSUtils::EvaluateOptions evaluateOptions;
+        evaluateOptions.setReportUncaught(false);
+
+        nsresult rv = nsJSUtils::EvaluateString(wndCx,
+                                                srcDepString,
+                                                targetScope,
+                                                compileOptions,
+                                                evaluateOptions,
+                                                args.rval().address());
+
+        if (NS_FAILED(rv)) {
+            // If there was an exception we get it as a return value, if
+            // the evaluation failed for some other reason, then a default
+            // exception is raised.
+            MOZ_ASSERT(!JS_IsExceptionPending(wndCx),
+                       "Exception should be delivered as return value.");
+            if (args.rval().isUndefined()) {
+                MOZ_ASSERT(rv == NS_ERROR_OUT_OF_MEMORY);
+                return false;
+            }
+
+            // If there was an exception thrown we should set it
+            // on the calling context.
+            RootedValue exn(wndCx, args.rval());
+            // First we should reset the return value.
+            args.rval().set(UndefinedValue());
+
+            // Then clone the exception.
+            if (CloneNonReflectors(cx, &exn))
+                JS_SetPendingException(cx, exn);
+
+            return false;
+        }
+    }
+
+    // Let's clone the return value back to the callers compartment.
+    if (!CloneNonReflectors(cx, args.rval())) {
+        args.rval().set(UndefinedValue());
+        return false;
+    }
+
+    return true;
+}
+
 static bool
 sandbox_enumerate(JSContext *cx, HandleObject obj)
 {
     return JS_EnumerateStandardClasses(cx, obj);
 }
 
 static bool
 sandbox_resolve(JSContext *cx, HandleObject obj, HandleId id)
@@ -3345,16 +3694,22 @@ xpc_CreateSandboxObject(JSContext *cx, j
           return NS_ERROR_XPC_UNEXPECTED;
 
         if (!JS_DefineFunctions(cx, sandbox, SandboxFunctions))
             return NS_ERROR_XPC_UNEXPECTED;
 
         if (options.wantXHRConstructor &&
             !JS_DefineFunction(cx, sandbox, "XMLHttpRequest", CreateXMLHttpRequest, 0, JSFUN_CONSTRUCTOR))
             return NS_ERROR_XPC_UNEXPECTED;
+
+        if (options.wantExportHelpers &&
+            (!JS_DefineFunction(cx, sandbox, "exportFunction", ExportFunction, 3, 0) ||
+             !JS_DefineFunction(cx, sandbox, "evalInWindow", EvalInWindow, 2, 0)))
+            return NS_ERROR_XPC_UNEXPECTED;
+
     }
 
     if (vp) {
         // We have this crazy behavior where wantXrays=false also implies that the
         // returned sandbox is implicitly waived. We've stopped advertising it, but
         // keep supporting it for now.
         *vp = OBJECT_TO_JSVAL(sandbox);
         if (options.wantXrays && !JS_WrapValue(cx, vp))
@@ -3615,16 +3970,20 @@ ParseOptionsObject(JSContext *cx, jsval 
     rv = GetBoolPropFromOptions(cx, optionsObject,
                                 "wantComponents", &options.wantComponents);
     NS_ENSURE_SUCCESS(rv, rv);
 
     rv = GetBoolPropFromOptions(cx, optionsObject,
                                 "wantXHRConstructor", &options.wantXHRConstructor);
     NS_ENSURE_SUCCESS(rv, rv);
 
+    rv = GetBoolPropFromOptions(cx, optionsObject,
+                                "wantExportHelpers", &options.wantExportHelpers);
+    NS_ENSURE_SUCCESS(rv, rv);
+
     rv = GetStringPropFromOptions(cx, optionsObject,
                                   "sandboxName", options.sandboxName);
     NS_ENSURE_SUCCESS(rv, rv);
 
     rv = GetObjPropFromOptions(cx, optionsObject,
                                "sameZoneAs", options.sameZoneAs.address());
     NS_ENSURE_SUCCESS(rv, rv);
 
@@ -4200,41 +4559,78 @@ nsXPCComponents_Utils::CreateDateIn(cons
 
     if (!JS_WrapObject(cx, obj.address()))
         return NS_ERROR_FAILURE;
     *rval = ObjectValue(*obj);
     return NS_OK;
 }
 
 bool
-FunctionWrapper(JSContext *cx, unsigned argc, Value *vp)
+NonCloningFunctionForwarder(JSContext *cx, unsigned argc, Value *vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     RootedValue v(cx, js::GetFunctionNativeReserved(&args.callee(), 0));
     MOZ_ASSERT(v.isObject(), "weird function");
 
     JSObject *obj = JS_THIS_OBJECT(cx, vp);
     if (!obj) {
         return false;
     }
     return JS_CallFunctionValue(cx, obj, v, args.length(), args.array(), vp);
 }
 
+/*
+ * Forwards the call to the exported function. Clones all the non reflectors, ignores
+ * the |this| argument.
+ */
 bool
-WrapCallable(JSContext *cx, HandleObject obj, HandleId id, HandleObject propobj,
-             MutableHandleValue vp)
-{
-    JSFunction *fun = js::NewFunctionByIdWithReserved(cx, FunctionWrapper, 0, 0,
-                                                      JS_GetGlobalForObject(cx, obj), id);
+CloningFunctionForwarder(JSContext *cx, unsigned argc, Value *vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    RootedValue v(cx, js::GetFunctionNativeReserved(&args.callee(), 0));
+    NS_ASSERTION(v.isObject(), "weird function");
+    RootedObject origFunObj(cx, UncheckedUnwrap(&v.toObject()));
+    {
+        JSAutoCompartment ac(cx, origFunObj);
+        // Note: only the arguments are cloned not the |this| or the |callee|.
+        // Function forwarder does not use those.
+        for (unsigned i = 0; i < args.length(); i++) {
+            if (!CloneNonReflectors(cx, args[i])) {
+                return false;
+            }
+        }
+
+        // JS API does not support any JSObject to JSFunction conversion,
+        // so let's use JS_CallFunctionValue instead.
+        RootedValue functionVal(cx);
+        functionVal.setObject(*origFunObj);
+
+        if (!JS_CallFunctionValue(cx, nullptr, functionVal, args.length(), args.array(), vp))
+            return false;
+    }
+
+    // Return value must be wrapped.
+    return JS_WrapValue(cx, vp);
+}
+
+bool
+NewFunctionForwarder(JSContext *cx, HandleId id, HandleObject callable, bool doclone,
+                     MutableHandleValue vp)
+{
+    JSFunction *fun = js::NewFunctionByIdWithReserved(cx, doclone ? CloningFunctionForwarder :
+                                                                    NonCloningFunctionForwarder,
+                                                                    0,0, JS::CurrentGlobalOrNull(cx), id);
+
     if (!fun)
         return false;
 
     JSObject *funobj = JS_GetFunctionObject(fun);
-    js::SetFunctionNativeReserved(funobj, 0, ObjectValue(*propobj));
+    js::SetFunctionNativeReserved(funobj, 0, ObjectValue(*callable));
     vp.setObject(*funobj);
     return true;
 }
 
 /* void makeObjectPropsNormal(jsval vobj); */
 NS_IMETHODIMP
 nsXPCComponents_Utils::MakeObjectPropsNormal(const Value &vobj, JSContext *cx)
 {
@@ -4262,17 +4658,17 @@ nsXPCComponents_Utils::MakeObjectPropsNo
         if (v.isPrimitive())
             continue;
 
         RootedObject propobj(cx, &v.toObject());
         // TODO Deal with non-functions.
         if (!js::IsWrapper(propobj) || !JS_ObjectIsCallable(cx, propobj))
             continue;
 
-        if (!WrapCallable(cx, obj, id, propobj, &v) ||
+        if (!NewFunctionForwarder(cx, id, propobj, /* doclone = */ false, &v) ||
             !JS_SetPropertyById(cx, obj, id, v))
             return NS_ERROR_FAILURE;
     }
 
     return NS_OK;
 }
 
 NS_IMETHODIMP
--- a/js/xpconnect/src/xpcprivate.h
+++ b/js/xpconnect/src/xpcprivate.h
@@ -3695,23 +3695,25 @@ xpc_GetSafeJSContext()
 }
 
 namespace xpc {
 struct SandboxOptions {
     SandboxOptions(JSContext *cx)
         : wantXrays(true)
         , wantComponents(true)
         , wantXHRConstructor(false)
+        , wantExportHelpers(false)
         , proto(xpc_GetSafeJSContext())
         , sameZoneAs(xpc_GetSafeJSContext())
     { }
 
     bool wantXrays;
     bool wantComponents;
     bool wantXHRConstructor;
+    bool wantExportHelpers;
     JS::RootedObject proto;
     nsCString sandboxName;
     JS::RootedObject sameZoneAs;
 };
 
 JSObject *
 CreateGlobalObject(JSContext *cx, JSClass *clasp, nsIPrincipal *principal,
                    JS::CompartmentOptions& aOptions);
--- a/js/xpconnect/src/xpcpublic.h
+++ b/js/xpconnect/src/xpcpublic.h
@@ -61,16 +61,18 @@ GetXBLScope(JSContext *cx, JSObject *con
 // Returns whether XBL scopes have been explicitly disabled for code running
 // in this compartment. See the comment around mAllowXBLScope.
 bool
 AllowXBLScope(JSCompartment *c);
 
 bool
 IsSandboxPrototypeProxy(JSObject *obj);
 
+bool
+IsReflector(JSObject *obj);
 } /* namespace xpc */
 
 namespace JS {
 
 struct RuntimeStats;
 
 }
 
--- a/js/xpconnect/tests/chrome/Makefile.in
+++ b/js/xpconnect/tests/chrome/Makefile.in
@@ -56,16 +56,17 @@ MOCHITEST_CHROME_FILES = \
 		outoflinexulscript.js \
 		subscript.js \
 		utf8_subscript.js \
 		test_cows.xul \
 		test_documentdomain.xul \
 		test_doublewrappedcompartments.xul \
 		test_evalInSandbox.xul \
 		file_evalInSandbox.html \
+		test_evalInWindow.xul \
 		test_exnstack.xul \
 		test_expandosharing.xul \
 		file_expandosharing.jsm \
 		test_exposeInDerived.xul \
 		test_getweakmapkeys.xul \
 		test_mozMatchesSelector.xul \
 		test_nodelists.xul \
 		test_precisegc.xul \
new file mode 100644
--- /dev/null
+++ b/js/xpconnect/tests/chrome/test_evalInWindow.xul
@@ -0,0 +1,75 @@
+<?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=877673
+-->
+<window title="Mozilla Bug 877673"
+  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <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">
+  </body>
+
+  <!-- test code goes here -->
+  <script type="application/javascript"><![CDATA[
+      SimpleTest.waitForExplicitFinish();
+      const Cu = Components.utils;
+      var sb = new Cu.Sandbox("http://example.org", {wantExportHelpers: true});
+      sb.ok = ok;
+
+      function executeIn(frame, script, exceptionCb) {
+        sb.frame = frame;
+        sb.exceptionCb = exceptionCb;
+        if (exceptionCb) {
+          return Cu.evalInSandbox("try {evalInWindow('" + script + "',frame); ok(false, 'Exception should have been thrown.')} catch(e) {exceptionCb(e)}", sb);
+        }
+
+        return Cu.evalInSandbox("evalInWindow('" + script + "',frame)", sb);
+      }
+
+      function testSameOrigin(frame) {
+        frame.contentWindow.document.wrappedJSObject.str = "foobar";
+        is(executeIn(frame.contentWindow, "document.str"), "foobar",
+           "Same origin string property access.");
+
+        executeIn(frame.contentWindow, 'document.obj = {prop: "foobar"}');
+        is((executeIn(frame.contentWindow, "document.obj")).prop, "foobar",
+           "Same origin object property access (cloning).");
+        isnot(executeIn(frame.contentWindow, "document.obj"), frame.contentWindow.document.wrappedJSObject.obj,
+              "Ensure cloning for js objects.");
+        is(executeIn(frame.contentWindow, "document"), frame.contentWindow.document,
+           "Xrayables should just pass without cloning.");
+        is( executeIn(frame.contentWindow, "({a:{doc: document}})").a.doc, frame.contentWindow.document,
+           "Deep cloning works.");
+
+        executeIn(frame.contentWindow, "throw 42", function(e){is(e, 42,
+                                                                  "Exception was thrown from script.")});
+
+        executeIn(frame.contentDocument, "var a = 42;", function(e){ok(e.toString().indexOf("Second argument must be a window") > -1,
+                                                                       "Passing non-window to evalInWindow should throw.");});
+        testDone();
+      }
+
+      function testCrossOrigin(frame) {
+        executeIn(frame.contentWindow, "var a = 42;", function(e){ok(e.toString().indexOf("Permission denied") > -1,
+                                                                     "Executing script in a window from cross origin should throw.");});
+        testDone();
+      }
+
+      var testsRun = 0;
+      function testDone() {
+        if (++testsRun == 2)
+          SimpleTest.finish();
+      }
+  ]]></script>
+  <iframe src="http://example.org/tests/js/xpconnect/tests/mochitest/file_empty.html"
+          onload="testSameOrigin(this)">
+  </iframe>
+  <iframe src="http://mochi.test:8888/tests/js/xpconnect/tests/mochitest/file_empty.html"
+          onload="testCrossOrigin(this)">
+  </iframe>
+</window>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/js/xpconnect/tests/unit/test_exportFunction.js
@@ -0,0 +1,73 @@
+function run_test() {
+  var Cu = Components.utils;
+  var epsb = new Cu.Sandbox(["http://example.com", "http://example.org"], { wantExportHelpers: true });
+  subsb = new Cu.Sandbox("http://example.com", { wantXHRConstructor: true });
+  subsb2 = new Cu.Sandbox("http://example.com", { wantXHRConstructor: true });
+  xorigsb = new Cu.Sandbox("http://test.com");
+
+  epsb.subsb = subsb;
+  epsb.xorigsb = xorigsb;
+  epsb.do_check_true = do_check_true;
+  epsb.do_check_eq = do_check_eq;
+  epsb.do_check_neq = do_check_neq;
+
+  // Exporting should work if prinicipal of the source sandbox
+  // subsumes the principal of the target sandbox.
+  Cu.evalInSandbox("(" + function() {
+    Object.prototype.protoProp = "common";
+    var wasCalled = false;
+    var _this = this;
+    var funToExport = function(a, obj, native, mixed) {
+      do_check_eq(a, 42);
+      do_check_neq(obj, subsb.tobecloned);
+      do_check_eq(obj.cloned, "cloned");
+      do_check_eq(obj.protoProp, "common");
+      do_check_eq(native, subsb.native);
+      do_check_eq(_this, this);
+      do_check_eq(mixed.xrayed, subsb.xrayed);
+      do_check_eq(mixed.xrayed2, subsb.xrayed2);
+      wasCalled = true;
+    };
+    this.checkIfCalled = function() {
+      do_check_true(wasCalled);
+      wasCalled = false;
+    }
+    exportFunction(funToExport, subsb, "imported");
+  }.toSource() + ")()", epsb);
+
+  subsb.xrayed = Cu.evalInSandbox("(" + function () {
+      return new XMLHttpRequest();
+  }.toSource() + ")()", subsb2);
+
+  // Exported function should be able to be call from the
+  // target sandbox. Native arguments should be just wrapped
+  // every other argument should be cloned.
+  Cu.evalInSandbox("(" + function () {
+    native = new XMLHttpRequest();
+    xrayed2 = XPCNativeWrapper(new XMLHttpRequest());
+    mixed = { xrayed: xrayed, xrayed2: xrayed2 };
+    tobecloned = { cloned: "cloned" };
+    imported(42,tobecloned, native, mixed);
+  }.toSource() + ")()", subsb);
+
+  // Apply should work but the |this| argument should not be
+  // possible to be changed.
+  Cu.evalInSandbox("(" + function() {
+    imported.apply("something", [42, tobecloned, native, mixed]);
+  }.toSource() + ")()", subsb);
+
+  Cu.evalInSandbox("(" + function() {
+    checkIfCalled();
+  }.toSource() + ")()", epsb);
+
+  // Exporting should throw if princpal of the source sandbox does
+  // not subsume the principal of the target.
+  Cu.evalInSandbox("(" + function() {
+    try{
+      exportFunction(function(){}, this.xorigsb, "denied");
+      do_check_true(false);
+    } catch (e) {
+      do_check_true(e.toString().indexOf('Permission denied') > -1);
+    }
+  }.toSource() + ")()", epsb);
+}
--- a/js/xpconnect/tests/unit/xpcshell.ini
+++ b/js/xpconnect/tests/unit/xpcshell.ini
@@ -42,9 +42,10 @@ fail-if = os == "android"
 [test_attributes.js]
 [test_params.js]
 [test_tearoffs.js]
 [test_want_components.js]
 [test_components.js]
 [test_allowedDomains.js]
 [test_allowedDomainsXHR.js]
 [test_nuke_sandbox.js]
+[test_exportFunction.js]
 [test_watchdog.js]