summaryrefslogtreecommitdiffstats
path: root/mfbt/tests/TestPoisonArea.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /mfbt/tests/TestPoisonArea.cpp
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mfbt/tests/TestPoisonArea.cpp')
-rw-r--r--mfbt/tests/TestPoisonArea.cpp530
1 files changed, 530 insertions, 0 deletions
diff --git a/mfbt/tests/TestPoisonArea.cpp b/mfbt/tests/TestPoisonArea.cpp
new file mode 100644
index 0000000000..9df3929834
--- /dev/null
+++ b/mfbt/tests/TestPoisonArea.cpp
@@ -0,0 +1,530 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+/* Code in this file needs to be kept in sync with code in nsPresArena.cpp.
+ *
+ * We want to use a fixed address for frame poisoning so that it is readily
+ * identifiable in crash dumps. Whether such an address is available
+ * without any special setup depends on the system configuration.
+ *
+ * All current 64-bit CPUs (with the possible exception of PowerPC64)
+ * reserve the vast majority of the virtual address space for future
+ * hardware extensions; valid addresses must be below some break point
+ * between 2**48 and 2**54, depending on exactly which chip you have. Some
+ * chips (notably amd64) also allow the use of the *highest* 2**48 -- 2**54
+ * addresses. Thus, if user space pointers are 64 bits wide, we can just
+ * use an address outside this range, and no more is required. To
+ * accommodate the chips that allow very high addresses to be valid, the
+ * value chosen is close to 2**63 (that is, in the middle of the space).
+ *
+ * In most cases, a purely 32-bit operating system must reserve some
+ * fraction of the address space for its own use. Contemporary 32-bit OSes
+ * tend to take the high gigabyte or so (0xC000_0000 on up). If we can
+ * prove that high addresses are reserved to the kernel, we can use an
+ * address in that region. Unfortunately, not all 32-bit OSes do this;
+ * OSX 10.4 might not, and it is unclear what mobile OSes are like
+ * (some 32-bit CPUs make it very easy for the kernel to exist in its own
+ * private address space).
+ *
+ * Furthermore, when a 32-bit user space process is running on a 64-bit
+ * kernel, the operating system has no need to reserve any of the space that
+ * the process can see, and generally does not do so. This is the scenario
+ * of greatest concern, since it covers all contemporary OSX iterations
+ * (10.5+) as well as Windows Vista and 7 on newer amd64 hardware. Linux on
+ * amd64 is generally run as a pure 64-bit environment, but its 32-bit
+ * compatibility mode also has this property.
+ *
+ * Thus, when user space pointers are 32 bits wide, we need to validate
+ * our chosen address, and possibly *make* it a good poison address by
+ * allocating a page around it and marking it inaccessible. The algorithm
+ * for this is:
+ *
+ * 1. Attempt to make the page surrounding the poison address a reserved,
+ * inaccessible memory region using OS primitives. On Windows, this is
+ * done with VirtualAlloc(MEM_RESERVE); on Unix, mmap(PROT_NONE).
+ *
+ * 2. If mmap/VirtualAlloc failed, there are two possible reasons: either
+ * the region is reserved to the kernel and no further action is
+ * required, or there is already usable memory in this area and we have
+ * to pick a different address. The tricky part is knowing which case
+ * we have, without attempting to access the region. On Windows, we
+ * rely on GetSystemInfo()'s reported upper and lower bounds of the
+ * application memory area. On Unix, there is nothing devoted to the
+ * purpose, but seeing if madvise() fails is close enough (it *might*
+ * disrupt someone else's use of the memory region, but not by as much
+ * as anything else available).
+ *
+ * Be aware of these gotchas:
+ *
+ * 1. We cannot use mmap() with MAP_FIXED. MAP_FIXED is defined to
+ * _replace_ any existing mapping in the region, if necessary to satisfy
+ * the request. Obviously, as we are blindly attempting to acquire a
+ * page at a constant address, we must not do this, lest we overwrite
+ * someone else's allocation.
+ *
+ * 2. For the same reason, we cannot blindly use mprotect() if mmap() fails.
+ *
+ * 3. madvise() may fail when applied to a 'magic' memory region provided as
+ * a kernel/user interface. Fortunately, the only such case I know about
+ * is the "vsyscall" area (not to be confused with the "vdso" area) for
+ * *64*-bit processes on Linux - and we don't even run this code for
+ * 64-bit processes.
+ *
+ * 4. VirtualQuery() does not produce any useful information if
+ * applied to kernel memory - in fact, it doesn't write its output
+ * at all. Thus, it is not used here.
+ */
+
+// MAP_ANON(YMOUS) is not in any standard. Add defines as necessary.
+#define _GNU_SOURCE 1
+#define _DARWIN_C_SOURCE 1
+
+#include <errno.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifdef _WIN32
+# include <windows.h>
+#else
+# include <sys/types.h>
+# include <unistd.h>
+# include <sys/wait.h>
+
+# include <sys/mman.h>
+# ifndef MAP_ANON
+# ifdef MAP_ANONYMOUS
+# define MAP_ANON MAP_ANONYMOUS
+# else
+# error "Don't know how to get anonymous memory"
+# endif
+# endif
+#endif
+
+#define SIZxPTR ((int)(sizeof(uintptr_t) * 2))
+
+/* This program assumes that a whole number of return instructions fit into
+ * 32 bits, and that 32-bit alignment is sufficient for a branch destination.
+ * For architectures where this is not true, fiddling with RETURN_INSTR_TYPE
+ * can be enough.
+ */
+
+#if defined __i386__ || defined __x86_64__ || defined __i386 || \
+ defined __x86_64 || defined _M_IX86 || defined _M_AMD64
+# define RETURN_INSTR 0xC3C3C3C3 /* ret; ret; ret; ret */
+
+#elif defined __arm__ || defined _M_ARM
+# define RETURN_INSTR 0xE12FFF1E /* bx lr */
+
+// PPC has its own style of CPU-id #defines. There is no Windows for
+// PPC as far as I know, so no _M_ variant.
+#elif defined _ARCH_PPC || defined _ARCH_PWR || defined _ARCH_PWR2
+# define RETURN_INSTR 0x4E800020 /* blr */
+
+#elif defined __m68k__
+# define RETURN_INSTR 0x4E754E75 /* rts; rts */
+
+#elif defined __riscv
+# define RETURN_INSTR 0x80828082 /* ret; ret */
+
+#elif defined __sparc || defined __sparcv9
+# define RETURN_INSTR 0x81c3e008 /* retl */
+
+#elif defined __alpha
+# define RETURN_INSTR 0x6bfa8001 /* ret */
+
+#elif defined __hppa
+# define RETURN_INSTR 0xe840c002 /* bv,n r0(rp) */
+
+#elif defined __mips
+# define RETURN_INSTR 0x03e00008 /* jr ra */
+
+# ifdef __MIPSEL
+/* On mipsel, jr ra needs to be followed by a nop.
+ 0x03e00008 as a 64 bits integer just does that */
+# define RETURN_INSTR_TYPE uint64_t
+# endif
+
+#elif defined __s390__
+# define RETURN_INSTR 0x07fe0000 /* br %r14 */
+
+#elif defined __sh__
+# define RETURN_INSTR 0x0b000b00 /* rts; rts */
+
+#elif defined __aarch64__ || defined _M_ARM64
+# define RETURN_INSTR 0xd65f03c0 /* ret */
+
+#elif defined __loongarch64
+# define RETURN_INSTR 0x4c000020 /* jirl zero, ra, 0 */
+
+#elif defined __ia64
+struct ia64_instr {
+ uint32_t mI[4];
+};
+static const ia64_instr _return_instr = {
+ {0x00000011, 0x00000001, 0x80000200, 0x00840008}}; /* br.ret.sptk.many b0 */
+
+# define RETURN_INSTR _return_instr
+# define RETURN_INSTR_TYPE ia64_instr
+
+#else
+# error "Need return instruction for this architecture"
+#endif
+
+#ifndef RETURN_INSTR_TYPE
+# define RETURN_INSTR_TYPE uint32_t
+#endif
+
+// Miscellaneous Windows/Unix portability gumph
+
+#ifdef _WIN32
+// Uses of this function deliberately leak the string.
+static LPSTR StrW32Error(DWORD aErrcode) {
+ LPSTR errmsg;
+ FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
+ FORMAT_MESSAGE_IGNORE_INSERTS,
+ nullptr, aErrcode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
+ (LPSTR)&errmsg, 0, nullptr);
+
+ // FormatMessage puts an unwanted newline at the end of the string
+ size_t n = strlen(errmsg) - 1;
+ while (errmsg[n] == '\r' || errmsg[n] == '\n') {
+ n--;
+ }
+ errmsg[n + 1] = '\0';
+ return errmsg;
+}
+# define LastErrMsg() (StrW32Error(GetLastError()))
+
+// Because we use VirtualAlloc in MEM_RESERVE mode, the "page size" we want
+// is the allocation granularity.
+static SYSTEM_INFO sInfo_;
+
+static inline uint32_t PageSize() { return sInfo_.dwAllocationGranularity; }
+
+static void* ReserveRegion(uintptr_t aRequest, bool aAccessible) {
+ return VirtualAlloc((void*)aRequest, PageSize(),
+ aAccessible ? MEM_RESERVE | MEM_COMMIT : MEM_RESERVE,
+ aAccessible ? PAGE_EXECUTE_READWRITE : PAGE_NOACCESS);
+}
+
+static void ReleaseRegion(void* aPage) {
+ VirtualFree(aPage, PageSize(), MEM_RELEASE);
+}
+
+static bool ProbeRegion(uintptr_t aPage) {
+ return aPage >= (uintptr_t)sInfo_.lpMaximumApplicationAddress &&
+ aPage + PageSize() >= (uintptr_t)sInfo_.lpMaximumApplicationAddress;
+}
+
+static bool MakeRegionExecutable(void*) { return false; }
+
+# undef MAP_FAILED
+# define MAP_FAILED 0
+
+#else // Unix
+
+# define LastErrMsg() (strerror(errno))
+
+static unsigned long gUnixPageSize;
+
+static inline unsigned long PageSize() { return gUnixPageSize; }
+
+static void* ReserveRegion(uintptr_t aRequest, bool aAccessible) {
+ return mmap(reinterpret_cast<void*>(aRequest), PageSize(),
+ aAccessible ? PROT_READ | PROT_WRITE : PROT_NONE,
+ MAP_PRIVATE | MAP_ANON, -1, 0);
+}
+
+static void ReleaseRegion(void* aPage) { munmap(aPage, PageSize()); }
+
+static bool ProbeRegion(uintptr_t aPage) {
+# ifdef XP_SOLARIS
+ return !!posix_madvise(reinterpret_cast<void*>(aPage), PageSize(),
+ POSIX_MADV_NORMAL);
+# else
+ return !!madvise(reinterpret_cast<void*>(aPage), PageSize(), MADV_NORMAL);
+# endif
+}
+
+static int MakeRegionExecutable(void* aPage) {
+ return mprotect((caddr_t)aPage, PageSize(),
+ PROT_READ | PROT_WRITE | PROT_EXEC);
+}
+
+#endif
+
+static uintptr_t ReservePoisonArea() {
+ if (sizeof(uintptr_t) == 8) {
+ // Use the hardware-inaccessible region.
+ // We have to avoid 64-bit constants and shifts by 32 bits, since this
+ // code is compiled in 32-bit mode, although it is never executed there.
+ uintptr_t result =
+ (((uintptr_t(0x7FFFFFFFu) << 31) << 1 | uintptr_t(0xF0DEAFFFu)) &
+ ~uintptr_t(PageSize() - 1));
+ printf("INFO | poison area assumed at 0x%.*" PRIxPTR "\n", SIZxPTR, result);
+ return result;
+ }
+
+ // First see if we can allocate the preferred poison address from the OS.
+ uintptr_t candidate = (0xF0DEAFFF & ~(PageSize() - 1));
+ void* result = ReserveRegion(candidate, false);
+ if (result == reinterpret_cast<void*>(candidate)) {
+ // success - inaccessible page allocated
+ printf("INFO | poison area allocated at 0x%.*" PRIxPTR
+ " (preferred addr)\n",
+ SIZxPTR, reinterpret_cast<uintptr_t>(result));
+ return candidate;
+ }
+
+ // That didn't work, so see if the preferred address is within a range
+ // of permanently inacessible memory.
+ if (ProbeRegion(candidate)) {
+ // success - selected page cannot be usable memory
+ if (result != MAP_FAILED) {
+ ReleaseRegion(result);
+ }
+ printf("INFO | poison area assumed at 0x%.*" PRIxPTR " (preferred addr)\n",
+ SIZxPTR, candidate);
+ return candidate;
+ }
+
+ // The preferred address is already in use. Did the OS give us a
+ // consolation prize?
+ if (result != MAP_FAILED) {
+ uintptr_t ures = reinterpret_cast<uintptr_t>(result);
+ printf("INFO | poison area allocated at 0x%.*" PRIxPTR
+ " (consolation prize)\n",
+ SIZxPTR, ures);
+ return ures;
+ }
+
+ // It didn't, so try to allocate again, without any constraint on
+ // the address.
+ result = ReserveRegion(0, false);
+ if (result != MAP_FAILED) {
+ uintptr_t ures = reinterpret_cast<uintptr_t>(result);
+ printf("INFO | poison area allocated at 0x%.*" PRIxPTR " (fallback)\n",
+ SIZxPTR, ures);
+ return ures;
+ }
+
+ printf("ERROR | no usable poison area found\n");
+ return 0;
+}
+
+/* The "positive control" area confirms that we can allocate a page with the
+ * proper characteristics.
+ */
+static uintptr_t ReservePositiveControl() {
+ void* result = ReserveRegion(0, false);
+ if (result == MAP_FAILED) {
+ printf("ERROR | allocating positive control | %s\n", LastErrMsg());
+ return 0;
+ }
+ printf("INFO | positive control allocated at 0x%.*" PRIxPTR "\n", SIZxPTR,
+ (uintptr_t)result);
+ return (uintptr_t)result;
+}
+
+/* The "negative control" area confirms that our probe logic does detect a
+ * page that is readable, writable, or executable.
+ */
+static uintptr_t ReserveNegativeControl() {
+ void* result = ReserveRegion(0, true);
+ if (result == MAP_FAILED) {
+ printf("ERROR | allocating negative control | %s\n", LastErrMsg());
+ return 0;
+ }
+
+ // Fill the page with return instructions.
+ RETURN_INSTR_TYPE* p = reinterpret_cast<RETURN_INSTR_TYPE*>(result);
+ RETURN_INSTR_TYPE* limit = reinterpret_cast<RETURN_INSTR_TYPE*>(
+ reinterpret_cast<char*>(result) + PageSize());
+ while (p < limit) {
+ *p++ = RETURN_INSTR;
+ }
+
+ // Now mark it executable as well as readable and writable.
+ // (mmap(PROT_EXEC) may fail when applied to anonymous memory.)
+
+ if (MakeRegionExecutable(result)) {
+ printf("ERROR | making negative control executable | %s\n", LastErrMsg());
+ return 0;
+ }
+
+ printf("INFO | negative control allocated at 0x%.*" PRIxPTR "\n", SIZxPTR,
+ (uintptr_t)result);
+ return (uintptr_t)result;
+}
+
+#ifndef _WIN32
+static void JumpTo(uintptr_t aOpaddr) {
+# ifdef __ia64
+ struct func_call {
+ uintptr_t mFunc;
+ uintptr_t mGp;
+ } call = {
+ aOpaddr,
+ };
+ ((void (*)()) & call)();
+# else
+ ((void (*)())aOpaddr)();
+# endif
+}
+#endif
+
+/* Test each page. */
+static bool TestPage(const char* aPageLabel, uintptr_t aPageAddr,
+ int aShouldSucceed) {
+ const char* oplabel;
+ uintptr_t opaddr;
+
+ bool failed = false;
+ for (unsigned int test = 0; test < 3; test++) {
+ switch (test) {
+ // The execute test must be done before the write test, because the
+ // write test will clobber memory at the target address.
+ case 0:
+ oplabel = "reading";
+ opaddr = aPageAddr + PageSize() / 2 - 1;
+ break;
+ case 1:
+ oplabel = "executing";
+ opaddr = aPageAddr + PageSize() / 2;
+ break;
+ case 2:
+ oplabel = "writing";
+ opaddr = aPageAddr + PageSize() / 2 - 1;
+ break;
+ default:
+ abort();
+ }
+
+#ifdef _WIN32
+ bool badptr = true;
+ MEMORY_BASIC_INFORMATION mbi = {};
+
+ if (VirtualQuery((LPCVOID)opaddr, &mbi, sizeof(mbi)) &&
+ mbi.State == MEM_COMMIT) {
+ switch (test) {
+ case 0: // read
+ badptr = !(mbi.Protect & (PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE |
+ PAGE_READONLY | PAGE_READWRITE));
+ break;
+ case 1: // execute
+ badptr =
+ !(mbi.Protect & (PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE));
+ break;
+ case 2: // write
+ badptr = !(mbi.Protect & (PAGE_READWRITE | PAGE_EXECUTE_READWRITE));
+ break;
+ default:
+ abort();
+ }
+ }
+
+ if (badptr) {
+ if (aShouldSucceed) {
+ printf("TEST-UNEXPECTED-FAIL | %s %s\n", oplabel, aPageLabel);
+ failed = true;
+ } else {
+ printf("TEST-PASS | %s %s\n", oplabel, aPageLabel);
+ }
+ } else {
+ // if control reaches this point the probe succeeded
+ if (aShouldSucceed) {
+ printf("TEST-PASS | %s %s\n", oplabel, aPageLabel);
+ } else {
+ printf("TEST-UNEXPECTED-FAIL | %s %s\n", oplabel, aPageLabel);
+ failed = true;
+ }
+ }
+#else
+ pid_t pid = fork();
+ if (pid == -1) {
+ printf("ERROR | %s %s | fork=%s\n", oplabel, aPageLabel, LastErrMsg());
+ exit(2);
+ } else if (pid == 0) {
+ volatile unsigned char scratch;
+ switch (test) {
+ case 0:
+ scratch = *(volatile unsigned char*)opaddr;
+ break;
+ case 1:
+ JumpTo(opaddr);
+ break;
+ case 2:
+ *(volatile unsigned char*)opaddr = 0;
+ break;
+ default:
+ abort();
+ }
+ (void)scratch;
+ _exit(0);
+ } else {
+ int status;
+ if (waitpid(pid, &status, 0) != pid) {
+ printf("ERROR | %s %s | wait=%s\n", oplabel, aPageLabel, LastErrMsg());
+ exit(2);
+ }
+
+ if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
+ if (aShouldSucceed) {
+ printf("TEST-PASS | %s %s\n", oplabel, aPageLabel);
+ } else {
+ printf("TEST-UNEXPECTED-FAIL | %s %s | unexpected successful exit\n",
+ oplabel, aPageLabel);
+ failed = true;
+ }
+ } else if (WIFEXITED(status)) {
+ printf("ERROR | %s %s | unexpected exit code %d\n", oplabel, aPageLabel,
+ WEXITSTATUS(status));
+ exit(2);
+ } else if (WIFSIGNALED(status)) {
+ if (aShouldSucceed) {
+ printf("TEST-UNEXPECTED-FAIL | %s %s | unexpected signal %d\n",
+ oplabel, aPageLabel, WTERMSIG(status));
+ failed = true;
+ } else {
+ printf("TEST-PASS | %s %s | signal %d (as expected)\n", oplabel,
+ aPageLabel, WTERMSIG(status));
+ }
+ } else {
+ printf("ERROR | %s %s | unexpected exit status %d\n", oplabel,
+ aPageLabel, status);
+ exit(2);
+ }
+ }
+#endif
+ }
+ return failed;
+}
+
+int main() {
+#ifdef _WIN32
+ GetSystemInfo(&sInfo_);
+#else
+ gUnixPageSize = sysconf(_SC_PAGESIZE);
+#endif
+
+ uintptr_t ncontrol = ReserveNegativeControl();
+ uintptr_t pcontrol = ReservePositiveControl();
+ uintptr_t poison = ReservePoisonArea();
+
+ if (!ncontrol || !pcontrol || !poison) {
+ return 2;
+ }
+
+ bool failed = false;
+ failed |= TestPage("negative control", ncontrol, 1);
+ failed |= TestPage("positive control", pcontrol, 0);
+ failed |= TestPage("poison area", poison, 0);
+
+ return failed ? 1 : 0;
+}