BaseHeap::Dispose() walks the page table and calls DeallocFixed(addr, 0, kRelease) for every allocated page region. These addresses are within file-backed view mappings (i.e. guest memory). On Windows, VirtualFree on file-mapped pages silently returns FALSE. On POSIX, the code hit assert_always() and crashed. Changed to return false to match Windows behavior. Memory leaks from munmap(addr, 0): Callers passed length=0 to DeallocFixed(kRelease). On Windows this works because VirtualFree(addr, 0, MEM_RELEASE) means "release the entire region" — Windows ignores the length parameter for MEM_RELEASE and always frees the whole allocation. On POSIX, munmap(addr, 0) fails with EINVAL, so the memory was silently leaked every time.
375 lines
11 KiB
C++
375 lines
11 KiB
C++
/**
|
|
******************************************************************************
|
|
* Xenia : Xbox 360 Emulator Research Project *
|
|
******************************************************************************
|
|
* Copyright 2025 Ben Vanik. All rights reserved. *
|
|
* Released under the BSD license - see LICENSE in the root for more details. *
|
|
******************************************************************************
|
|
*/
|
|
|
|
#include "xenia/base/memory.h"
|
|
|
|
#include <fcntl.h>
|
|
#include <sys/mman.h>
|
|
#include <unistd.h>
|
|
#include <algorithm>
|
|
#include <cstddef>
|
|
#include <cstdlib>
|
|
#include <fstream>
|
|
#include <mutex>
|
|
#include <sstream>
|
|
|
|
#include "xenia/base/math.h"
|
|
#include "xenia/base/platform.h"
|
|
#include "xenia/base/string.h"
|
|
|
|
#if XE_PLATFORM_ANDROID
|
|
#include <dlfcn.h>
|
|
#include <linux/ashmem.h>
|
|
#include <string.h>
|
|
#include <sys/ioctl.h>
|
|
|
|
#include "xenia/base/main_android.h"
|
|
#endif
|
|
|
|
namespace xe {
|
|
namespace memory {
|
|
|
|
#if XE_PLATFORM_ANDROID
|
|
// May be null if no dynamically loaded functions are required.
|
|
static void* libandroid_;
|
|
// API 26+.
|
|
static int (*android_ASharedMemory_create_)(const char* name, size_t size);
|
|
|
|
void AndroidInitialize() {
|
|
if (xe::GetAndroidApiLevel() >= 26) {
|
|
libandroid_ = dlopen("libandroid.so", RTLD_NOW);
|
|
assert_not_null(libandroid_);
|
|
if (libandroid_) {
|
|
android_ASharedMemory_create_ =
|
|
reinterpret_cast<decltype(android_ASharedMemory_create_)>(
|
|
dlsym(libandroid_, "ASharedMemory_create"));
|
|
assert_not_null(android_ASharedMemory_create_);
|
|
}
|
|
}
|
|
}
|
|
|
|
void AndroidShutdown() {
|
|
android_ASharedMemory_create_ = nullptr;
|
|
if (libandroid_) {
|
|
dlclose(libandroid_);
|
|
libandroid_ = nullptr;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
size_t page_size() { return getpagesize(); }
|
|
size_t allocation_granularity() { return page_size(); }
|
|
|
|
uint32_t ToPosixProtectFlags(PageAccess access) {
|
|
switch (access) {
|
|
case PageAccess::kNoAccess:
|
|
return PROT_NONE;
|
|
case PageAccess::kReadOnly:
|
|
return PROT_READ;
|
|
case PageAccess::kReadWrite:
|
|
return PROT_READ | PROT_WRITE;
|
|
case PageAccess::kExecuteReadOnly:
|
|
return PROT_READ | PROT_EXEC;
|
|
case PageAccess::kExecuteReadWrite:
|
|
return PROT_READ | PROT_WRITE | PROT_EXEC;
|
|
default:
|
|
assert_unhandled_case(access);
|
|
return PROT_NONE;
|
|
}
|
|
}
|
|
|
|
PageAccess ToXeniaProtectFlags(const char* protection) {
|
|
if (protection[0] == 'r' && protection[1] == 'w' && protection[2] == 'x') {
|
|
return PageAccess::kExecuteReadWrite;
|
|
}
|
|
if (protection[0] == 'r' && protection[1] == '-' && protection[2] == 'x') {
|
|
return PageAccess::kExecuteReadOnly;
|
|
}
|
|
if (protection[0] == 'r' && protection[1] == 'w' && protection[2] == '-') {
|
|
return PageAccess::kReadWrite;
|
|
}
|
|
if (protection[0] == 'r' && protection[1] == '-' && protection[2] == '-') {
|
|
return PageAccess::kReadOnly;
|
|
}
|
|
return PageAccess::kNoAccess;
|
|
}
|
|
|
|
bool IsWritableExecutableMemorySupported() { return true; }
|
|
|
|
struct MappedFileRange {
|
|
uintptr_t region_begin;
|
|
uintptr_t region_end;
|
|
};
|
|
|
|
std::vector<MappedFileRange> mapped_file_ranges;
|
|
std::mutex g_mapped_file_ranges_mutex;
|
|
|
|
// Track shm file names for cleanup on exit
|
|
std::vector<std::string> g_shm_file_names;
|
|
std::mutex g_shm_file_names_mutex;
|
|
static bool g_cleanup_handlers_installed = false;
|
|
|
|
#if !XE_PLATFORM_ANDROID
|
|
static void CleanupAtExit() {
|
|
for (const auto& name : g_shm_file_names) {
|
|
shm_unlink(name.c_str());
|
|
}
|
|
}
|
|
|
|
static void InstallCleanupHandlers() {
|
|
if (g_cleanup_handlers_installed) {
|
|
return;
|
|
}
|
|
g_cleanup_handlers_installed = true;
|
|
|
|
std::atexit(CleanupAtExit);
|
|
std::at_quick_exit(CleanupAtExit);
|
|
}
|
|
#endif // !XE_PLATFORM_ANDROID
|
|
|
|
void* AllocFixed(void* base_address, size_t length,
|
|
AllocationType allocation_type, PageAccess access) {
|
|
// mmap does not support reserve / commit, so ignore allocation_type.
|
|
uint32_t prot = ToPosixProtectFlags(access);
|
|
int flags = MAP_PRIVATE | MAP_ANONYMOUS;
|
|
|
|
if (base_address != nullptr) {
|
|
if (allocation_type == AllocationType::kCommit) {
|
|
if (Protect(base_address, length, access)) {
|
|
return base_address;
|
|
}
|
|
return nullptr;
|
|
}
|
|
flags |= MAP_FIXED_NOREPLACE;
|
|
}
|
|
|
|
void* result = mmap(base_address, length, prot, flags, -1, 0);
|
|
|
|
if (result != MAP_FAILED) {
|
|
return result;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
bool DeallocFixed(void* base_address, size_t length,
|
|
DeallocationType deallocation_type) {
|
|
const auto region_begin = reinterpret_cast<uintptr_t>(base_address);
|
|
const uintptr_t region_end =
|
|
reinterpret_cast<uintptr_t>(base_address) + length;
|
|
|
|
std::lock_guard guard(g_mapped_file_ranges_mutex);
|
|
for (const auto& mapped_range : mapped_file_ranges) {
|
|
if (region_begin >= mapped_range.region_begin &&
|
|
region_end <= mapped_range.region_end) {
|
|
switch (deallocation_type) {
|
|
case DeallocationType::kDecommit:
|
|
return Protect(base_address, length, PageAccess::kNoAccess);
|
|
case DeallocationType::kRelease:
|
|
return false;
|
|
default:
|
|
assert_unhandled_case(deallocation_type);
|
|
}
|
|
}
|
|
}
|
|
|
|
switch (deallocation_type) {
|
|
case DeallocationType::kDecommit:
|
|
return Protect(base_address, length, PageAccess::kNoAccess);
|
|
case DeallocationType::kRelease:
|
|
return munmap(base_address, length) == 0;
|
|
default:
|
|
assert_unhandled_case(deallocation_type);
|
|
}
|
|
}
|
|
|
|
bool Protect(void* base_address, size_t length, PageAccess access,
|
|
PageAccess* out_old_access) {
|
|
if (out_old_access) {
|
|
size_t length_copy = length;
|
|
QueryProtect(base_address, length_copy, *out_old_access);
|
|
}
|
|
|
|
uint32_t prot = ToPosixProtectFlags(access);
|
|
return mprotect(base_address, length, prot) == 0;
|
|
}
|
|
|
|
bool QueryProtect(void* base_address, size_t& length, PageAccess& access_out) {
|
|
// No generic POSIX solution exists. The Linux solution should work on all
|
|
// Linux kernel based OS, including Android.
|
|
std::ifstream memory_maps;
|
|
memory_maps.open("/proc/self/maps", std::ios_base::in);
|
|
std::string maps_entry_string;
|
|
|
|
while (std::getline(memory_maps, maps_entry_string)) {
|
|
std::stringstream entry_stream(maps_entry_string);
|
|
uintptr_t map_region_begin, map_region_end;
|
|
char separator;
|
|
char protection[5]; // 4 chars (e.g., "r-xp") + null terminator
|
|
|
|
entry_stream >> std::hex >> map_region_begin >> separator >>
|
|
map_region_end >> protection;
|
|
|
|
if (map_region_begin <= reinterpret_cast<uintptr_t>(base_address) &&
|
|
map_region_end > reinterpret_cast<uintptr_t>(base_address)) {
|
|
length = map_region_end - reinterpret_cast<uintptr_t>(base_address);
|
|
|
|
access_out = ToXeniaProtectFlags(protection);
|
|
|
|
// Look at the next consecutive mappings
|
|
while (std::getline(memory_maps, maps_entry_string)) {
|
|
std::stringstream next_entry_stream(maps_entry_string);
|
|
uintptr_t next_map_region_begin, next_map_region_end;
|
|
char next_protection[5]; // 4 chars (e.g., "r-xp") + null terminator
|
|
|
|
next_entry_stream >> std::hex >> next_map_region_begin >> separator >>
|
|
next_map_region_end >> next_protection;
|
|
if (map_region_end == next_map_region_begin &&
|
|
access_out == ToXeniaProtectFlags(next_protection)) {
|
|
length =
|
|
next_map_region_end - reinterpret_cast<uintptr_t>(base_address);
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
memory_maps.close();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
memory_maps.close();
|
|
return false;
|
|
}
|
|
|
|
FileMappingHandle CreateFileMappingHandle(const std::filesystem::path& path,
|
|
size_t length, PageAccess access,
|
|
bool commit) {
|
|
#if XE_PLATFORM_ANDROID
|
|
// TODO(Triang3l): Check if memfd can be used instead on API 30+.
|
|
if (android_ASharedMemory_create_) {
|
|
int sharedmem_fd = android_ASharedMemory_create_(path.c_str(), length);
|
|
return sharedmem_fd >= 0 ? sharedmem_fd : kFileMappingHandleInvalid;
|
|
}
|
|
|
|
// Use /dev/ashmem on API versions below 26, which added ASharedMemory.
|
|
// /dev/ashmem was disabled on API 29 for apps targeting it.
|
|
// https://chromium.googlesource.com/chromium/src/+/master/third_party/ashmem/ashmem-dev.c
|
|
int ashmem_fd = open("/" ASHMEM_NAME_DEF, O_RDWR);
|
|
if (ashmem_fd < 0) {
|
|
return kFileMappingHandleInvalid;
|
|
}
|
|
char ashmem_name[ASHMEM_NAME_LEN];
|
|
strlcpy(ashmem_name, path.c_str(), xe::countof(ashmem_name));
|
|
if (ioctl(ashmem_fd, ASHMEM_SET_NAME, ashmem_name) < 0 ||
|
|
ioctl(ashmem_fd, ASHMEM_SET_SIZE, length) < 0) {
|
|
close(ashmem_fd);
|
|
return kFileMappingHandleInvalid;
|
|
}
|
|
return ashmem_fd;
|
|
#else
|
|
int oflag;
|
|
switch (access) {
|
|
case PageAccess::kNoAccess:
|
|
oflag = 0;
|
|
break;
|
|
case PageAccess::kReadOnly:
|
|
case PageAccess::kExecuteReadOnly:
|
|
oflag = O_RDONLY;
|
|
break;
|
|
case PageAccess::kReadWrite:
|
|
case PageAccess::kExecuteReadWrite:
|
|
oflag = O_RDWR;
|
|
break;
|
|
default:
|
|
assert_always();
|
|
return kFileMappingHandleInvalid;
|
|
}
|
|
oflag |= O_CREAT;
|
|
auto full_path = "/" / path;
|
|
int ret = shm_open(full_path.c_str(), oflag, 0777);
|
|
if (ret < 0) {
|
|
return kFileMappingHandleInvalid;
|
|
}
|
|
if (ftruncate64(ret, length) < 0) {
|
|
close(ret);
|
|
shm_unlink(full_path.c_str());
|
|
return kFileMappingHandleInvalid;
|
|
}
|
|
// Track for cleanup on abnormal exit and install cleanup handlers
|
|
{
|
|
std::lock_guard guard(g_shm_file_names_mutex);
|
|
g_shm_file_names.push_back(full_path.string());
|
|
}
|
|
InstallCleanupHandlers();
|
|
return ret;
|
|
#endif
|
|
}
|
|
|
|
void CloseFileMappingHandle(FileMappingHandle handle,
|
|
const std::filesystem::path& path) {
|
|
close(handle);
|
|
#if !XE_PLATFORM_ANDROID
|
|
auto full_path = "/" / path;
|
|
shm_unlink(full_path.c_str());
|
|
// Remove from tracking
|
|
{
|
|
std::lock_guard guard(g_shm_file_names_mutex);
|
|
auto it = std::find(g_shm_file_names.begin(), g_shm_file_names.end(),
|
|
full_path.string());
|
|
if (it != g_shm_file_names.end()) {
|
|
g_shm_file_names.erase(it);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void* MapFileView(FileMappingHandle handle, void* base_address, size_t length,
|
|
PageAccess access, size_t file_offset) {
|
|
uint32_t prot = ToPosixProtectFlags(access);
|
|
|
|
int flags = MAP_SHARED;
|
|
if (base_address != nullptr) {
|
|
flags |= MAP_FIXED_NOREPLACE;
|
|
}
|
|
|
|
void* result = mmap(base_address, length, prot, flags, handle, file_offset);
|
|
|
|
if (result != MAP_FAILED) {
|
|
std::lock_guard guard(g_mapped_file_ranges_mutex);
|
|
mapped_file_ranges.push_back(
|
|
{reinterpret_cast<uintptr_t>(result),
|
|
reinterpret_cast<uintptr_t>(result) + length});
|
|
return result;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
bool UnmapFileView(FileMappingHandle handle, void* base_address,
|
|
size_t length) {
|
|
std::lock_guard guard(g_mapped_file_ranges_mutex);
|
|
for (auto mapped_range = mapped_file_ranges.begin();
|
|
mapped_range != mapped_file_ranges.end();) {
|
|
if (mapped_range->region_begin ==
|
|
reinterpret_cast<uintptr_t>(base_address) &&
|
|
mapped_range->region_end ==
|
|
reinterpret_cast<uintptr_t>(base_address) + length) {
|
|
mapped_file_ranges.erase(mapped_range);
|
|
return munmap(base_address, length) == 0;
|
|
}
|
|
++mapped_range;
|
|
}
|
|
// TODO: Implement partial file unmapping.
|
|
assert_always("Error: Partial unmapping of files not yet supported.");
|
|
return munmap(base_address, length) == 0;
|
|
}
|
|
|
|
} // namespace memory
|
|
} // namespace xe
|