From 81a19d221d5df78706072a7f3d37d2eb081a8f21 Mon Sep 17 00:00:00 2001
From: Ryan Hunt <rhunt@eqrion.net>
Date: Thu, 9 Sep 2021 16:20:07 +0000
Subject: [PATCH] Bug 1721686 - Add test for exposed interfaces on WebAssembly
 namespace. r=smaug

This commit modifies test_interfaces.js to also test the exposed interfaces
in the WebAssembly namespace. We have conditional features that we'd like
to have confidence that we're not accidentally exposing to web content.

Currently there are:
  * WebAssembly exceptions, enabled only in nightly with a default-off pref
  * mozIntGemm accelerator function, available only in system or addon principals

Depends on D120731

Differential Revision: https://phabricator.services.mozilla.com/D120732
---
 .../test/test_serviceworker_interfaces.js     | 107 ++++++++++------
 .../mochitest/general/test_interfaces.js      | 119 ++++++++++-------
 dom/workers/test/test_worker_interfaces.js    | 120 +++++++++++-------
 3 files changed, 217 insertions(+), 129 deletions(-)

diff --git a/dom/serviceworkers/test/test_serviceworker_interfaces.js b/dom/serviceworkers/test/test_serviceworker_interfaces.js
index 4ae800b126f8d..7f64e273b3ead 100644
--- a/dom/serviceworkers/test/test_serviceworker_interfaces.js
+++ b/dom/serviceworkers/test/test_serviceworker_interfaces.js
@@ -19,6 +19,28 @@
 //
 // See createInterfaceMap() below for a complete list of properties.
 
