savestates on waterbox

This commit is contained in:
nattthebear 2020-06-07 09:26:08 -04:00
parent 95dbd7c20b
commit 2952ac24aa
9 changed files with 510 additions and 127 deletions

BIN
output/linguard.so Normal file

Binary file not shown.

View File

@ -25,7 +25,7 @@ namespace BizHawk.BizInvoke
_dirtydata = (WriteDetectionStatus[])(object)new byte[GetPage(EndExclusive - 1) + 1];
_pal = OSTailoredCode.IsUnixHost
? (IMemoryBlockPal)new MemoryBlockUnixPal(Start, Size)
? (IMemoryBlockPal)new MemoryBlockLinuxPal(Start, Size)
: new MemoryBlockWindowsPal(Start, Size);
}

View File

@ -0,0 +1,205 @@
using System;
using System.Runtime.InteropServices;
using static BizHawk.BizInvoke.MemoryBlock;
using static BizHawk.BizInvoke.POSIXLibC;
namespace BizHawk.BizInvoke
{
internal sealed unsafe class MemoryBlockLinuxPal : IMemoryBlockPal
{
/*
Differences compared with MemoryBlockWindowsPal:
1) Commit is handled by only mapping up to the commit size, and then expanding commit is handled by unmap + truncate + remap.
So all unmanaged structures (including LinGuard) are always looking at the committed size, not total size.
2) Because of sigaltstack, RW_Stack is not needed and is made to behave the same as regular write guarding.
*/
/// <summary>handle returned by <see cref="memfd_create"/></summary>
private int _fd = -1;
private ulong _start;
private ulong _size;
private ulong _committedSize;
private bool _active;
/// <summary>
/// Reserve bytes to later be swapped in, but do not map them
/// </summary>
/// <param name="start">eventual mapped address</param>
/// <param name="size"></param>
/// <exception cref="InvalidOperationException">
/// failed to get file descriptor
/// </exception>
public MemoryBlockLinuxPal(ulong start, ulong size)
{
// Console.WriteLine($".ctor {start:x16} {size:x16}");
_start = start;
_size = size;
_fd = memfd_create("MemoryBlockUnix", 1 /*MFD_CLOEXEC*/);
if (_fd == -1)
throw new InvalidOperationException($"{nameof(memfd_create)}() failed with error {Marshal.GetLastWin32Error()}");
}
public void Dispose()
{
if (_fd == -1)
return;
if (_active)
{
try
{
Deactivate();
}
catch
{}
}
close(_fd);
_fd = -1;
GC.SuppressFinalize(this);
}
~MemoryBlockLinuxPal()
{
Dispose();
}
public void Activate()
{
if (_committedSize > 0)
{
var ptr = mmap(Z.US(_start), Z.UU(_committedSize),
MemoryProtection.Read | MemoryProtection.Write | MemoryProtection.Execute,
17, // MAP_SHARED | MAP_FIXED
_fd, IntPtr.Zero);
if (ptr != Z.US(_start))
{
throw new InvalidOperationException($"{nameof(mmap)}() failed with error {Marshal.GetLastWin32Error()}");
}
// Console.WriteLine($"Add {_start} {_committedSize}");
if ((IntPtr)LinGuard.AddTripGuard(Z.UU(_start), Z.UU(_committedSize)) == IntPtr.Zero)
{
throw new InvalidOperationException($"{nameof(LinGuard.AddTripGuard)}() returned NULL");
}
}
_active = true;
}
public void Deactivate()
{
if (_committedSize > 0)
{
var errorCode = munmap(Z.US(_start), Z.UU(_committedSize));
if (errorCode != 0)
throw new InvalidOperationException($"{nameof(munmap)}() failed with error {Marshal.GetLastWin32Error()}");
// Console.WriteLine($"Remove {_start} {_committedSize}");
if (!LinGuard.RemoveTripGuard(Z.UU(_start), Z.UU(_committedSize)))
throw new InvalidOperationException($"{nameof(LinGuard.RemoveTripGuard)}() returned FALSE");
}
_active = false;
}
public void Commit(ulong length)
{
// Console.WriteLine($"commit {length:x16}");
Deactivate();
var errorCode = ftruncate(_fd, Z.US(length));
if (errorCode != 0)
throw new InvalidOperationException($"{nameof(ftruncate)}() failed with error {Marshal.GetLastWin32Error()}");
_committedSize = length;
Activate();
}
private static MemoryProtection ToMemoryProtection(Protection prot)
{
switch (prot)
{
case Protection.None:
return MemoryProtection.None;
case Protection.R:
return MemoryProtection.Read;
case Protection.RW:
return MemoryProtection.Read | MemoryProtection.Write;
case Protection.RX:
return MemoryProtection.Read | MemoryProtection.Execute;
case Protection.RW_Invisible:
return MemoryProtection.Read | MemoryProtection.Write;
case Protection.RW_Stack:
// because of sigaltstack, LinGuard has no issues with readonly stacks and the special distinction that
// the windows port draws between stack vs non stack memory is ignored here
return MemoryProtection.Read;
default:
throw new ArgumentOutOfRangeException(nameof(prot));
}
}
public void Protect(ulong start, ulong size, Protection prot)
{
// Console.WriteLine($"protect {start:x16} {size:x16} {prot}");
var errorCode = mprotect(
Z.US(start),
Z.UU(size),
ToMemoryProtection(prot)
);
if (errorCode != 0)
throw new InvalidOperationException($"{nameof(mprotect)}() failed with error {Marshal.GetLastWin32Error()}!");
}
public void GetWriteStatus(WriteDetectionStatus[] dest, Protection[] pagedata)
{
if (_committedSize > 0)
{
// Console.WriteLine($"Examine {_start} {_committedSize}");
var p = (IntPtr)LinGuard.ExamineTripGuard(Z.UU(_start), Z.UU(_committedSize));
if (p == IntPtr.Zero)
throw new InvalidOperationException($"{nameof(LinGuard.ExamineTripGuard)}() returned NULL!");
Marshal.Copy(p, (byte[])(object)dest, 0, (int)(_committedSize >> WaterboxUtils.PageShift));
}
}
public void SetWriteStatus(WriteDetectionStatus[] src)
{
if (_committedSize > 0)
{
// Console.WriteLine($"Examine {_start} {_committedSize}");
var p = (IntPtr)LinGuard.ExamineTripGuard(Z.UU(_start), Z.UU(_committedSize));
if (p == IntPtr.Zero)
throw new InvalidOperationException($"{nameof(LinGuard.ExamineTripGuard)}() returned NULL!");
Marshal.Copy((byte[])(object)src, 0, p, (int)(_committedSize >> WaterboxUtils.PageShift));
}
}
private static unsafe class LinGuard
{
/// <summary>
/// Add write detection to an area of memory. Any page in the specified range that has CanChange
/// set and triggers an access violation on write
/// will be noted, set to read+write permissions, and execution will be continued.
/// CALLER'S RESPONSIBILITY: All addresses are page aligned.
/// CALLER'S RESPONSIBILITY: No other thread enters any LinGuard function, or trips any tracked page during this call.
/// CALLER'S RESPONSIBILITY: Pages to be tracked are mprotected to R. Pages with write permission
/// cause no issues, but they will not trip.
/// </summary>
/// <returns>The same information as ExamineTripGuard, or null on failure</returns>
[DllImport("linguard.so")]
public static extern WriteDetectionStatus* AddTripGuard(UIntPtr start, UIntPtr length);
/// <summary>
/// Remove write detection from the specified addresses.
/// CALLER'S RESPONSIBILITY: All addresses are page aligned.
/// CALLER'S RESPONSIBILITY: No other thread enters any LinGuard function, or trips any tracked guard page during this call.
/// </summary>
/// <returns>false on failure (usually, the address range did not match a known one)</returns>
[DllImport("linguard.so")]
public static extern bool RemoveTripGuard(UIntPtr start, UIntPtr length);
/// <summary>
/// Examines a previously installed guard page detection.
/// CALLER'S RESPONSIBILITY: All addresses are page aligned.
/// CALLER'S RESPONSIBILITY: No other thread enters any LinGuard function, or trips any tracked guard page during this call.
/// </summary>
/// <returns>
/// A pointer to an array of bytes, one byte for each memory page in the range. Caller should set CanChange on pages to
/// observe, and read back DidChange to see if things changed.
/// </returns>
[DllImport("linguard.so")]
public static extern WriteDetectionStatus* ExamineTripGuard(UIntPtr start, UIntPtr length);
}
}
}

