diff options
Diffstat (limited to 'js/src/wasm/WasmMemory.cpp')
-rw-r--r-- | js/src/wasm/WasmMemory.cpp | 385 |
1 files changed, 385 insertions, 0 deletions
diff --git a/js/src/wasm/WasmMemory.cpp b/js/src/wasm/WasmMemory.cpp new file mode 100644 index 0000000000..b5566921e0 --- /dev/null +++ b/js/src/wasm/WasmMemory.cpp @@ -0,0 +1,385 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * + * Copyright 2021 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "wasm/WasmMemory.h" + +#include "mozilla/MathAlgorithms.h" + +#include "js/Conversions.h" +#include "js/ErrorReport.h" +#include "vm/ArrayBufferObject.h" +#include "wasm/WasmCodegenTypes.h" +#include "wasm/WasmProcess.h" + +using mozilla::IsPowerOfTwo; + +using namespace js; +using namespace js::wasm; + +const char* wasm::ToString(IndexType indexType) { + switch (indexType) { + case IndexType::I32: + return "i32"; + case IndexType::I64: + return "i64"; + default: + MOZ_CRASH(); + } +} + +bool wasm::ToIndexType(JSContext* cx, HandleValue value, IndexType* indexType) { + RootedString typeStr(cx, ToString(cx, value)); + if (!typeStr) { + return false; + } + + Rooted<JSLinearString*> typeLinearStr(cx, typeStr->ensureLinear(cx)); + if (!typeLinearStr) { + return false; + } + + if (StringEqualsLiteral(typeLinearStr, "i32")) { + *indexType = IndexType::I32; + } else if (StringEqualsLiteral(typeLinearStr, "i64")) { + *indexType = IndexType::I64; + } else { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_WASM_BAD_STRING_IDX_TYPE); + return false; + } + return true; +} + +/* + * [SMDOC] Linear memory addresses and bounds checking + * + * (Also see "WASM Linear Memory structure" in vm/ArrayBufferObject.cpp) + * + * + * Memory addresses, bounds check avoidance, and the huge memory trick. + * + * A memory address in an access instruction has three components, the "memory + * base", the "pointer", and the "offset". The "memory base" - the HeapReg on + * most platforms and a value loaded from the instance on x86 - is a native + * pointer that points to the start of the linear memory array; we'll ignore the + * memory base in the following. The "pointer" is the i32 or i64 index supplied + * by the program as a separate value argument to the access instruction; it is + * usually variable but can be constant. The "offset" is a constant encoded in + * the access instruction. + * + * The "effective address" (EA) is the non-overflowed sum of the pointer and the + * offset (if the sum overflows the program traps); the pointer, offset, and EA + * all have the same type, i32 or i64. + * + * An access has an "access size", which is the number of bytes that are + * accessed - currently up to 16 (for V128). The highest-addressed byte to be + * accessed by an access is thus the byte at (pointer+offset+access_size-1), + * where offset+access_size-1 is compile-time evaluable. + * + * Bounds checking ensures that the entire access is in bounds, ie, that the + * highest-addressed byte is at an offset in the linear memory below that of the + * memory's current byteLength. + * + * To avoid performing an addition with overflow check and a compare-and-branch + * bounds check for every memory access, we use some tricks: + * + * - An access-protected guard region of size R at the end of each memory is + * used to trap accesses to out-of-bounds offsets in the range + * 0..R-access_size. Thus the offset and the access size need not be added + * into the pointer before the bounds check, saving the add and overflow + * check. The offset is added into the pointer without an overflow check + * either directly before the access or in the access instruction itself + * (depending on the ISA). The pointer must still be explicitly + * bounds-checked. + * + * - On 64-bit systems where we determine there is plenty of virtual memory + * space (and ideally we determine that the VM system uses overcommit), a + * 32-bit memory is implemented as a 4GB + R reservation, where the memory + * from the current heap length through the end of the reservation is + * access-protected. The protected area R allows offsets up to R-access_size + * to be encoded in the access instruction. The pointer need not be bounds + * checked explicitly, since it has only a 4GB range and thus points into the + * 4GB part of the reservation. The offset can be added into the pointer + * (using 64-bit arithmetic) either directly before the access or in the + * access instruction. + * + * The value of R differs in the two situations; in the first case it tends to + * be small, currently 64KB; in the second case it is large, currently 2GB+64KB. + * The difference is due to explicit bounds checking tending to be used on + * 32-bit systems where memory and address space are scarce, while the implicit + * bounds check is used only on 64-bit systems after ensuring that sufficient + * address space is available in the process. (2GB is really overkill, and + * there's nothing magic about it; we could use something much smaller.) + * + * The implicit bounds checking strategy with the large reservation is known + * below and elsewhere as the "huge memory trick" or just "huge memory". + * + * All memories in a process use the same strategy, selected at process startup. + * The immediate reason for that is that the machine code embeds the strategy + * it's been compiled with, and may later be exposed to memories originating + * from different modules or directly from JS. If the memories did not all use + * the same strategy, and the same strategy as the code, linking would fail or + * we would have to recompile the code. + * + * + * The boundsCheckLimit. + * + * The bounds check limit that is stored in the instance is always valid and is + * always a 64-bit datum, and it is always correct to load it and use it as a + * 64-bit value. However, in situations when the 32 upper bits are known to be + * zero, it is also correct to load just the low 32 bits from the address of the + * limit (which is always little-endian when a JIT is enabled), and use that + * value as the limit. + * + * On x86 and arm32 (and on any other 32-bit platform, should there ever be + * one), there is explicit bounds checking and the heap, whether memory32 or + * memory64, is limited to 2GB; the bounds check limit can be treated as a + * 32-bit quantity. + * + * On all 64-bit platforms, we may use explicit bounds checking or the huge + * memory trick for memory32, but must always use explicit bounds checking for + * memory64. If the heap does not have a known maximum size or the known + * maximum is greater than or equal to 4GB, then the bounds check limit must be + * treated as a 64-bit quantity; otherwise it can be treated as a 32-bit + * quantity. + * + * On x64 and arm64 with Baseline and Ion, we allow 32-bit memories up to 4GB, + * and 64-bit memories can be larger. + * + * On mips64, memories are limited to 2GB, for now. + * + * Asm.js memories are limited to 2GB even on 64-bit platforms, and we can + * always assume a 32-bit bounds check limit for asm.js. + * + * + * Constant pointers. + * + * If the pointer is constant then the EA can be computed at compile time, and + * if the EA is below the initial memory size then the bounds check can be + * elided. + * + * + * Alignment checks. + * + * On all platforms, some accesses (currently atomics) require an alignment + * check: the EA must be naturally aligned for the datum being accessed. + * However, we do not need to compute the EA properly, we care only about the + * low bits - a cheap, overflowing add is fine, and if the offset is known + * to be aligned, only the pointer need be checked. + */ + +// Bounds checks always compare the base of the memory access with the bounds +// check limit. If the memory access is unaligned, this means that, even if the +// bounds check succeeds, a few bytes of the access can extend past the end of +// memory. To guard against this, extra space is included in the guard region to +// catch the overflow. MaxMemoryAccessSize is a conservative approximation of +// the maximum guard space needed to catch all unaligned overflows. +// +// Also see "Linear memory addresses and bounds checking" above. + +static const unsigned MaxMemoryAccessSize = LitVal::sizeofLargestValue(); + +// All plausible targets must be able to do at least IEEE754 double +// loads/stores, hence the lower limit of 8. Some Intel processors support +// AVX-512 loads/stores, hence the upper limit of 64. +static_assert(MaxMemoryAccessSize >= 8, "MaxMemoryAccessSize too low"); +static_assert(MaxMemoryAccessSize <= 64, "MaxMemoryAccessSize too high"); +static_assert((MaxMemoryAccessSize & (MaxMemoryAccessSize - 1)) == 0, + "MaxMemoryAccessSize is not a power of two"); + +#ifdef WASM_SUPPORTS_HUGE_MEMORY + +static_assert(MaxMemoryAccessSize <= HugeUnalignedGuardPage, + "rounded up to static page size"); +static_assert(HugeOffsetGuardLimit < UINT32_MAX, + "checking for overflow against OffsetGuardLimit is enough."); + +// We have only tested huge memory on x64 and arm64. +# if !(defined(JS_CODEGEN_X64) || defined(JS_CODEGEN_ARM64)) +# error "Not an expected configuration" +# endif + +#endif + +// On !WASM_SUPPORTS_HUGE_MEMORY platforms: +// - To avoid OOM in ArrayBuffer::prepareForAsmJS, asm.js continues to use the +// original ArrayBuffer allocation which has no guard region at all. +// - For WebAssembly memories, an additional GuardSize is mapped after the +// accessible region of the memory to catch folded (base+offset) accesses +// where `offset < OffsetGuardLimit` as well as the overflow from unaligned +// accesses, as described above for MaxMemoryAccessSize. + +static const size_t OffsetGuardLimit = PageSize - MaxMemoryAccessSize; + +static_assert(MaxMemoryAccessSize < GuardSize, + "Guard page handles partial out-of-bounds"); +static_assert(OffsetGuardLimit < UINT32_MAX, + "checking for overflow against OffsetGuardLimit is enough."); + +size_t wasm::GetMaxOffsetGuardLimit(bool hugeMemory) { +#ifdef WASM_SUPPORTS_HUGE_MEMORY + return hugeMemory ? HugeOffsetGuardLimit : OffsetGuardLimit; +#else + return OffsetGuardLimit; +#endif +} + +// Assert that our minimum offset guard limit covers our inline +// memory.copy/fill optimizations. +static const size_t MinOffsetGuardLimit = OffsetGuardLimit; +static_assert(MaxInlineMemoryCopyLength < MinOffsetGuardLimit, "precondition"); +static_assert(MaxInlineMemoryFillLength < MinOffsetGuardLimit, "precondition"); + +#ifdef JS_64BIT +wasm::Pages wasm::MaxMemoryPages(IndexType t) { + MOZ_ASSERT_IF(t == IndexType::I64, !IsHugeMemoryEnabled(t)); + size_t desired = MaxMemoryLimitField(t); + constexpr size_t actual = ArrayBufferObject::MaxByteLength / PageSize; + return wasm::Pages(std::min(desired, actual)); +} + +size_t wasm::MaxMemoryBoundsCheckLimit(IndexType t) { + return MaxMemoryPages(t).byteLength(); +} + +#else +// On 32-bit systems, the heap limit must be representable in the nonnegative +// range of an int32_t, which means the maximum heap size as observed by wasm +// code is one wasm page less than 2GB. +wasm::Pages wasm::MaxMemoryPages(IndexType t) { + static_assert(ArrayBufferObject::MaxByteLength >= INT32_MAX / PageSize); + return wasm::Pages(INT32_MAX / PageSize); +} + +// The max bounds check limit can be larger than the MaxMemoryPages because it +// is really MaxMemoryPages rounded up to the next valid bounds check immediate, +// see ComputeMappedSize(). +size_t wasm::MaxMemoryBoundsCheckLimit(IndexType t) { + size_t boundsCheckLimit = size_t(INT32_MAX) + 1; + MOZ_ASSERT(IsValidBoundsCheckImmediate(boundsCheckLimit)); + return boundsCheckLimit; +} +#endif + +// Because ARM has a fixed-width instruction encoding, ARM can only express a +// limited subset of immediates (in a single instruction). + +static const uint64_t HighestValidARMImmediate = 0xff000000; + +// Heap length on ARM should fit in an ARM immediate. We approximate the set +// of valid ARM immediates with the predicate: +// 2^n for n in [16, 24) +// or +// 2^24 * n for n >= 1. +bool wasm::IsValidARMImmediate(uint32_t i) { + bool valid = (IsPowerOfTwo(i) || (i & 0x00ffffff) == 0); + + MOZ_ASSERT_IF(valid, i % PageSize == 0); + + return valid; +} + +uint64_t wasm::RoundUpToNextValidARMImmediate(uint64_t i) { + MOZ_ASSERT(i <= HighestValidARMImmediate); + static_assert(HighestValidARMImmediate == 0xff000000, + "algorithm relies on specific constant"); + + if (i <= 16 * 1024 * 1024) { + i = i ? mozilla::RoundUpPow2(i) : 0; + } else { + i = (i + 0x00ffffff) & ~0x00ffffff; + } + + MOZ_ASSERT(IsValidARMImmediate(i)); + + return i; +} + +Pages wasm::ClampedMaxPages(IndexType t, Pages initialPages, + const Maybe<Pages>& sourceMaxPages, + bool useHugeMemory) { + Pages clampedMaxPages; + + if (sourceMaxPages.isSome()) { + // There is a specified maximum, clamp it to the implementation limit of + // maximum pages + clampedMaxPages = std::min(*sourceMaxPages, wasm::MaxMemoryPages(t)); + +#ifndef JS_64BIT + static_assert(sizeof(uintptr_t) == 4, "assuming not 64 bit implies 32 bit"); + + // On 32-bit platforms, prevent applications specifying a large max (like + // MaxMemoryPages()) from unintentially OOMing the browser: they just want + // "a lot of memory". Maintain the invariant that initialPages <= + // clampedMaxPages. + static const uint64_t OneGib = 1 << 30; + static const Pages OneGibPages = Pages(OneGib >> wasm::PageBits); + static_assert(HighestValidARMImmediate > OneGib, + "computing mapped size on ARM requires clamped max size"); + + Pages clampedPages = std::max(OneGibPages, initialPages); + clampedMaxPages = std::min(clampedPages, clampedMaxPages); +#endif + } else { + // There is not a specified maximum, fill it in with the implementation + // limit of maximum pages + clampedMaxPages = wasm::MaxMemoryPages(t); + } + + // Double-check our invariants + MOZ_RELEASE_ASSERT(sourceMaxPages.isNothing() || + clampedMaxPages <= *sourceMaxPages); + MOZ_RELEASE_ASSERT(clampedMaxPages <= wasm::MaxMemoryPages(t)); + MOZ_RELEASE_ASSERT(initialPages <= clampedMaxPages); + + return clampedMaxPages; +} + +size_t wasm::ComputeMappedSize(wasm::Pages clampedMaxPages) { + // Caller is responsible to ensure that clampedMaxPages has been clamped to + // implementation limits. + size_t maxSize = clampedMaxPages.byteLength(); + + // It is the bounds-check limit, not the mapped size, that gets baked into + // code. Thus round up the maxSize to the next valid immediate value + // *before* adding in the guard page. + // + // Also see "Wasm Linear Memory Structure" in vm/ArrayBufferObject.cpp. + uint64_t boundsCheckLimit = RoundUpToNextValidBoundsCheckImmediate(maxSize); + MOZ_ASSERT(IsValidBoundsCheckImmediate(boundsCheckLimit)); + + MOZ_ASSERT(boundsCheckLimit % gc::SystemPageSize() == 0); + MOZ_ASSERT(GuardSize % gc::SystemPageSize() == 0); + return boundsCheckLimit + GuardSize; +} + +bool wasm::IsValidBoundsCheckImmediate(uint32_t i) { +#ifdef JS_CODEGEN_ARM + return IsValidARMImmediate(i); +#else + return true; +#endif +} + +uint64_t wasm::RoundUpToNextValidBoundsCheckImmediate(uint64_t i) { +#ifdef JS_CODEGEN_ARM + return RoundUpToNextValidARMImmediate(i); +#else + return i; +#endif +} |