summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAshley Nelson <nashley@google.com>2022-12-09 16:18:44 -0800
committerGitHub <noreply@github.com>2022-12-09 16:18:44 -0800
commitf5e71e6d2be82639681fc7d45794645e03d2ad93 (patch)
treea4438a50c46f3c96aefdc469e5fde272a87518dd
parent082dbe25b7377809b1b3dc429cb334fc80fac286 (diff)
downloadbinaryen-f5e71e6d2be82639681fc7d45794645e03d2ad93.tar.gz
binaryen-f5e71e6d2be82639681fc7d45794645e03d2ad93.tar.bz2
binaryen-f5e71e6d2be82639681fc7d45794645e03d2ad93.zip
Adds bounds checks to Load/Store in Multi-Memories Lowering Pass (#5256)
Per the wasm spec guidelines for Load (rule 10) & Store (rule 12), this PR adds an option for bounds checking, producing a runtime error if the instruction exceeds the bounds of the particular memory within the combined memory.
-rw-r--r--src/passes/MultiMemoryLowering.cpp180
-rw-r--r--src/passes/pass.cpp5
-rw-r--r--src/passes/passes.h1
-rw-r--r--test/lit/help/wasm-opt.test5
-rw-r--r--test/lit/help/wasm2js.test5
-rw-r--r--test/lit/passes/multi-memory-lowering.wast339
6 files changed, 465 insertions, 70 deletions
diff --git a/src/passes/MultiMemoryLowering.cpp b/src/passes/MultiMemoryLowering.cpp
index 527a15804..57be529fa 100644
--- a/src/passes/MultiMemoryLowering.cpp
+++ b/src/passes/MultiMemoryLowering.cpp
@@ -23,11 +23,14 @@
// multi-memories feature also prevents later passes from adding additional
// memories.
//
-// Also worth noting that we are diverging from the spec with regards to
-// handling load and store instructions. We are not trapping if the offset +
-// write size is larger than the length of the memory's data. Warning:
-// out-of-bounds loads and stores can read junk out of or corrupt other
-// memories instead of trapping.
+// The offset computation in function maybeMakeBoundsCheck is not precise
+// according to the spec. In the spec offsets do not overflow as
+// twos-complement, but i32.add does. Concretely, a load from address 1000 with
+// offset 0xffffffff should actually trap, as the combined number is greater
+// than 32 bits. But with an add, 1000 + 0xffffffff = 999 due to overflow, which
+// would not trap. In theory we could compute like the spec, by expanding the
+// i32s to i64s and adding there (where we won't overflow), but we don't have
+// i128s to handle i64 overflow.
#include "ir/module-utils.h"
#include "ir/names.h"
@@ -67,6 +70,103 @@ struct MultiMemoryLowering : public Pass {
// each memory
std::vector<Name> memoryGrowNames;
+ bool checkBounds = false;
+
+ MultiMemoryLowering(bool checkBounds) : checkBounds(checkBounds) {}
+
+ struct Replacer : public WalkerPass<PostWalker<Replacer>> {
+ MultiMemoryLowering& parent;
+ Builder builder;
+ Replacer(MultiMemoryLowering& parent, Module& wasm)
+ : parent(parent), builder(wasm) {}
+ // Avoid visiting the custom functions added by the parent pass
+ // MultiMemoryLowering
+ void walkFunction(Function* func) {
+ for (Name funcName : parent.memorySizeNames) {
+ if (funcName == func->name) {
+ return;
+ }
+ }
+ for (Name funcName : parent.memoryGrowNames) {
+ if (funcName == func->name) {
+ return;
+ }
+ }
+ super::walkFunction(func);
+ }
+
+ void visitMemoryGrow(MemoryGrow* curr) {
+ auto idx = parent.memoryIdxMap.at(curr->memory);
+ Name funcName = parent.memoryGrowNames[idx];
+ replaceCurrent(builder.makeCall(funcName, {curr->delta}, curr->type));
+ }
+
+ void visitMemorySize(MemorySize* curr) {
+ auto idx = parent.memoryIdxMap.at(curr->memory);
+ Name funcName = parent.memorySizeNames[idx];
+ replaceCurrent(builder.makeCall(funcName, {}, curr->type));
+ }
+
+ template<typename T> Expression* getPtr(T* curr, Function* func) {
+ auto memoryIdx = parent.memoryIdxMap.at(curr->memory);
+ auto offsetGlobal = parent.getOffsetGlobal(memoryIdx);
+ Expression* ptrValue;
+ if (offsetGlobal) {
+ ptrValue = builder.makeBinary(
+ Abstract::getBinary(parent.pointerType, Abstract::Add),
+ builder.makeGlobalGet(offsetGlobal, parent.pointerType),
+ curr->ptr);
+ } else {
+ ptrValue = curr->ptr;
+ }
+
+ if (parent.checkBounds) {
+ Index ptrIdx = Builder::addVar(getFunction(), parent.pointerType);
+ Expression* ptrSet = builder.makeLocalSet(ptrIdx, ptrValue);
+ Expression* boundsCheck = makeBoundsCheck(curr, ptrIdx, memoryIdx);
+ Expression* ptrGet = builder.makeLocalGet(ptrIdx, parent.pointerType);
+ return builder.makeBlock({ptrSet, boundsCheck, ptrGet});
+ }
+
+ return ptrValue;
+ }
+
+ template<typename T>
+ Expression* makeBoundsCheck(T* curr, Index ptrIdx, Index memoryIdx) {
+ Name memorySizeFunc = parent.memorySizeNames[memoryIdx];
+ Expression* boundsCheck = builder.makeIf(
+ builder.makeBinary(
+ Abstract::getBinary(parent.pointerType, Abstract::GtU),
+ builder.makeBinary(
+ // ptr + offset (ea from wasm spec) + bit width
+ // two builder Adds, we'll add the first two operands in the first
+ // add and then add the third operand in the second add
+ Abstract::getBinary(parent.pointerType, Abstract::Add),
+ builder.makeBinary(
+ Abstract::getBinary(parent.pointerType, Abstract::Add),
+ builder.makeLocalGet(ptrIdx, parent.pointerType),
+ builder.makeConstPtr(curr->offset, parent.pointerType)),
+ builder.makeConstPtr(curr->bytes, parent.pointerType)),
+ builder.makeCall(memorySizeFunc, {}, parent.pointerType)),
+ builder.makeUnreachable());
+ return boundsCheck;
+ }
+
+ template<typename T> void setMemory(T* curr) {
+ curr->memory = parent.combinedMemory;
+ }
+
+ void visitLoad(Load* curr) {
+ curr->ptr = getPtr(curr, getFunction());
+ setMemory(curr);
+ }
+
+ void visitStore(Store* curr) {
+ curr->ptr = getPtr(curr, getFunction());
+ setMemory(curr);
+ }
+ };
+
void run(Module* module) override {
module->features.disable(FeatureSet::MultiMemories);
@@ -85,70 +185,6 @@ struct MultiMemoryLowering : public Pass {
removeExistingMemories();
addCombinedMemory();
- struct Replacer : public WalkerPass<PostWalker<Replacer>> {
- MultiMemoryLowering& parent;
- Builder builder;
- Replacer(MultiMemoryLowering& parent, Module& wasm)
- : parent(parent), builder(wasm) {}
- // Avoid visiting the custom functions added by the parent pass
- // MultiMemoryLowering
- void walkFunction(Function* func) {
- for (Name funcName : parent.memorySizeNames) {
- if (funcName == func->name) {
- return;
- }
- }
- for (Name funcName : parent.memoryGrowNames) {
- if (funcName == func->name) {
- return;
- }
- }
- super::walkFunction(func);
- }
-
- void visitMemoryGrow(MemoryGrow* curr) {
- auto idx = parent.memoryIdxMap.at(curr->memory);
- Name funcName = parent.memoryGrowNames[idx];
- replaceCurrent(builder.makeCall(funcName, {curr->delta}, curr->type));
- }
-
- void visitMemorySize(MemorySize* curr) {
- auto idx = parent.memoryIdxMap.at(curr->memory);
- Name funcName = parent.memorySizeNames[idx];
- replaceCurrent(builder.makeCall(funcName, {}, curr->type));
- }
-
- // TODO: Add an option to add bounds checks.
- void visitLoad(Load* curr) {
- auto idx = parent.memoryIdxMap.at(curr->memory);
- auto global = parent.getOffsetGlobal(idx);
- curr->memory = parent.combinedMemory;
- if (!global) {
- return;
- }
- curr->ptr = builder.makeBinary(
- Abstract::getBinary(parent.pointerType, Abstract::Add),
- builder.makeGlobalGet(global, parent.pointerType),
- curr->ptr);
- }
-
- // We diverge from the spec here and are not trapping if the offset + type
- // / 8 is larger than the length of the memory's data. Warning,
- // out-of-bounds loads and stores can read junk out of or corrupt other
- // memories instead of trapping
- void visitStore(Store* curr) {
- auto idx = parent.memoryIdxMap.at(curr->memory);
- auto global = parent.getOffsetGlobal(idx);
- curr->memory = parent.combinedMemory;
- if (!global) {
- return;
- }
- curr->ptr = builder.makeBinary(
- Abstract::getBinary(parent.pointerType, Abstract::Add),
- builder.makeGlobalGet(global, parent.pointerType),
- curr->ptr);
- }
- };
Replacer(*this, *wasm).run(getPassRunner(), wasm);
}
@@ -421,6 +457,10 @@ struct MultiMemoryLowering : public Pass {
}
};
-Pass* createMultiMemoryLoweringPass() { return new MultiMemoryLowering(); }
+Pass* createMultiMemoryLoweringPass() { return new MultiMemoryLowering(false); }
+
+Pass* createMultiMemoryLoweringWithBoundsChecksPass() {
+ return new MultiMemoryLowering(true);
+}
} // namespace wasm
diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp
index d7f2a310f..e62b1f3a6 100644
--- a/src/passes/pass.cpp
+++ b/src/passes/pass.cpp
@@ -281,6 +281,11 @@ void PassRegistry::registerPasses() {
registerPass("multi-memory-lowering",
"combines multiple memories into a single memory",
createMultiMemoryLoweringPass);
+ registerPass(
+ "multi-memory-lowering-with-bounds-checks",
+ "combines multiple memories into a single memory, trapping if the read or "
+ "write is larger than the length of the memory's data",
+ createMultiMemoryLoweringWithBoundsChecksPass);
registerPass("nm", "name list", createNameListPass);
registerPass("name-types", "(re)name all heap types", createNameTypesPass);
registerPass("once-reduction",
diff --git a/src/passes/passes.h b/src/passes/passes.h
index 1543ae794..8133c867e 100644
--- a/src/passes/passes.h
+++ b/src/passes/passes.h
@@ -87,6 +87,7 @@ Pass* createMetricsPass();
Pass* createMonomorphizePass();
Pass* createMonomorphizeAlwaysPass();
Pass* createMultiMemoryLoweringPass();
+Pass* createMultiMemoryLoweringWithBoundsChecksPass();
Pass* createNameListPass();
Pass* createNameTypesPass();
Pass* createOnceReductionPass();
diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test
index a5c21af8b..22e3baaf1 100644
--- a/test/lit/help/wasm-opt.test
+++ b/test/lit/help/wasm-opt.test
@@ -282,6 +282,11 @@
;; CHECK-NEXT: --multi-memory-lowering combines multiple memories into
;; CHECK-NEXT: a single memory
;; CHECK-NEXT:
+;; CHECK-NEXT: --multi-memory-lowering-with-bounds-checks combines multiple memories into
+;; CHECK-NEXT: a single memory, trapping if the
+;; CHECK-NEXT: read or write is larger than the
+;; CHECK-NEXT: length of the memory's data
+;; CHECK-NEXT:
;; CHECK-NEXT: --name-types (re)name all heap types
;; CHECK-NEXT:
;; CHECK-NEXT: --nm name list
diff --git a/test/lit/help/wasm2js.test b/test/lit/help/wasm2js.test
index 3073bf2f9..b901c253c 100644
--- a/test/lit/help/wasm2js.test
+++ b/test/lit/help/wasm2js.test
@@ -241,6 +241,11 @@
;; CHECK-NEXT: --multi-memory-lowering combines multiple memories into
;; CHECK-NEXT: a single memory
;; CHECK-NEXT:
+;; CHECK-NEXT: --multi-memory-lowering-with-bounds-checks combines multiple memories into
+;; CHECK-NEXT: a single memory, trapping if the
+;; CHECK-NEXT: read or write is larger than the
+;; CHECK-NEXT: length of the memory's data
+;; CHECK-NEXT:
;; CHECK-NEXT: --name-types (re)name all heap types
;; CHECK-NEXT:
;; CHECK-NEXT: --nm name list
diff --git a/test/lit/passes/multi-memory-lowering.wast b/test/lit/passes/multi-memory-lowering.wast
index fc402d877..d66db1b02 100644
--- a/test/lit/passes/multi-memory-lowering.wast
+++ b/test/lit/passes/multi-memory-lowering.wast
@@ -1,5 +1,6 @@
;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
;; RUN: wasm-opt %s --enable-multi-memories --multi-memory-lowering --enable-bulk-memory --enable-extended-const -S -o - | filecheck %s
+;; RUN: wasm-opt %s --enable-multi-memories --multi-memory-lowering-with-bounds-checks --enable-bulk-memory --enable-extended-const -S -o - | filecheck %s --check-prefix BOUNDS
(module
(memory $memory1 1)
@@ -49,6 +50,105 @@
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
+ ;; BOUNDS: (type $none_=>_i32 (func (result i32)))
+
+ ;; BOUNDS: (type $i32_=>_i32 (func (param i32) (result i32)))
+
+ ;; BOUNDS: (type $none_=>_none (func))
+
+ ;; BOUNDS: (global $memory2_byte_offset (mut i32) (i32.const 65536))
+
+ ;; BOUNDS: (global $memory3_byte_offset (mut i32) (i32.const 196608))
+
+ ;; BOUNDS: (memory $combined_memory 6)
+
+ ;; BOUNDS: (data (i32.const 0) "a")
+
+ ;; BOUNDS: (data (i32.add
+ ;; BOUNDS-NEXT: (global.get $memory3_byte_offset)
+ ;; BOUNDS-NEXT: (i32.const 1)
+ ;; BOUNDS-NEXT: ) "123")
+
+ ;; BOUNDS: (func $loads
+ ;; BOUNDS-NEXT: (local $0 i32)
+ ;; BOUNDS-NEXT: (local $1 i32)
+ ;; BOUNDS-NEXT: (local $2 i32)
+ ;; BOUNDS-NEXT: (drop
+ ;; BOUNDS-NEXT: (i32.load
+ ;; BOUNDS-NEXT: (block (result i32)
+ ;; BOUNDS-NEXT: (local.set $0
+ ;; BOUNDS-NEXT: (i32.const 10)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (if
+ ;; BOUNDS-NEXT: (i32.gt_u
+ ;; BOUNDS-NEXT: (i32.add
+ ;; BOUNDS-NEXT: (i32.add
+ ;; BOUNDS-NEXT: (local.get $0)
+ ;; BOUNDS-NEXT: (i32.const 0)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (i32.const 4)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (call $memory1_size)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (unreachable)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (local.get $0)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (drop
+ ;; BOUNDS-NEXT: (i32.load
+ ;; BOUNDS-NEXT: (block (result i32)
+ ;; BOUNDS-NEXT: (local.set $1
+ ;; BOUNDS-NEXT: (i32.add
+ ;; BOUNDS-NEXT: (global.get $memory2_byte_offset)
+ ;; BOUNDS-NEXT: (i32.const 11)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (if
+ ;; BOUNDS-NEXT: (i32.gt_u
+ ;; BOUNDS-NEXT: (i32.add
+ ;; BOUNDS-NEXT: (i32.add
+ ;; BOUNDS-NEXT: (local.get $1)
+ ;; BOUNDS-NEXT: (i32.const 0)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (i32.const 4)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (call $memory2_size)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (unreachable)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (local.get $1)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (drop
+ ;; BOUNDS-NEXT: (i32.load
+ ;; BOUNDS-NEXT: (block (result i32)
+ ;; BOUNDS-NEXT: (local.set $2
+ ;; BOUNDS-NEXT: (i32.add
+ ;; BOUNDS-NEXT: (global.get $memory3_byte_offset)
+ ;; BOUNDS-NEXT: (i32.const 12)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (if
+ ;; BOUNDS-NEXT: (i32.gt_u
+ ;; BOUNDS-NEXT: (i32.add
+ ;; BOUNDS-NEXT: (i32.add
+ ;; BOUNDS-NEXT: (local.get $2)
+ ;; BOUNDS-NEXT: (i32.const 0)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (i32.const 4)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (call $memory3_size)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (unreachable)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (local.get $2)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: )
(func $loads
(drop
(i32.load $memory1
@@ -86,6 +186,83 @@
;; CHECK-NEXT: (i32.const 115)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
+ ;; BOUNDS: (func $stores
+ ;; BOUNDS-NEXT: (local $0 i32)
+ ;; BOUNDS-NEXT: (local $1 i32)
+ ;; BOUNDS-NEXT: (local $2 i32)
+ ;; BOUNDS-NEXT: (i32.store
+ ;; BOUNDS-NEXT: (block (result i32)
+ ;; BOUNDS-NEXT: (local.set $0
+ ;; BOUNDS-NEXT: (i32.const 10)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (if
+ ;; BOUNDS-NEXT: (i32.gt_u
+ ;; BOUNDS-NEXT: (i32.add
+ ;; BOUNDS-NEXT: (i32.add
+ ;; BOUNDS-NEXT: (local.get $0)
+ ;; BOUNDS-NEXT: (i32.const 0)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (i32.const 4)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (call $memory1_size)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (unreachable)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (local.get $0)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (i32.const 115)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (i32.store
+ ;; BOUNDS-NEXT: (block (result i32)
+ ;; BOUNDS-NEXT: (local.set $1
+ ;; BOUNDS-NEXT: (i32.add
+ ;; BOUNDS-NEXT: (global.get $memory2_byte_offset)
+ ;; BOUNDS-NEXT: (i32.const 11)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (if
+ ;; BOUNDS-NEXT: (i32.gt_u
+ ;; BOUNDS-NEXT: (i32.add
+ ;; BOUNDS-NEXT: (i32.add
+ ;; BOUNDS-NEXT: (local.get $1)
+ ;; BOUNDS-NEXT: (i32.const 0)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (i32.const 4)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (call $memory2_size)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (unreachable)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (local.get $1)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (i32.const 115)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (i32.store
+ ;; BOUNDS-NEXT: (block (result i32)
+ ;; BOUNDS-NEXT: (local.set $2
+ ;; BOUNDS-NEXT: (i32.add
+ ;; BOUNDS-NEXT: (global.get $memory3_byte_offset)
+ ;; BOUNDS-NEXT: (i32.const 12)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (if
+ ;; BOUNDS-NEXT: (i32.gt_u
+ ;; BOUNDS-NEXT: (i32.add
+ ;; BOUNDS-NEXT: (i32.add
+ ;; BOUNDS-NEXT: (local.get $2)
+ ;; BOUNDS-NEXT: (i32.const 0)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (i32.const 4)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (call $memory3_size)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (unreachable)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (local.get $2)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: (i32.const 115)
+ ;; BOUNDS-NEXT: )
+ ;; BOUNDS-NEXT: )
(func $stores
(i32.store $memory1
(i32.const 10)
@@ -263,3 +440,165 @@
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.get $return_size)
;; CHECK-NEXT: )
+
+;; BOUNDS: (func $memory1_size (result i32)
+;; BOUNDS-NEXT: (return
+;; BOUNDS-NEXT: (i32.div_u
+;; BOUNDS-NEXT: (global.get $memory2_byte_offset)
+;; BOUNDS-NEXT: (i32.const 65536)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: )
+
+;; BOUNDS: (func $memory2_size (result i32)
+;; BOUNDS-NEXT: (return
+;; BOUNDS-NEXT: (i32.sub
+;; BOUNDS-NEXT: (i32.div_u
+;; BOUNDS-NEXT: (global.get $memory3_byte_offset)
+;; BOUNDS-NEXT: (i32.const 65536)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (i32.div_u
+;; BOUNDS-NEXT: (global.get $memory2_byte_offset)
+;; BOUNDS-NEXT: (i32.const 65536)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: )
+
+;; BOUNDS: (func $memory3_size (result i32)
+;; BOUNDS-NEXT: (return
+;; BOUNDS-NEXT: (i32.sub
+;; BOUNDS-NEXT: (memory.size)
+;; BOUNDS-NEXT: (i32.div_u
+;; BOUNDS-NEXT: (global.get $memory3_byte_offset)
+;; BOUNDS-NEXT: (i32.const 65536)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: )
+
+;; BOUNDS: (func $memory1_grow (param $page_delta i32) (result i32)
+;; BOUNDS-NEXT: (local $return_size i32)
+;; BOUNDS-NEXT: (local $memory_size i32)
+;; BOUNDS-NEXT: (local.set $return_size
+;; BOUNDS-NEXT: (call $memory1_size)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (local.set $memory_size
+;; BOUNDS-NEXT: (memory.size)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (if
+;; BOUNDS-NEXT: (i32.eq
+;; BOUNDS-NEXT: (memory.grow
+;; BOUNDS-NEXT: (local.get $page_delta)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (i32.const -1)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (return
+;; BOUNDS-NEXT: (i32.const -1)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (memory.copy
+;; BOUNDS-NEXT: (i32.add
+;; BOUNDS-NEXT: (global.get $memory2_byte_offset)
+;; BOUNDS-NEXT: (i32.mul
+;; BOUNDS-NEXT: (local.get $page_delta)
+;; BOUNDS-NEXT: (i32.const 65536)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (global.get $memory2_byte_offset)
+;; BOUNDS-NEXT: (i32.sub
+;; BOUNDS-NEXT: (i32.mul
+;; BOUNDS-NEXT: (local.get $memory_size)
+;; BOUNDS-NEXT: (i32.const 65536)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (global.get $memory2_byte_offset)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (global.set $memory2_byte_offset
+;; BOUNDS-NEXT: (i32.add
+;; BOUNDS-NEXT: (global.get $memory2_byte_offset)
+;; BOUNDS-NEXT: (i32.mul
+;; BOUNDS-NEXT: (local.get $page_delta)
+;; BOUNDS-NEXT: (i32.const 65536)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (global.set $memory3_byte_offset
+;; BOUNDS-NEXT: (i32.add
+;; BOUNDS-NEXT: (global.get $memory3_byte_offset)
+;; BOUNDS-NEXT: (i32.mul
+;; BOUNDS-NEXT: (local.get $page_delta)
+;; BOUNDS-NEXT: (i32.const 65536)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (local.get $return_size)
+;; BOUNDS-NEXT: )
+
+;; BOUNDS: (func $memory2_grow (param $page_delta i32) (result i32)
+;; BOUNDS-NEXT: (local $return_size i32)
+;; BOUNDS-NEXT: (local $memory_size i32)
+;; BOUNDS-NEXT: (local.set $return_size
+;; BOUNDS-NEXT: (call $memory2_size)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (local.set $memory_size
+;; BOUNDS-NEXT: (memory.size)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (if
+;; BOUNDS-NEXT: (i32.eq
+;; BOUNDS-NEXT: (memory.grow
+;; BOUNDS-NEXT: (local.get $page_delta)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (i32.const -1)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (return
+;; BOUNDS-NEXT: (i32.const -1)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (memory.copy
+;; BOUNDS-NEXT: (i32.add
+;; BOUNDS-NEXT: (global.get $memory3_byte_offset)
+;; BOUNDS-NEXT: (i32.mul
+;; BOUNDS-NEXT: (local.get $page_delta)
+;; BOUNDS-NEXT: (i32.const 65536)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (global.get $memory3_byte_offset)
+;; BOUNDS-NEXT: (i32.sub
+;; BOUNDS-NEXT: (i32.mul
+;; BOUNDS-NEXT: (local.get $memory_size)
+;; BOUNDS-NEXT: (i32.const 65536)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (global.get $memory3_byte_offset)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (global.set $memory3_byte_offset
+;; BOUNDS-NEXT: (i32.add
+;; BOUNDS-NEXT: (global.get $memory3_byte_offset)
+;; BOUNDS-NEXT: (i32.mul
+;; BOUNDS-NEXT: (local.get $page_delta)
+;; BOUNDS-NEXT: (i32.const 65536)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (local.get $return_size)
+;; BOUNDS-NEXT: )
+
+;; BOUNDS: (func $memory3_grow (param $page_delta i32) (result i32)
+;; BOUNDS-NEXT: (local $return_size i32)
+;; BOUNDS-NEXT: (local.set $return_size
+;; BOUNDS-NEXT: (call $memory3_size)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (if
+;; BOUNDS-NEXT: (i32.eq
+;; BOUNDS-NEXT: (memory.grow
+;; BOUNDS-NEXT: (local.get $page_delta)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (i32.const -1)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (return
+;; BOUNDS-NEXT: (i32.const -1)
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: )
+;; BOUNDS-NEXT: (local.get $return_size)
+;; BOUNDS-NEXT: )