View File

@ -1,104 +0,0 @@
using System;
using System.Runtime.InteropServices;
using static BizHawk.BizInvoke.MemoryBlock;
using static BizHawk.BizInvoke.POSIXLibC;
namespace BizHawk.BizInvoke
{
public sealed class MemoryBlockUnixPal : IMemoryBlockPal
{
/// <summary>handle returned by <see cref="memfd_create"/></summary>
private int _fd = -1;
private ulong _start;
private ulong _size;
private ulong _committedSize;
/// <summary>
/// Reserve bytes to later be swapped in, but do not map them
/// </summary>
/// <param name="start">eventual mapped address</param>
/// <param name="size"></param>
/// <exception cref="InvalidOperationException">
/// failed to get file descriptor
/// </exception>
public MemoryBlockUnixPal(ulong start, ulong size)
{
// Console.WriteLine($".ctor {start:x16} {size:x16}");
_start = start;
_size = size;
_fd = memfd_create("MemoryBlockUnix", 1 /*MFD_CLOEXEC*/);
if (_fd == -1)
throw new InvalidOperationException($"{nameof(memfd_create)}() failed with error {Marshal.GetLastWin32Error()}");
}
public void Dispose()
{
if (_fd == -1)
return;
close(_fd);
_fd = -1;
GC.SuppressFinalize(this);
}
~MemoryBlockUnixPal()
{
Dispose();
}
public void Activate()
{
if (_committedSize > 0)
{
var ptr = mmap(Z.US(_start), Z.UU(_committedSize),
MemoryProtection.Read | MemoryProtection.Write | MemoryProtection.Execute,
17, // MAP_SHARED | MAP_FIXED
_fd, IntPtr.Zero);
if (ptr != Z.US(_start))
throw new InvalidOperationException($"{nameof(mmap)}() failed with error {Marshal.GetLastWin32Error()}");
}
}
public void Deactivate()
{
if (_committedSize > 0)
{
var errorCode = munmap(Z.US(_start), Z.UU(_committedSize));
if (errorCode != 0)
throw new InvalidOperationException($"{nameof(munmap)}() failed with error {Marshal.GetLastWin32Error()}");
}
}
public void Commit(ulong length)
{
// Console.WriteLine($"commit {length:x16}");
Deactivate();
var errorCode = ftruncate(_fd, Z.US(length));
if (errorCode != 0)
throw new InvalidOperationException($"{nameof(ftruncate)}() failed with error {Marshal.GetLastWin32Error()}");
_committedSize = length;
Activate();
}
public void Protect(ulong start, ulong size, Protection prot)
{
// Console.WriteLine($"protect {start:x16} {size:x16} {prot}");
var errorCode = mprotect(
Z.US(start),
Z.UU(size),
prot.ToMemoryProtection()
);
if (errorCode != 0)
throw new InvalidOperationException($"{nameof(mprotect)}() failed with error {Marshal.GetLastWin32Error()}!");
}
public void GetWriteStatus(WriteDetectionStatus[] dest, Protection[] pagedata)
{
// TODO
}
public void SetWriteStatus(WriteDetectionStatus[] src)
{
// TODO
}
}
}

