From fad53d3345fc5117a6f83f8f59f82e9245759979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC=20=D0=9F=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=BE=D0=B2=20=5BArtyom=20Pavlov=5D?= Date: Wed, 4 Dec 2024 21:00:20 +0300 Subject: [PATCH] wasm_js: remove TLS --- src/backends/wasm_js.rs | 134 ++++++++++++++++++++++++---------------- 1 file changed, 80 insertions(+), 54 deletions(-) diff --git a/src/backends/wasm_js.rs b/src/backends/wasm_js.rs index 15a4e66b..b66ce038 100644 --- a/src/backends/wasm_js.rs +++ b/src/backends/wasm_js.rs @@ -4,6 +4,8 @@ use crate::Error; extern crate std; use std::{mem::MaybeUninit, thread_local}; +use core::sync::atomic::{AtomicU8, Ordering}; + pub use crate::util::{inner_u32, inner_u64}; #[cfg(not(all(target_arch = "wasm32", target_os = "unknown",)))] @@ -18,63 +20,89 @@ const WEB_CRYPTO_BUFFER_SIZE: u16 = 256; // Node.js's crypto.randomFillSync requires the size to be less than 2**31. const NODE_MAX_BUFFER_SIZE: usize = (1 << 31) - 1; -enum RngSource { - Node(NodeCrypto), - Web(WebCrypto, Uint8Array), -} +const KIND_UNINIT: u8 = 0; +const KIND_WEB: u8 = 1; +const KIND_NODE: u8 = 2; +const KIND_NA: u8 = 255; -// JsValues are always per-thread, so we initialize RngSource for each thread. -// See: https://github.com/rustwasm/wasm-bindgen/pull/955 -thread_local!( - static RNG_SOURCE: Result = getrandom_init(); -); +static KIND: AtomicU8 = AtomicU8::new(KIND_UNINIT); pub fn fill_inner(dest: &mut [MaybeUninit]) -> Result<(), Error> { - RNG_SOURCE.with(|result| { - let source = result.as_ref().map_err(|&e| e)?; - - match source { - RngSource::Node(n) => { - for chunk in dest.chunks_mut(NODE_MAX_BUFFER_SIZE) { - // SAFETY: chunk is never used directly, the memory is only - // modified via the Uint8Array view, which is passed - // directly to JavaScript. Also, crypto.randomFillSync does - // not resize the buffer. We know the length is less than - // u32::MAX because of the chunking above. - // Note that this uses the fact that JavaScript doesn't - // have a notion of "uninitialized memory", this is purely - // a Rust/C/C++ concept. - let res = n.random_fill_sync(unsafe { - Uint8Array::view_mut_raw(chunk.as_mut_ptr().cast::(), chunk.len()) - }); - if res.is_err() { - return Err(Error::NODE_RANDOM_FILL_SYNC); - } - } - } - RngSource::Web(crypto, buf) => { - // getRandomValues does not work with all types of WASM memory, - // so we initially write to browser memory to avoid exceptions. - for chunk in dest.chunks_mut(WEB_CRYPTO_BUFFER_SIZE.into()) { - let chunk_len: u32 = chunk - .len() - .try_into() - .expect("chunk length is bounded by WEB_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::()) }; - } + loop { + break match KIND.load(Ordering::Relaxed) { + KIND_UNINIT => { + let global: Global = global().unchecked_into(); + let crypto = global.crypto(); + let val = if crypot.is_object() { + KIND_WEB + } else if is_node(&global) { + KIND_NODE + } else { + KIND_NA + }; + KIND.store(val, Ordering::Relaxed); + continue; } + KIND_WEB => web_fill(dest), + KIND_NODE => node_fill(dest), + _ => Err(Error::WEB_CRYPTO), }; - Ok(()) - }) + } +} + +pub fn web_fill(dest: &mut [MaybeUninit]) -> Result<(), Error> { + let global: Global = global().unchecked_into(); + let crypto = global.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(WEB_CRYPTO_BUFFER_SIZE.into()); + for chunk in dest.chunks_mut(WEB_CRYPTO_BUFFER_SIZE.into()) { + let chunk_len: u32 = chunk + .len() + .try_into() + .expect("chunk length is bounded by WEB_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::()) }; + } + Ok(()) +} + +pub fn web_fill(dest: &mut [MaybeUninit]) -> Result<(), Error> { + // If module.require isn't a valid function, we are in an ES module. + let require_fn = Module::require_fn() + .and_then(JsCast::dyn_into::) + .map_err(|_| Error::NODE_ES_MODULE)?; + let n = require_fn + .call1(&global, &JsValue::from_str("crypto")) + .map_err(|_| Error::NODE_CRYPTO)? + .unchecked_into(); + + for chunk in dest.chunks_mut(NODE_MAX_BUFFER_SIZE) { + // SAFETY: chunk is never used directly, the memory is only + // modified via the Uint8Array view, which is passed + // directly to JavaScript. Also, crypto.randomFillSync does + // not resize the buffer. We know the length is less than + // u32::MAX because of the chunking above. + // Note that this uses the fact that JavaScript doesn't + // have a notion of "uninitialized memory", this is purely + // a Rust/C/C++ concept. + let res = n.random_fill_sync(unsafe { + Uint8Array::view_mut_raw(chunk.as_mut_ptr().cast::(), chunk.len()) + }); + if res.is_err() { + return Err(Error::NODE_RANDOM_FILL_SYNC); + } + } + Ok(()) } fn getrandom_init() -> Result { @@ -124,8 +152,6 @@ extern "C" { // Getters for the WebCrypto API #[wasm_bindgen(method, getter)] fn crypto(this: &Global) -> WebCrypto; - #[wasm_bindgen(method, getter, js_name = msCrypto)] - fn ms_crypto(this: &Global) -> WebCrypto; // Crypto.getRandomValues() #[wasm_bindgen(method, js_name = getRandomValues, catch)] fn get_random_values(this: &WebCrypto, buf: &Uint8Array) -> Result<(), JsValue>;