| /* -*- 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/TypeTraits.h" |
| |
| #include "jscntxt.h" |
| #include "jscompartment.h" |
| #include "jsfriendapi.h" |
| #include "jshashutil.h" |
| #include "jsnum.h" |
| #include "jsobj.h" |
| #include "jswrapper.h" |
| |
| #include "frontend/BytecodeCompiler.h" |
| #include "gc/Marking.h" |
| #include "jit/BaselineDebugModeOSR.h" |
| #include "jit/BaselineJIT.h" |
| #include "jit/JSONSpewer.h" |
| #include "jit/MIRGraph.h" |
| #include "js/GCAPI.h" |
| #include "js/UbiNodeBreadthFirst.h" |
| #include "js/Vector.h" |
| #include "vm/ArgumentsObject.h" |
| #include "vm/DebuggerMemory.h" |
| #include "vm/SPSProfiler.h" |
| #include "vm/TraceLogging.h" |
| #include "vm/WrapperObject.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::UniquePtr; |
| |
| |
| /*** Forward declarations ************************************************************************/ |
| |
| extern const Class DebuggerFrame_class; |
| |
| enum { |
| JSSLOT_DEBUGFRAME_OWNER, |
| JSSLOT_DEBUGFRAME_ARGUMENTS, |
| JSSLOT_DEBUGFRAME_ONSTEP_HANDLER, |
| JSSLOT_DEBUGFRAME_ONPOP_HANDLER, |
| JSSLOT_DEBUGFRAME_COUNT |
| }; |
| |
| extern const Class DebuggerArguments_class; |
| |
| enum { |
| JSSLOT_DEBUGARGUMENTS_FRAME, |
| JSSLOT_DEBUGARGUMENTS_COUNT |
| }; |
| |
| extern const Class DebuggerEnv_class; |
| |
| enum { |
| JSSLOT_DEBUGENV_OWNER, |
| JSSLOT_DEBUGENV_COUNT |
| }; |
| |
| extern const Class DebuggerObject_class; |
| |
| enum { |
| JSSLOT_DEBUGOBJECT_OWNER, |
| JSSLOT_DEBUGOBJECT_COUNT |
| }; |
| |
| extern const Class DebuggerScript_class; |
| |
| enum { |
| JSSLOT_DEBUGSCRIPT_OWNER, |
| JSSLOT_DEBUGSCRIPT_COUNT |
| }; |
| |
| extern const Class DebuggerSource_class; |
| |
| enum { |
| JSSLOT_DEBUGSOURCE_OWNER, |
| JSSLOT_DEBUGSOURCE_TEXT, |
| JSSLOT_DEBUGSOURCE_COUNT |
| }; |
| |
| void DebuggerObject_trace(JSTracer* trc, JSObject* obj); |
| void DebuggerEnv_trace(JSTracer* trc, JSObject* obj); |
| void DebuggerScript_trace(JSTracer* trc, JSObject* obj); |
| void DebuggerSource_trace(JSTracer* trc, JSObject* obj); |
| |
| |
| /*** Utils ***************************************************************************************/ |
| |
| static inline bool |
| EnsureFunctionHasScript(JSContext* cx, HandleFunction fun) |
| { |
| if (fun->isInterpretedLazy()) { |
| AutoCompartment ac(cx, fun); |
| return !!fun->getOrCreateScript(cx); |
| } |
| return true; |
| } |
| |
| static inline JSScript* |
| GetOrCreateFunctionScript(JSContext* cx, HandleFunction fun) |
| { |
| MOZ_ASSERT(fun->isInterpreted()); |
| if (!EnsureFunctionHasScript(cx, fun)) |
| return nullptr; |
| return fun->nonLazyScript(); |
| } |
| |
| static bool |
| ValueToIdentifier(JSContext* cx, HandleValue v, MutableHandleId id) |
| { |
| if (!ValueToId<CanGC>(cx, v, id)) |
| return false; |
| if (!JSID_IS_ATOM(id) || !IsIdentifier(JSID_TO_ATOM(id))) { |
| RootedValue val(cx, v); |
| ReportValueErrorFlags(cx, JSREPORT_ERROR, JSMSG_UNEXPECTED_TYPE, |
| JSDVG_SEARCH_STACK, val, nullptr, "not an identifier", |
| nullptr); |
| return false; |
| } |
| return true; |
| } |
| |
| /* |
| * A range of all the Debugger.Frame objects for a particular AbstractFramePtr. |
| * |
| * FIXME This checks only current debuggers, so it relies on a hack in |
| * Debugger::removeDebuggeeGlobal to make sure only current debuggers |
| * have Frame objects with .live === true. |
| */ |
| class Debugger::FrameRange |
| { |
| AbstractFramePtr frame; |
| |
| /* The debuggers in |fp|'s compartment, or nullptr if there are none. */ |
| GlobalObject::DebuggerVector* debuggers; |
| |
| /* |
| * The index of the front Debugger.Frame's debugger in debuggers. |
| * nextDebugger < debuggerCount if and only if the range is not empty. |
| */ |
| size_t debuggerCount, nextDebugger; |
| |
| /* |
| * If the range is not empty, this is front Debugger.Frame's entry in its |
| * debugger's frame table. |
| */ |
| FrameMap::Ptr entry; |
| |
| public: |
| /* |
| * Return a range containing all Debugger.Frame instances referring to |
| * |fp|. |global| is |fp|'s global object; if nullptr or omitted, we |
| * compute it ourselves from |fp|. |
| * |
| * We keep an index into the compartment's debugger list, and a |
| * FrameMap::Ptr into the current debugger's frame map. Thus, if the set of |
| * debuggers in |fp|'s compartment changes, this range becomes invalid. |
| * Similarly, if stack frames are added to or removed from frontDebugger(), |
| * then the range's front is invalid until popFront is called. |
| */ |
| explicit FrameRange(AbstractFramePtr frame, GlobalObject* global = nullptr) |
| : frame(frame) |
| { |
| nextDebugger = 0; |
| |
| /* Find our global, if we were not given one. */ |
| if (!global) |
| global = &frame.script()->global(); |
| |
| /* The frame and global must match. */ |
| MOZ_ASSERT(&frame.script()->global() == global); |
| |
| /* Find the list of debuggers we'll iterate over. There may be none. */ |
| debuggers = global->getDebuggers(); |
| if (debuggers) { |
| debuggerCount = debuggers->length(); |
| findNext(); |
| } else { |
| debuggerCount = 0; |
| } |
| } |
| |
| bool empty() const { |
| return nextDebugger >= debuggerCount; |
| } |
| |
| NativeObject* frontFrame() const { |
| MOZ_ASSERT(!empty()); |
| return entry->value(); |
| } |
| |
| Debugger* frontDebugger() const { |
| MOZ_ASSERT(!empty()); |
| return (*debuggers)[nextDebugger]; |
| } |
| |
| /* |
| * Delete the front frame from its Debugger's frame map. After this call, |
| * the range's front is invalid until popFront is called. |
| */ |
| void removeFrontFrame() const { |
| MOZ_ASSERT(!empty()); |
| frontDebugger()->frames.remove(entry); |
| } |
| |
| void popFront() { |
| MOZ_ASSERT(!empty()); |
| nextDebugger++; |
| findNext(); |
| } |
| |
| private: |
| /* |
| * Either make this range refer to the first appropriate Debugger.Frame at |
| * or after nextDebugger, or make it empty. |
| */ |
| void findNext() { |
| while (!empty()) { |
| Debugger* dbg = (*debuggers)[nextDebugger]; |
| entry = dbg->frames.lookup(frame); |
| if (entry) |
| break; |
| nextDebugger++; |
| } |
| } |
| }; |
| |
| |
| /*** 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), |
| uncaughtExceptionHook(nullptr), |
| enabled(true), |
| allowUnobservedAsmJS(false), |
| collectCoverageInfo(false), |
| observedGCs(cx), |
| tenurePromotionsLog(cx), |
| trackingTenurePromotions(false), |
| maxTenurePromotionsLogLength(DEFAULT_MAX_LOG_LENGTH), |
| tenurePromotionsLogOverflowed(false), |
| 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), |
| #ifdef NIGHTLY_BUILD |
| traceLoggerLastDrainedSize(0), |
| traceLoggerLastDrainedIteration(0), |
| #endif |
| traceLoggerScriptedCallsLastDrainedSize(0), |
| traceLoggerScriptedCallsLastDrainedIteration(0) |
| { |
| assertSameCompartment(cx, dbg); |
| |
| cx->runtime()->debuggerList.insertBack(this); |
| JS_INIT_CLIST(&breakpoints); |
| JS_INIT_CLIST(&onNewGlobalObjectWatchersLink); |
| } |
| |
| Debugger::~Debugger() |
| { |
| MOZ_ASSERT_IF(debuggees.initialized(), debuggees.empty()); |
| allocationsLog.clear(); |
| tenurePromotionsLog.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) |
| { |
| bool ok = debuggees.init() && |
| debuggeeZones.init() && |
| frames.init() && |
| scripts.init() && |
| sources.init() && |
| objects.init() && |
| observedGCs.init() && |
| environments.init(); |
| if (!ok) |
| ReportOutOfMemory(cx); |
| return ok; |
| } |
| |
| 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(JSSLOT_DEBUGENV_OWNER)); |
| |
| /* 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() == &DebuggerEnv_class); |
| JSObject* dbgobj = &obj->as<NativeObject>().getReservedSlot(JSSLOT_DEBUGOBJECT_OWNER).toObject(); |
| return fromJSObject(dbgobj); |
| } |
| |
| bool |
| Debugger::hasMemory() const |
| { |
| return object->getReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE).isObject(); |
| } |
| |
| DebuggerMemory& |
| Debugger::memory() const |
| { |
| MOZ_ASSERT(hasMemory()); |
| return object->getReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE).toObject().as<DebuggerMemory>(); |
| } |
| |
| bool |
| Debugger::getScriptFrameWithIter(JSContext* cx, AbstractFramePtr frame, |
| const ScriptFrameIter* maybeIter, MutableHandleValue vp) |
| { |
| MOZ_ASSERT_IF(maybeIter, maybeIter->abstractFramePtr() == frame); |
| MOZ_ASSERT(!frame.script()->selfHosted()); |
| |
| FrameMap::AddPtr p = frames.lookupForAdd(frame); |
| if (!p) { |
| /* Create and populate the Debugger.Frame object. */ |
| RootedObject proto(cx, &object->getReservedSlot(JSSLOT_DEBUG_FRAME_PROTO).toObject()); |
| RootedNativeObject frameobj(cx, NewNativeObjectWithGivenProto(cx, &DebuggerFrame_class, |
| proto)); |
| if (!frameobj) |
| return false; |
| |
| // Eagerly copy ScriptFrameIter data if we've already walked the |
| // stack. |
| if (maybeIter) { |
| AbstractFramePtr data = maybeIter->copyDataAsAbstractFramePtr(); |
| if (!data) |
| return false; |
| frameobj->setPrivate(data.raw()); |
| } else { |
| frameobj->setPrivate(frame.raw()); |
| } |
| |
| frameobj->setReservedSlot(JSSLOT_DEBUGFRAME_OWNER, ObjectValue(*object)); |
| |
| if (!ensureExecutionObservabilityOfFrame(cx, frame)) |
| return false; |
| |
| if (!frames.add(p, frame, frameobj)) { |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| } |
| vp.setObject(*p->value()); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::hasLiveHook(GlobalObject* global, Hook which) |
| { |
| if (GlobalObject::DebuggerVector* debuggers = global->getDebuggers()) { |
| for (Debugger** 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() 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(&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, frame, &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, bool frameOk) |
| { |
| Handle<GlobalObject*> global = cx->global(); |
| |
| // 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. |
| FrameRange frameRange(frame, global); |
| if (frameRange.empty()) |
| return frameOk; |
| |
| auto frameMapsGuard = MakeScopeExit([&] { |
| // Clean up all Debugger.Frame instances. This call creates a fresh |
| // FrameRange, as one debugger's onPop handler could have caused another |
| // debugger to create its own Debugger.Frame instance. |
| removeFromFrameMapsAndClearBreakpointsIn(cx, frame); |
| }); |
| |
| /* 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()) { |
| /* Build a list of the recipients. */ |
| AutoObjectVector frames(cx); |
| for (; !frameRange.empty(); frameRange.popFront()) { |
| if (!frames.append(frameRange.frontFrame())) { |
| cx->clearPendingException(); |
| return false; |
| } |
| } |
| |
| /* For each Debugger.Frame, fire its onPop handler, if any. */ |
| for (JSObject** p = frames.begin(); p != frames.end(); p++) { |
| RootedNativeObject frameobj(cx, &(*p)->as<NativeObject>()); |
| Debugger* dbg = Debugger::fromChildJSObject(frameobj); |
| |
| if (dbg->enabled && |
| !frameobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONPOP_HANDLER).isUndefined()) { |
| RootedValue handler(cx, frameobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONPOP_HANDLER)); |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, dbg->object); |
| |
| RootedValue completion(cx); |
| if (!dbg->newCompletionValue(cx, status, value, &completion)) { |
| status = dbg->handleUncaughtException(ac, false); |
| break; |
| } |
| |
| /* Call the onPop handler. */ |
| RootedValue rval(cx); |
| bool hookOk = Invoke(cx, ObjectValue(*frameobj), handler, 1, completion.address(), |
| &rval); |
| RootedValue nextValue(cx); |
| JSTrapStatus nextStatus = dbg->parseResumptionValue(ac, hookOk, rval, &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() == global->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; |
| } |
| |
| bool |
| Debugger::wrapEnvironment(JSContext* cx, Handle<Env*> env, MutableHandleValue rval) |
| { |
| if (!env) { |
| rval.setNull(); |
| return true; |
| } |
| |
| /* |
| * DebuggerEnv should only wrap a debug scope chain obtained (transitively) |
| * from GetDebugScopeFor(Frame|Function). |
| */ |
| MOZ_ASSERT(!IsSyntacticScope(env)); |
| |
| NativeObject* envobj; |
| DependentAddPtr<ObjectWeakMap> p(cx, environments, env); |
| if (p) { |
| envobj = &p->value()->as<NativeObject>(); |
| } else { |
| /* Create a new Debugger.Environment for env. */ |
| RootedObject proto(cx, &object->getReservedSlot(JSSLOT_DEBUG_ENV_PROTO).toObject()); |
| envobj = NewNativeObjectWithGivenProto(cx, &DebuggerEnv_class, proto, |
| TenuredObject); |
| if (!envobj) |
| return false; |
| envobj->setPrivateGCThing(env); |
| envobj->setReservedSlot(JSSLOT_DEBUGENV_OWNER, ObjectValue(*object)); |
| if (!p.add(cx, environments, env, envobj)) |
| return false; |
| |
| CrossCompartmentKey key(CrossCompartmentKey::DebuggerEnvironment, object, env); |
| if (!object->compartment()->putWrapper(cx, key, ObjectValue(*envobj))) { |
| environments.remove(env); |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| } |
| rval.setObject(*envobj); |
| return true; |
| } |
| |
| bool |
| Debugger::wrapDebuggeeValue(JSContext* cx, MutableHandleValue vp) |
| { |
| assertSameCompartment(cx, object.get()); |
| |
| if (vp.isObject()) { |
| RootedObject obj(cx, &vp.toObject()); |
| |
| if (obj->is<JSFunction>()) { |
| MOZ_ASSERT(!IsInternalFunctionObject(*obj)); |
| RootedFunction fun(cx, &obj->as<JSFunction>()); |
| if (!EnsureFunctionHasScript(cx, fun)) |
| return false; |
| } |
| |
| DependentAddPtr<ObjectWeakMap> p(cx, objects, obj); |
| if (p) { |
| vp.setObject(*p->value()); |
| } else { |
| /* Create a new Debugger.Object for obj. */ |
| RootedObject proto(cx, &object->getReservedSlot(JSSLOT_DEBUG_OBJECT_PROTO).toObject()); |
| NativeObject* dobj = |
| NewNativeObjectWithGivenProto(cx, &DebuggerObject_class, proto, |
| TenuredObject); |
| if (!dobj) |
| return false; |
| dobj->setPrivateGCThing(obj); |
| dobj->setReservedSlot(JSSLOT_DEBUGOBJECT_OWNER, ObjectValue(*object)); |
| |
| if (!p.add(cx, objects, obj, dobj)) |
| return false; |
| |
| if (obj->compartment() != object->compartment()) { |
| CrossCompartmentKey key(CrossCompartmentKey::DebuggerObject, object, obj); |
| if (!object->compartment()->putWrapper(cx, key, ObjectValue(*dobj))) { |
| objects.remove(obj); |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| } |
| |
| vp.setObject(*dobj); |
| } |
| } else if (vp.isMagic()) { |
| RootedPlainObject optObj(cx, NewBuiltinClassInstance<PlainObject>(cx)); |
| if (!optObj) |
| return false; |
| |
| // We handle three sentinel values: missing arguments (overloading |
| // JS_OPTIMIZED_ARGUMENTS), optimized out slots (JS_OPTIMIZED_OUT), |
| // and uninitialized bindings (JS_UNINITIALIZED_LEXICAL). |
| // |
| // Other magic values should not have escaped. |
| PropertyName* name; |
| switch (vp.whyMagic()) { |
| case JS_OPTIMIZED_ARGUMENTS: name = cx->names().missingArguments; break; |
| case JS_OPTIMIZED_OUT: name = cx->names().optimizedOut; break; |
| case JS_UNINITIALIZED_LEXICAL: name = cx->names().uninitialized; break; |
| default: MOZ_CRASH("Unsupported magic value escaped to Debugger"); |
| } |
| |
| RootedValue trueVal(cx, BooleanValue(true)); |
| if (!DefineProperty(cx, optObj, name, trueVal)) |
| return false; |
| |
| vp.setObject(*optObj); |
| } else if (!cx->compartment()->wrap(cx, vp)) { |
| vp.setUndefined(); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool |
| Debugger::unwrapDebuggeeObject(JSContext* cx, MutableHandleObject obj) |
| { |
| if (obj->getClass() != &DebuggerObject_class) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE, |
| "Debugger", "Debugger.Object", obj->getClass()->name); |
| return false; |
| } |
| NativeObject* ndobj = &obj->as<NativeObject>(); |
| |
| Value owner = ndobj->getReservedSlot(JSSLOT_DEBUGOBJECT_OWNER); |
| if (owner.isUndefined() || &owner.toObject() != object) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, |
| owner.isUndefined() |
| ? JSMSG_DEBUG_OBJECT_PROTO |
| : JSMSG_DEBUG_OBJECT_WRONG_OWNER); |
| return false; |
| } |
| |
| obj.set(static_cast<JSObject*>(ndobj->getPrivate())); |
| return true; |
| } |
| |
| bool |
| Debugger::unwrapDebuggeeValue(JSContext* cx, MutableHandleValue vp) |
| { |
| assertSameCompartment(cx, object.get(), vp); |
| if (vp.isObject()) { |
| RootedObject dobj(cx, &vp.toObject()); |
| if (!unwrapDebuggeeObject(cx, &dobj)) |
| return false; |
| vp.setObject(*dobj); |
| } |
| return true; |
| } |
| |
| static bool |
| CheckArgCompartment(JSContext* cx, JSObject* obj, JSObject* arg, |
| const char* methodname, const char* propname) |
| { |
| if (arg->compartment() != obj->compartment()) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_COMPARTMENT_MISMATCH, |
| methodname, propname); |
| return false; |
| } |
| return true; |
| } |
| |
| static bool |
| CheckArgCompartment(JSContext* cx, JSObject* obj, HandleValue v, |
| const char* methodname, const char* propname) |
| { |
| if (v.isObject()) |
| return CheckArgCompartment(cx, obj, &v.toObject(), methodname, propname); |
| return true; |
| } |
| |
| bool |
| Debugger::unwrapPropertyDescriptor(JSContext* cx, HandleObject obj, |
| MutableHandle<PropertyDescriptor> desc) |
| { |
| if (desc.hasValue()) { |
| RootedValue value(cx, desc.value()); |
| if (!unwrapDebuggeeValue(cx, &value) || |
| !CheckArgCompartment(cx, obj, value, "defineProperty", "value")) |
| { |
| return false; |
| } |
| desc.setValue(value); |
| } |
| |
| if (desc.hasGetterObject()) { |
| RootedObject get(cx, desc.getterObject()); |
| if (get) { |
| if (!unwrapDebuggeeObject(cx, &get)) |
| return false; |
| if (!CheckArgCompartment(cx, obj, get, "defineProperty", "get")) |
| return false; |
| } |
| desc.setGetterObject(get); |
| } |
| |
| if (desc.hasSetterObject()) { |
| RootedObject set(cx, desc.setterObject()); |
| if (set) { |
| if (!unwrapDebuggeeObject(cx, &set)) |
| return false; |
| if (!CheckArgCompartment(cx, obj, set, "defineProperty", "set")) |
| return false; |
| } |
| desc.setSetterObject(set); |
| } |
| |
| return true; |
| } |
| |
| namespace { |
| class MOZ_STACK_CLASS ReportExceptionClosure : public ScriptEnvironmentPreparer::Closure |
| { |
| public: |
| explicit ReportExceptionClosure(RootedValue& exn) |
| : exn_(exn) |
| { |
| } |
| |
| bool operator()(JSContext* cx) override |
| { |
| cx->setPendingException(exn_); |
| return false; |
| } |
| |
| private: |
| RootedValue& exn_; |
| }; |
| } // anonymous namespace |
| |
| JSTrapStatus |
| Debugger::handleUncaughtExceptionHelper(Maybe<AutoCompartment>& ac, |
| MutableHandleValue* vp, bool callHook) |
| { |
| JSContext* cx = ac->context()->asJSContext(); |
| if (cx->isExceptionPending()) { |
| if (callHook && uncaughtExceptionHook) { |
| RootedValue exc(cx); |
| if (!cx->getPendingException(&exc)) |
| return JSTRAP_ERROR; |
| cx->clearPendingException(); |
| RootedValue fval(cx, ObjectValue(*uncaughtExceptionHook)); |
| RootedValue rv(cx); |
| if (Invoke(cx, ObjectValue(*object), fval, 1, exc.address(), &rv)) |
| return vp ? parseResumptionValue(ac, true, rv, *vp, false) : JSTRAP_CONTINUE; |
| } |
| |
| 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::handleUncaughtException(Maybe<AutoCompartment>& ac, MutableHandleValue vp, bool callHook) |
| { |
| return handleUncaughtExceptionHelper(ac, &vp, callHook); |
| } |
| |
| JSTrapStatus |
| Debugger::handleUncaughtException(Maybe<AutoCompartment>& ac, bool callHook) |
| { |
| return handleUncaughtExceptionHelper(ac, nullptr, callHook); |
| } |
| |
| /* 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, 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()); |
| |
| RootedId key(cx); |
| RootedValue value(cx, value_); |
| |
| switch (status) { |
| case JSTRAP_RETURN: |
| key = NameToId(cx->names().return_); |
| break; |
| |
| case JSTRAP_THROW: |
| key = NameToId(cx->names().throw_); |
| break; |
| |
| case JSTRAP_ERROR: |
| result.setNull(); |
| return true; |
| |
| default: |
| MOZ_CRASH("bad status passed to Debugger::newCompletionValue"); |
| } |
| |
| /* Common tail for JSTRAP_RETURN and JSTRAP_THROW. */ |
| RootedPlainObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx)); |
| if (!obj || |
| !wrapDebuggeeValue(cx, &value) || |
| !NativeDefineProperty(cx, obj, key, value, nullptr, nullptr, JSPROP_ENUMERATE)) |
| { |
| return false; |
| } |
| |
| result.setObject(*obj); |
| return true; |
| } |
| |
| bool |
| Debugger::receiveCompletionValue(Maybe<AutoCompartment>& ac, bool ok, |
| HandleValue val, |
| MutableHandleValue vp) |
| { |
| JSContext* cx = ac->context()->asJSContext(); |
| |
| JSTrapStatus status; |
| RootedValue value(cx); |
| resultToCompletion(cx, ok, val, &status, &value); |
| ac.reset(); |
| return newCompletionValue(cx, status, value, vp); |
| } |
| |
| static bool |
| GetStatusProperty(JSContext* cx, HandleObject obj, HandlePropertyName name, JSTrapStatus status, |
| JSTrapStatus* statusOut, MutableHandleValue vp, int* hits) |
| { |
| bool found; |
| if (!HasProperty(cx, obj, name, &found)) |
| return false; |
| if (found) { |
| ++*hits; |
| *statusOut = 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_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_BAD_RESUMPTION); |
| return false; |
| } |
| return true; |
| } |
| |
| JSTrapStatus |
| Debugger::parseResumptionValue(Maybe<AutoCompartment>& ac, bool ok, const Value& rv, MutableHandleValue vp, |
| bool callHook) |
| { |
| vp.setUndefined(); |
| if (!ok) |
| return handleUncaughtException(ac, vp, callHook); |
| if (rv.isUndefined()) { |
| ac.reset(); |
| return JSTRAP_CONTINUE; |
| } |
| if (rv.isNull()) { |
| ac.reset(); |
| return JSTRAP_ERROR; |
| } |
| |
| JSContext* cx = ac->context()->asJSContext(); |
| JSTrapStatus status = JSTRAP_CONTINUE; |
| RootedValue v(cx); |
| RootedValue rvRoot(cx, rv); |
| if (!ParseResumptionValueAsObject(cx, rvRoot, &status, &v) || |
| !unwrapDebuggeeValue(cx, &v)) |
| { |
| return handleUncaughtException(ac, vp, callHook); |
| } |
| |
| ac.reset(); |
| if (!cx->compartment()->wrap(cx, &v)) { |
| vp.setUndefined(); |
| return JSTRAP_ERROR; |
| } |
| vp.set(v); |
| |
| return status; |
| } |
| |
| static bool |
| CallMethodIfPresent(JSContext* cx, HandleObject obj, const char* name, int 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); |
| return GetProperty(cx, obj, obj, id, &fval) && |
| (!IsCallable(fval) || Invoke(cx, ObjectValue(*obj), fval, argc, argv, rval)); |
| } |
| |
| JSTrapStatus |
| Debugger::fireDebuggerStatement(JSContext* cx, MutableHandleValue vp) |
| { |
| RootedObject hook(cx, getHook(OnDebuggerStatement)); |
| MOZ_ASSERT(hook); |
| MOZ_ASSERT(hook->isCallable()); |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, object); |
| |
| ScriptFrameIter iter(cx); |
| RootedValue scriptFrame(cx); |
| if (!getScriptFrame(cx, iter, &scriptFrame)) |
| return handleUncaughtException(ac, false); |
| |
| RootedValue rv(cx); |
| bool ok = Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, scriptFrame.address(), &rv); |
| return parseResumptionValue(ac, ok, rv, vp); |
| } |
| |
| JSTrapStatus |
| Debugger::fireExceptionUnwind(JSContext* cx, MutableHandleValue vp) |
| { |
| RootedObject hook(cx, getHook(OnExceptionUnwind)); |
| MOZ_ASSERT(hook); |
| MOZ_ASSERT(hook->isCallable()); |
| |
| RootedValue exc(cx); |
| if (!cx->getPendingException(&exc)) |
| return JSTRAP_ERROR; |
| cx->clearPendingException(); |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, object); |
| |
| JS::AutoValueArray<2> argv(cx); |
| argv[0].setUndefined(); |
| argv[1].set(exc); |
| |
| ScriptFrameIter iter(cx); |
| if (!getScriptFrame(cx, iter, argv[0]) || !wrapDebuggeeValue(cx, argv[1])) |
| return handleUncaughtException(ac, false); |
| |
| RootedValue rv(cx); |
| bool ok = Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 2, argv.begin(), &rv); |
| JSTrapStatus st = parseResumptionValue(ac, ok, rv, vp); |
| if (st == JSTRAP_CONTINUE) |
| cx->setPendingException(exc); |
| return st; |
| } |
| |
| JSTrapStatus |
| Debugger::fireEnterFrame(JSContext* cx, AbstractFramePtr frame, MutableHandleValue vp) |
| { |
| RootedObject hook(cx, getHook(OnEnterFrame)); |
| MOZ_ASSERT(hook); |
| MOZ_ASSERT(hook->isCallable()); |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, object); |
| |
| RootedValue scriptFrame(cx); |
| if (!getScriptFrame(cx, frame, &scriptFrame)) |
| return handleUncaughtException(ac, false); |
| |
| RootedValue rv(cx); |
| bool ok = Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, scriptFrame.address(), &rv); |
| return parseResumptionValue(ac, ok, rv, vp); |
| } |
| |
| void |
| Debugger::fireNewScript(JSContext* cx, HandleScript script) |
| { |
| RootedObject hook(cx, getHook(OnNewScript)); |
| MOZ_ASSERT(hook); |
| MOZ_ASSERT(hook->isCallable()); |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, object); |
| |
| JSObject* dsobj = wrapScript(cx, script); |
| if (!dsobj) { |
| handleUncaughtException(ac, false); |
| return; |
| } |
| |
| RootedValue scriptObject(cx, ObjectValue(*dsobj)); |
| RootedValue rv(cx); |
| if (!Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, scriptObject.address(), &rv)) |
| handleUncaughtException(ac, true); |
| } |
| |
| void |
| Debugger::fireOnGarbageCollectionHook(JSContext* cx, |
| const JS::dbg::GarbageCollectionEvent::Ptr& gcData) |
| { |
| MOZ_ASSERT(observedGC(gcData->majorGCNumber())); |
| observedGCs.remove(gcData->majorGCNumber()); |
| |
| RootedObject hook(cx, getHook(OnGarbageCollection)); |
| MOZ_ASSERT(hook); |
| MOZ_ASSERT(hook->isCallable()); |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, object); |
| |
| JSObject* dataObj = gcData->toJSObject(cx); |
| if (!dataObj) { |
| handleUncaughtException(ac, false); |
| return; |
| } |
| |
| RootedValue dataVal(cx, ObjectValue(*dataObj)); |
| RootedValue rv(cx); |
| if (!Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, dataVal.address(), &rv)) |
| handleUncaughtException(ac, true); |
| } |
| |
| JSTrapStatus |
| Debugger::fireOnIonCompilationHook(JSContext* cx, Handle<ScriptVector> scripts, LSprinter& graph) |
| { |
| RootedObject hook(cx, getHook(OnIonCompilation)); |
| MOZ_ASSERT(hook); |
| MOZ_ASSERT(hook->isCallable()); |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, object); |
| |
| // Copy the vector of scripts to a JS Array of Debugger.Script |
| RootedObject tmpObj(cx); |
| RootedValue tmpVal(cx); |
| AutoValueVector dbgScripts(cx); |
| for (size_t i = 0; i < scripts.length(); i++) { |
| tmpObj = wrapScript(cx, scripts[i]); |
| if (!tmpObj) |
| return handleUncaughtException(ac, false); |
| |
| tmpVal.setObject(*tmpObj); |
| if (!dbgScripts.append(tmpVal)) |
| return handleUncaughtException(ac, false); |
| } |
| |
| RootedObject dbgScriptsArray(cx, JS_NewArrayObject(cx, dbgScripts)); |
| if (!dbgScriptsArray) |
| return handleUncaughtException(ac, false); |
| |
| // Copy the JSON compilation graph to a JS String which is allocated as part |
| // of the Debugger compartment. |
| Sprinter jsonPrinter(cx); |
| if (!jsonPrinter.init()) |
| return handleUncaughtException(ac, false); |
| |
| graph.exportInto(jsonPrinter); |
| if (jsonPrinter.hadOutOfMemory()) |
| return handleUncaughtException(ac, false); |
| |
| RootedString json(cx, JS_NewStringCopyZ(cx, jsonPrinter.string())); |
| if (!json) |
| return handleUncaughtException(ac, false); |
| |
| // Create a JS Object which has the array of scripts, and the string of the |
| // JSON graph. |
| const char* names[] = { "scripts", "json" }; |
| JS::AutoValueArray<2> values(cx); |
| values[0].setObject(*dbgScriptsArray); |
| values[1].setString(json); |
| |
| RootedObject obj(cx, JS_NewObject(cx, nullptr)); |
| if (!obj) |
| return handleUncaughtException(ac, false); |
| |
| MOZ_ASSERT(mozilla::ArrayLength(names) == values.length()); |
| for (size_t i = 0; i < mozilla::ArrayLength(names); i++) { |
| if (!JS_DefineProperty(cx, obj, names[i], values[i], JSPROP_ENUMERATE, nullptr, nullptr)) |
| return handleUncaughtException(ac, false); |
| } |
| |
| // Call Debugger.onIonCompilation hook. |
| JS::AutoValueArray<1> argv(cx); |
| argv[0].setObject(*obj); |
| |
| RootedValue rv(cx); |
| if (!Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, argv.begin(), &rv)) |
| return handleUncaughtException(ac, true); |
| return JSTRAP_CONTINUE; |
| } |
| |
| template <typename HookIsEnabledFun /* bool (Debugger*) */, |
| typename FireHookFun /* JSTrapStatus (Debugger*) */> |
| /* static */ JSTrapStatus |
| Debugger::dispatchHook(JSContext* cx, HookIsEnabledFun hookIsEnabled, FireHookFun fireHook) |
| { |
| /* |
| * Determine which debuggers will receive this event, and in what order. |
| * Make a copy of the list, since the original is mutable and we will be |
| * calling into arbitrary JS. |
| * |
| * Note: In the general case, 'triggered' contains references to objects in |
| * different compartments--every compartment *except* this one. |
| */ |
| AutoValueVector triggered(cx); |
| Handle<GlobalObject*> global = cx->global(); |
| if (GlobalObject::DebuggerVector* debuggers = global->getDebuggers()) { |
| for (Debugger** 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()); |
| 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 { |
| dbg->fireNewScript(cx, script); |
| return JSTRAP_CONTINUE; |
| }); |
| |
| if (status == JSTRAP_ERROR) { |
| ReportOutOfMemory(cx); |
| return; |
| } |
| |
| MOZ_ASSERT(status == JSTRAP_CONTINUE); |
| } |
| |
| /* static */ JSTrapStatus |
| Debugger::onTrap(JSContext* cx, MutableHandleValue vp) |
| { |
| ScriptFrameIter iter(cx); |
| RootedScript script(cx, iter.script()); |
| MOZ_ASSERT(script->isDebuggee()); |
| Rooted<GlobalObject*> scriptGlobal(cx, &script->global()); |
| jsbytecode* pc = iter.pc(); |
| BreakpointSite* site = script->getBreakpointSite(pc); |
| JSOp op = JSOp(*pc); |
| |
| /* Build list of breakpoint handlers. */ |
| Vector<Breakpoint*> triggered(cx); |
| for (Breakpoint* bp = site->firstBreakpoint(); bp; bp = bp->nextInSite()) { |
| if (!triggered.append(bp)) |
| return JSTRAP_ERROR; |
| } |
| |
| for (Breakpoint** p = triggered.begin(); p != triggered.end(); p++) { |
| Breakpoint* bp = *p; |
| |
| /* Handlers can clear breakpoints. Check that bp still exists. */ |
| if (!site || !site->hasBreakpoint(bp)) |
| continue; |
| |
| /* |
| * There are two reasons we have to check whether dbg is enabled and |
| * debugging scriptGlobal. |
| * |
| * One is just that one breakpoint handler can disable other Debuggers |
| * or remove debuggees. |
| * |
| * The other has to do with non-compile-and-go scripts, which have no |
| * specific global--until they are executed. Only now do we know which |
| * global the script is running against. |
| */ |
| Debugger* dbg = bp->debugger; |
| bool hasDebuggee = dbg->enabled && dbg->debuggees.has(scriptGlobal); |
| if (hasDebuggee) { |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, dbg->object); |
| |
| RootedValue scriptFrame(cx); |
| if (!dbg->getScriptFrame(cx, iter, &scriptFrame)) |
| return dbg->handleUncaughtException(ac, false); |
| RootedValue rv(cx); |
| Rooted<JSObject*> handler(cx, bp->handler); |
| bool ok = CallMethodIfPresent(cx, handler, "hit", 1, scriptFrame.address(), &rv); |
| JSTrapStatus st = dbg->parseResumptionValue(ac, ok, rv, vp, true); |
| 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. |
| */ |
| AutoObjectVector frames(cx); |
| for (FrameRange r(iter.abstractFramePtr()); !r.empty(); r.popFront()) { |
| NativeObject* frame = r.frontFrame(); |
| if (!frame->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined() && |
| !frames.append(frame)) |
| { |
| 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 (Debugger** 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 all the onStep handlers we found. */ |
| for (JSObject** p = frames.begin(); p != frames.end(); p++) { |
| RootedNativeObject frame(cx, &(*p)->as<NativeObject>()); |
| Debugger* dbg = Debugger::fromChildJSObject(frame); |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, dbg->object); |
| |
| const Value& handler = frame->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER); |
| RootedValue rval(cx); |
| bool ok = Invoke(cx, ObjectValue(*frame), handler, 0, nullptr, &rval); |
| JSTrapStatus st = dbg->parseResumptionValue(ac, ok, rval, vp); |
| if (st != JSTRAP_CONTINUE) |
| return st; |
| } |
| |
| vp.setUndefined(); |
| if (exceptionPending) |
| cx->setPendingException(exception); |
| return JSTRAP_CONTINUE; |
| } |
| |
| JSTrapStatus |
| Debugger::fireNewGlobalObject(JSContext* cx, Handle<GlobalObject*> global, MutableHandleValue vp) |
| { |
| RootedObject hook(cx, getHook(OnNewGlobalObject)); |
| MOZ_ASSERT(hook); |
| MOZ_ASSERT(hook->isCallable()); |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, object); |
| |
| RootedValue wrappedGlobal(cx, ObjectValue(*global)); |
| if (!wrapDebuggeeValue(cx, &wrappedGlobal)) |
| return handleUncaughtException(ac, false); |
| |
| RootedValue rv(cx); |
| |
| // 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. |
| bool ok = Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, wrappedGlobal.address(), &rv); |
| if (ok && !rv.isUndefined()) { |
| JS_ReportErrorNumber(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, true); |
| MOZ_ASSERT(!cx->isExceptionPending()); |
| return status; |
| } |
| |
| void |
| Debugger::slowPathOnNewGlobalObject(JSContext* cx, Handle<GlobalObject*> global) |
| { |
| MOZ_ASSERT(!JS_CLIST_IS_EMPTY(&cx->runtime()->onNewGlobalObjectWatchers)); |
| if (global->compartment()->options().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)) |
| return; |
| } |
| |
| JSTrapStatus status = JSTRAP_CONTINUE; |
| RootedValue value(cx); |
| |
| for (size_t i = 0; i < watchers.length(); i++) { |
| Debugger* dbg = fromJSObject(watchers[i]); |
| |
| // 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<Debugger**> begin = dbgs.begin(); |
| |
| for (Debugger** 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; |
| } |
| |
| /* static */ void |
| Debugger::slowPathOnIonCompilation(JSContext* cx, Handle<ScriptVector> scripts, LSprinter& graph) |
| { |
| JSTrapStatus status = dispatchHook( |
| cx, |
| [](Debugger* dbg) -> bool { return dbg->getHook(OnIonCompilation); }, |
| [&](Debugger* dbg) -> JSTrapStatus { |
| (void) dbg->fireOnIonCompilationHook(cx, scripts, graph); |
| return JSTRAP_CONTINUE; |
| }); |
| |
| if (status == JSTRAP_ERROR) { |
| cx->clearPendingException(); |
| return; |
| } |
| |
| MOZ_ASSERT(status == JSTRAP_CONTINUE); |
| } |
| |
| bool |
| Debugger::isDebuggeeUnbarriered(const JSCompartment* compartment) const |
| { |
| MOZ_ASSERT(compartment); |
| return compartment->isDebuggee() && debuggees.has(compartment->unsafeUnbarrieredMaybeGlobal()); |
| } |
| |
| Debugger::TenurePromotionsLogEntry::TenurePromotionsLogEntry(JSRuntime* rt, JSObject& obj, double when) |
| : className(obj.getClass()->name), |
| when(when), |
| frame(getObjectAllocationSite(obj)), |
| size(JS::ubi::Node(&obj).size(rt->debuggerMallocSizeOf)) |
| { } |
| |
| |
| void |
| Debugger::logTenurePromotion(JSRuntime* rt, JSObject& obj, double when) |
| { |
| AutoEnterOOMUnsafeRegion oomUnsafe; |
| |
| if (!tenurePromotionsLog.emplaceBack(rt, obj, when)) |
| oomUnsafe.crash("Debugger::logTenurePromotion"); |
| |
| if (tenurePromotionsLog.length() > maxTenurePromotionsLogLength) { |
| if (!tenurePromotionsLog.popFront()) |
| oomUnsafe.crash("Debugger::logTenurePromotion"); |
| MOZ_ASSERT(tenurePromotionsLog.length() == maxTenurePromotionsLogLength); |
| tenurePromotionsLogOverflowed = true; |
| } |
| } |
| |
| bool |
| Debugger::appendAllocationSite(JSContext* cx, HandleObject obj, HandleSavedFrame frame, |
| double when) |
| { |
| MOZ_ASSERT(trackingAllocationSites && enabled); |
| |
| AutoCompartment ac(cx, object); |
| RootedObject wrappedFrame(cx, frame); |
| if (!cx->compartment()->wrap(cx, &wrappedFrame)) |
| return false; |
| |
| RootedAtom ctorName(cx); |
| { |
| AutoCompartment ac(cx, obj); |
| if (!obj->constructorDisplayAtom(cx, &ctorName)) |
| return false; |
| } |
| |
| auto className = obj->getClass()->name; |
| auto size = JS::ubi::Node(obj.get()).size(cx->runtime()->debuggerMallocSizeOf); |
| auto inNursery = gc::IsInsideNursery(obj); |
| |
| if (!allocationsLog.emplaceBack(wrappedFrame, when, className, ctorName, size, inNursery)) { |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| |
| if (allocationsLog.length() > maxAllocationsLogLength) { |
| if (!allocationsLog.popFront()) { |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| MOZ_ASSERT(allocationsLog.length() == maxAllocationsLogLength); |
| allocationsLogOverflowed = true; |
| } |
| |
| return true; |
| } |
| |
| JSTrapStatus |
| Debugger::firePromiseHook(JSContext* cx, Hook hook, HandleObject promise, MutableHandleValue vp) |
| { |
| MOZ_ASSERT(hook == OnNewPromise || hook == OnPromiseSettled); |
| |
| RootedObject hookObj(cx, getHook(hook)); |
| MOZ_ASSERT(hookObj); |
| MOZ_ASSERT(hookObj->isCallable()); |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, object); |
| |
| RootedValue dbgObj(cx, ObjectValue(*promise)); |
| if (!wrapDebuggeeValue(cx, &dbgObj)) |
| return handleUncaughtException(ac, false); |
| |
| // Like onNewGlobalObject, the Promise hooks are infallible and the comments |
| // in |Debugger::fireNewGlobalObject| apply here as well. |
| |
| RootedValue rv(cx); |
| bool ok = Invoke(cx, ObjectValue(*object), ObjectValue(*hookObj), 1, dbgObj.address(), &rv); |
| if (ok && !rv.isUndefined()) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, |
| JSMSG_DEBUG_RESUMPTION_VALUE_DISALLOWED); |
| ok = false; |
| } |
| |
| JSTrapStatus status = ok ? JSTRAP_CONTINUE |
| : handleUncaughtException(ac, vp, true); |
| MOZ_ASSERT(!cx->isExceptionPending()); |
| return status; |
| } |
| |
| /* static */ void |
| Debugger::slowPathPromiseHook(JSContext* cx, Hook hook, HandleObject promise) |
| { |
| MOZ_ASSERT(hook == OnNewPromise || hook == OnPromiseSettled); |
| RootedValue rval(cx); |
| |
| JSTrapStatus status = dispatchHook( |
| cx, |
| [hook](Debugger* dbg) -> bool { return dbg->getHook(hook); }, |
| [&](Debugger* dbg) -> JSTrapStatus { |
| (void) dbg->firePromiseHook(cx, hook, promise, &rval); |
| return JSTRAP_CONTINUE; |
| }); |
| |
| if (status == JSTRAP_ERROR) { |
| // The dispatch hook function might fail to append into the list of |
| // Debuggers which are watching for the hook. |
| cx->clearPendingException(); |
| return; |
| } |
| |
| // Promise hooks are infallible and we ignore errors from uncaught |
| // exceptions by design. |
| MOZ_ASSERT(status == JSTRAP_CONTINUE); |
| } |
| |
| |
| /*** Debugger code invalidation for observing execution ******************************************/ |
| |
| class MOZ_RAII ExecutionObservableCompartments : public Debugger::ExecutionObservableSet |
| { |
| HashSet<JSCompartment*> compartments_; |
| HashSet<Zone*> zones_; |
| |
| public: |
| explicit ExecutionObservableCompartments(JSContext* cx |
| MOZ_GUARD_OBJECT_NOTIFIER_PARAM) |
| : compartments_(cx), |
| zones_(cx) |
| { |
| MOZ_GUARD_OBJECT_NOTIFIER_INIT; |
| } |
| |
| bool init() { return compartments_.init() && zones_.init(); } |
| bool add(JSCompartment* comp) { return compartments_.put(comp) && zones_.put(comp->zone()); } |
| |
| typedef HashSet<JSCompartment*>::Range CompartmentRange; |
| const HashSet<JSCompartment*>* compartments() const { return &compartments_; } |
| |
| const HashSet<Zone*>* zones() const { return &zones_; } |
| bool shouldRecompileOrInvalidate(JSScript* script) const { |
| return script->hasBaselineScript() && compartments_.has(script->compartment()); |
| } |
| bool shouldMarkAsDebuggee(ScriptFrameIter& iter) const { |
| // AbstractFramePtr can't refer to non-remateralized Ion frames, so if |
| // iter refers to one such, we know we don't match. |
| return iter.hasUsableAbstractFramePtr() && compartments_.has(iter.compartment()); |
| } |
| |
| MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER |
| }; |
| |
| // Given a particular AbstractFramePtr F that has become observable, this |
| // represents the stack frames that need to be bailed out or marked as |
| // debuggees, and the scripts that need to be recompiled, taking inlining into |
| // account. |
| class MOZ_RAII ExecutionObservableFrame : public Debugger::ExecutionObservableSet |
| { |
| AbstractFramePtr frame_; |
| |
| public: |
| explicit ExecutionObservableFrame(AbstractFramePtr frame |
| MOZ_GUARD_OBJECT_NOTIFIER_PARAM) |
| : frame_(frame) |
| { |
| MOZ_GUARD_OBJECT_NOTIFIER_INIT; |
| } |
| |
| Zone* singleZone() const { |
| // We never inline across compartments, let alone across zones, so |
| // frames_'s script's zone is the only one of interest. |
| return frame_.script()->compartment()->zone(); |
| } |
| |
| JSScript* singleScriptForZoneInvalidation() const { |
| MOZ_CRASH("ExecutionObservableFrame shouldn't need zone-wide invalidation."); |
| return nullptr; |
| } |
| |
| bool shouldRecompileOrInvalidate(JSScript* script) const { |
| // Normally, *this represents exactly one script: the one frame_ is |
| // running. |
| // |
| // However, debug-mode OSR uses *this for both invalidating Ion frames, |
| // and recompiling the Baseline scripts that those Ion frames will bail |
| // out into. Suppose frame_ is an inline frame, executing a copy of its |
| // JSScript, S_inner, that has been inlined into the IonScript of some |
| // other JSScript, S_outer. We must match S_outer, to decide which Ion |
| // frame to invalidate; and we must match S_inner, to decide which |
| // Baseline script to recompile. |
| // |
| // Note that this does not, by design, invalidate *all* inliners of |
| // frame_.script(), as only frame_ is made observable, not |
| // frame_.script(). |
| if (!script->hasBaselineScript()) |
| return false; |
| |
| if (script == frame_.script()) |
| return true; |
| |
| return frame_.isRematerializedFrame() && |
| script == frame_.asRematerializedFrame()->outerScript(); |
| } |
| |
| bool shouldMarkAsDebuggee(ScriptFrameIter& iter) const { |
| // AbstractFramePtr can't refer to non-remateralized Ion frames, so if |
| // iter refers to one such, we know we don't match. |
| // |
| // We never use this 'has' overload for frame invalidation, only for |
| // frame debuggee marking; so this overload doesn't need a parallel to |
| // the just-so inlining logic above. |
| return iter.hasUsableAbstractFramePtr() && iter.abstractFramePtr() == frame_; |
| } |
| |
| MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER |
| }; |
| |
| class MOZ_RAII ExecutionObservableScript : public Debugger::ExecutionObservableSet |
| { |
| RootedScript script_; |
| |
| public: |
| ExecutionObservableScript(JSContext* cx, JSScript* script |
| MOZ_GUARD_OBJECT_NOTIFIER_PARAM) |
| : script_(cx, script) |
| { |
| MOZ_GUARD_OBJECT_NOTIFIER_INIT; |
| } |
| |
| Zone* singleZone() const { return script_->compartment()->zone(); } |
| JSScript* singleScriptForZoneInvalidation() const { return script_; } |
| bool shouldRecompileOrInvalidate(JSScript* script) const { |
| return script->hasBaselineScript() && script == script_; |
| } |
| bool shouldMarkAsDebuggee(ScriptFrameIter& iter) const { |
| // AbstractFramePtr can't refer to non-remateralized Ion frames, and |
| // while a non-rematerialized Ion frame may indeed be running script_, |
| // we cannot mark them as debuggees until they bail out. |
| // |
| // Upon bailing out, any newly constructed Baseline frames that came |
| // from Ion frames with scripts that are isDebuggee() is marked as |
| // debuggee. This is correct in that the only other way a frame may be |
| // marked as debuggee is via Debugger.Frame reflection, which would |
| // have rematerialized any Ion frames. |
| return iter.hasUsableAbstractFramePtr() && iter.abstractFramePtr().script() == script_; |
| } |
| |
| MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER |
| }; |
| |
| /* static */ bool |
| Debugger::updateExecutionObservabilityOfFrames(JSContext* cx, const ExecutionObservableSet& obs, |
| IsObserving observing) |
| { |
| AutoSuppressProfilerSampling suppressProfilerSampling(cx); |
| |
| { |
| jit::JitContext jctx(cx, nullptr); |
| if (!jit::RecompileOnStackBaselineScriptsForDebugMode(cx, obs, observing)) { |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| } |
| |
| AbstractFramePtr oldestEnabledFrame; |
| for (ScriptFrameIter iter(cx, ScriptFrameIter::ALL_CONTEXTS, |
| ScriptFrameIter::GO_THROUGH_SAVED); |
| !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. |
| FrameRange r(iter.abstractFramePtr()); |
| MOZ_ASSERT(r.empty()); |
| #endif |
| iter.abstractFramePtr().unsetIsDebuggee(); |
| } |
| } |
| } |
| |
| // See comment in unsetPrevUpToDateUntil. |
| if (oldestEnabledFrame) { |
| AutoCompartment ac(cx, oldestEnabledFrame.compartment()); |
| DebugScopes::unsetPrevUpToDateUntil(cx, oldestEnabledFrame); |
| } |
| |
| return true; |
| } |
| |
| static inline void |
| MarkBaselineScriptActiveIfObservable(JSScript* script, const Debugger::ExecutionObservableSet& obs) |
| { |
| if (obs.shouldRecompileOrInvalidate(script)) |
| script->baselineScript()->setActive(); |
| } |
| |
| static bool |
| AppendAndInvalidateScript(JSContext* cx, Zone* zone, JSScript* script, Vector<JSScript*>& scripts) |
| { |
| // Enter the script's compartment as addPendingRecompile attempts to |
| // cancel off-thread compilations, whose books are kept on the |
| // script's compartment. |
| MOZ_ASSERT(script->compartment()->zone() == zone); |
| AutoCompartment ac(cx, script->compartment()); |
| zone->types.addPendingRecompile(cx, script); |
| return scripts.append(script); |
| } |
| |
| static bool |
| UpdateExecutionObservabilityOfScriptsInZone(JSContext* cx, Zone* zone, |
| const Debugger::ExecutionObservableSet& obs, |
| Debugger::IsObserving observing) |
| { |
| using namespace js::jit; |
| |
| // See note in js::ReleaseAllJITCode. |
| cx->runtime()->gc.evictNursery(); |
| |
| AutoSuppressProfilerSampling suppressProfilerSampling(cx); |
| |
| JSRuntime* rt = cx->runtime(); |
| FreeOp* fop = cx->runtime()->defaultFreeOp(); |
| |
| // 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:; |
| } |
| } |
| } |
| |
| Vector<JSScript*> scripts(cx); |
| |
| // Iterate through observable scripts, invalidating their Ion scripts and |
| // appending them to a vector for discarding their baseline scripts later. |
| { |
| AutoEnterAnalysis enter(fop, zone); |
| if (JSScript* script = obs.singleScriptForZoneInvalidation()) { |
| if (obs.shouldRecompileOrInvalidate(script)) { |
| if (!AppendAndInvalidateScript(cx, zone, script, scripts)) |
| return false; |
| } |
| } else { |
| for (gc::ZoneCellIter iter(zone, gc::AllocKind::SCRIPT); !iter.done(); iter.next()) { |
| JSScript* script = iter.get<JSScript>(); |
| if (obs.shouldRecompileOrInvalidate(script) && |
| !gc::IsAboutToBeFinalizedUnbarriered(&script)) |
| { |
| if (!AppendAndInvalidateScript(cx, zone, script, scripts)) |
| return false; |
| } |
| } |
| } |
| } |
| |
| // 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; |
| } |
| |
| /* 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; |
| |
| comp->updateDebuggerObservesAllExecution(); |
| } |
| |
| return updateExecutionObservability(cx, obs, observing); |
| } |
| |
| 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, ScriptFrameIter::ALL_CONTEXTS, |
| ScriptFrameIter::GO_THROUGH_SAVED); |
| !iter.done(); |
| ++iter) |
| { |
| if (obs.shouldMarkAsDebuggee(iter)) { |
| JS_ReportErrorNumber(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()->getObjectMetadataCallback(); |
| return existingCallback && existingCallback != SavedStacksMetadataCallback; |
| } |
| |
| /* static */ bool |
| Debugger::isObservedByDebuggerTrackingAllocations(const GlobalObject& debuggee) |
| { |
| if (auto* v = debuggee.getDebuggers()) { |
| Debugger** p; |
| for (p = v->begin(); p != v->end(); p++) { |
| if ((*p)->trackingAllocationSites && (*p)->enabled) { |
| return true; |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| /* static */ bool |
| Debugger::addAllocationsTracking(JSContext* cx, Handle<GlobalObject*> debuggee) |
| { |
| // Precondition: the given global object is being observed by at least one |
| // Debugger that is tracking allocations. |
| MOZ_ASSERT(isObservedByDebuggerTrackingAllocations(*debuggee)); |
| |
| if (Debugger::cannotTrackAllocations(*debuggee)) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, |
| JSMSG_OBJECT_METADATA_CALLBACK_ALREADY_SET); |
| return false; |
| } |
| |
| debuggee->compartment()->setObjectMetadataCallback(SavedStacksMetadataCallback); |
| 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()->forgetObjectMetadataCallback(); |
| } |
| |
| 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_ReportErrorNumber(cx, GetErrorMessage, nullptr, |
| JSMSG_OBJECT_METADATA_CALLBACK_ALREADY_SET); |
| return false; |
| } |
| } |
| |
| Rooted<GlobalObject*> g(cx); |
| for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) { |
| // This should always succeed, since we already checked for the |
| // error case above. |
| g = r.front().get(); |
| MOZ_ALWAYS_TRUE(Debugger::addAllocationsTracking(cx, g)); |
| } |
| |
| return true; |
| } |
| |
| void |
| Debugger::removeAllocationsTrackingForAllDebuggees() |
| { |
| for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) |
| Debugger::removeAllocationsTracking(*r.front().get()); |
| |
| allocationsLog.clear(); |
| } |
| |
| |
| |
| /*** Debugger JSObjects **************************************************************************/ |
| |
| void |
| Debugger::markCrossCompartmentEdges(JSTracer* trc) |
| { |
| objects.markCrossCompartmentEdges<DebuggerObject_trace>(trc); |
| environments.markCrossCompartmentEdges<DebuggerEnv_trace>(trc); |
| scripts.markCrossCompartmentEdges<DebuggerScript_trace>(trc); |
| sources.markCrossCompartmentEdges<DebuggerSource_trace>(trc); |
| |
| // Because we don't have access to a `cx` inside |
| // `Debugger::logTenurePromotion`, we can't hold onto CCWs inside the log, |
| // and instead have unwrapped cross-compartment edges. We need to be sure to |
| // mark those here. |
| TenurePromotionsLog::trace(&tenurePromotionsLog, 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::MARK_ROOTS || state == gc::COMPACT); |
| |
| for (Debugger* dbg : rt->debuggerList) { |
| Zone* zone = dbg->object->zone(); |
| if ((state == gc::MARK_ROOTS && !zone->isCollecting()) || |
| (state == gc::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(&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 (Debugger * const* 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 |
| */ |
| HeapPtrNativeObject& dbgobj = dbg->toJSObjectRef(); |
| if (!dbgobj->zone()->isGCMarking()) |
| continue; |
| |
| bool dbgMarked = IsMarked(&dbgobj); |
| if (!dbgMarked && dbg->hasAnyLiveHooks()) { |
| /* |
| * 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(&bp->site->script)) { |
| /* |
| * The debugger and the script are both live. |
| * Therefore the breakpoint handler is live. |
| */ |
| if (!IsMarked(&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"); |
| |
| HeapPtrNativeObject& dbgobj = dbg->toJSObjectRef(); |
| TraceEdge(trc, &dbgobj, "Debugger Object"); |
| |
| dbg->scripts.trace(trc); |
| dbg->sources.trace(trc); |
| dbg->objects.trace(trc); |
| dbg->environments.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) |
| { |
| if (uncaughtExceptionHook) |
| TraceEdge(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()) { |
| RelocatablePtrNativeObject& frameobj = r.front().value(); |
| MOZ_ASSERT(MaybeForwarded(frameobj.get())->getPrivate()); |
| TraceEdge(trc, &frameobj, "live Debugger.Frame"); |
| } |
| |
| AllocationsLog::trace(&allocationsLog, trc); |
| TenurePromotionsLog::trace(&tenurePromotionsLog, 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); |
| } |
| |
| /* 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::ComponentFinder<Zone>& 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)) |
| { |
| finder.addEdgeTo(w); |
| } |
| } |
| } |
| |
| /* static */ void |
| Debugger::finalize(FreeOp* fop, JSObject* obj) |
| { |
| Debugger* dbg = fromJSObject(obj); |
| if (!dbg) |
| return; |
| fop->delete_(dbg); |
| } |
| |
| const Class Debugger::jsclass = { |
| "Debugger", |
| JSCLASS_HAS_PRIVATE | |
| JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUG_COUNT), |
| nullptr, nullptr, nullptr, nullptr, |
| nullptr, nullptr, nullptr, Debugger::finalize, |
| nullptr, /* call */ |
| nullptr, /* hasInstance */ |
| nullptr, /* construct */ |
| Debugger::traceObject |
| }; |
| |
| /* 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::jsclass) { |
| JS_ReportErrorNumber(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 = fromJSObject(thisobj); |
| if (!dbg) { |
| JS_ReportErrorNumber(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_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_NOT_CALLABLE_OR_UNDEFINED); |
| return false; |
| } |
| dbg.object->setReservedSlot(JSSLOT_DEBUG_HOOK_START + which, args[0]); |
| if (hookObservesAllExecution(which)) { |
| if (!dbg.updateObservesAllExecutionOnDebuggees(cx, dbg.observesAllExecution())) |
| 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) { |
|