v8/test/mjsunit/wasm/grow-memory.js
Clemens Backes d6e2554d11 [wasm] Fix memory growth near the maximum
If we grow memory (out-of-place, so only without trap handling and only
if the maximum is >1GB) and the previous size is close to the maximum,
then the minimum growth we calculate can be bigger than the allowed
maximum. In this situation, the {std::clamp} has undefined behaviour,
since the provided lower limit is bigger then the upper limit.

Thus apply {std::min} and {std::max} in an order such that {max_pages}
has precedence over {min_growth}.

R=thibaudm@chromium.org

Bug: chromium:1348335
Change-Id: I4f9e9ce10a0685892248eaf0e06ffd2e84b9a069
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3793396
Commit-Queue: Clemens Backes <clemensb@chromium.org>
Reviewed-by: Thibaud Michaud <thibaudm@chromium.org>
Cr-Commit-Position: refs/heads/main@{#82081}
2022-07-29 14:39:19 +00:00

479 lines
15 KiB
JavaScript

// Copyright 2016 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --expose-wasm --stress-compaction
d8.file.execute("test/mjsunit/wasm/wasm-module-builder.js");
function genMemoryGrowBuilder() {
var builder = new WasmModuleBuilder();
builder.addFunction("grow_memory", kSig_i_i)
.addBody([kExprLocalGet, 0, kExprMemoryGrow, kMemoryZero])
.exportFunc();
builder.addFunction("load", kSig_i_i)
.addBody([kExprLocalGet, 0, kExprI32LoadMem, 0, 0])
.exportFunc();
builder.addFunction("store", kSig_i_ii)
.addBody([kExprLocalGet, 0, kExprLocalGet, 1, kExprI32StoreMem, 0, 0,
kExprLocalGet, 1])
.exportFunc();
builder.addFunction("load16", kSig_i_i)
.addBody([kExprLocalGet, 0, kExprI32LoadMem16U, 0, 0])
.exportFunc();
builder.addFunction("store16", kSig_i_ii)
.addBody([kExprLocalGet, 0, kExprLocalGet, 1, kExprI32StoreMem16, 0, 0,
kExprLocalGet, 1])
.exportFunc();
builder.addFunction("load8", kSig_i_i)
.addBody([kExprLocalGet, 0, kExprI32LoadMem8U, 0, 0])
.exportFunc();
builder.addFunction("store8", kSig_i_ii)
.addBody([kExprLocalGet, 0, kExprLocalGet, 1, kExprI32StoreMem8, 0, 0,
kExprLocalGet, 1])
.exportFunc();
return builder;
}
// V8 internal memory size limit.
var kV8MaxPages = 65536;
function testMemoryGrowReadWriteBase(size, load_fn, store_fn) {
// size is the number of bytes for load and stores.
var builder = genMemoryGrowBuilder();
builder.addMemory(1, undefined, false);
var module = builder.instantiate();
var offset;
var load = module.exports[load_fn];
var store = module.exports[store_fn];
function peek() { return load(offset); }
function poke(value) { return store(offset, value); }
function growMem(pages) { return module.exports.grow_memory(pages); }
// Instead of checking every n-th offset, check the first 5.
for(offset = 0; offset <= (4*size); offset+=size) {
poke(20);
assertEquals(20, peek());
}
for (offset = kPageSize - (size - 1); offset < kPageSize + size; offset++) {
assertTraps(kTrapMemOutOfBounds, poke);
assertTraps(kTrapMemOutOfBounds, peek);
}
assertEquals(1, growMem(3));
for (let n = 1; n <= 3; n++) {
for (offset = n * kPageSize - 5 * size; offset <= n * kPageSize + 4 * size;
offset += size) {
// Check the 5 offsets to the before and after the n-th page.
// page n-1 page n page n+1
// +---- ... ------------+---------- ... +------ ...
// | | | ... | | | | | | | | | | | | ... | | | | ...
// <+> ^ ^
// | first offset last offset
// +-> size bytes
poke(20);
assertEquals(20, peek());
}
}
// Check the last 5 valid offsets of the last page.
for (offset = 4*kPageSize-size-(4*size); offset <= 4*kPageSize -size; offset+=size) {
poke(20);
assertEquals(20, peek());
}
for (offset = 4*kPageSize - (size-1); offset < 4*kPageSize + size; offset++) {
assertTraps(kTrapMemOutOfBounds, poke);
assertTraps(kTrapMemOutOfBounds, peek);
}
assertEquals(4, growMem(15));
for (offset = 4*kPageSize - (size-1); offset <= 4*kPageSize + size; offset+=size) {
poke(20);
assertEquals(20, peek());
}
for (offset = 19*kPageSize - 10; offset <= 19*kPageSize - size; offset+=size) {
poke(20);
assertEquals(20, peek());
}
for (offset = 19*kPageSize - (size-1); offset < 19*kPageSize + 5; offset++) {
assertTraps(kTrapMemOutOfBounds, poke);
assertTraps(kTrapMemOutOfBounds, peek);
}
}
(function testMemoryGrowReadWrite32() {
print(arguments.callee.name);
testMemoryGrowReadWriteBase(4, "load", "store");
})();
(function testMemoryGrowReadWrite16() {
print(arguments.callee.name);
testMemoryGrowReadWriteBase(2, "load16", "store16");
})();
(function testMemoryGrowReadWrite8() {
print(arguments.callee.name);
testMemoryGrowReadWriteBase(1, "load8", "store8");
})();
(function testMemoryGrowZeroInitialSize() {
print(arguments.callee.name);
var builder = genMemoryGrowBuilder();
builder.addMemory(0, undefined, false);
var module = builder.instantiate();
var offset;
function peek() { return module.exports.load(offset); }
function poke(value) { return module.exports.store(offset, value); }
function growMem(pages) { return module.exports.grow_memory(pages); }
assertTraps(kTrapMemOutOfBounds, peek);
assertTraps(kTrapMemOutOfBounds, poke);
assertEquals(0, growMem(1));
// Check first 5 offsets.
for(offset = 0; offset <= 5; offset++) {
poke(20);
assertEquals(20, peek());
}
// Check last 5 offsets.
for(offset = kPageSize - 5*4; offset <= kPageSize - 4; offset++) {
poke(20);
assertEquals(20, peek());
}
for(offset = kPageSize - 3; offset <= kPageSize + 5; offset++) {
assertTraps(kTrapMemOutOfBounds, peek);
}
offset = 3*kPageSize;
for (var i = 1; i < 4; i++) {
assertTraps(kTrapMemOutOfBounds, poke);
assertEquals(i, growMem(1));
}
poke(20);
assertEquals(20, peek());
})();
function testMemoryGrowZeroInitialSizeBase(size, load_fn, store_fn) {
var builder = genMemoryGrowBuilder();
builder.addMemory(0, undefined, false);
var module = builder.instantiate();
var offset;
var load = module.exports[load_fn];
var store = module.exports[store_fn];
function peek() { return load(offset); }
function poke(value) { return store(offset, value); }
function growMem(pages) { return module.exports.grow_memory(pages); }
assertTraps(kTrapMemOutOfBounds, peek);
assertTraps(kTrapMemOutOfBounds, poke);
assertEquals(0, growMem(1));
// Instead of checking every offset, check the first 5.
for(offset = 0; offset <= 4; offset++) {
poke(20);
assertEquals(20, peek());
}
// Check the last 5 valid ones.
for(offset = kPageSize - (size * 4); offset <= kPageSize - size; offset++) {
poke(20);
assertEquals(20, peek());
}
for(offset = kPageSize - (size - 1); offset <= kPageSize + 5; offset++) {
assertTraps(kTrapMemOutOfBounds, peek);
}
}
(function testMemoryGrowZeroInitialSize32() {
print(arguments.callee.name);
testMemoryGrowZeroInitialSizeBase(4, "load", "store");
})();
(function testMemoryGrowZeroInitialSize16() {
print(arguments.callee.name);
testMemoryGrowZeroInitialSizeBase(2, "load16", "store16");
})();
(function testMemoryGrowZeroInitialSize8() {
print(arguments.callee.name);
testMemoryGrowZeroInitialSizeBase(1, "load8", "store8");
})();
(function testMemoryGrowTrapMaxPagesZeroInitialMemory() {
print(arguments.callee.name);
var builder = genMemoryGrowBuilder();
builder.addMemory(0, undefined, false);
var module = builder.instantiate();
function growMem(pages) { return module.exports.grow_memory(pages); }
assertEquals(-1, growMem(kV8MaxPages + 1));
})();
(function testMemoryGrowTrapMaxPages() {
print(arguments.callee.name);
var builder = genMemoryGrowBuilder();
builder.addMemory(1, 1, false);
var module = builder.instantiate();
function growMem(pages) { return module.exports.grow_memory(pages); }
assertEquals(-1, growMem(kV8MaxPages));
})();
(function testMemoryGrowTrapsWithNonSmiInput() {
print(arguments.callee.name);
var builder = genMemoryGrowBuilder();
builder.addMemory(0, undefined, false);
var module = builder.instantiate();
function growMem(pages) { return module.exports.grow_memory(pages); }
// The parameter of grow_memory is unsigned. Therefore -1 stands for
// UINT32_MIN, which cannot be represented as SMI.
assertEquals(-1, growMem(-1));
})();
(function testMemoryGrowCurrentMemory() {
print(arguments.callee.name);
var builder = genMemoryGrowBuilder();
builder.addMemory(1, undefined, false);
builder.addFunction("memory_size", kSig_i_v)
.addBody([kExprMemorySize, kMemoryZero])
.exportFunc();
var module = builder.instantiate();
function growMem(pages) { return module.exports.grow_memory(pages); }
function MemSize() { return module.exports.memory_size(); }
assertEquals(1, MemSize());
assertEquals(1, growMem(1));
assertEquals(2, MemSize());
})();
function testMemoryGrowPreservesDataMemOpBase(size, load_fn, store_fn) {
var builder = genMemoryGrowBuilder();
builder.addMemory(1, undefined, false);
var module = builder.instantiate();
var offset;
var load = module.exports[load_fn];
var store = module.exports[store_fn];
function peek() { return load(offset); }
function poke(value) { return store(offset, value); }
function growMem(pages) { return module.exports.grow_memory(pages); }
// Maximum unsigned integer of size bits.
const max = Math.pow(2, (size * 8)) - 1;
// Check the first 5 offsets.
for(offset = 0; offset <= (4*size); offset+=size) {
poke(offset % max);
assertEquals(offset % max, peek());
}
// Check the last 5 valid offsets.
for(offset = kPageSize - 5*size; offset <= (kPageSize - size); offset+=size) {
poke(offset % max);
assertEquals(offset % max, peek());
}
assertEquals(1, growMem(3));
// Check the first 5 offsets are preserved by growMem.
for(offset = 0; offset <= (4*size); offset+=size) {
assertEquals(offset % max, peek());
}
// Check the last 5 valid offsets are preserved by growMem.
for(offset = kPageSize - 5*size; offset <= (kPageSize - size); offset+=size) {
assertEquals(offset % max, peek());
}
}
(function testMemoryGrowPreservesDataMemOp32() {
print(arguments.callee.name);
testMemoryGrowPreservesDataMemOpBase(4, "load", "store");
})();
(function testMemoryGrowPreservesDataMemOp16() {
print(arguments.callee.name);
testMemoryGrowPreservesDataMemOpBase(2, "load16", "store16");
})();
(function testMemoryGrowPreservesDataMemOp8() {
print(arguments.callee.name);
testMemoryGrowPreservesDataMemOpBase(1, "load8", "store8");
})();
(function testMemoryGrowOutOfBoundsOffset() {
print(arguments.callee.name);
var builder = genMemoryGrowBuilder();
builder.addMemory(1, undefined, false);
var module = builder.instantiate();
var offset, val;
function peek() { return module.exports.load(offset); }
function poke(value) { return module.exports.store(offset, value); }
function growMem(pages) { return module.exports.grow_memory(pages); }
offset = 3*kPageSize + 4;
assertTraps(kTrapMemOutOfBounds, poke);
assertEquals(1, growMem(1));
assertTraps(kTrapMemOutOfBounds, poke);
assertEquals(2, growMem(1));
assertTraps(kTrapMemOutOfBounds, poke);
assertEquals(3, growMem(1));
for (offset = 3*kPageSize; offset <= 3*kPageSize + 4; offset++) {
poke(0xaced);
assertEquals(0xaced, peek());
}
for (offset = 4*kPageSize-8; offset <= 4*kPageSize - 4; offset++) {
poke(0xaced);
assertEquals(0xaced, peek());
}
for (offset = 4*kPageSize - 3; offset <= 4*kPageSize + 4; offset++) {
assertTraps(kTrapMemOutOfBounds, poke);
}
})();
(function testMemoryGrowOutOfBoundsOffset2() {
print(arguments.callee.name);
var builder = new WasmModuleBuilder();
builder.addMemory(16, 128, false);
builder.addFunction("main", kSig_v_v)
.addBody([
kExprI32Const, 20,
kExprI32Const, 29,
kExprMemoryGrow, kMemoryZero,
kExprI32StoreMem, 0, 0xFF, 0xFF, 0xFF, 0x3a
])
.exportAs("main");
var module = builder.instantiate();
assertTraps(kTrapMemOutOfBounds, module.exports.main);
})();
(function testMemoryGrowDeclaredMaxTraps() {
print(arguments.callee.name);
var builder = genMemoryGrowBuilder();
builder.addMemory(1, 16, false);
var module = builder.instantiate();
function growMem(pages) { return module.exports.grow_memory(pages); }
assertEquals(1, growMem(5));
assertEquals(6, growMem(5));
assertEquals(-1, growMem(6));
})();
(function testMemoryGrowInternalMaxTraps() {
print(arguments.callee.name);
// This test checks that grow_memory does not grow past the internally
// defined maximum memory size.
var builder = genMemoryGrowBuilder();
builder.addMemory(1, kSpecMaxPages, false);
var module = builder.instantiate();
function growMem(pages) { return module.exports.grow_memory(pages); }
assertEquals(1, growMem(20));
assertEquals(-1, growMem(kV8MaxPages - 20));
})();
(function testMemoryGrow4Gb() {
print(arguments.callee.name);
var builder = genMemoryGrowBuilder();
builder.addMemory(1, undefined, false);
var module = builder.instantiate();
var offset, val;
function peek() { return module.exports.load(offset); }
function poke(value) { return module.exports.store(offset, value); }
function growMem(pages) { return module.exports.grow_memory(pages); }
// Check first 5 offsets.
for (offset = 0; offset <= 4 * 4; offset += 4) {
poke(100000 - offset);
assertEquals(100000 - offset, peek());
}
// Check last 5 offsets.
for (offset = (kPageSize - 5 * 4); offset <= (kPageSize - 4); offset += 4) {
poke(100000 - offset);
assertEquals(100000 - offset, peek());
}
let result = growMem(kV8MaxPages - 1);
if (result == 1) {
// Check first 5 offsets.
for (offset = 0; offset <= 4 * 4; offset += 4) {
assertEquals(100000 - offset, peek());
}
// Check last 5 offsets.
for (offset = (kPageSize - 5 * 4); offset <= (kPageSize - 4); offset += 4) {
assertEquals(100000 - offset, peek());
}
// Bounds check for large mem size.
let kMemSize = (kV8MaxPages * kPageSize);
let kLastValidOffset = kMemSize - 4; // Accommodate a 4-byte read/write.
// Check first 5 offsets of last page.
for (offset = kMemSize - kPageSize; offset <= kMemSize - kPageSize + 4 * 4;
offset += 4) {
poke(0xaced);
assertEquals(0xaced, peek());
}
for (offset = kLastValidOffset - 5 * 4; offset <= kLastValidOffset;
offset += 4) {
poke(0xaced);
assertEquals(0xaced, peek());
}
for (offset = kLastValidOffset + 1; offset < kMemSize; offset++) {
assertTraps(kTrapMemOutOfBounds, poke);
}
} else {
// Allocating big chunks of memory can fail on gc_stress, especially on 32
// bit platforms. When grow_memory fails, expected result is -1.
assertEquals(-1, result);
}
})();
(function testGrowFromNearlyMaximum() {
print(arguments.callee.name);
// Regression test for https://crbug.com/1347668.
const builder = genMemoryGrowBuilder();
// The maximum needs to be >1GB, so we do not reserve everything upfront.
const GB = 1024 * 1024 * 1024;
const max_pages = 1 * GB / kPageSize + 10;
builder.addMemory(0, max_pages);
let module;
const is_oom = e =>
(e instanceof RangeError) && e.message.includes('Out of memory');
// Allow instantiation to fail with OOM.
try {
module = builder.instantiate();
} catch (e) {
if (is_oom(e)) return;
// Everything else is a bug.
throw e;
}
const grow = module.exports.grow_memory;
// First, grow close to the limit.
// Growing can always fail if the system runs out of resources.
let grow_result = grow(max_pages - 1);
if (grow_result == -1) return;
assertEquals(0, grow_result);
// Then, grow by another page (this triggered the error in
// https://crbug.com/1347668).
grow_result = grow(1);
if (grow_result == -1) return;
assertEquals(max_pages - 1, grow_result);
assertEquals(max_pages, grow(0));
assertEquals(-1, grow(1)); // Fails.
})();