View File

@ -12,7 +12,7 @@ namespace BizHawk.BizInvoke
private IntPtr _handle;
private ulong _start;
private ulong _size;
private bool _guardActive;
private bool _active;
/// <summary>
/// Reserve bytes to later be swapped in, but do not map them
@ -54,7 +54,7 @@ namespace BizHawk.BizInvoke
{
throw new InvalidOperationException($"{nameof(WinGuard.AddTripGuard)}() returned NULL");
}
_guardActive = true;
_active = true;
}
public void Deactivate()
@ -63,7 +63,7 @@ namespace BizHawk.BizInvoke
throw new InvalidOperationException($"{nameof(Kernel32.UnmapViewOfFile)}() returned NULL");
if (!WinGuard.RemoveTripGuard(Z.UU(_start), Z.UU(_size)))
throw new InvalidOperationException($"{nameof(WinGuard.RemoveTripGuard)}() returned FALSE");
_guardActive = false;
_active = false;
}
public void Protect(ulong start, ulong size, Protection prot)
@ -101,13 +101,17 @@ namespace BizHawk.BizInvoke
{
if (_handle != IntPtr.Zero)
{
if (_active)
{
try
{
Deactivate();
}
catch
{}
}
Kernel32.CloseHandle(_handle);
_handle = IntPtr.Zero;
if (_guardActive)
{
WinGuard.RemoveTripGuard(Z.UU(_start), Z.UU(_size));
_guardActive = false;
}
GC.SuppressFinalize(this);
}
}

