From 8ec8fb6a7ba7ddd03057ff8f72b3f267fd2939f2 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 8 Aug 2026 01:54:50 +0000 Subject: [PATCH 1/2] CallFrame::unsafeCallerFrame: treat a null entry frame as the end of the sampling profiler's walk SamplingProfiler::takeSample only gates on vm.entryScope, but vm.topEntryFrame can be null while entryScope is set: VMEntryScope is created and destroyed in C++ around vmEntryToJavaScript, while topCallFrame/topEntryFrame are stored and restored inside doVMEntry (after the O(paddedArgCount) argument copy loops). A sample landing in those windows walks a half-built entry frame or a stale topCallFrame with FrameWalker::m_entryFrame == nullptr. The first walked frame whose caller slot reads null then matches the null entry frame, and unsafeCallerFrame dereferences vmEntryRecord(nullptr), which faults just below address zero (0xFFFFFFFFFFFFFFC8 on Windows x64). Bail out when currEntryFrame is null: there is no VMEntryRecord to consult, and returning null ends the walk the same way reaching prevTopCallFrame == nullptr at the outermost record does. unsafeCallerFrame's only caller is the sampling profiler's FrameWalker::advanceToParentFrame. --- Source/JavaScriptCore/interpreter/CallFrame.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Source/JavaScriptCore/interpreter/CallFrame.cpp b/Source/JavaScriptCore/interpreter/CallFrame.cpp index 6a870c752969a..9a325c1907521 100644 --- a/Source/JavaScriptCore/interpreter/CallFrame.cpp +++ b/Source/JavaScriptCore/interpreter/CallFrame.cpp @@ -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(); From 62128f58f399ec1de71eef01ddc6aaf12e7ad6b3 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 8 Aug 2026 03:18:21 +0000 Subject: [PATCH 2/2] TestWebKitAPI: add a deterministic test for unsafeCallerFrame with a null entry frame A zeroed frame whose caller slot reads null, walked with a null EntryFrame cursor, models the state the sampling profiler hits when a sample lands inside a VM entry/exit transition. Without the guard the test faults in VMEntryRecord::unsafePrevTopEntryFrame reading just below address zero, the same location as the CI crash; with it unsafeCallerFrame reports the end of the stack. --- Tools/TestWebKitAPI/CMakeLists.txt | 1 + .../JavaScriptCore/UnsafeCallerFrame.cpp | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 Tools/TestWebKitAPI/Tests/JavaScriptCore/UnsafeCallerFrame.cpp diff --git a/Tools/TestWebKitAPI/CMakeLists.txt b/Tools/TestWebKitAPI/CMakeLists.txt index 514785fb7b8fc..19996c7d9e40c 100644 --- a/Tools/TestWebKitAPI/CMakeLists.txt +++ b/Tools/TestWebKitAPI/CMakeLists.txt @@ -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 diff --git a/Tools/TestWebKitAPI/Tests/JavaScriptCore/UnsafeCallerFrame.cpp b/Tools/TestWebKitAPI/Tests/JavaScriptCore/UnsafeCallerFrame.cpp new file mode 100644 index 0000000000000..28ee3cb8f271d --- /dev/null +++ b/Tools/TestWebKitAPI/Tests/JavaScriptCore/UnsafeCallerFrame.cpp @@ -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 +#include + +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(zeroedFrame)); + + JSC::EntryFrame* entryFrame = nullptr; + EXPECT_EQ(callFrame->unsafeCallerFrame(entryFrame), nullptr); + EXPECT_EQ(entryFrame, nullptr); +} + +} // namespace TestWebKitAPI