June 24, 2021
Understanding SharedArrayBuffer and cross-origin isolation

The JavaScript memory stores every piece of data and instruction used during program development and runtime in binary form. JavaScript, also known as ECMAScript, is a memory-managed language.

The JavaScript engine accesses and manages the memory by itself, and it allocates memory for each program or chunk of code written and executed. It also performs the garbage collection of data that is no longer found in the memory.

Although JavaScript is a memory-managed language, it helps manage data, too. But it has flaws. For example, JavaScript can allocate more than the free space needed in the memory for a particular program or variable. In some cases, garbage collection in JavaScript can be slow.

To give developers the ability to allocate and share data in views (using typed arrays) between multiple threads from a memory, the ArrayBuffer and SharedArrayBuffer features were introduced.

What is SharedArrayBuffer?

When discussing SharedArrayBuffer, we can easily focus on the physical words: “shared,” “array,” and “buffer.”

An array is a data structure used in programming to store data elements consisting of different data types (strings, booleans, numbers, and objects). A buffer is part of the memory storage used to store data temporarily before it is sent or received for usage.

ArrayBuffer is an array unlike any other — it is an array of bytes, meaning that only bytes are accepted.

To use shared memory in JavaScript, you need to create the SharedArrayBuffer. This is done by using the SharedArrayBuffer object, which creates a new object constructor for writing and sharing data between multiple threads.

The history of SharedArrayBuffer

On January 5, 2018, SharedArrayBuffer was disabled in all major browsers due to a vulnerability attack that was found in modern CPU architecture.

Since then, SharedArrayBuffer was re-enabled in Google Chrome v67 and can now be used on platforms that have its site isolation feature enabled, which we’ll cover in a subsequent section of this article. This update protects against the Spectre vulnerability attack and makes your site more secure.

Below, we are going to explore how to share memory using SharedArrayBuffer in JavaScript. First, we’ll share memory, then update and synchronize it, and finally, debug it.

Sharing memory using SharedArrayBuffer in JavaScript

One of the perks of using SharedArrayBuffer is the ability to share memory in JavaScript. In JavaScript, web workers serve as a means of creating threads in JS code.

However, web workers are also used side-by-side with SharedArrayBuffer, which enables the sharing of raw-binary data between the web workers by pointing directly to the memory where each data has been stored or previously accessed.

Let’s take a look at an example of how to share memory using SharedArrayBuffer.

Before we begin, create three files: index.html (where we linked the scripts), script.js (main thread), and worker.js (worker thread).

<!–index.html file–>

<DOCTYPE html>
<html>
<head>
<title>using shared array buffer</title>
<meta charset=”UTF-8″>
<meta name=”sharedArrayBuffer” description=”using cross-orgiin-isolation in the web browser”>
<script type=”text/JavaScript” src=”script.js”></script>
</head>
<body>
<h3>Take a look at your browser console :)</h3>
<script type=”text/JavaScript” src=”worker.js”></script>
</body>
</html>

Let’s look at the main thread (the script.js file) first. Here, we access worker.js, then create a shared memory using the SharedArrayBuffer object, and set its bytes per length to 1024 (note: any required byte per length can be used).

Using a typed array of type Int16Array to interpret the data being passed, we assign a number to the typed array (20)to be shared from the main thread. We send the buffer to the worker thread with the use of postMessage.

/*MAIN THREAD*/

const newWorker = new Worker(‘worker.js’);
const buffMemLength = new SharedArrayBuffer(1024); //byte length
let typedArr = new Int16Array(buffMemLength);
//original data
typedArr[0] = 20;
//sending the buffer to worker
newWorker.postMessage(buffMemLength);

In order to share the data from the main thread with the worker thread, we set an eventListener to run when the data is received. Here, notice that we are using an Int16 typed array to display the data in the browser console.

/*WORKER THREAD*/

addEventListener(‘message’, ({ data }) => {
let arr = new Int16Array(data);
console.group(‘[the worker thread]’);
console.log(‘Data received from the main thread: %i’, arr[0]);
console.groupEnd();
postMessage(‘Updated’);
})

In your browser console, you should see this:

[the worker thread] worker.js:7
Data received from main thread: 20 worker.js:8

