diff --git a/Source/JavaScriptCore/PlatformJSCOnly.cmake b/Source/JavaScriptCore/PlatformJSCOnly.cmake index 3456829c931ae..b38346399ed99 100644 --- a/Source/JavaScriptCore/PlatformJSCOnly.cmake +++ b/Source/JavaScriptCore/PlatformJSCOnly.cmake @@ -40,8 +40,10 @@ if (USE_LIBBACKTRACE) endif () if (WIN32) - # WasmDebugServer uses winsock functions + # WasmDebugServer uses winsock functions; + # ntdll for RtlAddGrowableFunctionTable (no kernel32 forwarder). list(APPEND JavaScriptCore_LIBRARIES + ntdll ws2_32 ) endif () diff --git a/Source/JavaScriptCore/jit/ExecutableAllocator.cpp b/Source/JavaScriptCore/jit/ExecutableAllocator.cpp index c9e3589bacaa4..cac6fb0d08625 100644 --- a/Source/JavaScriptCore/jit/ExecutableAllocator.cpp +++ b/Source/JavaScriptCore/jit/ExecutableAllocator.cpp @@ -57,6 +57,10 @@ WTF_ALLOW_UNSAFE_BUFFER_USAGE_END #include #endif +#if OS(WINDOWS) +#include +#endif + #if HAVE(IOS_JIT_RESTRICTIONS) || HAVE(MAC_JIT_RESTRICTIONS) #include #endif @@ -358,6 +362,230 @@ struct JITReservation { size_t size { 0 }; }; +#if OS(WINDOWS) && (CPU(X86_64) || CPU(ARM64)) + +// Register a dynamic function table covering the fixed JIT pool so that +// RtlLookupFunctionEntry / RtlVirtualUnwind / SEH dispatch can unwind through +// JIT frames. The RVAs in RUNTIME_FUNCTION and the unwind info are relative to +// the function-table base (the original pool base), so both the unwind info +// and the language-handler thunk must live inside the pool; the first page(s) +// of the reservation are carved out for that. +// +// The unwind info describes the uniform prologue that every JIT tier emits +// (AssemblyHelpers::emitFunctionPrologue and LLInt functionPrologue), so +// frame-pointer-chain unwinding is valid for the whole range. A UNW_FLAG_ +// EHANDLER language handler calls an embedder-settable callback, which lets a +// crash reporter observe an unhandled fault deterministically at the JIT +// boundary instead of relying on second-chance dispatch reaching the +// top-level filter (it cannot when the SEH walk derails). +// +// This mirrors V8's RegisterNonABICompliantCodeRange +// (src/diagnostics/unwinding-info-win64.cc) and SpiderMonkey's +// RegisterExecutableMemory (js/src/jit/ProcessExecutableMemory.cpp). +// RtlAddGrowableFunctionTable rather than RtlAddFunctionTable so that +// out-of-process stack walkers (ETW, WPA, WinDbg) see the entry too. + +static Atomic g_jitSEHFunctionTable { nullptr }; +static Atomic g_jitSEHCallback { nullptr }; + +static EXCEPTION_DISPOSITION jscJITSEHHandler(PEXCEPTION_RECORD exceptionRecord, PVOID establisherFrame, PCONTEXT contextRecord, PDISPATCHER_CONTEXT dispatcherContext) +{ + if (auto callback = g_jitSEHCallback.loadRelaxed()) + return static_cast(callback(exceptionRecord, establisherFrame, contextRecord, dispatcherContext)); + return ExceptionContinueSearch; +} + +#if CPU(X86_64) + +// https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64 +// UNWIND_INFO is not in winnt.h; the format is fixed by the platform ABI. +#pragma pack(push, 1) +struct JITUnwindRecord { + RUNTIME_FUNCTION runtimeFunction; + struct { + uint8_t versionAndFlags; + uint8_t sizeOfProlog; + uint8_t countOfCodes; + uint8_t frameRegisterAndOffset; + uint16_t unwindCodes[2]; + uint32_t exceptionHandlerRVA; + } unwindInfo; + alignas(16) uint8_t thunk[16]; +}; +#pragma pack(pop) +static_assert(!(offsetof(JITUnwindRecord, unwindInfo) % sizeof(uint32_t)), "UNWIND_INFO must be DWORD-aligned"); + +static void registerJITUnwindInfo(PageReservation& pageReservation, void*& base, size_t& size) +{ + size_t pageSize = executablePageSize(); + if (size <= pageSize * 2 || size > UINT32_MAX) + return; + + void* recordBase = base; + pageReservation.commit(recordBase, pageSize); + + auto* record = new (recordBase) JITUnwindRecord(); + record->runtimeFunction.BeginAddress = static_cast(pageSize); + record->runtimeFunction.EndAddress = static_cast(size); + record->runtimeFunction.UnwindData = static_cast(offsetof(JITUnwindRecord, unwindInfo)); + + // emitFunctionPrologue: push rbp (1 byte); mov rbp, rsp (3 bytes). + // UWOP_PUSH_NONVOL = 0, UWOP_SET_FPREG = 3; rbp = 5. Codes are emitted in + // reverse prologue order. + constexpr uint8_t prologSize = 4; + auto unwindCode = [](uint8_t codeOffset, uint8_t op, uint8_t opInfo) -> uint16_t { + return static_cast(codeOffset) | (static_cast(op | (opInfo << 4)) << 8); + }; + record->unwindInfo.versionAndFlags = 1 | (UNW_FLAG_EHANDLER << 3); + record->unwindInfo.sizeOfProlog = prologSize; + record->unwindInfo.countOfCodes = 2; + record->unwindInfo.frameRegisterAndOffset = 5; + record->unwindInfo.unwindCodes[0] = unwindCode(prologSize, 3, 0); + record->unwindInfo.unwindCodes[1] = unwindCode(1, 0, 5); + record->unwindInfo.exceptionHandlerRVA = static_cast(offsetof(JITUnwindRecord, thunk)); + + // mov rax, imm64; jmp rax + uint8_t* thunk = record->thunk; + thunk[0] = 0x48; + thunk[1] = 0xB8; + *reinterpret_cast(thunk + 2) = reinterpret_cast(&jscJITSEHHandler); + thunk[10] = 0xFF; + thunk[11] = 0xE0; + + FlushInstructionCache(GetCurrentProcess(), recordBase, pageSize); + + // RtlAddGrowableFunctionTable writes into the region, so write-protect + // only after it returns. + void* dynamicTable = nullptr; + DWORD result = RtlAddGrowableFunctionTable(&dynamicTable, &record->runtimeFunction, 1, 1, reinterpret_cast(recordBase), reinterpret_cast(recordBase) + size); + if (result) + return; + + DWORD oldProtect; + VirtualProtect(recordBase, pageSize, PAGE_EXECUTE_READ, &oldProtect); + + g_jitSEHFunctionTable.storeRelaxed(dynamicTable); + base = static_cast(base) + pageSize; + size -= pageSize; +} + +#elif CPU(ARM64) + +// https://learn.microsoft.com/en-us/cpp/build/arm64-exception-handling +// The .xdata header's FunctionLength is 18 bits of instruction count (<< 2 for +// bytes), so one RUNTIME_FUNCTION can cover at most ~1 MB and the 512 MB pool +// needs an array of entries. All full-size chunks share one .xdata; a tail +// chunk with a shorter FunctionLength uses a second one. +static constexpr size_t arm64MaxFunctionLength = ((1u << 18) - 1) << 2; + +// emitFunctionPrologue: stp fp, lr, [sp, #-16]!; mov fp, sp (see +// MacroAssemblerARM64::pushPair / prologueStackPointerDelta()). Encoded as +// set_fp (0xE1), save_fplr_x Z (0x80 | Z where -(Z+1)*8 = offset), end +// (0xE4), nop (0xE3) padding. +static constexpr uint32_t arm64JITUnwindCodes() +{ + constexpr int offset = -16; + static_assert(offset <= -8 && offset >= -512 && !(offset & 7)); + uint8_t saveFpLrX = 0x80 | static_cast((-offset >> 3) - 1); + return 0xE1u | (static_cast(saveFpLrX) << 8) | (0xE4u << 16) | (0xE3u << 24); +} + +static constexpr uint32_t arm64XdataHeader(size_t functionLengthBytes) +{ + // FunctionLength:18 | Version:2=0 | X:1=1 | E:1=0 | EpilogCount:5=0 | CodeWords:5=1 + return static_cast(functionLengthBytes >> 2) | (1u << 20) | (1u << 27); +} + +#pragma pack(push, 1) +struct JITUnwindHeader { + struct { + uint32_t header; + uint32_t unwindCodes; + uint32_t exceptionHandlerRVA; + } unwindInfoFull, unwindInfoTail; + alignas(16) uint8_t thunk[16]; +}; +#pragma pack(pop) +static_assert(!(offsetof(JITUnwindHeader, unwindInfoFull) % sizeof(uint32_t))); +static_assert(!(offsetof(JITUnwindHeader, unwindInfoTail) % sizeof(uint32_t))); + +static void registerJITUnwindInfo(PageReservation& pageReservation, void*& base, size_t& size) +{ + size_t pageSize = executablePageSize(); + if (size <= pageSize * 2 || size > UINT32_MAX) + return; + + size_t maxEntries = (size + arm64MaxFunctionLength - 1) / arm64MaxFunctionLength; + size_t recordSize = roundUpToMultipleOf(pageSize, sizeof(JITUnwindHeader) + maxEntries * sizeof(RUNTIME_FUNCTION)); + if (size <= recordSize + pageSize) + return; + + void* recordBase = base; + pageReservation.commit(recordBase, recordSize); + + auto* header = new (recordBase) JITUnwindHeader(); + header->unwindInfoFull.header = arm64XdataHeader(arm64MaxFunctionLength); + header->unwindInfoFull.unwindCodes = arm64JITUnwindCodes(); + header->unwindInfoFull.exceptionHandlerRVA = static_cast(offsetof(JITUnwindHeader, thunk)); + header->unwindInfoTail.unwindCodes = arm64JITUnwindCodes(); + header->unwindInfoTail.exceptionHandlerRVA = static_cast(offsetof(JITUnwindHeader, thunk)); + + // ldr x16, #8; br x16; .quad jscJITSEHHandler + uint32_t* thunk = reinterpret_cast(header->thunk); + thunk[0] = 0x58000050; // LDR (literal) x16, 8 + thunk[1] = 0xD61F0200; // BR x16 + *reinterpret_cast(thunk + 2) = reinterpret_cast(&jscJITSEHHandler); + + auto* entries = reinterpret_cast(static_cast(recordBase) + sizeof(JITUnwindHeader)); + size_t codeSize = size - recordSize; + size_t remaining = codeSize; + uint32_t begin = static_cast(recordSize); + DWORD entryCount = 0; + while (remaining > arm64MaxFunctionLength) { + entries[entryCount].BeginAddress = begin; + entries[entryCount].UnwindData = static_cast(offsetof(JITUnwindHeader, unwindInfoFull)); + begin += static_cast(arm64MaxFunctionLength); + remaining -= arm64MaxFunctionLength; + entryCount++; + } + RELEASE_ASSERT(entryCount <= maxEntries); + if (remaining) { + header->unwindInfoTail.header = arm64XdataHeader(remaining); + entries[entryCount].BeginAddress = begin; + entries[entryCount].UnwindData = static_cast(offsetof(JITUnwindHeader, unwindInfoTail)); + entryCount++; + } + RELEASE_ASSERT(entryCount <= maxEntries); + + FlushInstructionCache(GetCurrentProcess(), recordBase, recordSize); + + // RtlAddGrowableFunctionTable writes into the region, so write-protect + // only after it returns. + void* dynamicTable = nullptr; + DWORD result = RtlAddGrowableFunctionTable(&dynamicTable, entries, entryCount, entryCount, reinterpret_cast(recordBase), reinterpret_cast(recordBase) + size); + if (result) + return; + + DWORD oldProtect; + VirtualProtect(recordBase, recordSize, PAGE_EXECUTE_READ, &oldProtect); + + g_jitSEHFunctionTable.storeRelaxed(dynamicTable); + base = static_cast(base) + recordSize; + size -= recordSize; +} + +#endif + +// LLInt / vmEntryToJavaScript live in image .text with no .pdata of their own +// (offlineasm emits no .seh_* directives). A dynamic function table cannot +// cover those: RtlLookupFunctionEntry for a PC inside a loaded module consults +// only that module's static .pdata. V8 solves this at build time by emitting +// .pdata/.xdata for its embedded builtins (platform-embedded-file-writer- +// win.cc); the JSC equivalent is offlineasm emitting .seh_* directives, which +// is a separate change. + +#endif // OS(WINDOWS) && (CPU(X86_64) || CPU(ARM64)) + WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN static ALWAYS_INLINE JITReservation initializeJITPageReservation() @@ -429,6 +657,10 @@ static ALWAYS_INLINE JITReservation initializeJITPageReservation() } #endif +#if OS(WINDOWS) && (CPU(X86_64) || CPU(ARM64)) + registerJITUnwindInfo(reservation.pageReservation, reservation.base, reservation.size); +#endif + void* reservationEnd = static_cast(reservation.base) + reservation.size; g_jscConfig.startExecutableMemory = reservation.base; g_jscConfig.endExecutableMemory = reservationEnd; @@ -1319,6 +1551,18 @@ void* endOfFixedExecutableMemoryPoolImpl() return allocator->memoryEnd(); } +#if OS(WINDOWS) && (CPU(X86_64) || CPU(ARM64)) +void setJITExceptionHandlerWin(JITExceptionHandlerWin callback) +{ + g_jitSEHCallback.storeRelaxed(callback); +} + +bool hasJITUnwindInfoWin() +{ + return g_jitSEHFunctionTable.loadRelaxed(); +} +#endif + void dumpJITMemory(const void* dst, const void* src, size_t size) { RELEASE_ASSERT(Options::dumpJITMemoryPath()); diff --git a/Source/JavaScriptCore/jit/ExecutableAllocator.h b/Source/JavaScriptCore/jit/ExecutableAllocator.h index a50106303d3ae..6eb54000eecc1 100644 --- a/Source/JavaScriptCore/jit/ExecutableAllocator.h +++ b/Source/JavaScriptCore/jit/ExecutableAllocator.h @@ -97,6 +97,16 @@ class ExecutableAllocatorBase { JS_EXPORT_PRIVATE void* NODELETE startOfFixedExecutableMemoryPoolImpl(); JS_EXPORT_PRIVATE void* NODELETE endOfFixedExecutableMemoryPoolImpl(); +#if OS(WINDOWS) && (CPU(X86_64) || CPU(ARM64)) +// Set the language-specific SEH handler invoked when exception dispatch +// reaches a JIT frame. See registerJITUnwindInfo in ExecutableAllocator.cpp. +// Signature: EXCEPTION_DISPOSITION(PEXCEPTION_RECORD, PVOID establisherFrame, +// PCONTEXT, PDISPATCHER_CONTEXT). +using JITExceptionHandlerWin = long(__cdecl*)(void*, void*, void*, void*); +JS_EXPORT_PRIVATE void setJITExceptionHandlerWin(JITExceptionHandlerWin); +JS_EXPORT_PRIVATE bool hasJITUnwindInfoWin(); +#endif + template T startOfFixedExecutableMemoryPool() {