test_sab_verify.py raw

   1  """SAB verification: confirm SharedArrayBuffer + Atomics.wait work in a
   2  COOP+COEP Worker context.
   3  
   4  This is the go/no-go gate for the SAB channel substrate used by spawn().
   5  If this fails, blocking channel_recv must be replaced with a MessagePort
   6  async fallback - the entire channel ABI changes.
   7  
   8  The signer no longer runs in a browser extension; it is an in-page WASM
   9  Worker. The Firefox MV2 extension SAB test has been removed - that question
  10  (SAB in extension origins) is no longer relevant to the architecture.
  11  
  12  Run:
  13      python3 -m pytest test/test_sab_verify.py -v
  14  """
  15  
  16  import http.server
  17  import os
  18  import socketserver
  19  import sys
  20  import tempfile
  21  import threading
  22  import time
  23  
  24  import pytest
  25  
  26  PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  27  
  28  # HTML + worker JS served with COOP/COEP for the Chrome web-page proxy test.
  29  _SAB_PAGE_HTML = b"""<!DOCTYPE html>
  30  <html><body>
  31  <div id="status">running...</div>
  32  <script>
  33  const w = new Worker('worker.js');
  34  w.onmessage = function(e) {
  35    window.__sabResult = e.data;
  36    document.getElementById('status').textContent = JSON.stringify(e.data);
  37  };
  38  w.onerror = function(e) {
  39    window.__sabResult = {error: 'worker error: ' + e.message};
  40    document.getElementById('status').textContent = window.__sabResult.error;
  41  };
  42  </script>
  43  </body></html>
  44  """
  45  
  46  _SAB_WORKER_JS = b"""
  47  const r = {
  48    crossOriginIsolated: self.crossOriginIsolated,
  49    hasSABType: typeof SharedArrayBuffer !== 'undefined',
  50    canCreate: false,
  51    atomicsWait: null,
  52    error: null
  53  };
  54  try {
  55    const sab = new SharedArrayBuffer(4);
  56    r.canCreate = true;
  57    const i32 = new Int32Array(sab);
  58    r.atomicsWait = Atomics.wait(i32, 0, 0, 0);
  59  } catch(e) {
  60    r.error = e.message;
  61  }
  62  self.postMessage(r);
  63  """
  64  
  65  
  66  class _CoopHandler(http.server.BaseHTTPRequestHandler):
  67      def log_message(self, *args): pass
  68  
  69      def do_GET(self):
  70          if self.path == "/" or self.path == "/index.html":
  71              body, ctype = _SAB_PAGE_HTML, b"text/html"
  72          elif self.path == "/worker.js":
  73              body, ctype = _SAB_WORKER_JS, b"application/javascript"
  74          else:
  75              self.send_response(404); self.end_headers(); return
  76          self.send_response(200)
  77          self.send_header("Content-Type", ctype.decode())
  78          self.send_header("Content-Length", str(len(body)))
  79          self.send_header("Cross-Origin-Opener-Policy", "same-origin")
  80          self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
  81          self.end_headers()
  82          self.wfile.write(body)
  83  
  84  
  85  def _start_coop_server():
  86      srv = socketserver.TCPServer(("127.0.0.1", 0), _CoopHandler)
  87      t = threading.Thread(target=srv.serve_forever, daemon=True)
  88      t.start()
  89      return srv, srv.server_address[1]
  90  
  91  
  92  # ── Chrome test (Playwright + COOP/COEP web page) ─────────────────────────
  93  
  94  
  95  class TestChromeSAB:
  96      """Chrome: SAB in a Worker on a COOP+COEP page.
  97  
  98      The Chrome MV3 extension manifest declares `cross_origin_opener_policy`
  99      and `cross_origin_embedder_policy` with value "same-origin" / "require-corp".
 100      These keys apply those headers to all extension pages, producing the same
 101      crossOriginIsolated=true context as a web page served with those headers.
 102      This test validates that Chrome grants SAB+Atomics.wait in Workers under
 103      that condition, which is the proxy for what the offscreen document will see.
 104      """
 105  
 106      def test_sab_in_coop_worker(self):
 107          """SharedArrayBuffer and Atomics.wait work in a Chrome Worker under COOP+COEP."""
 108          from playwright.sync_api import sync_playwright
 109  
 110          srv, port = _start_coop_server()
 111          try:
 112              with sync_playwright() as pw:
 113                  browser = pw.chromium.launch(headless=True, args=["--no-sandbox"])
 114                  page = browser.new_page()
 115                  page.goto(f"http://127.0.0.1:{port}/", wait_until="domcontentloaded")
 116                  page.wait_for_function("window.__sabResult !== undefined", timeout=8000)
 117                  result = page.evaluate("window.__sabResult")
 118                  browser.close()
 119          finally:
 120              srv.shutdown()
 121  
 122          assert result is not None, "No result from Worker"
 123          assert result.get("error") is None, f"Worker error: {result.get('error')}"
 124          assert result.get("crossOriginIsolated") is True, \
 125              f"crossOriginIsolated not true in Chrome COOP+COEP Worker: {result}"
 126          assert result.get("canCreate") is True, \
 127              f"SharedArrayBuffer creation failed in Chrome COOP+COEP Worker: {result}"
 128          assert result.get("atomicsWait") in ("timed-out", "ok"), \
 129              f"Atomics.wait did not work in Chrome COOP+COEP Worker: {result}"
 130  
 131  
 132  # The Firefox MV2 extension SAB test was removed. The signer now runs as an
 133  # in-page WASM Worker, not a browser extension. SAB availability in extension
 134  # origins is no longer a constraint for this codebase.
 135