Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Source/JavaScriptCore/interpreter/CallFrame.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,15 @@ CallFrame* CallFrame::callerFrame(EntryFrame*& currEntryFrame) const
SUPPRESS_ASAN CallFrame* CallFrame::unsafeCallerFrame(EntryFrame*& currEntryFrame) const
{
if (unsafeCallerFrameOrEntryFrame() == currEntryFrame) {
// The sampling profiler walks the sampled thread's unsafe state: it can
// start from a stale vm.topCallFrame, or from a machine frame inside
// vmEntryToJavaScript's prologue/epilogue, while its vm.topEntryFrame
// snapshot is null. A walked frame whose caller slot reads null then
// matches the null entry frame here, and vmEntryRecord(nullptr) faults
// reading near address zero. Treat a null entry frame as the end of the
// walk instead.
if (!currEntryFrame)
return nullptr;
VMEntryRecord* currVMEntryRecord = vmEntryRecord(currEntryFrame);
currEntryFrame = currVMEntryRecord->unsafePrevTopEntryFrame();
return currVMEntryRecord->unsafePrevTopCallFrame();
Expand Down
1 change: 1 addition & 0 deletions Tools/TestWebKitAPI/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ if (ENABLE_JAVASCRIPTCORE)
Tests/JavaScriptCore/MarkedVector.cpp
Tests/JavaScriptCore/PropertySlot.cpp
Tests/JavaScriptCore/RegularExpression.cpp
Tests/JavaScriptCore/UnsafeCallerFrame.cpp
)

set(TestJavaScriptCore_LIBRARIES
Expand Down
52 changes: 52 additions & 0 deletions Tools/TestWebKitAPI/Tests/JavaScriptCore/UnsafeCallerFrame.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2026 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 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 "config.h"
#include <JavaScriptCore/CallFrame.h>
#include <JavaScriptCore/InitializeThreading.h>

namespace TestWebKitAPI {

// The sampling profiler walks stacks through CallFrame::unsafeCallerFrame
// while its EntryFrame cursor can be null: vm.topEntryFrame is null in the
// windows around vmEntryToJavaScript where vm.entryScope is already set, and
// the walk can start from a stale vm.topCallFrame or a half-built entry
// frame. A walked frame whose caller slot reads null used to match the null
// cursor and dereference vmEntryRecord(nullptr), faulting just below address
// zero. Model that exact state with a zeroed frame and a null entry frame:
// unsafeCallerFrame must report the end of the stack instead of crashing.
TEST(JavaScriptCore_CallFrame, UnsafeCallerFrameWithNullEntryFrame)
{
JSC::initialize();

alignas(JSC::Register) uint64_t zeroedFrame[16] = { };
JSC::CallFrame* callFrame = JSC::CallFrame::create(reinterpret_cast<JSC::Register*>(zeroedFrame));

JSC::EntryFrame* entryFrame = nullptr;
EXPECT_EQ(callFrame->unsafeCallerFrame(entryFrame), nullptr);

Check warning on line 48 in Tools/TestWebKitAPI/Tests/JavaScriptCore/UnsafeCallerFrame.cpp

View check run for this annotation

Claude / Claude Code Review

unsafeCallerFrame lacks JS_EXPORT_PRIVATE — test won't link on shared-library JSC ports

`unsafeCallerFrame` is declared in `CallFrame.h:227` without `JS_EXPORT_PRIVATE` (unlike the adjacent `callerFrame` on line 228), so on ports that build JavaScriptCore as a shared library with hidden visibility (Win, GTK, WPE, Cocoa, PlayStation, JSCOnly without `ENABLE_STATIC_JSC`) this test won't link. Bun's own CI uses `ENABLE_STATIC_JSC=ON` and doesn't build TestWebKitAPI so nothing breaks here, but adding `JS_EXPORT_PRIVATE` to the declaration is a one-word fix that keeps the test portable

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 unsafeCallerFrame is declared in CallFrame.h:227 without JS_EXPORT_PRIVATE (unlike the adjacent callerFrame on line 228), so on ports that build JavaScriptCore as a shared library with hidden visibility (Win, GTK, WPE, Cocoa, PlayStation, JSCOnly without ENABLE_STATIC_JSC) this test won't link. Bun's own CI uses ENABLE_STATIC_JSC=ON and doesn't build TestWebKitAPI so nothing breaks here, but adding JS_EXPORT_PRIVATE to the declaration is a one-word fix that keeps the test portable and eases upstreaming.

Extended reasoning...

What the bug is

The new test at Tools/TestWebKitAPI/Tests/JavaScriptCore/UnsafeCallerFrame.cpp:48 calls callFrame->unsafeCallerFrame(entryFrame). In Source/JavaScriptCore/interpreter/CallFrame.h that method is declared as:

CallFrame* unsafeCallerFrame(EntryFrame*&) const;                 // line 227 — no export
JS_EXPORT_PRIVATE CallFrame* callerFrame(EntryFrame*&) const;     // line 228 — exported

and defined out-of-line in CallFrame.cpp (not inline in the header). Without JS_EXPORT_PRIVATE, the symbol is not exported from a shared JavaScriptCore library.

The code path that triggers it

The PR adds Tests/JavaScriptCore/UnsafeCallerFrame.cpp unconditionally to TestJavaScriptCore_SOURCES under if (ENABLE_JAVASCRIPTCORE). Several ports build TestJavaScriptCore and set JavaScriptCore_LIBRARY_TYPE SHARED with hidden default visibility:

  • OptionsGTK.cmake:491 — SHARED, plus CXX_VISIBILITY_PRESET hidden
  • OptionsWin.cmake:193 — SHARED (DLL export table)
  • OptionsCocoa.cmake:409 — SHARED, -fvisibility=hidden
  • OptionsPlayStation.cmake — SHARED, hidden
  • OptionsJSCOnly.cmake:124 — SHARED when ENABLE_STATIC_JSC is OFF, with hidden visibility (:7)

On any of those, linking TestJavaScriptCore will fail with an undefined reference / unresolved external for JSC::CallFrame::unsafeCallerFrame(JSC::EntryFrame*&) const.

Why existing code doesn't prevent it

Until this PR, unsafeCallerFrame's only caller was SamplingProfiler.cpp, which is compiled into the JavaScriptCore library itself, so the symbol never needed to cross the library boundary. The new test is the first out-of-library caller. The PR was verified only on a JSCOnly Linux build with ENABLE_STATIC_JSC=ON, where all symbols are visible regardless of the export macro, so the missing export was not observed.

Step-by-step proof

  1. Configure GTK (or JSCOnly with -DENABLE_STATIC_JSC=OFF): CMake sets JavaScriptCore_LIBRARY_TYPE=SHARED and CXX_VISIBILITY_PRESET hidden.
  2. CallFrame::unsafeCallerFrame is compiled into libJavaScriptCore.so with default (hidden) visibility because its declaration lacks JS_EXPORT_PRIVATE (which expands to __attribute__((visibility("default"))) / __declspec(dllexport)).
  3. nm -D libJavaScriptCore.so | c++filt | grep unsafeCallerFrame → no output; the symbol is local.
  4. TestJavaScriptCore compiles UnsafeCallerFrame.cpp, which references _ZNK3JSC9CallFrame17unsafeCallerFrameERPNS_10EntryFrameE.
  5. ld fails: undefined reference to 'JSC::CallFrame::unsafeCallerFrame(JSC::EntryFrame*&) const'.

Impact

None on this fork's CI: .github/workflows/build.yml builds with ENABLE_STATIC_JSC=ON and does not build the TestWebKitAPI target. But it will break the TestJavaScriptCore build on every shared-library port and would block upstreaming this fix to WebKit as-is.

Fix

One word — add JS_EXPORT_PRIVATE to the declaration in CallFrame.h:227, matching the adjacent callerFrame:

JS_EXPORT_PRIVATE CallFrame* unsafeCallerFrame(EntryFrame*&) const;

EXPECT_EQ(entryFrame, nullptr);
}

} // namespace TestWebKitAPI
Loading