Skip to content

Commit 2c2c1af

Browse files
committed
[JSC] Overhaul ArrayBuffer & Wasm::Memory sizing with memory64
https://bugs.webkit.org/show_bug.cgi?id=321182 rdar://184241045 Reviewed by Yijia Huang. Currently memory64 crashes when growing a shared buffer more than 4GB. We have an inconsistency in our current implementation in terms of size. ArrayBuffer's maximum size is right now 4GB, but Wasm::Memory's maximum reserved size is 2^37-1 pages, which is way more than the actually usable memory (8PB). This patch overhauls the design and the limit of ArrayBuffer / Wasm::Memory sizing. 1. Now ArrayBuffer max size is bumped from 4GB to 16GB, aligning to V8's number. Also this 16GB is aligned to the spec's limit size[1]. "The maximum size of a 64-bit memory is 262,144 pages (16 GiB)." Because of our LARGE_TYPED_ARRAYS work, size of ArrayBuffer can be larger than 4GB already, so changing this is not hard. On 32bit environment, no threshold change is done. 2. We introduce a limit for memory32 and memory64 based on the spec[1]. 4GB for memory32 and 16GB for memory64. 3. We also found that some of WebCore side is truncating size of ArrayBuffer to `unsinged` (32bit) while it can be 64bit size before this change. They are semantically incorrect but it is fine (just only handling small sized part), but anyway, fixing them. 4. Also avoid calling GC while taking a lock of BufferMemoryHandle. 5. This also revealed many of WebGL issues too, where it is not using `unsinged long long` while spec requires so. This patch fixes them too as they started becoming failures after ArrayBuffer can get larger size than 32bit. Three web-visible behavior changes fall out of this: 1. A memory64 may now declare a maximum of at most 262144 pages, down from PageCount's own 2^37-1. A module declaring more used to compile and instantiate at its initial size and now fails WebAssembly.Module(), which matches V8's kV8MaxWasmMemory64Pages. JSTests/wasm/v8/memory64.js's two tests for this are re-enabled. 2. A memory's buffer reports the maximum this platform could actually grow it to, rather than the one the address type permits declaring, so that a resize within maxByteLength never fails deterministically. The two differ wherever the Primitive address space budget is the tighter bound. 3. RTCPeerConnection.generateCertificate() reads publicExponent as the big endian WebCrypto BigInteger it is, rather than little endian, and accepts arbitrary leading zero padding. WebSocket.send() now fails the connection for a binary payload too large to frame instead of dropping it or crashing. [1]: https://www.w3.org/TR/wasm-js-api-2/#limits Tests: JSTests/stress/array-buffer-slice-larger-than-4gb.js JSTests/wasm/stress/memory64-grow-past-4gb.js JSTests/wasm/stress/memory64-maximum-limits.js webrtc/generate-certificate-public-exponent.html * JSTests/stress/array-buffer-slice-larger-than-4gb.js: Added. (shouldBe): * JSTests/stress/shared-array-buffer-large-maxbytelength.js: (catch): * JSTests/stress/typedarray-canonical-numeric-index-string-past-max-array-index.js: Added. (shouldBe): (const.key.of.keys.array.key.valueOf): * JSTests/stress/typedarray-index-past-max-array-index.js: Added. (shouldBe): (catch): (array.undefined.String): * JSTests/wasm/js-api/memory-toResizableBuffer.js: (assertTrue): * JSTests/wasm/js-api/memory64-js-api-errors.js: (assert.throws): * JSTests/wasm/js-api/memory64-js-api.js: (assert.throws): * JSTests/wasm/stress/memory64-grow-past-4gb.js: Added. (catch): (true.try.mem.await.instantiate.module.memory): (true.catch): (true.canHostPastFourGiB.catch): (assert.throws.grow): * JSTests/wasm/stress/memory64-maximum-limits.js: Added. (true.try.mem.await.instantiate.module.memory): (true.catch): * JSTests/wasm/stress/memory64-oversized-limits.js: (moduleBytesWithMemoryLimits): * JSTests/wasm/v8/memory64.js: (BasicMemory64Tests): (TestBulkMemoryOperations): Deleted. * LayoutTests/fast/canvas/webgl/array-unit-tests.html: * LayoutTests/fast/canvas/webgl/webgl-array-invalid-ranges.html: * LayoutTests/fast/canvas/webgl/webgl2-array-buffer-view-offset-past-32-bits-expected.txt: Added. * LayoutTests/fast/canvas/webgl/webgl2-array-buffer-view-offset-past-32-bits.html: Added. * LayoutTests/http/tests/websocket/tests/hybi/bufferedAmount-past-32-bits-after-close-expected.txt: Added. * LayoutTests/http/tests/websocket/tests/hybi/bufferedAmount-past-32-bits-after-close.html: Added. * LayoutTests/webrtc/generate-certificate-public-exponent-expected.txt: Added. * LayoutTests/webrtc/generate-certificate-public-exponent.html: Added. * Source/JavaScriptCore/runtime/ArrayBuffer.cpp: (JSC::tryAllocateResizableMemory): (JSC::ArrayBuffer::resize): (JSC::SharedArrayBufferContents::grow): (JSC::SharedArrayBufferContents::tryGrow): * Source/JavaScriptCore/runtime/ArrayBuffer.h: * Source/JavaScriptCore/runtime/BufferMemoryHandle.cpp: (JSC::BufferMemoryHandle::fastMappedBytes): * Source/JavaScriptCore/runtime/BufferMemoryHandle.h: * Source/JavaScriptCore/runtime/JSArrayBufferPrototype.cpp: (JSC::arrayBufferSlice): * Source/JavaScriptCore/runtime/JSGenericTypedArrayViewInlines.h: (JSC::JSGenericTypedArrayView<Adaptor>::getOwnPropertySlot): (JSC::JSGenericTypedArrayView<Adaptor>::put): (JSC::JSGenericTypedArrayView<Adaptor>::defineOwnProperty): (JSC::JSGenericTypedArrayView<Adaptor>::deleteProperty): * Source/JavaScriptCore/runtime/PageCount.h: * Source/JavaScriptCore/runtime/PropertyName.h: (JSC::isCanonicalNumericIndexString): * Source/JavaScriptCore/wasm/WasmAddressType.cpp: * Source/JavaScriptCore/wasm/WasmAddressType.h: (JSC::Wasm::AddressType::AddressType): (JSC::Wasm::AddressType::is64Bit const): * Source/JavaScriptCore/wasm/WasmLimits.h: (JSC::Wasm::maxDeclarablePages): (JSC::Wasm::maxBufferByteLength): (JSC::Wasm::static_assert): * Source/JavaScriptCore/wasm/WasmMemory.cpp: (JSC::Wasm::maxAllocatableBytes): (JSC::Wasm::Memory::tryCreate): (JSC::Wasm::Memory::growShared): (JSC::Wasm::Memory::grow): * Source/JavaScriptCore/wasm/WasmMemory.h: * Source/JavaScriptCore/wasm/WasmSectionParser.cpp: (JSC::Wasm::SectionParser::parseMemoryHelper): * Source/JavaScriptCore/wasm/js/JSWebAssemblyMemory.cpp: (JSC::JSWebAssemblyMemory::associateArrayBuffer): * Source/JavaScriptCore/wasm/js/WebAssemblyMemoryConstructor.cpp: (JSC::WebAssemblyMemoryConstructor::createMemoryFromDescriptor): * Source/WebCore/Modules/mediastream/RTCPeerConnection.cpp: (WebCore::certificateTypeFromAlgorithmIdentifier): * Source/WebCore/Modules/websockets/ThreadableWebSocketChannel.h: * Source/WebCore/Modules/websockets/ThreadableWebSocketChannelClientWrapper.cpp: (WebCore::ThreadableWebSocketChannelClientWrapper::didUpdateBufferedAmount): (WebCore::ThreadableWebSocketChannelClientWrapper::didClose): * Source/WebCore/Modules/websockets/ThreadableWebSocketChannelClientWrapper.h: * Source/WebCore/Modules/websockets/WebSocket.cpp: (WebCore::saturateAdd): (WebCore::isFramablePayloadSize): (WebCore::WebSocket::send): (WebCore::WebSocket::bufferedAmount const): (WebCore::WebSocket::didUpdateBufferedAmount): (WebCore::WebSocket::didClose): * Source/WebCore/Modules/websockets/WebSocket.h: * Source/WebCore/Modules/websockets/WebSocket.idl: * Source/WebCore/Modules/websockets/WebSocketChannelClient.h: * Source/WebCore/Modules/websockets/WorkerThreadableWebSocketChannel.cpp: (WebCore::WorkerThreadableWebSocketChannel::send): (WebCore::WorkerThreadableWebSocketChannel::Peer::didUpdateBufferedAmount): (WebCore::WorkerThreadableWebSocketChannel::Peer::didClose): (WebCore::WorkerThreadableWebSocketChannel::Bridge::send): * Source/WebCore/Modules/websockets/WorkerThreadableWebSocketChannel.h: * Source/WebCore/css/FontFace.cpp: (WebCore::FontFace::create): * Source/WebCore/fileapi/NetworkSendQueue.cpp: (WebCore::NetworkSendQueue::enqueue): * Source/WebCore/fileapi/NetworkSendQueue.h: * Source/WebCore/html/canvas/WebGL2RenderingContext.cpp: (WebCore::WebGL2RenderingContext::arrayBufferViewSliceFactory): (WebCore::WebGL2RenderingContext::sliceArrayBufferView): (WebCore::WebGL2RenderingContext::bufferData): (WebCore::WebGL2RenderingContext::bufferSubData): (WebCore::WebGL2RenderingContext::getBufferSubData): (WebCore::WebGL2RenderingContext::texImage2D): (WebCore::WebGL2RenderingContext::texImage3D): (WebCore::WebGL2RenderingContext::texSubImage2D): (WebCore::WebGL2RenderingContext::texSubImage3D): (WebCore::WebGL2RenderingContext::compressedTexImage2D): (WebCore::WebGL2RenderingContext::compressedTexImage3D): (WebCore::WebGL2RenderingContext::compressedTexSubImage2D): (WebCore::WebGL2RenderingContext::compressedTexSubImage3D): (WebCore::WebGL2RenderingContext::uniform1uiv): (WebCore::WebGL2RenderingContext::uniform2uiv): (WebCore::WebGL2RenderingContext::uniform3uiv): (WebCore::WebGL2RenderingContext::uniform4uiv): (WebCore::WebGL2RenderingContext::uniformMatrix2x3fv): (WebCore::WebGL2RenderingContext::uniformMatrix3x2fv): (WebCore::WebGL2RenderingContext::uniformMatrix2x4fv): (WebCore::WebGL2RenderingContext::uniformMatrix4x2fv): (WebCore::WebGL2RenderingContext::uniformMatrix3x4fv): (WebCore::WebGL2RenderingContext::uniformMatrix4x3fv): (WebCore::WebGL2RenderingContext::vertexAttribI4iv): (WebCore::WebGL2RenderingContext::vertexAttribI4uiv): (WebCore::WebGL2RenderingContext::clearBufferiv): (WebCore::WebGL2RenderingContext::clearBufferuiv): (WebCore::WebGL2RenderingContext::clearBufferfv): (WebCore::WebGL2RenderingContext::validateClearBuffer): (WebCore::WebGL2RenderingContext::uniform1fv): (WebCore::WebGL2RenderingContext::uniform2fv): (WebCore::WebGL2RenderingContext::uniform3fv): (WebCore::WebGL2RenderingContext::uniform4fv): (WebCore::WebGL2RenderingContext::uniform1iv): (WebCore::WebGL2RenderingContext::uniform2iv): (WebCore::WebGL2RenderingContext::uniform3iv): (WebCore::WebGL2RenderingContext::uniform4iv): (WebCore::WebGL2RenderingContext::uniformMatrix2fv): (WebCore::WebGL2RenderingContext::uniformMatrix3fv): (WebCore::WebGL2RenderingContext::uniformMatrix4fv): (WebCore::WebGL2RenderingContext::readPixels): * Source/WebCore/html/canvas/WebGL2RenderingContext.h: * Source/WebCore/html/canvas/WebGL2RenderingContext.idl: * Source/WebCore/html/canvas/WebGLMultiDraw.cpp: (WebCore::WebGLMultiDraw::validateOffset): * Source/WebCore/html/canvas/WebGLMultiDraw.h: * Source/WebCore/html/canvas/WebGLMultiDrawInstancedBaseVertexBaseInstance.cpp: (WebCore::WebGLMultiDrawInstancedBaseVertexBaseInstance::validateOffset): * Source/WebCore/html/canvas/WebGLMultiDrawInstancedBaseVertexBaseInstance.h: * Source/WebCore/html/canvas/WebGLRenderingContextBase.cpp: (WebCore::WebGLRenderingContextBase::texImageArrayBufferViewHelper): (WebCore::WebGLRenderingContextBase::validateTexFuncData): (WebCore::WebGLRenderingContextBase::validateUniformMatrixParameters): (WebCore::WebGLRenderingContextBase::vertexAttribfvImpl): * Source/WebCore/html/canvas/WebGLRenderingContextBase.h: (WebCore::WebGLRenderingContextBase::TypedList::length const): (WebCore::WebGLRenderingContextBase::validateUniformParameters): * Source/WebCore/platform/graphics/iso/ISOBox.cpp: (WebCore::isWalkableView): (WebCore::ISOBox::peekBox): (WebCore::ISOBox::parse): * Source/WebKit/WebProcess/Network/WebSocketChannel.cpp: (WebKit::WebSocketChannel::send): * Source/WebKit/WebProcess/Network/WebSocketChannel.h: * Source/WebKitLegacy/WebCoreSupport/WebSocketChannel.cpp: (WebCore::WebSocketChannel::send): * Source/WebKitLegacy/WebCoreSupport/WebSocketChannel.h: * Source/bmalloc/bmalloc/Gigacage.h: Canonical link: https://commits.webkit.org/318784@main
1 parent 411d06d commit 2c2c1af

