mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-09 11:41:35 -10:00
memmap: a reservation that shared memory is mapped into at several places
The other half of what a fastmem window needs. memshm gives a region that can be mapped more than once; this gives the address space those mappings live in: one contiguous reservation that stays reserved while pieces of a guest RAM are mapped into it at the offsets a recompiler computes and unmapped again as the guest remaps. Neither mmap nor VirtualAlloc does that directly, and every core with a window has carried its own. POSIX reserves PROT_NONE anonymous memory, maps MAP_FIXED over it and puts PROT_NONE back -- calls with no floor on Linux or Apple, with MAP_ANON aliased for the latter, which never took the MAP_ANONYMOUS spelling. All three functions require the same two macros, so a platform that has only one of them reaches the stub in every one of them rather than compiling a body that names a macro it lacks. Windows has two paths. From 10 1803 the reservation is a placeholder that mapping splits and unmapping restores and coalesces, through VirtualAlloc2, MapViewOfFile3 and UnmapViewOfFile2. Below that -- back to Windows 2000, on functions that have been kernel32 imports since NT -- the span is reserved one allocation-granularity slot at a time, because MEM_RELEASE frees a whole reservation and never part of one; mapping releases exactly the slots it covers and maps a view over them, and unmapping restores those reservations immediately, since an unreserved hole is an address something else can take. The brief window between release and map is retried, and a second failure puts the reservations back and fails cleanly rather than leaving the span holed. Runtime is identical on all of them: a mapped view is a mapped view and a load through the window is the same instruction. The legacy path costs one extra system call per map, on a path that runs at setup and on a guest remap. Which Windows path is decided by resolving all three placeholder entry points, not one: Windows 10 1709 exports UnmapViewOfFile2 and neither of the others, so resolving one and assuming the rest calls a null pointer on a shipping system. Checked against the binaries -- Windows 7, 8, 8.1, 1511 and 1607 export none of the three, 1709 exports that one, and every build from 1803 to 2004 exports all three with byte-identical argument validation. Those functions are thin shims that validate flags and forward to Nt*; the placeholder split and coalesce are the kernel's, so a disagreement between the tracked ranges and the real mappings comes back as a failed VirtualFree, and the coalesce calls check it. On 64-bit Windows the base is placed above 4 GB by sweeping a ladder of floors: a window based below that crosses into the system's own reservations or aliases mapped pages, and a fault there cannot be told from a real one. A single constrained request fails outright on a fragmented address space where a ladder still finds room. None of it applies to a 32-bit process and it is compiled out there. memshm_create gains a Darwin case with it: a shm name there is capped at 31 characters where Linux takes one ten times longer, so a name that will not fit is shortened from the front, keeping the tail that distinguishes one process from another. The same caller now works on both instead of failing at runtime on one. The test maps one region at three slots, writes through the first and last and reads through the others, unmaps the middle with live mappings either side, maps the freed slot again, and creates a region under a name too long for Darwin -- the failure modes that corrupt silently or fail on one platform only. MEMSHM_AREA_FORCE_LEGACY selects the pre-1803 path on a system that has the placeholder APIs, which is how that path is testable without a Windows 7 machine, and the sample builds a second binary that way. C89 -pedantic clean on Linux, an Apple shape, and both Win32 targets in both configurations; ASan and UBSan clean.
This commit is contained in:
1 parent
0e8f24baff
commit
3ac0a0b6c2
6 files changed
+864
No files matched your search
@@ -174,6 +174,7 @@ jobs:
|
||||
fastjmp_test
|
||||
rdds_block_test
|
||||
faulthandler_test
|
||||
memshm_area_test
|
||||
msg_fifo_queue_test
|
||||
audio_mixer_loop_test
|
||||
audio_mixer_rate_guard_test
|
||||
|
||||
@@ -258,6 +258,13 @@ threads-samples:
|
||||
$(MAKE) -C samples/memmap/memshm_test clean
|
||||
$(MAKE) -C samples/memmap/memshm_test check SANITIZER=$(CONCURRENCY_SAN)
|
||||
$(MAKE) -C samples/memmap/memshm_test clean
|
||||
# and the reservation those mappings go into: three mappings of one
|
||||
# region aliasing, the middle one unmapped with live neighbours
|
||||
# either side, and the freed slot mapped again -- the placeholder
|
||||
# split and coalesce, which is where Windows is easy to get wrong
|
||||
$(MAKE) -C samples/memmap/memshm_area_test clean
|
||||
$(MAKE) -C samples/memmap/memshm_area_test check SANITIZER=$(CONCURRENCY_SAN)
|
||||
$(MAKE) -C samples/memmap/memshm_area_test clean
|
||||
# fastjmp: a jump from three frames down, eight values held across
|
||||
# the set intact at -O2, a hundred thousand round trips; the ASan
|
||||
# fake stack cannot follow a longjmp that is not libc's
|
||||
|
||||
@@ -206,6 +206,11 @@ void memrelease(void *addr, size_t len);
|
||||
* the process. Windows uses a pagefile-backed file mapping, Android a
|
||||
* memfd (Bionic has no shm_open), everything else shm_open.
|
||||
*
|
||||
* Darwin caps a name at 31 characters and returns ENAMETOOLONG past it,
|
||||
* where Linux takes one ten times longer; a name that will not fit is
|
||||
* shortened from the front there, keeping the tail that distinguishes
|
||||
* one process from another, so the same caller works on both.
|
||||
*
|
||||
* Returns: a handle for memshm_map / memshm_destroy, or NULL. It is the
|
||||
* platform's own object -- the file descriptor on POSIX, the HANDLE on
|
||||
* Windows -- so a caller that must map the region a way memshm_map does
|
||||
@@ -243,6 +248,89 @@ void *memshm_map(void *handle, size_t offset, void *hint, size_t len, int prot);
|
||||
*/
|
||||
void memshm_unmap(void *addr, size_t len);
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* A reservation that shared memory is mapped into at several places */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* A fastmem window: one contiguous reservation of address space that
|
||||
* pieces of a guest RAM are mapped into, at offsets the recompiler
|
||||
* computes, and unmapped from again as the guest remaps. The
|
||||
* reservation must stay reserved while individual pieces come and go,
|
||||
* which neither mmap nor VirtualAlloc gives directly:
|
||||
*
|
||||
* - POSIX: the reservation is PROT_NONE anonymous memory, a piece is
|
||||
* MAP_FIXED over it, and unmapping restores PROT_NONE anonymous
|
||||
* memory in its place. The kernel splits and merges the VMAs.
|
||||
* - Windows 10 1803 and later: the reservation is a placeholder, a
|
||||
* piece replaces part of it with MEM_REPLACE_PLACEHOLDER, and
|
||||
* unmapping restores the placeholder and coalesces it with its
|
||||
* neighbours. The area tracks the ranges itself, because the API
|
||||
* cannot be asked where they are.
|
||||
* - Windows 2000 through 10 1709: no placeholder APIs, and
|
||||
* MEM_RELEASE frees a whole reservation rather than part of one, so
|
||||
* the span is reserved one allocation-granularity slot at a time.
|
||||
* Mapping releases exactly the slots it covers and maps a view over
|
||||
* them; unmapping restores those reservations at once.
|
||||
*
|
||||
* Runtime is the same either way: a mapped view is a mapped view, and a
|
||||
* load through the window is the same instruction. The legacy path
|
||||
* costs one extra system call per map and carries a brief window where
|
||||
* the address is free, which it retries through -- both on a path that
|
||||
* runs at setup and when the guest remaps, not per access.
|
||||
*/
|
||||
typedef struct memshm_area memshm_area_t;
|
||||
|
||||
/**
|
||||
* memshm_area_create:
|
||||
* @len : size of the reservation in bytes.
|
||||
*
|
||||
* Returns: the area, or NULL where there is no address space for it, or
|
||||
* no way to reserve one. A caller that gets NULL runs without a fastmem
|
||||
* window rather than failing. @len must be a whole number of allocation
|
||||
* granularity units on Windows.
|
||||
*
|
||||
* On Windows the base is placed above 4 GB where possible: a 4 GB
|
||||
* window based below that either crosses the 32-bit boundary into the
|
||||
* system's own reservations or aliases mapped pages, and a fault there
|
||||
* cannot be told apart from a legitimate miss.
|
||||
*/
|
||||
memshm_area_t *memshm_area_create(size_t len);
|
||||
|
||||
/**
|
||||
* memshm_area_free:
|
||||
* Releases the reservation and everything still mapped in it.
|
||||
*/
|
||||
void memshm_area_free(memshm_area_t *area);
|
||||
|
||||
/**
|
||||
* memshm_area_base / memshm_area_size:
|
||||
*/
|
||||
unsigned char *memshm_area_base(const memshm_area_t *area);
|
||||
size_t memshm_area_size(const memshm_area_t *area);
|
||||
|
||||
/**
|
||||
* memshm_area_map:
|
||||
* @area : from memshm_area_create.
|
||||
* @handle : from memshm_create.
|
||||
* @offset : byte offset into the shared region; page-aligned.
|
||||
* @at : where in the area to map it, within the reservation.
|
||||
* @len : bytes.
|
||||
* @prot : PROT_READ | PROT_WRITE | PROT_EXEC.
|
||||
*
|
||||
* Returns: @at on success, NULL on failure.
|
||||
*/
|
||||
unsigned char *memshm_area_map(memshm_area_t *area, void *handle,
|
||||
size_t offset, void *at, size_t len, int prot);
|
||||
|
||||
/**
|
||||
* memshm_area_unmap:
|
||||
* @at : an address a previous memshm_area_map returned.
|
||||
* @len : the length it was mapped with.
|
||||
*
|
||||
* Restores the reservation over that range. Returns true on success.
|
||||
*/
|
||||
bool memshm_area_unmap(memshm_area_t *area, void *at, size_t len);
|
||||
|
||||
/**
|
||||
* memjit_write_begin / memjit_write_end:
|
||||
*
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h> /* strlen, memcpy, memset */
|
||||
#include <memmap.h>
|
||||
|
||||
#ifndef PROT_READ
|
||||
@@ -508,12 +509,39 @@ void *memshm_create(const char *name, size_t len)
|
||||
if (fd < 0)
|
||||
return NULL;
|
||||
#else
|
||||
/* Darwin caps a shm name at PSHMNAMLEN, 31 characters including the
|
||||
* leading slash, and returns ENAMETOOLONG past it where Linux takes
|
||||
* a name ten times longer. Rather than let the same caller work on
|
||||
* one and fail on the other, a name that will not fit is shortened
|
||||
* from the front: the tail carries the part that varies between
|
||||
* processes, which is what has to survive. */
|
||||
#if defined(__APPLE__)
|
||||
{
|
||||
size_t nlen = strlen(name);
|
||||
if (nlen > 30)
|
||||
{
|
||||
char short_name[32];
|
||||
short_name[0] = '/';
|
||||
memcpy(short_name + 1, name + (nlen - 30), 30);
|
||||
short_name[31] = '\0';
|
||||
fd = shm_open(short_name, O_CREAT | O_EXCL | O_RDWR, 0600);
|
||||
if (fd >= 0)
|
||||
shm_unlink(short_name);
|
||||
goto have_fd;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
fd = shm_open(name, O_CREAT | O_EXCL | O_RDWR, 0600);
|
||||
if (fd < 0)
|
||||
return NULL;
|
||||
/* Unlink at once: the fd keeps the object alive, the name does not
|
||||
* outlive the process. */
|
||||
shm_unlink(name);
|
||||
#if defined(__APPLE__)
|
||||
have_fd:
|
||||
if (fd < 0)
|
||||
return NULL;
|
||||
#endif
|
||||
#endif
|
||||
/* off_t is 32 bits where _FILE_OFFSET_BITS is not set, and a cast
|
||||
* would wrap a 2 GB length to a negative one. Refuse rather than
|
||||
@@ -618,3 +646,561 @@ void memjit_write_end(void)
|
||||
void memjit_write_begin(void) { }
|
||||
void memjit_write_end(void) { }
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* A reservation that shared memory is mapped into at several places */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#if defined(_WIN32) && !defined(_XBOX)
|
||||
|
||||
/* The placeholder APIs are Windows 10 1803. Resolved once at first use;
|
||||
* a system without them gets no area, and its caller runs without a
|
||||
* fastmem window. */
|
||||
typedef PVOID (WINAPI *memshm_VirtualAlloc2_t)(HANDLE, PVOID, SIZE_T, ULONG, ULONG, MEM_EXTENDED_PARAMETER*, ULONG);
|
||||
typedef PVOID (WINAPI *memshm_MapViewOfFile3_t)(HANDLE, HANDLE, PVOID, ULONG64, SIZE_T, ULONG, ULONG, MEM_EXTENDED_PARAMETER*, ULONG);
|
||||
typedef BOOL (WINAPI *memshm_UnmapViewOfFile2_t)(HANDLE, PVOID, ULONG);
|
||||
|
||||
static memshm_VirtualAlloc2_t s_VirtualAlloc2;
|
||||
static memshm_MapViewOfFile3_t s_MapViewOfFile3;
|
||||
static memshm_UnmapViewOfFile2_t s_UnmapViewOfFile2;
|
||||
static int s_placeholder_state; /* 0 unknown, 1 yes, -1 no */
|
||||
|
||||
static bool memshm_placeholder_apis(void)
|
||||
{
|
||||
HMODULE k;
|
||||
if (s_placeholder_state)
|
||||
return s_placeholder_state > 0;
|
||||
/* A build or a test can force the legacy path on a system that has
|
||||
* the placeholder APIs, which is the only way to exercise it without
|
||||
* a pre-1803 machine. */
|
||||
#if defined(MEMSHM_AREA_FORCE_LEGACY)
|
||||
s_placeholder_state = -1;
|
||||
return false;
|
||||
#endif
|
||||
k = GetModuleHandleW(L"kernelbase.dll");
|
||||
if (k)
|
||||
{
|
||||
/* Through a union: C89 forbids casting between object and
|
||||
* function pointers, and this file builds under that lane. */
|
||||
union { FARPROC p; memshm_VirtualAlloc2_t va2;
|
||||
memshm_MapViewOfFile3_t mv3; memshm_UnmapViewOfFile2_t uv2; } u;
|
||||
u.p = GetProcAddress(k, "VirtualAlloc2"); s_VirtualAlloc2 = u.va2;
|
||||
u.p = GetProcAddress(k, "MapViewOfFile3"); s_MapViewOfFile3 = u.mv3;
|
||||
u.p = GetProcAddress(k, "UnmapViewOfFile2"); s_UnmapViewOfFile2 = u.uv2;
|
||||
}
|
||||
/* All three, not any one: Windows 10 1709 exports UnmapViewOfFile2
|
||||
* and neither of the others, so resolving one and assuming the rest
|
||||
* calls a NULL pointer on a shipping system. Verified against the
|
||||
* 1709 and 1803 binaries. */
|
||||
s_placeholder_state = (s_VirtualAlloc2 && s_MapViewOfFile3 && s_UnmapViewOfFile2) ? 1 : -1;
|
||||
return s_placeholder_state > 0;
|
||||
}
|
||||
|
||||
/* The placeholder ranges, as [start, end) offsets into the area, kept
|
||||
* sorted. The API cannot be asked where its placeholders are, so the
|
||||
* area tracks them: Map splits one, Unmap restores and coalesces. A
|
||||
* flat array is right for the few tens of ranges a fastmem window
|
||||
* splits into, and keeps the whole thing allocation-light. */
|
||||
#define MEMSHM_MAX_PLACEHOLDERS 256
|
||||
|
||||
typedef struct
|
||||
{
|
||||
size_t start;
|
||||
size_t end;
|
||||
} memshm_range_t;
|
||||
#endif
|
||||
|
||||
struct memshm_area
|
||||
{
|
||||
unsigned char *base;
|
||||
size_t len;
|
||||
size_t mappings;
|
||||
#if defined(_WIN32) && !defined(_XBOX)
|
||||
memshm_range_t ranges[MEMSHM_MAX_PLACEHOLDERS];
|
||||
int range_count;
|
||||
/* Legacy path: the span is reserved one allocation-granularity slot
|
||||
* at a time, because MEM_RELEASE frees a whole reservation and never
|
||||
* part of one. slot is that granularity; reserved[] says which slots
|
||||
* are still reservations rather than mapped views. */
|
||||
int legacy;
|
||||
size_t slot;
|
||||
unsigned char *reserved;
|
||||
size_t slot_count;
|
||||
#endif
|
||||
};
|
||||
|
||||
#if defined(_WIN32) && !defined(_XBOX)
|
||||
/* Index of the placeholder containing this offset, or -1. */
|
||||
static int memshm_find_range(const memshm_area_t *a, size_t off)
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < a->range_count; i++)
|
||||
if (off >= a->ranges[i].start && off < a->ranges[i].end)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void memshm_erase_range(memshm_area_t *a, int i)
|
||||
{
|
||||
int j;
|
||||
for (j = i; j < a->range_count - 1; j++)
|
||||
a->ranges[j] = a->ranges[j + 1];
|
||||
a->range_count--;
|
||||
}
|
||||
|
||||
static bool memshm_insert_range(memshm_area_t *a, size_t start, size_t end)
|
||||
{
|
||||
int i, j;
|
||||
if (a->range_count >= MEMSHM_MAX_PLACEHOLDERS)
|
||||
return false;
|
||||
for (i = 0; i < a->range_count; i++)
|
||||
if (a->ranges[i].start > start)
|
||||
break;
|
||||
for (j = a->range_count; j > i; j--)
|
||||
a->ranges[j] = a->ranges[j - 1];
|
||||
a->ranges[i].start = start;
|
||||
a->ranges[i].end = end;
|
||||
a->range_count++;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32) && !defined(_XBOX)
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* The legacy path: Windows 2000 through 10 1709 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Without the placeholder APIs a range cannot be carved out of a
|
||||
* reservation: MEM_RELEASE takes the base of a whole VirtualAlloc and
|
||||
* frees all of it, and MapViewOfFileEx refuses an address that is
|
||||
* already reserved. So the span is reserved one allocation-granularity
|
||||
* slot at a time -- each slot its own VirtualAlloc, each individually
|
||||
* releasable -- and mapping a range releases exactly the slots it
|
||||
* covers and maps a view over them.
|
||||
*
|
||||
* Between that release and the map, the address is briefly free, and
|
||||
* another thread in the process could allocate there. The window is a
|
||||
* few microseconds at a specific address a general allocator has no
|
||||
* reason to pick, but it is real: the map is retried, and on a second
|
||||
* failure the slots are re-reserved and the call fails cleanly rather
|
||||
* than leaving a hole in the span.
|
||||
*
|
||||
* Runtime is unaffected either way. Once a view is mapped these are
|
||||
* ordinary page-table entries and a fastmem load is the same
|
||||
* instruction; the difference is one extra syscall per map, on a path
|
||||
* that runs at setup and when the guest remaps. */
|
||||
|
||||
static size_t memshm_granularity(void)
|
||||
{
|
||||
static size_t g;
|
||||
if (!g)
|
||||
{
|
||||
SYSTEM_INFO si;
|
||||
GetSystemInfo(&si);
|
||||
g = (size_t)si.dwAllocationGranularity;
|
||||
if (!g)
|
||||
g = 65536;
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
/* Reserve every slot in [first, first + count). Rolls back on failure so
|
||||
* the span is never left partly reserved. */
|
||||
static bool memshm_legacy_reserve(memshm_area_t *a, size_t first, size_t count)
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; i < count; i++)
|
||||
{
|
||||
if (a->reserved[first + i])
|
||||
continue;
|
||||
if (!VirtualAlloc(a->base + (first + i) * a->slot, a->slot,
|
||||
MEM_RESERVE, PAGE_NOACCESS))
|
||||
{
|
||||
while (i--)
|
||||
{
|
||||
VirtualFree(a->base + (first + i) * a->slot, 0, MEM_RELEASE);
|
||||
a->reserved[first + i] = 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
a->reserved[first + i] = 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void memshm_legacy_release(memshm_area_t *a, size_t first, size_t count)
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; i < count; i++)
|
||||
{
|
||||
if (!a->reserved[first + i])
|
||||
continue;
|
||||
VirtualFree(a->base + (first + i) * a->slot, 0, MEM_RELEASE);
|
||||
a->reserved[first + i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
memshm_area_t *memshm_area_create(size_t len)
|
||||
{
|
||||
memshm_area_t *a;
|
||||
#if defined(_WIN32) && !defined(_XBOX)
|
||||
void *alloc = NULL;
|
||||
uintptr_t floor;
|
||||
|
||||
if (!memshm_placeholder_apis())
|
||||
{
|
||||
/* Legacy: find a free span, then reserve it slot by slot. The
|
||||
* probe reserves the whole span to learn an address the system
|
||||
* considers free, releases it, and immediately re-reserves the
|
||||
* slots; anything that steals part of it in between makes the
|
||||
* re-reservation fail and the call reports none. */
|
||||
size_t gran = memshm_granularity();
|
||||
size_t slots, i;
|
||||
void *probe;
|
||||
if (len % gran)
|
||||
return NULL; /* a span must be whole slots */
|
||||
slots = len / gran;
|
||||
probe = VirtualAlloc(NULL, len, MEM_RESERVE, PAGE_NOACCESS);
|
||||
if (!probe)
|
||||
return NULL;
|
||||
VirtualFree(probe, 0, MEM_RELEASE);
|
||||
a = (memshm_area_t*)calloc(1, sizeof(*a));
|
||||
if (!a)
|
||||
return NULL;
|
||||
a->reserved = (unsigned char*)calloc(slots, 1);
|
||||
if (!a->reserved)
|
||||
{
|
||||
free(a);
|
||||
return NULL;
|
||||
}
|
||||
a->base = (unsigned char*)probe;
|
||||
a->len = len;
|
||||
a->slot = gran;
|
||||
a->slot_count = slots;
|
||||
a->legacy = 1;
|
||||
for (i = 0; i < slots; i++)
|
||||
{
|
||||
if (!VirtualAlloc(a->base + i * gran, gran, MEM_RESERVE, PAGE_NOACCESS))
|
||||
{
|
||||
memshm_legacy_release(a, 0, i);
|
||||
free(a->reserved);
|
||||
free(a);
|
||||
return NULL;
|
||||
}
|
||||
a->reserved[i] = 1;
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
/* Ask for a base above 4 GB. Below that, a 4 GB window either
|
||||
* crosses the 32-bit boundary into the system's own reservations
|
||||
* around KUSER_SHARED_DATA, or on a fragmented address space starts
|
||||
* low and aliases mapped DLL and heap pages -- and then a fault in
|
||||
* the window cannot be told from a real one, because the window's
|
||||
* end is its start plus 0xFFFFFFFF and both are in range.
|
||||
*
|
||||
* A single constrained request can fail outright on a fragmented
|
||||
* address space even with room higher up, and falling straight back
|
||||
* to an unconstrained one tends to land low and be rejected below.
|
||||
* Sweeping a ladder of floors recovers a usable placement that one
|
||||
* attempt gives up on. MEM_ADDRESS_REQUIREMENTS arrived in the same
|
||||
* Windows release as the placeholder flags, so it narrows nothing. */
|
||||
/* Only on a 64-bit address space: on 32-bit Windows there is no
|
||||
* "above 4 GB", the floor constants do not fit a pointer, and the
|
||||
* window a 32-bit process reserves cannot overlap the way the
|
||||
* comment above describes. */
|
||||
#if defined(_WIN64)
|
||||
for (floor = 0x100000000ULL; floor <= 0x900000000ULL; floor += 0x100000000ULL)
|
||||
{
|
||||
MEM_ADDRESS_REQUIREMENTS req;
|
||||
MEM_EXTENDED_PARAMETER param;
|
||||
memset(&req, 0, sizeof(req));
|
||||
memset(¶m, 0, sizeof(param));
|
||||
req.LowestStartingAddress = (PVOID)floor;
|
||||
param.Type = MemExtendedParameterAddressRequirements;
|
||||
param.Pointer = &req;
|
||||
alloc = s_VirtualAlloc2(GetCurrentProcess(), NULL, len,
|
||||
MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, ¶m, 1);
|
||||
if (alloc)
|
||||
break;
|
||||
}
|
||||
#else
|
||||
(void)floor;
|
||||
#endif
|
||||
if (!alloc)
|
||||
alloc = s_VirtualAlloc2(GetCurrentProcess(), NULL, len,
|
||||
MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, NULL, 0);
|
||||
if (!alloc)
|
||||
return NULL;
|
||||
#if defined(_WIN64)
|
||||
if ((uintptr_t)alloc < 0x100000000ULL)
|
||||
{
|
||||
/* Unusable placement, as above: release it and report none. */
|
||||
VirtualFree(alloc, 0, MEM_RELEASE);
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
a = (memshm_area_t*)calloc(1, sizeof(*a));
|
||||
if (!a)
|
||||
{
|
||||
VirtualFree(alloc, 0, MEM_RELEASE);
|
||||
return NULL;
|
||||
}
|
||||
a->base = (unsigned char*)alloc;
|
||||
a->len = len;
|
||||
a->ranges[0].start = 0;
|
||||
a->ranges[0].end = len;
|
||||
a->range_count = 1;
|
||||
return a;
|
||||
#elif defined(HAVE_MMAN) && !defined(__EMSCRIPTEN__) && defined(MAP_ANONYMOUS)
|
||||
void *alloc = mmap(NULL, len, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (alloc == MAP_FAILED)
|
||||
return NULL;
|
||||
a = (memshm_area_t*)calloc(1, sizeof(*a));
|
||||
if (!a)
|
||||
{
|
||||
munmap(alloc, len);
|
||||
return NULL;
|
||||
}
|
||||
a->base = (unsigned char*)alloc;
|
||||
a->len = len;
|
||||
return a;
|
||||
#else
|
||||
(void)len;
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
void memshm_area_free(memshm_area_t *area)
|
||||
{
|
||||
if (!area)
|
||||
return;
|
||||
#if defined(_WIN32) && !defined(_XBOX)
|
||||
if (area->legacy)
|
||||
{
|
||||
/* Each slot is its own reservation or a mapped view; both have to
|
||||
* go one at a time. */
|
||||
size_t i;
|
||||
for (i = 0; i < area->slot_count; i++)
|
||||
{
|
||||
if (area->reserved[i])
|
||||
VirtualFree(area->base + i * area->slot, 0, MEM_RELEASE);
|
||||
else
|
||||
UnmapViewOfFile(area->base + i * area->slot);
|
||||
}
|
||||
free(area->reserved);
|
||||
free(area);
|
||||
return;
|
||||
}
|
||||
/* Every placeholder and every mapping goes with the reservation. */
|
||||
VirtualFree(area->base, 0, MEM_RELEASE);
|
||||
#elif defined(HAVE_MMAN) && !defined(__EMSCRIPTEN__)
|
||||
munmap(area->base, area->len);
|
||||
#endif
|
||||
free(area);
|
||||
}
|
||||
|
||||
unsigned char *memshm_area_base(const memshm_area_t *area)
|
||||
{
|
||||
return area ? area->base : NULL;
|
||||
}
|
||||
|
||||
size_t memshm_area_size(const memshm_area_t *area)
|
||||
{
|
||||
return area ? area->len : 0;
|
||||
}
|
||||
|
||||
unsigned char *memshm_area_map(memshm_area_t *area, void *handle,
|
||||
size_t offset, void *at, size_t len, int prot)
|
||||
{
|
||||
#if defined(_WIN32) && !defined(_XBOX)
|
||||
size_t map_off;
|
||||
size_t old_end;
|
||||
int idx;
|
||||
DWORD page_prot;
|
||||
|
||||
if (!area || !at || !len)
|
||||
return NULL;
|
||||
map_off = (unsigned char*)at - area->base;
|
||||
|
||||
if (area->legacy)
|
||||
{
|
||||
size_t first = map_off / area->slot;
|
||||
size_t count = (len + area->slot - 1) / area->slot;
|
||||
void *view;
|
||||
DWORD access;
|
||||
int attempt;
|
||||
if ((map_off % area->slot) || (first + count) > area->slot_count)
|
||||
return NULL; /* must start and end on a slot */
|
||||
access = FILE_MAP_READ;
|
||||
if (prot & PROT_WRITE)
|
||||
access |= FILE_MAP_WRITE;
|
||||
if (prot & PROT_EXEC)
|
||||
access |= FILE_MAP_EXECUTE;
|
||||
/* Release the slots, then map over them. If something takes the
|
||||
* address in between, retry once; if that fails too, put the
|
||||
* reservations back so the span stays whole. */
|
||||
for (attempt = 0; attempt < 2; attempt++)
|
||||
{
|
||||
memshm_legacy_release(area, first, count);
|
||||
view = MapViewOfFileEx((HANDLE)handle, access,
|
||||
(DWORD)(((uint64_t)offset) >> 32),
|
||||
(DWORD)(offset & 0xFFFFFFFFu), len, at);
|
||||
if (view == at)
|
||||
{
|
||||
area->mappings++;
|
||||
return (unsigned char*)view;
|
||||
}
|
||||
if (view)
|
||||
UnmapViewOfFile(view); /* landed elsewhere: not usable */
|
||||
if (!memshm_legacy_reserve(area, first, count))
|
||||
return NULL;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
idx = memshm_find_range(area, map_off);
|
||||
if (idx < 0)
|
||||
return NULL; /* not a placeholder: something is still mapped there */
|
||||
old_end = area->ranges[idx].end;
|
||||
|
||||
/* Split off anything to the left of this range, then anything to
|
||||
* the right; what is left is exactly the range being replaced. */
|
||||
if (map_off != area->ranges[idx].start)
|
||||
{
|
||||
area->ranges[idx].end = map_off;
|
||||
if (!VirtualFree(area->base + area->ranges[idx].start,
|
||||
map_off - area->ranges[idx].start,
|
||||
MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER))
|
||||
return NULL;
|
||||
}
|
||||
else
|
||||
memshm_erase_range(area, idx);
|
||||
|
||||
if ((map_off + len) != old_end)
|
||||
{
|
||||
if (!memshm_insert_range(area, map_off + len, old_end))
|
||||
return NULL;
|
||||
if (!VirtualFree(area->base + map_off, len,
|
||||
MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER))
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!s_MapViewOfFile3((HANDLE)handle, GetCurrentProcess(), at,
|
||||
(ULONG64)offset, len, MEM_REPLACE_PLACEHOLDER, PAGE_READWRITE, NULL, 0))
|
||||
return NULL;
|
||||
|
||||
if (prot & PROT_READ)
|
||||
page_prot = (prot & PROT_EXEC)
|
||||
? ((prot & PROT_WRITE) ? PAGE_EXECUTE_READWRITE : PAGE_EXECUTE_READ)
|
||||
: ((prot & PROT_WRITE) ? PAGE_READWRITE : PAGE_READONLY);
|
||||
else
|
||||
page_prot = PAGE_NOACCESS;
|
||||
if (page_prot != PAGE_READWRITE)
|
||||
{
|
||||
DWORD old_prot;
|
||||
VirtualProtect(at, len, page_prot, &old_prot);
|
||||
}
|
||||
area->mappings++;
|
||||
return (unsigned char*)at;
|
||||
#elif defined(HAVE_MMAN) && !defined(__EMSCRIPTEN__) && defined(MAP_FIXED) && defined(MAP_ANONYMOUS)
|
||||
void *p;
|
||||
if (!area || !at || !len)
|
||||
return NULL;
|
||||
p = mmap(at, len, prot, MAP_SHARED | MAP_FIXED,
|
||||
MEMSHM_FD(handle), (off_t)offset);
|
||||
if (p == MAP_FAILED)
|
||||
return NULL;
|
||||
area->mappings++;
|
||||
return (unsigned char*)p;
|
||||
#else
|
||||
(void)area; (void)handle; (void)offset; (void)at; (void)len; (void)prot;
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool memshm_area_unmap(memshm_area_t *area, void *at, size_t len)
|
||||
{
|
||||
#if defined(_WIN32) && !defined(_XBOX)
|
||||
size_t map_off;
|
||||
int left, right;
|
||||
|
||||
if (!area || !at || !len)
|
||||
return false;
|
||||
map_off = (unsigned char*)at - area->base;
|
||||
|
||||
if (area->legacy)
|
||||
{
|
||||
size_t first = map_off / area->slot;
|
||||
size_t count = (len + area->slot - 1) / area->slot;
|
||||
if ((map_off % area->slot) || (first + count) > area->slot_count)
|
||||
return false;
|
||||
if (!UnmapViewOfFile(at))
|
||||
return false;
|
||||
/* Put the reservations back at once: an unreserved hole in the
|
||||
* span is an address another allocation can take, and then the
|
||||
* next map there fails for good. */
|
||||
if (!memshm_legacy_reserve(area, first, count))
|
||||
return false;
|
||||
area->mappings--;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!s_UnmapViewOfFile2(GetCurrentProcess(), at, MEM_PRESERVE_PLACEHOLDER))
|
||||
return false;
|
||||
|
||||
/* Coalesce with the placeholder on the left, if the range that ends
|
||||
* where this one starts is one. */
|
||||
left = (map_off > 0) ? memshm_find_range(area, map_off - 1) : -1;
|
||||
if (left >= 0)
|
||||
{
|
||||
area->ranges[left].end = map_off + len;
|
||||
/* Checked: KernelBase validates the flags and forwards, so the
|
||||
* kernel is what decides whether this really is two adjacent
|
||||
* placeholders. A disagreement between these ranges and the VADs
|
||||
* comes back here, and silently ignoring it would let the two
|
||||
* drift apart until a later map fails for no visible reason. */
|
||||
if (!VirtualFree(area->base + area->ranges[left].start,
|
||||
area->ranges[left].end - area->ranges[left].start,
|
||||
MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS))
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!memshm_insert_range(area, map_off, map_off + len))
|
||||
return false;
|
||||
left = memshm_find_range(area, map_off);
|
||||
}
|
||||
|
||||
/* And with the one on the right. */
|
||||
right = ((map_off + len) < area->len) ? memshm_find_range(area, map_off + len) : -1;
|
||||
if (right >= 0 && right != left)
|
||||
{
|
||||
area->ranges[left].end = area->ranges[right].end;
|
||||
memshm_erase_range(area, right);
|
||||
if (!VirtualFree(area->base + area->ranges[left].start,
|
||||
area->ranges[left].end - area->ranges[left].start,
|
||||
MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS))
|
||||
return false;
|
||||
}
|
||||
area->mappings--;
|
||||
return true;
|
||||
/* MAP_ANONYMOUS as well as MAP_FIXED: the body below needs both, and
|
||||
* memshm_area_create's guard requires both, so a platform with only one
|
||||
* of them must reach the stub in every one of these functions rather
|
||||
* than compile a body that names a macro it does not have. */
|
||||
#elif defined(HAVE_MMAN) && !defined(__EMSCRIPTEN__) && defined(MAP_FIXED) && defined(MAP_ANONYMOUS)
|
||||
if (!area || !at || !len)
|
||||
return false;
|
||||
/* Anonymous PROT_NONE back over the range: the reservation is whole
|
||||
* again and the kernel merges the VMAs. */
|
||||
if (mmap(at, len, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == MAP_FAILED)
|
||||
return false;
|
||||
area->mappings--;
|
||||
return true;
|
||||
#else
|
||||
(void)area; (void)at; (void)len;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
TARGET := memshm_area_test
|
||||
LIBRETRO_COMM_DIR := ../../..
|
||||
SOURCES := memshm_area_test.c $(LIBRETRO_COMM_DIR)/memmap/memmap.c
|
||||
CFLAGS += -Wall -W -std=gnu99 -g -O1 -I$(LIBRETRO_COMM_DIR)/include
|
||||
LDFLAGS += -lrt
|
||||
OBJS := $(SOURCES:.c=.o)
|
||||
ifneq ($(SAN),)
|
||||
SANITIZER := $(SAN)
|
||||
endif
|
||||
ifneq ($(SANITIZER),)
|
||||
CFLAGS := -fsanitize=$(SANITIZER) -fno-omit-frame-pointer $(CFLAGS)
|
||||
LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS)
|
||||
endif
|
||||
all: $(TARGET)
|
||||
%.o: %.c
|
||||
$(CC) -c -o $@ $< $(CFLAGS)
|
||||
$(TARGET): $(OBJS)
|
||||
$(CC) -o $@ $^ $(LDFLAGS)
|
||||
# On Windows the legacy slot-reservation path is what every system
|
||||
# before 10 1803 uses; this builds it on any system so it can be run.
|
||||
legacy: $(SOURCES)
|
||||
$(CC) -o $(TARGET)_legacy $(SOURCES) $(CFLAGS) -DMEMSHM_AREA_FORCE_LEGACY $(LDFLAGS)
|
||||
|
||||
check: all
|
||||
./$(TARGET)
|
||||
clean:
|
||||
rm -f $(OBJS) $(TARGET) $(TARGET)_legacy
|
||||
.PHONY: all check clean legacy
|
||||
@@ -0,0 +1,154 @@
|
||||
/* memshm_area_test.c - one shared region mapped into a reservation at
|
||||
* several places, and unmapped from the middle.
|
||||
*
|
||||
* What the area is for: a guest RAM appearing at more than one offset
|
||||
* in a window the recompiler addresses directly. So the test is the
|
||||
* aliasing through the area -- write at one offset, read at another --
|
||||
* and then the case that is easy to get wrong on Windows, where the
|
||||
* reservation is a placeholder that Map splits and Unmap must coalesce
|
||||
* again: unmap a mapping with a live mapping on each side, and check
|
||||
* the neighbours still alias, and that the hole can be mapped again.
|
||||
*
|
||||
* Every failure mode here is silent corruption rather than a crash: a
|
||||
* placeholder split that loses a range makes the next Map fail, and a
|
||||
* coalesce that runs over its neighbour unmaps memory that is still in
|
||||
* use. The neighbours are read back after every step for that reason.
|
||||
*
|
||||
* On Windows this exercises whichever path the system supports.
|
||||
* Building the library with MEMSHM_AREA_FORCE_LEGACY selects the
|
||||
* pre-1803 slot-reservation path on a machine that has the placeholder
|
||||
* APIs, which is the only way to test it without a Windows 7 box; the
|
||||
* Makefile beside this builds a second binary that way.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <memmap.h>
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const size_t page = 65536; /* a granularity Windows accepts */
|
||||
const size_t slot = page;
|
||||
size_t shm_len = slot;
|
||||
size_t area_len = slot * 8;
|
||||
char name[64];
|
||||
void *h;
|
||||
memshm_area_t *area;
|
||||
unsigned char *base;
|
||||
volatile uint32_t *a, *b, *c;
|
||||
int ok = 1;
|
||||
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
printf("memshm area\n");
|
||||
|
||||
#if defined(_WIN32)
|
||||
sprintf(name, "memshm_area_test_%lu", (unsigned long)GetCurrentProcessId());
|
||||
#else
|
||||
sprintf(name, "/memshm_area_test_%lu", (unsigned long)getpid());
|
||||
#endif
|
||||
|
||||
area = memshm_area_create(area_len);
|
||||
if (!area)
|
||||
{
|
||||
printf(" no area on this platform (Windows before 10 1803, or no mman); nothing to test\n");
|
||||
printf("memshm area: ok (skipped)\n");
|
||||
return 0;
|
||||
}
|
||||
base = memshm_area_base(area);
|
||||
printf(" reserved %u bytes at %p%s\n", (unsigned)area_len, (void*)base,
|
||||
#if defined(MEMSHM_AREA_FORCE_LEGACY)
|
||||
" [legacy path forced]"
|
||||
#else
|
||||
""
|
||||
#endif
|
||||
);
|
||||
|
||||
h = memshm_create(name, shm_len);
|
||||
if (!h)
|
||||
{
|
||||
printf(" FAIL: memshm_create\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Three mappings of the same region, at slots 1, 3 and 5: the middle
|
||||
* one is what gets unmapped, with live neighbours on both sides. */
|
||||
a = (volatile uint32_t*)memshm_area_map(area, h, 0, base + slot * 1, shm_len, PROT_READ | PROT_WRITE);
|
||||
b = (volatile uint32_t*)memshm_area_map(area, h, 0, base + slot * 3, shm_len, PROT_READ | PROT_WRITE);
|
||||
c = (volatile uint32_t*)memshm_area_map(area, h, 0, base + slot * 5, shm_len, PROT_READ | PROT_WRITE);
|
||||
if (!a || !b || !c)
|
||||
{
|
||||
printf(" FAIL: map (%p %p %p)\n", (void*)a, (void*)b, (void*)c);
|
||||
return 1;
|
||||
}
|
||||
printf(" ok: three mappings of one region at slots 1, 3 and 5\n");
|
||||
|
||||
a[0] = 0x11111111u;
|
||||
{
|
||||
int good = (b[0] == 0x11111111u && c[0] == 0x11111111u);
|
||||
printf(" %s: a write at slot 1 is seen at slots 3 and 5\n", good ? "ok" : "FAIL");
|
||||
ok &= good;
|
||||
}
|
||||
c[1] = 0x22222222u;
|
||||
{
|
||||
int good = (a[1] == 0x22222222u && b[1] == 0x22222222u);
|
||||
printf(" %s: and a write at slot 5 is seen at slots 1 and 3\n", good ? "ok" : "FAIL");
|
||||
ok &= good;
|
||||
}
|
||||
|
||||
/* The middle one goes: its placeholder must be restored and coalesced
|
||||
* with the free ranges either side, without touching 1 or 5. */
|
||||
if (!memshm_area_unmap(area, (void*)b, shm_len))
|
||||
{
|
||||
printf(" FAIL: unmap of the middle mapping\n");
|
||||
ok = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
int good = (a[0] == 0x11111111u && c[0] == 0x11111111u && a[1] == 0x22222222u && c[1] == 0x22222222u);
|
||||
printf(" %s: after unmapping the middle, the neighbours still alias\n", good ? "ok" : "FAIL");
|
||||
ok &= good;
|
||||
}
|
||||
|
||||
/* And the hole is usable again -- the case a lost placeholder range
|
||||
* would fail. */
|
||||
{
|
||||
volatile uint32_t *d = (volatile uint32_t*)memshm_area_map(area, h, 0, base + slot * 3, shm_len, PROT_READ | PROT_WRITE);
|
||||
int good = (d != NULL) && (d[0] == 0x11111111u);
|
||||
printf(" %s: the freed slot maps again and aliases\n", good ? "ok" : "FAIL");
|
||||
ok &= good;
|
||||
if (d)
|
||||
memshm_area_unmap(area, (void*)d, shm_len);
|
||||
}
|
||||
|
||||
/* A name longer than Darwin's 31-character shm limit must still
|
||||
* work: Linux takes it as given, Darwin shortens it from the front.
|
||||
* A caller that works on one platform should work on the other. */
|
||||
{
|
||||
char longname[128];
|
||||
void *h2;
|
||||
memset(longname, 'x', sizeof(longname));
|
||||
#if defined(_WIN32)
|
||||
sprintf(longname, "memshm_area_test_a_very_long_name_indeed_%lu", (unsigned long)GetCurrentProcessId());
|
||||
#else
|
||||
sprintf(longname, "/memshm_area_test_a_very_long_name_indeed_%lu", (unsigned long)getpid());
|
||||
#endif
|
||||
h2 = memshm_create(longname, shm_len);
|
||||
printf(" %s: a %u-character region name is accepted\n", h2 ? "ok" : "FAIL", (unsigned)strlen(longname));
|
||||
ok &= (h2 != NULL);
|
||||
if (h2)
|
||||
memshm_destroy(h2);
|
||||
}
|
||||
|
||||
memshm_area_unmap(area, (void*)a, shm_len);
|
||||
memshm_area_unmap(area, (void*)c, shm_len);
|
||||
memshm_destroy(h);
|
||||
memshm_area_free(area);
|
||||
printf(ok ? "memshm area: ok\n" : "memshm area: FAILED\n");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
Reference in new issue
Block a user