iT邦幫忙

2023 iThome 鐵人賽

DAY 20
0
Modern Web

網頁的另一個大腦:從基礎到進階掌握 Web Worker 技術系列 第 20

在 Javascript 中共享記憶體 - SharedArrayBuffer

  • 分享至 

  • xImage
  •  

在這 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 使用的時機及使用時需注意的安全性問題。

創建 SharedArrayBuffer

建立的方式跟 ArrayBuffer 一樣,傳入的參數都是指分配多少 bytes 的記憶體空間,創建後直接使用 postMessage 就可以將整個記憶體傳到另一個線程

const sab = new SharedArrayBuffer(1024); // 1024 bytes
worker.postMessage(sab);

增加 SharedArrayBuffer 的記憶體空間

在創建 SharedArrayBuffer 的時候,可以在第二個參數傳入 maxByteLength 設定記憶體空間的上限,並且之後可以用 growablegrow 手動增加記憶體空間

但基於安全性的考量,創建後的 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

由於 SharedArrayBuffer 創建出來的記憶體可以在不同線程間直接操作,所以會有許多問題出現 (ex. race condition),所以實際上 SharedArrayBuffer 需要搭配 Atomic (明天會再介紹) 處理可能有的 race condition 問題

使用 SharedArrayBuffer 的安全性問題

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
https://ithelp.ithome.com.tw/upload/images/20231004/20162687kI3N7Ix7Zw.png

crossOriginIsolated
在瀏覽器中提供了一個方便的屬性 crossOriginIsolated,去判斷以上兩條 Cross-Origin-Opener-PolicyCross-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.comhttps://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 並不如預期般的可以使用
https://ithelp.ithome.com.tw/upload/images/20231004/20162687HqEp98V0ig.png

我找了一段時間才發現原因出在 codesanbox 的架構,我們編寫的 HTMLcodesanbox 網站裡是用 iframe 內嵌進去的
https://ithelp.ithome.com.tw/upload/images/20231004/20162687sIidZYpkkD.png

而前面做的 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 headercodesanbox 中是加到 iframe 所在的 HTML 文檔,而不是 top-level document,所以導致了 crossOriginIsolatedfalse,這一點也可以從 Chrome devtool 裡看到

整個 codesanbox 的 top-level documents
https://ithelp.ithome.com.tw/upload/images/20231004/20162687SZ0EXFjRXn.png

被內嵌在 iframe 裡的 HTML
https://ithelp.ithome.com.tw/upload/images/20231004/201626870C7HOk8FuC.png

而解決方式很簡單就是按下 codesanbox 網址列右上角的另開分頁,把網站單獨開出來
https://ithelp.ithome.com.tw/upload/images/20231004/20162687zODn1ejGJr.png

接著再查看網站的安全性部分,就會發現 response header 設定正確,可以正常使用 SharedArrayBuffer
https://ithelp.ithome.com.tw/upload/images/20231004/20162687PNCIQGCv0C.png

結論

SharedArrayBuffer 允許在不同線程中操作同一塊記憶體,達到高效能的並行運算,但因為會有 spectre 等惡意攻擊的風險,所以對於網站的安全性要求也較高,需要在回傳 HTMLresponse header 有相對應的設定才能使用 SharedArrayBuffer

補充小知識

  1. Cross-Origin-Resource-Policy
    當點開範例中的 network 選項時,會發現有一個檔案 https://codesandbox.io/p/preview-protocol.js 被偷偷塞入在我們撰寫的 HTML 檔案中使用,看起來這個檔案是為了讓內嵌在 iframe 裡的 HTML 可以做到一些換頁或是檢查等的作用
    https://ithelp.ithome.com.tw/upload/images/20231004/201626871dOOs3bOPg.png

而點開這個檔案時會發現它沒有辦法被載入進來,以及出現一個警告說要設定正確的 Cross-Origin-Resource-Policy
https://ithelp.ithome.com.tw/upload/images/20231004/20162687Xt9gdHzrL0.png

會有這個警告的出現是因為我們設置了 Cross-Origin-Embedder-Policy: require-corp,這代表了網站內的所有資源都必須 明確的標記為可從另一個來源載入的資源,所以這裡的警告就提出需要將這個資源 response headerCross-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 的警告,並且可以正常加載資源了。

Reference

A cartoon intro to ArrayBuffers and SharedArrayBuffers
推薦大家必看的文章,淺顯易懂的用漫畫說明什麼是記憶體,然後再講解到 SharedArrayBuffer

JavaScript: From Workers to Shared Memory
ES proposal: Shared memory and atomics
使用 COOP 和 COEP“跨源隔离”网站


上一篇
Shared worker 的生命週期
下一篇
Javascript 中的原子操作 - Atomic
系列文
網頁的另一個大腦:從基礎到進階掌握 Web Worker 技術30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言