Source code

Revision control

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/GuardObjects.h" // for MOZ_GUARD_OBJECT_NOTIFIER_PARAM
#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 <utility> // for std::move
#include "jsapi.h" // for CallArgs, CallArgsFromVp
#include "jsfriendapi.h" // for GetErrorMessage
#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/NameAnalysisTypes.h" // for ParseGoal, ParseGoal::Script
#include "frontend/ParseContext.h" // for UsedNameTracker
#include "frontend/Parser.h" // for Parser
#include "gc/Barrier.h" // for GCPtrNativeObject
#include "gc/FreeOp.h" // for JSFreeOp
#include "gc/GC.h" // for IterateLazyScripts
#include "gc/GCMarker.h" // for GCMarker
#include "gc/GCRuntime.h" // for GCRuntime, AutoEnterIteration
#include "gc/HashUtil.h" // for DependentAddPtr
#include "gc/Marking.h" // for IsMarkedUnbarriered, IsMarked
#include "gc/PublicIterators.h" // for RealmsIter, CompartmentsIter
#include "gc/Rooting.h" // for RootedNativeObject
#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/Ion.h" // for JitContext
#include "jit/JitScript.h" // for JitScript
#include "jit/JSJitFrameIter.h" // for InlineFrameIterator
#include "jit/RematerializedFrame.h" // for RematerializedFrame
#include "js/Conversions.h" // for ToBoolean, ToUint32
#include "js/Debug.h" // for Builder::Object, Builder
#include "js/GCAPI.h" // for GarbageCollectionEvent
#include "js/HeapAPI.h" // for ExposeObjectToActiveJS
#include "js/Promise.h" // for AutoDebuggerJobQueueInterruption
#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/Warnings.h" // for AutoSuppressWarningReporter
#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/ObjectGroup.h" // for TenuredObject
#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/TraceLogging.h" // for TraceLoggerForCurrentThread
#include "vm/TypeInference.h" // for TypeZone
#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/WasmTypes.h" // for WasmInstanceObjectVector
#include "debugger/DebugAPI-inl.h"
#include "debugger/Frame-inl.h" // for DebuggerFrame::hasGeneratorInfo
#include "debugger/Script-inl.h" // for DebuggerScript::getReferent
#include "gc/GC-inl.h" // for ZoneCellIter
#include "gc/Marking-inl.h" // for MaybeForwarded
#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
#include "vm/TypeInference-inl.h" // for AutoEnterAnalysis
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);
}
bool js::ValueToIdentifier(JSContext* cx, HandleValue v, MutableHandleId id) {
if (!ValueToId<CanGC>(cx, v, id)) {
return false;
}
if (!JSID_IS_ATOM(id) || !IsIdentifier(JSID_TO_ATOM(id))) {
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 inline void NukeDebuggerWrapper(NativeObject* wrapper) {
// In some OOM failure cases, we need to destroy the edge to the referent,
// to avoid trying to trace it during untimely collections.
wrapper->setPrivate(nullptr);
}
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);
}
static MOZ_MUST_USE bool AdjustGeneratorResumptionValue(JSContext* cx,
AbstractFramePtr frame,
ResumeMode& resumeMode,
MutableHandleValue vp);
static MOZ_MUST_USE bool ApplyFrameResumeMode(JSContext* cx,
AbstractFramePtr frame,
ResumeMode resumeMode,
HandleValue rv,
HandleSavedFrame exnStack) {
RootedValue rval(cx, rv);
// The value passed in here is unwrapped had has no guarantees about what
// compartment it may be associated with, so we explcitly 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->setPendingExceptionAndCaptureStack(rval);
}
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) {
RootedSavedFrame 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;
}
RootedLinearString 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_EncodeStringToLatin1(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);
}
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(JSFreeOp* fop) {
while (!breakpoints.isEmpty()) {
breakpoints.begin()->delete_(fop);
}
}
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_(JSFreeOp* fop) {
debugger->breakpoints.remove(this);
site->breakpoints.remove(this);
gc::Cell* cell = site->owningCell();
fop->delete_(cell, this, MemoryUse::Breakpoint);
}
void Breakpoint::remove(JSFreeOp* fop) {
BreakpointSite* savedSite = site;
delete_(fop);
savedSite->destroyIfEmpty(fop);
}
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(JSFreeOp* fop) {
DebugScript::destroyBreakpointSite(fop, script, pc);
}
void JSBreakpointSite::trace(JSTracer* trc) {
BreakpointSite::trace(trc);
TraceEdge(trc, &script, "breakpoint script");
}
void JSBreakpointSite::delete_(JSFreeOp* fop) {
BreakpointSite::finalize(fop);
fop->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(JSFreeOp* fop) {
instanceObject->instance().destroyBreakpointSite(fop, offset);
}
void WasmBreakpointSite::delete_(JSFreeOp* fop) {
BreakpointSite::finalize(fop);
fop->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),
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),
#ifdef NIGHTLY_BUILD
traceLoggerLastDrainedSize(0),
traceLoggerLastDrainedIteration(0),
#endif
traceLoggerScriptedCallsLastDrainedSize(0),
traceLoggerScriptedCallsLastDrainedIteration(0) {
cx->check(dbg);
#ifdef JS_TRACE_LOGGING
TraceLoggerThread* logger = TraceLoggerForCurrentThread(cx);
if (logger) {
# ifdef NIGHTLY_BUILD
logger->getIterationAndSize(&traceLoggerLastDrainedIteration,
&traceLoggerLastDrainedSize);
# endif
logger->getIterationAndSize(&traceLoggerScriptedCallsLastDrainedIteration,
&traceLoggerScriptedCallsLastDrainedSize);
}
#endif
cx->runtime()->debuggerList().insertBack(this);
}
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();
if (onNewGlobalObjectWatchersLink.mPrev ||
onNewGlobalObjectWatchersLink.mNext ||
cx->runtime()->onNewGlobalObjectWatchers().begin() ==
JSRuntime::WatchersList::Iterator(this)) {
cx->runtime()->onNewGlobalObjectWatchers().remove(this);
}
}
static_assert(unsigned(DebuggerFrame::OWNER_SLOT) ==
unsigned(DebuggerScript::OWNER_SLOT));
static_assert(unsigned(DebuggerFrame::OWNER_SLOT) ==
unsigned(DebuggerSource::OWNER_SLOT));
static_assert(unsigned(DebuggerFrame::OWNER_SLOT) ==
unsigned(JSSLOT_DEBUGOBJECT_OWNER));
static_assert(unsigned(DebuggerFrame::OWNER_SLOT) ==
unsigned(DebuggerEnvironment::OWNER_SLOT));
#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
/* static */
Debugger* Debugger::fromChildJSObject(JSObject* obj) {
MOZ_ASSERT(isChildJSObject(obj));
JSObject* dbgobj = &obj->as<NativeObject>()
.getReservedSlot(JSSLOT_DEBUGOBJECT_OWNER)
.toObject();
return fromJSObject(dbgobj);
}
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) {
RootedDebuggerFrame result(cx);
if (!Debugger::getFrame(cx, iter, &result)) {
return false;
}
vp.setObject(*result);
return true;
}
bool Debugger::getFrame(JSContext* cx, MutableHandleDebuggerFrame result) {
RootedObject proto(
cx, &object->getReservedSlot(JSSLOT_DEBUG_FRAME_PROTO).toObject());
RootedNativeObject debugger(cx, object);
// Since there is no frame/generator data to associate with this frame, this
// will create a new, "terminated" Debugger.Frame object.
RootedDebuggerFrame 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,
MutableHandleDebuggerFrame result) {
AbstractFramePtr referent = iter.abstractFramePtr();
MOZ_ASSERT_IF(referent.hasScript(), !referent.script()->selfHosted());
if (referent.hasScript() &&
!referent.script()->ensureHasAnalyzedArgsUsage(cx)) {
return false;
}
FrameMap::AddPtr p = frames.lookupForAdd(referent);
if (!p) {
Rooted<AbstractGeneratorObject*> genObj(cx);
if (referent.isGeneratorFrame()) {
AutoRealm ar(cx, referent.callee());
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());
RootedNativeObject debugger(cx, object);
RootedDebuggerFrame frame(
cx, DebuggerFrame::create(cx, proto, debugger, &iter, genObj));
if (!frame) {
return false;
}
auto terminateDebuggerFrameGuard = MakeScopeExit([&] {
terminateDebuggerFrame(cx->defaultFreeOp(), 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,
MutableHandleDebuggerFrame 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());
RootedNativeObject 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->runtime()->defaultFreeOp(), 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()) {
// 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::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();
for (Realm::DebuggerVectorEntry& entry : global->getDebuggers()) {
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 + 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);
for (Realm::DebuggerVectorEntry& entry : frame.global()->getDebuggers()) {
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
// 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.
//
// 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()) {
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;
}
// 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->setPendingExceptionAndCaptureStack(rval);
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* asyncGenObj = &genObj->as<AsyncGeneratorObject>();
asyncGenState_ = asyncGenObj->state();
asyncGenObj->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,
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, Debugger::DebuggerFrameVector(cx));
if (!Debugger::getDebuggerFrames(frame, &frames)) {
return false;
}
if (frames.empty()) {
return frameOk;
}
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++) {
HandleDebuggerFrame frameobj = frames[i];
Debugger* dbg = Debugger::fromChildJSObject(frameobj);
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);
RootedSavedFrame 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;
Debugger::forEachOnStackDebuggerFrame(
frame, [&](Debugger* dbg, DebuggerFrame* frameObjPtr) {
if (!ok) {
return;
}
RootedDebuggerFrame frameObj(cx, frameObjPtr);
AutoRealm ar(cx, frameObj);
if (!frameObj->setGeneratorInfo(cx, 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);
RootedSavedFrame 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;
}
RootedDebuggerEnvironment envobj(cx);
if (!wrapEnvironment(cx, env, &envobj)) {
return false;
}
rval.setObject(*envobj);
return true;
}
bool Debugger::wrapEnvironment(JSContext* cx, Handle<Env*> env,
MutableHandleDebuggerEnvironment 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());
RootedNativeObject debugger(cx, object);
RootedDebuggerEnvironment envobj(
cx, DebuggerEnvironment::create(cx, proto, env, debugger));
if (!envobj) {
return false;
}
if (!p.add(cx, environments, env, envobj)) {
NukeDebuggerWrapper(envobj);
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());
RootedDebuggerObject dobj(cx);
if (!wrapDebuggeeObject(cx, obj, &dobj)) {
return false;
}
vp.setObject(*dobj);
} else if (vp.isMagic()) {
RootedPlainObject optObj(cx, NewBuiltinClassInstance<PlainObject>(cx));
if (!optObj) {
return false;
}
// We handle three sentinel values: missing arguments (overloading
// JS_OPTIMIZED_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_OPTIMIZED_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,
MutableHandleDebuggerObject result) {
if (!obj) {
result.set(nullptr);
return true;
}
return wrapDebuggeeObject(cx, obj, result);
}
bool Debugger::wrapDebuggeeObject(JSContext* cx, HandleObject obj,
MutableHandleDebuggerObject 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.
RootedNativeObject debugger(cx, object);
RootedObject proto(
cx, &object->getReservedSlot(JSSLOT_DEBUG_OBJECT_PROTO).toObject());
RootedDebuggerObject dobj(cx,
DebuggerObject::create(cx, proto, obj, debugger));
if (!dobj) {
return false;
}
if (!p.add(cx, objects, obj, dobj)) {
NukeDebuggerWrapper(dobj);
return false;
}
result.set(dobj);
}
return true;
}
static NativeObject* ToNativeDebuggerObject(JSContext* cx,
MutableHandleObject obj) {
if (obj->getClass() != &DebuggerObject::class_) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_NOT_EXPECTED_TYPE, "Debugger",
"Debugger.Object", obj->getClass()->name);
return nullptr;
}
NativeObject* ndobj = &obj->as<NativeObject>();
Value owner = ndobj->getReservedSlot(JSSLOT_DEBUGOBJECT_OWNER);
if (owner.isUndefined()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_PROTO,
"Debugger.Object", "Debugger.Object");
return nullptr;
}
return ndobj;
}
bool Debugger::unwrapDebuggeeObject(JSContext* cx, MutableHandleObject obj) {
NativeObject* ndobj = ToNativeDebuggerObject(cx, obj);
if (!ndobj) {
return false;
}
Value owner = ndobj->getReservedSlot(JSSLOT_DEBUGOBJECT_OWNER);
if (&owner.toObject() != object) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEBUG_WRONG_OWNER, "Debugger.Object");
return false;
}
obj.set(static_cast<JSObject*>(ndobj->getPrivate()));
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.hasGetterObject()) {
RootedObject get(cx, desc.getterObject());
if (get) {
if (!unwrapDebuggeeObject(cx, &get)) {
return false;
}
if (!CheckArgCompartment(cx, obj, get, "defineProperty", "get")) {
return false;
}
}
desc.setGetterObject(get);
}
if (desc.hasSetterObject()) {
RootedObject set(cx, desc.setterObject());
if (set) {
if (!unwrapDebuggeeObject(cx, &set)) {
return false;
}
if (!CheckArgCompartment(cx, obj, set, "defineProperty", "set")) {
return false;
}
}
desc.setSetterObject(set);
}
return true;
}
/*** Debuggee resumption values and debugger error handling *****************/
static bool GetResumptionProperty(JSContext* cx, HandleObject obj,
HandlePropertyName 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,
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.
static MOZ_MUST_USE bool AdjustGeneratorResumptionValue(JSContext* cx,
AbstractFramePtr frame,
ResumeMode& resumeMode,
MutableHandleValue vp) {
if (resumeMode != ResumeMode::Return && resumeMode != ResumeMode::Throw) {
return true;
}
if (!frame || !frame.isFunctionFrame()) {
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.
if (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();
// 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 (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*> asyncGenObj(
cx, &genObj->as<AsyncFunctionGeneratorObject>());
// 1. `return <value>` fulfills and returns the async function's promise.
Rooted<PromiseObject*> promise(cx, asyncGenObj->promise());
if (promise->state() == JS::PromiseState::Pending) {
if (!AsyncFunctionResolve(cx, asyncGenObj, vp,
AsyncFunctionResolveKind::Fulfill)) {
return false;
}
}
vp.setObject(*promise);
// 2. The generator must be closed.
asyncGenObj->setClosed();
} 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,
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,
jsbytecode* pc, ResumeMode& resumeMode,
MutableHandleValue vp) {
return unwrapDebuggeeValue(cx, vp) &&
CheckResumptionValue(cx, frame, pc, resumeMode, vp);
}