惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
CERT Recently Published Vulnerability Notes
C
Cybersecurity and Infrastructure Security Agency CISA
P
Proofpoint News Feed
Security Latest
Security Latest
P
Privacy International News Feed
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
AI
AI
Cisco Talos Blog
Cisco Talos Blog
K
Kaspersky official blog
S
Secure Thoughts
PCI Perspectives
PCI Perspectives
Simon Willison's Weblog
Simon Willison's Weblog
D
DataBreaches.Net
GbyAI
GbyAI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
大猫的无限游戏
大猫的无限游戏
T
Tailwind CSS Blog
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
罗磊的独立博客
V
Visual Studio Blog
aimingoo的专栏
aimingoo的专栏
H
Hackread – Cybersecurity News, Data Breaches, AI and More
IT之家
IT之家
V
V2EX
Last Week in AI
Last Week in AI
有赞技术团队
有赞技术团队
月光博客
月光博客
酷 壳 – CoolShell
酷 壳 – CoolShell
T
Tenable Blog
T
Threat Research - Cisco Blogs
T
Troy Hunt's Blog
V2EX - 技术
V2EX - 技术
S
Security @ Cisco Blogs
Security Archives - TechRepublic
Security Archives - TechRepublic
Project Zero
Project Zero
The GitHub Blog
The GitHub Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
L
Lohrmann on Cybersecurity
F
Full Disclosure
H
Help Net Security
博客园 - Franky
Stack Overflow Blog
Stack Overflow Blog
N
Netflix TechBlog - Medium
Engineering at Meta
Engineering at Meta
A
Arctic Wolf
O
OpenAI News
S
Securelist

Arhan's Blog

Native Instant Space Switching on MacOS Memory-corrupting Pong The Generativity Pattern in Rust Turning Image Corruption into Art An Image Compression Tip with Astro Two Years of University Wasm-pack Optimization Flags you Never Knew About! How Many Iconic Computing Numbers can you Recognize? DEF CON 32 Photo Dump True Private State in JavaScript: a Chromium Rabbit Hole My GitHub Repository has 100,000 Contributors Hello World
Implementing === in JavaScript from Scratch
Arhan Chaudhary · 2024-08-17 · via Arhan's Blog

Yes, I know exactly what you’re thinking.

Reinventing the wheel? That’s the most moronic idea I’ve ever heard since that guy who wanted to invade Russia in the winter.

And you’re not wrong. For all practical purposes, this blog is pointless. What you’re about to read is so obscenely nutty that it only works on a legacy version of Chromium as an extension running millions of times slower.

The strict equality operator in JavaScript (===) compares two values for object equality. As you probably already know, this operator tests reference equality for non-primitive JavaScript values. In effect, because { foo: 42 } === { foo: 42 } is false, reimplementing spec-compliant functionality becomes much more interesting. I’m not talking about the obvious ways. Various other built-ins make object reference comparison trivial.

let obj1 = { foo: 42 };
let obj2 = obj1;

Object.is(obj1, obj2) && console.log("same");
[obj1].includes(obj2) && console.log("same");
switch (obj1) {
  case obj2:
    console.log("same");
}
new Set().add(obj1).has(obj2) && console.log("same");

We will implement this basic operation on a far lower level. But what does that mean, and how else could JavaScript differentiate two objects that hold the same data? Surface level research yields nothing, so we’re on our own. Let’s take a deep dive.

A few weeks ago I was looking into the Chromium DevTools heap profiler for a separate blog post of mine. I keenly noticed that the heap profiler distinguishes JavaScript objects by memory location. I might be able to utilize that to my advantage.

let obj1 = { foo: 42 };
let obj2 = { foo: 42 };

The heap profiler demonstrating that two objects have different memory locations

This isn’t useful if there isn’t a way to access these memory locations programmatically. Fortunately, Chrome uses the Chrome DevTools Protocol (CDP), a standardized JavaScript API that allows for direct inspection into the JavaScript runtime on Blink-based browsers.

