Blender V5.0
BLI_mmap.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2020 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
8
9#include "BLI_mmap.h"
10#include "BLI_assert.h"
11#include "BLI_fileops.h"
12#include "BLI_mutex.hh"
13#include "BLI_string_utils.hh"
14#include "BLI_vector.hh"
15#include "MEM_guardedalloc.h"
16
17#include <atomic>
18#include <cstring>
19
20#ifndef WIN32
21# include <csignal>
22# include <cstdlib>
23# include <sys/mman.h> /* For `mmap`. */
24# include <unistd.h> /* For `write`. */
25#else
26# include "BLI_winstuff.h"
27# include <io.h> /* For `_get_osfhandle`. */
28#endif
29
31 /* The address to which the file was mapped. */
32 char *memory;
33
34 /* The length of the file (and therefore the mapped region). */
35 size_t length;
36
37 /* Platform-specific handle for the mapping. */
38 void *volatile handle;
39
40 /* Flag to indicate IO errors. Needs to be volatile since it's being set from
41 * within the signal handler, which is not part of the normal execution flow. */
42 volatile bool io_error;
43
44 /* Used to break out of infinite loops when an error keeps occurring.
45 * See the comments in #try_handle_error_for_address for details. */
46 size_t id;
47};
48
49/* General mutex used to protect access to the list of open mapped files, ensure the handler is
50 * initialized only once and to prevent multiple threads from trying to remap the same
51 * memory-mapped region in parallel. */
53
54/* When using memory-mapped files, any IO errors will result in an EXCEPTION_IN_PAGE_ERROR on
55 * Windows and a SIGBUS signal on other platforms. Therefore, we need to catch that signal and
56 * stop reading the file in question. To do so, we keep a list of all currently opened
57 * memory-mapped files, and if a error is caught, we check if the failed address is inside one of
58 * the mapped regions. If it is, we set a flag to indicate a failed read and remap the memory in
59 * question to a zero-backed region in order to avoid additional signals. The code that actually
60 * reads the memory area has to check whether the flag was set after it's done reading. If the
61 * error occurred outside of a memory-mapped region or the remapping failed, we call the previous
62 * handler if one was initialized and abort the process otherwise on Linux and on Windows let the
63 * exception crash the program. */
65{
66 static blender::Vector<BLI_mmap_file *> open_mmaps;
67 return open_mmaps;
68}
69
70/* Print a message to the STDERR without using the standard library routines.
71 * If a MMAP error occurs while reading a pointer inside one of the standard library's IO routines,
72 * any global locks it was holding won't be unlocked when entering the handler.
73 * Using the normal printing routines could then cause a deadlock. */
74static void print_error(const char *message);
75
76/* Tries to replace the mapping with zeroes.
77 * Returns true on success. */
78static bool try_map_zeros(BLI_mmap_file *file);
79
80/* Find the file mapping containing the address and call #try_map_zeroes for it.
81 * Returns true when execution can continue. */
82static bool try_handle_error_for_address(const void *address)
83{
84 static thread_local size_t last_handled_file_id = -1;
85
86 std::unique_lock lock(mmap_mutex);
87
88 BLI_mmap_file *file = nullptr;
89 for (BLI_mmap_file *link_file : open_mmaps_vector()) {
90 /* Is the address where the error occurred in this file's mapped range? */
91 if (address >= link_file->memory && address < link_file->memory + link_file->length) {
92 file = link_file;
93 break;
94 }
95 }
96
97 if (file == nullptr) {
98 /* Not our error. */
99 return false;
100 }
101
102 /* Check if we already handled this error. */
103 if (file->io_error) {
104 /* If `file->io_error` is true, either a different thread has
105 * already replaced the mapping after this thread raised the
106 * exception, but before we got the lock, and execution can
107 * continue, or replacing the mapping did not avoid the current
108 * exception. We need to check if continuing execution fails to
109 * avoid an infinite loop in the second case. To detect such a
110 * situation, the last handled mapping's ID is stored per thread and
111 * compared against it to see if continuing execution was already
112 * tried for this mapping in this thread. If that is the case,
113 * forward the exception instead of continuing execution again. As
114 * multiple threads could encounter an exception for the same
115 * mapping at the same time, a boolean stored in `BLI_mmap_file`
116 * would not work for this detection, as the condition we need to
117 * detect is thread dependent. */
118 if (file->id == last_handled_file_id) {
119 /* Some possible causes of the error below are:
120 * - Thread safety issues in the error handling code.
121 * - Faulty remapping without having signaled an error in `try_map_zeros`.
122 * - Invalid usage of an address in the mapped range, such as
123 * unaligned access on some platforms.
124 */
126 "Error: Unexpected exception in mapped file which was already remapped with zeros.");
127 return false;
128 }
129 /* Another thread has already remapped the range, we can continue execution. */
130 last_handled_file_id = file->id;
131 return true;
132 }
133
134 last_handled_file_id = file->id;
135 file->io_error = true;
136
137 if (!try_map_zeros(file)) {
138 print_error("Error: Could not replace mapped file with zeros.");
139 return false;
140 }
141
142 return true;
143}
144
145#ifdef WIN32
146using MapViewOfFile3Fn = PVOID(WINAPI *)(HANDLE FileMapping,
147 HANDLE Process,
148 PVOID BaseAddress,
149 ULONG64 Offset,
150 SIZE_T ViewSize,
151 ULONG AllocationType,
152 ULONG PageProtection,
153 MEM_EXTENDED_PARAMETER *ExtendedParameters,
154 ULONG ParameterCount);
155
156using VirtualAlloc2Fn = PVOID(WINAPI *)(HANDLE Process,
157 PVOID BaseAddress,
158 SIZE_T Size,
159 ULONG AllocationType,
160 ULONG PageProtection,
161 MEM_EXTENDED_PARAMETER *ExtendedParameters,
162 ULONG ParameterCount);
163
164/* Pointers to `MapViewOfFile3` and `VirtualAlloc2`, as they need to be dynamically linked
165 * at run-time because they are only available on Windows 10 (1803) or newer.
166 * If they are not available, error handling is not used. */
167static MapViewOfFile3Fn mmap_MapViewOfFile3 = nullptr;
168
169static VirtualAlloc2Fn mmap_VirtualAlloc2 = nullptr;
170
171static void print_error(const char *message)
172{
173 char buffer[256];
174 size_t length = BLI_string_join(buffer, sizeof(buffer), "BLI_mmap: ", message, "\r\n");
175 HANDLE stderr_handle = GetStdHandle(STD_ERROR_HANDLE);
176 WriteFile(stderr_handle, buffer, length, nullptr, nullptr);
177}
178
179static bool try_map_zeros(BLI_mmap_file *file)
180{
181 if (!UnmapViewOfFileEx(file->memory, MEM_PRESERVE_PLACEHOLDER)) {
182 return false;
183 }
184
185 if (!CloseHandle(file->handle)) {
186 return false;
187 }
188
189 ULARGE_INTEGER length_ularge_int;
190 length_ularge_int.QuadPart = file->length;
191 file->handle = CreateFileMapping(INVALID_HANDLE_VALUE,
192 nullptr,
193 PAGE_READONLY,
194 length_ularge_int.HighPart,
195 length_ularge_int.LowPart,
196 nullptr);
197 if (file->handle == nullptr) {
198 return false;
199 }
200
201 void *memory = mmap_MapViewOfFile3(file->handle,
202 nullptr,
203 file->memory,
204 0,
205 file->length,
206 MEM_REPLACE_PLACEHOLDER,
207 PAGE_READONLY,
208 nullptr,
209 0);
210 if (memory == nullptr) {
211 return false;
212 }
213
214 BLI_assert(memory == file->memory);
215
216 return true;
217}
218
219static LONG page_exception_handler(EXCEPTION_POINTERS *ExceptionInfo) noexcept
220{
221 /* On Windows, if an IO error occurs trying to read from a mapped file, an
222 * EXCEPTION_IN_PAGE_ERROR error will be raised. Also check for
223 * EXCEPTION_ACCESS_VIOLATION, which can be raised when a thread tries to read from the mapping
224 * while it is being replaced by another. */
225 if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_IN_PAGE_ERROR ||
226 ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
227 {
228 if (ExceptionInfo->ExceptionRecord->NumberParameters >= 2) {
229 /* Currently, MMAP'd files are read only, so don't replace the mapping when a write is
230 * attempted. */
231 if (ExceptionInfo->ExceptionRecord->ExceptionInformation[0] == 1) {
232 return EXCEPTION_CONTINUE_SEARCH;
233 }
234 const void *address = reinterpret_cast<const void *>(
235 ExceptionInfo->ExceptionRecord->ExceptionInformation[1]);
236 if (try_handle_error_for_address(address)) {
237 return EXCEPTION_CONTINUE_EXECUTION;
238 }
239 }
240 }
241 return EXCEPTION_CONTINUE_SEARCH;
242}
243
244/* Ensures that the error handler is set up and ready. */
245static bool ensure_mmap_initialized()
246{
247 static std::atomic_bool initialized = false;
248 if (initialized) {
249 return true;
250 }
251
252 std::unique_lock lock(mmap_mutex);
253
254 if (!initialized) {
255 HMODULE kernelbase = ::LoadLibraryA("kernelbase.dll");
256 if (kernelbase) {
257 mmap_MapViewOfFile3 = reinterpret_cast<MapViewOfFile3Fn>(
258 ::GetProcAddress(kernelbase, "MapViewOfFile3"));
259 mmap_VirtualAlloc2 = reinterpret_cast<VirtualAlloc2Fn>(
260 ::GetProcAddress(kernelbase, "VirtualAlloc2"));
261 }
262 if (mmap_MapViewOfFile3 && mmap_VirtualAlloc2) {
263 /* First has to be FALSE to avoid our handler being called before ASAN's handler. */
264 AddVectoredExceptionHandler(FALSE, page_exception_handler);
265 }
266 else {
267 print_error("Could not load necessary functions for MMAP error handling.");
268 }
269 initialized = true;
270 }
271 return true;
272}
273#else /* !WIN32 */
274static void print_error(const char *message)
275{
276 char buffer[256];
277 size_t length = BLI_string_join(buffer, sizeof(buffer), "BLI_mmap: ", message, "\n");
278 if (write(STDERR_FILENO, buffer, length) < 0) {
279 /* If writing to stderr fails, there is nowhere to write an error about that. */
280 }
281}
282
283static bool try_map_zeros(BLI_mmap_file *file)
284{
285 /* Replace the mapped memory with zeroes. */
286 const void *mapped_memory = mmap(
287 file->memory, file->length, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
288 if (mapped_memory == MAP_FAILED) {
289 return false;
290 }
291
292 return true;
293}
294
295static struct sigaction next_handler = {};
296
297static void sigbus_handler(int sig, siginfo_t *siginfo, void *ptr) noexcept
298{
299 /* We only handle SIGBUS here for now. */
300 BLI_assert(sig == SIGBUS);
301
302 if (try_handle_error_for_address(siginfo->si_addr)) {
303 return;
304 }
305
306 /* Fall back to the other handler if there was one.
307 *
308 * No lock is needed here, as #try_handle_error_for_address
309 * unconditionally locks `mmap_mutex`, and as such
310 * #ensure_mmap_initialized must have finished and #next_handler
311 * will be set up. */
312 if (next_handler.sa_sigaction && (next_handler.sa_flags & SA_SIGINFO)) {
313 next_handler.sa_sigaction(sig, siginfo, ptr);
314 }
315 else if (!ELEM(next_handler.sa_handler, nullptr, SIG_DFL, SIG_IGN)) {
316 next_handler.sa_handler(sig);
317 }
318 else {
319 print_error("Unhandled SIGBUS caught");
320 abort();
321 }
322}
323
324/* Ensures that the error handler is set up and ready. */
326{
327 static std::atomic_bool initialized = false;
328 if (initialized) {
329 return true;
330 }
331
332 std::unique_lock lock(mmap_mutex);
333 if (!initialized) {
334 struct sigaction newact = {{nullptr}}, oldact = {{nullptr}};
335
336 newact.sa_sigaction = sigbus_handler;
337 newact.sa_flags = SA_SIGINFO;
338
339 if (sigaction(SIGBUS, &newact, &oldact)) {
340 return false;
341 }
342
343 /* Remember the previous handler to fall back to it if the error
344 * does not belong to any of the mapped files. */
345 next_handler = oldact;
346 initialized = true;
347 }
348
349 return true;
350}
351#endif /* !WIN32 */
352
353/* Adds a file to the list that the error handler checks. */
355{
356 std::unique_lock lock(mmap_mutex);
357 open_mmaps_vector().append(file);
358}
359
360/* Removes a file from the list that the error handler checks. */
362{
363 std::unique_lock lock(mmap_mutex);
364 open_mmaps_vector().remove_first_occurrence_and_reorder(file);
365}
366
368{
369 static std::atomic_size_t id_counter = 0;
370
371 void *memory, *handle = nullptr;
372 const size_t length = BLI_lseek(fd, 0, SEEK_END);
373 if (UNLIKELY(length == size_t(-1))) {
374 return nullptr;
375 }
376
377 /* Ensures that the error handler is set up and ready. */
379 return nullptr;
380 }
381
382#ifndef WIN32
383 /* Map the given file to memory. */
384 memory = mmap(nullptr, length, PROT_READ, MAP_PRIVATE, fd, 0);
385 if (memory == MAP_FAILED) {
386 return nullptr;
387 }
388#else /* WIN32 */
389 /* Convert the POSIX-style file descriptor to a Windows handle. */
390 void *file_handle = (void *)_get_osfhandle(fd);
391
392 /* Memory mapping on Windows is a multi-step process - first we create a placeholder
393 * allocation. Then we create a mapping, and after that we create a view into that mapping
394 * on top of the placeholder. In our case, one view that spans the entire file is enough.
395 * NOTE: Changes to protection flags should also be reflected in #try_map_zeros. If write
396 * support is added, the write check in #page_exception_handler should be updated. */
397 if (mmap_MapViewOfFile3 && mmap_VirtualAlloc2) {
398 memory = mmap_VirtualAlloc2(nullptr,
399 nullptr,
400 length,
401 MEM_RESERVE | MEM_RESERVE_PLACEHOLDER,
402 PAGE_NOACCESS,
403 nullptr,
404 0);
405 if (memory == nullptr) {
406 return nullptr;
407 }
408
409 handle = CreateFileMapping(file_handle, nullptr, PAGE_READONLY, 0, 0, nullptr);
410 if (handle == nullptr) {
411 VirtualFree(memory, 0, MEM_RELEASE);
412 return nullptr;
413 }
414
415 if (mmap_MapViewOfFile3(handle,
416 nullptr,
417 memory,
418 0,
419 length,
420 MEM_REPLACE_PLACEHOLDER,
421 PAGE_READONLY,
422 nullptr,
423 0) == nullptr)
424 {
425 VirtualFree(memory, 0, MEM_RELEASE);
426 CloseHandle(handle);
427 return nullptr;
428 }
429 }
430 else {
431 /* Fallback without error handling in case `MapViewOfFile3` or `VirtualAlloc2` is not
432 * available. */
433 handle = CreateFileMapping(file_handle, nullptr, PAGE_READONLY, 0, 0, nullptr);
434 if (handle == nullptr) {
435 return nullptr;
436 }
437
438 memory = MapViewOfFile(handle, FILE_MAP_READ, 0, 0, 0);
439 if (memory == nullptr) {
440 CloseHandle(handle);
441 return nullptr;
442 }
443 }
444#endif /* WIN32 */
445
446 /* Now that the mapping was successful, allocate memory and set up the #BLI_mmap_file. */
448 file->memory = static_cast<char *>(memory);
449 file->handle = handle;
450 file->length = length;
451 file->id = id_counter++;
452
453 /* Register the file with the error handler. */
454 error_handler_add(file);
455
456 return file;
457}
458
459bool BLI_mmap_read(BLI_mmap_file *file, void *dest, size_t offset, size_t length)
460{
461 /* If a previous read has already failed or we try to read past the end,
462 * don't even attempt to read any further. */
463 if (file->io_error || (offset + length > file->length)) {
464 return false;
465 }
466
467 memcpy(dest, file->memory + offset, length);
468
469 return !file->io_error;
470}
471
473{
474 return file->memory;
475}
476
478{
479 return file->length;
480}
481
483{
484 return file->io_error;
485}
486
488{
490#ifndef WIN32
491 munmap((void *)file->memory, file->length);
492#else
493 UnmapViewOfFile(file->memory);
494 CloseHandle(file->handle);
495#endif
496
497 MEM_freeN(file);
498}
#define BLI_assert(a)
Definition BLI_assert.h:46
File and directory operations.
int64_t BLI_lseek(int fd, int64_t offset, int whence)
Definition storage.cc:208
static void print_error(const char *message)
Definition BLI_mmap.cc:274
void BLI_mmap_free(BLI_mmap_file *file)
Definition BLI_mmap.cc:487
bool BLI_mmap_read(BLI_mmap_file *file, void *dest, size_t offset, size_t length)
Definition BLI_mmap.cc:459
static blender::Mutex mmap_mutex
Definition BLI_mmap.cc:52
static bool try_handle_error_for_address(const void *address)
Definition BLI_mmap.cc:82
BLI_mmap_file * BLI_mmap_open(int fd)
Definition BLI_mmap.cc:367
static blender::Vector< BLI_mmap_file * > & open_mmaps_vector()
Definition BLI_mmap.cc:64
static void sigbus_handler(int sig, siginfo_t *siginfo, void *ptr) noexcept
Definition BLI_mmap.cc:297
static bool try_map_zeros(BLI_mmap_file *file)
Definition BLI_mmap.cc:283
static struct sigaction next_handler
Definition BLI_mmap.cc:295
size_t BLI_mmap_get_length(const BLI_mmap_file *file)
Definition BLI_mmap.cc:477
bool BLI_mmap_any_io_error(const BLI_mmap_file *file)
Definition BLI_mmap.cc:482
static void error_handler_remove(BLI_mmap_file *file)
Definition BLI_mmap.cc:361
void * BLI_mmap_get_pointer(BLI_mmap_file *file)
Definition BLI_mmap.cc:472
static void error_handler_add(BLI_mmap_file *file)
Definition BLI_mmap.cc:354
static bool ensure_mmap_initialized()
Definition BLI_mmap.cc:325
#define BLI_string_join(...)
#define UNLIKELY(x)
#define ELEM(...)
Compatibility-like things for windows.
#define FALSE
Read Guarded memory(de)allocation.
volatile int lock
static bool initialized
float length(VecOp< float, D >) RET
void * MEM_callocN(size_t len, const char *str)
Definition mallocn.cc:118
void MEM_freeN(void *vmemh)
Definition mallocn.cc:113
std::mutex Mutex
Definition BLI_mutex.hh:47
char * memory
Definition BLI_mmap.cc:32
void *volatile handle
Definition BLI_mmap.cc:38
size_t length
Definition BLI_mmap.cc:35
volatile bool io_error
Definition BLI_mmap.cc:42
PointerRNA * ptr
Definition wm_files.cc:4238