Bug 1182409 - Add 'allowWaivers' sandbox option. r=gabor
authorBobby Holley <bobbyholley@gmail.com>
Tue, 14 Jul 2015 18:54:44 -0700
changeset 253085 eca18bb1b558d77259ecf4d41e4354fb8bada232
parent 253084 9a14aef38b62b4fef18c7356dfa860725d4cf924
child 253086 c0afcf6ed3244f4b516b722c20b054807322536e
push id29061
push userryanvm@gmail.com
push dateThu, 16 Jul 2015 18:53:45 +0000
treeherdermozilla-central@a0f4a688433d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgabor
bugs1182409
milestone42.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 1182409 - Add 'allowWaivers' sandbox option. r=gabor
js/xpconnect/src/Sandbox.cpp
js/xpconnect/src/XPCWrapper.cpp
js/xpconnect/src/xpcprivate.h
js/xpconnect/tests/unit/test_allowWaivers.js
js/xpconnect/tests/unit/xpcshell.ini
js/xpconnect/wrappers/WrapperFactory.cpp
js/xpconnect/wrappers/WrapperFactory.h
js/xpconnect/wrappers/XrayWrapper.cpp
--- a/js/xpconnect/src/Sandbox.cpp
+++ b/js/xpconnect/src/Sandbox.cpp
@@ -1024,29 +1024,30 @@ xpc::CreateSandboxObject(JSContext* cx, 
                        ? &SandboxWriteToProtoClass
                        : &SandboxClass;
 
     RootedObject sandbox(cx, xpc::CreateGlobalObject(cx, js::Jsvalify(clasp),
                                                      principal, compartmentOptions));
     if (!sandbox)
         return NS_ERROR_FAILURE;
 
-    CompartmentPrivate::Get(sandbox)->writeToGlobalPrototype =
-      options.writeToGlobalPrototype;
+    CompartmentPrivate* priv = CompartmentPrivate::Get(sandbox);
+    priv->allowWaivers = options.allowWaivers;
+    priv->writeToGlobalPrototype = options.writeToGlobalPrototype;
 
     // Set up the wantXrays flag, which indicates whether xrays are desired even
     // for same-origin access.
     //
     // This flag has historically been ignored for chrome sandboxes due to
     // quirks in the wrapping implementation that have now been removed. Indeed,
     // same-origin Xrays for chrome->chrome access seems a bit superfluous.
     // Arguably we should just flip the default for chrome and still honor the
     // flag, but such a change would break code in subtle ways for minimal
     // benefit. So we just switch it off here.
-    CompartmentPrivate::Get(sandbox)->wantXrays =
+    priv->wantXrays =
       AccessCheck::isChrome(sandbox) ? false : options.wantXrays;
 
     {
         JSAutoCompartment ac(cx, sandbox);
 
         if (options.proto) {
             bool ok = JS_WrapObject(cx, &options.proto);
             if (!ok)
@@ -1475,16 +1476,17 @@ SandboxOptions::ParseGlobalProperties()
 /*
  * Helper that parsing the sandbox options object (from) and sets the fields of the incoming options struct (options).
  */
 bool
 SandboxOptions::Parse()
 {
     bool ok = ParseObject("sandboxPrototype", &proto) &&
               ParseBoolean("wantXrays", &wantXrays) &&
+              ParseBoolean("allowWaivers", &allowWaivers) &&
               ParseBoolean("wantComponents", &wantComponents) &&
               ParseBoolean("wantExportHelpers", &wantExportHelpers) &&
               ParseString("sandboxName", sandboxName) &&
               ParseObject("sameZoneAs", &sameZoneAs) &&
               ParseBoolean("freshZone", &freshZone) &&
               ParseBoolean("invisibleToDebugger", &invisibleToDebugger) &&
               ParseBoolean("discardSource", &discardSource) &&
               ParseJSString("addonId", &addonId) &&
--- a/js/xpconnect/src/XPCWrapper.cpp
+++ b/js/xpconnect/src/XPCWrapper.cpp
@@ -27,26 +27,24 @@ static bool
 UnwrapNW(JSContext* cx, unsigned argc, jsval* vp)
 {
   JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
   if (args.length() != 1) {
     return ThrowException(NS_ERROR_XPC_NOT_ENOUGH_ARGS, cx);
   }
 
   JS::RootedValue v(cx, args[0]);
-  if (!v.isObject() || !js::IsWrapper(&v.toObject())) {
+  if (!v.isObject() || !js::IsCrossCompartmentWrapper(&v.toObject()) ||
+      !WrapperFactory::AllowWaiver(&v.toObject())) {
     args.rval().set(v);
     return true;
   }
 
-  if (AccessCheck::wrapperSubsumes(&v.toObject())) {
-    bool ok = xpc::WrapperFactory::WaiveXrayAndWrap(cx, &v);
-    NS_ENSURE_TRUE(ok, false);
-  }
-
+  bool ok = xpc::WrapperFactory::WaiveXrayAndWrap(cx, &v);
+  NS_ENSURE_TRUE(ok, false);
   args.rval().set(v);
   return true;
 }
 
 static bool
 XrayWrapperConstructor(JSContext* cx, unsigned argc, jsval* vp)
 {
   JS::CallArgs args = CallArgsFromVp(argc, vp);
--- a/js/xpconnect/src/xpcprivate.h
+++ b/js/xpconnect/src/xpcprivate.h
@@ -3443,31 +3443,33 @@ protected:
 };
 
 class MOZ_STACK_CLASS SandboxOptions : public OptionsBase {
 public:
     explicit SandboxOptions(JSContext* cx = xpc_GetSafeJSContext(),
                             JSObject* options = nullptr)
         : OptionsBase(cx, options)
         , wantXrays(true)
+        , allowWaivers(true)
         , wantComponents(true)
         , wantExportHelpers(false)
         , proto(cx)
         , addonId(cx)
         , writeToGlobalPrototype(false)
         , sameZoneAs(cx)
         , freshZone(false)
         , invisibleToDebugger(false)
         , discardSource(false)
         , metadata(cx)
     { }
 
     virtual bool Parse();
 
     bool wantXrays;
+    bool allowWaivers;
     bool wantComponents;
     bool wantExportHelpers;
     JS::RootedObject proto;
     nsCString sandboxName;
     JS::RootedString addonId;
     bool writeToGlobalPrototype;
     JS::RootedObject sameZoneAs;
     bool freshZone;
@@ -3654,16 +3656,17 @@ class CompartmentPrivate
 public:
     enum LocationHint {
         LocationHintRegular,
         LocationHintAddon
     };
 
     explicit CompartmentPrivate(JSCompartment* c)
         : wantXrays(false)
+        , allowWaivers(true)
         , writeToGlobalPrototype(false)
         , skipWriteToGlobalPrototype(false)
         , universalXPConnectEnabled(false)
         , forcePermissiveCOWs(false)
         , skipCOWCallableChecks(false)
         , scriptability(c)
         , scope(nullptr)
     {
@@ -3681,19 +3684,27 @@ public:
     }
 
     static CompartmentPrivate* Get(JSObject* object)
     {
         JSCompartment* compartment = js::GetObjectCompartment(object);
         return Get(compartment);
     }
 
-
+    // Controls whether this compartment gets Xrays to same-origin. This behavior
+    // is deprecated, but is still the default for sandboxes for compatibity
+    // reasons.
     bool wantXrays;
 
+    // Controls whether this compartment is allowed to waive Xrays to content
+    // that it subsumes. This should generally be true, except in cases where we
+    // want to prevent code from depending on Xray Waivers (which might make it
+    // more portable to other browser architectures).
+    bool allowWaivers;
+
     // This flag is intended for a very specific use, internal to Gecko. It may
     // go away or change behavior at any time. It should not be added to any
     // documentation and it should not be used without consulting the XPConnect
     // module owner.
     bool writeToGlobalPrototype;
 
     // When writeToGlobalPrototype is true, we use this flag to temporarily
     // disable the writeToGlobalPrototype behavior (when resolving standard
new file mode 100644
--- /dev/null
+++ b/js/xpconnect/tests/unit/test_allowWaivers.js
@@ -0,0 +1,30 @@
+const Cu = Components.utils;
+function checkWaivers(from, allowed) {
+  var sb = new Cu.Sandbox('http://example.com');
+  from.test = sb.eval('var o = {prop: 2, f: function() {return 42;}}; o');
+
+  // Make sure that |from| has Xrays to sb.
+  do_check_eq(from.eval('test.prop'), 2);
+  do_check_eq(from.eval('test.f'), undefined);
+
+  // Make sure that waivability works as expected.
+  do_check_eq(from.eval('!!test.wrappedJSObject'), allowed);
+  do_check_eq(from.eval('XPCNativeWrapper.unwrap(test) !== test'), allowed);
+
+  // Make a sandbox with the same principal as |from|, but without any waiver
+  // restrictions, and make sure that the waiver does not transfer.
+  var friend = new Cu.Sandbox(Cu.getObjectPrincipal(from));
+  friend.test = from.test;
+  friend.eval('var waived = test.wrappedJSObject;');
+  do_check_true(friend.eval('waived.f()'), 42);
+  friend.from = from;
+  friend.eval('from.waived = waived');
+  do_check_eq(from.eval('!!waived.f'), allowed);
+}
+
+function run_test() {
+  checkWaivers(new Cu.Sandbox('http://example.com'), true);
+  checkWaivers(new Cu.Sandbox('http://example.com', {allowWaivers: false}), false);
+  checkWaivers(new Cu.Sandbox(['http://example.com']), true);
+  checkWaivers(new Cu.Sandbox(['http://example.com'], {allowWaivers: false}), false);
+}
--- a/js/xpconnect/tests/unit/xpcshell.ini
+++ b/js/xpconnect/tests/unit/xpcshell.ini
@@ -13,16 +13,17 @@ support-files =
   component-file.manifest
   component_import.js
   component_import.manifest
   importer.jsm
   recursive_importA.jsm
   recursive_importB.jsm
   syntax_error.jsm
 
+[test_allowWaivers.js]
 [test_bogus_files.js]
 [test_bug408412.js]
 [test_bug451678.js]
 [test_bug604362.js]
 [test_bug641378.js]
 [test_bug677864.js]
 [test_bug711404.js]
 [test_bug742444.js]
--- a/js/xpconnect/wrappers/WrapperFactory.cpp
+++ b/js/xpconnect/wrappers/WrapperFactory.cpp
@@ -101,16 +101,30 @@ WrapperFactory::WaiveXray(JSContext* cx,
     MOZ_ASSERT(!js::IsInnerObject(obj));
 
     JSObject* waiver = GetXrayWaiver(obj);
     if (waiver)
         return waiver;
     return CreateXrayWaiver(cx, obj);
 }
 
+/* static */ bool
+WrapperFactory::AllowWaiver(JSCompartment* target, JSCompartment* origin)
+{
+    return CompartmentPrivate::Get(target)->allowWaivers &&
+           AccessCheck::subsumes(target, origin);
+}
+
+/* static */ bool
+WrapperFactory::AllowWaiver(JSObject* wrapper) {
+    MOZ_ASSERT(js::IsCrossCompartmentWrapper(wrapper));
+    return AllowWaiver(js::GetObjectCompartment(wrapper),
+                       js::GetObjectCompartment(js::UncheckedUnwrap(wrapper)));
+}
+
 inline bool
 ShouldWaiveXray(JSContext* cx, JSObject* originalObj)
 {
     unsigned flags;
     (void) js::UncheckedUnwrap(originalObj, /* stopAtOuter = */ true, &flags);
 
     // If the original object did not point through an Xray waiver, we're done.
     if (!(flags & WrapperFactory::WAIVE_XRAY_WRAPPER_FLAG))
@@ -463,18 +477,20 @@ WrapperFactory::Rewrap(JSContext* cx, Ha
         //
         // Xrays are a bidirectional protection, since it affords clarity to the
         // caller and privacy to the callee.
         bool sameOriginXrays = CompartmentPrivate::Get(origin)->wantXrays ||
                                CompartmentPrivate::Get(target)->wantXrays;
         bool wantXrays = !sameOrigin || sameOriginXrays;
 
         // If Xrays are warranted, the caller may waive them for non-security
-        // wrappers.
-        bool waiveXrays = wantXrays && !securityWrapper && HasWaiveXrayFlag(obj);
+        // wrappers (unless explicitly forbidden from doing so).
+        bool waiveXrays = wantXrays && !securityWrapper &&
+                          CompartmentPrivate::Get(target)->allowWaivers &&
+                          HasWaiveXrayFlag(obj);
 
         // We have slightly different behavior for the case when the object
         // being wrapped is in an XBL scope.
         bool originIsContentXBLScope = IsContentXBLScope(origin);
 
         wrapper = SelectWrapper(securityWrapper, wantXrays, xrayType, waiveXrays,
                                 originIsContentXBLScope, obj);
 
@@ -537,17 +553,17 @@ WrapperFactory::WaiveXrayAndWrap(JSConte
     // handing out waivers to callers that can't use them. The transitive waiving
     // machinery unconditionally calls WaiveXrayAndWrap on return values from
     // waived functions, even though the return value might be not be same-origin
     // with the function. So if we find ourselves trying to create a waiver for
     // |cx|, we should check whether the caller has any business with waivers
     // to things in |obj|'s compartment.
     JSCompartment* target = js::GetContextCompartment(cx);
     JSCompartment* origin = js::GetObjectCompartment(obj);
-    obj = AccessCheck::subsumes(target, origin) ? WaiveXray(cx, obj) : obj;
+    obj = AllowWaiver(target, origin) ? WaiveXray(cx, obj) : obj;
     if (!obj)
         return false;
 
     if (!JS_WrapObject(cx, &obj))
         return false;
     argObj.set(obj);
     return true;
 }
--- a/js/xpconnect/wrappers/WrapperFactory.h
+++ b/js/xpconnect/wrappers/WrapperFactory.h
@@ -32,16 +32,23 @@ class WrapperFactory {
     }
 
     static bool IsCOW(JSObject* wrapper);
 
     static JSObject* GetXrayWaiver(JS::HandleObject obj);
     static JSObject* CreateXrayWaiver(JSContext* cx, JS::HandleObject obj);
     static JSObject* WaiveXray(JSContext* cx, JSObject* obj);
 
+    // Computes whether we should allow the creation of an Xray waiver from
+    // |target| to |origin|.
+    static bool AllowWaiver(JSCompartment* target, JSCompartment* origin);
+
+    // Convenience method for the above, operating on a wrapper.
+    static bool AllowWaiver(JSObject* wrapper);
+
     // Prepare a given object for wrapping in a new compartment.
     static JSObject* PrepareForWrapping(JSContext* cx,
                                         JS::HandleObject scope,
                                         JS::HandleObject obj,
                                         JS::HandleObject objectPassedToWrap);
 
     // Rewrap an object that is about to cross compartment boundaries.
     static JSObject* Rewrap(JSContext* cx,
--- a/js/xpconnect/wrappers/XrayWrapper.cpp
+++ b/js/xpconnect/wrappers/XrayWrapper.cpp
@@ -1310,17 +1310,17 @@ wrappedJSObject_getter(JSContext* cx, un
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     if (!args.thisv().isObject()) {
         JS_ReportError(cx, "This value not an object");
         return false;
     }
     RootedObject wrapper(cx, &args.thisv().toObject());
     if (!IsWrapper(wrapper) || !WrapperFactory::IsXrayWrapper(wrapper) ||
-        !AccessCheck::wrapperSubsumes(wrapper)) {
+        !WrapperFactory::AllowWaiver(wrapper)) {
         JS_ReportError(cx, "Unexpected object");
         return false;
     }
 
     args.rval().setObject(*wrapper);
 
     return WrapperFactory::WaiveXrayAndWrap(cx, args.rval());
 }
@@ -1373,17 +1373,17 @@ XrayTraits::resolveOwnProperty(JSContext
         // Pretend the property lives on the wrapper.
         desc.object().set(wrapper);
         return true;
     }
 
     // Handle .wrappedJSObject for subsuming callers. This should move once we
     // sort out own-ness for the holder.
     if (id == GetRTIdByIndex(cx, XPCJSRuntime::IDX_WRAPPED_JSOBJECT) &&
-        AccessCheck::wrapperSubsumes(wrapper))
+        WrapperFactory::AllowWaiver(wrapper))
     {
         if (!JS_AlreadyHasOwnPropertyById(cx, holder, id, &found))
             return false;
         if (!found && !JS_DefinePropertyById(cx, holder, id, UndefinedHandleValue,
                                              JSPROP_ENUMERATE | JSPROP_SHARED,
                                              wrappedJSObject_getter)) {
             return false;
         }