Skip to content

Commit 19389b7

Browse files
authored
Merge pull request webpack#7447 from xtuc/fix-wasm-check-for-invalid-signatures
wasm: finalizer for checking exports
2 parents bc6b5b0 + 78b3193 commit 19389b7

20 files changed

+215
-28
lines changed

declarations.d.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,14 @@ declare module "@webassemblyjs/ast" {
6161
}
6262
export class ModuleExport extends Node {
6363
name: string;
64-
descr: {
65-
type: string;
66-
exportType: string;
67-
id?: Identifier;
68-
};
64+
descr: ModuleExportDescr;
65+
}
66+
type Index = Identifier | NumberLiteral;
67+
export class ModuleExportDescr extends Node {
68+
type: string;
69+
exportType: string;
70+
id: Index;
6971
}
70-
export class ModuleExportDescr extends Node {}
71-
export class IndexLiteral extends Node {}
7272
export class NumberLiteral extends Node {
7373
value: number;
7474
raw: string;
@@ -94,12 +94,13 @@ declare module "@webassemblyjs/ast" {
9494
signature: Signature;
9595
}
9696
export class Signature {
97+
type: "Signature";
9798
params: FuncParam[];
9899
results: string[];
99100
}
100101
export class TypeInstruction extends Node {}
101102
export class IndexInFuncSection extends Node {}
102-
export function indexLiteral(index: number): IndexLiteral;
103+
export function indexLiteral(index: number): Index;
103104
export function numberLiteralFromRaw(num: number): NumberLiteral;
104105
export function floatLiteral(
105106
value: number,
@@ -111,29 +112,33 @@ declare module "@webassemblyjs/ast" {
111112
export function identifier(indentifier: string): Identifier;
112113
export function funcParam(valType: string, id: Identifier): FuncParam;
113114
export function instruction(inst: string, args: Node[]): Instruction;
114-
export function callInstruction(funcIndex: IndexLiteral): CallInstruction;
115+
export function callInstruction(funcIndex: Index): CallInstruction;
115116
export function objectInstruction(
116117
kind: string,
117118
type: string,
118119
init: Node[]
119120
): ObjectInstruction;
120121
export function signature(params: FuncParam[], results: string[]): Signature;
121-
export function func(initFuncId, Signature, funcBody): Func;
122+
export function func(initFuncId, signature: Signature, funcBody): Func;
122123
export function typeInstruction(
123124
id: Identifier,
124125
functype: Signature
125126
): TypeInstruction;
126-
export function indexInFuncSection(index: IndexLiteral): IndexInFuncSection;
127+
export function indexInFuncSection(index: Index): IndexInFuncSection;
127128
export function moduleExport(
128129
identifier: string,
129130
descr: ModuleExportDescr
130131
): ModuleExport;
131132
export function moduleExportDescr(
132133
type: string,
133-
index: ModuleExportDescr
134-
): ModuleExport;
134+
index: Index
135+
): ModuleExportDescr;
135136

136137
export function getSectionMetadata(ast: any, section: string);
138+
export class FuncSignature {
139+
args: string[];
140+
result: string[];
141+
}
137142
}
138143

139144
/**

lib/ModuleDependencyError.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class ModuleDependencyError extends WebpackError {
2626
this.module = module;
2727
this.loc = loc;
2828
this.error = err;
29+
this.origin = module.issuer;
2930

3031
Error.captureStackTrace(this, this.constructor);
3132
}

lib/ModuleDependencyWarning.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module.exports = class ModuleDependencyWarning extends WebpackError {
1818
this.module = module;
1919
this.loc = loc;
2020
this.error = err;
21+
this.origin = module.issuer;
2122

2223
Error.captureStackTrace(this, this.constructor);
2324
}

lib/WebpackOptionsApply.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const NamedModulesPlugin = require("./NamedModulesPlugin");
5858
const NamedChunksPlugin = require("./NamedChunksPlugin");
5959
const DefinePlugin = require("./DefinePlugin");
6060
const SizeLimitsPlugin = require("./performance/SizeLimitsPlugin");
61+
const WasmFinalizeExportsPlugin = require("./wasm/WasmFinalizeExportsPlugin");
6162

6263
class WebpackOptionsApply extends OptionsApply {
6364
constructor() {
@@ -339,6 +340,9 @@ class WebpackOptionsApply extends OptionsApply {
339340
if (options.optimization.noEmitOnErrors) {
340341
new NoEmitOnErrorsPlugin().apply(compiler);
341342
}
343+
if (options.optimization.checkWasmTypes) {
344+
new WasmFinalizeExportsPlugin().apply(compiler);
345+
}
342346
if (options.optimization.namedModules) {
343347
new NamedModulesPlugin().apply(compiler);
344348
}

lib/WebpackOptionsDefaulter.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,9 @@ class WebpackOptionsDefaulter extends OptionsDefaulter {
254254
this.set("optimization.noEmitOnErrors", "make", options =>
255255
isProductionLikeMode(options)
256256
);
257+
this.set("optimization.checkWasmTypes", "make", options =>
258+
isProductionLikeMode(options)
259+
);
257260
this.set(
258261
"optimization.namedModules",
259262
"make",

lib/dependencies/WebAssemblyImportDependency.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class WebAssemblyImportDependency extends ModuleDependency {
3232
) {
3333
return [
3434
new UnsupportedWebAssemblyFeatureError(
35-
`Import with ${
35+
`Import "${this.name}" from "${this.request}" with ${
3636
this.onlyDirectImport
3737
} can only be used for direct wasm to wasm dependencies`
3838
)

lib/wasm/WasmFinalizeExportsPlugin.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
MIT License http://www.opensource.org/licenses/mit-license.php
3+
Author Tobias Koppers @sokra
4+
*/
5+
"use strict";
6+
7+
const UnsupportedWebAssemblyFeatureError = require("../wasm/UnsupportedWebAssemblyFeatureError");
8+
9+
class WasmFinalizeExportsPlugin {
10+
apply(compiler) {
11+
compiler.hooks.compilation.tap("WasmFinalizeExportsPlugin", compilation => {
12+
compilation.hooks.finishModules.tap(
13+
"WasmFinalizeExportsPlugin",
14+
modules => {
15+
for (const module of modules) {
16+
// 1. if a WebAssembly module
17+
if (module.type.startsWith("webassembly") === true) {
18+
const jsIncompatibleExports =
19+
module.buildMeta.jsIncompatibleExports;
20+
21+
if (jsIncompatibleExports === undefined) {
22+
continue;
23+
}
24+
25+
for (const reason of module.reasons) {
26+
// 2. is referenced by a non-WebAssembly module
27+
if (reason.module.type.startsWith("webassembly") === false) {
28+
const ref = reason.dependency.getReference();
29+
30+
const importedNames = ref.importedNames;
31+
32+
if (Array.isArray(importedNames)) {
33+
importedNames.forEach(name => {
34+
// 3. and uses a func with an incompatible JS signature
35+
if (
36+
Object.prototype.hasOwnProperty.call(
37+
jsIncompatibleExports,
38+
name
39+
)
40+
) {
41+
// 4. error
42+
/** @type {any} */
43+
const error = new UnsupportedWebAssemblyFeatureError(
44+
`Export "${name}" with ${
45+
jsIncompatibleExports[name]
46+
} can only be used for direct wasm to wasm dependencies`
47+
);
48+
error.module = module;
49+
error.origin = reason.module;
50+
error.originLoc = reason.dependency.loc;
51+
error.dependencies = [reason.dependency];
52+
compilation.errors.push(error);
53+
}
54+
});
55+
}
56+
}
57+
}
58+
}
59+
}
60+
}
61+
);
62+
});
63+
}
64+
}
65+
66+
module.exports = WasmFinalizeExportsPlugin;

lib/wasm/WebAssemblyGenerator.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ function getCountImportedFunc(ast) {
131131
* Get next type index
132132
*
133133
* @param {Object} ast - Module's AST
134-
* @returns {t.IndexLiteral} - index
134+
* @returns {t.Index} - index
135135
*/
136136
function getNextTypeIndex(ast) {
137137
const typeSectionMetadata = t.getSectionMetadata(ast, "type");
@@ -152,7 +152,7 @@ function getNextTypeIndex(ast) {
152152
*
153153
* @param {Object} ast - Module's AST
154154
* @param {Number} countImportedFunc - number of imported funcs
155-
* @returns {t.IndexLiteral} - index
155+
* @returns {t.Index} - index
156156
*/
157157
function getNextFuncIndex(ast, countImportedFunc) {
158158
const funcSectionMetadata = t.getSectionMetadata(ast, "func");
@@ -309,11 +309,11 @@ const rewriteImports = ({ ast, usedDependencyMap }) => bin => {
309309
* @param {Object} state transformation state
310310
* @param {Object} state.ast - Module's ast
311311
* @param {t.Identifier} state.initFuncId identifier of the init function
312-
* @param {t.IndexLiteral} state.startAtFuncIndex index of the start function
312+
* @param {t.Index} state.startAtFuncIndex index of the start function
313313
* @param {t.ModuleImport[]} state.importedGlobals list of imported globals
314314
* @param {t.Instruction[]} state.additionalInitCode list of addition instructions for the init function
315-
* @param {t.IndexLiteral} state.nextFuncIndex index of the next function
316-
* @param {t.IndexLiteral} state.nextTypeIndex index of the next type
315+
* @param {t.Index} state.nextFuncIndex index of the next function
316+
* @param {t.Index} state.nextTypeIndex index of the next type
317317
* @returns {ArrayBufferTransform} transform
318318
*/
319319
const addInitFunction = ({

lib/wasm/WebAssemblyParser.js

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
const t = require("@webassemblyjs/ast");
88
const { decode } = require("@webassemblyjs/wasm-parser");
9+
const {
10+
moduleContextFromModuleAST
11+
} = require("@webassemblyjs/helper-module-context");
912

1013
const { Tapable } = require("tapable");
1114
const WebAssemblyImportDependency = require("../dependencies/WebAssemblyImportDependency");
@@ -40,25 +43,44 @@ const isGlobalImport = n => n.descr.type === "GlobalType";
4043
const JS_COMPAT_TYPES = new Set(["i32", "f32", "f64"]);
4144

4245
/**
43-
* @param {t.ModuleImport} moduleImport the import
46+
* @param {t.Signature} signature the func signature
4447
* @returns {null | string} the type incompatible with js types
4548
*/
46-
const getJsIncompatibleType = moduleImport => {
47-
if (moduleImport.descr.type !== "FuncImportDescr") return null;
48-
const signature = moduleImport.descr.signature;
49+
const getJsIncompatibleType = signature => {
4950
for (const param of signature.params) {
50-
if (!JS_COMPAT_TYPES.has(param.valtype))
51+
if (!JS_COMPAT_TYPES.has(param.valtype)) {
5152
return `${param.valtype} as parameter`;
53+
}
5254
}
5355
for (const type of signature.results) {
5456
if (!JS_COMPAT_TYPES.has(type)) return `${type} as result`;
5557
}
5658
return null;
5759
};
5860

61+
/**
62+
* TODO why are there two different Signature types?
63+
* @param {t.FuncSignature} signature the func signature
64+
* @returns {null | string} the type incompatible with js types
65+
*/
66+
const getJsIncompatibleTypeOfFuncSignature = signature => {
67+
for (const param of signature.args) {
68+
if (!JS_COMPAT_TYPES.has(param)) {
69+
return `${param} as parameter`;
70+
}
71+
}
72+
for (const type of signature.result) {
73+
if (!JS_COMPAT_TYPES.has(type)) return `${type} as result`;
74+
}
75+
return null;
76+
};
77+
5978
const decoderOpts = {
6079
ignoreCodeSection: true,
61-
ignoreDataSection: true
80+
ignoreDataSection: true,
81+
82+
// this will avoid having to lookup with identifiers in the ModuleContext
83+
ignoreCustomNameSection: true
6284
};
6385

6486
class WebAssemblyParser extends Tapable {
@@ -73,13 +95,35 @@ class WebAssemblyParser extends Tapable {
7395
state.module.buildMeta.exportsType = "namespace";
7496

7597
// parse it
76-
const ast = decode(binary, decoderOpts);
98+
const program = decode(binary, decoderOpts);
99+
const module = program.body[0];
100+
101+
const moduleContext = moduleContextFromModuleAST(module);
77102

78103
// extract imports and exports
79104
const exports = (state.module.buildMeta.providedExports = []);
105+
const jsIncompatibleExports = (state.module.buildMeta.jsIncompatibleExports = []);
106+
80107
const importedGlobals = [];
81-
t.traverse(ast, {
108+
t.traverse(module, {
82109
ModuleExport({ node }) {
110+
const descriptor = node.descr;
111+
112+
if (descriptor.exportType === "Func") {
113+
const funcidx = descriptor.id.value;
114+
115+
/** @type {t.FuncSignature} */
116+
const funcSignature = moduleContext.getFunction(funcidx);
117+
118+
const incompatibleType = getJsIncompatibleTypeOfFuncSignature(
119+
funcSignature
120+
);
121+
122+
if (incompatibleType) {
123+
jsIncompatibleExports[node.name] = incompatibleType;
124+
}
125+
}
126+
83127
exports.push(node.name);
84128

85129
if (node.descr && node.descr.exportType === "Global") {
@@ -121,10 +165,15 @@ class WebAssemblyParser extends Tapable {
121165
} else if (isTableImport(node) === true) {
122166
onlyDirectImport = "Table";
123167
} else if (isFuncImport(node) === true) {
124-
const incompatibleType = getJsIncompatibleType(node);
168+
const incompatibleType = getJsIncompatibleType(node.descr.signature);
125169
if (incompatibleType) {
126170
onlyDirectImport = `Non-JS-compatible Func Sigurature (${incompatibleType})`;
127171
}
172+
} else if (isGlobalImport(node) === true) {
173+
const type = node.descr.valtype;
174+
if (!JS_COMPAT_TYPES.has(type)) {
175+
onlyDirectImport = `Non-JS-compatible Global Type (${type})`;
176+
}
128177
}
129178

130179
const dep = new WebAssemblyImportDependency(

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"license": "MIT",
77
"dependencies": {
88
"@webassemblyjs/ast": "1.5.10",
9+
"@webassemblyjs/helper-module-context": "1.5.10",
910
"@webassemblyjs/wasm-edit": "1.5.10",
1011
"@webassemblyjs/wasm-opt": "1.5.10",
1112
"@webassemblyjs/wasm-parser": "1.5.10",

schemas/WebpackOptions.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,6 +1542,10 @@
15421542
"description": "Avoid emitting assets when errors occur",
15431543
"type": "boolean"
15441544
},
1545+
"checkWasmTypes": {
1546+
"description": "Check for incompatible wasm types when importing/exporting from/to ESM",
1547+
"type": "boolean"
1548+
},
15451549
"namedModules": {
15461550
"description": "Use readable module identifiers for better debugging",
15471551
"type": "boolean"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const n = 1;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module.exports = [
2+
[
3+
/export-i64-param\.wat/,
4+
/Export "a" with i64 as parameter can only be used for direct wasm to wasm dependencies/,
5+
/export-i64-param\.js/
6+
],
7+
[
8+
/export-i64-result\.wat/,
9+
/Export "a" with i64 as result can only be used for direct wasm to wasm dependencies/,
10+
/export-i64-result\.js/
11+
],
12+
[
13+
/import-i64\.wat/,
14+
/Import "n" from "\.\/env.js" with Non-JS-compatible Global Type \(i64\) can only be used for direct wasm to wasm dependencies/,
15+
/index\.js/
16+
]
17+
]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { a } from "./export-i64-param.wat";
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
(module
2+
(func (export "a") (param i64) (nop))
3+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { a } from "./export-i64-result.wat";
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
(module
2+
(func (export "a") (result i64)
3+
(i64.const 1)
4+
)
5+
)

0 commit comments

Comments
 (0)