View File

@ -31,19 +31,5 @@ namespace BizHawk.BizInvoke
/// <remarks>32-bit signed int</remarks>
[Flags]
public enum MemoryProtection : int { None = 0x0, Read = 0x1, Write = 0x2, Execute = 0x4 }
public static MemoryProtection ToMemoryProtection(this Protection prot)
{
switch (prot)
{
case Protection.None: return MemoryProtection.None;
case Protection.R: return MemoryProtection.Read | MemoryProtection.Write; // FIXME
case Protection.RW: return MemoryProtection.Read | MemoryProtection.Write;
case Protection.RX: return MemoryProtection.Read | MemoryProtection.Execute;
case Protection.RW_Invisible: return MemoryProtection.Read | MemoryProtection.Write;
case Protection.RW_Stack: return MemoryProtection.Read | MemoryProtection.Write; // FIXME
default: throw new ArgumentOutOfRangeException(nameof(prot));
}
}
}
}

View File

@ -0,0 +1,13 @@
ifeq (,$(findstring Linux,$(shell uname)))
$(error This must be built with linux)
endif
linguard.so: linguard.c
gcc -Wall -O2 -s -o $@ $< -shared -fPIC
install: linguard.so
cp $< ../../output
clean:
rm linguard.so
.PHONY: install clean

View File

