diff --git a/output/linguard.so b/output/linguard.so new file mode 100644 index 0000000000..e0a5910687 Binary files /dev/null and b/output/linguard.so differ diff --git a/src/BizHawk.BizInvoke/MemoryBlock.cs b/src/BizHawk.BizInvoke/MemoryBlock.cs index 1ae0fb1fec..fe8c4edae4 100644 --- a/src/BizHawk.BizInvoke/MemoryBlock.cs +++ b/src/BizHawk.BizInvoke/MemoryBlock.cs @@ -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); } diff --git a/src/BizHawk.BizInvoke/MemoryBlockLinuxPal.cs b/src/BizHawk.BizInvoke/MemoryBlockLinuxPal.cs new file mode 100644 index 0000000000..48caa1595e --- /dev/null +++ b/src/BizHawk.BizInvoke/MemoryBlockLinuxPal.cs @@ -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. + */ + + /// handle returned by + private int _fd = -1; + private ulong _start; + private ulong _size; + private ulong _committedSize; + private bool _active; + + /// + /// Reserve bytes to later be swapped in, but do not map them + /// + /// eventual mapped address + /// + /// + /// failed to get file descriptor + /// + 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 + { + /// + /// 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. + /// + /// The same information as ExamineTripGuard, or null on failure + [DllImport("linguard.so")] + public static extern WriteDetectionStatus* AddTripGuard(UIntPtr start, UIntPtr length); + /// + /// 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. + /// + /// false on failure (usually, the address range did not match a known one) + [DllImport("linguard.so")] + public static extern bool RemoveTripGuard(UIntPtr start, UIntPtr length); + /// + /// 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. + /// + /// + /// 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. + /// + [DllImport("linguard.so")] + public static extern WriteDetectionStatus* ExamineTripGuard(UIntPtr start, UIntPtr length); + } + } +} diff --git a/src/BizHawk.BizInvoke/MemoryBlockUnixPal.cs b/src/BizHawk.BizInvoke/MemoryBlockUnixPal.cs deleted file mode 100644 index 56250aadaf..0000000000 --- a/src/BizHawk.BizInvoke/MemoryBlockUnixPal.cs +++ /dev/null @@ -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 - { - /// handle returned by - private int _fd = -1; - private ulong _start; - private ulong _size; - private ulong _committedSize; - - /// - /// Reserve bytes to later be swapped in, but do not map them - /// - /// eventual mapped address - /// - /// - /// failed to get file descriptor - /// - 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 - } - } -} diff --git a/src/BizHawk.BizInvoke/MemoryBlockWindowsPal.cs b/src/BizHawk.BizInvoke/MemoryBlockWindowsPal.cs index bbb9305e7a..df67e22a96 100644 --- a/src/BizHawk.BizInvoke/MemoryBlockWindowsPal.cs +++ b/src/BizHawk.BizInvoke/MemoryBlockWindowsPal.cs @@ -12,7 +12,7 @@ namespace BizHawk.BizInvoke private IntPtr _handle; private ulong _start; private ulong _size; - private bool _guardActive; + private bool _active; /// /// 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); } } diff --git a/src/BizHawk.BizInvoke/POSIXLibC.cs b/src/BizHawk.BizInvoke/POSIXLibC.cs index c5337f22c0..b62d2fbbc8 100644 --- a/src/BizHawk.BizInvoke/POSIXLibC.cs +++ b/src/BizHawk.BizInvoke/POSIXLibC.cs @@ -31,19 +31,5 @@ namespace BizHawk.BizInvoke /// 32-bit signed int [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)); - } - } } } diff --git a/waterbox/linguard/Makefile b/waterbox/linguard/Makefile new file mode 100644 index 0000000000..6c48b41f08 --- /dev/null +++ b/waterbox/linguard/Makefile @@ -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 diff --git a/waterbox/linguard/linguard.c b/waterbox/linguard/linguard.c new file mode 100644 index 0000000000..cd7fb647df --- /dev/null +++ b/waterbox/linguard/linguard.c @@ -0,0 +1,279 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + 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); + } +} + +*/ diff --git a/waterbox/linguard/linguard.so b/waterbox/linguard/linguard.so new file mode 100644 index 0000000000..e0a5910687 Binary files /dev/null and b/waterbox/linguard/linguard.so differ