1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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 });
}
});