import assert from "node:assert/strict"; import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { test } from "node:test"; import { createNodeContext } from "../server/index.js"; import { capMemory } from "./index.js"; // exports memory (min pages, no maximum, as rustc emits), alloc, and grow: memory.grow(2000), then // {"grew":false} when that returned -1, else {"grew":true}. The same bytes are in rust/host/src/wasm.rs. const grow = (min) => Uint8Array.from([ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x0c, 0x02, 0x60, 0x01, 0x7f, 0x01, 0x7f, 0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7e, 0x03, 0x03, 0x02, 0x00, 0x01, ...(min === 1 ? [0x05, 0x03, 0x01, 0x00, 0x01] : [0x05, 0x04, 0x01, 0x00, 0xd0, 0x0f]), 0x07, 0x19, 0x03, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x00, 0x00, 0x04, 0x67, 0x72, 0x6f, 0x77, 0x00, 0x01, 0x0a, 0x1f, 0x02, 0x05, 0x00, 0x41, 0xc0, 0x00, 0x0b, 0x17, 0x00, 0x41, 0xd0, 0x0f, 0x40, 0x00, 0x41, 0x7f, 0x46, 0x04, 0x7e, 0x42, 0x0e, 0x05, 0x42, 0x8d, 0x80, 0x80, 0x80, 0x80, 0x02, 0x0b, 0x0b, 0x0b, 0x23, 0x01, 0x00, 0x41, 0x00, 0x0b, 0x1d, 0x7b, 0x22, 0x67, 0x72, 0x65, 0x77, 0x22, 0x3a, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x7d, 0x00, 0x00, 0x7b, 0x22, 0x67, 0x72, 0x65, 0x77, 0x22, 0x3a, 0x74, 0x72, 0x75, 0x65, 0x7d, ]); test("capMemory: maximum set to the cap (or kept when lower), a minimum over it refused", () => { const limits = (bytes) => new WebAssembly.Instance(new WebAssembly.Module(bytes)).exports.memory; assert.equal(new WebAssembly.Instance(new WebAssembly.Module(grow(1))).exports.grow(0, 0), (16n << 32n) | 13n, "uncapped, V8 grows past 1024 pages"); const capped = capMemory(grow(1), 1024); assert.equal(capped.length, grow(1).length + 2); assert.equal(limits(capped).grow(1023), 1); assert.throws(() => limits(capped).grow(1024), RangeError); assert.equal(capMemory(capMemory(grow(1), 8), 1024)[31], 0x01, "flags: has max"); assert.throws(() => limits(capMemory(capMemory(grow(1), 8), 1024)).grow(8), RangeError, "a lower maximum is kept"); assert.throws(() => capMemory(grow(2000), 1024), { code: "WASM_TOO_LARGE" }); const twoMemories = Uint8Array.from([...grow(1).subarray(0, 27), 0x05, 0x05, 0x02, 0x00, 0x01, 0x00, 0x01, ...grow(1).subarray(32)]); assert.throws(() => capMemory(twoMemories, 1024), { code: "WASM_ABI_INVALID", message: "WASM_ABI_INVALID: a module may declare at most one memory" }); const memory64 = grow(1); memory64[30] = 0x04; assert.throws(() => capMemory(memory64, 1024), { code: "WASM_ABI_INVALID", message: "WASM_ABI_INVALID: memory64 is not supported" }); const noMemory = Uint8Array.from([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]); assert.equal(capMemory(noMemory, 1024), noMemory); }); test("wasm.run: memory.grow past the cap fails inside the call; rustc-built modules still load", async () => { const dir = mkdtempSync(join(tmpdir(), "wasm-cap-")); const ctx = createNodeContext({ id: "t", stateDir: dir }); try { assert.deepEqual(await ctx.wasm.run({ module: grow(1), entry: "grow", input: {} }), { grew: false }); await assert.rejects(ctx.wasm.run({ module: grow(2000), entry: "grow", input: {} }), { code: "WASM_TOO_LARGE", message: "WASM_TOO_LARGE: memory exceeds 1024 pages" }); // input past 1 MiB: refused by default, let through when the call's ioBytes is larger (it then traps: one page of memory) const big = { x: "x".repeat(1 << 20) }; await assert.rejects(ctx.wasm.run({ module: grow(1), entry: "grow", input: big }), { message: `WASM_TOO_LARGE: input exceeds ${1 << 20} bytes` }); await assert.rejects(ctx.wasm.run({ module: grow(1), entry: "grow", input: big, ioBytes: 2 << 20 }), { code: "WASM_TRAP" }); await assert.rejects(ctx.wasm.run({ module: grow(1), entry: "grow", input: { x: "x".repeat(17 << 20) }, ioBytes: 1 << 30 }), { message: `WASM_TOO_LARGE: input exceeds ${16 << 20} bytes` }); const release = new URL("../../../target/wasm32-unknown-unknown/release/", import.meta.url); const built = existsSync(release) ? readdirSync(release).filter((f) => f.endsWith(".wasm")) : []; for (const file of built) await WebAssembly.compile(capMemory(readFileSync(new URL(file, release)), 1024)); } finally { ctx.close(); rmSync(dir, { recursive: true, force: true }); } });