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/BytecodeCompiler.h" // for IsIdentifier
#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/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/JSAtom.h" // for Atomize, 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/JSAtom-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 js::frontend::IsIdentifier;
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));
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),
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::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) {
// "onNativeCall" only works consistently in the context of an explicit eval
// (or a function call via DebuggerObject.call/apply) that has set the
// "insideDebuggerEvaluationWithOnNativeCallHook" state
// on the JSContext, so we fast-path this hook to bail right away if that is
// not currently set. If this flag is set to a _different_ debugger, the
// standard "isHookCallAllowed" debugger logic will apply and only hooks on
// that debugger will be callable.
if (!cx->insideDebuggerEvaluationWithOnNativeCallHook) {
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;
}