part 1.  Change the cross-compartment wrappers we use for web objects so we can avoid recomputing them when document.domain changes.

We want to use a transparent CCW if there is any pair of globals, one from each
compartment, which are, or have ever been, same origin-domain in the HTML spec
sense.  This is obviously required in the "are now same origin-domain" case, and in
the "were same origin-domain" case it's required because there may be existing
transparent CCWs between the compartments and we don't want them to become
opaque due to a roundtrip through the compartment boundary.

In practice, we need to consider two cases:

1) The two compartments started out same-origin.  In this case the two
CompartmentOriginInfos will have matching (in the Equals() sense)
GetPrincipalIgnoringDocumentDomain().  They will also have matching SiteRef(),
of course.

2) The two compartments started out different-origin but then at some point two
globals in the compartments ended up same origin-domain.  That requires that
the two globals be same TLD+1 and have both set document.domain.  So in this
case the two CompartmentOriginInfos have matching SiteRef() and both test true
for HasChangedDocumentDomain().

We only need to worry about this for web compartments, which means that we only
need to worry about cases when security checks are symmetric
(i.e. originSubsumesTarget == targetSubsumesOrigin) and neither compartment is
forcing Xrays.

Differential Revision: https://phabricator.services.mozilla.com/D18031

bugzilla-url: https://bugzilla.mozilla.org/show_bug.cgi?id=1514050
gecko-commit: 97aaced3f817773ab004df571077806b59632555
gecko-integration-branch: central
gecko-reviewers: bholley
diff --git a/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_access_details.sub.html b/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_access_details.sub.html
new file mode 100644
index 0000000..4628085
--- /dev/null
+++ b/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_access_details.sub.html
@@ -0,0 +1,193 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="support/document_domain_frame.sub.js"></script>
+<body>
+<script>
+promise_test(async (t) => {
+  let frame1 = await createFrame(t, "control1-1", "{{domains[www1]}}");
+  let frame2 = await createFrame(t, "control1-2", "{{domains[www1]}}");
+  let result = await postMessageToFrame(frame1, { 'poke-at-sibling': "control1-2" });
+  assert_equals(result.data, "omg!");
+}, "Access allowed if same-origin with no 'document.domain' modification. (Sanity check)");
+
+promise_test(async (t) => {
+  let frame1 = await createFrame(t, "control2-1", "{{domains[www1]}}");
+  let frame2 = await createFrame(t, "control2-2", "{{domains[www2]}}");
+  let result = await postMessageToFrame(frame1, { 'poke-at-sibling': "control2-2" });
+  assert_equals(result.data, "SecurityError");
+}, "Access not allowed if different-origin with no 'document.domain' modification. (Sanity check)");
+
+promise_test(async (t) => {
+  let frame1 = await createFrame(t, "one-set-one-not-1", "{{domains[www1]}}");
+  let frame2 = await createFrame(t, "one-set-one-not-2", "{{domains[www1]}}");
+  await postMessageToFrame(frame1, { domain: "{{domains[www1]}}" });
+
+  let result = await postMessageToFrame(frame1, { 'poke-at-sibling': "one-set-one-not-2" });
+  assert_equals(result.data, "SecurityError");
+
+  result = await postMessageToFrame(frame2, { 'poke-at-sibling': "one-set-one-not-1" });
+  assert_equals(result.data, "SecurityError");
+}, "Access disallowed if same-origin but only one sets document.domain.");
+
+promise_test(async (t) => {
+  var frame1 = await createFrame(t, "both-set-to-existing-1", "{{domains[www1]}}");
+  var frame2 = await createFrame(t, "both-set-to-existing-2", "{{domains[www1]}}");
+  let result = await postMessageToFrame(frame1, { domain: "{{domains[www1]}}" });
+  assert_equals(result.data, "Done");
+
+  result = await postMessageToFrame(frame2, { domain: "{{domains[www1]}}" });
+  assert_equals(result.data, "Done");
+
+  result = await postMessageToFrame(frame1, { 'poke-at-sibling': "both-set-to-existing-2" });
+  assert_equals(result.data, "omg!");
+
+  result = await postMessageToFrame(frame2, { 'poke-at-sibling': "both-set-to-existing-1" });
+  assert_equals(result.data, "omg!");
+}, "Access allowed if same-origin and both set document.domain to existing value.");
+
+promise_test(async (t) => {
+  var frame1 = await createFrame(t, "both-set-to-parent-1", "{{domains[www1]}}");
+  var frame2 = await createFrame(t, "both-set-to-parent-2", "{{domains[www2]}}");
+  let result = await postMessageToFrame(frame1, { domain: "{{domains[]}}" });
+  assert_equals(result.data, "Done");
+
+  result = await postMessageToFrame(frame2, { domain: "{{domains[]}}" });
+  assert_equals(result.data, "Done");
+
+  result = await postMessageToFrame(frame1, { 'poke-at-sibling': "both-set-to-parent-2" });
+  assert_equals(result.data, "omg!");
+
+  result = await postMessageToFrame(frame2, { 'poke-at-sibling': "both-set-to-parent-1" });
+  assert_equals(result.data, "omg!");
+}, "Access allowed if different-origin but both set document.domain to parent domain.");
+
+promise_test(async (t) => {
+  var frame1 = await createFrame(t, "allow-then-revoke-1", "{{domains[www1]}}");
+  var frame2 = await createFrame(t, "allow-then-revoke-2", "{{domains[www1]}}");
+  let result = await postMessageToFrame(frame1, { domain: "{{domains[www1]}}" });
+  assert_equals(result.data, "Done");
+
+  result = await postMessageToFrame(frame2, { domain: "{{domains[www1]}}" });
+  assert_equals(result.data, "Done");
+
+  result = await postMessageToFrame(frame1, { 'poke-at-sibling': "allow-then-revoke-2" });
+  assert_equals(result.data, "omg!");
+
+  result = await postMessageToFrame(frame2, { 'poke-at-sibling': "allow-then-revoke-1" });
+  assert_equals(result.data, "omg!");
+
+  result = await postMessageToFrame(frame1, { domain: "{{domains[]}}" });
+  assert_equals(result.data, "Done");
+
+  result = await postMessageToFrame(frame1, { 'poke-at-sibling': "allow-then-revoke-2" });
+  assert_equals(result.data, "SecurityError");
+
+  result = await postMessageToFrame(frame2, { 'poke-at-sibling': "allow-then-revoke-1" });
+  assert_equals(result.data, "SecurityError");
+}, "Access disallowed again if same-origin, both set document-domain to existing value, then one sets to parent.");
+
+promise_test(async (t) => {
+  let frame1 = await createFrame(t, "revoke-Window-1", "{{domains[www1]}}");
+  let frame2 = await createFrame(t, "revoke-Window-2", "{{domains[www1]}}");
+
+  let result = await postMessageToFrame(frame1, { cache: ["parent", "revoke-Window-2"] });
+  assert_equals(result.data, "cached");
+
+  result = await postMessageToFrame(frame1, 'touch-cached');
+  assert_equals(result.data, "Reachable 1");
+
+  result = await postMessageToFrame(frame2, { cache: ["parent", "revoke-Window-1"] });
+  assert_equals(result.data, "cached");
+
+  result = await postMessageToFrame(frame1, 'touch-cached');
+  assert_equals(result.data, "Reachable 1");
+
+  result = await postMessageToFrame(frame1, { domain: "{{domains[www1]}}" });
+  assert_equals(result.data, "Done");
+
+  result = await postMessageToFrame(frame1, 'touch-cached');
+  assert_equals(result.data, "SecurityError");
+
+  result = await postMessageToFrame(frame2, 'touch-cached');
+  assert_equals(result.data, "SecurityError");
+}, "Access is revoked to Window object when we stop being same effective script origin due to document.domain.");
+
+promise_test(async (t) => {
+  let frame1 = await createFrame(t, "revoke-Location-1", "{{hosts[][www1]}}");
+  let frame2 = await createFrame(t, "revoke-Location-2", "{{domains[www1]}}");
+
+  let result = await postMessageToFrame(frame1, { cache: ["parent", "revoke-Location-2", "location"] });
+  assert_equals(result.data, "cached");
+
+  result = await postMessageToFrame(frame1, 'touch-cached');
+  assert_equals(result.data, "Reachable 3");
+
+  result = await postMessageToFrame(frame2, { cache: ["parent", "revoke-Location-1", "location"] });
+  assert_equals(result.data, "cached");
+
+  result = await postMessageToFrame(frame1, 'touch-cached');
+  assert_equals(result.data, "Reachable 3");
+
+  result = await postMessageToFrame(frame1, { domain: "{{domains[www1]}}" });
+  assert_equals(result.data, "Done");
+
+  result = await postMessageToFrame(frame1, 'touch-cached');
+  assert_equals(result.data, "SecurityError");
+
+  result = await postMessageToFrame(frame2, 'touch-cached');
+  assert_equals(result.data, "SecurityError");
+}, "Access is revoked to Location object when we stop being same effective script origin due to document.domain.");
+
+promise_test(async (t) => {
+  let frame1 = await createFrame(t, "no-revoke-Document-1", "{{domains[www1]}}");
+  let frame2 = await createFrame(t, "no-revoke-Document-2", "{{domains[www1]}}");
+
+  let result = await postMessageToFrame(frame1, { cache: ["parent", "no-revoke-Document-2", "document"] });
+  assert_equals(result.data, "cached");
+
+  result = await postMessageToFrame(frame1, 'touch-cached');
+  assert_equals(result.data, "Reachable 4");
+
+  result = await postMessageToFrame(frame2, { cache: ["parent", "no-revoke-Document-1", "document"] });
+  assert_equals(result.data, "cached");
+
+  result = await postMessageToFrame(frame1, 'touch-cached');
+  assert_equals(result.data, "Reachable 4");
+
+  result = await postMessageToFrame(frame1, { domain: "{{domains[www1]}}" });
+  assert_equals(result.data, "Done");
+
+  result = await postMessageToFrame(frame1, 'touch-cached');
+  assert_equals(result.data, "Reachable 4");
+
+  result = await postMessageToFrame(frame2, 'touch-cached');
+  assert_equals(result.data, "Reachable 4");
+}, "Access is not revoked to Document object when we stop being same effective script origin due to document.domain.");
+
+promise_test(async (t) => {
+  let frame1 = await createFrame(t, "no-revoke-object-1", "{{domains[www1]}}");
+  let frame2 = await createFrame(t, "no-revoke-object-2", "{{domains[www1]}}");
+
+  let result = await postMessageToFrame(frame1, { cache: ["parent", "no-revoke-object-2", "bar"] });
+  assert_equals(result.data, "cached");
+
+  result = await postMessageToFrame(frame1, 'touch-cached');
+  assert_equals(result.data, "Reachable 2");
+
+  result = await postMessageToFrame(frame2, { cache: ["parent", "no-revoke-object-1", "bar"] });
+  assert_equals(result.data, "cached");
+
+  result = await postMessageToFrame(frame1, 'touch-cached');
+  assert_equals(result.data, "Reachable 2");
+
+  result = await postMessageToFrame(frame1, { domain: "{{domains[www1]}}" });
+  assert_equals(result.data, "Done");
+
+  result = await postMessageToFrame(frame1, 'touch-cached');
+  assert_equals(result.data, "Reachable 2");
+
+  result = await postMessageToFrame(frame2, 'touch-cached');
+  assert_equals(result.data, "Reachable 2");
+}, "Access is not revoked to random object when we stop being same effective script origin due to document.domain.");
+</script>
diff --git a/html/browsers/origin/relaxing-the-same-origin-restriction/support/document_domain_frame.html b/html/browsers/origin/relaxing-the-same-origin-restriction/support/document_domain_frame.html
new file mode 100644
index 0000000..61f54af
--- /dev/null
+++ b/html/browsers/origin/relaxing-the-same-origin-restriction/support/document_domain_frame.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<script>
+  let cache = window;
+  // "foo" needs to be a var so it's a property on the global.
+  var foo = 'Reachable 1';
+  // "bar" needs to be a var so it's a property on the global.
+  var bar = { foo: 'Reachable 2' };
+  location.foo = 'Reachable 3';
+  document.foo = 'Reachable 4';
+  window.addEventListener('message', e => {
+    if (e.data.domain !== undefined) {
+      try {
+        document.domain = e.data.domain;
+        e.ports[0].postMessage('Done');
+      } catch(error) {
+        e.ports[0].postMessage(error.name);
+      }
+    } else if (e.data['poke-at-sibling'] !== undefined) {
+      try {
+        var sekrit = parent[e.data['poke-at-sibling']].document.body.querySelector('#sekrit').value;
+        e.ports[0].postMessage(sekrit);
+      } catch(error) {
+        e.ports[0].postMessage(error.name);
+      }
+    } else if (e.data.cache != undefined) {
+      let path = e.data.cache;
+      try {
+        while (path.length != 0) {
+          cache = cache[path.shift()];
+        }
+        e.ports[0].postMessage('cached');
+      } catch (error) {
+        e.ports[0].postMessage(error.name);
+      }
+    } else if (e.data == 'touch-cached') {
+      try {
+        e.ports[0].postMessage(cache.foo);
+      } catch (error) {
+        e.ports[0].postMessage(error.name);
+      }
+    } else if (e.data == 'poke-at-parent') {
+      try {
+        var sekrit = window.parent.document.body.querySelector('#sekrit').value;
+        e.ports[0].postMessage(sekrit);
+      } catch(error) {
+        e.ports[0].postMessage(error.name);
+      }
+    }
+  });
+  window.parent.postMessage('Hi!', '*');
+</script>
+<input id="sekrit" value="omg!">
diff --git a/html/browsers/origin/relaxing-the-same-origin-restriction/support/document_domain_frame.sub.js b/html/browsers/origin/relaxing-the-same-origin-restriction/support/document_domain_frame.sub.js
new file mode 100644
index 0000000..b6631ea
--- /dev/null
+++ b/html/browsers/origin/relaxing-the-same-origin-restriction/support/document_domain_frame.sub.js
@@ -0,0 +1,65 @@
+/**
+ * Utilities to be used with document_domain_frame.html.
+ */
+
+/**
+ * Send a message to the frame and resolve a promise when a response is received.
+ *
+ * Supported messages:
+ *
+ * 1) { domain: something }.  Has the subframe try to set document.domain to the
+ * given value, and message back 'Done' if that succeeds or an error name if it
+ * fails.
+ *
+ * 2) 'poke-at-parent'.  Has the subframe try to synchronously attempt to access
+ * the parent's DOM, read out a string value, and message it back to the parent.
+ * Again, sends back the error name if that fails.
+ *
+ * 3) { 'poke-at-sibling': name }.  Has the subframe try to synchronously
+ * attempt to access the DOM of the sibling with the given name, read out a
+ * string value, and message it back to the parent.
+ */
+function postMessageToFrame(frame, message) {
+  return new Promise(resolve => {
+    var c = new MessageChannel();
+    c.port1.onmessage = e => {
+      resolve({ data: e.data, frame: frame })
+    };
+    frame.contentWindow.postMessage(message, '*', [c.port2]);
+  });
+}
+
+/**
+ * Create a frame that loads document_domain_frame.html and resolves a promise
+ * when the frame is loaded enough to be sending and receiving messages.
+ *
+ * If a "name" argument is provided, that name is used for the iframe, so
+ *
+ * If a "hostname" argument is provided, that hostname is used for the load, to
+ * allow testing details of the behavior when different sorts of hostnames are
+ * used.
+ */
+function createFrame(t, name, hostname) {
+  return new Promise(resolve => {
+    var i = document.createElement('iframe');
+    if (hostname) {
+      i.src = `//${hostname}:{{location[port]}}/html/browsers/origin/relaxing-the-same-origin-restriction/support/document_domain_frame.html`;
+    } else {
+      i.src = "support/document_domain_frame.html";
+    }
+    if (name) {
+      i.name = name;
+    }
+    var listener = m => {
+      if (m.source == i.contentWindow)
+        resolve(i);
+    }
+    window.addEventListener('message', listener);
+    t.add_cleanup(() => {
+      i.remove();
+      window.removeEventListener('message', listener);
+    });
+    document.body.appendChild(i);
+  });
+}
+