Merge mozilla-inbound to mozilla-central a=merge
authorRazvan Maries <rmaries@mozilla.com>
Sat, 09 Mar 2019 23:51:51 +0200
changeset 463379 30385b68bea1a7f52c109b28a67f0ea611d88534
parent 463378 34a3d6b4f10f37b10428063096cad6af7c023e1a (current diff)
parent 463366 d08b9d599f252613a3e68758ed2a4e9b0459066c (diff)
child 463388 72d3e9ad8d2c4ecd7a6971318ef07e46422304e9
push id112379
push userrmaries@mozilla.com
push dateSat, 09 Mar 2019 22:00:54 +0000
treeherdermozilla-inbound@30385b68bea1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone67.0a1
first release with
nightly linux32
30385b68bea1 / 67.0a1 / 20190309215319 / files
nightly linux64
30385b68bea1 / 67.0a1 / 20190309215319 / files
nightly mac
30385b68bea1 / 67.0a1 / 20190309215319 / files
nightly win32
30385b68bea1 / 67.0a1 / 20190309215319 / files
nightly win64
30385b68bea1 / 67.0a1 / 20190309215319 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-inbound to mozilla-central a=merge
testing/web-platform/meta/svg/painting/parsing/stroke-miterlimit-computed.svg.ini
testing/web-platform/meta/svg/painting/parsing/stroke-miterlimit-valid.svg.ini
new file mode 100644
--- /dev/null
+++ b/dom/svg/crashtests/1531578-1.html
@@ -0,0 +1,17 @@
+<style>
+* {
+  filter: brightness(0.1638);
+  outline: 64px dotted;
+}
+</style>
+<script>
+function go() {
+  a.setAttribute("text-decoration", "line-through")
+}
+</script>
+<body onload=go()>
+<svg>
+<marker>
+<foreignObject id="a">
+<ins>
+
--- a/dom/svg/crashtests/crashtests.list
+++ b/dom/svg/crashtests/crashtests.list
@@ -86,9 +86,10 @@ load 1347617-2.svg
 load 1347617-3.svg
 load 1402798.html
 load 1419250-1.html
 load 1420492.html
 load 1477853.html
 load 1486488.html
 load 1493447.html
 skip-if(Android) load 1507961-1.html  # times out on Android due to the test size
