From e8b7249facaa4b64f35027f2ebe5c6c6f0e46c3f Mon Sep 17 00:00:00 2001
From: legendecas <legendecas@gmail.com>
Date: Thu, 18 Aug 2022 00:46:14 +0800
Subject: [PATCH] perf_hooks: hrtime idlharness

1. Enforce receiver checks on IDL interface `Performance.now`.
2. Avoid prototype manipulation on constructing `Performance`.
3. `defineReplaceableAttribute` should create IDL getter/setter.
---
 lib/internal/bootstrap/browser.js            |  31 ++-
 lib/internal/perf/performance.js             | 228 ++++++++-----------
 lib/internal/perf/usertiming.js              |   2 +-
 lib/internal/perf/utils.js                   |   8 +-
 lib/internal/process/pre_execution.js        |   1 -
 lib/internal/validators.js                   |   9 +-
 test/common/wpt.js                           |  49 +++-
 test/sequential/test-worker-eventlooputil.js |   5 +-
 test/wpt/status/hr-time.json                 |   3 -
 test/wpt/test-hr-time.js                     |   2 +
 10 files changed, 187 insertions(+), 151 deletions(-)

diff --git a/lib/internal/bootstrap/browser.js b/lib/internal/bootstrap/browser.js
index d0c01ca2a512be5..92d57688e21115c 100644
--- a/lib/internal/bootstrap/browser.js
+++ b/lib/internal/bootstrap/browser.js
@@ -75,8 +75,8 @@ exposeInterface(globalThis, 'Blob', buffer.Blob);
 // https://www.w3.org/TR/hr-time-2/#the-performance-attribute
 const perf_hooks = require('perf_hooks');
 exposeInterface(globalThis, 'Performance', perf_hooks.Performance);
-defineReplacableAttribute(globalThis, 'performance',
-                          perf_hooks.performance);
+defineReplaceableAttribute(globalThis, 'performance',
+                           perf_hooks.performance);
 
 function createGlobalConsole() {
   const consoleFromNode =
@@ -114,14 +114,33 @@ function exposeGetterAndSetter(target, name, getter, setter = undefined) {
   });
 }
 