The original question still stands; how does JavaScript access the CDP? The documentation endorses two means of access. First, open “devtools-on-devtools” and interface with the CDP like so.

let Main = await import('./devtools-frontend/front_end/entrypoints/main/main.js');
await Main.MainImpl.sendOverProtocol('Emulation.setDeviceMetricsOverride', {
  mobile: true,
  width: 412,
  height: 732,
  deviceScaleFactor: 2.625,
});

const data = await Main.MainImpl.sendOverProtocol("Page.captureScreenshot");

This requires explicit access to DevTools and some setup, meaning it wouldn’t work plainly on a website. The alternative option is the chrome.debugger API within a Chrome extension. Although this would be a bit harder to use, it was actually programmatic and eventually the option I chose to work with.

I hacked up a Chrome extension but was surprised when my testing errored with the following message.

{"code":-32601,"message":"'HeapProfiler.enable' wasn't found"}

What’s going on here? As it turns out, internal documentation reveals that specific protocol domains including the heap profiler are restricted for security reasons.

However, since the protocol is also exposed to chrome extensions through chrome.debugger API, the backend implements additional access control in some of the methods to prevent extensio[n]s form accessing file system or otherwise escaping the sandbox. These restrictions are not extended to other types of clients.

Here is the v8 commit that adds these restrictions, dating back to May 2022 and Chromium version v104. The solution to our dilemma, you guessed it, is time travel.

Google doesn’t distribute legacy versions of Chrome. We’ll have to be a bit more hands-on. Archives of older versions of Chrome exist online. However, I’m wary of downloading unknown files from the Internet. Instead, I installed the official v103 Chromium build from source following their guide. You may have to run /usr/bin/xattr -cr /Applications/Chromium.app on an ARM-based Macs to fix broken metadata preventing Chromium from launching.

My extension didn’t initially work because global content scripts were only supported in Chromium v111 and later, so I had to resort to an older content script hack to get it to work.

That was a lot; let’s take a small step back. To reiterate, the goal is to implement a spec-compliant strict equality operation in JavaScript from scratch. We’ve found a version of Chromium that enables programmatic access to the DevTools heap profiler. Let’s build the bridge between the CDP and JavaScript object memory addresses.

A good place to start is the HeapProfiler.takeHeapSnapshot method. This will profile the entire page, and is obviously cumbersome, but whatever. Next, the Runtime.evaluate method creates a unique object identifier from the evaluation result of a stringified JavaScript expression. The caveat is that this only evaluates the global scope, coercing some really bizarre JavaScript.

strictEquality.js

async function strictEquality(obj1, obj2) {
  window.__obj1 = obj1;
  window.__obj2 = obj2;
  // Communicate with the extension content script
  // and perform the strict equality (will be explained later)
  document.dispatchEvent(new CustomEvent("strictEquality"));
  let e = await new Promise((resolve) =>
    document.addEventListener("strictEqualityResponse", resolve, { once: true })
  );
  delete window.__obj1;
  delete window.__obj2;
  if (e.detail.length) {
    throw new Error(e.detail);
  } else {
    return e.detail;
  }
}

Ugh. It’s async not because of I/O, but because of the event loop. It’s also non-reentrant, in other words it must always be await-ed. These types of unavoidably stateful functions are typically only present within C, not JavaScript!

Fine. Finally, we can pass that identifier to HeapProfiler.getHeapObjectId and earn our heap profiler memory address value. The full pipeline:

background.js

...
await chrome.debugger.attach({ tabId }, "1.3");
await chrome.debugger.sendCommand({ tabId }, "HeapProfiler.takeHeapSnapshot");
let objDetails = await Promise.all([
  chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", {
    expression: "window.__obj1",
  }),
  chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", {
    expression: "window.__obj2",
  }),
]);
let objHeapDetails = await Promise.all([
  chrome.debugger.sendCommand({ tabId }, "HeapProfiler.getHeapObjectId", {
    objectId: objDetails[0].result.objectId,
  }),
  chrome.debugger.sendCommand({ tabId }, "HeapProfiler.getHeapObjectId", {
    objectId: objDetails[1].result.objectId,
  }),
]);
await chrome.debugger.detach({ tabId: sender.tab.filePath });

