fix(csp,upload): allow WASM in CSP + delta-worker liveness watchdog

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) <noreply@anthropic.com>
This commit is contained in:
DioCrafts
2026-06-21 01:46:48 +02:00
parent 6123843dd0
commit 5812257071
2 changed files with 27 additions and 1 deletions
@@ -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<typeof setTimeout>;
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<WorkerMsg>) => {
armStall(); // worker is alive — reset the liveness watchdog
const msg = event.data;
if (msg.type === 'progress') {
savedBytes = msg.reusedBytes;
+8 -1
View File
@@ -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);