fix: back WASI random_get with crypto.getRandomValues #23

Merged
jorijnvdgraaf merged 1 commit from claude/issue-22 into master 2026-06-02 02:14:58 +02:00
Showing only changes of commit 08c28a46b7 - Show all commits

fix: back WASI random_get with crypto.getRandomValues
All checks were successful
CI / build-test-release (pull_request) Successful in 7m50s

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 <noreply@anthropic.com>
catbot 2026-06-02 00:09:08 +00:00

View file

@ -210,6 +210,8 @@ class Wasi {
// clock_res_get + clock_time_get touch this.instance.exports // clock_res_get + clock_time_get touch this.instance.exports
// .memory; the old stubs were no-ops and didn't need binding. // .memory; the old stubs were no-ops and didn't need binding.
"clock_res_get", "clock_time_get", "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); for (const name of m) this[name] = this[name].bind(this);
} }
@ -756,7 +758,25 @@ class Wasi {
path_filestat_set_times() { return 0; } path_filestat_set_times() { return 0; }
poll_oneoff() { return 0; } poll_oneoff() { return 0; }
sched_yield() { 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_accept() { return 0; }
sock_recv() { return 0; } sock_recv() { return 0; }
sock_send() { return 0; } sock_send() { return 0; }