| import * as assert from '../assert.js'; |
| import Builder from '../Builder.js'; |
| |
| // FIXME: add the following: https://bugs.webkit.org/show_bug.cgi?id=171936 |
| // - add set() and shadow memory, this requires tracking when memory is shared |
| // - Support: empty, exported |
| // - Imported memory created through the JS API (both before and after instantiation, to cause recompilation) |
| // - recursive calls (randomly call other instance's exports, potentially exhausting stack) |
| // - Simplify code by allowing .Code().ExportFunction(...) in builder |
| |
| const tune = { |
| numRandomIterations: 64, |
| numRandomInvokes: 4, |
| maxInitial: 8, |
| maxMax: 8, |
| maxGrow: 8, |
| memoryGetCount: 1024, |
| }; |
| |
| const pageSize = 64 * 1024; // As determined by the WebAssembly specification. |
| |
| let logString = ""; |
| let log = what => logString += '\n' + what; |
| |
| let instances = []; |
| |
| const insert = (builder, importObject = undefined) => { |
| const location = (Math.random() * instances.length) | 0; |
| instances.splice(location, 0, new WebAssembly.Instance(new WebAssembly.Module(builder.WebAssembly().get()), importObject)); |
| } |
| const initialMaxObject = hasMax => { |
| const initial = (Math.random() * tune.maxInitial) | 0; |
| if (!hasMax) |
| return { initial: initial, max: undefined }; |
| const max = initial + ((Math.random() * tune.maxMax) | 0); |
| return { initial: initial, max: max }; |
| }; |
| const initialMaxObjectFrom = instance => { |
| const hasMax = instance.exports["max"] !== undefined; |
| const initial = (Math.random() * instance.exports.initial()) | 0; |
| if (!hasMax) |
| return { initial: initial }; |
| const max = Math.max(initial, instance.exports.max()) + ((Math.random() * tune.maxMax) | 0); |
| return { initial: initial, max: max }; |
| }; |
| |
| const action = { |
| '- delete': () => { |
| for (let count = 1 + (Math.random() * instances.length) | 0; instances.length && count; --count) { |
| const which = (Math.random() * instances.length) | 0; |
| log(` delete ${which} / ${instances.length}`); |
| instances.splice(which, 1); |
| } |
| }, |
| '^ invoke': () => { |
| if (!instances.length) { |
| log(` nothing to invoke`); |
| return; |
| } |
| for (let iterations = 0; iterations < tune.numRandomInvokes; ++iterations) { |
| const whichInstance = (Math.random() * instances.length) | 0; |
| const instance = instances[whichInstance]; |
| const whichExport = (Math.random() * Object.keys(instance.exports).length) | 0; |
| log(` instances[${whichInstance}].${Object.keys(instance.exports)[whichExport]}()`); |
| switch (Object.keys(instance.exports)[whichExport]) { |
| default: |
| throw new Error(`Implementation problem for key '${Object.keys(instance.exports)[whichExport]}'`); |
| case 'func': |
| assert.eq(instance.exports.func(), 42); |
| break; |
| case 'throws': { |
| let threw = false; |
| let address = 0; |
| if (instance.exports["current"]) { |
| // The instance has a memory. |
| const current = instance.exports.current(); |
| const max = instance.exports["max"] ? instance.exports.max() : (1 << 15); |
| address = pageSize * (current + ((Math.random() * (max - current)) | 0)); |
| } |
| // No memory in this instance. |
| try { instance.exports.throws(address); } |
| catch (e) { threw = true; } |
| assert.truthy(threw); |
| } break; |
| case 'get': { |
| const current = instance.exports.current(); |
| for (let access = tune.memoryGetCount; current && access; --access) { |
| const address = (Math.random() * (current * pageSize - 4)) | 0; |
| assert.eq(instance.exports.get(address), 0); |
| } |
| } break; |
| case 'current': |
| assert.le(instance.exports.initial(), instance.exports.current()); |
| break; |
| case 'initial': |
| assert.le(0, instance.exports.initial()); |
| break; |
| case 'max': |
| assert.le(instance.exports.initial(), instance.exports.max()); |
| assert.le(instance.exports.current(), instance.exports.max()); |
| break; |
| case 'memory': { |
| const current = instance.exports.current(); |
| assert.eq(instance.exports.memory.buffer.byteLength, current * pageSize); |
| for (let access = tune.memoryGetCount; current && access; --access) { |
| const address = (Math.random() * (current * pageSize - 4)) | 0; |
| assert.eq(instance.exports.get(address), 0); |
| } |
| } break; |
| case 'grow': { |
| const current = instance.exports.current(); |
| const by = (Math.random() * tune.maxGrow) | 0; |
| const hasMax = instance.exports["max"] !== undefined; |
| const result = instance.exports.grow(current + by); |
| log(` Grow from ${current} (max ${hasMax ? instance.exports.max() : "none"}) to ${current + by} returned ${result}, current now ${instance.exports.current()}`); |
| assert.le(current, instance.exports.current()); |
| if (result === -1) |
| assert.eq(current, instance.exports.current()); |
| if (hasMax) |
| assert.le(instance.exports.current(), instance.exports.max()); |
| } break; |
| } |
| } |
| }, |
| '+ memory: none': () => { |
| const builder = (new Builder()) |
| .Type().End() |
| .Function().End() |
| .Export().Function("func").Function("throws").End() |
| .Code() |
| .Function("func", { params: [], ret: "i32" }).I32Const(42).Return().End() |
| .Function("throws", { params: ["i32"] }).Unreachable().Return().End() |
| .End(); |
| insert(builder); |
| }, |
| '+ memory: empty, section': () => { |
| const builder = (new Builder()) |
| .Type().End() |
| .Function().End() |
| .Memory().InitialMaxPages(0, 0).End() |
| .Export().Function("func").Function("throws").Function("current").Function("initial").Function("grow").Function("max").End() |
| .Code() |
| .Function("func", { params: [], ret: "i32" }).I32Const(42).Return().End() |
| .Function("throws", { params: ["i32"] }).GetLocal(0).I32Load(2, 0).Return().End() |
| .Function("current", { params: [], ret: "i32" }).CurrentMemory(0).Return().End() |
| .Function("grow", { params: ["i32"], ret: "i32" }).GetLocal(0).GrowMemory(0).Return().End() |
| .Function("initial", { params: [], ret: "i32" }).I32Const(0).Return().End() |
| .Function("max", { params: [], ret: "i32" }).I32Const(0).Return().End() |
| .End(); |
| insert(builder); |
| }, |
| '+ memory: section': () => { |
| const initialMax = initialMaxObject(false); |
| const builder = (new Builder()) |
| .Type().End() |
| .Function().End() |
| .Memory().InitialMaxPages(initialMax.initial, initialMax.max).End() |
| .Export().Function("func").Function("throws").Function("get").Function("current").Function("initial").Function("grow").End() |
| .Code() |
| .Function("func", { params: [], ret: "i32" }).I32Const(42).Return().End() |
| .Function("throws", { params: ["i32"] }).GetLocal(0).I32Load(2, 0).Return().End() |
| .Function("get", { params: ["i32"], ret: "i32" }).GetLocal(0).I32Load(2, 0).Return().End() |
| .Function("current", { params: [], ret: "i32" }).CurrentMemory(0).Return().End() |
| .Function("grow", { params: ["i32"], ret: "i32" }).GetLocal(0).GrowMemory(0).Return().End() |
| .Function("initial", { params: [], ret: "i32" }).I32Const(initialMax.initial).Return().End() |
| .End(); |
| insert(builder); |
| }, |
| '+ memory: section, max': () => { |
| const initialMax = initialMaxObject(true); |
| const builder = (new Builder()) |
| .Type().End() |
| .Function().End() |
| .Memory().InitialMaxPages(initialMax.initial, initialMax.max).End() |
| .Export().Function("func").Function("throws").Function("get").Function("current").Function("initial").Function("grow").Function("max").End() |
| .Code() |
| .Function("func", { params: [], ret: "i32" }).I32Const(42).Return().End() |
| .Function("throws", { params: ["i32"] }).GetLocal(0).I32Load(2, 0).Return().End() |
| .Function("get", { params: ["i32"], ret: "i32" }).GetLocal(0).I32Load(2, 0).Return().End() |
| .Function("current", { params: [], ret: "i32" }).CurrentMemory(0).Return().End() |
| .Function("grow", { params: ["i32"], ret: "i32" }).GetLocal(0).GrowMemory(0).Return().End() |
| .Function("initial", { params: [], ret: "i32" }).I32Const(initialMax.initial).Return().End() |
| .Function("max", { params: [], ret: "i32" }).I32Const(initialMax.max).Return().End() |
| .End(); |
| insert(builder); |
| }, |
| '+ memory: section, exported': () => { |
| const initialMax = initialMaxObject(false); |
| const builder = (new Builder()) |
| .Type().End() |
| .Function().End() |
| .Memory().InitialMaxPages(initialMax.initial, initialMax.max).End() |
| .Export() |
| .Function("func").Function("throws").Function("get").Function("current").Function("initial") |
| .Memory("memory", 0) |
| .End() |
| .Code() |
| .Function("func", { params: [], ret: "i32" }).I32Const(42).Return().End() |
| .Function("throws", { params: ["i32"] }).GetLocal(0).I32Load(2, 0).Return().End() |
| .Function("get", { params: ["i32"], ret: "i32" }).GetLocal(0).I32Load(2, 0).Return().End() |
| .Function("current", { params: [], ret: "i32" }).CurrentMemory(0).Return().End() |
| .Function("grow", { params: ["i32"], ret: "i32" }).GetLocal(0).GrowMemory(0).Return().End() |
| .Function("initial", { params: [], ret: "i32" }).I32Const(initialMax.initial).Return().End() |
| .End(); |
| insert(builder); |
| }, |
| '+ memory: section, exported, max': () => { |
| const initialMax = initialMaxObject(true); |
| const builder = (new Builder()) |
| .Type().End() |
| .Function().End() |
| .Memory().InitialMaxPages(initialMax.initial, initialMax.max).End() |
| .Export() |
| .Function("func").Function("throws").Function("get").Function("current").Function("initial").Function("grow").Function("max") |
| .Memory("memory", 0) |
| .End() |
| .Code() |
| .Function("func", { params: [], ret: "i32" }).I32Const(42).Return().End() |
| .Function("throws", { params: ["i32"] }).GetLocal(0).I32Load(2, 0).Return().End() |
| .Function("get", { params: ["i32"], ret: "i32" }).GetLocal(0).I32Load(2, 0).Return().End() |
| .Function("current", { params: [], ret: "i32" }).CurrentMemory(0).Return().End() |
| .Function("grow", { params: ["i32"], ret: "i32" }).GetLocal(0).GrowMemory(0).Return().End() |
| .Function("initial", { params: [], ret: "i32" }).I32Const(initialMax.initial).Return().End() |
| .Function("max", { params: [], ret: "i32" }).I32Const(initialMax.max).Return().End() |
| .End(); |
| insert(builder); |
| }, |
| '+ memory: imported, exported': () => { |
| let importFrom; |
| for (let idx = 0; idx < instances.length; ++idx) |
| if (instances[idx].exports.memory) { |
| importFrom = instances[idx]; |
| break; |
| } |
| if (!importFrom) |
| return; // No memory could be imported. |
| const initialMax = initialMaxObjectFrom(importFrom); |
| if (importFrom.exports["max"] === undefined) { |
| const builder = (new Builder()) |
| .Type().End() |
| .Import().Memory("imp", "memory", initialMax).End() |
| .Function().End() |
| .Export() |
| .Function("func").Function("throws").Function("get").Function("current").Function("initial") |
| .Memory("memory", 0) |
| .End() |
| .Code() |
| .Function("func", { params: [], ret: "i32" }).I32Const(42).Return().End() |
| .Function("throws", { params: ["i32"] }).GetLocal(0).I32Load(2, 0).Return().End() |
| .Function("get", { params: ["i32"], ret: "i32" }).GetLocal(0).I32Load(2, 0).Return().End() |
| .Function("current", { params: [], ret: "i32" }).CurrentMemory(0).Return().End() |
| .Function("grow", { params: ["i32"], ret: "i32" }).GetLocal(0).GrowMemory(0).Return().End() |
| .Function("initial", { params: [], ret: "i32" }).I32Const(initialMax.initial).Return().End() |
| .End(); |
| insert(builder, { imp: { memory: importFrom.exports.memory } }); |
| } else { |
| const builder = (new Builder()) |
| .Type().End() |
| .Import().Memory("imp", "memory", initialMax).End() |
| .Function().End() |
| .Export() |
| .Function("func").Function("throws").Function("get").Function("current").Function("initial").Function("grow").Function("max") |
| .Memory("memory", 0) |
| .End() |
| .Code() |
| .Function("func", { params: [], ret: "i32" }).I32Const(42).Return().End() |
| .Function("throws", { params: ["i32"] }).GetLocal(0).I32Load(2, 0).Return().End() |
| .Function("get", { params: ["i32"], ret: "i32" }).GetLocal(0).I32Load(2, 0).Return().End() |
| .Function("current", { params: [], ret: "i32" }).CurrentMemory(0).Return().End() |
| .Function("grow", { params: ["i32"], ret: "i32" }).GetLocal(0).GrowMemory(0).Return().End() |
| .Function("initial", { params: [], ret: "i32" }).I32Const(initialMax.initial).Return().End() |
| .Function("max", { params: [], ret: "i32" }).I32Const(initialMax.max).Return().End() |
| .End(); |
| insert(builder, { imp: { memory: importFrom.exports.memory } }); |
| } |
| }, |
| }; |
| const numActions = Object.keys(action).length; |
| const performAction = () => { |
| const which = (Math.random() * numActions) | 0; |
| const key = Object.keys(action)[which]; |
| log(`${key}:`); |
| action[key](); |
| }; |
| |
| try { |
| for (let iteration = 0; iteration < tune.numRandomIterations; ++iteration) |
| performAction(); |
| log(`Finalizing:`); |
| // Finish by deleting the instances in a random order. |
| while (instances.length) |
| action["- delete"](); |
| } catch (e) { |
| // Ignore OOM while fuzzing. It can just happen. |
| if (e.message !== "Out of memory") { |
| print(`Failed: ${e}`); |
| print(logString); |
| throw e; |
| } |
| } |