Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[middleware] Warn dynamic WASM compilation #37681

Merged
merged 4 commits into from
Jun 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions errors/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,10 @@
{
"title": "get-initial-props-export",
"path": "/errors/get-initial-props-export.md"
},
{
"title": "middleware-dynamic-wasm-compilation",
"path": "/errors/middleware-dynamic-wasm-compilation.md"
}
]
}
Expand Down
27 changes: 27 additions & 0 deletions errors/middleware-dynamic-wasm-compilation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Dynamic WASM compilation is not available in Middlewares

#### Why This Error Occurred

Compiling WASM binaries dynamically is not allowed in Middlewares. Specifically,
the following APIs are not supported:

- `WebAssembly.compile`
- `WebAssembly.instantiate` with [a buffer parameter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiate#primary_overload_%E2%80%94_taking_wasm_binary_code)

#### Possible Ways to Fix It

Bundle your WASM binaries using `import`:

```typescript
import { NextResponse } from 'next/server'
import squareWasm from './square.wasm?module'

export default async function middleware() {
const m = await WebAssembly.instantiate(squareWasm)
const answer = m.exports.square(9)

const response = NextResponse.next()
response.headers.set('x-square', answer.toString())
return response
}
```
62 changes: 61 additions & 1 deletion packages/next/build/webpack/plugins/middleware-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,60 @@ function getCodeAnalizer(params: {
return true
}

/**
* This expression handler allows to wrap a WebAssembly.compile invocation with a
* function call where we can warn about WASM code generation not being allowed
* but actually execute the expression.
*/
const handleWrapWasmCompileExpression = (expr: any) => {
if (!isInMiddlewareLayer(parser)) {
return
}

if (dev) {
const { ConstDependency } = wp.dependencies
const dep1 = new ConstDependency(
'__next_webassembly_compile__(function() { return ',
expr.range[0]
)
dep1.loc = expr.loc
parser.state.module.addPresentationalDependency(dep1)
const dep2 = new ConstDependency('})', expr.range[1])
dep2.loc = expr.loc
parser.state.module.addPresentationalDependency(dep2)
}

handleExpression()
}

/**
* This expression handler allows to wrap a WebAssembly.instatiate invocation with a
* function call where we can warn about WASM code generation not being allowed
* but actually execute the expression.
*
* Note that we don't update `usingIndirectEval`, i.e. we don't abort a production build
* since we can't determine statically if the first parameter is a module (legit use) or
* a buffer (dynamic code generation).
*/
const handleWrapWasmInstantiateExpression = (expr: any) => {
if (!isInMiddlewareLayer(parser)) {
return
}

if (dev) {
const { ConstDependency } = wp.dependencies
const dep1 = new ConstDependency(
'__next_webassembly_instantiate__(function() { return ',
expr.range[0]
)
dep1.loc = expr.loc
parser.state.module.addPresentationalDependency(dep1)
const dep2 = new ConstDependency('})', expr.range[1])
dep2.loc = expr.loc
parser.state.module.addPresentationalDependency(dep2)
}
}

/**
* For an expression this will check the graph to ensure it is being used
* by exports. Then it will store in the module buildInfo a boolean to
Expand Down Expand Up @@ -232,6 +286,12 @@ function getCodeAnalizer(params: {
hooks.new.for(`${prefix}Function`).tap(NAME, handleWrapExpression)
hooks.expression.for(`${prefix}eval`).tap(NAME, handleExpression)
hooks.expression.for(`${prefix}Function`).tap(NAME, handleExpression)
hooks.call
.for(`${prefix}WebAssembly.compile`)
.tap(NAME, handleWrapWasmCompileExpression)
hooks.call
.for(`${prefix}WebAssembly.instantiate`)
.tap(NAME, handleWrapWasmInstantiateExpression)
}
hooks.new.for('Response').tap(NAME, handleNewResponseExpression)
hooks.new.for('NextResponse').tap(NAME, handleNewResponseExpression)
Expand Down Expand Up @@ -331,7 +391,7 @@ function getExtractMetadata(params: {
}

const error = new wp.WebpackError(
`Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Middleware ${entryName}${
`Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Middleware ${entryName}${
typeof buildInfo.usingIndirectEval !== 'boolean'
? `\nUsed by ${Array.from(buildInfo.usingIndirectEval).join(
', '
Expand Down
43 changes: 43 additions & 0 deletions packages/next/server/web/sandbox/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export async function getModuleContext(options: ModuleContextOptions) {
*/
async function createModuleContext(options: ModuleContextOptions) {
const warnedEvals = new Set<string>()
const warnedWasmCodegens = new Set<string>()
const wasm = await loadWasm(options.wasm)
const runtime = new EdgeRuntime({
codeGeneration:
Expand All @@ -122,6 +123,48 @@ async function createModuleContext(options: ModuleContextOptions) {
return fn()
}

context.__next_webassembly_compile__ =
function __next_webassembly_compile__(fn: Function) {
const key = fn.toString()
if (!warnedWasmCodegens.has(key)) {
const warning = new Error(
"Dynamic WASM code generation (e. g. 'WebAssembly.compile') not allowed in Middleware.\n" +
'Learn More: https://nextjs.org/docs/messages/middleware-dynamic-wasm-compilation'
)
warning.name = 'DynamicWasmCodeGenerationWarning'
Error.captureStackTrace(warning, __next_webassembly_compile__)
warnedWasmCodegens.add(key)
options.onWarning(warning)
}
return fn()
}

context.__next_webassembly_instantiate__ =
async function __next_webassembly_instantiate__(fn: Function) {
const result = await fn()

// If a buffer is given, WebAssembly.instantiate returns an object
// containing both a module and an instance while it returns only an
// instance if a WASM module is given. Utilize the fact to determine
// if the WASM code generation happens.
//
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiate#primary_overload_%E2%80%94_taking_wasm_binary_code
const instantiatedFromBuffer = result.hasOwnProperty('module')

const key = fn.toString()
if (instantiatedFromBuffer && !warnedWasmCodegens.has(key)) {
const warning = new Error(
"Dynamic WASM code generation ('WebAssembly.instantiate' with a buffer parameter) not allowed in Middleware.\n" +
'Learn More: https://nextjs.org/docs/messages/middleware-dynamic-wasm-compilation'
)
warning.name = 'DynamicWasmCodeGenerationWarning'
Error.captureStackTrace(warning, __next_webassembly_instantiate__)
warnedWasmCodegens.add(key)
options.onWarning(warning)
}
return result
}

const __fetch = context.fetch
context.fetch = (input: RequestInfo, init: RequestInit = {}) => {
init.headers = new Headers(init.headers ?? {})
Expand Down
Binary file not shown.
35 changes: 35 additions & 0 deletions test/integration/middleware-dynamic-code/lib/wasm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// (module
// (type (;0;) (func (param i32) (result i32)))
// (func (;0;) (type 0) (param i32) (result i32)
// local.get 0
// local.get 0
// i32.mul)
// (table (;0;) 0 funcref)
// (memory (;0;) 1)
// (export "memory" (memory 0))
// (export "square" (func 0)))
const SQUARE_WASM_BUFFER = new Uint8Array([
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x06, 0x01, 0x60, 0x01,
0x7f, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00, 0x04, 0x04, 0x01, 0x70, 0x00, 0x00,
0x05, 0x03, 0x01, 0x00, 0x01, 0x07, 0x13, 0x02, 0x06, 0x6d, 0x65, 0x6d, 0x6f,
0x72, 0x79, 0x02, 0x00, 0x06, 0x73, 0x71, 0x75, 0x61, 0x72, 0x65, 0x00, 0x00,
0x0a, 0x09, 0x01, 0x07, 0x00, 0x20, 0x00, 0x20, 0x00, 0x6c, 0x0b,
])

import squareWasmModule from './square.wasm?module'

export async function usingWebAssemblyCompile(x) {
const module = await WebAssembly.compile(SQUARE_WASM_BUFFER)
const instance = await WebAssembly.instantiate(module, {})
return { value: instance.exports.square(x) }
}

export async function usingWebAssemblyInstantiateWithBuffer(x) {
const { instance } = await WebAssembly.instantiate(SQUARE_WASM_BUFFER, {})
return { value: instance.exports.square(x) }
}

export async function usingWebAssemblyInstantiate(x) {
const instance = await WebAssembly.instantiate(squareWasmModule)
return { value: instance.exports.square(x) }
}
27 changes: 27 additions & 0 deletions test/integration/middleware-dynamic-code/middleware.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { notUsingEval, usingEval } from './lib/utils'
import {
usingWebAssemblyCompile,
usingWebAssemblyInstantiate,
usingWebAssemblyInstantiateWithBuffer,
} from './lib/wasm'

export async function middleware(request) {
if (request.nextUrl.pathname === '/using-eval') {
Expand All @@ -12,4 +17,26 @@ export async function middleware(request) {
headers: { data: JSON.stringify(await notUsingEval()) },
})
}

if (request.nextUrl.pathname === '/using-webassembly-compile') {
return new Response(null, {
headers: { data: JSON.stringify(await usingWebAssemblyCompile(9)) },
})
}

if (request.nextUrl.pathname === '/using-webassembly-instantiate') {
return new Response(null, {
headers: { data: JSON.stringify(await usingWebAssemblyInstantiate(9)) },
})
}

if (
request.nextUrl.pathname === '/using-webassembly-instantiate-with-buffer'
) {
return new Response(null, {
headers: {
data: JSON.stringify(await usingWebAssemblyInstantiateWithBuffer(9)),
},
})
}
}
6 changes: 6 additions & 0 deletions test/integration/middleware-dynamic-code/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
webpack(config) {
config.experiments = { ...config.experiments, asyncWebAssembly: true }
return config
},
}
59 changes: 55 additions & 4 deletions test/integration/middleware-dynamic-code/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import {
} from 'next-test-utils'

const context = {}
const DYNAMIC_CODE_ERROR = `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Middleware`
const EVAL_ERROR = `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Middleware`
const DYNAMIC_CODE_ERROR = `Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Middleware`
const WASM_COMPILE_ERROR = `Dynamic WASM code generation (e. g. 'WebAssembly.compile') not allowed in Middleware`
const WASM_INSTANTIATE_ERROR = `Dynamic WASM code generation ('WebAssembly.instantiate' with a buffer parameter) not allowed in Middleware`

jest.setTimeout(1000 * 60 * 2)
context.appDir = join(__dirname, '../')
Expand Down Expand Up @@ -43,7 +46,7 @@ describe('Middleware usage of dynamic code evaluation', () => {
const json = JSON.parse(res.headers.get('data'))
await waitFor(500)
expect(json.value).toEqual(100)
expect(output).toContain(DYNAMIC_CODE_ERROR)
expect(output).toContain(EVAL_ERROR)
expect(output).toContain('DynamicCodeEvaluationWarning')
expect(output).toContain('./middleware')
// TODO check why that has a backslash on windows
Expand All @@ -57,14 +60,62 @@ describe('Middleware usage of dynamic code evaluation', () => {
const json = JSON.parse(res.headers.get('data'))
await waitFor(500)
expect(json.value).toEqual(100)
expect(output).not.toContain(DYNAMIC_CODE_ERROR)
expect(output).not.toContain('Dynamic Code Evaluation')
})

it('does not has problems with eval in page or server code', async () => {
const html = await renderViaHTTP(context.appPort, `/`)
expect(html).toMatch(/>.*?100.*?and.*?100.*?<\//)
await waitFor(500)
expect(output).not.toContain(DYNAMIC_CODE_ERROR)
expect(output).not.toContain('Dynamic Code Evaluation')
})

it('shows a warning when running WebAssembly.compile', async () => {
const res = await fetchViaHTTP(
context.appPort,
`/using-webassembly-compile`
)
const json = JSON.parse(res.headers.get('data'))
await waitFor(500)
expect(json.value).toEqual(81)
expect(output).toContain(WASM_COMPILE_ERROR)
expect(output).toContain('DynamicWasmCodeGenerationWarning')
expect(output).toContain('./middleware')
expect(output).toMatch(/lib[\\/]wasm\.js/)
expect(output).toContain('usingWebAssemblyCompile')
expect(stripAnsi(output)).toContain(
'await WebAssembly.compile(SQUARE_WASM_BUFFER)'
)
})

it('shows a warning when running WebAssembly.instantiate w/ a buffer parameter', async () => {
const res = await fetchViaHTTP(
context.appPort,
`/using-webassembly-instantiate-with-buffer`
)
const json = JSON.parse(res.headers.get('data'))
await waitFor(500)
expect(json.value).toEqual(81)
expect(output).toContain(WASM_INSTANTIATE_ERROR)
expect(output).toContain('DynamicWasmCodeGenerationWarning')
expect(output).toContain('./middleware')
expect(output).toMatch(/lib[\\/]wasm\.js/)
expect(output).toContain('usingWebAssemblyInstantiateWithBuffer')
expect(stripAnsi(output)).toContain(
'await WebAssembly.instantiate(SQUARE_WASM_BUFFER, {})'
)
})

it('does not show a warning when running WebAssembly.instantiate w/ a module parameter', async () => {
const res = await fetchViaHTTP(
context.appPort,
`/using-webassembly-instantiate`
)
const json = JSON.parse(res.headers.get('data'))
await waitFor(500)
expect(json.value).toEqual(81)
expect(output).not.toContain(WASM_INSTANTIATE_ERROR)
expect(output).not.toContain('DynamicWasmCodeGenerationWarning')
})
})

Expand Down
Loading