/*
* Copyright (C) 2017-2025 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. ``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
* 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 "VMInspector.h"
#include "CodeBlock.h"
#include "CodeBlockSet.h"
#include "HeapInlines.h"
#include "HeapIterationScope.h"
#include "JSCInlines.h"
#include "JSWebAssemblyModule.h"
#include "MarkedSpaceInlines.h"
#include "StackVisitor.h"
#include "VMEntryRecord.h"
#include "VMManager.h"
#include
#include
WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN
namespace JSC {
#if ENABLE(JIT)
static bool NODELETE ensureIsSafeToLock(Lock& lock) WTF_IGNORES_THREAD_SAFETY_ANALYSIS
{
static constexpr unsigned maxRetries = 2;
unsigned tryCount = 0;
while (tryCount++ jitType()))
return;
if (jitCode->contains(machinePC)) {
codeBlock = cb;
return;
}
});
if (codeBlock)
return IterationStatus::Done;
return IterationStatus::Continue;
});
if (!codeBlock && hasTimeout)
return makeUnexpected(Error::TimedOut);
return codeBlock;
#else
UNUSED_PARAM(machinePC);
return nullptr;
#endif
}
bool VMInspector::currentThreadOwnsJSLock(VM* vm)
{
return vm->currentThreadIsHoldingAPILock();
}
static bool ensureCurrentThreadOwnsJSLock(VM* vm)
{
if (VMInspector::currentThreadOwnsJSLock(vm))
return true;
dataLog("ERROR: current thread does not own the JSLock\n");
return false;
}
void VMInspector::gc(VM* vm)
{
if (!ensureCurrentThreadOwnsJSLock(vm))
return;
vm->heap.collectNow(Sync, CollectionScope::Full);
}
void VMInspector::edenGC(VM* vm)
{
if (!ensureCurrentThreadOwnsJSLock(vm))
return;
vm->heap.collectSync(CollectionScope::Eden);
}
bool VMInspector::isInHeap(Heap* heap, void* ptr)
{
MarkedBlock* candidate = MarkedBlock::blockFor(ptr);
if (heap->objectSpace().blocks().set().contains(candidate))
return true;
for (PreciseAllocation* allocation : heap->objectSpace().preciseAllocations()) {
if (allocation->contains(ptr))
return true;
}
return false;
}
struct CellAddressCheckFunctor : MarkedBlock::CountFunctor {
CellAddressCheckFunctor(JSCell* candidate)
: candidate(candidate)
{
}
IterationStatus NODELETE operator()(HeapCell* cell, HeapCell::Kind) const
{
if (cell == candidate) {
found = true;
return IterationStatus::Done;
}
return IterationStatus::Continue;
}
JSCell* candidate;
mutable bool found { false };
};
bool VMInspector::isValidCell(Heap* heap, JSCell* candidate)
{
HeapIterationScope iterationScope(*heap);
CellAddressCheckFunctor functor(candidate);
heap->objectSpace().forEachLiveCell(iterationScope, functor);
return functor.found;
}
bool VMInspector::isValidCodeBlock(VM* vm, CodeBlock* candidate)
{
if (!ensureCurrentThreadOwnsJSLock(vm))
return false;
struct CodeBlockValidationFunctor {
CodeBlockValidationFunctor(CodeBlock* candidate)
: candidate(candidate)
{
}
void NODELETE operator()(CodeBlock* codeBlock) const
{
if (codeBlock == candidate)
found = true;
}
CodeBlock* candidate;
mutable bool found { false };
};
CodeBlockValidationFunctor functor(candidate);
vm->heap.forEachCodeBlock(functor);
return functor.found;
}
CodeBlock* VMInspector::codeBlockForFrame(VM* vm, CallFrame* topCallFrame, unsigned frameNumber)
{
if (!ensureCurrentThreadOwnsJSLock(vm))
return nullptr;
if (!topCallFrame)
return nullptr;
struct FetchCodeBlockFunctor {
public:
FetchCodeBlockFunctor(unsigned targetFrameNumber)
: targetFrame(targetFrameNumber)
{
}
IterationStatus NODELETE operator()(StackVisitor& visitor) const
{
auto currentFrame = nextFrame++;
if (currentFrame == targetFrame) {
codeBlock = visitor->codeBlock();
return IterationStatus::Done;
}
return IterationStatus::Continue;
}
unsigned targetFrame;
mutable unsigned nextFrame { 0 };
mutable CodeBlock* codeBlock { nullptr };
};
FetchCodeBlockFunctor functor(frameNumber);
StackVisitor::visit(topCallFrame, *vm, functor);
return functor.codeBlock;
}
class DumpFrameFunctor {
public:
enum Action {
DumpOne,
DumpAll
};
DumpFrameFunctor(Action action, unsigned framesToSkip)
: m_action(action)
, m_framesToSkip(framesToSkip)
{
}
IterationStatus operator()(StackVisitor& visitor) const
{
m_currentFrame++;
if (m_currentFrame > m_framesToSkip) {
visitor->dump(WTF::dataFile(), Indenter(2), [&] (PrintStream& out) {
out.print("[", (m_currentFrame - m_framesToSkip - 1), "] ");
});
}
if (m_action == DumpOne && m_currentFrame > m_framesToSkip)
return IterationStatus::Done;
return IterationStatus::Continue;
}
private:
Action m_action;
unsigned m_framesToSkip;
mutable unsigned m_currentFrame { 0 };
};
void VMInspector::dumpCallFrame(VM* vm, CallFrame* callFrame, unsigned framesToSkip)
{
if (!ensureCurrentThreadOwnsJSLock(vm))
return;
DumpFrameFunctor functor(DumpFrameFunctor::DumpOne, framesToSkip);
StackVisitor::visit(callFrame, *vm, functor);
}
SUPPRESS_ASAN void VMInspector::dumpRegisters(CallFrame* callFrame)
{
VM* vmPtr = vmForCallFrame(callFrame);
if (!vmPtr) {
dataLogLn("Cannot find callFrame on any VM stack.");
return;
}
VM& vm = *vmPtr;
auto valueAsString = [&] (JSValue v) -> CString {
if (!v.isCell() || VMInspector::isValidCell(&vm.heap, reinterpret_cast(JSValue::encode(v))))
return toCString(v);
return ""_s;
};
CallFrame* topCallFrame = vm.topCallFrame;
CallFrame* nextCallFrame = nullptr;
EntryFrame* entryFrame = nullptr;
// Check if frame is an entryFrame.
entryFrame = vm.topEntryFrame;
while (entryFrame) {
if (entryFrame == std::bit_cast(callFrame)) {
dataLogLn("CallFrame ", RawPointer(callFrame), " is an EntryFrame.");
auto* entryRecord = vmEntryRecord(entryFrame);
dataLogLn(" previous entryFrame: ", RawPointer(entryRecord->prevTopEntryFrame()));
dataLogLn(" previous topCallFrame: ", RawPointer(entryRecord->prevTopCallFrame()));
return;
}
auto* entryRecord = vmEntryRecord(entryFrame);
entryFrame = entryRecord->prevTopEntryFrame();
}
// Find entryFrame and next frame.
if (topCallFrame) {
StackVisitor::visit(topCallFrame, vm, [&] (StackVisitor& visitor) {
if (callFrame == visitor->callFrame()) {
entryFrame = visitor->entryFrame();
return IterationStatus::Done;
}
nextCallFrame = visitor->callFrame();
return IterationStatus::Continue;
});
}
// Dumping from low memory to high memory.
JSCell* owner = callFrame->codeOwnerCell();
CodeBlock* codeBlock = dynamicDowncast(owner);
unsigned numCalleeLocals = codeBlock ? codeBlock->numCalleeLocals() : 0;
unsigned numVars = codeBlock ? codeBlock->numVars() : 0;
bool isWasm = false;
#if ENABLE(WEBASSEMBLY)
isWasm = owner->inherits();
#endif
const Register* it;
const Register* callFrameTop = callFrame->registers();
const Register* startOfLocals = callFrameTop - numCalleeLocals;
const Register* startOfVars = callFrameTop - numVars;
if (nextCallFrame)
it = nextCallFrame->registers() + static_cast(CallFrameSlot::thisArgument);
else
it = startOfLocals;
int registerNumber = it - callFrame->registers();
const char* frameType = isWasm ? "Wasm" : codeBlock ? "JS" : "native";
dataLogF("Registers for %s frame 0x%llx (entryFrame ", frameType, (long long)callFrame);
if (entryFrame)
dataLogF("0x%llx):\n", (long long)entryFrame);
else
dataLogF("unknown):\n");
dataLogF("-----------------------------------------------------------------------------\n");
dataLogF(" VirtualRegister : address value\n");
if (codeBlock) {
dataLogF("---------------------------------------------------- Outgoing Args + Misc ---\n");
while (it < startOfVars) {
JSValue v = it->jsValue();
String name = codeBlock->nameForRegister(VirtualRegister(registerNumber));
dataLogF("% 4d %-16s : %10p 0x%llx %s\n", registerNumber++, name.ascii().data(), it++, (long long)JSValue::encode(v), valueAsString(v).data());
}
dataLogF("--------------------------------------------------------------- Variables ---\n");
size_t numberOfCalleeSaveSlots = CodeBlock::calleeSaveSpaceAsVirtualRegisters(*codeBlock->jitCode()->calleeSaveRegisters());
const Register* endOfCalleeSaves = callFrameTop - numberOfCalleeSaveSlots;
while (it < endOfCalleeSaves) {
JSValue v = it->jsValue();
String name = codeBlock->nameForRegister(VirtualRegister(registerNumber));
dataLogF("% 4d %-16s : %10p 0x%llx %s\n", registerNumber++, name.ascii().data(), it++, (long long)JSValue::encode(v), valueAsString(v).data());
}
dataLogF("------------------------------------------------------------ Callee Saves ---\n");
while (it != callFrameTop) {
JSValue v = it->jsValue();
dataLogF("% 4d %-16s : %10p 0x%llx %s\n", registerNumber++, "CalleeSaveReg", it++, (long long)JSValue::encode(v), valueAsString(v).data());
}
}
dataLogF("-------------------------------------------------------- CallFrame Header ---\n");
dataLogF("% 4d CallerFrame : %10p %p \n", registerNumber++, it++, callFrame->callerFrame());
if constexpr (isARM64E())
dataLogF("% 4d ReturnPC : %10p %p (pac signed %p) \n", registerNumber++, it++, callFrame->returnPCForInspection(), callFrame->rawReturnPC());
else
dataLogF("% 4d ReturnPC : %10p %p \n", registerNumber++, it++, callFrame->returnPCForInspection());
dataLogF("% 4d CodeBlock : %10p 0x%llx ", registerNumber++, it++, (long long)codeBlock);
dataLogLn(codeBlock);
long long calleeBits = (long long)callFrame->callee().rawPtr();
auto calleeString = valueAsString(it->jsValue());
dataLogF("% 4d Callee : %10p 0x%llx %s\n", registerNumber++, it++, calleeBits, calleeString.data());
StackVisitor::visit(callFrame, vm, [&] (StackVisitor& visitor) {
if (visitor->callFrame() == callFrame) {
auto lineColumn = visitor->computeLineAndColumn();
dataLogF("% 2d.1 ReturnVPC : %10p %d (line %d)\n", registerNumber, it, visitor->bytecodeIndex().offset(), lineColumn.line);
return IterationStatus::Done;
}
return IterationStatus::Continue;
});
dataLogF("% 2d.2 ArgumentCount : %10p %lu \n", registerNumber++, it++, (unsigned long) callFrame->argumentCount());
dataLogF("--------------------------------------------------------------- Arguments ---\n");
const Register* bottom = callFrame->registers() + (CallFrameSlot::thisArgument + callFrame->argumentCount());
while (it jsValue();
String name = codeBlock ? codeBlock->nameForRegister(VirtualRegister(registerNumber)) : emptyString();
dataLogF("% 4d %-16s : %10p 0x%llx %s\n", registerNumber++, name.ascii().data(), it++, (long long)JSValue::encode(v), valueAsString(v).data());
}
dataLogF("--------------------------------------------------------------------- End ---\n");
}
void VMInspector::dumpStack(VM* vm, CallFrame* topCallFrame, unsigned framesToSkip)
{
if (!ensureCurrentThreadOwnsJSLock(vm))
return;
if (!topCallFrame)
return;
DumpFrameFunctor functor(DumpFrameFunctor::DumpAll, framesToSkip);
StackVisitor::visit(topCallFrame, *vm, functor);
}
void VMInspector::dumpValue(JSValue value)
{
dataLogLn(value);
}
void VMInspector::dumpCellMemory(JSCell* cell)
{
dumpCellMemoryToStream(cell, WTF::dataFile());
}
class IndentationScope {
public:
IndentationScope(unsigned& indentation)
: m_indentation(indentation)
{
++m_indentation;
}
~IndentationScope()
{
--m_indentation;
}
private:
unsigned& m_indentation;
};
void VMInspector::dumpCellMemoryToStream(JSCell* cell, PrintStream& out)
{
StructureID structureID = cell->structureID();
Structure* structure = cell->structure();
IndexingType indexingTypeAndMisc = cell->indexingTypeAndMisc();
IndexingType indexingType = structure->indexingType();
IndexingType indexingMode = structure->indexingMode();
JSType type = cell->type();
TypeInfo::InlineTypeFlags inlineTypeFlags = cell->inlineTypeFlags();
CellState cellState = cell->cellState();
size_t cellSize = cell->cellSize();
size_t slotCount = cellSize / sizeof(EncodedJSValue);
EncodedJSValue* slots = std::bit_cast(cell);
unsigned indentation = 0;
auto indent = [&] {
for (unsigned i = 0 ; i < indentation; ++i)
out.print(" ");
};
#define INDENT indent(),
auto dumpSlot = [&] (EncodedJSValue* slots, unsigned index, const char* label = nullptr) {
out.print("[", index, "] ", format("%p : 0x%016" PRIx64, &slots[index], slots[index]));
if (label)
out.print(" ", label);
out.print("\n");
};
out.printf("\n", cell, cell->className().characters());
IndentationScope scope(indentation);
INDENT dumpSlot(slots, 0, "header");
{
IndentationScope scope(indentation);
INDENT out.println("structureID ", format("%d 0x%" PRIx32, structureID, structureID), " structure ", RawPointer(structure));
INDENT out.println("indexingTypeAndMisc ", format("%d 0x%" PRIx8, indexingTypeAndMisc, indexingTypeAndMisc), " ", IndexingTypeDump(indexingMode));
INDENT out.println("type ", format("%d 0x%" PRIx8, type, type));
INDENT out.println("flags ", format("%d 0x%" PRIx8, inlineTypeFlags, inlineTypeFlags));
INDENT out.println("cellState ", format("%d", cellState));
}
unsigned slotIndex = 1;
if (cell->isObject()) {
JSObject* obj = static_cast(const_cast(cell));
Butterfly* butterfly = obj->butterfly();
size_t butterflySize = obj->butterflyTotalSize();
INDENT dumpSlot(slots, slotIndex, "butterfly");
slotIndex++;
if (butterfly) {
IndentationScope scope(indentation);
bool hasIndexingHeader = structure->hasIndexingHeader(cell);
bool hasAnyArrayStorage = JSC::hasAnyArrayStorage(indexingType);
size_t preCapacity = obj->butterflyPreCapacity();
size_t propertyCapacity = structure->outOfLineCapacity();
void* base = hasIndexingHeader
? butterfly->base(preCapacity, propertyCapacity)
: butterfly->base(structure);
unsigned publicLength = butterfly->publicLength();
unsigned vectorLength = butterfly->vectorLength();
size_t butterflyCellSize = MarkedSpace::optimalSizeFor(butterflySize);
size_t endOfIndexedPropertiesIndex = butterflySize / sizeof(EncodedJSValue);
size_t endOfButterflyIndex = butterflyCellSize / sizeof(EncodedJSValue);
INDENT out.println("base ", RawPointer(base));
INDENT out.println("hasIndexingHeader ", (hasIndexingHeader ? "YES" : "NO"), " hasAnyArrayStorage ", (hasAnyArrayStorage ? "YES" : "NO"));
if (hasIndexingHeader) {
INDENT out.print("publicLength ", publicLength, " vectorLength ", vectorLength);
if (hasAnyArrayStorage)
out.print(" indexBias ", butterfly->arrayStorage()->m_indexBias);
out.print("\n");
}
INDENT out.println("preCapacity ", preCapacity, " propertyCapacity ", propertyCapacity);
unsigned index = 0;
EncodedJSValue* slots = reinterpret_cast(base);
auto asVoidPtr = [] (void* p) {
return p;
};
auto dumpSectionHeader = [&] (const char* name) {
out.println("