+load 1531578-1.html
 load test_nested_svg.html
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/basic/bug1403679.js
@@ -0,0 +1,195 @@
+load(libdir + "asserts.js");
+
+const thisGlobal = this;
+const otherGlobalSameCompartment = newGlobal({sameCompartmentAs: thisGlobal});
+const otherGlobalNewCompartment = newGlobal({newCompartment: true});
+
+const globals = [thisGlobal, otherGlobalSameCompartment, otherGlobalNewCompartment];
+
+function testWithOptions(fn, variants = [undefined]) {
+    for (let variant of variants) {
+        for (let global of globals) {
+            for (let options of [
+                {},
+                {proxy: true},
+                {object: new FakeDOMObject()},
+            ]) {
+                fn(options, global, variant);
+            }
+        }
+    }
+}
+
+function testWithGlobals(fn) {
+    for (let global of globals) {
+        fn(global);
+    }
+}
+
+function testBasic(options, global) {
+    let {object: source, transplant} = transplantableObject(options);
+
+    // Validate that |source| is an object and |transplant| is a function.
+    assertEq(typeof source, "object");
+    assertEq(typeof transplant, "function");
+
+    // |source| is created in the current global.
+    assertEq(objectGlobal(source), this);
+
+    // |source|'s prototype is %ObjectPrototype%, unless it's a FakeDOMObject.
+    let oldPrototype;
+    if (options.object) {
+        oldPrototype = FakeDOMObject.prototype;
+    } else {
+        oldPrototype = Object.prototype;
+    }
+    assertEq(Object.getPrototypeOf(source), oldPrototype);
+
+    // Properties can be created on |source|.
+    assertEq(source.foo, undefined);
+    source.foo = 1;
+    assertEq(source.foo, 1);
+
+    // Calling |transplant| transplants the object and then returns undefined.
+    assertEq(transplant(global), undefined);
+
+    // |source| was moved into the new global. If the new global is in a
+    // different compartment, |source| is a now a CCW.
+    if (global !== otherGlobalNewCompartment) {
+        assertEq(objectGlobal(source), global);
+    } else {
+        assertEq(objectGlobal(source), null);
+        assertEq(isProxy(source), true);
+    }
+
+    // The properties are copied over to the swapped object.
+    assertEq(source.foo, 1);
+
+    // The prototype was changed to %ObjectPrototype% of |global| or the
+    // FakeDOMObject.prototype.
+    let newPrototype;
+    if (options.object) {
+        newPrototype = global.FakeDOMObject.prototype;
+    } else {
+        newPrototype = global.Object.prototype;
+    }
+    assertEq(Object.getPrototypeOf(source), newPrototype);
+}
+testWithOptions(testBasic);
+
+// Objects can be transplanted multiple times between globals.
+function testTransplantMulti(options, global1, global2) {
+    let {object: source, transplant} = transplantableObject(options);
+
+    transplant(global1);
+    transplant(global2);
+}
+testWithOptions(testTransplantMulti, globals);
+
+// Test the case when the source object already has a wrapper in the target global.
+function testHasWrapperInTarget(options, global) {
+    let {object: source, transplant} = transplantableObject(options);
+
+    // Create a wrapper for |source| in the other global.
+    global.p = source;
+    assertEq(global.eval("p"), source);
+
+    if (options.proxy) {
+        // It's a proxy object either way.
+        assertEq(global.eval("isProxy(p)"), true);
+    } else {
+        if (global === otherGlobalNewCompartment) {
+            // |isProxy| returns true because |p| is a CCW.
+            assertEq(global.eval("isProxy(p)"), true);
+        } else {
+            // |isProxy| returns false because |p| is not a CCW.
+            assertEq(global.eval("isProxy(p)"), false);
+        }
+    }
+
+    // And now transplant it into that global.
+    transplant(global);
+
+    assertEq(global.eval("p"), source);
+
+    if (options.proxy) {
+        // It's a proxy object either way.
+        assertEq(global.eval("isProxy(p)"), true);
+    } else {
+        // The previous CCW was replaced with a same-compartment object.
+        assertEq(global.eval("isProxy(p)"), false);
+    }
+}
+testWithOptions(testHasWrapperInTarget);
+
+// Test the case when the source object has a wrapper, but in a different compartment.
+function testHasWrapperOtherCompartment(options, global) {
+    let thirdGlobal = newGlobal({newCompartment: true});
+    let {object: source, transplant} = transplantableObject(options);
+
+    // Create a wrapper for |source| in the new global.
+    thirdGlobal.p = source;
+    assertEq(thirdGlobal.eval("p"), source);
+
+    // And now transplant the object.
+    transplant(global);
+
+    assertEq(thirdGlobal.eval("p"), source);
+}
+testWithOptions(testHasWrapperOtherCompartment);
+
+// Ensure a transplanted object is correctly handled by (weak) collections.
+function testCollections(options, global, AnySet) {
+    let {object, transplant} = transplantableObject(options);
+
+    let set = new AnySet();
+
+    assertEq(set.has(object), false);
+    set.add(object);
+    assertEq(set.has(object), true);
+
+    transplant(global);
+
+    assertEq(set.has(object), true);
+}
+testWithOptions(testCollections, [Set, WeakSet]);
+
+// Ensure DOM object slot is correctly transplanted.
+function testDOMObjectSlot(global) {
+    let domObject = new FakeDOMObject();
+    let expectedValue = domObject.x;
+    assertEq(typeof expectedValue, "number");
+
+    let {object, transplant} = transplantableObject({object: domObject});
+    assertEq(object, domObject);
+
+    transplant(global);
+
+    assertEq(object, domObject);
+    assertEq(domObject.x, expectedValue);
+}
+testWithGlobals(testDOMObjectSlot);
+
+function testArgumentValidation() {
+    // Throws an error if too many arguments are present.
+    assertThrowsInstanceOf(() => transplantableObject(thisGlobal, {}), Error);
+
+    let {object, transplant} = transplantableObject();
+
+    // Throws an error if called with no arguments.
+    assertThrowsInstanceOf(() => transplant(), Error);
+
+    // Throws an error if called with too many arguments.
+    assertThrowsInstanceOf(() => transplant(thisGlobal, {}), Error);
+
+    // Throws an error if the first argument isn't an object
+    assertThrowsInstanceOf(() => transplant(null), Error);
+
+    // Throws an error if the argument isn't a global object.
+    assertThrowsInstanceOf(() => transplant({}), Error);
+
+    // Throws an error if the 'object' option isn't a FakeDOMObject.
+    assertThrowsInstanceOf(() => transplant({object: null}), Error);
+    assertThrowsInstanceOf(() => transplant({object: {}}), Error);
+}
+testArgumentValidation();
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -93,16 +93,17 @@
 #include "js/Debug.h"
 #include "js/Equality.h"  // JS::SameValue
 #include "js/GCVector.h"
 #include "js/Initialization.h"
 #include "js/JSON.h"
 #include "js/MemoryFunctions.h"
 #include "js/Printf.h"
 #include "js/PropertySpec.h"
+#include "js/Realm.h"
 #include "js/SourceText.h"
 #include "js/StableStringChars.h"
 #include "js/StructuredClone.h"
 #include "js/SweepingAPI.h"
 #include "js/Wrapper.h"
 #include "perf/jsperf.h"
 #include "shell/jsoptparse.h"
 #include "shell/jsshell.h"
@@ -8055,16 +8056,297 @@ static bool WasmLoop(JSContext* cx, unsi
     }
   }
 
 #ifdef __AFL_HAVE_MANUAL_CONTROL  // to silence unreachable code warning
   return true;
 #endif
 }
 