63 files changed

Lines changed: 1248 additions & 477 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//@ memoryHog!
2+
//@ skip if $addressBits <= 32
3+
//@ runDefault
4+
5+
function shouldBe(actual, expected) {
6+
if (actual !== expected)
7+
throw new Error(`bad value: expected ${expected} but got ${actual}`);
8+
}
9+
10+
const gib = 1024 * 1024 * 1024;
11+
const size = 5 * gib;
12+
13+
for (const ArrayBufferClass of [ArrayBuffer, SharedArrayBuffer]) {
14+
const buffer = new ArrayBufferClass(size);
15+
shouldBe(buffer.byteLength, size);
16+
17+
new Uint8Array(buffer).set([1, 2, 3, 4], size - 4);
18+
19+
// Slicing only a window at the end keeps the copy small while still forcing the byte length
20+
// itself through the argument clamping.
21+
shouldBe(buffer.slice(size - 4).byteLength, 4);
22+
shouldBe([...new Uint8Array(buffer.slice(size - 4))].join(), "1,2,3,4");
23+
shouldBe(buffer.slice(-4).byteLength, 4);
24+
shouldBe(buffer.slice(size - 4, size).byteLength, 4);
25+
shouldBe(buffer.slice(size - 4, -1).byteLength, 3);
26+
shouldBe(buffer.slice(4 * gib, 4 * gib + 8).byteLength, 8);
27+
shouldBe(buffer.slice(size, size).byteLength, 0);
28+
shouldBe(buffer.slice(size + 1).byteLength, 0);
29+
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
const MAX64 = 4294967296 + 1;
1+
// One byte past MAX_ARRAY_BUFFER_SIZE, so neither constructor can satisfy it.
2+
const MAX64 = 2 ** 34 + 1;
23
// Okay to throw, but should not crash.
34
try {
45
new SharedArrayBuffer(4, { maxByteLength: MAX64 });
@@ -7,4 +8,4 @@ try {
78
try {
89
new ArrayBuffer(4, { maxByteLength: MAX64 });
910
} catch (e) {
10-
}
11+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// A canonical numeric index string on a typed array never reaches ordinary property lookup, whatever
2+
// number it denotes. https://tc39.es/ecma262/#sec-canonicalnumericindexstring
3+
//
4+
// Keys past MAX_ARRAY_INDEX are recognized as integer indices, so they must behave like any other
5+
// out-of-bounds index on a short array rather than becoming ordinary properties.
6+
7+
function shouldBe(actual, expected, what) {
8+
if (actual !== expected)
9+
throw new Error(`bad value for ${what}: expected ${expected} but got ${actual}`);
10+
}
11+
12+
function shouldThrowTypeError(f, what) {
13+
try {
14+
f();
15+
} catch (e) {
16+
if (e instanceof TypeError)
17+
return;
18+
throw new Error(`${what}: expected a TypeError but got ${e}`);
19+
}
20+
throw new Error(`${what}: expected a TypeError but nothing was thrown`);
21+
}
22+
23+
const array = new Uint8Array(4);
24+
const prototype = Object.getPrototypeOf(array);
25+
26+
// String(n) is by construction the canonical spelling of n, so each of these is a
27+
// CanonicalNumericIndexString. They span both sides of every internal cutoff: MAX_ARRAY_INDEX, 2^32,
28+
// 2^53, and 2^64, plus the values that are canonical but are not integer indices at all.
29+
const keys = [
30+
2 ** 32 - 1, // 0xFFFFFFFF, excluded by isIndex()
31+
2 ** 32,
32+
2 ** 53 - 1, // the largest integer index the spec allows
33+
2 ** 53,
34+
2 ** 60,
35+
2 ** 64, // past uint64_t, so no index is recoverable
36+
1e21, // spelled in exponential form
37+
-1,
38+
1.5,
39+
2 ** 32 + 0.5, // canonical, past MAX_ARRAY_INDEX, and not an integer
40+
NaN,
41+
Infinity,
42+
-Infinity,
43+
].map(String).concat(["-0"]); // String(-0) is "0", so -0 has to be spelled out.
44+
45+
for (const key of keys) {
46+
// An element that does not exist must not be found on the prototype either.
47+
prototype[key] = "fromPrototype";
48+
49+
shouldBe(array[key], undefined, `array["${key}"]`);
50+
shouldBe(key in array, false, `"${key}" in array`);
51+
shouldBe(array.hasOwnProperty(key), false, `hasOwnProperty("${key}")`);
52+
shouldBe(Object.getOwnPropertyDescriptor(array, key), undefined, `getOwnPropertyDescriptor("${key}")`);
53+
54+
// The store is ignored, but the right hand side is still coerced first.
55+
let coerced = false;
56+
array[key] = { valueOf() { coerced = true; return 1; } };
57+
shouldBe(coerced, true, `array["${key}"] = ... coerces the right hand side`);
58+
shouldBe(array.hasOwnProperty(key), false, `"${key}" was not added as a property`);
59+
60+
// Deleting an index that does not exist succeeds vacuously.
61+
shouldBe(delete array[key], true, `delete array["${key}"]`);
62+
63+
shouldThrowTypeError(() => Object.defineProperty(array, key, { value: 1 }), `defineProperty("${key}")`);
64+
65+
delete prototype[key];
66+
}
67+
68+
// Numeric-looking strings that are not canonical are ordinary properties, and stay that way.
69+
for (const key of ["042", "1e3", " 1", "0x10", "4294967296 ", "+4294967296"]) {
70+
array[key] = "ordinary";
71+
shouldBe(array.hasOwnProperty(key), true, `"${key}" is an ordinary property`);
72+
shouldBe(array[key], "ordinary", `array["${key}"]`);
73+
shouldBe(delete array[key], true, `delete array["${key}"]`);
74+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//@ memoryHog!
2+
//@ skip if $addressBits <= 32
3+
//@ runDefault
4+
5+
// Indexed access on a typed array longer than MAX_ARRAY_INDEX elements.
6+
//
7+
// MAX_ARRAY_BUFFER_SIZE is 2^34, so a Uint8Array can be longer than MAX_ARRAY_INDEX (0xFFFFFFFE),
8+
// the ceiling on a property key expressed as an index. Every element below the view's length is a
9+
// valid integer index per IsValidIntegerIndex, so [] must reach it. Such a key arrives as a string,
10+
// where parseIndex() cannot hold it, so it is recognized by isCanonicalNumericIndexString() instead.
11+
12+
function shouldBe(actual, expected, what) {
13+
if (actual !== expected)
14+
throw new Error(`bad value for ${what}: expected ${expected} but got ${actual}`);
15+
}
16+
17+
// 4GiB + 2 bytes: the smallest view with indices on both sides of 0xFFFFFFFF and above 2^32, so one
18+
// allocation covers every case.
19+
let array;
20+
try {
21+
array = new Uint8Array(4294967298);
22+
} catch (e) {
23+
// A port that cannot spare 4GiB has nothing to test here.
24+
if (!(e instanceof RangeError))
25+
throw e;
26+
}
27+
28+
if (array !== undefined) {
29+
// fill() and subarray() take the size_t path, so they establish what a byte holds independently of [].
30+
const trueValueAt = index => array.subarray(index, index + 1)[0];
31+
32+
for (const index of [
33+
4294967294, // 0xFFFFFFFE, the last index a property key can express: the control.
34+
4294967295, // 0xFFFFFFFF: fits a uint32 but is excluded by isIndex().
35+
4294967296, // 2^32: does not fit a uint32 at all.
36+
array.length - 1,
37+
]) {
38+
array.fill(7, index, index + 1);
39+
shouldBe(trueValueAt(index), 7, `fill() at ${index}`);
40+
41+
shouldBe(array[index], 7, `array[${index}]`);
42+
shouldBe(array.at(index), 7, `array.at(${index})`);
43+
44+
array[index] = 99;
45+
shouldBe(trueValueAt(index), 99, `array[${index}] = 99`);
46+
shouldBe(array[index], 99, `array[${index}] after assignment`);
47+
48+
shouldBe(index in array, true, `${index} in array`);
49+
shouldBe(array.hasOwnProperty(String(index)), true, `hasOwnProperty(${index})`);
50+
shouldBe(Reflect.has(array, String(index)), true, `Reflect.has(array, "${index}")`);
51+
52+
const descriptor = Object.getOwnPropertyDescriptor(array, String(index));
53+
shouldBe(descriptor !== undefined, true, `getOwnPropertyDescriptor(${index}) exists`);
54+
shouldBe(descriptor.value, 99, `getOwnPropertyDescriptor(${index}).value`);
55+
shouldBe(descriptor.writable, true, `getOwnPropertyDescriptor(${index}).writable`);
56+
shouldBe(descriptor.enumerable, true, `getOwnPropertyDescriptor(${index}).enumerable`);
57+
shouldBe(descriptor.configurable, true, `getOwnPropertyDescriptor(${index}).configurable`);
58+
59+
// An integer index that exists cannot be deleted.
60+
shouldBe(delete array[index], false, `delete array[${index}]`);
61+
shouldBe(trueValueAt(index), 99, `array[${index}] survives delete`);
62+
63+
Object.defineProperty(array, String(index), { value: 123 });
64+
shouldBe(trueValueAt(index), 123, `defineProperty at ${index}`);
65+
66+
// Reading through the prototype must not shadow an element that exists.
67+
Object.getPrototypeOf(array)[String(index)] = "fromProto";
68+
shouldBe(array[index], 123, `array[${index}] is not shadowed by the prototype`);
69+
delete Object.getPrototypeOf(array)[String(index)];
70+
71+
array.fill(0, index, index + 1);
72+
}
73+
}

‎JSTests/wasm/js-api/memory-toResizableBuffer.js‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,21 +67,21 @@ function assertSharedGrowableBufferOfPageSize(pageCount, buffer, maxPageCount) {
6767
assertTrue(memory.buffer !== buffer);
6868
}
6969

70-
// A resize whose page-aligned growth delta exceeds the maximum representable
71-
// PageCount must throw a catchable RangeError, not hit a release assertion.
70+
// A resize whose page-aligned growth delta exceeds what a memory32 may declare
71+
// must throw a catchable RangeError, not hit a release assertion.
7272
// https://bugs.webkit.org/show_bug.cgi?id=318524
7373
{
7474
let memory = new WebAssembly.Memory({ initial: 1, maximum: 65536 });
7575
let buffer = memory.toResizableBuffer();
7676
assertIsolatedResizableBufferOfPageSize(1, buffer, 65536);
7777

78-
// Growth delta of 65537 pages is not representable as a PageCount.
78+
// 65538 pages is not a page count a memory32 may reach at all.
7979
assertThrows(() => buffer.resize(65538 * pageSize), RangeError, "");
8080
// The buffer is unchanged and remains usable after the failed resize.
8181
assertIsolatedResizableBufferOfPageSize(1, buffer, 65536);
8282

83-
// A representable delta that would still exceed the declared maximum must
84-
// also throw catchably.
83+
// A page count a memory32 may reach, but which still exceeds the declared
84+
// maximum, must also throw catchably.
8585
assertThrows(() => buffer.resize(65537 * pageSize), RangeError, "");
8686
assertIsolatedResizableBufferOfPageSize(1, buffer, 65536);
8787

‎JSTests/wasm/js-api/memory64-js-api-errors.js‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,12 @@ for (const address of ["i65", "", "I64", "u64", null, 1, {}]) {
5757
assert.throws(() => table.grow(-1n), TypeError, "Expect an integer argument in the range: [0, 2^64 - 1]");
5858
}
5959

60-
// A memory64's declared maximum may exceed the memory32 page limit.
60+
// A memory64's declared maximum may exceed the memory32 page limit, up to 262144 pages (16 GiB).
6161
{
6262
const mem = new WebAssembly.Memory({ initial: 1n, maximum: 131072n, address: "i64" });
6363
assert.eq(mem.type().maximum, 131072n);
64-
assert.eq(new WebAssembly.Memory({ initial: 0n, maximum: 137438953471n, address: "i64" }).type().maximum, 137438953471n);
65-
assert.throws(() => new WebAssembly.Memory({ initial: 1n, maximum: 137438953472n, address: "i64" }), RangeError,
64+
assert.eq(new WebAssembly.Memory({ initial: 0n, maximum: 262144n, address: "i64" }).type().maximum, 262144n);
65+
assert.throws(() => new WebAssembly.Memory({ initial: 1n, maximum: 262145n, address: "i64" }), RangeError,
6666
"WebAssembly.Memory 'maximum' page count is too large");
6767
// An i32 memory keeps the memory32 limit.
6868
assert.throws(() => new WebAssembly.Memory({ initial: 1, maximum: 65537 }), RangeError,

‎JSTests/wasm/js-api/memory64-js-api.js‎

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,31 +52,27 @@ const pageSize = 64 * 1024;
5252
}
5353

5454
// Constructor: initial page count too large throws. An initial page count is declarative in the same
55-
// way a maximum is, so the limit is the one a module may declare for an i64 memory; a count within
56-
// that limit which cannot be allocated is out of memory instead.
55+
// way a maximum is, so the limit is the largest count a module may declare for an i64 memory, which
56+
// is 262144 pages (16 GiB).
5757
{
5858
assert.throws(
59-
() => new WebAssembly.Memory({ initial: 137438953472n, address: "i64" }),
59+
() => new WebAssembly.Memory({ initial: 262145n, address: "i64" }),
6060
RangeError,
6161
"WebAssembly.Memory 'initial' page count is too large"
6262
);
63-
assert.throws(
64-
() => new WebAssembly.Memory({ initial: 65537n, address: "i64" }),
65-
RangeError,
66-
"Out of memory"
67-
);
6863
}
6964

70-
// Constructor: maximum page count too large throws. A maximum is declarative, so the limit is the
71-
// one a module may declare for an i64 memory, not the memory32 page limit.
65+
// Constructor: maximum page count too large throws. A maximum is bounded by that same i64 limit
66+
// rather than by the memory32 page limit.
7267
{
7368
assert.throws(
74-
() => new WebAssembly.Memory({ initial: 1n, maximum: 137438953472n, address: "i64" }),
69+
() => new WebAssembly.Memory({ initial: 1n, maximum: 262145n, address: "i64" }),
7570
RangeError,
7671
"WebAssembly.Memory 'maximum' page count is too large"
7772
);
7873
const memory = new WebAssembly.Memory({ initial: 1n, maximum: 65537n, address: "i64" });
7974
assert.eq(memory.type().maximum, 65537n);
75+
assert.eq(new WebAssembly.Memory({ initial: 1n, maximum: 262144n, address: "i64" }).type().maximum, 262144n);
8076
}
8177

8278
// Constructor: passing invalid address
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
//@ memoryHog!
2+
//@ skip if $addressBits <= 32
3+
import { instantiate } from "../wabt-wrapper.js";
4+
import * as assert from "../assert.js";
5+
6+
// A memory64 exists to address more than the 4GiB a memory32 can, so both the wasm-level grow and
7+
// the resizable-buffer path must cross that boundary.
8+
9+
const options = { memory64: true, threads: true };
10+
const pageSize = 65536;
11+
const maxMemory64Pages = 262144;
12+
const fourGiB = 4 * 1024 * 1024 * 1024;
13+
const pastFourGiB = fourGiB + pageSize;
14+
15+
// A memory with no declared maximum reports the most this platform could grow it to, which is what
16+
// bounds every maxByteLength below.
17+
const ceilingBytes = new WebAssembly.Memory({ initial: 1n, address: "i64" }).toResizableBuffer().maxByteLength;
18+
19+
// A memory whose initial size is already past 4GiB proves this port can host one at all. Where it
20+
// can, growing past 4GiB below must succeed; where it cannot, refusing cleanly is the only option.
21+
// Only the constructor belongs in the try: an assertion inside it would be swallowed by the catch.
22+
let probe;
23+
try {
24+
probe = new WebAssembly.Memory({ initial: BigInt(pastFourGiB / pageSize), address: "i64" });
25+
} catch (e) {
26+
assert.truthy(e instanceof RangeError && e.message === "Out of memory", `expected an out of memory RangeError, got ${e}`);
27+
}
28+
const canHostPastFourGiB = probe !== undefined;
29+
if (canHostPastFourGiB)
30+
assert.eq(probe.buffer.byteLength, pastFourGiB);
31+
// Nothing below needs the probe, and holding 4GiB across the rest of the file would starve it.
32+
probe = undefined;
33+
34+
for (const shared of [false, true]) {
35+
let mem;
36+
try {
37+
mem = (await instantiate(`
38+
(module
39+
(memory (export "mem") i64 1 ${pastFourGiB / pageSize} ${shared ? "shared" : ""}))
40+
`, {}, options)).exports.mem;
41+
} catch (e) {
42+
// A shared memory reserves its whole maximum up front, which this port may not be able to spare.
43+
assert.truthy(e instanceof RangeError && e.message === "Out of memory", `expected an out of memory RangeError, got ${e}`);
44+
continue;
45+
}
46+
47+
const buffer = mem.toResizableBuffer();
48+
assert.eq(buffer.maxByteLength, Math.min(pastFourGiB, ceilingBytes));
49+
50+
const grow = shared ? size => buffer.grow(size) : size => buffer.resize(size);
51+
if (canHostPastFourGiB) {
52+
// The non-shared path grows by copying, so it holds the old and new regions at once. Hosting one
53+
// 4GiB memory does not imply room for both.
54+
let grown = true;
55+
try {
56+
grow(pastFourGiB);
57+
} catch (e) {
58+
assert.truthy(e instanceof RangeError, `expected a RangeError, got ${e}`);
59+
grown = false;
60+
}
61+
if (grown)
62+
assert.eq(buffer.byteLength, pastFourGiB);
63+
} else {
64+
assert.throws(() => grow(pastFourGiB), RangeError, "failed with new byte length");
65+
assert.eq(buffer.byteLength, pageSize);
66+
}
67+
68+
// Past the declared maximum is a clean RangeError either way. The shared and non-shared paths
69+
// word it differently ("grow failed ...", "ArrayBuffer resize failed ...") around a common core.
70+
assert.throws(() => grow(pastFourGiB + pageSize), RangeError, "failed with new byte length");
71+
}
72+
73+
// A shared memory's buffer reports the maximum it can actually reach, and growth within the mapping
74+
// succeeds.
75+
{
76+
let memory;
77+
try {
78+
memory = new WebAssembly.Memory({ initial: 1n, maximum: BigInt(maxMemory64Pages), address: "i64", shared: true });
79+
} catch (e) {
80+
assert.truthy(e instanceof RangeError && e.message === "Out of memory", `expected an out of memory RangeError, got ${e}`);
81+
}
82+
if (memory !== undefined) {
83+
const buffer = memory.toResizableBuffer();
84+
assert.eq(buffer.maxByteLength, Math.min(maxMemory64Pages * pageSize, ceilingBytes));
85+
buffer.grow(2 * pageSize);
86+
assert.eq(buffer.byteLength, 2 * pageSize);
87+
}
88+
}

0 commit comments

Comments
 (0)