From 581225707133dcbf2906aa0eca96bb04ba161acc Mon Sep 17 00:00:00 2001 From: DioCrafts Date: Sun, 21 Jun 2026 01:46:48 +0200 Subject: [PATCH] fix(csp,upload): allow WASM in CSP + delta-worker liveness watchdog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of folder uploads "freezing at ~95%": the global Content-Security- Policy `script-src` was `'self'` + inline-script hashes with NO `'wasm-unsafe-eval'`. Chromium therefore blocked `WebAssembly.instantiate` ("Wasm code generation disallowed by embedder"), so the vendored BLAKE3/FastCDC WASM threw on instantiation — both on the main thread (instant by-hash uploads and the batch dedup check) and inside the delta-upload worker. Every file then fell back to a plain byte upload, and the backend logs showed 0 check-batch / 0 negotiate calls. Large files (32 MB service logs) compounded it and the session token expired mid-upload, so the last handful failed. - web/mod.rs: add `'wasm-unsafe-eval'` to `script-src`. WASM-only, safe variant — does NOT enable `eval()`/`new Function()`. Restores instant uploads, delta (sub-file dedup), and the client hashing the idempotent re-upload relies on. - deltaUpload.ts: liveness watchdog on the delta worker. A healthy worker posts progress sub-second; if it goes silent for 20 s it is wedged (WASM init or chunking hung without throwing) — disable delta for this file AND every later one so they fall straight through to a plain upload instead of each burning the full 120 s+ delta timeout. Defense-in-depth so a broken WASM path can never again freeze an upload for minutes. cargo test: pass. npm run check: clean, 58 tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/lib/api/endpoints/deltaUpload.ts | 19 +++++++++++++++++++ src/interfaces/web/mod.rs | 9 ++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/api/endpoints/deltaUpload.ts b/frontend/src/lib/api/endpoints/deltaUpload.ts index 4290a443..af5b5a48 100644 --- a/frontend/src/lib/api/endpoints/deltaUpload.ts +++ b/frontend/src/lib/api/endpoints/deltaUpload.ts @@ -80,14 +80,33 @@ export function tryDeltaUpload( const timeoutMs = DELTA_TIMEOUT_BASE_MS + Math.ceil(sizeGB) * DELTA_TIMEOUT_PER_GB_MS; let savedBytes = 0; + let stallTimer: ReturnType; const settle = (answer: DeltaUploadAnswer | null) => { clearTimeout(timer); + clearTimeout(stallTimer); worker.terminate(); resolve(answer); }; const timer = setTimeout(() => settle(null), timeoutMs); + // Liveness watchdog: a healthy worker posts progress sub-second while it + // hashes and uploads. If it goes SILENT this long it is wedged (WASM init + // or chunking hung without throwing, emitting neither fallback nor error) + // — exactly what freezes a folder upload ~2 min per large file. Disable + // delta for this file AND every later one so they fall straight to a plain + // upload instead of each burning the full size-scaled delta timeout. + const STALL_MS = 20_000; + const armStall = () => { + clearTimeout(stallTimer); + stallTimer = setTimeout(() => { + usable = false; + settle(null); + }, STALL_MS); + }; + armStall(); + worker.onmessage = (event: MessageEvent) => { + armStall(); // worker is alive — reset the liveness watchdog const msg = event.data; if (msg.type === 'progress') { savedBytes = msg.reusedBytes; diff --git a/src/interfaces/web/mod.rs b/src/interfaces/web/mod.rs index a2668038..0667d590 100644 --- a/src/interfaces/web/mod.rs +++ b/src/interfaces/web/mod.rs @@ -112,7 +112,14 @@ pub fn content_security_policy(config: &AppConfig) -> String { ); } - let mut script_src = String::from("script-src 'self'"); + // `'wasm-unsafe-eval'` is required for WebAssembly compilation/instantiation + // under a strict CSP (Chromium blocks `WebAssembly.instantiate` otherwise with + // "Wasm code generation disallowed by embedder"). The frontend instantiates the + // vendored BLAKE3/FastCDC WASM both on the main thread (instant by-hash uploads) + // and inside the delta-upload worker; without this they throw and every large + // file silently falls back to a plain byte upload. It is the WASM-only, safe + // variant — it does NOT permit `eval()`/`new Function()` (no `'unsafe-eval'`). + let mut script_src = String::from("script-src 'self' 'wasm-unsafe-eval'"); for hash in &hashes { script_src.push(' '); script_src.push_str(hash);