+static constexpr uint32_t DOM_OBJECT_SLOT = 0;
+
+static const JSClass* GetDomClass();
+
+static JSObject* GetDOMPrototype(JSObject* global);
+
+static const JSClass TransplantableDOMObjectClass = {
+    "TransplantableDOMObject",
+    JSCLASS_IS_DOMJSCLASS | JSCLASS_HAS_RESERVED_SLOTS(1)};
+
+static const Class TransplantableDOMProxyObjectClass =
+    PROXY_CLASS_DEF("TransplantableDOMProxyObject",
+                    JSCLASS_IS_DOMJSCLASS | JSCLASS_HAS_RESERVED_SLOTS(1));
+
+class TransplantableDOMProxyHandler final : public ForwardingProxyHandler {
+ public:
+  static const TransplantableDOMProxyHandler singleton;
+  static const char family;
+
+  constexpr TransplantableDOMProxyHandler() : ForwardingProxyHandler(&family) {}
+
+  // These two proxy traps are called in |js::DeadProxyTargetValue|, which in
+  // turn is called when nuking proxies. Because this proxy can temporarily be
+  // without an object in its private slot, see |EnsureExpandoObject|, the
+  // default implementation inherited from ForwardingProxyHandler can't be used,
+  // since it tries to derive the callable/constructible value from the target.
+  bool isCallable(JSObject* obj) const override { return false; }
+  bool isConstructor(JSObject* obj) const override { return false; }
+
+  // Simplified implementation of |DOMProxyHandler::GetAndClearExpandoObject|.
+  static JSObject* GetAndClearExpandoObject(JSObject* obj) {
+    const Value& v = GetProxyPrivate(obj);
+    if (v.isUndefined()) {
+      return nullptr;
+    }
+
+    JSObject* expandoObject = &v.toObject();
+    SetProxyPrivate(obj, UndefinedValue());
+    return expandoObject;
+  }
+
+  // Simplified implementation of |DOMProxyHandler::EnsureExpandoObject|.
+  static JSObject* EnsureExpandoObject(JSContext* cx, JS::HandleObject obj) {
+    const Value& v = GetProxyPrivate(obj);
+    if (v.isObject()) {
+      return &v.toObject();
+    }
+    MOZ_ASSERT(v.isUndefined());
+
+    JSObject* expando = JS_NewObjectWithGivenProto(cx, nullptr, nullptr);
+    if (!expando) {
+      return nullptr;
+    }
+    SetProxyPrivate(obj, ObjectValue(*expando));
+    return expando;
+  }
+};
+
+const TransplantableDOMProxyHandler TransplantableDOMProxyHandler::singleton;
+const char TransplantableDOMProxyHandler::family = 0;
+
+enum TransplantObjectSlots {
+  TransplantSourceObject = 0,
+};
+
+static bool TransplantObject(JSContext* cx, unsigned argc, Value* vp) {
+  CallArgs args = CallArgsFromVp(argc, vp);
+  RootedFunction callee(cx, &args.callee().as<JSFunction>());
+
+  if (args.length() != 1 || !args[0].isObject()) {
+    JS_ReportErrorASCII(cx, "transplant() must be called with an object");
+    return false;
+  }
+
+  // |newGlobal| needs to be a GlobalObject.
+  RootedObject newGlobal(cx, CheckedUnwrapDynamic(&args[0].toObject(), cx));
+  if (!newGlobal) {
+    ReportAccessDenied(cx);
+    return false;
+  }
+  if (!JS_IsGlobalObject(newGlobal)) {
+    JS_ReportErrorNumberASCII(
+        cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE,
+        "\"global\" passed to transplant()", "not a global object");
+    return false;
+  }
+
+  const Value& reserved =
+      GetFunctionNativeReserved(callee, TransplantSourceObject);
+  RootedObject source(cx, CheckedUnwrapStatic(&reserved.toObject()));
+  if (!source) {
+    ReportAccessDenied(cx);
+    return false;
+  }
+  MOZ_ASSERT(source->getClass()->isDOMClass());
+
+  // The following steps aim to replicate the behavior of UpdateReflectorGlobal
+  // in dom/bindings/BindingUtils.cpp. In detail:
+  // 1. Check the recursion depth using CheckRecursionLimitConservative.
+  // 2. Enter the target compartment.
+  // 3. Clone the source object using JS_CloneObject.
+  // 4. Copy all properties from source to a temporary holder object.
+  // 5. Actually transplant the object.
+  // 6. And finally copy the properties back to the source object.
+  //
+  // As an extension to the algorithm in UpdateReflectorGlobal, we also allow
+  // to transplant an object into the same compartment as the source object to
+  // cover all operations supported by JS_TransplantObject.
+
+  if (!CheckRecursionLimitConservative(cx)) {
+    return false;
+  }
+
+  bool isProxy = IsProxy(source);
+  RootedObject expandoObject(cx);
+  if (isProxy) {
+    expandoObject =
+        TransplantableDOMProxyHandler::GetAndClearExpandoObject(source);
+  }
+
+  JSAutoRealm ar(cx, newGlobal);
+
+  RootedObject proto(cx);
+  if (JS_GetClass(source) == GetDomClass()) {
+    proto = GetDOMPrototype(newGlobal);
+  } else {
+    proto = JS::GetRealmObjectPrototype(cx);
+    if (!proto) {
+      return false;
+    }
+  }
+
+  RootedObject target(cx, JS_CloneObject(cx, source, proto));
+  if (!target) {
+    return false;
+  }
+
+  RootedObject copyFrom(cx, isProxy ? expandoObject : source);
+  RootedObject propertyHolder(cx,
+                              JS_NewObjectWithGivenProto(cx, nullptr, nullptr));
+  if (!propertyHolder) {
+    return false;
+  }
+
+  if (!JS_CopyPropertiesFrom(cx, propertyHolder, copyFrom)) {
+    return false;
+  }
+
+  SetReservedSlot(target, DOM_OBJECT_SLOT,
+                  GetReservedSlot(source, DOM_OBJECT_SLOT));
+  SetReservedSlot(source, DOM_OBJECT_SLOT, JS::PrivateValue(nullptr));
+
+  source = JS_TransplantObject(cx, source, target);
+  if (!source) {
+    return false;
+  }
+
+  RootedObject copyTo(cx);
+  if (isProxy) {
+    copyTo = TransplantableDOMProxyHandler::EnsureExpandoObject(cx, source);
+    if (!copyTo) {
+      return false;
+    }
+  } else {
+    copyTo = source;
+  }
+  if (!JS_CopyPropertiesFrom(cx, copyTo, propertyHolder)) {
+    return false;
+  }
+
+  args.rval().setUndefined();
+  return true;
+}
+
+static bool TransplantableObject(JSContext* cx, unsigned argc, Value* vp) {
+  CallArgs args = CallArgsFromVp(argc, vp);
+  RootedObject callee(cx, &args.callee());
+
+  if (args.length() > 1) {
+    ReportUsageErrorASCII(cx, callee, "Wrong number of arguments");
+    return false;
+  }
+
+  bool createProxy = false;
+  RootedObject source(cx);
+  if (args.length() == 1 && !args[0].isUndefined()) {
+    if (!args[0].isObject()) {
+      ReportUsageErrorASCII(cx, callee, "Argument must be an object");
+      return false;
+    }
+
+    RootedObject options(cx, &args[0].toObject());
+    RootedValue value(cx);
+
+    if (!JS_GetProperty(cx, options, "proxy", &value)) {
+      return false;
+    }
+    createProxy = JS::ToBoolean(value);
+
+    if (!JS_GetProperty(cx, options, "object", &value)) {
+      return false;
+    }
+    if (!value.isUndefined()) {
+      if (!value.isObject()) {
+        ReportUsageErrorASCII(cx, callee, "'object' option must be an object");
+        return false;
+      }
+
+      source = &value.toObject();
+      if (JS_GetClass(source) != GetDomClass()) {
+        ReportUsageErrorASCII(cx, callee, "Object not a FakeDOMObject");
+        return false;
+      }
+
+      // |source| must be a tenured object to be transplantable.
+      if (gc::IsInsideNursery(source)) {
+        JS_GC(cx);
+
+        MOZ_ASSERT(!gc::IsInsideNursery(source),
+                   "Live objects should be tenured after one GC, because "
+                   "the nursery has only a single generation");
+      }
+    }
+  }
+
+  if (!source) {
+    if (!createProxy) {
+      source = NewBuiltinClassInstance(
+          cx, Valueify(&TransplantableDOMObjectClass), TenuredObject);
+      if (!source) {
+        return false;
+      }
+
+      SetReservedSlot(source, DOM_OBJECT_SLOT, JS::PrivateValue(nullptr));
+    } else {
+      JSObject* expando = JS_NewPlainObject(cx);
+      if (!expando) {
+        return false;
+      }
+      RootedValue expandoVal(cx, ObjectValue(*expando));
+
+      ProxyOptions options;
+      options.setClass(&TransplantableDOMProxyObjectClass);
+      options.setLazyProto(true);
+
+      source = NewProxyObject(cx, &TransplantableDOMProxyHandler::singleton,
+                              expandoVal, nullptr, options);
+      if (!source) {
+        return false;
+      }
+
+      SetProxyReservedSlot(source, DOM_OBJECT_SLOT, JS::PrivateValue(nullptr));
+    }
+  }
+
+  jsid emptyId = NameToId(cx->names().empty);
+  RootedObject transplant(
+      cx, NewFunctionByIdWithReserved(cx, TransplantObject, 0, 0, emptyId));
+  if (!transplant) {
+    return false;
+  }
+
+  SetFunctionNativeReserved(transplant, TransplantSourceObject,
+                            ObjectValue(*source));
+
+  RootedObject result(cx, JS_NewPlainObject(cx));
+  if (!result) {
+    return false;
+  }
+
+  RootedValue sourceVal(cx, ObjectValue(*source));
+  RootedValue transplantVal(cx, ObjectValue(*transplant));
+  if (!JS_DefineProperty(cx, result, "object", sourceVal, 0) ||
+      !JS_DefineProperty(cx, result, "transplant", transplantVal, 0)) {
+    return false;
+  }
+
+  args.rval().setObject(*result);
+  return true;
+}
+
 // clang-format off
 static const JSFunctionSpecWithHelp shell_functions[] = {
     JS_FN_HELP("clone", Clone, 1, 0,
 "clone(fun[, scope])",
 "  Clone function object."),
 
     JS_FN_HELP("options", Options, 0, 0,
 "options([option ...])",
@@ -8700,16 +8982,29 @@ JS_FN_HELP("parseBin", BinParse, 1, 0,
 #endif // ENABLE_INTL_API
 
     JS_FN_HELP("wasmCompileInSeparateProcess", WasmCompileInSeparateProcess, 1, 0,
 "wasmCompileInSeparateProcess(buffer)",
 "  Compile the given buffer in a separate process, serialize the resulting\n"
 "  wasm::Module into bytes, and deserialize those bytes in the current\n"
 "  process, returning the resulting WebAssembly.Module."),
 
+    JS_FN_HELP("transplantableObject", TransplantableObject, 0, 0,
+"transplantableObject([options])",
+"  Returns the pair {object, transplant}. |object| is an object which can be\n"
+"  transplanted into a new object when the |transplant| function, which must\n"
+"  be invoked with a global object, is called.\n"
+"  |object| is swapped with a cross-compartment wrapper if the global object\n"
+"  is in a different compartment.\n"
+"\n"
+"  If options is given, it may have any of the following properties:\n"
+"    proxy: Create a DOM Proxy object instead of a plain DOM object.\n"
+"    object: Don't create a new DOM object, but instead use the supplied\n"
+"            FakeDOMObject."),
+
     JS_FS_HELP_END
 };
 // clang-format on
 
 // clang-format off
 static const JSFunctionSpecWithHelp fuzzing_unsafe_functions[] = {
     JS_FN_HELP("getSelfHostedValue", GetSelfHostedValue, 1, 0,
 "getSelfHostedValue()",
@@ -9216,83 +9511,86 @@ static const JSClassOps global_classOps 
                                            global_resolve,
                                            global_mayResolve,
                                            nullptr,
                                            nullptr,
                                            nullptr,
                                            nullptr,
                                            JS_GlobalObjectTraceHook};
 
-static const JSClass global_class = {"global", JSCLASS_GLOBAL_FLAGS,
-                                     &global_classOps};
+static constexpr uint32_t DOM_PROTOTYPE_SLOT = JSCLASS_GLOBAL_SLOT_COUNT;
+static constexpr uint32_t DOM_GLOBAL_SLOTS = 1;
+
+static const JSClass global_class = {
+    "global",
+    JSCLASS_GLOBAL_FLAGS | JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(DOM_GLOBAL_SLOTS),
+    &global_classOps};
 
 /*
  * Define a FakeDOMObject constructor. It returns an object with a getter,
  * setter and method with attached JitInfo. This object can be used to test
  * IonMonkey DOM optimizations in the shell.
  */
-static const uint32_t DOM_OBJECT_SLOT = 0;
+
+/* Fow now just use to a constant we can check. */
+static const void* DOM_PRIVATE_VALUE = (void*)0x1234;
 
 static bool dom_genericGetter(JSContext* cx, unsigned argc, JS::Value* vp);
 
 static bool dom_genericSetter(JSContext* cx, unsigned argc, JS::Value* vp);
 
 static bool dom_genericMethod(JSContext* cx, unsigned argc, JS::Value* vp);
 
-#ifdef DEBUG
-static const JSClass* GetDomClass();
-#endif
-
 static bool dom_get_x(JSContext* cx, HandleObject obj, void* self,
                       JSJitGetterCallArgs args) {
   MOZ_ASSERT(JS_GetClass(obj) == GetDomClass());
-  MOZ_ASSERT(self == (void*)0x1234);
+  MOZ_ASSERT(self == DOM_PRIVATE_VALUE);
   args.rval().set(JS_NumberValue(double(3.14)));
   return true;
 }
 
 static bool dom_set_x(JSContext* cx, HandleObject obj, void* self,
                       JSJitSetterCallArgs args) {
   MOZ_ASSERT(JS_GetClass(obj) == GetDomClass());
-  MOZ_ASSERT(self == (void*)0x1234);
+  MOZ_ASSERT(self == DOM_PRIVATE_VALUE);
   return true;
 }
 
 static bool dom_get_global(JSContext* cx, HandleObject obj, void* self,
                            JSJitGetterCallArgs args) {
   MOZ_ASSERT(JS_GetClass(obj) == GetDomClass());
-  MOZ_ASSERT(self == (void*)0x1234);
+  MOZ_ASSERT(self == DOM_PRIVATE_VALUE);
 
   // Return the current global (instead of obj->global()) to test cx->realm
   // switching in the JIT.
   args.rval().setObject(*ToWindowProxyIfWindow(cx->global()));
 
   return true;
 }
 
 static bool dom_set_global(JSContext* cx, HandleObject obj, void* self,
                            JSJitSetterCallArgs args) {
   MOZ_ASSERT(JS_GetClass(obj) == GetDomClass());
-  MOZ_ASSERT(self == (void*)0x1234);
+  MOZ_ASSERT(self == DOM_PRIVATE_VALUE);
 
   // Throw an exception if our argument is not the current global. This lets
   // us test cx->realm switching.
   if (!args[0].isObject() ||
       ToWindowIfWindowProxy(&args[0].toObject()) != cx->global()) {
     JS_ReportErrorASCII(cx, "Setter not called with matching global argument");
     return false;
   }
 
   return true;
 }
 
 static bool dom_doFoo(JSContext* cx, HandleObject obj, void* self,
                       const JSJitMethodCallArgs& args) {
   MOZ_ASSERT(JS_GetClass(obj) == GetDomClass());
-  MOZ_ASSERT(self == (void*)0x1234);
+  MOZ_ASSERT(self == DOM_PRIVATE_VALUE);
   MOZ_ASSERT(cx->realm() == args.callee().as<JSFunction>().realm());
 
   /* Just return args.length(). */
   args.rval().setInt32(args.length());
   return true;
 }
 
 static const JSJitInfo dom_x_getterinfo = {
@@ -9396,19 +9694,17 @@ static const JSPropertySpec dom_props[] 
 static const JSFunctionSpec dom_methods[] = {
     JS_FNINFO("doFoo", dom_genericMethod, &doFoo_methodinfo, 3,
               JSPROP_ENUMERATE),
     JS_FS_END};
 
 static const JSClass dom_class = {
     "FakeDOMObject", JSCLASS_IS_DOMJSCLASS | JSCLASS_HAS_RESERVED_SLOTS(2)};
 
-#ifdef DEBUG
 static const JSClass* GetDomClass() { return &dom_class; }
-#endif
 
 static bool dom_genericGetter(JSContext* cx, unsigned argc, JS::Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
 
   if (!args.thisv().isObject()) {
     args.rval().setUndefined();
     return true;
   }
@@ -9471,18 +9767,24 @@ static bool dom_genericMethod(JSContext*
 
   const JSJitInfo* info = FUNCTION_VALUE_TO_JITINFO(args.calleev());
   MOZ_ASSERT(info->type() == JSJitInfo::Method);
   JSJitMethodOp method = info->method;
   return method(cx, obj, val.toPrivate(), JSJitMethodCallArgs(args));
 }
 
 static void InitDOMObject(HandleObject obj) {
-  /* Fow now just initialize to a constant we can check. */
-  SetReservedSlot(obj, DOM_OBJECT_SLOT, PrivateValue((void*)0x1234));
+  SetReservedSlot(obj, DOM_OBJECT_SLOT,
+                  PrivateValue(const_cast<void*>(DOM_PRIVATE_VALUE)));
+}
+
+static JSObject* GetDOMPrototype(JSObject* global) {
+  MOZ_ASSERT(JS_IsGlobalObject(global));
+  MOZ_ASSERT(GetReservedSlot(global, DOM_PROTOTYPE_SLOT).isObject());
+  return &GetReservedSlot(global, DOM_PROTOTYPE_SLOT).toObject();
 }
 
 static bool dom_constructor(JSContext* cx, unsigned argc, JS::Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
 
   RootedObject callee(cx, &args.callee());
   RootedValue protov(cx);
   if (!GetProperty(cx, callee, callee, cx->names().prototype, &protov)) {
@@ -9504,19 +9806,18 @@ static bool dom_constructor(JSContext* c
   InitDOMObject(domObj);
 
   args.rval().setObject(*domObj);
   return true;
 }
 
 static bool InstanceClassHasProtoAtDepth(const Class* clasp, uint32_t protoID,
                                          uint32_t depth) {
-  // There's only a single (fake) DOM object in the shell, so just return
-  // true.
-  return true;
+  // Only the (fake) DOM object supports any JIT optimizations.
+  return clasp == Valueify(GetDomClass());
 }
 
 static bool ShellBuildId(JS::BuildIdCharVector* buildId) {
   // The browser embeds the date into the buildid and the buildid is embedded
   // in the binary, so every 'make' necessarily builds a new firefox binary.
   // Fortunately, the actual firefox executable is tiny -- all the code is in
   // libxul.so and other shared modules -- so this isn't a big deal. Not so
   // for the statically-linked JS shell. To avoid recompiling js.cpp and
@@ -9635,16 +9936,21 @@ static JSObject* NewGlobalObject(JSConte
 
     RootedObject domProto(
         cx, JS_InitClass(cx, glob, nullptr, &dom_class, dom_constructor, 0,
                          dom_props, dom_methods, nullptr, nullptr));
     if (!domProto) {
       return nullptr;
     }
 
+    // FakeDOMObject.prototype is the only DOM object which needs to retrieved
+    // in the shell; store it directly instead of creating a separate layer
+    // (ProtoAndIfaceCache) as done in the browser.
+    SetReservedSlot(glob, DOM_PROTOTYPE_SLOT, ObjectValue(*domProto));
+
     /* Initialize FakeDOMObject.prototype */
     InitDOMObject(domProto);
 
     JS_FireOnNewGlobalObject(cx, glob);
   }
 
   return glob;
 }
--- a/js/src/vm/JSObject.cpp
+++ b/js/src/vm/JSObject.cpp
@@ -1352,17 +1352,20 @@ JSObject* js::CloneObject(JSContext* cx,
   if (!obj->isNative() && !obj->is<ProxyObject>()) {
     JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                               JSMSG_CANT_CLONE_OBJECT);
     return nullptr;
   }
 
   RootedObject clone(cx);
   if (obj->isNative()) {
-    clone = NewObjectWithGivenTaggedProto(cx, obj->getClass(), proto);
+    // CloneObject is used to create the target object for JSObject::swap() and
+    // swap() requires its arguments are tenured, so ensure tenure allocation.
+    clone = NewObjectWithGivenTaggedProto(cx, obj->getClass(), proto,
+                                          NewObjectKind::TenuredObject);
     if (!clone) {
       return nullptr;
     }
 
     if (clone->is<JSFunction>() &&
         (obj->compartment() != clone->compartment())) {
       JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                 JSMSG_CANT_CLONE_OBJECT);
@@ -1372,18 +1375,27 @@ JSObject* js::CloneObject(JSContext* cx,
     if (obj->as<NativeObject>().hasPrivate()) {
       clone->as<NativeObject>().setPrivate(
           obj->as<NativeObject>().getPrivate());
     }
   } else {
     ProxyOptions options;
     options.setClass(obj->getClass());
 
-    clone = ProxyObject::New(cx, GetProxyHandler(obj), JS::NullHandleValue,
-                             proto, options);
+    auto* handler = GetProxyHandler(obj);
+
+    // Same as above, require tenure allocation of the clone. This means for
+    // proxy objects we need to reject nursery allocatable proxies.
+    if (handler->canNurseryAllocate()) {
+      JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                JSMSG_CANT_CLONE_OBJECT);
+      return nullptr;
+    }
+
+    clone = ProxyObject::New(cx, handler, JS::NullHandleValue, proto, options);
     if (!clone) {
       return nullptr;
     }
 
     if (!CopyProxyObject(cx, obj.as<ProxyObject>(), clone.as<ProxyObject>())) {
       return nullptr;
     }
   }
--- a/layout/base/RestyleManager.cpp
+++ b/layout/base/RestyleManager.cpp
@@ -1613,17 +1613,18 @@ void RestyleManager::ProcessRestyledFram
       NS_ASSERTION(frame, "This shouldn't happen");
 
       if (!frame->FrameMaintainsOverflow()) {
         // frame does not maintain overflow rects, so avoid calling
         // FinishAndStoreOverflow on it:
         hint &=
             ~(nsChangeHint_UpdateOverflow | nsChangeHint_ChildrenOnlyTransform |
               nsChangeHint_UpdatePostTransformOverflow |
-              nsChangeHint_UpdateParentOverflow);
+              nsChangeHint_UpdateParentOverflow |
+              nsChangeHint_UpdateSubtreeOverflow);
       }
 
       if (primaryFrame &&
           !(primaryFrame->GetStateBits() & NS_FRAME_MAY_BE_TRANSFORMED)) {
         // Frame can not be transformed, and thus a change in transform will
         // have no effect and we should not use the
         // nsChangeHint_UpdatePostTransformOverflow hint.
         hint &= ~nsChangeHint_UpdatePostTransformOverflow;
--- a/layout/style/test/property_database.js
+++ b/layout/style/test/property_database.js
@@ -5589,18 +5589,18 @@ var gCSSProperties = {
     other_values: [ "round", "bevel" ],
     invalid_values: []
   },
   "stroke-miterlimit": {
     domProp: "strokeMiterlimit",
     inherited: true,
     type: CSS_TYPE_LONGHAND,
     initial_values: [ "4" ],
-    other_values: [ "1", "7", "5000", "1.1" ],
-    invalid_values: [ "0.9", "0", "-1", "3px", "-0.3" ]
+    other_values: [ "0", "0.9", "1", "7", "5000", "1.1" ],
+    invalid_values: [ "-1", "3px", "-0.3" ]
   },
   "stroke-opacity": {
     domProp: "strokeOpacity",
     inherited: true,
     type: CSS_TYPE_LONGHAND,
     initial_values: [ "1", "2.8", "1.000" ],
     other_values: [ "0", "0.3", "-7.3", "context-fill-opacity", "context-stroke-opacity" ],
     invalid_values: []
--- a/layout/style/test/test_transitions_per_property.html
+++ b/layout/style/test/test_transitions_per_property.html
@@ -258,18 +258,19 @@ var supported_properties = {
                        // opacity is clamped in computed style
                        // (not parsing/interpolation)
                        test_float_zeroToOne_clamped ],
     "stroke": [ test_color_transition,
                 test_currentcolor_transition ],
     "stroke-dasharray": [ test_dasharray_transition ],
     "stroke-dashoffset": [ test_length_transition, test_percent_transition,
                            test_length_unclamped, test_percent_unclamped, ],
-    "stroke-miterlimit": [ test_float_aboveOne_transition,
-                           test_float_aboveOne_clamped ],
+    "stroke-miterlimit": [ test_float_zeroToOne_transition,
+                           test_float_aboveOne_transition,
+                           test_float_aboveZero_clamped ],
     "stroke-opacity" : [ test_float_zeroToOne_transition,
                          // opacity is clamped in computed style
                          // (not parsing/interpolation)
                          test_float_zeroToOne_clamped ],
     "stroke-width": [ test_length_transition, test_percent_transition,
                       test_length_clamped, test_percent_clamped, ],
     "-moz-tab-size": [ test_float_zeroToOne_transition,
                        test_float_aboveOne_transition, test_length_clamped ],
@@ -1345,38 +1346,38 @@ function test_float_zeroToOne_clamped_or
      "float-valued property " + prop + ": flush before clamping test");
   div.style.setProperty("transition-property", prop, "");
   div.style.setProperty(prop, "1", "");
   (is_clamped ? is : isnot)(cs.getPropertyValue(prop), "0",
      "float-valued property " + prop + ": clamping of negatives");
   div.style.setProperty("transition-timing-function", "linear", "");
 }
 
-// Test using float values in the range [1, infinity) (e.g. stroke-miterlimit)
+// Test using float values in the range [1, infinity)
 function test_float_aboveOne_transition(prop) {
   div.style.setProperty("transition-property", "none", "");
   div.style.setProperty(prop, "1", "");
   is(cs.getPropertyValue(prop), "1",
      "float-valued property " + prop + ": computed value before transition");
   div.style.setProperty("transition-property", prop, "");
   div.style.setProperty(prop, "2.1", "");
   is(cs.getPropertyValue(prop), "1.275",
      "float-valued property " + prop + ": interpolation of floats");
   check_distance(prop, "1", "1.275", "2.1");
 }
 
-function test_float_aboveOne_clamped(prop) {
+function test_float_aboveZero_clamped(prop) {
   div.style.setProperty("transition-timing-function", FUNC_NEGATIVE, "");
   div.style.setProperty("transition-property", "none", "");
-  div.style.setProperty(prop, "1", "");
-  is(cs.getPropertyValue(prop), "1",
+  div.style.setProperty(prop, "0", "");
+  is(cs.getPropertyValue(prop), "0",
      "float-valued property " + prop + ": flush before clamping test");
   div.style.setProperty("transition-property", prop, "");
   div.style.setProperty(prop, "5", "");
-  is(cs.getPropertyValue(prop), "1",
+  is(cs.getPropertyValue(prop), "0",
      "float-valued property " + prop + ": clamping of negatives");
   div.style.setProperty("transition-timing-function", "linear", "");
 }
 
 function test_percent_transition(prop) {
   div.style.setProperty("transition-property", "none", "");
   div.style.setProperty(prop, "25%", "");
   var av = cs.getPropertyValue(prop);
--- a/layout/svg/SVGTextFrame.cpp
+++ b/layout/svg/SVGTextFrame.cpp
@@ -3012,18 +3012,17 @@ void SVGTextFrame::ReflowSVGNonDisplayTe
   // We had a style change, so we mark this frame as dirty so that the next
   // time it is painted, we reflow the anonymous block frame.
   AddStateBits(NS_FRAME_IS_DIRTY);
 
   // We also need to call InvalidateRenderingObservers, so that if the <text>
   // element is within a <mask>, say, the element referencing the <mask> will
   // be updated, which will then cause this SVGTextFrame to be painted and
   // in doing so cause the anonymous block frame to be reflowed.
-  nsLayoutUtils::PostRestyleEvent(mContent->AsElement(), nsRestyleHint(0),
-                                  nsChangeHint_InvalidateRenderingObservers);
+  SVGObserverUtils::InvalidateRenderingObservers(this);
 
   // Finally, we need to actually reflow the anonymous block frame and update
   // mPositions, in case we are being reflowed immediately after a DOM
   // mutation that needs frame reconstruction.
   MaybeReflowAnonymousBlockChild();
   UpdateGlyphPositioning();
 }
 
--- a/layout/svg/nsSVGUtils.cpp
+++ b/layout/svg/nsSVGUtils.cpp
@@ -1290,16 +1290,18 @@ gfxRect nsSVGUtils::PathExtentsToMaxStro
   // For a shape without corners the stroke can only extend half the stroke
   // width from the path in the x/y-axis directions. For shapes with corners
   // the stroke can extend by sqrt(1/2) (think 45 degree rotated rect, or line
   // with stroke-linecaps="square").
   double styleExpansionFactor = strokeMayHaveCorners ? M_SQRT1_2 : 0.5;
 
   // The stroke can extend even further for paths that can be affected by
   // stroke-miterlimit.
+  // We only need to do this if the limit is greater than 1, but it's probably
+  // not worth optimizing for that.
   bool affectedByMiterlimit = aFrame->GetContent()->IsAnyOfSVGElements(
       nsGkAtoms::path, nsGkAtoms::polyline, nsGkAtoms::polygon);
 
   if (affectedByMiterlimit) {
     const nsStyleSVG* style = aFrame->StyleSVG();
     if (style->mStrokeLinejoin == NS_STYLE_STROKE_LINEJOIN_MITER &&
         styleExpansionFactor < style->mStrokeMiterlimit / 2.0) {
       styleExpansionFactor = style->mStrokeMiterlimit / 2.0;
--- a/servo/components/style/properties/longhands/inherited_svg.mako.rs
+++ b/servo/components/style/properties/longhands/inherited_svg.mako.rs
@@ -105,21 +105,21 @@
     "miter round bevel",
     products="gecko",
     animation_value_type="discrete",
     spec="https://www.w3.org/TR/SVG11/painting.html#StrokeLinejoinProperty",
 )}
 
 ${helpers.predefined_type(
     "stroke-miterlimit",
-    "GreaterThanOrEqualToOneNumber",
+    "NonNegativeNumber",
     "From::from(4.0)",
     products="gecko",
-    animation_value_type="crate::values::computed::GreaterThanOrEqualToOneNumber",
-    spec="https://www.w3.org/TR/SVG11/painting.html#StrokeMiterlimitProperty",
+    animation_value_type="crate::values::computed::NonNegativeNumber",
+    spec="https://www.w3.org/TR/SVG2/painting.html#StrokeMiterlimitProperty",
 )}
 
 ${helpers.predefined_type(
     "stroke-opacity",
     "SVGOpacity",
     "Default::default()",
     products="gecko",
     animation_value_type="ComputedValue",
deleted file mode 100644
--- a/testing/web-platform/meta/svg/painting/parsing/stroke-miterlimit-computed.svg.ini
+++ /dev/null
@@ -1,7 +0,0 @@
-[stroke-miterlimit-computed.svg]
-  [Property stroke-miterlimit value '0.5' computes to '0.5']
-    expected: FAIL
-
-  [Property stroke-miterlimit value '0' computes to '0']
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/svg/painting/parsing/stroke-miterlimit-valid.svg.ini
+++ /dev/null
@@ -1,7 +0,0 @@
-[stroke-miterlimit-valid.svg]
-  [e.style['stroke-miterlimit'\] = "0.5" should set the property value]
-    expected: FAIL
-
-  [e.style['stroke-miterlimit'\] = "0" should set the property value]
-    expected: FAIL
-