From cfee145b265ddf39a1f5adba2f31eaae0544b5b4 Mon Sep 17 00:00:00 2001 From: Julian Seward Date: Fri, 8 Jun 2018 18:37:42 +0200 Subject: [PATCH] Bug 1467071 - Wasm: import embedding_limits "limits.js" test and fix any resulting failures. r=lth. The WebAssembly Specification, branch [1] (see also, more generally, comments in [2]), contains a new test, limits.js, to check whether the generally agreed embedding limits (numbers of functions, imports, etc) are observed. This bug is to import the test and fix any resulting breakage detected with it. [1] https://github.com/WebAssembly/spec/tree/embedding_limits [2] https://github.com/WebAssembly/spec/issues/607 * js/src/wasm/WasmBinaryConstants.h: - Added MaxTableMaximumLength as a counterpart to MaxTableInitialLength. - Split the constant group into two parts: spec-required, and those pertaining only to our own implementation. * js/src/wasm/WasmJS.cpp WasmTableObject::construct(): - Update GetLimits call with correct max size bound * js/src/wasm/WasmValidate.cpp DecodeTableLimits(): - Implement missing check for a Table's maximum size. * js/src/jit-test/tests/wasm/import-export.js: js/src/jit-test/tests/wasm/spec/jsapi.js: testing/web-platform/mozilla/tests/wasm/js/jsapi.js: - Update Table maximum size tests. All tests trying to make a Table with more than 10,000,000 entries now throw instead of succeeding. * js/src/jit-test/tests/wasm/spec/harness/wasm-module-builder.js: - Import minimal updates and bug fixes from [1], needed to make the new tests work. * js/src/jit-test/tests/wasm/spec/limits.js - New file. Derived from [1], with comments added to each test to show SM's compliance situation, and with two tests disabled. --- js/src/jit-test/tests/wasm/import-export.js | 4 +- .../wasm/spec/harness/wasm-module-builder.js | 39 +- js/src/jit-test/tests/wasm/spec/jsapi.js | 2 +- js/src/jit-test/tests/wasm/spec/limits.js | 380 ++++++++++++++++++ js/src/wasm/WasmBinaryConstants.h | 18 +- js/src/wasm/WasmJS.cpp | 2 +- js/src/wasm/WasmValidate.cpp | 7 +- .../mozilla/tests/wasm/js/jsapi.js | 2 +- 8 files changed, 430 insertions(+), 24 deletions(-) create mode 100644 js/src/jit-test/tests/wasm/spec/limits.js diff --git a/js/src/jit-test/tests/wasm/import-export.js b/js/src/jit-test/tests/wasm/import-export.js index 62a0846fe9d1..4916c1e35250 100644 --- a/js/src/jit-test/tests/wasm/import-export.js +++ b/js/src/jit-test/tests/wasm/import-export.js @@ -37,8 +37,8 @@ assertErrorMessage(() => new Memory({initial: 0, maximum: 65537}), RangeError, / assertErrorMessage(() => new Table({initial:2, maximum:1, element:"anyfunc"}), RangeError, /bad Table maximum size/); new Table({ initial: 10000000, element:"anyfunc" }); assertErrorMessage(() => new Table({initial:10000001, element:"anyfunc"}), RangeError, /bad Table initial size/); -new Table({ initial: 0, maximum: 2**32 - 1, element:"anyfunc" }); -assertErrorMessage(() => new Table({initial:0, maximum: 2**32, element:"anyfunc"}), RangeError, /bad Table maximum size/); +new Table({ initial: 0, maximum: 10000000, element:"anyfunc" }); +assertErrorMessage(() => new Table({initial:0, maximum: 10000001, element:"anyfunc"}), RangeError, /bad Table maximum size/); const m1 = new Module(wasmTextToBinary('(module (import "foo" "bar") (import "baz" "quux"))')); assertErrorMessage(() => new Instance(m1), TypeError, /second argument must be an object/); diff --git a/js/src/jit-test/tests/wasm/spec/harness/wasm-module-builder.js b/js/src/jit-test/tests/wasm/spec/harness/wasm-module-builder.js index 0a66f46f61a4..f7b1d04e081b 100644 --- a/js/src/jit-test/tests/wasm/spec/harness/wasm-module-builder.js +++ b/js/src/jit-test/tests/wasm/spec/harness/wasm-module-builder.js @@ -69,12 +69,14 @@ class Binary extends Array { // Emit section name. this.emit_u8(section_code); // Emit the section to a temporary buffer: its full length isn't know yet. - let section = new Binary; + const section = new Binary; content_generator(section); // Emit section length. this.emit_u32v(section.length); // Copy the temporary buffer. - this.push(...section); + for (const b of section) { + this.push(b); + } } } @@ -238,11 +240,22 @@ class WasmModuleBuilder { } appendToTable(array) { + for (let n of array) { + if (typeof n != 'number') + throw new Error('invalid table (entries have to be numbers): ' + array); + } return this.addFunctionTableInit(this.function_table.length, false, array); } + setFunctionTableBounds(min, max) { + this.function_table_length_min = min; + this.function_table_length_max = max; + return this; + } + setFunctionTableLength(length) { - this.function_table_length = length; + this.function_table_length_min = length; + this.function_table_length_max = length; return this; } @@ -320,25 +333,29 @@ class WasmModuleBuilder { } // Add function_table. - if (wasm.function_table_length > 0) { + if (wasm.function_table_length_min > 0) { if (debug) print("emitting table @ " + binary.length); binary.emit_section(kTableSectionCode, section => { section.emit_u8(1); // one table entry section.emit_u8(kWasmAnyFunctionTypeForm); - section.emit_u8(1); - section.emit_u32v(wasm.function_table_length); - section.emit_u32v(wasm.function_table_length); + const max = wasm.function_table_length_max; + const has_max = max !== undefined; + section.emit_u8(has_max ? kResizableMaximumFlag : 0); + section.emit_u32v(wasm.function_table_length_min); + if (has_max) section.emit_u32v(max); }); } // Add memory section - if (wasm.memory != undefined) { + if (wasm.memory !== undefined) { if (debug) print("emitting memory @ " + binary.length); binary.emit_section(kMemorySectionCode, section => { section.emit_u8(1); // one memory entry - section.emit_u32v(kResizableMaximumFlag); + const max = wasm.memory.max; + const has_max = max !== undefined; + section.emit_u32v(has_max ? kResizableMaximumFlag : 0); section.emit_u32v(wasm.memory.min); - section.emit_u32v(wasm.memory.max); + if (has_max) section.emit_u32v(max); }); } @@ -426,9 +443,9 @@ class WasmModuleBuilder { binary.emit_section(kElementSectionCode, section => { var inits = wasm.function_table_inits; section.emit_u32v(inits.length); - section.emit_u8(0); // table index for (let init of inits) { + section.emit_u8(0); // table index if (init.is_global) { section.emit_u8(kExprGetGlobal); } else { diff --git a/js/src/jit-test/tests/wasm/spec/jsapi.js b/js/src/jit-test/tests/wasm/spec/jsapi.js index f9e8a47678e9..efaf26711e06 100644 --- a/js/src/jit-test/tests/wasm/spec/jsapi.js +++ b/js/src/jit-test/tests/wasm/spec/jsapi.js @@ -557,7 +557,7 @@ test(() => { assert_equals(new Table({initial:1, element:"anyfunc"}) instanceof Table, true); assert_equals(new Table({initial:1.5, element:"anyfunc"}) instanceof Table, true); assert_equals(new Table({initial:1, maximum:1.5, element:"anyfunc"}) instanceof Table, true); - assert_equals(new Table({initial:1, maximum:Math.pow(2,32)-1, element:"anyfunc"}) instanceof Table, true); + assertThrows(() => new Table({initial:1, maximum:Math.pow(2,32)-1, element:"anyfunc"}), RangeError); }, "'WebAssembly.Table' constructor function"); test(() => { diff --git a/js/src/jit-test/tests/wasm/spec/limits.js b/js/src/jit-test/tests/wasm/spec/limits.js new file mode 100644 index 000000000000..18bd5dbb61fd --- /dev/null +++ b/js/src/jit-test/tests/wasm/spec/limits.js @@ -0,0 +1,380 @@ +// |jit-test| slow; + +/* + * Copyright 2018 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +let kJSEmbeddingMaxTypes = 1000000; +let kJSEmbeddingMaxFunctions = 1000000; +let kJSEmbeddingMaxImports = 100000; +let kJSEmbeddingMaxExports = 100000; +let kJSEmbeddingMaxGlobals = 1000000; +let kJSEmbeddingMaxDataSegments = 100000; + +let kJSEmbeddingMaxMemoryPages = 65536; +let kJSEmbeddingMaxModuleSize = 1024 * 1024 * 1024; // = 1 GiB +let kJSEmbeddingMaxFunctionSize = 7654321; +let kJSEmbeddingMaxFunctionLocals = 50000; +let kJSEmbeddingMaxFunctionParams = 1000; +let kJSEmbeddingMaxFunctionReturns = 1; +let kJSEmbeddingMaxTableSize = 10000000; +let kJSEmbeddingMaxTableEntries = 10000000; +let kJSEmbeddingMaxTables = 1; +let kJSEmbeddingMaxMemories = 1; + +function verbose(...args) { + if (false) print(...args); +} + +let kTestValidate = true; +let kTestSyncCompile = true; +let kTestAsyncCompile = true; + +//======================================================================= +// HARNESS SNIPPET, DO NOT COMMIT +//======================================================================= +const known_failures = { + // Enter failing tests like follows: + // "'WebAssembly.Instance.prototype.exports' accessor property": + // 'https://bugs.chromium.org/p/v8/issues/detail?id=5507', +}; + +let failures = []; +let unexpected_successes = []; + +let last_promise = new Promise((resolve, reject) => { resolve(); }); + +function test(func, description) { + let maybeErr; + try { func(); } + catch(e) { maybeErr = e; } + if (typeof maybeErr !== 'undefined') { + var known = ""; + if (known_failures[description]) { + known = " (known)"; + } + print(`${description}: FAIL${known}. ${maybeErr}`); + failures.push(description); + } else { + if (known_failures[description]) { + unexpected_successes.push(description); + } + print(`${description}: PASS.`); + } +} + +function promise_test(func, description) { + last_promise = last_promise.then(func) + .then(_ => { + if (known_failures[description]) { + unexpected_successes.push(description); + } + print(`${description}: PASS.`); + }) + .catch(err => { + var known = ""; + if (known_failures[description]) { + known = " (known)"; + } + print(`${description}: FAIL${known}. ${err}`); + failures.push(description); + }); +} +//======================================================================= + +function testLimit(name, min, limit, gen) { + print(""); + print(`==== Test ${name} limit = ${limit} ====`); + function run_validate(count) { + let expected = count <= limit; + verbose(` ${expected ? "(expect ok) " : "(expect fail)"} = ${count}...`); + + // TODO(titzer): builder is slow for large modules; make manual? + let builder = new WasmModuleBuilder(); + gen(builder, count); + let result = WebAssembly.validate(builder.toBuffer()); + + if (result != expected) { + let msg = `UNEXPECTED ${expected ? "FAIL" : "PASS"}: ${name} == ${count}`; + verbose(`=====> ${msg}`); + throw new Error(msg); + } + } + + function run_compile(count) { + let expected = count <= limit; + verbose(` ${expected ? "(expect ok) " : "(expect fail)"} = ${count}...`); + + // TODO(titzer): builder is slow for large modules; make manual? + let builder = new WasmModuleBuilder(); + gen(builder, count); + try { + let result = new WebAssembly.Module(builder.toBuffer()); + } catch (e) { + if (expected) { + let msg = `UNEXPECTED FAIL: ${name} == ${count} (${e})`; + verbose(`=====> ${msg}`); + throw new Error(msg); + } + return; + } + if (!expected) { + let msg = `UNEXPECTED PASS: ${name} == ${count}`; + verbose(`=====> ${msg}`); + throw new Error(msg); + } + } + + function run_async_compile(count) { + let expected = count <= limit; + verbose(` ${expected ? "(expect ok) " : "(expect fail)"} = ${count}...`); + + // TODO(titzer): builder is slow for large modules; make manual? + let builder = new WasmModuleBuilder(); + gen(builder, count); + let buffer = builder.toBuffer(); + WebAssembly.compile(buffer) + .then(result => { + if (!expected) { + let msg = `UNEXPECTED PASS: ${name} == ${count}`; + verbose(`=====> ${msg}`); + throw new Error(msg); + } + }) + .catch(err => { + if (expected) { + let msg = `UNEXPECTED FAIL: ${name} == ${count} (${e})`; + verbose(`=====> ${msg}`); + throw new Error(msg); + } + }) + } + + if (kTestValidate) { + print(""); + test(() => { + run_validate(min); + }, `Validate ${name} mininum`); + print(""); + test(() => { + run_validate(limit); + }, `Validate ${name} limit`); + print(""); + test(() => { + run_validate(limit+1); + }, `Validate ${name} over limit`); + } + + if (kTestSyncCompile) { + print(""); + test(() => { + run_compile(min); + }, `Compile ${name} mininum`); + print(""); + test(() => { + run_compile(limit); + }, `Compile ${name} limit`); + print(""); + test(() => { + run_compile(limit+1); + }, `Compile ${name} over limit`); + } + + if (kTestAsyncCompile) { + print(""); + promise_test(() => { + run_async_compile(min); + }, `Async compile ${name} mininum`); + print(""); + promise_test(() => { + run_async_compile(limit); + }, `Async compile ${name} limit`); + print(""); + promise_test(() => { + run_async_compile(limit+1); + }, `Async compile ${name} over limit`); + } +} + +// A little doodad to disable a test easily +let DISABLED = {testLimit: () => 0}; +let X = DISABLED; + +// passes +testLimit("types", 1, kJSEmbeddingMaxTypes, (builder, count) => { + for (let i = 0; i < count; i++) { + builder.addType(kSig_i_i); + } + }); + +// passes +testLimit("functions", 1, kJSEmbeddingMaxFunctions, (builder, count) => { + let type = builder.addType(kSig_v_v); + let body = [kExprEnd]; + for (let i = 0; i < count; i++) { + builder.addFunction(/*name=*/ undefined, type).addBody(body); + } + }); + +// passes +testLimit("imports", 1, kJSEmbeddingMaxImports, (builder, count) => { + let type = builder.addType(kSig_v_v); + for (let i = 0; i < count; i++) { + builder.addImport("", "", type); + } + }); + +// passes +testLimit("exports", 1, kJSEmbeddingMaxExports, (builder, count) => { + let type = builder.addType(kSig_v_v); + let f = builder.addFunction(/*name=*/ undefined, type); + f.addBody([kExprEnd]); + for (let i = 0; i < count; i++) { + builder.addExport("f" + i, f.index); + } + }); + +// passes +testLimit("globals", 1, kJSEmbeddingMaxGlobals, (builder, count) => { + for (let i = 0; i < count; i++) { + builder.addGlobal(kWasmI32, true); + } + }); + +// passes +testLimit("data segments", 1, kJSEmbeddingMaxDataSegments, (builder, count) => { + let data = []; + builder.addMemory(1, 1, false, false); + for (let i = 0; i < count; i++) { + builder.addDataSegment(0, data); + } + }); + +// fails +// jseward: we expect this to fail, because we support a max initial memory +// page count of 16384, whereas this expects an initial value of 63336 to be +// accepted. +X.testLimit("initial declared memory pages", 1, kJSEmbeddingMaxMemoryPages, + (builder, count) => { + builder.addMemory(count, undefined, false, false); + }); + +// passes +testLimit("maximum declared memory pages", 1, kJSEmbeddingMaxMemoryPages, + (builder, count) => { + builder.addMemory(1, count, false, false); + }); + +// fails +// jseward: we expect this to fail, because we support a max initial memory +// page count of 16384, whereas this expects an initial value of 63336 to be +// accepted. +X.testLimit("initial imported memory pages", 1, kJSEmbeddingMaxMemoryPages, + (builder, count) => { + builder.addImportedMemory("mod", "mem", count, undefined); + }); + +// passes +testLimit("maximum imported memory pages", 1, kJSEmbeddingMaxMemoryPages, + (builder, count) => { + builder.addImportedMemory("mod", "mem", 1, count); + }); + +// disabled +// TODO(titzer): ugh, that's hard to test. +DISABLED.testLimit("module size", 1, kJSEmbeddingMaxModuleSize, + (builder, count) => { + }); + +// passes +testLimit("function size", 2, kJSEmbeddingMaxFunctionSize, (builder, count) => { + let type = builder.addType(kSig_v_v); + let nops = count-2; + let array = new Array(nops); + for (let i = 0; i < nops; i++) array[i] = kExprNop; + array[nops] = kExprEnd; + builder.addFunction(undefined, type).addBody(array); + }); + +// passes +testLimit("function locals", 1, kJSEmbeddingMaxFunctionLocals, (builder, count) => { + let type = builder.addType(kSig_v_v); + builder.addFunction(undefined, type) + .addLocals({i32_count: count}) + .addBody([kExprEnd]); + }); + +// passes +testLimit("function params", 1, kJSEmbeddingMaxFunctionParams, (builder, count) => { + let array = new Array(count); + for (let i = 0; i < count; i++) array[i] = kWasmI32; + let type = builder.addType({params: array, results: []}); + }); + +// passes +testLimit("function params+locals", 1, kJSEmbeddingMaxFunctionLocals - 2, (builder, count) => { + let type = builder.addType(kSig_i_ii); + builder.addFunction(undefined, type) + .addLocals({i32_count: count}) + .addBody([kExprUnreachable, kExprEnd]); + }); + +// passes +testLimit("function returns", 0, kJSEmbeddingMaxFunctionReturns, (builder, count) => { + let array = new Array(count); + for (let i = 0; i < count; i++) array[i] = kWasmI32; + let type = builder.addType({params: [], results: array}); + }); + +// passes +testLimit("initial table size", 1, kJSEmbeddingMaxTableSize, (builder, count) => { + builder.setFunctionTableBounds(count, undefined); + }); + +// passes +testLimit("maximum table size", 1, kJSEmbeddingMaxTableSize, (builder, count) => { + builder.setFunctionTableBounds(1, count); + }); + +// passes +testLimit("table entries", 1, kJSEmbeddingMaxTableEntries, (builder, count) => { + builder.setFunctionTableBounds(1, 1); + let array = []; + for (let i = 0; i < count; i++) { + builder.addFunctionTableInit(0, false, array, false); + } + }); + +// passes +testLimit("tables", 0, kJSEmbeddingMaxTables, (builder, count) => { + for (let i = 0; i < count; i++) { + builder.addImportedTable("", "", 1, 1); + } + }); + +// passed +testLimit("memories", 0, kJSEmbeddingMaxMemories, (builder, count) => { + for (let i = 0; i < count; i++) { + builder.addImportedMemory("", "", 1, 1, false); + } + }); + +//======================================================================= +// HARNESS SNIPPET, DO NOT COMMIT +//======================================================================= +if (false && failures.length > 0) { + throw failures[0]; +} +//======================================================================= diff --git a/js/src/wasm/WasmBinaryConstants.h b/js/src/wasm/WasmBinaryConstants.h index fe887deab251..74bd2366ea6d 100644 --- a/js/src/wasm/WasmBinaryConstants.h +++ b/js/src/wasm/WasmBinaryConstants.h @@ -600,17 +600,21 @@ static const unsigned MaxExports = 100000; static const unsigned MaxGlobals = 1000000; static const unsigned MaxDataSegments = 100000; static const unsigned MaxElemSegments = 10000000; -static const unsigned MaxTableInitialLength = 10000000; -static const unsigned MaxStringBytes = 100000; +static const unsigned MaxTableMaximumLength = 10000000; static const unsigned MaxLocals = 50000; static const unsigned MaxParams = 1000; -static const unsigned MaxBrTableElems = 1000000; -static const unsigned MaxMemoryInitialPages = 16384; -static const unsigned MaxMemoryMaximumPages = 65536; -static const unsigned MaxCodeSectionBytes = 1024 * 1024 * 1024; -static const unsigned MaxModuleBytes = MaxCodeSectionBytes; +static const unsigned MaxMemoryMaximumPages = 65536; +static const unsigned MaxStringBytes = 100000; +static const unsigned MaxModuleBytes = 1024 * 1024 * 1024; static const unsigned MaxFunctionBytes = 7654321; +// These limits pertain to our WebAssembly implementation only. + +static const unsigned MaxTableInitialLength = 10000000; +static const unsigned MaxBrTableElems = 1000000; +static const unsigned MaxMemoryInitialPages = 16384; +static const unsigned MaxCodeSectionBytes = MaxModuleBytes; + // A magic value of the FramePointer to indicate after a return to the entry // stub that an exception has been caught and that we should throw. diff --git a/js/src/wasm/WasmJS.cpp b/js/src/wasm/WasmJS.cpp index c0da76bde16a..3f3f07c9eebc 100644 --- a/js/src/wasm/WasmJS.cpp +++ b/js/src/wasm/WasmJS.cpp @@ -1903,7 +1903,7 @@ WasmTableObject::construct(JSContext* cx, unsigned argc, Value* vp) } Limits limits; - if (!GetLimits(cx, obj, MaxTableInitialLength, UINT32_MAX, "Table", &limits, + if (!GetLimits(cx, obj, MaxTableInitialLength, MaxTableMaximumLength, "Table", &limits, Shareable::False)) { return false; diff --git a/js/src/wasm/WasmValidate.cpp b/js/src/wasm/WasmValidate.cpp index 31aad8fea3b3..146e580b9e7d 100644 --- a/js/src/wasm/WasmValidate.cpp +++ b/js/src/wasm/WasmValidate.cpp @@ -1226,7 +1226,12 @@ DecodeTableLimits(Decoder& d, TableDescVector* tables) if (!DecodeLimits(d, &limits)) return false; - if (limits.initial > MaxTableInitialLength) + // If there's a maximum, check it is in range. The check to exclude + // initial > maximum is carried out by the DecodeLimits call above, so + // we don't repeat it here. + if (limits.initial > MaxTableInitialLength || + ((limits.maximum.isSome() && + limits.maximum.value() > MaxTableMaximumLength))) return d.fail("too many table elements"); if (tables->length()) diff --git a/testing/web-platform/mozilla/tests/wasm/js/jsapi.js b/testing/web-platform/mozilla/tests/wasm/js/jsapi.js index 4bf9a7d8a98d..c0b9c4a200b6 100644 --- a/testing/web-platform/mozilla/tests/wasm/js/jsapi.js +++ b/testing/web-platform/mozilla/tests/wasm/js/jsapi.js @@ -526,7 +526,7 @@ test(() => { assert_equals(new Table({initial:1, element:"anyfunc"}) instanceof Table, true); assert_equals(new Table({initial:1.5, element:"anyfunc"}) instanceof Table, true); assert_equals(new Table({initial:1, maximum:1.5, element:"anyfunc"}) instanceof Table, true); - assert_equals(new Table({initial:1, maximum:Math.pow(2,32)-1, element:"anyfunc"}) instanceof Table, true); + assertThrows(() => new Table({initial:1, maximum:Math.pow(2,32)-1, element:"anyfunc"}), RangeError); }, "'WebAssembly.Table' constructor function"); test(() => {