From 08c28a46b73b12c0bcfd16d0b338f4408ba16f50 Mon Sep 17 00:00:00 2001 From: catbot Date: Tue, 2 Jun 2026 00:09:08 +0000 Subject: [PATCH] fix: back WASI random_get with crypto.getRandomValues The random_get import was stubbed to return success without writing any bytes, so every std::random_device user in wasm got all-zero "randomness". This collided WebRTC peer ids across browser tabs in 3DForts (Catcrafts/3DForts#50). Fill the target buffer from crypto.getRandomValues (a CSPRNG), chunking at 65536 bytes to stay under WebCrypto's per-call quota, and add random_get to the bind list since it now touches this.instance. Co-Authored-By: Claude Opus 4.8 --- wasi-runtime/runtime.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/wasi-runtime/runtime.js b/wasi-runtime/runtime.js index fd8445c..8f67141 100644 --- a/wasi-runtime/runtime.js +++ b/wasi-runtime/runtime.js @@ -210,6 +210,8 @@ class Wasi { // clock_res_get + clock_time_get touch this.instance.exports // .memory; the old stubs were no-ops and didn't need binding. "clock_res_get", "clock_time_get", + // random_get now writes into this.instance.exports.memory too. + "random_get", ]; for (const name of m) this[name] = this[name].bind(this); } @@ -756,7 +758,25 @@ class Wasi { path_filestat_set_times() { return 0; } poll_oneoff() { return 0; } sched_yield() { return 0; } - random_get() { return 0; } + // WASI random_get(buf, buf_len): fill buf_len bytes at *buf with + // high-quality randomness. The old stub returned success without + // writing anything, so the destination kept its prior contents (zero + // in practice) — every std::random_device user got all-zero + // "randomness". That collided WebRTC peer ids across tabs in 3DForts + // (Catcrafts/3DForts#50). Back it with crypto.getRandomValues, which + // is a CSPRNG. crypto.getRandomValues rejects views longer than 65536 + // bytes, so chunk for buffers above that bound. + random_get(ptr, len) { + const MAX = 65536; // QuotaExceededError above this per the WebCrypto spec + for (let off = 0; off < len; off += MAX) { + const chunk = Math.min(MAX, len - off); + // Re-view per chunk: the buffer can detach/grow between calls, + // and a fresh view also keeps each slice within the 65536 cap. + const view = new Uint8Array(this.instance.exports.memory.buffer, ptr + off, chunk); + crypto.getRandomValues(view); + } + return 0; + } sock_accept() { return 0; } sock_recv() { return 0; } sock_send() { return 0; } -- 2.54.0