Updating and synchronizing shared memory

Since the addition of SharedArrayBuffer to JavaScript, updating shared memory has become easier. Using the previous example, we are going to update the data from the worker thread.

Let’s set the original arr[0] from the main thread to the dataChanged variable declared above in the scope (in the worker thread).

/*WORKER THREAD*/

let BYTE_PER_LENTH = 5;
addEventListener(‘message’, ({ data }) => {
var arr = new Int16Array(data);
console.group(‘[worker thread]’);
console.log(‘Data received from main thread: %i’, arr[0]);
console.groupEnd();

//updating the data from the worker thread
let dataChanged = 5 * BYTE_PER_LENTH;
arr[0] = dataChanged;
//Sending to the main thread
postMessage(‘Updated’);
})

In order for the data to be updated from the worker thread, we call an onmessage event that will fire in the main thread, indicating data was updated from the worker thread.

/*MAIN THREAD*/

const newWorker = new Worker(‘worker.js’);
const buffMemLength = new SharedArrayBuffer(1024); //byte length
var typedArr = new Int16Array(buffMemLength);
//original data
typedArr[0] = 20;
//sending the buffer to worker
newWorker.postMessage(buffMemLength);

//onmessage event
newWorker.onmessage = (e) => {
console.group(‘[the main thread]’);
console.log(‘Data updated from the worker thread: %i’, typedArr[0]);
console.groupEnd();
}

In your browser console, you should see:

[the worker thread] worker.js:7
Data received from main thread: 20 worker.js:8
[the main thread] script.js:15
Data updated from the worker thread: 25 script.js:16

Synchronizing shared memory is important because, when implemented, synchronizing causes the shared memory to run simultaneously without been altered. To incorporate synchronization in shared memory, developers use Atomic operations.

Atomics ensures each process is executed consecutively before the next, and that all data that’s been read from a memory or written to a particular memory are being executed one after the other with the help of the wait() and notify() methods.

Recent updates on SharedArrayBuffer and cross-origin isolation

Since May 2021, there have been several critical updates to shared memory — including cross-origin isolation — in JavaScript that enable developers to debug shared memory more efficiently. It’s currently supported in Firefox 79+ and on desktop Chrome, but updates from Chrome 92 will be accessible to sites with cross-origin isolated pages.

To implement SharedArrayBuffer, you need a secure environment that limits access using one or more response header directives. This is called cross-origin isolation, and, despite the previously discouraged use of shared memory, it has proven to be a better way to secure your website.

What is cross-origin isolation?

Cross-origin isolation is a new security feature (as of April 2021) that was added to the browser. In short, it is the result of sending two HTTP headers on your top-level document (COOP and COEP). These headers enable your website to gain access to web APIs such as SharedArrayBuffer and prevent outer attacks (Spectre attacks, cross-origin attacks, and the like).

Previously, websites using shared memory could load cross-origin content without permission. These websites could interact with window pop-ups that are not of the same origins, potentially causing a security breach or a loophole to gain access to user information on the website. For websites using shared memory, it became of great importance to be secure and also protect user information.

Enabling cross-origin isolation on your website

Now that we know more about the context around cross-origin isolation, let’s enable it on our website.

First, enable the cross-origin-opener-policy (COOP) header at the top level of your document, with same-origin:

Cross-Origin-Opener-Policy: same-origin

This header isolates the page from any cross-origin pop-ups in the browser so that they will not be able to access documents or send direct messages to them. It also ensures your page is in a secure context with pages with the same top-level origins.

Next, send a cross-origin-embedder-policy header (COEP) with a value indicating require-CORP (cross-origin resource policy).

Cross-Origin-Embedder-Policy: require-corp

This ensures all resources loaded from your website have been loaded with CORP. The COEP header breaks every integration that requires communication with cross-origin windows in the browser, such as authentications from third-party servers and payments (checkouts).

By setting these headers at the top level of your documents, your website is now in a secure context and provides access to the use of web APIs.

The post Understanding <code>SharedArrayBuffer</code> and cross-origin isolation appeared first on LogRocket Blog.

Leave a Reply

Your email address will not be published. Required fields are marked *

Send