diff options
Diffstat (limited to 'js/src/vm/SavedStacks.cpp')
-rw-r--r-- | js/src/vm/SavedStacks.cpp | 2090 |
1 files changed, 2090 insertions, 0 deletions
diff --git a/js/src/vm/SavedStacks.cpp b/js/src/vm/SavedStacks.cpp new file mode 100644 index 0000000000..4f72e547ec --- /dev/null +++ b/js/src/vm/SavedStacks.cpp @@ -0,0 +1,2090 @@ +/* -*- 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/StringBuffer.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::StringBuffer& sb, + JS::Handle<js::SavedFrame*> frame) { + if (frame->isWasm()) { + // See comment in WasmFrameIter::computeLine(). + return sb.append("wasm-function[") && + NumberValueToStringBuffer(NumberValue(frame->wasmFuncIndex()), sb) && + sb.append(']'); + } + + return NumberValueToStringBuffer(NumberValue(frame->getLine()), sb); +} + +static bool FormatStackFrameColumn(js::StringBuffer& 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 NumberValueToStringBuffer( + NumberValue(frame->getColumn().oneOriginValue()), sb); +} + +static bool FormatSpiderMonkeyStackFrame(JSContext* cx, js::StringBuffer& 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::StringBuffer& 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 StringBuffer, 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()) */) { + 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)); +} + +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) { + // 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); + + 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. + // - Similarly, if we've seen an evalInFrame frame but haven't reached + // its target yet, we don't stop when we reach an async parent, so we + // can reach frames that weren't cached in a previous traversal that + // didn't include the evalInFrame. + DebugOnly<bool> hasGoodExcuse = framePtr->isRematerializedFrame() || + capture.is<JS::FirstSubsumedFrame>() || + !unreachedEvalTargets.empty(); + 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 (!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; + } + + ++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; + } + } + + 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; + } + + return frame; +} + +bool SavedStacks::getLocation(JSContext* cx, const FrameIter& iter, + MutableHandle<LocationValue> locationp) { + // We should only ever be caching location values for scripts in this + // compartment. Otherwise, we would get dead cross-compartment scripts in + // the cache because our compartment's sweep method isn't called when their + // compartment gets collected. + MOZ_DIAGNOSTIC_ASSERT(&cx->realm()->savedStacks() == this); + cx->check(iter.compartment()); + + // When we have a |JSScript| for this frame, use a potentially memoized + // location from our PCLocationMap and copy it into |locationp|. When we do + // not have a |JSScript| for this frame (wasm frames), we take a slow path + // that doesn't employ memoization, and update |locationp|'s slots directly. + + if (iter.isWasm()) { + // Only asm.js has a displayURL. + if (const char16_t* displayURL = iter.displayURL()) { + locationp.setSource(AtomizeChars(cx, displayURL, js_strlen(displayURL))); + } else { + const char* filename = iter.filename() ? iter.filename() : ""; + locationp.setSource(AtomizeUTF8Chars(cx, filename, strlen(filename))); + } + if (!locationp.source()) { + return false; + } + + JS::TaggedColumnNumberOneOrigin column; + locationp.setLine(iter.computeLine(&column)); + locationp.setColumn(column); + return true; + } + + RootedScript script(cx, iter.script()); + jsbytecode* pc = iter.pc(); + + PCLocationMap::AddPtr p = pcLocationMap.lookupForAdd(PCKey(script, pc)); + + if (!p) { + Rooted<JSAtom*> source(cx); + if (const char16_t* displayURL = iter.displayURL()) { + source = AtomizeChars(cx, displayURL, js_strlen(displayURL)); + } else { + const char* filename = script->filename() ? script->filename() : ""; + source = AtomizeUTF8Chars(cx, filename, strlen(filename)); + } + if (!source) { + return false; + } + + uint32_t sourceId = script->scriptSource()->id(); + JS::LimitedColumnNumberOneOrigin column; + uint32_t line = PCToLineNumber(script, pc, &column); + + PCKey key(script, pc); + LocationValue value(source, sourceId, line, + JS::TaggedColumnNumberOneOrigin(column)); + if (!pcLocationMap.add(p, key, value)) { + ReportOutOfMemory(cx); + return false; + } + } + + locationp.set(p->value()); + return true; +} + +void SavedStacks::chooseSamplingProbability(Realm* realm) { + { + JSRuntime* runtime = realm->runtimeFromMainThread(); + if (runtime->recordAllocationCallback) { + // The runtime is tracking allocations across all realms, in this case + // ignore all of the debugger values, and use the runtime's probability. + this->setSamplingProbability(runtime->allocationSamplingProbability); + return; + } + } + + // Use unbarriered version to prevent triggering read barrier while + // collecting, this is safe as long as global does not escape. + GlobalObject* global = realm->unsafeUnbarrieredMaybeGlobal(); + if (!global) { + return; + } + + Maybe<double> probability = DebugAPI::allocationSamplingProbability(global); + if (probability.isNothing()) { + return; + } + + this->setSamplingProbability(*probability); +} + +void SavedStacks::setSamplingProbability(double probability) { + if (!bernoulliSeeded) { + mozilla::Array<uint64_t, 2> seed; + GenerateXorShift128PlusSeed(seed); + bernoulli.setRandomState(seed[0], seed[1]); + bernoulliSeeded = true; + } + + bernoulli.setProbability(probability); +} + +JSObject* SavedStacks::MetadataBuilder::build( + JSContext* cx, HandleObject target, + AutoEnterOOMUnsafeRegion& oomUnsafe) const { + RootedObject obj(cx, target); + + SavedStacks& stacks = cx->realm()->savedStacks(); + if (!stacks.bernoulli.trial()) { + return nullptr; + } + + Rooted<SavedFrame*> frame(cx); + if (!stacks.saveCurrentStack(cx, &frame)) { + oomUnsafe.crash("SavedStacksMetadataBuilder"); + } + + if (!DebugAPI::onLogAllocationSite(cx, obj, frame, + mozilla::TimeStamp::Now())) { + oomUnsafe.crash("SavedStacksMetadataBuilder"); + } + + auto recordAllocationCallback = + cx->realm()->runtimeFromMainThread()->recordAllocationCallback; + if (recordAllocationCallback) { + // The following code translates the JS-specific information, into an + // RecordAllocationInfo object that can be consumed outside of SpiderMonkey. + + auto node = JS::ubi::Node(obj.get()); + + // Pass the non-SpiderMonkey specific information back to the + // callback to get it out of the JS engine. + recordAllocationCallback(JS::RecordAllocationInfo{ + node.typeName(), node.jsObjectClassName(), node.descriptiveTypeName(), + JS::ubi::CoarseTypeToString(node.coarseType()), + node.size(cx->runtime()->debuggerMallocSizeOf), + gc::IsInsideNursery(obj)}); + } + + MOZ_ASSERT_IF(frame, !frame->is<WrapperObject>()); + return frame; +} + +const SavedStacks::MetadataBuilder SavedStacks::metadataBuilder; + +/* static */ +ReconstructedSavedFramePrincipals ReconstructedSavedFramePrincipals::IsSystem; +/* static */ +ReconstructedSavedFramePrincipals + ReconstructedSavedFramePrincipals::IsNotSystem; + +UniqueChars BuildUTF8StackString(JSContext* cx, JSPrincipals* principals, + HandleObject stack) { + RootedString stackStr(cx); + if (!JS::BuildStackString(cx, principals, stack, &stackStr)) { + return nullptr; + } + + return JS_EncodeStringToUTF8(cx, stackStr); +} + +} /* namespace js */ + +namespace JS { +namespace ubi { + +bool ConcreteStackFrame<SavedFrame>::isSystem() const { + auto trustedPrincipals = get().runtimeFromAnyThread()->trustedPrincipals(); + return get().getPrincipals() == trustedPrincipals || + get().getPrincipals() == + &js::ReconstructedSavedFramePrincipals::IsSystem; +} + +bool ConcreteStackFrame<SavedFrame>::constructSavedFrameStack( + JSContext* cx, MutableHandleObject outSavedFrameStack) const { + outSavedFrameStack.set(&get()); + if (!cx->compartment()->wrap(cx, outSavedFrameStack)) { + outSavedFrameStack.set(nullptr); + return false; + } + return true; +} + +// A `mozilla::Variant` matcher that converts the inner value of a +// `JS::ubi::AtomOrTwoByteChars` string to a `JSAtom*`. +struct MOZ_STACK_CLASS AtomizingMatcher { + JSContext* cx; + size_t length; + + explicit AtomizingMatcher(JSContext* cx, size_t length) + : cx(cx), length(length) {} + + JSAtom* operator()(JSAtom* atom) { + MOZ_ASSERT(atom); + return atom; + } + + JSAtom* operator()(const char16_t* chars) { + MOZ_ASSERT(chars); + return AtomizeChars(cx, chars, length); + } +}; + +JS_PUBLIC_API bool ConstructSavedFrameStackSlow( + JSContext* cx, JS::ubi::StackFrame& frame, + MutableHandleObject outSavedFrameStack) { + Rooted<js::GCLookupVector> stackChain(cx, js::GCLookupVector(cx)); + Rooted<JS::ubi::StackFrame> ubiFrame(cx, frame); + + while (ubiFrame.get()) { + // Convert the source and functionDisplayName strings to atoms. + + Rooted<JSAtom*> source(cx); + AtomizingMatcher atomizer(cx, ubiFrame.get().sourceLength()); + source = ubiFrame.get().source().match(atomizer); + if (!source) { + return false; + } + + Rooted<JSAtom*> functionDisplayName(cx); + auto nameLength = ubiFrame.get().functionDisplayNameLength(); + if (nameLength > 0) { + AtomizingMatcher atomizer(cx, nameLength); + functionDisplayName = + ubiFrame.get().functionDisplayName().match(atomizer); + if (!functionDisplayName) { + return false; + } + } + + auto principals = + js::ReconstructedSavedFramePrincipals::getSingleton(ubiFrame.get()); + + if (!stackChain.emplaceBack(source, ubiFrame.get().sourceId(), + ubiFrame.get().line(), ubiFrame.get().column(), + functionDisplayName, + /* asyncCause */ nullptr, + /* parent */ nullptr, principals, + /* mutedErrors */ true)) { + ReportOutOfMemory(cx); + return false; + } + + ubiFrame = ubiFrame.get().parent(); + } + + Rooted<js::SavedFrame*> parentFrame(cx); + for (size_t i = stackChain.length(); i != 0; i--) { + MutableHandle<SavedFrame::Lookup> lookup = stackChain[i - 1]; + lookup.setParent(parentFrame); + parentFrame = cx->realm()->savedStacks().getOrCreateSavedFrame(cx, lookup); + if (!parentFrame) { + return false; + } + } + + outSavedFrameStack.set(parentFrame); + return true; +} + +} // namespace ubi +} // namespace JS |