From 8d0bf4d721410618c1788a82bd7ad86327d90423 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Thu, 5 Dec 2024 00:13:53 +0100 Subject: [PATCH 1/4] Cache global `Crypto` object --- src/backends/wasm_js.rs | 69 +++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/src/backends/wasm_js.rs b/src/backends/wasm_js.rs index 7753daf9..ddc4e366 100644 --- a/src/backends/wasm_js.rs +++ b/src/backends/wasm_js.rs @@ -1,58 +1,61 @@ //! Implementation for WASM based on Web and Node.js + +/// Proc-macro is not hygienic. +/// See . +#[cfg(feature = "std")] +extern crate std; + use crate::Error; use core::mem::MaybeUninit; +#[cfg(feature = "std")] +use std::thread_local; pub use crate::util::{inner_u32, inner_u64}; #[cfg(not(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none"))))] compile_error!("`wasm_js` backend can be enabled only for OS-less WASM targets!"); -use js_sys::{global, Uint8Array}; -use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue}; +use js_sys::Uint8Array; +use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; // Size of our temporary Uint8Array buffer used with WebCrypto methods // Maximum is 65536 bytes see https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues const CRYPTO_BUFFER_SIZE: u16 = 256; pub fn fill_inner(dest: &mut [MaybeUninit]) -> Result<(), Error> { - let global: Global = global().unchecked_into(); - let crypto = global.crypto(); - - if !crypto.is_object() { - return Err(Error::WEB_CRYPTO); - } - - // getRandomValues does not work with all types of WASM memory, - // so we initially write to browser memory to avoid exceptions. - let buf = Uint8Array::new_with_length(CRYPTO_BUFFER_SIZE.into()); - for chunk in dest.chunks_mut(CRYPTO_BUFFER_SIZE.into()) { - let chunk_len: u32 = chunk - .len() - .try_into() - .expect("chunk length is bounded by CRYPTO_BUFFER_SIZE"); - // The chunk can be smaller than buf's length, so we call to - // JS to create a smaller view of buf without allocation. - let sub_buf = buf.subarray(0, chunk_len); - - if crypto.get_random_values(&sub_buf).is_err() { - return Err(Error::WEB_GET_RANDOM_VALUES); + CRYPTO.with(|crypto| { + let crypto = crypto.as_ref().ok_or(Error::WEB_CRYPTO)?; + + // getRandomValues does not work with all types of WASM memory, + // so we initially write to browser memory to avoid exceptions. + let buf = Uint8Array::new_with_length(CRYPTO_BUFFER_SIZE.into()); + for chunk in dest.chunks_mut(CRYPTO_BUFFER_SIZE.into()) { + let chunk_len: u32 = chunk + .len() + .try_into() + .expect("chunk length is bounded by CRYPTO_BUFFER_SIZE"); + // The chunk can be smaller than buf's length, so we call to + // JS to create a smaller view of buf without allocation. + let sub_buf = buf.subarray(0, chunk_len); + + if crypto.get_random_values(&sub_buf).is_err() { + return Err(Error::WEB_GET_RANDOM_VALUES); + } + + // SAFETY: `sub_buf`'s length is the same length as `chunk` + unsafe { sub_buf.raw_copy_to_ptr(chunk.as_mut_ptr().cast::()) }; } - - // SAFETY: `sub_buf`'s length is the same length as `chunk` - unsafe { sub_buf.raw_copy_to_ptr(chunk.as_mut_ptr().cast::()) }; - } - Ok(()) + Ok(()) + }) } #[wasm_bindgen] extern "C" { - // Return type of js_sys::global() - type Global; // Web Crypto API: Crypto interface (https://www.w3.org/TR/WebCryptoAPI/) type Crypto; - // Getters for the Crypto API - #[wasm_bindgen(method, getter)] - fn crypto(this: &Global) -> Crypto; + // Holds the global `Crypto` object. + #[wasm_bindgen(thread_local_v2, js_namespace = globalThis, js_name = crypto)] + static CRYPTO: Option; // Crypto.getRandomValues() #[wasm_bindgen(method, js_name = getRandomValues, catch)] fn get_random_values(this: &Crypto, buf: &Uint8Array) -> Result<(), JsValue>; From de63b7ffe40a2a911a4c25c6301144fffeed53a9 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Fri, 6 Dec 2024 09:30:50 +0100 Subject: [PATCH 2/4] Don't create sub-array unless necessary --- src/backends/wasm_js.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/backends/wasm_js.rs b/src/backends/wasm_js.rs index ddc4e366..b4087caa 100644 --- a/src/backends/wasm_js.rs +++ b/src/backends/wasm_js.rs @@ -36,7 +36,11 @@ pub fn fill_inner(dest: &mut [MaybeUninit]) -> Result<(), Error> { .expect("chunk length is bounded by CRYPTO_BUFFER_SIZE"); // The chunk can be smaller than buf's length, so we call to // JS to create a smaller view of buf without allocation. - let sub_buf = buf.subarray(0, chunk_len); + let sub_buf = if chunk_len == u32::from(CRYPTO_BUFFER_SIZE) { + buf.clone() + } else { + buf.subarray(0, chunk_len) + }; if crypto.get_random_values(&sub_buf).is_err() { return Err(Error::WEB_GET_RANDOM_VALUES); From 18288d76970a7baa0007c28518df083c8833d50e Mon Sep 17 00:00:00 2001 From: daxpedda Date: Thu, 5 Dec 2024 01:28:27 +0100 Subject: [PATCH 3/4] Don't use `Uint8Array` when not necessary --- .github/workflows/tests.yml | 45 +++++++++++----- .github/workflows/workspace.yml | 4 ++ src/backends/wasm_js.rs | 93 +++++++++++++++++++++++++-------- 3 files changed, 106 insertions(+), 36 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 045a6bb9..3cf37352 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -229,11 +229,27 @@ jobs: # run: cargo test web: - name: Web + name: Web ${{ matrix.rust.description }} runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + rust: + - { version: stable, atomics: false } + - { + description: with Atomics, + atomics: true, + version: nightly, + components: rust-src, + flags: '-Ctarget-feature=+atomics,+bulk-memory', + args: '-Zbuild-std=panic_abort,std', + } steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust.version }} + components: ${{ matrix.rust.components }} - name: Install precompiled wasm-pack shell: bash run: | @@ -243,35 +259,36 @@ jobs: wasm-pack --version - uses: Swatinem/rust-cache@v2 - name: Test (Node) + if: matrix.rust.atomics == false env: - RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js" - run: wasm-pack test --node -- --features std + RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js" ${{ matrix.rust.flags }} + run: wasm-pack test --node -- --features std ${{ matrix.rust.args }} - name: Test (Firefox) env: WASM_BINDGEN_USE_BROWSER: 1 - RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js" - run: wasm-pack test --headless --firefox -- --features std + RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js" ${{ matrix.rust.flags }} + run: wasm-pack test --headless --firefox -- --features std ${{ matrix.rust.args }} - name: Test (Chrome) env: WASM_BINDGEN_USE_BROWSER: 1 - RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js" - run: wasm-pack test --headless --chrome -- --features std + RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js" ${{ matrix.rust.flags }} + run: wasm-pack test --headless --chrome -- --features std ${{ matrix.rust.args }} - name: Test (dedicated worker) env: WASM_BINDGEN_USE_DEDICATED_WORKER: 1 - RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js" - run: wasm-pack test --headless --firefox -- --features std + RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js" ${{ matrix.rust.flags }} + run: wasm-pack test --headless --firefox -- --features std ${{ matrix.rust.args }} - name: Test (shared worker) env: WASM_BINDGEN_USE_SHARED_WORKER: 1 - RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js" - run: wasm-pack test --headless --firefox -- --features std + RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js" ${{ matrix.rust.flags }} + run: wasm-pack test --headless --firefox -- --features std ${{ matrix.rust.args }} - name: Test (service worker) env: WASM_BINDGEN_USE_SERVICE_WORKER: 1 - RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js" + RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js" ${{ matrix.rust.flags }} # Firefox doesn't support module service workers and therefor can't import scripts - run: wasm-pack test --headless --chrome -- --features std + run: wasm-pack test --headless --chrome -- --features std ${{ matrix.rust.args }} wasi: name: WASI diff --git a/.github/workflows/workspace.yml b/.github/workflows/workspace.yml index 2653984d..7f62034a 100644 --- a/.github/workflows/workspace.yml +++ b/.github/workflows/workspace.yml @@ -49,6 +49,10 @@ jobs: env: RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js" run: cargo clippy -Zbuild-std --target wasm32-unknown-unknown + - name: Web WASM with atomics (wasm_js.rs) + env: + RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js" -Ctarget-feature=+atomics,+bulk-memory + run: cargo clippy -Zbuild-std --target wasm32-unknown-unknown - name: Linux (linux_android.rs) env: RUSTFLAGS: -Dwarnings --cfg getrandom_backend="linux_getrandom" diff --git a/src/backends/wasm_js.rs b/src/backends/wasm_js.rs index b4087caa..9cb8757f 100644 --- a/src/backends/wasm_js.rs +++ b/src/backends/wasm_js.rs @@ -5,7 +5,7 @@ #[cfg(feature = "std")] extern crate std; -use crate::Error; +use crate::{util, Error}; use core::mem::MaybeUninit; #[cfg(feature = "std")] use std::thread_local; @@ -15,7 +15,7 @@ pub use crate::util::{inner_u32, inner_u64}; #[cfg(not(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none"))))] compile_error!("`wasm_js` backend can be enabled only for OS-less WASM targets!"); -use js_sys::Uint8Array; +use js_sys::{JsString, Uint8Array}; use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; // Size of our temporary Uint8Array buffer used with WebCrypto methods @@ -26,34 +26,75 @@ pub fn fill_inner(dest: &mut [MaybeUninit]) -> Result<(), Error> { CRYPTO.with(|crypto| { let crypto = crypto.as_ref().ok_or(Error::WEB_CRYPTO)?; - // getRandomValues does not work with all types of WASM memory, - // so we initially write to browser memory to avoid exceptions. - let buf = Uint8Array::new_with_length(CRYPTO_BUFFER_SIZE.into()); - for chunk in dest.chunks_mut(CRYPTO_BUFFER_SIZE.into()) { - let chunk_len: u32 = chunk - .len() - .try_into() - .expect("chunk length is bounded by CRYPTO_BUFFER_SIZE"); - // The chunk can be smaller than buf's length, so we call to - // JS to create a smaller view of buf without allocation. - let sub_buf = if chunk_len == u32::from(CRYPTO_BUFFER_SIZE) { - buf.clone() - } else { - buf.subarray(0, chunk_len) - }; + if is_sab() { + // getRandomValues does not work with all types of WASM memory, + // so we initially write to browser memory to avoid exceptions. + let buf = Uint8Array::new_with_length(CRYPTO_BUFFER_SIZE.into()); + for chunk in dest.chunks_mut(CRYPTO_BUFFER_SIZE.into()) { + let chunk_len: u32 = chunk + .len() + .try_into() + .expect("chunk length is bounded by CRYPTO_BUFFER_SIZE"); + // The chunk can be smaller than buf's length, so we call to + // JS to create a smaller view of buf without allocation. + let sub_buf = if chunk_len == u32::from(CRYPTO_BUFFER_SIZE) { + buf.clone() + } else { + buf.subarray(0, chunk_len) + }; - if crypto.get_random_values(&sub_buf).is_err() { - return Err(Error::WEB_GET_RANDOM_VALUES); - } + if crypto.get_random_values(&sub_buf).is_err() { + return Err(Error::WEB_GET_RANDOM_VALUES); + } - // SAFETY: `sub_buf`'s length is the same length as `chunk` - unsafe { sub_buf.raw_copy_to_ptr(chunk.as_mut_ptr().cast::()) }; + // SAFETY: `sub_buf`'s length is the same length as `chunk` + unsafe { sub_buf.raw_copy_to_ptr(chunk.as_mut_ptr().cast::()) }; + } + } else { + for chunk in dest.chunks_mut(CRYPTO_BUFFER_SIZE.into()) { + // SAFETY: this is only safe because on Wasm the issues with unitialized data don't exist + if crypto + .get_random_values_ref(unsafe { util::slice_assume_init_mut(chunk) }) + .is_err() + { + return Err(Error::WEB_GET_RANDOM_VALUES); + } + } } Ok(()) }) } +#[cfg(not(target_feature = "atomics"))] +fn is_sab() -> bool { + use js_sys::WebAssembly::Memory; + use js_sys::{Object, SharedArrayBuffer}; + use wasm_bindgen::JsCast; + + let buffer: Object = wasm_bindgen::memory() + .unchecked_into::() + .buffer() + .unchecked_into(); + + // `crossOriginIsolated` is not available on Node.js and Safari () + } else { + // `SharedArrayBuffer` is only available with COOP & COEP. But even + // without its possible to create a shared `WebAssembly.Memory`, so we + // check for that via the constructor name. + let constructor_name = buffer.constructor().name(); + SHARED_ARRAY_BUFFER_NAME.with(|sab_name| &constructor_name == sab_name) + } +} + +#[cfg(target_feature = "atomics")] +fn is_sab() -> bool { + true +} + #[wasm_bindgen] +#[rustfmt::skip] extern "C" { // Web Crypto API: Crypto interface (https://www.w3.org/TR/WebCryptoAPI/) type Crypto; @@ -63,4 +104,12 @@ extern "C" { // Crypto.getRandomValues() #[wasm_bindgen(method, js_name = getRandomValues, catch)] fn get_random_values(this: &Crypto, buf: &Uint8Array) -> Result<(), JsValue>; + #[wasm_bindgen(method, js_name = getRandomValues, catch)] + fn get_random_values_ref(this: &Crypto, buf: &mut [u8]) -> Result<(), JsValue>; + // Holds the `crossOriginIsolated` (https://developer.mozilla.org/en-US/docs/Web/API/crossOriginIsolated) global property. + #[wasm_bindgen(thread_local_v2, js_namespace = globalThis, js_name = crossOriginIsolated)] + static CROSS_ORIGIN_ISOLATED: Option; + // Holds the constructor name of the `SharedArrayBuffer` class. + #[wasm_bindgen(thread_local_v2, static_string)] + static SHARED_ARRAY_BUFFER_NAME: JsString = "SharedArrayBuffer"; } From 896e036951ecd3e9180eb5ff418df8227832a023 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Fri, 6 Dec 2024 09:48:52 +0100 Subject: [PATCH 4/4] Cache `SharedArrayBuffer` detection --- src/backends/wasm_js.rs | 52 +++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/src/backends/wasm_js.rs b/src/backends/wasm_js.rs index 9cb8757f..d60b698f 100644 --- a/src/backends/wasm_js.rs +++ b/src/backends/wasm_js.rs @@ -67,24 +67,43 @@ pub fn fill_inner(dest: &mut [MaybeUninit]) -> Result<(), Error> { #[cfg(not(target_feature = "atomics"))] fn is_sab() -> bool { + use core::sync::atomic::{AtomicU8, Ordering}; + + use js_sys::Object; use js_sys::WebAssembly::Memory; - use js_sys::{Object, SharedArrayBuffer}; use wasm_bindgen::JsCast; - let buffer: Object = wasm_bindgen::memory() - .unchecked_into::() - .buffer() - .unchecked_into(); - - // `crossOriginIsolated` is not available on Node.js and Safari () - } else { - // `SharedArrayBuffer` is only available with COOP & COEP. But even - // without its possible to create a shared `WebAssembly.Memory`, so we - // check for that via the constructor name. - let constructor_name = buffer.constructor().name(); - SHARED_ARRAY_BUFFER_NAME.with(|sab_name| &constructor_name == sab_name) + const MEMORY_KIND_UNINIT: u8 = 0; + const MEMORY_KIND_NOT_SHARED: u8 = 1; + const MEMORY_KIND_SHARED: u8 = 2; + + static MEMORY_KIND: AtomicU8 = AtomicU8::new(0); + + loop { + break match MEMORY_KIND.load(Ordering::Relaxed) { + MEMORY_KIND_NOT_SHARED => false, + MEMORY_KIND_SHARED => true, + MEMORY_KIND_UNINIT => { + let buffer: Object = wasm_bindgen::memory() + .unchecked_into::() + .buffer() + .unchecked_into(); + + // `SharedArrayBuffer` is only available with COOP & COEP. But even without its + // possible to create a shared `WebAssembly.Memory`, so we check for that via + // the constructor name. + let constructor_name = buffer.constructor().name(); + let val = if SHARED_ARRAY_BUFFER_NAME.with(|sab_name| &constructor_name == sab_name) + { + MEMORY_KIND_SHARED + } else { + MEMORY_KIND_NOT_SHARED + }; + MEMORY_KIND.store(val, Ordering::Relaxed); + continue; + } + _ => unreachable!(), + }; } } @@ -106,9 +125,6 @@ extern "C" { fn get_random_values(this: &Crypto, buf: &Uint8Array) -> Result<(), JsValue>; #[wasm_bindgen(method, js_name = getRandomValues, catch)] fn get_random_values_ref(this: &Crypto, buf: &mut [u8]) -> Result<(), JsValue>; - // Holds the `crossOriginIsolated` (https://developer.mozilla.org/en-US/docs/Web/API/crossOriginIsolated) global property. - #[wasm_bindgen(thread_local_v2, js_namespace = globalThis, js_name = crossOriginIsolated)] - static CROSS_ORIGIN_ISOLATED: Option; // Holds the constructor name of the `SharedArrayBuffer` class. #[wasm_bindgen(thread_local_v2, static_string)] static SHARED_ARRAY_BUFFER_NAME: JsString = "SharedArrayBuffer";