From dbb960f372480dd4b9a155b51d96a257f4ae432c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 09:43:55 +0000 Subject: [PATCH 1/2] [JSC] Windows: emit a .pdata/.xdata record over the offlineasm code The LLInt opcode handlers and the vmEntry* trampolines are linked into the image's .text with no RUNTIME_FUNCTION, because offlineasm emits no .seh_* directives. The Windows unwinder consults only the image's static .pdata for an in-image PC, so the dynamic function table registered for the JIT pool cannot help, and every unwind-table based stack walk (RtlVirtualUnwind, RtlCaptureStackBackTrace, SEH dispatch, ETW, WinDbg) stops or derails at the first interpreter frame. Every function in jsc_llint_begin..jsc_llint_end keeps the frame pointer on a CallFrame, the frame shape the JIT pool's unwind info already describes, so LowLevelInterpreter.cpp now assembles one RUNTIME_FUNCTION over that range with the same unwind codes registerJITUnwindInfo hand-encodes, and with the same language-specific handler, which gets a C name so the record can address it directly. The record is written out as data rather than with .seh_* directives because LLVM cannot compute an ARM64 SEH function length across the alignment directives in the offlineasm output (llvm/llvm-project#47432). On ARM64 the 18-bit FunctionLength field is guarded by an ADR whose reach matches it, so the build fails instead of emitting a bad record if the interpreter ever outgrows one entry. --- .../jit/ExecutableAllocator.cpp | 19 +++-- .../JavaScriptCore/jit/ExecutableAllocator.h | 3 +- .../llint/LowLevelInterpreter.cpp | 82 +++++++++++++++++++ 3 files changed, 94 insertions(+), 10 deletions(-) diff --git a/Source/JavaScriptCore/jit/ExecutableAllocator.cpp b/Source/JavaScriptCore/jit/ExecutableAllocator.cpp index 19af78ad8ed42..148102f63b65b 100644 --- a/Source/JavaScriptCore/jit/ExecutableAllocator.cpp +++ b/Source/JavaScriptCore/jit/ExecutableAllocator.cpp @@ -369,11 +369,20 @@ struct JITReservation { // RegisterExecutableMemory (js/src/jit/ProcessExecutableMemory.cpp). // RtlAddGrowableFunctionTable rather than RtlAddFunctionTable so that // out-of-process stack walkers (ETW, WPA, WinDbg) see the entry too. +// +// The offlineasm code linked into the image (LLInt, vmEntryToJavaScript and +// the other vmEntry* trampolines) has the same frame shape but cannot be +// covered by a dynamic table: for a PC inside a loaded module the unwinder +// consults only that module's static .pdata. LowLevelInterpreter.cpp therefore +// assembles a static .pdata/.xdata record with the same unwind codes over +// jsc_llint_begin..jsc_llint_end (as V8 does for its embedded builtins in +// platform-embedded-file-writer-win.cc). Being in the image, that record can +// name jscJITSEHHandler directly, which is why the handler has a C name. static Atomic g_jitSEHFunctionTable { nullptr }; static Atomic g_jitSEHCallback { nullptr }; -static EXCEPTION_DISPOSITION jscJITSEHHandler(PEXCEPTION_RECORD exceptionRecord, PVOID establisherFrame, PCONTEXT contextRecord, PDISPATCHER_CONTEXT dispatcherContext) +extern "C" 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)); @@ -561,14 +570,6 @@ static void registerJITUnwindInfo(PageReservation& pageReservation, void*& base, #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 diff --git a/Source/JavaScriptCore/jit/ExecutableAllocator.h b/Source/JavaScriptCore/jit/ExecutableAllocator.h index 6eb54000eecc1..90fad406e3e03 100644 --- a/Source/JavaScriptCore/jit/ExecutableAllocator.h +++ b/Source/JavaScriptCore/jit/ExecutableAllocator.h @@ -99,7 +99,8 @@ 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. +// reaches a JIT frame or an LLInt / vmEntry* frame. See registerJITUnwindInfo +// in ExecutableAllocator.cpp and the .pdata record in LowLevelInterpreter.cpp. // Signature: EXCEPTION_DISPOSITION(PEXCEPTION_RECORD, PVOID establisherFrame, // PCONTEXT, PDISPATCHER_CONTEXT). using JITExceptionHandlerWin = long(__cdecl*)(void*, void*, void*, void*); diff --git a/Source/JavaScriptCore/llint/LowLevelInterpreter.cpp b/Source/JavaScriptCore/llint/LowLevelInterpreter.cpp index e7f5aeab4784d..d590cf648e58f 100644 --- a/Source/JavaScriptCore/llint/LowLevelInterpreter.cpp +++ b/Source/JavaScriptCore/llint/LowLevelInterpreter.cpp @@ -648,6 +648,88 @@ __asm__( #endif #endif +#if OS(WINDOWS) && ENABLE(JIT) && (CPU(X86_64) || CPU(ARM64)) +// The Windows unwinder (RtlLookupFunctionEntry, and through it RtlVirtualUnwind, +// RtlCaptureStackBackTrace, SEH dispatch, ETW and debuggers) only knows code that has +// a RUNTIME_FUNCTION in the image's .pdata: the DWARF CFI emitted above on ARM64 means +// nothing to it, and the dynamic function table that registerJITUnwindInfo +// (ExecutableAllocator.cpp) registers for the JIT pool cannot cover code inside the +// image. offlineasm emits no .seh_* directives, so without the record below every +// walk stops or derails at the first LLInt or vmEntry* frame. +// +// Everything between jsc_llint_begin and jsc_llint_end keeps the frame pointer on a +// CallFrame whose first two slots are the caller's frame pointer and the return +// address (functionPrologue), the frame shape registerJITUnwindInfo describes for the +// JIT pool. So one RUNTIME_FUNCTION over the whole range, carrying the unwind info +// registerJITUnwindInfo hand-encodes, unwinds all of it, and its language-specific +// handler gives a fault under these frames the same catch point a fault under a JIT +// frame has (hence ENABLE(JIT): the handler is defined in ExecutableAllocator.cpp). +// The range starts with the jsc_llint_begin and llintPCRangeStart trap instructions, +// so no real PC falls inside the prologue the record describes. +// +// The record is written out by hand rather than with .seh_* directives: on ARM64 LLVM +// needs the function length when it reaches .seh_endproc and cannot compute it across +// the alignment directives in the offlineasm output ("Failed to evaluate function +// length in SEH unwind info", llvm/llvm-project#47432, the bug the -fno-unwind-tables +// in CMakeLists.txt works around), whereas the data fixups below are resolved after +// layout. +#if CPU(X86_64) +__asm__( + ".section .xdata,\"dr\"\n" + ".p2align 2\n" + LOCAL_LABEL_STRING(jsc_llint_unwind_info) ":\n" + // UNWIND_INFO (https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64): + // the bytes of JITUnwindRecord::unwindInfo in ExecutableAllocator.cpp, except that + // the handler is addressed directly instead of through a thunk in the pool. + ".byte 0x09\n" // Version 1, UNW_FLAG_EHANDLER + ".byte 4\n" // SizeOfProlog: push rbp (1 byte); mov rbp, rsp (3 bytes) + ".byte 2\n" // CountOfCodes + ".byte 0x05\n" // FrameRegister rbp, FrameOffset 0 + ".byte 4, 0x03\n" // offset 4: UWOP_SET_FPREG + ".byte 1, 0x50\n" // offset 1: UWOP_PUSH_NONVOL rbp + ".rva " SYMBOL_STRING(jscJITSEHHandler) "\n" + + ".section .pdata,\"dr\"\n" + ".p2align 2\n" + ".rva " SYMBOL_STRING(jsc_llint_begin) "\n" + ".rva " SYMBOL_STRING(jsc_llint_end) "\n" + ".rva " LOCAL_LABEL_STRING(jsc_llint_unwind_info) "\n" + + ".text\n" +); +#elif CPU(ARM64) +__asm__( + // The .xdata header holds the function length as 18 bits of instructions, and the + // .long below would silently overflow into the flag bits once the range outgrows + // that. ADR reaches exactly 2^18 instructions back, so this instruction, which + // nothing executes (it sits after the trap that precedes jsc_llint_end), assembled + // right after jsc_llint_end and aimed one instruction before jsc_llint_begin, fails + // to assemble ("fixup value out of range") as soon as the range no longer fits. + ".text\n" + "adr xzr, " SYMBOL_STRING(jsc_llint_begin) " - 4\n" + + ".section .xdata,\"dr\"\n" + ".p2align 2\n" + LOCAL_LABEL_STRING(jsc_llint_unwind_info) ":\n" + // .xdata record (https://learn.microsoft.com/en-us/cpp/build/arm64-exception-handling): + // the layout and codes of JITUnwindHeader::unwindInfoFull in ExecutableAllocator.cpp + // (arm64XdataHeader / arm64JITUnwindCodes), with the real length and the handler + // addressed directly instead of through a thunk in the pool. Header fields: + // FunctionLength:18 | Version:2 = 0 | X:1 = 1 (handler present) | E:1 = 0 | EpilogCount:5 = 0 | CodeWords:5 = 1 + ".long ((" SYMBOL_STRING(jsc_llint_end) " - " SYMBOL_STRING(jsc_llint_begin) ") >> 2) | (1 << 20) | (1 << 27)\n" + ".byte 0xE1, 0x81, 0xE4, 0xE3\n" // set_fp; save_fplr_x 16; end; nop (padding) + ".rva " SYMBOL_STRING(jscJITSEHHandler) "\n" + + ".section .pdata,\"dr\"\n" + ".p2align 2\n" + ".rva " SYMBOL_STRING(jsc_llint_begin) "\n" + ".rva " LOCAL_LABEL_STRING(jsc_llint_unwind_info) "\n" + + ".text\n" +); +#endif +#endif // OS(WINDOWS) && ENABLE(JIT) && (CPU(X86_64) || CPU(ARM64)) + DEBUGGER_ANNOTATION_MARKER(after_llint_asm) #endif // ENABLE(C_LOOP) From 644fda92949a4b7e24a548f0f26867246e1f3d75 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 10:52:11 +0000 Subject: [PATCH 2/2] Spell the RVAs as .long symbol@IMGREL so LTO sees the handler reference LTO collects the symbols a module's inline asm references by parsing it with a recording streamer, and .rva operands never reach it. With .rva a -flto build internalized jscJITSEHHandler and lld-link failed with an undefined symbol referenced from .xdata (the windows-amd64-lto build). .long symbol@IMGREL assembles to the same ADDR32NB relocation but goes through an ordinary data expression, which the scan records. --- .../llint/LowLevelInterpreter.cpp | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Source/JavaScriptCore/llint/LowLevelInterpreter.cpp b/Source/JavaScriptCore/llint/LowLevelInterpreter.cpp index d590cf648e58f..853e3ad36f553 100644 --- a/Source/JavaScriptCore/llint/LowLevelInterpreter.cpp +++ b/Source/JavaScriptCore/llint/LowLevelInterpreter.cpp @@ -672,7 +672,11 @@ __asm__( // the alignment directives in the offlineasm output ("Failed to evaluate function // length in SEH unwind info", llvm/llvm-project#47432, the bug the -fno-unwind-tables // in CMakeLists.txt works around), whereas the data fixups below are resolved after -// layout. +// layout. The RVAs are spelled ".long symbol@IMGREL" rather than ".rva symbol" (the +// two assemble to the same IMAGE_REL_*_ADDR32NB relocation) because LTO's scan of the +// module asm for the symbols it references does not see .rva operands: with .rva, a +// -flto build internalizes jscJITSEHHandler and the link fails with an undefined +// symbol referenced from .xdata. #if CPU(X86_64) __asm__( ".section .xdata,\"dr\"\n" @@ -687,13 +691,13 @@ __asm__( ".byte 0x05\n" // FrameRegister rbp, FrameOffset 0 ".byte 4, 0x03\n" // offset 4: UWOP_SET_FPREG ".byte 1, 0x50\n" // offset 1: UWOP_PUSH_NONVOL rbp - ".rva " SYMBOL_STRING(jscJITSEHHandler) "\n" + ".long " SYMBOL_STRING(jscJITSEHHandler) "@IMGREL\n" ".section .pdata,\"dr\"\n" ".p2align 2\n" - ".rva " SYMBOL_STRING(jsc_llint_begin) "\n" - ".rva " SYMBOL_STRING(jsc_llint_end) "\n" - ".rva " LOCAL_LABEL_STRING(jsc_llint_unwind_info) "\n" + ".long " SYMBOL_STRING(jsc_llint_begin) "@IMGREL\n" + ".long " SYMBOL_STRING(jsc_llint_end) "@IMGREL\n" + ".long " LOCAL_LABEL_STRING(jsc_llint_unwind_info) "@IMGREL\n" ".text\n" ); @@ -718,12 +722,12 @@ __asm__( // FunctionLength:18 | Version:2 = 0 | X:1 = 1 (handler present) | E:1 = 0 | EpilogCount:5 = 0 | CodeWords:5 = 1 ".long ((" SYMBOL_STRING(jsc_llint_end) " - " SYMBOL_STRING(jsc_llint_begin) ") >> 2) | (1 << 20) | (1 << 27)\n" ".byte 0xE1, 0x81, 0xE4, 0xE3\n" // set_fp; save_fplr_x 16; end; nop (padding) - ".rva " SYMBOL_STRING(jscJITSEHHandler) "\n" + ".long " SYMBOL_STRING(jscJITSEHHandler) "@IMGREL\n" ".section .pdata,\"dr\"\n" ".p2align 2\n" - ".rva " SYMBOL_STRING(jsc_llint_begin) "\n" - ".rva " LOCAL_LABEL_STRING(jsc_llint_unwind_info) "\n" + ".long " SYMBOL_STRING(jsc_llint_begin) "@IMGREL\n" + ".long " LOCAL_LABEL_STRING(jsc_llint_unwind_info) "@IMGREL\n" ".text\n" );