Bug 844196 - Add SetJitExceptionHandler friend API and call it if unwinding reaches JIT code (r=jandem,dmajor)
authorLuke Wagner <luke@mozilla.com>
Wed, 08 Oct 2014 14:24:15 -0500
changeset 209484 f920a1e7d15a1b43b3c2d08e28e454fc2d3c0f04
parent 209483 541dbaa3fec2170c525424ac8a7dc2faafbbf04d
child 209485 eabbf84195071b3e2aa6035c3f972841bc451fbe
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersjandem, dmajor
bugs844196
milestone35.0a1
Bug 844196 - Add SetJitExceptionHandler friend API and call it if unwinding reaches JIT code (r=jandem,dmajor)
js/src/jit/ExecutableAllocator.h
js/src/jit/ExecutableAllocatorWin.cpp
js/src/jsfriendapi.h
--- a/js/src/jit/ExecutableAllocator.h
+++ b/js/src/jit/ExecutableAllocator.h
@@ -184,25 +184,30 @@ class ExecutableAllocator {
     DestroyCallback destroyCallback;
 
 public:
     ExecutableAllocator()
       : destroyCallback(NULL)
     {
         if (!pageSize) {
             pageSize = determinePageSize();
-            /*
-             * On Windows, VirtualAlloc effectively allocates in 64K chunks.
-             * (Technically, it allocates in page chunks, but the starting
-             * address is always a multiple of 64K, so each allocation uses up
-             * 64K of address space.)  So a size less than that would be
-             * pointless.  But it turns out that 64KB is a reasonable size for
-             * all platforms.  (This assumes 4KB pages.)
-             */
+            // On Windows, VirtualAlloc effectively allocates in 64K chunks.
+            // (Technically, it allocates in page chunks, but the starting
+            // address is always a multiple of 64K, so each allocation uses up
+            // 64K of address space.)  So a size less than that would be
+            // pointless.  But it turns out that 64KB is a reasonable size for
+            // all platforms.  (This assumes 4KB pages.) On 64-bit windows,
+            // AllocateExecutableMemory prepends an extra page for structured
+            // exception handling data (see comments in function) onto whatever
+            // is passed in, so subtract one page here.
+#if defined(JS_CPU_X64) && defined(XP_WIN)
+            largeAllocSize = pageSize * 15;
+#else
             largeAllocSize = pageSize * 16;
+#endif
         }
 
         MOZ_ASSERT(m_smallPools.empty());
     }
 
     ~ExecutableAllocator()
     {
         for (size_t i = 0; i < m_smallPools.length(); i++)
--- a/js/src/jit/ExecutableAllocatorWin.cpp
+++ b/js/src/jit/ExecutableAllocatorWin.cpp
@@ -20,22 +20,22 @@
  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
 #include "mozilla/WindowsVersion.h"
 
+#include "jsfriendapi.h"
 #include "jsmath.h"
+#include "jswin.h"
 
 #include "jit/ExecutableAllocator.h"
 
-#include "jswin.h"
-
 using namespace js::jit;
 
 uint64_t ExecutableAllocator::rngSeed;
 
 size_t ExecutableAllocator::determinePageSize()
 {
     SYSTEM_INFO system_info;
     GetSystemInfo(&system_info);
@@ -80,28 +80,154 @@ static bool
 RandomizeIsBroken()
 {
     // Use the compiler's intrinsic guards for |static type value = expr| to avoid some potential
     // races if runtimes are created from multiple threads.
     static int result = RandomizeIsBrokenImpl();
     return !!result;
 }
 
+#ifdef JS_CPU_X64
+static js::JitExceptionHandler sJitExceptionHandler;
+
+JS_FRIEND_API(void)
+js::SetJitExceptionHandler(JitExceptionHandler handler)
+{
+    MOZ_ASSERT(!sJitExceptionHandler);
+    sJitExceptionHandler = handler;
+}
+
+// From documentation for UNWIND_INFO on
+// http://msdn.microsoft.com/en-us/library/ddssxxy8.aspx
+struct UnwindInfo
+{
+    uint8_t version : 3;
+    uint8_t flags : 5;
+    uint8_t sizeOfPrologue;
+    uint8_t countOfUnwindCodes;
+    uint8_t frameRegister : 4;
+    uint8_t frameOffset : 4;
+    ULONG exceptionHandler;
+};
+
+static const unsigned ThunkLength = 12;
+
+struct ExceptionHandlerRecord
+{
+    RUNTIME_FUNCTION runtimeFunction;
+    UnwindInfo unwindInfo;
+    uint8_t thunk[ThunkLength];
+};
+
+// This function must match the function pointer type PEXCEPTION_HANDLER
+// mentioned in:
+//   http://msdn.microsoft.com/en-us/library/ssa62fwe.aspx.
+// This type is rather elusive in documentation; Wine is the best I've found:
+//   http://source.winehq.org/source/include/winnt.h
+static DWORD
+ExceptionHandler(PEXCEPTION_RECORD exceptionRecord, _EXCEPTION_REGISTRATION_RECORD *,
+                 PCONTEXT context, _EXCEPTION_REGISTRATION_RECORD **)
+{
+    return sJitExceptionHandler(exceptionRecord, context);
+}
+
+// For an explanation of the problem being solved here, see
+// SetJitExceptionFilter in jsfriendapi.h.
+static bool
+RegisterExecutableMemory(void *p, size_t bytes, size_t pageSize)
+{
+    ExceptionHandlerRecord *r = reinterpret_cast<ExceptionHandlerRecord*>(p);
+
+    // All these fields are specified to be offsets from the base of the
+    // executable code (which is 'p'), even if they have 'Address' in their
+    // names. In particular, exceptionHandler is a ULONG offset which is a
+    // 32-bit integer. Since 'p' can be farther than INT32_MAX away from
+    // sJitExceptionHandler, we must generate a little thunk inside the
+    // record. The record is put on its own page so that we can take away write
+    // access to protect against accidental clobbering.
+
+    r->runtimeFunction.BeginAddress = pageSize;
+    r->runtimeFunction.EndAddress = (DWORD)bytes;
+    r->runtimeFunction.UnwindData = offsetof(ExceptionHandlerRecord, unwindInfo);
+
+    r->unwindInfo.version = 1;
+    r->unwindInfo.flags = UNW_FLAG_EHANDLER;
+    r->unwindInfo.sizeOfPrologue = 0;
+    r->unwindInfo.countOfUnwindCodes = 0;
+    r->unwindInfo.frameRegister = 0;
+    r->unwindInfo.frameOffset = 0;
+    r->unwindInfo.exceptionHandler = offsetof(ExceptionHandlerRecord, thunk);
+
+    // mov imm64, rax
+    r->thunk[0]  = 0x48;
+    r->thunk[1]  = 0xb8;
+    void *handler = &ExceptionHandler;
+    memcpy(&r->thunk[2], &handler, 8);
+
+    // jmp rax
+    r->thunk[10] = 0xff;
+    r->thunk[11] = 0xe0;
+
+    DWORD oldProtect;
+    if (!VirtualProtect(p, pageSize, PAGE_EXECUTE_READ, &oldProtect))
+        return false;
+
+    return RtlAddFunctionTable(&r->runtimeFunction, 1, reinterpret_cast<DWORD64>(p));
+}
+
+static void
+UnregisterExecutableMemory(void *p, size_t bytes, size_t pageSize)
+{
+    ExceptionHandlerRecord *r = reinterpret_cast<ExceptionHandlerRecord*>(p);
+    RtlDeleteFunctionTable(&r->runtimeFunction);
+}
+#endif
+
 void *
 js::jit::AllocateExecutableMemory(void *addr, size_t bytes, unsigned permissions, const char *tag,
                                   size_t pageSize)
 {
     MOZ_ASSERT(bytes % pageSize == 0);
-    return VirtualAlloc(addr, bytes, MEM_COMMIT | MEM_RESERVE, permissions);
+    MOZ_ASSERT(permissions == PAGE_EXECUTE_READWRITE);
+
+#ifdef JS_CPU_X64
+    if (sJitExceptionHandler)
+        bytes += pageSize;
+#endif
+
+    void *p = VirtualAlloc(addr, bytes, MEM_COMMIT | MEM_RESERVE, permissions);
+    if (!p)
+        return nullptr;
+
+#ifdef JS_CPU_X64
+    if (sJitExceptionHandler) {
+        if (!RegisterExecutableMemory(p, bytes, pageSize)) {
+            VirtualFree(p, 0, MEM_RELEASE);
+            return nullptr;
+        }
+
+        p = (uint8_t*)p + pageSize;
+    }
+#endif
+
+    return p;
 }
 
 void
 js::jit::DeallocateExecutableMemory(void *addr, size_t bytes, size_t pageSize)
 {
     MOZ_ASSERT(bytes % pageSize == 0);
+
+#ifdef JS_CPU_X64
+    if (sJitExceptionHandler) {
+        addr = (uint8_t*)addr - pageSize;
+        UnregisterExecutableMemory(addr, bytes, pageSize);
+    }
+#endif
+
     VirtualFree(addr, 0, MEM_RELEASE);
 }
 
 ExecutablePool::Allocation ExecutableAllocator::systemAlloc(size_t n)
 {
     void *allocation = NULL;
     // Randomization disabled to avoid a performance fault on x64 builds.
     // See bug 728623.
--- a/js/src/jsfriendapi.h
+++ b/js/src/jsfriendapi.h
@@ -2661,16 +2661,44 @@ SetPropertyIgnoringNamedGetter(JSContext
 JS_FRIEND_API(void)
 ReportErrorWithId(JSContext *cx, const char *msg, JS::HandleId id);
 
 // This function is for one specific use case, please don't use this for anything else!
 extern JS_FRIEND_API(bool)
 ExecuteInGlobalAndReturnScope(JSContext *cx, JS::HandleObject obj, JS::HandleScript script,
                               JS::MutableHandleObject scope);
 
+#if defined(XP_WIN) && defined(_WIN64)
+// Parameters use void* types to avoid #including windows.h. The return value of
+// this function is returned from the exception handler.
+typedef long
+(*JitExceptionHandler)(void *exceptionRecord,  // PEXECTION_RECORD
+                       void *context);         // PCONTEXT
+
+// Windows uses "structured exception handling" to handle faults. When a fault
+// occurs, the stack is searched for a handler (similar to C++ exception
+// handling). If the search does not find a handler, the "unhandled exception
+// filter" is called. Breakpad uses the unhandled exception filter to do crash
+// reporting. Unfortunately, on Win64, JIT code on the stack completely throws
+// off this unwinding process and prevents the unhandled exception filter from
+// being called. The reason is that Win64 requires unwind information be
+// registered for all code regions and JIT code has none. While it is possible
+// to register full unwind information for JIT code, this is a lot of work (one
+// has to be able to recover the frame pointer at any PC) so instead we register
+// a handler for all JIT code that simply calls breakpad's unhandled exception
+// filter (which will perform crash reporting and then terminate the process).
+// This would be wrong if there was an outer __try block that expected to handle
+// the fault, but this is not generally allowed.
+//
+// Gecko must call SetJitExceptionFilter before any JIT code is compiled and
+// only once per process.
+extern JS_FRIEND_API(void)
+SetJitExceptionHandler(JitExceptionHandler handler);
+#endif
+
 } /* namespace js */
 
 extern JS_FRIEND_API(bool)
 js_DefineOwnProperty(JSContext *cx, JSObject *objArg, jsid idArg,
                      JS::Handle<JSPropertyDescriptor> descriptor, bool *bp);
 
 extern JS_FRIEND_API(bool)
 js_ReportIsNotFunction(JSContext *cx, JS::HandleValue v);