fix(code): Workers 實部署修正 —— wasmfile+Module 載入 + tick-budget timeout(CF 實測)
leo21c 實部署發現兩個 CF 限制並修正:
1. CF 禁 runtime 從 bytes 編譯 wasm(WebAssembly.instantiate(bytes) 被 embedder 擋)
→ singlefile(base64) 內嵌不可用。改 wasmfile variant + `import wasm from './vendor/quickjs.wasm'`
(wrangler CompiledWasm rule 綁成預編 WebAssembly.Module),newVariant({wasmModule}) 注入。
sandbox 改 variant 注入制(setVariant):Worker 注 wrangler Module、Node 由 bytes 建 Module,
同一 production 路徑受測。vendor/quickjs.wasm 由 postinstall 自 node_modules 複製(gitignored)。
2. CF 凍結同步執行期 Date.now → wall-clock deadline 對純同步迴圈失效(撞 CF CPU 回 1102)。
改指令計數 interrupt(max_ticks,CF-safe),wall-clock 留 Node 保護。校準:cadence≈5000
指令/tick、真實 card 解析≈4 ticks、CF 門檻≈1000+ ticks → 預設 max_ticks=500。
有 body 的迴圈(含 100M 迴圈)皆乾淨回 TimeoutError;空體 while(true){} 仍由 CF CPU guard 容納。
Worker live: arcrun-code.leo21c.workers.dev(cypher-executor 以此 workers.dev 慣例位址呼叫)。
Node 單測 12/12 綠(wasmfile variant + 注入 Module,同 production 路徑)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJiLCRUU2o3aSpPEzVCt2o
This commit is contained in:
@@ -10,16 +10,30 @@
|
||||
// 要給的能力,只能由 host 明確、逐一注入 —— 目前唯一 curated builtin = 純函式 `sha256`,
|
||||
// 且以「純 JS 演算法字串 prelude」注入(不呼叫 host、不用 async Web Crypto,Node/Worker 皆決定性)。
|
||||
//
|
||||
// 封裝方式:quickjs-emscripten「singlefile」variant(wasm 內嵌為 base64、同步載入),
|
||||
// 這是 Cloudflare Workers 相容的 loading 路徑(不靠 fetch/fs 取 .wasm)。
|
||||
// 封裝方式:quickjs-emscripten「wasmfile」variant + 預編 WebAssembly.Module 注入(見下方 setVariant)。
|
||||
// CF Workers 禁 runtime 從 bytes 編譯 wasm,故不用 singlefile(base64);改由 build 時編好 Module。
|
||||
//
|
||||
// 對齊 arcrun 契約:io_model=stdin_stdout_json、no_network_syscall、no_filesystem_syscall。
|
||||
|
||||
import variant from '@jitl/quickjs-singlefile-mjs-release-sync';
|
||||
// wasm 載入以「variant 注入」制:CF Workers 禁止 runtime 從 bytes 編譯 wasm
|
||||
// (WebAssembly.instantiate(bytes) 被 embedder 擋),故必須用「已編好的 WebAssembly.Module」。
|
||||
// - Worker(index.ts):import 的 .wasm 由 wrangler 綁成 WebAssembly.Module → newVariant 注入。
|
||||
// - Node/vitest:由 .wasm bytes 建 new WebAssembly.Module(...) → 同一 newVariant 路徑注入。
|
||||
// 呼叫方必須在 runCode 前 setVariant()。sandbox 本身不綁定任何 variant(不 bundle 錯的 loader)。
|
||||
import { newQuickJSWASMModuleFromVariant } from 'quickjs-emscripten-core';
|
||||
|
||||
let _variant = null;
|
||||
/** 注入 quickjs variant(已含預編 WebAssembly.Module)。Worker 與 Node 各自注入自己的。 */
|
||||
export function setVariant(v) { _variant = v; _modulePromise = null; }
|
||||
|
||||
export const DEFAULT_LIMITS = {
|
||||
timeout_ms: 1000, // 執行牆鐘上限(interrupt handler 逐指令檢查 deadline)
|
||||
timeout_ms: 1000, // 牆鐘上限(Node/本機保護;CF 同步執行會凍結 Date.now,故非主保護)
|
||||
max_ticks: 500, // ★ 指令計數上限(CF 主保護):interrupt 回呼被叫超過此數即中止。
|
||||
// CF Workers 凍結同步 Date.now → 純同步無窮迴圈只能靠此計數中止。
|
||||
// 校準(leo21c 實測):interrupt cadence ≈ 5000 指令/tick;
|
||||
// 真實 card 解析 ≈ 4 ticks;CF CPU 1102 門檻 ≈ 1000+ ticks。
|
||||
// 500 ticks(≈ 2.5M 指令)= card 的 125× 餘裕、且穩在 CF 門檻下。
|
||||
// 需更多算力的 user code 可提高 limits.max_ticks(但別逼近 ~1000)。
|
||||
memory_bytes: 16 * 1024 * 1024, // QuickJS runtime 記憶體硬上限
|
||||
max_stack_bytes: 512 * 1024, // 遞迴/深堆疊上限
|
||||
max_output_bytes: 1024 * 1024, // stdout JSON 大小上限(防跑飛)
|
||||
@@ -103,7 +117,8 @@ function sha256(ascii) {
|
||||
|
||||
let _modulePromise = null;
|
||||
function getModule() {
|
||||
if (!_modulePromise) _modulePromise = newQuickJSWASMModuleFromVariant(variant);
|
||||
if (!_variant) throw new Error('sandbox variant not set — 呼叫 setVariant() 注入預編 WebAssembly.Module');
|
||||
if (!_modulePromise) _modulePromise = newQuickJSWASMModuleFromVariant(_variant);
|
||||
return _modulePromise;
|
||||
}
|
||||
|
||||
@@ -129,7 +144,11 @@ export async function runCode(code, input, opts = {}) {
|
||||
|
||||
const deadline = Date.now() + limits.timeout_ms;
|
||||
let interrupted = false;
|
||||
let ticks = 0;
|
||||
runtime.setInterruptHandler(() => {
|
||||
// 主保護(CF-safe):指令計數。CF 凍結同步 Date.now,純同步無窮迴圈只能靠此中止。
|
||||
if (++ticks > limits.max_ticks) { interrupted = true; return true; }
|
||||
// 次保護(Node/本機):牆鐘 deadline(CF 同步期間不會推進,故僅在有 I/O 或非 CF 生效)。
|
||||
if (Date.now() > deadline) { interrupted = true; return true; }
|
||||
return false;
|
||||
});
|
||||
@@ -154,7 +173,7 @@ export async function runCode(code, input, opts = {}) {
|
||||
if (evalResult.error) {
|
||||
const detail = ctx.dump(evalResult.error);
|
||||
evalResult.error.dispose();
|
||||
if (interrupted) return err(`execution timed out after ${limits.timeout_ms}ms`, 'TimeoutError');
|
||||
if (interrupted) return err(`execution aborted: exceeded time (${limits.timeout_ms}ms) or instruction budget (${limits.max_ticks} ticks)`, 'TimeoutError');
|
||||
const msg = typeof detail === 'object' && detail
|
||||
? `${detail.name || 'Error'}: ${detail.message || ''}`.trim()
|
||||
: String(detail);
|
||||
@@ -169,7 +188,7 @@ export async function runCode(code, input, opts = {}) {
|
||||
}
|
||||
return { success: true, data: JSON.parse(outJson) };
|
||||
} catch (e) {
|
||||
if (interrupted) return err(`execution timed out after ${limits.timeout_ms}ms`, 'TimeoutError');
|
||||
if (interrupted) return err(`execution aborted: exceeded time (${limits.timeout_ms}ms) or instruction budget (${limits.max_ticks} ticks)`, 'TimeoutError');
|
||||
const m = e instanceof Error ? e.message : String(e);
|
||||
if (/out of memory|memory/i.test(m)) return err('out of memory', 'ResourceError');
|
||||
return err(m, 'SandboxError');
|
||||
|
||||
Reference in New Issue
Block a user