+// IMPORTANT: Do not change this list without review from
+//            a JavaScript Engine peer!
+var wasmGlobalEntry = {
+  name: "WebAssembly",
+  insecureContext: true,
+  disabled: !getJSTestingFunctions().wasmIsSupportedByHardware(),
+};
+var wasmGlobalInterfaces = [
+  { name: "Module", insecureContext: true },
+  { name: "Instance", insecureContext: true },
+  { name: "Memory", insecureContext: true },
+  { name: "Table", insecureContext: true },
+  { name: "Global", insecureContext: true },
+  { name: "CompileError", insecureContext: true },
+  { name: "LinkError", insecureContext: true },
+  { name: "RuntimeError", insecureContext: true },
+  {
+    name: "Function",
+    insecureContext: true,
+    nightly: true,
+  },
+];
 // IMPORTANT: Do not change this list without review from
 //            a JavaScript Engine peer!
 var ecmaGlobals = [
@@ -76,7 +98,7 @@ var ecmaGlobals = [
   "WeakMap",
   "WeakRef",
   "WeakSet",
-  { name: "WebAssembly", optional: true },
+  wasmGlobalEntry,
 ];
 // IMPORTANT: Do not change the list above without review from
 //            a JavaScript Engine peer!
@@ -265,16 +287,37 @@ var interfaceNamesInGlobalScope = [
 ];
 // IMPORTANT: Do not change the list above without review from a DOM peer!
 
-function createInterfaceMap({
-  isNightly,
-  isEarlyBetaOrEarlier,
-  isRelease,
-  isDesktop,
-  isAndroid,
-  isInsecureContext,
-  isFennec,
-  isCrossOriginIsolated,
-}) {
+function entryDisabled(
+  entry,
+  {
+    isNightly,
+    isEarlyBetaOrEarlier,
+    isRelease,
+    isDesktop,
+    isAndroid,
+    isInsecureContext,
+    isFennec,
+    isCrossOriginIsolated,
+  }
+) {
+  return (
+    entry.nightly === !isNightly ||
+    (entry.nightlyAndroid === !(isAndroid && isNightly) && isAndroid) ||
+    (entry.nonReleaseAndroid === !(isAndroid && !isRelease) && isAndroid) ||
+    entry.desktop === !isDesktop ||
+    (entry.android === !isAndroid &&
+      !entry.nonReleaseAndroid &&
+      !entry.nightlyAndroid) ||
+    entry.fennecOrDesktop === (isAndroid && !isFennec) ||
+    entry.fennec === !isFennec ||
+    entry.release === !isRelease ||
+    entry.earlyBetaOrEarlier === !isEarlyBetaOrEarlier ||
+    entry.crossOriginIsolated === !isCrossOriginIsolated ||
+    entry.disabled
+  );
+}
+
+function createInterfaceMap(data, ...interfaceGroups) {
   var interfaceMap = {};
 
   function addInterfaces(interfaces) {
@@ -283,22 +326,7 @@ function createInterfaceMap({
         interfaceMap[entry] = true;
       } else {
         ok(!("pref" in entry), "Bogus pref annotation for " + entry.name);
-        if (
-          entry.nightly === !isNightly ||
-          (entry.nightlyAndroid === !(isAndroid && isNightly) && isAndroid) ||
-          (entry.nonReleaseAndroid === !(isAndroid && !isRelease) &&
-            isAndroid) ||
-          entry.desktop === !isDesktop ||
-          (entry.android === !isAndroid &&
-            !entry.nonReleaseAndroid &&
-            !entry.nightlyAndroid) ||
-          entry.fennecOrDesktop === (isAndroid && !isFennec) ||
-          entry.fennec === !isFennec ||
-          entry.release === !isRelease ||
-          entry.earlyBetaOrEarlier === !isEarlyBetaOrEarlier ||
-          entry.crossOriginIsolated === !isCrossOriginIsolated ||
-          entry.disabled
-        ) {
+        if (entryDisabled(entry, data)) {
           interfaceMap[entry.name] = false;
         } else if (entry.optional) {
           interfaceMap[entry.name] = "optional";
@@ -309,15 +337,16 @@ function createInterfaceMap({
     }
   }
 
-  addInterfaces(ecmaGlobals);
-  addInterfaces(interfaceNamesInGlobalScope);
+  for (let interfaceGroup of interfaceGroups) {
+    addInterfaces(interfaceGroup);
+  }
 
   return interfaceMap;
 }
 
-function runTest(data) {
-  var interfaceMap = createInterfaceMap(data);
-  for (var name of Object.getOwnPropertyNames(self)) {
+function runTest(parentName, parent, data, ...interfaceGroups) {
+  var interfaceMap = createInterfaceMap(data, ...interfaceGroups);
+  for (var name of Object.getOwnPropertyNames(parent)) {
     // An interface name should start with an upper case character.
     if (!/^[A-Z]/.test(name)) {
       continue;
@@ -326,7 +355,9 @@ function runTest(data) {
       interfaceMap[name] === "optional" || interfaceMap[name],
       "If this is failing: DANGER, are you sure you want to expose the new interface " +
         name +
-        " to all webpages as a property on the service worker? Do not make a change to this file without a " +
+        " to all webpages as a property on " +
+        parentName +
+        "? Do not make a change to this file without a " +
         " review from a DOM peer for that specific change!!! (or a JS peer for changes to ecmaGlobals)"
     );
     delete interfaceMap[name];
@@ -336,11 +367,12 @@ function runTest(data) {
       delete interfaceMap[name];
     } else {
       ok(
-        name in self === interfaceMap[name],
+        name in parent === interfaceMap[name],
         name +
           " should " +
           (interfaceMap[name] ? "" : " NOT") +
-          " be defined on the global scope"
+          " be defined on " +
+          parentName
       );
       if (!interfaceMap[name]) {
         delete interfaceMap[name];
@@ -356,6 +388,9 @@ function runTest(data) {
 }
 
 workerTestGetHelperData(function(data) {
-  runTest(data);
+  runTest("self", self, data, ecmaGlobals, interfaceNamesInGlobalScope);
+  if (WebAssembly && !entryDisabled(wasmGlobalEntry, data)) {
+    runTest("WebAssembly", WebAssembly, data, wasmGlobalInterfaces);
+  }
   workerTestDone();
 });
diff --git a/dom/tests/mochitest/general/test_interfaces.js b/dom/tests/mochitest/general/test_interfaces.js
index 092b3a839efdd..631240cea2e9a 100644
--- a/dom/tests/mochitest/general/test_interfaces.js
+++ b/dom/tests/mochitest/general/test_interfaces.js
@@ -47,6 +47,28 @@ const isFennec =
   ).isFennec;
 const isCrossOriginIsolated = window.crossOriginIsolated;
 
+// IMPORTANT: Do not change this list without review from
+//            a JavaScript Engine peer!
+var wasmGlobalEntry = {
+  name: "WebAssembly",
+  insecureContext: true,
+  disabled: !SpecialPowers.Cu.getJSTestingFunctions().wasmIsSupportedByHardware(),
+};
+var wasmGlobalInterfaces = [
+  { name: "Module", insecureContext: true },
+  { name: "Instance", insecureContext: true },
+  { name: "Memory", insecureContext: true },
+  { name: "Table", insecureContext: true },
+  { name: "Global", insecureContext: true },
+  { name: "CompileError", insecureContext: true },
+  { name: "LinkError", insecureContext: true },
+  { name: "RuntimeError", insecureContext: true },
+  {
+    name: "Function",
+    insecureContext: true,
+    nightly: true,
+  },
+];
 // IMPORTANT: Do not change this list without review from
 //            a JavaScript Engine peer!
 var ecmaGlobals = [
@@ -105,11 +127,7 @@ var ecmaGlobals = [
   { name: "WeakMap", insecureContext: true },
   { name: "WeakRef", insecureContext: true },
   { name: "WeakSet", insecureContext: true },
-  {
-    name: "WebAssembly",
-    insecureContext: true,
-    disabled: !SpecialPowers.Cu.getJSTestingFunctions().wasmIsSupportedByHardware(),
-  },
+  wasmGlobalEntry,
 ];
 // IMPORTANT: Do not change the list above without review from
 //            a JavaScript Engine peer!
@@ -1381,7 +1399,33 @@ var interfaceNamesInGlobalScope = [
 ];
 // IMPORTANT: Do not change the list above without review from a DOM peer!
 
-function createInterfaceMap() {
+function entryDisabled(entry) {
+  return (
+    entry.nightly === !isNightly ||
+    (entry.nightlyAndroid === !(isAndroid && isNightly) && isAndroid) ||
+    entry.desktop === !isDesktop ||
+    entry.windows === !isWindows ||
+    entry.mac === !isMac ||
+    entry.linux === !isLinux ||
+    (entry.android === !isAndroid && !entry.nightlyAndroid) ||
+    entry.fennecOrDesktop === (isAndroid && !isFennec) ||
+    entry.fennec === !isFennec ||
+    entry.release === !isRelease ||
+    entry.releaseNonWindowsAndMac === !(isRelease && !isWindows && !isMac) ||
+    entry.releaseNonWindows === !(isRelease && !isWindows) ||
+    // The insecureContext test is very purposefully converting
+    // entry.insecureContext to boolean, so undefined will convert to
+    // false.  That way entries without an insecureContext annotation
+    // will get treated as "insecureContext: false", which means exposed
+    // only in secure contexts.
+    (isInsecureContext && !entry.insecureContext) ||
+    entry.earlyBetaOrEarlier === !isEarlyBetaOrEarlier ||
+    entry.crossOriginIsolated === !isCrossOriginIsolated ||
+    entry.disabled
+  );
+}
+
+function createInterfaceMap(...interfaceGroups) {
   var interfaceMap = {};
 
   function addInterfaces(interfaces) {
@@ -1390,47 +1434,21 @@ function createInterfaceMap() {
         interfaceMap[entry] = !isInsecureContext;
       } else {
         ok(!("pref" in entry), "Bogus pref annotation for " + entry.name);
-        if (
-          entry.nightly === !isNightly ||
-          (entry.nightlyAndroid === !(isAndroid && isNightly) && isAndroid) ||
-          entry.desktop === !isDesktop ||
-          entry.windows === !isWindows ||
-          entry.mac === !isMac ||
-          entry.linux === !isLinux ||
-          (entry.android === !isAndroid && !entry.nightlyAndroid) ||
-          entry.fennecOrDesktop === (isAndroid && !isFennec) ||
-          entry.fennec === !isFennec ||
-          entry.release === !isRelease ||
-          entry.releaseNonWindowsAndMac ===
-            !(isRelease && !isWindows && !isMac) ||
-          entry.releaseNonWindows === !(isRelease && !isWindows) ||
-          // The insecureContext test is very purposefully converting
-          // entry.insecureContext to boolean, so undefined will convert to
-          // false.  That way entries without an insecureContext annotation
-          // will get treated as "insecureContext: false", which means exposed
-          // only in secure contexts.
-          (isInsecureContext && !entry.insecureContext) ||
-          entry.earlyBetaOrEarlier === !isEarlyBetaOrEarlier ||
-          entry.crossOriginIsolated === !isCrossOriginIsolated ||
-          entry.disabled
-        ) {
-          interfaceMap[entry.name] = false;
-        } else {
-          interfaceMap[entry.name] = true;
-        }
+        interfaceMap[entry.name] = !entryDisabled(entry);
       }
     }
   }
 
-  addInterfaces(ecmaGlobals);
-  addInterfaces(interfaceNamesInGlobalScope);
+  for (let interfaceGroup of interfaceGroups) {
+    addInterfaces(interfaceGroup);
+  }
 
   return interfaceMap;
 }
 
-function runTest() {
-  var interfaceMap = createInterfaceMap();
-  for (var name of Object.getOwnPropertyNames(window)) {
+function runTest(parentName, parent, ...interfaceGroups) {
+  var interfaceMap = createInterfaceMap(...interfaceGroups);
+  for (var name of Object.getOwnPropertyNames(parent)) {
     // An interface name should start with an upper case character.
     // However, we have a couple of legacy interfaces that start with 'moz', so
     // we want to allow those until we can remove them.
@@ -1441,28 +1459,32 @@ function runTest() {
       interfaceMap[name],
       "If this is failing: DANGER, are you sure you want to expose the new interface " +
         name +
-        " to all webpages as a property on the window? Do not make a change to this file without a " +
+        " to all webpages as a property on '" +
+        parentName +
+        "'? Do not make a change to this file without a " +
         " review from a DOM peer for that specific change!!! (or a JS peer for changes to ecmaGlobals)"
     );
 
     ok(
-      name in window,
-      `${name} is exposed as an own property on the window but tests false for "in" in the global scope`
+      name in parent,
+      `${name} is exposed as an own property on '" + parentName + "' but tests false for "in" in the global scope`
     );
     ok(
-      Object.getOwnPropertyDescriptor(window, name),
-      `${name} is exposed as an own property on the window but has no property descriptor in the global scope`
+      Object.getOwnPropertyDescriptor(parent, name),
+      `${name} is exposed as an own property on '" + parentName + "' but has no property descriptor in the global scope`
     );
 
     delete interfaceMap[name];
   }
   for (var name of Object.keys(interfaceMap)) {
     ok(
-      name in window === interfaceMap[name],
+      name in parent === interfaceMap[name],
       name +
         " should " +
         (interfaceMap[name] ? "" : " NOT") +
-        " be defined on the global scope"
+        " be defined on '" +
+        parentName +
+        "' scope"
     );
     if (!interfaceMap[name]) {
       delete interfaceMap[name];
@@ -1476,4 +1498,7 @@ function runTest() {
   );
 }
 
-runTest();
+runTest("window", window, ecmaGlobals, interfaceNamesInGlobalScope);
+if (window.WebAssembly && !entryDisabled(wasmGlobalEntry)) {
+  runTest("WebAssembly", window.WebAssembly, wasmGlobalInterfaces);
+}
diff --git a/dom/workers/test/test_worker_interfaces.js b/dom/workers/test/test_worker_interfaces.js
index 164315d1764bb..9cba351929844 100644
--- a/dom/workers/test/test_worker_interfaces.js
+++ b/dom/workers/test/test_worker_interfaces.js
@@ -25,6 +25,28 @@
 // value needs to depend on channel or OS, we will need to make sure
 // we have that information before setting up the property lists.
 
+// IMPORTANT: Do not change this list without review from
+//            a JavaScript Engine peer!
+var wasmGlobalEntry = {
+  name: "WebAssembly",
+  insecureContext: true,
+  disabled: !getJSTestingFunctions().wasmIsSupportedByHardware(),
+};
+var wasmGlobalInterfaces = [
+  { name: "Module", insecureContext: true },
+  { name: "Instance", insecureContext: true },
+  { name: "Memory", insecureContext: true },
+  { name: "Table", insecureContext: true },
+  { name: "Global", insecureContext: true },
+  { name: "CompileError", insecureContext: true },
+  { name: "LinkError", insecureContext: true },
+  { name: "RuntimeError", insecureContext: true },
+  {
+    name: "Function",
+    insecureContext: true,
+    nightly: true,
+  },
+];
 // IMPORTANT: Do not change this list without review from
 //            a JavaScript Engine peer!
 var ecmaGlobals = [
@@ -85,11 +107,7 @@ var ecmaGlobals = [
   { name: "WeakMap", insecureContext: true },
   { name: "WeakRef", insecureContext: true },
   { name: "WeakSet", insecureContext: true },
-  {
-    name: "WebAssembly",
-    insecureContext: true,
-    disabled: !getJSTestingFunctions().wasmIsSupportedByHardware(),
-  },
+  wasmGlobalEntry,
 ];
 // IMPORTANT: Do not change the list above without review from
 //            a JavaScript Engine peer!
@@ -293,16 +311,40 @@ var interfaceNamesInGlobalScope = [
 ];
 // IMPORTANT: Do not change the list above without review from a DOM peer!
 
-function createInterfaceMap({
-  isNightly,
-  isEarlyBetaOrEarlier,
-  isRelease,
-  isDesktop,
-  isAndroid,
-  isInsecureContext,
-  isFennec,
-  isCrossOringinIsolated,
-}) {
+function entryDisabled(
+  entry,
+  {
+    isNightly,
+    isEarlyBetaOrEarlier,
+    isRelease,
+    isDesktop,
+    isAndroid,
+    isInsecureContext,
+    isFennec,
+    isCrossOringinIsolated,
+  }
+) {
+  return (
+    entry.nightly === !isNightly ||
+    (entry.nightlyAndroid === !(isAndroid && isNightly) && isAndroid) ||
+    entry.desktop === !isDesktop ||
+    (entry.android === !isAndroid && !entry.nightlyAndroid) ||
+    entry.fennecOrDesktop === (isAndroid && !isFennec) ||
+    entry.fennec === !isFennec ||
+    entry.release === !isRelease ||
+    // The insecureContext test is very purposefully converting
+    // entry.insecureContext to boolean, so undefined will convert to
+    // false.  That way entries without an insecureContext annotation
+    // will get treated as "insecureContext: false", which means exposed
+    // only in secure contexts.
+    (isInsecureContext && !entry.insecureContext) ||
+    entry.earlyBetaOrEarlier === !isEarlyBetaOrEarlier ||
+    entry.crossOringinIsolated === !isCrossOringinIsolated ||
+    entry.disabled
+  );
+}
+
+function createInterfaceMap(data, ...interfaceGroups) {
   var interfaceMap = {};
 
   function addInterfaces(interfaces) {
@@ -311,41 +353,21 @@ function createInterfaceMap({
         interfaceMap[entry] = !isInsecureContext;
       } else {
         ok(!("pref" in entry), "Bogus pref annotation for " + entry.name);
-        if (
-          entry.nightly === !isNightly ||
-          (entry.nightlyAndroid === !(isAndroid && isNightly) && isAndroid) ||
-          entry.desktop === !isDesktop ||
-          (entry.android === !isAndroid && !entry.nightlyAndroid) ||
-          entry.fennecOrDesktop === (isAndroid && !isFennec) ||
-          entry.fennec === !isFennec ||
-          entry.release === !isRelease ||
-          // The insecureContext test is very purposefully converting
-          // entry.insecureContext to boolean, so undefined will convert to
-          // false.  That way entries without an insecureContext annotation
-          // will get treated as "insecureContext: false", which means exposed
-          // only in secure contexts.
-          (isInsecureContext && !entry.insecureContext) ||
-          entry.earlyBetaOrEarlier === !isEarlyBetaOrEarlier ||
-          entry.crossOringinIsolated === !isCrossOringinIsolated ||
-          entry.disabled
-        ) {
-          interfaceMap[entry.name] = false;
-        } else {
-          interfaceMap[entry.name] = true;
-        }
+        interfaceMap[entry.name] = !entryDisabled(entry, data);
       }
     }
   }
 
-  addInterfaces(ecmaGlobals);
-  addInterfaces(interfaceNamesInGlobalScope);
+  for (let interfaceGroup of interfaceGroups) {
+    addInterfaces(interfaceGroup);
+  }
 
   return interfaceMap;
 }
 
-function runTest(data) {
-  var interfaceMap = createInterfaceMap(data);
-  for (var name of Object.getOwnPropertyNames(self)) {
+function runTest(parentName, parent, data, ...interfaceGroups) {
+  var interfaceMap = createInterfaceMap(data, ...interfaceGroups);
+  for (var name of Object.getOwnPropertyNames(parent)) {
     // An interface name should start with an upper case character.
     if (!/^[A-Z]/.test(name)) {
       continue;
@@ -354,18 +376,21 @@ function runTest(data) {
       interfaceMap[name],
       "If this is failing: DANGER, are you sure you want to expose the new interface " +
         name +
-        " to all webpages as a property on the worker? Do not make a change to this file without a " +
+        " to all webpages as a property of " +
+        parentName +
+        "? Do not make a change to this file without a " +
         " review from a DOM peer for that specific change!!! (or a JS peer for changes to ecmaGlobals)"
     );
     delete interfaceMap[name];
   }
   for (var name of Object.keys(interfaceMap)) {
     ok(
-      name in self === interfaceMap[name],
+      name in parent === interfaceMap[name],
       name +
         " should " +
         (interfaceMap[name] ? "" : " NOT") +
-        " be defined on the global scope"
+        " be defined on " +
+        parentName
     );
     if (!interfaceMap[name]) {
       delete interfaceMap[name];
@@ -380,6 +405,9 @@ function runTest(data) {
 }
 
 workerTestGetHelperData(function(data) {
-  runTest(data);
+  runTest("self", self, data, ecmaGlobals, interfaceNamesInGlobalScope);
+  if (WebAssembly && !entryDisabled(wasmGlobalEntry, data)) {
+    runTest("WebAssembly", WebAssembly, data, wasmGlobalInterfaces);
+  }
   workerTestDone();
 });
-- 
GitLab