-// https://heycam.github.io/webidl/#Replaceable
-function defineReplacableAttribute(target, name, value) {
+// https://webidl.spec.whatwg.org/#Replaceable
+function defineReplaceableAttribute(target, name, value) {
+  let slot = value;
+
+  // https://webidl.spec.whatwg.org/#dfn-attribute-getter
+  function get() {
+    return slot;
+  }
+  ObjectDefineProperty(get, 'name', {
+    __proto__: null,
+    value: `get ${name}`,
+  });
+
+  function set(value) {
+    slot = value;
+  }
+  ObjectDefineProperty(set, 'name', {
+    __proto__: null,
+    value: `set ${name}`,
+  });
+
   ObjectDefineProperty(target, name, {
     __proto__: null,
-    writable: true,
     enumerable: true,
     configurable: true,
-    value,
+    get,
+    set,
   });
 }
 
diff --git a/lib/internal/perf/performance.js b/lib/internal/perf/performance.js
index 610523853f86f5d..44e5d6f868c42c9 100644
--- a/lib/internal/perf/performance.js
+++ b/lib/internal/perf/performance.js
@@ -1,9 +1,9 @@
 'use strict';
 
 const {
-  ObjectDefineProperty,
   ObjectDefineProperties,
-  ObjectSetPrototypeOf,
+  ReflectConstruct,
+  SymbolToStringTag,
 } = primordials;
 
 const {
@@ -17,9 +17,13 @@ const {
   EventTarget,
   Event,
   kTrustEvent,
+  initEventTarget,
 } = require('internal/event_target');
 
-const { now } = require('internal/perf/utils');
+const {
+  now,
+  kPerformanceBrand,
+} = require('internal/perf/utils');
 
 const { markResourceTiming } = require('internal/perf/resource_timing');
 
@@ -38,8 +42,9 @@ const {
 const { eventLoopUtilization } = require('internal/perf/event_loop_utilization');
 const nodeTiming = require('internal/perf/nodetiming');
 const timerify = require('internal/perf/timerify');
-const { customInspectSymbol: kInspect } = require('internal/util');
+const { customInspectSymbol: kInspect, kEnumerableProperty, kEmptyObject } = require('internal/util');
 const { inspect } = require('util');
+const { validateInternalField } = require('internal/validators');
 
 const {
   getTimeOriginTimestamp
@@ -63,121 +68,117 @@ class Performance extends EventTarget {
       timeOrigin: this.timeOrigin,
     }, opts)}`;
   }
-}
 
-function toJSON() {
-  return {
-    nodeTiming: this.nodeTiming,
-    timeOrigin: this.timeOrigin,
-    eventLoopUtilization: this.eventLoopUtilization()
-  };
-}
+  clearMarks(name) {
+    if (name !== undefined) {
+      name = `${name}`;
+    }
+    clearMarkTimings(name);
+    clearEntriesFromBuffer('mark', name);
+  }
 
-function clearMarks(name) {
-  if (name !== undefined) {
-    name = `${name}`;
+  clearMeasures(name) {
+    if (name !== undefined) {
+      name = `${name}`;
+    }
+    clearEntriesFromBuffer('measure', name);
   }
-  clearMarkTimings(name);
-  clearEntriesFromBuffer('mark', name);
-}
 
-function clearMeasures(name) {
-  if (name !== undefined) {
-    name = `${name}`;
+  clearResourceTimings(name = undefined) {
+    if (name !== undefined) {
+      name = `${name}`;
+    }
+    clearEntriesFromBuffer('resource', name);
+  }
+
+  getEntries() {
+    return filterBufferMapByNameAndType();
   }
-  clearEntriesFromBuffer('measure', name);
-}
 
-function clearResourceTimings(name) {
-  if (name !== undefined) {
+  getEntriesByName(name) {
+    if (arguments.length === 0) {
+      throw new ERR_MISSING_ARGS('name');
+    }
     name = `${name}`;
+    return filterBufferMapByNameAndType(name, undefined);
   }
-  clearEntriesFromBuffer('resource', name);
-}
 
-function getEntries() {
-  return filterBufferMapByNameAndType();
-}
+  getEntriesByType(type) {
+    if (arguments.length === 0) {
+      throw new ERR_MISSING_ARGS('type');
+    }
+    type = `${type}`;
+    return filterBufferMapByNameAndType(undefined, type);
+  }
 
-function getEntriesByName(name) {
-  if (arguments.length === 0) {
-    throw new ERR_MISSING_ARGS('name');
+  mark(name, options = kEmptyObject) {
+    return mark(name, options);
   }
-  name = `${name}`;
-  return filterBufferMapByNameAndType(name, undefined);
-}
 
-function getEntriesByType(type) {
-  if (arguments.length === 0) {
-    throw new ERR_MISSING_ARGS('type');
+  measure(name, startOrMeasureOptions, endMark) {
+    return measure(name, startOrMeasureOptions, endMark);
   }
-  type = `${type}`;
-  return filterBufferMapByNameAndType(undefined, type);
-}
 
-class InternalPerformance extends EventTarget {}
-InternalPerformance.prototype.constructor = Performance.prototype.constructor;
-ObjectSetPrototypeOf(InternalPerformance.prototype, Performance.prototype);
+  now() {
+    validateInternalField(this, kPerformanceBrand, 'Performance');
+    return now();
+  }
+
+  setResourceTimingBufferSize(maxSize) {
+    return setResourceTimingBufferSize(maxSize);
+  }
+
+  get timeOrigin() {
+    validateInternalField(this, kPerformanceBrand, 'Performance');
+    return getTimeOriginTimestamp();
+  }
+
+  toJSON() {
+    validateInternalField(this, kPerformanceBrand, 'Performance');
+    return {
+      nodeTiming: this.nodeTiming,
+      timeOrigin: this.timeOrigin,
+      eventLoopUtilization: this.eventLoopUtilization()
+    };
+  }
+}
 
 ObjectDefineProperties(Performance.prototype, {
-  clearMarks: {
-    __proto__: null,
-    configurable: true,
+  clearMarks: kEnumerableProperty,
+  clearMeasures: kEnumerableProperty,
+  clearResourceTimings: kEnumerableProperty,
+  getEntries: kEnumerableProperty,
+  getEntriesByName: kEnumerableProperty,
+  getEntriesByType: kEnumerableProperty,
+  mark: kEnumerableProperty,
+  measure: kEnumerableProperty,
+  now: kEnumerableProperty,
+  timeOrigin: kEnumerableProperty,
+  toJSON: kEnumerableProperty,
+  setResourceTimingBufferSize: kEnumerableProperty,
+  [SymbolToStringTag]: {
+    __proto__: null,
+    writable: false,
     enumerable: false,
-    value: clearMarks,
-  },
-  clearMeasures: {
-    __proto__: null,
     configurable: true,
-    enumerable: false,
-    value: clearMeasures,
-  },
-  clearResourceTimings: {
-    __proto__: null,
-    configurable: true,
-    enumerable: false,
-    value: clearResourceTimings,
+    value: 'Performance',
   },
+
+  // Node.js specific extensions.
   eventLoopUtilization: {
     __proto__: null,
     configurable: true,
+    // Node.js specific extensions.
     enumerable: false,
+    writable: true,
     value: eventLoopUtilization,
   },
-  getEntries: {
-    __proto__: null,
-    configurable: true,
-    enumerable: false,
-    value: getEntries,
-  },
-  getEntriesByName: {
-    __proto__: null,
-    configurable: true,
-    enumerable: false,
-    value: getEntriesByName,
-  },
-  getEntriesByType: {
-    __proto__: null,
-    configurable: true,
-    enumerable: false,
-    value: getEntriesByType,
-  },
-  mark: {
-    __proto__: null,
-    configurable: true,
-    enumerable: false,
-    value: mark,
-  },
-  measure: {
-    __proto__: null,
-    configurable: true,
-    enumerable: false,
-    value: measure,
-  },
   nodeTiming: {
     __proto__: null,
     configurable: true,
+    // Node.js specific extensions.
     enumerable: false,
+    writable: true,
     value: nodeTiming,
   },
   // In the browser, this function is not public.  However, it must be used inside fetch
@@ -185,55 +186,29 @@ ObjectDefineProperties(Performance.prototype, {
   markResourceTiming: {
     __proto__: null,
     configurable: true,
+    // Node.js specific extensions.
     enumerable: false,
+    writable: true,
     value: markResourceTiming,
   },
-  now: {
-    __proto__: null,
-    configurable: true,
-    enumerable: false,
-    value: now,
-  },
-  setResourceTimingBufferSize: {
-    __proto__: null,
-    configurable: true,
-    enumerable: false,
-    value: setResourceTimingBufferSize
-  },
   timerify: {
     __proto__: null,
     configurable: true,
+    // Node.js specific extensions.
     enumerable: false,
+    writable: true,
     value: timerify,
   },
-  // This would be updated during pre-execution in case
-  // the process is launched from a snapshot.
-  // TODO(joyeecheung): we may want to warn about access to
-  // this during snapshot building.
-  timeOrigin: {
-    __proto__: null,
-    configurable: true,
-    enumerable: true,
-    value: getTimeOriginTimestamp(),
-  },
-  toJSON: {
-    __proto__: null,
-    configurable: true,
-    enumerable: true,
-    value: toJSON,
-  }
 });
 
-function refreshTimeOrigin() {
-  ObjectDefineProperty(Performance.prototype, 'timeOrigin', {
-    __proto__: null,
-    configurable: true,
-    enumerable: true,
-    value: getTimeOriginTimestamp(),
-  });
+function createPerformance() {
+  return ReflectConstruct(function Performance() {
+    initEventTarget(this);
+    this[kPerformanceBrand] = true;
+  }, [], Performance);
 }
 
-const performance = new InternalPerformance();
+const performance = createPerformance();
 
 function dispatchBufferFull(type) {
   const event = new Event(type, {
@@ -246,5 +221,4 @@ setDispatchBufferFull(dispatchBufferFull);
 module.exports = {
   Performance,
   performance,
-  refreshTimeOrigin
 };
diff --git a/lib/internal/perf/usertiming.js b/lib/internal/perf/usertiming.js
index c61be708829db1a..298acc3738e1013 100644
--- a/lib/internal/perf/usertiming.js
+++ b/lib/internal/perf/usertiming.js
@@ -94,7 +94,7 @@ class PerformanceMeasure extends InternalPerformanceEntry {
   }
 }
 
-function mark(name, options = kEmptyObject) {
+function mark(name, options) {
   const mark = new PerformanceMark(name, options);
   enqueue(mark);
   bufferUserTiming(mark);
diff --git a/lib/internal/perf/utils.js b/lib/internal/perf/utils.js
index bcc7e223b8c882a..539ee0c62847e30 100644
--- a/lib/internal/perf/utils.js
+++ b/lib/internal/perf/utils.js
@@ -1,5 +1,9 @@
 'use strict';
 
+const {
+  Symbol,
+} = primordials;
+
 const binding = internalBinding('performance');
 const {
   milestones,
@@ -9,6 +13,7 @@ const {
 // TODO(joyeecheung): we may want to warn about access to
 // this during snapshot building.
 let timeOrigin = getTimeOrigin();
+const kPerformanceBrand = Symbol('performance');
 
 function now() {
   const hr = process.hrtime();
@@ -29,5 +34,6 @@ function refreshTimeOrigin() {
 module.exports = {
   now,
   getMilestoneTimestamp,
-  refreshTimeOrigin
+  refreshTimeOrigin,
+  kPerformanceBrand,
 };
diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js
index d2f2dad8dd445ad..aa1502607e838d1 100644
--- a/lib/internal/process/pre_execution.js
+++ b/lib/internal/process/pre_execution.js
@@ -363,7 +363,6 @@ function setupTraceCategoryState() {
 }
 
 function setupPerfHooks() {
-  require('internal/perf/performance').refreshTimeOrigin();
   require('internal/perf/utils').refreshTimeOrigin();
 }
 
diff --git a/lib/internal/validators.js b/lib/internal/validators.js
index de8a8bb9b83b343..a192f9e6302ddb6 100644
--- a/lib/internal/validators.js
+++ b/lib/internal/validators.js
@@ -418,6 +418,12 @@ function validateLinkHeaderValue(value, name) {
   }
 }
 
+const validateInternalField = hideStackFrames((object, fieldKey, className) => {
+  if (typeof object !== 'object' || object === null || !ObjectPrototypeHasOwnProperty(object, fieldKey)) {
+    throw new ERR_INVALID_ARG_TYPE('this', className, object);
+  }
+});
+
 module.exports = {
   isInt32,
   isUint32,
@@ -440,5 +446,6 @@ module.exports = {
   validateUndefined,
   validateUnion,
   validateAbortSignal,
-  validateLinkHeaderValue
+  validateLinkHeaderValue,
+  validateInternalField,
 };
diff --git a/test/common/wpt.js b/test/common/wpt.js
index 01f937fafce8410..abf862b374cfca3 100644
--- a/test/common/wpt.js
+++ b/test/common/wpt.js
@@ -298,7 +298,7 @@ class WPTRunner {
     this.resource = new ResourceLoader(path);
 
     this.flags = [];
-    this.dummyGlobalThisScript = null;
+    this.globalThisInitScripts = [];
     this.initScript = null;
 
     this.status = new StatusLoader(path);
@@ -340,17 +340,17 @@ class WPTRunner {
   }
 
   get fullInitScript() {
-    if (this.initScript === null && this.dummyGlobalThisScript === null) {
-      return null;
+    if (this.globalThisInitScripts.length === null) {
+      return this.initScript;
     }
 
+    const globalThisInitScript = this.globalThisInitScripts.join('\n\n//===\n');
+
     if (this.initScript === null) {
-      return this.dummyGlobalThisScript;
-    } else if (this.dummyGlobalThisScript === null) {
-      return this.initScript;
+      return globalThisInitScript;
     }
 
-    return `${this.dummyGlobalThisScript}\n\n//===\n${this.initScript}`;
+    return `${globalThisInitScript}\n\n//===\n${this.initScript}`;
   }
 
   /**
@@ -361,8 +361,9 @@ class WPTRunner {
   pretendGlobalThisAs(name) {
     switch (name) {
       case 'Window': {
-        this.dummyGlobalThisScript =
-          'global.Window = Object.getPrototypeOf(globalThis).constructor;';
+        this.globalThisInitScripts.push(
+          `global.Window = Object.getPrototypeOf(globalThis).constructor;
+          self.GLOBAL.isWorker = () => false;`);
         break;
       }
 
@@ -376,6 +377,36 @@ class WPTRunner {
     }
   }
 
+  brandCheckGlobalScopeAttribute(name) {
+    // TODO(legendecas): idlharness GlobalScope attribute receiver validation.
+    const script = `
+      const desc = Object.getOwnPropertyDescriptor(globalThis, '${name}');
+      function getter() {
+        // Mimic GlobalScope instance brand check.
+        if (this !== globalThis) {
+          throw new TypeError('Illegal invocation');
+        }
+        return desc.get();
+      }
+      Object.defineProperty(getter, 'name', { value: 'get ${name}' });
+
+      function setter(value) {
+        // Mimic GlobalScope instance brand check.
+        if (this !== globalThis) {
+          throw new TypeError('Illegal invocation');
+        }
+        desc.set(value);
+      }
+      Object.defineProperty(setter, 'name', { value: 'set ${name}' });
+
+      Object.defineProperty(globalThis, 'performance', {
+        get: getter,
+        set: setter,
+      });
+    `;
+    this.globalThisInitScripts.push(script);
+  }
+
   // TODO(joyeecheung): work with the upstream to port more tests in .html
   // to .js.
   async runJsTests() {
diff --git a/test/sequential/test-worker-eventlooputil.js b/test/sequential/test-worker-eventlooputil.js
index 7e012cb2b02e7af..5255051ab82dea8 100644
--- a/test/sequential/test-worker-eventlooputil.js
+++ b/test/sequential/test-worker-eventlooputil.js
@@ -9,6 +9,7 @@ const {
   MessagePort,
   parentPort,
 } = require('worker_threads');
+const { performance } = require('perf_hooks');
 const { eventLoopUtilization, now } = require('perf_hooks').performance;
 
 // Use argv to detect whether we're running as a Worker called by this test vs.
@@ -35,8 +36,8 @@ function workerOnMetricsMsg(msg) {
 
   if (msg.cmd === 'spin') {
     const elu = eventLoopUtilization();
-    const t = now();
-    while (now() - t < msg.dur);
+    const t = performance.now();
+    while (performance.now() - t < msg.dur);
     return this.postMessage(eventLoopUtilization(elu));
   }
 }
diff --git a/test/wpt/status/hr-time.json b/test/wpt/status/hr-time.json
index b23a5a4e96a6a4f..973e32b298a557b 100644
--- a/test/wpt/status/hr-time.json
+++ b/test/wpt/status/hr-time.json
@@ -1,7 +1,4 @@
 {
-  "idlharness.any.js": {
-    "skip": "TODO: update IDL parser"
-  },
   "window-worker-timeOrigin.window.js": {
     "skip": "depends on URL.createObjectURL(blob)"
   }
diff --git a/test/wpt/test-hr-time.js b/test/wpt/test-hr-time.js
index 36fdde8036c0cc2..7fb55de5cf835af 100644
--- a/test/wpt/test-hr-time.js
+++ b/test/wpt/test-hr-time.js
@@ -5,6 +5,8 @@ const { WPTRunner } = require('../common/wpt');
 
 const runner = new WPTRunner('hr-time');
 
+runner.pretendGlobalThisAs('Window');
+runner.brandCheckGlobalScopeAttribute('performance');
 runner.setInitScript(`
   const { Blob } = require('buffer');
   global.Blob = Blob;