diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /mfbt/tests/TestPoisonArea.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mfbt/tests/TestPoisonArea.cpp')
-rw-r--r-- | mfbt/tests/TestPoisonArea.cpp | 530 |
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; +} |