在這 Web worker
系列文的一開始就提到 worker
線程之間的資料傳遞使用 postMessage
const worker = new Worker('worker.js);
worker.postMessage('data');
而在 postMessage
背後執行的是 structuredClone
算法,需要複製再將資料傳遞,所以會非常慢
接著介紹了 transfer
參數,只要是 Transferable objects
型態的資料都可以將資料的擁有權轉移到另一個線程中
const uInt8Array = new Uint8Array(1024 * 1).map((v, i) => i);
console.log(uInt8Array.byteLength); // 1024
// 將資料轉移到 worker 線程
worker.postMessage(uInt8Array, [uInt8Array.buffer]);
// 轉移過後資料就不在原本的線程了,所以 byteLength === 0
console.log(uInt8Array.byteLength); // 0
這種方式不會整個複製資料,而是將 原本資料在記憶體緩衝區(buffer)裡的所有權移轉到 worker 線程中,所以轉移後原線程就無法取得資料了,由 第 7 天文章 可轉移的物件 - Transferable objects 來看,使用 transfer
因為略過了複製資料,資料傳遞會更加快速。但 transfer
的方式只是將記憶體中的 buffer
從原本的線程轉移到另一個線程,實際上同時間只能有一個線程處理這筆資料,所以 SharedArrayBuffer 出現了。
SharedArrayBuffer
做到的是真正的共享記憶體,當 SharedArrayBuffer
在不同線程中傳遞時,實際上傳遞就是記憶體位址。
以下讓我們先介紹下 SharedArrayBuffer
的用法後再來說明 SharedArrayBuffer
使用的時機及使用時需注意的安全性問題。
建立的方式跟 ArrayBuffer
一樣,傳入的參數都是指分配多少 bytes
的記憶體空間,創建後直接使用 postMessage
就可以將整個記憶體傳到另一個線程
const sab = new SharedArrayBuffer(1024); // 1024 bytes
worker.postMessage(sab);
在創建 SharedArrayBuffer
的時候,可以在第二個參數傳入 maxByteLength
設定記憶體空間的上限,並且之後可以用 growable 及 grow 手動增加記憶體空間
但基於安全性的考量,創建後的 SharedArrayBuffer
只能擴增記憶體不能減少
// 初始 8 bytes,上限 16 bytes
const buffer = new SharedArrayBuffer(8, { maxByteLength: 16 });
if (buffer.growable) {
console.log("SAB is growable!");
// 記憶體空間增長到 12 bytes
buffer.grow(12);
}
由於 SharedArrayBuffer
創建出來的記憶體可以在不同線程間直接操作,所以會有許多問題出現 (ex. race condition),所以實際上 SharedArrayBuffer
需要搭配 Atomic (明天會再介紹) 處理可能有的 race condition
問題
MDN 官方文件 提到大約 2018 年左右因為 spectre 的攻擊,導致不同線程間共用記憶體這件事是有資安漏洞的,因此瀏覽器也停用了 SharedArrayBuffer
,直到 2020 年,因為提出了新的安全性預防方法,才讓 SharedArrayBuffer
的功能可以再次啟用。所以目前使用 SharedArrayBuffer
時都必須在回傳的 html
檔案標頭(response header) 設定以下兩項:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
如果沒有設定這兩項的話,SharedArrayBuffer
會直接丟出沒有定義的錯誤:Uncaught ReferenceError: SharedArrayBuffer is not defined
crossOriginIsolated
在瀏覽器中提供了一個方便的屬性 crossOriginIsolated,去判斷以上兩條 Cross-Origin-Opener-Policy
、Cross-Origin-Embedder-Policy
設定的值是不是安全性都夠高,讓 SharedArrayBuffer
可以使用
const myWorker = new Worker("worker.js");
if (crossOriginIsolated) {
// 通過才有辦法使用 SharedArrayBuffer
const buffer = new SharedArrayBuffer(16);
myWorker.postMessage(buffer);
} else {
// 沒有的話替代方案用 ArrayBuffer
const buffer = new ArrayBuffer(16);
myWorker.postMessage(buffer);
}
Cross-Origin-Opener-Policy (COOP)
這個屬性控制了不同 window
視窗間是否可以取得另一個 window
的瀏覽器上下文環境(browsing context)
如果 window A
開啟了 window B
,則使用 B.opener
會回傳 A
// window A (https://window-a.example.com)
window.open('https://window-b.example.com');
// window B (https://window-b.example.com)
console.log(window.opener) // 取得 window A 的 browsing context
但在跨域的狀況下 (也就是以上的 https://window-a.example.com 跟 https://window-b.example.com) , window B
是無法透過 window.opener 取得 windowA
的變數、函數等資料。
但即便大部分的資料都無法取得,有些資料還是被瀏覽器允許取到的,例如:window B
還是可以取得原網站 window A
的網址(https://window-a.example.com) 。而有辦法取到原網站的網址就可能做到所謂的網路釣魚攻擊,window B
可以在自己的網站中,拿到 window A
的網址,進而在自己的網頁中開啟 window A
的頁面,並把裡面原本正確的內容替換掉,讓使用者誤以為這是原本 window A
網頁的內容,而達到了釣魚攻擊。
而 Cross-Origin-Opener-Policy (COOP)
的設定就是用來解決這種問題,當設定了 Cross-Origin-Opener-Policy (COOP)
為 same-origin
後,跨域網站的 window B
就再也拿不到 window.opener
的相關資訊了
// window B (https://window-b.example.com)
console.log(window.opener) // null
所以設定了 Cross-Origin-Opener-Policy: same-origin
保證了 SharedArrayBuffer
不會被額外開啟的跨域網站拿到原本網站的資訊,導致進一步的惡意攻擊。
Cross-Origin-Embedder-Policy (COEP)Embedder-Policy
這個詞代表網頁內嵌資源的政策,將這個屬性設為 require-corp
的時候,意味著 html
文檔以下載入的所有資源,都必須是 同源 或是 明確標記為可從另一個來源載入的資源。
Cross-Origin-Embedder-Policy: require-corp
什麼是明確標記為可從另一個來源載入的資源呢?
原本在 HTML
中引入圖片只需要給予 src
指向的 url
即可
<img src="https://thirdparty.com/img.png" />
但當設置 Cross-Origin-Embedder-Policy (COEP)
為 require-corp
後,所有資源都必須明確標記是可以從另一個來源載入的,這裡的明確標記指的就是需要為 img
標籤增加 crossorigin
,明確告知我就是要用 CORS
跨域的方式拿取這個圖像資源
<img src="https://thirdparty.com/img.png" crossorigin />
而當使用 crossorigin
引用外部圖片的時候,也意味著圖片伺服器至少需要回傳允許跨域的回應標頭(response header),例如:
Access-Control-Allow-Origin: *
但我們無法控制引用的外部圖片伺服器是否都有這些 response header,當遇到這個狀況產生時,可以將 Cross-Origin-Embedder-Policy (COEP)
設置為 credentialless
,代表在使用以上這種 <img src="..." />
請求圖片的時候不會帶上任何憑證資訊(Cookie),但 credentialless 目前是實驗性屬性,Firefox 將會在這個月底開始支援,但 Safari 尚未支援
Cross-Origin-Embedder-Policy: credentialless
所以將 Cross-Origin-Embedder-Policy
設定為 require-corp
或是 credentialless
保證了 SharedArrayBuffer
不會讓跨域的其他資源有機會進行一些的惡意攻擊(ex. 前面提到的 spectre
)
額外補充
這裡提到的 Cross-Origin-Opener-Policy (COOP)
, Cross-Origin-Embedder-Policy (COEP)
其實都只是網頁中安全性設定的一小部分,推薦大家閱讀胡立大大今年新推出的專業資安文章 跨來源的安全性問題,可以更全面的瞭解怎麼保護網頁。
提到這麼多東西我想大家還是會覺得蠻模糊的,以下就讓我們用範例來看看如何實際使用 SharedArrayBuffer
範例 Demo
設置回傳標頭 (response header)
首先要讓網站達到足夠的安全性,才能使用 SharedArrayBuffer
,我們先在 server
端設定了需要回傳的 response header
const express = require("express");
const app = express();
app.get("/", (req, res) => {
// 考量安全性問題,需要回傳的兩項 header
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
res.sendFile(__dirname + "/index.html");
});
index.html
接著在 html
檔案中,會顯示 crossOriginIsolated
的值,如果網站可以正常使用 SharedArrayBuffer
的話,crossOriginIsolated
就會是 true
,反之為 false
<body>
<div id="app">
<h1>SharedArrayBuffer</h1>
<!-- 顯示 crossOriginIsolated 的值 -->
<h3 class="cross-origin-isolated">
crossOriginIsolated:
</h3>
</div>
<script src="public/index.mjs" type="module"></script>
</body>
index.mjs
在 javascript
中,會獲取 crossOriginIsolated
的值,並顯示在畫面上
document.querySelector('.cross-origin-isolated').textContent =
`crossOriginIsolated: ${crossOriginIsolated}`;
// 如果 crossOriginIsolated 為 true,可以使用 SharedArrayBuffer
if (crossOriginIsolated) {
const sab = new SharedArrayBuffer(1024);
console.log('The value of SharedArrayBuffer', sab);
}
令人意外的是 server
都按照安全性的要求設定 response header
了,但 crossOriginIsolated
還是 false
,這代表 SharedArrayBuffer
並不如預期般的可以使用
我找了一段時間才發現原因出在 codesanbox
的架構,我們編寫的 HTML
在 codesanbox
網站裡是用 iframe
內嵌進去的
而前面做的 response header
設定,影響到的是這個 iframe
載入的 HTML
文檔,回頭看 MDN 寫到使用 SharedArrayBuffer
的安全性設定:
For top-level documents, two headers need to be set to cross-origin isolate your site:
關鍵就在這裡的 top-level documents
是之前被我忽略的,我們加的兩個 response header
在codesanbox
中是加到 iframe
所在的 HTML
文檔,而不是 top-level document
,所以導致了 crossOriginIsolated
是 false
,這一點也可以從 Chrome devtool
裡看到
整個 codesanbox 的 top-level documents
被內嵌在 iframe 裡的 HTML
而解決方式很簡單就是按下 codesanbox
網址列右上角的另開分頁,把網站單獨開出來
接著再查看網站的安全性部分,就會發現 response header
設定正確,可以正常使用 SharedArrayBuffer
了
SharedArrayBuffer
允許在不同線程中操作同一塊記憶體,達到高效能的並行運算,但因為會有 spectre
等惡意攻擊的風險,所以對於網站的安全性要求也較高,需要在回傳 HTML
的 response header
有相對應的設定才能使用 SharedArrayBuffer
https://codesandbox.io/p/preview-protocol.js
被偷偷塞入在我們撰寫的 HTML
檔案中使用,看起來這個檔案是為了讓內嵌在 iframe
裡的 HTML
可以做到一些換頁或是檢查等的作用而點開這個檔案時會發現它沒有辦法被載入進來,以及出現一個警告說要設定正確的 Cross-Origin-Resource-Policy
會有這個警告的出現是因為我們設置了 Cross-Origin-Embedder-Policy: require-corp
,這代表了網站內的所有資源都必須 明確的標記為可從另一個來源載入的資源,所以這裡的警告就提出需要將這個資源 response header
的 Cross-Origin-Resource-Policy
設為 same-site
或是 cross-origin
, 分別代表著這個資源是 同站點的(same-site) 或是 跨域的來源(cross-origin),如此才能正確載入這個檔案。
但就像之前所說的,我們無法對於這個 外來的資源(https://codesandbox.io/p/preview-protocol.js)
設定其 response header
,所以遇到這個狀況,真的需要載入這個資源的時候,可以考慮將 Cross-Origin-Embedder-Policy
設定成 credentialless
,這樣就不會出現 Cross-Origin-Resource-Policy
的警告,並且可以正常加載資源了。
A cartoon intro to ArrayBuffers and SharedArrayBuffers
推薦大家必看的文章,淺顯易懂的用漫畫說明什麼是記憶體,然後再講解到 SharedArrayBuffer
JavaScript: From Workers to Shared Memory
ES proposal: Shared memory and atomics
使用 COOP 和 COEP“跨源隔离”网站