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

LinkError: WebAssembly.instantiate(): memory import # is smaller than initial #, got # #561

Closed
dOrgJelli opened this issue Dec 1, 2021 · 0 comments
Assignees
Labels
type: bug Something isn't working

Comments

@dOrgJelli
Copy link
Contributor

dOrgJelli commented Dec 1, 2021

Problem

As reported by @krisbitney, we've encountered this error while instantiating one of the UniV3 modules.
image

The problem stems from this validation step within Chromium's WebAssembly run-time:
https://chromium.googlesource.com/v8/v8/+/644556e6ed0e6e4fac2dfabb441439820ec59813/src/wasm/module-instantiate.cc#924

It is being thrown when we run:

const memory = WebAssembly.Memory({ initial: 1 });
const { instance } = await WebAssembly.instantiate(wasmBytecode, {
  env: {
    memory,
  },
  w3: w3Imports,
});

Root Cause

After doing some digging into this, I've found that the problem stems from how Wasm modules expect their imported memory to be configured.

WebAssembly memory buffers must be configured with an initial number of memory pages (64 kb each). The module's bytecode is actually hard-coded with an expected number of initial pages:
UniswapV2 query.wasm Module

(module
  ...
  (import "env" "memory" (memory (;0;) 1))
  ...
)

and UniswapV3 query.wasm Module

(module
  ...
  (import "env" "memory" (memory (;0;) 2))
  ...
)

Notice the difference? The import command for the env.memory object is defined differently, using the number 2 instead of 1.

If we look at the Wasm bytecode standard, we can see that the memory import configuration is represented like so:
https://github.com/sunfishcode/wasm-reference-manual/blob/master/WebAssembly.md#linear-memory-description

If we change our above JavaScript code to:

// Change initial to anything >= 2
const memory = WebAssembly.Memory({ initial: 2 });
const { instance } = await WebAssembly.instantiate(wasmBytecode, {
  env: {
    memory,
  },
  w3: w3Imports,
});

everything builds and runs just fine. Hooray! Now how do we make this future proof?

Solution

Since it looks like we can no longer assume initial: 1 for all Wasm modules, we should instead check (before instantiation) what the module expects the memory page size to be. How do we fetch this information though? Well we can do a simple search through the bytecode.

If we take the following wat code:

(module
  (import "env" "memory" (memory (;0;) 2)))

and run wat2wasm ./test.wat -d we get:

0000000: 0061 736d                                 ; WASM_BINARY_MAGIC
0000004: 0100 0000                                 ; WASM_BINARY_VERSION
; section "Import" (2)
0000008: 02                                        ; section code
0000009: 00                                        ; section size (guess)
000000a: 01                                        ; num imports
; import header 0
000000b: 03                                        ; string length
000000c: 656e 76                                  env  ; import module name
000000f: 06                                        ; string length
0000010: 6d65 6d6f 7279                           memory  ; import field name
0000016: 02                                        ; import kind
0000017: 00                                        ; limits: flags
0000018: 02                                        ; limits: initial
0000009: 0f                                        ; FIXUP section size

From this we can extract the following bytecode signature:

const envMemoryImportSignature = Buffer.from([
  // string length
  0x03,
  // env ; import module name
  0x65, 0x6e, 0x76,
  // string length
  0x06,
  // memory ; import field name
  0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79,
  // import kind
  0x02,
  // limits ; https://github.com/sunfishcode/wasm-reference-manual/blob/master/WebAssembly.md#resizable-limits
  // limits ; flags
  // 0x??,
  // limits ; initial
  // 0x__,
]);

And now we simply do an indexOf(...) and we have our initial memory page size:

const sigIdx = wasmSource.indexOf(envMemoryImportSignature);
const memoryInitalLimits = wasmSource.readUInt8(
  sigIdx + envMemoryImportSignature.length + 1
);

const memory = new WebAssembly.Memory({ initial: memoryInitalLimits });

Now, regardless of how the Wasm module is compiled, we can always match the exact memory page layout it's expecting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: bug Something isn't working
Projects
None yet
Development

No branches or pull requests

1 participant