Files
test/source/blender/blenlib/intern/BLI_mmap.cc
Jorn Visser 7df175c326 Fix: Unused result warning
Check the result of the write function to avoid an unused result
warning.

Pull Request: https://projects.blender.org/blender/blender/pulls/148198
2025-10-16 15:29:03 +02:00

499 lines
16 KiB
C++

/* SPDX-FileCopyrightText: 2020 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup bli
*/
#include "BLI_mmap.h"
#include "BLI_assert.h"
#include "BLI_fileops.h"
#include "BLI_mutex.hh"
#include "BLI_string_utils.hh"
#include "BLI_vector.hh"
#include "MEM_guardedalloc.h"
#include <atomic>
#include <cstring>
#ifndef WIN32
# include <csignal>
# include <cstdlib>
# include <sys/mman.h> /* For `mmap`. */
# include <unistd.h> /* For `write`. */
#else
# include "BLI_winstuff.h"
# include <io.h> /* For `_get_osfhandle`. */
#endif
struct BLI_mmap_file {
/* The address to which the file was mapped. */
char *memory;
/* The length of the file (and therefore the mapped region). */
size_t length;
/* Platform-specific handle for the mapping. */
void *volatile handle;
/* Flag to indicate IO errors. Needs to be volatile since it's being set from
* within the signal handler, which is not part of the normal execution flow. */
volatile bool io_error;
/* Used to break out of infinite loops when an error keeps occurring.
* See the comments in #try_handle_error_for_address for details. */
size_t id;
};
/* General mutex used to protect access to the list of open mapped files, ensure the handler is
* initialized only once and to prevent multiple threads from trying to remap the same
* memory-mapped region in parallel. */
static blender::Mutex mmap_mutex;
/* When using memory-mapped files, any IO errors will result in an EXCEPTION_IN_PAGE_ERROR on
* Windows and a SIGBUS signal on other platforms. Therefore, we need to catch that signal and
* stop reading the file in question. To do so, we keep a list of all currently opened
* memory-mapped files, and if a error is caught, we check if the failed address is inside one of
* the mapped regions. If it is, we set a flag to indicate a failed read and remap the memory in
* question to a zero-backed region in order to avoid additional signals. The code that actually
* reads the memory area has to check whether the flag was set after it's done reading. If the
* error occurred outside of a memory-mapped region or the remapping failed, we call the previous
* handler if one was initialized and abort the process otherwise on Linux and on Windows let the
* exception crash the program. */
static blender::Vector<BLI_mmap_file *> &open_mmaps_vector()
{
static blender::Vector<BLI_mmap_file *> open_mmaps;
return open_mmaps;
}
/* Print a message to the STDERR without using the standard library routines.
* If a MMAP error occurs while reading a pointer inside one of the standard library's IO routines,
* any global locks it was holding won't be unlocked when entering the handler.
* Using the normal printing routines could then cause a deadlock. */
static void print_error(const char *message);
/* Tries to replace the mapping with zeroes.
* Returns true on success. */
static bool try_map_zeros(BLI_mmap_file *file);
/* Find the file mapping containing the address and call #try_map_zeroes for it.
* Returns true when execution can continue. */
static bool try_handle_error_for_address(const void *address)
{
static thread_local size_t last_handled_file_id = -1;
std::unique_lock lock(mmap_mutex);
BLI_mmap_file *file = nullptr;
for (BLI_mmap_file *link_file : open_mmaps_vector()) {
/* Is the address where the error occurred in this file's mapped range? */
if (address >= link_file->memory && address < link_file->memory + link_file->length) {
file = link_file;
break;
}
}
if (file == nullptr) {
/* Not our error. */
return false;
}
/* Check if we already handled this error. */
if (file->io_error) {
/* If `file->io_error` is true, either a different thread has
* already replaced the mapping after this thread raised the
* exception, but before we got the lock, and execution can
* continue, or replacing the mapping did not avoid the current
* exception. We need to check if continuing execution fails to
* avoid an infinite loop in the second case. To detect such a
* situation, the last handled mapping's ID is stored per thread and
* compared against it to see if continuing execution was already
* tried for this mapping in this thread. If that is the case,
* forward the exception instead of continuing execution again. As
* multiple threads could encounter an exception for the same
* mapping at the same time, a boolean stored in `BLI_mmap_file`
* would not work for this detection, as the condition we need to
* detect is thread dependent. */
if (file->id == last_handled_file_id) {
/* Some possible causes of the error below are:
* - Thread safety issues in the error handling code.
* - Faulty remapping without having signaled an error in `try_map_zeros`.
* - Invalid usage of an address in the mapped range, such as
* unaligned access on some platforms.
*/
print_error(
"Error: Unexpected exception in mapped file which was already remapped with zeros.");
return false;
}
/* Another thread has already remapped the range, we can continue execution. */
last_handled_file_id = file->id;
return true;
}
last_handled_file_id = file->id;
file->io_error = true;
if (!try_map_zeros(file)) {
print_error("Error: Could not replace mapped file with zeros.");
return false;
}
return true;
}
#ifdef WIN32
using MapViewOfFile3Fn = PVOID(WINAPI *)(HANDLE FileMapping,
HANDLE Process,
PVOID BaseAddress,
ULONG64 Offset,
SIZE_T ViewSize,
ULONG AllocationType,
ULONG PageProtection,
MEM_EXTENDED_PARAMETER *ExtendedParameters,
ULONG ParameterCount);
using VirtualAlloc2Fn = PVOID(WINAPI *)(HANDLE Process,
PVOID BaseAddress,
SIZE_T Size,
ULONG AllocationType,
ULONG PageProtection,
MEM_EXTENDED_PARAMETER *ExtendedParameters,
ULONG ParameterCount);
/* Pointers to `MapViewOfFile3` and `VirtualAlloc2`, as they need to be dynamically linked
* at run-time because they are only available on Windows 10 (1803) or newer.
* If they are not available, error handling is not used. */
static MapViewOfFile3Fn mmap_MapViewOfFile3 = nullptr;
static VirtualAlloc2Fn mmap_VirtualAlloc2 = nullptr;
static void print_error(const char *message)
{
char buffer[256];
size_t length = BLI_string_join(buffer, sizeof(buffer), "BLI_mmap: ", message, "\r\n");
HANDLE stderr_handle = GetStdHandle(STD_ERROR_HANDLE);
WriteFile(stderr_handle, buffer, length, nullptr, nullptr);
}
static bool try_map_zeros(BLI_mmap_file *file)
{
if (!UnmapViewOfFileEx(file->memory, MEM_PRESERVE_PLACEHOLDER)) {
return false;
}
if (!CloseHandle(file->handle)) {
return false;
}
ULARGE_INTEGER length_ularge_int;
length_ularge_int.QuadPart = file->length;
file->handle = CreateFileMapping(INVALID_HANDLE_VALUE,
nullptr,
PAGE_READONLY,
length_ularge_int.HighPart,
length_ularge_int.LowPart,
nullptr);
if (file->handle == nullptr) {
return false;
}
void *memory = mmap_MapViewOfFile3(file->handle,
nullptr,
file->memory,
0,
file->length,
MEM_REPLACE_PLACEHOLDER,
PAGE_READONLY,
nullptr,
0);
if (memory == nullptr) {
return false;
}
BLI_assert(memory == file->memory);
return true;
}
static LONG page_exception_handler(EXCEPTION_POINTERS *ExceptionInfo) noexcept
{
/* On Windows, if an IO error occurs trying to read from a mapped file, an
* EXCEPTION_IN_PAGE_ERROR error will be raised. Also check for
* EXCEPTION_ACCESS_VIOLATION, which can be raised when a thread tries to read from the mapping
* while it is being replaced by another. */
if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_IN_PAGE_ERROR ||
ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
{
if (ExceptionInfo->ExceptionRecord->NumberParameters >= 2) {
/* Currently, MMAP'd files are read only, so don't replace the mapping when a write is
* attempted. */
if (ExceptionInfo->ExceptionRecord->ExceptionInformation[0] == 1) {
return EXCEPTION_CONTINUE_SEARCH;
}
const void *address = reinterpret_cast<const void *>(
ExceptionInfo->ExceptionRecord->ExceptionInformation[1]);
if (try_handle_error_for_address(address)) {
return EXCEPTION_CONTINUE_EXECUTION;
}
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
/* Ensures that the error handler is set up and ready. */
static bool ensure_mmap_initialized()
{
static std::atomic_bool initialized = false;
if (initialized) {
return true;
}
std::unique_lock lock(mmap_mutex);
if (!initialized) {
HMODULE kernelbase = ::LoadLibraryA("kernelbase.dll");
if (kernelbase) {
mmap_MapViewOfFile3 = reinterpret_cast<MapViewOfFile3Fn>(
::GetProcAddress(kernelbase, "MapViewOfFile3"));
mmap_VirtualAlloc2 = reinterpret_cast<VirtualAlloc2Fn>(
::GetProcAddress(kernelbase, "VirtualAlloc2"));
}
if (mmap_MapViewOfFile3 && mmap_VirtualAlloc2) {
/* First has to be FALSE to avoid our handler being called before ASAN's handler. */
AddVectoredExceptionHandler(FALSE, page_exception_handler);
}
else {
print_error("Could not load necessary functions for MMAP error handling.");
}
initialized = true;
}
return true;
}
#else /* !WIN32 */
static void print_error(const char *message)
{
char buffer[256];
size_t length = BLI_string_join(buffer, sizeof(buffer), "BLI_mmap: ", message, "\n");
if (write(STDERR_FILENO, buffer, length) < 0) {
/* If writing to stderr fails, there is nowhere to write an error about that. */
}
}
static bool try_map_zeros(BLI_mmap_file *file)
{
/* Replace the mapped memory with zeroes. */
const void *mapped_memory = mmap(
file->memory, file->length, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
if (mapped_memory == MAP_FAILED) {
return false;
}
return true;
}
static struct sigaction next_handler = {};
static void sigbus_handler(int sig, siginfo_t *siginfo, void *ptr) noexcept
{
/* We only handle SIGBUS here for now. */
BLI_assert(sig == SIGBUS);
if (try_handle_error_for_address(siginfo->si_addr)) {
return;
}
/* Fall back to the other handler if there was one.
*
* No lock is needed here, as #try_handle_error_for_address
* unconditionally locks `mmap_mutex`, and as such
* #ensure_mmap_initialized must have finished and #next_handler
* will be set up. */
if (next_handler.sa_sigaction && (next_handler.sa_flags & SA_SIGINFO)) {
next_handler.sa_sigaction(sig, siginfo, ptr);
}
else if (!ELEM(next_handler.sa_handler, nullptr, SIG_DFL, SIG_IGN)) {
next_handler.sa_handler(sig);
}
else {
print_error("Unhandled SIGBUS caught");
abort();
}
}
/* Ensures that the error handler is set up and ready. */
static bool ensure_mmap_initialized()
{
static std::atomic_bool initialized = false;
if (initialized) {
return true;
}
std::unique_lock lock(mmap_mutex);
if (!initialized) {
struct sigaction newact = {{nullptr}}, oldact = {{nullptr}};
newact.sa_sigaction = sigbus_handler;
newact.sa_flags = SA_SIGINFO;
if (sigaction(SIGBUS, &newact, &oldact)) {
return false;
}
/* Remember the previous handler to fall back to it if the error
* does not belong to any of the mapped files. */
next_handler = oldact;
initialized = true;
}
return true;
}
#endif /* !WIN32 */
/* Adds a file to the list that the error handler checks. */
static void error_handler_add(BLI_mmap_file *file)
{
std::unique_lock lock(mmap_mutex);
open_mmaps_vector().append(file);
}
/* Removes a file from the list that the error handler checks. */
static void error_handler_remove(BLI_mmap_file *file)
{
std::unique_lock lock(mmap_mutex);
open_mmaps_vector().remove_first_occurrence_and_reorder(file);
}
BLI_mmap_file *BLI_mmap_open(int fd)
{
static std::atomic_size_t id_counter = 0;
void *memory, *handle = nullptr;
const size_t length = BLI_lseek(fd, 0, SEEK_END);
if (UNLIKELY(length == size_t(-1))) {
return nullptr;
}
/* Ensures that the error handler is set up and ready. */
if (!ensure_mmap_initialized()) {
return nullptr;
}
#ifndef WIN32
/* Map the given file to memory. */
memory = mmap(nullptr, length, PROT_READ, MAP_PRIVATE, fd, 0);
if (memory == MAP_FAILED) {
return nullptr;
}
#else /* WIN32 */
/* Convert the POSIX-style file descriptor to a Windows handle. */
void *file_handle = (void *)_get_osfhandle(fd);
/* Memory mapping on Windows is a multi-step process - first we create a placeholder
* allocation. Then we create a mapping, and after that we create a view into that mapping
* on top of the placeholder. In our case, one view that spans the entire file is enough.
* NOTE: Changes to protection flags should also be reflected in #try_map_zeros. If write
* support is added, the write check in #page_exception_handler should be updated. */
if (mmap_MapViewOfFile3 && mmap_VirtualAlloc2) {
memory = mmap_VirtualAlloc2(nullptr,
nullptr,
length,
MEM_RESERVE | MEM_RESERVE_PLACEHOLDER,
PAGE_NOACCESS,
nullptr,
0);
if (memory == nullptr) {
return nullptr;
}
handle = CreateFileMapping(file_handle, nullptr, PAGE_READONLY, 0, 0, nullptr);
if (handle == nullptr) {
VirtualFree(memory, 0, MEM_RELEASE);
return nullptr;
}
if (mmap_MapViewOfFile3(handle,
nullptr,
memory,
0,
length,
MEM_REPLACE_PLACEHOLDER,
PAGE_READONLY,
nullptr,
0) == nullptr)
{
VirtualFree(memory, 0, MEM_RELEASE);
CloseHandle(handle);
return nullptr;
}
}
else {
/* Fallback without error handling in case `MapViewOfFile3` or `VirtualAlloc2` is not
* available. */
handle = CreateFileMapping(file_handle, nullptr, PAGE_READONLY, 0, 0, nullptr);
if (handle == nullptr) {
return nullptr;
}
memory = MapViewOfFile(handle, FILE_MAP_READ, 0, 0, 0);
if (memory == nullptr) {
CloseHandle(handle);
return nullptr;
}
}
#endif /* WIN32 */
/* Now that the mapping was successful, allocate memory and set up the #BLI_mmap_file. */
BLI_mmap_file *file = MEM_callocN<BLI_mmap_file>(__func__);
file->memory = static_cast<char *>(memory);
file->handle = handle;
file->length = length;
file->id = id_counter++;
/* Register the file with the error handler. */
error_handler_add(file);
return file;
}
bool BLI_mmap_read(BLI_mmap_file *file, void *dest, size_t offset, size_t length)
{
/* If a previous read has already failed or we try to read past the end,
* don't even attempt to read any further. */
if (file->io_error || (offset + length > file->length)) {
return false;
}
memcpy(dest, file->memory + offset, length);
return !file->io_error;
}
void *BLI_mmap_get_pointer(BLI_mmap_file *file)
{
return file->memory;
}
size_t BLI_mmap_get_length(const BLI_mmap_file *file)
{
return file->length;
}
bool BLI_mmap_any_io_error(const BLI_mmap_file *file)
{
return file->io_error;
}
void BLI_mmap_free(BLI_mmap_file *file)
{
error_handler_remove(file);
#ifndef WIN32
munmap((void *)file->memory, file->length);
#else
UnmapViewOfFile(file->memory);
CloseHandle(file->handle);
#endif
MEM_freeN(file);
}