/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim: set ts=8 sts=2 et sw=2 tw=80:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "vm/SavedStacks.h"
#include "mozilla/Attributes.h"
#include "mozilla/DebugOnly.h"
#include <algorithm>
#include <utility>
#include "jsapi.h"
#include "jsmath.h"
#include "jsnum.h"
#include "gc/GCContext.h"
#include "gc/HashUtil.h"
#include "js/CharacterEncoding.h"
#include "js/ColumnNumber.h" // JS::LimitedColumnNumberOneOrigin, JS::TaggedColumnNumberOneOrigin
#include "js/ErrorReport.h" // JSErrorBase
#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
#include "js/PropertyAndElement.h" // JS_DefineProperty, JS_GetProperty
#include "js/PropertySpec.h"
#include "js/SavedFrameAPI.h"
#include "js/Stack.h"
#include "js/Vector.h"
#include "util/DifferentialTesting.h"
#include "util/StringBuilder.h"
#include "vm/Compartment.h"
#include "vm/FrameIter.h"
#include "vm/GeckoProfiler.h"
#include "vm/JSScript.h"
#include "vm/Realm.h"
#include "vm/SavedFrame.h"
#include "vm/WrapperObject.h"
#include "debugger/DebugAPI-inl.h"
#include "gc/StableCellHasher-inl.h"
#include "vm/GeckoProfiler-inl.h"
#include "vm/JSContext-inl.h"
using mozilla::AddToHash;
using mozilla::DebugOnly;
using mozilla::Maybe;
using mozilla::Nothing;
using mozilla::Some;
namespace js {
/**
* Maximum number of saved frames returned for an async stack.
*/
const uint32_t ASYNC_STACK_MAX_FRAME_COUNT = 60;
void LiveSavedFrameCache::trace(JSTracer* trc) {
if (!initialized()) {
return;
}
for (
auto* entry = frames->begin(); entry < frames->end(); entry++) {
TraceEdge(trc, &entry->savedFrame,
"LiveSavedFrameCache::frames SavedFrame");
}
}
bool LiveSavedFrameCache::insert(JSContext* cx, FramePtr&& framePtr,
const jsbytecode* pc,
Handle<SavedFrame*> savedFrame) {
MOZ_ASSERT(savedFrame);
MOZ_ASSERT(initialized());
#ifdef DEBUG
// There should not already be an entry for this frame. Checking the full
// stack really slows down some tests, so just check the first and last five
// hundred.
size_t limit = std::min(frames->length() / 2, size_t(500));
for (size_t i = 0; i < limit; i++) {
MOZ_ASSERT(Key(framePtr) != (*frames)[i].key);
MOZ_ASSERT(Key(framePtr) != (*frames)[frames->length() - 1 - i].key);
}
#endif
if (!frames->emplaceBack(framePtr, pc, savedFrame)) {
ReportOutOfMemory(cx);
return false;
}
framePtr.setHasCachedSavedFrame();
return true;
}
void LiveSavedFrameCache::find(JSContext* cx, FramePtr& framePtr,
const jsbytecode* pc,
MutableHandle<SavedFrame*> frame)
const {
MOZ_ASSERT(initialized());
MOZ_ASSERT(framePtr.hasCachedSavedFrame());
// The assertions here check that either 1) frames' hasCachedSavedFrame flags
// accurately indicate the presence of a cache entry for that frame (ignoring
// pc mismatches), or 2) the cache is completely empty, having been flushed
// for a realm mismatch.
// If we flushed the cache due to a realm mismatch, then we shouldn't
// expect to find any frames in the cache.
if (frames->empty()) {
frame.set(nullptr);
return;
}
// All our SavedFrames should be in the same realm. If the last
// entry's SavedFrame's realm doesn't match cx's, flush the cache.
if (frames->back().savedFrame->realm() != cx->realm()) {
#ifdef DEBUG
// Check that they are, indeed, all in the same realm.
auto realm = frames->back().savedFrame->realm();
for (
const auto& f : (*frames)) {
MOZ_ASSERT(realm == f.savedFrame->realm());
}
#endif
frames->clear();
frame.set(nullptr);
return;
}
Key key(framePtr);
while (key != frames->back().key) {
MOZ_ASSERT(frames->back().savedFrame->realm() == cx->realm());
// framePtr must have an entry, but apparently it's below this one on the
// stack; frames->back() must correspond to a frame younger than framePtr's.
// SavedStacks::insertFrames is going to push new cache entries for
// everything younger than framePtr, so this entry should be popped.
frames->popBack();
// If the frame's bit was set, the frame should always have an entry in
// the cache. (If we purged the entire cache because its SavedFrames had
// been captured for a different realm, then we would have
// returned early above.)
MOZ_RELEASE_ASSERT(!frames->empty());
}
// The youngest valid frame may have run some code, so its current pc may
// not match its cache entry's pc. In this case, just treat it as a miss. No
// older frame has executed any code; it would have been necessary to pop
// this frame for that to happen, but this frame's bit is set.
if (pc != frames->back().pc) {
frames->popBack();
frame.set(nullptr);
return;
}
frame.set(frames->back().savedFrame);
}
void LiveSavedFrameCache::findWithoutInvalidation(
const FramePtr& framePtr, MutableHandle<SavedFrame*> frame)
const {
MOZ_ASSERT(initialized());
MOZ_ASSERT(framePtr.hasCachedSavedFrame());
Key key(framePtr);
for (
auto& entry : (*frames)) {
if (entry.key == key) {
frame.set(entry.savedFrame);
return;
}
}
frame.set(nullptr);
}
struct MOZ_STACK_CLASS SavedFrame::Lookup {
Lookup(JSAtom* source, uint32_t sourceId, uint32_t line,
JS::TaggedColumnNumberOneOrigin column, JSAtom* functionDisplayName,
JSAtom* asyncCause, SavedFrame* parent, JSPrincipals* principals,
bool mutedErrors,
const Maybe<LiveSavedFrameCache::FramePtr>& framePtr = Nothing(),
jsbytecode* pc = nullptr, Activation* activation = nullptr)
: source(source),
sourceId(sourceId),
line(line),
column(column),
functionDisplayName(functionDisplayName),
asyncCause(asyncCause),
parent(parent),
principals(principals),
mutedErrors(mutedErrors),
framePtr(framePtr),
pc(pc),
activation(activation) {
MOZ_ASSERT(source);
MOZ_ASSERT_IF(framePtr.isSome(), activation);
if (js::SupportDifferentialTesting()) {
this->column = JS::TaggedColumnNumberOneOrigin::forDifferentialTesting();
}
}
explicit Lookup(SavedFrame& savedFrame)
: source(savedFrame.getSource()),
sourceId(savedFrame.getSourceId()),
line(savedFrame.getLine()),
column(savedFrame.getColumn()),
functionDisplayName(savedFrame.getFunctionDisplayName()),
asyncCause(savedFrame.getAsyncCause()),
parent(savedFrame.getParent()),
principals(savedFrame.getPrincipals()),
mutedErrors(savedFrame.getMutedErrors()),
framePtr(Nothing()),
pc(nullptr),
activation(nullptr) {
MOZ_ASSERT(source);
}
JSAtom* source;
uint32_t sourceId;
// Line number (1-origin).
uint32_t line;
// Columm number in UTF-16 code units.
JS::TaggedColumnNumberOneOrigin column;
JSAtom* functionDisplayName;
JSAtom* asyncCause;
SavedFrame* parent;
JSPrincipals* principals;
bool mutedErrors;
// These are used only by the LiveSavedFrameCache and not used for identity or
// hashing.
Maybe<LiveSavedFrameCache::FramePtr> framePtr;
jsbytecode* pc;
Activation* activation;
void trace(JSTracer* trc) {
TraceRoot(trc, &source,
"SavedFrame::Lookup::source");
TraceNullableRoot(trc, &functionDisplayName,
"SavedFrame::Lookup::functionDisplayName");
TraceNullableRoot(trc, &asyncCause,
"SavedFrame::Lookup::asyncCause");
TraceNullableRoot(trc, &parent,
"SavedFrame::Lookup::parent");
}
};
using GCLookupVector =
GCVector<SavedFrame::Lookup, ASYNC_STACK_MAX_FRAME_COUNT>;
template <
class Wrapper>
class WrappedPtrOperations<SavedFrame::Lookup, Wrapper> {
const SavedFrame::Lookup& value()
const {
return static_cast<
const Wrapper*>(
this)->get();
}
public:
JSAtom* source() {
return value().source; }
uint32_t sourceId() {
return value().sourceId; }
uint32_t line() {
return value().line; }
JS::TaggedColumnNumberOneOrigin column() {
return value().column; }
JSAtom* functionDisplayName() {
return value().functionDisplayName; }
JSAtom* asyncCause() {
return value().asyncCause; }
SavedFrame* parent() {
return value().parent; }
JSPrincipals* principals() {
return value().principals; }
bool mutedErrors() {
return value().mutedErrors; }
Maybe<LiveSavedFrameCache::FramePtr> framePtr() {
return value().framePtr; }
jsbytecode* pc() {
return value().pc; }
Activation* activation() {
return value().activation; }
};
template <
typename Wrapper>
class MutableWrappedPtrOperations<SavedFrame::Lookup, Wrapper>
:
public WrappedPtrOperations<SavedFrame::Lookup, Wrapper> {
SavedFrame::Lookup& value() {
return static_cast<Wrapper*>(
this)->get(); }
public:
void setParent(SavedFrame* parent) { value().parent = parent; }
void setAsyncCause(Handle<JSAtom*> asyncCause) {
value().asyncCause = asyncCause;
}
};
/* static */
bool SavedFrame::HashPolicy::maybeGetHash(
const Lookup& l,
HashNumber* hashOut) {
HashNumber parentHash;
if (!SavedFramePtrHasher::maybeGetHash(l.parent, &parentHash)) {
return false;
}
*hashOut = calculateHash(l, parentHash);
return true;
}
/* static */
bool SavedFrame::HashPolicy::ensureHash(
const Lookup& l, HashNumber* hashOut) {
HashNumber parentHash;
if (!SavedFramePtrHasher::ensureHash(l.parent, &parentHash)) {
return false;
}
*hashOut = calculateHash(l, parentHash);
return true;
}
/* static */
HashNumber SavedFrame::HashPolicy::hash(
const Lookup& lookup) {
return calculateHash(lookup, SavedFramePtrHasher::hash(lookup.parent));
}
/* static */
HashNumber SavedFrame::HashPolicy::calculateHash(
const Lookup& lookup,
HashNumber parentHash) {
JS::AutoCheckCannotGC nogc;
// Assume that we can take line mod 2^32 without losing anything of
// interest. If that assumption changes, we'll just need to start with 0
// and add another overload of AddToHash with more arguments.
return AddToHash(lookup.line, lookup.column.rawValue(), lookup.source,
lookup.functionDisplayName, lookup.asyncCause,
lookup.mutedErrors, parentHash,
JSPrincipalsPtrHasher::hash(lookup.principals));
}
/* static */
bool SavedFrame::HashPolicy::match(SavedFrame* existing,
const Lookup& lookup) {
MOZ_ASSERT(existing);
if (existing->getLine() != lookup.line) {
return false;
}
if (existing->getColumn() != lookup.column) {
return false;
}
if (existing->getParent() != lookup.parent) {
return false;
}
if (existing->getPrincipals() != lookup.principals) {
return false;
}
JSAtom* source = existing->getSource();
if (source != lookup.source) {
return false;
}
JSAtom* functionDisplayName = existing->getFunctionDisplayName();
if (functionDisplayName != lookup.functionDisplayName) {
return false;
}
JSAtom* asyncCause = existing->getAsyncCause();
if (asyncCause != lookup.asyncCause) {
return false;
}
return true;
}
/* static */
void SavedFrame::HashPolicy::rekey(Key& key,
const Key& newKey) {
key = newKey;
}
/* static */
bool SavedFrame::finishSavedFrameInit(JSContext* cx, HandleObject ctor,
HandleObject proto) {
return FreezeObject(cx, proto);
}
static const JSClassOps SavedFrameClassOps = {
nullptr,
// addProperty
nullptr,
// delProperty
nullptr,
// enumerate
nullptr,
// newEnumerate
nullptr,
// resolve
nullptr,
// mayResolve
SavedFrame::finalize,
// finalize
nullptr,
// call
nullptr,
// construct
nullptr,
// trace
};
const ClassSpec SavedFrame::classSpec_ = {
GenericCreateConstructor<SavedFrame::construct, 0, gc::AllocKind::FUNCTION>,
GenericCreatePrototype<SavedFrame>,
SavedFrame::staticFunctions,
nullptr,
SavedFrame::protoFunctions,
SavedFrame::protoAccessors,
SavedFrame::finishSavedFrameInit,
ClassSpec::DontDefineConstructor,
};
/* static */ const JSClass SavedFrame::class_ = {
"SavedFrame",
JSCLASS_HAS_RESERVED_SLOTS(SavedFrame::JSSLOT_COUNT) |
JSCLASS_HAS_CACHED_PROTO(JSProto_SavedFrame) |
JSCLASS_FOREGROUND_FINALIZE,
&SavedFrameClassOps,
&SavedFrame::classSpec_,
};
const JSClass SavedFrame::protoClass_ = {
"SavedFrame.prototype",
JSCLASS_HAS_CACHED_PROTO(JSProto_SavedFrame),
JS_NULL_CLASS_OPS,
&SavedFrame::classSpec_,
};
/* static */ const JSFunctionSpec SavedFrame::staticFunctions[] = {
JS_FS_END,
};
/* static */ const JSFunctionSpec SavedFrame::protoFunctions[] = {
JS_FN(
"constructor", SavedFrame::construct, 0, 0),
JS_FN(
"toString", SavedFrame::toStringMethod, 0, 0),
JS_FS_END,
};
/* static */ const JSPropertySpec SavedFrame::protoAccessors[] = {
JS_PSG(
"source", SavedFrame::sourceProperty, 0),
JS_PSG(
"sourceId", SavedFrame::sourceIdProperty, 0),
JS_PSG(
"line", SavedFrame::lineProperty, 0),
JS_PSG(
"column", SavedFrame::columnProperty, 0),
JS_PSG(
"functionDisplayName", SavedFrame::functionDisplayNameProperty, 0),
JS_PSG(
"asyncCause", SavedFrame::asyncCauseProperty, 0),
JS_PSG(
"asyncParent", SavedFrame::asyncParentProperty, 0),
JS_PSG(
"parent", SavedFrame::parentProperty, 0),
JS_STRING_SYM_PS(toStringTag,
"SavedFrame", JSPROP_READONLY),
JS_PS_END,
};
/* static */
void SavedFrame::finalize(JS::GCContext* gcx, JSObject* obj) {
MOZ_ASSERT(gcx->onMainThread());
JSPrincipals* p = obj->as<SavedFrame>().getPrincipals();
if (p) {
JSRuntime* rt = obj->runtimeFromMainThread();
JS_DropPrincipals(rt->mainContextFromOwnThread(), p);
}
}
JSAtom* SavedFrame::getSource() {
const Value& v = getReservedSlot(JSSLOT_SOURCE);
JSString* s = v.toString();
return &s->asAtom();
}
uint32_t SavedFrame::getSourceId() {
const Value& v = getReservedSlot(JSSLOT_SOURCEID);
return v.toPrivateUint32();
}
uint32_t SavedFrame::getLine() {
const Value& v = getReservedSlot(JSSLOT_LINE);
return v.toPrivateUint32();
}
JS::TaggedColumnNumberOneOrigin SavedFrame::getColumn() {
const Value& v = getReservedSlot(JSSLOT_COLUMN);
return JS::TaggedColumnNumberOneOrigin::fromRaw(v.toPrivateUint32());
}
JSAtom* SavedFrame::getFunctionDisplayName() {
const Value& v = getReservedSlot(JSSLOT_FUNCTIONDISPLAYNAME);
if (v.isNull()) {
return nullptr;
}
JSString* s = v.toString();
return &s->asAtom();
}
JSAtom* SavedFrame::getAsyncCause() {
const Value& v = getReservedSlot(JSSLOT_ASYNCCAUSE);
if (v.isNull()) {
return nullptr;
}
JSString* s = v.toString();
return &s->asAtom();
}
SavedFrame* SavedFrame::getParent()
const {
const Value& v = getReservedSlot(JSSLOT_PARENT);
return v.isObject() ? &v.toObject().as<SavedFrame>() : nullptr;
}
JSPrincipals* SavedFrame::getPrincipals() {
const Value& v = getReservedSlot(JSSLOT_PRINCIPALS);
if (v.isUndefined()) {
return nullptr;
}
return reinterpret_cast<JSPrincipals*>(uintptr_t(v.toPrivate()) & ~0b1);
}
bool SavedFrame::getMutedErrors() {
const Value& v = getReservedSlot(JSSLOT_PRINCIPALS);
if (v.isUndefined()) {
return true;
}
return bool(uintptr_t(v.toPrivate()) & 0b1);
}
void SavedFrame::initSource(JSAtom* source) {
MOZ_ASSERT(source);
initReservedSlot(JSSLOT_SOURCE, StringValue(source));
}
void SavedFrame::initSourceId(uint32_t sourceId) {
initReservedSlot(JSSLOT_SOURCEID, PrivateUint32Value(sourceId));
}
void SavedFrame::initLine(uint32_t line) {
initReservedSlot(JSSLOT_LINE, PrivateUint32Value(line));
}
void SavedFrame::initColumn(JS::TaggedColumnNumberOneOrigin column) {
if (js::SupportDifferentialTesting()) {
column = JS::TaggedColumnNumberOneOrigin::forDifferentialTesting();
}
initReservedSlot(JSSLOT_COLUMN, PrivateUint32Value(column.rawValue()));
}
void SavedFrame::initPrincipalsAndMutedErrors(JSPrincipals* principals,
bool mutedErrors) {
if (principals) {
JS_HoldPrincipals(principals);
}
initPrincipalsAlreadyHeldAndMutedErrors(principals, mutedErrors);
}
void SavedFrame::initPrincipalsAlreadyHeldAndMutedErrors(
JSPrincipals* principals,
bool mutedErrors) {
MOZ_ASSERT_IF(principals, principals->refcount > 0);
uintptr_t ptr = uintptr_t(principals) | mutedErrors;
initReservedSlot(JSSLOT_PRINCIPALS,
PrivateValue(
reinterpret_cast<
void*>(ptr)));
}
void SavedFrame::initFunctionDisplayName(JSAtom* maybeName) {
initReservedSlot(JSSLOT_FUNCTIONDISPLAYNAME,
maybeName ? StringValue(maybeName) : NullValue());
}
void SavedFrame::initAsyncCause(JSAtom* maybeCause) {
initReservedSlot(JSSLOT_ASYNCCAUSE,
maybeCause ? StringValue(maybeCause) : NullValue());
}
void SavedFrame::initParent(SavedFrame* maybeParent) {
initReservedSlot(JSSLOT_PARENT, ObjectOrNullValue(maybeParent));
}
void SavedFrame::initFromLookup(JSContext* cx, Handle<Lookup> lookup) {
// Make sure any atoms used in the lookup are marked in the current zone.
// Normally we would try to keep these mark bits up to date around the
// points where the context moves between compartments, but Lookups live on
// the stack (where the atoms are kept alive regardless) and this is a
// more convenient pinchpoint.
if (lookup.source()) {
cx->markAtom(lookup.source());
}
if (lookup.functionDisplayName()) {
cx->markAtom(lookup.functionDisplayName());
}
if (lookup.asyncCause()) {
cx->markAtom(lookup.asyncCause());
}
initSource(lookup.source());
initSourceId(lookup.sourceId());
initLine(lookup.line());
initColumn(lookup.column());
initFunctionDisplayName(lookup.functionDisplayName());
initAsyncCause(lookup.asyncCause());
initParent(lookup.parent());
initPrincipalsAndMutedErrors(lookup.principals(), lookup.mutedErrors());
}
/* static */
SavedFrame* SavedFrame::create(JSContext* cx) {
Rooted<GlobalObject*> global(cx, cx->global());
cx->check(global);
// Ensure that we don't try to capture the stack again in the
// `SavedStacksMetadataBuilder` for this new SavedFrame object, and
// accidentally cause O(n^2) behavior.
SavedStacks::AutoReentrancyGuard guard(cx->realm()->savedStacks());
RootedObject proto(cx,
GlobalObject::getOrCreateSavedFramePrototype(cx, global));
if (!proto) {
return nullptr;
}
cx->check(proto);
return NewTenuredObjectWithGivenProto<SavedFrame>(cx, proto);
}
bool SavedFrame::isSelfHosted(JSContext* cx) {
JSAtom* source = getSource();
return source == cx->names().self_hosted_;
}
bool SavedFrame::isWasm() {
return getColumn().isWasmFunctionIndex(); }
uint32_t SavedFrame::wasmFuncIndex() {
return getColumn().toWasmFunctionIndex().value();
}
uint32_t SavedFrame::wasmBytecodeOffset() {
MOZ_ASSERT(isWasm());
return getLine();
}
/* static */
bool SavedFrame::construct(JSContext* cx,
unsigned argc, Value* vp) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR,
"SavedFrame");
return false;
}
static bool SavedFrameSubsumedByPrincipals(JSContext* cx,
JSPrincipals* principals,
Handle<SavedFrame*> frame) {
auto subsumes = cx->runtime()->securityCallbacks->subsumes;
if (!subsumes) {
return true;
}
MOZ_ASSERT(!ReconstructedSavedFramePrincipals::is(principals));
auto framePrincipals = frame->getPrincipals();
// Handle SavedFrames that have been reconstructed from stacks in a heap
// snapshot.
if (framePrincipals == &ReconstructedSavedFramePrincipals::IsSystem) {
return cx->runningWithTrustedPrincipals();
}
if (framePrincipals == &ReconstructedSavedFramePrincipals::IsNotSystem) {
return true;
}
return subsumes(principals, framePrincipals);
}
// Return the first SavedFrame in the chain that starts with |frame| whose
// for which the given match function returns true. If there is no such frame,
// return nullptr. |skippedAsync| is set to true if any of the skipped frames
// had the |asyncCause| property set, otherwise it is explicitly set to false.
template <
typename Matcher>
static SavedFrame* GetFirstMatchedFrame(JSContext* cx, JSPrincipals* principals,
Matcher& matches,
Handle<SavedFrame*> frame,
JS::SavedFrameSelfHosted selfHosted,
bool& skippedAsync) {
skippedAsync =
false;
Rooted<SavedFrame*> rootedFrame(cx, frame);
while (rootedFrame) {
if ((selfHosted == JS::SavedFrameSelfHosted::Include ||
!rootedFrame->isSelfHosted(cx)) &&
matches(cx, principals, rootedFrame)) {
return rootedFrame;
}
if (rootedFrame->getAsyncCause()) {
skippedAsync =
true;
}
rootedFrame = rootedFrame->getParent();
}
return nullptr;
}
// Return the first SavedFrame in the chain that starts with |frame| whose
// principals are subsumed by |principals|, according to |subsumes|. If there is
// no such frame, return nullptr. |skippedAsync| is set to true if any of the
// skipped frames had the |asyncCause| property set, otherwise it is explicitly
// set to false.
static SavedFrame* GetFirstSubsumedFrame(JSContext* cx,
JSPrincipals* principals,
Handle<SavedFrame*> frame,
JS::SavedFrameSelfHosted selfHosted,
bool& skippedAsync) {
return GetFirstMatchedFrame(cx, principals, SavedFrameSubsumedByPrincipals,
frame, selfHosted, skippedAsync);
}
JS_PUBLIC_API JSObject* GetFirstSubsumedSavedFrame(
JSContext* cx, JSPrincipals* principals, HandleObject savedFrame,
JS::SavedFrameSelfHosted selfHosted) {
if (!savedFrame) {
return nullptr;
}
auto subsumes = cx->runtime()->securityCallbacks->subsumes;
if (!subsumes) {
return nullptr;
}
auto matcher = [subsumes](JSContext* cx, JSPrincipals* principals,
Handle<SavedFrame*> frame) ->
bool {
return subsumes(principals, frame->getPrincipals());
};
bool skippedAsync;
Rooted<SavedFrame*> frame(cx, &savedFrame->as<SavedFrame>());
return GetFirstMatchedFrame(cx, principals, matcher, frame, selfHosted,
skippedAsync);
}
[[nodiscard]]
static bool SavedFrame_checkThis(JSContext* cx, CallArgs& args,
const char* fnName,
MutableHandleObject frame) {
const Value& thisValue = args.thisv();
if (!thisValue.isObject()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_OBJECT_REQUIRED,
InformalValueTypeName(thisValue));
return false;
}
if (!thisValue.toObject().canUnwrapAs<SavedFrame>()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_INCOMPATIBLE_PROTO, SavedFrame::class_.name,
fnName,
"object");
return false;
}
// Now set "frame" to the actual object we were invoked in (which may be a
// wrapper), not the unwrapped version. Consumers will need to know what
// that original object was, and will do principal checks as needed.
frame.set(&thisValue.toObject());
return true;
}
// Get the SavedFrame * from the current this value and handle any errors that
// might occur therein.
//
// These parameters must already exist when calling this macro:
// - JSContext* cx
// - unsigned argc
// - Value* vp
// - const char* fnName
// These parameters will be defined after calling this macro:
// - CallArgs args
// - Rooted<SavedFrame*> frame (will be non-null)
#define THIS_SAVEDFRAME(cx, argc, vp, fnName, args, frame) \
CallArgs args = CallArgsFromVp(argc, vp); \
RootedObject frame(cx); \
if (!SavedFrame_checkThis(cx, args, fnName, &frame))
return false;
}
/* namespace js */
js::SavedFrame* js::UnwrapSavedFrame(JSContext* cx, JSPrincipals* principals,
HandleObject obj,
JS::SavedFrameSelfHosted selfHosted,
bool& skippedAsync) {
if (!obj) {
return nullptr;
}
Rooted<SavedFrame*> frame(cx, obj->maybeUnwrapAs<SavedFrame>());
if (!frame) {
return nullptr;
}
return GetFirstSubsumedFrame(cx, principals, frame, selfHosted, skippedAsync);
}
namespace JS {
JS_PUBLIC_API SavedFrameResult GetSavedFrameSource(
JSContext* cx, JSPrincipals* principals, HandleObject savedFrame,
MutableHandleString sourcep,
SavedFrameSelfHosted selfHosted
/* = SavedFrameSelfHosted::Include */) {
js::AssertHeapIsIdle();
CHECK_THREAD(cx);
MOZ_RELEASE_ASSERT(cx->realm());
{
bool skippedAsync;
Rooted<js::SavedFrame*> frame(
cx,
UnwrapSavedFrame(cx, principals, savedFrame, selfHosted, skippedAsync));
if (!frame) {
sourcep.set(cx->runtime()->emptyString);
return SavedFrameResult::AccessDenied;
}
sourcep.set(frame->getSource());
}
if (sourcep->isAtom()) {
cx->markAtom(&sourcep->asAtom());
}
return SavedFrameResult::Ok;
}
JS_PUBLIC_API SavedFrameResult GetSavedFrameSourceId(
JSContext* cx, JSPrincipals* principals, HandleObject savedFrame,
uint32_t* sourceIdp,
SavedFrameSelfHosted selfHosted
/* = SavedFrameSelfHosted::Include */) {
js::AssertHeapIsIdle();
CHECK_THREAD(cx);
MOZ_RELEASE_ASSERT(cx->realm());
bool skippedAsync;
Rooted<js::SavedFrame*> frame(cx, UnwrapSavedFrame(cx, principals, savedFrame,
selfHosted, skippedAsync));
if (!frame) {
*sourceIdp = 0;
return SavedFrameResult::AccessDenied;
}
*sourceIdp = frame->getSourceId();
return SavedFrameResult::Ok;
}
JS_PUBLIC_API SavedFrameResult GetSavedFrameLine(
JSContext* cx, JSPrincipals* principals, HandleObject savedFrame,
uint32_t* linep,
SavedFrameSelfHosted selfHosted
/* = SavedFrameSelfHosted::Include */) {
js::AssertHeapIsIdle();
CHECK_THREAD(cx);
MOZ_RELEASE_ASSERT(cx->realm());
MOZ_ASSERT(linep);
bool skippedAsync;
Rooted<js::SavedFrame*> frame(cx, UnwrapSavedFrame(cx, principals, savedFrame,
selfHosted, skippedAsync));
if (!frame) {
*linep = 0;
return SavedFrameResult::AccessDenied;
}
*linep = frame->getLine();
return SavedFrameResult::Ok;
}
JS_PUBLIC_API SavedFrameResult GetSavedFrameColumn(
JSContext* cx, JSPrincipals* principals, HandleObject savedFrame,
JS::TaggedColumnNumberOneOrigin* columnp,
SavedFrameSelfHosted selfHosted
/* = SavedFrameSelfHosted::Include */) {
js::AssertHeapIsIdle();
CHECK_THREAD(cx);
MOZ_RELEASE_ASSERT(cx->realm());
MOZ_ASSERT(columnp);
bool skippedAsync;
Rooted<js::SavedFrame*> frame(cx, UnwrapSavedFrame(cx, principals, savedFrame,
selfHosted, skippedAsync));
if (!frame) {
*columnp = JS::TaggedColumnNumberOneOrigin();
return SavedFrameResult::AccessDenied;
}
*columnp = frame->getColumn();
return SavedFrameResult::Ok;
}
JS_PUBLIC_API SavedFrameResult GetSavedFrameFunctionDisplayName(
JSContext* cx, JSPrincipals* principals, HandleObject savedFrame,
MutableHandleString namep,
SavedFrameSelfHosted selfHosted
/* = SavedFrameSelfHosted::Include */) {
js::AssertHeapIsIdle();
CHECK_THREAD(cx);
MOZ_RELEASE_ASSERT(cx->realm());
{
bool skippedAsync;
Rooted<js::SavedFrame*> frame(
cx,
UnwrapSavedFrame(cx, principals, savedFrame, selfHosted, skippedAsync));
if (!frame) {
namep.set(nullptr);
return SavedFrameResult::AccessDenied;
}
namep.set(frame->getFunctionDisplayName());
}
if (namep && namep->isAtom()) {
cx->markAtom(&namep->asAtom());
}
return SavedFrameResult::Ok;
}
JS_PUBLIC_API SavedFrameResult GetSavedFrameAsyncCause(
JSContext* cx, JSPrincipals* principals, HandleObject savedFrame,
MutableHandleString asyncCausep,
SavedFrameSelfHosted unused_
/* = SavedFrameSelfHosted::Include */) {
js::AssertHeapIsIdle();
CHECK_THREAD(cx);
MOZ_RELEASE_ASSERT(cx->realm());
{
bool skippedAsync;
// This function is always called with self-hosted frames excluded by
// GetValueIfNotCached in dom/bindings/Exceptions.cpp. However, we want
// to include them because our Promise implementation causes us to have
// the async cause on a self-hosted frame. So we just ignore the
// parameter and always include self-hosted frames.
Rooted<js::SavedFrame*> frame(
cx, UnwrapSavedFrame(cx, principals, savedFrame,
SavedFrameSelfHosted::Include, skippedAsync));
if (!frame) {
asyncCausep.set(nullptr);
return SavedFrameResult::AccessDenied;
}
asyncCausep.set(frame->getAsyncCause());
if (!asyncCausep && skippedAsync) {
asyncCausep.set(cx->names().Async);
}
}
if (asyncCausep && asyncCausep->isAtom()) {
cx->markAtom(&asyncCausep->asAtom());
}
return SavedFrameResult::Ok;
}
JS_PUBLIC_API SavedFrameResult GetSavedFrameAsyncParent(
JSContext* cx, JSPrincipals* principals, HandleObject savedFrame,
MutableHandleObject asyncParentp,
SavedFrameSelfHosted selfHosted
/* = SavedFrameSelfHosted::Include */) {
js::AssertHeapIsIdle();
CHECK_THREAD(cx);
MOZ_RELEASE_ASSERT(cx->realm());
bool skippedAsync;
Rooted<js::SavedFrame*> frame(cx, UnwrapSavedFrame(cx, principals, savedFrame,
selfHosted, skippedAsync));
if (!frame) {
asyncParentp.set(nullptr);
return SavedFrameResult::AccessDenied;
}
Rooted<js::SavedFrame*> parent(cx, frame->getParent());
// The current value of |skippedAsync| is not interesting, because we are
// interested in whether we would cross any async parents to get from here
// to the first subsumed parent frame instead.
Rooted<js::SavedFrame*> subsumedParent(
cx,
GetFirstSubsumedFrame(cx, principals, parent, selfHosted, skippedAsync));
// Even if |parent| is not subsumed, we still want to return a pointer to it
// rather than |subsumedParent| so it can pick up any |asyncCause| from the
// inaccessible part of the chain.
if (subsumedParent && (subsumedParent->getAsyncCause() || skippedAsync)) {
asyncParentp.set(parent);
}
else {
asyncParentp.set(nullptr);
}
return SavedFrameResult::Ok;
}
JS_PUBLIC_API SavedFrameResult GetSavedFrameParent(
JSContext* cx, JSPrincipals* principals, HandleObject savedFrame,
MutableHandleObject parentp,
SavedFrameSelfHosted selfHosted
/* = SavedFrameSelfHosted::Include */) {
js::AssertHeapIsIdle();
CHECK_THREAD(cx);
MOZ_RELEASE_ASSERT(cx->realm());
bool skippedAsync;
Rooted<js::SavedFrame*> frame(cx, UnwrapSavedFrame(cx, principals, savedFrame,
selfHosted, skippedAsync));
if (!frame) {
parentp.set(nullptr);
return SavedFrameResult::AccessDenied;
}
Rooted<js::SavedFrame*> parent(cx, frame->getParent());
// The current value of |skippedAsync| is not interesting, because we are
// interested in whether we would cross any async parents to get from here
// to the first subsumed parent frame instead.
Rooted<js::SavedFrame*> subsumedParent(
cx,
GetFirstSubsumedFrame(cx, principals, parent, selfHosted, skippedAsync));
// Even if |parent| is not subsumed, we still want to return a pointer to it
// rather than |subsumedParent| so it can pick up any |asyncCause| from the
// inaccessible part of the chain.
if (subsumedParent && !(subsumedParent->getAsyncCause() || skippedAsync)) {
parentp.set(parent);
}
else {
parentp.set(nullptr);
}
return SavedFrameResult::Ok;
}
static bool FormatStackFrameLine(js::StringBuilder& sb,
JS::Handle<js::SavedFrame*> frame) {
if (frame->isWasm()) {
// See comment in WasmFrameIter::computeLine().
return sb.append(
"wasm-function[") &&
NumberValueToStringBuilder(NumberValue(frame->wasmFuncIndex()),
sb) &&
sb.append(
']');
}
return NumberValueToStringBuilder(NumberValue(frame->getLine()), sb);
}
static bool FormatStackFrameColumn(js::StringBuilder& sb,
JS::Handle<js::SavedFrame*> frame) {
if (frame->isWasm()) {
// See comment in WasmFrameIter::computeLine().
js::Int32ToCStringBuf cbuf;
size_t cstrlen;
const char* cstr =
Uint32ToHexCString(&cbuf, frame->wasmBytecodeOffset(), &cstrlen);
MOZ_ASSERT(cstr);
return sb.append(
"0x") && sb.append(cstr, cstrlen);
}
return NumberValueToStringBuilder(
NumberValue(frame->getColumn().oneOriginValue()), sb);
}
static bool FormatSpiderMonkeyStackFrame(JSContext* cx, js::StringBuilder& sb,
JS::Handle<js::SavedFrame*> frame,
size_t indent,
bool skippedAsync) {
RootedString asyncCause(cx, frame->getAsyncCause());
if (!asyncCause && skippedAsync) {
asyncCause.set(cx->names().Async);
}
Rooted<JSAtom*> name(cx, frame->getFunctionDisplayName());
return (!indent || sb.appendN(
' ', indent)) &&
(!asyncCause || (sb.append(asyncCause) && sb.append(
'*'))) &&
(!name || sb.append(name)) && sb.append(
'@') &&
sb.append(frame->getSource()) && sb.append(
':') &&
FormatStackFrameLine(sb, frame) && sb.append(
':') &&
FormatStackFrameColumn(sb, frame) && sb.append(
'\n');
}
static bool FormatV8StackFrame(JSContext* cx, js::StringBuilder& sb,
JS::Handle<js::SavedFrame*> frame, size_t indent,
bool lastFrame) {
Rooted<JSAtom*> name(cx, frame->getFunctionDisplayName());
return sb.appendN(
' ', indent + 4) && sb.append(
'a') && sb.append(
't') &&
sb.append(
' ') &&
(!name || (sb.append(name) && sb.append(
' ') && sb.append(
'('))) &&
sb.append(frame->getSource()) && sb.append(
':') &&
FormatStackFrameLine(sb, frame) && sb.append(
':') &&
FormatStackFrameColumn(sb, frame) && (!name || sb.append(
')')) &&
(lastFrame || sb.append(
'\n'));
}
JS_PUBLIC_API
bool BuildStackString(JSContext* cx, JSPrincipals* principals,
HandleObject stack,
MutableHandleString stringp, size_t indent,
js::StackFormat format) {
js::AssertHeapIsIdle();
CHECK_THREAD(cx);
MOZ_RELEASE_ASSERT(cx->realm());
js::JSStringBuilder sb(cx);
if (format == js::StackFormat::
Default) {
format = cx->runtime()->stackFormat();
}
MOZ_ASSERT(format != js::StackFormat::
Default);
// Enter a new block to constrain the scope of possibly entering the stack's
// realm. This ensures that when we finish the StringBuilder, we are back in
// the cx's original compartment, and fulfill our contract with callers to
// place the output string in the cx's current realm.
{
bool skippedAsync;
Rooted<js::SavedFrame*> frame(
cx, UnwrapSavedFrame(cx, principals, stack,
SavedFrameSelfHosted::Exclude, skippedAsync));
if (!frame) {
stringp.set(cx->runtime()->emptyString);
return true;
}
Rooted<js::SavedFrame*> parent(cx);
do {
MOZ_ASSERT(SavedFrameSubsumedByPrincipals(cx, principals, frame));
MOZ_ASSERT(!frame->isSelfHosted(cx));
parent = frame->getParent();
bool skippedNextAsync;
Rooted<js::SavedFrame*> nextFrame(
cx, js::GetFirstSubsumedFrame(cx, principals, parent,
SavedFrameSelfHosted::Exclude,
skippedNextAsync));
switch (format) {
case js::StackFormat::SpiderMonkey:
if (!FormatSpiderMonkeyStackFrame(cx, sb, frame, indent,
skippedAsync)) {
return false;
}
break;
case js::StackFormat::V8:
if (!FormatV8StackFrame(cx, sb, frame, indent, !nextFrame)) {
return false;
}
break;
case js::StackFormat::
Default:
MOZ_CRASH(
"Unexpected value");
break;
}
frame = nextFrame;
skippedAsync = skippedNextAsync;
}
while (frame);
}
JSString* str = sb.finishString();
if (!str) {
return false;
}
cx->check(str);
stringp.set(str);
return true;
}
JS_PUBLIC_API
bool IsMaybeWrappedSavedFrame(JSObject* obj) {
MOZ_ASSERT(obj);
return obj->canUnwrapAs<js::SavedFrame>();
}
JS_PUBLIC_API
bool IsUnwrappedSavedFrame(JSObject* obj) {
MOZ_ASSERT(obj);
return obj->is<js::SavedFrame>();
}
static bool AssignProperty(JSContext* cx, HandleObject dst, HandleObject src,
const char* property) {
RootedValue v(cx);
return JS_GetProperty(cx, src, property, &v) &&
JS_DefineProperty(cx, dst, property, v, JSPROP_ENUMERATE);
}
JS_PUBLIC_API JSObject* ConvertSavedFrameToPlainObject(
JSContext* cx, HandleObject savedFrameArg,
SavedFrameSelfHosted selfHosted) {
MOZ_ASSERT(savedFrameArg);
RootedObject savedFrame(cx, savedFrameArg);
RootedObject baseConverted(cx), lastConverted(cx);
RootedValue v(cx);
baseConverted = lastConverted = JS_NewObject(cx, nullptr);
if (!baseConverted) {
return nullptr;
}
bool foundParent;
do {
if (!AssignProperty(cx, lastConverted, savedFrame,
"source") ||
!AssignProperty(cx, lastConverted, savedFrame,
"sourceId") ||
!AssignProperty(cx, lastConverted, savedFrame,
"line") ||
!AssignProperty(cx, lastConverted, savedFrame,
"column") ||
!AssignProperty(cx, lastConverted, savedFrame,
"functionDisplayName") ||
!AssignProperty(cx, lastConverted, savedFrame,
"asyncCause")) {
return nullptr;
}
const char* parentProperties[] = {
"parent",
"asyncParent"};
foundParent =
false;
for (
const char* prop : parentProperties) {
if (!JS_GetProperty(cx, savedFrame, prop, &v)) {
return nullptr;
}
if (v.isObject()) {
RootedObject nextConverted(cx, JS_NewObject(cx, nullptr));
if (!nextConverted ||
!JS_DefineProperty(cx, lastConverted, prop, nextConverted,
JSPROP_ENUMERATE)) {
return nullptr;
}
lastConverted = nextConverted;
savedFrame = &v.toObject();
foundParent =
true;
break;
}
}
}
while (foundParent);
return baseConverted;
}
}
/* namespace JS */
namespace js {
/* static */
bool SavedFrame::sourceProperty(JSContext* cx,
unsigned argc, Value* vp) {
THIS_SAVEDFRAME(cx, argc, vp,
"(get source)", args, frame);
JSPrincipals* principals = cx->realm()->principals();
RootedString source(cx);
if (JS::GetSavedFrameSource(cx, principals, frame, &source) ==
JS::SavedFrameResult::Ok) {
if (!cx->compartment()->wrap(cx, &source)) {
return false;
}
args.rval().setString(source);
}
else {
args.rval().setNull();
}
return true;
}
/* static */
bool SavedFrame::sourceIdProperty(JSContext* cx,
unsigned argc, Value* vp) {
THIS_SAVEDFRAME(cx, argc, vp,
"(get sourceId)", args, frame);
JSPrincipals* principals = cx->realm()->principals();
uint32_t sourceId;
if (JS::GetSavedFrameSourceId(cx, principals, frame, &sourceId) ==
JS::SavedFrameResult::Ok) {
args.rval().setNumber(sourceId);
}
else {
args.rval().setNull();
}
return true;
}
/* static */
bool SavedFrame::lineProperty(JSContext* cx,
unsigned argc, Value* vp) {
THIS_SAVEDFRAME(cx, argc, vp,
"(get line)", args, frame);
JSPrincipals* principals = cx->realm()->principals();
uint32_t line;
if (JS::GetSavedFrameLine(cx, principals, frame, &line) ==
JS::SavedFrameResult::Ok) {
args.rval().setNumber(line);
}
else {
args.rval().setNull();
}
return true;
}
/* static */
bool SavedFrame::columnProperty(JSContext* cx,
unsigned argc, Value* vp) {
THIS_SAVEDFRAME(cx, argc, vp,
"(get column)", args, frame);
JSPrincipals* principals = cx->realm()->principals();
JS::TaggedColumnNumberOneOrigin column;
if (JS::GetSavedFrameColumn(cx, principals, frame, &column) ==
JS::SavedFrameResult::Ok) {
args.rval().setNumber(column.oneOriginValue());
}
else {
args.rval().setNull();
}
return true;
}
/* static */
bool SavedFrame::functionDisplayNameProperty(JSContext* cx,
unsigned argc,
Value* vp) {
THIS_SAVEDFRAME(cx, argc, vp,
"(get functionDisplayName)", args, frame);
JSPrincipals* principals = cx->realm()->principals();
RootedString name(cx);
JS::SavedFrameResult result =
JS::GetSavedFrameFunctionDisplayName(cx, principals, frame, &name);
if (result == JS::SavedFrameResult::Ok && name) {
if (!cx->compartment()->wrap(cx, &name)) {
return false;
}
args.rval().setString(name);
}
else {
args.rval().setNull();
}
return true;
}
/* static */
bool SavedFrame::asyncCauseProperty(JSContext* cx,
unsigned argc, Value* vp) {
THIS_SAVEDFRAME(cx, argc, vp,
"(get asyncCause)", args, frame);
JSPrincipals* principals = cx->realm()->principals();
RootedString asyncCause(cx);
JS::SavedFrameResult result =
JS::GetSavedFrameAsyncCause(cx, principals, frame, &asyncCause);
if (result == JS::SavedFrameResult::Ok && asyncCause) {
if (!cx->compartment()->wrap(cx, &asyncCause)) {
return false;
}
args.rval().setString(asyncCause);
}
else {
args.rval().setNull();
}
return true;
}
/* static */
bool SavedFrame::asyncParentProperty(JSContext* cx,
unsigned argc, Value* vp) {
THIS_SAVEDFRAME(cx, argc, vp,
"(get asyncParent)", args, frame);
JSPrincipals* principals = cx->realm()->principals();
RootedObject asyncParent(cx);
(
void)JS::GetSavedFrameAsyncParent(cx, principals, frame, &asyncParent);
if (!cx->compartment()->wrap(cx, &asyncParent)) {
return false;
}
args.rval().setObjectOrNull(asyncParent);
return true;
}
/* static */
bool SavedFrame::parentProperty(JSContext* cx,
unsigned argc, Value* vp) {
THIS_SAVEDFRAME(cx, argc, vp,
"(get parent)", args, frame);
JSPrincipals* principals = cx->realm()->principals();
RootedObject parent(cx);
(
void)JS::GetSavedFrameParent(cx, principals, frame, &parent);
if (!cx->compartment()->wrap(cx, &parent)) {
return false;
}
args.rval().setObjectOrNull(parent);
return true;
}
/* static */
bool SavedFrame::toStringMethod(JSContext* cx,
unsigned argc, Value* vp) {
THIS_SAVEDFRAME(cx, argc, vp,
"toString", args, frame);
JSPrincipals* principals = cx->realm()->principals();
RootedString string(cx);
if (!JS::BuildStackString(cx, principals, frame, &string)) {
return false;
}
args.rval().setString(string);
return true;
}
bool SavedStacks::saveCurrentStack(
JSContext* cx, MutableHandle<SavedFrame*> frame,
JS::StackCapture&& capture
/* = JS::StackCapture(JS::AllFrames()) */,
HandleObject startAt
/* nullptr */) {
MOZ_RELEASE_ASSERT(cx->realm());
MOZ_DIAGNOSTIC_ASSERT(&cx->realm()->savedStacks() ==
this);
if (creatingSavedFrame || cx->isExceptionPending() || !cx->global() ||
!cx->global()->isStandardClassResolved(JSProto_Object)) {
frame.set(nullptr);
return true;
}
AutoGeckoProfilerEntry labelFrame(cx,
"js::SavedStacks::saveCurrentStack");
return insertFrames(cx, frame, std::move(capture), startAt);
}
bool SavedStacks::copyAsyncStack(JSContext* cx, HandleObject asyncStack,
HandleString asyncCause,
MutableHandle<SavedFrame*> adoptedStack,
const Maybe<size_t>& maxFrameCount) {
MOZ_RELEASE_ASSERT(cx->realm());
MOZ_DIAGNOSTIC_ASSERT(&cx->realm()->savedStacks() ==
this);
Rooted<JSAtom*> asyncCauseAtom(cx, AtomizeString(cx, asyncCause));
if (!asyncCauseAtom) {
return false;
}
Rooted<SavedFrame*> asyncStackObj(
cx, asyncStack->maybeUnwrapAs<js::SavedFrame>());
MOZ_RELEASE_ASSERT(asyncStackObj);
adoptedStack.set(asyncStackObj);
if (!adoptAsyncStack(cx, adoptedStack, asyncCauseAtom, maxFrameCount)) {
return false;
}
return true;
}
void SavedStacks::traceWeak(JSTracer* trc) {
frames.traceWeak(trc);
pcLocationMap.traceWeak(trc);
}
void SavedStacks::trace(JSTracer* trc) { pcLocationMap.trace(trc); }
uint32_t SavedStacks::count() {
return frames.count(); }
void SavedStacks::clear() { frames.clear(); }
size_t SavedStacks::sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) {
return frames.shallowSizeOfExcludingThis(mallocSizeOf) +
pcLocationMap.shallowSizeOfExcludingThis(mallocSizeOf);
}
// Given that we have captured a stack frame with the given principals and
// source, return true if the requested `StackCapture` has been satisfied and
// stack walking can halt. Return false otherwise (and stack walking and frame
// capturing should continue).
static inline bool captureIsSatisfied(JSContext* cx, JSPrincipals* principals,
const JSAtom* source,
JS::StackCapture& capture) {
class Matcher {
JSContext* cx_;
JSPrincipals* framePrincipals_;
const JSAtom* frameSource_;
public:
Matcher(JSContext* cx, JSPrincipals* principals,
const JSAtom* source)
: cx_(cx), framePrincipals_(principals), frameSource_(source) {}
bool operator()(JS::FirstSubsumedFrame& target) {
auto subsumes = cx_->runtime()->securityCallbacks->subsumes;
return (!subsumes || subsumes(target.principals, framePrincipals_)) &&
(!target.ignoreSelfHosted ||
frameSource_ != cx_->names().self_hosted_);
}
bool operator()(JS::MaxFrames& target) {
return target.maxFrames == 1; }
bool operator()(JS::AllFrames&) {
return false; }
};
Matcher m(cx, principals, source);
return capture.match(m);
}
bool SavedStacks::insertFrames(JSContext* cx, MutableHandle<SavedFrame*> frame,
JS::StackCapture&& capture,
HandleObject startAtObj) {
MOZ_ASSERT_IF(startAtObj, startAtObj->isCallable());
// In order to look up a cached SavedFrame object, we need to have its parent
// SavedFrame, which means we need to walk the stack from oldest frame to
// youngest. However, FrameIter walks the stack from youngest frame to
// oldest. The solution is to append stack frames to a vector as we walk the
// stack with FrameIter, and then do a second pass through that vector in
// reverse order after the traversal has completed and get or create the
// SavedFrame objects at that time.
//
// To avoid making many copies of FrameIter (whose copy constructor is
// relatively slow), we use a vector of `SavedFrame::Lookup` objects, which
// only contain the FrameIter data we need. The `SavedFrame::Lookup`
// objects are partially initialized with everything except their parent
// pointers on the first pass, and then we fill in the parent pointers as we
// return in the second pass.
// Accumulate the vector of Lookup objects here, youngest to oldest.
Rooted<js::GCLookupVector> stackChain(cx, js::GCLookupVector(cx));
// If we find a cached saved frame, then that supplies the parent of the
// frames we have placed in stackChain. If we walk the stack all the way
// to the end, this remains null.
Rooted<SavedFrame*> cachedParentFrame(cx, nullptr);
// Choose the right frame iteration strategy to accomodate both
// evalInFramePrev links and the LiveSavedFrameCache. For background, see
// the LiveSavedFrameCache comments in Stack.h.
//
// If we're using the LiveSavedFrameCache, then don't handle evalInFramePrev
// links by skipping over the frames altogether; that violates the cache's
// assumptions. Instead, traverse the entire stack, but choose each
// SavedFrame's parent as directed by the evalInFramePrev link, if any.
//
// If we're not using the LiveSavedFrameCache, it's hard to recover the
// frame to which the evalInFramePrev link refers, so we just let FrameIter
// skip those frames. Then each SavedFrame's parent is simply the frame that
// follows it in the stackChain vector, even when it has an evalInFramePrev
// link.
FrameIter iter(cx, capture.is<JS::AllFrames>()
? FrameIter::IGNORE_DEBUGGER_EVAL_PREV_LINK
: FrameIter::FOLLOW_DEBUGGER_EVAL_PREV_LINK);
// Once we've seen one frame with its hasCachedSavedFrame bit set, all its
// parents (that can be cached) ought to have it set too.
DebugOnly<
bool> seenCached =
false;
// If we are using evalInFramePrev links to adjust the parents of debugger
// eval frames, we have to ensure the target frame is cached in the current
// realm. (This might not happen by default if the target frame is
// rematerialized, or if there is an async parent between the debugger eval
// frame and the target frame.) To accomplish this, we keep track of eval
// targets and ensure that we don't stop before they have all been reached.
Vector<AbstractFramePtr, 4, TempAllocPolicy> unreachedEvalTargets(cx);
Rooted<JSFunction*> startAt(cx, startAtObj && startAtObj->is<JSFunction>()
? &startAtObj->as<JSFunction>()
: nullptr);
bool seenStartAt = !startAt;
while (!iter.done()) {
Activation& activation = *iter.activation();
Maybe<LiveSavedFrameCache::FramePtr> framePtr =
LiveSavedFrameCache::FramePtr::create(iter);
if (capture.is<JS::AllFrames>() && iter.hasUsableAbstractFramePtr()) {
unreachedEvalTargets.eraseIfEqual(iter.abstractFramePtr());
}
if (framePtr) {
// In general, when we reach a frame with its hasCachedSavedFrame bit set,
// all its parents will have the bit set as well. See the
// LiveSavedFrameCache comment in Activation.h for more details. There are
// a few exceptions:
// - Rematerialized frames are always created with the bit clear.
// - Captures using FirstSubsumedFrame ignore async parents and walk the
// real stack. Because we're using different rules for walking the
// stack, we can reach frames that weren't cached in a previous
// AllFrames traversal.
DebugOnly<
bool> hasGoodExcuse = framePtr->isRematerializedFrame() ||
capture.is<JS::FirstSubsumedFrame>();
MOZ_ASSERT_IF(seenCached,
framePtr->hasCachedSavedFrame() || hasGoodExcuse);
seenCached |= framePtr->hasCachedSavedFrame();
if (capture.is<JS::AllFrames>() && framePtr->isInterpreterFrame() &&
framePtr->asInterpreterFrame().isDebuggerEvalFrame()) {
AbstractFramePtr target =
framePtr->asInterpreterFrame().evalInFramePrev();
if (!unreachedEvalTargets.append(target)) {
return false;
}
}
}
if (capture.is<JS::AllFrames>() && framePtr &&
framePtr->hasCachedSavedFrame()) {
auto* cache = activation.getLiveSavedFrameCache(cx);
if (!cache) {
return false;
}
cache->find(cx, *framePtr, iter.pc(), &cachedParentFrame);
// Even though iter.hasCachedSavedFrame() was true, we may still get a
// cache miss, if the frame's pc doesn't match the cache entry's, or if
// the cache was emptied due to a realm mismatch. If we got a cache hit,
// and we do not have to keep looking for unreached eval target frames,
// we can stop traversing the stack and start building the chain.
if (cachedParentFrame && unreachedEvalTargets.empty()) {
break;
}
// This frame doesn't have a cache entry, despite its hasCachedSavedFrame
// flag being set. If this was due to a pc mismatch, we can clear the flag
// here and set things right. If the cache was emptied due to a realm
// mismatch, we should clear all the frames' flags as we walk to the
// bottom of the stack, so that they are all clear before we start pushing
// any new entries.
framePtr->clearHasCachedSavedFrame();
}
// We'll be pushing this frame onto stackChain. Gather the information
// needed to construct the SavedFrame::Lookup.
Rooted<LocationValue> location(cx);
{
AutoRealmUnchecked ar(cx, iter.realm());
if (!cx->realm()->savedStacks().getLocation(cx, iter, &location)) {
return false;
}
}
Rooted<JSAtom*> displayAtom(cx, iter.maybeFunctionDisplayAtom());
auto principals = iter.realm()->principals();
MOZ_ASSERT_IF(framePtr && !iter.isWasm(), iter.pc());
// If we haven't yet seen the start, then don't add anything to the stack
// chain.
if (seenStartAt) {
if (!stackChain.emplaceBack(location.source(), location.sourceId(),
location.line(), location.column(),
displayAtom,
nullptr,
// asyncCause
nullptr,
// parent (not known yet)
principals, iter.mutedErrors(), framePtr,
iter.pc(), &activation)) {
return false;
}
}
if (captureIsSatisfied(cx, principals, location.source(), capture)) {
break;
}
if (!seenStartAt && iter.isFunctionFrame() &&
iter.matchCallee(cx, startAt)) {
seenStartAt =
true;
}
++iter;
framePtr = LiveSavedFrameCache::FramePtr::create(iter);
if (iter.activation() != &activation && capture.is<JS::AllFrames>()) {
// If there were no cache hits in the entire activation, clear its
// cache so we'll be able to push new ones when we build the
// SavedFrame chain.
activation.clearLiveSavedFrameCache();
}
// If we have crossed into a new activation, check whether the prior
// activation had an async parent set.
//
// If the async call was explicit (async function resumptions, most
// testing facilities), then the async parent stack has priority over
// any actual frames still on the JavaScript stack. If the async call
// was implicit (DOM CallbackObject::CallSetup calls), then the async
// parent stack is used only if there were no other frames on the
// stack.
//
// Captures using FirstSubsumedFrame expect us to ignore async parents.
if (iter.activation() != &activation && activation.asyncStack() &&
(activation.asyncCallIsExplicit() || iter.done()) &&
!capture.is<JS::FirstSubsumedFrame>()) {
// Atomize the async cause string. There should only be a few
// different strings used.
const char* cause = activation.asyncCause();
Rooted<JSAtom*> causeAtom(cx, AtomizeUTF8Chars(cx, cause, strlen(cause)));
if (!causeAtom) {
return false;
}
// Translate our capture into a frame count limit for
// adoptAsyncStack, which will impose further limits.
Maybe<size_t> maxFrames =
!capture.is<JS::MaxFrames>() ? Nothing()
: capture.as<JS::MaxFrames>().maxFrames == 0
? Nothing()
: Some(capture.as<JS::MaxFrames>().maxFrames);
// Clip the stack if needed, attach the async cause string to the
// top frame, and copy it into our compartment if necessary.
Rooted<SavedFrame*> asyncParent(cx, activation.asyncStack());
if (!adoptAsyncStack(cx, &asyncParent, causeAtom, maxFrames)) {
return false;
}
stackChain[stackChain.length() - 1].setParent(asyncParent);
if (!capture.is<JS::AllFrames>() || unreachedEvalTargets.empty()) {
// In the case of a JS::AllFrames capture, we will be populating the
// LiveSavedFrameCache in the second loop. In the case where there is
// a debugger eval frame on the stack, the second loop will use
// checkForEvalInFramePrev to skip from the eval frame to the "prev"
// frame and assert that when this happens, the "prev"
// frame is in the cache. In cases where there is an async stack
// activation between the debugger eval frame and the "prev" frame,
// breaking here would not populate the "prev" cache entry, causing
// checkForEvalInFramePrev to fail.
break;
}
// At this point, we would normally stop walking the stack, but
// we're continuing because of an unreached eval target. If a
// previous capture stopped here, it's possible that this frame was
// already cached, but its non-async parent wasn't, which violates
// our `seenCached` invariant. By clearing `seenCached` here, we
// avoid spurious assertions. We continue to enforce the invariant
// for subsequent frames: if any frame above this is cached, then
// all of that frame's parents should also be cached.
seenCached =
false;
}
if (capture.is<JS::MaxFrames>()) {
capture.as<JS::MaxFrames>().maxFrames--;
}
}
// Iterate through |stackChain| in reverse order and get or create the
// actual SavedFrame instances.
frame.set(cachedParentFrame);
for (size_t i = stackChain.length(); i != 0; i--) {
MutableHandle<SavedFrame::Lookup> lookup = stackChain[i - 1];
if (!lookup.parent()) {
// The frame may already have an async parent frame set explicitly
// on its activation.
lookup.setParent(frame);
}
// If necessary, adjust the parent of a debugger eval frame to point to
// the frame in whose scope the eval occurs - if we're using
// LiveSavedFrameCache. Otherwise, we simply ask the FrameIter to follow
// evalInFramePrev links, so that the parent is always the last frame we
// created.
if (capture.is<JS::AllFrames>() && lookup.framePtr()) {
if (!checkForEvalInFramePrev(cx, lookup)) {
return false;
}
}
frame.set(getOrCreateSavedFrame(cx, lookup));
if (!frame) {
return false;
}
if (capture.is<JS::AllFrames>() && lookup.framePtr()) {
auto* cache = lookup.activation()->getLiveSavedFrameCache(cx);
if (!cache ||
!cache->insert(cx, *lookup.framePtr(), lookup.pc(), frame)) {
return false;
}
}
}
return true;
}
bool SavedStacks::adoptAsyncStack(JSContext* cx,
MutableHandle<SavedFrame*> asyncStack,
Handle<JSAtom*> asyncCause,
const Maybe<size_t>& maxFrameCount) {
MOZ_ASSERT(asyncStack);
MOZ_ASSERT(asyncCause);
// If maxFrameCount is Nothing, the caller asked for an unlimited number of
// stack frames, but async stacks are not limited by the available stack
// memory, so we need to set an arbitrary limit when collecting them. We
// still don't enforce an upper limit if the caller requested more frames.
size_t maxFrames = maxFrameCount.valueOr(ASYNC_STACK_MAX_FRAME_COUNT);
// Turn the chain of frames starting with asyncStack into a vector of Lookup
// objects in |stackChain|, youngest to oldest.
Rooted<js::GCLookupVector> stackChain(cx, js::GCLookupVector(cx));
SavedFrame* currentSavedFrame = asyncStack;
while (currentSavedFrame && stackChain.length() < maxFrames) {
if (!stackChain.emplaceBack(*currentSavedFrame)) {
ReportOutOfMemory(cx);
return false;
}
currentSavedFrame = currentSavedFrame->getParent();
}
// Attach the asyncCause to the youngest frame.
stackChain[0].setAsyncCause(asyncCause);
// If we walked the entire stack, and it's in cx's realm, we don't
// need to rebuild the full chain again using the lookup objects - we can
// just use the existing chain. Only the asyncCause on the youngest frame
// needs to be changed.
if (currentSavedFrame == nullptr && asyncStack->realm() == cx->realm()) {
MutableHandle<SavedFrame::Lookup> lookup = stackChain[0];
lookup.setParent(asyncStack->getParent());
asyncStack.set(getOrCreateSavedFrame(cx, lookup));
return !!asyncStack;
}
// If we captured the maximum number of frames and the caller requested no
// specific limit, we only return half of them. This means that if we do
// many subsequent captures with the same async stack, it's likely we can
// use the optimization above.
if (maxFrameCount.isNothing() && currentSavedFrame) {
stackChain.shrinkBy(ASYNC_STACK_MAX_FRAME_COUNT / 2);
}
// Iterate through |stackChain| in reverse order and get or create the
// actual SavedFrame instances.
asyncStack.set(nullptr);
while (!stackChain.empty()) {
Rooted<SavedFrame::Lookup> lookup(cx, stackChain.back());
lookup.setParent(asyncStack);
asyncStack.set(getOrCreateSavedFrame(cx, lookup));
if (!asyncStack) {
return false;
}
stackChain.popBack();
}
return true;
}
// Given a |lookup| for which we're about to construct a SavedFrame, if it
// refers to a Debugger eval frame, adjust |lookup|'s parent to be the frame's
// evalInFramePrev target.
//
// Debugger eval frames run code in the scope of some random older frame on the
// stack (the 'target' frame). It is our custom to report the target as the
// immediate parent of the eval frame. The LiveSavedFrameCache requires us not
// to skip frames, so instead we walk the entire stack, and just give Debugger
// eval frames the right parents as we encounter them.
//
// Call this function only if we are using the LiveSavedFrameCache; otherwise,
// FrameIter has already taken care of getting us the right parent.
bool SavedStacks::checkForEvalInFramePrev(
JSContext* cx, MutableHandle<SavedFrame::Lookup> lookup) {
MOZ_ASSERT(lookup.framePtr());
if (!lookup.framePtr()->isInterpreterFrame()) {
return true;
}
InterpreterFrame& interpreterFrame = lookup.framePtr()->asInterpreterFrame();
if (!interpreterFrame.isDebuggerEvalFrame()) {
return true;
}
FrameIter iter(cx, FrameIter::IGNORE_DEBUGGER_EVAL_PREV_LINK);
while (!iter.done() &&
(!iter.hasUsableAbstractFramePtr() ||
iter.abstractFramePtr() != interpreterFrame.evalInFramePrev())) {
++iter;
}
Maybe<LiveSavedFrameCache::FramePtr> maybeTarget =
LiveSavedFrameCache::FramePtr::create(iter);
MOZ_ASSERT(maybeTarget);
LiveSavedFrameCache::FramePtr target = *maybeTarget;
// If we're caching the frame to which |lookup| refers, then we should
// definitely have the target frame in the cache as well.
MOZ_ASSERT(target.hasCachedSavedFrame());
// Search the chain of activations for a LiveSavedFrameCache that has an
// entry for target.
Rooted<SavedFrame*> saved(cx, nullptr);
for (Activation* act = lookup.activation(); act; act = act->prev()) {
// It's okay to force allocation of a cache here; we're about to put
// something in the top cache, and all the lower ones should exist
// already.
auto* cache = act->getLiveSavedFrameCache(cx);
if (!cache) {
return false;
}
cache->findWithoutInvalidation(target, &saved);
if (saved) {
break;
}
}
// Since |target| has its cached bit set, we should have found it.
MOZ_ALWAYS_TRUE(saved);
// Because we use findWithoutInvalidation here, we can technically get a
// SavedFrame here for any realm. That shouldn't happen here because
// checkForEvalInFramePrev is only called _after_ the parent frames have
// been constructed, but if something prevents the chain from being properly
// reconstructed, that invariant could be accidentally broken.
MOZ_ASSERT(saved->realm() == cx->realm());
lookup.setParent(saved);
return true;
}
SavedFrame* SavedStacks::getOrCreateSavedFrame(
JSContext* cx, Handle<SavedFrame::Lookup> lookup) {
const SavedFrame::Lookup& lookupInstance = lookup.get();
DependentAddPtr<SavedFrame::Set> p(cx, frames, lookupInstance);
if (p) {
MOZ_ASSERT(*p);
return *p;
}
Rooted<SavedFrame*> frame(cx, createFrameFromLookup(cx, lookup));
if (!frame) {
return nullptr;
}
if (!p.add(cx, frames, lookupInstance, frame)) {
return nullptr;
}
return frame;
}
SavedFrame* SavedStacks::createFrameFromLookup(
JSContext* cx, Handle<SavedFrame::Lookup> lookup) {
Rooted<SavedFrame*> frame(cx, SavedFrame::create(cx));
if (!frame) {
return nullptr;
}
frame->initFromLookup(cx, lookup);
if (!FreezeObject(cx, frame)) {
return nullptr;
}
--> --------------------
--> maximum size reached
--> --------------------