1 // Copyright 2019 the V8 project authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "src/objects/backing-store.h"
6
7 #include <cstring>
8
9 #include "src/base/platform/wrappers.h"
10 #include "src/execution/isolate.h"
11 #include "src/handles/global-handles.h"
12 #include "src/logging/counters.h"
13 #include "src/sandbox/sandbox.h"
14
15 #if V8_ENABLE_WEBASSEMBLY
16 #include "src/trap-handler/trap-handler.h"
17 #include "src/wasm/wasm-constants.h"
18 #include "src/wasm/wasm-engine.h"
19 #include "src/wasm/wasm-limits.h"
20 #include "src/wasm/wasm-objects-inl.h"
21 #endif // V8_ENABLE_WEBASSEMBLY
22
23 #define TRACE_BS(...) \
24 do { \
25 if (FLAG_trace_backing_store) PrintF(__VA_ARGS__); \
26 } while (false)
27
28 namespace v8 {
29 namespace internal {
30
31 namespace {
32
33 #if V8_ENABLE_WEBASSEMBLY
34 constexpr uint64_t kNegativeGuardSize = uint64_t{2} * GB;
35
36 #if V8_TARGET_ARCH_64_BIT
37 constexpr uint64_t kFullGuardSize = uint64_t{10} * GB;
38 #endif
39
40 #endif // V8_ENABLE_WEBASSEMBLY
41
42 std::atomic<uint32_t> next_backing_store_id_{1};
43
44 // Allocation results are reported to UMA
45 //
46 // See wasm_memory_allocation_result in counters-definitions.h
47 enum class AllocationStatus {
48 kSuccess, // Succeeded on the first try
49
50 kSuccessAfterRetry, // Succeeded after garbage collection
51
52 kAddressSpaceLimitReachedFailure, // Failed because Wasm is at its address
53 // space limit
54
55 kOtherFailure // Failed for an unknown reason
56 };
57
58 // Attempts to allocate memory inside the sandbox currently fall back to
59 // allocating memory outside of the sandbox if necessary. Once this fallback is
60 // no longer allowed/possible, these cases will become allocation failures
61 // instead. To track the frequency of such events, the outcome of memory
62 // allocation attempts inside the sandbox is reported to UMA.
63 //
64 // See caged_memory_allocation_outcome in counters-definitions.h
65 // This class and the entry in counters-definitions.h use the term "cage"
66 // instead of "sandbox" for historical reasons.
67 enum class CagedMemoryAllocationOutcome {
68 kSuccess, // Allocation succeeded inside the cage
69 kOutsideCage, // Allocation failed inside the cage but succeeded outside
70 kFailure, // Allocation failed inside and outside of the cage
71 };
72
GetReservedRegion(bool has_guard_regions,void * buffer_start,size_t byte_capacity)73 base::AddressRegion GetReservedRegion(bool has_guard_regions,
74 void* buffer_start,
75 size_t byte_capacity) {
76 #if V8_TARGET_ARCH_64_BIT && V8_ENABLE_WEBASSEMBLY
77 if (has_guard_regions) {
78 // Guard regions always look like this:
79 // |xxx(2GiB)xxx|.......(4GiB)..xxxxx|xxxxxx(4GiB)xxxxxx|
80 // ^ buffer_start
81 // ^ byte_length
82 // ^ negative guard region ^ positive guard region
83
84 Address start = reinterpret_cast<Address>(buffer_start);
85 DCHECK_EQ(8, sizeof(size_t)); // only use on 64-bit
86 DCHECK_EQ(0, start % AllocatePageSize());
87 return base::AddressRegion(start - kNegativeGuardSize,
88 static_cast<size_t>(kFullGuardSize));
89 }
90 #endif
91
92 DCHECK(!has_guard_regions);
93 return base::AddressRegion(reinterpret_cast<Address>(buffer_start),
94 byte_capacity);
95 }
96
GetReservationSize(bool has_guard_regions,size_t byte_capacity)97 size_t GetReservationSize(bool has_guard_regions, size_t byte_capacity) {
98 #if V8_TARGET_ARCH_64_BIT && V8_ENABLE_WEBASSEMBLY
99 if (has_guard_regions) return kFullGuardSize;
100 #else
101 DCHECK(!has_guard_regions);
102 #endif
103
104 return byte_capacity;
105 }
106
RecordStatus(Isolate * isolate,AllocationStatus status)107 void RecordStatus(Isolate* isolate, AllocationStatus status) {
108 isolate->counters()->wasm_memory_allocation_result()->AddSample(
109 static_cast<int>(status));
110 }
111
112 // When the sandbox is active, this function records the outcome of attempts to
113 // allocate memory inside the sandbox which fall back to allocating memory
114 // outside of the sandbox. Passing a value of nullptr for the result indicates
115 // that the memory could not be allocated at all.
RecordSandboxMemoryAllocationResult(Isolate * isolate,void * result)116 void RecordSandboxMemoryAllocationResult(Isolate* isolate, void* result) {
117 // This metric is only meaningful when the sandbox is active.
118 #ifdef V8_SANDBOX
119 if (GetProcessWideSandbox()->is_initialized()) {
120 CagedMemoryAllocationOutcome outcome;
121 if (result) {
122 bool allocation_in_cage = GetProcessWideSandbox()->Contains(result);
123 outcome = allocation_in_cage ? CagedMemoryAllocationOutcome::kSuccess
124 : CagedMemoryAllocationOutcome::kOutsideCage;
125 } else {
126 outcome = CagedMemoryAllocationOutcome::kFailure;
127 }
128 isolate->counters()->caged_memory_allocation_outcome()->AddSample(
129 static_cast<int>(outcome));
130 }
131 #endif
132 }
133
DebugCheckZero(void * start,size_t byte_length)134 inline void DebugCheckZero(void* start, size_t byte_length) {
135 #if DEBUG
136 // Double check memory is zero-initialized. Despite being DEBUG-only,
137 // this function is somewhat optimized for the benefit of test suite
138 // execution times (some tests allocate several gigabytes).
139 const byte* bytes = reinterpret_cast<const byte*>(start);
140 const size_t kBaseCase = 32;
141 for (size_t i = 0; i < kBaseCase && i < byte_length; i++) {
142 DCHECK_EQ(0, bytes[i]);
143 }
144 // Having checked the first kBaseCase bytes to be zero, we can now use
145 // {memcmp} to compare the range against itself shifted by that amount,
146 // thereby inductively checking the remaining bytes.
147 if (byte_length > kBaseCase) {
148 DCHECK_EQ(0, memcmp(bytes, bytes + kBaseCase, byte_length - kBaseCase));
149 }
150 #endif
151 }
152 } // namespace
153
154 // The backing store for a Wasm shared memory remembers all the isolates
155 // with which it has been shared.
156 struct SharedWasmMemoryData {
157 std::vector<Isolate*> isolates_;
158 };
159
Clear()160 void BackingStore::Clear() {
161 buffer_start_ = nullptr;
162 byte_length_ = 0;
163 has_guard_regions_ = false;
164 if (holds_shared_ptr_to_allocator_) {
165 type_specific_data_.v8_api_array_buffer_allocator_shared
166 .std::shared_ptr<v8::ArrayBuffer::Allocator>::~shared_ptr();
167 holds_shared_ptr_to_allocator_ = false;
168 }
169 type_specific_data_.v8_api_array_buffer_allocator = nullptr;
170 }
171
BackingStore(void * buffer_start,size_t byte_length,size_t max_byte_length,size_t byte_capacity,SharedFlag shared,ResizableFlag resizable,bool is_wasm_memory,bool free_on_destruct,bool has_guard_regions,bool custom_deleter,bool empty_deleter)172 BackingStore::BackingStore(void* buffer_start, size_t byte_length,
173 size_t max_byte_length, size_t byte_capacity,
174 SharedFlag shared, ResizableFlag resizable,
175 bool is_wasm_memory, bool free_on_destruct,
176 bool has_guard_regions, bool custom_deleter,
177 bool empty_deleter)
178 : buffer_start_(buffer_start),
179 byte_length_(byte_length),
180 max_byte_length_(max_byte_length),
181 byte_capacity_(byte_capacity),
182 id_(next_backing_store_id_.fetch_add(1)),
183 is_shared_(shared == SharedFlag::kShared),
184 is_resizable_(resizable == ResizableFlag::kResizable),
185 is_wasm_memory_(is_wasm_memory),
186 holds_shared_ptr_to_allocator_(false),
187 free_on_destruct_(free_on_destruct),
188 has_guard_regions_(has_guard_regions),
189 globally_registered_(false),
190 custom_deleter_(custom_deleter),
191 empty_deleter_(empty_deleter) {
192 // TODO(v8:11111): RAB / GSAB - Wasm integration.
193 DCHECK_IMPLIES(is_wasm_memory_, !is_resizable_);
194 DCHECK_IMPLIES(is_resizable_, !custom_deleter_);
195 DCHECK_IMPLIES(is_resizable_, free_on_destruct_);
196 DCHECK_IMPLIES(!is_wasm_memory && !is_resizable_,
197 byte_length_ == max_byte_length_);
198 DCHECK_GE(max_byte_length_, byte_length_);
199 DCHECK_GE(byte_capacity_, max_byte_length_);
200 }
201
~BackingStore()202 BackingStore::~BackingStore() {
203 GlobalBackingStoreRegistry::Unregister(this);
204
205 if (buffer_start_ == nullptr) {
206 Clear();
207 return;
208 }
209
210 PageAllocator* page_allocator = GetPlatformPageAllocator();
211 // TODO(saelo) here and elsewhere in this file, replace with
212 // GetArrayBufferPageAllocator once the fallback to the platform page
213 // allocator is no longer allowed.
214 #ifdef V8_SANDBOX
215 if (GetProcessWideSandbox()->Contains(buffer_start_)) {
216 page_allocator = GetSandboxPageAllocator();
217 } else {
218 DCHECK(kAllowBackingStoresOutsideSandbox);
219 }
220 #endif
221
222 #if V8_ENABLE_WEBASSEMBLY
223 if (is_wasm_memory_) {
224 // TODO(v8:11111): RAB / GSAB - Wasm integration.
225 DCHECK(!is_resizable_);
226 DCHECK(free_on_destruct_);
227 DCHECK(!custom_deleter_);
228 size_t reservation_size =
229 GetReservationSize(has_guard_regions_, byte_capacity_);
230 TRACE_BS(
231 "BSw:free bs=%p mem=%p (length=%zu, capacity=%zu, reservation=%zu)\n",
232 this, buffer_start_, byte_length(), byte_capacity_, reservation_size);
233 if (is_shared_) {
234 // Deallocate the list of attached memory objects.
235 SharedWasmMemoryData* shared_data = get_shared_wasm_memory_data();
236 delete shared_data;
237 type_specific_data_.shared_wasm_memory_data = nullptr;
238 }
239
240 // Wasm memories are always allocated through the page allocator.
241 auto region =
242 GetReservedRegion(has_guard_regions_, buffer_start_, byte_capacity_);
243
244 if (!region.is_empty()) {
245 FreePages(page_allocator, reinterpret_cast<void*>(region.begin()),
246 region.size());
247 }
248 Clear();
249 return;
250 }
251 #endif // V8_ENABLE_WEBASSEMBLY
252
253 if (is_resizable_) {
254 DCHECK(free_on_destruct_);
255 DCHECK(!custom_deleter_);
256 auto region =
257 GetReservedRegion(has_guard_regions_, buffer_start_, byte_capacity_);
258
259 if (!region.is_empty()) {
260 FreePages(page_allocator, reinterpret_cast<void*>(region.begin()),
261 region.size());
262 }
263 Clear();
264 return;
265 }
266 if (custom_deleter_) {
267 DCHECK(free_on_destruct_);
268 TRACE_BS("BS:custom deleter bs=%p mem=%p (length=%zu, capacity=%zu)\n",
269 this, buffer_start_, byte_length(), byte_capacity_);
270 type_specific_data_.deleter.callback(buffer_start_, byte_length_,
271 type_specific_data_.deleter.data);
272 Clear();
273 return;
274 }
275 if (free_on_destruct_) {
276 // JSArrayBuffer backing store. Deallocate through the embedder's allocator.
277 auto allocator = get_v8_api_array_buffer_allocator();
278 TRACE_BS("BS:free bs=%p mem=%p (length=%zu, capacity=%zu)\n", this,
279 buffer_start_, byte_length(), byte_capacity_);
280 allocator->Free(buffer_start_, byte_length_);
281 }
282 Clear();
283 }
284
285 // Allocate a backing store using the array buffer allocator from the embedder.
Allocate(Isolate * isolate,size_t byte_length,SharedFlag shared,InitializedFlag initialized)286 std::unique_ptr<BackingStore> BackingStore::Allocate(
287 Isolate* isolate, size_t byte_length, SharedFlag shared,
288 InitializedFlag initialized) {
289 void* buffer_start = nullptr;
290 auto allocator = isolate->array_buffer_allocator();
291 CHECK_NOT_NULL(allocator);
292 if (byte_length != 0) {
293 auto counters = isolate->counters();
294 int mb_length = static_cast<int>(byte_length / MB);
295 if (mb_length > 0) {
296 counters->array_buffer_big_allocations()->AddSample(mb_length);
297 }
298 if (shared == SharedFlag::kShared) {
299 counters->shared_array_allocations()->AddSample(mb_length);
300 }
301 auto allocate_buffer = [allocator, initialized](size_t byte_length) {
302 if (initialized == InitializedFlag::kUninitialized) {
303 return allocator->AllocateUninitialized(byte_length);
304 }
305 void* buffer_start = allocator->Allocate(byte_length);
306 if (buffer_start) {
307 // TODO(wasm): node does not implement the zero-initialization API.
308 // Reenable this debug check when node does implement it properly.
309 constexpr bool
310 kDebugCheckZeroDisabledDueToNodeNotImplementingZeroInitAPI = true;
311 if ((!(kDebugCheckZeroDisabledDueToNodeNotImplementingZeroInitAPI)) &&
312 !FLAG_mock_arraybuffer_allocator) {
313 DebugCheckZero(buffer_start, byte_length);
314 }
315 }
316 return buffer_start;
317 };
318
319 buffer_start = isolate->heap()->AllocateExternalBackingStore(
320 allocate_buffer, byte_length);
321
322 if (buffer_start == nullptr) {
323 // Allocation failed.
324 counters->array_buffer_new_size_failures()->AddSample(mb_length);
325 return {};
326 }
327 }
328
329 auto result = new BackingStore(buffer_start, // start
330 byte_length, // length
331 byte_length, // max length
332 byte_length, // capacity
333 shared, // shared
334 ResizableFlag::kNotResizable, // resizable
335 false, // is_wasm_memory
336 true, // free_on_destruct
337 false, // has_guard_regions
338 false, // custom_deleter
339 false); // empty_deleter
340
341 TRACE_BS("BS:alloc bs=%p mem=%p (length=%zu)\n", result,
342 result->buffer_start(), byte_length);
343 result->SetAllocatorFromIsolate(isolate);
344 return std::unique_ptr<BackingStore>(result);
345 }
346
SetAllocatorFromIsolate(Isolate * isolate)347 void BackingStore::SetAllocatorFromIsolate(Isolate* isolate) {
348 if (auto allocator_shared = isolate->array_buffer_allocator_shared()) {
349 holds_shared_ptr_to_allocator_ = true;
350 new (&type_specific_data_.v8_api_array_buffer_allocator_shared)
351 std::shared_ptr<v8::ArrayBuffer::Allocator>(
352 std::move(allocator_shared));
353 } else {
354 type_specific_data_.v8_api_array_buffer_allocator =
355 isolate->array_buffer_allocator();
356 }
357 }
358
359 #if V8_ENABLE_WEBASSEMBLY
360 // Allocate a backing store for a Wasm memory. Always use the page allocator
361 // and add guard regions.
TryAllocateWasmMemory(Isolate * isolate,size_t initial_pages,size_t maximum_pages,SharedFlag shared)362 std::unique_ptr<BackingStore> BackingStore::TryAllocateWasmMemory(
363 Isolate* isolate, size_t initial_pages, size_t maximum_pages,
364 SharedFlag shared) {
365 // Compute size of reserved memory.
366 size_t engine_max_pages = wasm::max_mem_pages();
367 maximum_pages = std::min(engine_max_pages, maximum_pages);
368
369 auto result = TryAllocateAndPartiallyCommitMemory(
370 isolate, initial_pages * wasm::kWasmPageSize,
371 maximum_pages * wasm::kWasmPageSize, wasm::kWasmPageSize, initial_pages,
372 maximum_pages, true, shared);
373 // Shared Wasm memories need an anchor for the memory object list.
374 if (result && shared == SharedFlag::kShared) {
375 result->type_specific_data_.shared_wasm_memory_data =
376 new SharedWasmMemoryData();
377 }
378 return result;
379 }
380 #endif // V8_ENABLE_WEBASSEMBLY
381
TryAllocateAndPartiallyCommitMemory(Isolate * isolate,size_t byte_length,size_t max_byte_length,size_t page_size,size_t initial_pages,size_t maximum_pages,bool is_wasm_memory,SharedFlag shared)382 std::unique_ptr<BackingStore> BackingStore::TryAllocateAndPartiallyCommitMemory(
383 Isolate* isolate, size_t byte_length, size_t max_byte_length,
384 size_t page_size, size_t initial_pages, size_t maximum_pages,
385 bool is_wasm_memory, SharedFlag shared) {
386 // Enforce engine limitation on the maximum number of pages.
387 if (maximum_pages > std::numeric_limits<size_t>::max() / page_size) {
388 return nullptr;
389 }
390
391 // Cannot reserve 0 pages on some OSes.
392 if (maximum_pages == 0) maximum_pages = 1;
393
394 TRACE_BS("BSw:try %zu pages, %zu max\n", initial_pages, maximum_pages);
395
396 #if V8_ENABLE_WEBASSEMBLY
397 bool guards = is_wasm_memory && trap_handler::IsTrapHandlerEnabled();
398 #else
399 CHECK(!is_wasm_memory);
400 bool guards = false;
401 #endif // V8_ENABLE_WEBASSEMBLY
402
403 // For accounting purposes, whether a GC was necessary.
404 bool did_retry = false;
405
406 // A helper to try running a function up to 3 times, executing a GC
407 // if the first and second attempts failed.
408 auto gc_retry = [&](const std::function<bool()>& fn) {
409 for (int i = 0; i < 3; i++) {
410 if (fn()) return true;
411 // Collect garbage and retry.
412 did_retry = true;
413 // TODO(wasm): try Heap::EagerlyFreeExternalMemory() first?
414 isolate->heap()->MemoryPressureNotification(
415 MemoryPressureLevel::kCritical, true);
416 }
417 return false;
418 };
419
420 size_t byte_capacity = maximum_pages * page_size;
421 size_t reservation_size = GetReservationSize(guards, byte_capacity);
422
423 //--------------------------------------------------------------------------
424 // Allocate pages (inaccessible by default).
425 //--------------------------------------------------------------------------
426 void* allocation_base = nullptr;
427 PageAllocator* page_allocator = GetPlatformPageAllocator();
428 auto allocate_pages = [&] {
429 #ifdef V8_SANDBOX
430 page_allocator = GetSandboxPageAllocator();
431 allocation_base = AllocatePages(page_allocator, nullptr, reservation_size,
432 page_size, PageAllocator::kNoAccess);
433 if (allocation_base) return true;
434 // We currently still allow falling back to the platform page allocator if
435 // the sandbox page allocator fails. This will eventually be removed.
436 // TODO(chromium:1218005) once we forbid the fallback, we should have a
437 // single API, e.g. GetArrayBufferPageAllocator(), that returns the correct
438 // page allocator to use here depending on whether the sandbox is enabled
439 // or not.
440 if (!kAllowBackingStoresOutsideSandbox) return false;
441 page_allocator = GetPlatformPageAllocator();
442 #endif
443 allocation_base = AllocatePages(page_allocator, nullptr, reservation_size,
444 page_size, PageAllocator::kNoAccess);
445 return allocation_base != nullptr;
446 };
447 if (!gc_retry(allocate_pages)) {
448 // Page allocator could not reserve enough pages.
449 RecordStatus(isolate, AllocationStatus::kOtherFailure);
450 RecordSandboxMemoryAllocationResult(isolate, nullptr);
451 TRACE_BS("BSw:try failed to allocate pages\n");
452 return {};
453 }
454
455 // Get a pointer to the start of the buffer, skipping negative guard region
456 // if necessary.
457 #if V8_ENABLE_WEBASSEMBLY
458 byte* buffer_start = reinterpret_cast<byte*>(allocation_base) +
459 (guards ? kNegativeGuardSize : 0);
460 #else
461 DCHECK(!guards);
462 byte* buffer_start = reinterpret_cast<byte*>(allocation_base);
463 #endif
464
465 //--------------------------------------------------------------------------
466 // Commit the initial pages (allow read/write).
467 //--------------------------------------------------------------------------
468 size_t committed_byte_length = initial_pages * page_size;
469 auto commit_memory = [&] {
470 return committed_byte_length == 0 ||
471 SetPermissions(page_allocator, buffer_start, committed_byte_length,
472 PageAllocator::kReadWrite);
473 };
474 if (!gc_retry(commit_memory)) {
475 TRACE_BS("BSw:try failed to set permissions (%p, %zu)\n", buffer_start,
476 committed_byte_length);
477 FreePages(page_allocator, allocation_base, reservation_size);
478 // SetPermissions put us over the process memory limit.
479 // We return an empty result so that the caller can throw an exception.
480 return {};
481 }
482
483 DebugCheckZero(buffer_start, byte_length); // touch the bytes.
484
485 RecordStatus(isolate, did_retry ? AllocationStatus::kSuccessAfterRetry
486 : AllocationStatus::kSuccess);
487 RecordSandboxMemoryAllocationResult(isolate, allocation_base);
488
489 ResizableFlag resizable =
490 is_wasm_memory ? ResizableFlag::kNotResizable : ResizableFlag::kResizable;
491
492 auto result = new BackingStore(buffer_start, // start
493 byte_length, // length
494 max_byte_length, // max_byte_length
495 byte_capacity, // capacity
496 shared, // shared
497 resizable, // resizable
498 is_wasm_memory, // is_wasm_memory
499 true, // free_on_destruct
500 guards, // has_guard_regions
501 false, // custom_deleter
502 false); // empty_deleter
503
504 TRACE_BS(
505 "BSw:alloc bs=%p mem=%p (length=%zu, capacity=%zu, reservation=%zu)\n",
506 result, result->buffer_start(), byte_length, byte_capacity,
507 reservation_size);
508
509 return std::unique_ptr<BackingStore>(result);
510 }
511
512 #if V8_ENABLE_WEBASSEMBLY
513 // Allocate a backing store for a Wasm memory. Always use the page allocator
514 // and add guard regions.
AllocateWasmMemory(Isolate * isolate,size_t initial_pages,size_t maximum_pages,SharedFlag shared)515 std::unique_ptr<BackingStore> BackingStore::AllocateWasmMemory(
516 Isolate* isolate, size_t initial_pages, size_t maximum_pages,
517 SharedFlag shared) {
518 // Wasm pages must be a multiple of the allocation page size.
519 DCHECK_EQ(0, wasm::kWasmPageSize % AllocatePageSize());
520
521 // Enforce engine limitation on the maximum number of pages.
522 if (initial_pages > wasm::max_mem_pages()) return nullptr;
523
524 auto backing_store =
525 TryAllocateWasmMemory(isolate, initial_pages, maximum_pages, shared);
526 if (maximum_pages == initial_pages) {
527 // If initial pages, and maximum are equal, nothing more to do return early.
528 return backing_store;
529 }
530
531 // Retry with smaller maximum pages at each retry.
532 const int kAllocationTries = 3;
533 auto delta = (maximum_pages - initial_pages) / (kAllocationTries + 1);
534 size_t sizes[] = {maximum_pages - delta, maximum_pages - 2 * delta,
535 maximum_pages - 3 * delta, initial_pages};
536
537 for (size_t i = 0; i < arraysize(sizes) && !backing_store; i++) {
538 backing_store =
539 TryAllocateWasmMemory(isolate, initial_pages, sizes[i], shared);
540 }
541 return backing_store;
542 }
543
CopyWasmMemory(Isolate * isolate,size_t new_pages,size_t max_pages)544 std::unique_ptr<BackingStore> BackingStore::CopyWasmMemory(Isolate* isolate,
545 size_t new_pages,
546 size_t max_pages) {
547 // Note that we could allocate uninitialized to save initialization cost here,
548 // but since Wasm memories are allocated by the page allocator, the zeroing
549 // cost is already built-in.
550 auto new_backing_store = BackingStore::AllocateWasmMemory(
551 isolate, new_pages, max_pages,
552 is_shared() ? SharedFlag::kShared : SharedFlag::kNotShared);
553
554 if (!new_backing_store ||
555 new_backing_store->has_guard_regions() != has_guard_regions_) {
556 return {};
557 }
558
559 if (byte_length_ > 0) {
560 // If the allocation was successful, then the new buffer must be at least
561 // as big as the old one.
562 DCHECK_GE(new_pages * wasm::kWasmPageSize, byte_length_);
563 memcpy(new_backing_store->buffer_start(), buffer_start_, byte_length_);
564 }
565
566 return new_backing_store;
567 }
568
569 // Try to grow the size of a wasm memory in place, without realloc + copy.
GrowWasmMemoryInPlace(Isolate * isolate,size_t delta_pages,size_t max_pages)570 base::Optional<size_t> BackingStore::GrowWasmMemoryInPlace(Isolate* isolate,
571 size_t delta_pages,
572 size_t max_pages) {
573 // This function grows wasm memory by
574 // * changing the permissions of additional {delta_pages} pages to kReadWrite;
575 // * increment {byte_length_};
576 //
577 // As this code is executed concurrently, the following steps are executed:
578 // 1) Read the current value of {byte_length_};
579 // 2) Change the permission of all pages from {buffer_start_} to
580 // {byte_length_} + {delta_pages} * {page_size} to kReadWrite;
581 // * This operation may be executed racefully. The OS takes care of
582 // synchronization.
583 // 3) Try to update {byte_length_} with a compare_exchange;
584 // 4) Repeat 1) to 3) until the compare_exchange in 3) succeeds;
585 //
586 // The result of this function is the {byte_length_} before growing in pages.
587 // The result of this function appears like the result of an RMW-update on
588 // {byte_length_}, i.e. two concurrent calls to this function will result in
589 // different return values if {delta_pages} != 0.
590 //
591 // Invariants:
592 // * Permissions are always set incrementally, i.e. for any page {b} with
593 // kReadWrite permission, all pages between the first page {a} and page {b}
594 // also have kReadWrite permission.
595 // * {byte_length_} is always lower or equal than the amount of memory with
596 // permissions set to kReadWrite;
597 // * This is guaranteed by incrementing {byte_length_} with a
598 // compare_exchange after changing the permissions.
599 // * This invariant is the reason why we cannot use a fetch_add.
600 DCHECK(is_wasm_memory_);
601 max_pages = std::min(max_pages, byte_capacity_ / wasm::kWasmPageSize);
602
603 // Do a compare-exchange loop, because we also need to adjust page
604 // permissions. Note that multiple racing grows both try to set page
605 // permissions for the entire range (to be RW), so the operating system
606 // should deal with that raciness. We know we succeeded when we can
607 // compare/swap the old length with the new length.
608 size_t old_length = byte_length_.load(std::memory_order_relaxed);
609
610 if (delta_pages == 0)
611 return {old_length / wasm::kWasmPageSize}; // degenerate grow.
612 if (delta_pages > max_pages) return {}; // would never work.
613
614 size_t new_length = 0;
615 while (true) {
616 size_t current_pages = old_length / wasm::kWasmPageSize;
617
618 // Check if we have exceed the supplied maximum.
619 if (current_pages > (max_pages - delta_pages)) return {};
620
621 new_length = (current_pages + delta_pages) * wasm::kWasmPageSize;
622
623 // Try to adjust the permissions on the memory.
624 if (!i::SetPermissions(GetPlatformPageAllocator(), buffer_start_,
625 new_length, PageAllocator::kReadWrite)) {
626 return {};
627 }
628 if (byte_length_.compare_exchange_weak(old_length, new_length,
629 std::memory_order_acq_rel)) {
630 // Successfully updated both the length and permissions.
631 break;
632 }
633 }
634
635 if (!is_shared_ && free_on_destruct_) {
636 // Only do per-isolate accounting for non-shared backing stores.
637 reinterpret_cast<v8::Isolate*>(isolate)
638 ->AdjustAmountOfExternalAllocatedMemory(new_length - old_length);
639 }
640 return {old_length / wasm::kWasmPageSize};
641 }
642
AttachSharedWasmMemoryObject(Isolate * isolate,Handle<WasmMemoryObject> memory_object)643 void BackingStore::AttachSharedWasmMemoryObject(
644 Isolate* isolate, Handle<WasmMemoryObject> memory_object) {
645 DCHECK(is_wasm_memory_);
646 DCHECK(is_shared_);
647 // We need to take the global registry lock for this operation.
648 GlobalBackingStoreRegistry::AddSharedWasmMemoryObject(isolate, this,
649 memory_object);
650 }
651
BroadcastSharedWasmMemoryGrow(Isolate * isolate,std::shared_ptr<BackingStore> backing_store)652 void BackingStore::BroadcastSharedWasmMemoryGrow(
653 Isolate* isolate, std::shared_ptr<BackingStore> backing_store) {
654 GlobalBackingStoreRegistry::BroadcastSharedWasmMemoryGrow(isolate,
655 backing_store);
656 }
657
RemoveSharedWasmMemoryObjects(Isolate * isolate)658 void BackingStore::RemoveSharedWasmMemoryObjects(Isolate* isolate) {
659 GlobalBackingStoreRegistry::Purge(isolate);
660 }
661
UpdateSharedWasmMemoryObjects(Isolate * isolate)662 void BackingStore::UpdateSharedWasmMemoryObjects(Isolate* isolate) {
663 GlobalBackingStoreRegistry::UpdateSharedWasmMemoryObjects(isolate);
664 }
665 #endif // V8_ENABLE_WEBASSEMBLY
666
667 // Commit already reserved memory (for RAB backing stores (not shared)).
ResizeInPlace(Isolate * isolate,size_t new_byte_length,size_t new_committed_length)668 BackingStore::ResizeOrGrowResult BackingStore::ResizeInPlace(
669 Isolate* isolate, size_t new_byte_length, size_t new_committed_length) {
670 DCHECK_LE(new_byte_length, new_committed_length);
671 DCHECK(!is_shared());
672
673 if (new_byte_length < byte_length_) {
674 // TOOO(v8:11111): Figure out a strategy for shrinking - when do we
675 // un-commit the memory?
676
677 // Zero the memory so that in case the buffer is grown later, we have
678 // zeroed the contents already.
679 memset(reinterpret_cast<byte*>(buffer_start_) + new_byte_length, 0,
680 byte_length_ - new_byte_length);
681
682 // Changing the byte length wouldn't strictly speaking be needed, since
683 // the JSArrayBuffer already stores the updated length. This is to keep
684 // the BackingStore and JSArrayBuffer in sync.
685 byte_length_ = new_byte_length;
686 return kSuccess;
687 }
688 if (new_byte_length == byte_length_) {
689 // i::SetPermissions with size 0 fails on some platforms, so special
690 // handling for the case byte_length_ == new_byte_length == 0 is required.
691 return kSuccess;
692 }
693
694 // Try to adjust the permissions on the memory.
695 if (!i::SetPermissions(GetPlatformPageAllocator(), buffer_start_,
696 new_committed_length, PageAllocator::kReadWrite)) {
697 return kFailure;
698 }
699
700 // Do per-isolate accounting for non-shared backing stores.
701 DCHECK(free_on_destruct_);
702 reinterpret_cast<v8::Isolate*>(isolate)
703 ->AdjustAmountOfExternalAllocatedMemory(new_byte_length - byte_length_);
704 byte_length_ = new_byte_length;
705 return kSuccess;
706 }
707
708 // Commit already reserved memory (for GSAB backing stores (shared)).
GrowInPlace(Isolate * isolate,size_t new_byte_length,size_t new_committed_length)709 BackingStore::ResizeOrGrowResult BackingStore::GrowInPlace(
710 Isolate* isolate, size_t new_byte_length, size_t new_committed_length) {
711 DCHECK_LE(new_byte_length, new_committed_length);
712 DCHECK(is_shared());
713 // See comment in GrowWasmMemoryInPlace.
714 // GrowableSharedArrayBuffer.prototype.grow can be called from several
715 // threads. If two threads try to grow() in a racy way, the spec allows the
716 // larger grow to throw also if the smaller grow succeeds first. The
717 // implementation below doesn't throw in that case - instead, it retries and
718 // succeeds. If the larger grow finishes first though, the smaller grow must
719 // throw.
720 size_t old_byte_length = byte_length_.load(std::memory_order_seq_cst);
721 while (true) {
722 if (new_byte_length < old_byte_length) {
723 // The caller checks for the new_byte_length < old_byte_length_ case. This
724 // can only happen if another thread grew the memory after that.
725 return kRace;
726 }
727 if (new_byte_length == old_byte_length) {
728 // i::SetPermissions with size 0 fails on some platforms, so special
729 // handling for the case old_byte_length == new_byte_length == 0 is
730 // required.
731 return kSuccess;
732 }
733
734 // Try to adjust the permissions on the memory.
735 if (!i::SetPermissions(GetPlatformPageAllocator(), buffer_start_,
736 new_committed_length, PageAllocator::kReadWrite)) {
737 return kFailure;
738 }
739
740 // compare_exchange_weak updates old_byte_length.
741 if (byte_length_.compare_exchange_weak(old_byte_length, new_byte_length,
742 std::memory_order_seq_cst)) {
743 // Successfully updated both the length and permissions.
744 break;
745 }
746 }
747 return kSuccess;
748 }
749
WrapAllocation(Isolate * isolate,void * allocation_base,size_t allocation_length,SharedFlag shared,bool free_on_destruct)750 std::unique_ptr<BackingStore> BackingStore::WrapAllocation(
751 Isolate* isolate, void* allocation_base, size_t allocation_length,
752 SharedFlag shared, bool free_on_destruct) {
753 auto result = new BackingStore(allocation_base, // start
754 allocation_length, // length
755 allocation_length, // max length
756 allocation_length, // capacity
757 shared, // shared
758 ResizableFlag::kNotResizable, // resizable
759 false, // is_wasm_memory
760 free_on_destruct, // free_on_destruct
761 false, // has_guard_regions
762 false, // custom_deleter
763 false); // empty_deleter
764 result->SetAllocatorFromIsolate(isolate);
765 TRACE_BS("BS:wrap bs=%p mem=%p (length=%zu)\n", result,
766 result->buffer_start(), result->byte_length());
767 return std::unique_ptr<BackingStore>(result);
768 }
769
WrapAllocation(void * allocation_base,size_t allocation_length,v8::BackingStore::DeleterCallback deleter,void * deleter_data,SharedFlag shared)770 std::unique_ptr<BackingStore> BackingStore::WrapAllocation(
771 void* allocation_base, size_t allocation_length,
772 v8::BackingStore::DeleterCallback deleter, void* deleter_data,
773 SharedFlag shared) {
774 bool is_empty_deleter = (deleter == v8::BackingStore::EmptyDeleter);
775 auto result = new BackingStore(allocation_base, // start
776 allocation_length, // length
777 allocation_length, // max length
778 allocation_length, // capacity
779 shared, // shared
780 ResizableFlag::kNotResizable, // resizable
781 false, // is_wasm_memory
782 true, // free_on_destruct
783 false, // has_guard_regions
784 true, // custom_deleter
785 is_empty_deleter); // empty_deleter
786 result->type_specific_data_.deleter = {deleter, deleter_data};
787 TRACE_BS("BS:wrap bs=%p mem=%p (length=%zu)\n", result,
788 result->buffer_start(), result->byte_length());
789 return std::unique_ptr<BackingStore>(result);
790 }
791
EmptyBackingStore(SharedFlag shared)792 std::unique_ptr<BackingStore> BackingStore::EmptyBackingStore(
793 SharedFlag shared) {
794 auto result = new BackingStore(nullptr, // start
795 0, // length
796 0, // max length
797 0, // capacity
798 shared, // shared
799 ResizableFlag::kNotResizable, // resizable
800 false, // is_wasm_memory
801 true, // free_on_destruct
802 false, // has_guard_regions
803 false, // custom_deleter
804 false); // empty_deleter
805
806 return std::unique_ptr<BackingStore>(result);
807 }
808
Reallocate(Isolate * isolate,size_t new_byte_length)809 bool BackingStore::Reallocate(Isolate* isolate, size_t new_byte_length) {
810 CHECK(!is_wasm_memory_ && !custom_deleter_ && !globally_registered_ &&
811 free_on_destruct_ && !is_resizable_);
812 auto allocator = get_v8_api_array_buffer_allocator();
813 CHECK_EQ(isolate->array_buffer_allocator(), allocator);
814 CHECK_EQ(byte_length_, byte_capacity_);
815 void* new_start =
816 allocator->Reallocate(buffer_start_, byte_length_, new_byte_length);
817 if (!new_start) return false;
818 buffer_start_ = new_start;
819 byte_capacity_ = new_byte_length;
820 byte_length_ = new_byte_length;
821 max_byte_length_ = new_byte_length;
822 return true;
823 }
824
get_v8_api_array_buffer_allocator()825 v8::ArrayBuffer::Allocator* BackingStore::get_v8_api_array_buffer_allocator() {
826 CHECK(!is_wasm_memory_);
827 auto array_buffer_allocator =
828 holds_shared_ptr_to_allocator_
829 ? type_specific_data_.v8_api_array_buffer_allocator_shared.get()
830 : type_specific_data_.v8_api_array_buffer_allocator;
831 CHECK_NOT_NULL(array_buffer_allocator);
832 return array_buffer_allocator;
833 }
834
get_shared_wasm_memory_data()835 SharedWasmMemoryData* BackingStore::get_shared_wasm_memory_data() {
836 CHECK(is_wasm_memory_ && is_shared_);
837 auto shared_wasm_memory_data = type_specific_data_.shared_wasm_memory_data;
838 CHECK(shared_wasm_memory_data);
839 return shared_wasm_memory_data;
840 }
841
842 namespace {
843 // Implementation details of GlobalBackingStoreRegistry.
844 struct GlobalBackingStoreRegistryImpl {
845 GlobalBackingStoreRegistryImpl() = default;
846 base::Mutex mutex_;
847 std::unordered_map<const void*, std::weak_ptr<BackingStore>> map_;
848 };
849 base::LazyInstance<GlobalBackingStoreRegistryImpl>::type global_registry_impl_ =
850 LAZY_INSTANCE_INITIALIZER;
impl()851 inline GlobalBackingStoreRegistryImpl* impl() {
852 return global_registry_impl_.Pointer();
853 }
854 } // namespace
855
Register(std::shared_ptr<BackingStore> backing_store)856 void GlobalBackingStoreRegistry::Register(
857 std::shared_ptr<BackingStore> backing_store) {
858 if (!backing_store || !backing_store->buffer_start()) return;
859 // Only wasm memory backing stores need to be registered globally.
860 CHECK(backing_store->is_wasm_memory());
861
862 base::MutexGuard scope_lock(&impl()->mutex_);
863 if (backing_store->globally_registered_) return;
864 TRACE_BS("BS:reg bs=%p mem=%p (length=%zu, capacity=%zu)\n",
865 backing_store.get(), backing_store->buffer_start(),
866 backing_store->byte_length(), backing_store->byte_capacity());
867 std::weak_ptr<BackingStore> weak = backing_store;
868 auto result = impl()->map_.insert({backing_store->buffer_start(), weak});
869 CHECK(result.second);
870 backing_store->globally_registered_ = true;
871 }
872
Unregister(BackingStore * backing_store)873 void GlobalBackingStoreRegistry::Unregister(BackingStore* backing_store) {
874 if (!backing_store->globally_registered_) return;
875
876 CHECK(backing_store->is_wasm_memory());
877
878 DCHECK_NOT_NULL(backing_store->buffer_start());
879
880 base::MutexGuard scope_lock(&impl()->mutex_);
881 const auto& result = impl()->map_.find(backing_store->buffer_start());
882 if (result != impl()->map_.end()) {
883 DCHECK(!result->second.lock());
884 impl()->map_.erase(result);
885 }
886 backing_store->globally_registered_ = false;
887 }
888
Purge(Isolate * isolate)889 void GlobalBackingStoreRegistry::Purge(Isolate* isolate) {
890 // We need to keep a reference to all backing stores that are inspected
891 // in the purging loop below. Otherwise, we might get a deadlock
892 // if the temporary backing store reference created in the loop is
893 // the last reference. In that case the destructor of the backing store
894 // may try to take the &impl()->mutex_ in order to unregister itself.
895 std::vector<std::shared_ptr<BackingStore>> prevent_destruction_under_lock;
896 base::MutexGuard scope_lock(&impl()->mutex_);
897 // Purge all entries in the map that refer to the given isolate.
898 for (auto& entry : impl()->map_) {
899 auto backing_store = entry.second.lock();
900 prevent_destruction_under_lock.emplace_back(backing_store);
901 if (!backing_store) continue; // skip entries where weak ptr is null
902 CHECK(backing_store->is_wasm_memory());
903 if (!backing_store->is_shared()) continue; // skip non-shared memory
904 SharedWasmMemoryData* shared_data =
905 backing_store->get_shared_wasm_memory_data();
906 // Remove this isolate from the isolates list.
907 auto& isolates = shared_data->isolates_;
908 for (size_t i = 0; i < isolates.size(); i++) {
909 if (isolates[i] == isolate) isolates[i] = nullptr;
910 }
911 }
912 }
913
914 #if V8_ENABLE_WEBASSEMBLY
AddSharedWasmMemoryObject(Isolate * isolate,BackingStore * backing_store,Handle<WasmMemoryObject> memory_object)915 void GlobalBackingStoreRegistry::AddSharedWasmMemoryObject(
916 Isolate* isolate, BackingStore* backing_store,
917 Handle<WasmMemoryObject> memory_object) {
918 // Add to the weak array list of shared memory objects in the isolate.
919 isolate->AddSharedWasmMemory(memory_object);
920
921 // Add the isolate to the list of isolates sharing this backing store.
922 base::MutexGuard scope_lock(&impl()->mutex_);
923 SharedWasmMemoryData* shared_data =
924 backing_store->get_shared_wasm_memory_data();
925 auto& isolates = shared_data->isolates_;
926 int free_entry = -1;
927 for (size_t i = 0; i < isolates.size(); i++) {
928 if (isolates[i] == isolate) return;
929 if (isolates[i] == nullptr) free_entry = static_cast<int>(i);
930 }
931 if (free_entry >= 0)
932 isolates[free_entry] = isolate;
933 else
934 isolates.push_back(isolate);
935 }
936
BroadcastSharedWasmMemoryGrow(Isolate * isolate,std::shared_ptr<BackingStore> backing_store)937 void GlobalBackingStoreRegistry::BroadcastSharedWasmMemoryGrow(
938 Isolate* isolate, std::shared_ptr<BackingStore> backing_store) {
939 {
940 // The global lock protects the list of isolates per backing store.
941 base::MutexGuard scope_lock(&impl()->mutex_);
942 SharedWasmMemoryData* shared_data =
943 backing_store->get_shared_wasm_memory_data();
944 for (Isolate* other : shared_data->isolates_) {
945 if (other && other != isolate) {
946 other->stack_guard()->RequestGrowSharedMemory();
947 }
948 }
949 }
950 // Update memory objects in this isolate.
951 UpdateSharedWasmMemoryObjects(isolate);
952 }
953
UpdateSharedWasmMemoryObjects(Isolate * isolate)954 void GlobalBackingStoreRegistry::UpdateSharedWasmMemoryObjects(
955 Isolate* isolate) {
956 HandleScope scope(isolate);
957 Handle<WeakArrayList> shared_wasm_memories =
958 isolate->factory()->shared_wasm_memories();
959
960 for (int i = 0; i < shared_wasm_memories->length(); i++) {
961 HeapObject obj;
962 if (!shared_wasm_memories->Get(i).GetHeapObject(&obj)) continue;
963
964 Handle<WasmMemoryObject> memory_object(WasmMemoryObject::cast(obj),
965 isolate);
966 Handle<JSArrayBuffer> old_buffer(memory_object->array_buffer(), isolate);
967 std::shared_ptr<BackingStore> backing_store = old_buffer->GetBackingStore();
968
969 Handle<JSArrayBuffer> new_buffer =
970 isolate->factory()->NewJSSharedArrayBuffer(std::move(backing_store));
971 memory_object->update_instances(isolate, new_buffer);
972 }
973 }
974 #endif // V8_ENABLE_WEBASSEMBLY
975
976 } // namespace internal
977 } // namespace v8
978
979 #undef TRACE_BS
980