@ -0,0 +1,279 @@
#define _GNU_SOURCE
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/mman.h>
#include <stdio.h>
#include <errno.h>
#define MAX_TRIPS 64
typedef struct {
uintptr_t start;
uintptr_t length;
uint8_t tripped[0];
} tripwire_t;
static tripwire_t* Trips[MAX_TRIPS];
static int HandlerInstalled;
static char altstack[SIGSTKSZ];
static struct sigaction sa_old;
static void SignalHandler(int sig, siginfo_t* info, void* ucontext)
{
uintptr_t faultAddress = (uintptr_t)info->si_addr;
for (int i = 0; i < MAX_TRIPS; i++)
{
if (Trips[i] && faultAddress >= Trips[i]->start && faultAddress < Trips[i]->start + Trips[i]->length)
{
uintptr_t page = (faultAddress - Trips[i]->start) >> 12;
if (Trips[i]->tripped[page] & 1) // should change
{
if (mprotect((void*)(faultAddress & ~0xffful), 0x1000, PROT_READ | PROT_WRITE) != 0)
{
abort();
while (1)
;
}
Trips[i]->tripped[page] = 3; // did change
return;
}
else
{
break;
}
}
}
if (sa_old.sa_flags & SA_SIGINFO)
sa_old.sa_sigaction(sig, info, ucontext);
else
sa_old.sa_handler(sig);
}
static int InstallHandler()
{
stack_t ss;
ss.ss_flags = 0;
ss.ss_sp = altstack;
ss.ss_size = sizeof(altstack);
if (sigaltstack(&ss, NULL) != 0)
{
fprintf(stderr, "sigaltstack: %i\n", errno);
return 0;
}
struct sigaction sa;
sa.sa_sigaction = SignalHandler;
sa.sa_flags = SA_ONSTACK | SA_SIGINFO;
sigfillset(&sa.sa_mask);
if (sigaction(SIGSEGV, &sa, &sa_old) != 0)
{
fprintf(stderr, "sigaction: %i\n", errno);
return 0;
}
return 1;
}
uint8_t* AddTripGuard(uintptr_t start, uintptr_t length)
{
if (!HandlerInstalled)
{
if (!InstallHandler())
return NULL;
HandlerInstalled = 1;
}
uintptr_t npage = length >> 12;
for (int i = 0; i < MAX_TRIPS; i++)
{
if (!Trips[i])
{
Trips[i] = calloc(1, sizeof(*Trips[i]) + npage);
if (!Trips[i])
return NULL;
Trips[i]->start = start;
Trips[i]->length = length;
return &Trips[i]->tripped[0];
}
}
return NULL;
}
int64_t RemoveTripGuard(uintptr_t start, uintptr_t length)
{
for (int i = 0; i < MAX_TRIPS; i++)
{
if (Trips[i] && Trips[i]->start == start && Trips[i]->length == length)
{
free(Trips[i]);
Trips[i] = NULL;
return 1;
}
}
return 0;
}
uint8_t* ExamineTripGuard(uintptr_t start, uintptr_t length)
{
for (int i = 0; i < MAX_TRIPS; i++)
{
if (Trips[i] && Trips[i]->start == start && Trips[i]->length == length)
return &Trips[i]->tripped[0];
}
return NULL;
}
/*
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
struct uffdio_writeprotect wp;
wp.range.start = start;
wp.range.len = length;
wp.mode = 0;
if (ioctl(fd, UFFDIO_WRITEPROTECT, &wp) == -1)
{
free(Trips[i]);
Trips[i] = NULL;
return NULL;
}
static void v(const char* msg, int value)
{
if (v < 0)
{
printf("ERROR: %s %i\n", msg, errno);
exit(1);
}
}
const uint64_t addr = 0x36f00000000ul;
const uint64_t size = 0x100000ul;
static void* threadproc(void* arg)
{
int wpfd = (int)(long)arg;
// should be able to register once for a large range,
// then wp smaller ranges
struct uffdio_register uffdio_register;
uffdio_register.range.start = addr;
uffdio_register.range.len = size;
uffdio_register.mode = UFFDIO_REGISTER_MODE_WP;
v("ioctl:UFFDIO_REGISTER", ioctl(wpfd, UFFDIO_REGISTER, &uffdio_register));
v("uffdio_register.ioctls", (uffdio_register.ioctls & UFFD_API_RANGE_IOCTLS) == UFFD_API_RANGE_IOCTLS ? 0 : -1);
// wp
{
struct uffdio_writeprotect wp;
wp.range.start = addr;
wp.range.len = size;
wp.mode = UFFDIO_WRITEPROTECT_MODE_WP;
v("ioctl:UFFDIO_WRITEPROTECT", ioctl(wpfd, UFFDIO_WRITEPROTECT, &wp));
}
while (1)
{
struct uffd_msg msg;
int nb = read(wpfd, &msg, sizeof(msg));
if (nb == -1)
{
if (errno == EAGAIN)
continue;
v("read", errno);
}
if (nb != sizeof(msg))
v("sizeof(msg)", -1);
if (msg.event & UFFD_EVENT_PAGEFAULT)
{
printf("==> Event is pagefault on %p flags 0x%llx write? 0x%llx wp? 0x%llx\n"
, (void *)msg.arg.pagefault.address
, msg.arg.pagefault.flags
, msg.arg.pagefault.flags & UFFD_PAGEFAULT_FLAG_WRITE
, msg.arg.pagefault.flags & UFFD_PAGEFAULT_FLAG_WP
);
}
if (msg.arg.pagefault.flags & UFFD_PAGEFAULT_FLAG_WP)
{
// send write unlock
struct uffdio_writeprotect wp;
wp.range.start = addr;
wp.range.len = size;
wp.mode = 0;
printf("sending !UFFDIO_WRITEPROTECT event to userfaultfd\n");
v("ioctl:UFFDIO_WRITEPROTECT", ioctl(wpfd, UFFDIO_WRITEPROTECT, &wp));
}
}
return NULL;
}
int main(void)
{
int fd = memfd_create("pewps", MFD_CLOEXEC);
v("memfd_create", fd);
printf("fd: %i\n", fd);
v("ftruncate", ftruncate(fd, size));
char* ptr = mmap((void*)addr, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED | MAP_FIXED, fd, 0);
if (ptr == MAP_FAILED || ptr != (void*)addr)
v("mmap", (int)(long)ptr);
v("mprotect", mprotect(ptr, size, PROT_READ | PROT_WRITE));
int wpfd = syscall(SYS_userfaultfd, O_CLOEXEC);
v("SYS_userfaultfd", wpfd);
struct uffdio_api uffdio_api;
uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
v("ioctl:UFFDIO_API", ioctl(wpfd, UFFDIO_API, &uffdio_api));
v("uffdio_api.api", uffdio_api.api == UFFD_API ? 0 : -1);
pthread_t thr;
v("pthread_create", pthread_create(&thr, NULL, threadproc, (void*)(long)wpfd));
for (uint64_t a = addr; a < addr + size; a += 4096)
{
sleep(1);
printf("Gonna read");
if (*(uint64_t*)a == 77777)
{
printf("Lucky!");
}
sleep(1);
printf("Gonna write");
strcpy((void*)a, "string in mem area");
printf("%s\n", (char*)a);
}
}
*/

Binary file not shown.