/* -*- 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/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 !!JSFunction::getOrCreateScript(cx, fun); } 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(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(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(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(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()) { const char* isWrapper = ""; const char* isWindowProxy = ""; /* Help the poor programmer by pointing out wrappers around globals... */ if (obj->is()) { 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()) { 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().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(); } 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()); 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> 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 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 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, 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, MutableHandleDebuggerEnvironment result) { MOZ_ASSERT(env); /* * DebuggerEnv should only wrap a debug scope chain obtained (transitively) * from GetDebugEnvironmentFor(Frame|Function). */ MOZ_ASSERT(!IsSyntacticEnvironment(env)); DependentAddPtr p(cx, environments, env); if (p) { result.set(&p->value()->as()); } 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(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()) { MOZ_ASSERT(!IsInternalFunctionObject(*obj)); RootedFunction fun(cx, &obj->as()); if (!EnsureFunctionHasScript(cx, fun)) return false; } DependentAddPtr p(cx, objects, obj); if (p) { result.set(&p->value()->as()); } 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(); 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(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 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& 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& ac, MutableHandleValue* vp, const Maybe& 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& ac, MutableHandleValue vp, const Maybe& thisVForCheck, AbstractFramePtr frame) { return handleUncaughtExceptionHelper(ac, &vp, thisVForCheck, frame); } JSTrapStatus Debugger::handleUncaughtException(Maybe& 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(cx)); if (!obj || !NativeDefineProperty(cx, obj, key, value, nullptr, nullptr, JSPROP_ENUMERATE)) { return false; } result.setObject(*obj); return true; } bool Debugger::receiveCompletionValue(Maybe& 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& maybeThisv, JSTrapStatus status, MutableHandleValue vp) { if (status == JSTRAP_RETURN && frame && frame.isFunctionFrame()) { // Don't let a { return: ... } resumption value make a generator // function violate the iterator protocol. The return value from // such a frame must have the form { done: , value: }. RootedFunction callee(cx, frame.callee()); 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& 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& ac, AbstractFramePtr frame, const Maybe& 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& ac, AbstractFramePtr frame, const Maybe& 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& ac, AbstractFramePtr frame, jsbytecode* pc, bool success, JSTrapStatus status, MutableHandleValue vp) { JSContext* cx = ac->context()->asJSContext(); RootedValue thisv(cx); Maybe 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& ac, bool success, const Value& rv, AbstractFramePtr frame, jsbytecode* pc, MutableHandleValue vp) { JSContext* cx = ac->context()->asJSContext(); RootedValue thisv(cx); Maybe 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 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 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 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 scriptReferent) { RootedObject hook(cx, getHook(OnNewScript)); MOZ_ASSERT(hook); MOZ_ASSERT(hook->isCallable()); Maybe 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 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 /* 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 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 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 wasmInstance) { JSTrapStatus status = dispatchHook( cx, [wasmInstance](Debugger* dbg) -> bool { return dbg->observesNewScript() && dbg->observesGlobal(&wasmInstance->global()); }, [&](Debugger* dbg) -> JSTrapStatus { Rooted 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 scriptGlobal(cx, &script->global()); jsbytecode* pc = iter.pc(); BreakpointSite* site = script->getBreakpointSite(pc); JSOp op = JSOp(*pc); /* Build list of breakpoint handlers. */ Vector 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 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 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 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 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 global, MutableHandleValue vp) { RootedObject hook(cx, getHook(OnNewGlobalObject)); MOZ_ASSERT(hook); MOZ_ASSERT(hook->isCallable()); Maybe 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 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*> 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> activeDebuggers(cx, GCVector(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 (!JSObject::constructorDisplayAtom(cx, obj, &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 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 compartments_; HashSet 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::Range CompartmentRange; const HashSet* compartments() const { return &compartments_; } const HashSet* 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& 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 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(); !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 /* 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 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 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 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(trc); environments.markCrossCompartmentEdges(trc); scripts.markCrossCompartmentEdges(trc); sources.markCrossCompartmentEdges(trc); wasmInstanceScripts.markCrossCompartmentEdges(trc); wasmInstanceSources.markCrossCompartmentEdges(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& 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()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, "argument", "not a global object"); return nullptr; } return &obj->as(); } /* 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 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 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 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 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()) { 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()); 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(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 debuggee(cx, &args[i].toObject().as().private_().toObject().global()); if (!debugger->addDebuggeeGlobal(cx, debuggee)) return false; } args.rval().setObject(*obj); return true; } bool Debugger::addDebuggeeGlobal(JSContext* cx, Handle 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 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 static T* findDebuggerInVector(Debugger* dbg, Vector* 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(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() .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 foundScripts() const { return vector; } Handle 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, 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 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, 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, instead of creating * the JS array as we go, because we mustn't allocate JS objects or GC * while we use the CellIter. */ Rooted vector; /* * Like above, but for wasm modules. */ Rooted 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(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() && source.as()->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 scripts(query.foundScripts()); Handle 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 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 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() || referent.exposeToJS().isUndefined()) return true; JSObject* obj = referent.as(); 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 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 textIds(cx); if (!textIds.reserve(ids.length())) return false; Vector 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 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(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(obj->as().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(cell)); MOZ_ASSERT(cell->getTraceKind() == JS::TraceKind::Object); return AsVariant(&static_cast(cell)->as()); } return AsVariant(static_cast(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(cell); TraceManuallyBarrieredCrossCompartmentEdge(trc, obj, &script, "Debugger.Script script referent"); obj->as().setPrivateUnbarriered(script); } else { JSObject* wasm = static_cast(cell); TraceManuallyBarrieredCrossCompartmentEdge(trc, obj, &wasm, "Debugger.Script wasm referent"); MOZ_ASSERT(wasm->is()); obj->as().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 instance) { obj_->setPrivateGCThing(instance); } }; NativeObject* Debugger::newDebuggerScript(JSContext* cx, Handle 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 JSObject* Debugger::wrapVariantReferent(JSContext* cx, Map& map, Handle key, Handle referent) { assertSameCompartment(cx, object); Handle untaggedReferent = referent.template as(); MOZ_ASSERT(cx->compartment() != untaggedReferent->compartment()); DependentAddPtr 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 referent) { JSObject* obj; if (referent.is()) { Handle untaggedReferent = referent.template as(); Rooted key(cx, CrossCompartmentKey(object, untaggedReferent)); obj = wrapVariantReferent( cx, scripts, key, referent); } else { Handle untaggedReferent = referent.template as(); Rooted key(cx, CrossCompartmentKey(object, untaggedReferent, CrossCompartmentKey::DebuggerObjectKind::DebuggerWasmScript)); obj = wrapVariantReferent( cx, wasmInstanceScripts, key, referent); } MOZ_ASSERT_IF(obj, GetScriptReferent(obj) == referent); return obj; } JSObject* Debugger::wrapScript(JSContext* cx, HandleScript script) { Rooted referent(cx, script.get()); return wrapVariantReferent(cx, referent); } JSObject* Debugger::wrapWasmScript(JSContext* cx, Handle wasmInstance) { Rooted 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 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()) { 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 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(cx, args, fnname, \ "a JS script")); \ if (!obj) \ return false; \ RootedScript script(cx, GetScriptReferent(obj).as()) 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(cx, script->scriptSource()->introducerFilename()); else str = NewStringCopyZ(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()); return dbg_->wrapSource(cx_, source); } ReturnType match(Handle 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 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()) { fun = &obj->as(); // 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 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(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(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(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 instance) { if (!result_) return false; Vector 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 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_(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(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(obj->as().getPrivate()); } static inline DebuggerSourceReferent GetSourceReferent(JSObject* obj) { if (NativeObject* referent = GetSourceReferentRawObject(obj)) { if (referent->is()) return AsVariant(&referent->as()); return AsVariant(&referent->as()); } return AsVariant(static_cast(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().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 instance) { obj_->setPrivateGCThing(instance); } }; NativeObject* Debugger::newDebuggerSource(JSContext* cx, Handle 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 referent) { JSObject* obj; if (referent.is()) { Handle untaggedReferent = referent.template as(); Rooted key(cx, CrossCompartmentKey(object, untaggedReferent, CrossCompartmentKey::DebuggerObjectKind::DebuggerSource)); obj = wrapVariantReferent( cx, sources, key, referent); } else { Handle untaggedReferent = referent.template as(); Rooted key(cx, CrossCompartmentKey(object, untaggedReferent, CrossCompartmentKey::DebuggerObjectKind::DebuggerWasmSource)); obj = wrapVariantReferent( cx, wasmInstanceSources, key, referent); } MOZ_ASSERT_IF(obj, GetSourceReferent(obj) == referent); return obj; } JSObject* Debugger::wrapSource(JSContext* cx, HandleScriptSource source) { Rooted referent(cx, source.get()); return wrapVariantReferent(cx, referent); } JSObject* Debugger::wrapWasmSource(JSContext* cx, Handle wasmInstance) { Rooted 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(); if (!GetSourceReferentRawObject(thisobj)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, "Debugger.Source", fnname, "prototype object"); return nullptr; } return nthisobj; } template 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()) { 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 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(cx, args, fnname, \ "a JS source")); \ if (!obj) \ return false; \ RootedScriptSource sourceObject(cx, GetSourceReferent(obj).as()) 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; if (!hasSourceData) return NewStringCopyZ(cx_, "[no source]"); if (ss->isFunctionBody()) return ss->functionBodyString(cx_); return ss->substring(cx_, 0, ss->length()); } ReturnType match(Handle 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; ReturnType match(HandleScriptSource sourceObject) { ScriptSource* ss = sourceObject->source(); MOZ_ASSERT(ss); if (ss->filename()) { JSString* str = NewStringCopyZ(cx_, ss->filename()); return Some(str); } return Nothing(); } ReturnType match(Handle 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(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 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 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 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 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 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 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 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(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(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 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) { Handle global = obj.as(); RootedObject objProto(cx, GlobalObject::getOrCreateObjectPrototype(cx, global)); 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(); // 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 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 maybeIter; if (!DebuggerFrame::getScriptFrameIter(cx, frame, maybeIter)) return false; ScriptFrameIter& iter = *maybeIter; Rooted 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()->isStarGenerator() || DebuggerFrame::getReferent(frame).script()->isLegacyGenerator(); } /* static */ bool DebuggerFrame::getOffset(JSContext* cx, HandleDebuggerFrame frame, size_t& result) { MOZ_ASSERT(frame->isLive()); Maybe 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 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 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, AbstractFramePtr frame, jsbytecode* pc, mozilla::Range 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 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 ac; if (iter) ac.emplace(cx, iter->environmentChain(cx)); else ac.emplace(cx, envArg); Rooted 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(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 chars, HandleObject bindings, const EvalOptions& options, JSTrapStatus& status, MutableHandleValue value) { MOZ_ASSERT(frame->isLive()); Debugger* dbg = frame->owner(); Maybe 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& result) { AbstractFramePtr referent = AbstractFramePtr::FromRaw(frame->getPrivate()); if (referent.isScriptFrameIterData()) { result.emplace(*reinterpret_cast(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().getPrivate()); if (frame.isScriptFrameIterData()) fop->delete_((ScriptFrameIter::Data*) frame.raw()); obj->as().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()); /* * 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 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().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().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 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 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 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 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().getPrivate()) { TraceManuallyBarrieredCrossCompartmentEdge(trc, obj, &referent, "Debugger.Object referent"); obj->as().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(); 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().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().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()) { \ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE,\ "Debugger", "Promise", obj->getClass()->name); \ return false; \ } \ Rooted promise(cx, &obj->as()); #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()) { \ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE,\ "Debugger", "Promise", obj->getClass()->name); \ return false; \ } \ Rooted promise(cx, &obj->as()); /* 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 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()) { args.rval().setUndefined(); return true; } RootedFunction fun(cx, &obj->as()); 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() || !obj->as().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(cx); { AutoCompartment ac(cx, obj); RootedFunction fun(cx, &obj->as()); 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 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::errorNotesGetter(JSContext *cx, unsigned argc, Value* vp) { THIS_DEBUGOBJECT(cx, argc, vp, "get errorNotes", args, object) return DebuggerObject::getErrorNotes(cx, object, args.rval()); } /* 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 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 result(cx); if (!DebuggerObject::getScriptedProxyHandler(cx, object, &result)) return false; args.rval().setObjectOrNull(result); return true; } /* 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> values(cx, GCVector(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; } /* 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 ids) { Rooted 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(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 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 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(cx, args.get(0), &id)) return false; Rooted 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(cx, args[0], &id)) return false; Rooted 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 descs(cx, PropertyDescriptorVector(cx)); if (!ReadPropertyDescriptors(cx, props, false, &ids, &descs)) return false; Rooted 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(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 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 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(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 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 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("errorNotes", DebuggerObject::errorNotesGetter, 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 }; 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 }; 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) { Handle global = obj.as(); RootedObject objProto(cx, GlobalObject::getOrCreateObjectPrototype(cx, global)); RootedNativeObject objectProto(cx, InitClass(cx, debugCtor, objProto, &class_, construct, 0, properties_, methods_, nullptr, nullptr)); if (!objectProto) return nullptr; if (!DefinePropertiesAndFunctions(cx, objectProto, promiseProperties_, nullptr)) return nullptr; 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(); 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(); } bool DebuggerObject::isDebuggeeFunction() const { return referent()->is() && owner()->observesGlobal(&referent()->as().global()); } bool DebuggerObject::isBoundFunction() const { MOZ_ASSERT(isDebuggeeFunction()); return referent()->isBoundFunction(); } bool DebuggerObject::isArrowFunction() const { MOZ_ASSERT(isDebuggeeFunction()); return referent()->as().isArrow(); } bool DebuggerObject::isGlobal() const { return referent()->is(); } bool DebuggerObject::isScriptedProxy() const { return js::IsScriptedProxy(referent()); } bool DebuggerObject::isPromise() const { JSObject* referent = this->referent(); if (IsCrossCompartmentWrapper(referent)) { referent = CheckedUnwrap(referent); if (!referent) return false; } return referent->is(); } /* 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().explicitName(); } JSAtom* DebuggerObject::displayName() const { MOZ_ASSERT(isFunction()); return referent()->as().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 result) { MOZ_ASSERT(object->isDebuggeeFunction()); RootedFunction referent(cx, &object->referent()->as()); 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()); 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()); Debugger* dbg = object->owner(); result.set(referent->getBoundFunctionThis()); return dbg->wrapDebuggeeValue(cx, result); } /* static */ bool DebuggerObject::getBoundArguments(JSContext* cx, HandleDebuggerObject object, MutableHandle result) { MOZ_ASSERT(object->isBoundFunction()); RootedFunction referent(cx, &object->referent()->as()); 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(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()); return SavedFrame::isSavedFrameAndNotProto(*metadata) ? &metadata->as() : 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()) { report = nullptr; return true; } report = obj->as().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::getErrorNotes(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; } RootedObject errorNotesArray(cx, CreateErrorNotesArray(cx, report)); if (!errorNotesArray) return false; if (!cx->compartment()->wrap(cx, &errorNotesArray)) return false; result.setObject(*errorNotesArray); 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; } /* 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); } /* static */ bool DebuggerObject::isExtensible(JSContext* cx, HandleDebuggerObject object, bool& result) { RootedObject referent(cx, object->referent()); Maybe 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 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 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 result) { RootedObject referent(cx, object->referent()); AutoIdVector ids(cx); { Maybe 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 result) { RootedObject referent(cx, object->referent()); AutoIdVector ids(cx); { Maybe 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 desc) { RootedObject referent(cx, object->referent()); Debugger* dbg = object->owner(); /* Bug: This can cause the debuggee to run! */ { Maybe 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 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 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 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 desc_) { RootedObject referent(cx, object->referent()); Debugger* dbg = object->owner(); Rooted desc(cx, desc_); if (!dbg->unwrapPropertyDescriptor(cx, referent, &desc)) return false; if (!CheckPropertyDescriptorAccessors(cx, desc)) return false; Maybe 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 ids, Handle descs_) { RootedObject referent(cx, object->referent()); Debugger* dbg = object->owner(); Rooted 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 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 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 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 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 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 referent(cx, &object->referent()->as()); 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().getSlot(shape->slot()); if (shape->hasSlot() && v.isMagic() && v.whyMagic() == JS_UNINITIALIZED_LEXICAL) { globalLexical->as().setSlot(shape->slot(), UndefinedValue()); result = true; } } return true; } /* static */ bool DebuggerObject::executeInGlobal(JSContext* cx, HandleDebuggerObject object, mozilla::Range chars, HandleObject bindings, const EvalOptions& options, JSTrapStatus& status, MutableHandleValue value) { MOZ_ASSERT(object->isGlobal()); Rooted referent(cx, &object->referent()->as()); 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()) { 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()) { 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; } /* 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()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE, "Debugger", "Promise", object->getClass()->name); return false; } return true; } /* 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().getPrivate()) { TraceManuallyBarrieredCrossCompartmentEdge(trc, obj, &referent, "Debugger.Environment referent"); obj->as().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(); 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(cx, static_cast(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 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() && env->as().isForDeclarative(); } template static bool IsDebugEnvironmentWrapper(Env* env) { return env->is() && env->as().environment().is(); } 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 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) { Handle global = obj.as(); RootedObject objProto(cx, GlobalObject::getOrCreateObjectPrototype(cx, global)); 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(); 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(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 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(referent())) { object.set(&referent()->as() .environment().as().object()); } else if (IsDebugEnvironmentWrapper(referent())) { object.set(&referent()->as() .environment().as()); } else { object.set(referent()); MOZ_ASSERT(!object->is()); } return owner()->wrapDebuggeeObject(cx, object, result); } bool DebuggerEnvironment::getCallee(JSContext* cx, MutableHandleDebuggerObject result) const { if (!referent()->is()) { result.set(nullptr); return true; } JSObject& scope = referent()->as().environment(); if (!scope.is()) { result.set(nullptr); return true; } RootedObject callee(cx, &scope.as().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()); return owner()->observesGlobal(&referent()->global()); } bool DebuggerEnvironment::isOptimized() const { return referent()->is() && referent()->as().isOptimizedOut(); } /* static */ bool DebuggerEnvironment::getNames(JSContext* cx, HandleDebuggerEnvironment environment, MutableHandle result) { MOZ_ASSERT(environment->isDebuggee()); Rooted referent(cx, environment->referent()); AutoIdVector ids(cx); { Maybe 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(cx, environment->referent()); Debugger* dbg = environment->owner(); { Maybe 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 referent(cx, environment->referent()); Debugger* dbg = environment->owner(); { Maybe 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()) { Rooted env(cx, &referent->as()); if (!DebugEnvironmentProxy::getMaybeSentinelValue(cx, env, 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() && IsInternalFunctionObject(obj->as())) 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 referent(cx, environment->referent()); Debugger* dbg = environment->owner(); RootedValue value(cx, value_); if (!dbg->unwrapDebuggeeValue(cx, &value)) return false; { Maybe 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(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); Handle global = obj.as(); objProto = GlobalObject::getOrCreateObjectPrototype(cx, global); 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(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(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(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(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