//------------------------------------------------------------------------------------------------------- // Copyright (C) Microsoft Corporation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. //------------------------------------------------------------------------------------------------------- let verbose = false; let doInvalid = true; const buf = readbuffer("binaries/api.wasm"); const imports = { test: { fn() {return 1;}, fn2() {return 2.14;}, memory: new WebAssembly.Memory({initial: 1, maximum: 1}), g1: 45, g2: -8, }, table: {"": new WebAssembly.Table({element: "anyfunc", initial: 30, maximum: 100})} }; function overrideImports(overrides) { return { test: Object.assign({}, imports.test, overrides), table: overrides.table ? {"":overrides.table} : imports.table }; } function objectToString(o, maxDepth = 5, depth = 0) { if (depth >= maxDepth) { return o.toString(); } if (Array.isArray(o)) { return `[${o.map(e => objectToString(e, maxDepth, depth + 1)).join(", ")}]`; } else if (typeof o === "object") { return `{${Object.keys(o).map(k => `"${k}":"${objectToString(o[k], maxDepth, depth + 1)}"`).join(", ")}}`; } return o.toString(); } function test(module, {exports} = {}) { if (module) { console.log("Testing module"); console.log("exports"); console.log(objectToString(WebAssembly.Module.exports(module))); console.log("imports"); console.log(objectToString(WebAssembly.Module.imports(module))); } if (exports) { console.log("Testing instance"); console.log(`f1: ${exports.f1()}`); console.log(`fn: ${exports.fn()}`); console.log(`fn2: ${exports.fn2()}`); console.log(`g1: ${exports.g1}`); console.log(`g2: ${exports.g2}`); console.log(`g3: ${exports.g3}`); } } async function testInvalidCases(tests) { if (!doInvalid) return; let i = 0; for (const testCase of tests) { if (verbose) { console.log(testCase); } try { await testCase(); console.log(`Test ${i++} failed. Should have thrown error`); } catch (e) { console.log(`Test ${i++} passed. Expected Error: ${e}`); } } } function createView(bytes) { const buffer = new ArrayBuffer(bytes.length); const view = new Uint8Array(buffer); for (let i = 0; i < bytes.length; ++i) { view[i] = bytes.charCodeAt(i); } return view; } const invalidBuffers = [ , "", "123", 4568, {}, {length: 15}, function() {}, new ArrayBuffer(), createView("\x00\x61\x73\x6d\x01\x00\x00\x00\x01\x85\x80\x80"), new Proxy(buf, {get(target, name) {return target[name];}}), ]; const invalidImports = [ , "", 123, function() {}, {wrongNamespace: imports.test, table: imports.table}, ]; const invalidModules = [ null, "123", {}, new Proxy({}, {}) ]; async function testValidate() { console.log("\nWebAssembly.validate tests"); await testInvalidCases([ // Invalid buffer source ...(invalidBuffers.map(b => () => { if (!WebAssembly.validate(b)) { throw new Error("Buffer source is the right type, but doesn't validate"); } })), ]); // Make sure empty buffer doesn't validate and doesn't throw const emptyBuffer = new ArrayBuffer(); console.log(`WebAssembly.validate(new ArrayBuffer()) = ${WebAssembly.validate(emptyBuffer)}`); console.log(`WebAssembly.validate(new Uint8Array(emptyBuffer)) = ${WebAssembly.validate(new Uint8Array(emptyBuffer))}`); console.log(`api.wasm valid: ${WebAssembly.validate(buf)}`); } async function testCompile() { console.log("\nWebAssembly.compile tests"); await testInvalidCases([ ...(invalidBuffers.map(b => () => WebAssembly.compile(b))), ]); const module = await WebAssembly.compile(buf); test(module); } async function testInstantiate(baseModule) { console.log("\nWebAssembly.instantiate tests"); await testInvalidCases([ // Invalid buffer source ...(invalidBuffers.map(b => () => WebAssembly.instantiate(b))), // Invalid Imports ...(invalidImports.map(i => () => WebAssembly.instantiate(buf, i))), ...(invalidImports.map(i => () => WebAssembly.instantiate(baseModule, i))), ]); const {module, instance} = await WebAssembly.instantiate(buf, imports); test(module, instance); const instance2 = await WebAssembly.instantiate(baseModule, imports); test(baseModule, instance2); } async function testModuleConstructor() { console.log("\nnew WebAssembly.Module tests"); await testInvalidCases([ () => WebAssembly.Module(), () => WebAssembly.Module(buf), ...(invalidBuffers.map(b => () => new WebAssembly.Module(b))), ]); const module = new WebAssembly.Module(buf); test(module); } async function testModuleApi() { console.log("\nWebAssembly.Module api tests"); console.log("\nWebAssembly.Module.exports invalid tests"); await testInvalidCases([ ...(invalidModules.map(b => () => WebAssembly.Module.exports(b))), ]); console.log("\nWebAssembly.Module.imports invalid tests"); await testInvalidCases([ ...(invalidModules.map(b => () => WebAssembly.Module.imports(b))), ]); } async function testModuleCustomSection(baseModule) { console.log("\nWebAssembly.Module.customSections tests"); await testInvalidCases([ ...(invalidModules.map(b => () => WebAssembly.Module.customSections(b))), () => WebAssembly.Module.customSections(baseModule, {toString() {throw new Error("Doesn't support toString");}}), () => WebAssembly.Module.customSections(baseModule, Symbol()), ]); const baseSections = WebAssembly.Module.customSections(baseModule, ""); if (!Array.isArray(baseSections) || baseSections.length !== 0) { console.log("Invalid result for WebAssembly.Module.customSections"); console.log(baseSections); } let passed = 0, failed = 0; function compare(module, customSectionName, expected) { const sections = WebAssembly.Module.customSections(module, customSectionName); if (sections.length !== expected.length) { console.log(`Invalid length. Got ${sections.length}, expected ${expected.length}. ${(new Error()).stack}`); ++failed; return; } let asExpected = true; for (let iSection = 0; iSection < sections.length; ++iSection) { const resBuffer = sections[iSection]; const expectedStr = expected[iSection]; const view = new Uint8Array(resBuffer); let resStr = ""; for (let i = 0; i < view.length; ++i) { resStr += String.fromCharCode(view[i]); } if (resStr !== expectedStr) { asExpected = false; console.log(`Invalid buffer result. Got ${resStr}, expected ${expectedStr}. ${(new Error()).stack}`); } } passed += asExpected; failed += !asExpected; } // See wasts/custom_section.wast for reference const module1 = new WebAssembly.Module(createView("\x00\x61\x73\x6d\x01\x00\x00\x00\x00\x24\x10\x61\x20\x63\x75\x73\x74\x6f\x6d\x20\x73\x65\x63\x74\x69\x6f\x6e\x74\x68\x69\x73\x20\x69\x73\x20\x74\x68\x65\x20\x70\x61\x79\x6c\x6f\x61\x64\x00\x20\x10\x61\x20\x63\x75\x73\x74\x6f\x6d\x20\x73\x65\x63\x74\x69\x6f\x6e\x74\x68\x69\x73\x20\x69\x73\x20\x70\x61\x79\x6c\x6f\x61\x64\x00\x11\x10\x61\x20\x63\x75\x73\x74\x6f\x6d\x20\x73\x65\x63\x74\x69\x6f\x6e\x00\x10\x00\x74\x68\x69\x73\x20\x69\x73\x20\x70\x61\x79\x6c\x6f\x61\x64\x00\x01\x00\x00\x24\x10\x00\x00\x63\x75\x73\x74\x6f\x6d\x20\x73\x65\x63\x74\x69\x6f\x00\x74\x68\x69\x73\x20\x69\x73\x20\x74\x68\x65\x20\x70\x61\x79\x6c\x6f\x61\x64")); compare(module1, "a custom section", [ "this is the payload", "this is payload", "", ]); compare(module1, "", [ "this is payload", "", ]); compare(module1, "\00\00custom sectio\00", [ "this is the payload", ]); const module2 = new WebAssembly.Module(createView("\x00\x61\x73\x6d\x01\x00\x00\x00\x00\x0e\x06\x63\x75\x73\x74\x6f\x6d\x70\x61\x79\x6c\x6f\x61\x64\x00\x0e\x06\x63\x75\x73\x74\x6f\x6d\x70\x61\x79\x6c\x6f\x61\x64\x01\x01\x00\x00\x0e\x06\x63\x75\x73\x74\x6f\x6d\x70\x61\x79\x6c\x6f\x61\x64\x00\x0e\x06\x63\x75\x73\x74\x6f\x6d\x70\x61\x79\x6c\x6f\x61\x64\x02\x01\x00\x00\x0e\x06\x63\x75\x73\x74\x6f\x6d\x70\x61\x79\x6c\x6f\x61\x64\x00\x0e\x06\x63\x75\x73\x74\x6f\x6d\x70\x61\x79\x6c\x6f\x61\x64\x03\x01\x00\x00\x0e\x06\x63\x75\x73\x74\x6f\x6d\x70\x61\x79\x6c\x6f\x61\x64\x00\x0e\x06\x63\x75\x73\x74\x6f\x6d\x70\x61\x79\x6c\x6f\x61\x64\x04\x01\x00\x00\x0e\x06\x63\x75\x73\x74\x6f\x6d\x70\x61\x79\x6c\x6f\x61\x64\x00\x0e\x06\x63\x75\x73\x74\x6f\x6d\x70\x61\x79\x6c\x6f\x61\x64\x05\x01\x00\x00\x0e\x06\x63\x75\x73\x74\x6f\x6d\x70\x61\x79\x6c\x6f\x61\x64\x00\x0e\x06\x63\x75\x73\x74\x6f\x6d\x70\x61\x79\x6c\x6f\x61\x64\x06\x01\x00\x00\x0e\x06\x63\x75\x73\x74\x6f\x6d\x70\x61\x79\x6c\x6f\x61\x64\x00\x0e\x06\x63\x75\x73\x74\x6f\x6d\x70\x61\x79\x6c\x6f\x61\x64\x07\x01\x00\x00\x0e\x06\x63\x75\x73\x74\x6f\x6d\x70\x61\x79\x6c\x6f\x61\x64\x00\x0e\x06\x63\x75\x73\x74\x6f\x6d\x70\x61\x79\x6c\x6f\x61\x64\x09\x01\x00\x00\x0e\x06\x63\x75\x73\x74\x6f\x6d\x70\x61\x79\x6c\x6f\x61\x64\x00\x0e\x06\x63\x75\x73\x74\x6f\x6d\x70\x61\x79\x6c\x6f\x61\x64\x0a\x01\x00\x00\x0e\x06\x63\x75\x73\x74\x6f\x6d\x70\x61\x79\x6c\x6f\x61\x64\x00\x0e\x06\x63\x75\x73\x74\x6f\x6d\x70\x61\x79\x6c\x6f\x61\x64\x0b\x01\x00\x00\x0e\x06\x63\x75\x73\x74\x6f\x6d\x70\x61\x79\x6c\x6f\x61\x64\x00\x0e\x06\x63\x75\x73\x74\x6f\x6d\x70\x61\x79\x6c\x6f\x61\x64")); compare(module2, "custom", new Proxy({length: 22}, { get: (target, name) => name in target ? target[name] : "payload" })); const module3 = new WebAssembly.Module(createView("\x00\x61\x73\x6d\x01\x00\x00\x00\x01\x07\x01\x60\x02\x7f\x7f\x01\x7f\x00\x1a\x06\x63\x75\x73\x74\x6f\x6d\x74\x68\x69\x73\x20\x69\x73\x20\x74\x68\x65\x20\x70\x61\x79\x6c\x6f\x61\x64\x03\x02\x01\x00\x07\x0a\x01\x06\x61\x64\x64\x54\x77\x6f\x00\x00\x0a\x09\x01\x07\x00\x20\x00\x20\x01\x6a\x0b\x00\x1b\x07\x63\x75\x73\x74\x6f\x6d\x32\x74\x68\x69\x73\x20\x69\x73\x20\x74\x68\x65\x20\x70\x61\x79\x6c\x6f\x61\x64")); compare(module3, "custom", ["this is the payload"]); compare(module3, "custom2", ["this is the payload"]); console.log(`${passed}/${passed + failed} tests passed`); } async function testInstanceConstructor(module) { console.log("\nnew WebAssembly.Instance tests"); const instance = new WebAssembly.Instance(module, imports); await testInvalidCases([ () => WebAssembly.Instance(), () => WebAssembly.Instance(module, imports), ...(invalidImports.map(i => () => new WebAssembly.Instance(module, i))), () => new WebAssembly.Instance(module, overrideImports({fn: 4})), () => new WebAssembly.Instance(module, overrideImports({fn2: instance.exports.fn /*wrong signature*/})), () => new WebAssembly.Instance(module, overrideImports({memory: 123})), () => new WebAssembly.Instance(module, overrideImports({memory: ""})), () => new WebAssembly.Instance(module, overrideImports({memory: {}})), () => new WebAssembly.Instance(module, overrideImports({table: 123})), () => new WebAssembly.Instance(module, overrideImports({table: "135"})), () => new WebAssembly.Instance(module, overrideImports({table: {}})), ]); test(null, instance); } async function testMemoryApi(baseModule) { console.log("\nWebAssembly.Memory tests"); await testInvalidCases([ () => WebAssembly.Memory({initial: 1, maximum: 2}), () => new WebAssembly.Memory(), () => new WebAssembly.Memory(0), () => new WebAssembly.Memory(""), () => Reflect.apply(WebAssembly.Memory.prototype.buffer, null, []), () => Reflect.apply(WebAssembly.Memory.prototype.buffer, {}, []), () => Reflect.apply(WebAssembly.Memory.prototype.buffer, baseModule, []), () => Reflect.apply(WebAssembly.Memory.prototype.grow, null, []), () => Reflect.apply(WebAssembly.Memory.prototype.grow, {}, []), () => Reflect.apply(WebAssembly.Memory.prototype.grow, baseModule, []), // todo:: test invalid memory imports, need to find the spec ]); const toHex = v => typeof v === "number" ? `0x${v.toString(16)}` : "undefined"; const memory = new WebAssembly.Memory({initial: 1, maximum: 2}); const {exports: {load}} = new WebAssembly.Instance(baseModule, overrideImports({memory})); const v1 = load(1); const v2 = load(5); const v3 = load(9) & 0xFFFF; console.log(`${toHex(v1)} == "0123"`); console.log(`${toHex(v2)} == "4567"`); console.log(`${toHex(v3)} == "89"`); memory.buffer = null; // make sure we can't modify the buffer let view = new Int32Array(memory.buffer); view[0] = 45; console.log(`heap32[0] = ${load(0)}`); function testOutOfBounds(index) { try { load(index); console.log("Should have trap with out of bounds error"); } catch (e) { if (e instanceof WebAssembly.RuntimeError) { console.log(`Correctly trap on heap access at ${index}`); } else { console.log(`Unexpected Error: ${e}`); } } } const pageSize = 64 * 1024; console.log(`memory.buffer.byteLength = ${memory.buffer.byteLength} == ${pageSize}`); { view[pageSize / 4 - 1] = 0x12345678; console.log(`view32[${pageSize / 4 - 1}] = ${toHex(view[pageSize / 4 - 1])}`); console.log(`view32[${pageSize / 4}] = ${toHex(view[pageSize / 4])}`); console.log(`heap[${pageSize - 4}] = ${toHex(load(pageSize - 4))}`); testOutOfBounds(pageSize - 3); testOutOfBounds(pageSize - 2); testOutOfBounds(pageSize - 1); testOutOfBounds(pageSize); } console.log("grow by 1 page"); memory.grow(1); console.log(`memory.buffer.byteLength = ${memory.buffer.byteLength} == 2 * ${pageSize}`); view = new Int32Array(memory.buffer); // buffer has been detached, reset the view { view[2 * pageSize / 4 - 1] = 0x87654321; console.log(`view32[${2 * pageSize / 4 - 1}] = ${toHex(view[2 * pageSize / 4 - 1])}`); console.log(`view32[${2 * pageSize / 4}] = ${toHex(view[2 * pageSize / 4])}`); console.log(`heap[${pageSize - 4}] = ${toHex(load(pageSize - 4))}`); console.log(`heap[${2 * pageSize - 4}] = ${toHex(load(2 * pageSize - 4))}`); testOutOfBounds(2 * pageSize - 3); testOutOfBounds(2 * pageSize - 2); testOutOfBounds(2 * pageSize - 1); testOutOfBounds(2 * pageSize); } try { memory.grow(1); console.log("Should have trap when growing past maximum"); } catch (e) { if (e instanceof RangeError) { console.log("Correctly trap when growing past maximum"); } else { console.log(`Unexpected Error: ${e}`); } } } async function testTableApi(baseModule) { //new WebAssembly.Table({element: "anyfunc", initial: 30, maximum: 100}) const table = new WebAssembly.Table({element: "anyfunc", initial: 30, maximum: 100}); console.log("\nWebAssembly.Table tests"); await testInvalidCases([ () => WebAssembly.Table({element: "anyfunc", initial: 30, maximum: 100}), () => new WebAssembly.Table(), () => new WebAssembly.Table(0), () => new WebAssembly.Table(""), () => new WebAssembly.Table({element: "notanyfunc", initial: 30, maximum: 100}), () => Reflect.apply(WebAssembly.Table.prototype.length, null, []), () => Reflect.apply(WebAssembly.Table.prototype.length, {}, []), () => Reflect.apply(WebAssembly.Table.prototype.length, baseModule, []), () => Reflect.apply(WebAssembly.Table.prototype.grow, null, []), () => Reflect.apply(WebAssembly.Table.prototype.grow, {}, []), () => Reflect.apply(WebAssembly.Table.prototype.grow, baseModule, []), () => Reflect.apply(WebAssembly.Table.prototype.get, null, [0]), () => Reflect.apply(WebAssembly.Table.prototype.get, {}, [0]), () => Reflect.apply(WebAssembly.Table.prototype.get, baseModule, [0]), () => table.get(30), () => table.get(100), () => Reflect.apply(WebAssembly.Table.prototype.get, table, [30]), () => Reflect.apply(WebAssembly.Table.prototype.get, table, [100]), ]); const {instance: {exports: {"f32.load": load}}} = await WebAssembly.instantiate(readbuffer("f32address.wasm"), {}); const {exports: {fn, call_i32, call_f32}} = new WebAssembly.Instance(baseModule, overrideImports({table, fn2: load})); const {exports: {fn: myFn}} = new WebAssembly.Instance(baseModule, overrideImports({fn: () => 123456})); await testInvalidCases([ () => table.set(30, fn), () => table.set(100, fn), () => table.set(0), () => table.set(0, {}), () => table.set(0, ""), () => table.set(0, 123), () => table.set(0, function(){}), () => table.set(0, () => 2), () => call_i32(0), () => call_f32(0), () => call_i32(29), () => call_i32(30), () => call_i32(2), () => call_f32(1), ]); console.log(`Current length: ${table.length}`); table.length = 456; console.log(`Length after attempt to modify : ${table.length}`); console.log(`Is element in table the same as the one exported: ${table.get(1) === fn}`); console.log(`Unset element should be null: ${table.get(0)}`); table.set(0, myFn); console.log(`call_i32(0): ${call_i32(0)}`); console.log(`call_i32(1): ${call_i32(1)}`); console.log(`call_f32(2): ${call_f32(2)}`); table.set(0, fn); console.log(`call_i32(0): ${call_i32(0)}`); table.set(29, myFn); table.grow(1); table.set(30, myFn); console.log(`call_i32(29): ${call_i32(29)}`); console.log(`call_i32(30): ${call_i32(30)}`); const table2 = new WebAssembly.Table({element: "anyfunc", initial: 0}); table.grow(0); try {table2.get(0); console.log("Failed. Unexpected successfull call to table2.get(0)");} catch (e) {} table.grow(1); table.set(0, myFn); console.log(`table2[0](): ${table.get(0)()}`); } async function main() { const args = WScript.Arguments; verbose = args.includes("-v"); doInvalid = !args.includes("-i"); const [start = 0, end = 99] = args .filter(a => !a.startsWith("-")) .map(a => parseInt(a)); const baseModule = new WebAssembly.Module(buf); const tests = [ /*0*/ testValidate, /*1*/ testCompile, /*2*/ testInstantiate, /*3*/ testModuleConstructor, /*4*/ testModuleApi, /*5*/ testModuleCustomSection, /*6*/ testInstanceConstructor, /*7*/ testMemoryApi, /*8*/ testTableApi, ]; for (let i = 0; i < tests.length; ++i) { if (i >= start && i <= end) { try { await tests[i](baseModule); } catch (e) { console.log("Unexpected error"); console.log(e.stack); } } } } main().then(() => console.log("done"), err => { console.log(err); console.log(err.stack); });