Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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 "debugger/Debugger-inl.h"
#include "mozilla/Attributes.h" // for MOZ_STACK_CLASS, MOZ_RAII
#include "mozilla/DebugOnly.h" // for DebugOnly
#include "mozilla/DoublyLinkedList.h" // for DoublyLinkedList<>::Iterator
#include "mozilla/HashTable.h" // for HashSet<>::Range, HashMapEntry
#include "mozilla/Maybe.h" // for Maybe, Nothing, Some
#include "mozilla/ScopeExit.h" // for MakeScopeExit, ScopeExit
#include "mozilla/ThreadLocal.h" // for ThreadLocal
#include "mozilla/TimeStamp.h" // for TimeStamp, TimeDuration
#include "mozilla/UniquePtr.h" // for UniquePtr
#include "mozilla/Variant.h" // for AsVariant, AsVariantTemporary
#include "mozilla/Vector.h" // for Vector, Vector<>::ConstRange
#include <algorithm> // for std::find, std::max
#include <functional> // for function
#include <stddef.h> // for size_t
#include <stdint.h> // for uint32_t, uint64_t, int32_t
#include <string.h> // for strlen, strcmp
#include <type_traits> // for std::underlying_type_t
#include <utility> // for std::move
#include "jsapi.h" // for CallArgs, CallArgsFromVp
#include "jstypes.h" // for JS_PUBLIC_API
#include "builtin/Array.h" // for NewDenseFullyAllocatedArray
#include "debugger/DebugAPI.h" // for ResumeMode, DebugAPI
#include "debugger/DebuggerMemory.h" // for DebuggerMemory
#include "debugger/DebugScript.h" // for DebugScript
#include "debugger/Environment.h" // for DebuggerEnvironment
#include "debugger/Frame.h" // for DebuggerFrame
#include "debugger/NoExecute.h" // for EnterDebuggeeNoExecute
#include "debugger/Object.h" // for DebuggerObject
#include "debugger/Script.h" // for DebuggerScript
#include "debugger/Source.h" // for DebuggerSource
#include "frontend/CompilationStencil.h" // for CompilationStencil
#include "frontend/FrontendContext.h" // for AutoReportFrontendContext
#include "frontend/Parser.h" // for Parser
#include "gc/GC.h" // for IterateScripts
#include "gc/GCContext.h" // for JS::GCContext
#include "gc/GCMarker.h" // for GCMarker
#include "gc/GCRuntime.h" // for GCRuntime, AutoEnterIteration
#include "gc/HashUtil.h" // for DependentAddPtr
#include "gc/Marking.h" // for IsAboutToBeFinalized
#include "gc/PublicIterators.h" // for RealmsIter, CompartmentsIter
#include "gc/Statistics.h" // for Statistics::SliceData
#include "gc/Tracer.h" // for TraceEdge
#include "gc/Zone.h" // for Zone
#include "gc/ZoneAllocator.h" // for ZoneAllocPolicy
#include "jit/BaselineDebugModeOSR.h" // for RecompileOnStackBaselineScriptsForDebugMode
#include "jit/BaselineJIT.h" // for FinishDiscardBaselineScript
#include "jit/Invalidation.h" // for RecompileInfoVector
#include "jit/JitContext.h" // for JitContext
#include "jit/JitOptions.h" // for fuzzingSafe
#include "jit/JitScript.h" // for JitScript
#include "jit/JSJitFrameIter.h" // for InlineFrameIterator
#include "jit/RematerializedFrame.h" // for RematerializedFrame
#include "js/CallAndConstruct.h" // JS::IsCallable
#include "js/Conversions.h" // for ToBoolean, ToUint32
#include "js/Debug.h" // for Builder::Object, Builder
#include "js/friend/ErrorMessages.h" // for GetErrorMessage, JSMSG_*
#include "js/GCAPI.h" // for GarbageCollectionEvent
#include "js/GCVariant.h" // for GCVariant
#include "js/HeapAPI.h" // for ExposeObjectToActiveJS
#include "js/Promise.h" // for AutoDebuggerJobQueueInterruption
#include "js/PropertyAndElement.h" // for JS_GetProperty
#include "js/Proxy.h" // for PropertyDescriptor
#include "js/SourceText.h" // for SourceOwnership, SourceText
#include "js/StableStringChars.h" // for AutoStableStringChars
#include "js/UbiNode.h" // for Node, RootList, Edge
#include "js/UbiNodeBreadthFirst.h" // for BreadthFirst
#include "js/Wrapper.h" // for CheckedUnwrapStatic
#include "util/Identifier.h" // for IsIdentifier
#include "util/Text.h" // for DuplicateString, js_strlen
#include "vm/ArrayObject.h" // for ArrayObject
#include "vm/AsyncFunction.h" // for AsyncFunctionGeneratorObject
#include "vm/AsyncIteration.h" // for AsyncGeneratorObject
#include "vm/BytecodeUtil.h" // for JSDVG_IGNORE_STACK
#include "vm/Compartment.h" // for CrossCompartmentKey
#include "vm/EnvironmentObject.h" // for IsSyntacticEnvironment
#include "vm/ErrorReporting.h" // for ReportErrorToGlobal
#include "vm/GeneratorObject.h" // for AbstractGeneratorObject
#include "vm/GlobalObject.h" // for GlobalObject
#include "vm/Interpreter.h" // for Call, ReportIsNotFunction
#include "vm/Iteration.h" // for CreateIterResultObject
#include "vm/JSAtomUtils.h" // for Atomize, AtomizeUTF8Chars, AtomIsMarked, AtomToId, ClassName
#include "vm/JSContext.h" // for JSContext
#include "vm/JSFunction.h" // for JSFunction
#include "vm/JSObject.h" // for JSObject, RequireObject,
#include "vm/JSScript.h" // for BaseScript, ScriptSourceObject
#include "vm/ObjectOperations.h" // for DefineDataProperty
#include "vm/PlainObject.h" // for js::PlainObject
#include "vm/PromiseObject.h" // for js::PromiseObject
#include "vm/ProxyObject.h" // for ProxyObject, JSObject::is
#include "vm/Realm.h" // for AutoRealm, Realm
#include "vm/Runtime.h" // for ReportOutOfMemory, JSRuntime
#include "vm/SavedFrame.h" // for SavedFrame
#include "vm/SavedStacks.h" // for SavedStacks
#include "vm/Scope.h" // for Scope
#include "vm/StringType.h" // for JSString, PropertyName
#include "vm/WrapperObject.h" // for CrossCompartmentWrapperObject
#include "wasm/WasmDebug.h" // for DebugState
#include "wasm/WasmInstance.h" // for Instance
#include "wasm/WasmJS.h" // for WasmInstanceObject
#include "wasm/WasmRealm.h" // for Realm
#include "wasm/WasmTypeDecls.h" // for WasmInstanceObjectVector
#include "debugger/DebugAPI-inl.h"
#include "debugger/Environment-inl.h" // for DebuggerEnvironment::owner
#include "debugger/Frame-inl.h" // for DebuggerFrame::hasGeneratorInfo
#include "debugger/Object-inl.h" // for DebuggerObject::owner and isInstance.
#include "debugger/Script-inl.h" // for DebuggerScript::getReferent
#include "gc/GC-inl.h" // for ZoneCellIter
#include "gc/Marking-inl.h" // for MaybeForwarded
#include "gc/StableCellHasher-inl.h"
#include "gc/WeakMap-inl.h" // for DebuggerWeakMap::trace
#include "vm/Compartment-inl.h" // for Compartment::wrap
#include "vm/GeckoProfiler-inl.h" // for AutoSuppressProfilerSampling
#include "vm/JSAtomUtils-inl.h" // for AtomToId, ValueToId
#include "vm/JSContext-inl.h" // for JSContext::check
#include "vm/JSObject-inl.h" // for JSObject::isCallable, NewTenuredObjectWithGivenProto
#include "vm/JSScript-inl.h" // for JSScript::isDebuggee, JSScript
#include "vm/NativeObject-inl.h" // for NativeObject::ensureDenseInitializedLength
#include "vm/ObjectOperations-inl.h" // for GetProperty, HasProperty
#include "vm/Realm-inl.h" // for AutoRealm::AutoRealm
#include "vm/Stack-inl.h" // for AbstractFramePtr::script
namespace js {
namespace frontend {
class FullParseHandler;
}
namespace gc {
struct Cell;
}
namespace jit {
class BaselineFrame;
}
} /* namespace js */
using namespace js;
using JS::AutoStableStringChars;
using JS::CompileOptions;
using JS::SourceOwnership;
using JS::SourceText;
using JS::dbg::AutoEntryMonitor;
using JS::dbg::Builder;
using mozilla::AsVariant;
using mozilla::DebugOnly;
using mozilla::MakeScopeExit;
using mozilla::Maybe;
using mozilla::Nothing;
using mozilla::Some;
using mozilla::TimeDuration;
using mozilla::TimeStamp;
/*** Utils ******************************************************************/
bool js::IsInterpretedNonSelfHostedFunction(JSFunction* fun) {
return fun->isInterpreted() && !fun->isSelfHostedBuiltin();
}
JSScript* js::GetOrCreateFunctionScript(JSContext* cx, HandleFunction fun) {
MOZ_ASSERT(IsInterpretedNonSelfHostedFunction(fun));
AutoRealm ar(cx, fun);
return JSFunction::getOrCreateScript(cx, fun);
}
ArrayObject* js::GetFunctionParameterNamesArray(JSContext* cx,
HandleFunction fun) {
RootedValueVector names(cx);
// The default value for each argument is |undefined|.
if (!names.growBy(fun->nargs())) {
return nullptr;
}
if (IsInterpretedNonSelfHostedFunction(fun) && fun->nargs() > 0) {
RootedScript script(cx, GetOrCreateFunctionScript(cx, fun));
if (!script) {
return nullptr;
}
MOZ_ASSERT(fun->nargs() == script->numArgs());
PositionalFormalParameterIter fi(script);
for (size_t i = 0; i < fun->nargs(); i++, fi++) {
MOZ_ASSERT(fi.argumentSlot() == i);
if (JSAtom* atom = fi.name()) {
// Skip any internal, non-identifier names, like for example ".args".
if (IsIdentifier(atom)) {
cx->markAtom(atom);
names[i].setString(atom);
}
}
}
}
return NewDenseCopiedArray(cx, names.length(), names.begin());
}
bool js::ValueToIdentifier(JSContext* cx, HandleValue v, MutableHandleId id) {
if (!ToPropertyKey(cx, v, id)) {
return false;
}
if (!id.isAtom() || !IsIdentifier(id.toAtom())) {
RootedValue val(cx, v);
ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, val,
nullptr, "not an identifier");
return false;
}
return true;
}
class js::AutoRestoreRealmDebugMode {
Realm* realm_;
unsigned bits_;
public:
explicit AutoRestoreRealmDebugMode(Realm* realm)
: realm_(realm), bits_(realm->debugModeBits_) {
MOZ_ASSERT(realm_);
}
~AutoRestoreRealmDebugMode() {
if (realm_) {
realm_->debugModeBits_ = bits_;
}
}
void release() { realm_ = nullptr; }
};
/* static */
bool DebugAPI::slowPathCheckNoExecute(JSContext* cx, HandleScript script) {
MOZ_ASSERT(cx->realm()->isDebuggee());
MOZ_ASSERT(cx->noExecuteDebuggerTop);
return EnterDebuggeeNoExecute::reportIfFoundInStack(cx, script);
}
static void PropagateForcedReturn(JSContext* cx, AbstractFramePtr frame,
HandleValue rval) {
// The Debugger's hooks may return a value that affects the completion
// value of the given frame. For example, a hook may return `{ return: 42 }`
// to terminate the frame and return `42` as the final frame result.
// To accomplish this, the debugger treats these return values as if
// execution of the JS function has been terminated without a pending
// exception, but with a special flag. When the error is handled by the
// interpreter or JIT, the special flag and the error state will be cleared
// and execution will continue from the end of the frame.
MOZ_ASSERT(!cx->isExceptionPending());
cx->setPropagatingForcedReturn();
frame.setReturnValue(rval);
}
[[nodiscard]] static bool AdjustGeneratorResumptionValue(JSContext* cx,
AbstractFramePtr frame,
ResumeMode& resumeMode,
MutableHandleValue vp);
[[nodiscard]] static bool ApplyFrameResumeMode(JSContext* cx,
AbstractFramePtr frame,
ResumeMode resumeMode,
HandleValue rv,
Handle<SavedFrame*> exnStack) {
RootedValue rval(cx, rv);
// The value passed in here is unwrapped and has no guarantees about what
// compartment it may be associated with, so we explicitly wrap it into the
// debuggee compartment.
if (!cx->compartment()->wrap(cx, &rval)) {
return false;
}
if (!AdjustGeneratorResumptionValue(cx, frame, resumeMode, &rval)) {
return false;
}
switch (resumeMode) {
case ResumeMode::Continue:
break;
case ResumeMode::Throw:
// If we have a stack from the original throw, use it instead of
// associating the throw with the current execution point.
if (exnStack) {
cx->setPendingException(rval, exnStack);
} else {
cx->setPendingException(rval, ShouldCaptureStack::Always);
}
return false;
case ResumeMode::Terminate:
cx->clearPendingException();
return false;
case ResumeMode::Return:
PropagateForcedReturn(cx, frame, rval);
return false;
default:
MOZ_CRASH("bad Debugger::onEnterFrame resume mode");
}
return true;
}
static bool ApplyFrameResumeMode(JSContext* cx, AbstractFramePtr frame,
ResumeMode resumeMode, HandleValue rval) {
Rooted<SavedFrame*> nullStack(cx);
return ApplyFrameResumeMode(cx, frame, resumeMode, rval, nullStack);
}
bool js::ValueToStableChars(JSContext* cx, const char* fnname,
HandleValue value,
AutoStableStringChars& stableChars) {
if (!value.isString()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_NOT_EXPECTED_TYPE, fnname, "string",
InformalValueTypeName(value));
return false;
}
Rooted<JSLinearString*> linear(cx, value.toString()->ensureLinear(cx));
if (!linear) {
return false;
}
if (!stableChars.initTwoByte(cx, linear)) {
return false;
}
return true;
}
bool EvalOptions::setFilename(JSContext* cx, const char* filename) {
JS::UniqueChars copy;
if (filename) {
copy = DuplicateString(cx, filename);
if (!copy) {
return false;
}
}
filename_ = std::move(copy);
return true;
}
bool js::ParseEvalOptions(JSContext* cx, HandleValue value,
EvalOptions& options) {
if (!value.isObject()) {
return true;
}
RootedObject opts(cx, &value.toObject());
RootedValue v(cx);
if (!JS_GetProperty(cx, opts, "url", &v)) {
return false;
}
if (!v.isUndefined()) {
RootedString url_str(cx, ToString<CanGC>(cx, v));
if (!url_str) {
return false;
}
UniqueChars url_bytes = JS_EncodeStringToUTF8(cx, url_str);
if (!url_bytes) {
return false;
}
if (!options.setFilename(cx, url_bytes.get())) {
return false;
}
}
if (!JS_GetProperty(cx, opts, "lineNumber", &v)) {
return false;
}
if (!v.isUndefined()) {
uint32_t lineno;
if (!ToUint32(cx, v, &lineno)) {
return false;
}
options.setLineno(lineno);
}
if (!JS_GetProperty(cx, opts, "hideFromDebugger", &v)) {
return false;
}
options.setHideFromDebugger(ToBoolean(v));
if (options.kind() == EvalOptions::EnvKind::GlobalWithExtraOuterBindings) {
if (!JS_GetProperty(cx, opts, "useInnerBindings", &v)) {
return false;
}
if (ToBoolean(v)) {
options.setUseInnerBindings();
}
}
return true;
}
/*** Breakpoints ************************************************************/
bool BreakpointSite::isEmpty() const { return breakpoints.isEmpty(); }
void BreakpointSite::trace(JSTracer* trc) {
for (auto p = breakpoints.begin(); p; p++) {
p->trace(trc);
}
}
void BreakpointSite::finalize(JS::GCContext* gcx) {
while (!breakpoints.isEmpty()) {
breakpoints.begin()->delete_(gcx);
}
}
Breakpoint* BreakpointSite::firstBreakpoint() const {
if (isEmpty()) {
return nullptr;
}
return &(*breakpoints.begin());
}
bool BreakpointSite::hasBreakpoint(Breakpoint* toFind) {
const BreakpointList::Iterator bp(toFind);
for (auto p = breakpoints.begin(); p; p++) {
if (p == bp) {
return true;
}
}
return false;
}
Breakpoint::Breakpoint(Debugger* debugger, HandleObject wrappedDebugger,
BreakpointSite* site, HandleObject handler)
: debugger(debugger),
wrappedDebugger(wrappedDebugger),
site(site),
handler(handler) {
MOZ_ASSERT(UncheckedUnwrap(wrappedDebugger) == debugger->object);
MOZ_ASSERT(handler->compartment() == wrappedDebugger->compartment());
debugger->breakpoints.pushBack(this);
site->breakpoints.pushBack(this);
}
void Breakpoint::trace(JSTracer* trc) {
TraceEdge(trc, &wrappedDebugger, "breakpoint owner");
TraceEdge(trc, &handler, "breakpoint handler");
}
void Breakpoint::delete_(JS::GCContext* gcx) {
debugger->breakpoints.remove(this);
site->breakpoints.remove(this);
gc::Cell* cell = site->owningCell();
gcx->delete_(cell, this, MemoryUse::Breakpoint);
}
void Breakpoint::remove(JS::GCContext* gcx) {
BreakpointSite* savedSite = site;
delete_(gcx);
savedSite->destroyIfEmpty(gcx);
}
Breakpoint* Breakpoint::nextInDebugger() { return debuggerLink.mNext; }
Breakpoint* Breakpoint::nextInSite() { return siteLink.mNext; }
JSBreakpointSite::JSBreakpointSite(JSScript* script, jsbytecode* pc)
: script(script), pc(pc) {
MOZ_ASSERT(!DebugAPI::hasBreakpointsAt(script, pc));
}
void JSBreakpointSite::remove(JS::GCContext* gcx) {
DebugScript::destroyBreakpointSite(gcx, script, pc);
}
void JSBreakpointSite::trace(JSTracer* trc) {
BreakpointSite::trace(trc);
TraceEdge(trc, &script, "breakpoint script");
}
void JSBreakpointSite::delete_(JS::GCContext* gcx) {
BreakpointSite::finalize(gcx);
gcx->delete_(script, this, MemoryUse::BreakpointSite);
}
gc::Cell* JSBreakpointSite::owningCell() { return script; }
Realm* JSBreakpointSite::realm() const { return script->realm(); }
WasmBreakpointSite::WasmBreakpointSite(WasmInstanceObject* instanceObject_,
uint32_t offset_)
: instanceObject(instanceObject_), offset(offset_) {
MOZ_ASSERT(instanceObject_);
MOZ_ASSERT(instanceObject_->instance().debugEnabled());
}
void WasmBreakpointSite::trace(JSTracer* trc) {
BreakpointSite::trace(trc);
TraceEdge(trc, &instanceObject, "breakpoint Wasm instance");
}
void WasmBreakpointSite::remove(JS::GCContext* gcx) {
instanceObject->instance().destroyBreakpointSite(gcx, offset);
}
void WasmBreakpointSite::delete_(JS::GCContext* gcx) {
BreakpointSite::finalize(gcx);
gcx->delete_(instanceObject, this, MemoryUse::BreakpointSite);
}
gc::Cell* WasmBreakpointSite::owningCell() { return instanceObject; }
Realm* WasmBreakpointSite::realm() const { return instanceObject->realm(); }
/*** Debugger hook dispatch *************************************************/
Debugger::Debugger(JSContext* cx, NativeObject* dbg)
: object(dbg),
debuggees(cx->zone()),
uncaughtExceptionHook(nullptr),
allowUnobservedAsmJS(false),
allowUnobservedWasm(false),
exclusiveDebuggerOnEval(false),
inspectNativeCallArguments(false),
collectCoverageInfo(false),
observedGCs(cx->zone()),
allocationsLog(cx),
trackingAllocationSites(false),
allocationSamplingProbability(1.0),
maxAllocationsLogLength(DEFAULT_MAX_LOG_LENGTH),
allocationsLogOverflowed(false),
frames(cx->zone()),
generatorFrames(cx),
scripts(cx),
sources(cx),
objects(cx),
environments(cx),
wasmInstanceScripts(cx),
wasmInstanceSources(cx) {
cx->check(dbg);
cx->runtime()->debuggerList().insertBack(this);
}
template <typename ElementAccess>
static void RemoveDebuggerEntry(
mozilla::DoublyLinkedList<Debugger, ElementAccess>& list, Debugger* dbg) {
// The "probably" here is because there could technically be multiple lists
// with this type signature and theoretically the debugger could be an entry
// in a different one. That is not actually possible however because there
// is only one list the debugger could be in.
if (list.ElementProbablyInList(dbg)) {
list.remove(dbg);
}
}
Debugger::~Debugger() {
MOZ_ASSERT(debuggees.empty());
allocationsLog.clear();
// Breakpoints should hold us alive, so any breakpoints remaining must be set
// in dying JSScripts. We should clean them up, but this never asserts. I'm
// not sure why.
MOZ_ASSERT(breakpoints.isEmpty());
// We don't have to worry about locking here since Debugger is not
// background finalized.
JSContext* cx = TlsContext.get();
RemoveDebuggerEntry(cx->runtime()->onNewGlobalObjectWatchers(), this);
RemoveDebuggerEntry(cx->runtime()->onGarbageCollectionWatchers(), this);
}
#ifdef DEBUG
/* static */
bool Debugger::isChildJSObject(JSObject* obj) {
return obj->getClass() == &DebuggerFrame::class_ ||
obj->getClass() == &DebuggerScript::class_ ||
obj->getClass() == &DebuggerSource::class_ ||
obj->getClass() == &DebuggerObject::class_ ||
obj->getClass() == &DebuggerEnvironment::class_;
}
#endif
bool Debugger::hasMemory() const {
return object->getReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE).isObject();
}
DebuggerMemory& Debugger::memory() const {
MOZ_ASSERT(hasMemory());
return object->getReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE)
.toObject()
.as<DebuggerMemory>();
}
/*** Debugger accessors *******************************************************/
bool Debugger::getFrame(JSContext* cx, const FrameIter& iter,
MutableHandleValue vp) {
Rooted<DebuggerFrame*> result(cx);
if (!Debugger::getFrame(cx, iter, &result)) {
return false;
}
vp.setObject(*result);
return true;
}
bool Debugger::getFrame(JSContext* cx, MutableHandle<DebuggerFrame*> result) {
RootedObject proto(
cx, &object->getReservedSlot(JSSLOT_DEBUG_FRAME_PROTO).toObject());
Rooted<NativeObject*> debugger(cx, object);
// Since there is no frame/generator data to associate with this frame, this
// will create a new, "terminated" Debugger.Frame object.
Rooted<DebuggerFrame*> frame(
cx, DebuggerFrame::create(cx, proto, debugger, nullptr, nullptr));
if (!frame) {
return false;
}
result.set(frame);
return true;
}
bool Debugger::getFrame(JSContext* cx, const FrameIter& iter,
MutableHandle<DebuggerFrame*> result) {
AbstractFramePtr referent = iter.abstractFramePtr();
MOZ_ASSERT_IF(referent.hasScript(), !referent.script()->selfHosted());
FrameMap::AddPtr p = frames.lookupForAdd(referent);
if (!p) {
Rooted<AbstractGeneratorObject*> genObj(cx);
if (referent.isGeneratorFrame()) {
if (referent.isFunctionFrame()) {
AutoRealm ar(cx, referent.callee());
genObj = GetGeneratorObjectForFrame(cx, referent);
} else {
MOZ_ASSERT(referent.isModuleFrame());
AutoRealm ar(cx, referent.script()->module());
genObj = GetGeneratorObjectForFrame(cx, referent);
}
// If this frame has a generator associated with it, but no on-stack
// Debugger.Frame object was found, there should not be a suspended
// Debugger.Frame either because otherwise slowPathOnResumeFrame would
// have already populated the "frames" map with a Debugger.Frame.
MOZ_ASSERT_IF(genObj, !generatorFrames.has(genObj));
// If the frame's generator is closed, there is no way to associate the
// generator with the frame successfully because there is no way to
// get the generator's callee script, and even if we could, having it
// there would in no way affect the behavior of the frame.
if (genObj && genObj->isClosed()) {
genObj = nullptr;
}
// If no AbstractGeneratorObject exists yet, we create a Debugger.Frame
// below anyway, and Debugger::onNewGenerator() will associate it
// with the AbstractGeneratorObject later when we hit JSOp::Generator.
}
// Create and populate the Debugger.Frame object.
RootedObject proto(
cx, &object->getReservedSlot(JSSLOT_DEBUG_FRAME_PROTO).toObject());
Rooted<NativeObject*> debugger(cx, object);
Rooted<DebuggerFrame*> frame(
cx, DebuggerFrame::create(cx, proto, debugger, &iter, genObj));
if (!frame) {
return false;
}
auto terminateDebuggerFrameGuard = MakeScopeExit([&] {
terminateDebuggerFrame(cx->gcContext(), this, frame, referent);
});
if (genObj) {
DependentAddPtr<GeneratorWeakMap> genPtr(cx, generatorFrames, genObj);
if (!genPtr.add(cx, generatorFrames, genObj, frame)) {
return false;
}
}
if (!ensureExecutionObservabilityOfFrame(cx, referent)) {
return false;
}
if (!frames.add(p, referent, frame)) {
ReportOutOfMemory(cx);
return false;
}
terminateDebuggerFrameGuard.release();
}
result.set(p->value());
return true;
}
bool Debugger::getFrame(JSContext* cx, Handle<AbstractGeneratorObject*> genObj,
MutableHandle<DebuggerFrame*> result) {
// To create a Debugger.Frame for a running generator, we'd also need a
// FrameIter for its stack frame. We could make this work by searching the
// stack for the generator's frame, but for the moment, we only need this
// function to handle generators we've found on promises' reaction records,
// which should always be suspended.
MOZ_ASSERT(genObj->isSuspended());
// Do we have an existing Debugger.Frame for this generator?
DependentAddPtr<GeneratorWeakMap> p(cx, generatorFrames, genObj);
if (p) {
MOZ_ASSERT(&p->value()->unwrappedGenerator() == genObj);
result.set(p->value());
return true;
}
// Create a new Debugger.Frame.
RootedObject proto(
cx, &object->getReservedSlot(JSSLOT_DEBUG_FRAME_PROTO).toObject());
Rooted<NativeObject*> debugger(cx, object);
result.set(DebuggerFrame::create(cx, proto, debugger, nullptr, genObj));
if (!result) {
return false;
}
if (!p.add(cx, generatorFrames, genObj, result)) {
terminateDebuggerFrame(cx->gcContext(), this, result, NullFramePtr());
return false;
}
return true;
}
static bool DebuggerExists(
GlobalObject* global, const std::function<bool(Debugger* dbg)>& predicate) {
// The GC analysis can't determine that the predicate can't GC, so let it know
// explicitly.
JS::AutoSuppressGCAnalysis nogc;
for (Realm::DebuggerVectorEntry& entry : global->getDebuggers(nogc)) {
// Callbacks should not create new references to the debugger, so don't
// use a barrier. This allows this method to be called during GC.
if (predicate(entry.dbg.unbarrieredGet())) {
return true;
}
}
return false;
}
/* static */
bool Debugger::hasLiveHook(GlobalObject* global, Hook which) {
return DebuggerExists(global,
[=](Debugger* dbg) { return dbg->getHook(which); });
}
/* static */
bool DebugAPI::debuggerObservesAllExecution(GlobalObject* global) {
return DebuggerExists(
global, [=](Debugger* dbg) { return dbg->observesAllExecution(); });
}
/* static */
bool DebugAPI::debuggerObservesCoverage(GlobalObject* global) {
return DebuggerExists(global,
[=](Debugger* dbg) { return dbg->observesCoverage(); });
}
/* static */
bool DebugAPI::debuggerObservesAsmJS(GlobalObject* global) {
return DebuggerExists(global,
[=](Debugger* dbg) { return dbg->observesAsmJS(); });
}
/* static */
bool DebugAPI::debuggerObservesWasm(GlobalObject* global) {
return DebuggerExists(global,
[=](Debugger* dbg) { return dbg->observesWasm(); });
}
/* static */
bool DebugAPI::debuggerObservesNativeCall(GlobalObject* global) {
return DebuggerExists(
global, [=](Debugger* dbg) { return dbg->observesNativeCalls(); });
}
/* static */
bool DebugAPI::hasExceptionUnwindHook(GlobalObject* global) {
return Debugger::hasLiveHook(global, Debugger::OnExceptionUnwind);
}
/* static */
bool DebugAPI::hasDebuggerStatementHook(GlobalObject* global) {
return Debugger::hasLiveHook(global, Debugger::OnDebuggerStatement);
}
template <typename HookIsEnabledFun /* bool (Debugger*) */>
bool DebuggerList<HookIsEnabledFun>::init(JSContext* cx) {
// Determine which debuggers will receive this event, and in what order.
// Make a copy of the list, since the original is mutable and we will be
// calling into arbitrary JS.
Handle<GlobalObject*> global = cx->global();
JS::AutoAssertNoGC nogc;
for (Realm::DebuggerVectorEntry& entry : global->getDebuggers(nogc)) {
Debugger* dbg = entry.dbg;
if (dbg->isHookCallAllowed(cx) && hookIsEnabled(dbg)) {
if (!debuggers.append(ObjectValue(*dbg->toJSObject()))) {
return false;
}
}
}
return true;
}
template <typename HookIsEnabledFun /* bool (Debugger*) */>
template <typename FireHookFun /* bool (Debugger*) */>
bool DebuggerList<HookIsEnabledFun>::dispatchHook(JSContext* cx,
FireHookFun fireHook) {
// Preserve the debuggee's microtask event queue while we run the hooks, so
// the debugger's microtask checkpoints don't run from the debuggee's
// microtasks, and vice versa.
JS::AutoDebuggerJobQueueInterruption adjqi;
if (!adjqi.init(cx)) {
return false;
}
// Deliver the event to each debugger, checking again to make sure it
// should still be delivered.
Handle<GlobalObject*> global = cx->global();
for (Value* p = debuggers.begin(); p != debuggers.end(); p++) {
Debugger* dbg = Debugger::fromJSObject(&p->toObject());
EnterDebuggeeNoExecute nx(cx, *dbg, adjqi);
if (dbg->debuggees.has(global) && hookIsEnabled(dbg)) {
bool result =
dbg->enterDebuggerHook(cx, [&]() -> bool { return fireHook(dbg); });
adjqi.runJobs();
if (!result) {
return false;
}
}
}
return true;
}
template <typename HookIsEnabledFun /* bool (Debugger*) */>
template <typename FireHookFun /* bool (Debugger*) */>
void DebuggerList<HookIsEnabledFun>::dispatchQuietHook(JSContext* cx,
FireHookFun fireHook) {
bool result =
dispatchHook(cx, [&](Debugger* dbg) -> bool { return fireHook(dbg); });
// dispatchHook may fail due to OOM. This OOM is not handlable at the
// callsites of dispatchQuietHook in the engine.
if (!result) {
cx->clearPendingException();
}
}
template <typename HookIsEnabledFun /* bool (Debugger*) */>
template <typename FireHookFun /* bool (Debugger*, ResumeMode&, MutableHandleValue vp) */>
bool DebuggerList<HookIsEnabledFun>::dispatchResumptionHook(
JSContext* cx, AbstractFramePtr frame, FireHookFun fireHook) {
ResumeMode resumeMode = ResumeMode::Continue;
RootedValue rval(cx);
return dispatchHook(cx,
[&](Debugger* dbg) -> bool {
return fireHook(dbg, resumeMode, &rval);
}) &&
ApplyFrameResumeMode(cx, frame, resumeMode, rval);
}
JSObject* Debugger::getHook(Hook hook) const {
MOZ_ASSERT(hook >= 0 && hook < HookCount);
const Value& v = object->getReservedSlot(JSSLOT_DEBUG_HOOK_START +
std::underlying_type_t<Hook>(hook));
return v.isUndefined() ? nullptr : &v.toObject();
}
bool Debugger::hasAnyLiveHooks() const {
// A onNewGlobalObject hook does not hold its Debugger live, so its behavior
// is nondeterministic. This behavior is not satisfying, but it is at least
// documented.
if (getHook(OnDebuggerStatement) || getHook(OnExceptionUnwind) ||
getHook(OnNewScript) || getHook(OnEnterFrame)) {
return true;
}
return false;
}
/* static */
bool DebugAPI::slowPathOnEnterFrame(JSContext* cx, AbstractFramePtr frame) {
return Debugger::dispatchResumptionHook(
cx, frame,
[frame](Debugger* dbg) -> bool {
return dbg->observesFrame(frame) && dbg->observesEnterFrame();
},
[&](Debugger* dbg, ResumeMode& resumeMode, MutableHandleValue vp)
-> bool { return dbg->fireEnterFrame(cx, resumeMode, vp); });
}
/* static */
bool DebugAPI::slowPathOnResumeFrame(JSContext* cx, AbstractFramePtr frame) {
// Don't count on this method to be called every time a generator is
// resumed! This is called only if the frame's debuggee bit is set,
// i.e. the script has breakpoints or the frame is stepping.
MOZ_ASSERT(frame.isGeneratorFrame());
MOZ_ASSERT(frame.isDebuggee());
Rooted<AbstractGeneratorObject*> genObj(
cx, GetGeneratorObjectForFrame(cx, frame));
MOZ_ASSERT(genObj);
// If there is an OOM, we mark all of the Debugger.Frame objects terminated
// because we want to ensure that none of the frames are in a partially
// initialized state where they are in "generatorFrames" but not "frames".
auto terminateDebuggerFramesGuard = MakeScopeExit([&] {
Debugger::terminateDebuggerFrames(cx, frame);
MOZ_ASSERT(!DebugAPI::inFrameMaps(frame));
});
// For each debugger, if there is an existing Debugger.Frame object for the
// resumed `frame`, update it with the new frame pointer and make sure the
// frame is observable.
FrameIter iter(cx);
MOZ_ASSERT(iter.abstractFramePtr() == frame);
{
JS::AutoAssertNoGC nogc;
for (Realm::DebuggerVectorEntry& entry :
frame.global()->getDebuggers(nogc)) {
Debugger* dbg = entry.dbg;
if (Debugger::GeneratorWeakMap::Ptr generatorEntry =
dbg->generatorFrames.lookup(genObj)) {
DebuggerFrame* frameObj = generatorEntry->value();
MOZ_ASSERT(&frameObj->unwrappedGenerator() == genObj);
if (!dbg->frames.putNew(frame, frameObj)) {
ReportOutOfMemory(cx);
return false;
}
if (!frameObj->resume(iter)) {
return false;
}
}
}
}
terminateDebuggerFramesGuard.release();
return slowPathOnEnterFrame(cx, frame);
}
/* static */
NativeResumeMode DebugAPI::slowPathOnNativeCall(JSContext* cx,
const CallArgs& args,
CallReason reason) {
if (!cx->realm()->debuggerObservesNativeCall()) {
return NativeResumeMode::Continue;
}
DebuggerList debuggerList(cx, [](Debugger* dbg) -> bool {
return dbg->getHook(Debugger::OnNativeCall);
});
if (!debuggerList.init(cx)) {
return NativeResumeMode::Abort;
}
if (debuggerList.empty()) {
return NativeResumeMode::Continue;
}
// The onNativeCall hook is fired when self hosted functions are called,
// and any other self hosted function or C++ native that is directly called
// by the self hosted function is considered to be part of the same
// native call, except for the following 4 cases:
//
// * callContentFunction and constructContentFunction,
// which uses CallReason::CallContent
// * Function.prototype.call and Function.prototype.apply,
// which uses CallReason::FunCall
// * Getter call which uses CallReason::Getter
// * Setter call which uses CallReason::Setter
//
// We check this only after checking that debuggerList has items in order
// to avoid unnecessary calls to cx->currentScript(), which can be expensive
// when the top frame is in jitcode.
JSScript* script = cx->currentScript();
if (script && script->selfHosted() && reason != CallReason::CallContent &&
reason != CallReason::FunCall && reason != CallReason::Getter &&
reason != CallReason::Setter) {
return NativeResumeMode::Continue;
}
RootedValue rval(cx);
ResumeMode resumeMode = ResumeMode::Continue;
bool result = debuggerList.dispatchHook(cx, [&](Debugger* dbg) -> bool {
return dbg->fireNativeCall(cx, args, reason, resumeMode, &rval);
});
if (!result) {
return NativeResumeMode::Abort;
}
// Hook must follow normal native function conventions and not return
// primitive values.
if (resumeMode == ResumeMode::Return) {
if (args.isConstructing() && !rval.isObject()) {
JS_ReportErrorASCII(
cx, "onNativeCall hook must return an object for constructor call");
return NativeResumeMode::Abort;
}
}
// The value is not in any particular compartment, so it needs to be
// explicitly wrapped into the debuggee compartment.
if (!cx->compartment()->wrap(cx, &rval)) {
return NativeResumeMode::Abort;
}
switch (resumeMode) {
case ResumeMode::Continue:
break;
case ResumeMode::Throw:
cx->setPendingException(rval, ShouldCaptureStack::Always);
return NativeResumeMode::Abort;
case ResumeMode::Terminate:
cx->clearPendingException();
return NativeResumeMode::Abort;
case ResumeMode::Return:
args.rval().set(rval);
return NativeResumeMode::Override;
}
return NativeResumeMode::Continue;
}
/*
* RAII class to mark a generator as "running" temporarily while running
* debugger code.
*
* When Debugger::slowPathOnLeaveFrame is called for a frame that is yielding
* or awaiting, its generator is in the "suspended" state. Letting script
* observe this state, with the generator on stack yet also reenterable, would
* be bad, so we mark it running while we fire events.
*/
class MOZ_RAII AutoSetGeneratorRunning {
int32_t resumeIndex_;
AsyncGeneratorObject::State asyncGenState_;
Rooted<AbstractGeneratorObject*> genObj_;
public:
AutoSetGeneratorRunning(JSContext* cx,
Handle<AbstractGeneratorObject*> genObj)
: resumeIndex_(0),
asyncGenState_(static_cast<AsyncGeneratorObject::State>(0)),
genObj_(cx, genObj) {
if (genObj) {
if (!genObj->isClosed() && !genObj->isBeforeInitialYield() &&
genObj->isSuspended()) {
// Yielding or awaiting.
resumeIndex_ = genObj->resumeIndex();
genObj->setRunning();
// Async generators have additionally bookkeeping which must be
// adjusted when switching over to the running state.
if (genObj->is<AsyncGeneratorObject>()) {
auto* generator = &genObj->as<AsyncGeneratorObject>();
asyncGenState_ = generator->state();
generator->setExecuting();
}
} else {
// Returning or throwing. The generator is already closed, if
// it was ever exposed at all.
genObj_ = nullptr;
}
}
}
~AutoSetGeneratorRunning() {
if (genObj_) {
MOZ_ASSERT(genObj_->isRunning());
genObj_->setResumeIndex(resumeIndex_);
if (genObj_->is<AsyncGeneratorObject>()) {
genObj_->as<AsyncGeneratorObject>().setState(asyncGenState_);
}
}
}
};
/*
* Handle leaving a frame with debuggers watching. |frameOk| indicates whether
* the frame is exiting normally or abruptly. Set |cx|'s exception and/or
* |cx->fp()|'s return value, and return a new success value.
*/
/* static */
bool DebugAPI::slowPathOnLeaveFrame(JSContext* cx, AbstractFramePtr frame,
const jsbytecode* pc, bool frameOk) {
MOZ_ASSERT_IF(!frame.isWasmDebugFrame(), pc);
mozilla::DebugOnly<Handle<GlobalObject*>> debuggeeGlobal = cx->global();
// These are updated below, but consulted by the cleanup code we register now,
// so declare them here, initialized to quiescent values.
Rooted<Completion> completion(cx);
bool success = false;
auto frameMapsGuard = MakeScopeExit([&] {
// Clean up all Debugger.Frame instances on exit. On suspending, pass the
// flag that says to leave those frames `.live`. Note that if the completion
// is a suspension but success is false, the generator gets closed, not
// suspended.
if (success && completion.get().suspending()) {
Debugger::suspendGeneratorDebuggerFrames(cx, frame);
} else {
Debugger::terminateDebuggerFrames(cx, frame);
}
});
// The onPop handler and associated clean up logic should not run multiple
// times on the same frame. If slowPathOnLeaveFrame has already been
// called, the frame will not be present in the Debugger frame maps.
Rooted<Debugger::DebuggerFrameVector> frames(cx);
if (!Debugger::getDebuggerFrames(frame, &frames)) {
// There is at least one match Debugger.Frame we failed to process, so drop
// the pending exception and raise an out-of-memory instead.
if (!frameOk) {
cx->clearPendingException();
}
ReportOutOfMemory(cx);
return false;
}
if (frames.empty()) {
return frameOk;
}
// Convert current exception state into a Completion and clear exception off
// of the JSContext.
completion = Completion::fromJSFramePop(cx, frame, pc, frameOk);
ResumeMode resumeMode = ResumeMode::Continue;
RootedValue rval(cx);
{
// Preserve the debuggee's microtask event queue while we run the hooks, so
// the debugger's microtask checkpoints don't run from the debuggee's
// microtasks, and vice versa.
JS::AutoDebuggerJobQueueInterruption adjqi;
if (!adjqi.init(cx)) {
return false;
}
// This path can be hit via unwinding the stack due to over-recursion or
// OOM. In those cases, don't fire the frames' onPop handlers, because
// invoking JS will only trigger the same condition. See
// slowPathOnExceptionUnwind.
if (!cx->isThrowingOverRecursed() && !cx->isThrowingOutOfMemory()) {
Rooted<AbstractGeneratorObject*> genObj(
cx, frame.isGeneratorFrame() ? GetGeneratorObjectForFrame(cx, frame)
: nullptr);
// For each Debugger.Frame, fire its onPop handler, if any.
for (size_t i = 0; i < frames.length(); i++) {
Handle<DebuggerFrame*> frameobj = frames[i];
Debugger* dbg = frameobj->owner();
EnterDebuggeeNoExecute nx(cx, *dbg, adjqi);
// Removing a global from a Debugger's debuggee set kills all of that
// Debugger's D.Fs in that global. This means that one D.F's onPop can
// kill the next D.F. So we have to check whether frameobj is still "on
// the stack".
if (frameobj->isOnStack() && frameobj->onPopHandler()) {
OnPopHandler* handler = frameobj->onPopHandler();
bool result = dbg->enterDebuggerHook(cx, [&]() -> bool {
ResumeMode nextResumeMode = ResumeMode::Continue;
RootedValue nextValue(cx);
// Call the onPop handler.
bool success;
{
// Mark the generator as running, to prevent reentrance.
//
// At certain points in a generator's lifetime,
// GetGeneratorObjectForFrame can return null even when the
// generator exists, but at those points the generator has not yet
// been exposed to JavaScript, so reentrance isn't possible
// anyway. So there's no harm done if this has no effect in that
// case.
AutoSetGeneratorRunning asgr(cx, genObj);
success = handler->onPop(cx, frameobj, completion, nextResumeMode,
&nextValue);
}
return dbg->processParsedHandlerResult(cx, frame, pc, success,
nextResumeMode, nextValue,
resumeMode, &rval);
});
adjqi.runJobs();
if (!result) {
return false;
}
// At this point, we are back in the debuggee compartment, and
// any error has been wrapped up as a completion value.
MOZ_ASSERT(!cx->isExceptionPending());
}
}
}
}
completion.get().updateFromHookResult(resumeMode, rval);
// Now that we've run all the handlers, extract the final resumption mode. */
ResumeMode completionResumeMode;
RootedValue completionValue(cx);
Rooted<SavedFrame*> completionStack(cx);
completion.get().toResumeMode(completionResumeMode, &completionValue,
&completionStack);
// If we are returning the original value used to create the completion, then
// we don't want to treat the resumption value as a Return completion, because
// that would cause us to apply AdjustGeneratorResumptionValue to the
// already-adjusted value that the generator actually returned.
if (resumeMode == ResumeMode::Continue &&
completionResumeMode == ResumeMode::Return) {
completionResumeMode = ResumeMode::Continue;
}
if (!ApplyFrameResumeMode(cx, frame, completionResumeMode, completionValue,
completionStack)) {
if (!cx->isPropagatingForcedReturn()) {
// If this is an exception or termination, we just propagate that along.
return false;
}
// Since we are leaving the frame here, we can convert a forced return
// into a normal return right away.
cx->clearPropagatingForcedReturn();
}
success = true;
return true;
}
/* static */
bool DebugAPI::slowPathOnNewGenerator(JSContext* cx, AbstractFramePtr frame,
Handle<AbstractGeneratorObject*> genObj) {
// This is called from JSOp::Generator, after default parameter expressions
// are evaluated and well after onEnterFrame, so Debugger.Frame objects for
// `frame` may already have been exposed to debugger code. The
// AbstractGeneratorObject for this generator call, though, has just been
// created. It must be associated with any existing Debugger.Frames.
// Initializing frames with their associated generator is critical to the
// functionality of the debugger, so if there is an OOM, we want to
// cleanly terminate all of the frames.
auto terminateDebuggerFramesGuard =
MakeScopeExit([&] { Debugger::terminateDebuggerFrames(cx, frame); });
bool ok = true;
gc::AutoSuppressGC nogc(cx);
Debugger::forEachOnStackDebuggerFrame(
frame, nogc, [&](Debugger* dbg, DebuggerFrame* frameObjPtr) {
if (!ok) {
return;
}
Rooted<DebuggerFrame*> frameObj(cx, frameObjPtr);
AutoRealm ar(cx, frameObj);
if (!DebuggerFrame::setGeneratorInfo(cx, frameObj, genObj)) {
// This leaves `genObj` and `frameObj` unassociated. It's OK
// because we won't pause again with this generator on the stack:
// the caller will immediately discard `genObj` and unwind `frame`.
ok = false;
return;
}
DependentAddPtr<Debugger::GeneratorWeakMap> genPtr(
cx, dbg->generatorFrames, genObj);
if (!genPtr.add(cx, dbg->generatorFrames, genObj, frameObj)) {
ok = false;
}
});
if (!ok) {
return false;
}
terminateDebuggerFramesGuard.release();
return true;
}
/* static */
bool DebugAPI::slowPathOnDebuggerStatement(JSContext* cx,
AbstractFramePtr frame) {
return Debugger::dispatchResumptionHook(
cx, frame,
[](Debugger* dbg) -> bool {
return dbg->getHook(Debugger::OnDebuggerStatement);
},
[&](Debugger* dbg, ResumeMode& resumeMode, MutableHandleValue vp)
-> bool { return dbg->fireDebuggerStatement(cx, resumeMode, vp); });
}
/* static */
bool DebugAPI::slowPathOnExceptionUnwind(JSContext* cx,
AbstractFramePtr frame) {
// Invoking more JS on an over-recursed stack or after OOM is only going
// to result in more of the same error.
if (cx->isThrowingOverRecursed() || cx->isThrowingOutOfMemory()) {
return true;
}
// The Debugger API mustn't muck with frames from self-hosted scripts.
if (frame.hasScript() && frame.script()->selfHosted()) {
return true;
}
DebuggerList debuggerList(cx, [](Debugger* dbg) -> bool {
return dbg->getHook(Debugger::OnExceptionUnwind);
});
if (!debuggerList.init(cx)) {
return false;
}
if (debuggerList.empty()) {
return true;
}
// We save and restore the exception once up front to avoid having to do it
// for each 'onExceptionUnwind' hook that has been registered, and we also
// only do it if the debuggerList contains items in order to avoid extra work.
RootedValue exc(cx);
Rooted<SavedFrame*> stack(cx, cx->getPendingExceptionStack());
if (!cx->getPendingException(&exc)) {
return false;
}
cx->clearPendingException();
bool result = debuggerList.dispatchResumptionHook(
cx, frame,
[&](Debugger* dbg, ResumeMode& resumeMode,
MutableHandleValue vp) -> bool {
return dbg->fireExceptionUnwind(cx, exc, resumeMode, vp);
});
if (!result) {
return false;
}
cx->setPendingException(exc, stack);
return true;
}
// TODO: Remove Remove this function when all properties/methods returning a
/// DebuggerEnvironment have been given a C++ interface (bug 1271649).
bool Debugger::wrapEnvironment(JSContext* cx, Handle<Env*> env,
MutableHandleValue rval) {
if (!env) {
rval.setNull();
return true;
}
Rooted<DebuggerEnvironment*> envobj(cx);
if (!wrapEnvironment(cx, env, &envobj)) {
return false;
}
rval.setObject(*envobj);
return true;
}
bool Debugger::wrapEnvironment(JSContext* cx, Handle<Env*> env,
MutableHandle<DebuggerEnvironment*> result) {
MOZ_ASSERT(env);
// DebuggerEnv should only wrap a debug scope chain obtained (transitively)
// from GetDebugEnvironmentFor(Frame|Function).
MOZ_ASSERT(!IsSyntacticEnvironment(env));
DependentAddPtr<EnvironmentWeakMap> p(cx, environments, env);
if (p) {
result.set(&p->value()->as<DebuggerEnvironment>());
} else {
// Create a new Debugger.Environment for env.
RootedObject proto(
cx, &object->getReservedSlot(JSSLOT_DEBUG_ENV_PROTO).toObject());
Rooted<NativeObject*> debugger(cx, object);
Rooted<DebuggerEnvironment*> envobj(
cx, DebuggerEnvironment::create(cx, proto, env, debugger));
if (!envobj) {
return false;
}
if (!p.add(cx, environments, env, envobj)) {
// We need to destroy the edge to the referent, to avoid trying to trace
// it during untimely collections.
envobj->clearReferent();
return false;
}
result.set(envobj);
}
return true;
}
bool Debugger::wrapDebuggeeValue(JSContext* cx, MutableHandleValue vp) {
cx->check(object.get());
if (vp.isObject()) {
RootedObject obj(cx, &vp.toObject());
Rooted<DebuggerObject*> dobj(cx);
if (!wrapDebuggeeObject(cx, obj, &dobj)) {
return false;
}
vp.setObject(*dobj);
} else if (vp.isMagic()) {
Rooted<PlainObject*> optObj(cx, NewPlainObject(cx));
if (!optObj) {
return false;
}
// We handle three sentinel values: missing arguments
// (JS_MISSING_ARGUMENTS), optimized out slots (JS_OPTIMIZED_OUT),
// and uninitialized bindings (JS_UNINITIALIZED_LEXICAL).
//
// Other magic values should not have escaped.
PropertyName* name;
switch (vp.whyMagic()) {
case JS_MISSING_ARGUMENTS:
name = cx->names().missingArguments;
break;
case JS_OPTIMIZED_OUT:
name = cx->names().optimizedOut;
break;
case JS_UNINITIALIZED_LEXICAL:
name = cx->names().uninitialized;
break;
default:
MOZ_CRASH("Unsupported magic value escaped to Debugger");
}
RootedValue trueVal(cx, BooleanValue(true));
if (!DefineDataProperty(cx, optObj, name, trueVal)) {
return false;
}
vp.setObject(*optObj);
} else if (!cx->compartment()->wrap(cx, vp)) {
vp.setUndefined();
return false;
}
return true;
}
bool Debugger::wrapNullableDebuggeeObject(
JSContext* cx, HandleObject obj, MutableHandle<DebuggerObject*> result) {
if (!obj) {
result.set(nullptr);
return true;
}
return wrapDebuggeeObject(cx, obj, result);
}
bool Debugger::wrapDebuggeeObject(JSContext* cx, HandleObject obj,
MutableHandle<DebuggerObject*> result) {
MOZ_ASSERT(obj);
DependentAddPtr<ObjectWeakMap> p(cx, objects, obj);
if (p) {
result.set(&p->value()->as<DebuggerObject>());
} else {
// Create a new Debugger.Object for obj.
Rooted<NativeObject*> debugger(cx, object);
RootedObject proto(
cx, &object->getReservedSlot(JSSLOT_DEBUG_OBJECT_PROTO).toObject());
Rooted<DebuggerObject*> dobj(
cx, DebuggerObject::create(cx, proto, obj, debugger));
if (!dobj) {
return false;
}
if (!p.add(cx, objects, obj, dobj)) {
// We need to destroy the edge to the referent, to avoid trying to trace
// it during untimely collections.
dobj->clearReferent();
return false;
}
result.set(dobj);
}
return true;
}
static DebuggerObject* ToNativeDebuggerObject(JSContext* cx,
MutableHandleObject obj) {
if (!obj->is<DebuggerObject>()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_NOT_EXPECTED_TYPE, "Debugger",
"Debugger.Object", obj->getClass()->name);
return nullptr;
}
return &obj->as<DebuggerObject>();
}
bool Debugger::unwrapDebuggeeObject(JSContext* cx, MutableHandleObject obj) {
DebuggerObject* ndobj = ToNativeDebuggerObject(cx, obj);
if (!ndobj) {
return false;
}
if (ndobj->owner() != Debugger::fromJSObject(object)) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEBUG_WRONG_OWNER, "Debugger.Object");
return false;
}
obj.set(ndobj->referent());
return true;
}
bool Debugger::unwrapDebuggeeValue(JSContext* cx, MutableHandleValue vp) {
cx->check(object.get(), vp);
if (vp.isObject()) {
RootedObject dobj(cx, &vp.toObject());
if (!unwrapDebuggeeObject(cx, &dobj)) {
return false;
}
vp.setObject(*dobj);
}
return true;
}
static bool CheckArgCompartment(JSContext* cx, JSObject* obj, JSObject* arg,
const char* methodname, const char* propname) {
if (arg->compartment() != obj->compartment()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEBUG_COMPARTMENT_MISMATCH, methodname,
propname);
return false;
}
return true;
}
static bool CheckArgCompartment(JSContext* cx, JSObject* obj, HandleValue v,
const char* methodname, const char* propname) {
if (v.isObject()) {
return CheckArgCompartment(cx, obj, &v.toObject(), methodname, propname);
}
return true;
}
bool Debugger::unwrapPropertyDescriptor(
JSContext* cx, HandleObject obj, MutableHandle<PropertyDescriptor> desc) {
if (desc.hasValue()) {
RootedValue value(cx, desc.value());
if (!unwrapDebuggeeValue(cx, &value) ||
!CheckArgCompartment(cx, obj, value, "defineProperty", "value")) {
return false;
}
desc.setValue(value);
}
if (desc.hasGetter()) {
RootedObject get(cx, desc.getter());
if (get) {
if (!unwrapDebuggeeObject(cx, &get)) {
return false;
}
if (!CheckArgCompartment(cx, obj, get, "defineProperty", "get")) {
return false;
}
}
desc.setGetter(get);
}
if (desc.hasSetter()) {
RootedObject set(cx, desc.setter());
if (set) {
if (!unwrapDebuggeeObject(cx, &set)) {
return false;
}
if (!CheckArgCompartment(cx, obj, set, "defineProperty", "set")) {
return false;
}
}
desc.setSetter(set);
}
return true;
}
/*** Debuggee resumption values and debugger error handling *****************/
static bool GetResumptionProperty(JSContext* cx, HandleObject obj,
Handle<PropertyName*> name,
ResumeMode namedMode, ResumeMode& resumeMode,
MutableHandleValue vp, int* hits) {
bool found;
if (!HasProperty(cx, obj, name, &found)) {
return false;
}
if (found) {
++*hits;
resumeMode = namedMode;
if (!GetProperty(cx, obj, obj, name, vp)) {
return false;
}
}
return true;
}
bool js::ParseResumptionValue(JSContext* cx, HandleValue rval,
ResumeMode& resumeMode, MutableHandleValue vp) {
if (rval.isUndefined()) {
resumeMode = ResumeMode::Continue;
vp.setUndefined();
return true;
}
if (rval.isNull()) {
resumeMode = ResumeMode::Terminate;
vp.setUndefined();
return true;
}
int hits = 0;
if (rval.isObject()) {
RootedObject obj(cx, &rval.toObject());
if (!GetResumptionProperty(cx, obj, cx->names().return_, ResumeMode::Return,
resumeMode, vp, &hits)) {
return false;
}
if (!GetResumptionProperty(cx, obj, cx->names().throw_, ResumeMode::Throw,
resumeMode, vp, &hits)) {
return false;
}
}
if (hits != 1) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEBUG_BAD_RESUMPTION);
return false;
}
return true;
}
static bool CheckResumptionValue(JSContext* cx, AbstractFramePtr frame,
const jsbytecode* pc, ResumeMode resumeMode,
MutableHandleValue vp) {
// Only forced returns from a frame need to be validated because forced
// throw values behave just like debuggee `throw` statements. Since
// forced-return is all custom logic within SpiderMonkey itself, we need
// our own custom validation for it to conform with what is expected.
if (resumeMode != ResumeMode::Return || !frame) {
return true;
}
// This replicates the ECMA spec's behavior for [[Construct]] in derived
// class constructors (section 9.2.2 of ECMA262-2020), where returning a
// non-undefined primitive causes an exception tobe thrown.
if (frame.debuggerNeedsCheckPrimitiveReturn() && vp.isPrimitive()) {
if (!vp.isUndefined()) {
ReportValueError(cx, JSMSG_BAD_DERIVED_RETURN, JSDVG_IGNORE_STACK, vp,
nullptr);
return false;
}
RootedValue thisv(cx);
{
AutoRealm ar(cx, frame.environmentChain());
if (!GetThisValueForDebuggerFrameMaybeOptimizedOut(cx, frame, pc,
&thisv)) {
return false;
}
}
if (thisv.isMagic(JS_UNINITIALIZED_LEXICAL)) {
return ThrowUninitializedThis(cx);
}
MOZ_ASSERT(!thisv.isMagic());
if (!cx->compartment()->wrap(cx, &thisv)) {
return false;
}
vp.set(thisv);
}
// Check for forcing return from a generator before the initial yield. This
// is not supported because some engine-internal code assumes a call to a
// generator will return a GeneratorObject; see bug 1477084.
if (frame.isFunctionFrame() && frame.callee()->isGenerator()) {
Rooted<AbstractGeneratorObject*> genObj(cx);
{
AutoRealm ar(cx, frame.callee());
genObj = GetGeneratorObjectForFrame(cx, frame);
}
if (!genObj || genObj->isBeforeInitialYield()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEBUG_FORCED_RETURN_DISALLOWED);
return false;
}
}
return true;
}
// Last-minute sanity adjustments to resumption.
//
// This is called last, as we leave the debugger. It must happen outside the
// control of the uncaughtExceptionHook, because this code assumes we won't
// change our minds and continue execution--we must not close the generator
// object unless we're really going to force-return.
[[nodiscard]] static bool AdjustGeneratorResumptionValue(
JSContext* cx, AbstractFramePtr frame, ResumeMode& resumeMode,
MutableHandleValue vp) {
if (resumeMode != ResumeMode::Return && resumeMode != ResumeMode::Throw) {
return true;
}
if (!frame) {
return true;
}
// Async modules need to be handled separately, as they do not have a callee.
// frame.callee will throw if it is called on a moduleFrame.
bool isAsyncModule = frame.isModuleFrame() && frame.script()->isAsync();
if (!frame.isFunctionFrame() && !isAsyncModule) {
return true;
}
// Treat `{return: <value>}` like a `return` statement. Simulate what the
// debuggee would do for an ordinary `return` statement, using a few bytecode
// instructions. It's simpler to do the work manually than to count on that
// bytecode sequence existing in the debuggee, somehow jump to it, and then
// avoid re-entering the debugger from it.
//
// Similarly treat `{throw: <value>}` like a `throw` statement.
//
// Note: Async modules use the same handling as async functions.
if (frame.isFunctionFrame() && frame.callee()->isGenerator()) {
// Throw doesn't require any special processing for (async) generators.
if (resumeMode == ResumeMode::Throw) {
return true;
}
// Forcing return from a (possibly async) generator.
Rooted<AbstractGeneratorObject*> genObj(
cx, GetGeneratorObjectForFrame(cx, frame));
// We already went through CheckResumptionValue, which would have replaced
// this invalid resumption value with an error if we were trying to force
// return before the initial yield.
MOZ_RELEASE_ASSERT(genObj && !genObj->isBeforeInitialYield());
// 1. `return <value>` creates and returns a new object,
// `{value: <value>, done: true}`.
//
// For non-async generators, the iterator result object is created in
// bytecode, so we have to simulate that here. For async generators, our
// C++ implementation of AsyncGeneratorResolve will do this. So don't do it
// twice:
if (!genObj->is<AsyncGeneratorObject>()) {
PlainObject* pair = CreateIterResultObject(cx, vp, true);
if (!pair) {
return false;
}
vp.setObject(*pair);
}
// 2. The generator must be closed.
genObj->setClosed(cx);
// Async generators have additionally bookkeeping which must be adjusted
// when switching over to the closed state.
if (genObj->is<AsyncGeneratorObject>()) {
genObj->as<AsyncGeneratorObject>().setCompleted();
}
} else if (isAsyncModule || frame.callee()->isAsync()) {
if (AbstractGeneratorObject* genObj =
GetGeneratorObjectForFrame(cx, frame)) {
// Throw doesn't require any special processing for async functions when
// the internal generator object is already present.
if (resumeMode == ResumeMode::Throw) {
return true;
}
Rooted<AsyncFunctionGeneratorObject*> generator(
cx, &genObj->as<AsyncFunctionGeneratorObject>());
// 1. `return <value>` fulfills and returns the async function's promise.
Rooted<PromiseObject*> promise(cx, generator->promise());
if (promise->state() == JS::PromiseState::Pending) {
if (!AsyncFunctionResolve(cx, generator, vp)) {
return false;
}
}
vp.setObject(*promise);
// 2. The generator must be closed.
generator->setClosed(cx);
} else {
// We're before entering the actual function code.
// 1. `throw <value>` creates a promise rejected with the value *vp.
// 1. `return <value>` creates a promise resolved with the value *vp.
JSObject* promise = resumeMode == ResumeMode::Throw
? PromiseObject::unforgeableReject(cx, vp)
: PromiseObject::unforgeableResolve(cx, vp);
if (!promise) {
return false;
}
vp.setObject(*promise);
// 2. Return normally in both cases.
resumeMode = ResumeMode::Return;
}
}
return true;
}
bool Debugger::processParsedHandlerResult(JSContext* cx, AbstractFramePtr frame,
const jsbytecode* pc, bool success,
ResumeMode resumeMode,
HandleValue value,
ResumeMode& resultMode,
MutableHandleValue vp) {
RootedValue rootValue(cx, value);
if (!success || !prepareResumption(cx, frame, pc, resumeMode, &rootValue)) {
RootedValue exceptionRv(cx);
if (!callUncaughtExceptionHandler(cx, &exceptionRv) ||
!ParseResumptionValue(cx, exceptionRv, resumeMode, &rootValue) ||
!prepareResumption(cx, frame, pc, resumeMode, &rootValue)) {
return false;
}
}
// Since debugger hooks accumulate into the same final value handle, we
// use that to throw if multiple hooks try to set a resumption value.
if (resumeMode != ResumeMode::Continue) {
if (resultMode != ResumeMode::Continue) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEBUG_RESUMPTION_CONFLICT);
return false;
}
vp.set(rootValue);
resultMode = resumeMode;
}
return true;
}
bool Debugger::processHandlerResult(JSContext* cx, bool success, HandleValue rv,
AbstractFramePtr frame, jsbytecode* pc,
ResumeMode& resultMode,
MutableHandleValue vp) {
ResumeMode resumeMode = ResumeMode::Continue;
RootedValue value(cx);
if (success) {
success = ParseResumptionValue(cx, rv, resumeMode, &value);
}
return processParsedHandlerResult(cx, frame, pc, success, resumeMode, value,
resultMode, vp);
}
bool Debugger::prepareResumption(JSContext* cx, AbstractFramePtr frame,
const jsbytecode* pc, ResumeMode& resumeMode,
MutableHandleValue vp) {
return unwrapDebuggeeValue(cx, vp) &&
CheckResumptionValue(cx, frame, pc, resumeMode, vp);
}
bool Debugger::callUncaughtExceptionHandler(JSContext* cx,
MutableHandleValue vp) {
// Uncaught exceptions arise from Debugger code, and so we must already be in
// an NX section. This also establishes that we are already within the scope
// of an AutoDebuggerJobQueueInterruption object.
MOZ_ASSERT(EnterDebuggeeNoExecute::isLockedInStack(cx, *this));
if (cx->isExceptionPending() && uncaughtExceptionHook) {
RootedValue exc(cx);
if (!cx->getPendingException(&exc)) {
return false;
}
cx->clearPendingException();
RootedValue fval(cx, ObjectValue(*uncaughtExceptionHook));
if (js::Call(cx, fval, object, exc, vp)) {
return true;
}
}
return false;
}
bool Debugger::handleUncaughtException(JSContext* cx) {
RootedValue rv(cx);
return callUncaughtExceptionHandler(cx, &rv);
}
void Debugger::reportUncaughtException(JSContext* cx) {
// Uncaught exceptions arise from Debugger code, and so we must already be
// in an NX section.
MOZ_ASSERT(EnterDebuggeeNoExecute::isLockedInStack(cx, *this));
if (cx->isExceptionPending()) {
// We want to report the pending exception, but we want to let the
// embedding handle it however it wants to. So pretend like we're
// starting a new script execution on our current compartment (which
// is the debugger compartment, so reported errors won't get
// reported to various onerror handlers in debuggees) and as part of
// that "execution" simply throw our exception so the embedding can
// deal.
RootedValue exn(cx);
if (cx->getPendingException(&exn)) {
// Clear the exception, because ReportErrorToGlobal will assert that
// we don't have one.
cx->clearPendingException();
ReportErrorToGlobal(cx, cx->global(), exn);
}
// And if not, or if PrepareScriptEnvironmentAndInvoke somehow left an
// exception on cx (which it totally shouldn't do), just give up.
cx->clearPendingException();
}
}
/*** Debuggee completion values *********************************************/
/* static */
Completion Completion::fromJSResult(JSContext* cx, bool ok, const Value& rv) {
MOZ_ASSERT_IF(ok, !cx->isExceptionPending());
if (ok) {
return Completion(Return(rv));
}
if (!cx->isExceptionPending()) {
return Completion(Terminate());
}
RootedValue exception(cx);
Rooted<SavedFrame*> stack(cx, cx->getPendingExceptionStack());
bool getSucceeded = cx->getPendingException(&exception);
cx->clearPendingException();
if (!getSucceeded) {
return Completion(Terminate());
}
return Completion(Throw(exception, stack));
}
/* static */
Completion Completion::fromJSFramePop(JSContext* cx, AbstractFramePtr frame,
const jsbytecode* pc, bool ok) {
// Only Wasm frames get a null pc.
MOZ_ASSERT_IF(!frame.isWasmDebugFrame(), pc);
// If this isn't a generator suspension, then that's already handled above.
if (!ok || !frame.isGeneratorFrame()) {
return fromJSResult(cx, ok, frame.returnValue());
}
// A generator is being suspended or returning.
// Since generators are never wasm, we can assume pc is not nullptr, and
// that analyzing bytecode is meaningful.
MOZ_ASSERT(!frame.isWasmDebugFrame());
// If we're leaving successfully at a yield opcode, we're probably
// suspending; the `isClosed()` check detects a debugger forced return from
// an `onStep` handler, which looks almost the same.
//
// GetGeneratorObjectForFrame can return nullptr even when a generator
// object does exist, if the frame is paused between the Generator and
// SetAliasedVar opcodes. But by checking the opcode first we eliminate that
// possibility, so it's fine to call genObj->isClosed().
Rooted<AbstractGeneratorObject*> generatorObj(
cx, GetGeneratorObjectForFrame(cx, frame));
switch (JSOp(*pc)) {
case JSOp::InitialYield:
MOZ_ASSERT(!generatorObj->isClosed());
return Completion(InitialYield(generatorObj));
case JSOp::Yield:
MOZ_ASSERT(!generatorObj->isClosed());
return Completion(Yield(generatorObj, frame.returnValue()));
case JSOp::Await:
MOZ_ASSERT(!generatorObj->isClosed());
return Completion(Await(generatorObj, frame.returnValue()));
default:
return Completion(Return(frame.returnValue()));
}
}
void Completion::trace(JSTracer* trc) {
variant.match([=](auto& var) { var.trace(trc); });
}
struct MOZ_STACK_CLASS Completion::BuildValueMatcher {
JSContext* cx;
Debugger* dbg;
MutableHandleValue result;
BuildValueMatcher(JSContext* cx, Debugger* dbg, MutableHandleValue result)
: cx(cx), dbg(dbg), result(result) {
cx->check(dbg->toJSObject());
}
bool operator()(const Completion::Return& ret) {
Rooted<NativeObject*> obj(cx, newObject());
RootedValue retval(cx, ret.value);
if (!obj || !wrap(&retval) || !add(obj, cx->names().return_, retval)) {
return false;
}
result.setObject(*obj);
return true;
}
bool operator()(const Completion::Throw& thr) {
Rooted<NativeObject*> obj(cx, newObject());
RootedValue exc(cx, thr.exception);
if (!obj || !wrap(&exc) || !add(obj, cx->names().throw_, exc)) {
return false;
}
if (thr.stack) {
RootedValue stack(cx, ObjectValue(*thr.stack));
if (!wrapStack(&stack) || !add(obj, cx->names().stack, stack)) {
return false;
}
}
result.setObject(*obj);
return true;
}
bool operator()(const Completion::Terminate& term) {
result.setNull();
return true;
}
bool operator()(const Completion::InitialYield& initialYield) {
Rooted<NativeObject*> obj(cx, newObject());
RootedValue gen(cx, ObjectValue(*initialYield.generatorObject));
if (!obj || !wrap(&gen) || !add(obj, cx->names().return_, gen) ||
!add(obj, cx->names().yield, TrueHandleValue) ||
!add(obj, cx->names().initial, TrueHandleValue)) {
return false;
}
result.setObject(*obj);
return true;
}
bool operator()(const Completion::Yield& yield) {
Rooted<NativeObject*> obj(cx, newObject());
RootedValue iteratorResult(cx, yield.iteratorResult);
if (!obj || !wrap(&iteratorResult) ||
!add(obj, cx->names().return_, iteratorResult) ||
!add(obj, cx->names().yield, TrueHandleValue)) {
return false;
}
result.setObject(*obj);
return true;
}
bool operator()(const Completion::Await& await) {
Rooted<NativeObject*> obj(cx, newObject());
RootedValue awaitee(cx, await.awaitee);
if (!obj || !wrap(&awaitee) || !add(obj, cx->names().return_, awaitee) ||
!add(obj, cx->names().await, TrueHandleValue)) {
return false;
}
result.setObject(*obj);
return true;
}
private:
NativeObject* newObject() const { return NewPlainObject(cx); }
bool add(Handle<NativeObject*> obj, PropertyName* name,
HandleValue value) const {
return NativeDefineDataProperty(cx, obj, name, value, JSPROP_ENUMERATE);
}
bool wrap(MutableHandleValue v) const {
return dbg->wrapDebuggeeValue(cx, v);
}
// Saved stacks are wrapped for direct consumption by debugger code.
bool wrapStack(MutableHandleValue stack) const {
return cx->compartment()->wrap(cx, stack);
}
};
bool Completion::buildCompletionValue(JSContext* cx, Debugger* dbg,
MutableHandleValue result) const {
return variant.match(BuildValueMatcher(cx, dbg, result));
}
void Completion::updateFromHookResult(ResumeMode resumeMode,
HandleValue value) {
switch (resumeMode) {
case ResumeMode::Continue:
// No change to how we'll resume.
break;
case ResumeMode::Throw:
// Since this is a new exception, the stack for the old one may not apply.
// If we extend resumption values to specify stacks, we could revisit
// this.
variant = Variant(Throw(value, nullptr));
break;
case ResumeMode::Terminate:
variant = Variant(Terminate());
break;
case ResumeMode::Return:
variant = Variant(Return(value));
break;
default:
MOZ_CRASH("invalid resumeMode value");
}
}
struct MOZ_STACK_CLASS Completion::ToResumeModeMatcher {
MutableHandleValue value;
MutableHandle<SavedFrame*> exnStack;
ToResumeModeMatcher(MutableHandleValue value,
MutableHandle<SavedFrame*> exnStack)
: value(value), exnStack(exnStack) {}
ResumeMode operator()(const Return& ret) {
value.set(ret.value);
return ResumeMode::Return;
}
ResumeMode operator()(const Throw& thr) {
value.set(thr.exception);
exnStack.set(thr.stack);
return ResumeMode::Throw;
}
ResumeMode operator()(const Terminate& term) {
value.setUndefined();
return ResumeMode::Terminate;
}
ResumeMode operator()(const InitialYield& initialYield) {
value.setObject(*initialYield.generatorObject);
return ResumeMode::Return;
}
ResumeMode operator()(const Yield& yield) {
value.set(yield.iteratorResult);
return ResumeMode::Return;
}
ResumeMode operator()(const Await& await) {
value.set(await.awaitee);
return ResumeMode::Return;
}
};
void Completion::toResumeMode(ResumeMode& resumeMode, MutableHandleValue value,
MutableHandle<SavedFrame*> exnStack) const {
resumeMode = variant.match(ToResumeModeMatcher(value, exnStack));
}
/*** Firing debugger hooks **************************************************/
static bool CallMethodIfPresent(JSContext* cx, HandleObject obj,
const char* name, size_t argc, Value* argv,
MutableHandleValue rval) {
rval.setUndefined();
JSAtom* atom = Atomize(cx, name, strlen(name));
if (!atom) {
return false;
}
RootedId id(cx, AtomToId(atom));
RootedValue fval(cx);
if (!GetProperty(cx, obj, obj, id, &fval)) {
return false;
}
if (!IsCallable(fval)) {
return true;
}
InvokeArgs args(cx);
if (!args.init(cx, argc)) {
return false;
}
for (size_t i = 0; i < argc; i++) {
args[i].set(argv[i]);
}
rval.setObject(*obj); // overwritten by successful Call
return js::Call(cx, fval, rval, args, rval);
}
bool Debugger::fireDebuggerStatement(JSContext* cx, ResumeMode& resumeMode,
MutableHandleValue vp) {
RootedObject hook(cx, getHook(OnDebuggerStatement));
MOZ_ASSERT(hook);
MOZ_ASSERT(hook->isCallable());
ScriptFrameIter iter(cx);
RootedValue scriptFrame(cx);
if (!getFrame(cx, iter, &scriptFrame)) {
return false;
}
RootedValue fval(cx, ObjectValue(*hook));
RootedValue rv(cx);
bool ok = js::Call(cx, fval, object, scriptFrame, &rv);
return processHandlerResult(cx, ok, rv, iter.abstractFramePtr(), iter.pc(),
resumeMode, vp);
}
bool Debugger::fireExceptionUnwind(JSContext* cx, HandleValue exc,
ResumeMode& resumeMode,
MutableHandleValue vp) {
RootedObject hook(cx, getHook(OnExceptionUnwind));
MOZ_ASSERT(hook);
MOZ_ASSERT(hook->isCallable());
RootedValue scriptFrame(cx);
RootedValue wrappedExc(cx, exc);
FrameIter iter(cx);
if (!getFrame(cx, iter, &scriptFrame) ||
!wrapDebuggeeValue(cx, &wrappedExc)) {
return false;
}
RootedValue fval(cx, ObjectValue(*hook));
RootedValue rv(cx);
bool ok = js::Call(cx, fval, object, scriptFrame, wrappedExc, &rv);
return processHandlerResult(cx, ok, rv, iter.abstractFramePtr(), iter.pc(),
resumeMode, vp);
}
bool Debugger::fireEnterFrame(JSContext* cx, ResumeMode& resumeMode,
MutableHandleValue vp) {
RootedObject hook(cx, getHook(OnEnterFrame));
MOZ_ASSERT(hook);
MOZ_ASSERT(hook->isCallable());
RootedValue scriptFrame(cx);
FrameIter iter(cx);
#if DEBUG
// Assert that the hook won't be able to re-enter the generator.
if (iter.hasScript() && JSOp(*iter.pc()) == JSOp::AfterYield) {
AutoRealm ar(cx, iter.script());
auto* genObj = GetGeneratorObjectForFrame(cx, iter.abstractFramePtr());
MOZ_ASSERT(genObj->isRunning());
}
#endif
if (!getFrame(cx, iter, &scriptFrame)) {
return false;
}
RootedValue fval(cx, ObjectValue(*hook));
RootedValue rv(cx);
bool ok = js::Call(cx, fval, object, scriptFrame, &rv);
return processHandlerResult(cx, ok, rv, iter.abstractFramePtr(), iter.pc(),
resumeMode, vp);
}
bool Debugger::fireNativeCall(JSContext* cx, const CallArgs& args,
CallReason reason, ResumeMode& resumeMode,
MutableHandleValue vp) {
RootedObject hook(cx, getHook(OnNativeCall));
MOZ_ASSERT(hook);
MOZ_ASSERT(hook->isCallable());
RootedValue fval(cx, ObjectValue(*hook));
RootedValue calleeval(cx, args.calleev());
if (!wrapDebuggeeValue(cx, &calleeval)) {
return false;
}
JSAtom* reasonAtom = nullptr;
switch (reason) {
case CallReason::Call:
reasonAtom = cx->names().call;
break;
case CallReason::CallContent:
reasonAtom = cx->names().call;
break;
case CallReason::FunCall:
reasonAtom = cx->names().call;
break;
case CallReason::Getter:
reasonAtom = cx->names().get;
break;
case CallReason::Setter:
reasonAtom = cx->names().set;
break;
}
MOZ_ASSERT(AtomIsMarked(cx->zone(), reasonAtom));
RootedValue reasonval(cx, StringValue(reasonAtom));
bool ok = false;
RootedValue rv(cx);
if (inspectNativeCallArguments) {
RootedValue thisVal(cx, args.thisv());
// Ignore anything that may make wrapDebuggeeValue to throw
if (thisVal.isMagic() && thisVal.whyMagic() != JS_MISSING_ARGUMENTS &&
thisVal.whyMagic() != JS_UNINITIALIZED_LEXICAL) {
thisVal.setMagic(JS_OPTIMIZED_OUT);
}
if (!wrapDebuggeeValue(cx, &thisVal)) {
return false;
}
unsigned arrsize = args.length();
Rooted<ArrayObject*> arrobj(cx, NewDenseFullyAllocatedArray(cx, arrsize));
if (!arrobj) {
return false;
}
arrobj->ensureDenseInitializedLength(0, arrsize);
for (unsigned i = 0; i < arrsize; i++) {
RootedValue v(cx, args.get(i));
if (!wrapDebuggeeValue(cx, &v)) {
return false;
}
arrobj->setDenseElement(i, v);
}
RootedValue arrayval(cx, ObjectValue(*arrobj));
if (!wrapDebuggeeValue(cx, &arrayval)) {
return false;
}
FixedInvokeArgs<4> iargs(cx);
iargs[0].set(calleeval);
iargs[1].set(reasonval);
iargs[2].set(thisVal);
iargs[3].set(arrayval);
RootedValue thisv(cx, ObjectOrNullValue(object));
ok = js::Call(cx, fval, thisv, iargs, &rv);
} else {
ok = js::Call(cx, fval, object, calleeval, reasonval, &rv);
}
return processHandlerResult(cx, ok, rv, NullFramePtr(), nullptr, resumeMode,
vp);
}
bool Debugger::fireNewScript(JSContext* cx,
Handle<DebuggerScriptReferent> scriptReferent) {
RootedObject hook(cx, getHook(OnNewScript));
MOZ_ASSERT(hook);
MOZ_ASSERT(hook->isCallable());
JSObject* dsobj = wrapVariantReferent(cx, scriptReferent);
if (!dsobj) {
return false;
}
RootedValue fval(cx, ObjectValue(*hook));
RootedValue dsval(cx, ObjectValue(*dsobj));
RootedValue rv(cx);
return js::Call(cx, fval, object, dsval, &rv) || handleUncaughtException(cx);
}
bool Debugger::fireOnGarbageCollectionHook(
JSContext* cx, const JS::dbg::GarbageCollectionEvent::Ptr& gcData) {
MOZ_ASSERT(observedGC(gcData->majorGCNumber()));
observedGCs.remove(gcData->majorGCNumber());
RootedObject hook(cx, getHook(OnGarbageCollection));
MOZ_ASSERT(hook);
MOZ_ASSERT(hook->isCallable());
JSObject* dataObj = gcData->toJSObject(cx);
if (!dataObj) {
return false;
}
RootedValue fval(cx, ObjectValue(*hook));
RootedValue dataVal(cx, ObjectValue(*dataObj));
RootedValue rv(cx);
return js::Call(cx, fval, object, dataVal, &rv) ||
handleUncaughtException(cx);
}
template <typename HookIsEnabledFun /* bool (Debugger*) */,
typename FireHookFun /* bool (Debugger*) */>
/* static */
void Debugger::dispatchQuietHook(JSContext* cx, HookIsEnabledFun hookIsEnabled,
FireHookFun fireHook) {
DebuggerList<HookIsEnabledFun> debuggerList(cx, hookIsEnabled);
if (!debuggerList.init(cx)) {
// init may fail due to OOM. This OOM is not handlable at the
// callsites of dispatchQuietHook in the engine.
cx->clearPendingException();
return;
}
debuggerList.dispatchQuietHook(cx, fireHook);
}
template <typename HookIsEnabledFun /* bool (Debugger*) */, typename FireHookFun /* bool (Debugger*, ResumeMode&, MutableHandleValue vp) */>
/* static */
bool Debugger::dispatchResumptionHook(JSContext* cx, AbstractFramePtr frame,
HookIsEnabledFun hookIsEnabled,
FireHookFun fireHook) {
DebuggerList<HookIsEnabledFun> debuggerList(cx, hookIsEnabled);
if (!debuggerList.init(cx)) {
return false;
}
return debuggerList.dispatchResumptionHook(cx, frame, fireHook);
}
// Maximum length for source URLs that can be remembered.
static const size_t SourceURLMaxLength = 1024;
// Maximum number of source URLs that can be remembered in a realm.
static const size_t SourceURLRealmLimit = 100;
static bool RememberSourceURL(JSContext* cx, HandleScript script) {
cx->check(script);
// Sources introduced dynamically are not remembered.
if (script->sourceObject()->unwrappedIntroductionScript()) {
return true;
}
const char* filename = script->filename();
if (!filename ||
strnlen(filename, SourceURLMaxLength + 1) > SourceURLMaxLength) {
return true;
}
Rooted<ArrayObject*> holder(cx, script->global().getSourceURLsHolder());
if (!holder) {
holder = NewDenseEmptyArray(cx);
if (!holder) {
return false;
}
script->global().setSourceURLsHolder(holder);
}
if (holder->length() >= SourceURLRealmLimit) {
return true;
}
RootedString filenameString(cx,
AtomizeUTF8Chars(cx, filename, strlen(filename)));
if (!filenameString) {
return false;
}
// The source URLs holder never escapes to script, so we can treat it as a
// newborn array for the purpose of adding elements.
return NewbornArrayPush(cx, holder, StringValue(filenameString));
}
void DebugAPI::onNewScript(JSContext* cx, HandleScript script) {
if (!script->realm()->isDebuggee()) {
// Remember the URLs associated with scripts in non-system realms,
// in case the debugger is attached later.
if (!script->realm()->isSystem()) {
if (!RememberSourceURL(cx, script)) {
cx->clearPendingException();
}
}
return;
}
Debugger::dispatchQuietHook(
cx,
[script](Debugger* dbg) -> bool {
return dbg->observesNewScript() && dbg->observesScript(script);
},
[&](Debugger* dbg) -> bool {
BaseScript* base = script.get();
Rooted<DebuggerScriptReferent> scriptReferent(cx, base);
return dbg->fireNewScript(cx, scriptReferent);
});
}
void DebugAPI::slowPathOnNewWasmInstance(
JSContext* cx, Handle<WasmInstanceObject*> wasmInstance) {
Debugger::dispatchQuietHook(
cx,
[wasmInstance](Debugger* dbg) -> bool {
return dbg->observesNewScript() &&
dbg->observesGlobal(&wasmInstance->global());
},
[&](Debugger* dbg) -> bool {
Rooted<DebuggerScriptReferent> scriptReferent(cx, wasmInstance.get());
return dbg->fireNewScript(cx, scriptReferent);
});
}
/* static */
bool DebugAPI::onTrap(JSContext* cx) {
FrameIter iter(cx);
JS::AutoSaveExceptionState savedExc(cx);
Rooted<GlobalObject*> global(cx);
BreakpointSite* site;
bool isJS; // true when iter.hasScript(), false when iter.isWasm()
jsbytecode* pc; // valid when isJS == true
uint32_t bytecodeOffset; // valid when isJS == false
if (iter.hasScript()) {
RootedScript script(cx, iter.script());
MOZ_ASSERT(script->isDebuggee());
global.set(&script->global());
isJS = true;
pc = iter.pc();
bytecodeOffset = 0;
site = DebugScript::getBreakpointSite(script, pc);
} else {
MOZ_ASSERT(iter.isWasm());
global.set(&iter.wasmInstance()->object()->global());
isJS = false;
pc = nullptr;
bytecodeOffset = iter.wasmBytecodeOffset();
site = iter.wasmInstance()->debug().getBreakpointSite(bytecodeOffset);
}
// Build list of breakpoint handlers.
//
// This does not need to be rooted: since the JSScript/WasmInstance is on the
// stack, the Breakpoints will not be GC'd. However, they may be deleted, and
// we check for that case below.
Vector<Breakpoint*> triggered(cx);
for (Breakpoint* bp = site->firstBreakpoint(); bp; bp = bp->nextInSite()) {
if (!triggered.append(bp)) {
return false;
}
}
ResumeMode resumeMode = ResumeMode::Continue;
RootedValue rval(cx);
if (triggered.length() > 0) {
// Preserve the debuggee's microtask event queue while we run the hooks, so
// the debugger's microtask checkpoints don't run from the debuggee's
// microtasks, and vice versa.
JS::AutoDebuggerJobQueueInterruption adjqi;
if (!adjqi.init(cx)) {
return false;
}
for (Breakpoint* bp : triggered) {
// Handlers can clear breakpoints. Check that bp still exists.
if (!site || !site->hasBreakpoint(bp)) {
continue;
}
// We have to check whether dbg is debugging this global here: a
// breakpoint handler can disable other Debuggers or remove debuggees.
Debugger* dbg = bp->debugger;
if (dbg->debuggees.has(global)) {
EnterDebuggeeNoExecute nx(cx, *dbg, adjqi);
bool result = dbg->enterDebuggerHook(cx, [&]() -> bool {
RootedValue scriptFrame(cx);
if (!dbg->getFrame(cx, iter, &scriptFrame)) {
return false;
}
// Re-wrap the breakpoint's handler for the Debugger's compartment.
// When the handler and the Debugger are in the same compartment (the
// usual case), this actually unwraps it, but there's no requirement
// that they be in the same compartment, so we can't be sure.
Rooted<JSObject*> handler(cx, bp->handler);
if (!cx->compartment()->wrap(cx, &handler)) {
return false;
}
RootedValue rv(cx);
bool ok = CallMethodIfPresent(cx, handler, "hit", 1,
scriptFrame.address(), &rv);
return dbg->processHandlerResult(cx, ok, rv, iter.abstractFramePtr(),
iter.pc(), resumeMode, &rval);
});
adjqi.runJobs();
if (!result) {
return false;
}
// Calling JS code invalidates site. Reload it.
if (isJS) {
site = DebugScript::getBreakpointSite(iter.script(), pc);
} else {
site = iter.wasmInstance()->debug().getBreakpointSite(bytecodeOffset);
}
}
}
}
if (!ApplyFrameResumeMode(cx, iter.abstractFramePtr(), resumeMode, rval)) {
savedExc.drop();
return false;
}
return true;
}
/* static */
bool DebugAPI::onSingleStep(JSContext* cx) {
FrameIter iter(cx);
// We may be stepping over a JSOp::Exception, that pushes the context's
// pending exception for a 'catch' clause to handle. Don't let the onStep
// handlers mess with that (other than by returning a resumption value).
JS::AutoSaveExceptionState savedExc(cx);
// Build list of Debugger.Frame instances referring to this frame with
// onStep handlers.
Rooted<Debugger::DebuggerFrameVector> frames(cx);
if (!Debugger::getDebuggerFrames(iter.abstractFramePtr(), &frames)) {
ReportOutOfMemory(cx);
return false;
}
#ifdef DEBUG
// Validate the single-step count on this frame's script, to ensure that
// we're not receiving traps we didn't ask for. Even when frames is
// non-empty (and thus we know this trap was requested), do the check
// anyway, to make sure the count has the correct non-zero value.
//
// The converse --- ensuring that we do receive traps when we should --- can
// be done with unit tests.
if (iter.hasScript()) {
uint32_t liveStepperCount = 0;
uint32_t suspendedStepperCount = 0;
JSScript* trappingScript = iter.script();
JS::AutoAssertNoGC nogc;
for (Realm::DebuggerVectorEntry& entry : cx->global()->getDebuggers(nogc)) {
Debugger* dbg = entry.dbg;
for (Debugger::FrameMap::Range r = dbg->frames.all(); !r.empty();
r.popFront()) {
AbstractFramePtr frame = r.front().key();
NativeObject* frameobj = r.front().value();
if (frame.isWasmDebugFrame()) {
continue;
}
if (frame.script() == trappingScript &&
!frameobj->getReservedSlot(DebuggerFrame::ONSTEP_HANDLER_SLOT)
.isUndefined()) {
liveStepperCount++;
}
}
// Also count hooks set on suspended generator frames.
for (Debugger::GeneratorWeakMap::Range r = dbg->generatorFrames.all();
!r.empty(); r.popFront()) {
AbstractGeneratorObject& genObj = *r.front().key();
DebuggerFrame& frameObj = *r.front().value();
MOZ_ASSERT(&frameObj.unwrappedGenerator() == &genObj);
// Live Debugger.Frames were already counted in dbg->frames loop.
if (frameObj.isOnStack()) {
continue;
}
// A closed generator no longer has a callee so it will not be able to
// compare with the trappingScript.
if (genObj.isClosed()) {
continue;
}
// If a frame isn't live, but it has an entry in generatorFrames,
// it had better be suspended.
MOZ_ASSERT(genObj.isSuspended());
if (genObj.callee().hasBaseScript() &&
genObj.callee().baseScript() == trappingScript &&
!frameObj.getReservedSlot(DebuggerFrame::ONSTEP_HANDLER_SLOT)
.isUndefined()) {
suspendedStepperCount++;
}
}
}
MOZ_ASSERT(liveStepperCount + suspendedStepperCount ==
DebugScript::getStepperCount(trappingScript));
}
#endif
RootedValue rval(cx);
ResumeMode resumeMode = ResumeMode::Continue;
if (frames.length() > 0) {
// Preserve the debuggee's microtask event queue while we run the hooks, so
// the debugger's microtask checkpoints don't run from the debuggee's
// microtasks, and vice versa.
JS::AutoDebuggerJobQueueInterruption adjqi;
if (!adjqi.init(cx)) {
return false;
}
// Call onStep for frames that have the handler set.
for (size_t i = 0; i < frames.length(); i++) {
Handle<DebuggerFrame*> frame = frames[i];
OnStepHandler* handler = frame->onStepHandler();
if (!handler) {
continue;
}
Debugger* dbg = frame->owner();
EnterDebuggeeNoExecute nx(cx, *dbg, adjqi);
bool result = dbg->enterDebuggerHook(cx, [&]() -> bool {
ResumeMode nextResumeMode = ResumeMode::Continue;
RootedValue nextValue(cx);
bool success = handler->onStep(cx, frame, nextResumeMode, &nextValue);
return dbg->processParsedHandlerResult(
cx, iter.abstractFramePtr(), iter.pc(), success, nextResumeMode,
nextValue, resumeMode, &rval);
});
adjqi.runJobs();
if (!result) {
return false;
}
}
}
if (!ApplyFrameResumeMode(cx, iter.abstractFramePtr(), resumeMode, rval)) {
savedExc.drop();
return false;
}
return true;
}
bool Debugger::fireNewGlobalObject(JSContext* cx,
Handle<GlobalObject*> global) {
RootedObject hook(cx, getHook(OnNewGlobalObject));
MOZ_ASSERT(hook);
MOZ_ASSERT(hook->isCallable());
RootedValue wrappedGlobal(cx, ObjectValue(*global));
if (!wrapDebuggeeValue(cx, &wrappedGlobal)) {
return false;
}
// onNewGlobalObject is infallible, and thus is only allowed to return
// undefined as a resumption value. If it returns anything else, we throw.
// And if that happens, or if the hook itself throws, we invoke the
// uncaughtExceptionHook so that we never leave an exception pending on the
// cx. This allows JS_NewGlobalObject to avoid handling failures from
// debugger hooks.
RootedValue rv(cx);
RootedValue fval(cx, ObjectValue(*hook));
bool ok = js::Call(cx, fval, object, wrappedGlobal, &rv);
if (ok && !rv.isUndefined()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEBUG_RESUMPTION_VALUE_DISALLOWED);
ok = false;
}
return ok || handleUncaughtException(cx);
}
void DebugAPI::slowPathOnNewGlobalObject(JSContext* cx,
Handle<GlobalObject*> global) {
MOZ_ASSERT(!cx->runtime()->onNewGlobalObjectWatchers().isEmpty());
if (global->realm()->creationOptions().invisibleToDebugger()) {
return;
}
// Make a copy of the runtime's onNewGlobalObjectWatchers before running the
// handlers. Since one Debugger's handler can disable another's, the list
// can be mutated while we're walking it.
RootedObjectVector watchers(cx);
for (auto& dbg : cx->runtime()->onNewGlobalObjectWatchers()) {
MOZ_ASSERT(dbg.observesNewGlobalObject());
JSObject* obj = dbg.object;
JS::ExposeObjectToActiveJS(obj);
if (!watchers.append(obj)) {
if (cx->isExceptionPending()) {
cx->clearPendingException();
}
return;
}
}
// Preserve the debuggee's microtask event queue while we run the hooks, so
// the debugger's microtask checkpoints don't run from the debuggee's
// microtasks, and vice versa.
JS::AutoDebuggerJobQueueInterruption adjqi;
if (!adjqi.init(cx)) {
cx->clearPendingException();
return;
}
for (size_t i = 0; i < watchers.length(); i++) {
Debugger* dbg = Debugger::fromJSObject(watchers[i]);
EnterDebuggeeNoExecute nx(cx, *dbg, adjqi);
if (dbg->observesNewGlobalObject()) {
bool result = dbg->enterDebuggerHook(
cx, [&]() -> bool { return dbg->fireNewGlobalObject(cx, global); });
adjqi.runJobs();
if (!result) {
// Like other quiet hooks using dispatchQuietHook, this hook
// silently ignores all errors that propagate out of it and aren't
// already handled by the hook error reporting.
cx->clearPendingException();
break;
}
}
}
MOZ_ASSERT(!cx->isExceptionPending());
}
/* static */
void DebugAPI::slowPathOnGeneratorClosed(JSContext* cx,
AbstractGeneratorObject* genObj) {
JS::AutoAssertNoGC nogc;
for (Realm::DebuggerVectorEntry& entry : cx->global()->getDebuggers(nogc)) {
Debugger* dbg = entry.dbg;
if (Debugger::GeneratorWeakMap::Ptr frameEntry =
dbg->generatorFrames.lookup(genObj)) {
DebuggerFrame* frameObj = frameEntry->value();
frameObj->onGeneratorClosed(cx->gcContext());
}
}
}
/* static */
void DebugAPI::slowPathNotifyParticipatesInGC(uint64_t majorGCNumber,
Realm::DebuggerVector& dbgs,
const JS::AutoRequireNoGC& nogc) {
for (Realm::DebuggerVector::Range r = dbgs.all(); !r.empty(); r.popFront()) {
if (!r.front().dbg.unbarrieredGet()->debuggeeIsBeingCollected(
majorGCNumber)) {
#ifdef DEBUG
fprintf(stderr,
"OOM while notifying observing Debuggers of a GC: The "
"onGarbageCollection\n"
"hook will not be fired for this GC for some Debuggers!\n");
#endif
return;
}
}
}
/* static */
Maybe<double> DebugAPI::allocationSamplingProbability(GlobalObject* global) {
JS::AutoAssertNoGC nogc;
Realm::DebuggerVector& dbgs = global->getDebuggers(nogc);
if (dbgs.empty()) {
return Nothing();
}
DebugOnly<Realm::DebuggerVectorEntry*> begin = dbgs.begin();
double probability = 0;
bool foundAnyDebuggers = false;
for (auto p = dbgs.begin(); p < dbgs.end(); p++) {
// The set of debuggers had better not change while we're iterating,
// such that the vector gets reallocated.
MOZ_ASSERT(dbgs.begin() == begin);
// Use unbarrieredGet() to prevent triggering read barrier while collecting,
// this is safe as long as dbgp does not escape.
Debugger* dbgp = p->dbg.unbarrieredGet();
if (dbgp->trackingAllocationSites) {
foundAnyDebuggers = true;
probability = std::max(dbgp->allocationSamplingProbability, probability);
}
}
return foundAnyDebuggers ? Some(probability) : Nothing();
}
/* static */
bool DebugAPI::slowPathOnLogAllocationSite(JSContext* cx, HandleObject obj,
Handle<SavedFrame*> frame,
mozilla::TimeStamp when,
Realm::DebuggerVector& dbgs,
const gc::AutoSuppressGC& nogc) {
MOZ_ASSERT(!dbgs.empty());
mozilla::DebugOnly<Realm::DebuggerVectorEntry*> begin = dbgs.begin();
// GC is suppressed so we can iterate over the debuggers; appendAllocationSite
// calls Compartment::wrap, and thus could GC.
for (auto p = dbgs.begin(); p < dbgs.end(); p++) {
// The set of debuggers had better not change while we're iterating,
// such that the vector gets reallocated.
MOZ_ASSERT(dbgs.begin() == begin);
if (p->dbg->trackingAllocationSites &&
!p->dbg->appendAllocationSite(cx, obj, frame, when)) {
return false;
}
}
return true;
}
bool Debugger::isDebuggeeUnbarriered(const Realm* realm) const {
MOZ_ASSERT(realm);
return realm->isDebuggee() &&
debuggees.has(realm->unsafeUnbarrieredMaybeGlobal());
}
bool Debugger::appendAllocationSite(JSContext* cx, HandleObject obj,
Handle<SavedFrame*> frame,
mozilla::TimeStamp when) {
MOZ_ASSERT(trackingAllocationSites);
AutoRealm ar(cx, object);
RootedObject wrappedFrame(cx, frame);
if (!cx->compartment()->wrap(cx, &wrappedFrame)) {
return false;
}
auto className = obj->getClass()->name;
auto size =
JS::ubi::Node(obj.get()).size(cx->runtime()->debuggerMallocSizeOf);
auto inNursery = gc::IsInsideNursery(obj);
if (!allocationsLog.emplaceBack(wrappedFrame, when, className, size,
inNursery)) {
ReportOutOfMemory(cx);
return false;
}
if (allocationsLog.length() > maxAllocationsLogLength) {
allocationsLog.popFront();
MOZ_ASSERT(allocationsLog.length() == maxAllocationsLogLength);
allocationsLogOverflowed = true;
}
return true;
}
bool Debugger::firePromiseHook(JSContext* cx, Hook hook, HandleObject promise) {
MOZ_ASSERT(hook == OnNewPromise || hook == OnPromiseSettled);
RootedObject hookObj(cx, getHook(hook));
MOZ_ASSERT(hookObj);
MOZ_ASSERT(hookObj->isCallable());
RootedValue dbgObj(cx, ObjectValue(*promise));
if (!wrapDebuggeeValue(cx, &dbgObj)) {
return false;
}
// Like onNewGlobalObject, the Promise hooks are infallible and the comments
// in |Debugger::fireNewGlobalObject| apply here as well.
RootedValue fval(cx, ObjectValue(*hookObj));
RootedValue rv(cx);
bool ok = js::Call(cx, fval, object, dbgObj, &rv);
if (ok && !rv.isUndefined()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEBUG_RESUMPTION_VALUE_DISALLOWED);
ok = false;
}
return ok || handleUncaughtException(cx);
}
/* static */
void Debugger::slowPathPromiseHook(JSContext* cx, Hook hook,
Handle<PromiseObject*> promise) {
MOZ_ASSERT(hook == OnNewPromise || hook == OnPromiseSettled);
if (hook == OnPromiseSettled) {
// We should be in the right compartment, but for simplicity always enter
// the promise's realm below.
cx->check(promise);
}
AutoRealm ar(cx, promise);
Debugger::dispatchQuietHook(
cx, [hook](Debugger* dbg) -> bool { return dbg->getHook(hook); },
[&](Debugger* dbg) -> bool {
return dbg->firePromiseHook(cx, hook, promise);
});
}
/* static */
void DebugAPI::slowPathOnNewPromise(JSContext* cx,
Handle<PromiseObject*> promise) {
Debugger::slowPathPromiseHook(cx, Debugger::OnNewPromise, promise);
}
/* static */
void DebugAPI::slowPathOnPromiseSettled(JSContext* cx,
Handle<PromiseObject*> promise) {
Debugger::slowPathPromiseHook(cx, Debugger::OnPromiseSettled, promise);
}
/*** Debugger code invalidation for observing execution *********************/
class MOZ_RAII ExecutionObservableRealms
: public DebugAPI::ExecutionObservableSet {
HashSet<Realm*> realms_;
HashSet<Zone*> zones_;
public:
explicit ExecutionObservableRealms(JSContext* cx) : realms_(cx), zones_(cx) {}
bool add(Realm* realm) {
return realms_.put(realm) && zones_.put(realm->zone());
}
using RealmRange = HashSet<Realm*>::Range;
const HashSet<Realm*>* realms() const { return &realms_; }
const HashSet<Zone*>* zones() const override { return &zones_; }
bool shouldRecompileOrInvalidate(JSScript* script) const override {
return script->hasBaselineScript() && realms_.has(script->realm());
}
bool shouldMarkAsDebuggee(FrameIter& iter) const override {
// AbstractFramePtr can't refer to non-remateralized Ion frames or
// non-debuggee wasm frames, so if iter refers to one such, we know we
// don't match.
return iter.hasUsableAbstractFramePtr() && realms_.has(iter.realm());
}
};
// Given a particular AbstractFramePtr F that has become observable, this
// represents the stack frames that need to be bailed out or marked as
// debuggees, and the scripts that need to be recompiled, taking inlining into
// account.
class MOZ_RAII ExecutionObservableFrame
: public DebugAPI::ExecutionObservableSet {
AbstractFramePtr frame_;
public:
explicit ExecutionObservableFrame(AbstractFramePtr frame) : frame_(frame) {}
Zone* singleZone() const override {
// We never inline across realms, let alone across zones, so
// frames_'s script's zone is the only one of interest.
return frame_.script()->zone();
}
JSScript* singleScriptForZoneInvalidation() const override {
MOZ_CRASH(
"ExecutionObservableFrame shouldn't need zone-wide invalidation.");
return nullptr;
}
bool shouldRecompileOrInvalidate(JSScript* script) const override {
// Normally, *this represents exactly one script: the one frame_ is
// running.
//
// However, debug-mode OSR uses *this for both invalidating Ion frames,
// and recompiling the Baseline scripts that those Ion frames will bail
// out into. Suppose frame_ is an inline frame, executing a copy of its
// JSScript, S_inner, that has been inlined into the IonScript of some
// other JSScript, S_outer. We must match S_outer, to decide which Ion
// frame to invalidate; and we must match S_inner, to decide which
// Baseline script to recompile.
//
// Note that this does not, by design, invalidate *all* inliners of
// frame_.script(), as only frame_ is made observable, not
// frame_.script().
if (!script->hasBaselineScript()) {
return false;
}
if (frame_.hasScript() && script == frame_.script()) {
return true;
}
return frame_.isRematerializedFrame() &&
script == frame_.asRematerializedFrame()->outerScript();
}
bool shouldMarkAsDebuggee(FrameIter& iter) const override {
// AbstractFramePtr can't refer to non-remateralized Ion frames or
// non-debuggee wasm frames, so if iter refers to one such, we know we
// don't match.
//
// We never use this 'has' overload for frame invalidation, only for
// frame debuggee marking; so this overload doesn't need a parallel to
// the just-so inlining logic above.
return iter.hasUsableAbstractFramePtr() &&
iter.abstractFramePtr() == frame_;
}
};
class MOZ_RAII ExecutionObservableScript
: public DebugAPI::ExecutionObservableSet {
RootedScript script_;
public:
ExecutionObservableScript(JSContext* cx, JSScript* script)
: script_(cx, script) {}
Zone* singleZone() const override { return script_->zone(); }
JSScript* singleScriptForZoneInvalidation() const override { return script_; }
bool shouldRecompileOrInvalidate(JSScript* script) const override {
return script->hasBaselineScript() && script == script_;
}
bool shouldMarkAsDebuggee(FrameIter& iter) const override {
// AbstractFramePtr can't refer to non-remateralized Ion frames, and
// while a non-rematerialized Ion frame may indeed be running script_,
// we cannot mark them as debuggees until they bail out.
//
// Upon bailing out, any newly constructed Baseline frames that came
// from Ion frames with scripts that are isDebuggee() is marked as
// debuggee. This is correct in that the only other way a frame may be
// marked as debuggee is via Debugger.Frame reflection, which would
// have rematerialized any Ion frames.
//
// Also AbstractFramePtr can't refer to non-debuggee wasm frames, so if
// iter refers to one such, we know we don't match.
return iter.hasUsableAbstractFramePtr() && !iter.isWasm() &&
iter.abstractFramePtr().script() == script_;
}
};
/* static */
bool Debugger::updateExecutionObservabilityOfFrames(
JSContext* cx, const DebugAPI::ExecutionObservableSet& obs,
IsObserving observing) {
AutoSuppressProfilerSampling suppressProfilerSampling(cx);
if (!jit::RecompileOnStackBaselineScriptsForDebugMode(cx, obs, observing)) {
return false;
}
AbstractFramePtr oldestEnabledFrame;
for (AllFramesIter iter(cx); !iter.done(); ++iter) {
if (obs.shouldMarkAsDebuggee(iter)) {
if (observing) {
if (!iter.abstractFramePtr().isDebuggee()) {
oldestEnabledFrame = iter.abstractFramePtr();
oldestEnabledFrame.setIsDebuggee();
}
if (iter.abstractFramePtr().isWasmDebugFrame()) {
iter.abstractFramePtr().asWasmDebugFrame()->observe(cx);
}
} else {
#ifdef DEBUG
// Debugger.Frame lifetimes are managed by the debug epilogue,
// so in general it's unsafe to unmark a frame if it has a
// Debugger.Frame associated with it.
MOZ_ASSERT(!DebugAPI::inFrameMaps(iter.abstractFramePtr()));
#endif
iter.abstractFramePtr().unsetIsDebuggee();
}
}
}
// See comment in unsetPrevUpToDateUntil.
if (oldestEnabledFrame) {
AutoRealm ar(cx, oldestEnabledFrame.environmentChain());
DebugEnvironments::unsetPrevUpToDateUntil(cx, oldestEnabledFrame);
}
return true;
}
static inline void MarkJitScriptActiveIfObservable(
JSScript* script, const DebugAPI::ExecutionObservableSet& obs) {
if (obs.shouldRecompileOrInvalidate(script)) {
script->jitScript()->icScript()->setActive();
}
}
static bool AppendAndInvalidateScript(JSContext* cx, Zone* zone,
JSScript* script,
jit::RecompileInfoVector& invalid,
Vector<JSScript*>& scripts) {
// Enter the script's realm as AddPendingInvalidation attempts to
// cancel off-thread compilations, whose books are kept on the
// script's realm.
MOZ_ASSERT(script->zone() == zone);
AutoRealm ar(cx, script);
AddPendingInvalidation(invalid, script);
return scripts.append(script);
}
static bool UpdateExecutionObservabilityOfScriptsInZone(
JSContext* cx, Zone* zone, const DebugAPI::ExecutionObservableSet& obs,
Debugger::IsObserving observing) {
using namespace js::jit;
AutoSuppressProfilerSampling suppressProfilerSampling(cx);
JS::GCContext* gcx = cx->gcContext();
Vector<JSScript*> scripts(cx);
// Iterate through observable scripts, invalidating their Ion scripts and
// appending them to a vector for discarding their baseline scripts later.
{
RecompileInfoVector invalid;
if (JSScript* script = obs.singleScriptForZoneInvalidation()) {
if (obs.shouldRecompileOrInvalidate(script)) {
if (!AppendAndInvalidateScript(cx, zone, script, invalid, scripts)) {
return false;
}
}
} else {
for (auto base = zone->cellIter<BaseScript>(); !base.done();
base.next()) {
if (!base->hasJitScript()) {
continue;
}
JSScript* script = base->asJSScript();
if (obs.shouldRecompileOrInvalidate(script)) {
if (!AppendAndInvalidateScript(cx, zone, script, invalid, scripts)) {
return false;
}
}
}
}
Invalidate(cx, invalid);
}
for (size_t i = 0; i < scripts.length(); i++) {
MOZ_ASSERT(!scripts[i]->jitScript()->icScript()->active());
}
// Code below this point must be infallible to ensure the active bit of
// BaselineScripts is in a consistent state.
//
// Mark active baseline scripts in the observable set so that they don't
// get discarded. They will be recompiled.
for (JitActivationIterator actIter(cx); !actIter.done(); ++actIter) {
if (actIter->compartment()->zone() != zone) {
continue;
}
for (OnlyJSJitFrameIter iter(actIter); !iter.done(); ++iter) {
const JSJitFrameIter& frame = iter.frame();
switch (frame.type()) {
case FrameType::BaselineJS:
MarkJitScriptActiveIfObservable(frame.script(), obs);
break;
case FrameType::IonJS:
MarkJitScriptActiveIfObservable(frame.script(), obs);
for (InlineFrameIterator inlineIter(cx, &frame); inlineIter.more();
++inlineIter) {
MarkJitScriptActiveIfObservable(inlineIter.script(), obs);
}
break;
default:;
}
}
}
// Iterate through the scripts again and finish discarding
// BaselineScripts. This must be done as a separate phase as we can only
// discard the BaselineScript on scripts that have no IonScript.
for (size_t i = 0; i < scripts.length(); i++) {
MOZ_ASSERT_IF(scripts[i]->isDebuggee(), observing);
if (!scripts[i]->jitScript()->icScript()->active()) {
FinishDiscardBaselineScript(gcx, scripts[i]);
}
scripts[i]->jitScript()->icScript()->resetActive();
}
// Iterate through all wasm instances to find ones that need to be updated.
for (RealmsInZoneIter r(zone); !r.done(); r.next()) {
for (wasm::Instance* instance : r->wasm.instances()) {
if (!instance->debugEnabled()) {
continue;
}
bool enableTrap = observing == Debugger::Observing;
instance->debug().ensureEnterFrameTrapsState(cx, instance, enableTrap);
}
}
return true;
}
/* static */
bool Debugger::updateExecutionObservabilityOfScripts(
JSContext* cx, const DebugAPI::ExecutionObservableSet& obs,
IsObserving observing) {
if (Zone* zone = obs.singleZone()) {
return UpdateExecutionObservabilityOfScriptsInZone(cx, zone, obs,
observing);
}
using ZoneRange = DebugAPI::ExecutionObservableSet::ZoneRange;
for (ZoneRange r = obs.zones()->all(); !r.empty(); r.popFront()) {
if (!UpdateExecutionObservabilityOfScriptsInZone(cx, r.front(), obs,
observing)) {
return false;
}
}
return true;
}
template <typename FrameFn>
/* static */
void Debugger::forEachOnStackDebuggerFrame(AbstractFramePtr frame,
const JS::AutoRequireNoGC& nogc,
FrameFn fn) {
for (Realm::DebuggerVectorEntry& entry : frame.global()->getDebuggers(nogc)) {
Debugger* dbg = entry.dbg;
if (FrameMap::Ptr frameEntry = dbg->frames.lookup(frame)) {
fn(dbg, frameEntry->value());
}
}
}
template <typename FrameFn>
/* static */
void Debugger::forEachOnStackOrSuspendedDebuggerFrame(
JSContext* cx, AbstractFramePtr frame, const JS::AutoRequireNoGC& nogc,
FrameFn fn) {
Rooted<AbstractGeneratorObject*> genObj(
cx, frame.isGeneratorFrame() ? GetGeneratorObjectForFrame(cx, frame)
: nullptr);
for (Realm::DebuggerVectorEntry& entry : frame.global()->getDebuggers(nogc)) {
Debugger* dbg = entry.dbg;
DebuggerFrame* frameObj = nullptr;
if (FrameMap::Ptr frameEntry = dbg->frames.lookup(frame)) {
frameObj = frameEntry->value();
} else if (GeneratorWeakMap::Ptr frameEntry =
dbg->generatorFrames.lookup(genObj)) {
frameObj = frameEntry->value();
}
if (frameObj) {
fn(dbg, frameObj);
}
}
}
/* static */
bool Debugger::getDebuggerFrames(AbstractFramePtr frame,
MutableHandle<DebuggerFrameVector> frames) {
bool hadOOM = false;
JS::AutoAssertNoGC nogc;
forEachOnStackDebuggerFrame(frame, nogc,
[&](Debugger*, DebuggerFrame* frameobj) {
if (!hadOOM && !frames.append(frameobj)) {
hadOOM = true;
}
});
return !hadOOM;
}
/* static */
bool Debugger::updateExecutionObservability(
JSContext* cx, DebugAPI::ExecutionObservableSet& obs,
IsObserving observing) {
if (!obs.singleZone() && obs.zones()->empty()) {
return true;
}
// Invalidate scripts first so we can set the needsArgsObj flag on scripts
// before patching frames.
return updateExecutionObservabilityOfScripts(cx, obs, observing) &&
updateExecutionObservabilityOfFrames(cx, obs, observing);
}
/* static */
bool Debugger::ensureExecutionObservabilityOfScript(JSContext* cx,
JSScript* script) {
if (script->isDebuggee()) {
return true;
}
ExecutionObservableScript obs(cx, script);
return updateExecutionObservability(cx, obs, Observing);
}
/* static */
bool DebugAPI::ensureExecutionObservabilityOfOsrFrame(
JSContext* cx, AbstractFramePtr osrSourceFrame) {
MOZ_ASSERT(osrSourceFrame.isDebuggee());
if (osrSourceFrame.script()->hasBaselineScript() &&
osrSourceFrame.script()->baselineScript()->hasDebugInstrumentation()) {
return true;
}
ExecutionObservableFrame obs(osrSourceFrame);
return Debugger::updateExecutionObservabilityOfFrames(cx, obs, Observing);
}
/* static */
bool Debugger::ensureExecutionObservabilityOfFrame(JSContext* cx,
AbstractFramePtr frame) {
MOZ_ASSERT_IF(frame.hasScript() && frame.script()->isDebuggee(),
frame.isDebuggee());
MOZ_ASSERT_IF(frame.isWasmDebugFrame(), frame.wasmInstance()->debugEnabled());
if (frame.isDebuggee()) {
return true;
}
ExecutionObservableFrame obs(frame);
return updateExecutionObservabilityOfFrames(cx, obs, Observing);
}
/* static */
bool Debugger::ensureExecutionObservabilityOfRealm(JSContext* cx,
Realm* realm) {
if (realm->debuggerObservesAllExecution()) {
return true;
}
ExecutionObservableRealms obs(cx);
if (!obs.add(realm)) {
return false;
}
realm->updateDebuggerObservesAllExecution();
return updateExecutionObservability(cx, obs, Observing);
}
/* static */
bool Debugger::hookObservesAllExecution(Hook which) {
return which == OnEnterFrame;
}
Debugger::IsObserving Debugger::observesAllExecution() const {
if (!!getHook(OnEnterFrame)) {
return Observing;
}
return NotObserving;
}
Debugger::IsObserving Debugger::observesAsmJS() const {
if (!allowUnobservedAsmJS) {
return Observing;
}
return NotObserving;
}
Debugger::IsObserving Debugger::observesWasm() const {
if (!allowUnobservedWasm) {
return Observing;
}
return NotObserving;
}
Debugger::IsObserving Debugger::observesCoverage() const {
if (collectCoverageInfo) {
return Observing;
}
return NotObserving;
}
Debugger::IsObserving Debugger::observesNativeCalls() const {
if (getHook(Debugger::OnNativeCall)) {
return Observing;
}
return NotObserving;
}
bool Debugger::isExclusiveDebuggerOnEval() const {
return exclusiveDebuggerOnEval;
}
// Toggle whether this Debugger's debuggees observe all execution. This is
// called when a hook that observes all execution is set or unset. See
// hookObservesAllExecution.
bool Debugger::updateObservesAllExecutionOnDebuggees(JSContext* cx,
IsObserving observing) {
ExecutionObservableRealms obs(cx);
for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty();
r.popFront()) {
GlobalObject* global = r.front();
JS::Realm* realm = global->realm();
if (realm->debuggerObservesAllExecution() == observing) {
continue;
}
// It's expensive to eagerly invalidate and recompile a realm,
// so add the realm to the set only if we are observing.
if (observing && !obs.add(realm)) {
return false;
}
}
if (!updateExecutionObservability(cx, obs, observing)) {
return false;
}
using RealmRange = ExecutionObservableRealms::RealmRange;
for (RealmRange r = obs.realms()->all(); !r.empty(); r.popFront()) {
r.front()->updateDebuggerObservesAllExecution();
}
return true;
}
bool Debugger::updateObservesCoverageOnDebuggees(JSContext* cx,
IsObserving observing) {
ExecutionObservableRealms obs(cx);
for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty();
r.popFront()) {
GlobalObject* global = r.front();
Realm* realm = global->realm();
if (realm->debuggerObservesCoverage() == observing) {
continue;
}
// Invalidate and recompile a realm to add or remove PCCounts
// increments. We have to eagerly invalidate, as otherwise we might have
// dangling pointers to freed PCCounts.
if (!obs.add(realm)) {
return false;
}
}
// If any frame on the stack belongs to the debuggee, then we cannot update
// the ScriptCounts, because this would imply to invalidate a Debugger.Frame
// to recompile it with/without ScriptCount support.
for (FrameIter iter(cx); !iter.done(); ++iter) {
if (obs.shouldMarkAsDebuggee(iter)) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEBUG_NOT_IDLE);
return false;
}
}
if (!updateExecutionObservability(cx, obs, observing)) {
return false;
}
// All realms can safely be toggled, and all scripts will be recompiled.
// Thus we can update each realm accordingly.
using RealmRange = ExecutionObservableRealms::RealmRange;
for (RealmRange r = obs.realms()->all(); !r.empty(); r.popFront()) {
r.front()->updateDebuggerObservesCoverage();
}
return true;
}
void Debugger::updateObservesAsmJSOnDebuggees(IsObserving observing) {
for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty();
r.popFront()) {
GlobalObject* global = r.front();
Realm* realm = global->realm();
if (realm->debuggerObservesAsmJS() == observing) {
continue;
}
realm->updateDebuggerObservesAsmJS();
}
}
void Debugger::updateObservesWasmOnDebuggees(IsObserving observing) {
for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty();
r.popFront()) {
GlobalObject* global = r.front();
Realm* realm = global->realm();
if (realm->debuggerObservesWasm() == observing) {
continue;
}
realm->updateDebuggerObservesWasm();
}
}
void Debugger::updateObservesNativeCallOnDebuggees(IsObserving observing) {
for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty();
r.popFront()) {
GlobalObject* global = r.front();
Realm* realm = global->realm();
if (realm->debuggerObservesNativeCall() == observing) {
continue;
}
realm->updateDebuggerObservesNativeCall();
}
}
/*** Allocations Tracking ***************************************************/
/* static */
bool Debugger::cannotTrackAllocations(const GlobalObject& global) {
auto existingCallback = global.realm()->getAllocationMetadataBuilder();
return existingCallback && existingCallback != &SavedStacks::metadataBuilder;
}
/* static */
bool DebugAPI::isObservedByDebuggerTrackingAllocations(
const GlobalObject& debuggee) {
JS::AutoAssertNoGC nogc;
for (Realm::DebuggerVectorEntry& entry : debuggee.getDebuggers(nogc)) {
// Use unbarrieredGet() to prevent triggering read barrier while
// collecting, this is safe as long as dbg does not escape.
Debugger* dbg = entry.dbg.unbarrieredGet();
if (dbg->trackingAllocationSites) {
return true;
}
}
return false;
}
/* static */
bool Debugger::addAllocationsTracking(JSContext* cx,
Handle<GlobalObject*> debuggee) {
// Precondition: the given global object is being observed by at least one
// Debugger that is tracking allocations.
MOZ_ASSERT(DebugAPI::isObservedByDebuggerTrackingAllocations(*debuggee));
if (Debugger::cannotTrackAllocations(*debuggee)) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_OBJECT_METADATA_CALLBACK_ALREADY_SET);
return false;
}
debuggee->realm()->setAllocationMetadataBuilder(
&SavedStacks::metadataBuilder);
debuggee->realm()->chooseAllocationSamplingProbability();
return true;
}
/* static */
void Debugger::removeAllocationsTracking(GlobalObject& global) {
// If there are still Debuggers that are observing allocations, we cannot
// remove the metadata callback yet. Recompute the sampling probability
// based on the remaining debuggers' needs.
if (DebugAPI::isObservedByDebuggerTrackingAllocations(global)) {
global.realm()->chooseAllocationSamplingProbability();
return;
}
if (!global.realm()->runtimeFromMainThread()->recordAllocationCallback) {
// Something like the Gecko Profiler could request from the the JS runtime
// to record allocations. If it is recording allocations, then do not
// destroy the allocation metadata builder at this time.
global.realm()->forgetAllocationMetadataBuilder();
}
}
bool Debugger::addAllocationsTrackingForAllDebuggees(JSContext* cx) {
MOZ_ASSERT(trackingAllocationSites);
// We don't want to end up in a state where we added allocations
// tracking to some of our debuggees, but failed to do so for
// others. Before attempting to start tracking allocations in *any* of
// our debuggees, ensure that we will be able to track allocations for
// *all* of our debuggees.
for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty();
r.popFront()) {
if (Debugger::cannotTrackAllocations(*r.front().get())) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_OBJECT_METADATA_CALLBACK_ALREADY_SET);
return false;
}
}
Rooted<GlobalObject*> g(cx);
for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty();
r.popFront()) {
// This should always succeed, since we already checked for the
// error case above.
g = r.front().get();
MOZ_ALWAYS_TRUE(Debugger::addAllocationsTracking(cx, g));
}
return true;
}
void Debugger::removeAllocationsTrackingForAllDebuggees() {
for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty();
r.popFront()) {
Debugger::removeAllocationsTracking(*r.front().get());
}
allocationsLog.clear();
}
/*** Debugger JSObjects *****************************************************/
template <typename F>
inline void Debugger::forEachWeakMap(const F& f) {
f(generatorFrames);
f(objects);
f(environments);
f(scripts);
f(sources);
f(wasmInstanceScripts);
f(wasmInstanceSources);
}
void Debugger::traceCrossCompartmentEdges(JSTracer* trc) {
forEachWeakMap(
[trc](auto& weakMap) { weakMap.traceCrossCompartmentEdges(trc); });
}
/*
* Ordinarily, WeakMap keys and values are marked because at some point it was
* discovered that the WeakMap was live; that is, some object containing the
* WeakMap was marked during mark phase.
*
* However, during zone GC, we have to do something about cross-compartment
* edges in non-GC'd compartments. Since the source may be live, we
* conservatively assume it is and mark the edge.
*
* Each Debugger object keeps five cross-compartment WeakMaps: objects, scripts,
* lazy scripts, script source objects, and environments. They have the property
* that all their values are in the same compartment as the Debugger object,
* but we have to mark the keys and the private pointer in the wrapper object.
*
* We must scan all Debugger objects regardless of whether they *currently* have
* any debuggees in a compartment being GC'd, because the WeakMap entries
* persist even when debuggees are removed.
*
* This happens during the initial mark phase, not iterative marking, because
* all the edges being reported here are strong references.
*
* This method is also used during compacting GC to update cross compartment
* pointers into zones that are being compacted.
*/
/* static */
void DebugAPI::traceCrossCompartmentEdges(JSTracer* trc) {
MOZ_ASSERT(JS::RuntimeHeapIsMajorCollecting());
JSRuntime* rt = trc->runtime();
gc::State state = rt->gc.state();
for (Debugger* dbg : rt->debuggerList()) {
Zone* zone = MaybeForwarded(dbg->object.get())->zone();
if (!zone->isCollecting() || state == gc::State::Compact) {
dbg->traceCrossCompartmentEdges(trc);
}
}
}
#ifdef DEBUG
static bool RuntimeHasDebugger(JSRuntime* rt, Debugger* dbg) {
for (Debugger* d : rt->debuggerList()) {
if (d == dbg) {
return true;
}
}
return false;
}
/* static */
bool DebugAPI::edgeIsInDebuggerWeakmap(JSRuntime* rt, JSObject* src,
JS::GCCellPtr dst) {
if (!Debugger::isChildJSObject(src)) {
return false;
}
if (src->is<DebuggerFrame>()) {
DebuggerFrame* frame = &src->as<DebuggerFrame>();
Debugger* dbg = frame->owner();
MOZ_ASSERT(RuntimeHasDebugger(rt, dbg));
if (dst.is<BaseScript>()) {
// The generatorFrames map is not keyed on the associated JSScript. Get
// the key from the source object and check everything matches.
AbstractGeneratorObject* genObj = &frame->unwrappedGenerator();
return frame->generatorScript() == &dst.as<BaseScript>() &&
dbg->generatorFrames.hasEntry(genObj, src);
}
return dst.is<JSObject>() &&
dst.as<JSObject>().is<AbstractGeneratorObject>() &&
dbg->generatorFrames.hasEntry(
&dst.as<JSObject>().as<AbstractGeneratorObject>(), src);
}
if (src->is<DebuggerObject>()) {
Debugger* dbg = src->as<DebuggerObject>().owner();
MOZ_ASSERT(RuntimeHasDebugger(rt, dbg));
return dst.is<JSObject>() &&
dbg->objects.hasEntry(&dst.as<JSObject>(), src);
}
if (src->is<DebuggerEnvironment>()) {
Debugger* dbg = src->as<DebuggerEnvironment>().owner();
MOZ_ASSERT(RuntimeHasDebugger(rt, dbg));
return dst.is<JSObject>() &&
dbg->environments.hasEntry(&dst.as<JSObject>(), src);
}
if (src->is<DebuggerScript>()) {
Debugger* dbg = src->as<DebuggerScript>().owner();
MOZ_ASSERT(RuntimeHasDebugger(rt, dbg));
return src->as<DebuggerScript>().getReferent().match(
[=](BaseScript* script) {
return dst.is<BaseScript>() && script == &dst.as<BaseScript>() &&
dbg->scripts.hasEntry(script, src);
},
[=](WasmInstanceObject* instance) {
return dst.is<JSObject>() && instance == &dst.as<JSObject>() &&
dbg->wasmInstanceScripts.hasEntry(instance, src);
});
}
if (src->is<DebuggerSource>()) {
Debugger* dbg = src->as<DebuggerSource>().owner();
MOZ_ASSERT(RuntimeHasDebugger(rt, dbg));
return src->as<DebuggerSource>().getReferent().match(
[=](ScriptSourceObject* sso) {
return dst.is<JSObject>() && sso == &dst.as<JSObject>() &&
dbg->sources.hasEntry(sso, src);
},
[=](WasmInstanceObject* instance) {
return dst.is<JSObject>() && instance == &dst.as<JSObject>() &&
dbg->wasmInstanceSources.hasEntry(instance, src);
});
}
MOZ_ASSERT_UNREACHABLE("Unhandled cross-compartment edge");
}
#endif
/* See comments in DebugAPI.h. */
void DebugAPI::traceFramesWithLiveHooks(JSTracer* tracer) {
JSRuntime* rt = tracer->runtime();
// Note that we must loop over all Debuggers here, not just those known to be
// reachable from JavaScript. The existence of hooks set on a Debugger.Frame
// for a live stack frame makes the Debuger.Frame (and hence its Debugger)
// reachable.
for (Debugger* dbg : rt->debuggerList()) {
// Callback tracers set their own traversal boundaries, but otherwise we're
// only interested in Debugger.Frames participating in the collection.
if (!dbg->zone()->isGCMarking() && !tracer->isCallbackTracer()) {
continue;
}
for (Debugger::FrameMap::Range r = dbg->frames.all(); !r.empty();
r.popFront()) {
HeapPtr<DebuggerFrame*>& frameobj = r.front().value();
MOZ_ASSERT(frameobj->isOnStack());
if (frameobj->hasAnyHooks()) {
TraceEdge(tracer, &frameobj, "Debugger.Frame with live hooks");
}
}
}
}
void DebugAPI::slowPathTraceGeneratorFrame(JSTracer* tracer,
AbstractGeneratorObject* generator) {
MOZ_ASSERT(generator->realm()->isDebuggee());
// Ignore generic tracers.
//
// There are two kinds of generic tracers we need to bar: MovingTracers used
// by compacting GC; and CompartmentCheckTracers.
//
// MovingTracers are used by the compacting GC to update pointers to objects
// that have been moved: the MovingTracer checks each outgoing pointer to see
// if it refers to a forwarding pointer, and if so, updates the pointer stored
// in the object.
//
// Generator objects are background finalized, so the compacting GC assumes it
// can update their pointers in the background as well. Since we treat
// generator objects as having an owning edge to their Debugger.Frame objects,
// a helper thread trying to update a generator object will end up calling
// this function. However, it is verboten to do weak map lookups (e.g., in
// Debugger::generatorFrames) off the main thread, since StableCellHasher
// must consult the Zone to find the key's unique id.
//
// Fortunately, it's not necessary for compacting GC to worry about that edge
// in the first place: the edge isn't a literal pointer stored on the
// generator object, it's only inferred from the realm's debuggee status and
// its Debuggers' generatorFrames weak maps. Those get relocated when the
// Debugger itself is visited, so compacting GC can just ignore this edge.
//
// CompartmentCheckTracers walk the graph and verify that all
// cross-compartment edges are recorded in the cross-compartment wrapper
// tables. But edges between Debugger.Foo objects and their referents are not
// in the CCW tables, so a CrossCompartmentCheckTracers also calls
// DebugAPI::edgeIsInDebuggerWeakmap to see if a given cross-compartment edge
// is accounted for there. However, edgeIsInDebuggerWeakmap only handles
// debugger -> debuggee edges, so it won't recognize the edge we're
// potentially traversing here, from a generator object to its Debugger.Frame.
//
// But since the purpose of this function is to retrieve such edges, if they
// exist, from the very tables that edgeIsInDebuggerWeakmap would consult,
// we're at no risk of reporting edges that they do not cover. So we can
// safely hide the edges from CompartmentCheckTracers.
//
// We can't quite recognize MovingTracers and CompartmentCheckTracers
// precisely, but they're both generic tracers, so we just show them all the
// door. This means the generator -> Debugger.Frame edge is going to be
// invisible to some traversals. We'll cope with that when it's a problem.
if (!tracer->isMarkingTracer()) {
return;
}
mozilla::Maybe<AutoLockGC> lock;
GCMarker* marker = GCMarker::fromTracer(tracer);
if (marker->isParallelMarking()) {
// Synchronise access to generatorFrames.
lock.emplace(marker->runtime());
}
JS::AutoAssertNoGC nogc;
for (Realm::DebuggerVectorEntry& entry :
generator->realm()->getDebuggers(nogc)) {
Debugger* dbg = entry.dbg.unbarrieredGet();
if (Debugger::GeneratorWeakMap::Ptr entry =
dbg->generatorFrames.lookupUnbarriered(generator)) {
HeapPtr<DebuggerFrame*>& frameObj = entry->value();
if (frameObj->hasAnyHooks()) {
// See comment above.
TraceCrossCompartmentEdge(tracer, generator, &frameObj,
"Debugger.Frame with hooks for generator");
}
}
}
}
/* static */
void DebugAPI::traceAllForMovingGC(JSTracer* trc) {
JSRuntime* rt = trc->runtime();
for (Debugger* dbg : rt->debuggerList()) {
dbg->traceForMovingGC(trc);
}
}
/*
* Trace all debugger-owned GC things unconditionally. This is used during
* compacting GC and in minor GC: the minor GC cannot apply the weak constraints
* of the full GC because it visits only part of the heap.
*/
void Debugger::traceForMovingGC(JSTracer* trc) {
trace(trc);
for (WeakGlobalObjectSet::Enum e(debuggees); !e.empty(); e.popFront()) {
TraceEdge(trc, &e.mutableFront(), "Global Object");
}
}
/* static */
void Debugger::traceObject(JSTracer* trc, JSObject* obj) {
if (Debugger* dbg = Debugger::fromJSObject(obj)) {
dbg->trace(trc);
}
}
void Debugger::trace(JSTracer* trc) {
TraceEdge(trc, &object, "Debugger Object");
TraceNullableEdge(trc, &uncaughtExceptionHook, "hooks");
// Mark Debugger.Frame objects. Since the Debugger is reachable, JS could call
// getNewestFrame and then walk the stack, so these are all reachable from JS.
//
// Note that if a Debugger.Frame has hooks set, it must be retained even if
// its Debugger is unreachable, since JS could observe that its hooks did not
// fire. That case is handled by DebugAPI::traceFrames.
//
// (We have weakly-referenced Debugger.Frame objects as well, for suspended
// generator frames; these are traced via generatorFrames just below.)
for (FrameMap::Range r = frames.all(); !r.empty(); r.popFront()) {
HeapPtr<DebuggerFrame*>& frameobj = r.front().value();
TraceEdge(trc, &frameobj, "live Debugger.Frame");
MOZ_ASSERT(frameobj->isOnStack());
}
allocationsLog.trace(trc);
forEachWeakMap([trc](auto& weakMap) { weakMap.trace(trc); });
}
/* static */
void DebugAPI::traceFromRealm(JSTracer* trc, Realm* realm) {
JS::AutoAssertNoGC nogc;
for (Realm::DebuggerVectorEntry& entry : realm->getDebuggers(nogc)) {
TraceEdge(trc, &entry.debuggerLink, "realm debugger");
}
}
/* static */
void DebugAPI::sweepAll(JS::GCContext* gcx) {
JSRuntime* rt = gcx->runtime();
Debugger* next;
for (Debugger* dbg = rt->debuggerList().getFirst(); dbg; dbg = next) {
next = dbg->getNext();
// Debugger.Frames for generator calls bump the JSScript's
// generatorObserverCount, so the JIT will instrument the code to notify
// Debugger when the generator is resumed. When a Debugger.Frame gets GC'd,
// generatorObserverCount needs to be decremented. It's much easier to do
// this when we know that all parties involved - the Debugger.Frame, the
// generator object, and the JSScript - have not yet been finalized.
//
// Since DebugAPI::sweepAll is called after everything is marked, but before
// anything has been finalized, this is the perfect place to drop the count.
if (dbg->zone()->isGCSweeping()) {
for (Debugger::GeneratorWeakMap::Enum e(dbg->generatorFrames); !e.empty();
e.popFront()) {
DebuggerFrame* frameObj = e.front().value();
if (IsAboutToBeFinalizedUnbarriered(frameObj)) {
// If the DebuggerFrame is being finalized, that means either:
// 1) It is not present in "frames".
// 2) The Debugger itself is also being finalized.
//
// In the first case, passing the frame is not necessary because there
// isn't a frame entry to clear, and in the second case,
// removeDebuggeeGlobal below will iterate and remove the entries
// anyway, so things will be cleaned up properly.
Debugger::terminateDebuggerFrame(gcx, dbg, frameObj, NullFramePtr(),
nullptr, &e);
}
}
}
// Detach dying debuggers and debuggees from each other. Since this
// requires access to both objects it must be done before either
// object is finalized.
bool debuggerDying = IsAboutToBeFinalized(dbg->object);
for (WeakGlobalObjectSet::Enum e(dbg->debuggees); !e.empty();
e.popFront()) {
GlobalObject* global = e.front().unbarrieredGet();
if (debuggerDying || IsAboutToBeFinalizedUnbarriered(global)) {
dbg->removeDebuggeeGlobal(gcx, e.front().unbarrieredGet(), &e,
Debugger::FromSweep::Yes);
}
}
if (debuggerDying) {
gcx->delete_(dbg->object, dbg, MemoryUse::Debugger);
}
dbg = next;
}
}
static inline bool SweepZonesInSameGroup(Zone* a, Zone* b) {
// Ensure two zones are swept in the same sweep group by adding an edge
// between them in each direction.
return a->addSweepGroupEdgeTo(b) && b->addSweepGroupEdgeTo(a);
}
/* static */
bool DebugAPI::findSweepGroupEdges(JSRuntime* rt) {
// Ensure that debuggers and their debuggees are finalized in the same group
// by adding edges in both directions for debuggee zones. These are weak
// references that are not in the cross compartment wrapper map.
for (Debugger* dbg : rt->debuggerList()) {
Zone* debuggerZone = dbg->object->zone();
if (!debuggerZone->isGCMarking()) {
continue;
}
for (auto e = dbg->debuggeeZones.all(); !e.empty(); e.popFront()) {
Zone* debuggeeZone = e.front();
if (!debuggeeZone->isGCMarking()) {
continue;
}
if (!SweepZonesInSameGroup(debuggerZone, debuggeeZone)) {
return false;
}
}
}
return true;
}
template <class UnbarrieredKey, class Wrapper, bool InvisibleKeysOk>
bool DebuggerWeakMap<UnbarrieredKey, Wrapper,
InvisibleKeysOk>::findSweepGroupEdges() {
Zone* debuggerZone = zone();
MOZ_ASSERT(debuggerZone->isGCMarking());
for (Enum e(*this); !e.empty(); e.popFront()) {
MOZ_ASSERT(e.front().value()->zone() == debuggerZone);
Zone* keyZone = e.front().key()->zone();
if (keyZone->isGCMarking() &&
!SweepZonesInSameGroup(debuggerZone, keyZone)) {
return false;
}
}
// Add in edges for delegates, if relevant for the key type.
return Base::findSweepGroupEdges();
}
const JSClassOps DebuggerInstanceObject::classOps_ = {
nullptr, // addProperty
nullptr, // delProperty
nullptr, // enumerate
nullptr, // newEnumerate
nullptr, // resolve
nullptr, // mayResolve
nullptr, // finalize
nullptr, // call
nullptr, // construct
Debugger::traceObject, // trace
};
const JSClass DebuggerInstanceObject::class_ = {
"Debugger", JSCLASS_HAS_RESERVED_SLOTS(Debugger::JSSLOT_DEBUG_COUNT),
&classOps_};
static_assert(Debugger::JSSLOT_DEBUG_PROTO_START == 0,
"DebuggerPrototypeObject only needs slots for the proto objects");
const JSClass DebuggerPrototypeObject::class_ = {
"DebuggerPrototype",
JSCLASS_HAS_RESERVED_SLOTS(Debugger::JSSLOT_DEBUG_PROTO_STOP)};
static Debugger* Debugger_fromThisValue(JSContext* cx, const CallArgs& args,
const char* fnname) {
JSObject* thisobj = RequireObject(cx, args.thisv());
if (!thisobj) {
return nullptr;
}
if (!thisobj->is<DebuggerInstanceObject>()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_INCOMPATIBLE_PROTO, "Debugger", fnname,
thisobj->getClass()->name);
return nullptr;
}
Debugger* dbg = Debugger::fromJSObject(thisobj);
MOZ_ASSERT(dbg);
return dbg;
}
struct MOZ_STACK_CLASS Debugger::CallData {
JSContext* cx;
const CallArgs& args;
Debugger* dbg;
CallData(JSContext* cx, const CallArgs& args, Debugger* dbg)
: cx(cx), args(args), dbg(dbg) {}
bool getOnDebuggerStatement();
bool setOnDebuggerStatement();
bool getOnExceptionUnwind();
bool setOnExceptionUnwind();
bool getOnNewScript();
bool setOnNewScript();
bool getOnEnterFrame();
bool setOnEnterFrame();
bool getOnNativeCall();
bool setOnNativeCall();
bool getOnNewGlobalObject();
bool setOnNewGlobalObject();
bool getOnNewPromise();
bool setOnNewPromise();
bool getOnPromiseSettled();
bool setOnPromiseSettled();
bool getUncaughtExceptionHook();
bool setUncaughtExceptionHook();
bool getAllowUnobservedAsmJS();
bool setAllowUnobservedAsmJS();
bool getAllowUnobservedWasm();
bool setAllowUnobservedWasm();
bool getExclusiveDebuggerOnEval();
bool setExclusiveDebuggerOnEval();
bool getInspectNativeCallArguments();
bool setInspectNativeCallArguments();
bool getCollectCoverageInfo();
bool setCollectCoverageInfo();
bool getMemory();
bool addDebuggee();
bool addAllGlobalsAsDebuggees();
bool removeDebuggee();
bool removeAllDebuggees();
bool hasDebuggee();
bool getDebuggees();
bool getNewestFrame();
bool clearAllBreakpoints();
bool findScripts();
bool findSources();
bool findObjects();
bool findAllGlobals();
bool findSourceURLs();
bool makeGlobalObjectReference();
bool adoptDebuggeeValue();
bool adoptFrame();
bool adoptSource();
bool enableAsyncStack();
bool disableAsyncStack();
bool enableUnlimitedStacksCapturing();
bool disableUnlimitedStacksCapturing();
using Method = bool (CallData::*)();
template <Method MyMethod>
static bool ToNative(JSContext* cx, unsigned argc, Value* vp);
};
template <Debugger::CallData::Method MyMethod>
/* static */
bool Debugger::CallData::ToNative(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
Debugger* dbg = Debugger_fromThisValue(cx, args, "method");
if (!dbg) {
return false;
}
CallData data(cx, args, dbg);
return (data.*MyMethod)();
}
/* static */
bool Debugger::getHookImpl(JSContext* cx, const CallArgs& args, Debugger& dbg,
Hook which) {
MOZ_ASSERT(which >= 0 && which < HookCount);
args.rval().set(dbg.object->getReservedSlot(
JSSLOT_DEBUG_HOOK_START + std::underlying_type_t<Hook>(which)));
return true;
}
/* static */
bool Debugger::setHookImpl(JSContext* cx, const CallArgs& args, Debugger& dbg,
Hook which) {
MOZ_ASSERT(which >= 0 && which < HookCount);
if (!args.requireAtLeast(cx, "Debugger.setHook", 1)) {
return false;
}
if (args[0].isObject()) {
if (!args[0].toObject().isCallable()) {
return ReportIsNotFunction(cx, args[0], args.length() - 1);
}
} else if (!args[0].isUndefined()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_NOT_CALLABLE_OR_UNDEFINED);
return false;
}
// Disallow simultaneous activation of OnEnterFrame and code coverage support;
// as they both use the execution observer flag. See Bug 1608891.
if (dbg.collectCoverageInfo && which == Hook::OnEnterFrame) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEBUG_EXCLUSIVE_FRAME_COVERAGE);
return false;
}
uint32_t slot = JSSLOT_DEBUG_HOOK_START + std::underlying_type_t<Hook>(which);
RootedValue oldHook(cx, dbg.object->getReservedSlot(slot));
dbg.object->setReservedSlot(slot, args[0]);
if (hookObservesAllExecution(which)) {
if (!dbg.updateObservesAllExecutionOnDebuggees(
cx, dbg.observesAllExecution())) {
dbg.object->setReservedSlot(slot, oldHook);
return false;
}
}
Rooted<DebuggerDebuggeeLink*> debuggeeLink(cx, dbg.getDebuggeeLink());
if (dbg.hasAnyLiveHooks()) {
debuggeeLink->setLinkSlot(dbg);
} else {
debuggeeLink->clearLinkSlot();
}
args.rval().setUndefined();
return true;
}
/* static */
bool Debugger::getGarbageCollectionHook(JSContext* cx, const CallArgs& args,
Debugger& dbg) {
return getHookImpl(cx, args, dbg, OnGarbageCollection);
}
/* static */
bool Debugger::setGarbageCollectionHook(JSContext* cx, const CallArgs& args,
Debugger& dbg) {
Rooted<JSObject*> oldHook(cx, dbg.getHook(OnGarbageCollection));
if (!setHookImpl(cx, args, dbg, OnGarbageCollection)) {
// We want to maintain the invariant that the hook is always set when the
// Debugger is in the runtime's list, and vice-versa, so if we return early
// and don't adjust the watcher list below, we need to be sure that the
// hook didn't change.
MOZ_ASSERT(dbg.getHook(OnGarbageCollection) == oldHook);
return false;
}
// Add or remove ourselves from the runtime's list of Debuggers that care
// about garbage collection.
JSObject* newHook = dbg.getHook(OnGarbageCollection);
if (!oldHook && newHook) {
cx->runtime()->onGarbageCollectionWatchers().pushBack(&dbg);
} else if (oldHook && !newHook) {
cx->runtime()->onGarbageCollectionWatchers().remove(&dbg);
}
return true;
}
bool Debugger::CallData::getOnDebuggerStatement() {
return getHookImpl(cx, args, *dbg, OnDebuggerStatement);
}
bool Debugger::CallData::setOnDebuggerStatement() {
return setHookImpl(cx, args, *dbg, OnDebuggerStatement);
}
bool Debugger::CallData::getOnExceptionUnwind() {
return getHookImpl(cx, args, *dbg, OnExceptionUnwind);
}
bool Debugger::CallData::setOnExceptionUnwind() {
return setHookImpl(cx, args, *dbg, OnExceptionUnwind);
}
bool Debugger::CallData::getOnNewScript() {
return getHookImpl(cx, args, *dbg, OnNewScript);
}
bool Debugger::CallData::setOnNewScript() {
return setHookImpl(cx, args, *dbg, OnNewScript);
}
bool Debugger::CallData::getOnNewPromise() {
return getHookImpl(cx, args, *dbg, OnNewPromise);
}
bool Debugger::CallData::setOnNewPromise() {
return setHookImpl(cx, args, *dbg, OnNewPromise);
}
bool Debugger::CallData::getOnPromiseSettled() {
return getHookImpl(cx, args, *dbg, OnPromiseSettled);
}
bool Debugger::CallData::setOnPromiseSettled() {
return setHookImpl(cx, args, *dbg, OnPromiseSettled);
}
bool Debugger::CallData::getOnEnterFrame() {
return getHookImpl(cx, args, *dbg, OnEnterFrame);
}
bool Debugger::CallData::setOnEnterFrame() {
return setHookImpl(cx, args, *dbg, OnEnterFrame);
}
bool Debugger::CallData::getOnNativeCall() {
return getHookImpl(cx, args, *dbg, OnNativeCall);
}
bool Debugger::CallData::setOnNativeCall() {
RootedObject oldHook(cx, dbg->getHook(OnNativeCall));
if (!setHookImpl(cx, args, *dbg, OnNativeCall)) {
return false;
}
JSObject* newHook = dbg->getHook(OnNativeCall);
if (!oldHook && newHook) {
dbg->updateObservesNativeCallOnDebuggees(Observing);
} else if (oldHook && !newHook) {
dbg->updateObservesNativeCallOnDebuggees(NotObserving);
}
return true;
}
bool Debugger::CallData::getOnNewGlobalObject() {
return getHookImpl(cx, args, *dbg, OnNewGlobalObject);
}
bool Debugger::CallData::setOnNewGlobalObject() {
RootedObject oldHook(cx, dbg->getHook(OnNewGlobalObject));
if (!setHookImpl(cx, args, *dbg, OnNewGlobalObject)) {
return false;
}
// Add or remove ourselves from the runtime's list of Debuggers that care
// about new globals.
JSObject* newHook = dbg->getHook(OnNewGlobalObject);
if (!oldHook && newHook) {
cx->runtime()->onNewGlobalObjectWatchers().pushBack(dbg);
} else if (oldHook && !newHook) {
cx->runtime()->onNewGlobalObjectWatchers().remove(dbg);
}
return true;
}
bool Debugger::CallData::getUncaughtExceptionHook() {
args.rval().setObjectOrNull(dbg->uncaughtExceptionHook);
return true;
}
bool Debugger::CallData::setUncaughtExceptionHook() {
if (!args.requireAtLeast(cx, "Debugger.set uncaughtExceptionHook", 1)) {
return false;
}
if (!args[0].isNull() &&
(!args[0].isObject() || !args[0].toObject().isCallable())) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_ASSIGN_FUNCTION_OR_NULL,
"uncaughtExceptionHook");
return false;
}
dbg->uncaughtExceptionHook = args[0].toObjectOrNull();
args.rval().setUndefined();
return true;
}
bool Debugger::CallData::getAllowUnobservedAsmJS() {
args.rval().setBoolean(dbg->allowUnobservedAsmJS);
return true;
}
bool Debugger::CallData::setAllowUnobservedAsmJS() {
if (!args.requireAtLeast(cx, "Debugger.set allowUnobservedAsmJS", 1)) {
return false;
}
dbg->allowUnobservedAsmJS = ToBoolean(args[0]);
for (WeakGlobalObjectSet::Range r = dbg->debuggees.all(); !r.empty();
r.popFront()) {
GlobalObject* global = r.front();
Realm* realm = global->realm();
realm->updateDebuggerObservesAsmJS();
}
args.rval().setUndefined();
return true;
}
bool Debugger::CallData::getAllowUnobservedWasm() {
args.rval().setBoolean(dbg->allowUnobservedWasm);
return true;
}
bool Debugger::CallData::setAllowUnobservedWasm() {
if (!args.requireAtLeast(cx, "Debugger.set allowUnobservedWasm", 1)) {
return false;
}
dbg->allowUnobservedWasm = ToBoolean(args[0]);
for (WeakGlobalObjectSet::Range r = dbg->debuggees.all(); !r.empty();
r.popFront()) {
GlobalObject* global = r.front();
Realm* realm = global->realm();
realm->updateDebuggerObservesWasm();
}
args.rval().setUndefined();
return true;
}
bool Debugger::CallData::getExclusiveDebuggerOnEval() {
args.rval().setBoolean(dbg->exclusiveDebuggerOnEval);
return true;
}
bool Debugger::CallData::setExclusiveDebuggerOnEval() {
if (!args.requireAtLeast(cx, "Debugger.set exclusiveDebuggerOnEval", 1)) {
return false;
}
dbg->exclusiveDebuggerOnEval = ToBoolean(args[0]);
args.rval().setUndefined();
return true;
}
bool Debugger::CallData::getInspectNativeCallArguments() {
args.rval().setBoolean(dbg->inspectNativeCallArguments);
return true;
}
bool Debugger::CallData::setInspectNativeCallArguments() {
if (!args.requireAtLeast(cx, "Debugger.set inspectNativeCallArguments", 1)) {
return false;
}
dbg->inspectNativeCallArguments = ToBoolean(args[0]);
args.rval().setUndefined();
return true;
}
bool Debugger::CallData::getCollectCoverageInfo() {
args.rval().setBoolean(dbg->collectCoverageInfo);
return true;
}
bool Debugger::CallData::setCollectCoverageInfo() {
if (!args.requireAtLeast(cx, "Debugger.set collectCoverageInfo", 1)) {
return false;
}
// Disallow simultaneous activation of OnEnterFrame and code coverage support;
// as they both use the execution observer flag. See Bug 1608891.
uint32_t slot = JSSLOT_DEBUG_HOOK_START +
std::underlying_type_t<Hook>(Hook::OnEnterFrame);
if (!dbg->object->getReservedSlot(slot).isUndefined()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEBUG_EXCLUSIVE_FRAME_COVERAGE);
return false;
}
dbg->collectCoverageInfo = ToBoolean(args[0]);
IsObserving observing = dbg->collectCoverageInfo ? Observing : NotObserving;
if (!dbg->updateObservesCoverageOnDebuggees(cx, observing)) {
return false;
}
args.rval().setUndefined();
return true;
}
bool Debugger::CallData::getMemory() {
Value memoryValue =
dbg->object->getReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE);
if (!memoryValue.isObject()) {
RootedObject memory(cx, DebuggerMemory::create(cx, dbg));
if (!memory) {
return false;
}
memoryValue = ObjectValue(*memory);
}
args.rval().set(memoryValue);
return true;
}
/*
* Given a value used to designate a global (there's quite a variety; see the
* docs), return the actual designee.
*
* Note that this does not check whether the designee is marked "invisible to
* Debugger" or not; different callers need to handle invisible-to-Debugger
* globals in different ways.
*/
GlobalObject* Debugger::unwrapDebuggeeArgument(JSContext* cx, const Value& v) {
if (!v.isObject()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_UNEXPECTED_TYPE, "argument",
"not a global object");
return nullptr;
}
RootedObject obj(cx, &v.toObject());
// If it's a Debugger.Object belonging to this debugger, dereference that.
if (obj->getClass() == &DebuggerObject::class_) {
RootedValue rv(cx, v);
if (!unwrapDebuggeeValue(cx, &rv)) {
return nullptr;
}
obj = &rv.toObject();
}
// If we have a cross-compartment wrapper, dereference as far as is secure.
//
// Since we're dealing with globals, we may have a WindowProxy here. So we
// have to make sure to do a dynamic unwrap, and we want to unwrap the
// WindowProxy too, if we have one.
obj = CheckedUnwrapDynamic(obj, cx, /* stopAtWindowProxy = */ false);
if (!obj) {
ReportAccessDenied(cx);
return nullptr;
}
// If that didn't produce a global object, it's an error.
if (!obj->is<GlobalObject>()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_UNEXPECTED_TYPE, "argument",
"not a global object");
return nullptr;
}
return &obj->as<GlobalObject>();
}
bool Debugger::CallData::addDebuggee() {
if (!args.requireAtLeast(cx, "Debugger.addDebuggee", 1)) {
return false;
}
Rooted<GlobalObject*> global(cx, dbg->unwrapDebuggeeArgument(cx, args[0]));
if (!global) {
return false;
}
if (!dbg->addDebuggeeGlobal(cx, global)) {
return false;
}
RootedValue v(cx, ObjectValue(*global));
if (!dbg->wrapDebuggeeValue(cx, &v)) {
return false;
}
args.rval().set(v);
return true;
}
bool Debugger::CallData::addAllGlobalsAsDebuggees() {
for (CompartmentsIter comp(cx->runtime()); !comp.done(); comp.next()) {
if (comp == dbg->object->compartment()) {
continue;
}
for (RealmsInCompartmentIter r(comp); !r.done(); r.next()) {
if (r->creationOptions().invisibleToDebugger()) {
continue;
}
if (!r->hasInitializedGlobal()) {
continue;
}
r->compartment()->gcState.scheduledForDestruction = false;
Rooted<GlobalObject*> global(cx, r->maybeGlobal());
MOZ_ASSERT(global);
if (!dbg->addDebuggeeGlobal(cx, global)) {
return false;
}
}
}
args.rval().setUndefined();
return true;
}
bool Debugger::CallData::removeDebuggee() {
if (!args.requireAtLeast(cx, "Debugger.removeDebuggee", 1)) {
return false;
}
Rooted<GlobalObject*> global(cx, dbg->unwrapDebuggeeArgument(cx, args[0]));
if (!global) {
return false;
}
ExecutionObservableRealms obs(cx);
if (dbg->debuggees.has(global)) {
dbg->removeDebuggeeGlobal(cx->gcContext(), global, nullptr, FromSweep::No);
// Only update the realm if there are no Debuggers left, as it's
// expensive to check if no other Debugger has a live script or frame
// hook on any of the current on-stack debuggee frames.
if (!global->hasDebuggers() && !obs.add(global->realm())) {
return false;
}
if (!updateExecutionObservability(cx, obs, NotObserving)) {
return false;
}
}
args.rval().setUndefined();
return true;
}
bool Debugger::CallData::removeAllDebuggees() {
ExecutionObservableRealms obs(cx);
for (WeakGlobalObjectSet::Enum e(dbg->debuggees); !e.empty(); e.popFront()) {
Rooted<GlobalObject*> global(cx, e.front());
dbg->removeDebuggeeGlobal(cx->gcContext(), global, &e, FromSweep::No);
// See note about adding to the observable set in removeDebuggee.
if (!global->hasDebuggers() && !obs.add(global->realm())) {
return false;
}
}
if (!updateExecutionObservability(cx, obs, NotObserving)) {
return false;
}
args.rval().setUndefined();
return true;
}
bool Debugger::CallData::hasDebuggee() {
if (!args.requireAtLeast(cx, "Debugger.hasDebuggee", 1)) {
return false;
}
GlobalObject* global = dbg->unwrapDebuggeeArgument(cx, args[0]);
if (!global) {
return false;
}
args.rval().setBoolean(!!dbg->debuggees.lookup(global));
return true;
}
bool Debugger::CallData::getDebuggees() {
// Obtain the list of debuggees before wrapping each debuggee, as a GC could
// update the debuggees set while we are iterating it.
unsigned count = dbg->debuggees.count();
RootedValueVector debuggees(cx);
if (!debuggees.resize(count)) {
return false;
}
unsigned i = 0;
{
JS::AutoCheckCannotGC nogc;
for (WeakGlobalObjectSet::Enum e(dbg->debuggees); !e.empty();
e.popFront()) {
debuggees[i++].setObject(*e.front().get());
}
}
Rooted<ArrayObject*> arrobj(cx, NewDenseFullyAllocatedArray(cx, count));
if (!arrobj) {
return false;
}
arrobj->ensureDenseInitializedLength(0, count);
for (i = 0; i < count; i++) {
RootedValue v(cx, debuggees[i]);
if (!dbg->wrapDebuggeeValue(cx, &v)) {
return false;
}
arrobj->setDenseElement(i, v);
}
args.rval().setObject(*arrobj);
return true;
}
bool Debugger::CallData::getNewestFrame() {
// Since there may be multiple contexts, use AllFramesIter.
for (AllFramesIter i(cx); !i.done(); ++i) {
if (dbg->observesFrame(i)) {
// Ensure that Ion frames are rematerialized. Only rematerialized
// Ion frames may be used as AbstractFramePtrs.
if (i.isIon() && !i.ensureHasRematerializedFrame(cx)) {
return false;
}
AbstractFramePtr frame = i.abstractFramePtr();
FrameIter iter(i.activation()->cx());
while (!iter.hasUsableAbstractFramePtr() ||
iter.abstractFramePtr() != frame) {
++iter;
}
return dbg->getFrame(cx, iter, args.rval());
}
}
args.rval().setNull();
return true;
}
bool Debugger::CallData::clearAllBreakpoints() {
JS::GCContext* gcx = cx->gcContext();
Breakpoint* nextbp;
for (Breakpoint* bp = dbg->firstBreakpoint(); bp; bp = nextbp) {
nextbp = bp->nextInDebugger();
bp->remove(gcx);
}
MOZ_ASSERT(!dbg->firstBreakpoint());
return true;
}
/* static */
bool Debugger::construct(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
// Check that the arguments, if any, are cross-compartment wrappers.
for (unsigned i = 0; i < args.length(); i++) {
JSObject* argobj = RequireObject(cx, args[i]);
if (!argobj) {
return false;
}
if (!argobj->is<CrossCompartmentWrapperObject>()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEBUG_CCW_REQUIRED, "Debugger");
return false;
}
}
// Get Debugger.prototype.
RootedValue v(cx);
RootedObject callee(cx, &args.callee());
if (!GetProperty(cx, callee, callee, cx->names().prototype, &v)) {
return false;
}
Rooted<NativeObject*> proto(cx, &v.toObject().as<NativeObject>());
MOZ_ASSERT(proto->is<DebuggerPrototypeObject>());
// Make the new Debugger object. Each one has a reference to
// Debugger.{Frame,Object,Script,Memory}.prototype in reserved slots. The
// rest of the reserved slots are for hooks; they default to undefined.
Rooted<DebuggerInstanceObject*> obj(
cx, NewTenuredObjectWithGivenProto<DebuggerInstanceObject>(cx, proto));
if (!obj) {
return false;
}
for (unsigned slot = JSSLOT_DEBUG_PROTO_START; slot < JSSLOT_DEBUG_PROTO_STOP;
slot++) {
obj->setReservedSlot(slot, proto->getReservedSlot(slot));
}
obj->setReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE, NullValue());
Rooted<NativeObject*> livenessLink(
cx, NewObjectWithGivenProto<DebuggerDebuggeeLink>(cx, nullptr));
if (!livenessLink) {
return false;
}
obj->setReservedSlot(JSSLOT_DEBUG_DEBUGGEE_LINK, ObjectValue(*livenessLink));
Debugger* debugger;
{
// Construct the underlying C++ object.
auto dbg = cx->make_unique<Debugger>(cx, obj.get());
if (!dbg) {
return false;
}
// The object owns the released pointer.
debugger = dbg.release();
InitReservedSlot(obj, JSSLOT_DEBUG_DEBUGGER, debugger, MemoryUse::Debugger);
}
// Add the initial debuggees, if any.
for (unsigned i = 0; i < args.length(); i++) {
JSObject& wrappedObj =
args[i].toObject().as<ProxyObject>().private_().toObject();
Rooted<GlobalObject*> debuggee(cx, &wrappedObj.nonCCWGlobal());
if (!debugger->addDebuggeeGlobal(cx, debuggee)) {
return false;
}
}
args.rval().setObject(*obj);
return true;
}
bool Debugger::addDebuggeeGlobal(JSContext* cx, Handle<GlobalObject*> global) {
if (debuggees.has(global)) {
return true;
}
// Callers should generally be unable to get a reference to a debugger-
// invisible global in order to pass it to addDebuggee. But this is possible
// with certain testing aides we expose in the shell, so just make addDebuggee
// throw in that case.
Realm* debuggeeRealm = global->realm();
if (debuggeeRealm->creationOptions().invisibleToDebugger()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEBUG_CANT_DEBUG_GLOBAL);
return false;
}
// Debugger and debuggee must be in different compartments.
if (debuggeeRealm->compartment() == object->compartment()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEBUG_SAME_COMPARTMENT);
return false;
}
// Check for cycles. If global's realm is reachable from this Debugger
// object's realm by following debuggee-to-debugger links, then adding
// global would create a cycle. (Typically nobody is debugging the
// debugger, in which case we zip through this code without looping.)
Vector<Realm*> visited(cx);
if (!visited.append(object->realm())) {
return false;
}
for (size_t i = 0; i < visited.length(); i++) {
Realm* realm = visited[i];
if (realm == debuggeeRealm) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_LOOP);
return false;
}
// Find all realms containing debuggers debugging realm's global object.
// Add those realms to visited.
if (realm->isDebuggee()) {
JS::AutoAssertNoGC nogc;
for (Realm::DebuggerVectorEntry& entry : realm->getDebuggers(nogc)) {
Realm* next = entry.dbg->object->realm();
if (std::find(visited.begin(), visited.end(), next) == visited.end()) {
if (!visited.append(next)) {
return false;
}
}
}
}
}
// For global to become this js::Debugger's debuggee:
//
// 1. this js::Debugger must be in global->getDebuggers(),
// 2. global must be in this->debuggees,
// 3. the debuggee's zone must be in this->debuggeeZones,
// 4. if we are tracking allocations, the SavedStacksMetadataBuilder must be
// installed for this realm, and
// 5. Realm::isDebuggee()'s bit must be set.
//
// All five indications must be kept consistent.
AutoRealm ar(cx, global);
Zone* zone = global->zone();
RootedObject debuggeeLink(cx, getDebuggeeLink());
if (!cx->compartment()->wrap(cx, &debuggeeLink)) {
return false;
}
// (1)
JS::AutoAssertNoGC nogc;
auto& globalDebuggers = global->getDebuggers(nogc);
if (!globalDebuggers.append(Realm::DebuggerVectorEntry(this, debuggeeLink))) {
ReportOutOfMemory(cx);
return false;
}
auto globalDebuggersGuard = MakeScopeExit([&] { globalDebuggers.popBack(); });
// (2)
if (!debuggees.put(global)) {
ReportOutOfMemory(cx);
return false;
}
auto debuggeesGuard = MakeScopeExit([&] { debuggees.remove(global); });
bool addingZoneRelation = !debuggeeZones.has(zone);
// (3)
if (addingZoneRelation && !debuggeeZones.put(zone)) {
ReportOutOfMemory(cx);
return false;
}
auto debuggeeZonesGuard = MakeScopeExit([&] {
if (addingZoneRelation) {
debuggeeZones.remove(zone);
}
});
// (4)
if (trackingAllocationSites &&
!Debugger::addAllocationsTracking(cx, global)) {
return false;
}
auto allocationsTrackingGuard = MakeScopeExit([&] {
if (trackingAllocationSites) {
Debugger::removeAllocationsTracking(*global);
}
});
// (5)
AutoRestoreRealmDebugMode debugModeGuard(debuggeeRealm);
debuggeeRealm->setIsDebuggee();
debuggeeRealm->updateDebuggerObservesAsmJS();
debuggeeRealm->updateDebuggerObservesWasm();
debuggeeRealm->updateDebuggerObservesCoverage();
if (observesAllExecution() &&
!ensureExecutionObservabilityOfRealm(cx, debuggeeRealm)) {
return false;
}
globalDebuggersGuard.release();
debuggeesGuard.release();
debuggeeZonesGuard.release();
allocationsTrackingGuard.release();
debugModeGuard.release();
return true;
}
void Debugger::recomputeDebuggeeZoneSet() {
AutoEnterOOMUnsafeRegion oomUnsafe;
debuggeeZones.clear();
for (auto range = debuggees.all(); !range.empty(); range.popFront()) {
if (!debuggeeZones.put(range.front().unbarrieredGet()->zone())) {
oomUnsafe.crash("Debugger::removeDebuggeeGlobal");
}
}
}
template <typename T, typename AP>
static T* findDebuggerInVector(Debugger* dbg, Vector<T, 0, AP>* vec) {
T* p;
for (p = vec->begin(); p != vec->end(); p++) {
if (p->dbg == dbg) {
break;
}
}
MOZ_ASSERT(p != vec->end());
return p;
}
void Debugger::removeDebuggeeGlobal(JS::GCContext* gcx, GlobalObject* global,
WeakGlobalObjectSet::Enum* debugEnum,
FromSweep fromSweep) {
// The caller might have found global by enumerating this->debuggees; if
// so, use HashSet::Enum::removeFront rather than HashSet::remove below,
// to avoid invalidating the live enumerator.
MOZ_ASSERT(debuggees.has(global));
MOZ_ASSERT(debuggeeZones.has(global->zone()));
MOZ_ASSERT_IF(debugEnum, debugEnum->front().unbarrieredGet() == global);
// Clear this global's generators from generatorFrames as well.
//
// This method can be called either from script (dbg.removeDebuggee) or during
// GC sweeping, because the Debugger, debuggee global, or both are being GC'd.
//
// When called from script, it's okay to iterate over generatorFrames and
// touch its keys and values (even when an incremental GC is in progress).
// When called from GC, it's not okay; the keys and values may be dying. But
// in that case, we can actually just skip the loop entirely! If the Debugger
// is going away, it doesn't care about the state of its generatorFrames
// table, and the Debugger.Frame finalizer will fix up the generator observer
// counts.
if (fromSweep == FromSweep::No) {
for (GeneratorWeakMap::Enum e(generatorFrames); !e.empty(); e.popFront()) {
AbstractGeneratorObject& genObj = *e.front().key();
if (&genObj.global() == global) {
terminateDebuggerFrame(gcx, this, e.front().value(), NullFramePtr(),
nullptr, &e);
}
}
}
for (FrameMap::Enum e(frames); !e.empty(); e.popFront()) {
AbstractFramePtr frame = e.front().key();
if (frame.hasGlobal(global)) {
terminateDebuggerFrame(gcx, this, e.front().value(), frame, &e);
}
}
JS::AutoAssertNoGC nogc;
auto& globalDebuggersVector = global->getDebuggers(nogc);
// The relation must be removed from up to three places:
// globalDebuggersVector and debuggees for sure, and possibly the
// compartment's debuggee set.
//
// The debuggee zone set is recomputed on demand. This avoids refcounting
// and in practice we have relatively few debuggees that tend to all be in
// the same zone. If after recomputing the debuggee zone set, this global's
// zone is not in the set, then we must remove ourselves from the zone's
// vector of observing debuggers.
globalDebuggersVector.erase(
findDebuggerInVector(this, &globalDebuggersVector));
if (debugEnum) {
debugEnum->removeFront();
} else {
debuggees.remove(global);
}
recomputeDebuggeeZoneSet();
// Remove all breakpoints for the debuggee.
Breakpoint* nextbp;
for (Breakpoint* bp = firstBreakpoint(); bp; bp = nextbp) {
nextbp = bp->nextInDebugger();
if (bp->site->realm() == global->realm()) {
bp->remove(gcx);
}
}
MOZ_ASSERT_IF(debuggees.empty(), !firstBreakpoint());
// If we are tracking allocation sites, we need to remove the object
// metadata callback from this global's realm.
if (trackingAllocationSites) {
Debugger::removeAllocationsTracking(*global);
}
if (!global->realm()->hasDebuggers()) {
global->realm()->unsetIsDebuggee();
} else {
global->realm()->updateDebuggerObservesAllExecution();
global->realm()->updateDebuggerObservesAsmJS();
global->realm()->updateDebuggerObservesWasm();
global->realm()->updateDebuggerObservesCoverage();
}
}
class MOZ_STACK_CLASS Debugger::QueryBase {
protected:
QueryBase(JSContext* cx, Debugger* dbg)
: cx(cx),
debugger(dbg),
iterMarker(&cx->runtime()->gc),
realms(cx->zone()) {}
// The context in which we should do our work.
JSContext* cx;
// The debugger for which we conduct queries.
Debugger* debugger;
// Require the set of realms to stay fixed while the query is alive.
gc::AutoEnterIteration iterMarker;
using RealmSet = HashSet<Realm*, DefaultHasher<Realm*>, ZoneAllocPolicy>;
// A script must be in one of these realms to match the query.
RealmSet realms;
// Indicates whether OOM has occurred while matching.
bool oom = false;
bool addRealm(Realm* realm) { return realms.put(realm); }
// Arrange for this query to match only scripts that run in |global|.
bool matchSingleGlobal(GlobalObject* global) {
MOZ_ASSERT(realms.count() == 0);
if (!addRealm(global->realm())) {
ReportOutOfMemory(cx);
return false;
}
return true;
}
// Arrange for this ScriptQuery to match all scripts running in debuggee
// globals.
bool matchAllDebuggeeGlobals() {
MOZ_ASSERT(realms.count() == 0);
// Build our realm set from the debugger's set of debuggee globals.
for (WeakGlobalObjectSet::Range r = debugger->debuggees.all(); !r.empty();
r.popFront()) {
if (!addRealm(r.front()->realm())) {
ReportOutOfMemory(cx);
return false;
}
}
return true;
}
};
/*
* A class for parsing 'findScripts' query arguments and searching for
* scripts that match the criteria they represent.
*/
class MOZ_STACK_CLASS Debugger::ScriptQuery : public Debugger::QueryBase {
public:
/* Construct a ScriptQuery to use matching scripts for |dbg|. */
ScriptQuery(JSContext* cx, Debugger* dbg)
: QueryBase(cx, dbg),
url(cx),
displayURLString(cx),
source(cx, AsVariant(static_cast<ScriptSourceObject*>(nullptr))),
scriptVector(cx, BaseScriptVector(cx)),
partialMatchVector(cx, BaseScriptVector(cx)),
wasmInstanceVector(cx, WasmInstanceObjectVector(cx)) {}
/*
* Parse the query object |query|, and prepare to match only the scripts
* it specifies.
*/
bool parseQuery(HandleObject query) {
// Check for a 'global' property, which limits the results to those
// scripts scoped to a particular global object.
RootedValue global(cx);
if (!GetProperty(cx, query, query, cx->names().global, &global)) {
return false;
}
if (global.isUndefined()) {
if (!matchAllDebuggeeGlobals()) {
return false;
}
} else {
GlobalObject* globalObject = debugger->unwrapDebuggeeArgument(cx, global);
if (!globalObject) {
return false;
}
// If the given global isn't a debuggee, just leave the set of
// acceptable globals empty; we'll return no scripts.
if (debugger->debuggees.has(globalObject)) {
if (!matchSingleGlobal(globalObject)) {
return false;
}
}
}
// Check for a 'url' property.
if (!GetProperty(cx, query, query, cx->names().url, &url)) {
return false;
}
if (!url.isUndefined() && !url.isString()) {
JS_ReportErrorNumberASCII(
cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE,
"query object's 'url' property", "neither undefined nor a string");
return false;
}
// Check for a 'source' property
RootedValue debuggerSource(cx);
if (!GetProperty(cx, query, query, cx->names().source, &debuggerSource)) {
return false;
}
if (!debuggerSource.isUndefined()) {
if (!debuggerSource.isObject() ||
!debuggerSource.toObject().is<DebuggerSource>()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_UNEXPECTED_TYPE,
"query object's 'source' property",
"not undefined nor a Debugger.Source object");
return false;
}
DebuggerSource& debuggerSourceObj =
debuggerSource.toObject().as<DebuggerSource>();
// If it does have an owner, it should match the Debugger we're
// calling findScripts on. It would work fine even if it didn't,
// but mixing Debugger.Sources is probably a sign of confusion.
if (debuggerSourceObj.owner() != debugger) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEBUG_WRONG_OWNER, "Debugger.Source");
return false;
}
hasSource = true;
source = debuggerSourceObj.getReferent();
}
// Check for a 'displayURL' property.
RootedValue displayURL(cx);
if (!GetProperty(cx, query, query, cx->names().displayURL, &displayURL)) {
return false;
}
if (!displayURL.isUndefined() && !displayURL.isString()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_UNEXPECTED_TYPE,
"query object's 'displayURL' property",
"neither undefined nor a string");
return false;
}
if (displayURL.isString()) {
displayURLString = displayURL.toString()->ensureLinear(cx);
if (!displayURLString) {
return false;
}
}
// Check for a 'line' property.
RootedValue lineProperty(cx);
if (!GetProperty(cx, query, query, cx->names().line, &lineProperty)) {
return false;
}
if (lineProperty.isUndefined()) {
hasLine = false;
} else if (lineProperty.isNumber()) {
if (displayURL.isUndefined() && url.isUndefined() && !hasSource) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_QUERY_LINE_WITHOUT_URL);
return false;
}
double doubleLine = lineProperty.toNumber();
uint32_t uintLine = (uint32_t)doubleLine;
if (doubleLine <= 0 || uintLine != doubleLine) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEBUG_BAD_LINE);
return false;
}
hasLine = true;
line = uintLine;
} else {
JS_ReportErrorNumberASCII(
cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE,
"query object's 'line' property", "neither undefined nor an integer");
return false;
}
// Check for an 'innermost' property.
PropertyName* innermostName = cx->names().innermost;
RootedValue innermostProperty(cx);
if (!GetProperty(cx, query, query, innermostName, &innermostProperty)) {
return false;
}
innermost = ToBoolean(innermostProperty);
if (innermost) {
// Technically, we need only check hasLine, but this is clearer.
if ((displayURL.isUndefined() && url.isUndefined() && !hasSource) ||
!hasLine) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_QUERY_INNERMOST_WITHOUT_LINE_URL);
return false;
}
}
return true;
}
/* Set up this ScriptQuery appropriately for a missing query argument. */
bool omittedQuery() {
url.setUndefined();
hasLine = false;
innermost = false;
displayURLString = nullptr;
return matchAllDebuggeeGlobals();
}
/*
* Search all relevant realms and the stack for scripts matching
* this query, and append the matching scripts to |scriptVector|.
*/
bool findScripts() {
if (!prepareQuery()) {
return false;
}
Realm* singletonRealm = nullptr;
if (realms.count() == 1) {
singletonRealm = realms.all().front();
}
// Search each realm for debuggee scripts.
MOZ_ASSERT(scriptVector.empty());
MOZ_ASSERT(partialMatchVector.empty());
oom = false;
IterateScripts(cx, singletonRealm, this, considerScript);
if (oom) {
ReportOutOfMemory(cx);
return false;
}
// If we are filtering by line number, the lazy BaseScripts were not checked
// yet since they do not implement `GetScriptLineExtent`. Instead we revisit
// each result script and delazify its children and add any matching ones to
// the results list.
MOZ_ASSERT(hasLine || partialMatchVector.empty());
Rooted<BaseScript*> script(cx);
RootedFunction fun(cx);
while (!partialMatchVector.empty()) {
script = partialMatchVector.popCopy();
// As a performance optimization, we can skip scripts that are definitely
// out-of-bounds for the target line. This was checked before adding to
// the partialMatchVector, but the bound may have improved since then.
if (script->extent().sourceEnd <= sourceOffsetLowerBound) {
continue;
}
MOZ_ASSERT(script->isFunction());
MOZ_ASSERT(script->isReadyForDelazification());
fun = script->function();
// Ignore any delazification placeholder functions. These should not be
// exposed to debugger in any way.
if (fun->isGhost()) {
continue;
}
// Delazify script.
JSScript* compiledScript = GetOrCreateFunctionScript(cx, fun);
if (!compiledScript) {
return false;
}
// If target line isn't in script, we are done with it.
if (!scriptIsLineMatch(compiledScript)) {
continue;
}
// Add script to results now that we've completed checks.
if (!scriptVector.append(compiledScript)) {
return false;
}
// If script was a leaf we are done with it. This is an optional
// optimization to avoid inspecting the `gcthings` list below.
if (!script->hasInnerFunctions()) {
continue;
}
// Now add inner scripts to `partialMatchVector` work list to determine if
// they are matches. Note that out IterateScripts callback ignored them
// already since they did not have a compiled parent at the time.
for (JS::GCCellPtr thing : script->gcthings()) {
if (!thing.is<JSObject>() || !thing.as<JSObject>().is<JSFunction>()) {
continue;
}
JSFunction* fun = &thing.as<JSObject>().as<JSFunction>();
if (!fun->hasBaseScript()) {
continue;
}
BaseScript* inner = fun->baseScript();
MOZ_ASSERT(inner);
if (!inner) {
// If the function doesn't have script, ignore it.
continue;
}
if (!scriptIsPartialLineMatch(inner)) {
continue;
}
// Add the matching inner script to the back of the results queue
// where it will be processed recursively.
if (!partialMatchVector.append(inner)) {
return false;
}
}
}
// If this is an 'innermost' query, we want to filter the results again to
// only return the innermost script for each realm. To do this we build a
// hashmap to track innermost and then recreate the `scriptVector` with the
// results that remain in the hashmap.
if (innermost) {
using RealmToScriptMap =
GCHashMap<Realm*, BaseScript*, DefaultHasher<Realm*>>;
Rooted<RealmToScriptMap> innermostForRealm(cx, cx);
// Visit each candidate script and find innermost in each realm.
for (BaseScript* script : scriptVector) {
Realm* realm = script->realm();
RealmToScriptMap::AddPtr p = innermostForRealm.lookupForAdd(realm);
if (p) {
// Is our newly found script deeper than the last one we found?
BaseScript* incumbent = p->value();
if (script->asJSScript()->innermostScope()->chainLength() >
incumbent->asJSScript()->innermostScope()->chainLength()) {
p->value() = script;
}
} else {
// This is the first matching script we've encountered for this
// realm, so it is thus the innermost such script.
if (!innermostForRealm.add(p, realm, script)) {
return false;
}
}
}
// Reset the results vector.
scriptVector.clear();
// Re-add only the innermost scripts to the results.
for (RealmToScriptMap::Range r = innermostForRealm.all(); !r.empty();
r.popFront()) {
if (!scriptVector.append(r.front().value())) {
return false;
}
}
}
// TODO: Until such time that wasm modules are real ES6 modules,
// unconditionally consider all wasm toplevel instance scripts.
for (WeakGlobalObjectSet::Range r = debugger->allDebuggees(); !r.empty();
r.popFront()) {
for (wasm::Instance* instance : r.front()->realm()->wasm.instances()) {
consider(instance->object());
if (oom) {
ReportOutOfMemory(cx);
return false;
}
}
}
return true;
}
Handle<BaseScriptVector> foundScripts() const { return scriptVector; }
Handle<WasmInstanceObjectVector> foundWasmInstances() const {
return wasmInstanceVector;
}
private:
/* If this is a string, matching scripts have urls equal to it. */
RootedValue url;
/* url as a C string. */
UniqueChars urlCString;
/* If this is a string, matching scripts' sources have displayURLs equal to
* it. */
Rooted<JSLinearString*> displayURLString;
/*
* If this is a source referent, matching scripts will have sources equal
* to this instance. Ideally we'd use a Maybe here, but Maybe interacts
* very badly with Rooted's LIFO invariant.
*/
bool hasSource = false;
Rooted<DebuggerSourceReferent> source;
/* True if the query contained a 'line' property. */
bool hasLine = false;
/* The line matching scripts must cover. */
uint32_t line = 0;
// As a performance optimization (and to avoid delazifying as many scripts),
// we would like to know the source offset of the target line.
//
// Since we do not have a simple way to compute this precisely, we instead
// track a lower-bound of the offset value. As we collect SourceExtent
// examples with (line,column) <-> sourceStart mappings, we can improve the
// bound. The target line is within the range [sourceOffsetLowerBound, Inf).
//
// NOTE: Using a SourceExtent for updating the bound happens independently of
// if the script matches the target line or not in the in the end.
mutable uint32_t sourceOffsetLowerBound = 0;
/* True if the query has an 'innermost' property whose value is true. */
bool innermost = false;
/*
* Accumulate the scripts in an Rooted<BaseScriptVector> instead of creating
* the JS array as we go, because we mustn't allocate JS objects or GC while
* we use the CellIter.
*/
Rooted<BaseScriptVector> scriptVector;
/*
* While in the CellIter we may find BaseScripts that need to be compiled
* before the query can be fully checked. Since we cannot compile while under
* CellIter we accumulate them here instead.
*
* This occurs when matching line numbers since `GetScriptLineExtent` cannot
* be computed without bytecode existing.
*/
Rooted<BaseScriptVector> partialMatchVector;
/*
* Like above, but for wasm modules.
*/
Rooted<WasmInstanceObjectVector> wasmInstanceVector;
/*
* Given that parseQuery or omittedQuery has been called, prepare to match
* scripts. Set urlCString and displayURLChars as appropriate.
*/
bool prepareQuery() {
// Compute urlCString and displayURLChars, if a url or displayURL was
// given respectively.
if (url.isString()) {
Rooted<JSString*> str(cx, url.toString());
urlCString = JS_EncodeStringToUTF8(cx, str);
if (!urlCString) {
return false;
}
}
return true;
}
void updateSourceOffsetLowerBound(const SourceExtent& extent) {
// We trying to find the offset of (target-line, 0) so just ignore any
// extents on target line to keep things simple.
MOZ_ASSERT(extent.lineno <= line);
if (extent.lineno == line) {
return;
}
// The extent.sourceStart position is now definitely *before* the target
// line, so update sourceOffsetLowerBound if extent.sourceStart is a tighter
// bound.
if (extent.sourceStart > sourceOffsetLowerBound) {
sourceOffsetLowerBound = extent.sourceStart;
}
}
// A partial match is a script that starts before the target line, but may or
// may not end before it. If we can prove the script definitely ends before
// the target line, we may return false here.
bool scriptIsPartialLineMatch(BaseScript* script) {
const SourceExtent& extent = script->extent();
// Check that start of script is before or on target line.
if (extent.lineno > line) {
return false;
}
// Use the implicit (line, column) <-> sourceStart mapping from the
// SourceExtent to update our bounds on possible matches. We call this
// without knowing if the script is a match or not.
updateSourceOffsetLowerBound(script->extent());
// As an optional performance optimization, we rule out any script that ends
// before the lower-bound on where target line exists.
return extent.sourceEnd > sourceOffsetLowerBound;
}
// True if any part of script source is on the target line.
bool scriptIsLineMatch(JSScript* script) {
MOZ_ASSERT(scriptIsPartialLineMatch(script));
uint32_t lineCount = GetScriptLineExtent(script);
return (script->lineno() + lineCount > line);
}
static void considerScript(JSRuntime* rt, void* data, BaseScript* script,
const JS::AutoRequireNoGC& nogc) {
ScriptQuery* self = static_cast<ScriptQuery*>(data);
self->consider(script, nogc);
}
template <typename T>
[[nodiscard]] bool commonFilter(T script, const JS::AutoRequireNoGC& nogc) {
if (urlCString) {
bool gotFilename = false;
if (script->filename() &&
strcmp(script->filename(), urlCString.get()) == 0) {
gotFilename = true;
}
bool gotSourceURL = false;
if (!gotFilename && script->scriptSource()->introducerFilename() &&
strcmp(script->scriptSource()->introducerFilename(),
urlCString.get()) == 0) {
gotSourceURL = true;
}
if (!gotFilename && !gotSourceURL) {
return false;
}
}
if (displayURLString) {
if (!script->scriptSource() || !script->scriptSource()->hasDisplayURL()) {
return false;
}
const char16_t* s = script->scriptSource()->displayURL();
if (CompareChars(s, js_strlen(s), displayURLString) != 0) {
return false;
}
}
if (hasSource && !(source.is<ScriptSourceObject*>() &&
source.as<ScriptSourceObject*>()->source() ==
script->scriptSource())) {
return false;
}
return true;
}
/*
* If |script| matches this query, append it to |scriptVector|. Set |oom| if
* an out of memory condition occurred.
*/
void consider(BaseScript* script, const JS::AutoRequireNoGC& nogc) {
if (oom || script->selfHosted()) {
return;
}
Realm* realm = script->realm();
if (!realms.has(realm)) {
return;
}
if (!commonFilter(script, nogc)) {
return;
}
bool partial = false;
if (hasLine) {
if (!scriptIsPartialLineMatch(script)) {
return;
}
if (script->hasBytecode()) {
// Check if line is within script (or any of its inner scripts).
if (!scriptIsLineMatch(script->asJSScript())) {
return;
}
} else {
// GetScriptLineExtent is not available on lazy scripts so instead to
// the partial match list for be compiled and reprocessed later. We only
// add scripts that are ready for delazification and they may in turn
// process their inner functions.
if (!script->isReadyForDelazification()) {
return;
}
partial = true;
}
}
// If innermost filter is required, we collect everything that matches the
// line number and filter at the end of `findScripts`.
MOZ_ASSERT_IF(innermost, hasLine);
Rooted<BaseScriptVector>& vec = partial ? partialMatchVector : scriptVector;
if (!vec.append(script)) {
oom = true;
}
}
/*
* If |instanceObject| matches this query, append it to |wasmInstanceVector|.
* Set |oom| if an out of memory condition occurred.
*/
void consider(WasmInstanceObject* instanceObject) {
if (oom) {
return;
}
if (hasSource && source != AsVariant(instanceObject)) {
return;
}
if (!wasmInstanceVector.append(instanceObject)) {
oom = true;
}
}
};
bool Debugger::CallData::findScripts() {
ScriptQuery query(cx, dbg);
if (args.length() >= 1) {
RootedObject queryObject(cx, RequireObject(cx, args[0]));
if (!queryObject || !query.parseQuery(queryObject)) {
return false;
}
} else {
if (!query.omittedQuery()) {
return false;
}
}
if (!query.findScripts()) {
return false;
}
Handle<BaseScriptVector> scripts(query.foundScripts());
Handle<WasmInstanceObjectVector> wasmInstances(query.foundWasmInstances());
size_t resultLength = scripts.length() + wasmInstances.length();
Rooted<ArrayObject*> result(cx,
NewDenseFullyAllocatedArray(cx, resultLength));
if (!result) {
return false;
}
result->ensureDenseInitializedLength(0, resultLength);
for (size_t i = 0; i < scripts.length(); i++) {
JSObject* scriptObject = dbg->wrapScript(cx, scripts[i]);
if (!scriptObject) {
return false;
}
result->setDenseElement(i, ObjectValue(*scriptObject));
}
size_t wasmStart = scripts.length();
for (size_t i = 0; i < wasmInstances.length(); i++) {
JSObject* scriptObject = dbg->wrapWasmScript(cx, wasmInstances[i]);
if (!scriptObject) {
return false;
}
result->setDenseElement(wasmStart + i, ObjectValue(*scriptObject));
}
args.rval().setObject(*result);
return true;
}
/*
* A class for searching sources for 'findSources'.
*/
class MOZ_STACK_CLASS Debugger::SourceQuery : public Debugger::QueryBase {
public:
using SourceSet = JS::GCHashSet<JSObject*, js::StableCellHasher<JSObject*>,
ZoneAllocPolicy>;
SourceQuery(JSContext* cx, Debugger* dbg)
: QueryBase(cx, dbg), sources(cx, SourceSet(cx->zone())) {}
bool findSources() {
if (!matchAllDebuggeeGlobals()) {
return false;
}
Realm* singletonRealm = nullptr;
if (realms.count() == 1) {
singletonRealm = realms.all().front();
}
// Search each realm for debuggee scripts.
MOZ_ASSERT(sources.empty());
oom = false;
IterateScripts(cx, singletonRealm, this, considerScript);
if (oom) {
ReportOutOfMemory(cx);
return false;
}
// TODO: Until such time that wasm modules are real ES6 modules,
// unconditionally consider all wasm toplevel instance scripts.
for (WeakGlobalObjectSet::Range r = debugger->allDebuggees(); !r.empty();
r.popFront()) {
for (wasm::Instance* instance : r.front()->realm()->wasm.instances()) {
consider(instance->object());
if (oom) {
ReportOutOfMemory(cx);
return false;
}
}
}
return true;
}
Handle<SourceSet> foundSources() const { return sources; }
private:
Rooted<SourceSet> sources;
static void considerScript(JSRuntime* rt, void* data, BaseScript* script,
const JS::AutoRequireNoGC& nogc) {
SourceQuery* self = static_cast<SourceQuery*>(data);
self->consider(script, nogc);
}
void consider(BaseScript* script, const JS::AutoRequireNoGC& nogc) {
if (oom || script->selfHosted()) {
return;
}
Realm* realm = script->realm();
if (!realms.has(realm)) {
return;
}
ScriptSourceObject* source = script->sourceObject();
if (!sources.put(source)) {
oom = true;
}
}
void consider(WasmInstanceObject* instanceObject) {
if (oom) {
return;
}
if (!sources.put(instanceObject)) {
oom = true;
}
}
};
static inline DebuggerSourceReferent AsSourceReferent(JSObject* obj) {
if (obj->is<ScriptSourceObject>()) {
return AsVariant(&obj->as<ScriptSourceObject>());
}
return AsVariant(&obj->as<WasmInstanceObject>());
}
bool Debugger::CallData::findSources() {
SourceQuery query(cx, dbg);
if (!query.findSources()) {
return false;
}
Handle<SourceQuery::SourceSet> sources(query.foundSources());
size_t resultLength = sources.count();
Rooted<ArrayObject*> result(cx,
NewDenseFullyAllocatedArray(cx, resultLength));
if (!result) {
return false;
}
result->ensureDenseInitializedLength(0, resultLength);
size_t i = 0;
for (auto iter = sources.get().iter(); !iter.done(); iter.next()) {
Rooted<DebuggerSourceReferent> sourceReferent(cx,
AsSourceReferent(iter.get()));
RootedObject sourceObject(cx, dbg->wrapVariantReferent(cx, sourceReferent));
if (!sourceObject) {
return false;
}
result->setDenseElement(i, ObjectValue(*sourceObject));
i++;
}
args.rval().setObject(*result);
return true;
}
/*
* A class for parsing 'findObjects' query arguments and searching for objects
* that match the criteria they represent.
*/
class MOZ_STACK_CLASS Debugger::ObjectQuery {
public:
/* Construct an ObjectQuery to use matching scripts for |dbg|. */
ObjectQuery(JSContext* cx, Debugger* dbg)
: objects(cx), cx(cx), dbg(dbg), className(cx) {}
/* The vector that we are accumulating results in. */
RootedObjectVector objects;
/* The set of debuggee compartments. */
JS::CompartmentSet debuggeeCompartments;
/*
* Parse the query object |query|, and prepare to match only the objects it
* specifies.
*/
bool parseQuery(HandleObject query) {
// Check for the 'class' property
RootedValue cls(cx);
if (!GetProperty(cx, query, query, cx->names().class_, &cls)) {
return false;
}
if (!cls.isUndefined()) {
if (!cls.isString()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_UNEXPECTED_TYPE,
"query object's 'class' property",
"neither undefined nor a string");
return false;
}
JSLinearString* str = cls.toString()->ensureLinear(cx);
if (!str) {
return false;
}
if (!StringIsAscii(str)) {
JS_ReportErrorNumberASCII(
cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE,
"query object's 'class' property",
"not a string containing only ASCII characters");
return false;
}
className = cls;
}
return true;
}
/* Set up this ObjectQuery appropriately for a missing query argument. */
void omittedQuery() { className.setUndefined(); }
/*
* Traverse the heap to find all relevant objects and add them to the
* provided vector.
*/
bool findObjects() {
if (!prepareQuery()) {
return false;
}
for (WeakGlobalObjectSet::Range r = dbg->allDebuggees(); !r.empty();
r.popFront()) {
if (!debuggeeCompartments.put(r.front()->compartment())) {
ReportOutOfMemory(cx);
return false;
}
}
{
// We can't tolerate the GC moving things around while we're
// searching the heap. Check that nothing we do causes a GC.
RootedObject dbgObj(cx, dbg->object);
JS::ubi::RootList rootList(cx);
auto [ok, nogc] = rootList.init(dbgObj);
if (!ok) {
ReportOutOfMemory(cx);
return false;
}
Traversal traversal(cx, *this, nogc);
traversal.wantNames = false;
return traversal.addStart(JS::ubi::Node(&rootList)) &&
traversal.traverse();
}
}
/*
* |ubi::Node::BreadthFirst| interface.
*/
class NodeData {};
using Traversal = JS::ubi::BreadthFirst<ObjectQuery>;
bool operator()(Traversal& traversal, JS::ubi::Node origin,
const JS::ubi::Edge& edge, NodeData*, bool first) {
if (!first) {
return true;
}
JS::ubi::Node referent = edge.referent;
// Only follow edges within our set of debuggee compartments; we don't
// care about the heap's subgraphs outside of our debuggee compartments,
// so we abandon the referent. Either (1) there is not a path from this
// non-debuggee node back to a node in our debuggee compartments, and we
// don't need to follow edges to or from this node, or (2) there does
// exist some path from this non-debuggee node back to a node in our
// debuggee compartments. However, if that were true, then the incoming
// cross compartment edge back into a debuggee compartment is already
// listed as an edge in the RootList we started traversal with, and
// therefore we don't need to follow edges to or from this non-debuggee
// node.
JS::Compartment* comp = referent.compartment();
if (comp && !debuggeeCompartments.has(comp)) {
traversal.abandonReferent();
return true;
}
// If the referent has an associated realm and it's not a debuggee
// realm, skip it. Don't abandonReferent() here like above: realms
// within a compartment can reference each other without going through
// cross-compartment wrappers.
Realm* realm = referent.realm();
if (realm && !dbg->isDebuggeeUnbarriered(realm)) {
return true;
}
// If the referent is an object and matches our query's restrictions,
// add it to the vector accumulating results. Skip objects that should
// never be exposed to JS, like EnvironmentObjects and internal
// functions.
if (!referent.is<JSObject>() || referent.exposeToJS().isUndefined()) {
return true;
}
JSObject* obj = referent.as<JSObject>();
if (!className.isUndefined()) {
const char* objClassName = obj->getClass()->name;
if (strcmp(objClassName, classNameCString.get()) != 0) {
return true;
}
}
return objects.append(obj);
}
private:
/* The context in which we should do our work. */
JSContext* cx;
/* The debugger for which we conduct queries. */
Debugger* dbg;
/*
* If this is non-null, matching objects will have a class whose name is
* this property.
*/
RootedValue className;
/* The className member, as a C string. */
UniqueChars classNameCString;
/*
* Given that either omittedQuery or parseQuery has been called, prepare the
* query for matching objects.
*/
bool prepareQuery() {
if (className.isString()) {
classNameCString = JS_EncodeStringToASCII(cx, className.toString());
if (!classNameCString) {
return false;
}
}
return true;
}
};
bool Debugger::CallData::findObjects() {
ObjectQuery query(cx, dbg);
if (args.length() >= 1) {
RootedObject queryObject(cx, RequireObject(cx, args[0]));
if (!queryObject || !query.parseQuery(queryObject)) {
return false;
}
} else {
query.omittedQuery();
}
if (!query.findObjects()) {
return false;
}
// Returning internal objects (such as self-hosting intrinsics) to JS is not
// fuzzing-safe. We still want to call parseQuery/findObjects when fuzzing so
// just clear the Vector here.
if (fuzzingSafe) {
query.objects.clear();
}
size_t length = query.objects.length();
Rooted<ArrayObject*> result(cx, NewDenseFullyAllocatedArray(cx, length));
if (!result) {
return false;
}
result->ensureDenseInitializedLength(0, length);
for (size_t i = 0; i < length; i++) {
RootedValue debuggeeVal(cx, ObjectValue(*query.objects[i]));
if (!dbg->wrapDebuggeeValue(cx, &debuggeeVal)) {
return false;
}
result->setDenseElement(i, debuggeeVal);
}
args.rval().setObject(*result);
return true;
}
bool Debugger::CallData::findAllGlobals() {
RootedObjectVector globals(cx);
{
// Accumulate the list of globals before wrapping them, because
// wrapping can GC and collect realms from under us, while iterating.
JS::AutoCheckCannotGC nogc;
for (RealmsIter r(cx->runtime()); !r.done(); r.next()) {
if (r->creationOptions().invisibleToDebugger()) {
continue;
}
if (!r->hasInitializedGlobal()) {
continue;
}
if (JS::RealmBehaviorsRef(r).isNonLive()) {
continue;
}
r->compartment()->gcState.scheduledForDestruction = false;
GlobalObject* global = r->maybeGlobal();
// We pulled |global| out of nowhere, so it's possible that it was
// marked gray by XPConnect. Since we're now exposing it to JS code,
// we need to mark it black.
JS::ExposeObjectToActiveJS(global);
if (!globals.append(global)) {
return false;
}
}
}
RootedObject result(cx, NewDenseEmptyArray(cx));
if (!result) {
return false;
}
for (size_t i = 0; i < globals.length(); i++) {
RootedValue globalValue(cx, ObjectValue(*globals[i]));
if (!dbg->wrapDebuggeeValue(cx, &globalValue)) {
return false;
}
if (!NewbornArrayPush(cx, result, globalValue)) {
return false;
}
}
args.rval().setObject(*result);
return true;
}
bool Debugger::CallData::findSourceURLs() {
RootedObject result(cx, NewDenseEmptyArray(cx));
if (!result) {
return false;
}
for (WeakGlobalObjectSet::Range r = dbg->allDebuggees(); !r.empty();
r.popFront()) {
RootedObject holder(cx, r.front()->getSourceURLsHolder());
if (holder) {
for (size_t i = 0; i < holder->as<ArrayObject>().length(); i++) {
Value v = holder->as<ArrayObject>().getDenseElement(i);
// The value is an atom and doesn't need wrapping, but the holder may be
// in another zone and the atom must be marked when we create a
// reference in this zone.
MOZ_ASSERT(v.isString() && v.toString()->isAtom());
cx->markAtomValue(v);
if (!NewbornArrayPush(cx, result, v)) {
return false;
}
}
}
}
args.rval().setObject(*result);
return true;
}
bool Debugger::CallData::makeGlobalObjectReference() {
if (!args.requireAtLeast(cx, "Debugger.makeGlobalObjectReference", 1)) {
return false;
}
Rooted<GlobalObject*> global(cx, dbg->unwrapDebuggeeArgument(cx, args[0]));
if (!global) {
return false;
}
// If we create a D.O referring to a global in an invisible realm,
// then from it we can reach function objects, scripts, environments, etc.,
// none of which we're ever supposed to see.
if (global->realm()->creationOptions().invisibleToDebugger()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEBUG_INVISIBLE_COMPARTMENT);
return false;
}
args.rval().setObject(*global);
return dbg->wrapDebuggeeValue(cx, args.rval());
}
bool Debugger::isCompilableUnit(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (!args.requireAtLeast(cx, "Debugger.isCompilableUnit", 1)) {
return false;
}
if (!args[0].isString()) {
JS_ReportErrorNumberASCII(
cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE,
"Debugger.isCompilableUnit", "string", InformalValueTypeName(args[0]));
return false;
}
JSString* str = args[0].toString();
size_t length = str->length();
AutoStableStringChars chars(cx);
if (!chars.initTwoByte(cx, str)) {
return false;
}
bool result = true;
AutoReportFrontendContext fc(cx,
AutoReportFrontendContext::Warning::Suppress);
CompileOptions options(cx);
Rooted<frontend::CompilationInput> input(cx,
frontend::CompilationInput(options));
if (!input.get().initForGlobal(&fc)) {
return false;
}
LifoAllocScope allocScope(&cx->tempLifoAlloc());
frontend::NoScopeBindingCache scopeCache;
frontend::CompilationState compilationState(&fc, allocScope, input.get());
if (!compilationState.init(&fc, &scopeCache)) {
return false;
}
frontend::Parser<frontend::FullParseHandler, char16_t> parser(
&fc, options, chars.twoByteChars(), length,
/* foldConstants = */ true, compilationState,
/* syntaxParser = */ nullptr);
if (!parser.checkOptions() || parser.parse().isErr()) {
// We ran into an error. If it was because we ran out of memory we report
// it in the usual way.
if (fc.hadOutOfMemory()) {
return false;
}
// If it was because we ran out of source, we return false so our caller
// knows to try to collect more [source].
if (parser.isUnexpectedEOF()) {
result = false;
}
fc.clearAutoReport();
}
args.rval().setBoolean(result);
return true;
}
bool Debugger::CallData::adoptDebuggeeValue() {
if (!args.requireAtLeast(cx, "Debugger.adoptDebuggeeValue", 1)) {
return false;
}
RootedValue v(cx, args[0]);
if (v.isObject()) {
RootedObject obj(cx, &v.toObject());
DebuggerObject* ndobj = ToNativeDebuggerObject(cx, &obj);
if (!ndobj) {
return false;
}
obj.set(ndobj->referent());
v = ObjectValue(*obj);
if (!dbg->wrapDebuggeeValue(cx, &v)) {
return false;
}
}
args.rval().set(v);
return true;
}
class DebuggerAdoptSourceMatcher {
JSContext* cx_;
Debugger* dbg_;
public:
explicit DebuggerAdoptSourceMatcher(JSContext* cx, Debugger* dbg)
: cx_(cx), dbg_(dbg) {}
using ReturnType = DebuggerSource*;
ReturnType match(Handle<ScriptSourceObject*> source) {
if (source->compartment() == cx_->compartment()) {
JS_ReportErrorASCII(cx_,
"Source is in the same compartment as this debugger");
return nullptr;
}
return dbg_->wrapSource(cx_, source);
}
ReturnType match(Handle<WasmInstanceObject*> wasmInstance) {
if (wasmInstance->compartment() == cx_->compartment()) {
JS_ReportErrorASCII(
cx_, "WasmInstance is in the same compartment as this debugger");
return nullptr;
}
return dbg_->wrapWasmSource(cx_, wasmInstance);
}
};
bool Debugger::CallData::adoptFrame() {
if (!args.requireAtLeast(cx, "Debugger.adoptFrame", 1)) {
return false;
}
RootedObject obj(cx, RequireObject(cx, args[0]));
if (!obj) {
return false;
}
obj = UncheckedUnwrap(obj);
if (!obj->is<DebuggerFrame>()) {
JS_ReportErrorASCII(cx, "Argument is not a Debugger.Frame");
return false;
}
RootedValue objVal(cx, ObjectValue(*obj));
Rooted<DebuggerFrame*> frameObj(cx, DebuggerFrame::check(cx, objVal));
if (!frameObj) {
return false;
}
Rooted<DebuggerFrame*> adoptedFrame(cx);
if (frameObj->isOnStack()) {
FrameIter iter = frameObj->getFrameIter(cx);
if (!dbg->observesFrame(iter)) {
JS_ReportErrorASCII(cx, "Debugger.Frame's global is not a debuggee");
return false;
}
if (!dbg->getFrame(cx, iter, &adoptedFrame)) {
return false;
}
} else if (frameObj->isSuspended()) {
Rooted<AbstractGeneratorObject*> gen(cx, &frameObj->unwrappedGenerator());
if (!dbg->observesGlobal(&gen->global())) {
JS_ReportErrorASCII(cx, "Debugger.Frame's global is not a debuggee");
return false;
}
if (!dbg->getFrame(cx, gen, &adoptedFrame)) {
return false;
}
} else {
if (!dbg->getFrame(cx, &adoptedFrame)) {
return false;
}
}
args.rval().setObject(*adoptedFrame);
return true;
}
bool Debugger::CallData::adoptSource() {
if (!args.requireAtLeast(cx, "Debugger.adoptSource", 1)) {
return false;
}
RootedObject obj(cx, RequireObject(cx, args[0]));
if (!obj) {
return false;
}
obj = UncheckedUnwrap(obj);
if (!obj->is<DebuggerSource>()) {
JS_ReportErrorASCII(cx, "Argument is not a Debugger.Source");
return false;
}
Rooted<DebuggerSource*> sourceObj(cx, &obj->as<DebuggerSource>());
if (!sourceObj->getReferentRawObject()) {
JS_ReportErrorASCII(cx, "Argument is Debugger.Source.prototype");
return false;
}
Rooted<DebuggerSourceReferent> referent(cx, sourceObj->getReferent());
DebuggerAdoptSourceMatcher matcher(cx, dbg);
DebuggerSource* res = referent.match(matcher);
if (!res) {
return false;
}
args.rval().setObject(*res);
return true;
}
bool Debugger::CallData::enableAsyncStack() {
if (!args.requireAtLeast(cx, "Debugger.enableAsyncStack", 1)) {
return false;
}
Rooted<GlobalObject*> global(cx, dbg->unwrapDebuggeeArgument(cx, args[0]));
if (!global) {
return false;
}
global->realm()->isAsyncStackCapturingEnabled = true;
args.rval().setUndefined();
return true;
}
bool Debugger::CallData::disableAsyncStack() {
if (!args.requireAtLeast(cx, "Debugger.disableAsyncStack", 1)) {
return false;
}
Rooted<GlobalObject*> global(cx, dbg->unwrapDebuggeeArgument(cx, args[0]));
if (!global) {
return false;
}
global->realm()->isAsyncStackCapturingEnabled = false;
args.rval().setUndefined();
return true;
}
bool Debugger::CallData::enableUnlimitedStacksCapturing() {
if (!args.requireAtLeast(cx, "Debugger.enableUnlimitedStacksCapturing", 1)) {
return false;
}
Rooted<GlobalObject*> global(cx, dbg->unwrapDebuggeeArgument(cx, args[0]));
if (!global) {
return false;
}
global->realm()->isUnlimitedStacksCapturingEnabled = true;
args.rval().setUndefined();
return true;
}
bool Debugger::CallData::disableUnlimitedStacksCapturing() {
if (!args.requireAtLeast(cx, "Debugger.disableUnlimitedStacksCapturing", 1)) {
return false;
}
Rooted<GlobalObject*> global(cx, dbg->unwrapDebuggeeArgument(cx, args[0]));
if (!global) {
return false;
}
global->realm()->isUnlimitedStacksCapturingEnabled = false;
args.rval().setUndefined();
return true;
}
const JSPropertySpec Debugger::properties[] = {
JS_DEBUG_PSGS("onDebuggerStatement", getOnDebuggerStatement,
setOnDebuggerStatement),
JS_DEBUG_PSGS("onExceptionUnwind", getOnExceptionUnwind,
setOnExceptionUnwind),
JS_DEBUG_PSGS("onNewScript", getOnNewScript, setOnNewScript),
JS_DEBUG_PSGS("onNewPromise", getOnNewPromise, setOnNewPromise),
JS_DEBUG_PSGS("onPromiseSettled", getOnPromiseSettled, setOnPromiseSettled),
JS_DEBUG_PSGS("onEnterFrame", getOnEnterFrame, setOnEnterFrame),
JS_DEBUG_PSGS("onNativeCall", getOnNativeCall, setOnNativeCall),
JS_DEBUG_PSGS("onNewGlobalObject", getOnNewGlobalObject,
setOnNewGlobalObject),
JS_DEBUG_PSGS("uncaughtExceptionHook", getUncaughtExceptionHook,
setUncaughtExceptionHook),
JS_DEBUG_PSGS("allowUnobservedAsmJS", getAllowUnobservedAsmJS,
setAllowUnobservedAsmJS),
JS_DEBUG_PSGS("allowUnobservedWasm", getAllowUnobservedWasm,
setAllowUnobservedWasm),
JS_DEBUG_PSGS("collectCoverageInfo", getCollectCoverageInfo,
setCollectCoverageInfo),
JS_DEBUG_PSGS("exclusiveDebuggerOnEval", getExclusiveDebuggerOnEval,
setExclusiveDebuggerOnEval),
JS_DEBUG_PSGS("inspectNativeCallArguments", getInspectNativeCallArguments,
setInspectNativeCallArguments),
JS_DEBUG_PSG("memory", getMemory),
JS_STRING_SYM_PS(toStringTag, "Debugger", JSPROP_READONLY),
JS_PS_END};
const JSFunctionSpec Debugger::methods[] = {
JS_DEBUG_FN("addDebuggee", addDebuggee, 1),
JS_DEBUG_FN("addAllGlobalsAsDebuggees", addAllGlobalsAsDebuggees, 0),
JS_DEBUG_FN("removeDebuggee", removeDebuggee, 1),
JS_DEBUG_FN("removeAllDebuggees", removeAllDebuggees, 0),
JS_DEBUG_FN("hasDebuggee", hasDebuggee, 1),
JS_DEBUG_FN("getDebuggees", getDebuggees, 0),
JS_DEBUG_FN("getNewestFrame", getNewestFrame, 0),
JS_DEBUG_FN("clearAllBreakpoints", clearAllBreakpoints, 0),
JS_DEBUG_FN("findScripts", findScripts, 1),
JS_DEBUG_FN("findSources", findSources, 1),
JS_DEBUG_FN("findObjects", findObjects, 1),
JS_DEBUG_FN("findAllGlobals", findAllGlobals, 0),
JS_DEBUG_FN("findSourceURLs", findSourceURLs, 0),
JS_DEBUG_FN("makeGlobalObjectReference", makeGlobalObjectReference, 1),
JS_DEBUG_FN("adoptDebuggeeValue", adoptDebuggeeValue, 1),
JS_DEBUG_FN("adoptFrame", adoptFrame, 1),
JS_DEBUG_FN("adoptSource", adoptSource, 1),
JS_DEBUG_FN("enableAsyncStack", enableAsyncStack, 1),
JS_DEBUG_FN("disableAsyncStack", disableAsyncStack, 1),
JS_DEBUG_FN("enableUnlimitedStacksCapturing",
enableUnlimitedStacksCapturing, 1),
JS_DEBUG_FN("disableUnlimitedStacksCapturing",
disableUnlimitedStacksCapturing, 1),
JS_FS_END};
const JSFunctionSpec Debugger::static_methods[]{
JS_FN("isCompilableUnit", Debugger::isCompilableUnit, 1, 0), JS_FS_END};
DebuggerScript* Debugger::newDebuggerScript(
JSContext* cx, Handle<DebuggerScriptReferent> referent) {
cx->check(object.get());
RootedObject proto(
cx, &object->getReservedSlot(JSSLOT_DEBUG_SCRIPT_PROTO).toObject());
MOZ_ASSERT(proto);
Rooted<NativeObject*> debugger(cx, object);
return DebuggerScript::create(cx, proto, referent, debugger);
}
template <typename ReferentType, typename Map>
typename Map::WrapperType* Debugger::wrapVariantReferent(
JSContext* cx, Map& map,
Handle<typename Map::WrapperType::ReferentVariant> referent) {
cx->check(object);
Handle<ReferentType*> untaggedReferent =
referent.template as<ReferentType*>();
MOZ_ASSERT(cx->compartment() != untaggedReferent->compartment());
DependentAddPtr<Map> p(cx, map, untaggedReferent);
if (!p) {
typename Map::WrapperType* wrapper = newVariantWrapper(cx, referent);
if (!wrapper) {
return nullptr;
}
if (!p.add(cx, map, untaggedReferent, wrapper)) {
// We need to destroy the edge to the referent, to avoid trying to trace
// it during untimely collections.
wrapper->clearReferent();
return nullptr;
}
}
return &p->value()->template as<typename Map::WrapperType>();
}
DebuggerScript* Debugger::wrapVariantReferent(
JSContext* cx, Handle<DebuggerScriptReferent> referent) {
if (referent.is<BaseScript*>()) {
return wrapVariantReferent<BaseScript>(cx, scripts, referent);
}
return wrapVariantReferent<WasmInstanceObject>(cx, wasmInstanceScripts,
referent);
}
DebuggerScript* Debugger::wrapScript(JSContext* cx,
Handle<BaseScript*> script) {
Rooted<DebuggerScriptReferent> referent(cx,
DebuggerScriptReferent(script.get()));
return wrapVariantReferent(cx, referent);
}
DebuggerScript* Debugger::wrapWasmScript(
JSContext* cx, Handle<WasmInstanceObject*> wasmInstance) {
Rooted<DebuggerScriptReferent> referent(cx, wasmInstance.get());
return wrapVariantReferent(cx, referent);
}
DebuggerSource* Debugger::newDebuggerSource(
JSContext* cx, Handle<DebuggerSourceReferent> referent) {
cx->check(object.get());
RootedObject proto(
cx, &object->getReservedSlot(JSSLOT_DEBUG_SOURCE_PROTO).toObject());
MOZ_ASSERT(proto);
Rooted<NativeObject*> debugger(cx, object);
return DebuggerSource::create(cx, proto, referent, debugger);
}
DebuggerSource* Debugger::wrapVariantReferent(
JSContext* cx, Handle<DebuggerSourceReferent> referent) {
DebuggerSource* obj;
if (referent.is<ScriptSourceObject*>()) {
obj = wrapVariantReferent<ScriptSourceObject>(cx, sources, referent);
} else {
obj = wrapVariantReferent<WasmInstanceObject>(cx, wasmInstanceSources,
referent);
}
MOZ_ASSERT_IF(obj, obj->getReferent() == referent);
return obj;
}
DebuggerSource* Debugger::wrapSource(JSContext* cx,
Handle<ScriptSourceObject*> source) {
Rooted<DebuggerSourceReferent> referent(cx, source.get());
return wrapVariantReferent(cx, referent);
}
DebuggerSource* Debugger::wrapWasmSource(
JSContext* cx, Handle<WasmInstanceObject*> wasmInstance) {
Rooted<DebuggerSourceReferent> referent(cx, wasmInstance.get());
return wrapVariantReferent(cx, referent);
}
bool Debugger::observesFrame(AbstractFramePtr frame) const {
if (frame.isWasmDebugFrame()) {
return observesWasm(frame.wasmInstance());
}
return observesScript(frame.script());
}
bool Debugger::observesFrame(const FrameIter& iter) const {
// Skip frames not yet fully initialized during their prologue.
if (iter.isInterp() && iter.isFunctionFrame()) {
const Value& thisVal = iter.interpFrame()->thisArgument();
if (thisVal.isMagic() && thisVal.whyMagic() == JS_IS_CONSTRUCTING) {
return false;
}
}
if (iter.isWasm()) {
// Skip frame of wasm instances we cannot observe.
if (!iter.wasmDebugEnabled()) {
return false;
}
return observesWasm(iter.wasmInstance());
}
return observesScript(iter.script());
}
bool Debugger::observesScript(JSScript* script) const {
// Don't ever observe self-hosted scripts: the Debugger API can break
// self-hosted invariants.
return observesGlobal(&script->global()) && !script->selfHosted();
}
bool Debugger::observesWasm(wasm::Instance* instance) const {
if (!instance->debugEnabled()) {
return false;
}
return observesGlobal(&instance->object()->global());
}
/* static */
bool Debugger::replaceFrameGuts(JSContext* cx, AbstractFramePtr from,
AbstractFramePtr to, ScriptFrameIter& iter) {
MOZ_ASSERT(from != to);
// Rekey missingScopes to maintain Debugger.Environment identity and
// forward liveScopes to point to the new frame.
DebugEnvironments::forwardLiveFrame(cx, from, to);
// If we hit an OOM anywhere in here, we need to make sure there aren't any
// Debugger.Frame objects left partially-initialized.
auto terminateDebuggerFramesOnExit = MakeScopeExit([&] {
terminateDebuggerFrames(cx, from);
terminateDebuggerFrames(cx, to);
MOZ_ASSERT(!DebugAPI::inFrameMaps(from));
MOZ_ASSERT(!DebugAPI::inFrameMaps(to));
});
// Forward live Debugger.Frame objects.
Rooted<DebuggerFrameVector> frames(cx);
if (!getDebuggerFrames(from, &frames)) {
// An OOM here means that all Debuggers' frame maps still contain
// entries for 'from' and no entries for 'to'. Since the 'from' frame
// will be gone, they are removed by terminateDebuggerFramesOnExit
// above.
ReportOutOfMemory(cx);
return false;
}
for (size_t i = 0; i < frames.length(); i++) {
Handle<DebuggerFrame*> frameobj = frames[i];
Debugger* dbg = frameobj->owner();
// Update frame object's ScriptFrameIter::data pointer.
if (!frameobj->replaceFrameIterData(cx, iter)) {
return false;
}
// Add the frame object with |to| as key.
if (!dbg->frames.putNew(to, frameobj)) {
ReportOutOfMemory(cx);
return false;
}
// Remove the old frame entry after all fallible operations are completed
// so that an OOM will be able to clean up properly.
dbg->frames.remove(from);
}
// All frames successfuly replaced, cancel the rollback.
terminateDebuggerFramesOnExit.release();
MOZ_ASSERT(!DebugAPI::inFrameMaps(from));
MOZ_ASSERT_IF(!frames.empty(), DebugAPI::inFrameMaps(to));
return true;
}
/* static */
bool DebugAPI::inFrameMaps(AbstractFramePtr frame) {
bool foundAny = false;
JS::AutoAssertNoGC nogc;
Debugger::forEachOnStackDebuggerFrame(
frame, nogc,
[&](Debugger*, DebuggerFrame* frameobj) { foundAny = true; });
return foundAny;
}
/* static */
void Debugger::suspendGeneratorDebuggerFrames(JSContext* cx,
AbstractFramePtr frame) {
JS::GCContext* gcx = cx->gcContext();
JS::AutoAssertNoGC nogc;
forEachOnStackDebuggerFrame(
frame, nogc, [&](Debugger* dbg, DebuggerFrame* dbgFrame) {
dbg->frames.remove(frame);
#if DEBUG
MOZ_ASSERT(dbgFrame->hasGeneratorInfo());
AbstractGeneratorObject& genObj = dbgFrame->unwrappedGenerator();
GeneratorWeakMap::Ptr p = dbg->generatorFrames.lookup(&genObj);
MOZ_ASSERT(p);
MOZ_ASSERT(p->value() == dbgFrame);
#endif
dbgFrame->suspend(gcx);
});
}
/* static */
void Debugger::terminateDebuggerFrames(JSContext* cx, AbstractFramePtr frame) {
JS::GCContext* gcx = cx->gcContext();
JS::AutoAssertNoGC nogc;
forEachOnStackOrSuspendedDebuggerFrame(
cx, frame, nogc, [&](Debugger* dbg, DebuggerFrame* dbgFrame) {
Debugger::terminateDebuggerFrame(gcx, dbg, dbgFrame, frame);
});
// If this is an eval frame, then from the debugger's perspective the
// script is about to be destroyed. Remove any breakpoints in it.
if (frame.isEvalFrame()) {
RootedScript script(cx, frame.script());
DebugScript::clearBreakpointsIn(cx->gcContext(), script, nullptr, nullptr);
}
}
/* static */
void Debugger::terminateDebuggerFrame(
JS::GCContext* gcx, Debugger* dbg, DebuggerFrame* dbgFrame,
AbstractFramePtr frame, FrameMap::Enum* maybeFramesEnum,
GeneratorWeakMap::Enum* maybeGeneratorFramesEnum) {
// If we were not passed the frame, either we are destroying a frame early
// on before it was inserted into the "frames" list, or else we are
// terminating a frame from "generatorFrames" and the "frames" entries will
// be cleaned up later on with a second call to this function.
MOZ_ASSERT_IF(!frame, !maybeFramesEnum);
MOZ_ASSERT_IF(!frame, dbgFrame->hasGeneratorInfo());
MOZ_ASSERT_IF(!dbgFrame->hasGeneratorInfo(), !maybeGeneratorFramesEnum);
if (frame) {
if (maybeFramesEnum) {
maybeFramesEnum->removeFront();
} else {
dbg->frames.remove(frame);
}
}
if (dbgFrame->hasGeneratorInfo()) {
if (maybeGeneratorFramesEnum) {
maybeGeneratorFramesEnum->removeFront();
} else {
dbg->generatorFrames.remove(&dbgFrame->unwrappedGenerator());
}
}
dbgFrame->terminate(gcx, frame);
}
DebuggerDebuggeeLink* Debugger::getDebuggeeLink() {
return &object->getReservedSlot(JSSLOT_DEBUG_DEBUGGEE_LINK)
.toObject()
.as<DebuggerDebuggeeLink>();
}
void DebuggerDebuggeeLink::setLinkSlot(Debugger& dbg) {
setReservedSlot(DEBUGGER_LINK_SLOT, ObjectValue(*dbg.toJSObject()));
}
void DebuggerDebuggeeLink::clearLinkSlot() {
setReservedSlot(DEBUGGER_LINK_SLOT, UndefinedValue());
}
const JSClass DebuggerDebuggeeLink::class_ = {
"DebuggerDebuggeeLink", JSCLASS_HAS_RESERVED_SLOTS(RESERVED_SLOTS)};
/* static */
bool DebugAPI::handleBaselineOsr(JSContext* cx, InterpreterFrame* from,
jit::BaselineFrame* to) {
ScriptFrameIter iter(cx);
MOZ_ASSERT(iter.abstractFramePtr() == to);
return Debugger::replaceFrameGuts(cx, from, to, iter);
}
/* static */
bool DebugAPI::handleIonBailout(JSContext* cx, jit::RematerializedFrame* from,
jit::BaselineFrame* to) {
// When we return to a bailed-out Ion real frame, we must update all
// Debugger.Frames that refer to its inline frames. However, since we
// can't pop individual inline frames off the stack (we can only pop the
// real frame that contains them all, as a unit), we cannot assume that
// the frame we're dealing with is the top frame. Advance the iterator
// across any inlined frames younger than |to|, the baseline frame
// reconstructed during bailout from the Ion frame corresponding to
// |from|.
ScriptFrameIter iter(cx);
while (iter.abstractFramePtr() != to) {
++iter;
}
return Debugger::replaceFrameGuts(cx, from, to, iter);
}
/* static */
void DebugAPI::handleUnrecoverableIonBailoutError(
JSContext* cx, jit::RematerializedFrame* frame) {
// Ion bailout can fail due to overrecursion. In such cases we cannot
// honor any further Debugger hooks on the frame, and need to ensure that
// its Debugger.Frame entry is cleaned up.
Debugger::terminateDebuggerFrames(cx, frame);
}
/*** JS::dbg::Builder *******************************************************/
Builder::Builder(JSContext* cx, js::Debugger* debugger)
: debuggerObject(cx, debugger->toJSObject().get()), debugger(debugger) {}
#if DEBUG
void Builder::assertBuilt(JSObject* obj) {
// We can't use assertSameCompartment here, because that is always keyed to
// some JSContext's current compartment, whereas BuiltThings can be
// constructed and assigned to without respect to any particular context;
// the only constraint is that they should be in their debugger's compartment.
MOZ_ASSERT_IF(obj, debuggerObject->compartment() == obj->compartment());
}
#endif
bool Builder::Object::definePropertyToTrusted(JSContext* cx, const char* name,
JS::MutableHandleValue trusted) {
// We should have checked for false Objects before calling this.
MOZ_ASSERT(value);
JSAtom* atom = Atomize(cx, name, strlen(name));
if (!atom) {
return false;
}
RootedId id(cx, AtomToId(atom));
return DefineDataProperty(cx, value, id, trusted);
}
bool Builder::Object::defineProperty(JSContext* cx, const char* name,
JS::HandleValue propval_) {
AutoRealm ar(cx, debuggerObject());
RootedValue propval(cx, propval_);
if (!debugger()->wrapDebuggeeValue(cx, &propval)) {
return false;
}
return definePropertyToTrusted(cx, name, &propval);
}
bool Builder::Object::defineProperty(JSContext* cx, const char* name,
JS::HandleObject propval_) {
RootedValue propval(cx, ObjectOrNullValue(propval_));
return defineProperty(cx, name, propval);
}
bool Builder::Object::defineProperty(JSContext* cx, const char* name,
Builder::Object& propval_) {
AutoRealm ar(cx, debuggerObject());
RootedValue propval(cx, ObjectOrNullValue(propval_.value));
return definePropertyToTrusted(cx, name, &propval);
}
Builder::Object Builder::newObject(JSContext* cx) {
AutoRealm ar(cx, debuggerObject);
Rooted<PlainObject*> obj(cx, NewPlainObject(cx));
// If the allocation failed, this will return a false Object, as the spec
// promises.
return Object(cx, *this, obj);
}
/*** JS::dbg::AutoEntryMonitor **********************************************/
AutoEntryMonitor::AutoEntryMonitor(JSContext* cx)
: cx_(cx), savedMonitor_(cx->entryMonitor) {
cx->entryMonitor = this;
}
AutoEntryMonitor::~AutoEntryMonitor() { cx_->entryMonitor = savedMonitor_; }
/*** Glue *******************************************************************/
extern JS_PUBLIC_API bool JS_DefineDebuggerObject(JSContext* cx,
HandleObject obj) {
Rooted<NativeObject*> debugCtor(cx), debugProto(cx), frameProto(cx),
scriptProto(cx), sourceProto(cx), objectProto(cx), envProto(cx),
memoryProto(cx);
RootedObject debuggeeWouldRunProto(cx);
RootedValue debuggeeWouldRunCtor(cx);
Handle<GlobalObject*> global = obj.as<GlobalObject>();
debugProto = InitClass(cx, global, &DebuggerPrototypeObject::class_, nullptr,
"Debugger", Debugger::construct, 1,
Debugger::properties, Debugger::methods, nullptr,
Debugger::static_methods, debugCtor.address());
if (!debugProto) {
return false;
}
frameProto = DebuggerFrame::initClass(cx, global, debugCtor);
if (!frameProto) {
return false;
}
scriptProto = DebuggerScript::initClass(cx, global, debugCtor);
if (!scriptProto) {
return false;
}
sourceProto = DebuggerSource::initClass(cx, global, debugCtor);
if (!sourceProto) {
return false;
}
objectProto = DebuggerObject::initClass(cx, global, debugCtor);
if (!objectProto) {
return false;
}
envProto = DebuggerEnvironment::initClass(cx, global, debugCtor);
if (!envProto) {
return false;
}
memoryProto = InitClass(
cx, debugCtor, nullptr, nullptr, "Memory", DebuggerMemory::construct, 0,
DebuggerMemory::properties, DebuggerMemory::methods, nullptr, nullptr);
if (!memoryProto) {
return false;
}
debuggeeWouldRunProto = GlobalObject::getOrCreateCustomErrorPrototype(
cx, global, JSEXN_DEBUGGEEWOULDRUN);
if (!debuggeeWouldRunProto) {
return false;
}
debuggeeWouldRunCtor =
ObjectValue(global->getConstructor(JSProto_DebuggeeWouldRun));
RootedId debuggeeWouldRunId(
cx, NameToId(ClassName(JSProto_DebuggeeWouldRun, cx)));
if (!DefineDataProperty(cx, debugCtor, debuggeeWouldRunId,
debuggeeWouldRunCtor, 0)) {
return false;
}
debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_FRAME_PROTO,
ObjectValue(*frameProto));
debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_OBJECT_PROTO,
ObjectValue(*objectProto));
debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_SCRIPT_PROTO,
ObjectValue(*scriptProto));
debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_SOURCE_PROTO,
ObjectValue(*sourceProto));
debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_ENV_PROTO,
ObjectValue(*envProto));
debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_MEMORY_PROTO,
ObjectValue(*memoryProto));
return true;
}
JS_PUBLIC_API bool JS::dbg::IsDebugger(JSObject& obj) {
/* We only care about debugger objects, so CheckedUnwrapStatic is OK. */
JSObject* unwrapped = CheckedUnwrapStatic(&obj);
if (!unwrapped || !unwrapped->is<DebuggerInstanceObject>()) {
return false;
}
MOZ_ASSERT(js::Debugger::fromJSObject(unwrapped));
return true;
}
JS_PUBLIC_API bool JS::dbg::GetDebuggeeGlobals(
JSContext* cx, JSObject& dbgObj, MutableHandleObjectVector vector) {
MOZ_ASSERT(IsDebugger(dbgObj));
/* Since we know we have a debugger object, CheckedUnwrapStatic is fine. */
js::Debugger* dbg = js::Debugger::fromJSObject(CheckedUnwrapStatic(&dbgObj));
if (!vector.reserve(vector.length() + dbg->debuggees.count())) {
JS_ReportOutOfMemory(cx);
return false;
}
for (WeakGlobalObjectSet::Range r = dbg->allDebuggees(); !r.empty();
r.popFront()) {
vector.infallibleAppend(static_cast<JSObject*>(r.front()));
}
return true;
}
#ifdef DEBUG
/* static */
bool Debugger::isDebuggerCrossCompartmentEdge(JSObject* obj,
const gc::Cell* target) {
MOZ_ASSERT(target);
const gc::Cell* referent = nullptr;
if (obj->is<DebuggerScript>()) {
referent = obj->as<DebuggerScript>().getReferentCell();
} else if (obj->is<DebuggerSource>()) {
referent = obj->as<DebuggerSource>().getReferentRawObject();
} else if (obj->is<DebuggerObject>()) {
referent = obj->as<DebuggerObject>().referent();
} else if (obj->is<DebuggerEnvironment>()) {
referent = obj->as<DebuggerEnvironment>().referent();
}
return referent == target;
}
static void CheckDebuggeeThingRealm(Realm* realm, bool invisibleOk) {
MOZ_ASSERT_IF(!invisibleOk, !realm->creationOptions().invisibleToDebugger());
}
void js::CheckDebuggeeThing(BaseScript* script, bool invisibleOk) {
CheckDebuggeeThingRealm(script->realm(), invisibleOk);
}
void js::CheckDebuggeeThing(JSObject* obj, bool invisibleOk) {
if (Realm* realm = JS::GetObjectRealmOrNull(obj)) {
CheckDebuggeeThingRealm(realm, invisibleOk);
}
}
#endif // DEBUG
/*** JS::dbg::GarbageCollectionEvent ****************************************/
namespace JS {
namespace dbg {
/* static */ GarbageCollectionEvent::Ptr GarbageCollectionEvent::Create(
JSRuntime* rt, ::js::gcstats::Statistics& stats, uint64_t gcNumber) {
auto data = MakeUnique<GarbageCollectionEvent>(gcNumber);
if (!data) {
return nullptr;
}
data->nonincrementalReason = stats.nonincrementalReason();
for (auto& slice : stats.slices()) {
if (!data->reason) {
// There is only one GC reason for the whole cycle, but for legacy
// reasons this data is stored and replicated on each slice. Each
// slice used to have its own GCReason, but now they are all the
// same.
data->reason = ExplainGCReason(slice.reason);
MOZ_ASSERT(data->reason);
}
if (!data->collections.growBy(1)) {
return nullptr;
}
data->collections.back().startTimestamp = slice.start;
data->collections.back().endTimestamp = slice.end;
}
return data;
}
static bool DefineStringProperty(JSContext* cx, HandleObject obj,
PropertyName* propName, const char* strVal) {
RootedValue val(cx, UndefinedValue());
if (strVal) {
JSAtom* atomized = Atomize(cx, strVal, strlen(strVal));
if (!atomized) {
return false;
}
val = StringValue(atomized);
}
return DefineDataProperty(cx, obj, propName, val);
}
JSObject* GarbageCollectionEvent::toJSObject(JSContext* cx) const {
RootedObject obj(cx, NewPlainObject(cx));
RootedValue gcCycleNumberVal(cx, NumberValue(majorGCNumber_));
if (!obj ||
!DefineStringProperty(cx, obj, cx->names().nonincrementalReason,
nonincrementalReason) ||
!DefineStringProperty(cx, obj, cx->names().reason, reason) ||
!DefineDataProperty(cx, obj, cx->names().gcCycleNumber,
gcCycleNumberVal)) {
return nullptr;
}
Rooted<ArrayObject*> slicesArray(cx, NewDenseEmptyArray(cx));
if (!slicesArray) {
return nullptr;
}
TimeStamp originTime = TimeStamp::ProcessCreation();
size_t idx = 0;
for (auto range = collections.all(); !range.empty(); range.popFront()) {
Rooted<PlainObject*> collectionObj(cx, NewPlainObject(cx));
if (!collectionObj) {
return nullptr;
}
RootedValue start(cx), end(cx);
start = NumberValue(
(range.front().startTimestamp - originTime).ToMilliseconds());
end =
NumberValue((range.front().endTimestamp - originTime).ToMilliseconds());
if (!DefineDataProperty(cx, collectionObj, cx->names().startTimestamp,
start) ||
!DefineDataProperty(cx, collectionObj, cx->names().endTimestamp, end)) {
return nullptr;
}
RootedValue collectionVal(cx, ObjectValue(*collectionObj));
if (!DefineDataElement(cx, slicesArray, idx++, collectionVal)) {
return nullptr;
}
}
RootedValue slicesValue(cx, ObjectValue(*slicesArray));
if (!DefineDataProperty(cx, obj, cx->names().collections, slicesValue)) {
return nullptr;
}
return obj;
}
JS_PUBLIC_API bool FireOnGarbageCollectionHookRequired(JSContext* cx) {
AutoCheckCannotGC noGC;
for (auto& dbg : cx->runtime()->onGarbageCollectionWatchers()) {
MOZ_ASSERT(dbg.getHook(Debugger::OnGarbageCollection));
if (dbg.observedGC(cx->runtime()->gc.majorGCCount())) {
return true;
}
}
return false;
}
JS_PUBLIC_API bool FireOnGarbageCollectionHook(
JSContext* cx, JS::dbg::GarbageCollectionEvent::Ptr&& data) {
RootedObjectVector triggered(cx);
{
// We had better not GC (and potentially get a dangling Debugger
// pointer) while finding all Debuggers observing a debuggee that
// participated in this GC.
AutoCheckCannotGC noGC;
for (auto& dbg : cx->runtime()->onGarbageCollectionWatchers()) {
MOZ_ASSERT(dbg.getHook(Debugger::OnGarbageCollection));
if (dbg.observedGC(data->majorGCNumber())) {
if (!triggered.append(dbg.object)) {
JS_ReportOutOfMemory(cx);
return false;
}
}
}
}
for (; !triggered.empty(); triggered.popBack()) {
Debugger* dbg = Debugger::fromJSObject(triggered.back());
if (dbg->getHook(Debugger::OnGarbageCollection)) {
(void)dbg->enterDebuggerHook(cx, [&]() -> bool {
return dbg->fireOnGarbageCollectionHook(cx, data);
});
MOZ_ASSERT(!cx->isExceptionPending());
}
}
return true;
}
} // namespace dbg
} // namespace JS