diff options
Diffstat (limited to 'js/src/vm/Debugger.cpp')
-rw-r--r-- | js/src/vm/Debugger.cpp | 11183 |
1 files changed, 11183 insertions, 0 deletions
diff --git a/js/src/vm/Debugger.cpp b/js/src/vm/Debugger.cpp new file mode 100644 index 000000000..b6bc7d62a --- /dev/null +++ b/js/src/vm/Debugger.cpp @@ -0,0 +1,11183 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "vm/Debugger-inl.h" + +#include "mozilla/DebugOnly.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Sprintf.h" +#include "mozilla/TypeTraits.h" + +#include "jscntxt.h" +#include "jscompartment.h" +#include "jsfriendapi.h" +#include "jshashutil.h" +#include "jsnum.h" +#include "jsobj.h" +#include "jsprf.h" +#include "jswrapper.h" + +#include "frontend/BytecodeCompiler.h" +#include "frontend/Parser.h" +#include "gc/Marking.h" +#include "gc/Policy.h" +#include "jit/BaselineDebugModeOSR.h" +#include "jit/BaselineJIT.h" +#include "js/Date.h" +#include "js/GCAPI.h" +#include "js/UbiNodeBreadthFirst.h" +#include "js/Vector.h" +#include "proxy/ScriptedProxyHandler.h" +#include "vm/ArgumentsObject.h" +#include "vm/AsyncFunction.h" +#include "vm/DebuggerMemory.h" +#include "vm/GeneratorObject.h" +#include "vm/SPSProfiler.h" +#include "vm/TraceLogging.h" +#include "vm/WrapperObject.h" +#include "wasm/WasmInstance.h" + +#include "jsgcinlines.h" +#include "jsobjinlines.h" +#include "jsopcodeinlines.h" +#include "jsscriptinlines.h" + +#include "vm/NativeObject-inl.h" +#include "vm/Stack-inl.h" + +using namespace js; + +using JS::dbg::AutoEntryMonitor; +using JS::dbg::Builder; +using js::frontend::IsIdentifier; +using mozilla::ArrayLength; +using mozilla::DebugOnly; +using mozilla::MakeScopeExit; +using mozilla::Maybe; +using mozilla::Some; +using mozilla::Nothing; +using mozilla::Variant; +using mozilla::AsVariant; + + +/*** Forward declarations, ClassOps and Classes **************************************************/ + +static void DebuggerFrame_finalize(FreeOp* fop, JSObject* obj); +static void DebuggerEnv_trace(JSTracer* trc, JSObject* obj); +static void DebuggerObject_trace(JSTracer* trc, JSObject* obj); +static void DebuggerScript_trace(JSTracer* trc, JSObject* obj); +static void DebuggerSource_trace(JSTracer* trc, JSObject* obj); + +enum { + JSSLOT_DEBUGFRAME_OWNER, + JSSLOT_DEBUGFRAME_ARGUMENTS, + JSSLOT_DEBUGFRAME_ONSTEP_HANDLER, + JSSLOT_DEBUGFRAME_ONPOP_HANDLER, + JSSLOT_DEBUGFRAME_COUNT +}; + +const ClassOps DebuggerFrame::classOps_ = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + DebuggerFrame_finalize, + nullptr, /* call */ + nullptr, /* hasInstance */ + nullptr, /* construct */ + nullptr, /* trace */ +}; + +const Class DebuggerFrame::class_ = { + "Frame", + JSCLASS_HAS_PRIVATE | + JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUGFRAME_COUNT) | + JSCLASS_BACKGROUND_FINALIZE, + &DebuggerFrame::classOps_ +}; + +enum { + JSSLOT_DEBUGARGUMENTS_FRAME, + JSSLOT_DEBUGARGUMENTS_COUNT +}; + +static const Class DebuggerArguments_class = { + "Arguments", + JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUGARGUMENTS_COUNT) +}; + +const ClassOps DebuggerEnvironment::classOps_ = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + nullptr, /* finalize */ + nullptr, /* call */ + nullptr, /* hasInstance */ + nullptr, /* construct */ + DebuggerEnv_trace +}; + +const Class DebuggerEnvironment::class_ = { + "Environment", + JSCLASS_HAS_PRIVATE | + JSCLASS_HAS_RESERVED_SLOTS(DebuggerEnvironment::RESERVED_SLOTS), + &classOps_ +}; + +enum { + JSSLOT_DEBUGOBJECT_OWNER, + JSSLOT_DEBUGOBJECT_COUNT +}; + +const ClassOps DebuggerObject::classOps_ = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + nullptr, /* finalize */ + nullptr, /* call */ + nullptr, /* hasInstance */ + nullptr, /* construct */ + DebuggerObject_trace +}; + +const Class DebuggerObject::class_ = { + "Object", + JSCLASS_HAS_PRIVATE | + JSCLASS_HAS_RESERVED_SLOTS(RESERVED_SLOTS), + &classOps_ +}; + +enum { + JSSLOT_DEBUGSCRIPT_OWNER, + JSSLOT_DEBUGSCRIPT_COUNT +}; + +static const ClassOps DebuggerScript_classOps = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + nullptr, /* finalize */ + nullptr, /* call */ + nullptr, /* hasInstance */ + nullptr, /* construct */ + DebuggerScript_trace +}; + +static const Class DebuggerScript_class = { + "Script", + JSCLASS_HAS_PRIVATE | + JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUGSCRIPT_COUNT), + &DebuggerScript_classOps +}; + +enum { + JSSLOT_DEBUGSOURCE_OWNER, + JSSLOT_DEBUGSOURCE_TEXT, + JSSLOT_DEBUGSOURCE_COUNT +}; + +static const ClassOps DebuggerSource_classOps = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + nullptr, /* finalize */ + nullptr, /* call */ + nullptr, /* hasInstance */ + nullptr, /* construct */ + DebuggerSource_trace +}; + +static const Class DebuggerSource_class = { + "Source", + JSCLASS_HAS_PRIVATE | + JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUGSOURCE_COUNT), + &DebuggerSource_classOps +}; + + +/*** Utils ***************************************************************************************/ + +static inline bool +EnsureFunctionHasScript(JSContext* cx, HandleFunction fun) +{ + if (fun->isInterpretedLazy()) { + AutoCompartment ac(cx, fun); + return !!fun->getOrCreateScript(cx); + } + return true; +} + +static inline JSScript* +GetOrCreateFunctionScript(JSContext* cx, HandleFunction fun) +{ + MOZ_ASSERT(fun->isInterpreted()); + if (!EnsureFunctionHasScript(cx, fun)) + return nullptr; + return fun->nonLazyScript(); +} + +static bool +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); + ReportValueErrorFlags(cx, JSREPORT_ERROR, JSMSG_UNEXPECTED_TYPE, + JSDVG_SEARCH_STACK, val, nullptr, "not an identifier", + nullptr); + return false; + } + return true; +} + +class AutoRestoreCompartmentDebugMode +{ + JSCompartment* comp_; + unsigned bits_; + + public: + explicit AutoRestoreCompartmentDebugMode(JSCompartment* comp) + : comp_(comp), bits_(comp->debugModeBits) + { + MOZ_ASSERT(comp_); + } + + ~AutoRestoreCompartmentDebugMode() { + if (comp_) + comp_->debugModeBits = bits_; + } + + void release() { + comp_ = nullptr; + } +}; + +// Given a Debugger instance dbg, if it is enabled, prevents all its debuggee +// compartments from executing scripts. Attempts to run script will throw an +// instance of Debugger.DebuggeeWouldRun from the topmost locked Debugger's +// compartment. +class MOZ_RAII js::EnterDebuggeeNoExecute +{ + friend class js::LeaveDebuggeeNoExecute; + + Debugger& dbg_; + EnterDebuggeeNoExecute** stack_; + EnterDebuggeeNoExecute* prev_; + + // Non-nullptr when unlocked temporarily by a LeaveDebuggeeNoExecute. + LeaveDebuggeeNoExecute* unlocked_; + + // When DebuggeeWouldRun is a warning instead of an error, whether we've + // reported a warning already. + bool reported_; + + public: + explicit EnterDebuggeeNoExecute(JSContext* cx, Debugger& dbg) + : dbg_(dbg), + unlocked_(nullptr), + reported_(false) + { + stack_ = &cx->runtime()->noExecuteDebuggerTop; + prev_ = *stack_; + *stack_ = this; + } + + ~EnterDebuggeeNoExecute() { + MOZ_ASSERT(*stack_ == this); + *stack_ = prev_; + } + + Debugger& debugger() const { + return dbg_; + } + +#ifdef DEBUG + static bool isLockedInStack(JSContext* cx, Debugger& dbg) { + JSRuntime* rt = cx->runtime(); + for (EnterDebuggeeNoExecute* it = rt->noExecuteDebuggerTop; it; it = it->prev_) { + if (&it->debugger() == &dbg) + return !it->unlocked_; + } + return false; + } +#endif + + // Given a JSContext entered into a debuggee compartment, find the lock + // that locks it. Returns nullptr if not found. + static EnterDebuggeeNoExecute* findInStack(JSContext* cx) { + JSRuntime* rt = cx->runtime(); + JSCompartment* debuggee = cx->compartment(); + for (EnterDebuggeeNoExecute* it = rt->noExecuteDebuggerTop; it; it = it->prev_) { + Debugger& dbg = it->debugger(); + if (!it->unlocked_ && dbg.isEnabled() && dbg.observesGlobal(debuggee->maybeGlobal())) + return it; + } + return nullptr; + } + + // Given a JSContext entered into a debuggee compartment, report a + // warning or an error if there is a lock that locks it. + static bool reportIfFoundInStack(JSContext* cx, HandleScript script) { + if (EnterDebuggeeNoExecute* nx = findInStack(cx)) { + bool warning = !cx->options().throwOnDebuggeeWouldRun(); + if (!warning || !nx->reported_) { + AutoCompartment ac(cx, nx->debugger().toJSObject()); + nx->reported_ = true; + if (cx->options().dumpStackOnDebuggeeWouldRun()) { + fprintf(stdout, "Dumping stack for DebuggeeWouldRun:\n"); + DumpBacktrace(cx); + } + const char* filename = script->filename() ? script->filename() : "(none)"; + char linenoStr[15]; + SprintfLiteral(linenoStr, "%" PRIuSIZE, script->lineno()); + unsigned flags = warning ? JSREPORT_WARNING : JSREPORT_ERROR; + // FIXME: filename should be UTF-8 (bug 987069). + return JS_ReportErrorFlagsAndNumberLatin1(cx, flags, GetErrorMessage, nullptr, + JSMSG_DEBUGGEE_WOULD_RUN, + filename, linenoStr); + } + } + return true; + } +}; + +// Given a JSContext entered into a debuggee compartment, if it is in +// an NX section, unlock the topmost EnterDebuggeeNoExecute instance. +// +// Does nothing if debuggee is not in an NX section. For example, this +// situation arises when invocation functions are called without entering +// Debugger code, e.g., calling D.O.p.executeInGlobal or D.O.p.apply. +class MOZ_RAII js::LeaveDebuggeeNoExecute +{ + EnterDebuggeeNoExecute* prevLocked_; + + public: + explicit LeaveDebuggeeNoExecute(JSContext* cx) + : prevLocked_(EnterDebuggeeNoExecute::findInStack(cx)) + { + if (prevLocked_) { + MOZ_ASSERT(!prevLocked_->unlocked_); + prevLocked_->unlocked_ = this; + } + } + + ~LeaveDebuggeeNoExecute() { + if (prevLocked_) { + MOZ_ASSERT(prevLocked_->unlocked_ == this); + prevLocked_->unlocked_ = nullptr; + } + } +}; + +/* static */ bool +Debugger::slowPathCheckNoExecute(JSContext* cx, HandleScript script) +{ + MOZ_ASSERT(cx->compartment()->isDebuggee()); + MOZ_ASSERT(cx->runtime()->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 bool +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; +} + +EvalOptions::~EvalOptions() +{ + js_free(const_cast<char*>(filename_)); +} + +bool +EvalOptions::setFilename(JSContext* cx, const char* filename) +{ + char* copy = nullptr; + if (filename) { + copy = JS_strdup(cx, filename); + if (!copy) + return false; + } + + // EvalOptions always owns filename_, so this cast is okay. + js_free(const_cast<char*>(filename_)); + + filename_ = copy; + return true; +} + +static bool +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; + JSAutoByteString url_bytes(cx, url_str); + if (!url_bytes) + return false; + if (!options.setFilename(cx, url_bytes.ptr())) + 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; +} + +static bool +RequireGlobalObject(JSContext* cx, HandleValue dbgobj, HandleObject referent) +{ + RootedObject obj(cx, referent); + + if (!obj->is<GlobalObject>()) { + const char* isWrapper = ""; + const char* isWindowProxy = ""; + + /* Help the poor programmer by pointing out wrappers around globals... */ + if (obj->is<WrapperObject>()) { + obj = js::UncheckedUnwrap(obj); + isWrapper = "a wrapper around "; + } + + /* ... and WindowProxies around Windows. */ + if (IsWindowProxy(obj)) { + obj = ToWindowIfWindowProxy(obj); + isWindowProxy = "a WindowProxy referring to "; + } + + if (obj->is<GlobalObject>()) { + ReportValueErrorFlags(cx, JSREPORT_ERROR, JSMSG_DEBUG_WRAPPER_IN_WAY, + JSDVG_SEARCH_STACK, dbgobj, nullptr, + isWrapper, isWindowProxy); + } else { + ReportValueErrorFlags(cx, JSREPORT_ERROR, JSMSG_DEBUG_BAD_REFERENT, + JSDVG_SEARCH_STACK, dbgobj, nullptr, + "a global object", nullptr); + } + return false; + } + + return true; +} + + +/*** Breakpoints *********************************************************************************/ + +BreakpointSite::BreakpointSite(JSScript* script, jsbytecode* pc) + : script(script), pc(pc), enabledCount(0) +{ + MOZ_ASSERT(!script->hasBreakpointsAt(pc)); + JS_INIT_CLIST(&breakpoints); +} + +void +BreakpointSite::recompile(FreeOp* fop) +{ + if (script->hasBaselineScript()) + script->baselineScript()->toggleDebugTraps(script, pc); +} + +void +BreakpointSite::inc(FreeOp* fop) +{ + enabledCount++; + if (enabledCount == 1) + recompile(fop); +} + +void +BreakpointSite::dec(FreeOp* fop) +{ + MOZ_ASSERT(enabledCount > 0); + enabledCount--; + if (enabledCount == 0) + recompile(fop); +} + +void +BreakpointSite::destroyIfEmpty(FreeOp* fop) +{ + if (JS_CLIST_IS_EMPTY(&breakpoints)) + script->destroyBreakpointSite(fop, pc); +} + +Breakpoint* +BreakpointSite::firstBreakpoint() const +{ + if (JS_CLIST_IS_EMPTY(&breakpoints)) + return nullptr; + return Breakpoint::fromSiteLinks(JS_NEXT_LINK(&breakpoints)); +} + +bool +BreakpointSite::hasBreakpoint(Breakpoint* bp) +{ + for (Breakpoint* p = firstBreakpoint(); p; p = p->nextInSite()) + if (p == bp) + return true; + return false; +} + +Breakpoint::Breakpoint(Debugger* debugger, BreakpointSite* site, JSObject* handler) + : debugger(debugger), site(site), handler(handler) +{ + MOZ_ASSERT(handler->compartment() == debugger->object->compartment()); + JS_APPEND_LINK(&debuggerLinks, &debugger->breakpoints); + JS_APPEND_LINK(&siteLinks, &site->breakpoints); +} + +Breakpoint* +Breakpoint::fromDebuggerLinks(JSCList* links) +{ + return (Breakpoint*) ((unsigned char*) links - offsetof(Breakpoint, debuggerLinks)); +} + +Breakpoint* +Breakpoint::fromSiteLinks(JSCList* links) +{ + return (Breakpoint*) ((unsigned char*) links - offsetof(Breakpoint, siteLinks)); +} + +void +Breakpoint::destroy(FreeOp* fop) +{ + if (debugger->enabled) + site->dec(fop); + JS_REMOVE_LINK(&debuggerLinks); + JS_REMOVE_LINK(&siteLinks); + site->destroyIfEmpty(fop); + fop->delete_(this); +} + +Breakpoint* +Breakpoint::nextInDebugger() +{ + JSCList* link = JS_NEXT_LINK(&debuggerLinks); + return (link == &debugger->breakpoints) ? nullptr : fromDebuggerLinks(link); +} + +Breakpoint* +Breakpoint::nextInSite() +{ + JSCList* link = JS_NEXT_LINK(&siteLinks); + return (link == &site->breakpoints) ? nullptr : fromSiteLinks(link); +} + + +/*** Debugger hook dispatch **********************************************************************/ + +Debugger::Debugger(JSContext* cx, NativeObject* dbg) + : object(dbg), + debuggees(cx->runtime()), + uncaughtExceptionHook(nullptr), + enabled(true), + allowUnobservedAsmJS(false), + collectCoverageInfo(false), + observedGCs(cx->runtime()), + allocationsLog(cx), + trackingAllocationSites(false), + allocationSamplingProbability(1.0), + maxAllocationsLogLength(DEFAULT_MAX_LOG_LENGTH), + allocationsLogOverflowed(false), + frames(cx->runtime()), + scripts(cx), + sources(cx), + objects(cx), + environments(cx), + wasmInstanceScripts(cx), + wasmInstanceSources(cx), +#ifdef NIGHTLY_BUILD + traceLoggerLastDrainedSize(0), + traceLoggerLastDrainedIteration(0), +#endif + traceLoggerScriptedCallsLastDrainedSize(0), + traceLoggerScriptedCallsLastDrainedIteration(0) +{ + assertSameCompartment(cx, dbg); + + JS_INIT_CLIST(&breakpoints); + JS_INIT_CLIST(&onNewGlobalObjectWatchersLink); + +#ifdef JS_TRACE_LOGGING + TraceLoggerThread* logger = TraceLoggerForMainThread(cx->runtime()); + if (logger) { +#ifdef NIGHTLY_BUILD + logger->getIterationAndSize(&traceLoggerLastDrainedIteration, &traceLoggerLastDrainedSize); +#endif + logger->getIterationAndSize(&traceLoggerScriptedCallsLastDrainedIteration, + &traceLoggerScriptedCallsLastDrainedSize); + } +#endif +} + +Debugger::~Debugger() +{ + MOZ_ASSERT_IF(debuggees.initialized(), debuggees.empty()); + allocationsLog.clear(); + + /* + * Since the inactive state for this link is a singleton cycle, it's always + * safe to apply JS_REMOVE_LINK to it, regardless of whether we're in the list or not. + * + * We don't have to worry about locking here since Debugger is not + * background finalized. + */ + JS_REMOVE_LINK(&onNewGlobalObjectWatchersLink); +} + +bool +Debugger::init(JSContext* cx) +{ + if (!debuggees.init() || + !debuggeeZones.init() || + !frames.init() || + !scripts.init() || + !sources.init() || + !objects.init() || + !observedGCs.init() || + !environments.init() || + !wasmInstanceScripts.init() || + !wasmInstanceSources.init()) + { + ReportOutOfMemory(cx); + return false; + } + + cx->runtime()->debuggerList.insertBack(this); + return true; +} + +JS_STATIC_ASSERT(unsigned(JSSLOT_DEBUGFRAME_OWNER) == unsigned(JSSLOT_DEBUGSCRIPT_OWNER)); +JS_STATIC_ASSERT(unsigned(JSSLOT_DEBUGFRAME_OWNER) == unsigned(JSSLOT_DEBUGSOURCE_OWNER)); +JS_STATIC_ASSERT(unsigned(JSSLOT_DEBUGFRAME_OWNER) == unsigned(JSSLOT_DEBUGOBJECT_OWNER)); +JS_STATIC_ASSERT(unsigned(JSSLOT_DEBUGFRAME_OWNER) == unsigned(DebuggerEnvironment::OWNER_SLOT)); + +/* static */ Debugger* +Debugger::fromChildJSObject(JSObject* obj) +{ + MOZ_ASSERT(obj->getClass() == &DebuggerFrame::class_ || + obj->getClass() == &DebuggerScript_class || + obj->getClass() == &DebuggerSource_class || + obj->getClass() == &DebuggerObject::class_ || + obj->getClass() == &DebuggerEnvironment::class_); + 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>(); +} + +bool +Debugger::getScriptFrameWithIter(JSContext* cx, AbstractFramePtr referent, + const ScriptFrameIter* maybeIter, MutableHandleValue vp) +{ + RootedDebuggerFrame result(cx); + if (!Debugger::getScriptFrameWithIter(cx, referent, maybeIter, &result)) + return false; + + vp.setObject(*result); + return true; +} + +bool +Debugger::getScriptFrameWithIter(JSContext* cx, AbstractFramePtr referent, + const ScriptFrameIter* maybeIter, + MutableHandleDebuggerFrame result) +{ + MOZ_ASSERT_IF(maybeIter, maybeIter->abstractFramePtr() == referent); + MOZ_ASSERT(!referent.script()->selfHosted()); + + if (!referent.script()->ensureHasAnalyzedArgsUsage(cx)) + return false; + + FrameMap::AddPtr p = frames.lookupForAdd(referent); + if (!p) { + /* 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, referent, maybeIter, + debugger)); + if (!frame) + return false; + + if (!ensureExecutionObservabilityOfFrame(cx, referent)) + return false; + + if (!frames.add(p, referent, frame)) { + ReportOutOfMemory(cx); + return false; + } + } + + result.set(&p->value()->as<DebuggerFrame>()); + return true; +} + +/* static */ bool +Debugger::hasLiveHook(GlobalObject* global, Hook which) +{ + if (GlobalObject::DebuggerVector* debuggers = global->getDebuggers()) { + for (auto p = debuggers->begin(); p != debuggers->end(); p++) { + Debugger* dbg = *p; + if (dbg->enabled && dbg->getHook(which)) + return true; + } + } + return false; +} + +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(JSRuntime* rt) const +{ + if (!enabled) + return false; + + if (getHook(OnDebuggerStatement) || + getHook(OnExceptionUnwind) || + getHook(OnNewScript) || + getHook(OnEnterFrame)) + { + return true; + } + + /* If any breakpoints are in live scripts, return true. */ + for (Breakpoint* bp = firstBreakpoint(); bp; bp = bp->nextInDebugger()) { + if (IsMarkedUnbarriered(rt, &bp->site->script)) + return true; + } + + for (FrameMap::Range r = frames.all(); !r.empty(); r.popFront()) { + NativeObject* frameObj = r.front().value(); + if (!frameObj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined() || + !frameObj->getReservedSlot(JSSLOT_DEBUGFRAME_ONPOP_HANDLER).isUndefined()) + return true; + } + + return false; +} + +/* static */ JSTrapStatus +Debugger::slowPathOnEnterFrame(JSContext* cx, AbstractFramePtr frame) +{ + RootedValue rval(cx); + JSTrapStatus status = dispatchHook( + cx, + [frame](Debugger* dbg) -> bool { + return dbg->observesFrame(frame) && dbg->observesEnterFrame(); + }, + [&](Debugger* dbg) -> JSTrapStatus { + return dbg->fireEnterFrame(cx, &rval); + }); + + switch (status) { + case JSTRAP_CONTINUE: + break; + + case JSTRAP_THROW: + cx->setPendingException(rval); + break; + + case JSTRAP_ERROR: + cx->clearPendingException(); + break; + + case JSTRAP_RETURN: + frame.setReturnValue(rval); + break; + + default: + MOZ_CRASH("bad Debugger::onEnterFrame JSTrapStatus value"); + } + + return status; +} + +static void +DebuggerFrame_maybeDecrementFrameScriptStepModeCount(FreeOp* fop, AbstractFramePtr frame, + NativeObject* frameobj); + +static void +DebuggerFrame_freeScriptFrameIterData(FreeOp* fop, JSObject* obj); + +/* + * 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 +Debugger::slowPathOnLeaveFrame(JSContext* cx, AbstractFramePtr frame, jsbytecode* pc, bool frameOk) +{ + mozilla::DebugOnly<Handle<GlobalObject*>> debuggeeGlobal = cx->global(); + + auto frameMapsGuard = MakeScopeExit([&] { + // Clean up all Debugger.Frame instances. + removeFromFrameMapsAndClearBreakpointsIn(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<DebuggerFrameVector> frames(cx, DebuggerFrameVector(cx)); + if (!getDebuggerFrames(frame, &frames)) + return false; + if (frames.empty()) + return frameOk; + + /* Save the frame's completion value. */ + JSTrapStatus status; + RootedValue value(cx); + Debugger::resultToCompletion(cx, frameOk, frame.returnValue(), &status, &value); + + // 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()) { + /* 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); + + if (dbg->enabled && + !frameobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONPOP_HANDLER).isUndefined()) + { + RootedValue handler(cx, frameobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONPOP_HANDLER)); + + Maybe<AutoCompartment> ac; + ac.emplace(cx, dbg->object); + + RootedValue wrappedValue(cx, value); + RootedValue completion(cx); + if (!dbg->wrapDebuggeeValue(cx, &wrappedValue) || + !dbg->newCompletionValue(cx, status, wrappedValue, &completion)) + { + status = dbg->reportUncaughtException(ac); + break; + } + + /* Call the onPop handler. */ + RootedValue rval(cx); + bool hookOk = js::Call(cx, handler, frameobj, completion, &rval); + RootedValue nextValue(cx); + JSTrapStatus nextStatus = dbg->processHandlerResult(ac, hookOk, rval, + frame, pc, &nextValue); + + /* + * At this point, we are back in the debuggee compartment, and any error has + * been wrapped up as a completion value. + */ + MOZ_ASSERT(cx->compartment() == debuggeeGlobal->compartment()); + MOZ_ASSERT(!cx->isExceptionPending()); + + /* JSTRAP_CONTINUE means "make no change". */ + if (nextStatus != JSTRAP_CONTINUE) { + status = nextStatus; + value = nextValue; + } + } + } + } + + /* Establish (status, value) as our resumption value. */ + switch (status) { + case JSTRAP_RETURN: + frame.setReturnValue(value); + return true; + + case JSTRAP_THROW: + cx->setPendingException(value); + return false; + + case JSTRAP_ERROR: + MOZ_ASSERT(!cx->isExceptionPending()); + return false; + + default: + MOZ_CRASH("bad final trap status"); + } +} + +/* static */ JSTrapStatus +Debugger::slowPathOnDebuggerStatement(JSContext* cx, AbstractFramePtr frame) +{ + RootedValue rval(cx); + JSTrapStatus status = dispatchHook( + cx, + [](Debugger* dbg) -> bool { return dbg->getHook(OnDebuggerStatement); }, + [&](Debugger* dbg) -> JSTrapStatus { + return dbg->fireDebuggerStatement(cx, &rval); + }); + + switch (status) { + case JSTRAP_CONTINUE: + case JSTRAP_ERROR: + break; + + case JSTRAP_RETURN: + frame.setReturnValue(rval); + break; + + case JSTRAP_THROW: + cx->setPendingException(rval); + break; + + default: + MOZ_CRASH("Invalid onDebuggerStatement trap status"); + } + + return status; +} + +/* static */ JSTrapStatus +Debugger::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 JSTRAP_CONTINUE; + + // The Debugger API mustn't muck with frames from self-hosted scripts. + if (frame.script()->selfHosted()) + return JSTRAP_CONTINUE; + + RootedValue rval(cx); + JSTrapStatus status = dispatchHook( + cx, + [](Debugger* dbg) -> bool { return dbg->getHook(OnExceptionUnwind); }, + [&](Debugger* dbg) -> JSTrapStatus { + return dbg->fireExceptionUnwind(cx, &rval); + }); + + switch (status) { + case JSTRAP_CONTINUE: + break; + + case JSTRAP_THROW: + cx->setPendingException(rval); + break; + + case JSTRAP_ERROR: + cx->clearPendingException(); + break; + + case JSTRAP_RETURN: + cx->clearPendingException(); + frame.setReturnValue(rval); + break; + + default: + MOZ_CRASH("Invalid onExceptionUnwind trap status"); + } + + return status; +} + +// 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<ObjectWeakMap> 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; + } + + CrossCompartmentKey key(object, env, CrossCompartmentKey::DebuggerEnvironment); + if (!object->compartment()->putWrapper(cx, key, ObjectValue(*envobj))) { + NukeDebuggerWrapper(envobj); + environments.remove(env); + return false; + } + + result.set(envobj); + } + + return true; +} + +bool +Debugger::wrapDebuggeeValue(JSContext* cx, MutableHandleValue vp) +{ + assertSameCompartment(cx, 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 (!DefineProperty(cx, optObj, name, trueVal)) + return false; + + vp.setObject(*optObj); + } else if (!cx->compartment()->wrap(cx, vp)) { + vp.setUndefined(); + return false; + } + + return true; +} + +bool +Debugger::wrapDebuggeeObject(JSContext* cx, HandleObject obj, + MutableHandleDebuggerObject result) +{ + MOZ_ASSERT(obj); + + if (obj->is<JSFunction>()) { + MOZ_ASSERT(!IsInternalFunctionObject(*obj)); + RootedFunction fun(cx, &obj->as<JSFunction>()); + if (!EnsureFunctionHasScript(cx, fun)) + return false; + } + + 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; + } + + if (obj->compartment() != object->compartment()) { + CrossCompartmentKey key(object, obj, CrossCompartmentKey::DebuggerObject); + if (!object->compartment()->putWrapper(cx, key, ObjectValue(*dobj))) { + NukeDebuggerWrapper(dobj); + objects.remove(obj); + ReportOutOfMemory(cx); + 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) +{ + assertSameCompartment(cx, 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; +} + +namespace { +class MOZ_STACK_CLASS ReportExceptionClosure : public ScriptEnvironmentPreparer::Closure +{ +public: + explicit ReportExceptionClosure(RootedValue& exn) + : exn_(exn) + { + } + + bool operator()(JSContext* cx) override + { + cx->setPendingException(exn_); + return false; + } + +private: + RootedValue& exn_; +}; +} // anonymous namespace + +JSTrapStatus +Debugger::reportUncaughtException(Maybe<AutoCompartment>& ac) +{ + JSContext* cx = ac->context()->asJSContext(); + + // Uncaught exceptions arise from Debugger code, and so we must already be + // in an NX section. + MOZ_ASSERT(EnterDebuggeeNoExecute::isLockedInStack(cx, *this)); + + if (cx->isExceptionPending()) { + /* + * We want to report the pending exception, but we want to let the + * embedding handle it however it wants to. So pretend like we're + * starting a new script execution on our current compartment (which + * is the debugger compartment, so reported errors won't get + * reported to various onerror handlers in debuggees) and as part of + * that "execution" simply throw our exception so the embedding can + * deal. + */ + RootedValue exn(cx); + if (cx->getPendingException(&exn)) { + /* + * Clear the exception, because + * PrepareScriptEnvironmentAndInvoke will assert that we don't + * have one. + */ + cx->clearPendingException(); + ReportExceptionClosure reportExn(exn); + PrepareScriptEnvironmentAndInvoke(cx, cx->global(), reportExn); + } + /* + * And if not, or if PrepareScriptEnvironmentAndInvoke somehow left + * an exception on cx (which it totally shouldn't do), just give + * up. + */ + cx->clearPendingException(); + } + + ac.reset(); + return JSTRAP_ERROR; +} + +JSTrapStatus +Debugger::handleUncaughtExceptionHelper(Maybe<AutoCompartment>& ac, MutableHandleValue* vp, + const Maybe<HandleValue>& thisVForCheck, + AbstractFramePtr frame) +{ + JSContext* cx = ac->context()->asJSContext(); + + // Uncaught exceptions arise from Debugger code, and so we must already be + // in an NX section. + MOZ_ASSERT(EnterDebuggeeNoExecute::isLockedInStack(cx, *this)); + + if (cx->isExceptionPending()) { + if (uncaughtExceptionHook) { + RootedValue exc(cx); + if (!cx->getPendingException(&exc)) + return JSTRAP_ERROR; + cx->clearPendingException(); + + RootedValue fval(cx, ObjectValue(*uncaughtExceptionHook)); + RootedValue rv(cx); + if (js::Call(cx, fval, object, exc, &rv)) { + if (vp) { + JSTrapStatus status = JSTRAP_CONTINUE; + if (processResumptionValue(ac, frame, thisVForCheck, rv, status, *vp)) + return status; + } else { + return JSTRAP_CONTINUE; + } + } + } + + return reportUncaughtException(ac); + } + + ac.reset(); + return JSTRAP_ERROR; +} + +JSTrapStatus +Debugger::handleUncaughtException(Maybe<AutoCompartment>& ac, MutableHandleValue vp, + const Maybe<HandleValue>& thisVForCheck, AbstractFramePtr frame) +{ + return handleUncaughtExceptionHelper(ac, &vp, thisVForCheck, frame); +} + +JSTrapStatus +Debugger::handleUncaughtException(Maybe<AutoCompartment>& ac) +{ + return handleUncaughtExceptionHelper(ac, nullptr, mozilla::Nothing(), NullFramePtr()); +} + +/* static */ void +Debugger::resultToCompletion(JSContext* cx, bool ok, const Value& rv, + JSTrapStatus* status, MutableHandleValue value) +{ + MOZ_ASSERT_IF(ok, !cx->isExceptionPending()); + + if (ok) { + *status = JSTRAP_RETURN; + value.set(rv); + } else if (cx->isExceptionPending()) { + *status = JSTRAP_THROW; + if (!cx->getPendingException(value)) + *status = JSTRAP_ERROR; + cx->clearPendingException(); + } else { + *status = JSTRAP_ERROR; + value.setUndefined(); + } +} + +bool +Debugger::newCompletionValue(JSContext* cx, JSTrapStatus status, const Value& value_, + MutableHandleValue result) +{ + /* + * We must be in the debugger's compartment, since that's where we want + * to construct the completion value. + */ + assertSameCompartment(cx, object.get()); + assertSameCompartment(cx, value_); + + RootedId key(cx); + RootedValue value(cx, value_); + + switch (status) { + case JSTRAP_RETURN: + key = NameToId(cx->names().return_); + break; + + case JSTRAP_THROW: + key = NameToId(cx->names().throw_); + break; + + case JSTRAP_ERROR: + result.setNull(); + return true; + + default: + MOZ_CRASH("bad status passed to Debugger::newCompletionValue"); + } + + /* Common tail for JSTRAP_RETURN and JSTRAP_THROW. */ + RootedPlainObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx)); + if (!obj || + !NativeDefineProperty(cx, obj, key, value, nullptr, nullptr, JSPROP_ENUMERATE)) + { + return false; + } + + result.setObject(*obj); + return true; +} + +bool +Debugger::receiveCompletionValue(Maybe<AutoCompartment>& ac, bool ok, + HandleValue val, + MutableHandleValue vp) +{ + JSContext* cx = ac->context()->asJSContext(); + + JSTrapStatus status; + RootedValue value(cx); + resultToCompletion(cx, ok, val, &status, &value); + ac.reset(); + return wrapDebuggeeValue(cx, &value) && + newCompletionValue(cx, status, value, vp); +} + +static bool +GetStatusProperty(JSContext* cx, HandleObject obj, HandlePropertyName name, JSTrapStatus status, + JSTrapStatus& statusp, MutableHandleValue vp, int* hits) +{ + bool found; + if (!HasProperty(cx, obj, name, &found)) + return false; + if (found) { + ++*hits; + statusp = status; + if (!GetProperty(cx, obj, obj, name, vp)) + return false; + } + return true; +} + +static bool +ParseResumptionValueAsObject(JSContext* cx, HandleValue rv, JSTrapStatus& statusp, + MutableHandleValue vp) +{ + int hits = 0; + if (rv.isObject()) { + RootedObject obj(cx, &rv.toObject()); + if (!GetStatusProperty(cx, obj, cx->names().return_, JSTRAP_RETURN, statusp, vp, &hits)) + return false; + if (!GetStatusProperty(cx, obj, cx->names().throw_, JSTRAP_THROW, statusp, vp, &hits)) + return false; + } + + if (hits != 1) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_BAD_RESUMPTION); + return false; + } + return true; +} + +static bool +ParseResumptionValue(JSContext* cx, HandleValue rval, JSTrapStatus& statusp, MutableHandleValue vp) +{ + if (rval.isUndefined()) { + statusp = JSTRAP_CONTINUE; + vp.setUndefined(); + return true; + } + if (rval.isNull()) { + statusp = JSTRAP_ERROR; + vp.setUndefined(); + return true; + } + return ParseResumptionValueAsObject(cx, rval, statusp, vp); +} + +static bool +CheckResumptionValue(JSContext* cx, AbstractFramePtr frame, const Maybe<HandleValue>& maybeThisv, + JSTrapStatus status, MutableHandleValue vp) +{ + if (status == JSTRAP_RETURN && frame && frame.isFunctionFrame()) { + // Don't let a { return: ... } resumption value make a generator or + // async function violate the iterator protocol. The return value from + // such a frame must have the form { done: <bool>, value: <anything> }. + RootedFunction callee(cx, frame.callee()); + if (callee->isAsync()) { + if (!CheckAsyncResumptionValue(cx, vp)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_BAD_AWAIT); + return false; + } + } else if (callee->isStarGenerator()) { + if (!CheckStarGeneratorResumptionValue(cx, vp)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_BAD_YIELD); + return false; + } + } + } + + if (maybeThisv.isSome()) { + const HandleValue& thisv = maybeThisv.ref(); + if (status == JSTRAP_RETURN && vp.isPrimitive()) { + if (vp.isUndefined()) { + if (thisv.isMagic(JS_UNINITIALIZED_LEXICAL)) + return ThrowUninitializedThis(cx, frame); + + vp.set(thisv); + } else { + ReportValueError(cx, JSMSG_BAD_DERIVED_RETURN, JSDVG_IGNORE_STACK, vp, nullptr); + return false; + } + } + } + return true; +} + +static bool +GetThisValueForCheck(JSContext* cx, AbstractFramePtr frame, jsbytecode* pc, + MutableHandleValue thisv, Maybe<HandleValue>& maybeThisv) +{ + if (frame.debuggerNeedsCheckPrimitiveReturn()) { + { + AutoCompartment ac(cx, frame.environmentChain()); + if (!GetThisValueForDebuggerMaybeOptimizedOut(cx, frame, pc, thisv)) + return false; + } + + if (!cx->compartment()->wrap(cx, thisv)) + return false; + + MOZ_ASSERT_IF(thisv.isMagic(), thisv.isMagic(JS_UNINITIALIZED_LEXICAL)); + maybeThisv.emplace(HandleValue(thisv)); + } + + return true; +} + +bool +Debugger::processResumptionValue(Maybe<AutoCompartment>& ac, AbstractFramePtr frame, + const Maybe<HandleValue>& maybeThisv, HandleValue rval, + JSTrapStatus& statusp, MutableHandleValue vp) +{ + JSContext* cx = ac->context()->asJSContext(); + + if (!ParseResumptionValue(cx, rval, statusp, vp) || + !unwrapDebuggeeValue(cx, vp) || + !CheckResumptionValue(cx, frame, maybeThisv, statusp, vp)) + { + return false; + } + + ac.reset(); + if (!cx->compartment()->wrap(cx, vp)) { + statusp = JSTRAP_ERROR; + vp.setUndefined(); + } + + return true; +} + +JSTrapStatus +Debugger::processParsedHandlerResultHelper(Maybe<AutoCompartment>& ac, AbstractFramePtr frame, + const Maybe<HandleValue>& maybeThisv, bool success, + JSTrapStatus status, MutableHandleValue vp) +{ + if (!success) + return handleUncaughtException(ac, vp, maybeThisv, frame); + + JSContext* cx = ac->context()->asJSContext(); + + if (!unwrapDebuggeeValue(cx, vp) || + !CheckResumptionValue(cx, frame, maybeThisv, status, vp)) + { + return handleUncaughtException(ac, vp, maybeThisv, frame); + } + + ac.reset(); + if (!cx->compartment()->wrap(cx, vp)) { + status = JSTRAP_ERROR; + vp.setUndefined(); + } + + return status; +} + +JSTrapStatus +Debugger::processParsedHandlerResult(Maybe<AutoCompartment>& ac, AbstractFramePtr frame, + jsbytecode* pc, bool success, JSTrapStatus status, + MutableHandleValue vp) +{ + JSContext* cx = ac->context()->asJSContext(); + + RootedValue thisv(cx); + Maybe<HandleValue> maybeThisv; + if (!GetThisValueForCheck(cx, frame, pc, &thisv, maybeThisv)) { + ac.reset(); + return JSTRAP_ERROR; + } + + return processParsedHandlerResultHelper(ac, frame, maybeThisv, success, status, vp); +} + +JSTrapStatus +Debugger::processHandlerResult(Maybe<AutoCompartment>& ac, bool success, const Value& rv, + AbstractFramePtr frame, jsbytecode* pc, MutableHandleValue vp) +{ + JSContext* cx = ac->context()->asJSContext(); + + RootedValue thisv(cx); + Maybe<HandleValue> maybeThisv; + if (!GetThisValueForCheck(cx, frame, pc, &thisv, maybeThisv)) { + ac.reset(); + return JSTRAP_ERROR; + } + + if (!success) + return handleUncaughtException(ac, vp, maybeThisv, frame); + + RootedValue rootRv(cx, rv); + JSTrapStatus status = JSTRAP_CONTINUE; + success = ParseResumptionValue(cx, rootRv, status, vp); + + return processParsedHandlerResultHelper(ac, frame, maybeThisv, success, status, vp); +} + +static bool +CallMethodIfPresent(JSContext* cx, HandleObject obj, const char* name, size_t argc, Value* argv, + MutableHandleValue rval) +{ + rval.setUndefined(); + JSAtom* atom = Atomize(cx, name, strlen(name)); + if (!atom) + return false; + + RootedId id(cx, AtomToId(atom)); + RootedValue fval(cx); + if (!GetProperty(cx, obj, obj, id, &fval)) + return false; + + if (!IsCallable(fval)) + return true; + + InvokeArgs args(cx); + if (!args.init(cx, argc)) + return false; + + for (size_t i = 0; i < argc; i++) + args[i].set(argv[i]); + + rval.setObject(*obj); // overwritten by successful Call + return js::Call(cx, fval, rval, args, rval); +} + +JSTrapStatus +Debugger::fireDebuggerStatement(JSContext* cx, MutableHandleValue vp) +{ + RootedObject hook(cx, getHook(OnDebuggerStatement)); + MOZ_ASSERT(hook); + MOZ_ASSERT(hook->isCallable()); + + Maybe<AutoCompartment> ac; + ac.emplace(cx, object); + + ScriptFrameIter iter(cx); + RootedValue scriptFrame(cx); + if (!getScriptFrame(cx, iter, &scriptFrame)) + return reportUncaughtException(ac); + + RootedValue fval(cx, ObjectValue(*hook)); + RootedValue rv(cx); + bool ok = js::Call(cx, fval, object, scriptFrame, &rv); + return processHandlerResult(ac, ok, rv, iter.abstractFramePtr(), iter.pc(), vp); +} + +JSTrapStatus +Debugger::fireExceptionUnwind(JSContext* cx, MutableHandleValue vp) +{ + RootedObject hook(cx, getHook(OnExceptionUnwind)); + MOZ_ASSERT(hook); + MOZ_ASSERT(hook->isCallable()); + + RootedValue exc(cx); + if (!cx->getPendingException(&exc)) + return JSTRAP_ERROR; + cx->clearPendingException(); + + Maybe<AutoCompartment> ac; + ac.emplace(cx, object); + + RootedValue scriptFrame(cx); + RootedValue wrappedExc(cx, exc); + + ScriptFrameIter iter(cx); + if (!getScriptFrame(cx, iter, &scriptFrame) || !wrapDebuggeeValue(cx, &wrappedExc)) + return reportUncaughtException(ac); + + RootedValue fval(cx, ObjectValue(*hook)); + RootedValue rv(cx); + bool ok = js::Call(cx, fval, object, scriptFrame, wrappedExc, &rv); + JSTrapStatus st = processHandlerResult(ac, ok, rv, iter.abstractFramePtr(), iter.pc(), vp); + if (st == JSTRAP_CONTINUE) + cx->setPendingException(exc); + return st; +} + +JSTrapStatus +Debugger::fireEnterFrame(JSContext* cx, MutableHandleValue vp) +{ + RootedObject hook(cx, getHook(OnEnterFrame)); + MOZ_ASSERT(hook); + MOZ_ASSERT(hook->isCallable()); + + Maybe<AutoCompartment> ac; + ac.emplace(cx, object); + + RootedValue scriptFrame(cx); + + ScriptFrameIter iter(cx); + if (!getScriptFrame(cx, iter, &scriptFrame)) + return reportUncaughtException(ac); + + RootedValue fval(cx, ObjectValue(*hook)); + RootedValue rv(cx); + bool ok = js::Call(cx, fval, object, scriptFrame, &rv); + + return processHandlerResult(ac, ok, rv, iter.abstractFramePtr(), iter.pc(), vp); +} + +void +Debugger::fireNewScript(JSContext* cx, Handle<DebuggerScriptReferent> scriptReferent) +{ + RootedObject hook(cx, getHook(OnNewScript)); + MOZ_ASSERT(hook); + MOZ_ASSERT(hook->isCallable()); + + Maybe<AutoCompartment> ac; + ac.emplace(cx, object); + + JSObject* dsobj = wrapVariantReferent(cx, scriptReferent); + if (!dsobj) { + reportUncaughtException(ac); + return; + } + + RootedValue fval(cx, ObjectValue(*hook)); + RootedValue dsval(cx, ObjectValue(*dsobj)); + RootedValue rv(cx); + if (!js::Call(cx, fval, object, dsval, &rv)) + handleUncaughtException(ac); +} + +void +Debugger::fireOnGarbageCollectionHook(JSContext* cx, + const JS::dbg::GarbageCollectionEvent::Ptr& gcData) +{ + MOZ_ASSERT(observedGC(gcData->majorGCNumber())); + observedGCs.remove(gcData->majorGCNumber()); + + RootedObject hook(cx, getHook(OnGarbageCollection)); + MOZ_ASSERT(hook); + MOZ_ASSERT(hook->isCallable()); + + Maybe<AutoCompartment> ac; + ac.emplace(cx, object); + + JSObject* dataObj = gcData->toJSObject(cx); + if (!dataObj) { + reportUncaughtException(ac); + return; + } + + RootedValue fval(cx, ObjectValue(*hook)); + RootedValue dataVal(cx, ObjectValue(*dataObj)); + RootedValue rv(cx); + if (!js::Call(cx, fval, object, dataVal, &rv)) + handleUncaughtException(ac); +} + +template <typename HookIsEnabledFun /* bool (Debugger*) */, + typename FireHookFun /* JSTrapStatus (Debugger*) */> +/* static */ JSTrapStatus +Debugger::dispatchHook(JSContext* cx, HookIsEnabledFun hookIsEnabled, FireHookFun fireHook) +{ + /* + * 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. + * + * Note: In the general case, 'triggered' contains references to objects in + * different compartments--every compartment *except* this one. + */ + AutoValueVector triggered(cx); + Handle<GlobalObject*> global = cx->global(); + if (GlobalObject::DebuggerVector* debuggers = global->getDebuggers()) { + for (auto p = debuggers->begin(); p != debuggers->end(); p++) { + Debugger* dbg = *p; + if (dbg->enabled && hookIsEnabled(dbg)) { + if (!triggered.append(ObjectValue(*dbg->toJSObject()))) + return JSTRAP_ERROR; + } + } + } + + /* + * Deliver the event to each debugger, checking again to make sure it + * should still be delivered. + */ + for (Value* p = triggered.begin(); p != triggered.end(); p++) { + Debugger* dbg = Debugger::fromJSObject(&p->toObject()); + EnterDebuggeeNoExecute nx(cx, *dbg); + if (dbg->debuggees.has(global) && dbg->enabled && hookIsEnabled(dbg)) { + JSTrapStatus st = fireHook(dbg); + if (st != JSTRAP_CONTINUE) + return st; + } + } + return JSTRAP_CONTINUE; +} + +void +Debugger::slowPathOnNewScript(JSContext* cx, HandleScript script) +{ + JSTrapStatus status = dispatchHook( + cx, + [script](Debugger* dbg) -> bool { + return dbg->observesNewScript() && dbg->observesScript(script); + }, + [&](Debugger* dbg) -> JSTrapStatus { + Rooted<DebuggerScriptReferent> scriptReferent(cx, script.get()); + dbg->fireNewScript(cx, scriptReferent); + return JSTRAP_CONTINUE; + }); + + if (status == JSTRAP_ERROR) + return; + + MOZ_ASSERT(status == JSTRAP_CONTINUE); +} + +void +Debugger::slowPathOnNewWasmInstance(JSContext* cx, Handle<WasmInstanceObject*> wasmInstance) +{ + JSTrapStatus status = dispatchHook( + cx, + [wasmInstance](Debugger* dbg) -> bool { + return dbg->observesNewScript() && dbg->observesGlobal(&wasmInstance->global()); + }, + [&](Debugger* dbg) -> JSTrapStatus { + Rooted<DebuggerScriptReferent> scriptReferent(cx, wasmInstance.get()); + dbg->fireNewScript(cx, scriptReferent); + return JSTRAP_CONTINUE; + }); + + if (status == JSTRAP_ERROR) + return; + + MOZ_ASSERT(status == JSTRAP_CONTINUE); +} + +/* static */ JSTrapStatus +Debugger::onTrap(JSContext* cx, MutableHandleValue vp) +{ + ScriptFrameIter iter(cx); + RootedScript script(cx, iter.script()); + MOZ_ASSERT(script->isDebuggee()); + Rooted<GlobalObject*> scriptGlobal(cx, &script->global()); + jsbytecode* pc = iter.pc(); + BreakpointSite* site = script->getBreakpointSite(pc); + JSOp op = JSOp(*pc); + + /* Build list of breakpoint handlers. */ + Vector<Breakpoint*> triggered(cx); + for (Breakpoint* bp = site->firstBreakpoint(); bp; bp = bp->nextInSite()) { + if (!triggered.append(bp)) + return JSTRAP_ERROR; + } + + for (Breakpoint** p = triggered.begin(); p != triggered.end(); p++) { + Breakpoint* bp = *p; + + /* Handlers can clear breakpoints. Check that bp still exists. */ + if (!site || !site->hasBreakpoint(bp)) + continue; + + /* + * There are two reasons we have to check whether dbg is enabled and + * debugging scriptGlobal. + * + * One is just that one breakpoint handler can disable other Debuggers + * or remove debuggees. + * + * The other has to do with non-compile-and-go scripts, which have no + * specific global--until they are executed. Only now do we know which + * global the script is running against. + */ + Debugger* dbg = bp->debugger; + bool hasDebuggee = dbg->enabled && dbg->debuggees.has(scriptGlobal); + if (hasDebuggee) { + Maybe<AutoCompartment> ac; + ac.emplace(cx, dbg->object); + EnterDebuggeeNoExecute nx(cx, *dbg); + + RootedValue scriptFrame(cx); + if (!dbg->getScriptFrame(cx, iter, &scriptFrame)) + return dbg->reportUncaughtException(ac); + RootedValue rv(cx); + Rooted<JSObject*> handler(cx, bp->handler); + bool ok = CallMethodIfPresent(cx, handler, "hit", 1, scriptFrame.address(), &rv); + JSTrapStatus st = dbg->processHandlerResult(ac, ok, rv, iter.abstractFramePtr(), + iter.pc(), vp); + if (st != JSTRAP_CONTINUE) + return st; + + /* Calling JS code invalidates site. Reload it. */ + site = script->getBreakpointSite(pc); + } + } + + /* By convention, return the true op to the interpreter in vp. */ + vp.setInt32(op); + return JSTRAP_CONTINUE; +} + +/* static */ JSTrapStatus +Debugger::onSingleStep(JSContext* cx, MutableHandleValue vp) +{ + ScriptFrameIter iter(cx); + + /* + * We may be stepping over a JSOP_EXCEPTION, that pushes the context's + * pending exception for a 'catch' clause to handle. Don't let the + * onStep handlers mess with that (other than by returning a resumption + * value). + */ + RootedValue exception(cx, UndefinedValue()); + bool exceptionPending = cx->isExceptionPending(); + if (exceptionPending) { + if (!cx->getPendingException(&exception)) + return JSTRAP_ERROR; + cx->clearPendingException(); + } + + /* + * Build list of Debugger.Frame instances referring to this frame with + * onStep handlers. + */ + Rooted<DebuggerFrameVector> frames(cx, DebuggerFrameVector(cx)); + if (!getDebuggerFrames(iter.abstractFramePtr(), &frames)) + return JSTRAP_ERROR; + +#ifdef DEBUG + /* + * Validate the single-step count on this frame's script, to ensure that + * we're not receiving traps we didn't ask for. Even when frames is + * non-empty (and thus we know this trap was requested), do the check + * anyway, to make sure the count has the correct non-zero value. + * + * The converse --- ensuring that we do receive traps when we should --- can + * be done with unit tests. + */ + { + uint32_t stepperCount = 0; + JSScript* trappingScript = iter.script(); + GlobalObject* global = cx->global(); + if (GlobalObject::DebuggerVector* debuggers = global->getDebuggers()) { + for (auto p = debuggers->begin(); p != debuggers->end(); p++) { + Debugger* dbg = *p; + for (FrameMap::Range r = dbg->frames.all(); !r.empty(); r.popFront()) { + AbstractFramePtr frame = r.front().key(); + NativeObject* frameobj = r.front().value(); + if (frame.script() == trappingScript && + !frameobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined()) + { + stepperCount++; + } + } + } + } + MOZ_ASSERT(stepperCount == trappingScript->stepModeCount()); + } +#endif + + // Call onStep for frames that have the handler set. + for (size_t i = 0; i < frames.length(); i++) { + HandleDebuggerFrame frame = frames[i]; + if (frame->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined()) + continue; + + Debugger* dbg = Debugger::fromChildJSObject(frame); + EnterDebuggeeNoExecute nx(cx, *dbg); + + Maybe<AutoCompartment> ac; + ac.emplace(cx, dbg->object); + + RootedValue fval(cx, frame->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER)); + RootedValue rval(cx); + bool ok = js::Call(cx, fval, frame, &rval); + JSTrapStatus st = dbg->processHandlerResult(ac, ok, rval, iter.abstractFramePtr(), + iter.pc(), vp); + if (st != JSTRAP_CONTINUE) + return st; + } + + vp.setUndefined(); + if (exceptionPending) + cx->setPendingException(exception); + return JSTRAP_CONTINUE; +} + +JSTrapStatus +Debugger::fireNewGlobalObject(JSContext* cx, Handle<GlobalObject*> global, MutableHandleValue vp) +{ + RootedObject hook(cx, getHook(OnNewGlobalObject)); + MOZ_ASSERT(hook); + MOZ_ASSERT(hook->isCallable()); + + Maybe<AutoCompartment> ac; + ac.emplace(cx, object); + + RootedValue wrappedGlobal(cx, ObjectValue(*global)); + if (!wrapDebuggeeValue(cx, &wrappedGlobal)) + return reportUncaughtException(ac); + + // onNewGlobalObject is infallible, and thus is only allowed to return + // undefined as a resumption value. If it returns anything else, we throw. + // And if that happens, or if the hook itself throws, we invoke the + // uncaughtExceptionHook so that we never leave an exception pending on the + // cx. This allows JS_NewGlobalObject to avoid handling failures from debugger + // hooks. + RootedValue rv(cx); + RootedValue fval(cx, ObjectValue(*hook)); + bool ok = js::Call(cx, fval, object, wrappedGlobal, &rv); + if (ok && !rv.isUndefined()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_RESUMPTION_VALUE_DISALLOWED); + ok = false; + } + // NB: Even though we don't care about what goes into it, we have to pass vp + // to handleUncaughtException so that it parses resumption values from the + // uncaughtExceptionHook and tells the caller whether we should execute the + // rest of the onNewGlobalObject hooks or not. + JSTrapStatus status = ok ? JSTRAP_CONTINUE + : handleUncaughtException(ac, vp); + MOZ_ASSERT(!cx->isExceptionPending()); + return status; +} + +void +Debugger::slowPathOnNewGlobalObject(JSContext* cx, Handle<GlobalObject*> global) +{ + MOZ_ASSERT(!JS_CLIST_IS_EMPTY(&cx->runtime()->onNewGlobalObjectWatchers)); + if (global->compartment()->creationOptions().invisibleToDebugger()) + return; + + /* + * Make a copy of the runtime's onNewGlobalObjectWatchers before running the + * handlers. Since one Debugger's handler can disable another's, the list + * can be mutated while we're walking it. + */ + AutoObjectVector watchers(cx); + for (JSCList* link = JS_LIST_HEAD(&cx->runtime()->onNewGlobalObjectWatchers); + link != &cx->runtime()->onNewGlobalObjectWatchers; + link = JS_NEXT_LINK(link)) + { + Debugger* dbg = fromOnNewGlobalObjectWatchersLink(link); + MOZ_ASSERT(dbg->observesNewGlobalObject()); + JSObject* obj = dbg->object; + JS::ExposeObjectToActiveJS(obj); + if (!watchers.append(obj)) { + if (cx->isExceptionPending()) + cx->clearPendingException(); + return; + } + } + + JSTrapStatus status = JSTRAP_CONTINUE; + RootedValue value(cx); + + for (size_t i = 0; i < watchers.length(); i++) { + Debugger* dbg = fromJSObject(watchers[i]); + EnterDebuggeeNoExecute nx(cx, *dbg); + + // We disallow resumption values from onNewGlobalObject hooks, because we + // want the debugger hooks for global object creation to be infallible. + // But if an onNewGlobalObject hook throws, and the uncaughtExceptionHook + // decides to raise an error, we want to at least avoid invoking the rest + // of the onNewGlobalObject handlers in the list (not for any super + // compelling reason, just because it seems like the right thing to do). + // So we ignore whatever comes out in |value|, but break out of the loop + // if a non-success trap status is returned. + if (dbg->observesNewGlobalObject()) { + status = dbg->fireNewGlobalObject(cx, global, &value); + if (status != JSTRAP_CONTINUE && status != JSTRAP_RETURN) + break; + } + } + MOZ_ASSERT(!cx->isExceptionPending()); +} + +/* static */ bool +Debugger::slowPathOnLogAllocationSite(JSContext* cx, HandleObject obj, HandleSavedFrame frame, + double when, GlobalObject::DebuggerVector& dbgs) +{ + MOZ_ASSERT(!dbgs.empty()); + mozilla::DebugOnly<ReadBarriered<Debugger*>*> begin = dbgs.begin(); + + // Root all the Debuggers while we're iterating over them; + // appendAllocationSite calls JSCompartment::wrap, and thus can GC. + // + // SpiderMonkey protocol is generally for the caller to prove that it has + // rooted the stuff it's asking you to operate on (i.e. by passing a + // Handle), but in this case, we're iterating over a global's list of + // Debuggers, and globals only hold their Debuggers weakly. + Rooted<GCVector<JSObject*>> activeDebuggers(cx, GCVector<JSObject*>(cx)); + for (auto dbgp = dbgs.begin(); dbgp < dbgs.end(); dbgp++) { + if (!activeDebuggers.append((*dbgp)->object)) + return false; + } + + for (auto dbgp = dbgs.begin(); dbgp < dbgs.end(); dbgp++) { + // The set of debuggers had better not change while we're iterating, + // such that the vector gets reallocated. + MOZ_ASSERT(dbgs.begin() == begin); + + if ((*dbgp)->trackingAllocationSites && + (*dbgp)->enabled && + !(*dbgp)->appendAllocationSite(cx, obj, frame, when)) + { + return false; + } + } + + return true; +} + +bool +Debugger::isDebuggeeUnbarriered(const JSCompartment* compartment) const +{ + MOZ_ASSERT(compartment); + return compartment->isDebuggee() && debuggees.has(compartment->unsafeUnbarrieredMaybeGlobal()); +} + +bool +Debugger::appendAllocationSite(JSContext* cx, HandleObject obj, HandleSavedFrame frame, + double when) +{ + MOZ_ASSERT(trackingAllocationSites && enabled); + + AutoCompartment ac(cx, object); + RootedObject wrappedFrame(cx, frame); + if (!cx->compartment()->wrap(cx, &wrappedFrame)) + return false; + + RootedAtom ctorName(cx); + { + AutoCompartment ac(cx, obj); + if (!obj->constructorDisplayAtom(cx, &ctorName)) + return false; + } + + auto className = obj->getClass()->name; + auto size = JS::ubi::Node(obj.get()).size(cx->runtime()->debuggerMallocSizeOf); + auto inNursery = gc::IsInsideNursery(obj); + + if (!allocationsLog.emplaceBack(wrappedFrame, when, className, ctorName, size, inNursery)) { + ReportOutOfMemory(cx); + return false; + } + + if (allocationsLog.length() > maxAllocationsLogLength) { + if (!allocationsLog.popFront()) { + ReportOutOfMemory(cx); + return false; + } + MOZ_ASSERT(allocationsLog.length() == maxAllocationsLogLength); + allocationsLogOverflowed = true; + } + + return true; +} + +JSTrapStatus +Debugger::firePromiseHook(JSContext* cx, Hook hook, HandleObject promise, MutableHandleValue vp) +{ + MOZ_ASSERT(hook == OnNewPromise || hook == OnPromiseSettled); + + RootedObject hookObj(cx, getHook(hook)); + MOZ_ASSERT(hookObj); + MOZ_ASSERT(hookObj->isCallable()); + + Maybe<AutoCompartment> ac; + ac.emplace(cx, object); + + RootedValue dbgObj(cx, ObjectValue(*promise)); + if (!wrapDebuggeeValue(cx, &dbgObj)) + return reportUncaughtException(ac); + + // Like onNewGlobalObject, the Promise hooks are infallible and the comments + // in |Debugger::fireNewGlobalObject| apply here as well. + RootedValue fval(cx, ObjectValue(*hookObj)); + RootedValue rv(cx); + bool ok = js::Call(cx, fval, object, dbgObj, &rv); + if (ok && !rv.isUndefined()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_RESUMPTION_VALUE_DISALLOWED); + ok = false; + } + + JSTrapStatus status = ok ? JSTRAP_CONTINUE + : handleUncaughtException(ac, vp); + MOZ_ASSERT(!cx->isExceptionPending()); + return status; +} + +/* static */ void +Debugger::slowPathPromiseHook(JSContext* cx, Hook hook, HandleObject promise) +{ + MOZ_ASSERT(hook == OnNewPromise || hook == OnPromiseSettled); + RootedValue rval(cx); + + JSTrapStatus status = dispatchHook( + cx, + [hook](Debugger* dbg) -> bool { return dbg->getHook(hook); }, + [&](Debugger* dbg) -> JSTrapStatus { + (void) dbg->firePromiseHook(cx, hook, promise, &rval); + return JSTRAP_CONTINUE; + }); + + if (status == JSTRAP_ERROR) { + // The dispatch hook function might fail to append into the list of + // Debuggers which are watching for the hook. + cx->clearPendingException(); + return; + } + + // Promise hooks are infallible and we ignore errors from uncaught + // exceptions by design. + MOZ_ASSERT(status == JSTRAP_CONTINUE); +} + + +/*** Debugger code invalidation for observing execution ******************************************/ + +class MOZ_RAII ExecutionObservableCompartments : public Debugger::ExecutionObservableSet +{ + HashSet<JSCompartment*> compartments_; + HashSet<Zone*> zones_; + + public: + explicit ExecutionObservableCompartments(JSContext* cx + MOZ_GUARD_OBJECT_NOTIFIER_PARAM) + : compartments_(cx), + zones_(cx) + { + MOZ_GUARD_OBJECT_NOTIFIER_INIT; + } + + bool init() { return compartments_.init() && zones_.init(); } + bool add(JSCompartment* comp) { return compartments_.put(comp) && zones_.put(comp->zone()); } + + typedef HashSet<JSCompartment*>::Range CompartmentRange; + const HashSet<JSCompartment*>* compartments() const { return &compartments_; } + + const HashSet<Zone*>* zones() const { return &zones_; } + bool shouldRecompileOrInvalidate(JSScript* script) const { + return script->hasBaselineScript() && compartments_.has(script->compartment()); + } + bool shouldMarkAsDebuggee(ScriptFrameIter& iter) const { + // AbstractFramePtr can't refer to non-remateralized Ion frames, so if + // iter refers to one such, we know we don't match. + return iter.hasUsableAbstractFramePtr() && compartments_.has(iter.compartment()); + } + + MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER +}; + +// Given a particular AbstractFramePtr F that has become observable, this +// represents the stack frames that need to be bailed out or marked as +// debuggees, and the scripts that need to be recompiled, taking inlining into +// account. +class MOZ_RAII ExecutionObservableFrame : public Debugger::ExecutionObservableSet +{ + AbstractFramePtr frame_; + + public: + explicit ExecutionObservableFrame(AbstractFramePtr frame + MOZ_GUARD_OBJECT_NOTIFIER_PARAM) + : frame_(frame) + { + MOZ_GUARD_OBJECT_NOTIFIER_INIT; + } + + Zone* singleZone() const { + // We never inline across compartments, let alone across zones, so + // frames_'s script's zone is the only one of interest. + return frame_.script()->compartment()->zone(); + } + + JSScript* singleScriptForZoneInvalidation() const { + MOZ_CRASH("ExecutionObservableFrame shouldn't need zone-wide invalidation."); + return nullptr; + } + + bool shouldRecompileOrInvalidate(JSScript* script) const { + // Normally, *this represents exactly one script: the one frame_ is + // running. + // + // However, debug-mode OSR uses *this for both invalidating Ion frames, + // and recompiling the Baseline scripts that those Ion frames will bail + // out into. Suppose frame_ is an inline frame, executing a copy of its + // JSScript, S_inner, that has been inlined into the IonScript of some + // other JSScript, S_outer. We must match S_outer, to decide which Ion + // frame to invalidate; and we must match S_inner, to decide which + // Baseline script to recompile. + // + // Note that this does not, by design, invalidate *all* inliners of + // frame_.script(), as only frame_ is made observable, not + // frame_.script(). + if (!script->hasBaselineScript()) + return false; + + if (script == frame_.script()) + return true; + + return frame_.isRematerializedFrame() && + script == frame_.asRematerializedFrame()->outerScript(); + } + + bool shouldMarkAsDebuggee(ScriptFrameIter& iter) const { + // AbstractFramePtr can't refer to non-remateralized Ion frames, so if + // iter refers to one such, we know we don't match. + // + // We never use this 'has' overload for frame invalidation, only for + // frame debuggee marking; so this overload doesn't need a parallel to + // the just-so inlining logic above. + return iter.hasUsableAbstractFramePtr() && iter.abstractFramePtr() == frame_; + } + + MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER +}; + +class MOZ_RAII ExecutionObservableScript : public Debugger::ExecutionObservableSet +{ + RootedScript script_; + + public: + ExecutionObservableScript(JSContext* cx, JSScript* script + MOZ_GUARD_OBJECT_NOTIFIER_PARAM) + : script_(cx, script) + { + MOZ_GUARD_OBJECT_NOTIFIER_INIT; + } + + Zone* singleZone() const { return script_->compartment()->zone(); } + JSScript* singleScriptForZoneInvalidation() const { return script_; } + bool shouldRecompileOrInvalidate(JSScript* script) const { + return script->hasBaselineScript() && script == script_; + } + bool shouldMarkAsDebuggee(ScriptFrameIter& iter) const { + // AbstractFramePtr can't refer to non-remateralized Ion frames, and + // while a non-rematerialized Ion frame may indeed be running script_, + // we cannot mark them as debuggees until they bail out. + // + // Upon bailing out, any newly constructed Baseline frames that came + // from Ion frames with scripts that are isDebuggee() is marked as + // debuggee. This is correct in that the only other way a frame may be + // marked as debuggee is via Debugger.Frame reflection, which would + // have rematerialized any Ion frames. + return iter.hasUsableAbstractFramePtr() && iter.abstractFramePtr().script() == script_; + } + + MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER +}; + +/* static */ bool +Debugger::updateExecutionObservabilityOfFrames(JSContext* cx, const ExecutionObservableSet& obs, + IsObserving observing) +{ + AutoSuppressProfilerSampling suppressProfilerSampling(cx); + + { + jit::JitContext jctx(cx, nullptr); + if (!jit::RecompileOnStackBaselineScriptsForDebugMode(cx, obs, observing)) { + ReportOutOfMemory(cx); + return false; + } + } + + AbstractFramePtr oldestEnabledFrame; + for (ScriptFrameIter iter(cx); + !iter.done(); + ++iter) + { + if (obs.shouldMarkAsDebuggee(iter)) { + if (observing) { + if (!iter.abstractFramePtr().isDebuggee()) { + oldestEnabledFrame = iter.abstractFramePtr(); + oldestEnabledFrame.setIsDebuggee(); + } + } else { +#ifdef DEBUG + // Debugger.Frame lifetimes are managed by the debug epilogue, + // so in general it's unsafe to unmark a frame if it has a + // Debugger.Frame associated with it. + MOZ_ASSERT(!inFrameMaps(iter.abstractFramePtr())); +#endif + iter.abstractFramePtr().unsetIsDebuggee(); + } + } + } + + // See comment in unsetPrevUpToDateUntil. + if (oldestEnabledFrame) { + AutoCompartment ac(cx, oldestEnabledFrame.compartment()); + DebugEnvironments::unsetPrevUpToDateUntil(cx, oldestEnabledFrame); + } + + return true; +} + +static inline void +MarkBaselineScriptActiveIfObservable(JSScript* script, const Debugger::ExecutionObservableSet& obs) +{ + if (obs.shouldRecompileOrInvalidate(script)) + script->baselineScript()->setActive(); +} + +static bool +AppendAndInvalidateScript(JSContext* cx, Zone* zone, JSScript* script, Vector<JSScript*>& scripts) +{ + // Enter the script's compartment as addPendingRecompile attempts to + // cancel off-thread compilations, whose books are kept on the + // script's compartment. + MOZ_ASSERT(script->compartment()->zone() == zone); + AutoCompartment ac(cx, script->compartment()); + zone->types.addPendingRecompile(cx, script); + return scripts.append(script); +} + +static bool +UpdateExecutionObservabilityOfScriptsInZone(JSContext* cx, Zone* zone, + const Debugger::ExecutionObservableSet& obs, + Debugger::IsObserving observing) +{ + using namespace js::jit; + + AutoSuppressProfilerSampling suppressProfilerSampling(cx); + + JSRuntime* rt = cx->runtime(); + FreeOp* fop = cx->runtime()->defaultFreeOp(); + + Vector<JSScript*> scripts(cx); + + // Iterate through observable scripts, invalidating their Ion scripts and + // appending them to a vector for discarding their baseline scripts later. + { + AutoEnterAnalysis enter(fop, zone); + if (JSScript* script = obs.singleScriptForZoneInvalidation()) { + if (obs.shouldRecompileOrInvalidate(script)) { + if (!AppendAndInvalidateScript(cx, zone, script, scripts)) + return false; + } + } else { + for (auto iter = zone->cellIter<JSScript>(); !iter.done(); iter.next()) { + JSScript* script = iter; + if (obs.shouldRecompileOrInvalidate(script) && + !gc::IsAboutToBeFinalizedUnbarriered(&script)) + { + if (!AppendAndInvalidateScript(cx, zone, script, scripts)) + return false; + } + } + } + } + + // Code below this point must be infallible to ensure the active bit of + // BaselineScripts is in a consistent state. + // + // Mark active baseline scripts in the observable set so that they don't + // get discarded. They will be recompiled. + for (JitActivationIterator actIter(rt); !actIter.done(); ++actIter) { + if (actIter->compartment()->zone() != zone) + continue; + + for (JitFrameIterator iter(actIter); !iter.done(); ++iter) { + switch (iter.type()) { + case JitFrame_BaselineJS: + MarkBaselineScriptActiveIfObservable(iter.script(), obs); + break; + case JitFrame_IonJS: + MarkBaselineScriptActiveIfObservable(iter.script(), obs); + for (InlineFrameIterator inlineIter(rt, &iter); inlineIter.more(); ++inlineIter) + MarkBaselineScriptActiveIfObservable(inlineIter.script(), obs); + break; + default:; + } + } + } + + // Iterate through the scripts again and finish discarding + // BaselineScripts. This must be done as a separate phase as we can only + // discard the BaselineScript on scripts that have no IonScript. + for (size_t i = 0; i < scripts.length(); i++) { + MOZ_ASSERT_IF(scripts[i]->isDebuggee(), observing); + FinishDiscardBaselineScript(fop, scripts[i]); + } + + return true; +} + +/* static */ bool +Debugger::updateExecutionObservabilityOfScripts(JSContext* cx, const ExecutionObservableSet& obs, + IsObserving observing) +{ + if (Zone* zone = obs.singleZone()) + return UpdateExecutionObservabilityOfScriptsInZone(cx, zone, obs, observing); + + typedef ExecutionObservableSet::ZoneRange ZoneRange; + for (ZoneRange r = obs.zones()->all(); !r.empty(); r.popFront()) { + if (!UpdateExecutionObservabilityOfScriptsInZone(cx, r.front(), obs, observing)) + return false; + } + + return true; +} + +template <typename FrameFn> +/* static */ void +Debugger::forEachDebuggerFrame(AbstractFramePtr frame, FrameFn fn) +{ + GlobalObject* global = &frame.script()->global(); + if (GlobalObject::DebuggerVector* debuggers = global->getDebuggers()) { + for (auto p = debuggers->begin(); p != debuggers->end(); p++) { + Debugger* dbg = *p; + if (FrameMap::Ptr entry = dbg->frames.lookup(frame)) + fn(entry->value()); + } + } +} + +/* static */ bool +Debugger::getDebuggerFrames(AbstractFramePtr frame, MutableHandle<DebuggerFrameVector> frames) +{ + bool hadOOM = false; + forEachDebuggerFrame(frame, [&](DebuggerFrame* frameobj) { + if (!hadOOM && !frames.append(frameobj)) + hadOOM = true; + }); + return !hadOOM; +} + +/* static */ bool +Debugger::updateExecutionObservability(JSContext* cx, ExecutionObservableSet& obs, + IsObserving observing) +{ + if (!obs.singleZone() && obs.zones()->empty()) + return true; + + // Invalidate scripts first so we can set the needsArgsObj flag on scripts + // before patching frames. + return updateExecutionObservabilityOfScripts(cx, obs, observing) && + updateExecutionObservabilityOfFrames(cx, obs, observing); +} + +/* static */ bool +Debugger::ensureExecutionObservabilityOfScript(JSContext* cx, JSScript* script) +{ + if (script->isDebuggee()) + return true; + ExecutionObservableScript obs(cx, script); + return updateExecutionObservability(cx, obs, Observing); +} + +/* static */ bool +Debugger::ensureExecutionObservabilityOfOsrFrame(JSContext* cx, InterpreterFrame* frame) +{ + MOZ_ASSERT(frame->isDebuggee()); + if (frame->script()->hasBaselineScript() && + frame->script()->baselineScript()->hasDebugInstrumentation()) + { + return true; + } + ExecutionObservableFrame obs(frame); + return updateExecutionObservabilityOfFrames(cx, obs, Observing); +} + +/* static */ bool +Debugger::ensureExecutionObservabilityOfFrame(JSContext* cx, AbstractFramePtr frame) +{ + MOZ_ASSERT_IF(frame.script()->isDebuggee(), frame.isDebuggee()); + if (frame.isDebuggee()) + return true; + ExecutionObservableFrame obs(frame); + return updateExecutionObservabilityOfFrames(cx, obs, Observing); +} + +/* static */ bool +Debugger::ensureExecutionObservabilityOfCompartment(JSContext* cx, JSCompartment* comp) +{ + if (comp->debuggerObservesAllExecution()) + return true; + ExecutionObservableCompartments obs(cx); + if (!obs.init() || !obs.add(comp)) + return false; + comp->updateDebuggerObservesAllExecution(); + return updateExecutionObservability(cx, obs, Observing); +} + +/* static */ bool +Debugger::hookObservesAllExecution(Hook which) +{ + return which == OnEnterFrame; +} + +Debugger::IsObserving +Debugger::observesAllExecution() const +{ + if (enabled && !!getHook(OnEnterFrame)) + return Observing; + return NotObserving; +} + +Debugger::IsObserving +Debugger::observesAsmJS() const +{ + if (enabled && !allowUnobservedAsmJS) + return Observing; + return NotObserving; +} + +Debugger::IsObserving +Debugger::observesCoverage() const +{ + if (enabled && collectCoverageInfo) + return Observing; + return NotObserving; +} + +// Toggle whether this Debugger's debuggees observe all execution. This is +// called when a hook that observes all execution is set or unset. See +// hookObservesAllExecution. +bool +Debugger::updateObservesAllExecutionOnDebuggees(JSContext* cx, IsObserving observing) +{ + ExecutionObservableCompartments obs(cx); + if (!obs.init()) + return false; + + for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) { + GlobalObject* global = r.front(); + JSCompartment* comp = global->compartment(); + + if (comp->debuggerObservesAllExecution() == observing) + continue; + + // It's expensive to eagerly invalidate and recompile a compartment, + // so add the compartment to the set only if we are observing. + if (observing && !obs.add(comp)) + return false; + } + + if (!updateExecutionObservability(cx, obs, observing)) + return false; + + typedef ExecutionObservableCompartments::CompartmentRange CompartmentRange; + for (CompartmentRange r = obs.compartments()->all(); !r.empty(); r.popFront()) + r.front()->updateDebuggerObservesAllExecution(); + + return true; +} + +bool +Debugger::updateObservesCoverageOnDebuggees(JSContext* cx, IsObserving observing) +{ + ExecutionObservableCompartments obs(cx); + if (!obs.init()) + return false; + + for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) { + GlobalObject* global = r.front(); + JSCompartment* comp = global->compartment(); + + if (comp->debuggerObservesCoverage() == observing) + continue; + + // Invalidate and recompile a compartment to add or remove PCCounts + // increments. We have to eagerly invalidate, as otherwise we might have + // dangling pointers to freed PCCounts. + if (!obs.add(comp)) + return false; + } + + // If any frame on the stack belongs to the debuggee, then we cannot update + // the ScriptCounts, because this would imply to invalidate a Debugger.Frame + // to recompile it with/without ScriptCount support. + for (ScriptFrameIter iter(cx); + !iter.done(); + ++iter) + { + if (obs.shouldMarkAsDebuggee(iter)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_NOT_IDLE); + return false; + } + } + + if (!updateExecutionObservability(cx, obs, observing)) + return false; + + // All compartments can safely be toggled, and all scripts will be + // recompiled. Thus we can update each compartment accordingly. + typedef ExecutionObservableCompartments::CompartmentRange CompartmentRange; + for (CompartmentRange r = obs.compartments()->all(); !r.empty(); r.popFront()) + r.front()->updateDebuggerObservesCoverage(); + + return true; +} + +void +Debugger::updateObservesAsmJSOnDebuggees(IsObserving observing) +{ + for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) { + GlobalObject* global = r.front(); + JSCompartment* comp = global->compartment(); + + if (comp->debuggerObservesAsmJS() == observing) + continue; + + comp->updateDebuggerObservesAsmJS(); + } +} + + +/*** Allocations Tracking *************************************************************************/ + +/* static */ bool +Debugger::cannotTrackAllocations(const GlobalObject& global) +{ + auto existingCallback = global.compartment()->getAllocationMetadataBuilder(); + return existingCallback && existingCallback != &SavedStacks::metadataBuilder; +} + +/* static */ bool +Debugger::isObservedByDebuggerTrackingAllocations(const GlobalObject& debuggee) +{ + if (auto* v = debuggee.getDebuggers()) { + for (auto p = v->begin(); p != v->end(); p++) { + if ((*p)->trackingAllocationSites && (*p)->enabled) { + return true; + } + } + } + + return false; +} + +/* static */ bool +Debugger::addAllocationsTracking(JSContext* cx, Handle<GlobalObject*> debuggee) +{ + // Precondition: the given global object is being observed by at least one + // Debugger that is tracking allocations. + MOZ_ASSERT(isObservedByDebuggerTrackingAllocations(*debuggee)); + + if (Debugger::cannotTrackAllocations(*debuggee)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_OBJECT_METADATA_CALLBACK_ALREADY_SET); + return false; + } + + debuggee->compartment()->setAllocationMetadataBuilder(&SavedStacks::metadataBuilder); + debuggee->compartment()->chooseAllocationSamplingProbability(); + return true; +} + +/* static */ void +Debugger::removeAllocationsTracking(GlobalObject& global) +{ + // If there are still Debuggers that are observing allocations, we cannot + // remove the metadata callback yet. Recompute the sampling probability + // based on the remaining debuggers' needs. + if (isObservedByDebuggerTrackingAllocations(global)) { + global.compartment()->chooseAllocationSamplingProbability(); + return; + } + + global.compartment()->forgetAllocationMetadataBuilder(); +} + +bool +Debugger::addAllocationsTrackingForAllDebuggees(JSContext* cx) +{ + MOZ_ASSERT(trackingAllocationSites); + + // We don't want to end up in a state where we added allocations + // tracking to some of our debuggees, but failed to do so for + // others. Before attempting to start tracking allocations in *any* of + // our debuggees, ensure that we will be able to track allocations for + // *all* of our debuggees. + for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) { + if (Debugger::cannotTrackAllocations(*r.front().get())) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_OBJECT_METADATA_CALLBACK_ALREADY_SET); + return false; + } + } + + Rooted<GlobalObject*> g(cx); + for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) { + // This should always succeed, since we already checked for the + // error case above. + g = r.front().get(); + MOZ_ALWAYS_TRUE(Debugger::addAllocationsTracking(cx, g)); + } + + return true; +} + +void +Debugger::removeAllocationsTrackingForAllDebuggees() +{ + for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) + Debugger::removeAllocationsTracking(*r.front().get()); + + allocationsLog.clear(); +} + + + +/*** Debugger JSObjects **************************************************************************/ + +void +Debugger::markCrossCompartmentEdges(JSTracer* trc) +{ + objects.markCrossCompartmentEdges<DebuggerObject_trace>(trc); + environments.markCrossCompartmentEdges<DebuggerEnv_trace>(trc); + scripts.markCrossCompartmentEdges<DebuggerScript_trace>(trc); + sources.markCrossCompartmentEdges<DebuggerSource_trace>(trc); + wasmInstanceScripts.markCrossCompartmentEdges<DebuggerScript_trace>(trc); + wasmInstanceSources.markCrossCompartmentEdges<DebuggerSource_trace>(trc); +} + +/* + * Ordinarily, WeakMap keys and values are marked because at some point it was + * discovered that the WeakMap was live; that is, some object containing the + * WeakMap was marked during mark phase. + * + * However, during zone GC, we have to do something about cross-compartment + * edges in non-GC'd compartments. Since the source may be live, we + * conservatively assume it is and mark the edge. + * + * Each Debugger object keeps four cross-compartment WeakMaps: objects, scripts, + * script source objects, and environments. They have the property that all + * their values are in the same compartment as the Debugger object, but we have + * to mark the keys and the private pointer in the wrapper object. + * + * We must scan all Debugger objects regardless of whether they *currently* have + * any debuggees in a compartment being GC'd, because the WeakMap entries + * persist even when debuggees are removed. + * + * This happens during the initial mark phase, not iterative marking, because + * all the edges being reported here are strong references. + * + * This method is also used during compacting GC to update cross compartment + * pointers in zones that are not currently being compacted. + */ +/* static */ void +Debugger::markIncomingCrossCompartmentEdges(JSTracer* trc) +{ + JSRuntime* rt = trc->runtime(); + gc::State state = rt->gc.state(); + MOZ_ASSERT(state == gc::State::MarkRoots || state == gc::State::Compact); + + for (Debugger* dbg : rt->debuggerList) { + Zone* zone = MaybeForwarded(dbg->object.get())->zone(); + if ((state == gc::State::MarkRoots && !zone->isCollecting()) || + (state == gc::State::Compact && !zone->isGCCompacting())) + { + dbg->markCrossCompartmentEdges(trc); + } + } +} + +/* + * This method has two tasks: + * 1. Mark Debugger objects that are unreachable except for debugger hooks that + * may yet be called. + * 2. Mark breakpoint handlers. + * + * This happens during the iterative part of the GC mark phase. This method + * returns true if it has to mark anything; GC calls it repeatedly until it + * returns false. + */ +/* static */ bool +Debugger::markAllIteratively(GCMarker* trc) +{ + bool markedAny = false; + + /* + * Find all Debugger objects in danger of GC. This code is a little + * convoluted since the easiest way to find them is via their debuggees. + */ + JSRuntime* rt = trc->runtime(); + for (CompartmentsIter c(rt, SkipAtoms); !c.done(); c.next()) { + if (c->isDebuggee()) { + GlobalObject* global = c->unsafeUnbarrieredMaybeGlobal(); + if (!IsMarkedUnbarriered(rt, &global)) + continue; + + /* + * Every debuggee has at least one debugger, so in this case + * getDebuggers can't return nullptr. + */ + const GlobalObject::DebuggerVector* debuggers = global->getDebuggers(); + MOZ_ASSERT(debuggers); + for (auto p = debuggers->begin(); p != debuggers->end(); p++) { + Debugger* dbg = *p; + + /* + * dbg is a Debugger with at least one debuggee. Check three things: + * - dbg is actually in a compartment that is being marked + * - it isn't already marked + * - it actually has hooks that might be called + */ + GCPtrNativeObject& dbgobj = dbg->toJSObjectRef(); + if (!dbgobj->zone()->isGCMarking()) + continue; + + bool dbgMarked = IsMarked(rt, &dbgobj); + if (!dbgMarked && dbg->hasAnyLiveHooks(rt)) { + /* + * obj could be reachable only via its live, enabled + * debugger hooks, which may yet be called. + */ + TraceEdge(trc, &dbgobj, "enabled Debugger"); + markedAny = true; + dbgMarked = true; + } + + if (dbgMarked) { + /* Search for breakpoints to mark. */ + for (Breakpoint* bp = dbg->firstBreakpoint(); bp; bp = bp->nextInDebugger()) { + if (IsMarkedUnbarriered(rt, &bp->site->script)) { + /* + * The debugger and the script are both live. + * Therefore the breakpoint handler is live. + */ + if (!IsMarked(rt, &bp->getHandlerRef())) { + TraceEdge(trc, &bp->getHandlerRef(), "breakpoint handler"); + markedAny = true; + } + } + } + } + } + } + } + return markedAny; +} + +/* + * Mark all debugger-owned GC things unconditionally. This is used by the minor + * GC: the minor GC cannot apply the weak constraints of the full GC because it + * visits only part of the heap. + */ +/* static */ void +Debugger::markAll(JSTracer* trc) +{ + JSRuntime* rt = trc->runtime(); + for (Debugger* dbg : rt->debuggerList) { + for (WeakGlobalObjectSet::Enum e(dbg->debuggees); !e.empty(); e.popFront()) + TraceManuallyBarrieredEdge(trc, e.mutableFront().unsafeGet(), "Global Object"); + + GCPtrNativeObject& dbgobj = dbg->toJSObjectRef(); + TraceEdge(trc, &dbgobj, "Debugger Object"); + + dbg->scripts.trace(trc); + dbg->sources.trace(trc); + dbg->objects.trace(trc); + dbg->environments.trace(trc); + dbg->wasmInstanceScripts.trace(trc); + dbg->wasmInstanceSources.trace(trc); + + for (Breakpoint* bp = dbg->firstBreakpoint(); bp; bp = bp->nextInDebugger()) { + TraceManuallyBarrieredEdge(trc, &bp->site->script, "breakpoint script"); + TraceEdge(trc, &bp->getHandlerRef(), "breakpoint handler"); + } + } +} + +/* static */ void +Debugger::traceObject(JSTracer* trc, JSObject* obj) +{ + if (Debugger* dbg = Debugger::fromJSObject(obj)) + dbg->trace(trc); +} + +void +Debugger::trace(JSTracer* trc) +{ + TraceNullableEdge(trc, &uncaughtExceptionHook, "hooks"); + + /* + * Mark Debugger.Frame objects. These are all reachable from JS, because the + * corresponding JS frames are still on the stack. + * + * (Once we support generator frames properly, we will need + * weakly-referenced Debugger.Frame objects as well, for suspended generator + * frames.) + */ + for (FrameMap::Range r = frames.all(); !r.empty(); r.popFront()) { + HeapPtr<DebuggerFrame*>& frameobj = r.front().value(); + MOZ_ASSERT(MaybeForwarded(frameobj.get())->getPrivate()); + TraceEdge(trc, &frameobj, "live Debugger.Frame"); + } + + allocationsLog.trace(trc); + + /* Trace the weak map from JSScript instances to Debugger.Script objects. */ + scripts.trace(trc); + + /* Trace the referent -> Debugger.Source weak map */ + sources.trace(trc); + + /* Trace the referent -> Debugger.Object weak map. */ + objects.trace(trc); + + /* Trace the referent -> Debugger.Environment weak map. */ + environments.trace(trc); + + /* Trace the WasmInstanceObject -> synthesized Debugger.Script weak map. */ + wasmInstanceScripts.trace(trc); + + /* Trace the WasmInstanceObject -> synthesized Debugger.Source weak map. */ + wasmInstanceSources.trace(trc); +} + +/* static */ void +Debugger::sweepAll(FreeOp* fop) +{ + JSRuntime* rt = fop->runtime(); + + for (Debugger* dbg : rt->debuggerList) { + if (IsAboutToBeFinalized(&dbg->object)) { + /* + * dbg is being GC'd. Detach it from its debuggees. The debuggee + * might be GC'd too. Since detaching requires access to both + * objects, this must be done before finalize time. + */ + for (WeakGlobalObjectSet::Enum e(dbg->debuggees); !e.empty(); e.popFront()) + dbg->removeDebuggeeGlobal(fop, e.front().unbarrieredGet(), &e); + } + } +} + +/* static */ void +Debugger::detachAllDebuggersFromGlobal(FreeOp* fop, GlobalObject* global) +{ + const GlobalObject::DebuggerVector* debuggers = global->getDebuggers(); + MOZ_ASSERT(!debuggers->empty()); + while (!debuggers->empty()) + debuggers->back()->removeDebuggeeGlobal(fop, global, nullptr); +} + +/* static */ void +Debugger::findZoneEdges(Zone* zone, js::gc::ZoneComponentFinder& finder) +{ + /* + * For debugger cross compartment wrappers, add edges in the opposite + * direction to those already added by JSCompartment::findOutgoingEdges. + * This ensure that debuggers and their debuggees are finalized in the same + * group. + */ + for (Debugger* dbg : zone->runtimeFromMainThread()->debuggerList) { + Zone* w = dbg->object->zone(); + if (w == zone || !w->isGCMarking()) + continue; + if (dbg->debuggeeZones.has(zone) || + dbg->scripts.hasKeyInZone(zone) || + dbg->sources.hasKeyInZone(zone) || + dbg->objects.hasKeyInZone(zone) || + dbg->environments.hasKeyInZone(zone) || + dbg->wasmInstanceScripts.hasKeyInZone(zone) || + dbg->wasmInstanceSources.hasKeyInZone(zone)) + { + finder.addEdgeTo(w); + } + } +} + +/* static */ void +Debugger::finalize(FreeOp* fop, JSObject* obj) +{ + MOZ_ASSERT(fop->onMainThread()); + + Debugger* dbg = fromJSObject(obj); + if (!dbg) + return; + fop->delete_(dbg); +} + +const ClassOps Debugger::classOps_ = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + Debugger::finalize, + nullptr, /* call */ + nullptr, /* hasInstance */ + nullptr, /* construct */ + Debugger::traceObject +}; + +const Class Debugger::class_ = { + "Debugger", + JSCLASS_HAS_PRIVATE | + JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUG_COUNT) | + JSCLASS_FOREGROUND_FINALIZE, + &Debugger::classOps_ +}; + +static Debugger* +Debugger_fromThisValue(JSContext* cx, const CallArgs& args, const char* fnname) +{ + JSObject* thisobj = NonNullObject(cx, args.thisv()); + if (!thisobj) + return nullptr; + if (thisobj->getClass() != &Debugger::class_) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, + "Debugger", fnname, thisobj->getClass()->name); + return nullptr; + } + + /* + * Forbid Debugger.prototype, which is of the Debugger JSClass but isn't + * really a Debugger object. The prototype object is distinguished by + * having a nullptr private value. + */ + Debugger* dbg = Debugger::fromJSObject(thisobj); + if (!dbg) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, + "Debugger", fnname, "prototype object"); + } + return dbg; +} + +#define THIS_DEBUGGER(cx, argc, vp, fnname, args, dbg) \ + CallArgs args = CallArgsFromVp(argc, vp); \ + Debugger* dbg = Debugger_fromThisValue(cx, args, fnname); \ + if (!dbg) \ + return false + +/* static */ bool +Debugger::getEnabled(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "get enabled", args, dbg); + args.rval().setBoolean(dbg->enabled); + return true; +} + +/* static */ bool +Debugger::setEnabled(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "set enabled", args, dbg); + if (!args.requireAtLeast(cx, "Debugger.set enabled", 1)) + return false; + + bool wasEnabled = dbg->enabled; + dbg->enabled = ToBoolean(args[0]); + + if (wasEnabled != dbg->enabled) { + if (dbg->trackingAllocationSites) { + if (wasEnabled) { + dbg->removeAllocationsTrackingForAllDebuggees(); + } else { + if (!dbg->addAllocationsTrackingForAllDebuggees(cx)) { + dbg->enabled = false; + return false; + } + } + } + + for (Breakpoint* bp = dbg->firstBreakpoint(); bp; bp = bp->nextInDebugger()) { + if (!wasEnabled) + bp->site->inc(cx->runtime()->defaultFreeOp()); + else + bp->site->dec(cx->runtime()->defaultFreeOp()); + } + + /* + * Add or remove ourselves from the runtime's list of Debuggers + * that care about new globals. + */ + if (dbg->getHook(OnNewGlobalObject)) { + if (!wasEnabled) { + /* If we were not enabled, the link should be a singleton list. */ + MOZ_ASSERT(JS_CLIST_IS_EMPTY(&dbg->onNewGlobalObjectWatchersLink)); + JS_APPEND_LINK(&dbg->onNewGlobalObjectWatchersLink, + &cx->runtime()->onNewGlobalObjectWatchers); + } else { + /* If we were enabled, the link should be inserted in the list. */ + MOZ_ASSERT(!JS_CLIST_IS_EMPTY(&dbg->onNewGlobalObjectWatchersLink)); + JS_REMOVE_AND_INIT_LINK(&dbg->onNewGlobalObjectWatchersLink); + } + } + + // Ensure the compartment is observable if we are re-enabling a + // Debugger with hooks that observe all execution. + if (!dbg->updateObservesAllExecutionOnDebuggees(cx, dbg->observesAllExecution())) + return false; + + // Note: To toogle code coverage, we currently need to have no live + // stack frame, thus the coverage does not depend on the enabled flag. + + dbg->updateObservesAsmJSOnDebuggees(dbg->observesAsmJS()); + } + + args.rval().setUndefined(); + return true; +} + +/* static */ bool +Debugger::getHookImpl(JSContext* cx, CallArgs& args, Debugger& dbg, Hook which) +{ + MOZ_ASSERT(which >= 0 && which < HookCount); + args.rval().set(dbg.object->getReservedSlot(JSSLOT_DEBUG_HOOK_START + which)); + return true; +} + +/* static */ bool +Debugger::setHookImpl(JSContext* cx, CallArgs& args, Debugger& dbg, Hook which) +{ + MOZ_ASSERT(which >= 0 && which < HookCount); + if (!args.requireAtLeast(cx, "Debugger.setHook", 1)) + return false; + if (args[0].isObject()) { + if (!args[0].toObject().isCallable()) + return ReportIsNotFunction(cx, args[0], args.length() - 1); + } else if (!args[0].isUndefined()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_CALLABLE_OR_UNDEFINED); + return false; + } + uint32_t slot = JSSLOT_DEBUG_HOOK_START + which; + RootedValue oldHook(cx, dbg.object->getReservedSlot(slot)); + dbg.object->setReservedSlot(slot, args[0]); + if (hookObservesAllExecution(which)) { + if (!dbg.updateObservesAllExecutionOnDebuggees(cx, dbg.observesAllExecution())) { + dbg.object->setReservedSlot(slot, oldHook); + return false; + } + } + args.rval().setUndefined(); + return true; +} + +/* static */ bool +Debugger::getOnDebuggerStatement(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "(get onDebuggerStatement)", args, dbg); + return getHookImpl(cx, args, *dbg, OnDebuggerStatement); +} + +/* static */ bool +Debugger::setOnDebuggerStatement(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "(set onDebuggerStatement)", args, dbg); + return setHookImpl(cx, args, *dbg, OnDebuggerStatement); +} + +/* static */ bool +Debugger::getOnExceptionUnwind(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "(get onExceptionUnwind)", args, dbg); + return getHookImpl(cx, args, *dbg, OnExceptionUnwind); +} + +/* static */ bool +Debugger::setOnExceptionUnwind(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "(set onExceptionUnwind)", args, dbg); + return setHookImpl(cx, args, *dbg, OnExceptionUnwind); +} + +/* static */ bool +Debugger::getOnNewScript(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "(get onNewScript)", args, dbg); + return getHookImpl(cx, args, *dbg, OnNewScript); +} + +/* static */ bool +Debugger::setOnNewScript(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "(set onNewScript)", args, dbg); + return setHookImpl(cx, args, *dbg, OnNewScript); +} + +/* static */ bool +Debugger::getOnNewPromise(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "(get onNewPromise)", args, dbg); + return getHookImpl(cx, args, *dbg, OnNewPromise); +} + +/* static */ bool +Debugger::setOnNewPromise(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "(set onNewPromise)", args, dbg); + return setHookImpl(cx, args, *dbg, OnNewPromise); +} + +/* static */ bool +Debugger::getOnPromiseSettled(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "(get onPromiseSettled)", args, dbg); + return getHookImpl(cx, args, *dbg, OnPromiseSettled); +} + +/* static */ bool +Debugger::setOnPromiseSettled(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "(set onPromiseSettled)", args, dbg); + return setHookImpl(cx, args, *dbg, OnPromiseSettled); +} + +/* static */ bool +Debugger::getOnEnterFrame(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "(get onEnterFrame)", args, dbg); + return getHookImpl(cx, args, *dbg, OnEnterFrame); +} + +/* static */ bool +Debugger::setOnEnterFrame(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "(set onEnterFrame)", args, dbg); + return setHookImpl(cx, args, *dbg, OnEnterFrame); +} + +/* static */ bool +Debugger::getOnNewGlobalObject(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "(get onNewGlobalObject)", args, dbg); + return getHookImpl(cx, args, *dbg, OnNewGlobalObject); +} + +/* static */ bool +Debugger::setOnNewGlobalObject(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "setOnNewGlobalObject", args, dbg); + RootedObject oldHook(cx, dbg->getHook(OnNewGlobalObject)); + + if (!setHookImpl(cx, args, *dbg, OnNewGlobalObject)) + return false; + + /* + * Add or remove ourselves from the runtime's list of Debuggers that + * care about new globals. + */ + if (dbg->enabled) { + JSObject* newHook = dbg->getHook(OnNewGlobalObject); + if (!oldHook && newHook) { + /* If we didn't have a hook, the link should be a singleton list. */ + MOZ_ASSERT(JS_CLIST_IS_EMPTY(&dbg->onNewGlobalObjectWatchersLink)); + JS_APPEND_LINK(&dbg->onNewGlobalObjectWatchersLink, + &cx->runtime()->onNewGlobalObjectWatchers); + } else if (oldHook && !newHook) { + /* If we did have a hook, the link should be inserted in the list. */ + MOZ_ASSERT(!JS_CLIST_IS_EMPTY(&dbg->onNewGlobalObjectWatchersLink)); + JS_REMOVE_AND_INIT_LINK(&dbg->onNewGlobalObjectWatchersLink); + } + } + + return true; +} + +/* static */ bool +Debugger::getUncaughtExceptionHook(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "get uncaughtExceptionHook", args, dbg); + args.rval().setObjectOrNull(dbg->uncaughtExceptionHook); + return true; +} + +/* static */ bool +Debugger::setUncaughtExceptionHook(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "set uncaughtExceptionHook", args, dbg); + if (!args.requireAtLeast(cx, "Debugger.set uncaughtExceptionHook", 1)) + return false; + if (!args[0].isNull() && (!args[0].isObject() || !args[0].toObject().isCallable())) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ASSIGN_FUNCTION_OR_NULL, + "uncaughtExceptionHook"); + return false; + } + dbg->uncaughtExceptionHook = args[0].toObjectOrNull(); + args.rval().setUndefined(); + return true; +} + +/* static */ bool +Debugger::getAllowUnobservedAsmJS(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "get allowUnobservedAsmJS", args, dbg); + args.rval().setBoolean(dbg->allowUnobservedAsmJS); + return true; +} + +/* static */ bool +Debugger::setAllowUnobservedAsmJS(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "set allowUnobservedAsmJS", args, dbg); + if (!args.requireAtLeast(cx, "Debugger.set allowUnobservedAsmJS", 1)) + return false; + dbg->allowUnobservedAsmJS = ToBoolean(args[0]); + + for (WeakGlobalObjectSet::Range r = dbg->debuggees.all(); !r.empty(); r.popFront()) { + GlobalObject* global = r.front(); + JSCompartment* comp = global->compartment(); + comp->updateDebuggerObservesAsmJS(); + } + + args.rval().setUndefined(); + return true; +} + +/* static */ bool +Debugger::getCollectCoverageInfo(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "get collectCoverageInfo", args, dbg); + args.rval().setBoolean(dbg->collectCoverageInfo); + return true; +} + +/* static */ bool +Debugger::setCollectCoverageInfo(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "set collectCoverageInfo", args, dbg); + if (!args.requireAtLeast(cx, "Debugger.set collectCoverageInfo", 1)) + return false; + dbg->collectCoverageInfo = ToBoolean(args[0]); + + IsObserving observing = dbg->collectCoverageInfo ? Observing : NotObserving; + if (!dbg->updateObservesCoverageOnDebuggees(cx, observing)) + return false; + + args.rval().setUndefined(); + return true; +} + +/* static */ bool +Debugger::getMemory(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "get memory", args, dbg); + Value memoryValue = dbg->object->getReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE); + + if (!memoryValue.isObject()) { + RootedObject memory(cx, DebuggerMemory::create(cx, dbg)); + if (!memory) + return false; + memoryValue = ObjectValue(*memory); + } + + args.rval().set(memoryValue); + return true; +} + +/* + * Given a value used to designate a global (there's quite a variety; see the + * docs), return the actual designee. + * + * Note that this does not check whether the designee is marked "invisible to + * Debugger" or not; different callers need to handle invisible-to-Debugger + * globals in different ways. + */ +GlobalObject* +Debugger::unwrapDebuggeeArgument(JSContext* cx, const Value& v) +{ + if (!v.isObject()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "argument", "not a global object"); + return nullptr; + } + + RootedObject obj(cx, &v.toObject()); + + /* If it's a Debugger.Object belonging to this debugger, dereference that. */ + if (obj->getClass() == &DebuggerObject::class_) { + RootedValue rv(cx, v); + if (!unwrapDebuggeeValue(cx, &rv)) + return nullptr; + obj = &rv.toObject(); + } + + /* If we have a cross-compartment wrapper, dereference as far as is secure. */ + obj = CheckedUnwrap(obj); + if (!obj) { + JS_ReportErrorASCII(cx, "Permission denied to access object"); + return nullptr; + } + + /* If that produced a WindowProxy, get the Window (global). */ + obj = ToWindowIfWindowProxy(obj); + + /* If that didn't produce a global object, it's an error. */ + if (!obj->is<GlobalObject>()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "argument", "not a global object"); + return nullptr; + } + + return &obj->as<GlobalObject>(); +} + +/* static */ bool +Debugger::addDebuggee(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "addDebuggee", args, dbg); + if (!args.requireAtLeast(cx, "Debugger.addDebuggee", 1)) + return false; + Rooted<GlobalObject*> global(cx, dbg->unwrapDebuggeeArgument(cx, args[0])); + if (!global) + return false; + + if (!dbg->addDebuggeeGlobal(cx, global)) + return false; + + RootedValue v(cx, ObjectValue(*global)); + if (!dbg->wrapDebuggeeValue(cx, &v)) + return false; + args.rval().set(v); + return true; +} + +/* static */ bool +Debugger::addAllGlobalsAsDebuggees(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "addAllGlobalsAsDebuggees", args, dbg); + for (ZonesIter zone(cx->runtime(), SkipAtoms); !zone.done(); zone.next()) { + for (CompartmentsInZoneIter c(zone); !c.done(); c.next()) { + if (c == dbg->object->compartment() || c->creationOptions().invisibleToDebugger()) + continue; + c->scheduledForDestruction = false; + GlobalObject* global = c->maybeGlobal(); + if (global) { + Rooted<GlobalObject*> rg(cx, global); + if (!dbg->addDebuggeeGlobal(cx, rg)) + return false; + } + } + } + + args.rval().setUndefined(); + return true; +} + +/* static */ bool +Debugger::removeDebuggee(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "removeDebuggee", args, dbg); + + if (!args.requireAtLeast(cx, "Debugger.removeDebuggee", 1)) + return false; + Rooted<GlobalObject*> global(cx, dbg->unwrapDebuggeeArgument(cx, args[0])); + if (!global) + return false; + + ExecutionObservableCompartments obs(cx); + if (!obs.init()) + return false; + + if (dbg->debuggees.has(global)) { + dbg->removeDebuggeeGlobal(cx->runtime()->defaultFreeOp(), global, nullptr); + + // Only update the compartment if there are no Debuggers left, as it's + // expensive to check if no other Debugger has a live script or frame hook + // on any of the current on-stack debuggee frames. + if (global->getDebuggers()->empty() && !obs.add(global->compartment())) + return false; + if (!updateExecutionObservability(cx, obs, NotObserving)) + return false; + } + + args.rval().setUndefined(); + return true; +} + +/* static */ bool +Debugger::removeAllDebuggees(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "removeAllDebuggees", args, dbg); + + ExecutionObservableCompartments obs(cx); + if (!obs.init()) + return false; + + for (WeakGlobalObjectSet::Enum e(dbg->debuggees); !e.empty(); e.popFront()) { + Rooted<GlobalObject*> global(cx, e.front()); + dbg->removeDebuggeeGlobal(cx->runtime()->defaultFreeOp(), global, &e); + + // See note about adding to the observable set in removeDebuggee. + if (global->getDebuggers()->empty() && !obs.add(global->compartment())) + return false; + } + + if (!updateExecutionObservability(cx, obs, NotObserving)) + return false; + + args.rval().setUndefined(); + return true; +} + +/* static */ bool +Debugger::hasDebuggee(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "hasDebuggee", args, dbg); + if (!args.requireAtLeast(cx, "Debugger.hasDebuggee", 1)) + return false; + GlobalObject* global = dbg->unwrapDebuggeeArgument(cx, args[0]); + if (!global) + return false; + args.rval().setBoolean(!!dbg->debuggees.lookup(global)); + return true; +} + +/* static */ bool +Debugger::getDebuggees(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "getDebuggees", args, dbg); + + // Obtain the list of debuggees before wrapping each debuggee, as a GC could + // update the debuggees set while we are iterating it. + unsigned count = dbg->debuggees.count(); + AutoValueVector debuggees(cx); + if (!debuggees.resize(count)) + return false; + unsigned i = 0; + { + JS::AutoCheckCannotGC nogc; + for (WeakGlobalObjectSet::Enum e(dbg->debuggees); !e.empty(); e.popFront()) + debuggees[i++].setObject(*e.front().get()); + } + + RootedArrayObject arrobj(cx, NewDenseFullyAllocatedArray(cx, count)); + if (!arrobj) + return false; + arrobj->ensureDenseInitializedLength(cx, 0, count); + for (i = 0; i < count; i++) { + RootedValue v(cx, debuggees[i]); + if (!dbg->wrapDebuggeeValue(cx, &v)) + return false; + arrobj->setDenseElement(i, v); + } + + args.rval().setObject(*arrobj); + return true; +} + +/* static */ bool +Debugger::getNewestFrame(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "getNewestFrame", args, dbg); + + /* Since there may be multiple contexts, use AllScriptFramesIter. */ + for (AllScriptFramesIter i(cx); !i.done(); ++i) { + if (dbg->observesFrame(i)) { + // Ensure that Ion frames are rematerialized. Only rematerialized + // Ion frames may be used as AbstractFramePtrs. + if (i.isIon() && !i.ensureHasRematerializedFrame(cx)) + return false; + AbstractFramePtr frame = i.abstractFramePtr(); + ScriptFrameIter iter(i.activation()->cx()); + while (!iter.hasUsableAbstractFramePtr() || iter.abstractFramePtr() != frame) + ++iter; + return dbg->getScriptFrame(cx, iter, args.rval()); + } + } + args.rval().setNull(); + return true; +} + +/* static */ bool +Debugger::clearAllBreakpoints(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "clearAllBreakpoints", args, dbg); + for (WeakGlobalObjectSet::Range r = dbg->debuggees.all(); !r.empty(); r.popFront()) + r.front()->compartment()->clearBreakpointsIn(cx->runtime()->defaultFreeOp(), + dbg, nullptr); + return true; +} + +/* static */ bool +Debugger::construct(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + /* Check that the arguments, if any, are cross-compartment wrappers. */ + for (unsigned i = 0; i < args.length(); i++) { + JSObject* argobj = NonNullObject(cx, args[i]); + if (!argobj) + return false; + if (!argobj->is<CrossCompartmentWrapperObject>()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_CCW_REQUIRED, + "Debugger"); + return false; + } + } + + /* Get Debugger.prototype. */ + RootedValue v(cx); + RootedObject callee(cx, &args.callee()); + if (!GetProperty(cx, callee, callee, cx->names().prototype, &v)) + return false; + RootedNativeObject proto(cx, &v.toObject().as<NativeObject>()); + MOZ_ASSERT(proto->getClass() == &Debugger::class_); + /* + * Make the new Debugger object. Each one has a reference to + * Debugger.{Frame,Object,Script,Memory}.prototype in reserved slots. The + * rest of the reserved slots are for hooks; they default to undefined. + */ + RootedNativeObject obj(cx, NewNativeObjectWithGivenProto(cx, &Debugger::class_, proto)); + if (!obj) + return false; + for (unsigned slot = JSSLOT_DEBUG_PROTO_START; slot < JSSLOT_DEBUG_PROTO_STOP; slot++) + obj->setReservedSlot(slot, proto->getReservedSlot(slot)); + obj->setReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE, NullValue()); + + Debugger* debugger; + { + /* Construct the underlying C++ object. */ + auto dbg = cx->make_unique<Debugger>(cx, obj.get()); + if (!dbg || !dbg->init(cx)) + return false; + + debugger = dbg.release(); + obj->setPrivate(debugger); // owns the released pointer + } + + /* Add the initial debuggees, if any. */ + for (unsigned i = 0; i < args.length(); i++) { + Rooted<GlobalObject*> + debuggee(cx, &args[i].toObject().as<ProxyObject>().private_().toObject().global()); + if (!debugger->addDebuggeeGlobal(cx, debuggee)) + return false; + } + + args.rval().setObject(*obj); + return true; +} + +bool +Debugger::addDebuggeeGlobal(JSContext* cx, Handle<GlobalObject*> global) +{ + if (debuggees.has(global)) + return true; + + // Callers should generally be unable to get a reference to a debugger- + // invisible global in order to pass it to addDebuggee. But this is possible + // with certain testing aides we expose in the shell, so just make addDebuggee + // throw in that case. + JSCompartment* debuggeeCompartment = global->compartment(); + if (debuggeeCompartment->creationOptions().invisibleToDebugger()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_CANT_DEBUG_GLOBAL); + return false; + } + + /* + * Check for cycles. If global's compartment is reachable from this + * Debugger object's compartment by following debuggee-to-debugger links, + * then adding global would create a cycle. (Typically nobody is debugging + * the debugger, in which case we zip through this code without looping.) + */ + Vector<JSCompartment*> visited(cx); + if (!visited.append(object->compartment())) + return false; + for (size_t i = 0; i < visited.length(); i++) { + JSCompartment* c = visited[i]; + if (c == debuggeeCompartment) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_LOOP); + return false; + } + + /* + * Find all compartments containing debuggers debugging c's global + * object. Add those compartments to visited. + */ + if (c->isDebuggee()) { + GlobalObject::DebuggerVector* v = c->maybeGlobal()->getDebuggers(); + for (auto p = v->begin(); p != v->end(); p++) { + JSCompartment* next = (*p)->object->compartment(); + if (Find(visited, next) == visited.end() && !visited.append(next)) + return false; + } + } + } + + /* + * For global to become this js::Debugger's debuggee: + * + * 1. this js::Debugger must be in global->getDebuggers(), + * 2. global must be in this->debuggees, + * 3. it must be in zone->getDebuggers(), + * 4. the debuggee's zone must be in this->debuggeeZones, + * 5. if we are tracking allocations, the SavedStacksMetadataBuilder must be + * installed for this compartment, and + * 6. JSCompartment::isDebuggee()'s bit must be set. + * + * All six indications must be kept consistent. + */ + + AutoCompartment ac(cx, global); + Zone* zone = global->zone(); + + // (1) + auto* globalDebuggers = GlobalObject::getOrCreateDebuggers(cx, global); + if (!globalDebuggers) + return false; + if (!globalDebuggers->append(this)) { + ReportOutOfMemory(cx); + return false; + } + auto globalDebuggersGuard = MakeScopeExit([&] { + globalDebuggers->popBack(); + }); + + // (2) + if (!debuggees.put(global)) { + ReportOutOfMemory(cx); + return false; + } + auto debuggeesGuard = MakeScopeExit([&] { + debuggees.remove(global); + }); + + bool addingZoneRelation = !debuggeeZones.has(zone); + + // (3) + auto* zoneDebuggers = zone->getOrCreateDebuggers(cx); + if (!zoneDebuggers) + return false; + if (addingZoneRelation && !zoneDebuggers->append(this)) { + ReportOutOfMemory(cx); + return false; + } + auto zoneDebuggersGuard = MakeScopeExit([&] { + if (addingZoneRelation) + zoneDebuggers->popBack(); + }); + + // (4) + if (addingZoneRelation && !debuggeeZones.put(zone)) { + ReportOutOfMemory(cx); + return false; + } + auto debuggeeZonesGuard = MakeScopeExit([&] { + if (addingZoneRelation) + debuggeeZones.remove(zone); + }); + + // (5) + if (trackingAllocationSites && enabled && !Debugger::addAllocationsTracking(cx, global)) + return false; + + auto allocationsTrackingGuard = MakeScopeExit([&] { + if (trackingAllocationSites && enabled) + Debugger::removeAllocationsTracking(*global); + }); + + // (6) + AutoRestoreCompartmentDebugMode debugModeGuard(debuggeeCompartment); + debuggeeCompartment->setIsDebuggee(); + debuggeeCompartment->updateDebuggerObservesAsmJS(); + debuggeeCompartment->updateDebuggerObservesCoverage(); + if (observesAllExecution() && !ensureExecutionObservabilityOfCompartment(cx, debuggeeCompartment)) + return false; + + globalDebuggersGuard.release(); + debuggeesGuard.release(); + zoneDebuggersGuard.release(); + debuggeeZonesGuard.release(); + allocationsTrackingGuard.release(); + debugModeGuard.release(); + return true; +} + +void +Debugger::recomputeDebuggeeZoneSet() +{ + AutoEnterOOMUnsafeRegion oomUnsafe; + debuggeeZones.clear(); + for (auto range = debuggees.all(); !range.empty(); range.popFront()) { + if (!debuggeeZones.put(range.front().unbarrieredGet()->zone())) + oomUnsafe.crash("Debugger::removeDebuggeeGlobal"); + } +} + +template <typename T> +static T* +findDebuggerInVector(Debugger* dbg, Vector<T, 0, js::SystemAllocPolicy>* vec) +{ + T* p; + for (p = vec->begin(); p != vec->end(); p++) { + if (*p == dbg) + break; + } + MOZ_ASSERT(p != vec->end()); + return p; +} + +void +Debugger::removeDebuggeeGlobal(FreeOp* fop, GlobalObject* global, + WeakGlobalObjectSet::Enum* debugEnum) +{ + /* + * The caller might have found global by enumerating this->debuggees; if + * so, use HashSet::Enum::removeFront rather than HashSet::remove below, + * to avoid invalidating the live enumerator. + */ + MOZ_ASSERT(debuggees.has(global)); + MOZ_ASSERT(debuggeeZones.has(global->zone())); + MOZ_ASSERT_IF(debugEnum, debugEnum->front().unbarrieredGet() == global); + + /* + * FIXME Debugger::slowPathOnLeaveFrame needs to kill all Debugger.Frame + * objects referring to a particular JS stack frame. This is hard if + * Debugger objects that are no longer debugging the relevant global might + * have live Frame objects. So we take the easy way out and kill them here. + * This is a bug, since it's observable and contrary to the spec. One + * possible fix would be to put such objects into a compartment-wide bag + * which slowPathOnLeaveFrame would have to examine. + */ + for (FrameMap::Enum e(frames); !e.empty(); e.popFront()) { + AbstractFramePtr frame = e.front().key(); + NativeObject* frameobj = e.front().value(); + if (&frame.script()->global() == global) { + DebuggerFrame_freeScriptFrameIterData(fop, frameobj); + DebuggerFrame_maybeDecrementFrameScriptStepModeCount(fop, frame, frameobj); + e.removeFront(); + } + } + + auto *globalDebuggersVector = global->getDebuggers(); + auto *zoneDebuggersVector = global->zone()->getDebuggers(); + + /* + * The relation must be removed from up to three places: + * globalDebuggersVector and debuggees for sure, and possibly the + * compartment's debuggee set. + * + * The debuggee zone set is recomputed on demand. This avoids refcounting + * and in practice we have relatively few debuggees that tend to all be in + * the same zone. If after recomputing the debuggee zone set, this global's + * zone is not in the set, then we must remove ourselves from the zone's + * vector of observing debuggers. + */ + globalDebuggersVector->erase(findDebuggerInVector(this, globalDebuggersVector)); + + if (debugEnum) + debugEnum->removeFront(); + else + debuggees.remove(global); + + recomputeDebuggeeZoneSet(); + + if (!debuggeeZones.has(global->zone())) + zoneDebuggersVector->erase(findDebuggerInVector(this, zoneDebuggersVector)); + + /* Remove all breakpoints for the debuggee. */ + Breakpoint* nextbp; + for (Breakpoint* bp = firstBreakpoint(); bp; bp = nextbp) { + nextbp = bp->nextInDebugger(); + if (bp->site->script->compartment() == global->compartment()) + bp->destroy(fop); + } + MOZ_ASSERT_IF(debuggees.empty(), !firstBreakpoint()); + + /* + * If we are tracking allocation sites, we need to remove the object + * metadata callback from this global's compartment. + */ + if (trackingAllocationSites) + Debugger::removeAllocationsTracking(*global); + + if (global->getDebuggers()->empty()) { + global->compartment()->unsetIsDebuggee(); + } else { + global->compartment()->updateDebuggerObservesAllExecution(); + global->compartment()->updateDebuggerObservesAsmJS(); + global->compartment()->updateDebuggerObservesCoverage(); + } +} + + +static inline DebuggerSourceReferent GetSourceReferent(JSObject* obj); + +/* + * A class for parsing 'findScripts' query arguments and searching for + * scripts that match the criteria they represent. + */ +class MOZ_STACK_CLASS Debugger::ScriptQuery +{ + public: + /* Construct a ScriptQuery to use matching scripts for |dbg|. */ + ScriptQuery(JSContext* cx, Debugger* dbg): + cx(cx), + debugger(dbg), + iterMarker(&cx->runtime()->gc), + compartments(cx->runtime()), + url(cx), + displayURLString(cx), + hasSource(false), + source(cx, AsVariant(static_cast<ScriptSourceObject*>(nullptr))), + innermostForCompartment(cx->runtime()), + vector(cx, ScriptVector(cx)), + wasmInstanceVector(cx, WasmInstanceObjectVector(cx)) + {} + + /* + * Initialize this ScriptQuery. Raise an error and return false if we + * haven't enough memory. + */ + bool init() { + if (!compartments.init() || + !innermostForCompartment.init()) + { + ReportOutOfMemory(cx); + return false; + } + + return true; + } + + /* + * Parse the query object |query|, and prepare to match only the scripts + * it specifies. + */ + bool parseQuery(HandleObject query) { + /* + * Check for a 'global' property, which limits the results to those + * scripts scoped to a particular global object. + */ + RootedValue global(cx); + if (!GetProperty(cx, query, query, cx->names().global, &global)) + return false; + if (global.isUndefined()) { + if (!matchAllDebuggeeGlobals()) + return false; + } else { + GlobalObject* globalObject = debugger->unwrapDebuggeeArgument(cx, global); + if (!globalObject) + return false; + + /* + * If the given global isn't a debuggee, just leave the set of + * acceptable globals empty; we'll return no scripts. + */ + if (debugger->debuggees.has(globalObject)) { + if (!matchSingleGlobal(globalObject)) + return false; + } + } + + /* Check for a 'url' property. */ + if (!GetProperty(cx, query, query, cx->names().url, &url)) + return false; + if (!url.isUndefined() && !url.isString()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "query object's 'url' property", + "neither undefined nor a string"); + return false; + } + + /* Check for a 'source' property */ + RootedValue debuggerSource(cx); + if (!GetProperty(cx, query, query, cx->names().source, &debuggerSource)) + return false; + if (!debuggerSource.isUndefined()) { + if (!debuggerSource.isObject() || + debuggerSource.toObject().getClass() != &DebuggerSource_class) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "query object's 'source' property", + "not undefined nor a Debugger.Source object"); + return false; + } + + Value owner = debuggerSource.toObject() + .as<NativeObject>() + .getReservedSlot(JSSLOT_DEBUGSOURCE_OWNER); + + /* + * The given source must have an owner. Otherwise, it's a + * Debugger.Source.prototype, which would match no scripts, and is + * probably a mistake. + */ + if (!owner.isObject()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_PROTO, + "Debugger.Source", "Debugger.Source"); + return false; + } + + /* + * If it does have an owner, it should match the Debugger we're + * calling findScripts on. It would work fine even if it didn't, + * but mixing Debugger.Sources is probably a sign of confusion. + */ + if (&owner.toObject() != debugger->object) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_WRONG_OWNER, + "Debugger.Source"); + return false; + } + + hasSource = true; + source = GetSourceReferent(&debuggerSource.toObject()); + } + + /* Check for a 'displayURL' property. */ + RootedValue displayURL(cx); + if (!GetProperty(cx, query, query, cx->names().displayURL, &displayURL)) + return false; + if (!displayURL.isUndefined() && !displayURL.isString()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "query object's 'displayURL' property", + "neither undefined nor a string"); + return false; + } + + if (displayURL.isString()) { + displayURLString = displayURL.toString()->ensureLinear(cx); + if (!displayURLString) + return false; + } + + /* Check for a 'line' property. */ + RootedValue lineProperty(cx); + if (!GetProperty(cx, query, query, cx->names().line, &lineProperty)) + return false; + if (lineProperty.isUndefined()) { + hasLine = false; + } else if (lineProperty.isNumber()) { + if (displayURL.isUndefined() && url.isUndefined() && !hasSource) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_QUERY_LINE_WITHOUT_URL); + return false; + } + double doubleLine = lineProperty.toNumber(); + if (doubleLine <= 0 || (unsigned int) doubleLine != doubleLine) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_BAD_LINE); + return false; + } + hasLine = true; + line = doubleLine; + } else { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "query object's 'line' property", + "neither undefined nor an integer"); + return false; + } + + /* Check for an 'innermost' property. */ + PropertyName* innermostName = cx->names().innermost; + RootedValue innermostProperty(cx); + if (!GetProperty(cx, query, query, innermostName, &innermostProperty)) + return false; + innermost = ToBoolean(innermostProperty); + if (innermost) { + /* Technically, we need only check hasLine, but this is clearer. */ + if ((displayURL.isUndefined() && url.isUndefined() && !hasSource) || !hasLine) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_QUERY_INNERMOST_WITHOUT_LINE_URL); + return false; + } + } + + return true; + } + + /* Set up this ScriptQuery appropriately for a missing query argument. */ + bool omittedQuery() { + url.setUndefined(); + hasLine = false; + innermost = false; + displayURLString = nullptr; + return matchAllDebuggeeGlobals(); + } + + /* + * Search all relevant compartments and the stack for scripts matching + * this query, and append the matching scripts to |vector|. + */ + bool findScripts() { + if (!prepareQuery() || !delazifyScripts()) + return false; + + JSCompartment* singletonComp = nullptr; + if (compartments.count() == 1) + singletonComp = compartments.all().front(); + + /* Search each compartment for debuggee scripts. */ + MOZ_ASSERT(vector.empty()); + oom = false; + IterateScripts(cx, singletonComp, this, considerScript); + if (oom) { + ReportOutOfMemory(cx); + return false; + } + + /* We cannot touch the gray bits while isHeapBusy, so do this now. */ + for (JSScript** i = vector.begin(); i != vector.end(); ++i) + JS::ExposeScriptToActiveJS(*i); + + /* + * For most queries, we just accumulate results in 'vector' as we find + * them. But if this is an 'innermost' query, then we've accumulated the + * results in the 'innermostForCompartment' map. In that case, we now need to + * walk that map and populate 'vector'. + */ + if (innermost) { + for (CompartmentToScriptMap::Range r = innermostForCompartment.all(); + !r.empty(); + r.popFront()) + { + JS::ExposeScriptToActiveJS(r.front().value()); + if (!vector.append(r.front().value())) { + ReportOutOfMemory(cx); + return false; + } + } + } + + // TODOshu: Until such time that wasm modules are real ES6 modules, + // unconditionally consider all wasm toplevel instance scripts. + for (WeakGlobalObjectSet::Range r = debugger->allDebuggees(); !r.empty(); r.popFront()) { + for (wasm::Instance* instance : r.front()->compartment()->wasm.instances()) { + consider(instance->object()); + if (oom) { + ReportOutOfMemory(cx); + return false; + } + } + } + + return true; + } + + Handle<ScriptVector> foundScripts() const { + return vector; + } + + Handle<WasmInstanceObjectVector> foundWasmInstances() const { + return wasmInstanceVector; + } + + private: + /* The context in which we should do our work. */ + JSContext* cx; + + /* The debugger for which we conduct queries. */ + Debugger* debugger; + + /* Require the set of compartments to stay fixed while the ScriptQuery is alive. */ + gc::AutoEnterIteration iterMarker; + + typedef HashSet<JSCompartment*, DefaultHasher<JSCompartment*>, RuntimeAllocPolicy> + CompartmentSet; + + /* A script must be in one of these compartments to match the query. */ + CompartmentSet compartments; + + /* If this is a string, matching scripts have urls equal to it. */ + RootedValue url; + + /* url as a C string. */ + JSAutoByteString urlCString; + + /* If this is a string, matching scripts' sources have displayURLs equal to + * it. */ + RootedLinearString displayURLString; + + /* + * If this is a source referent, matching scripts will have sources equal + * to this instance. Ideally we'd use a Maybe here, but Maybe interacts + * very badly with Rooted's LIFO invariant. + */ + bool hasSource; + Rooted<DebuggerSourceReferent> source; + + /* True if the query contained a 'line' property. */ + bool hasLine; + + /* The line matching scripts must cover. */ + unsigned int line; + + /* True if the query has an 'innermost' property whose value is true. */ + bool innermost; + + typedef HashMap<JSCompartment*, JSScript*, DefaultHasher<JSCompartment*>, RuntimeAllocPolicy> + CompartmentToScriptMap; + + /* + * For 'innermost' queries, a map from compartments to the innermost script + * we've seen so far in that compartment. (Template instantiation code size + * explosion ho!) + */ + CompartmentToScriptMap innermostForCompartment; + + /* + * Accumulate the scripts in an Rooted<ScriptVector>, instead of creating + * the JS array as we go, because we mustn't allocate JS objects or GC + * while we use the CellIter. + */ + Rooted<ScriptVector> vector; + + /* + * Like above, but for wasm modules. + */ + Rooted<WasmInstanceObjectVector> wasmInstanceVector; + + /* Indicates whether OOM has occurred while matching. */ + bool oom; + + bool addCompartment(JSCompartment* comp) { + return compartments.put(comp); + } + + /* Arrange for this ScriptQuery to match only scripts that run in |global|. */ + bool matchSingleGlobal(GlobalObject* global) { + MOZ_ASSERT(compartments.count() == 0); + if (!addCompartment(global->compartment())) { + ReportOutOfMemory(cx); + return false; + } + return true; + } + + /* + * Arrange for this ScriptQuery to match all scripts running in debuggee + * globals. + */ + bool matchAllDebuggeeGlobals() { + MOZ_ASSERT(compartments.count() == 0); + /* Build our compartment set from the debugger's set of debuggee globals. */ + for (WeakGlobalObjectSet::Range r = debugger->debuggees.all(); !r.empty(); r.popFront()) { + if (!addCompartment(r.front()->compartment())) { + ReportOutOfMemory(cx); + return false; + } + } + return true; + } + + /* + * Given that parseQuery or omittedQuery has been called, prepare to match + * scripts. Set urlCString and displayURLChars as appropriate. + */ + bool prepareQuery() { + /* Compute urlCString and displayURLChars, if a url or displayURL was + * given respectively. */ + if (url.isString()) { + if (!urlCString.encodeLatin1(cx, url.toString())) + return false; + } + + return true; + } + + bool delazifyScripts() { + // All scripts in debuggee compartments must be visible, so delazify + // everything. + for (auto r = compartments.all(); !r.empty(); r.popFront()) { + JSCompartment* comp = r.front(); + AutoCompartment ac(cx, comp); + if (!comp->ensureDelazifyScriptsForDebugger(cx)) + return false; + } + return true; + } + + static void considerScript(JSRuntime* rt, void* data, JSScript* script) { + ScriptQuery* self = static_cast<ScriptQuery*>(data); + self->consider(script); + } + + /* + * If |script| matches this query, append it to |vector| or place it in + * |innermostForCompartment|, as appropriate. Set |oom| if an out of memory + * condition occurred. + */ + void consider(JSScript* script) { + // We check for presence of script->code() because it is possible that + // the script was created and thus exposed to GC, but *not* fully + // initialized from fullyInit{FromEmitter,Trivial} due to errors. + if (oom || script->selfHosted() || !script->code()) + return; + JSCompartment* compartment = script->compartment(); + if (!compartments.has(compartment)) + return; + if (urlCString.ptr()) { + bool gotFilename = false; + if (script->filename() && strcmp(script->filename(), urlCString.ptr()) == 0) + gotFilename = true; + + bool gotSourceURL = false; + if (!gotFilename && script->scriptSource()->introducerFilename() && + strcmp(script->scriptSource()->introducerFilename(), urlCString.ptr()) == 0) + { + gotSourceURL = true; + } + if (!gotFilename && !gotSourceURL) + return; + } + if (hasLine) { + if (line < script->lineno() || script->lineno() + GetScriptLineExtent(script) < line) + return; + } + if (displayURLString) { + if (!script->scriptSource() || !script->scriptSource()->hasDisplayURL()) + return; + + const char16_t* s = script->scriptSource()->displayURL(); + if (CompareChars(s, js_strlen(s), displayURLString) != 0) + return; + } + if (hasSource && !(source.is<ScriptSourceObject*>() && + source.as<ScriptSourceObject*>()->source() == script->scriptSource())) + { + return; + } + + if (innermost) { + /* + * For 'innermost' queries, we don't place scripts in |vector| right + * away; we may later find another script that is nested inside this + * one. Instead, we record the innermost script we've found so far + * for each compartment in innermostForCompartment, and only + * populate |vector| at the bottom of findScripts, when we've + * traversed all the scripts. + * + * So: check this script against the innermost one we've found so + * far (if any), as recorded in innermostForCompartment, and replace + * that if it's better. + */ + CompartmentToScriptMap::AddPtr p = innermostForCompartment.lookupForAdd(compartment); + if (p) { + /* Is our newly found script deeper than the last one we found? */ + JSScript* incumbent = p->value(); + if (script->innermostScope()->chainLength() > + incumbent->innermostScope()->chainLength()) + { + p->value() = script; + } + } else { + /* + * This is the first matching script we've encountered for this + * compartment, so it is thus the innermost such script. + */ + if (!innermostForCompartment.add(p, compartment, script)) { + oom = true; + return; + } + } + } else { + /* Record this matching script in the results vector. */ + if (!vector.append(script)) { + oom = true; + return; + } + } + + return; + } + + /* + * If |instanceObject| matches this query, append it to |wasmInstanceVector|. + * Set |oom| if an out of memory condition occurred. + */ + void consider(WasmInstanceObject* instanceObject) { + if (oom) + return; + + if (hasSource && source != AsVariant(instanceObject)) + return; + + if (!wasmInstanceVector.append(instanceObject)) + oom = true; + } +}; + +/* static */ bool +Debugger::findScripts(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "findScripts", args, dbg); + + ScriptQuery query(cx, dbg); + if (!query.init()) + return false; + + if (args.length() >= 1) { + RootedObject queryObject(cx, NonNullObject(cx, args[0])); + if (!queryObject || !query.parseQuery(queryObject)) + return false; + } else { + if (!query.omittedQuery()) + return false; + } + + if (!query.findScripts()) + return false; + + Handle<ScriptVector> scripts(query.foundScripts()); + Handle<WasmInstanceObjectVector> wasmInstances(query.foundWasmInstances()); + + size_t resultLength = scripts.length() + wasmInstances.length(); + RootedArrayObject result(cx, NewDenseFullyAllocatedArray(cx, resultLength)); + if (!result) + return false; + + result->ensureDenseInitializedLength(cx, 0, resultLength); + + for (size_t i = 0; i < scripts.length(); i++) { + JSObject* scriptObject = dbg->wrapScript(cx, scripts[i]); + if (!scriptObject) + return false; + result->setDenseElement(i, ObjectValue(*scriptObject)); + } + + size_t wasmStart = scripts.length(); + for (size_t i = 0; i < wasmInstances.length(); i++) { + JSObject* scriptObject = dbg->wrapWasmScript(cx, wasmInstances[i]); + if (!scriptObject) + return false; + result->setDenseElement(wasmStart + i, ObjectValue(*scriptObject)); + } + + args.rval().setObject(*result); + return true; +} + +/* + * A class for parsing 'findObjects' query arguments and searching for objects + * that match the criteria they represent. + */ +class MOZ_STACK_CLASS Debugger::ObjectQuery +{ + public: + /* Construct an ObjectQuery to use matching scripts for |dbg|. */ + ObjectQuery(JSContext* cx, Debugger* dbg) : + objects(cx), cx(cx), dbg(dbg), className(cx) + { } + + /* The vector that we are accumulating results in. */ + AutoObjectVector objects; + + /* + * Parse the query object |query|, and prepare to match only the objects it + * specifies. + */ + bool parseQuery(HandleObject query) { + /* Check for the 'class' property */ + RootedValue cls(cx); + if (!GetProperty(cx, query, query, cx->names().class_, &cls)) + return false; + if (!cls.isUndefined()) { + if (!cls.isString()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "query object's 'class' property", + "neither undefined nor a string"); + return false; + } + className = cls; + } + return true; + } + + /* Set up this ObjectQuery appropriately for a missing query argument. */ + void omittedQuery() { + className.setUndefined(); + } + + /* + * Traverse the heap to find all relevant objects and add them to the + * provided vector. + */ + bool findObjects() { + if (!prepareQuery()) + return false; + + { + /* + * We can't tolerate the GC moving things around while we're + * searching the heap. Check that nothing we do causes a GC. + */ + Maybe<JS::AutoCheckCannotGC> maybeNoGC; + RootedObject dbgObj(cx, dbg->object); + JS::ubi::RootList rootList(cx, maybeNoGC); + if (!rootList.init(dbgObj)) { + ReportOutOfMemory(cx); + return false; + } + + Traversal traversal(cx, *this, maybeNoGC.ref()); + if (!traversal.init()) { + ReportOutOfMemory(cx); + return false; + } + traversal.wantNames = false; + + return traversal.addStart(JS::ubi::Node(&rootList)) && + traversal.traverse(); + } + } + + /* + * |ubi::Node::BreadthFirst| interface. + */ + class NodeData {}; + typedef JS::ubi::BreadthFirst<ObjectQuery> Traversal; + bool operator() (Traversal& traversal, JS::ubi::Node origin, const JS::ubi::Edge& edge, + NodeData*, bool first) + { + if (!first) + return true; + + JS::ubi::Node referent = edge.referent; + /* + * Only follow edges within our set of debuggee compartments; we don't + * care about the heap's subgraphs outside of our debuggee compartments, + * so we abandon the referent. Either (1) there is not a path from this + * non-debuggee node back to a node in our debuggee compartments, and we + * don't need to follow edges to or from this node, or (2) there does + * exist some path from this non-debuggee node back to a node in our + * debuggee compartments. However, if that were true, then the incoming + * cross compartment edge back into a debuggee compartment is already + * listed as an edge in the RootList we started traversal with, and + * therefore we don't need to follow edges to or from this non-debuggee + * node. + */ + JSCompartment* comp = referent.compartment(); + if (comp && !dbg->isDebuggeeUnbarriered(comp)) { + traversal.abandonReferent(); + return true; + } + + /* + * If the referent is an object and matches our query's restrictions, + * add it to the vector accumulating results. Skip objects that should + * never be exposed to JS, like EnvironmentObjects and internal + * functions. + */ + + if (!referent.is<JSObject>() || referent.exposeToJS().isUndefined()) + return true; + + JSObject* obj = referent.as<JSObject>(); + + if (!className.isUndefined()) { + const char* objClassName = obj->getClass()->name; + if (strcmp(objClassName, classNameCString.ptr()) != 0) + return true; + } + + return objects.append(obj); + } + + private: + /* The context in which we should do our work. */ + JSContext* cx; + + /* The debugger for which we conduct queries. */ + Debugger* dbg; + + /* + * If this is non-null, matching objects will have a class whose name is + * this property. + */ + RootedValue className; + + /* The className member, as a C string. */ + JSAutoByteString classNameCString; + + /* + * Given that either omittedQuery or parseQuery has been called, prepare the + * query for matching objects. + */ + bool prepareQuery() { + if (className.isString()) { + if (!classNameCString.encodeLatin1(cx, className.toString())) + return false; + } + + return true; + } +}; + +bool +Debugger::findObjects(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "findObjects", args, dbg); + + ObjectQuery query(cx, dbg); + + if (args.length() >= 1) { + RootedObject queryObject(cx, NonNullObject(cx, args[0])); + if (!queryObject || !query.parseQuery(queryObject)) + return false; + } else { + query.omittedQuery(); + } + + if (!query.findObjects()) + return false; + + size_t length = query.objects.length(); + RootedArrayObject result(cx, NewDenseFullyAllocatedArray(cx, length)); + if (!result) + return false; + + result->ensureDenseInitializedLength(cx, 0, length); + + for (size_t i = 0; i < length; i++) { + RootedValue debuggeeVal(cx, ObjectValue(*query.objects[i])); + if (!dbg->wrapDebuggeeValue(cx, &debuggeeVal)) + return false; + result->setDenseElement(i, debuggeeVal); + } + + args.rval().setObject(*result); + return true; +} + +/* static */ bool +Debugger::findAllGlobals(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "findAllGlobals", args, dbg); + + AutoObjectVector globals(cx); + + { + // Accumulate the list of globals before wrapping them, because + // wrapping can GC and collect compartments from under us, while + // iterating. + JS::AutoCheckCannotGC nogc; + + for (CompartmentsIter c(cx->runtime(), SkipAtoms); !c.done(); c.next()) { + if (c->creationOptions().invisibleToDebugger()) + continue; + + c->scheduledForDestruction = false; + + GlobalObject* global = c->maybeGlobal(); + + if (cx->runtime()->isSelfHostingGlobal(global)) + continue; + + if (global) { + /* + * We pulled |global| out of nowhere, so it's possible that it was + * marked gray by XPConnect. Since we're now exposing it to JS code, + * we need to mark it black. + */ + JS::ExposeObjectToActiveJS(global); + if (!globals.append(global)) + return false; + } + } + } + + RootedObject result(cx, NewDenseEmptyArray(cx)); + if (!result) + return false; + + for (size_t i = 0; i < globals.length(); i++) { + RootedValue globalValue(cx, ObjectValue(*globals[i])); + if (!dbg->wrapDebuggeeValue(cx, &globalValue)) + return false; + if (!NewbornArrayPush(cx, result, globalValue)) + return false; + } + + args.rval().setObject(*result); + return true; +} + +/* static */ bool +Debugger::makeGlobalObjectReference(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "makeGlobalObjectReference", args, dbg); + if (!args.requireAtLeast(cx, "Debugger.makeGlobalObjectReference", 1)) + return false; + + Rooted<GlobalObject*> global(cx, dbg->unwrapDebuggeeArgument(cx, args[0])); + if (!global) + return false; + + // If we create a D.O referring to a global in an invisible compartment, + // then from it we can reach function objects, scripts, environments, etc., + // none of which we're ever supposed to see. + JSCompartment* globalCompartment = global->compartment(); + if (globalCompartment->creationOptions().invisibleToDebugger()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_INVISIBLE_COMPARTMENT); + return false; + } + + args.rval().setObject(*global); + return dbg->wrapDebuggeeValue(cx, args.rval()); +} + +static bool +DefineProperty(JSContext* cx, HandleObject obj, HandleId id, const char* value, size_t n) +{ + JSString* text = JS_NewStringCopyN(cx, value, n); + if (!text) + return false; + + RootedValue str(cx, StringValue(text)); + return JS_DefinePropertyById(cx, obj, id, str, JSPROP_ENUMERATE); +} + +#ifdef JS_TRACE_LOGGING +# ifdef NIGHTLY_BUILD +bool +Debugger::setupTraceLogger(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "setupTraceLogger", args, dbg); + if (!args.requireAtLeast(cx, "Debugger.setupTraceLogger", 1)) + return false; + + RootedObject obj(cx, ToObject(cx, args[0])); + if (!obj) + return false; + + AutoIdVector ids(cx); + if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY, &ids)) + return false; + + if (ids.length() == 0) { + args.rval().setBoolean(true); + return true; + } + + Vector<uint32_t> textIds(cx); + if (!textIds.reserve(ids.length())) + return false; + + Vector<bool> values(cx); + if (!values.reserve(ids.length())) + return false; + + for (size_t i = 0; i < ids.length(); i++) { + if (!JSID_IS_STRING(ids[i])) { + args.rval().setBoolean(false); + return true; + } + + JSString* id = JSID_TO_STRING(ids[i]); + JSLinearString* linear = id->ensureLinear(cx); + if (!linear) + return false; + + uint32_t textId = TLStringToTextId(linear); + + if (!TLTextIdIsTogglable(textId)) { + args.rval().setBoolean(false); + return true; + } + + RootedValue v(cx); + if (!GetProperty(cx, obj, obj, ids[i], &v)) + return false; + + textIds.infallibleAppend(textId); + values.infallibleAppend(ToBoolean(v)); + } + + MOZ_ASSERT(ids.length() == textIds.length()); + MOZ_ASSERT(textIds.length() == values.length()); + + for (size_t i = 0; i < textIds.length(); i++) { + if (values[i]) + TraceLogEnableTextId(cx, textIds[i]); + else + TraceLogDisableTextId(cx, textIds[i]); + } + + args.rval().setBoolean(true); + return true; +} + +bool +Debugger::drainTraceLogger(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "drainTraceLogger", args, dbg); + + TraceLoggerThread* logger = TraceLoggerForMainThread(cx->runtime()); + bool lostEvents = logger->lostEvents(dbg->traceLoggerLastDrainedIteration, + dbg->traceLoggerLastDrainedSize); + + size_t numEvents; + EventEntry* events = logger->getEventsStartingAt(&dbg->traceLoggerLastDrainedIteration, + &dbg->traceLoggerLastDrainedSize, + &numEvents); + + RootedObject array(cx, NewDenseEmptyArray(cx)); + if (!array) + return false; + + JSAtom* dataAtom = Atomize(cx, "data", strlen("data")); + if (!dataAtom) + return false; + + RootedId dataId(cx, AtomToId(dataAtom)); + + /* Add all events to the array. */ + uint32_t index = 0; + for (EventEntry* eventItem = events; eventItem < events + numEvents; eventItem++, index++) { + RootedObject item(cx, NewObjectWithGivenProto(cx, &PlainObject::class_, nullptr)); + if (!item) + return false; + + const char* eventText = logger->eventText(eventItem->textId); + if (!DefineProperty(cx, item, dataId, eventText, strlen(eventText))) + return false; + + RootedValue obj(cx, ObjectValue(*item)); + if (!JS_DefineElement(cx, array, index, obj, JSPROP_ENUMERATE)) + return false; + } + + /* Add "lostEvents" indicating if there are events that were lost. */ + RootedValue lost(cx, BooleanValue(lostEvents)); + if (!JS_DefineProperty(cx, array, "lostEvents", lost, JSPROP_ENUMERATE)) + return false; + + args.rval().setObject(*array); + + return true; +} +# endif // NIGHTLY_BUILD + +bool +Debugger::setupTraceLoggerScriptCalls(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "setupTraceLoggerScriptCalls", args, dbg); + if (!args.requireAtLeast(cx, "Debugger.setupTraceLoggerScriptCalls", 0)) + return false; + + TraceLogEnableTextId(cx, TraceLogger_Scripts); + TraceLogEnableTextId(cx, TraceLogger_InlinedScripts); + TraceLogDisableTextId(cx, TraceLogger_AnnotateScripts); + + args.rval().setBoolean(true); + + return true; +} + +bool +Debugger::startTraceLogger(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "startTraceLogger", args, dbg); + if (!args.requireAtLeast(cx, "Debugger.startTraceLogger", 0)) + return false; + + TraceLoggerThread* logger = TraceLoggerForMainThread(cx->runtime()); + if (!TraceLoggerEnable(logger, cx)) + return false; + + args.rval().setUndefined(); + + return true; +} + +bool +Debugger::endTraceLogger(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "endTraceLogger", args, dbg); + if (!args.requireAtLeast(cx, "Debugger.endTraceLogger", 0)) + return false; + + TraceLoggerThread* logger = TraceLoggerForMainThread(cx->runtime()); + TraceLoggerDisable(logger); + + args.rval().setUndefined(); + + return true; +} + +bool +Debugger::isCompilableUnit(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "Debugger.isCompilableUnit", 1)) + return false; + + if (!args[0].isString()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE, + "Debugger.isCompilableUnit", "string", + InformalValueTypeName(args[0])); + return false; + } + + JSString* str = args[0].toString(); + size_t length = GetStringLength(str); + + AutoStableStringChars chars(cx); + if (!chars.initTwoByte(cx, str)) + return false; + + bool result = true; + + CompileOptions options(cx); + frontend::UsedNameTracker usedNames(cx); + if (!usedNames.init()) + return false; + frontend::Parser<frontend::FullParseHandler> parser(cx, cx->tempLifoAlloc(), + options, chars.twoByteChars(), + length, /* foldConstants = */ true, + usedNames, nullptr, nullptr); + JS::WarningReporter older = JS::SetWarningReporter(cx, nullptr); + if (!parser.checkOptions() || !parser.parse()) { + // We ran into an error. If it was because we ran out of memory we report + // it in the usual way. + if (cx->isThrowingOutOfMemory()) { + JS::SetWarningReporter(cx, older); + return false; + } + + // If it was because we ran out of source, we return false so our caller + // knows to try to collect more [source]. + if (parser.isUnexpectedEOF()) + result = false; + + cx->clearPendingException(); + } + JS::SetWarningReporter(cx, older); + args.rval().setBoolean(result); + return true; +} + + +bool +Debugger::drainTraceLoggerScriptCalls(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "drainTraceLoggerScriptCalls", args, dbg); + + TraceLoggerThread* logger = TraceLoggerForMainThread(cx->runtime()); + bool lostEvents = logger->lostEvents(dbg->traceLoggerScriptedCallsLastDrainedIteration, + dbg->traceLoggerScriptedCallsLastDrainedSize); + + size_t numEvents; + EventEntry* events = logger->getEventsStartingAt( + &dbg->traceLoggerScriptedCallsLastDrainedIteration, + &dbg->traceLoggerScriptedCallsLastDrainedSize, + &numEvents); + + RootedObject array(cx, NewDenseEmptyArray(cx)); + if (!array) + return false; + + JSAtom* logTypeAtom = Atomize(cx, "logType", strlen("logType")); + if (!logTypeAtom) + return false; + + RootedId fileNameId(cx, AtomToId(cx->names().fileName)); + RootedId lineNumberId(cx, AtomToId(cx->names().lineNumber)); + RootedId columnNumberId(cx, AtomToId(cx->names().columnNumber)); + RootedId logTypeId(cx, AtomToId(logTypeAtom)); + + /* Add all events to the array. */ + uint32_t index = 0; + for (EventEntry* eventItem = events; eventItem < events + numEvents; eventItem++) { + RootedObject item(cx, NewObjectWithGivenProto(cx, &PlainObject::class_, nullptr)); + if (!item) + return false; + + // Filter out internal time. + uint32_t textId = eventItem->textId; + if (textId == TraceLogger_Internal) { + eventItem++; + MOZ_ASSERT(eventItem->textId == TraceLogger_Stop); + continue; + } + + if (textId != TraceLogger_Stop && !logger->textIdIsScriptEvent(textId)) + continue; + + const char* type = (textId == TraceLogger_Stop) ? "Stop" : "Script"; + if (!DefineProperty(cx, item, logTypeId, type, strlen(type))) + return false; + + if (textId != TraceLogger_Stop) { + const char* filename; + const char* lineno; + const char* colno; + size_t filename_len, lineno_len, colno_len; + logger->extractScriptDetails(textId, &filename, &filename_len, &lineno, &lineno_len, + &colno, &colno_len); + + if (!DefineProperty(cx, item, fileNameId, filename, filename_len)) + return false; + if (!DefineProperty(cx, item, lineNumberId, lineno, lineno_len)) + return false; + if (!DefineProperty(cx, item, columnNumberId, colno, colno_len)) + return false; + } + + RootedValue obj(cx, ObjectValue(*item)); + if (!JS_DefineElement(cx, array, index, obj, JSPROP_ENUMERATE)) + return false; + + index++; + } + + /* Add "lostEvents" indicating if there are events that were lost. */ + RootedValue lost(cx, BooleanValue(lostEvents)); + if (!JS_DefineProperty(cx, array, "lostEvents", lost, JSPROP_ENUMERATE)) + return false; + + args.rval().setObject(*array); + + return true; +} +#endif + +bool +Debugger::adoptDebuggeeValue(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "adoptDebuggeeValue", args, dbg); + if (!args.requireAtLeast(cx, "Debugger.adoptDebuggeeValue", 1)) + return false; + + RootedValue v(cx, args[0]); + if (v.isObject()) { + RootedObject obj(cx, &v.toObject()); + NativeObject* ndobj = ToNativeDebuggerObject(cx, &obj); + if (!ndobj) { + return false; + } + + obj.set(static_cast<JSObject*>(ndobj->getPrivate())); + v = ObjectValue(*obj); + + if (!dbg->wrapDebuggeeValue(cx, &v)) { + return false; + } + } + + args.rval().set(v); + return true; +} + +const JSPropertySpec Debugger::properties[] = { + JS_PSGS("enabled", Debugger::getEnabled, Debugger::setEnabled, 0), + JS_PSGS("onDebuggerStatement", Debugger::getOnDebuggerStatement, + Debugger::setOnDebuggerStatement, 0), + JS_PSGS("onExceptionUnwind", Debugger::getOnExceptionUnwind, + Debugger::setOnExceptionUnwind, 0), + JS_PSGS("onNewScript", Debugger::getOnNewScript, Debugger::setOnNewScript, 0), + JS_PSGS("onNewPromise", Debugger::getOnNewPromise, Debugger::setOnNewPromise, 0), + JS_PSGS("onPromiseSettled", Debugger::getOnPromiseSettled, Debugger::setOnPromiseSettled, 0), + JS_PSGS("onEnterFrame", Debugger::getOnEnterFrame, Debugger::setOnEnterFrame, 0), + JS_PSGS("onNewGlobalObject", Debugger::getOnNewGlobalObject, Debugger::setOnNewGlobalObject, 0), + JS_PSGS("uncaughtExceptionHook", Debugger::getUncaughtExceptionHook, + Debugger::setUncaughtExceptionHook, 0), + JS_PSGS("allowUnobservedAsmJS", Debugger::getAllowUnobservedAsmJS, + Debugger::setAllowUnobservedAsmJS, 0), + JS_PSGS("collectCoverageInfo", Debugger::getCollectCoverageInfo, + Debugger::setCollectCoverageInfo, 0), + JS_PSG("memory", Debugger::getMemory, 0), + JS_PS_END +}; + +const JSFunctionSpec Debugger::methods[] = { + JS_FN("addDebuggee", Debugger::addDebuggee, 1, 0), + JS_FN("addAllGlobalsAsDebuggees", Debugger::addAllGlobalsAsDebuggees, 0, 0), + JS_FN("removeDebuggee", Debugger::removeDebuggee, 1, 0), + JS_FN("removeAllDebuggees", Debugger::removeAllDebuggees, 0, 0), + JS_FN("hasDebuggee", Debugger::hasDebuggee, 1, 0), + JS_FN("getDebuggees", Debugger::getDebuggees, 0, 0), + JS_FN("getNewestFrame", Debugger::getNewestFrame, 0, 0), + JS_FN("clearAllBreakpoints", Debugger::clearAllBreakpoints, 0, 0), + JS_FN("findScripts", Debugger::findScripts, 1, 0), + JS_FN("findObjects", Debugger::findObjects, 1, 0), + JS_FN("findAllGlobals", Debugger::findAllGlobals, 0, 0), + JS_FN("makeGlobalObjectReference", Debugger::makeGlobalObjectReference, 1, 0), +#ifdef JS_TRACE_LOGGING + JS_FN("setupTraceLoggerScriptCalls", Debugger::setupTraceLoggerScriptCalls, 0, 0), + JS_FN("drainTraceLoggerScriptCalls", Debugger::drainTraceLoggerScriptCalls, 0, 0), + JS_FN("startTraceLogger", Debugger::startTraceLogger, 0, 0), + JS_FN("endTraceLogger", Debugger::endTraceLogger, 0, 0), +# ifdef NIGHTLY_BUILD + JS_FN("setupTraceLogger", Debugger::setupTraceLogger, 1, 0), + JS_FN("drainTraceLogger", Debugger::drainTraceLogger, 0, 0), +# endif +#endif + JS_FN("adoptDebuggeeValue", Debugger::adoptDebuggeeValue, 1, 0), + JS_FS_END +}; + +const JSFunctionSpec Debugger::static_methods[] { + JS_FN("isCompilableUnit", Debugger::isCompilableUnit, 1, 0), + JS_FS_END +}; + +/*** Debugger.Script *****************************************************************************/ + +// Get the Debugger.Script referent as bare Cell. This should only be used for +// GC operations like tracing. Please use GetScriptReferent below. +static inline gc::Cell* +GetScriptReferentCell(JSObject* obj) +{ + MOZ_ASSERT(obj->getClass() == &DebuggerScript_class); + return static_cast<gc::Cell*>(obj->as<NativeObject>().getPrivate()); +} + +static inline DebuggerScriptReferent +GetScriptReferent(JSObject* obj) +{ + MOZ_ASSERT(obj->getClass() == &DebuggerScript_class); + if (gc::Cell* cell = GetScriptReferentCell(obj)) { + if (cell->getTraceKind() == JS::TraceKind::Script) + return AsVariant(static_cast<JSScript*>(cell)); + MOZ_ASSERT(cell->getTraceKind() == JS::TraceKind::Object); + return AsVariant(&static_cast<NativeObject*>(cell)->as<WasmInstanceObject>()); + } + return AsVariant(static_cast<JSScript*>(nullptr)); +} + +void +DebuggerScript_trace(JSTracer* trc, JSObject* obj) +{ + /* This comes from a private pointer, so no barrier needed. */ + gc::Cell* cell = GetScriptReferentCell(obj); + if (cell) { + if (cell->getTraceKind() == JS::TraceKind::Script) { + JSScript* script = static_cast<JSScript*>(cell); + TraceManuallyBarrieredCrossCompartmentEdge(trc, obj, &script, + "Debugger.Script script referent"); + obj->as<NativeObject>().setPrivateUnbarriered(script); + } else { + JSObject* wasm = static_cast<JSObject*>(cell); + TraceManuallyBarrieredCrossCompartmentEdge(trc, obj, &wasm, + "Debugger.Script wasm referent"); + MOZ_ASSERT(wasm->is<WasmInstanceObject>()); + obj->as<NativeObject>().setPrivateUnbarriered(wasm); + } + } +} + +class DebuggerScriptSetPrivateMatcher +{ + NativeObject* obj_; + public: + explicit DebuggerScriptSetPrivateMatcher(NativeObject* obj) : obj_(obj) { } + using ReturnType = void; + ReturnType match(HandleScript script) { obj_->setPrivateGCThing(script); } + ReturnType match(Handle<WasmInstanceObject*> instance) { obj_->setPrivateGCThing(instance); } +}; + +NativeObject* +Debugger::newDebuggerScript(JSContext* cx, Handle<DebuggerScriptReferent> referent) +{ + assertSameCompartment(cx, object.get()); + + RootedObject proto(cx, &object->getReservedSlot(JSSLOT_DEBUG_SCRIPT_PROTO).toObject()); + MOZ_ASSERT(proto); + NativeObject* scriptobj = NewNativeObjectWithGivenProto(cx, &DebuggerScript_class, + proto, TenuredObject); + if (!scriptobj) + return nullptr; + scriptobj->setReservedSlot(JSSLOT_DEBUGSCRIPT_OWNER, ObjectValue(*object)); + DebuggerScriptSetPrivateMatcher matcher(scriptobj); + referent.match(matcher); + + return scriptobj; +} + +template <typename ReferentVariant, typename Referent, typename Map> +JSObject* +Debugger::wrapVariantReferent(JSContext* cx, Map& map, Handle<CrossCompartmentKey> key, + Handle<ReferentVariant> referent) +{ + assertSameCompartment(cx, object); + + Handle<Referent> untaggedReferent = referent.template as<Referent>(); + MOZ_ASSERT(cx->compartment() != untaggedReferent->compartment()); + + DependentAddPtr<Map> p(cx, map, untaggedReferent); + if (!p) { + NativeObject* wrapper = newVariantWrapper(cx, referent); + if (!wrapper) + return nullptr; + + if (!p.add(cx, map, untaggedReferent, wrapper)) { + NukeDebuggerWrapper(wrapper); + return nullptr; + } + + if (!object->compartment()->putWrapper(cx, key, ObjectValue(*wrapper))) { + NukeDebuggerWrapper(wrapper); + map.remove(untaggedReferent); + ReportOutOfMemory(cx); + return nullptr; + } + + } + + return p->value(); +} + +JSObject* +Debugger::wrapVariantReferent(JSContext* cx, Handle<DebuggerScriptReferent> referent) +{ + JSObject* obj; + if (referent.is<JSScript*>()) { + Handle<JSScript*> untaggedReferent = referent.template as<JSScript*>(); + Rooted<CrossCompartmentKey> key(cx, CrossCompartmentKey(object, untaggedReferent)); + obj = wrapVariantReferent<DebuggerScriptReferent, JSScript*, ScriptWeakMap>( + cx, scripts, key, referent); + } else { + Handle<WasmInstanceObject*> untaggedReferent = referent.template as<WasmInstanceObject*>(); + Rooted<CrossCompartmentKey> key(cx, CrossCompartmentKey(object, untaggedReferent, + CrossCompartmentKey::DebuggerObjectKind::DebuggerWasmScript)); + obj = wrapVariantReferent<DebuggerScriptReferent, WasmInstanceObject*, WasmInstanceWeakMap>( + cx, wasmInstanceScripts, key, referent); + } + MOZ_ASSERT_IF(obj, GetScriptReferent(obj) == referent); + return obj; +} + +JSObject* +Debugger::wrapScript(JSContext* cx, HandleScript script) +{ + Rooted<DebuggerScriptReferent> referent(cx, script.get()); + return wrapVariantReferent(cx, referent); +} + +JSObject* +Debugger::wrapWasmScript(JSContext* cx, Handle<WasmInstanceObject*> wasmInstance) +{ + Rooted<DebuggerScriptReferent> referent(cx, wasmInstance.get()); + return wrapVariantReferent(cx, referent); +} + +static JSObject* +DebuggerScript_check(JSContext* cx, const Value& v, const char* fnname) +{ + JSObject* thisobj = NonNullObject(cx, v); + if (!thisobj) + return nullptr; + if (thisobj->getClass() != &DebuggerScript_class) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, + "Debugger.Script", fnname, thisobj->getClass()->name); + return nullptr; + } + + /* + * Check for Debugger.Script.prototype, which is of class DebuggerScript_class + * but whose script is null. + */ + if (!GetScriptReferentCell(thisobj)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, + "Debugger.Script", fnname, "prototype object"); + return nullptr; + } + + return thisobj; +} + +template <typename ReferentT> +static JSObject* +DebuggerScript_checkThis(JSContext* cx, const CallArgs& args, const char* fnname, + const char* refname) +{ + JSObject* thisobj = DebuggerScript_check(cx, args.thisv(), fnname); + if (!thisobj) + return nullptr; + + if (!GetScriptReferent(thisobj).is<ReferentT>()) { + ReportValueErrorFlags(cx, JSREPORT_ERROR, JSMSG_DEBUG_BAD_REFERENT, + JSDVG_SEARCH_STACK, args.thisv(), nullptr, + refname, nullptr); + return nullptr; + } + + return thisobj; +} + +#define THIS_DEBUGSCRIPT_REFERENT(cx, argc, vp, fnname, args, obj, referent) \ + CallArgs args = CallArgsFromVp(argc, vp); \ + RootedObject obj(cx, DebuggerScript_check(cx, args.thisv(), fnname)); \ + if (!obj) \ + return false; \ + Rooted<DebuggerScriptReferent> referent(cx, GetScriptReferent(obj)) + +#define THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, fnname, args, obj, script) \ + CallArgs args = CallArgsFromVp(argc, vp); \ + RootedObject obj(cx, DebuggerScript_checkThis<JSScript*>(cx, args, fnname, \ + "a JS script")); \ + if (!obj) \ + return false; \ + RootedScript script(cx, GetScriptReferent(obj).as<JSScript*>()) + +static bool +DebuggerScript_getDisplayName(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "(get displayName)", args, obj, script); + Debugger* dbg = Debugger::fromChildJSObject(obj); + + JSFunction* func = script->functionNonDelazifying(); + JSString* name = func ? func->displayAtom() : nullptr; + if (!name) { + args.rval().setUndefined(); + return true; + } + + RootedValue namev(cx, StringValue(name)); + if (!dbg->wrapDebuggeeValue(cx, &namev)) + return false; + args.rval().set(namev); + return true; +} + +static bool +DebuggerScript_getUrl(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "(get url)", args, obj, script); + + if (script->filename()) { + JSString* str; + if (script->scriptSource()->introducerFilename()) + str = NewStringCopyZ<CanGC>(cx, script->scriptSource()->introducerFilename()); + else + str = NewStringCopyZ<CanGC>(cx, script->filename()); + if (!str) + return false; + args.rval().setString(str); + } else { + args.rval().setNull(); + } + return true; +} + +static bool +DebuggerScript_getStartLine(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "(get startLine)", args, obj, script); + args.rval().setNumber(uint32_t(script->lineno())); + return true; +} + +static bool +DebuggerScript_getLineCount(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "(get lineCount)", args, obj, script); + + unsigned maxLine = GetScriptLineExtent(script); + args.rval().setNumber(double(maxLine)); + return true; +} + +class DebuggerScriptGetSourceMatcher +{ + JSContext* cx_; + Debugger* dbg_; + + public: + DebuggerScriptGetSourceMatcher(JSContext* cx, Debugger* dbg) + : cx_(cx), dbg_(dbg) + { } + + using ReturnType = JSObject*; + + ReturnType match(HandleScript script) { + RootedScriptSource source(cx_, + &UncheckedUnwrap(script->sourceObject())->as<ScriptSourceObject>()); + return dbg_->wrapSource(cx_, source); + } + + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { + return dbg_->wrapWasmSource(cx_, wasmInstance); + } +}; + +static bool +DebuggerScript_getSource(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSCRIPT_REFERENT(cx, argc, vp, "(get source)", args, obj, referent); + Debugger* dbg = Debugger::fromChildJSObject(obj); + + DebuggerScriptGetSourceMatcher matcher(cx, dbg); + RootedObject sourceObject(cx, referent.match(matcher)); + if (!sourceObject) + return false; + + args.rval().setObject(*sourceObject); + return true; +} + +static bool +DebuggerScript_getSourceStart(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "(get sourceStart)", args, obj, script); + args.rval().setNumber(uint32_t(script->sourceStart())); + return true; +} + +static bool +DebuggerScript_getSourceLength(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "(get sourceEnd)", args, obj, script); + args.rval().setNumber(uint32_t(script->sourceEnd() - script->sourceStart())); + return true; +} + +static bool +DebuggerScript_getGlobal(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "(get global)", args, obj, script); + Debugger* dbg = Debugger::fromChildJSObject(obj); + + RootedValue v(cx, ObjectValue(script->global())); + if (!dbg->wrapDebuggeeValue(cx, &v)) + return false; + args.rval().set(v); + return true; +} + +class DebuggerScriptGetFormatMatcher +{ + const JSAtomState& names_; + public: + explicit DebuggerScriptGetFormatMatcher(const JSAtomState& names) : names_(names) { } + using ReturnType = JSAtom*; + ReturnType match(HandleScript script) { return names_.js; } + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { return names_.wasm; } +}; + +static bool +DebuggerScript_getFormat(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSCRIPT_REFERENT(cx, argc, vp, "(get format)", args, obj, referent); + DebuggerScriptGetFormatMatcher matcher(cx->names()); + args.rval().setString(referent.match(matcher)); + return true; +} + +static bool +DebuggerScript_getChildScripts(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "getChildScripts", args, obj, script); + Debugger* dbg = Debugger::fromChildJSObject(obj); + + RootedObject result(cx, NewDenseEmptyArray(cx)); + if (!result) + return false; + if (script->hasObjects()) { + /* + * script->savedCallerFun indicates that this is a direct eval script + * and the calling function is stored as script->objects()->vector[0]. + * It is not really a child script of this script, so skip it using + * innerObjectsStart(). + */ + ObjectArray* objects = script->objects(); + RootedFunction fun(cx); + RootedScript funScript(cx); + RootedObject obj(cx), s(cx); + for (uint32_t i = 0; i < objects->length; i++) { + obj = objects->vector[i]; + if (obj->is<JSFunction>()) { + fun = &obj->as<JSFunction>(); + // The inner function could be a wasm native. + if (fun->isNative()) + continue; + funScript = GetOrCreateFunctionScript(cx, fun); + if (!funScript) + return false; + s = dbg->wrapScript(cx, funScript); + if (!s || !NewbornArrayPush(cx, result, ObjectValue(*s))) + return false; + } + } + } + args.rval().setObject(*result); + return true; +} + +static bool +ScriptOffset(JSContext* cx, JSScript* script, const Value& v, size_t* offsetp) +{ + double d; + size_t off; + + bool ok = v.isNumber(); + if (ok) { + d = v.toNumber(); + off = size_t(d); + } + if (!ok || off != d || !IsValidBytecodeOffset(cx, script, off)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_BAD_OFFSET); + return false; + } + *offsetp = off; + return true; +} + +namespace { + +class BytecodeRangeWithPosition : private BytecodeRange +{ + public: + using BytecodeRange::empty; + using BytecodeRange::frontPC; + using BytecodeRange::frontOpcode; + using BytecodeRange::frontOffset; + + BytecodeRangeWithPosition(JSContext* cx, JSScript* script) + : BytecodeRange(cx, script), lineno(script->lineno()), column(0), + sn(script->notes()), snpc(script->code()), isEntryPoint(false), + wasArtifactEntryPoint(false) + { + if (!SN_IS_TERMINATOR(sn)) + snpc += SN_DELTA(sn); + updatePosition(); + while (frontPC() != script->main()) + popFront(); + + if (frontOpcode() != JSOP_JUMPTARGET) + isEntryPoint = true; + else + wasArtifactEntryPoint = true; + } + + void popFront() { + BytecodeRange::popFront(); + if (empty()) + isEntryPoint = false; + else + updatePosition(); + + // The following conditions are handling artifacts introduced by the + // bytecode emitter, such that we do not add breakpoints on empty + // statements of the source code of the user. + if (wasArtifactEntryPoint) { + wasArtifactEntryPoint = false; + isEntryPoint = true; + } + + if (isEntryPoint && frontOpcode() == JSOP_JUMPTARGET) { + wasArtifactEntryPoint = isEntryPoint; + isEntryPoint = false; + } + } + + size_t frontLineNumber() const { return lineno; } + size_t frontColumnNumber() const { return column; } + + // Entry points are restricted to bytecode offsets that have an + // explicit mention in the line table. This restriction avoids a + // number of failing cases caused by some instructions not having + // sensible (to the user) line numbers, and it is one way to + // implement the idea that the bytecode emitter should tell the + // debugger exactly which offsets represent "interesting" (to the + // user) places to stop. + bool frontIsEntryPoint() const { return isEntryPoint; } + + private: + void updatePosition() { + /* + * Determine the current line number by reading all source notes up to + * and including the current offset. + */ + jsbytecode *lastLinePC = nullptr; + while (!SN_IS_TERMINATOR(sn) && snpc <= frontPC()) { + SrcNoteType type = (SrcNoteType) SN_TYPE(sn); + if (type == SRC_COLSPAN) { + ptrdiff_t colspan = SN_OFFSET_TO_COLSPAN(GetSrcNoteOffset(sn, 0)); + MOZ_ASSERT(ptrdiff_t(column) + colspan >= 0); + column += colspan; + lastLinePC = snpc; + } else if (type == SRC_SETLINE) { + lineno = size_t(GetSrcNoteOffset(sn, 0)); + column = 0; + lastLinePC = snpc; + } else if (type == SRC_NEWLINE) { + lineno++; + column = 0; + lastLinePC = snpc; + } + + sn = SN_NEXT(sn); + snpc += SN_DELTA(sn); + } + isEntryPoint = lastLinePC == frontPC(); + } + + size_t lineno; + size_t column; + jssrcnote* sn; + jsbytecode* snpc; + bool isEntryPoint; + bool wasArtifactEntryPoint; +}; + +/* + * FlowGraphSummary::populate(cx, script) computes a summary of script's + * control flow graph used by DebuggerScript_{getAllOffsets,getLineOffsets}. + * + * An instruction on a given line is an entry point for that line if it can be + * reached from (an instruction on) a different line. We distinguish between the + * following cases: + * - hasNoEdges: + * The instruction cannot be reached, so the instruction is not an entry + * point for the line it is on. + * - hasSingleEdge: + * - hasMultipleEdgesFromSingleLine: + * The instruction can be reached from a single line. If this line is + * different from the line the instruction is on, the instruction is an + * entry point for that line. + * - hasMultipleEdgesFromMultipleLines: + * The instruction can be reached from multiple lines. At least one of + * these lines is guaranteed to be different from the line the instruction + * is on, so the instruction is an entry point for that line. + * + * Similarly, an instruction on a given position (line/column pair) is an + * entry point for that position if it can be reached from (an instruction on) a + * different position. Again, we distinguish between the following cases: + * - hasNoEdges: + * The instruction cannot be reached, so the instruction is not an entry + * point for the position it is on. + * - hasSingleEdge: + * The instruction can be reached from a single position. If this line is + * different from the position the instruction is on, the instruction is + * an entry point for that position. + * - hasMultipleEdgesFromSingleLine: + * - hasMultipleEdgesFromMultipleLines: + * The instruction can be reached from multiple positions. At least one + * of these positions is guaranteed to be different from the position the + * instruction is on, so the instruction is an entry point for that + * position. + */ +class FlowGraphSummary { + public: + class Entry { + public: + static Entry createWithNoEdges() { + return Entry(SIZE_MAX, 0); + } + + static Entry createWithSingleEdge(size_t lineno, size_t column) { + return Entry(lineno, column); + } + + static Entry createWithMultipleEdgesFromSingleLine(size_t lineno) { + return Entry(lineno, SIZE_MAX); + } + + static Entry createWithMultipleEdgesFromMultipleLines() { + return Entry(SIZE_MAX, SIZE_MAX); + } + + Entry() : lineno_(SIZE_MAX), column_(0) {} + + bool hasNoEdges() const { + return lineno_ == SIZE_MAX && column_ != SIZE_MAX; + } + + bool hasSingleEdge() const { + return lineno_ != SIZE_MAX && column_ != SIZE_MAX; + } + + bool hasMultipleEdgesFromSingleLine() const { + return lineno_ != SIZE_MAX && column_ == SIZE_MAX; + } + + bool hasMultipleEdgesFromMultipleLines() const { + return lineno_ == SIZE_MAX && column_ == SIZE_MAX; + } + + bool operator==(const Entry& other) const { + return lineno_ == other.lineno_ && column_ == other.column_; + } + + bool operator!=(const Entry& other) const { + return lineno_ != other.lineno_ || column_ != other.column_; + } + + size_t lineno() const { + return lineno_; + } + + size_t column() const { + return column_; + } + + private: + Entry(size_t lineno, size_t column) : lineno_(lineno), column_(column) {} + + size_t lineno_; + size_t column_; + }; + + explicit FlowGraphSummary(JSContext* cx) : entries_(cx) {} + + Entry& operator[](size_t index) { + return entries_[index]; + } + + bool populate(JSContext* cx, JSScript* script) { + if (!entries_.growBy(script->length())) + return false; + unsigned mainOffset = script->pcToOffset(script->main()); + entries_[mainOffset] = Entry::createWithMultipleEdgesFromMultipleLines(); + + size_t prevLineno = script->lineno(); + size_t prevColumn = 0; + JSOp prevOp = JSOP_NOP; + for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) { + size_t lineno = prevLineno; + size_t column = prevColumn; + JSOp op = r.frontOpcode(); + + if (FlowsIntoNext(prevOp)) + addEdge(prevLineno, prevColumn, r.frontOffset()); + + if (BytecodeIsJumpTarget(op)) { + lineno = entries_[r.frontOffset()].lineno(); + column = entries_[r.frontOffset()].column(); + } + + if (r.frontIsEntryPoint()) { + lineno = r.frontLineNumber(); + column = r.frontColumnNumber(); + } + + if (CodeSpec[op].type() == JOF_JUMP) { + addEdge(lineno, column, r.frontOffset() + GET_JUMP_OFFSET(r.frontPC())); + } else if (op == JSOP_TABLESWITCH) { + jsbytecode* pc = r.frontPC(); + size_t offset = r.frontOffset(); + ptrdiff_t step = JUMP_OFFSET_LEN; + size_t defaultOffset = offset + GET_JUMP_OFFSET(pc); + pc += step; + addEdge(lineno, column, defaultOffset); + + int32_t low = GET_JUMP_OFFSET(pc); + pc += JUMP_OFFSET_LEN; + int ncases = GET_JUMP_OFFSET(pc) - low + 1; + pc += JUMP_OFFSET_LEN; + + for (int i = 0; i < ncases; i++) { + size_t target = offset + GET_JUMP_OFFSET(pc); + addEdge(lineno, column, target); + pc += step; + } + } else if (op == JSOP_TRY) { + // As there is no literal incoming edge into the catch block, we + // make a fake one by copying the JSOP_TRY location, as-if this + // was an incoming edge of the catch block. This is needed + // because we only report offsets of entry points which have + // valid incoming edges. + JSTryNote* tn = script->trynotes()->vector; + JSTryNote* tnlimit = tn + script->trynotes()->length; + for (; tn < tnlimit; tn++) { + uint32_t startOffset = script->mainOffset() + tn->start; + if (startOffset == r.frontOffset() + 1) { + uint32_t catchOffset = startOffset + tn->length; + if (tn->kind == JSTRY_CATCH || tn->kind == JSTRY_FINALLY) + addEdge(lineno, column, catchOffset); + } + } + } + + prevLineno = lineno; + prevColumn = column; + prevOp = op; + } + + return true; + } + + private: + void addEdge(size_t sourceLineno, size_t sourceColumn, size_t targetOffset) { + if (entries_[targetOffset].hasNoEdges()) + entries_[targetOffset] = Entry::createWithSingleEdge(sourceLineno, sourceColumn); + else if (entries_[targetOffset].lineno() != sourceLineno) + entries_[targetOffset] = Entry::createWithMultipleEdgesFromMultipleLines(); + else if (entries_[targetOffset].column() != sourceColumn) + entries_[targetOffset] = Entry::createWithMultipleEdgesFromSingleLine(sourceLineno); + } + + Vector<Entry> entries_; +}; + +} /* anonymous namespace */ + +static bool +DebuggerScript_getOffsetLocation(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "getOffsetLocation", args, obj, script); + if (!args.requireAtLeast(cx, "Debugger.Script.getOffsetLocation", 1)) + return false; + size_t offset; + if (!ScriptOffset(cx, script, args[0], &offset)) + return false; + + FlowGraphSummary flowData(cx); + if (!flowData.populate(cx, script)) + return false; + + RootedPlainObject result(cx, NewBuiltinClassInstance<PlainObject>(cx)); + if (!result) + return false; + + BytecodeRangeWithPosition r(cx, script); + while (!r.empty() && r.frontOffset() < offset) + r.popFront(); + + offset = r.frontOffset(); + bool isEntryPoint = r.frontIsEntryPoint(); + + // Line numbers are only correctly defined on entry points. Thus looks + // either for the next valid offset in the flowData, being the last entry + // point flowing into the current offset, or for the next valid entry point. + while (!r.frontIsEntryPoint() && !flowData[r.frontOffset()].hasSingleEdge()) { + r.popFront(); + MOZ_ASSERT(!r.empty()); + } + + // If this is an entry point, take the line number associated with the entry + // point, otherwise settle on the next instruction and take the incoming + // edge position. + size_t lineno; + size_t column; + if (r.frontIsEntryPoint()) { + lineno = r.frontLineNumber(); + column = r.frontColumnNumber(); + } else { + MOZ_ASSERT(flowData[r.frontOffset()].hasSingleEdge()); + lineno = flowData[r.frontOffset()].lineno(); + column = flowData[r.frontOffset()].column(); + } + + RootedId id(cx, NameToId(cx->names().lineNumber)); + RootedValue value(cx, NumberValue(lineno)); + if (!DefineProperty(cx, result, id, value)) + return false; + + value = NumberValue(column); + if (!DefineProperty(cx, result, cx->names().columnNumber, value)) + return false; + + // The same entry point test that is used by getAllColumnOffsets. + isEntryPoint = (isEntryPoint && + !flowData[offset].hasNoEdges() && + (flowData[offset].lineno() != r.frontLineNumber() || + flowData[offset].column() != r.frontColumnNumber())); + value.setBoolean(isEntryPoint); + if (!DefineProperty(cx, result, cx->names().isEntryPoint, value)) + return false; + + args.rval().setObject(*result); + return true; +} + +static bool +DebuggerScript_getAllOffsets(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "getAllOffsets", args, obj, script); + + /* + * First pass: determine which offsets in this script are jump targets and + * which line numbers jump to them. + */ + FlowGraphSummary flowData(cx); + if (!flowData.populate(cx, script)) + return false; + + /* Second pass: build the result array. */ + RootedObject result(cx, NewDenseEmptyArray(cx)); + if (!result) + return false; + for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) { + if (!r.frontIsEntryPoint()) + continue; + + size_t offset = r.frontOffset(); + size_t lineno = r.frontLineNumber(); + + /* Make a note, if the current instruction is an entry point for the current line. */ + if (!flowData[offset].hasNoEdges() && flowData[offset].lineno() != lineno) { + /* Get the offsets array for this line. */ + RootedObject offsets(cx); + RootedValue offsetsv(cx); + + RootedId id(cx, INT_TO_JSID(lineno)); + + bool found; + if (!HasOwnProperty(cx, result, id, &found)) + return false; + if (found && !GetProperty(cx, result, result, id, &offsetsv)) + return false; + + if (offsetsv.isObject()) { + offsets = &offsetsv.toObject(); + } else { + MOZ_ASSERT(offsetsv.isUndefined()); + + /* + * Create an empty offsets array for this line. + * Store it in the result array. + */ + RootedId id(cx); + RootedValue v(cx, NumberValue(lineno)); + offsets = NewDenseEmptyArray(cx); + if (!offsets || + !ValueToId<CanGC>(cx, v, &id)) + { + return false; + } + + RootedValue value(cx, ObjectValue(*offsets)); + if (!DefineProperty(cx, result, id, value)) + return false; + } + + /* Append the current offset to the offsets array. */ + if (!NewbornArrayPush(cx, offsets, NumberValue(offset))) + return false; + } + } + + args.rval().setObject(*result); + return true; +} + +static bool +DebuggerScript_getAllColumnOffsets(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "getAllColumnOffsets", args, obj, script); + + /* + * First pass: determine which offsets in this script are jump targets and + * which positions jump to them. + */ + FlowGraphSummary flowData(cx); + if (!flowData.populate(cx, script)) + return false; + + /* Second pass: build the result array. */ + RootedObject result(cx, NewDenseEmptyArray(cx)); + if (!result) + return false; + for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) { + size_t lineno = r.frontLineNumber(); + size_t column = r.frontColumnNumber(); + size_t offset = r.frontOffset(); + + /* Make a note, if the current instruction is an entry point for the current position. */ + if (r.frontIsEntryPoint() && + !flowData[offset].hasNoEdges() && + (flowData[offset].lineno() != lineno || + flowData[offset].column() != column)) { + RootedPlainObject entry(cx, NewBuiltinClassInstance<PlainObject>(cx)); + if (!entry) + return false; + + RootedId id(cx, NameToId(cx->names().lineNumber)); + RootedValue value(cx, NumberValue(lineno)); + if (!DefineProperty(cx, entry, id, value)) + return false; + + value = NumberValue(column); + if (!DefineProperty(cx, entry, cx->names().columnNumber, value)) + return false; + + id = NameToId(cx->names().offset); + value = NumberValue(offset); + if (!DefineProperty(cx, entry, id, value)) + return false; + + if (!NewbornArrayPush(cx, result, ObjectValue(*entry))) + return false; + } + } + + args.rval().setObject(*result); + return true; +} + +class DebuggerScriptGetLineOffsetsMatcher +{ + JSContext* cx_; + size_t lineno_; + RootedObject result_; + + public: + explicit DebuggerScriptGetLineOffsetsMatcher(JSContext* cx, size_t lineno) + : cx_(cx), lineno_(lineno), result_(cx, NewDenseEmptyArray(cx)) { } + using ReturnType = bool; + ReturnType match(HandleScript script) { + if (!result_) + return false; + + /* + * First pass: determine which offsets in this script are jump targets and + * which line numbers jump to them. + */ + FlowGraphSummary flowData(cx_); + if (!flowData.populate(cx_, script)) + return false; + + /* Second pass: build the result array. */ + for (BytecodeRangeWithPosition r(cx_, script); !r.empty(); r.popFront()) { + if (!r.frontIsEntryPoint()) + continue; + + size_t offset = r.frontOffset(); + + /* If the op at offset is an entry point, append offset to result. */ + if (r.frontLineNumber() == lineno_ && + !flowData[offset].hasNoEdges() && + flowData[offset].lineno() != lineno_) + { + if (!NewbornArrayPush(cx_, result_, NumberValue(offset))) + return false; + } + } + + return true; + } + + ReturnType match(Handle<WasmInstanceObject*> instance) { + if (!result_) + return false; + + Vector<uint32_t> offsets(cx_); + if (!instance->instance().code().getLineOffsets(lineno_, offsets)) + return false; + for (uint32_t i = 0; i < offsets.length(); i++) { + if (!NewbornArrayPush(cx_, result_, NumberValue(offsets[i]))) + return false; + } + return true; + } + + RootedObject& result() { return result_; } +}; + +static bool +DebuggerScript_getLineOffsets(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSCRIPT_REFERENT(cx, argc, vp, "getLineOffsets", args, obj, referent); + if (!args.requireAtLeast(cx, "Debugger.Script.getLineOffsets", 1)) + return false; + + /* Parse lineno argument. */ + RootedValue linenoValue(cx, args[0]); + size_t lineno; + if (!ToNumber(cx, &linenoValue)) + return false; + { + double d = linenoValue.toNumber(); + lineno = size_t(d); + if (lineno != d) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_BAD_LINE); + return false; + } + } + + DebuggerScriptGetLineOffsetsMatcher matcher(cx, lineno); + if (!referent.match(matcher)) + return false; + + args.rval().setObject(*matcher.result()); + return true; +} + +bool +Debugger::observesFrame(AbstractFramePtr frame) const +{ + return observesScript(frame.script()); +} + +bool +Debugger::observesFrame(const FrameIter& iter) const +{ + // Skip frames not yet fully initialized during their prologue. + if (iter.isInterp() && iter.isFunctionFrame()) { + const Value& thisVal = iter.interpFrame()->thisArgument(); + if (thisVal.isMagic() && thisVal.whyMagic() == JS_IS_CONSTRUCTING) + return false; + } + if (iter.isWasm()) + return false; + return observesScript(iter.script()); +} + +bool +Debugger::observesScript(JSScript* script) const +{ + if (!enabled) + return false; + // Don't ever observe self-hosted scripts: the Debugger API can break + // self-hosted invariants. + return observesGlobal(&script->global()) && !script->selfHosted(); +} + +/* static */ bool +Debugger::replaceFrameGuts(JSContext* cx, AbstractFramePtr from, AbstractFramePtr to, + ScriptFrameIter& iter) +{ + auto removeFromDebuggerFramesOnExit = MakeScopeExit([&] { + // Remove any remaining old entries on exit, as the 'from' frame will + // be gone. This is only done in the failure case. On failure, the + // removeToDebuggerFramesOnExit lambda below will rollback any frames + // that were replaced, resulting in !frameMaps(to). On success, the + // range will be empty, as all from Frame.Debugger instances will have + // been removed. + MOZ_ASSERT_IF(inFrameMaps(to), !inFrameMaps(from)); + removeFromFrameMapsAndClearBreakpointsIn(cx, from); + + // Rekey missingScopes to maintain Debugger.Environment identity and + // forward liveScopes to point to the new frame. + DebugEnvironments::forwardLiveFrame(cx, from, to); + }); + + // Forward live Debugger.Frame objects. + Rooted<DebuggerFrameVector> frames(cx, DebuggerFrameVector(cx)); + if (!getDebuggerFrames(from, &frames)) { + // An OOM here means that all Debuggers' frame maps still contain + // entries for 'from' and no entries for 'to'. Since the 'from' frame + // will be gone, they are removed by removeFromDebuggerFramesOnExit + // above. + return false; + } + + // If during the loop below we hit an OOM, we must also rollback any of + // the frames that were successfully replaced. For OSR frames, OOM here + // means those frames will pop from the OSR trampoline, which does not + // call Debugger::onLeaveFrame. + auto removeToDebuggerFramesOnExit = MakeScopeExit([&] { + removeFromFrameMapsAndClearBreakpointsIn(cx, to); + }); + + for (size_t i = 0; i < frames.length(); i++) { + HandleDebuggerFrame frameobj = frames[i]; + Debugger* dbg = Debugger::fromChildJSObject(frameobj); + + // Update frame object's ScriptFrameIter::data pointer. + DebuggerFrame_freeScriptFrameIterData(cx->runtime()->defaultFreeOp(), frameobj); + ScriptFrameIter::Data* data = iter.copyData(); + if (!data) { + // An OOM here means that some Debuggers' frame maps may still + // contain entries for 'from' and some Debuggers' frame maps may + // also contain entries for 'to'. Thus both + // removeFromDebuggerFramesOnExit and + // removeToDebuggerFramesOnExit must both run. + // + // The current frameobj in question is still in its Debugger's + // frame map keyed by 'from', so it will be covered by + // removeFromDebuggerFramesOnExit. + return false; + } + frameobj->setPrivate(data); + + // Remove old frame. + dbg->frames.remove(from); + + // Add the frame object with |to| as key. + if (!dbg->frames.putNew(to, frameobj)) { + // This OOM is subtle. At this point, both + // removeFromDebuggerFramesOnExit and removeToDebuggerFramesOnExit + // must both run for the same reason given above. + // + // The difference is that the current frameobj is no longer in its + // Debugger's frame map, so it will not be cleaned up by neither + // lambda. Manually clean it up here. + FreeOp* fop = cx->runtime()->defaultFreeOp(); + DebuggerFrame_freeScriptFrameIterData(fop, frameobj); + DebuggerFrame_maybeDecrementFrameScriptStepModeCount(fop, to, frameobj); + + ReportOutOfMemory(cx); + return false; + } + } + + // All frames successfuly replaced, cancel the rollback. + removeToDebuggerFramesOnExit.release(); + + return true; +} + +/* static */ bool +Debugger::inFrameMaps(AbstractFramePtr frame) +{ + bool foundAny = false; + forEachDebuggerFrame(frame, [&](NativeObject* frameobj) { foundAny = true; }); + return foundAny; +} + +/* static */ void +Debugger::removeFromFrameMapsAndClearBreakpointsIn(JSContext* cx, AbstractFramePtr frame) +{ + forEachDebuggerFrame(frame, [&](NativeObject* frameobj) { + Debugger* dbg = Debugger::fromChildJSObject(frameobj); + + FreeOp* fop = cx->runtime()->defaultFreeOp(); + DebuggerFrame_freeScriptFrameIterData(fop, frameobj); + DebuggerFrame_maybeDecrementFrameScriptStepModeCount(fop, frame, frameobj); + + dbg->frames.remove(frame); + }); + + /* + * If this is an eval frame, then from the debugger's perspective the + * script is about to be destroyed. Remove any breakpoints in it. + */ + if (frame.isEvalFrame()) { + RootedScript script(cx, frame.script()); + script->clearBreakpointsIn(cx->runtime()->defaultFreeOp(), nullptr, nullptr); + } +} + +/* static */ bool +Debugger::handleBaselineOsr(JSContext* cx, InterpreterFrame* from, jit::BaselineFrame* to) +{ + ScriptFrameIter iter(cx); + MOZ_ASSERT(iter.abstractFramePtr() == to); + return replaceFrameGuts(cx, from, to, iter); +} + +/* static */ bool +Debugger::handleIonBailout(JSContext* cx, jit::RematerializedFrame* from, jit::BaselineFrame* to) +{ + // When we return to a bailed-out Ion real frame, we must update all + // Debugger.Frames that refer to its inline frames. However, since we + // can't pop individual inline frames off the stack (we can only pop the + // real frame that contains them all, as a unit), we cannot assume that + // the frame we're dealing with is the top frame. Advance the iterator + // across any inlined frames younger than |to|, the baseline frame + // reconstructed during bailout from the Ion frame corresponding to + // |from|. + ScriptFrameIter iter(cx); + while (iter.abstractFramePtr() != to) + ++iter; + return replaceFrameGuts(cx, from, to, iter); +} + +/* static */ void +Debugger::handleUnrecoverableIonBailoutError(JSContext* cx, jit::RematerializedFrame* frame) +{ + // Ion bailout can fail due to overrecursion. In such cases we cannot + // honor any further Debugger hooks on the frame, and need to ensure that + // its Debugger.Frame entry is cleaned up. + removeFromFrameMapsAndClearBreakpointsIn(cx, frame); +} + +/* static */ void +Debugger::propagateForcedReturn(JSContext* cx, AbstractFramePtr frame, HandleValue rval) +{ + // Invoking the interrupt handler is considered a step and invokes the + // youngest frame's onStep handler, if any. However, we cannot handle + // { return: ... } resumption values straightforwardly from the interrupt + // handler. Instead, we set the intended return value in the frame's rval + // slot and set the propagating-forced-return flag on the JSContext. + // + // The interrupt handler then returns false with no exception set, + // signaling an uncatchable exception. In the exception handlers, we then + // check for the special propagating-forced-return flag. + MOZ_ASSERT(!cx->isExceptionPending()); + cx->setPropagatingForcedReturn(); + frame.setReturnValue(rval); +} + +static bool +DebuggerScript_setBreakpoint(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "setBreakpoint", args, obj, script); + if (!args.requireAtLeast(cx, "Debugger.Script.setBreakpoint", 2)) + return false; + Debugger* dbg = Debugger::fromChildJSObject(obj); + + if (!dbg->observesScript(script)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_NOT_DEBUGGING); + return false; + } + + size_t offset; + if (!ScriptOffset(cx, script, args[0], &offset)) + return false; + + RootedObject handler(cx, NonNullObject(cx, args[1])); + if (!handler) + return false; + + // Ensure observability *before* setting the breakpoint. If the script is + // not already a debuggee, trying to ensure observability after setting + // the breakpoint (and thus marking the script as a debuggee) will skip + // actually ensuring observability. + if (!dbg->ensureExecutionObservabilityOfScript(cx, script)) + return false; + + jsbytecode* pc = script->offsetToPC(offset); + BreakpointSite* site = script->getOrCreateBreakpointSite(cx, pc); + if (!site) + return false; + site->inc(cx->runtime()->defaultFreeOp()); + if (cx->runtime()->new_<Breakpoint>(dbg, site, handler)) { + args.rval().setUndefined(); + return true; + } + site->dec(cx->runtime()->defaultFreeOp()); + site->destroyIfEmpty(cx->runtime()->defaultFreeOp()); + return false; +} + +static bool +DebuggerScript_getBreakpoints(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "getBreakpoints", args, obj, script); + Debugger* dbg = Debugger::fromChildJSObject(obj); + + jsbytecode* pc; + if (args.length() > 0) { + size_t offset; + if (!ScriptOffset(cx, script, args[0], &offset)) + return false; + pc = script->offsetToPC(offset); + } else { + pc = nullptr; + } + + RootedObject arr(cx, NewDenseEmptyArray(cx)); + if (!arr) + return false; + + for (unsigned i = 0; i < script->length(); i++) { + BreakpointSite* site = script->getBreakpointSite(script->offsetToPC(i)); + if (site && (!pc || site->pc == pc)) { + for (Breakpoint* bp = site->firstBreakpoint(); bp; bp = bp->nextInSite()) { + if (bp->debugger == dbg && + !NewbornArrayPush(cx, arr, ObjectValue(*bp->getHandler()))) + { + return false; + } + } + } + } + args.rval().setObject(*arr); + return true; +} + +static bool +DebuggerScript_clearBreakpoint(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "clearBreakpoint", args, obj, script); + if (!args.requireAtLeast(cx, "Debugger.Script.clearBreakpoint", 1)) + return false; + Debugger* dbg = Debugger::fromChildJSObject(obj); + + JSObject* handler = NonNullObject(cx, args[0]); + if (!handler) + return false; + + script->clearBreakpointsIn(cx->runtime()->defaultFreeOp(), dbg, handler); + args.rval().setUndefined(); + return true; +} + +static bool +DebuggerScript_clearAllBreakpoints(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "clearAllBreakpoints", args, obj, script); + Debugger* dbg = Debugger::fromChildJSObject(obj); + script->clearBreakpointsIn(cx->runtime()->defaultFreeOp(), dbg, nullptr); + args.rval().setUndefined(); + return true; +} + +static bool +DebuggerScript_isInCatchScope(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "isInCatchScope", args, obj, script); + if (!args.requireAtLeast(cx, "Debugger.Script.isInCatchScope", 1)) + return false; + + size_t offset; + if (!ScriptOffset(cx, script, args[0], &offset)) + return false; + + /* + * Try note ranges are relative to the mainOffset of the script, so adjust + * offset accordingly. + */ + offset -= script->mainOffset(); + + args.rval().setBoolean(false); + if (script->hasTrynotes()) { + JSTryNote* tnBegin = script->trynotes()->vector; + JSTryNote* tnEnd = tnBegin + script->trynotes()->length; + while (tnBegin != tnEnd) { + if (tnBegin->start <= offset && + offset <= tnBegin->start + tnBegin->length && + tnBegin->kind == JSTRY_CATCH) + { + args.rval().setBoolean(true); + break; + } + ++tnBegin; + } + } + return true; +} + +static bool +DebuggerScript_getOffsetsCoverage(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "getOffsetsCoverage", args, obj, script); + + // If the script has no coverage information, then skip this and return null + // instead. + if (!script->hasScriptCounts()) { + args.rval().setNull(); + return true; + } + + ScriptCounts* sc = &script->getScriptCounts(); + + // If the main ever got visited, then assume that any code before main got + // visited once. + uint64_t hits = 0; + const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(script->main())); + if (counts->numExec()) + hits = 1; + + // Build an array of objects which are composed of 4 properties: + // - offset PC offset of the current opcode. + // - lineNumber Line of the current opcode. + // - columnNumber Column of the current opcode. + // - count Number of times the instruction got executed. + RootedObject result(cx, NewDenseEmptyArray(cx)); + if (!result) + return false; + + RootedId offsetId(cx, AtomToId(cx->names().offset)); + RootedId lineNumberId(cx, AtomToId(cx->names().lineNumber)); + RootedId columnNumberId(cx, AtomToId(cx->names().columnNumber)); + RootedId countId(cx, AtomToId(cx->names().count)); + + RootedObject item(cx); + RootedValue offsetValue(cx); + RootedValue lineNumberValue(cx); + RootedValue columnNumberValue(cx); + RootedValue countValue(cx); + + // Iterate linearly over the bytecode. + for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) { + size_t offset = r.frontOffset(); + + // The beginning of each non-branching sequences of instruction set the + // number of execution of the current instruction and any following + // instruction. + counts = sc->maybeGetPCCounts(offset); + if (counts) + hits = counts->numExec(); + + offsetValue.setNumber(double(offset)); + lineNumberValue.setNumber(double(r.frontLineNumber())); + columnNumberValue.setNumber(double(r.frontColumnNumber())); + countValue.setNumber(double(hits)); + + // Create a new object with the offset, line number, column number, the + // number of hit counts, and append it to the array. + item = NewObjectWithGivenProto<PlainObject>(cx, nullptr); + if (!item || + !DefineProperty(cx, item, offsetId, offsetValue) || + !DefineProperty(cx, item, lineNumberId, lineNumberValue) || + !DefineProperty(cx, item, columnNumberId, columnNumberValue) || + !DefineProperty(cx, item, countId, countValue) || + !NewbornArrayPush(cx, result, ObjectValue(*item))) + { + return false; + } + + // If the current instruction has thrown, then decrement the hit counts + // with the number of throws. + counts = sc->maybeGetThrowCounts(offset); + if (counts) + hits -= counts->numExec(); + } + + args.rval().setObject(*result); + return true; +} + +static bool +DebuggerScript_construct(JSContext* cx, unsigned argc, Value* vp) +{ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, + "Debugger.Script"); + return false; +} + +static const JSPropertySpec DebuggerScript_properties[] = { + JS_PSG("displayName", DebuggerScript_getDisplayName, 0), + JS_PSG("url", DebuggerScript_getUrl, 0), + JS_PSG("startLine", DebuggerScript_getStartLine, 0), + JS_PSG("lineCount", DebuggerScript_getLineCount, 0), + JS_PSG("source", DebuggerScript_getSource, 0), + JS_PSG("sourceStart", DebuggerScript_getSourceStart, 0), + JS_PSG("sourceLength", DebuggerScript_getSourceLength, 0), + JS_PSG("global", DebuggerScript_getGlobal, 0), + JS_PSG("format", DebuggerScript_getFormat, 0), + JS_PS_END +}; + +static const JSFunctionSpec DebuggerScript_methods[] = { + JS_FN("getChildScripts", DebuggerScript_getChildScripts, 0, 0), + JS_FN("getAllOffsets", DebuggerScript_getAllOffsets, 0, 0), + JS_FN("getAllColumnOffsets", DebuggerScript_getAllColumnOffsets, 0, 0), + JS_FN("getLineOffsets", DebuggerScript_getLineOffsets, 1, 0), + JS_FN("getOffsetLocation", DebuggerScript_getOffsetLocation, 0, 0), + JS_FN("setBreakpoint", DebuggerScript_setBreakpoint, 2, 0), + JS_FN("getBreakpoints", DebuggerScript_getBreakpoints, 1, 0), + JS_FN("clearBreakpoint", DebuggerScript_clearBreakpoint, 1, 0), + JS_FN("clearAllBreakpoints", DebuggerScript_clearAllBreakpoints, 0, 0), + JS_FN("isInCatchScope", DebuggerScript_isInCatchScope, 1, 0), + JS_FN("getOffsetsCoverage", DebuggerScript_getOffsetsCoverage, 0, 0), + JS_FS_END +}; + + +/*** Debugger.Source *****************************************************************************/ + +// For internal use only. +static inline NativeObject* +GetSourceReferentRawObject(JSObject* obj) +{ + MOZ_ASSERT(obj->getClass() == &DebuggerSource_class); + return static_cast<NativeObject*>(obj->as<NativeObject>().getPrivate()); +} + +static inline DebuggerSourceReferent +GetSourceReferent(JSObject* obj) +{ + if (NativeObject* referent = GetSourceReferentRawObject(obj)) { + if (referent->is<ScriptSourceObject>()) + return AsVariant(&referent->as<ScriptSourceObject>()); + return AsVariant(&referent->as<WasmInstanceObject>()); + } + return AsVariant(static_cast<ScriptSourceObject*>(nullptr)); +} + +void +DebuggerSource_trace(JSTracer* trc, JSObject* obj) +{ + /* + * There is a barrier on private pointers, so the Unbarriered marking + * is okay. + */ + if (JSObject *referent = GetSourceReferentRawObject(obj)) { + TraceManuallyBarrieredCrossCompartmentEdge(trc, obj, &referent, + "Debugger.Source referent"); + obj->as<NativeObject>().setPrivateUnbarriered(referent); + } +} + +class SetDebuggerSourcePrivateMatcher +{ + NativeObject* obj_; + public: + explicit SetDebuggerSourcePrivateMatcher(NativeObject* obj) : obj_(obj) { } + using ReturnType = void; + ReturnType match(HandleScriptSource source) { obj_->setPrivateGCThing(source); } + ReturnType match(Handle<WasmInstanceObject*> instance) { obj_->setPrivateGCThing(instance); } +}; + +NativeObject* +Debugger::newDebuggerSource(JSContext* cx, Handle<DebuggerSourceReferent> referent) +{ + assertSameCompartment(cx, object.get()); + + RootedObject proto(cx, &object->getReservedSlot(JSSLOT_DEBUG_SOURCE_PROTO).toObject()); + MOZ_ASSERT(proto); + NativeObject* sourceobj = NewNativeObjectWithGivenProto(cx, &DebuggerSource_class, + proto, TenuredObject); + if (!sourceobj) + return nullptr; + sourceobj->setReservedSlot(JSSLOT_DEBUGSOURCE_OWNER, ObjectValue(*object)); + SetDebuggerSourcePrivateMatcher matcher(sourceobj); + referent.match(matcher); + + return sourceobj; +} + +JSObject* +Debugger::wrapVariantReferent(JSContext* cx, Handle<DebuggerSourceReferent> referent) +{ + JSObject* obj; + if (referent.is<ScriptSourceObject*>()) { + Handle<ScriptSourceObject*> untaggedReferent = referent.template as<ScriptSourceObject*>(); + Rooted<CrossCompartmentKey> key(cx, CrossCompartmentKey(object, untaggedReferent, + CrossCompartmentKey::DebuggerObjectKind::DebuggerSource)); + obj = wrapVariantReferent<DebuggerSourceReferent, ScriptSourceObject*, SourceWeakMap>( + cx, sources, key, referent); + } else { + Handle<WasmInstanceObject*> untaggedReferent = referent.template as<WasmInstanceObject*>(); + Rooted<CrossCompartmentKey> key(cx, CrossCompartmentKey(object, untaggedReferent, + CrossCompartmentKey::DebuggerObjectKind::DebuggerWasmSource)); + obj = wrapVariantReferent<DebuggerSourceReferent, WasmInstanceObject*, WasmInstanceWeakMap>( + cx, wasmInstanceSources, key, referent); + } + MOZ_ASSERT_IF(obj, GetSourceReferent(obj) == referent); + return obj; +} + +JSObject* +Debugger::wrapSource(JSContext* cx, HandleScriptSource source) +{ + Rooted<DebuggerSourceReferent> referent(cx, source.get()); + return wrapVariantReferent(cx, referent); +} + +JSObject* +Debugger::wrapWasmSource(JSContext* cx, Handle<WasmInstanceObject*> wasmInstance) +{ + Rooted<DebuggerSourceReferent> referent(cx, wasmInstance.get()); + return wrapVariantReferent(cx, referent); +} + +static bool +DebuggerSource_construct(JSContext* cx, unsigned argc, Value* vp) +{ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, + "Debugger.Source"); + return false; +} + +static NativeObject* +DebuggerSource_check(JSContext* cx, HandleValue thisv, const char* fnname) +{ + JSObject* thisobj = NonNullObject(cx, thisv); + if (!thisobj) + return nullptr; + if (thisobj->getClass() != &DebuggerSource_class) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, + "Debugger.Source", fnname, thisobj->getClass()->name); + return nullptr; + } + + NativeObject* nthisobj = &thisobj->as<NativeObject>(); + + if (!GetSourceReferentRawObject(thisobj)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, + "Debugger.Source", fnname, "prototype object"); + return nullptr; + } + + return nthisobj; +} + +template <typename ReferentT> +static NativeObject* +DebuggerSource_checkThis(JSContext* cx, const CallArgs& args, const char* fnname, + const char* refname) +{ + NativeObject* thisobj = DebuggerSource_check(cx, args.thisv(), fnname); + if (!thisobj) + return nullptr; + + if (!GetSourceReferent(thisobj).is<ReferentT>()) { + ReportValueErrorFlags(cx, JSREPORT_ERROR, JSMSG_DEBUG_BAD_REFERENT, + JSDVG_SEARCH_STACK, args.thisv(), nullptr, + refname, nullptr); + return nullptr; + } + + return thisobj; +} + +#define THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, fnname, args, obj, referent) \ + CallArgs args = CallArgsFromVp(argc, vp); \ + RootedNativeObject obj(cx, DebuggerSource_check(cx, args.thisv(), fnname)); \ + if (!obj) \ + return false; \ + Rooted<DebuggerSourceReferent> referent(cx, GetSourceReferent(obj)) + +#define THIS_DEBUGSOURCE_SOURCE(cx, argc, vp, fnname, args, obj, sourceObject) \ + CallArgs args = CallArgsFromVp(argc, vp); \ + RootedNativeObject obj(cx, \ + DebuggerSource_checkThis<ScriptSourceObject*>(cx, args, fnname, \ + "a JS source")); \ + if (!obj) \ + return false; \ + RootedScriptSource sourceObject(cx, GetSourceReferent(obj).as<ScriptSourceObject*>()) + +class DebuggerSourceGetTextMatcher +{ + JSContext* cx_; + + public: + explicit DebuggerSourceGetTextMatcher(JSContext* cx) : cx_(cx) { } + + using ReturnType = JSString*; + + ReturnType match(HandleScriptSource sourceObject) { + ScriptSource* ss = sourceObject->source(); + bool hasSourceData = ss->hasSourceData(); + if (!ss->hasSourceData() && !JSScript::loadSource(cx_, ss, &hasSourceData)) + return nullptr; + return hasSourceData ? ss->substring(cx_, 0, ss->length()) + : NewStringCopyZ<CanGC>(cx_, "[no source]"); + } + + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { + return wasmInstance->instance().code().createText(cx_); + } +}; + +static bool +DebuggerSource_getText(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get text)", args, obj, referent); + Value textv = obj->getReservedSlot(JSSLOT_DEBUGSOURCE_TEXT); + if (!textv.isUndefined()) { + MOZ_ASSERT(textv.isString()); + args.rval().set(textv); + return true; + } + + DebuggerSourceGetTextMatcher matcher(cx); + JSString* str = referent.match(matcher); + if (!str) + return false; + + args.rval().setString(str); + obj->setReservedSlot(JSSLOT_DEBUGSOURCE_TEXT, args.rval()); + return true; +} + +class DebuggerSourceGetURLMatcher +{ + JSContext* cx_; + + public: + explicit DebuggerSourceGetURLMatcher(JSContext* cx) : cx_(cx) { } + + using ReturnType = Maybe<JSString*>; + + ReturnType match(HandleScriptSource sourceObject) { + ScriptSource* ss = sourceObject->source(); + MOZ_ASSERT(ss); + if (ss->filename()) { + JSString* str = NewStringCopyZ<CanGC>(cx_, ss->filename()); + return Some(str); + } + return Nothing(); + } + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { + // TODOshu: Until wasm modules have real URLs, append "> wasm" to the + // end to prevent them from being blacklisted by devtools by having + // the same value as a source mapped URL. + char* buf = JS_smprintf("%s > wasm", wasmInstance->instance().metadata().filename.get()); + if (!buf) + return Nothing(); + JSString* str = NewStringCopyZ<CanGC>(cx_, buf); + JS_smprintf_free(buf); + return Some(str); + } +}; + +static bool +DebuggerSource_getURL(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get url)", args, obj, referent); + + DebuggerSourceGetURLMatcher matcher(cx); + Maybe<JSString*> str = referent.match(matcher); + if (str.isSome()) { + if (!*str) + return false; + args.rval().setString(*str); + } else { + args.rval().setNull(); + } + return true; +} + +struct DebuggerSourceGetDisplayURLMatcher +{ + using ReturnType = const char16_t*; + ReturnType match(HandleScriptSource sourceObject) { + ScriptSource* ss = sourceObject->source(); + MOZ_ASSERT(ss); + return ss->hasDisplayURL() ? ss->displayURL() : nullptr; + } + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { + return wasmInstance->instance().metadata().displayURL(); + } +}; + +static bool +DebuggerSource_getDisplayURL(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get url)", args, obj, referent); + + DebuggerSourceGetDisplayURLMatcher matcher; + if (const char16_t* displayURL = referent.match(matcher)) { + JSString* str = JS_NewUCStringCopyZ(cx, displayURL); + if (!str) + return false; + args.rval().setString(str); + } else { + args.rval().setNull(); + } + return true; +} + +struct DebuggerSourceGetElementMatcher +{ + using ReturnType = JSObject*; + ReturnType match(HandleScriptSource sourceObject) { + return sourceObject->element(); + } + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { + return nullptr; + } +}; + +static bool +DebuggerSource_getElement(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get element)", args, obj, referent); + + DebuggerSourceGetElementMatcher matcher; + if (JSObject* element = referent.match(matcher)) { + args.rval().setObjectOrNull(element); + if (!Debugger::fromChildJSObject(obj)->wrapDebuggeeValue(cx, args.rval())) + return false; + } else { + args.rval().setUndefined(); + } + return true; +} + +struct DebuggerSourceGetElementPropertyMatcher +{ + using ReturnType = Value; + ReturnType match(HandleScriptSource sourceObject) { + return sourceObject->elementAttributeName(); + } + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { + return UndefinedValue(); + } +}; + +static bool +DebuggerSource_getElementProperty(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get elementAttributeName)", args, obj, referent); + DebuggerSourceGetElementPropertyMatcher matcher; + args.rval().set(referent.match(matcher)); + return Debugger::fromChildJSObject(obj)->wrapDebuggeeValue(cx, args.rval()); +} + +class DebuggerSourceGetIntroductionScriptMatcher +{ + JSContext* cx_; + Debugger* dbg_; + MutableHandleValue rval_; + + public: + DebuggerSourceGetIntroductionScriptMatcher(JSContext* cx, Debugger* dbg, + MutableHandleValue rval) + : cx_(cx), + dbg_(dbg), + rval_(rval) + { } + + using ReturnType = bool; + + ReturnType match(HandleScriptSource sourceObject) { + RootedScript script(cx_, sourceObject->introductionScript()); + if (script) { + RootedObject scriptDO(cx_, dbg_->wrapScript(cx_, script)); + if (!scriptDO) + return false; + rval_.setObject(*scriptDO); + } else { + rval_.setUndefined(); + } + return true; + } + + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { + RootedObject ds(cx_, dbg_->wrapWasmScript(cx_, wasmInstance)); + if (!ds) + return false; + rval_.setObject(*ds); + return true; + } +}; + +static bool +DebuggerSource_getIntroductionScript(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get introductionScript)", args, obj, referent); + Debugger* dbg = Debugger::fromChildJSObject(obj); + DebuggerSourceGetIntroductionScriptMatcher matcher(cx, dbg, args.rval()); + return referent.match(matcher); +} + +struct DebuggerGetIntroductionOffsetMatcher +{ + using ReturnType = Value; + ReturnType match(HandleScriptSource sourceObject) { + // Regardless of what's recorded in the ScriptSourceObject and + // ScriptSource, only hand out the introduction offset if we also have + // the script within which it applies. + ScriptSource* ss = sourceObject->source(); + if (ss->hasIntroductionOffset() && sourceObject->introductionScript()) + return Int32Value(ss->introductionOffset()); + return UndefinedValue(); + } + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { + return UndefinedValue(); + } +}; + +static bool +DebuggerSource_getIntroductionOffset(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get introductionOffset)", args, obj, referent); + DebuggerGetIntroductionOffsetMatcher matcher; + args.rval().set(referent.match(matcher)); + return true; +} + +struct DebuggerSourceGetIntroductionTypeMatcher +{ + using ReturnType = const char*; + ReturnType match(HandleScriptSource sourceObject) { + ScriptSource* ss = sourceObject->source(); + MOZ_ASSERT(ss); + return ss->hasIntroductionType() ? ss->introductionType() : nullptr; + } + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { + return "wasm"; + } +}; + +static bool +DebuggerSource_getIntroductionType(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get introductionType)", args, obj, referent); + + DebuggerSourceGetIntroductionTypeMatcher matcher; + if (const char* introductionType = referent.match(matcher)) { + JSString* str = NewStringCopyZ<CanGC>(cx, introductionType); + if (!str) + return false; + args.rval().setString(str); + } else { + args.rval().setUndefined(); + } + + return true; +} + +static bool +DebuggerSource_setSourceMapURL(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSOURCE_SOURCE(cx, argc, vp, "sourceMapURL", args, obj, sourceObject); + ScriptSource* ss = sourceObject->source(); + MOZ_ASSERT(ss); + + JSString* str = ToString<CanGC>(cx, args[0]); + if (!str) + return false; + + AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, str)) + return false; + + ss->setSourceMapURL(cx, stableChars.twoByteChars()); + args.rval().setUndefined(); + return true; +} + +struct DebuggerSourceGetSourceMapURLMatcher +{ + using ReturnType = const char16_t*; + ReturnType match(HandleScriptSource sourceObject) { + ScriptSource* ss = sourceObject->source(); + MOZ_ASSERT(ss); + return ss->hasSourceMapURL() ? ss->sourceMapURL() : nullptr; + } + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { + return nullptr; + } +}; + +static bool +DebuggerSource_getSourceMapURL(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get sourceMapURL)", args, obj, referent); + + DebuggerSourceGetSourceMapURLMatcher matcher; + if (const char16_t* sourceMapURL = referent.match(matcher)) { + JSString* str = JS_NewUCStringCopyZ(cx, sourceMapURL); + if (!str) + return false; + args.rval().setString(str); + } else { + args.rval().setNull(); + } + return true; +} + +static const JSPropertySpec DebuggerSource_properties[] = { + JS_PSG("text", DebuggerSource_getText, 0), + JS_PSG("url", DebuggerSource_getURL, 0), + JS_PSG("element", DebuggerSource_getElement, 0), + JS_PSG("displayURL", DebuggerSource_getDisplayURL, 0), + JS_PSG("introductionScript", DebuggerSource_getIntroductionScript, 0), + JS_PSG("introductionOffset", DebuggerSource_getIntroductionOffset, 0), + JS_PSG("introductionType", DebuggerSource_getIntroductionType, 0), + JS_PSG("elementAttributeName", DebuggerSource_getElementProperty, 0), + JS_PSGS("sourceMapURL", DebuggerSource_getSourceMapURL, DebuggerSource_setSourceMapURL, 0), + JS_PS_END +}; + +static const JSFunctionSpec DebuggerSource_methods[] = { + JS_FS_END +}; + + +/*** Debugger.Frame ******************************************************************************/ + +/* static */ NativeObject* +DebuggerFrame::initClass(JSContext* cx, HandleObject dbgCtor, HandleObject obj) +{ + Rooted<GlobalObject*> global(cx, &obj->as<GlobalObject>()); + RootedObject objProto(cx, global->getOrCreateObjectPrototype(cx)); + + return InitClass(cx, dbgCtor, objProto, &class_, construct, 0, properties_, + methods_, nullptr, nullptr); +} + +/* static */ DebuggerFrame* +DebuggerFrame::create(JSContext* cx, HandleObject proto, AbstractFramePtr referent, + const ScriptFrameIter* maybeIter, HandleNativeObject debugger) +{ + JSObject* obj = NewObjectWithGivenProto(cx, &DebuggerFrame::class_, proto); + if (!obj) + return nullptr; + + DebuggerFrame& frame = obj->as<DebuggerFrame>(); + + // Eagerly copy ScriptFrameIter data if we've already walked the stack. + if (maybeIter) { + AbstractFramePtr data = maybeIter->copyDataAsAbstractFramePtr(); + if (!data) + return nullptr; + frame.setPrivate(data.raw()); + } else { + frame.setPrivate(referent.raw()); + } + + frame.setReservedSlot(JSSLOT_DEBUGFRAME_OWNER, ObjectValue(*debugger)); + + return &frame; +} + +/* static */ bool +DebuggerFrame::getCallee(JSContext* cx, HandleDebuggerFrame frame, + MutableHandleDebuggerObject result) +{ + MOZ_ASSERT(frame->isLive()); + + AbstractFramePtr referent = DebuggerFrame::getReferent(frame); + if (!referent.isFunctionFrame()) { + result.set(nullptr); + return true; + } + + Debugger* dbg = frame->owner(); + + RootedObject callee(cx, referent.callee()); + return dbg->wrapDebuggeeObject(cx, callee, result); +} + +/* static */ bool +DebuggerFrame::getIsConstructing(JSContext* cx, HandleDebuggerFrame frame, bool& result) +{ + MOZ_ASSERT(frame->isLive()); + + Maybe<ScriptFrameIter> maybeIter; + if (!DebuggerFrame::getScriptFrameIter(cx, frame, maybeIter)) + return false; + ScriptFrameIter& iter = *maybeIter; + + result = iter.isFunctionFrame() && iter.isConstructing(); + return true; +} + +static void +UpdateFrameIterPc(FrameIter& iter) +{ + if (iter.abstractFramePtr().isRematerializedFrame()) { +#ifdef DEBUG + // Rematerialized frames don't need their pc updated. The reason we + // need to update pc is because we might get the same Debugger.Frame + // object for multiple re-entries into debugger code from debuggee + // code. This reentrancy is not possible with rematerialized frames, + // because when returning to debuggee code, we would have bailed out + // to baseline. + // + // We walk the stack to assert that it doesn't need updating. + jit::RematerializedFrame* frame = iter.abstractFramePtr().asRematerializedFrame(); + jit::JitFrameLayout* jsFrame = (jit::JitFrameLayout*)frame->top(); + jit::JitActivation* activation = iter.activation()->asJit(); + + ActivationIterator activationIter(activation->cx()->runtime()); + while (activationIter.activation() != activation) + ++activationIter; + + jit::JitFrameIterator jitIter(activationIter); + while (!jitIter.isIonJS() || jitIter.jsFrame() != jsFrame) + ++jitIter; + + jit::InlineFrameIterator ionInlineIter(activation->cx(), &jitIter); + while (ionInlineIter.frameNo() != frame->frameNo()) + ++ionInlineIter; + + MOZ_ASSERT(ionInlineIter.pc() == iter.pc()); +#endif + return; + } + + iter.updatePcQuadratic(); +} + +/* static */ bool +DebuggerFrame::getEnvironment(JSContext* cx, HandleDebuggerFrame frame, + MutableHandleDebuggerEnvironment result) +{ + MOZ_ASSERT(frame->isLive()); + + Debugger* dbg = frame->owner(); + + Maybe<ScriptFrameIter> maybeIter; + if (!DebuggerFrame::getScriptFrameIter(cx, frame, maybeIter)) + return false; + ScriptFrameIter& iter = *maybeIter; + + Rooted<Env*> env(cx); + { + AutoCompartment ac(cx, iter.abstractFramePtr().environmentChain()); + UpdateFrameIterPc(iter); + env = GetDebugEnvironmentForFrame(cx, iter.abstractFramePtr(), iter.pc()); + if (!env) + return false; + } + + return dbg->wrapEnvironment(cx, env, result); +} + +/* static */ bool +DebuggerFrame::getIsGenerator(HandleDebuggerFrame frame) +{ + return DebuggerFrame::getReferent(frame).script()->isGenerator(); +} + +/* static */ bool +DebuggerFrame::getOffset(JSContext* cx, HandleDebuggerFrame frame, size_t& result) +{ + MOZ_ASSERT(frame->isLive()); + + Maybe<ScriptFrameIter> maybeIter; + if (!DebuggerFrame::getScriptFrameIter(cx, frame, maybeIter)) + return false; + ScriptFrameIter& iter = *maybeIter; + + JSScript* script = iter.script(); + UpdateFrameIterPc(iter); + jsbytecode* pc = iter.pc(); + result = script->pcToOffset(pc); + return true; +} + +/* static */ bool +DebuggerFrame::getOlder(JSContext* cx, HandleDebuggerFrame frame, + MutableHandleDebuggerFrame result) +{ + MOZ_ASSERT(frame->isLive()); + + Debugger* dbg = frame->owner(); + + Maybe<ScriptFrameIter> maybeIter; + if (!DebuggerFrame::getScriptFrameIter(cx, frame, maybeIter)) + return false; + ScriptFrameIter& iter = *maybeIter; + + for (++iter; !iter.done(); ++iter) { + if (dbg->observesFrame(iter)) { + if (iter.isIon() && !iter.ensureHasRematerializedFrame(cx)) + return false; + return dbg->getScriptFrame(cx, iter, result); + } + } + + result.set(nullptr); + return true; +} + +/* static */ bool +DebuggerFrame::getThis(JSContext* cx, HandleDebuggerFrame frame, MutableHandleValue result) +{ + MOZ_ASSERT(frame->isLive()); + + Debugger* dbg = frame->owner(); + + Maybe<ScriptFrameIter> maybeIter; + if (!DebuggerFrame::getScriptFrameIter(cx, frame, maybeIter)) + return false; + ScriptFrameIter& iter = *maybeIter; + + { + AbstractFramePtr frame = iter.abstractFramePtr(); + AutoCompartment ac(cx, frame.environmentChain()); + + UpdateFrameIterPc(iter); + + if (!GetThisValueForDebuggerMaybeOptimizedOut(cx, frame, iter.pc(), result)) + return false; + } + + return dbg->wrapDebuggeeValue(cx, result); +} + +/* static */ DebuggerFrameType +DebuggerFrame::getType(HandleDebuggerFrame frame) +{ + AbstractFramePtr referent = DebuggerFrame::getReferent(frame); + + /* + * Indirect eval frames are both isGlobalFrame() and isEvalFrame(), so the + * order of checks here is significant. + */ + if (referent.isEvalFrame()) + return DebuggerFrameType::Eval; + else if (referent.isGlobalFrame()) + return DebuggerFrameType::Global; + else if (referent.isFunctionFrame()) + return DebuggerFrameType::Call; + else if (referent.isModuleFrame()) + return DebuggerFrameType::Module; + MOZ_CRASH("Unknown frame type"); +} + +/* static */ DebuggerFrameImplementation +DebuggerFrame::getImplementation(HandleDebuggerFrame frame) +{ + AbstractFramePtr referent = DebuggerFrame::getReferent(frame); + + if (referent.isBaselineFrame()) + return DebuggerFrameImplementation::Baseline; + else if (referent.isRematerializedFrame()) + return DebuggerFrameImplementation::Ion; + return DebuggerFrameImplementation::Interpreter; +} + +/* + * Evaluate |chars[0..length-1]| in the environment |env|, treating that + * source as appearing starting at |lineno| in |filename|. Store the return + * value in |*rval|. Use |thisv| as the 'this' value. + * + * If |frame| is non-nullptr, evaluate as for a direct eval in that frame; |env| + * must be either |frame|'s DebugScopeObject, or some extension of that + * environment; either way, |frame|'s scope is where newly declared variables + * go. In this case, |frame| must have a computed 'this' value, equal to |thisv|. + */ +static bool +EvaluateInEnv(JSContext* cx, Handle<Env*> env, AbstractFramePtr frame, + jsbytecode* pc, mozilla::Range<const char16_t> chars, const char* filename, + unsigned lineno, MutableHandleValue rval) +{ + assertSameCompartment(cx, env, frame); + MOZ_ASSERT_IF(frame, pc); + + CompileOptions options(cx); + options.setIsRunOnce(true) + .setNoScriptRval(false) + .setFileAndLine(filename, lineno) + .setCanLazilyParse(false) + .setIntroductionType("debugger eval") + .maybeMakeStrictMode(frame ? frame.script()->strict() : false); + RootedScript callerScript(cx, frame ? frame.script() : nullptr); + SourceBufferHolder srcBuf(chars.begin().get(), chars.length(), SourceBufferHolder::NoOwnership); + RootedScript script(cx); + + ScopeKind scopeKind; + if (IsGlobalLexicalEnvironment(env)) + scopeKind = ScopeKind::Global; + else + scopeKind = ScopeKind::NonSyntactic; + + if (frame) { + MOZ_ASSERT(scopeKind == ScopeKind::NonSyntactic); + RootedScope scope(cx, GlobalScope::createEmpty(cx, ScopeKind::NonSyntactic)); + if (!scope) + return false; + script = frontend::CompileEvalScript(cx, cx->tempLifoAlloc(), env, scope, + options, srcBuf); + if (script) + script->setActiveEval(); + } else { + // Do not consider executeInGlobal{WithBindings} as an eval, but instead + // as executing a series of statements at the global level. This is to + // circumvent the fresh lexical scope that all eval have, so that the + // users of executeInGlobal, like the web console, may add new bindings to + // the global scope. + script = frontend::CompileGlobalScript(cx, cx->tempLifoAlloc(), scopeKind, options, + srcBuf); + } + + if (!script) + return false; + + return ExecuteKernel(cx, script, *env, NullValue(), frame, rval.address()); +} + +static bool +DebuggerGenericEval(JSContext* cx, const mozilla::Range<const char16_t> chars, + HandleObject bindings, const EvalOptions& options, + JSTrapStatus& status, MutableHandleValue value, + Debugger* dbg, HandleObject envArg, ScriptFrameIter* iter) +{ + /* Either we're specifying the frame, or a global. */ + MOZ_ASSERT_IF(iter, !envArg); + MOZ_ASSERT_IF(!iter, envArg && IsGlobalLexicalEnvironment(envArg)); + + /* + * Gather keys and values of bindings, if any. This must be done in the + * debugger compartment, since that is where any exceptions must be + * thrown. + */ + AutoIdVector keys(cx); + AutoValueVector values(cx); + if (bindings) { + if (!GetPropertyKeys(cx, bindings, JSITER_OWNONLY, &keys) || + !values.growBy(keys.length())) + { + return false; + } + for (size_t i = 0; i < keys.length(); i++) { + MutableHandleValue valp = values[i]; + if (!GetProperty(cx, bindings, bindings, keys[i], valp) || + !dbg->unwrapDebuggeeValue(cx, valp)) + { + return false; + } + } + } + + Maybe<AutoCompartment> ac; + if (iter) + ac.emplace(cx, iter->environmentChain(cx)); + else + ac.emplace(cx, envArg); + + Rooted<Env*> env(cx); + if (iter) { + env = GetDebugEnvironmentForFrame(cx, iter->abstractFramePtr(), iter->pc()); + if (!env) + return false; + } else { + env = envArg; + } + + /* If evalWithBindings, create the inner environment. */ + if (bindings) { + RootedPlainObject nenv(cx, NewObjectWithGivenProto<PlainObject>(cx, nullptr)); + if (!nenv) + return false; + RootedId id(cx); + for (size_t i = 0; i < keys.length(); i++) { + id = keys[i]; + MutableHandleValue val = values[i]; + if (!cx->compartment()->wrap(cx, val) || + !NativeDefineProperty(cx, nenv, id, val, nullptr, nullptr, 0)) + { + return false; + } + } + + AutoObjectVector envChain(cx); + if (!envChain.append(nenv)) + return false; + + RootedObject newEnv(cx); + if (!CreateObjectsForEnvironmentChain(cx, envChain, env, &newEnv)) + return false; + + env = newEnv; + } + + /* Run the code and produce the completion value. */ + LeaveDebuggeeNoExecute nnx(cx); + RootedValue rval(cx); + AbstractFramePtr frame = iter ? iter->abstractFramePtr() : NullFramePtr(); + jsbytecode* pc = iter ? iter->pc() : nullptr; + + bool ok = EvaluateInEnv(cx, env, frame, pc, chars, + options.filename() ? options.filename() : "debugger eval code", + options.lineno(), &rval); + Debugger::resultToCompletion(cx, ok, rval, &status, value); + ac.reset(); + return dbg->wrapDebuggeeValue(cx, value); +} + +/* static */ bool +DebuggerFrame::eval(JSContext* cx, HandleDebuggerFrame frame, mozilla::Range<const char16_t> chars, + HandleObject bindings, const EvalOptions& options, JSTrapStatus& status, + MutableHandleValue value) +{ + MOZ_ASSERT(frame->isLive()); + + Debugger* dbg = frame->owner(); + + Maybe<ScriptFrameIter> maybeIter; + if (!DebuggerFrame::getScriptFrameIter(cx, frame, maybeIter)) + return false; + ScriptFrameIter& iter = *maybeIter; + + UpdateFrameIterPc(iter); + + return DebuggerGenericEval(cx, chars, bindings, options, status, value, dbg, nullptr, &iter); +} + +/* statuc */ bool +DebuggerFrame::isLive() const +{ + return !!getPrivate(); +} + +static bool +DebuggerFrame_requireLive(JSContext* cx, HandleDebuggerFrame frame) +{ + if (!frame->isLive()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_NOT_LIVE, + "Debugger.Frame"); + return false; + } + + return true; +} + +/* static */ AbstractFramePtr +DebuggerFrame::getReferent(HandleDebuggerFrame frame) +{ + AbstractFramePtr referent = AbstractFramePtr::FromRaw(frame->getPrivate()); + if (referent.isScriptFrameIterData()) { + ScriptFrameIter iter(*(ScriptFrameIter::Data*)(referent.raw())); + referent = iter.abstractFramePtr(); + } + return referent; +} + +/* static */ bool +DebuggerFrame::getScriptFrameIter(JSContext* cx, HandleDebuggerFrame frame, + Maybe<ScriptFrameIter>& result) +{ + AbstractFramePtr referent = AbstractFramePtr::FromRaw(frame->getPrivate()); + if (referent.isScriptFrameIterData()) { + result.emplace(*reinterpret_cast<ScriptFrameIter::Data*>(referent.raw())); + } else { + result.emplace(cx, ScriptFrameIter::IGNORE_DEBUGGER_EVAL_PREV_LINK); + ScriptFrameIter& iter = *result; + while (!iter.hasUsableAbstractFramePtr() || iter.abstractFramePtr() != referent) + ++iter; + AbstractFramePtr data = iter.copyDataAsAbstractFramePtr(); + if (!data) + return false; + frame->setPrivate(data.raw()); + } + return true; +} + +static void +DebuggerFrame_freeScriptFrameIterData(FreeOp* fop, JSObject* obj) +{ + AbstractFramePtr frame = AbstractFramePtr::FromRaw(obj->as<NativeObject>().getPrivate()); + if (frame.isScriptFrameIterData()) + fop->delete_((ScriptFrameIter::Data*) frame.raw()); + obj->as<NativeObject>().setPrivate(nullptr); +} + +static void +DebuggerFrame_maybeDecrementFrameScriptStepModeCount(FreeOp* fop, AbstractFramePtr frame, + NativeObject* frameobj) +{ + /* If this frame has an onStep handler, decrement the script's count. */ + if (!frameobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined()) + frame.script()->decrementStepModeCount(fop); +} + +static void +DebuggerFrame_finalize(FreeOp* fop, JSObject* obj) +{ + MOZ_ASSERT(fop->maybeOffMainThread()); + DebuggerFrame_freeScriptFrameIterData(fop, obj); +} + +static DebuggerFrame* +DebuggerFrame_checkThis(JSContext* cx, const CallArgs& args, const char* fnname, bool checkLive) +{ + JSObject* thisobj = NonNullObject(cx, args.thisv()); + if (!thisobj) + return nullptr; + if (thisobj->getClass() != &DebuggerFrame::class_) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, + "Debugger.Frame", fnname, thisobj->getClass()->name); + return nullptr; + } + + RootedDebuggerFrame frame(cx, &thisobj->as<DebuggerFrame>()); + + /* + * Forbid Debugger.Frame.prototype, which is of class DebuggerFrame::class_ + * but isn't really a working Debugger.Frame object. The prototype object + * is distinguished by having a nullptr private value. Also, forbid popped + * frames. + */ + if (!frame->getPrivate() && + frame->getReservedSlot(JSSLOT_DEBUGFRAME_OWNER).isUndefined()) + { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, + "Debugger.Frame", fnname, "prototype object"); + return nullptr; + } + + if (checkLive) { + if (!DebuggerFrame_requireLive(cx, frame)) + return nullptr; + } + + return frame; +} + +/* + * To make frequently fired hooks like onEnterFrame more performant, + * Debugger.Frame methods should not create a ScriptFrameIter unless it + * absolutely needs to. That is, unless the method has to call a method on + * ScriptFrameIter that's otherwise not available on AbstractFramePtr. + * + * When a Debugger.Frame is first created, its private slot is set to the + * AbstractFramePtr itself. The first time the users asks for a + * ScriptFrameIter, we construct one, have it settle on the frame pointed to + * by the AbstractFramePtr and cache its internal Data in the Debugger.Frame + * object's private slot. Subsequent uses of the Debugger.Frame object will + * always create a ScriptFrameIter from the cached Data. + * + * Methods that only need the AbstractFramePtr should use THIS_FRAME. + * Methods that need a ScriptFrameIterator should use THIS_FRAME_ITER. + */ + +#define THIS_DEBUGGER_FRAME(cx, argc, vp, fnname, args, frame) \ + CallArgs args = CallArgsFromVp(argc, vp); \ + RootedDebuggerFrame frame(cx, DebuggerFrame_checkThis(cx, args, fnname, true)); \ + if (!frame) \ + return false; + +#define THIS_FRAME_THISOBJ(cx, argc, vp, fnname, args, thisobj) \ + CallArgs args = CallArgsFromVp(argc, vp); \ + RootedNativeObject thisobj(cx, DebuggerFrame_checkThis(cx, args, fnname, true)); \ + if (!thisobj) \ + return false + +#define THIS_FRAME(cx, argc, vp, fnname, args, thisobj, frame) \ + THIS_FRAME_THISOBJ(cx, argc, vp, fnname, args, thisobj); \ + AbstractFramePtr frame = AbstractFramePtr::FromRaw(thisobj->getPrivate()); \ + if (frame.isScriptFrameIterData()) { \ + ScriptFrameIter iter(*(ScriptFrameIter::Data*)(frame.raw())); \ + frame = iter.abstractFramePtr(); \ + } + +#define THIS_FRAME_ITER(cx, argc, vp, fnname, args, thisobj, maybeIter, iter) \ + THIS_FRAME_THISOBJ(cx, argc, vp, fnname, args, thisobj); \ + Maybe<ScriptFrameIter> maybeIter; \ + { \ + AbstractFramePtr f = AbstractFramePtr::FromRaw(thisobj->getPrivate()); \ + if (f.isScriptFrameIterData()) { \ + maybeIter.emplace(*(ScriptFrameIter::Data*)(f.raw())); \ + } else { \ + maybeIter.emplace(cx, ScriptFrameIter::IGNORE_DEBUGGER_EVAL_PREV_LINK); \ + ScriptFrameIter& iter = *maybeIter; \ + while (!iter.hasUsableAbstractFramePtr() || iter.abstractFramePtr() != f) \ + ++iter; \ + AbstractFramePtr data = iter.copyDataAsAbstractFramePtr(); \ + if (!data) \ + return false; \ + thisobj->setPrivate(data.raw()); \ + } \ + } \ + ScriptFrameIter& iter = *maybeIter + +#define THIS_FRAME_OWNER(cx, argc, vp, fnname, args, thisobj, frame, dbg) \ + THIS_FRAME(cx, argc, vp, fnname, args, thisobj, frame); \ + Debugger* dbg = Debugger::fromChildJSObject(thisobj) + +#define THIS_FRAME_OWNER_ITER(cx, argc, vp, fnname, args, thisobj, maybeIter, iter, dbg) \ + THIS_FRAME_ITER(cx, argc, vp, fnname, args, thisobj, maybeIter, iter); \ + Debugger* dbg = Debugger::fromChildJSObject(thisobj) + +/* static */ bool +DebuggerFrame::typeGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER_FRAME(cx, argc, vp, "get type", args, frame); + + DebuggerFrameType type = DebuggerFrame::getType(frame); + + JSString* str; + switch (type) { + case DebuggerFrameType::Eval: + str = cx->names().eval; + break; + case DebuggerFrameType::Global: + str = cx->names().global; + break; + case DebuggerFrameType::Call: + str = cx->names().call; + break; + case DebuggerFrameType::Module: + str = cx->names().module; + break; + default: + MOZ_CRASH("bad DebuggerFrameType value"); + } + + args.rval().setString(str); + return true; +} + +/* static */ bool +DebuggerFrame::implementationGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER_FRAME(cx, argc, vp, "get implementation", args, frame); + + DebuggerFrameImplementation implementation = DebuggerFrame::getImplementation(frame); + + const char* s; + switch (implementation) { + case DebuggerFrameImplementation::Baseline: + s = "baseline"; + break; + case DebuggerFrameImplementation::Ion: + s = "ion"; + break; + case DebuggerFrameImplementation::Interpreter: + s = "interpreter"; + break; + default: + MOZ_CRASH("bad DebuggerFrameImplementation value"); + } + + JSAtom* str = Atomize(cx, s, strlen(s)); + if (!str) + return false; + + args.rval().setString(str); + return true; +} + +/* static */ bool +DebuggerFrame::environmentGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER_FRAME(cx, argc, vp, "get environment", args, frame); + + RootedDebuggerEnvironment result(cx); + if (!DebuggerFrame::getEnvironment(cx, frame, &result)) + return false; + + args.rval().setObject(*result); + return true; +} + +/* static */ bool +DebuggerFrame::calleeGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER_FRAME(cx, argc, vp, "get callee", args, frame); + + RootedDebuggerObject result(cx); + if (!DebuggerFrame::getCallee(cx, frame, &result)) + return false; + + args.rval().setObjectOrNull(result); + return true; +} + +/* static */ bool +DebuggerFrame::generatorGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER_FRAME(cx, argc, vp, "get callee", args, frame); + + args.rval().setBoolean(DebuggerFrame::getIsGenerator(frame)); + return true; +} + +/* static */ bool +DebuggerFrame::constructingGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER_FRAME(cx, argc, vp, "get callee", args, frame); + + bool result; + if (!DebuggerFrame::getIsConstructing(cx, frame, result)) + return false; + + args.rval().setBoolean(result); + return true; +} + +/* static */ bool +DebuggerFrame::thisGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER_FRAME(cx, argc, vp, "get this", args, frame); + + return DebuggerFrame::getThis(cx, frame, args.rval()); +} + +/* static */ bool +DebuggerFrame::olderGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER_FRAME(cx, argc, vp, "get older", args, frame); + + RootedDebuggerFrame result(cx); + if (!DebuggerFrame::getOlder(cx, frame, &result)) + return false; + + args.rval().setObjectOrNull(result); + return true; +} + +/* The getter used for each element of frame.arguments. See DebuggerFrame_getArguments. */ +static bool +DebuggerArguments_getArg(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + int32_t i = args.callee().as<JSFunction>().getExtendedSlot(0).toInt32(); + + /* Check that the this value is an Arguments object. */ + RootedObject argsobj(cx, NonNullObject(cx, args.thisv())); + if (!argsobj) + return false; + if (argsobj->getClass() != &DebuggerArguments_class) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, + "Arguments", "getArgument", argsobj->getClass()->name); + return false; + } + + /* + * Put the Debugger.Frame into the this-value slot, then use THIS_FRAME + * to check that it is still live and get the fp. + */ + args.setThis(argsobj->as<NativeObject>().getReservedSlot(JSSLOT_DEBUGARGUMENTS_FRAME)); + THIS_FRAME(cx, argc, vp, "get argument", ca2, thisobj, frame); + + /* + * Since getters can be extracted and applied to other objects, + * there is no guarantee this object has an ith argument. + */ + MOZ_ASSERT(i >= 0); + RootedValue arg(cx); + RootedScript script(cx); + if (unsigned(i) < frame.numActualArgs()) { + script = frame.script(); + { + AutoCompartment ac(cx, script->compartment()); + if (!script->ensureHasAnalyzedArgsUsage(cx)) + return false; + } + if (unsigned(i) < frame.numFormalArgs()) { + for (PositionalFormalParameterIter fi(script); fi; fi++) { + if (fi.argumentSlot() == unsigned(i)) { + // We might've been called before the CallObject was + // created. + if (fi.closedOver() && frame.hasInitialEnvironment()) + arg = frame.callObj().aliasedBinding(fi); + else + arg = frame.unaliasedActual(i, DONT_CHECK_ALIASING); + break; + } + } + } else if (script->argsObjAliasesFormals() && frame.hasArgsObj()) { + arg = frame.argsObj().arg(i); + } else { + arg = frame.unaliasedActual(i, DONT_CHECK_ALIASING); + } + } else { + arg.setUndefined(); + } + + if (!Debugger::fromChildJSObject(thisobj)->wrapDebuggeeValue(cx, &arg)) + return false; + args.rval().set(arg); + return true; +} + +static bool +DebuggerFrame_getArguments(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_FRAME(cx, argc, vp, "get arguments", args, thisobj, frame); + Value argumentsv = thisobj->getReservedSlot(JSSLOT_DEBUGFRAME_ARGUMENTS); + if (!argumentsv.isUndefined()) { + MOZ_ASSERT(argumentsv.isObjectOrNull()); + args.rval().set(argumentsv); + return true; + } + + RootedNativeObject argsobj(cx); + if (frame.hasArgs()) { + /* Create an arguments object. */ + Rooted<GlobalObject*> global(cx, &args.callee().global()); + RootedObject proto(cx, GlobalObject::getOrCreateArrayPrototype(cx, global)); + if (!proto) + return false; + argsobj = NewNativeObjectWithGivenProto(cx, &DebuggerArguments_class, proto); + if (!argsobj) + return false; + SetReservedSlot(argsobj, JSSLOT_DEBUGARGUMENTS_FRAME, ObjectValue(*thisobj)); + + MOZ_ASSERT(frame.numActualArgs() <= 0x7fffffff); + unsigned fargc = frame.numActualArgs(); + RootedValue fargcVal(cx, Int32Value(fargc)); + if (!NativeDefineProperty(cx, argsobj, cx->names().length, fargcVal, nullptr, nullptr, + JSPROP_PERMANENT | JSPROP_READONLY)) + { + return false; + } + + Rooted<jsid> id(cx); + for (unsigned i = 0; i < fargc; i++) { + RootedFunction getobj(cx); + getobj = NewNativeFunction(cx, DebuggerArguments_getArg, 0, nullptr, + gc::AllocKind::FUNCTION_EXTENDED); + if (!getobj) + return false; + id = INT_TO_JSID(i); + if (!getobj || + !NativeDefineProperty(cx, argsobj, id, UndefinedHandleValue, + JS_DATA_TO_FUNC_PTR(GetterOp, getobj.get()), nullptr, + JSPROP_ENUMERATE | JSPROP_SHARED | JSPROP_GETTER)) + { + return false; + } + getobj->setExtendedSlot(0, Int32Value(i)); + } + } else { + argsobj = nullptr; + } + args.rval().setObjectOrNull(argsobj); + thisobj->setReservedSlot(JSSLOT_DEBUGFRAME_ARGUMENTS, args.rval()); + return true; +} + +static bool +DebuggerFrame_getScript(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_FRAME(cx, argc, vp, "get script", args, thisobj, frame); + Debugger* debug = Debugger::fromChildJSObject(thisobj); + + RootedObject scriptObject(cx); + if (frame.isFunctionFrame()) { + RootedFunction callee(cx, frame.callee()); + if (callee->isInterpreted()) { + RootedScript script(cx, callee->nonLazyScript()); + scriptObject = debug->wrapScript(cx, script); + if (!scriptObject) + return false; + } + } else { + /* + * We got eval, JS_Evaluate*, or JS_ExecuteScript non-function script + * frames. + */ + RootedScript script(cx, frame.script()); + scriptObject = debug->wrapScript(cx, script); + if (!scriptObject) + return false; + } + args.rval().setObjectOrNull(scriptObject); + return true; +} + +/* static */ bool +DebuggerFrame::offsetGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER_FRAME(cx, argc, vp, "get offset", args, frame); + + size_t result; + if (!DebuggerFrame::getOffset(cx, frame, result)) + return false; + + args.rval().setNumber(double(result)); + return true; +} + +/* static */ bool +DebuggerFrame::liveGetter(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + RootedDebuggerFrame frame(cx, DebuggerFrame_checkThis(cx, args, "get live", false)); + if (!frame) + return false; + + args.rval().setBoolean(frame->isLive()); + return true; +} + +static bool +IsValidHook(const Value& v) +{ + return v.isUndefined() || (v.isObject() && v.toObject().isCallable()); +} + +static bool +DebuggerFrame_getOnStep(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_FRAME(cx, argc, vp, "get onStep", args, thisobj, frame); + (void) frame; // Silence GCC warning + RootedValue handler(cx, thisobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER)); + MOZ_ASSERT(IsValidHook(handler)); + args.rval().set(handler); + return true; +} + +static bool +DebuggerFrame_setOnStep(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_FRAME(cx, argc, vp, "set onStep", args, thisobj, frame); + if (!args.requireAtLeast(cx, "Debugger.Frame.set onStep", 1)) + return false; + if (!IsValidHook(args[0])) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_CALLABLE_OR_UNDEFINED); + return false; + } + + Value prior = thisobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER); + if (!args[0].isUndefined() && prior.isUndefined()) { + // Single stepping toggled off->on. + AutoCompartment ac(cx, frame.environmentChain()); + // Ensure observability *before* incrementing the step mode + // count. Calling this function after calling incrementStepModeCount + // will make it a no-op. + Debugger* dbg = Debugger::fromChildJSObject(thisobj); + if (!dbg->ensureExecutionObservabilityOfScript(cx, frame.script())) + return false; + if (!frame.script()->incrementStepModeCount(cx)) + return false; + } else if (args[0].isUndefined() && !prior.isUndefined()) { + // Single stepping toggled on->off. + frame.script()->decrementStepModeCount(cx->runtime()->defaultFreeOp()); + } + + /* Now that the step mode switch has succeeded, we can install the handler. */ + thisobj->setReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER, args[0]); + args.rval().setUndefined(); + return true; +} + +static bool +DebuggerFrame_getOnPop(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_FRAME(cx, argc, vp, "get onPop", args, thisobj, frame); + (void) frame; // Silence GCC warning + RootedValue handler(cx, thisobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONPOP_HANDLER)); + MOZ_ASSERT(IsValidHook(handler)); + args.rval().set(handler); + return true; +} + +static bool +DebuggerFrame_setOnPop(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_FRAME(cx, argc, vp, "set onPop", args, thisobj, frame); + if (!args.requireAtLeast(cx, "Debugger.Frame.set onPop", 1)) + return false; + (void) frame; // Silence GCC warning + if (!IsValidHook(args[0])) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_CALLABLE_OR_UNDEFINED); + return false; + } + + thisobj->setReservedSlot(JSSLOT_DEBUGFRAME_ONPOP_HANDLER, args[0]); + args.rval().setUndefined(); + return true; +} + +/* static */ bool +DebuggerFrame::evalMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER_FRAME(cx, argc, vp, "eval", args, frame); + if (!args.requireAtLeast(cx, "Debugger.Frame.prototype.eval", 1)) + return false; + + AutoStableStringChars stableChars(cx); + if (!ValueToStableChars(cx, "Debugger.Frame.prototype.eval", args[0], stableChars)) + return false; + mozilla::Range<const char16_t> chars = stableChars.twoByteRange(); + + EvalOptions options; + if (!ParseEvalOptions(cx, args.get(1), options)) + return false; + + JSTrapStatus status; + RootedValue value(cx); + if (!DebuggerFrame::eval(cx, frame, chars, nullptr, options, status, &value)) + return false; + + return frame->owner()->newCompletionValue(cx, status, value, args.rval()); +} + +/* static */ bool +DebuggerFrame::evalWithBindingsMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER_FRAME(cx, argc, vp, "evalWithBindings", args, frame); + if (!args.requireAtLeast(cx, "Debugger.Frame.prototype.evalWithBindings", 2)) + return false; + + AutoStableStringChars stableChars(cx); + if (!ValueToStableChars(cx, "Debugger.Frame.prototype.evalWithBindings", args[0], + stableChars)) + { + return false; + } + mozilla::Range<const char16_t> chars = stableChars.twoByteRange(); + + RootedObject bindings(cx, NonNullObject(cx, args[1])); + if (!bindings) + return false; + + EvalOptions options; + if (!ParseEvalOptions(cx, args.get(2), options)) + return false; + + JSTrapStatus status; + RootedValue value(cx); + if (!DebuggerFrame::eval(cx, frame, chars, bindings, options, status, &value)) + return false; + + return frame->owner()->newCompletionValue(cx, status, value, args.rval()); +} + +/* static */ bool +DebuggerFrame::construct(JSContext* cx, unsigned argc, Value* vp) +{ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, + "Debugger.Frame"); + return false; +} + +const JSPropertySpec DebuggerFrame::properties_[] = { + JS_PSG("arguments", DebuggerFrame_getArguments, 0), + JS_PSG("callee", DebuggerFrame::calleeGetter, 0), + JS_PSG("constructing", DebuggerFrame::constructingGetter, 0), + JS_PSG("environment", DebuggerFrame::environmentGetter, 0), + JS_PSG("generator", DebuggerFrame::generatorGetter, 0), + JS_PSG("live", DebuggerFrame::liveGetter, 0), + JS_PSG("offset", DebuggerFrame::offsetGetter, 0), + JS_PSG("older", DebuggerFrame::olderGetter, 0), + JS_PSG("script", DebuggerFrame_getScript, 0), + JS_PSG("this", DebuggerFrame::thisGetter, 0), + JS_PSG("type", DebuggerFrame::typeGetter, 0), + JS_PSG("implementation", DebuggerFrame::implementationGetter, 0), + JS_PSGS("onStep", DebuggerFrame_getOnStep, DebuggerFrame_setOnStep, 0), + JS_PSGS("onPop", DebuggerFrame_getOnPop, DebuggerFrame_setOnPop, 0), + JS_PS_END +}; + +const JSFunctionSpec DebuggerFrame::methods_[] = { + JS_FN("eval", DebuggerFrame::evalMethod, 1, 0), + JS_FN("evalWithBindings", DebuggerFrame::evalWithBindingsMethod, 1, 0), + JS_FS_END +}; + + +/*** Debugger.Object *****************************************************************************/ + +void +DebuggerObject_trace(JSTracer* trc, JSObject* obj) +{ + /* + * There is a barrier on private pointers, so the Unbarriered marking + * is okay. + */ + if (JSObject* referent = (JSObject*) obj->as<NativeObject>().getPrivate()) { + TraceManuallyBarrieredCrossCompartmentEdge(trc, obj, &referent, + "Debugger.Object referent"); + obj->as<NativeObject>().setPrivateUnbarriered(referent); + } +} + +static DebuggerObject* +DebuggerObject_checkThis(JSContext* cx, const CallArgs& args, const char* fnname) +{ + JSObject* thisobj = NonNullObject(cx, args.thisv()); + if (!thisobj) + return nullptr; + if (thisobj->getClass() != &DebuggerObject::class_) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, + "Debugger.Object", fnname, thisobj->getClass()->name); + return nullptr; + } + + /* + * Forbid Debugger.Object.prototype, which is of class DebuggerObject::class_ + * but isn't a real working Debugger.Object. The prototype object is + * distinguished by having no referent. + */ + DebuggerObject* nthisobj = &thisobj->as<DebuggerObject>(); + if (!nthisobj->getPrivate()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, + "Debugger.Object", fnname, "prototype object"); + return nullptr; + } + return nthisobj; +} + +#define THIS_DEBUGOBJECT(cx, argc, vp, fnname, args, object) \ + CallArgs args = CallArgsFromVp(argc, vp); \ + RootedDebuggerObject object(cx, DebuggerObject_checkThis(cx, args, fnname)); \ + if (!object) \ + return false; \ + +#define THIS_DEBUGOBJECT_REFERENT(cx, argc, vp, fnname, args, obj) \ + CallArgs args = CallArgsFromVp(argc, vp); \ + RootedObject obj(cx, DebuggerObject_checkThis(cx, args, fnname)); \ + if (!obj) \ + return false; \ + obj = (JSObject*) obj->as<NativeObject>().getPrivate(); \ + MOZ_ASSERT(obj) + +#define THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, fnname, args, dbg, obj) \ + CallArgs args = CallArgsFromVp(argc, vp); \ + RootedObject obj(cx, DebuggerObject_checkThis(cx, args, fnname)); \ + if (!obj) \ + return false; \ + Debugger* dbg = Debugger::fromChildJSObject(obj); \ + obj = (JSObject*) obj->as<NativeObject>().getPrivate(); \ + MOZ_ASSERT(obj) + +#define THIS_DEBUGOBJECT_PROMISE(cx, argc, vp, fnname, args, obj) \ + THIS_DEBUGOBJECT_REFERENT(cx, argc, vp, fnname, args, obj); \ + obj = CheckedUnwrap(obj); \ + if (!obj) { \ + JS_ReportErrorASCII(cx, "Permission denied to access object"); \ + return false; \ + } \ + if (!obj->is<PromiseObject>()) { \ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE,\ + "Debugger", "Promise", obj->getClass()->name); \ + return false; \ + } \ + Rooted<PromiseObject*> promise(cx, &obj->as<PromiseObject>()); + +#define THIS_DEBUGOBJECT_OWNER_PROMISE(cx, argc, vp, fnname, args, dbg, obj) \ + THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, fnname, args, dbg, obj); \ + obj = CheckedUnwrap(obj); \ + if (!obj) { \ + JS_ReportErrorASCII(cx, "Permission denied to access object"); \ + return false; \ + } \ + if (!obj->is<PromiseObject>()) { \ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE,\ + "Debugger", "Promise", obj->getClass()->name); \ + return false; \ + } \ + Rooted<PromiseObject*> promise(cx, &obj->as<PromiseObject>()); + +/* static */ bool +DebuggerObject::construct(JSContext* cx, unsigned argc, Value* vp) +{ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, + "Debugger.Object"); + return false; +} + +/* static */ bool +DebuggerObject::callableGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get callable", args, object) + + args.rval().setBoolean(object->isCallable()); + return true; +} + +/* static */ bool +DebuggerObject::isBoundFunctionGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get isBoundFunction", args, object) + + if (!object->isDebuggeeFunction()) { + args.rval().setUndefined(); + return true; + } + + args.rval().setBoolean(object->isBoundFunction()); + return true; +} + +/* static */ bool +DebuggerObject::isArrowFunctionGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get isArrowFunction", args, object) + + if (!object->isDebuggeeFunction()) { + args.rval().setUndefined(); + return true; + } + + args.rval().setBoolean(object->isArrowFunction()); + return true; +} + +/* static */ bool +DebuggerObject::protoGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get proto", args, object) + + RootedDebuggerObject result(cx); + if (!DebuggerObject::getPrototypeOf(cx, object, &result)) + return false; + + args.rval().setObjectOrNull(result); + return true; +} + +/* static */ bool +DebuggerObject::classGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get class", args, object) + + RootedString result(cx); + if (!DebuggerObject::getClassName(cx, object, &result)) + return false; + + args.rval().setString(result); + return true; +} + +/* static */ bool +DebuggerObject::nameGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get name", args, object) + + if (!object->isFunction()) { + args.rval().setUndefined(); + return true; + } + + RootedString result(cx, object->name()); + if (result) + args.rval().setString(result); + else + args.rval().setUndefined(); + return true; +} + +/* static */ bool +DebuggerObject::displayNameGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get displayName", args, object) + + if (!object->isFunction()) { + args.rval().setUndefined(); + return true; + } + + RootedString result(cx, object->displayName()); + if (result) + args.rval().setString(result); + else + args.rval().setUndefined(); + return true; +} + +/* static */ bool +DebuggerObject::parameterNamesGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get parameterNames", args, object) + + if (!object->isDebuggeeFunction()) { + args.rval().setUndefined(); + return true; + } + + Rooted<StringVector> names(cx, StringVector(cx)); + if (!DebuggerObject::getParameterNames(cx, object, &names)) + return false; + + RootedArrayObject obj(cx, NewDenseFullyAllocatedArray(cx, names.length())); + if (!obj) + return false; + + obj->ensureDenseInitializedLength(cx, 0, names.length()); + for (size_t i = 0; i < names.length(); ++i) { + Value v; + if (names[i]) + v = StringValue(names[i]); + else + v = UndefinedValue(); + obj->setDenseElement(i, v); + } + + args.rval().setObject(*obj); + return true; +} + +/* static */ bool +DebuggerObject::scriptGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "get script", args, dbg, obj); + + if (!obj->is<JSFunction>()) { + args.rval().setUndefined(); + return true; + } + + RootedFunction fun(cx, &obj->as<JSFunction>()); + if (!fun->isInterpreted()) { + args.rval().setUndefined(); + return true; + } + + RootedScript script(cx, GetOrCreateFunctionScript(cx, fun)); + if (!script) + return false; + + /* Only hand out debuggee scripts. */ + if (!dbg->observesScript(script)) { + args.rval().setNull(); + return true; + } + + RootedObject scriptObject(cx, dbg->wrapScript(cx, script)); + if (!scriptObject) + return false; + + args.rval().setObject(*scriptObject); + return true; +} + +/* static */ bool +DebuggerObject::environmentGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "get environment", args, dbg, obj); + + /* Don't bother switching compartments just to check obj's type and get its env. */ + if (!obj->is<JSFunction>() || !obj->as<JSFunction>().isInterpreted()) { + args.rval().setUndefined(); + return true; + } + + /* Only hand out environments of debuggee functions. */ + if (!dbg->observesGlobal(&obj->global())) { + args.rval().setNull(); + return true; + } + + Rooted<Env*> env(cx); + { + AutoCompartment ac(cx, obj); + RootedFunction fun(cx, &obj->as<JSFunction>()); + env = GetDebugEnvironmentForFunction(cx, fun); + if (!env) + return false; + } + + return dbg->wrapEnvironment(cx, env, args.rval()); +} + +/* static */ bool +DebuggerObject::boundTargetFunctionGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get boundTargetFunction", args, object) + + if (!object->isDebuggeeFunction() || !object->isBoundFunction()) { + args.rval().setUndefined(); + return true; + } + + RootedDebuggerObject result(cx); + if (!DebuggerObject::getBoundTargetFunction(cx, object, &result)) + return false; + + args.rval().setObject(*result); + return true; +} + +/* static */ bool +DebuggerObject::boundThisGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get boundThis", args, object) + + if (!object->isDebuggeeFunction() || !object->isBoundFunction()) { + args.rval().setUndefined(); + return true; + } + + return DebuggerObject::getBoundThis(cx, object, args.rval()); +} + +/* static */ bool +DebuggerObject::boundArgumentsGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get boundArguments", args, object) + + if (!object->isDebuggeeFunction() || !object->isBoundFunction()) { + args.rval().setUndefined(); + return true; + } + + Rooted<ValueVector> result(cx, ValueVector(cx)); + if (!DebuggerObject::getBoundArguments(cx, object, &result)) + return false; + + RootedObject obj(cx, NewDenseCopiedArray(cx, result.length(), result.begin())); + if (!obj) + return false; + + args.rval().setObject(*obj); + return true; +} + +/* static */ bool +DebuggerObject::globalGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get global", args, object) + + RootedDebuggerObject result(cx); + if (!DebuggerObject::getGlobal(cx, object, &result)) + return false; + + args.rval().setObject(*result); + return true; +} + +/* static */ bool +DebuggerObject::allocationSiteGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get allocationSite", args, object) + + RootedObject result(cx); + if (!DebuggerObject::getAllocationSite(cx, object, &result)) + return false; + + args.rval().setObjectOrNull(result); + return true; +} + +// Returns the "name" field (see js.msg), which may be used as a unique +// identifier, for any error object with a JSErrorReport or undefined +// if the object has no JSErrorReport. +/* static */ bool +DebuggerObject::errorMessageNameGetter(JSContext *cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get errorMessageName", args, object) + + RootedString result(cx); + if (!DebuggerObject::getErrorMessageName(cx, object, &result)) + return false; + + if (result) + args.rval().setString(result); + else + args.rval().setUndefined(); + return true; +} + +/* static */ bool +DebuggerObject::errorLineNumberGetter(JSContext *cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get errorLineNumber", args, object) + + return DebuggerObject::getErrorLineNumber(cx, object, args.rval()); +} + +/* static */ bool +DebuggerObject::errorColumnNumberGetter(JSContext *cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get errorColumnNumber", args, object) + + return DebuggerObject::getErrorColumnNumber(cx, object, args.rval()); +} + +/* static */ bool +DebuggerObject::isProxyGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get isProxy", args, object) + + args.rval().setBoolean(object->isScriptedProxy()); + return true; +} + +/* static */ bool +DebuggerObject::proxyTargetGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get proxyTarget", args, object) + + if (!object->isScriptedProxy()) { + args.rval().setUndefined(); + return true; + } + + Rooted<DebuggerObject*> result(cx); + if (!DebuggerObject::getScriptedProxyTarget(cx, object, &result)) + return false; + + args.rval().setObjectOrNull(result); + return true; +} + +/* static */ bool +DebuggerObject::proxyHandlerGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get proxyHandler", args, object) + + if (!object->isScriptedProxy()) { + args.rval().setUndefined(); + return true; + } + Rooted<DebuggerObject*> result(cx); + if (!DebuggerObject::getScriptedProxyHandler(cx, object, &result)) + return false; + + args.rval().setObjectOrNull(result); + return true; +} + +#ifdef SPIDERMONKEY_PROMISE +/* static */ bool +DebuggerObject::isPromiseGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get isPromise", args, object) + + args.rval().setBoolean(object->isPromise()); + return true; +} + +/* static */ bool +DebuggerObject::promiseStateGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get promiseState", args, object); + + if (!DebuggerObject::requirePromise(cx, object)) + return false; + + RootedValue result(cx); + switch (object->promiseState()) { + case JS::PromiseState::Pending: + result.setString(cx->names().pending); + break; + case JS::PromiseState::Fulfilled: + result.setString(cx->names().fulfilled); + break; + case JS::PromiseState::Rejected: + result.setString(cx->names().rejected); + break; + } + + args.rval().set(result); + return true; +} + +/* static */ bool +DebuggerObject::promiseValueGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get promiseValue", args, object); + + if (!DebuggerObject::requirePromise(cx, object)) + return false; + + if (object->promiseState() != JS::PromiseState::Fulfilled) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_PROMISE_NOT_FULFILLED); + return false; + } + + return DebuggerObject::getPromiseValue(cx, object, args.rval());; +} + +/* static */ bool +DebuggerObject::promiseReasonGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get promiseReason", args, object); + + if (!DebuggerObject::requirePromise(cx, object)) + return false; + + if (object->promiseState() != JS::PromiseState::Rejected) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_PROMISE_NOT_REJECTED); + return false; + } + + return DebuggerObject::getPromiseReason(cx, object, args.rval());; +} + +/* static */ bool +DebuggerObject::promiseLifetimeGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get promiseLifetime", args, object); + + if (!DebuggerObject::requirePromise(cx, object)) + return false; + + args.rval().setNumber(object->promiseLifetime()); + return true; +} + +/* static */ bool +DebuggerObject::promiseTimeToResolutionGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "get promiseTimeToResolution", args, object); + + if (!DebuggerObject::requirePromise(cx, object)) + return false; + + if (object->promiseState() == JS::PromiseState::Pending) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_PROMISE_NOT_RESOLVED); + return false; + } + + args.rval().setNumber(object->promiseTimeToResolution()); + return true; +} + +/* static */ bool +DebuggerObject::promiseAllocationSiteGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT_PROMISE(cx, argc, vp, "get promiseAllocationSite", args, refobj); + + RootedObject allocSite(cx, promise->allocationSite()); + if (!allocSite) { + args.rval().setNull(); + return true; + } + + if (!cx->compartment()->wrap(cx, &allocSite)) + return false; + args.rval().set(ObjectValue(*allocSite)); + return true; +} + +/* static */ bool +DebuggerObject::promiseResolutionSiteGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT_PROMISE(cx, argc, vp, "get promiseResolutionSite", args, refobj); + + if (promise->state() == JS::PromiseState::Pending) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_PROMISE_NOT_RESOLVED); + return false; + } + + RootedObject resolutionSite(cx, promise->resolutionSite()); + if (!resolutionSite) { + args.rval().setNull(); + return true; + } + + if (!cx->compartment()->wrap(cx, &resolutionSite)) + return false; + args.rval().set(ObjectValue(*resolutionSite)); + return true; +} + +/* static */ bool +DebuggerObject::promiseIDGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT_PROMISE(cx, argc, vp, "get promiseID", args, refobj); + + args.rval().setNumber(double(promise->getID())); + return true; +} + +/* static */ bool +DebuggerObject::promiseDependentPromisesGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT_OWNER_PROMISE(cx, argc, vp, "get promiseDependentPromises", args, dbg, refobj); + + Rooted<GCVector<Value>> values(cx, GCVector<Value>(cx)); + { + JSAutoCompartment ac(cx, promise); + if (!promise->dependentPromises(cx, &values)) + return false; + } + for (size_t i = 0; i < values.length(); i++) { + if (!dbg->wrapDebuggeeValue(cx, values[i])) + return false; + } + RootedArrayObject promises(cx); + if (values.length() == 0) + promises = NewDenseEmptyArray(cx); + else + promises = NewDenseCopiedArray(cx, values.length(), values[0].address()); + if (!promises) + return false; + args.rval().setObject(*promises); + return true; +} +#endif // SPIDERMONKEY_PROMISE + +/* static */ bool +DebuggerObject::isExtensibleMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "isExtensible", args, object) + + bool result; + if (!DebuggerObject::isExtensible(cx, object, result)) + return false; + + args.rval().setBoolean(result); + return true; +} + +/* static */ bool +DebuggerObject::isSealedMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "isSealed", args, object) + + bool result; + if (!DebuggerObject::isSealed(cx, object, result)) + return false; + + args.rval().setBoolean(result); + return true; +} + +/* static */ bool +DebuggerObject::isFrozenMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "isFrozen", args, object) + + bool result; + if (!DebuggerObject::isFrozen(cx, object, result)) + return false; + + args.rval().setBoolean(result); + return true; +} + +static JSObject* +IdVectorToArray(JSContext* cx, Handle<IdVector> ids) +{ + Rooted<ValueVector> vals(cx, ValueVector(cx)); + if (!vals.growBy(ids.length())) + return nullptr; + + for (size_t i = 0, len = ids.length(); i < len; i++) { + jsid id = ids[i]; + if (JSID_IS_INT(id)) { + JSString* str = Int32ToString<CanGC>(cx, JSID_TO_INT(id)); + if (!str) + return nullptr; + vals[i].setString(str); + } else if (JSID_IS_ATOM(id)) { + vals[i].setString(JSID_TO_STRING(id)); + } else if (JSID_IS_SYMBOL(id)) { + vals[i].setSymbol(JSID_TO_SYMBOL(id)); + } else { + MOZ_ASSERT_UNREACHABLE("IdVector must contain only string, int, and Symbol jsids"); + } + } + + return NewDenseCopiedArray(cx, vals.length(), vals.begin()); +} + +/* static */ bool +DebuggerObject::getOwnPropertyNamesMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "getOwnPropertyNames", args, object) + + Rooted<IdVector> ids(cx, IdVector(cx)); + if (!DebuggerObject::getOwnPropertyNames(cx, object, &ids)) + return false; + + RootedObject obj(cx, IdVectorToArray(cx, ids)); + if (!obj) + return false; + + args.rval().setObject(*obj); + return true; +} + +/* static */ bool +DebuggerObject::getOwnPropertySymbolsMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "getOwnPropertySymbols", args, object) + + Rooted<IdVector> ids(cx, IdVector(cx)); + if (!DebuggerObject::getOwnPropertySymbols(cx, object, &ids)) + return false; + + RootedObject obj(cx, IdVectorToArray(cx, ids)); + if (!obj) + return false; + + args.rval().setObject(*obj); + return true; +} + +/* static */ bool +DebuggerObject::getOwnPropertyDescriptorMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "getOwnPropertyDescriptor", args, object) + + RootedId id(cx); + if (!ValueToId<CanGC>(cx, args.get(0), &id)) + return false; + + Rooted<PropertyDescriptor> desc(cx); + if (!DebuggerObject::getOwnPropertyDescriptor(cx, object, id, &desc)) + return false; + + return JS::FromPropertyDescriptor(cx, desc, args.rval()); +} + +/* static */ bool +DebuggerObject::preventExtensionsMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "preventExtensions", args, object) + + if (!DebuggerObject::preventExtensions(cx, object)) + return false; + + args.rval().setUndefined(); + return true; +} + +/* static */ bool +DebuggerObject::sealMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "seal", args, object) + + if (!DebuggerObject::seal(cx, object)) + return false; + + args.rval().setUndefined(); + return true; +} + +/* static */ bool +DebuggerObject::freezeMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "freeze", args, object) + + if (!DebuggerObject::freeze(cx, object)) + return false; + + args.rval().setUndefined(); + return true; +} + +/* static */ bool +DebuggerObject::definePropertyMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "defineProperty", args, object) + if (!args.requireAtLeast(cx, "Debugger.Object.defineProperty", 2)) + return false; + + RootedId id(cx); + if (!ValueToId<CanGC>(cx, args[0], &id)) + return false; + + Rooted<PropertyDescriptor> desc(cx); + if (!ToPropertyDescriptor(cx, args[1], false, &desc)) + return false; + + if (!DebuggerObject::defineProperty(cx, object, id, desc)) + return false; + + args.rval().setUndefined(); + return true; +} + +/* static */ bool +DebuggerObject::definePropertiesMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "defineProperties", args, object); + if (!args.requireAtLeast(cx, "Debugger.Object.defineProperties", 1)) + return false; + + RootedValue arg(cx, args[0]); + RootedObject props(cx, ToObject(cx, arg)); + if (!props) + return false; + AutoIdVector ids(cx); + Rooted<PropertyDescriptorVector> descs(cx, PropertyDescriptorVector(cx)); + if (!ReadPropertyDescriptors(cx, props, false, &ids, &descs)) + return false; + Rooted<IdVector> ids2(cx, IdVector(cx)); + if (!ids2.append(ids.begin(), ids.end())) + return false; + + if (!DebuggerObject::defineProperties(cx, object, ids2, descs)) + return false; + + args.rval().setUndefined(); + return true; +} + +/* + * This does a non-strict delete, as a matter of API design. The case where the + * property is non-configurable isn't necessarily exceptional here. + */ +/* static */ bool +DebuggerObject::deletePropertyMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "deleteProperty", args, object) + + RootedId id(cx); + if (!ValueToId<CanGC>(cx, args.get(0), &id)) + return false; + + ObjectOpResult result; + if (!DebuggerObject::deleteProperty(cx, object, id, result)) + return false; + + args.rval().setBoolean(result.ok()); + return true; +} + +/* static */ bool +DebuggerObject::callMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "call", callArgs, object); + + RootedValue thisv(cx, callArgs.get(0)); + + Rooted<ValueVector> args(cx, ValueVector(cx)); + if (callArgs.length() >= 2) { + if (!args.growBy(callArgs.length() - 1)) + return false; + for (size_t i = 1; i < callArgs.length(); ++i) + args[i - 1].set(callArgs[i]); + } + + return object->call(cx, object, thisv, args, callArgs.rval()); +} + +/* static */ bool +DebuggerObject::applyMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "apply", callArgs, object); + + RootedValue thisv(cx, callArgs.get(0)); + + Rooted<ValueVector> args(cx, ValueVector(cx)); + if (callArgs.length() >= 2 && !callArgs[1].isNullOrUndefined()) { + if (!callArgs[1].isObject()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_APPLY_ARGS, + js_apply_str); + return false; + } + + RootedObject argsobj(cx, &callArgs[1].toObject()); + + unsigned argc = 0; + if (!GetLengthProperty(cx, argsobj, &argc)) + return false; + argc = unsigned(Min(argc, ARGS_LENGTH_MAX)); + + if (!args.growBy(argc) || !GetElements(cx, argsobj, argc, args.begin())) + return false; + } + + return object->call(cx, object, thisv, args, callArgs.rval()); +} + +/* static */ bool +DebuggerObject::asEnvironmentMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "asEnvironment", args, dbg, referent); + if (!RequireGlobalObject(cx, args.thisv(), referent)) + return false; + + Rooted<Env*> env(cx); + { + AutoCompartment ac(cx, referent); + env = GetDebugEnvironmentForGlobalLexicalEnvironment(cx); + if (!env) + return false; + } + + return dbg->wrapEnvironment(cx, env, args.rval()); +} + +// Lookup a binding on the referent's global scope and change it to undefined +// if it is an uninitialized lexical, otherwise do nothing. The method's +// JavaScript return value is true _only_ when an uninitialized lexical has been +// altered, otherwise it is false. +/* static */ bool +DebuggerObject::forceLexicalInitializationByNameMethod(JSContext *cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "forceLexicalInitializationByName", args, object) + if (!args.requireAtLeast(cx, "Debugger.Object.prototype.forceLexicalInitializationByName", 1)) + return false; + + if (!DebuggerObject::requireGlobal(cx, object)) + return false; + + RootedId id(cx); + if (!ValueToIdentifier(cx, args[0], &id)) + return false; + + bool result; + if (!DebuggerObject::forceLexicalInitializationByName(cx, object, id, result)) + return false; + + args.rval().setBoolean(result); + return true; +} + +/* static */ bool +DebuggerObject::executeInGlobalMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "executeInGlobal", args, object); + if (!args.requireAtLeast(cx, "Debugger.Object.prototype.executeInGlobal", 1)) + return false; + + if (!DebuggerObject::requireGlobal(cx, object)) + return false; + + AutoStableStringChars stableChars(cx); + if (!ValueToStableChars(cx, "Debugger.Object.prototype.executeInGlobal", args[0], + stableChars)) + { + return false; + } + mozilla::Range<const char16_t> chars = stableChars.twoByteRange(); + + EvalOptions options; + if (!ParseEvalOptions(cx, args.get(1), options)) + return false; + + JSTrapStatus status; + RootedValue value(cx); + if (!DebuggerObject::executeInGlobal(cx, object, chars, nullptr, options, status, &value)) + return false; + + return object->owner()->newCompletionValue(cx, status, value, args.rval()); +} + +/* static */ bool +DebuggerObject::executeInGlobalWithBindingsMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "executeInGlobalWithBindings", args, object); + if (!args.requireAtLeast(cx, "Debugger.Object.prototype.executeInGlobalWithBindings", 2)) + return false; + + if (!DebuggerObject::requireGlobal(cx, object)) + return false; + + AutoStableStringChars stableChars(cx); + if (!ValueToStableChars(cx, "Debugger.Object.prototype.executeInGlobalWithBindings", args[0], + stableChars)) + { + return false; + } + mozilla::Range<const char16_t> chars = stableChars.twoByteRange(); + + RootedObject bindings(cx, NonNullObject(cx, args[1])); + if (!bindings) + return false; + + EvalOptions options; + if (!ParseEvalOptions(cx, args.get(2), options)) + return false; + + JSTrapStatus status; + RootedValue value(cx); + if (!DebuggerObject::executeInGlobal(cx, object, chars, bindings, options, status, &value)) + return false; + + return object->owner()->newCompletionValue(cx, status, value, args.rval()); +} + +/* static */ bool +DebuggerObject::makeDebuggeeValueMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "makeDebuggeeValue", args, object); + if (!args.requireAtLeast(cx, "Debugger.Object.prototype.makeDebuggeeValue", 1)) + return false; + + return DebuggerObject::makeDebuggeeValue(cx, object, args[0], args.rval()); +} + +/* static */ bool +DebuggerObject::unsafeDereferenceMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "unsafeDereference", args, object); + + RootedObject result(cx); + if (!DebuggerObject::unsafeDereference(cx, object, &result)) + return false; + + args.rval().setObject(*result); + return true; +} + +/* static */ bool +DebuggerObject::unwrapMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGOBJECT(cx, argc, vp, "unwrap", args, object); + + RootedDebuggerObject result(cx); + if (!DebuggerObject::unwrap(cx, object, &result)) + return false; + + args.rval().setObjectOrNull(result); + return true; +} + +const JSPropertySpec DebuggerObject::properties_[] = { + JS_PSG("callable", DebuggerObject::callableGetter, 0), + JS_PSG("isBoundFunction", DebuggerObject::isBoundFunctionGetter, 0), + JS_PSG("isArrowFunction", DebuggerObject::isArrowFunctionGetter, 0), + JS_PSG("proto", DebuggerObject::protoGetter, 0), + JS_PSG("class", DebuggerObject::classGetter, 0), + JS_PSG("name", DebuggerObject::nameGetter, 0), + JS_PSG("displayName", DebuggerObject::displayNameGetter, 0), + JS_PSG("parameterNames", DebuggerObject::parameterNamesGetter, 0), + JS_PSG("script", DebuggerObject::scriptGetter, 0), + JS_PSG("environment", DebuggerObject::environmentGetter, 0), + JS_PSG("boundTargetFunction", DebuggerObject::boundTargetFunctionGetter, 0), + JS_PSG("boundThis", DebuggerObject::boundThisGetter, 0), + JS_PSG("boundArguments", DebuggerObject::boundArgumentsGetter, 0), + JS_PSG("global", DebuggerObject::globalGetter, 0), + JS_PSG("allocationSite", DebuggerObject::allocationSiteGetter, 0), + JS_PSG("errorMessageName", DebuggerObject::errorMessageNameGetter, 0), + JS_PSG("errorLineNumber", DebuggerObject::errorLineNumberGetter, 0), + JS_PSG("errorColumnNumber", DebuggerObject::errorColumnNumberGetter, 0), + JS_PSG("isProxy", DebuggerObject::isProxyGetter, 0), + JS_PSG("proxyTarget", DebuggerObject::proxyTargetGetter, 0), + JS_PSG("proxyHandler", DebuggerObject::proxyHandlerGetter, 0), + JS_PS_END +}; + +#ifdef SPIDERMONKEY_PROMISE +const JSPropertySpec DebuggerObject::promiseProperties_[] = { + JS_PSG("isPromise", DebuggerObject::isPromiseGetter, 0), + JS_PSG("promiseState", DebuggerObject::promiseStateGetter, 0), + JS_PSG("promiseValue", DebuggerObject::promiseValueGetter, 0), + JS_PSG("promiseReason", DebuggerObject::promiseReasonGetter, 0), + JS_PSG("promiseLifetime", DebuggerObject::promiseLifetimeGetter, 0), + JS_PSG("promiseTimeToResolution", DebuggerObject::promiseTimeToResolutionGetter, 0), + JS_PSG("promiseAllocationSite", DebuggerObject::promiseAllocationSiteGetter, 0), + JS_PSG("promiseResolutionSite", DebuggerObject::promiseResolutionSiteGetter, 0), + JS_PSG("promiseID", DebuggerObject::promiseIDGetter, 0), + JS_PSG("promiseDependentPromises", DebuggerObject::promiseDependentPromisesGetter, 0), + JS_PS_END +}; +#endif // SPIDERMONKEY_PROMISE + +const JSFunctionSpec DebuggerObject::methods_[] = { + JS_FN("isExtensible", DebuggerObject::isExtensibleMethod, 0, 0), + JS_FN("isSealed", DebuggerObject::isSealedMethod, 0, 0), + JS_FN("isFrozen", DebuggerObject::isFrozenMethod, 0, 0), + JS_FN("getOwnPropertyNames", DebuggerObject::getOwnPropertyNamesMethod, 0, 0), + JS_FN("getOwnPropertySymbols", DebuggerObject::getOwnPropertySymbolsMethod, 0, 0), + JS_FN("getOwnPropertyDescriptor", DebuggerObject::getOwnPropertyDescriptorMethod, 1, 0), + JS_FN("preventExtensions", DebuggerObject::preventExtensionsMethod, 0, 0), + JS_FN("seal", DebuggerObject::sealMethod, 0, 0), + JS_FN("freeze", DebuggerObject::freezeMethod, 0, 0), + JS_FN("defineProperty", DebuggerObject::definePropertyMethod, 2, 0), + JS_FN("defineProperties", DebuggerObject::definePropertiesMethod, 1, 0), + JS_FN("deleteProperty", DebuggerObject::deletePropertyMethod, 1, 0), + JS_FN("call", DebuggerObject::callMethod, 0, 0), + JS_FN("apply", DebuggerObject::applyMethod, 0, 0), + JS_FN("asEnvironment", DebuggerObject::asEnvironmentMethod, 0, 0), + JS_FN("forceLexicalInitializationByName", DebuggerObject::forceLexicalInitializationByNameMethod, 1, 0), + JS_FN("executeInGlobal", DebuggerObject::executeInGlobalMethod, 1, 0), + JS_FN("executeInGlobalWithBindings", DebuggerObject::executeInGlobalWithBindingsMethod, 2, 0), + JS_FN("makeDebuggeeValue", DebuggerObject::makeDebuggeeValueMethod, 1, 0), + JS_FN("unsafeDereference", DebuggerObject::unsafeDereferenceMethod, 0, 0), + JS_FN("unwrap", DebuggerObject::unwrapMethod, 0, 0), + JS_FS_END +}; + +/* static */ NativeObject* +DebuggerObject::initClass(JSContext* cx, HandleObject obj, HandleObject debugCtor) +{ + Rooted<GlobalObject*> global(cx, &obj->as<GlobalObject>()); + RootedObject objProto(cx, global->getOrCreateObjectPrototype(cx)); + + RootedNativeObject objectProto(cx, InitClass(cx, debugCtor, objProto, &class_, + construct, 0, properties_, + methods_, nullptr, nullptr)); + + if (!objectProto) + return nullptr; + +#ifdef SPIDERMONKEY_PROMISE + if (!DefinePropertiesAndFunctions(cx, objectProto, promiseProperties_, nullptr)) + return nullptr; +#endif // SPIDERMONKEY_PROMISE + + return objectProto; +} + +/* static */ DebuggerObject* +DebuggerObject::create(JSContext* cx, HandleObject proto, HandleObject referent, + HandleNativeObject debugger) +{ + NewObjectKind newKind = IsInsideNursery(referent) ? GenericObject : TenuredObject; + JSObject* obj = NewObjectWithGivenProto(cx, &DebuggerObject::class_, proto, newKind); + if (!obj) + return nullptr; + + DebuggerObject& object = obj->as<DebuggerObject>(); + object.setPrivateGCThing(referent); + object.setReservedSlot(JSSLOT_DEBUGOBJECT_OWNER, ObjectValue(*debugger)); + + return &object; +} + +bool +DebuggerObject::isCallable() const +{ + return referent()->isCallable(); +} + +bool +DebuggerObject::isFunction() const +{ + return referent()->is<JSFunction>(); +} + +bool +DebuggerObject::isDebuggeeFunction() const +{ + return referent()->is<JSFunction>() && + owner()->observesGlobal(&referent()->as<JSFunction>().global()); +} + +bool +DebuggerObject::isBoundFunction() const +{ + MOZ_ASSERT(isDebuggeeFunction()); + + return referent()->isBoundFunction(); +} + +bool +DebuggerObject::isArrowFunction() const +{ + MOZ_ASSERT(isDebuggeeFunction()); + + return referent()->as<JSFunction>().isArrow(); +} + +bool +DebuggerObject::isGlobal() const +{ + return referent()->is<GlobalObject>(); +} + +bool +DebuggerObject::isScriptedProxy() const +{ + return js::IsScriptedProxy(referent()); +} + +#ifdef SPIDERMONKEY_PROMISE +bool +DebuggerObject::isPromise() const +{ + JSObject* referent = this->referent(); + + if (IsCrossCompartmentWrapper(referent)) { + referent = CheckedUnwrap(referent); + if (!referent) + return false; + } + + return referent->is<PromiseObject>(); +} +#endif // SPIDERMONKEY_PROMISE + +/* static */ bool +DebuggerObject::getClassName(JSContext* cx, HandleDebuggerObject object, + MutableHandleString result) +{ + RootedObject referent(cx, object->referent()); + + const char* className; + { + AutoCompartment ac(cx, referent); + className = GetObjectClassName(cx, referent); + } + + JSAtom* str = Atomize(cx, className, strlen(className)); + if (!str) + return false; + + result.set(str); + return true; +} + +/* static */ bool +DebuggerObject::getGlobal(JSContext* cx, HandleDebuggerObject object, + MutableHandleDebuggerObject result) +{ + RootedObject referent(cx, object->referent()); + Debugger* dbg = object->owner(); + + RootedObject global(cx, &referent->global()); + return dbg->wrapDebuggeeObject(cx, global, result); +} + +JSAtom* +DebuggerObject::name() const +{ + MOZ_ASSERT(isFunction()); + + return referent()->as<JSFunction>().name(); +} + +JSAtom* +DebuggerObject::displayName() const +{ + MOZ_ASSERT(isFunction()); + + return referent()->as<JSFunction>().displayAtom(); +} + +JS::PromiseState +DebuggerObject::promiseState() const +{ + return promise()->state(); +} + +double +DebuggerObject::promiseLifetime() const +{ + return promise()->lifetime(); +} + +double +DebuggerObject::promiseTimeToResolution() const +{ + MOZ_ASSERT(promiseState() != JS::PromiseState::Pending); + + return promise()->timeToResolution(); +} + +/* static */ bool +DebuggerObject::getParameterNames(JSContext* cx, HandleDebuggerObject object, + MutableHandle<StringVector> result) +{ + MOZ_ASSERT(object->isDebuggeeFunction()); + + RootedFunction referent(cx, &object->referent()->as<JSFunction>()); + + if (!result.growBy(referent->nargs())) + return false; + if (referent->isInterpreted()) { + RootedScript script(cx, GetOrCreateFunctionScript(cx, referent)); + if (!script) + return false; + + MOZ_ASSERT(referent->nargs() == script->numArgs()); + + if (referent->nargs() > 0) { + PositionalFormalParameterIter fi(script); + for (size_t i = 0; i < referent->nargs(); i++, fi++) { + MOZ_ASSERT(fi.argumentSlot() == i); + result[i].set(fi.name()); + } + } + } else { + for (size_t i = 0; i < referent->nargs(); i++) + result[i].set(nullptr); + } + + return true; +} + +/* static */ bool +DebuggerObject::getBoundTargetFunction(JSContext* cx, HandleDebuggerObject object, + MutableHandleDebuggerObject result) +{ + MOZ_ASSERT(object->isBoundFunction()); + + RootedFunction referent(cx, &object->referent()->as<JSFunction>()); + Debugger* dbg = object->owner(); + + RootedObject target(cx, referent->getBoundFunctionTarget()); + return dbg->wrapDebuggeeObject(cx, target, result); +} + +/* static */ bool +DebuggerObject::getBoundThis(JSContext* cx, HandleDebuggerObject object, + MutableHandleValue result) +{ + MOZ_ASSERT(object->isBoundFunction()); + + RootedFunction referent(cx, &object->referent()->as<JSFunction>()); + Debugger* dbg = object->owner(); + + result.set(referent->getBoundFunctionThis()); + return dbg->wrapDebuggeeValue(cx, result); +} + +/* static */ bool +DebuggerObject::getBoundArguments(JSContext* cx, HandleDebuggerObject object, + MutableHandle<ValueVector> result) +{ + MOZ_ASSERT(object->isBoundFunction()); + + RootedFunction referent(cx, &object->referent()->as<JSFunction>()); + Debugger* dbg = object->owner(); + + size_t length = referent->getBoundFunctionArgumentCount(); + if (!result.resize(length)) + return false; + for (size_t i = 0; i < length; i++) { + result[i].set(referent->getBoundFunctionArgument(cx, i)); + if (!dbg->wrapDebuggeeValue(cx, result[i])) + return false; + } + return true; +} + +/* static */ SavedFrame* +Debugger::getObjectAllocationSite(JSObject& obj) +{ + JSObject* metadata = GetAllocationMetadata(&obj); + if (!metadata) + return nullptr; + + MOZ_ASSERT(!metadata->is<WrapperObject>()); + return SavedFrame::isSavedFrameAndNotProto(*metadata) + ? &metadata->as<SavedFrame>() + : nullptr; +} + +/* static */ bool +DebuggerObject::getAllocationSite(JSContext* cx, HandleDebuggerObject object, + MutableHandleObject result) +{ + RootedObject referent(cx, object->referent()); + + RootedObject allocSite(cx, Debugger::getObjectAllocationSite(*referent)); + if (!cx->compartment()->wrap(cx, &allocSite)) + return false; + + result.set(allocSite); + return true; +} + +/* static */ bool +DebuggerObject::getErrorReport(JSContext* cx, HandleObject maybeError, JSErrorReport*& report) +{ + JSObject* obj = maybeError; + if (IsCrossCompartmentWrapper(obj)) + obj = CheckedUnwrap(obj); + + if (!obj) { + JS_ReportErrorASCII(cx, "Permission denied to access object"); + return false; + } + + if (!obj->is<ErrorObject>()) { + report = nullptr; + return true; + } + + report = obj->as<ErrorObject>().getErrorReport(); + return true; +} + +/* static */ bool +DebuggerObject::getErrorMessageName(JSContext* cx, HandleDebuggerObject object, + MutableHandleString result) +{ + RootedObject referent(cx, object->referent()); + JSErrorReport* report; + if (!getErrorReport(cx, referent, report)) + return false; + + if (!report) { + result.set(nullptr); + return true; + } + + const JSErrorFormatString* efs = GetErrorMessage(nullptr, report->errorNumber); + if (!efs) { + result.set(nullptr); + return true; + } + + RootedString str(cx, JS_NewStringCopyZ(cx, efs->name)); + if (!cx->compartment()->wrap(cx, &str)) + return false; + + result.set(str); + return true; +} + +/* static */ bool +DebuggerObject::getErrorLineNumber(JSContext* cx, HandleDebuggerObject object, + MutableHandleValue result) +{ + RootedObject referent(cx, object->referent()); + JSErrorReport* report; + if (!getErrorReport(cx, referent, report)) + return false; + + if (!report) { + result.setUndefined(); + return true; + } + + result.setNumber(report->lineno); + return true; +} + +/* static */ bool +DebuggerObject::getErrorColumnNumber(JSContext* cx, HandleDebuggerObject object, + MutableHandleValue result) +{ + RootedObject referent(cx, object->referent()); + JSErrorReport* report; + if (!getErrorReport(cx, referent, report)) + return false; + + if (!report) { + result.setUndefined(); + return true; + } + + result.setNumber(report->column); + return true; +} + +#ifdef SPIDERMONKEY_PROMISE +/* static */ bool +DebuggerObject::getPromiseValue(JSContext* cx, HandleDebuggerObject object, + MutableHandleValue result) +{ + MOZ_ASSERT(object->promiseState() == JS::PromiseState::Fulfilled); + + result.set(object->promise()->value()); + return object->owner()->wrapDebuggeeValue(cx, result); +} + +/* static */ bool +DebuggerObject::getPromiseReason(JSContext* cx, HandleDebuggerObject object, + MutableHandleValue result) +{ + MOZ_ASSERT(object->promiseState() == JS::PromiseState::Rejected); + + result.set(object->promise()->reason()); + return object->owner()->wrapDebuggeeValue(cx, result); +} +#endif // SPIDERMONKEY_PROMISE + +/* static */ bool +DebuggerObject::isExtensible(JSContext* cx, HandleDebuggerObject object, bool& result) +{ + RootedObject referent(cx, object->referent()); + + Maybe<AutoCompartment> ac; + ac.emplace(cx, referent); + ErrorCopier ec(ac); + return IsExtensible(cx, referent, &result); +} + +/* static */ bool +DebuggerObject::isSealed(JSContext* cx, HandleDebuggerObject object, bool& result) +{ + RootedObject referent(cx, object->referent()); + + Maybe<AutoCompartment> ac; + ac.emplace(cx, referent); + + ErrorCopier ec(ac); + return TestIntegrityLevel(cx, referent, IntegrityLevel::Sealed, &result); +} + +/* static */ bool +DebuggerObject::isFrozen(JSContext* cx, HandleDebuggerObject object, bool& result) +{ + RootedObject referent(cx, object->referent()); + + Maybe<AutoCompartment> ac; + ac.emplace(cx, referent); + + ErrorCopier ec(ac); + return TestIntegrityLevel(cx, referent, IntegrityLevel::Frozen, &result); +} + +/* static */ bool +DebuggerObject::getPrototypeOf(JSContext* cx, HandleDebuggerObject object, + MutableHandleDebuggerObject result) +{ + RootedObject referent(cx, object->referent()); + Debugger* dbg = object->owner(); + + RootedObject proto(cx); + { + AutoCompartment ac(cx, referent); + if (!GetPrototype(cx, referent, &proto)) + return false; + } + + if (!proto) { + result.set(nullptr); + return true; + } + + return dbg->wrapDebuggeeObject(cx, proto, result); +} + +/* static */ bool +DebuggerObject::getOwnPropertyNames(JSContext* cx, HandleDebuggerObject object, + MutableHandle<IdVector> result) +{ + RootedObject referent(cx, object->referent()); + + AutoIdVector ids(cx); + { + Maybe<AutoCompartment> ac; + ac.emplace(cx, referent); + + ErrorCopier ec(ac); + if (!GetPropertyKeys(cx, referent, JSITER_OWNONLY | JSITER_HIDDEN, &ids)) + return false; + } + + return result.append(ids.begin(), ids.end()); +} + +/* static */ bool +DebuggerObject::getOwnPropertySymbols(JSContext* cx, HandleDebuggerObject object, + MutableHandle<IdVector> result) +{ + RootedObject referent(cx, object->referent()); + + AutoIdVector ids(cx); + { + Maybe<AutoCompartment> ac; + ac.emplace(cx, referent); + + ErrorCopier ec(ac); + if (!GetPropertyKeys(cx, referent, + JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS | JSITER_SYMBOLSONLY, + &ids)) + return false; + } + + return result.append(ids.begin(), ids.end()); +} + +/* static */ bool +DebuggerObject::getOwnPropertyDescriptor(JSContext* cx, HandleDebuggerObject object, + HandleId id, MutableHandle<PropertyDescriptor> desc) +{ + RootedObject referent(cx, object->referent()); + Debugger* dbg = object->owner(); + + /* Bug: This can cause the debuggee to run! */ + { + Maybe<AutoCompartment> ac; + ac.emplace(cx, referent); + + ErrorCopier ec(ac); + if (!GetOwnPropertyDescriptor(cx, referent, id, desc)) + return false; + } + + if (desc.object()) { + /* Rewrap the debuggee values in desc for the debugger. */ + if (!dbg->wrapDebuggeeValue(cx, desc.value())) + return false; + + if (desc.hasGetterObject()) { + RootedValue get(cx, ObjectOrNullValue(desc.getterObject())); + if (!dbg->wrapDebuggeeValue(cx, &get)) + return false; + desc.setGetterObject(get.toObjectOrNull()); + } + if (desc.hasSetterObject()) { + RootedValue set(cx, ObjectOrNullValue(desc.setterObject())); + if (!dbg->wrapDebuggeeValue(cx, &set)) + return false; + desc.setSetterObject(set.toObjectOrNull()); + } + + // Avoid tripping same-compartment assertions in JS::FromPropertyDescriptor(). + desc.object().set(object); + } + + return true; +} + +/* static */ bool +DebuggerObject::preventExtensions(JSContext* cx, HandleDebuggerObject object) +{ + RootedObject referent(cx, object->referent()); + + Maybe<AutoCompartment> ac; + ac.emplace(cx, referent); + + ErrorCopier ec(ac); + return PreventExtensions(cx, referent); +} + +/* static */ bool +DebuggerObject::seal(JSContext* cx, HandleDebuggerObject object) +{ + RootedObject referent(cx, object->referent()); + + Maybe<AutoCompartment> ac; + ac.emplace(cx, referent); + + ErrorCopier ec(ac); + return SetIntegrityLevel(cx, referent, IntegrityLevel::Sealed); +} + +/* static */ bool +DebuggerObject::freeze(JSContext* cx, HandleDebuggerObject object) +{ + RootedObject referent(cx, object->referent()); + + Maybe<AutoCompartment> ac; + ac.emplace(cx, referent); + + ErrorCopier ec(ac); + return SetIntegrityLevel(cx, referent, IntegrityLevel::Frozen); +} + +/* static */ bool +DebuggerObject::defineProperty(JSContext* cx, HandleDebuggerObject object, HandleId id, + Handle<PropertyDescriptor> desc_) +{ + RootedObject referent(cx, object->referent()); + Debugger* dbg = object->owner(); + + Rooted<PropertyDescriptor> desc(cx, desc_); + if (!dbg->unwrapPropertyDescriptor(cx, referent, &desc)) + return false; + if (!CheckPropertyDescriptorAccessors(cx, desc)) + return false; + + Maybe<AutoCompartment> ac; + ac.emplace(cx, referent); + if (!cx->compartment()->wrap(cx, &desc)) + return false; + + ErrorCopier ec(ac); + if (!DefineProperty(cx, referent, id, desc)) + return false; + + return true; +} + +/* static */ bool +DebuggerObject::defineProperties(JSContext* cx, HandleDebuggerObject object, + Handle<IdVector> ids, + Handle<PropertyDescriptorVector> descs_) +{ + RootedObject referent(cx, object->referent()); + Debugger* dbg = object->owner(); + + Rooted<PropertyDescriptorVector> descs(cx, PropertyDescriptorVector(cx)); + if (!descs.append(descs_.begin(), descs_.end())) + return false; + for (size_t i = 0; i < descs.length(); i++) { + if (!dbg->unwrapPropertyDescriptor(cx, referent, descs[i])) + return false; + if (!CheckPropertyDescriptorAccessors(cx, descs[i])) + return false; + } + + Maybe<AutoCompartment> ac; + ac.emplace(cx, referent); + for (size_t i = 0; i < descs.length(); i++) { + if (!cx->compartment()->wrap(cx, descs[i])) + return false; + } + + ErrorCopier ec(ac); + for (size_t i = 0; i < descs.length(); i++) { + if (!DefineProperty(cx, referent, ids[i], descs[i])) + return false; + } + + return true; +} + +/* static */ bool +DebuggerObject::deleteProperty(JSContext* cx, HandleDebuggerObject object, HandleId id, + ObjectOpResult& result) +{ + RootedObject referent(cx, object->referent()); + + Maybe<AutoCompartment> ac; + ac.emplace(cx, referent); + + ErrorCopier ec(ac); + return DeleteProperty(cx, referent, id, result); +} + +/* static */ bool +DebuggerObject::call(JSContext* cx, HandleDebuggerObject object, HandleValue thisv_, + Handle<ValueVector> args, MutableHandleValue result) +{ + RootedObject referent(cx, object->referent()); + Debugger* dbg = object->owner(); + + if (!referent->isCallable()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, + "Debugger.Object", "call", referent->getClass()->name); + return false; + } + + RootedValue calleev(cx, ObjectValue(*referent)); + + /* + * Unwrap Debugger.Objects. This happens in the debugger's compartment since + * that is where any exceptions must be reported. + */ + RootedValue thisv(cx, thisv_); + if (!dbg->unwrapDebuggeeValue(cx, &thisv)) + return false; + Rooted<ValueVector> args2(cx, ValueVector(cx)); + if (!args2.append(args.begin(), args.end())) + return false; + for (unsigned i = 0; i < args2.length(); ++i) { + if (!dbg->unwrapDebuggeeValue(cx, args2[i])) + return false; + } + + /* + * Enter the debuggee compartment and rewrap all input value for that compartment. + * (Rewrapping always takes place in the destination compartment.) + */ + Maybe<AutoCompartment> ac; + ac.emplace(cx, referent); + if (!cx->compartment()->wrap(cx, &calleev) || !cx->compartment()->wrap(cx, &thisv)) + return false; + for (unsigned i = 0; i < args2.length(); ++i) { + if (!cx->compartment()->wrap(cx, args2[i])) + return false; + } + + /* + * Call the function. Use receiveCompletionValue to return to the debugger + * compartment and populate args.rval(). + */ + LeaveDebuggeeNoExecute nnx(cx); + + bool ok; + { + InvokeArgs invokeArgs(cx); + + ok = invokeArgs.init(cx, args2.length()); + if (ok) { + for (size_t i = 0; i < args2.length(); ++i) + invokeArgs[i].set(args2[i]); + + ok = js::Call(cx, calleev, thisv, invokeArgs, result); + } + } + + return dbg->receiveCompletionValue(ac, ok, result, result); +} + +/* static */ bool +DebuggerObject::forceLexicalInitializationByName(JSContext* cx, HandleDebuggerObject object, + HandleId id, bool& result) +{ + if (!JSID_IS_STRING(id)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE, + "Debugger.Object.prototype.forceLexicalInitializationByName", + "string", InformalValueTypeName(IdToValue(id))); + return false; + } + + MOZ_ASSERT(object->isGlobal()); + + Rooted<GlobalObject*> referent(cx, &object->referent()->as<GlobalObject>()); + + RootedObject globalLexical(cx, &referent->lexicalEnvironment()); + RootedObject pobj(cx); + RootedShape shape(cx); + if (!LookupProperty(cx, globalLexical, id, &pobj, &shape)) + return false; + + result = false; + if (shape) { + Value v = globalLexical->as<NativeObject>().getSlot(shape->slot()); + if (shape->hasSlot() && v.isMagic() && v.whyMagic() == JS_UNINITIALIZED_LEXICAL) { + globalLexical->as<NativeObject>().setSlot(shape->slot(), UndefinedValue()); + result = true; + } + } + + return true; +} + +/* static */ bool +DebuggerObject::executeInGlobal(JSContext* cx, HandleDebuggerObject object, + mozilla::Range<const char16_t> chars, HandleObject bindings, + const EvalOptions& options, JSTrapStatus& status, + MutableHandleValue value) +{ + MOZ_ASSERT(object->isGlobal()); + + Rooted<GlobalObject*> referent(cx, &object->referent()->as<GlobalObject>()); + Debugger* dbg = object->owner(); + + RootedObject globalLexical(cx, &referent->lexicalEnvironment()); + return DebuggerGenericEval(cx, chars, bindings, options, status, value, dbg, globalLexical, + nullptr); +} + +/* static */ bool +DebuggerObject::makeDebuggeeValue(JSContext* cx, HandleDebuggerObject object, + HandleValue value_, MutableHandleValue result) +{ + RootedObject referent(cx, object->referent()); + Debugger* dbg = object->owner(); + + RootedValue value(cx, value_); + + /* Non-objects are already debuggee values. */ + if (value.isObject()) { + // Enter this Debugger.Object's referent's compartment, and wrap the + // argument as appropriate for references from there. + { + AutoCompartment ac(cx, referent); + if (!cx->compartment()->wrap(cx, &value)) + return false; + } + + // Back in the debugger's compartment, produce a new Debugger.Object + // instance referring to the wrapped argument. + if (!dbg->wrapDebuggeeValue(cx, &value)) + return false; + } + + result.set(value); + return true; +} + +/* static */ bool +DebuggerObject::unsafeDereference(JSContext* cx, HandleDebuggerObject object, + MutableHandleObject result) +{ + RootedObject referent(cx, object->referent()); + + if (!cx->compartment()->wrap(cx, &referent)) + return false; + + // Wrapping should return the WindowProxy. + MOZ_ASSERT(!IsWindow(referent)); + + result.set(referent); + return true; +} + +/* static */ bool +DebuggerObject::unwrap(JSContext* cx, HandleDebuggerObject object, + MutableHandleDebuggerObject result) +{ + RootedObject referent(cx, object->referent()); + Debugger* dbg = object->owner(); + + RootedObject unwrapped(cx, UnwrapOneChecked(referent)); + if (!unwrapped) { + result.set(nullptr); + return true; + } + + // Don't allow unwrapping to create a D.O whose referent is in an + // invisible-to-Debugger global. (If our referent is a *wrapper* to such, + // and the wrapper is in a visible compartment, that's fine.) + JSCompartment* unwrappedCompartment = unwrapped->compartment(); + if (unwrappedCompartment->creationOptions().invisibleToDebugger()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_INVISIBLE_COMPARTMENT); + return false; + } + + return dbg->wrapDebuggeeObject(cx, unwrapped, result); +} + +/* static */ bool +DebuggerObject::requireGlobal(JSContext* cx, HandleDebuggerObject object) +{ + if (!object->isGlobal()) { + RootedObject referent(cx, object->referent()); + + const char* isWrapper = ""; + const char* isWindowProxy = ""; + + /* Help the poor programmer by pointing out wrappers around globals... */ + if (referent->is<WrapperObject>()) { + referent = js::UncheckedUnwrap(referent); + isWrapper = "a wrapper around "; + } + + /* ... and WindowProxies around Windows. */ + if (IsWindowProxy(referent)) { + referent = ToWindowIfWindowProxy(referent); + isWindowProxy = "a WindowProxy referring to "; + } + + RootedValue dbgobj(cx, ObjectValue(*object)); + if (referent->is<GlobalObject>()) { + ReportValueErrorFlags(cx, JSREPORT_ERROR, JSMSG_DEBUG_WRAPPER_IN_WAY, + JSDVG_SEARCH_STACK, dbgobj, nullptr, + isWrapper, isWindowProxy); + } else { + ReportValueErrorFlags(cx, JSREPORT_ERROR, JSMSG_DEBUG_BAD_REFERENT, + JSDVG_SEARCH_STACK, dbgobj, nullptr, + "a global object", nullptr); + } + return false; + } + + return true; +} + +#ifdef SPIDERMONKEY_PROMISE +/* static */ bool +DebuggerObject::requirePromise(JSContext* cx, HandleDebuggerObject object) +{ + RootedObject referent(cx, object->referent()); + + if (IsCrossCompartmentWrapper(referent)) { + referent = CheckedUnwrap(referent); + if (!referent) { + JS_ReportErrorASCII(cx, "Permission denied to access object"); + return false; + } + } + + if (!referent->is<PromiseObject>()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE, + "Debugger", "Promise", object->getClass()->name); + return false; + } + + return true; +} +#endif // SPIDERMONKEY_PROMISE + +/* static */ bool +DebuggerObject::getScriptedProxyTarget(JSContext* cx, HandleDebuggerObject object, + MutableHandleDebuggerObject result) +{ + MOZ_ASSERT(object->isScriptedProxy()); + RootedObject referent(cx, object->referent()); + Debugger* dbg = object->owner(); + RootedObject unwrapped(cx, js::GetProxyTargetObject(referent)); + if(!unwrapped) { + result.set(nullptr); + return true; + } + return dbg->wrapDebuggeeObject(cx, unwrapped, result); +} + +/* static */ bool +DebuggerObject::getScriptedProxyHandler(JSContext* cx, HandleDebuggerObject object, + MutableHandleDebuggerObject result) +{ + MOZ_ASSERT(object->isScriptedProxy()); + RootedObject referent(cx, object->referent()); + Debugger* dbg = object->owner(); + RootedObject unwrapped(cx, ScriptedProxyHandler::handlerObject(referent)); + if(!unwrapped) { + result.set(nullptr); + return true; + } + return dbg->wrapDebuggeeObject(cx, unwrapped, result); +} + + +/*** Debugger.Environment ************************************************************************/ + +void +DebuggerEnv_trace(JSTracer* trc, JSObject* obj) +{ + /* + * There is a barrier on private pointers, so the Unbarriered marking + * is okay. + */ + if (Env* referent = (JSObject*) obj->as<NativeObject>().getPrivate()) { + TraceManuallyBarrieredCrossCompartmentEdge(trc, obj, &referent, + "Debugger.Environment referent"); + obj->as<NativeObject>().setPrivateUnbarriered(referent); + } +} + +static DebuggerEnvironment* +DebuggerEnvironment_checkThis(JSContext* cx, const CallArgs& args, const char* fnname, + bool requireDebuggee) +{ + JSObject* thisobj = NonNullObject(cx, args.thisv()); + if (!thisobj) + return nullptr; + if (thisobj->getClass() != &DebuggerEnvironment::class_) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, + "Debugger.Environment", fnname, thisobj->getClass()->name); + return nullptr; + } + + /* + * Forbid Debugger.Environment.prototype, which is of class DebuggerEnvironment::class_ + * but isn't a real working Debugger.Environment. The prototype object is + * distinguished by having no referent. + */ + DebuggerEnvironment* nthisobj = &thisobj->as<DebuggerEnvironment>(); + if (!nthisobj->getPrivate()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, + "Debugger.Environment", fnname, "prototype object"); + return nullptr; + } + + /* + * Forbid access to Debugger.Environment objects that are not debuggee + * environments. + */ + if (requireDebuggee) { + Rooted<Env*> env(cx, static_cast<Env*>(nthisobj->getPrivate())); + if (!Debugger::fromChildJSObject(nthisobj)->observesGlobal(&env->global())) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_NOT_DEBUGGEE, + "Debugger.Environment", "environment"); + return nullptr; + } + } + + return nthisobj; +} + +#define THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, fnname, args, environment) \ + CallArgs args = CallArgsFromVp(argc, vp); \ + Rooted<DebuggerEnvironment*> environment(cx, DebuggerEnvironment_checkThis(cx, args, fnname, false)); \ + if (!environment) \ + return false; \ + +/* static */ bool +DebuggerEnvironment::construct(JSContext* cx, unsigned argc, Value* vp) +{ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, + "Debugger.Environment"); + return false; +} + +static bool +IsDeclarative(Env* env) +{ + return env->is<DebugEnvironmentProxy>() && + env->as<DebugEnvironmentProxy>().isForDeclarative(); +} + +template <typename T> +static bool +IsDebugEnvironmentWrapper(Env* env) +{ + return env->is<DebugEnvironmentProxy>() && + env->as<DebugEnvironmentProxy>().environment().is<T>(); +} + +bool +DebuggerEnvironment::typeGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "get type", args, environment); + + if (!environment->requireDebuggee(cx)) + return false; + + DebuggerEnvironmentType type = environment->type(); + + const char* s; + switch (type) { + case DebuggerEnvironmentType::Declarative: + s = "declarative"; + break; + case DebuggerEnvironmentType::With: + s = "with"; + break; + case DebuggerEnvironmentType::Object: + s = "object"; + break; + } + + JSAtom* str = Atomize(cx, s, strlen(s), PinAtom); + if (!str) + return false; + + args.rval().setString(str); + return true; +} + +/* static */ bool +DebuggerEnvironment::parentGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "get type", args, environment); + + if (!environment->requireDebuggee(cx)) + return false; + + RootedDebuggerEnvironment result(cx); + if (!environment->getParent(cx, &result)) + return false; + + args.rval().setObjectOrNull(result); + return true; +} + +/* static */ bool +DebuggerEnvironment::objectGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "get type", args, environment); + + if (!environment->requireDebuggee(cx)) + return false; + + if (environment->type() == DebuggerEnvironmentType::Declarative) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_NO_ENV_OBJECT); + return false; + } + + RootedDebuggerObject result(cx); + if (!environment->getObject(cx, &result)) + return false; + + args.rval().setObject(*result); + return true; +} + +/* static */ bool +DebuggerEnvironment::calleeGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "get callee", args, environment); + + if (!environment->requireDebuggee(cx)) + return false; + + RootedDebuggerObject result(cx); + if (!environment->getCallee(cx, &result)) + return false; + + args.rval().setObjectOrNull(result); + return true; +} + +/* static */ bool +DebuggerEnvironment::inspectableGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "get inspectable", args, environment); + + args.rval().setBoolean(environment->isDebuggee()); + return true; +} + +/* static */ bool +DebuggerEnvironment::optimizedOutGetter(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "get optimizedOut", args, environment); + + args.rval().setBoolean(environment->isOptimized()); + return true; +} + +/* static */ bool +DebuggerEnvironment::namesMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "names", args, environment); + + if (!environment->requireDebuggee(cx)) + return false; + + Rooted<IdVector> ids(cx, IdVector(cx)); + if (!DebuggerEnvironment::getNames(cx, environment, &ids)) + return false; + + RootedObject obj(cx, IdVectorToArray(cx, ids)); + if (!obj) + return false; + + args.rval().setObject(*obj); + return true; +} + +/* static */ bool +DebuggerEnvironment::findMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "find", args, environment); + if (!args.requireAtLeast(cx, "Debugger.Environment.find", 1)) + return false; + + if (!environment->requireDebuggee(cx)) + return false; + + RootedId id(cx); + if (!ValueToIdentifier(cx, args[0], &id)) + return false; + + RootedDebuggerEnvironment result(cx); + if (!DebuggerEnvironment::find(cx, environment, id, &result)) + return false; + + args.rval().setObjectOrNull(result); + return true; +} + +/* static */ bool +DebuggerEnvironment::getVariableMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "getVariable", args, environment); + if (!args.requireAtLeast(cx, "Debugger.Environment.getVariable", 1)) + return false; + + if (!environment->requireDebuggee(cx)) + return false; + + RootedId id(cx); + if (!ValueToIdentifier(cx, args[0], &id)) + return false; + + return DebuggerEnvironment::getVariable(cx, environment, id, args.rval()); +} + +/* static */ bool +DebuggerEnvironment::setVariableMethod(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "setVariable", args, environment); + if (!args.requireAtLeast(cx, "Debugger.Environment.setVariable", 2)) + return false; + + if (!environment->requireDebuggee(cx)) + return false; + + RootedId id(cx); + if (!ValueToIdentifier(cx, args[0], &id)) + return false; + + if (!DebuggerEnvironment::setVariable(cx, environment, id, args[1])) + return false; + + args.rval().setUndefined(); + return true; +} + +bool +DebuggerEnvironment::requireDebuggee(JSContext* cx) const +{ + if (!isDebuggee()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_NOT_DEBUGGEE, + "Debugger.Environment", "environment"); + + return false; + } + + return true; +} + +const JSPropertySpec DebuggerEnvironment::properties_[] = { + JS_PSG("type", DebuggerEnvironment::typeGetter, 0), + JS_PSG("parent", DebuggerEnvironment::parentGetter, 0), + JS_PSG("object", DebuggerEnvironment::objectGetter, 0), + JS_PSG("callee", DebuggerEnvironment::calleeGetter, 0), + JS_PSG("inspectable", DebuggerEnvironment::inspectableGetter, 0), + JS_PSG("optimizedOut", DebuggerEnvironment::optimizedOutGetter, 0), + JS_PS_END +}; + +const JSFunctionSpec DebuggerEnvironment::methods_[] = { + JS_FN("names", DebuggerEnvironment::namesMethod, 0, 0), + JS_FN("find", DebuggerEnvironment::findMethod, 1, 0), + JS_FN("getVariable", DebuggerEnvironment::getVariableMethod, 1, 0), + JS_FN("setVariable", DebuggerEnvironment::setVariableMethod, 2, 0), + JS_FS_END +}; + +/* static */ NativeObject* +DebuggerEnvironment::initClass(JSContext* cx, HandleObject dbgCtor, HandleObject obj) +{ + Rooted<GlobalObject*> global(cx, &obj->as<GlobalObject>()); + RootedObject objProto(cx, global->getOrCreateObjectPrototype(cx)); + + return InitClass(cx, dbgCtor, objProto, &DebuggerEnvironment::class_, construct, 0, + properties_, methods_, nullptr, nullptr); +} + +/* static */ DebuggerEnvironment* +DebuggerEnvironment::create(JSContext* cx, HandleObject proto, HandleObject referent, + HandleNativeObject debugger) +{ + NewObjectKind newKind = IsInsideNursery(referent) ? GenericObject : TenuredObject; + RootedObject obj(cx, NewObjectWithGivenProto(cx, &DebuggerEnvironment::class_, proto, newKind)); + if (!obj) + return nullptr; + + DebuggerEnvironment& environment = obj->as<DebuggerEnvironment>(); + environment.setPrivateGCThing(referent); + environment.setReservedSlot(OWNER_SLOT, ObjectValue(*debugger)); + + return &environment; +} + +/* static */ DebuggerEnvironmentType +DebuggerEnvironment::type() const +{ + /* Don't bother switching compartments just to check env's type. */ + if (IsDeclarative(referent())) + return DebuggerEnvironmentType::Declarative; + if (IsDebugEnvironmentWrapper<WithEnvironmentObject>(referent())) + return DebuggerEnvironmentType::With; + return DebuggerEnvironmentType::Object; +} + +bool +DebuggerEnvironment::getParent(JSContext* cx, MutableHandleDebuggerEnvironment result) const +{ + /* Don't bother switching compartments just to get env's parent. */ + Rooted<Env*> parent(cx, referent()->enclosingEnvironment()); + if (!parent) { + result.set(nullptr); + return true; + } + + return owner()->wrapEnvironment(cx, parent, result); +} + +bool +DebuggerEnvironment::getObject(JSContext* cx, MutableHandleDebuggerObject result) const +{ + MOZ_ASSERT(type() != DebuggerEnvironmentType::Declarative); + + /* Don't bother switching compartments just to get env's object. */ + RootedObject object(cx); + if (IsDebugEnvironmentWrapper<WithEnvironmentObject>(referent())) { + object.set(&referent()->as<DebugEnvironmentProxy>() + .environment().as<WithEnvironmentObject>().object()); + } else if (IsDebugEnvironmentWrapper<NonSyntacticVariablesObject>(referent())) { + object.set(&referent()->as<DebugEnvironmentProxy>() + .environment().as<NonSyntacticVariablesObject>()); + } else { + object.set(referent()); + MOZ_ASSERT(!object->is<DebugEnvironmentProxy>()); + } + + return owner()->wrapDebuggeeObject(cx, object, result); +} + +bool +DebuggerEnvironment::getCallee(JSContext* cx, MutableHandleDebuggerObject result) const +{ + if (!referent()->is<DebugEnvironmentProxy>()) { + result.set(nullptr); + return true; + } + + JSObject& scope = referent()->as<DebugEnvironmentProxy>().environment(); + if (!scope.is<CallObject>()) { + result.set(nullptr); + return true; + } + + RootedObject callee(cx, &scope.as<CallObject>().callee()); + if (IsInternalFunctionObject(*callee)) { + result.set(nullptr); + return true; + } + + return owner()->wrapDebuggeeObject(cx, callee, result); +} + +bool +DebuggerEnvironment::isDebuggee() const +{ + MOZ_ASSERT(referent()); + MOZ_ASSERT(!referent()->is<EnvironmentObject>()); + + return owner()->observesGlobal(&referent()->global()); +} + +bool +DebuggerEnvironment::isOptimized() const +{ + return referent()->is<DebugEnvironmentProxy>() && + referent()->as<DebugEnvironmentProxy>().isOptimizedOut(); +} + +/* static */ bool +DebuggerEnvironment::getNames(JSContext* cx, HandleDebuggerEnvironment environment, + MutableHandle<IdVector> result) +{ + MOZ_ASSERT(environment->isDebuggee()); + + Rooted<Env*> referent(cx, environment->referent()); + + AutoIdVector ids(cx); + { + Maybe<AutoCompartment> ac; + ac.emplace(cx, referent); + + ErrorCopier ec(ac); + if (!GetPropertyKeys(cx, referent, JSITER_HIDDEN, &ids)) + return false; + } + + for (size_t i = 0; i < ids.length(); ++i) { + jsid id = ids[i]; + if (JSID_IS_ATOM(id) && IsIdentifier(JSID_TO_ATOM(id))) { + if (!result.append(id)) + return false; + } + } + + return true; +} + +/* static */ bool +DebuggerEnvironment::find(JSContext* cx, HandleDebuggerEnvironment environment, HandleId id, + MutableHandleDebuggerEnvironment result) +{ + MOZ_ASSERT(environment->isDebuggee()); + + Rooted<Env*> env(cx, environment->referent()); + Debugger* dbg = environment->owner(); + + { + Maybe<AutoCompartment> ac; + ac.emplace(cx, env); + + /* This can trigger resolve hooks. */ + ErrorCopier ec(ac); + for (; env; env = env->enclosingEnvironment()) { + bool found; + if (!HasProperty(cx, env, id, &found)) + return false; + if (found) + break; + } + } + + if (!env) { + result.set(nullptr); + return true; + } + + return dbg->wrapEnvironment(cx, env, result); +} + +/* static */ bool +DebuggerEnvironment::getVariable(JSContext* cx, HandleDebuggerEnvironment environment, + HandleId id, MutableHandleValue result) +{ + MOZ_ASSERT(environment->isDebuggee()); + + Rooted<Env*> referent(cx, environment->referent()); + Debugger* dbg = environment->owner(); + + { + Maybe<AutoCompartment> ac; + ac.emplace(cx, referent); + + /* This can trigger getters. */ + ErrorCopier ec(ac); + + bool found; + if (!HasProperty(cx, referent, id, &found)) + return false; + if (!found) { + result.setUndefined(); + return true; + } + + // For DebugEnvironmentProxys, we get sentinel values for optimized out + // slots and arguments instead of throwing (the default behavior). + // + // See wrapDebuggeeValue for how the sentinel values are wrapped. + if (referent->is<DebugEnvironmentProxy>()) { + if (!referent->as<DebugEnvironmentProxy>().getMaybeSentinelValue(cx, id, result)) + return false; + } else { + if (!GetProperty(cx, referent, referent, id, result)) + return false; + } + } + + // When we've faked up scope chain objects for optimized-out scopes, + // declarative environments may contain internal JSFunction objects, which + // we shouldn't expose to the user. + if (result.isObject()) { + RootedObject obj(cx, &result.toObject()); + if (obj->is<JSFunction>() && + IsInternalFunctionObject(obj->as<JSFunction>())) + result.setMagic(JS_OPTIMIZED_OUT); + } + + return dbg->wrapDebuggeeValue(cx, result); +} + +/* static */ bool +DebuggerEnvironment::setVariable(JSContext* cx, HandleDebuggerEnvironment environment, + HandleId id, HandleValue value_) +{ + MOZ_ASSERT(environment->isDebuggee()); + + Rooted<Env*> referent(cx, environment->referent()); + Debugger* dbg = environment->owner(); + + RootedValue value(cx, value_); + if (!dbg->unwrapDebuggeeValue(cx, &value)) + return false; + + { + Maybe<AutoCompartment> ac; + ac.emplace(cx, referent); + if (!cx->compartment()->wrap(cx, &value)) + return false; + + /* This can trigger setters. */ + ErrorCopier ec(ac); + + /* Make sure the environment actually has the specified binding. */ + bool found; + if (!HasProperty(cx, referent, id, &found)) + return false; + if (!found) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_VARIABLE_NOT_FOUND); + return false; + } + + /* Just set the property. */ + if (!SetProperty(cx, referent, id, value)) + return false; + } + + return true; +} + + +/*** JS::dbg::Builder ****************************************************************************/ + +Builder::Builder(JSContext* cx, js::Debugger* debugger) + : debuggerObject(cx, debugger->toJSObject().get()), + debugger(debugger) +{ } + + +#if DEBUG +void +Builder::assertBuilt(JSObject* obj) +{ + // We can't use assertSameCompartment here, because that is always keyed to + // some JSContext's current compartment, whereas BuiltThings can be + // constructed and assigned to without respect to any particular context; + // the only constraint is that they should be in their debugger's compartment. + MOZ_ASSERT_IF(obj, debuggerObject->compartment() == obj->compartment()); +} +#endif + +bool +Builder::Object::definePropertyToTrusted(JSContext* cx, const char* name, + JS::MutableHandleValue trusted) +{ + // We should have checked for false Objects before calling this. + MOZ_ASSERT(value); + + JSAtom* atom = Atomize(cx, name, strlen(name)); + if (!atom) + return false; + RootedId id(cx, AtomToId(atom)); + + return DefineProperty(cx, value, id, trusted); +} + +bool +Builder::Object::defineProperty(JSContext* cx, const char* name, JS::HandleValue propval_) +{ + AutoCompartment ac(cx, debuggerObject()); + + RootedValue propval(cx, propval_); + if (!debugger()->wrapDebuggeeValue(cx, &propval)) + return false; + + return definePropertyToTrusted(cx, name, &propval); +} + +bool +Builder::Object::defineProperty(JSContext* cx, const char* name, JS::HandleObject propval_) +{ + RootedValue propval(cx, ObjectOrNullValue(propval_)); + return defineProperty(cx, name, propval); +} + +bool +Builder::Object::defineProperty(JSContext* cx, const char* name, Builder::Object& propval_) +{ + AutoCompartment ac(cx, debuggerObject()); + + RootedValue propval(cx, ObjectOrNullValue(propval_.value)); + return definePropertyToTrusted(cx, name, &propval); +} + +Builder::Object +Builder::newObject(JSContext* cx) +{ + AutoCompartment ac(cx, debuggerObject); + + RootedPlainObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx)); + + // If the allocation failed, this will return a false Object, as the spec promises. + return Object(cx, *this, obj); +} + + +/*** JS::dbg::AutoEntryMonitor ******************************************************************/ + +AutoEntryMonitor::AutoEntryMonitor(JSContext* cx) + : runtime_(cx->runtime()), + savedMonitor_(cx->runtime()->entryMonitor) +{ + runtime_->entryMonitor = this; +} + +AutoEntryMonitor::~AutoEntryMonitor() +{ + runtime_->entryMonitor = savedMonitor_; +} + + +/*** Glue ****************************************************************************************/ + +extern JS_PUBLIC_API(bool) +JS_DefineDebuggerObject(JSContext* cx, HandleObject obj) +{ + RootedNativeObject + objProto(cx), + debugCtor(cx), + debugProto(cx), + frameProto(cx), + scriptProto(cx), + sourceProto(cx), + objectProto(cx), + envProto(cx), + memoryProto(cx); + RootedObject debuggeeWouldRunProto(cx); + RootedValue debuggeeWouldRunCtor(cx); + Rooted<GlobalObject*> global(cx, &obj->as<GlobalObject>()); + + objProto = global->getOrCreateObjectPrototype(cx); + if (!objProto) + return false; + debugProto = InitClass(cx, obj, + objProto, &Debugger::class_, Debugger::construct, + 1, Debugger::properties, Debugger::methods, nullptr, + Debugger::static_methods, debugCtor.address()); + if (!debugProto) + return false; + + frameProto = DebuggerFrame::initClass(cx, debugCtor, obj); + if (!frameProto) + return false; + + scriptProto = InitClass(cx, debugCtor, objProto, &DebuggerScript_class, + DebuggerScript_construct, 0, + DebuggerScript_properties, DebuggerScript_methods, + nullptr, nullptr); + if (!scriptProto) + return false; + + sourceProto = InitClass(cx, debugCtor, sourceProto, &DebuggerSource_class, + DebuggerSource_construct, 0, + DebuggerSource_properties, DebuggerSource_methods, + nullptr, nullptr); + if (!sourceProto) + return false; + + objectProto = DebuggerObject::initClass(cx, obj, debugCtor); + if (!objectProto) + return false; + + envProto = DebuggerEnvironment::initClass(cx, debugCtor, obj); + if (!envProto) + return false; + + memoryProto = InitClass(cx, debugCtor, objProto, &DebuggerMemory::class_, + DebuggerMemory::construct, 0, DebuggerMemory::properties, + DebuggerMemory::methods, nullptr, nullptr); + if (!memoryProto) + return false; + + debuggeeWouldRunProto = + GlobalObject::getOrCreateCustomErrorPrototype(cx, global, JSEXN_DEBUGGEEWOULDRUN); + if (!debuggeeWouldRunProto) + return false; + debuggeeWouldRunCtor = global->getConstructor(JSProto_DebuggeeWouldRun); + RootedId debuggeeWouldRunId(cx, NameToId(ClassName(JSProto_DebuggeeWouldRun, cx))); + if (!DefineProperty(cx, debugCtor, debuggeeWouldRunId, debuggeeWouldRunCtor, + nullptr, nullptr, 0)) + { + return false; + } + + debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_FRAME_PROTO, ObjectValue(*frameProto)); + debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_OBJECT_PROTO, ObjectValue(*objectProto)); + debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_SCRIPT_PROTO, ObjectValue(*scriptProto)); + debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_SOURCE_PROTO, ObjectValue(*sourceProto)); + debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_ENV_PROTO, ObjectValue(*envProto)); + debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_MEMORY_PROTO, ObjectValue(*memoryProto)); + return true; +} + +static inline void +AssertIsPromise(JSContext* cx, HandleObject promise) +{ + MOZ_ASSERT(promise); + assertSameCompartment(cx, promise); + MOZ_ASSERT(strcmp(promise->getClass()->name, "Promise") == 0); +} + +JS_PUBLIC_API(void) +JS::dbg::onNewPromise(JSContext* cx, HandleObject promise_) +{ + RootedObject promise(cx, promise_); + if (IsWrapper(promise)) + promise = UncheckedUnwrap(promise); + AutoCompartment ac(cx, promise); + AssertIsPromise(cx, promise); + Debugger::slowPathPromiseHook(cx, Debugger::OnNewPromise, promise); +} + +JS_PUBLIC_API(void) +JS::dbg::onPromiseSettled(JSContext* cx, HandleObject promise) +{ + AssertIsPromise(cx, promise); + Debugger::slowPathPromiseHook(cx, Debugger::OnPromiseSettled, promise); +} + +JS_PUBLIC_API(bool) +JS::dbg::IsDebugger(JSObject& obj) +{ + JSObject* unwrapped = CheckedUnwrap(&obj); + return unwrapped && + js::GetObjectClass(unwrapped) == &Debugger::class_ && + js::Debugger::fromJSObject(unwrapped) != nullptr; +} + +JS_PUBLIC_API(bool) +JS::dbg::GetDebuggeeGlobals(JSContext* cx, JSObject& dbgObj, AutoObjectVector& vector) +{ + MOZ_ASSERT(IsDebugger(dbgObj)); + js::Debugger* dbg = js::Debugger::fromJSObject(CheckedUnwrap(&dbgObj)); + + if (!vector.reserve(vector.length() + dbg->debuggees.count())) { + JS_ReportOutOfMemory(cx); + return false; + } + + for (WeakGlobalObjectSet::Range r = dbg->allDebuggees(); !r.empty(); r.popFront()) + vector.infallibleAppend(static_cast<JSObject*>(r.front())); + + return true; +} + + +/*** JS::dbg::GarbageCollectionEvent **************************************************************/ + +namespace JS { +namespace dbg { + +/* static */ GarbageCollectionEvent::Ptr +GarbageCollectionEvent::Create(JSRuntime* rt, ::js::gcstats::Statistics& stats, uint64_t gcNumber) +{ + auto data = rt->make_unique<GarbageCollectionEvent>(gcNumber); + if (!data) + return nullptr; + + data->nonincrementalReason = stats.nonincrementalReason(); + + for (auto range = stats.sliceRange(); !range.empty(); range.popFront()) { + if (!data->reason) { + // There is only one GC reason for the whole cycle, but for legacy + // reasons this data is stored and replicated on each slice. Each + // slice used to have its own GCReason, but now they are all the + // same. + data->reason = gcreason::ExplainReason(range.front().reason); + MOZ_ASSERT(data->reason); + } + + if (!data->collections.growBy(1)) + return nullptr; + + data->collections.back().startTimestamp = range.front().startTimestamp; + data->collections.back().endTimestamp = range.front().endTimestamp; + } + + + return data; +} + +static bool +DefineStringProperty(JSContext* cx, HandleObject obj, PropertyName* propName, const char* strVal) +{ + RootedValue val(cx, UndefinedValue()); + if (strVal) { + JSAtom* atomized = Atomize(cx, strVal, strlen(strVal)); + if (!atomized) + return false; + val = StringValue(atomized); + } + return DefineProperty(cx, obj, propName, val); +} + +JSObject* +GarbageCollectionEvent::toJSObject(JSContext* cx) const +{ + RootedObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx)); + RootedValue gcCycleNumberVal(cx, NumberValue(majorGCNumber_)); + if (!obj || + !DefineStringProperty(cx, obj, cx->names().nonincrementalReason, nonincrementalReason) || + !DefineStringProperty(cx, obj, cx->names().reason, reason) || + !DefineProperty(cx, obj, cx->names().gcCycleNumber, gcCycleNumberVal)) + { + return nullptr; + } + + RootedArrayObject slicesArray(cx, NewDenseEmptyArray(cx)); + if (!slicesArray) + return nullptr; + + size_t idx = 0; + for (auto range = collections.all(); !range.empty(); range.popFront()) { + RootedPlainObject collectionObj(cx, NewBuiltinClassInstance<PlainObject>(cx)); + if (!collectionObj) + return nullptr; + + RootedValue start(cx, NumberValue(range.front().startTimestamp)); + RootedValue end(cx, NumberValue(range.front().endTimestamp)); + if (!DefineProperty(cx, collectionObj, cx->names().startTimestamp, start) || + !DefineProperty(cx, collectionObj, cx->names().endTimestamp, end)) + { + return nullptr; + } + + RootedValue collectionVal(cx, ObjectValue(*collectionObj)); + if (!DefineElement(cx, slicesArray, idx++, collectionVal)) + return nullptr; + } + + RootedValue slicesValue(cx, ObjectValue(*slicesArray)); + if (!DefineProperty(cx, obj, cx->names().collections, slicesValue)) + return nullptr; + + return obj; +} + +JS_PUBLIC_API(bool) +FireOnGarbageCollectionHook(JSContext* cx, JS::dbg::GarbageCollectionEvent::Ptr&& data) +{ + AutoObjectVector triggered(cx); + + { + // We had better not GC (and potentially get a dangling Debugger + // pointer) while finding all Debuggers observing a debuggee that + // participated in this GC. + AutoCheckCannotGC noGC; + + for (Debugger* dbg : cx->runtime()->debuggerList) { + if (dbg->enabled && + dbg->observedGC(data->majorGCNumber()) && + dbg->getHook(Debugger::OnGarbageCollection)) + { + if (!triggered.append(dbg->object)) { + JS_ReportOutOfMemory(cx); + return false; + } + } + } + } + + for ( ; !triggered.empty(); triggered.popBack()) { + Debugger* dbg = Debugger::fromJSObject(triggered.back()); + dbg->fireOnGarbageCollectionHook(cx, data); + MOZ_ASSERT(!cx->isExceptionPending()); + } + + return true; +} + +} // namespace dbg +} // namespace JS |