diff --git a/src/compiler/wasm-compiler.cc b/src/compiler/wasm-compiler.cc index 30f9e94f1f5f..51e49a7bb27a 100644 --- a/src/compiler/wasm-compiler.cc +++ b/src/compiler/wasm-compiler.cc @@ -3405,37 +3405,32 @@ Node* WasmGraphBuilder::BoundsCheckMem(uint8_t access_size, Node* index, return index; } -// Check that the range [start, start + size) is in the range [0, max). -void WasmGraphBuilder::BoundsCheckRange(Node* start, Node* size, Node* max, - wasm::WasmCodePosition position) { - // The accessed memory is [start, end), where {end} is {start + size}. We - // want to check that {start + size <= max}, making sure that {start + size} - // doesn't overflow. This can be expressed as {start <= max - size} as long - // as {max - size} isn't negative, which is true if {size <= max}. +Node* WasmGraphBuilder::BoundsCheckRange(Node* start, Node** size, Node* max, + wasm::WasmCodePosition position) { auto m = mcgraph()->machine(); - Node* cond = graph()->NewNode(m->Uint32LessThanOrEqual(), size, max); - TrapIfFalse(wasm::kTrapMemOutOfBounds, cond, position); - - // This produces a positive number, since {size <= max}. - Node* effective_size = graph()->NewNode(m->Int32Sub(), max, size); - - // Introduce the actual bounds check. - Node* check = - graph()->NewNode(m->Uint32LessThanOrEqual(), start, effective_size); - TrapIfFalse(wasm::kTrapMemOutOfBounds, check, position); - - // TODO(binji): Does this need addtional untrusted_code_mitigations_ mask - // like BoundsCheckMem above? + // The region we are trying to access is [start, start+size). If + // {start} > {max}, none of this region is valid, so we trap. Otherwise, + // there may be a subset of the region that is valid. {max - start} is the + // maximum valid size, so if {max - start < size}, then the region is + // partially out-of-bounds. + TrapIfTrue(wasm::kTrapMemOutOfBounds, + graph()->NewNode(m->Uint32LessThan(), max, start), position); + Node* sub = graph()->NewNode(m->Int32Sub(), max, start); + Node* fail = graph()->NewNode(m->Uint32LessThan(), sub, *size); + Diamond d(graph(), mcgraph()->common(), fail, BranchHint::kFalse); + d.Chain(Control()); + *size = d.Phi(MachineRepresentation::kWord32, sub, *size); + return fail; } -Node* WasmGraphBuilder::BoundsCheckMemRange(Node* start, Node* size, +Node* WasmGraphBuilder::BoundsCheckMemRange(Node** start, Node** size, wasm::WasmCodePosition position) { - // TODO(binji): Support trap handler. - if (!FLAG_wasm_no_bounds_checks) { - BoundsCheckRange(start, size, instance_cache_->mem_size, position); - } - return graph()->NewNode(mcgraph()->machine()->IntAdd(), MemBuffer(0), - Uint32ToUintptr(start)); + // TODO(binji): Support trap handler and no bounds check mode. + Node* fail = + BoundsCheckRange(*start, size, instance_cache_->mem_size, position); + *start = graph()->NewNode(mcgraph()->machine()->IntAdd(), MemBuffer(0), + Uint32ToUintptr(*start)); + return fail; } const Operator* WasmGraphBuilder::GetSafeLoadOperator(int offset, @@ -4377,10 +4372,11 @@ Node* WasmGraphBuilder::MemoryInit(uint32_t data_segment_index, Node* dst, Node* src, Node* size, wasm::WasmCodePosition position) { CheckDataSegmentIsPassiveAndNotDropped(data_segment_index, position); - dst = BoundsCheckMemRange(dst, size, position); - MachineOperatorBuilder* m = mcgraph()->machine(); + Node* dst_fail = BoundsCheckMemRange(&dst, &size, position); + auto m = mcgraph()->machine(); Node* seg_index = Uint32Constant(data_segment_index); + Node* src_fail; { // Load segment size from WasmInstanceObject::data_segment_sizes. @@ -4394,7 +4390,7 @@ Node* WasmGraphBuilder::MemoryInit(uint32_t data_segment_index, Node* dst, Effect(), Control())); // Bounds check the src index against the segment size. - BoundsCheckRange(src, size, seg_size, position); + src_fail = BoundsCheckRange(src, &size, seg_size, position); } { @@ -4418,7 +4414,10 @@ Node* WasmGraphBuilder::MemoryInit(uint32_t data_segment_index, Node* dst, MachineType sig_types[] = {MachineType::Pointer(), MachineType::Pointer(), MachineType::Uint32()}; MachineSignature sig(0, 3, sig_types); - return BuildCCall(&sig, function, dst, src, size); + BuildCCall(&sig, function, dst, src, size); + return TrapIfTrue(wasm::kTrapMemOutOfBounds, + graph()->NewNode(m->Word32Or(), dst_fail, src_fail), + position); } Node* WasmGraphBuilder::DataDrop(uint32_t data_segment_index, @@ -4435,25 +4434,51 @@ Node* WasmGraphBuilder::DataDrop(uint32_t data_segment_index, Node* WasmGraphBuilder::MemoryCopy(Node* dst, Node* src, Node* size, wasm::WasmCodePosition position) { - dst = BoundsCheckMemRange(dst, size, position); - src = BoundsCheckMemRange(src, size, position); + auto m = mcgraph()->machine(); + // The data must be copied backward if the regions overlap and src < dst. The + // regions overlap if {src + size > dst && dst + size > src}. Since we already + // test that {src < dst}, we know that {dst + size > src}, so this simplifies + // to just {src + size > dst}. That sum can overflow, but if we subtract + // {size} from both sides of the inequality we get the equivalent test + // {size > dst - src}. + Node* copy_backward = graph()->NewNode( + m->Word32And(), graph()->NewNode(m->Uint32LessThan(), src, dst), + graph()->NewNode(m->Uint32LessThan(), + graph()->NewNode(m->Int32Sub(), dst, src), size)); + + Node* dst_fail = BoundsCheckMemRange(&dst, &size, position); + + // Trap without copying any bytes if we are copying backward and the copy is + // partially out-of-bounds. We only need to check that the dst region is + // out-of-bounds, because we know that {src < dst}, so the src region is + // always out of bounds if the dst region is. + TrapIfTrue(wasm::kTrapMemOutOfBounds, + graph()->NewNode(m->Word32And(), dst_fail, copy_backward), + position); + + Node* src_fail = BoundsCheckMemRange(&src, &size, position); + Node* function = graph()->NewNode(mcgraph()->common()->ExternalConstant( ExternalReference::wasm_memory_copy())); MachineType sig_types[] = {MachineType::Pointer(), MachineType::Pointer(), MachineType::Uint32()}; MachineSignature sig(0, 3, sig_types); - return BuildCCall(&sig, function, dst, src, size); + BuildCCall(&sig, function, dst, src, size); + return TrapIfTrue(wasm::kTrapMemOutOfBounds, + graph()->NewNode(m->Word32Or(), dst_fail, src_fail), + position); } Node* WasmGraphBuilder::MemoryFill(Node* dst, Node* value, Node* size, wasm::WasmCodePosition position) { - dst = BoundsCheckMemRange(dst, size, position); + Node* fail = BoundsCheckMemRange(&dst, &size, position); Node* function = graph()->NewNode(mcgraph()->common()->ExternalConstant( ExternalReference::wasm_memory_fill())); MachineType sig_types[] = {MachineType::Pointer(), MachineType::Uint32(), MachineType::Uint32()}; MachineSignature sig(0, 3, sig_types); - return BuildCCall(&sig, function, dst, value, size); + BuildCCall(&sig, function, dst, value, size); + return TrapIfTrue(wasm::kTrapMemOutOfBounds, fail, position); } Node* WasmGraphBuilder::CheckElemSegmentIsPassiveAndNotDropped( diff --git a/src/compiler/wasm-compiler.h b/src/compiler/wasm-compiler.h index efd6113f8412..971b077b8ce5 100644 --- a/src/compiler/wasm-compiler.h +++ b/src/compiler/wasm-compiler.h @@ -441,11 +441,16 @@ class WasmGraphBuilder { Node* BoundsCheckMem(uint8_t access_size, Node* index, uint32_t offset, wasm::WasmCodePosition, EnforceBoundsCheck); // Check that the range [start, start + size) is in the range [0, max). - void BoundsCheckRange(Node* start, Node* size, Node* max, - wasm::WasmCodePosition); - // BoundsCheckMemRange receives a uint32 {start} and {size} and returns - // a pointer into memory at that index, if it is in bounds. - Node* BoundsCheckMemRange(Node* start, Node* size, wasm::WasmCodePosition); + // Also updates *size with the valid range. Returns true if the range is + // partially out-of-bounds, traps if it is completely out-of-bounds. + Node* BoundsCheckRange(Node* start, Node** size, Node* max, + wasm::WasmCodePosition); + // BoundsCheckMemRange receives a uint32 {start} and {size}, and checks if it + // is in bounds. Also updates *size with the valid range, and converts *start + // to a pointer into memory at that index. Returns true if the range is + // partially out-of-bounds, traps if it is completely out-of-bounds. + Node* BoundsCheckMemRange(Node** start, Node** size, wasm::WasmCodePosition); + Node* CheckBoundsAndAlignment(uint8_t access_size, Node* index, uint32_t offset, wasm::WasmCodePosition); diff --git a/src/utils.h b/src/utils.h index a6b3502eec41..dd3a30cf69d1 100644 --- a/src/utils.h +++ b/src/utils.h @@ -74,6 +74,21 @@ inline constexpr bool IsInBounds(size_t index, size_t length, size_t max) { return length <= max && index <= (max - length); } +// Checks if [index, index+length) is in range [0, max). If not, {length} is +// clamped to its valid range. Note that this check works even if +// {index+length} would wrap around. +template +inline bool ClampToBounds(T index, T* length, T max) { + if (index > max) { + *length = 0; + return false; + } + T avail = max - index; + bool oob = *length > avail; + if (oob) *length = avail; + return !oob; +} + // X must be a power of 2. Returns the number of trailing zeros. template ::value>::type> diff --git a/src/wasm/module-instantiate.cc b/src/wasm/module-instantiate.cc index c2272a6dc00c..29789300f1ea 100644 --- a/src/wasm/module-instantiate.cc +++ b/src/wasm/module-instantiate.cc @@ -1481,11 +1481,12 @@ bool LoadElemSegmentImpl(Isolate* isolate, Handle instance, // TODO(wasm): Move this functionality into wasm-objects, since it is used // for both instantiation and in the implementation of the table.init // instruction. - if (!IsInBounds(dst, count, table_instance.table_size)) return false; - if (!IsInBounds(src, count, elem_segment.entries.size())) return false; + bool ok = ClampToBounds(dst, &count, table_instance.table_size); + // Use & instead of && so the clamp is not short-circuited. + ok &= ClampToBounds(src, &count, elem_segment.entries.size()); const WasmModule* module = instance->module(); - for (uint32_t i = 0; i < count; ++i) { + for (size_t i = 0; i < count; ++i) { uint32_t func_index = elem_segment.entries[src + i]; int entry_index = static_cast(dst + i); @@ -1547,7 +1548,7 @@ bool LoadElemSegmentImpl(Isolate* isolate, Handle instance, instance, func_index); } } - return true; + return ok; } void InstanceBuilder::LoadTableSegments(Handle instance) { diff --git a/src/wasm/wasm-objects.cc b/src/wasm/wasm-objects.cc index 80e4f0f11059..c0b3309d71f6 100644 --- a/src/wasm/wasm-objects.cc +++ b/src/wasm/wasm-objects.cc @@ -1442,9 +1442,9 @@ Address WasmInstanceObject::GetCallTarget(uint32_t func_index) { namespace { void CopyTableEntriesImpl(Handle instance, uint32_t dst, - uint32_t src, uint32_t count) { + uint32_t src, uint32_t count, bool copy_backward) { DCHECK(IsInBounds(dst, count, instance->indirect_function_table_size())); - if (src < dst) { + if (copy_backward) { for (uint32_t i = count; i > 0; i--) { auto to_entry = IndirectFunctionTableEntry(instance, dst + i - 1); auto from_entry = IndirectFunctionTableEntry(instance, src + i - 1); @@ -1471,14 +1471,21 @@ bool WasmInstanceObject::CopyTableEntries(Isolate* isolate, CHECK_EQ(0, table_src_index); CHECK_EQ(0, table_dst_index); auto max = instance->indirect_function_table_size(); - if (!IsInBounds(dst, count, max)) return false; - if (!IsInBounds(src, count, max)) return false; - if (dst == src) return true; // no-op + bool copy_backward = src < dst && dst - src < count; + bool ok = ClampToBounds(dst, &count, max); + // Use & instead of && so the clamp is not short-circuited. + ok &= ClampToBounds(src, &count, max); + + // If performing a partial copy when copying backward, then the first access + // will be out-of-bounds, so no entries should be copied. + if (copy_backward && !ok) return ok; + + if (dst == src || count == 0) return ok; // no-op if (!instance->has_table_object()) { // No table object, only need to update this instance. - CopyTableEntriesImpl(instance, dst, src, count); - return true; + CopyTableEntriesImpl(instance, dst, src, count, copy_backward); + return ok; } Handle table = @@ -1491,12 +1498,12 @@ bool WasmInstanceObject::CopyTableEntries(Isolate* isolate, WasmInstanceObject::cast( dispatch_tables->get(i + kDispatchTableInstanceOffset)), isolate); - CopyTableEntriesImpl(target_instance, dst, src, count); + CopyTableEntriesImpl(target_instance, dst, src, count, copy_backward); } // Copy the function entries. Handle functions(table->elements(), isolate); - if (src < dst) { + if (copy_backward) { for (uint32_t i = count; i > 0; i--) { functions->set(dst + i - 1, functions->get(src + i - 1)); } @@ -1505,7 +1512,7 @@ bool WasmInstanceObject::CopyTableEntries(Isolate* isolate, functions->set(dst + i, functions->get(src + i)); } } - return true; + return ok; } // static diff --git a/test/mjsunit/wasm/bulk-memory.js b/test/mjsunit/wasm/bulk-memory.js index 0e5996508518..e43ffe624d22 100644 --- a/test/mjsunit/wasm/bulk-memory.js +++ b/test/mjsunit/wasm/bulk-memory.js @@ -71,8 +71,35 @@ function getMemoryInit(mem, segment_data) { memoryInit(0, 5, 5); assertBufferContents(u8a, [5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + + // Copy 0 bytes does nothing. + memoryInit(10, 1, 0); + assertBufferContents(u8a, [5, 6, 7, 8, 9, 0, 0, 0, 0, 0, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + + // Copy 0 at end of memory region or data segment is OK. + memoryInit(kPageSize, 0, 0); + memoryInit(0, 10, 0); })(); +(function TestMemoryInitOutOfBoundsData() { + const mem = new WebAssembly.Memory({initial: 1}); + const memoryInit = getMemoryInit(mem, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + + const u8a = new Uint8Array(mem.buffer); + const last5Bytes = new Uint8Array(mem.buffer, kPageSize - 5); + + // Write all values up to the out-of-bounds write. + assertTraps(kTrapMemOutOfBounds, () => memoryInit(kPageSize - 5, 0, 6)); + assertBufferContents(last5Bytes, [0, 1, 2, 3, 4]); + + // Write all values up to the out-of-bounds read. + u8a.fill(0); + assertTraps(kTrapMemOutOfBounds, () => memoryInit(0, 5, 6)); + assertBufferContents(u8a, [5, 6, 7, 8, 9]); +})(); + + (function TestMemoryInitOutOfBounds() { const mem = new WebAssembly.Memory({initial: 1}); // Create a data segment that has a length of kPageSize. @@ -91,6 +118,10 @@ function getMemoryInit(mem, segment_data) { assertTraps(kTrapMemOutOfBounds, () => memoryInit(1000, 0, kPageSize)); assertTraps(kTrapMemOutOfBounds, () => memoryInit(kPageSize, 0, 1)); + // Copy 0 out-of-bounds fails. + assertTraps(kTrapMemOutOfBounds, () => memoryInit(kPageSize + 1, 0, 0)); + assertTraps(kTrapMemOutOfBounds, () => memoryInit(0, kPageSize + 1, 0)); + // Make sure bounds aren't checked with 32-bit wrapping. assertTraps(kTrapMemOutOfBounds, () => memoryInit(1, 1, -1)); @@ -206,6 +237,10 @@ function getMemoryCopy(mem) { memoryCopy(10, 1, 0); assertBufferContents(u8a, [0, 11, 22, 33, 44, 55, 66, 77, 0, 0, 11, 22, 33, 44, 55, 66, 77]); + + // Copy 0 at end of memory region is OK. + memoryCopy(kPageSize, 0, 0); + memoryCopy(0, kPageSize, 0); })(); (function TestMemoryCopyOverlapping() { @@ -226,6 +261,36 @@ function getMemoryCopy(mem) { assertBufferContents(u8a, [10, 20, 30, 20, 30]); })(); +(function TestMemoryCopyOutOfBoundsData() { + const mem = new WebAssembly.Memory({initial: 1}); + const memoryCopy = getMemoryCopy(mem); + + const u8a = new Uint8Array(mem.buffer); + const first5Bytes = new Uint8Array(mem.buffer, 0, 5); + const last5Bytes = new Uint8Array(mem.buffer, kPageSize - 5); + u8a.set([11, 22, 33, 44, 55, 66, 77, 88]); + + // Write all values up to the out-of-bounds access. + assertTraps(kTrapMemOutOfBounds, () => memoryCopy(kPageSize - 5, 0, 6)); + assertBufferContents(last5Bytes, [11, 22, 33, 44, 55]); + + // Copy overlapping with destination < source. Copy will happen forwards, up + // to the out-of-bounds access. + u8a.fill(0); + last5Bytes.set([11, 22, 33, 44, 55]); + assertTraps( + kTrapMemOutOfBounds, () => memoryCopy(0, kPageSize - 5, kPageSize)); + assertBufferContents(first5Bytes, [11, 22, 33, 44, 55]); + + // Copy overlapping with source < destination. Copy would happen backwards, + // but the first byte to copy is out-of-bounds, so no data should be written. + u8a.fill(0); + first5Bytes.set([11, 22, 33, 44, 55]); + assertTraps( + kTrapMemOutOfBounds, () => memoryCopy(kPageSize - 5, 0, kPageSize)); + assertBufferContents(last5Bytes, [0, 0, 0, 0, 0]); +})(); + (function TestMemoryCopyOutOfBounds() { const mem = new WebAssembly.Memory({initial: 1}); const memoryCopy = getMemoryCopy(mem); @@ -242,6 +307,10 @@ function getMemoryCopy(mem) { assertTraps(kTrapMemOutOfBounds, () => memoryCopy(1000, 0, kPageSize)); assertTraps(kTrapMemOutOfBounds, () => memoryCopy(kPageSize, 0, 1)); + // Copy 0 out-of-bounds fails. + assertTraps(kTrapMemOutOfBounds, () => memoryCopy(kPageSize + 1, 0, 0)); + assertTraps(kTrapMemOutOfBounds, () => memoryCopy(0, kPageSize + 1, 0)); + // Make sure bounds aren't checked with 32-bit wrapping. assertTraps(kTrapMemOutOfBounds, () => memoryCopy(1, 1, -1)); @@ -282,6 +351,9 @@ function getMemoryFill(mem) { // Fill 0 bytes does nothing. memoryFill(4, 66, 0); assertBufferContents(u8a, [0, 33, 33, 33, 66, 66, 66, 66]); + + // Fill 0 at end of memory region is OK. + memoryFill(kPageSize, 66, 0); })(); (function TestMemoryFillValueWrapsToByte() { @@ -295,6 +367,17 @@ function getMemoryFill(mem) { assertBufferContents(u8a, [expected, expected, expected]); })(); +(function TestMemoryFillOutOfBoundsData() { + const mem = new WebAssembly.Memory({initial: 1}); + const memoryFill = getMemoryFill(mem); + const v = 123; + + // Write all values up to the out-of-bound access. + assertTraps(kTrapMemOutOfBounds, () => memoryFill(kPageSize - 5, v, 999)); + const u8a = new Uint8Array(mem.buffer, kPageSize - 6); + assertBufferContents(u8a, [0, 123, 123, 123, 123, 123]); +})(); + (function TestMemoryFillOutOfBounds() { const mem = new WebAssembly.Memory({initial: 1}); const memoryFill = getMemoryFill(mem); @@ -307,6 +390,9 @@ function getMemoryFill(mem) { assertTraps(kTrapMemOutOfBounds, () => memoryFill(1000, v, kPageSize)); assertTraps(kTrapMemOutOfBounds, () => memoryFill(kPageSize, v, 1)); + // Fill 0 out-of-bounds fails. + assertTraps(kTrapMemOutOfBounds, () => memoryFill(kPageSize + 1, v, 0)); + // Make sure bounds aren't checked with 32-bit wrapping. assertTraps(kTrapMemOutOfBounds, () => memoryFill(1, v, -1)); diff --git a/test/mjsunit/wasm/table-copy.js b/test/mjsunit/wasm/table-copy.js index 7c5c49669f04..d317c63a5b09 100644 --- a/test/mjsunit/wasm/table-copy.js +++ b/test/mjsunit/wasm/table-copy.js @@ -24,7 +24,7 @@ load("test/mjsunit/wasm/wasm-module-builder.js"); let instance = builder.instantiate(); let copy = instance.exports.copy; - for (let i = 0; i < kTableSize; i++) { + for (let i = 0; i <= kTableSize; i++) { copy(0, 0, i); // nop copy(0, i, kTableSize - i); copy(i, 0, kTableSize - i); @@ -147,6 +147,54 @@ function assertCall(call, ...elems) { assertCall(call, 1, 2, 2, 1, 2); })(); +(function TestTableCopyOobWrites() { + print(arguments.callee.name); + + let builder = new WasmModuleBuilder(); + let sig_v_iii = builder.addType(kSig_v_iii); + let kTableSize = 5; + + builder.setTableBounds(kTableSize, kTableSize); + + { + let o = addFunctions(builder, kTableSize); + builder.addElementSegment(0, false, + [o.f0.index, o.f1.index, o.f2.index]); + } + + builder.addFunction("copy", sig_v_iii) + .addBody([ + kExprGetLocal, 0, + kExprGetLocal, 1, + kExprGetLocal, 2, + kNumericPrefix, kExprTableCopy, kTableZero, kTableZero]) + .exportAs("copy"); + + builder.addExportOfKind("table", kExternalTable, 0); + + let instance = builder.instantiate(); + let table = instance.exports.table; + let f0 = table.get(0), f1 = table.get(1), f2 = table.get(2); + let copy = instance.exports.copy; + + // Non-overlapping, src < dst. + assertThrows(() => copy(3, 0, 3)); + assertTable(table, f0, f1, f2, f0, f1); + + // Non-overlapping, dst < src. + assertThrows(() => copy(0, 4, 2)); + assertTable(table, f1, f1, f2, f0, f1); + + // Overlapping, src < dst. This is required to copy backward, but the first + // access will be out-of-bounds, so nothing changes. + assertThrows(() => copy(3, 0, 99)); + assertTable(table, f1, f1, f2, f0, f1); + + // Overlapping, dst < src. + assertThrows(() => copy(0, 1, 99)); + assertTable(table, f1, f2, f0, f1, f1); +})(); + (function TestTableCopyOob1() { print(arguments.callee.name); diff --git a/test/mjsunit/wasm/table-init.js b/test/mjsunit/wasm/table-init.js index c95e072c648b..3646ecaee02d 100644 --- a/test/mjsunit/wasm/table-init.js +++ b/test/mjsunit/wasm/table-init.js @@ -12,12 +12,12 @@ function addFunction(builder, k) { return m; } -function addFunctions(builder, count, exportf = false) { +function addFunctions(builder, count) { let o = {}; for (var i = 0; i < count; i++) { let name = `f${i}`; o[name] = addFunction(builder, i); - if (exportf) o[name].exportAs(name); + o[name].exportAs(name); } return o; } @@ -36,7 +36,7 @@ function assertTable(obj, ...elems) { builder.setTableBounds(kTableSize, kTableSize); { - let o = addFunctions(builder, kTableSize, true); + let o = addFunctions(builder, kTableSize); builder.addPassiveElementSegment( [o.f0.index, o.f1.index, o.f2.index, o.f3.index, o.f4.index, null]); } @@ -56,6 +56,11 @@ function assertTable(obj, ...elems) { assertTable(x.table, null, null, null, null, null); + // 0 count is ok in bounds, and at end of regions. + x.init0(0, 0, 0); + x.init0(kTableSize, 0, 0); + x.init0(0, kTableSize, 0); + // test actual writes. x.init0(0, 0, 1); assertTable(x.table, x.f0, null, null, null, null); @@ -105,6 +110,14 @@ function assertTable(obj, ...elems) { assertTable(x.table, null, null, null, null, null); + // Write all values up to the out-of-bounds write. + assertThrows(() => x.init0(3, 0, 3)); + assertTable(x.table, null, null, null, x.f0, x.f1); + + // Write all values up to the out-of-bounds read. + assertThrows(() => x.init0(0, 3, 3)); + assertTable(x.table, x.f3, x.f4, null, x.f0, x.f1); + // 0-count is oob. assertThrows(() => x.init0(kTableSize+1, 0, 0)); assertThrows(() => x.init0(0, kTableSize+1, 0));