summaryrefslogtreecommitdiffstats
path: root/js/src/wasm/WasmMemory.cpp
blob: b5566921e0f597c632d8f1add1534fb2ab8a46ba (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
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
}