sendResponse(
  !(
    objHeapDetails[0].heapSnapshotObjectId -
    objHeapDetails[1].heapSnapshotObjectId
  )
);
...

We’re almost done! We now just need to handle important edge cases. Because primitive types aren’t usually stored on the heap and instead on the stack (with the exception of Symbols), Runtime.evaluate treats them differently. The method skips to the last step and directly returns their memcpy-able value.

// `Runtime.evaluate` on `{ foo: 42 }`
{
  "className": "Object",
  "description": "Object",
  "objectId": "...",
  "type": "object"
}
// `Runtime.evaluate` on `42`
{
  "description": "42",
  "type": "number",
  "value": 42
}

While comparing two primitives would be a simple equality operation, I wanted to stay true to my word and avoid its use entirely. Rather, we need to implement the isStrictlyEqual ecma262 subroutine at the high level like so.

background.js

...
function primitiveEquality(p1, p2) {
  if (p1.subtype) {
    p1.type = p1.subtype;
  }
  if (p2.subtype) {
    p2.type = p2.subtype;
  }
  if (!stringEquality(p1.type, p2.type)) {
    return false;
  }
  if (stringEquality(p1.type, "number")) {
    if (
      stringEquality(p1.description, "NaN") ||
      stringEquality(p2.description, "NaN")
    ) {
      return false;
    }
    if (
      (stringEquality(p1.description, "0") &&
        stringEquality(p2.description, "-0")) ||
      (stringEquality(p1.description, "-0") &&
        stringEquality(p2.description, "0"))
    ) {
      return true;
    }
    if (p1.unserializableValue || p2.unserializableValue) {
      return stringEquality(p1.description, p2.description);
    }
    return !(p1.value - p2.value);
  }
  if (stringEquality(p1.type, "null") || stringEquality(p1.type, "undefined")) {
    return true;
  }
  if (stringEquality(p1.type, "bigint")) {
    return stringEquality(p1.description, p2.description);
  }
  if (stringEquality(p1.type, "string")) {
    return stringEquality(p1.value, p2.value);
  }
  if (stringEquality(p1.type, "boolean")) {
    return !(p1.value - p2.value);
  }
  throw new Error("Unsupported primitive type");
}

function stringEquality(str1, str2) {
  if (str1.length - str2.length) {
    return false;
  }
  for (let i = 0; i < str1.length; i++) {
    if (str1.charCodeAt(i) - str2.charCodeAt(i)) {
      return false;
    }
  }
  return true;
}

And… there we go. The full implementation is provided in a GitHub repository. I coughed together an ugly test module to verify its correctness.

Did I forget to mention that it doesn’t work on page load because content scripts aren’t available globally? You’ll have to have fun awaiting the strictEqualityLoaded event before usage.

test.html

(async () => {
  await new Promise((resolve) => {
    document.addEventListener("strictEqualityLoaded", resolve, { once: true });
  });
  let significantValues = [
    0,
    -0,
    1,
    0n,
    Symbol(),
    Math.PI,
    Number.MAX_SAFE_INTEGER,
    Number.MIN_SAFE_INTEGER,
    Number.MAX_SAFE_INTEGER + 1,
    Number.MIN_SAFE_INTEGER - 1,
    Infinity,
    -Infinity,
    NaN,
    null,
    undefined,
    true,
    false,
    "",
    {},
    [],
    new Date(),
  ];

  for (let obj1 of significantValues) {
    for (let obj2 of significantValues) {
      let expected = obj1 === obj2;
      let actual = await strictEquality(obj1, obj2);
      if (actual !== expected) {
        throw new Error(`Failed for ${obj1} and ${obj2}`);
      }
    }
  }
  console.log("Passed!");
})();

Until next time!

↑ Scroll to top ↑