From a2990b5e245ce08a9e9643361b79dc2f461da773 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Thu, 7 Jul 2022 13:27:35 -0600 Subject: [PATCH] Better error handling in SubtleCrypto workers (#71693) * Better error handling in SubtleCrypto workers Handle exceptions from SubtleCrypto by catching and logging exceptions coming from the crypto stack. Reset web worker when a request fails. Also, fix race conditions where the web worker can read its own response as part of the next request. Contributes to #69740 --- .../src/Resources/Strings.resx | 3 + .../AesSubtleCryptoTransform.Browser.cs | 2 +- .../HMACHashProvider.Browser.Native.cs | 35 ++- .../SHAHashProvider.Browser.Native.cs | 36 ++- src/mono/wasm/runtime/crypto-worker.ts | 236 ++++++++++++------ src/mono/wasm/runtime/debug.ts | 69 +++++ .../runtime/workers/dotnet-crypto-worker.js | 201 ++++++++++----- 7 files changed, 403 insertions(+), 179 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx index 3d35b291f4319..d995f81ff6186 100644 --- a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx @@ -753,6 +753,9 @@ Unknown error. + + SubtleCrypto returned an unknown error: '{0}'. + Only CipherMode.CBC is supported on this platform. diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesSubtleCryptoTransform.Browser.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesSubtleCryptoTransform.Browser.cs index 5e0cfc68641de..3706dd5a0f9bf 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesSubtleCryptoTransform.Browser.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesSubtleCryptoTransform.Browser.cs @@ -148,7 +148,7 @@ private unsafe int EncryptDecrypt(ReadOnlySpan input, Span output) pOutput, output.Length); if (bytesWritten < 0) - throw new Exception(SR.Unknown_Error); + throw new CryptographicException(SR.Format(SR.Unknown_SubtleCrypto_Error, bytesWritten)); return bytesWritten; } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HMACHashProvider.Browser.Native.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HMACHashProvider.Browser.Native.cs index 2b04019123240..a9bbcc1ffbce4 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HMACHashProvider.Browser.Native.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HMACHashProvider.Browser.Native.cs @@ -43,42 +43,35 @@ public override int GetCurrentHash(Span destination) { Debug.Assert(destination.Length >= _hashSizeInBytes); - byte[] srcArray = Array.Empty(); - int srcLength = 0; - if (_buffer != null) - { - srcArray = _buffer.GetBuffer(); - srcLength = (int)_buffer.Length; - } + ReadOnlySpan source = _buffer != null ? + new ReadOnlySpan(_buffer.GetBuffer(), 0, (int)_buffer.Length) : + default; - unsafe - { - fixed (byte* key = _key) - fixed (byte* src = srcArray) - fixed (byte* dest = destination) - { - int res = Interop.BrowserCrypto.Sign(_hashAlgorithm, key, _key.Length, src, srcLength, dest, destination.Length); - Debug.Assert(res != 0); - } - } + Sign(_hashAlgorithm, _key, source, destination); return _hashSizeInBytes; } - public static unsafe int MacDataOneShot(string hashAlgorithmId, ReadOnlySpan key, ReadOnlySpan data, Span destination) + public static int MacDataOneShot(string hashAlgorithmId, ReadOnlySpan key, ReadOnlySpan data, Span destination) { (SimpleDigest hashName, int hashSizeInBytes) = SHANativeHashProvider.HashAlgorithmToPal(hashAlgorithmId); Debug.Assert(destination.Length >= hashSizeInBytes); + Sign(hashName, key, data, destination); + + return hashSizeInBytes; + } + + private static unsafe void Sign(SimpleDigest hashName, ReadOnlySpan key, ReadOnlySpan data, Span destination) + { fixed (byte* k = key) fixed (byte* src = data) fixed (byte* dest = destination) { int res = Interop.BrowserCrypto.Sign(hashName, k, key.Length, src, data.Length, dest, destination.Length); - Debug.Assert(res != 0); + if (res != 0) + throw new CryptographicException(SR.Format(SR.Unknown_SubtleCrypto_Error, res)); } - - return hashSizeInBytes; } public override int HashSizeInBytes => _hashSizeInBytes; diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SHAHashProvider.Browser.Native.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SHAHashProvider.Browser.Native.cs index 6c5f70489affa..b2b7c0f032ca8 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SHAHashProvider.Browser.Native.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SHAHashProvider.Browser.Native.cs @@ -40,40 +40,34 @@ public override int GetCurrentHash(Span destination) { Debug.Assert(destination.Length >= _hashSizeInBytes); - byte[] srcArray = Array.Empty(); - int srcLength = 0; - if (_buffer != null) - { - srcArray = _buffer.GetBuffer(); - srcLength = (int)_buffer.Length; - } + ReadOnlySpan source = _buffer != null ? + new ReadOnlySpan(_buffer.GetBuffer(), 0, (int)_buffer.Length) : + default; - unsafe - { - fixed (byte* src = srcArray) - fixed (byte* dest = destination) - { - int res = Interop.BrowserCrypto.SimpleDigestHash(_impl, src, srcLength, dest, destination.Length); - Debug.Assert(res != 0); - } - } + SimpleDigestHash(_impl, source, destination); return _hashSizeInBytes; } - public static unsafe int HashOneShot(string hashAlgorithmId, ReadOnlySpan data, Span destination) + public static int HashOneShot(string hashAlgorithmId, ReadOnlySpan data, Span destination) { (SimpleDigest impl, int hashSizeInBytes) = HashAlgorithmToPal(hashAlgorithmId); Debug.Assert(destination.Length >= hashSizeInBytes); + SimpleDigestHash(impl, data, destination); + + return hashSizeInBytes; + } + + private static unsafe void SimpleDigestHash(SimpleDigest hashName, ReadOnlySpan data, Span destination) + { fixed (byte* src = data) fixed (byte* dest = destination) { - int res = Interop.BrowserCrypto.SimpleDigestHash(impl, src, data.Length, dest, destination.Length); - Debug.Assert(res != 0); + int res = Interop.BrowserCrypto.SimpleDigestHash(hashName, src, data.Length, dest, destination.Length); + if (res != 0) + throw new CryptographicException(SR.Format(SR.Unknown_SubtleCrypto_Error, res)); } - - return hashSizeInBytes; } public override int HashSizeInBytes => _hashSizeInBytes; diff --git a/src/mono/wasm/runtime/crypto-worker.ts b/src/mono/wasm/runtime/crypto-worker.ts index 674f1700d2272..d27e185ac6342 100644 --- a/src/mono/wasm/runtime/crypto-worker.ts +++ b/src/mono/wasm/runtime/crypto-worker.ts @@ -4,6 +4,13 @@ import { Module } from "./imports"; import { mono_assert } from "./types"; +class OperationFailedError extends Error { } + +const ERR_ARGS = -1; +const ERR_WORKER_FAILED = -2; +const ERR_OP_FAILED = -3; +const ERR_UNKNOWN = -100; + let mono_wasm_crypto: { channel: LibraryChannel worker: Worker @@ -14,28 +21,27 @@ export function dotnet_browser_can_use_subtle_crypto_impl(): number { } export function dotnet_browser_simple_digest_hash(ver: number, input_buffer: number, input_len: number, output_buffer: number, output_len: number): number { - mono_assert(!!mono_wasm_crypto, "subtle crypto not initialized"); - const msg = { func: "digest", type: ver, data: Array.from(Module.HEAPU8.subarray(input_buffer, input_buffer + input_len)) }; - const response = mono_wasm_crypto.channel.send_msg(JSON.stringify(msg)); - const digest = JSON.parse(response); - if (digest.length > output_len) { - console.info("call_digest: about to throw!"); - throw "DIGEST HASH: Digest length exceeds output length: " + digest.length + " > " + output_len; + const result = _send_msg_worker(msg); + if (typeof result === "number") { + return result; } - Module.HEAPU8.set(digest, output_buffer); - return 1; + if (result.length > output_len) { + console.error("DIGEST HASH: Digest length exceeds output length: " + result.length + " > " + output_len); + return ERR_ARGS; + } + + Module.HEAPU8.set(result, output_buffer); + return 0; } export function dotnet_browser_sign(hashAlgorithm: number, key_buffer: number, key_len: number, input_buffer: number, input_len: number, output_buffer: number, output_len: number): number { - mono_assert(!!mono_wasm_crypto, "subtle crypto not initialized"); - const msg = { func: "sign", type: hashAlgorithm, @@ -43,22 +49,23 @@ export function dotnet_browser_sign(hashAlgorithm: number, key_buffer: number, k data: Array.from(Module.HEAPU8.subarray(input_buffer, input_buffer + input_len)) }; - const response = mono_wasm_crypto.channel.send_msg(JSON.stringify(msg)); - const signResult = JSON.parse(response); - if (signResult.length > output_len) { - console.info("dotnet_browser_sign: about to throw!"); - throw "SIGN HASH: Sign length exceeds output length: " + signResult.length + " > " + output_len; + const result = _send_msg_worker(msg); + if (typeof result === "number") { + return result; } - Module.HEAPU8.set(signResult, output_buffer); - return 1; + if (result.length > output_len) { + console.error("SIGN HASH: Sign length exceeds output length: " + result.length + " > " + output_len); + return ERR_ARGS; + } + + Module.HEAPU8.set(result, output_buffer); + return 0; } const AesBlockSizeBytes = 16; // 128 bits export function dotnet_browser_encrypt_decrypt(isEncrypting: boolean, key_buffer: number, key_len: number, iv_buffer: number, iv_len: number, input_buffer: number, input_len: number, output_buffer: number, output_len: number): number { - mono_assert(!!mono_wasm_crypto, "subtle crypto not initialized"); - if (input_len <= 0 || input_len % AesBlockSizeBytes !== 0) { throw "ENCRYPT DECRYPT: data was not a full block: " + input_len; } @@ -71,12 +78,14 @@ export function dotnet_browser_encrypt_decrypt(isEncrypting: boolean, key_buffer data: Array.from(Module.HEAPU8.subarray(input_buffer, input_buffer + input_len)) }; - const response = mono_wasm_crypto.channel.send_msg(JSON.stringify(msg)); - const result = JSON.parse(response); + const result = _send_msg_worker(msg); + if (typeof result === "number") { + return result; + } if (result.length > output_len) { - console.info("dotnet_browser_encrypt_decrypt: about to throw!"); - throw "ENCRYPT DECRYPT: Encrypt/Decrypt length exceeds output length: " + result.length + " > " + output_len; + console.error("ENCRYPT DECRYPT: Encrypt/Decrypt length exceeds output length: " + result.length + " > " + output_len); + return ERR_ARGS; } Module.HEAPU8.set(result, output_buffer); @@ -108,6 +117,33 @@ export function init_crypto(): void { } } +function _send_msg_worker(msg: any): any { + mono_assert(!!mono_wasm_crypto, "subtle crypto not initialized"); + + try { + const response = mono_wasm_crypto.channel.send_msg(JSON.stringify(msg)); + const responseJson = JSON.parse(response); + + if (responseJson.error !== undefined) { + console.error(`Worker failed with: ${responseJson.error}`); + if (responseJson.error_type == "ArgumentsError") + return ERR_ARGS; + if (responseJson.error_type == "WorkerFailedError") + return ERR_WORKER_FAILED; + + return ERR_UNKNOWN; + } + + return responseJson.result; + } catch (err) { + if (err instanceof Error && err.stack !== undefined) + console.error(`${err.stack}`); + else + console.error(`_send_msg_worker failed: ${err}`); + return ERR_OP_FAILED; + } +} + class LibraryChannel { private msg_char_len: number; private comm_buf: SharedArrayBuffer; @@ -133,6 +169,8 @@ class LibraryChannel { private get STATE_REQ_P(): number { return 3; } // Request has multiple parts private get STATE_RESP_P(): number { return 4; } // Response has multiple parts private get STATE_AWAIT(): number { return 5; } // Awaiting the next part + private get STATE_REQ_FAILED(): number { return 6; } // The Request failed + private get STATE_RESET(): number { return 7; } // Reset to a known state private constructor(msg_char_len: number) { this.msg_char_len = msg_char_len; @@ -156,21 +194,50 @@ class LibraryChannel { public get_comm_buffer(): SharedArrayBuffer { return this.comm_buf; } public send_msg(msg: string): string { - if (Atomics.load(this.comm, this.STATE_IDX) !== this.STATE_IDLE) { - throw "OWNER: Invalid sync communication channel state. " + Atomics.load(this.comm, this.STATE_IDX); + try { + let state = Atomics.load(this.comm, this.STATE_IDX); + if (state !== this.STATE_IDLE) + console.log(`send_msg, waiting for idle now, ${state}`); + state = this.wait_for_state(pstate => pstate == this.STATE_IDLE, "waiting"); + + this.send_request(msg); + return this.read_response(); + } catch (err) { + this.reset(LibraryChannel._stringify_err(err)); + throw err; + } + finally { + const state = Atomics.load(this.comm, this.STATE_IDX); + if (state !== this.STATE_IDLE) + console.log(`state at end of send_msg: ${state}`); } - this.send_request(msg); - return this.read_response(); } public shutdown(): void { - if (Atomics.load(this.comm, this.STATE_IDX) !== this.STATE_IDLE) { - throw "OWNER: Invalid sync communication channel state. " + Atomics.load(this.comm, this.STATE_IDX); - } + console.debug("Shutting down crypto"); + const state = Atomics.load(this.comm, this.STATE_IDX); + if (state !== this.STATE_IDLE) + throw new Error(`OWNER: Invalid sync communication channel state: ${state}`); // Notify webworker Atomics.store(this.comm, this.MSG_SIZE_IDX, 0); - Atomics.store(this.comm, this.STATE_IDX, this.STATE_SHUTDOWN); + this._change_state_locked(this.STATE_SHUTDOWN); + Atomics.notify(this.comm, this.STATE_IDX); + } + + private reset(reason: string): void { + console.debug(`reset: ${reason}`); + const state = Atomics.load(this.comm, this.STATE_IDX); + if (state === this.STATE_SHUTDOWN) + return; + + if (state === this.STATE_RESET || state === this.STATE_IDLE) { + console.debug(`state is already RESET or idle: ${state}`); + return; + } + + Atomics.store(this.comm, this.MSG_SIZE_IDX, 0); + this._change_state_locked(this.STATE_RESET); Atomics.notify(this.comm, this.STATE_IDX); } @@ -182,20 +249,22 @@ class LibraryChannel { for (; ;) { this.acquire_lock(); - // Write the message and return how much was written. - const wrote = this.write_to_msg(msg, msg_written, msg_len); - msg_written += wrote; + try { + // Write the message and return how much was written. + const wrote = this.write_to_msg(msg, msg_written, msg_len); + msg_written += wrote; - // Indicate how much was written to the this.msg buffer. - Atomics.store(this.comm, this.MSG_SIZE_IDX, wrote); + // Indicate how much was written to the this.msg buffer. + Atomics.store(this.comm, this.MSG_SIZE_IDX, wrote); - // Indicate if this was the whole message or part of it. - state = msg_written === msg_len ? this.STATE_REQ : this.STATE_REQ_P; + // Indicate if this was the whole message or part of it. + state = msg_written === msg_len ? this.STATE_REQ : this.STATE_REQ_P; - // Notify webworker - Atomics.store(this.comm, this.STATE_IDX, state); - - this.release_lock(); + // Notify webworker + this._change_state_locked(state); + } finally { + this.release_lock(); + } Atomics.notify(this.comm, this.STATE_IDX); @@ -203,11 +272,7 @@ class LibraryChannel { if (state === this.STATE_REQ) break; - // Wait for the worker to be ready for the next part. - // - Atomics.wait() is not permissible on the main thread. - do { - state = Atomics.load(this.comm, this.STATE_IDX); - } while (state !== this.STATE_AWAIT); + this.wait_for_state(state => state == this.STATE_AWAIT, "send_request"); } } @@ -223,45 +288,61 @@ class LibraryChannel { } private read_response(): string { - let state; let response = ""; for (; ;) { - // Wait for webworker response. - // - Atomics.wait() is not permissible on the main thread. - do { - state = Atomics.load(this.comm, this.STATE_IDX); - } while (state !== this.STATE_RESP && state !== this.STATE_RESP_P); - + const state = this.wait_for_state(state => state == this.STATE_RESP || state == this.STATE_RESP_P, "read_response"); this.acquire_lock(); - const size_to_read = Atomics.load(this.comm, this.MSG_SIZE_IDX); + try { + const size_to_read = Atomics.load(this.comm, this.MSG_SIZE_IDX); + + // Append the latest part of the message. + response += this.read_from_msg(0, size_to_read); - // Append the latest part of the message. - response += this.read_from_msg(0, size_to_read); + // The response is complete. + if (state === this.STATE_RESP) { + Atomics.store(this.comm, this.MSG_SIZE_IDX, 0); + break; + } - // The response is complete. - if (state === this.STATE_RESP) { + // Reset the size and transition to await state. + Atomics.store(this.comm, this.MSG_SIZE_IDX, 0); + this._change_state_locked(this.STATE_AWAIT); + } finally { this.release_lock(); - break; } - - // Reset the size and transition to await state. - Atomics.store(this.comm, this.MSG_SIZE_IDX, 0); - Atomics.store(this.comm, this.STATE_IDX, this.STATE_AWAIT); - - this.release_lock(); - Atomics.notify(this.comm, this.STATE_IDX); } // Reset the communication channel's state and let the // webworker know we are done. - Atomics.store(this.comm, this.STATE_IDX, this.STATE_IDLE); + this._change_state_locked(this.STATE_IDLE); Atomics.notify(this.comm, this.STATE_IDX); return response; } + private _change_state_locked(newState: number): void { + Atomics.store(this.comm, this.STATE_IDX, newState); + } + + private wait_for_state(is_ready: (state: number) => boolean, msg: string): number { + // Wait for webworker + // - Atomics.wait() is not permissible on the main thread. + for (; ;) { + const lock_state = Atomics.load(this.comm, this.LOCK_IDX); + if (lock_state !== this.LOCK_UNLOCKED) + continue; + + const state = Atomics.load(this.comm, this.STATE_IDX); + if (state == this.STATE_REQ_FAILED) + throw new OperationFailedError(`Worker failed during ${msg} with state=${state}`); + + if (is_ready(state)) + return state; + } + } + private read_from_msg(begin: number, end: number): string { const slicedMessage: number[] = []; this.msg.slice(begin, end).forEach((value, index) => slicedMessage[index] = value); @@ -269,18 +350,29 @@ class LibraryChannel { } private acquire_lock() { - while (Atomics.compareExchange(this.comm, this.LOCK_IDX, this.LOCK_UNLOCKED, this.LOCK_OWNED) !== this.LOCK_UNLOCKED) { - // empty + for (; ;) { + const lock_state = Atomics.compareExchange(this.comm, this.LOCK_IDX, this.LOCK_UNLOCKED, this.LOCK_OWNED); + + if (lock_state === this.LOCK_UNLOCKED) { + const state = Atomics.load(this.comm, this.STATE_IDX); + if (state === this.STATE_REQ_FAILED) + throw new OperationFailedError("Worker failed"); + return; + } } } private release_lock() { const result = Atomics.compareExchange(this.comm, this.LOCK_IDX, this.LOCK_OWNED, this.LOCK_UNLOCKED); if (result !== this.LOCK_OWNED) { - throw "CRYPTO: LibraryChannel tried to release a lock that wasn't acquired: " + result; + throw new Error("CRYPTO: LibraryChannel tried to release a lock that wasn't acquired: " + result); } } + private static _stringify_err(err: any) { + return (err instanceof Error && err.stack !== undefined) ? err.stack : err; + } + public static create(msg_char_len: number): LibraryChannel { if (msg_char_len === undefined) { msg_char_len = 1024; // Default size is arbitrary but is in 'char' units (i.e. UTF-16 code points). diff --git a/src/mono/wasm/runtime/debug.ts b/src/mono/wasm/runtime/debug.ts index 0b13121b27adc..5ae913e3c75d8 100644 --- a/src/mono/wasm/runtime/debug.ts +++ b/src/mono/wasm/runtime/debug.ts @@ -472,6 +472,75 @@ export function mono_wasm_trace_logger(log_domain_ptr: CharPtr, log_level_ptr: C } } +export function setup_proxy_console(id: string, originalConsole: Console, origin: string): void { + function proxyConsoleMethod(prefix: string, func: any, asJson: boolean) { + return function (...args: any[]) { + try { + let payload = args[0]; + if (payload === undefined) payload = "undefined"; + else if (payload === null) payload = "null"; + else if (typeof payload === "function") payload = payload.toString(); + else if (typeof payload !== "string") { + try { + payload = JSON.stringify(payload); + } catch (e) { + payload = payload.toString(); + } + } + + if (typeof payload === "string") + payload = `[${id}] ${payload}`; + + if (asJson) { + func(JSON.stringify({ + method: prefix, + payload: payload, + arguments: args + })); + } else { + func([prefix + payload, ...args.slice(1)]); + } + } catch (err) { + originalConsole.error(`proxyConsole failed: ${err}`); + } + }; + } + + const originalConsoleObj : any = originalConsole; + const methods = ["debug", "trace", "warn", "info", "error"]; + for (const m of methods) { + if (typeof (originalConsoleObj[m]) !== "function") { + originalConsoleObj[m] = proxyConsoleMethod(`console.${m}: `, originalConsole.log, false); + } + } + + const consoleUrl = `${origin}/console`.replace("https://", "wss://").replace("http://", "ws://"); + + const consoleWebSocket = new WebSocket(consoleUrl); + consoleWebSocket.onopen = function () { + originalConsole.log(`browser: [${id}] Console websocket connected.`); + }; + consoleWebSocket.onerror = function (event) { + originalConsole.error(`[${id}] websocket error: ${event}`, event); + }; + consoleWebSocket.onclose = function (event) { + originalConsole.error(`[${id}] websocket closed: ${event}`, event); + }; + + const send = (msg: string) => { + if (consoleWebSocket.readyState === WebSocket.OPEN) { + consoleWebSocket.send(msg); + } + else { + originalConsole.log(msg); + } + }; + + // redirect output early, so that when emscripten starts it's already redirected + for (const m of ["log", ...methods]) + originalConsoleObj[m] = proxyConsoleMethod(`console.${m}`, send, true); +} + type CallDetails = { value: string } diff --git a/src/mono/wasm/runtime/workers/dotnet-crypto-worker.js b/src/mono/wasm/runtime/workers/dotnet-crypto-worker.js index f6d66c82da6d5..5fa89790c2283 100644 --- a/src/mono/wasm/runtime/workers/dotnet-crypto-worker.js +++ b/src/mono/wasm/runtime/workers/dotnet-crypto-worker.js @@ -5,6 +5,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +import { setup_proxy_console } from "../debug"; + +class FailedOrStoppedLoopError extends Error {} +class ArgumentsError extends Error {} +class WorkerFailedError extends Error {} + var ChannelWorker = { _impl: class { // LOCK states @@ -24,6 +30,8 @@ var ChannelWorker = { get STATE_REQ_P() { return 3; } // Request has multiple parts get STATE_RESP_P() { return 4; } // Response has multiple parts get STATE_AWAIT() { return 5; } // Awaiting the next part + get STATE_REQ_FAILED() { return 6; } // The Request failed + get STATE_RESET() { return 7; } // Reset to a known state // END ChannelOwner contract - shared constants. constructor(comm_buf, msg_buf, msg_char_len) { @@ -32,61 +40,102 @@ var ChannelWorker = { this.msg_char_len = msg_char_len; } - async await_request(async_call) { + async run_message_loop(async_op) { for (;;) { - // Wait for signal to perform operation - Atomics.wait(this.comm, this.STATE_IDX, this.STATE_IDLE); - - // Read in request - var req = this._read_request(); - if (req === this.STATE_SHUTDOWN) - break; - - var resp = null; try { - // Perform async action based on request - resp = await async_call(req); - } - catch (err) { - console.log("Request error: " + err); - resp = JSON.stringify(err); + // Wait for signal to perform operation + let state; + do { + this._wait(this.STATE_IDLE); + state = Atomics.load(this.comm, this.STATE_IDX); + } while (state !== this.STATE_REQ && state !== this.STATE_REQ_P && state !== this.STATE_SHUTDOWN && state !== this.STATE_REQ_FAILED && state !== this.STATE_RESET); + + this._throw_if_reset_or_shutdown(); + + // Read in request + var req = this._read_request(); + var resp = {}; + try { + // Perform async action based on request + resp.result = await async_op(req); + } + catch (err) { + resp.error_type = typeof err; + resp.error = _stringify_err(err); + console.error(`Request error: ${resp.error}. req was: ${req}`); + } + + // Send response + this._send_response(JSON.stringify(resp)); + } catch (err) { + if (err instanceof FailedOrStoppedLoopError) { + const state = Atomics.load(this.comm, this.STATE_IDX); + if (state === this.STATE_SHUTDOWN) + break; + if (state === this.STATE_RESET) + console.debug(`caller failed, reseting worker`); + } else { + console.error(`Worker failed to handle the request: ${_stringify_err(err)}`); + this._change_state_locked(this.STATE_REQ_FAILED); + Atomics.store(this.comm, this.LOCK_IDX, this.LOCK_UNLOCKED); + + console.debug(`set state to failed, now waiting to get RESET`); + Atomics.wait(this.comm, this.STATE_IDX, this.STATE_REQ_FAILED); + const state = Atomics.load(this.comm, this.STATE_IDX); + if (state !== this.STATE_RESET) { + throw new WorkerFailedError(`expected to RESET, but got ${state}`); + } + } + + Atomics.store(this.comm, this.MSG_SIZE_IDX, 0); + Atomics.store(this.comm, this.LOCK_IDX, this.LOCK_UNLOCKED); + this._change_state_locked(this.STATE_IDLE); } - // Send response - this._send_response(resp); + const state = Atomics.load(this.comm, this.STATE_IDX); + const lock_state = Atomics.load(this.comm, this.LOCK_IDX); + + if (state !== this.STATE_IDLE && state !== this.STATE_REQ && state !== this.STATE_REQ_P) + console.error(`-- state is not idle at the top of the loop: ${state}, and lock_state: ${lock_state}`); + if (lock_state !== this.LOCK_UNLOCKED && state !== this.STATE_REQ && state !== this.STATE_REQ_P && state !== this.STATE_IDLE) + console.error(`-- lock is not unlocked at the top of the loop: ${lock_state}, and state: ${state}`); } + + Atomics.store(this.comm, this.MSG_SIZE_IDX, 0); + this._change_state_locked(this.STATE_SHUTDOWN); + console.debug("******* run_message_loop ending"); } _read_request() { var request = ""; for (;;) { this._acquire_lock(); + try { + this._throw_if_reset_or_shutdown(); - // Get the current state and message size - var state = Atomics.load(this.comm, this.STATE_IDX); - var size_to_read = Atomics.load(this.comm, this.MSG_SIZE_IDX); + // Get the current state and message size + var state = Atomics.load(this.comm, this.STATE_IDX); + var size_to_read = Atomics.load(this.comm, this.MSG_SIZE_IDX); - // Append the latest part of the message. - request += this._read_from_msg(0, size_to_read); + // Append the latest part of the message. + request += this._read_from_msg(0, size_to_read); - // The request is complete. - if (state === this.STATE_REQ) { - this._release_lock(); - break; - } + // The request is complete. + if (state === this.STATE_REQ) { + break; + } - // Shutdown the worker. - if (state === this.STATE_SHUTDOWN) { + // Shutdown the worker. + this._throw_if_reset_or_shutdown(); + + // Reset the size and transition to await state. + Atomics.store(this.comm, this.MSG_SIZE_IDX, 0); + this._change_state_locked(this.STATE_AWAIT); + } finally { this._release_lock(); - return this.STATE_SHUTDOWN; } - // Reset the size and transition to await state. - Atomics.store(this.comm, this.MSG_SIZE_IDX, 0); - Atomics.store(this.comm, this.STATE_IDX, this.STATE_AWAIT); - this._release_lock(); - - Atomics.wait(this.comm, this.STATE_IDX, this.STATE_AWAIT); + this._wait(this.STATE_AWAIT); } return request; @@ -98,7 +147,7 @@ var ChannelWorker = { _send_response(msg) { if (Atomics.load(this.comm, this.STATE_IDX) !== this.STATE_REQ) - throw "WORKER: Invalid sync communication channel state."; + throw new WorkerFailedError(`WORKER: Invalid sync communication channel state.`); var state; // State machine variable const msg_len = msg.length; @@ -107,24 +156,26 @@ var ChannelWorker = { for (;;) { this._acquire_lock(); - // Write the message and return how much was written. - var wrote = this._write_to_msg(msg, msg_written, msg_len); - msg_written += wrote; - - // Indicate how much was written to the this.msg buffer. - Atomics.store(this.comm, this.MSG_SIZE_IDX, wrote); + try { + // Write the message and return how much was written. + var wrote = this._write_to_msg(msg, msg_written, msg_len); + msg_written += wrote; - // Indicate if this was the whole message or part of it. - state = msg_written === msg_len ? this.STATE_RESP : this.STATE_RESP_P; + // Indicate how much was written to the this.msg buffer. + Atomics.store(this.comm, this.MSG_SIZE_IDX, wrote); - // Update the state - Atomics.store(this.comm, this.STATE_IDX, state); + // Indicate if this was the whole message or part of it. + state = msg_written === msg_len ? this.STATE_RESP : this.STATE_RESP_P; - this._release_lock(); + // Update the state + this._change_state_locked(state); + } finally { + this._release_lock(); + } // Wait for the transition to know the main thread has // received the response by moving onto a new state. - Atomics.wait(this.comm, this.STATE_IDX, state); + this._wait(state); // Done sending response. if (state === this.STATE_RESP) @@ -143,18 +194,37 @@ var ChannelWorker = { return ii - start; } + _change_state_locked(newState) { + Atomics.store(this.comm, this.STATE_IDX, newState); + } + _acquire_lock() { - while (Atomics.compareExchange(this.comm, this.LOCK_IDX, this.LOCK_UNLOCKED, this.LOCK_OWNED) !== this.LOCK_UNLOCKED) { - // empty + for (;;) { + const lockState = Atomics.compareExchange(this.comm, this.LOCK_IDX, this.LOCK_UNLOCKED, this.LOCK_OWNED); + this._throw_if_reset_or_shutdown(); + + if (lockState === this.LOCK_UNLOCKED) + return; } } _release_lock() { const result = Atomics.compareExchange(this.comm, this.LOCK_IDX, this.LOCK_OWNED, this.LOCK_UNLOCKED); if (result !== this.LOCK_OWNED) { - throw "CRYPTO: ChannelWorker tried to release a lock that wasn't acquired: " + result; + throw new WorkerFailedError("CRYPTO: ChannelWorker tried to release a lock that wasn't acquired: " + result); } } + + _wait(expected_state) { + Atomics.wait(this.comm, this.STATE_IDX, expected_state); + this._throw_if_reset_or_shutdown(); + } + + _throw_if_reset_or_shutdown() { + const state = Atomics.load(this.comm, this.STATE_IDX); + if (state === this.STATE_RESET || state === this.STATE_SHUTDOWN) + throw new FailedOrStoppedLoopError(); + } }, create: function (comm_buf, msg_buf, msg_char_len) { @@ -193,7 +263,7 @@ function get_hash_name(type) { case 2: return "SHA-384"; case 3: return "SHA-512"; default: - throw "CRYPTO: Unknown digest: " + type; + throw new ArgumentsError("CRYPTO: Unknown digest: " + type); } } @@ -245,7 +315,7 @@ async function decrypt(algorithm, cryptoKey, data) { ); const encryptedPaddingBlock = new Uint8Array(encryptedPaddingBlockResult); - for (var i = 0; i < encryptedPaddingBlock.length; i++) { + for (let i = 0; i < encryptedPaddingBlock.length; i++) { data.push(encryptedPaddingBlock[i]); } @@ -267,28 +337,31 @@ function importKey(key, algorithmName, keyUsage) { } // Operation to perform. -async function async_call(msg) { +async function handle_req_async(msg) { const req = JSON.parse(msg); if (req.func === "digest") { - const digestArr = await call_digest(req.type, new Uint8Array(req.data)); - return JSON.stringify(digestArr); + return await call_digest(req.type, new Uint8Array(req.data)); } else if (req.func === "sign") { - const signResult = await sign(req.type, new Uint8Array(req.key), new Uint8Array(req.data)); - return JSON.stringify(signResult); + return await sign(req.type, new Uint8Array(req.key), new Uint8Array(req.data)); } else if (req.func === "encrypt_decrypt") { - const signResult = await encrypt_decrypt(req.isEncrypting, req.key, req.iv, req.data); - return JSON.stringify(signResult); + return await encrypt_decrypt(req.isEncrypting, req.key, req.iv, req.data); } else { - throw "CRYPTO: Unknown request: " + req.func; + throw new ArgumentsError("CRYPTO: Unknown request: " + req.func); } } +function _stringify_err(err) { + return (err instanceof Error && err.stack !== undefined) ? err.stack : err; +} + var s_channel; +setup_proxy_console("crypto-worker", console, self.location.origin); + // Initialize WebWorker onmessage = function (p) { var data = p; @@ -296,5 +369,5 @@ onmessage = function (p) { data = p.data; } s_channel = ChannelWorker.create(data.comm_buf, data.msg_buf, data.msg_char_len); - s_channel.await_request(async_call); + s_channel.run_message_loop(handle_req_async); };