From 54c641b6ce1a028fb28dcb21a8df295e6a7262c8 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:01:11 +0000 Subject: [PATCH 1/8] JSC(win-x64): register RtlAddFunctionTable unwind info for the fixed JIT pool On Windows x64 JavaScriptCore's JIT code has no RUNTIME_FUNCTION entries, so RtlLookupFunctionEntry returns NULL for any JIT PC and frame-based SEH dispatch cannot unwind through a JIT frame. This breaks RtlCaptureStackBackTrace at the first JIT frame and makes SetUnhandledExceptionFilter unreachable for faults under JIT. Reserve the first page of the fixed executable pool for a single RUNTIME_FUNCTION + UNWIND_INFO covering the rest of the pool and register it with RtlAddFunctionTable. The unwind codes describe the push rbp; mov rbp, rsp prologue every JIT tier emits (AssemblyHelpers::emitFunctionPrologue and LLInt functionPrologue), so rbp-chain unwinding is valid for the whole range. An EHANDLER thunk jumps to jscJITSEHHandler, which by default returns ExceptionContinueSearch; embedders can install a language-specific handler via JSC::setJITExceptionHandlerWin. This mirrors V8's RegisterNonABICompliantCodeRange (src/diagnostics/unwinding-info-win64.cc). --- .../jit/ExecutableAllocator.cpp | 116 ++++++++++++++++++ .../JavaScriptCore/jit/ExecutableAllocator.h | 10 ++ 2 files changed, 126 insertions(+) diff --git a/Source/JavaScriptCore/jit/ExecutableAllocator.cpp b/Source/JavaScriptCore/jit/ExecutableAllocator.cpp index c9e3589bacaa4..b2a02d01d8296 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,102 @@ struct JITReservation { size_t size { 0 }; }; +#if OS(WINDOWS) && CPU(X86_64) + +// Language-specific SEH handler for the JIT code range. The OS calls it during +// exception dispatch when the faulting/unwinding frame's PC is inside the +// fixed executable memory pool (via the RUNTIME_FUNCTION registered below). +// It lets the embedder observe exceptions that reach a JIT frame; by default +// it just continues the search so the unwind codes take over. +static std::atomic g_jitSEHFunctionTable { nullptr }; +static std::atomic g_jitSEHCallback { nullptr }; + +static EXCEPTION_DISPOSITION jscJITSEHHandler(PEXCEPTION_RECORD exceptionRecord, PVOID establisherFrame, PCONTEXT contextRecord, PDISPATCHER_CONTEXT dispatcherContext) +{ + if (auto callback = g_jitSEHCallback.load(std::memory_order_acquire)) + return static_cast(callback(exceptionRecord, establisherFrame, contextRecord, dispatcherContext)); + return ExceptionContinueSearch; +} + +// Layout of the first page of the reservation. RVAs in RUNTIME_FUNCTION and +// UNWIND_INFO are relative to the function-table base (the original pool base), +// so both the unwind info and the handler thunk must live inside the pool; +// the thunk is a 12-byte absolute jump to jscJITSEHHandler in the image. +#pragma pack(push, 1) +struct JITUnwindRecord { + RUNTIME_FUNCTION runtimeFunction; + struct { + uint8_t versionAndFlags; // Version(3) | Flags(5) + 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) == 0, "UNWIND_INFO must be DWORD-aligned"); +static_assert(sizeof(JITUnwindRecord) <= 64, "record should fit well inside one page"); + +static constexpr uint8_t kUnwFlagEHandler = 0x1; +static constexpr uint8_t kUwopPushNonvol = 0; +static constexpr uint8_t kUwopSetFpreg = 3; +static constexpr uint8_t kRbp = 5; +// push rbp (1 byte) + mov rbp, rsp (3 bytes) +static constexpr uint8_t kPrologSize = 4; + +static inline uint16_t unwindCode(uint8_t codeOffset, uint8_t op, uint8_t info) +{ + return static_cast(codeOffset) | (static_cast(op | (info << 4)) << 8); +} + +static void registerJITUnwindInfo(PageReservation& pageReservation, void*& base, size_t& size) +{ + size_t pageSize = executablePageSize(); + if (size <= pageSize * 2 || size > UINT32_MAX) + return; + + void* recordPage = base; + pageReservation.commit(recordPage, pageSize); + + auto* record = new (recordPage) JITUnwindRecord(); + record->runtimeFunction.BeginAddress = static_cast(pageSize); + record->runtimeFunction.EndAddress = static_cast(size); + record->runtimeFunction.UnwindData = static_cast(offsetof(JITUnwindRecord, unwindInfo)); + + // Every JIT entry prologue is `push rbp; mov rbp, rsp` + // (AssemblyHelpers::emitFunctionPrologue / LLInt functionPrologue), so + // rbp-chain unwinding is valid for the whole range. + record->unwindInfo.versionAndFlags = 1 | (kUnwFlagEHandler << 3); + record->unwindInfo.sizeOfProlog = kPrologSize; + record->unwindInfo.countOfCodes = 2; + record->unwindInfo.frameRegisterAndOffset = kRbp; // FrameOffset = 0 + record->unwindInfo.unwindCodes[0] = unwindCode(kPrologSize, kUwopSetFpreg, 0); + record->unwindInfo.unwindCodes[1] = unwindCode(1, kUwopPushNonvol, kRbp); + record->unwindInfo.exceptionHandlerRva = static_cast(offsetof(JITUnwindRecord, thunk)); + + // mov rax, imm64; jmp rax + uint8_t* t = record->thunk; + t[0] = 0x48; t[1] = 0xB8; + *reinterpret_cast(t + 2) = reinterpret_cast(&jscJITSEHHandler); + t[10] = 0xFF; t[11] = 0xE0; + + DWORD oldProtect; + VirtualProtect(recordPage, pageSize, PAGE_EXECUTE_READ, &oldProtect); + FlushInstructionCache(GetCurrentProcess(), recordPage, pageSize); + + if (!RtlAddFunctionTable(&record->runtimeFunction, 1, reinterpret_cast(recordPage))) + return; + + g_jitSEHFunctionTable.store(&record->runtimeFunction, std::memory_order_release); + base = static_cast(base) + pageSize; + size -= pageSize; +} + +#endif // OS(WINDOWS) && CPU(X86_64) + WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN static ALWAYS_INLINE JITReservation initializeJITPageReservation() @@ -429,6 +529,10 @@ static ALWAYS_INLINE JITReservation initializeJITPageReservation() } #endif +#if OS(WINDOWS) && CPU(X86_64) + 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 +1423,18 @@ void* endOfFixedExecutableMemoryPoolImpl() return allocator->memoryEnd(); } +#if OS(WINDOWS) && CPU(X86_64) +void setJITExceptionHandlerWin(JITExceptionHandlerWin callback) +{ + g_jitSEHCallback.store(callback, std::memory_order_release); +} + +bool hasJITUnwindInfoWin() +{ + return g_jitSEHFunctionTable.load(std::memory_order_acquire); +} +#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..1845ee4a52188 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) +// 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() { From 6c7132d5e2bd753c2b1228a9fcdd83a7eca97db0 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:08:56 +0000 Subject: [PATCH 2/8] JSC(win): add ARM64 unwind-info path and use WebKit-style naming ARM64 .xdata encodes FunctionLength in 18 bits of instruction count, so one RUNTIME_FUNCTION covers at most ~1 MB; the 512 MB pool needs an entry per chunk. All full-size chunks share one .xdata, the tail chunk a second. The unwind codes describe stp fp, lr, [sp, #-16]!; mov fp, sp (set_fp, save_fplr_x 1, end). The handler thunk is ldr x16, #8; br x16; .quad jscJITSEHHandler. Also drops the Chromium-style k-prefixed constants in the x64 path in favour of the UNW_FLAG_EHANDLER winnt.h macro and inline values. --- .../jit/ExecutableAllocator.cpp | 205 +++++++++++++----- .../JavaScriptCore/jit/ExecutableAllocator.h | 2 +- 2 files changed, 157 insertions(+), 50 deletions(-) diff --git a/Source/JavaScriptCore/jit/ExecutableAllocator.cpp b/Source/JavaScriptCore/jit/ExecutableAllocator.cpp index b2a02d01d8296..ccb184a9f0bae 100644 --- a/Source/JavaScriptCore/jit/ExecutableAllocator.cpp +++ b/Source/JavaScriptCore/jit/ExecutableAllocator.cpp @@ -362,13 +362,26 @@ struct JITReservation { size_t size { 0 }; }; -#if OS(WINDOWS) && CPU(X86_64) +#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). -// Language-specific SEH handler for the JIT code range. The OS calls it during -// exception dispatch when the faulting/unwinding frame's PC is inside the -// fixed executable memory pool (via the RUNTIME_FUNCTION registered below). -// It lets the embedder observe exceptions that reach a JIT frame; by default -// it just continues the search so the unwind codes take over. static std::atomic g_jitSEHFunctionTable { nullptr }; static std::atomic g_jitSEHCallback { nullptr }; @@ -379,39 +392,25 @@ static EXCEPTION_DISPOSITION jscJITSEHHandler(PEXCEPTION_RECORD exceptionRecord, return ExceptionContinueSearch; } -// Layout of the first page of the reservation. RVAs in RUNTIME_FUNCTION and -// UNWIND_INFO are relative to the function-table base (the original pool base), -// so both the unwind info and the handler thunk must live inside the pool; -// the thunk is a 12-byte absolute jump to jscJITSEHHandler in the image. +#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; // Version(3) | Flags(5) + uint8_t versionAndFlags; uint8_t sizeOfProlog; uint8_t countOfCodes; uint8_t frameRegisterAndOffset; uint16_t unwindCodes[2]; - uint32_t exceptionHandlerRva; + uint32_t exceptionHandlerRVA; } unwindInfo; alignas(16) uint8_t thunk[16]; }; #pragma pack(pop) - -static_assert(offsetof(JITUnwindRecord, unwindInfo) % sizeof(uint32_t) == 0, "UNWIND_INFO must be DWORD-aligned"); -static_assert(sizeof(JITUnwindRecord) <= 64, "record should fit well inside one page"); - -static constexpr uint8_t kUnwFlagEHandler = 0x1; -static constexpr uint8_t kUwopPushNonvol = 0; -static constexpr uint8_t kUwopSetFpreg = 3; -static constexpr uint8_t kRbp = 5; -// push rbp (1 byte) + mov rbp, rsp (3 bytes) -static constexpr uint8_t kPrologSize = 4; - -static inline uint16_t unwindCode(uint8_t codeOffset, uint8_t op, uint8_t info) -{ - return static_cast(codeOffset) | (static_cast(op | (info << 4)) << 8); -} +static_assert(!(offsetof(JITUnwindRecord, unwindInfo) % sizeof(uint32_t)), "UNWIND_INFO must be DWORD-aligned"); static void registerJITUnwindInfo(PageReservation& pageReservation, void*& base, size_t& size) { @@ -419,36 +418,42 @@ static void registerJITUnwindInfo(PageReservation& pageReservation, void*& base, if (size <= pageSize * 2 || size > UINT32_MAX) return; - void* recordPage = base; - pageReservation.commit(recordPage, pageSize); + void* recordBase = base; + pageReservation.commit(recordBase, pageSize); - auto* record = new (recordPage) JITUnwindRecord(); + 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)); - // Every JIT entry prologue is `push rbp; mov rbp, rsp` - // (AssemblyHelpers::emitFunctionPrologue / LLInt functionPrologue), so - // rbp-chain unwinding is valid for the whole range. - record->unwindInfo.versionAndFlags = 1 | (kUnwFlagEHandler << 3); - record->unwindInfo.sizeOfProlog = kPrologSize; + // 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 = kRbp; // FrameOffset = 0 - record->unwindInfo.unwindCodes[0] = unwindCode(kPrologSize, kUwopSetFpreg, 0); - record->unwindInfo.unwindCodes[1] = unwindCode(1, kUwopPushNonvol, kRbp); - record->unwindInfo.exceptionHandlerRva = static_cast(offsetof(JITUnwindRecord, thunk)); + 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* t = record->thunk; - t[0] = 0x48; t[1] = 0xB8; - *reinterpret_cast(t + 2) = reinterpret_cast(&jscJITSEHHandler); - t[10] = 0xFF; t[11] = 0xE0; + uint8_t* thunk = record->thunk; + thunk[0] = 0x48; + thunk[1] = 0xB8; + *reinterpret_cast(thunk + 2) = reinterpret_cast(&jscJITSEHHandler); + thunk[10] = 0xFF; + thunk[11] = 0xE0; DWORD oldProtect; - VirtualProtect(recordPage, pageSize, PAGE_EXECUTE_READ, &oldProtect); - FlushInstructionCache(GetCurrentProcess(), recordPage, pageSize); + VirtualProtect(recordBase, pageSize, PAGE_EXECUTE_READ, &oldProtect); + FlushInstructionCache(GetCurrentProcess(), recordBase, pageSize); - if (!RtlAddFunctionTable(&record->runtimeFunction, 1, reinterpret_cast(recordPage))) + if (!RtlAddFunctionTable(&record->runtimeFunction, 1, reinterpret_cast(recordBase))) return; g_jitSEHFunctionTable.store(&record->runtimeFunction, std::memory_order_release); @@ -456,7 +461,109 @@ static void registerJITUnwindInfo(PageReservation& pageReservation, void*& base, size -= pageSize; } -#endif // OS(WINDOWS) && CPU(X86_64) +#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); + + DWORD oldProtect; + VirtualProtect(recordBase, recordSize, PAGE_EXECUTE_READ, &oldProtect); + FlushInstructionCache(GetCurrentProcess(), recordBase, recordSize); + + if (!RtlAddFunctionTable(entries, entryCount, reinterpret_cast(recordBase))) + return; + + g_jitSEHFunctionTable.store(entries, std::memory_order_release); + base = static_cast(base) + recordSize; + size -= recordSize; +} + +#endif + +#endif // OS(WINDOWS) && (CPU(X86_64) || CPU(ARM64)) WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN @@ -529,7 +636,7 @@ static ALWAYS_INLINE JITReservation initializeJITPageReservation() } #endif -#if OS(WINDOWS) && CPU(X86_64) +#if OS(WINDOWS) && (CPU(X86_64) || CPU(ARM64)) registerJITUnwindInfo(reservation.pageReservation, reservation.base, reservation.size); #endif @@ -1423,7 +1530,7 @@ void* endOfFixedExecutableMemoryPoolImpl() return allocator->memoryEnd(); } -#if OS(WINDOWS) && CPU(X86_64) +#if OS(WINDOWS) && (CPU(X86_64) || CPU(ARM64)) void setJITExceptionHandlerWin(JITExceptionHandlerWin callback) { g_jitSEHCallback.store(callback, std::memory_order_release); diff --git a/Source/JavaScriptCore/jit/ExecutableAllocator.h b/Source/JavaScriptCore/jit/ExecutableAllocator.h index 1845ee4a52188..6eb54000eecc1 100644 --- a/Source/JavaScriptCore/jit/ExecutableAllocator.h +++ b/Source/JavaScriptCore/jit/ExecutableAllocator.h @@ -97,7 +97,7 @@ class ExecutableAllocatorBase { JS_EXPORT_PRIVATE void* NODELETE startOfFixedExecutableMemoryPoolImpl(); JS_EXPORT_PRIVATE void* NODELETE endOfFixedExecutableMemoryPoolImpl(); -#if OS(WINDOWS) && CPU(X86_64) +#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, From 2646b6b9c981d0ef7d933ec5a7b94045bfdf316d Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:13:34 +0000 Subject: [PATCH 3/8] JSC(win): use memory_order_relaxed for the JIT SEH globals Both are set once during single-threaded JSC::initialize() before any thread that could read them exists; matches the m_bytesAllocated pattern in this file. --- Source/JavaScriptCore/jit/ExecutableAllocator.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/JavaScriptCore/jit/ExecutableAllocator.cpp b/Source/JavaScriptCore/jit/ExecutableAllocator.cpp index ccb184a9f0bae..e9edd35783ccd 100644 --- a/Source/JavaScriptCore/jit/ExecutableAllocator.cpp +++ b/Source/JavaScriptCore/jit/ExecutableAllocator.cpp @@ -387,7 +387,7 @@ static std::atomic g_jitSEHCallback { nullptr }; static EXCEPTION_DISPOSITION jscJITSEHHandler(PEXCEPTION_RECORD exceptionRecord, PVOID establisherFrame, PCONTEXT contextRecord, PDISPATCHER_CONTEXT dispatcherContext) { - if (auto callback = g_jitSEHCallback.load(std::memory_order_acquire)) + if (auto callback = g_jitSEHCallback.load(std::memory_order_relaxed)) return static_cast(callback(exceptionRecord, establisherFrame, contextRecord, dispatcherContext)); return ExceptionContinueSearch; } @@ -456,7 +456,7 @@ static void registerJITUnwindInfo(PageReservation& pageReservation, void*& base, if (!RtlAddFunctionTable(&record->runtimeFunction, 1, reinterpret_cast(recordBase))) return; - g_jitSEHFunctionTable.store(&record->runtimeFunction, std::memory_order_release); + g_jitSEHFunctionTable.store(&record->runtimeFunction, std::memory_order_relaxed); base = static_cast(base) + pageSize; size -= pageSize; } @@ -556,7 +556,7 @@ static void registerJITUnwindInfo(PageReservation& pageReservation, void*& base, if (!RtlAddFunctionTable(entries, entryCount, reinterpret_cast(recordBase))) return; - g_jitSEHFunctionTable.store(entries, std::memory_order_release); + g_jitSEHFunctionTable.store(entries, std::memory_order_relaxed); base = static_cast(base) + recordSize; size -= recordSize; } @@ -1533,12 +1533,12 @@ void* endOfFixedExecutableMemoryPoolImpl() #if OS(WINDOWS) && (CPU(X86_64) || CPU(ARM64)) void setJITExceptionHandlerWin(JITExceptionHandlerWin callback) { - g_jitSEHCallback.store(callback, std::memory_order_release); + g_jitSEHCallback.store(callback, std::memory_order_relaxed); } bool hasJITUnwindInfoWin() { - return g_jitSEHFunctionTable.load(std::memory_order_acquire); + return g_jitSEHFunctionTable.load(std::memory_order_relaxed); } #endif From cadde6ac64c71f0f4fa977b18c70f9cf2f9c84ce Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:15:47 +0000 Subject: [PATCH 4/8] JSC(win): use WTF::Atomic instead of std::atomic --- Source/JavaScriptCore/jit/ExecutableAllocator.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Source/JavaScriptCore/jit/ExecutableAllocator.cpp b/Source/JavaScriptCore/jit/ExecutableAllocator.cpp index e9edd35783ccd..ab5499ab32506 100644 --- a/Source/JavaScriptCore/jit/ExecutableAllocator.cpp +++ b/Source/JavaScriptCore/jit/ExecutableAllocator.cpp @@ -382,12 +382,12 @@ struct JITReservation { // This mirrors V8's RegisterNonABICompliantCodeRange // (src/diagnostics/unwinding-info-win64.cc). -static std::atomic g_jitSEHFunctionTable { nullptr }; -static std::atomic g_jitSEHCallback { nullptr }; +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.load(std::memory_order_relaxed)) + if (auto callback = g_jitSEHCallback.loadRelaxed()) return static_cast(callback(exceptionRecord, establisherFrame, contextRecord, dispatcherContext)); return ExceptionContinueSearch; } @@ -456,7 +456,7 @@ static void registerJITUnwindInfo(PageReservation& pageReservation, void*& base, if (!RtlAddFunctionTable(&record->runtimeFunction, 1, reinterpret_cast(recordBase))) return; - g_jitSEHFunctionTable.store(&record->runtimeFunction, std::memory_order_relaxed); + g_jitSEHFunctionTable.storeRelaxed(&record->runtimeFunction); base = static_cast(base) + pageSize; size -= pageSize; } @@ -556,7 +556,7 @@ static void registerJITUnwindInfo(PageReservation& pageReservation, void*& base, if (!RtlAddFunctionTable(entries, entryCount, reinterpret_cast(recordBase))) return; - g_jitSEHFunctionTable.store(entries, std::memory_order_relaxed); + g_jitSEHFunctionTable.storeRelaxed(entries); base = static_cast(base) + recordSize; size -= recordSize; } @@ -1533,12 +1533,12 @@ void* endOfFixedExecutableMemoryPoolImpl() #if OS(WINDOWS) && (CPU(X86_64) || CPU(ARM64)) void setJITExceptionHandlerWin(JITExceptionHandlerWin callback) { - g_jitSEHCallback.store(callback, std::memory_order_relaxed); + g_jitSEHCallback.storeRelaxed(callback); } bool hasJITUnwindInfoWin() { - return g_jitSEHFunctionTable.load(std::memory_order_relaxed); + return g_jitSEHFunctionTable.loadRelaxed(); } #endif From a6d1712b41f791e0f884e93de8840db9f1cd2fb2 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 22 Jul 2026 19:24:05 +0000 Subject: [PATCH 5/8] JSC(win): use RtlAddGrowableFunctionTable so out-of-process walkers see the entry RtlAddFunctionTable works for in-process SEH and RtlVirtualUnwind, but out-of-process stack walkers (ETW, WPA, WinDbg) read the growable-table list from the target process. This is why V8's RtlAddFunctionTable path is the 'exception handling only' Win7 fallback and SpiderMonkey's RegisterExecutableMemory (js/src/jit/ProcessExecutableMemory.cpp) calls the growable API under the comment 'Required for enabling Stackwalking on windows using external tools'. RtlAddGrowableFunctionTable writes into the record region, so write-protect only after it returns. --- .../jit/ExecutableAllocator.cpp | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/Source/JavaScriptCore/jit/ExecutableAllocator.cpp b/Source/JavaScriptCore/jit/ExecutableAllocator.cpp index ab5499ab32506..4a833a9e15cf7 100644 --- a/Source/JavaScriptCore/jit/ExecutableAllocator.cpp +++ b/Source/JavaScriptCore/jit/ExecutableAllocator.cpp @@ -380,9 +380,12 @@ struct JITReservation { // top-level filter (it cannot when the SEH walk derails). // // This mirrors V8's RegisterNonABICompliantCodeRange -// (src/diagnostics/unwinding-info-win64.cc). +// (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_jitSEHFunctionTable { nullptr }; static Atomic g_jitSEHCallback { nullptr }; static EXCEPTION_DISPOSITION jscJITSEHHandler(PEXCEPTION_RECORD exceptionRecord, PVOID establisherFrame, PCONTEXT contextRecord, PDISPATCHER_CONTEXT dispatcherContext) @@ -449,14 +452,19 @@ static void registerJITUnwindInfo(PageReservation& pageReservation, void*& base, thunk[10] = 0xFF; thunk[11] = 0xE0; - DWORD oldProtect; - VirtualProtect(recordBase, pageSize, PAGE_EXECUTE_READ, &oldProtect); FlushInstructionCache(GetCurrentProcess(), recordBase, pageSize); - if (!RtlAddFunctionTable(&record->runtimeFunction, 1, reinterpret_cast(recordBase))) + // 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; - g_jitSEHFunctionTable.storeRelaxed(&record->runtimeFunction); + DWORD oldProtect; + VirtualProtect(recordBase, pageSize, PAGE_EXECUTE_READ, &oldProtect); + + g_jitSEHFunctionTable.storeRelaxed(dynamicTable); base = static_cast(base) + pageSize; size -= pageSize; } @@ -549,14 +557,19 @@ static void registerJITUnwindInfo(PageReservation& pageReservation, void*& base, } RELEASE_ASSERT(entryCount <= maxEntries); - DWORD oldProtect; - VirtualProtect(recordBase, recordSize, PAGE_EXECUTE_READ, &oldProtect); FlushInstructionCache(GetCurrentProcess(), recordBase, recordSize); - if (!RtlAddFunctionTable(entries, entryCount, reinterpret_cast(recordBase))) + // 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; - g_jitSEHFunctionTable.storeRelaxed(entries); + DWORD oldProtect; + VirtualProtect(recordBase, recordSize, PAGE_EXECUTE_READ, &oldProtect); + + g_jitSEHFunctionTable.storeRelaxed(dynamicTable); base = static_cast(base) + recordSize; size -= recordSize; } From 52cbe739f270e0e4267a58e514549424edf5789b Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 22 Jul 2026 19:27:22 +0000 Subject: [PATCH 6/8] JSC(win): register unwind info for the LLInt and wasm IPInt ranges too vmEntryToJavaScript and the LLInt bytecode handlers are emitted by offlineasm into .text without .seh_* directives (and ARM64 builds the LLInt TU with -fno-unwind-tables), so they have no static .pdata. Every JS stack has at least one such frame, so SEH / RtlCaptureStackBackTrace would still derail there even with the JIT pool covered. Register a second growable table over [llintPCRangeStart, llintPCRangeEnd) (and the wasm IPInt range) using the same prologue unwind codes. The unwind info lives in image .data so RUNTIME_FUNCTION.UnwindData can address it as an image-base-relative RVA; the handler is in image .text so no thunk is needed. Two fixed .data slots cover both callers. --- .../jit/ExecutableAllocator.cpp | 82 +++++++++++++++++++ .../JavaScriptCore/jit/ExecutableAllocator.h | 1 + .../runtime/InitializeThreading.cpp | 7 ++ 3 files changed, 90 insertions(+) diff --git a/Source/JavaScriptCore/jit/ExecutableAllocator.cpp b/Source/JavaScriptCore/jit/ExecutableAllocator.cpp index 4a833a9e15cf7..6047703ce9b6b 100644 --- a/Source/JavaScriptCore/jit/ExecutableAllocator.cpp +++ b/Source/JavaScriptCore/jit/ExecutableAllocator.cpp @@ -576,6 +576,88 @@ static void registerJITUnwindInfo(PageReservation& pageReservation, void*& base, #endif +// Cover a range inside the loaded image (LLInt, wasm IPInt) with the same +// prologue unwind info. offlineasm emits those into .text without .seh_* +// directives (and ARM64 builds the LLInt TU with -fno-unwind-tables), so they +// have no static .pdata. The unwind info here must itself live inside the +// image so that RUNTIME_FUNCTION.UnwindData (an image-base-relative u32 RVA) +// can address it; a fixed set of .data slots covers the two known callers. +static constexpr unsigned imageUnwindSlotCount = 2; +static Atomic g_imageUnwindSlotNext { 0 }; + +#if CPU(X86_64) +alignas(uint32_t) static decltype(JITUnwindRecord::unwindInfo) g_imageUnwindInfo[imageUnwindSlotCount] { }; +#else +alignas(uint32_t) static decltype(JITUnwindHeader::unwindInfoFull) g_imageUnwindInfo[imageUnwindSlotCount][2] { }; +#endif + +void registerImageUnwindInfoWin(void* rangeStart, void* rangeEnd) +{ + auto imageBase = reinterpret_cast(GetModuleHandleW(nullptr)); + if (!imageBase) + return; + auto rva = [&](const void* p) { return static_cast(reinterpret_cast(p) - imageBase); }; + auto begin = rva(rangeStart); + auto end = rva(rangeEnd); + if (begin >= end) + return; + + unsigned slot = g_imageUnwindSlotNext.exchangeAdd(1); + RELEASE_ASSERT(slot < imageUnwindSlotCount); + +#if CPU(X86_64) + auto& unwindInfo = g_imageUnwindInfo[slot]; + 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); + }; + unwindInfo.versionAndFlags = 1 | (UNW_FLAG_EHANDLER << 3); + unwindInfo.sizeOfProlog = prologSize; + unwindInfo.countOfCodes = 2; + unwindInfo.frameRegisterAndOffset = 5; + unwindInfo.unwindCodes[0] = unwindCode(prologSize, 3, 0); + unwindInfo.unwindCodes[1] = unwindCode(1, 0, 5); + unwindInfo.exceptionHandlerRVA = rva(reinterpret_cast(&jscJITSEHHandler)); + + auto* entry = static_cast(fastZeroedMalloc(sizeof(RUNTIME_FUNCTION))); + entry->BeginAddress = begin; + entry->EndAddress = end; + entry->UnwindData = rva(&unwindInfo); + + void* dynamicTable = nullptr; + RtlAddGrowableFunctionTable(&dynamicTable, entry, 1, 1, imageBase + begin, imageBase + end); +#else + auto& unwindInfoFull = g_imageUnwindInfo[slot][0]; + auto& unwindInfoTail = g_imageUnwindInfo[slot][1]; + unwindInfoFull.header = arm64XdataHeader(arm64MaxFunctionLength); + unwindInfoFull.unwindCodes = arm64JITUnwindCodes(); + unwindInfoFull.exceptionHandlerRVA = rva(reinterpret_cast(&jscJITSEHHandler)); + unwindInfoTail.unwindCodes = arm64JITUnwindCodes(); + unwindInfoTail.exceptionHandlerRVA = unwindInfoFull.exceptionHandlerRVA; + + size_t rangeSize = end - begin; + DWORD maxEntries = static_cast((rangeSize + arm64MaxFunctionLength - 1) / arm64MaxFunctionLength); + auto* entries = static_cast(fastZeroedMalloc(maxEntries * sizeof(RUNTIME_FUNCTION))); + size_t remaining = rangeSize; + DWORD entryCount = 0; + while (remaining > arm64MaxFunctionLength) { + entries[entryCount].BeginAddress = begin; + entries[entryCount].UnwindData = rva(&unwindInfoFull); + begin += static_cast(arm64MaxFunctionLength); + remaining -= arm64MaxFunctionLength; + entryCount++; + } + unwindInfoTail.header = arm64XdataHeader(remaining); + entries[entryCount].BeginAddress = begin; + entries[entryCount].UnwindData = rva(&unwindInfoTail); + entryCount++; + RELEASE_ASSERT(entryCount <= maxEntries); + + void* dynamicTable = nullptr; + RtlAddGrowableFunctionTable(&dynamicTable, entries, entryCount, entryCount, imageBase + end - rangeSize, imageBase + end); +#endif +} + #endif // OS(WINDOWS) && (CPU(X86_64) || CPU(ARM64)) WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN diff --git a/Source/JavaScriptCore/jit/ExecutableAllocator.h b/Source/JavaScriptCore/jit/ExecutableAllocator.h index 6eb54000eecc1..e2bebe701a49e 100644 --- a/Source/JavaScriptCore/jit/ExecutableAllocator.h +++ b/Source/JavaScriptCore/jit/ExecutableAllocator.h @@ -105,6 +105,7 @@ JS_EXPORT_PRIVATE void* NODELETE endOfFixedExecutableMemoryPoolImpl(); using JITExceptionHandlerWin = long(__cdecl*)(void*, void*, void*, void*); JS_EXPORT_PRIVATE void setJITExceptionHandlerWin(JITExceptionHandlerWin); JS_EXPORT_PRIVATE bool hasJITUnwindInfoWin(); +void registerImageUnwindInfoWin(void* rangeStart, void* rangeEnd); #endif template diff --git a/Source/JavaScriptCore/runtime/InitializeThreading.cpp b/Source/JavaScriptCore/runtime/InitializeThreading.cpp index 2652891e81f7c..f2153339a0c87 100644 --- a/Source/JavaScriptCore/runtime/InitializeThreading.cpp +++ b/Source/JavaScriptCore/runtime/InitializeThreading.cpp @@ -36,6 +36,7 @@ #include "JSCConfig.h" #include "JSCPtrTag.h" #include "LLIntData.h" +#include "LLIntPCRanges.h" #include "NativeCalleeRegistry.h" #include "Options.h" #include "StructureAlignedMemoryAllocator.h" @@ -122,6 +123,12 @@ void initializeWithOptionsCustomization(const ScopedLambda& optionsCusto AssemblyCommentRegistry::initialize(); LLInt::initialize(); +#if ENABLE(JIT) && OS(WINDOWS) && (CPU(X86_64) || CPU(ARM64)) + registerImageUnwindInfoWin(reinterpret_cast(LLInt::llintPCRangeStart), reinterpret_cast(LLInt::llintPCRangeEnd)); +#if ENABLE(WEBASSEMBLY) + registerImageUnwindInfoWin(reinterpret_cast(LLInt::wasmIPIntPCRangeStart), reinterpret_cast(LLInt::wasmIPIntPCRangeEnd)); +#endif +#endif AssertNoGC::initialize(); initializeSuperSampler(); From d23a372e30244be7117cb1ab658e2b21853dc7c4 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:44:20 +0000 Subject: [PATCH 7/8] JSC(win): fix build and registerImageUnwindInfoWin correctness - Link ntdll for RtlAddGrowableFunctionTable (kernel32 has no forwarder, unlike RtlAddFunctionTable). - registerImageUnwindInfoWin: pass imageBase as RangeBase. RVAs are image-base-relative, and RangeBase is what RtlLookupFunctionEntry returns as *ImageBase and resolves all RVAs against; the previous rangeStart base never matched so the LLInt table was dead. - Use &__ImageBase instead of GetModuleHandleW(nullptr) so the base is correct if JSC is ever linked as a DLL. - Drop the separate wasmIPInt registration: those labels sit between llintPCRangeStart/End in LowLevelInterpreter.asm, so the LLInt registration already covers them. One .data slot now suffices. --- Source/JavaScriptCore/PlatformJSCOnly.cmake | 4 ++- .../jit/ExecutableAllocator.cpp | 31 +++++++++---------- .../runtime/InitializeThreading.cpp | 6 ++-- 3 files changed, 20 insertions(+), 21 deletions(-) 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 6047703ce9b6b..381e539ac0576 100644 --- a/Source/JavaScriptCore/jit/ExecutableAllocator.cpp +++ b/Source/JavaScriptCore/jit/ExecutableAllocator.cpp @@ -580,33 +580,30 @@ static void registerJITUnwindInfo(PageReservation& pageReservation, void*& base, // prologue unwind info. offlineasm emits those into .text without .seh_* // directives (and ARM64 builds the LLInt TU with -fno-unwind-tables), so they // have no static .pdata. The unwind info here must itself live inside the -// image so that RUNTIME_FUNCTION.UnwindData (an image-base-relative u32 RVA) -// can address it; a fixed set of .data slots covers the two known callers. -static constexpr unsigned imageUnwindSlotCount = 2; -static Atomic g_imageUnwindSlotNext { 0 }; +// image so that RUNTIME_FUNCTION.UnwindData (a base-relative u32 RVA) can +// address it; the LLInt range is the one caller (it encloses the wasm IPInt +// range), so a single .data slot suffices. +extern "C" IMAGE_DOS_HEADER __ImageBase; #if CPU(X86_64) -alignas(uint32_t) static decltype(JITUnwindRecord::unwindInfo) g_imageUnwindInfo[imageUnwindSlotCount] { }; +alignas(uint32_t) static decltype(JITUnwindRecord::unwindInfo) g_imageUnwindInfo { }; #else -alignas(uint32_t) static decltype(JITUnwindHeader::unwindInfoFull) g_imageUnwindInfo[imageUnwindSlotCount][2] { }; +alignas(uint32_t) static decltype(JITUnwindHeader::unwindInfoFull) g_imageUnwindInfo[2] { }; #endif void registerImageUnwindInfoWin(void* rangeStart, void* rangeEnd) { - auto imageBase = reinterpret_cast(GetModuleHandleW(nullptr)); - if (!imageBase) - return; + // __ImageBase resolves to the base of whichever PE this TU links into + // (EXE or DLL); GetModuleHandleW(nullptr) would be the process EXE base. + auto imageBase = reinterpret_cast(&__ImageBase); auto rva = [&](const void* p) { return static_cast(reinterpret_cast(p) - imageBase); }; auto begin = rva(rangeStart); auto end = rva(rangeEnd); if (begin >= end) return; - unsigned slot = g_imageUnwindSlotNext.exchangeAdd(1); - RELEASE_ASSERT(slot < imageUnwindSlotCount); - #if CPU(X86_64) - auto& unwindInfo = g_imageUnwindInfo[slot]; + auto& unwindInfo = g_imageUnwindInfo; 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); @@ -625,10 +622,10 @@ void registerImageUnwindInfoWin(void* rangeStart, void* rangeEnd) entry->UnwindData = rva(&unwindInfo); void* dynamicTable = nullptr; - RtlAddGrowableFunctionTable(&dynamicTable, entry, 1, 1, imageBase + begin, imageBase + end); + RtlAddGrowableFunctionTable(&dynamicTable, entry, 1, 1, imageBase, imageBase + end); #else - auto& unwindInfoFull = g_imageUnwindInfo[slot][0]; - auto& unwindInfoTail = g_imageUnwindInfo[slot][1]; + auto& unwindInfoFull = g_imageUnwindInfo[0]; + auto& unwindInfoTail = g_imageUnwindInfo[1]; unwindInfoFull.header = arm64XdataHeader(arm64MaxFunctionLength); unwindInfoFull.unwindCodes = arm64JITUnwindCodes(); unwindInfoFull.exceptionHandlerRVA = rva(reinterpret_cast(&jscJITSEHHandler)); @@ -654,7 +651,7 @@ void registerImageUnwindInfoWin(void* rangeStart, void* rangeEnd) RELEASE_ASSERT(entryCount <= maxEntries); void* dynamicTable = nullptr; - RtlAddGrowableFunctionTable(&dynamicTable, entries, entryCount, entryCount, imageBase + end - rangeSize, imageBase + end); + RtlAddGrowableFunctionTable(&dynamicTable, entries, entryCount, entryCount, imageBase, imageBase + end); #endif } diff --git a/Source/JavaScriptCore/runtime/InitializeThreading.cpp b/Source/JavaScriptCore/runtime/InitializeThreading.cpp index f2153339a0c87..f4dad2ca0ee57 100644 --- a/Source/JavaScriptCore/runtime/InitializeThreading.cpp +++ b/Source/JavaScriptCore/runtime/InitializeThreading.cpp @@ -124,10 +124,10 @@ void initializeWithOptionsCustomization(const ScopedLambda& optionsCusto AssemblyCommentRegistry::initialize(); LLInt::initialize(); #if ENABLE(JIT) && OS(WINDOWS) && (CPU(X86_64) || CPU(ARM64)) + // The LLInt range encloses the wasm IPInt range (LowLevelInterpreter + // .asm places wasmIPIntPCRangeStart/End between llintPCRangeStart/End), + // so one registration covers both. registerImageUnwindInfoWin(reinterpret_cast(LLInt::llintPCRangeStart), reinterpret_cast(LLInt::llintPCRangeEnd)); -#if ENABLE(WEBASSEMBLY) - registerImageUnwindInfoWin(reinterpret_cast(LLInt::wasmIPIntPCRangeStart), reinterpret_cast(LLInt::wasmIPIntPCRangeEnd)); -#endif #endif AssertNoGC::initialize(); From ed1c14e9b34b00db7691c06f89f23b2d9777f76e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 23 Jul 2026 08:21:48 +0000 Subject: [PATCH 8/8] JSC(win): drop the dynamic LLInt unwind registration RtlLookupFunctionEntry for a PC inside a loaded module consults only that module's static .pdata; dynamic growable tables are for anonymous memory. The LLInt table therefore never resolved (verified on Windows 10/11 x64 and ARM64 via Bun CI build 78474). 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; that belongs in a separate change. A comment pointing at it is left where the dynamic registration used to be. The JIT pool registration (anonymous memory) is validated working on both arches and stays. --- .../jit/ExecutableAllocator.cpp | 85 ++----------------- .../JavaScriptCore/jit/ExecutableAllocator.h | 1 - .../runtime/InitializeThreading.cpp | 7 -- 3 files changed, 7 insertions(+), 86 deletions(-) diff --git a/Source/JavaScriptCore/jit/ExecutableAllocator.cpp b/Source/JavaScriptCore/jit/ExecutableAllocator.cpp index 381e539ac0576..cac6fb0d08625 100644 --- a/Source/JavaScriptCore/jit/ExecutableAllocator.cpp +++ b/Source/JavaScriptCore/jit/ExecutableAllocator.cpp @@ -576,84 +576,13 @@ static void registerJITUnwindInfo(PageReservation& pageReservation, void*& base, #endif -// Cover a range inside the loaded image (LLInt, wasm IPInt) with the same -// prologue unwind info. offlineasm emits those into .text without .seh_* -// directives (and ARM64 builds the LLInt TU with -fno-unwind-tables), so they -// have no static .pdata. The unwind info here must itself live inside the -// image so that RUNTIME_FUNCTION.UnwindData (a base-relative u32 RVA) can -// address it; the LLInt range is the one caller (it encloses the wasm IPInt -// range), so a single .data slot suffices. -extern "C" IMAGE_DOS_HEADER __ImageBase; - -#if CPU(X86_64) -alignas(uint32_t) static decltype(JITUnwindRecord::unwindInfo) g_imageUnwindInfo { }; -#else -alignas(uint32_t) static decltype(JITUnwindHeader::unwindInfoFull) g_imageUnwindInfo[2] { }; -#endif - -void registerImageUnwindInfoWin(void* rangeStart, void* rangeEnd) -{ - // __ImageBase resolves to the base of whichever PE this TU links into - // (EXE or DLL); GetModuleHandleW(nullptr) would be the process EXE base. - auto imageBase = reinterpret_cast(&__ImageBase); - auto rva = [&](const void* p) { return static_cast(reinterpret_cast(p) - imageBase); }; - auto begin = rva(rangeStart); - auto end = rva(rangeEnd); - if (begin >= end) - return; - -#if CPU(X86_64) - auto& unwindInfo = g_imageUnwindInfo; - 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); - }; - unwindInfo.versionAndFlags = 1 | (UNW_FLAG_EHANDLER << 3); - unwindInfo.sizeOfProlog = prologSize; - unwindInfo.countOfCodes = 2; - unwindInfo.frameRegisterAndOffset = 5; - unwindInfo.unwindCodes[0] = unwindCode(prologSize, 3, 0); - unwindInfo.unwindCodes[1] = unwindCode(1, 0, 5); - unwindInfo.exceptionHandlerRVA = rva(reinterpret_cast(&jscJITSEHHandler)); - - auto* entry = static_cast(fastZeroedMalloc(sizeof(RUNTIME_FUNCTION))); - entry->BeginAddress = begin; - entry->EndAddress = end; - entry->UnwindData = rva(&unwindInfo); - - void* dynamicTable = nullptr; - RtlAddGrowableFunctionTable(&dynamicTable, entry, 1, 1, imageBase, imageBase + end); -#else - auto& unwindInfoFull = g_imageUnwindInfo[0]; - auto& unwindInfoTail = g_imageUnwindInfo[1]; - unwindInfoFull.header = arm64XdataHeader(arm64MaxFunctionLength); - unwindInfoFull.unwindCodes = arm64JITUnwindCodes(); - unwindInfoFull.exceptionHandlerRVA = rva(reinterpret_cast(&jscJITSEHHandler)); - unwindInfoTail.unwindCodes = arm64JITUnwindCodes(); - unwindInfoTail.exceptionHandlerRVA = unwindInfoFull.exceptionHandlerRVA; - - size_t rangeSize = end - begin; - DWORD maxEntries = static_cast((rangeSize + arm64MaxFunctionLength - 1) / arm64MaxFunctionLength); - auto* entries = static_cast(fastZeroedMalloc(maxEntries * sizeof(RUNTIME_FUNCTION))); - size_t remaining = rangeSize; - DWORD entryCount = 0; - while (remaining > arm64MaxFunctionLength) { - entries[entryCount].BeginAddress = begin; - entries[entryCount].UnwindData = rva(&unwindInfoFull); - begin += static_cast(arm64MaxFunctionLength); - remaining -= arm64MaxFunctionLength; - entryCount++; - } - unwindInfoTail.header = arm64XdataHeader(remaining); - entries[entryCount].BeginAddress = begin; - entries[entryCount].UnwindData = rva(&unwindInfoTail); - entryCount++; - RELEASE_ASSERT(entryCount <= maxEntries); - - void* dynamicTable = nullptr; - RtlAddGrowableFunctionTable(&dynamicTable, entries, entryCount, entryCount, imageBase, imageBase + end); -#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)) diff --git a/Source/JavaScriptCore/jit/ExecutableAllocator.h b/Source/JavaScriptCore/jit/ExecutableAllocator.h index e2bebe701a49e..6eb54000eecc1 100644 --- a/Source/JavaScriptCore/jit/ExecutableAllocator.h +++ b/Source/JavaScriptCore/jit/ExecutableAllocator.h @@ -105,7 +105,6 @@ JS_EXPORT_PRIVATE void* NODELETE endOfFixedExecutableMemoryPoolImpl(); using JITExceptionHandlerWin = long(__cdecl*)(void*, void*, void*, void*); JS_EXPORT_PRIVATE void setJITExceptionHandlerWin(JITExceptionHandlerWin); JS_EXPORT_PRIVATE bool hasJITUnwindInfoWin(); -void registerImageUnwindInfoWin(void* rangeStart, void* rangeEnd); #endif template diff --git a/Source/JavaScriptCore/runtime/InitializeThreading.cpp b/Source/JavaScriptCore/runtime/InitializeThreading.cpp index f4dad2ca0ee57..2652891e81f7c 100644 --- a/Source/JavaScriptCore/runtime/InitializeThreading.cpp +++ b/Source/JavaScriptCore/runtime/InitializeThreading.cpp @@ -36,7 +36,6 @@ #include "JSCConfig.h" #include "JSCPtrTag.h" #include "LLIntData.h" -#include "LLIntPCRanges.h" #include "NativeCalleeRegistry.h" #include "Options.h" #include "StructureAlignedMemoryAllocator.h" @@ -123,12 +122,6 @@ void initializeWithOptionsCustomization(const ScopedLambda& optionsCusto AssemblyCommentRegistry::initialize(); LLInt::initialize(); -#if ENABLE(JIT) && OS(WINDOWS) && (CPU(X86_64) || CPU(ARM64)) - // The LLInt range encloses the wasm IPInt range (LowLevelInterpreter - // .asm places wasmIPIntPCRangeStart/End between llintPCRangeStart/End), - // so one registration covers both. - registerImageUnwindInfoWin(reinterpret_cast(LLInt::llintPCRangeStart), reinterpret_cast(LLInt::llintPCRangeEnd)); -#endif AssertNoGC::initialize(); initializeSuperSampler();