/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "debugger/Debugger-inl.h" #include "mozilla/Attributes.h" // for MOZ_STACK_CLASS, MOZ_RAII #include "mozilla/DebugOnly.h" // for DebugOnly #include "mozilla/DoublyLinkedList.h" // for DoublyLinkedList<>::Iterator #include "mozilla/HashTable.h" // for HashSet<>::Range, HashMapEntry #include "mozilla/Maybe.h" // for Maybe, Nothing, Some #include "mozilla/ScopeExit.h" // for MakeScopeExit, ScopeExit #include "mozilla/ThreadLocal.h" // for ThreadLocal #include "mozilla/TimeStamp.h" // for TimeStamp, TimeDuration #include "mozilla/UniquePtr.h" // for UniquePtr #include "mozilla/Variant.h" // for AsVariant, AsVariantTemporary #include "mozilla/Vector.h" // for Vector, Vector<>::ConstRange #include // for std::find, std::max #include // for function #include // for size_t #include // for uint32_t, uint64_t, int32_t #include // for strlen, strcmp #include // for std::underlying_type_t #include // for std::move #include "jsapi.h" // for CallArgs, CallArgsFromVp #include "jstypes.h" // for JS_PUBLIC_API #include "builtin/Array.h" // for NewDenseFullyAllocatedArray #include "debugger/DebugAPI.h" // for ResumeMode, DebugAPI #include "debugger/DebuggerMemory.h" // for DebuggerMemory #include "debugger/DebugScript.h" // for DebugScript #include "debugger/Environment.h" // for DebuggerEnvironment #include "debugger/Frame.h" // for DebuggerFrame #include "debugger/NoExecute.h" // for EnterDebuggeeNoExecute #include "debugger/Object.h" // for DebuggerObject #include "debugger/Script.h" // for DebuggerScript #include "debugger/Source.h" // for DebuggerSource #include "frontend/BytecodeCompiler.h" // for IsIdentifier #include "frontend/CompilationStencil.h" // for CompilationStencil #include "frontend/FrontendContext.h" // for AutoReportFrontendContext #include "frontend/Parser.h" // for Parser #include "gc/GC.h" // for IterateScripts #include "gc/GCContext.h" // for JS::GCContext #include "gc/GCMarker.h" // for GCMarker #include "gc/GCRuntime.h" // for GCRuntime, AutoEnterIteration #include "gc/HashUtil.h" // for DependentAddPtr #include "gc/Marking.h" // for IsAboutToBeFinalized #include "gc/PublicIterators.h" // for RealmsIter, CompartmentsIter #include "gc/Statistics.h" // for Statistics::SliceData #include "gc/Tracer.h" // for TraceEdge #include "gc/Zone.h" // for Zone #include "gc/ZoneAllocator.h" // for ZoneAllocPolicy #include "jit/BaselineDebugModeOSR.h" // for RecompileOnStackBaselineScriptsForDebugMode #include "jit/BaselineJIT.h" // for FinishDiscardBaselineScript #include "jit/Invalidation.h" // for RecompileInfoVector #include "jit/JitContext.h" // for JitContext #include "jit/JitOptions.h" // for fuzzingSafe #include "jit/JitScript.h" // for JitScript #include "jit/JSJitFrameIter.h" // for InlineFrameIterator #include "jit/RematerializedFrame.h" // for RematerializedFrame #include "js/CallAndConstruct.h" // JS::IsCallable #include "js/Conversions.h" // for ToBoolean, ToUint32 #include "js/Debug.h" // for Builder::Object, Builder #include "js/friend/ErrorMessages.h" // for GetErrorMessage, JSMSG_* #include "js/GCAPI.h" // for GarbageCollectionEvent #include "js/GCVariant.h" // for GCVariant #include "js/HeapAPI.h" // for ExposeObjectToActiveJS #include "js/Promise.h" // for AutoDebuggerJobQueueInterruption #include "js/PropertyAndElement.h" // for JS_GetProperty #include "js/Proxy.h" // for PropertyDescriptor #include "js/SourceText.h" // for SourceOwnership, SourceText #include "js/StableStringChars.h" // for AutoStableStringChars #include "js/UbiNode.h" // for Node, RootList, Edge #include "js/UbiNodeBreadthFirst.h" // for BreadthFirst #include "js/Wrapper.h" // for CheckedUnwrapStatic #include "util/Text.h" // for DuplicateString, js_strlen #include "vm/ArrayObject.h" // for ArrayObject #include "vm/AsyncFunction.h" // for AsyncFunctionGeneratorObject #include "vm/AsyncIteration.h" // for AsyncGeneratorObject #include "vm/BytecodeUtil.h" // for JSDVG_IGNORE_STACK #include "vm/Compartment.h" // for CrossCompartmentKey #include "vm/EnvironmentObject.h" // for IsSyntacticEnvironment #include "vm/ErrorReporting.h" // for ReportErrorToGlobal #include "vm/GeneratorObject.h" // for AbstractGeneratorObject #include "vm/GlobalObject.h" // for GlobalObject #include "vm/Interpreter.h" // for Call, ReportIsNotFunction #include "vm/Iteration.h" // for CreateIterResultObject #include "vm/JSAtom.h" // for Atomize, ClassName #include "vm/JSContext.h" // for JSContext #include "vm/JSFunction.h" // for JSFunction #include "vm/JSObject.h" // for JSObject, RequireObject, #include "vm/JSScript.h" // for BaseScript, ScriptSourceObject #include "vm/ObjectOperations.h" // for DefineDataProperty #include "vm/PlainObject.h" // for js::PlainObject #include "vm/PromiseObject.h" // for js::PromiseObject #include "vm/ProxyObject.h" // for ProxyObject, JSObject::is #include "vm/Realm.h" // for AutoRealm, Realm #include "vm/Runtime.h" // for ReportOutOfMemory, JSRuntime #include "vm/SavedFrame.h" // for SavedFrame #include "vm/SavedStacks.h" // for SavedStacks #include "vm/Scope.h" // for Scope #include "vm/StringType.h" // for JSString, PropertyName #include "vm/WrapperObject.h" // for CrossCompartmentWrapperObject #include "wasm/WasmDebug.h" // for DebugState #include "wasm/WasmInstance.h" // for Instance #include "wasm/WasmJS.h" // for WasmInstanceObject #include "wasm/WasmRealm.h" // for Realm #include "wasm/WasmTypeDecls.h" // for WasmInstanceObjectVector #include "debugger/DebugAPI-inl.h" #include "debugger/Environment-inl.h" // for DebuggerEnvironment::owner #include "debugger/Frame-inl.h" // for DebuggerFrame::hasGeneratorInfo #include "debugger/Object-inl.h" // for DebuggerObject::owner and isInstance. #include "debugger/Script-inl.h" // for DebuggerScript::getReferent #include "gc/GC-inl.h" // for ZoneCellIter #include "gc/Marking-inl.h" // for MaybeForwarded #include "gc/StableCellHasher-inl.h" #include "gc/WeakMap-inl.h" // for DebuggerWeakMap::trace #include "vm/Compartment-inl.h" // for Compartment::wrap #include "vm/GeckoProfiler-inl.h" // for AutoSuppressProfilerSampling #include "vm/JSAtom-inl.h" // for AtomToId, ValueToId #include "vm/JSContext-inl.h" // for JSContext::check #include "vm/JSObject-inl.h" // for JSObject::isCallable, NewTenuredObjectWithGivenProto #include "vm/JSScript-inl.h" // for JSScript::isDebuggee, JSScript #include "vm/NativeObject-inl.h" // for NativeObject::ensureDenseInitializedLength #include "vm/ObjectOperations-inl.h" // for GetProperty, HasProperty #include "vm/Realm-inl.h" // for AutoRealm::AutoRealm #include "vm/Stack-inl.h" // for AbstractFramePtr::script namespace js { namespace frontend { class FullParseHandler; } namespace gc { struct Cell; } namespace jit { class BaselineFrame; } } /* namespace js */ using namespace js; using JS::AutoStableStringChars; using JS::CompileOptions; using JS::SourceOwnership; using JS::SourceText; using JS::dbg::AutoEntryMonitor; using JS::dbg::Builder; using js::frontend::IsIdentifier; using mozilla::AsVariant; using mozilla::DebugOnly; using mozilla::MakeScopeExit; using mozilla::Maybe; using mozilla::Nothing; using mozilla::Some; using mozilla::TimeDuration; using mozilla::TimeStamp; /*** Utils ******************************************************************/ bool js::IsInterpretedNonSelfHostedFunction(JSFunction* fun) { return fun->isInterpreted() && !fun->isSelfHostedBuiltin(); } JSScript* js::GetOrCreateFunctionScript(JSContext* cx, HandleFunction fun) { MOZ_ASSERT(IsInterpretedNonSelfHostedFunction(fun)); AutoRealm ar(cx, fun); return JSFunction::getOrCreateScript(cx, fun); } ArrayObject* js::GetFunctionParameterNamesArray(JSContext* cx, HandleFunction fun) { RootedValueVector names(cx); // The default value for each argument is |undefined|. if (!names.growBy(fun->nargs())) { return nullptr; } if (IsInterpretedNonSelfHostedFunction(fun) && fun->nargs() > 0) { RootedScript script(cx, GetOrCreateFunctionScript(cx, fun)); if (!script) { return nullptr; } MOZ_ASSERT(fun->nargs() == script->numArgs()); PositionalFormalParameterIter fi(script); for (size_t i = 0; i < fun->nargs(); i++, fi++) { MOZ_ASSERT(fi.argumentSlot() == i); if (JSAtom* atom = fi.name()) { // Skip any internal, non-identifier names, like for example ".args". if (IsIdentifier(atom)) { cx->markAtom(atom); names[i].setString(atom); } } } } return NewDenseCopiedArray(cx, names.length(), names.begin()); } bool js::ValueToIdentifier(JSContext* cx, HandleValue v, MutableHandleId id) { if (!ToPropertyKey(cx, v, id)) { return false; } if (!id.isAtom() || !IsIdentifier(id.toAtom())) { RootedValue val(cx, v); ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, val, nullptr, "not an identifier"); return false; } return true; } class js::AutoRestoreRealmDebugMode { Realm* realm_; unsigned bits_; public: explicit AutoRestoreRealmDebugMode(Realm* realm) : realm_(realm), bits_(realm->debugModeBits_) { MOZ_ASSERT(realm_); } ~AutoRestoreRealmDebugMode() { if (realm_) { realm_->debugModeBits_ = bits_; } } void release() { realm_ = nullptr; } }; /* static */ bool DebugAPI::slowPathCheckNoExecute(JSContext* cx, HandleScript script) { MOZ_ASSERT(cx->realm()->isDebuggee()); MOZ_ASSERT(cx->noExecuteDebuggerTop); return EnterDebuggeeNoExecute::reportIfFoundInStack(cx, script); } static void PropagateForcedReturn(JSContext* cx, AbstractFramePtr frame, HandleValue rval) { // The Debugger's hooks may return a value that affects the completion // value of the given frame. For example, a hook may return `{ return: 42 }` // to terminate the frame and return `42` as the final frame result. // To accomplish this, the debugger treats these return values as if // execution of the JS function has been terminated without a pending // exception, but with a special flag. When the error is handled by the // interpreter or JIT, the special flag and the error state will be cleared // and execution will continue from the end of the frame. MOZ_ASSERT(!cx->isExceptionPending()); cx->setPropagatingForcedReturn(); frame.setReturnValue(rval); } [[nodiscard]] static bool AdjustGeneratorResumptionValue(JSContext* cx, AbstractFramePtr frame, ResumeMode& resumeMode, MutableHandleValue vp); [[nodiscard]] static bool ApplyFrameResumeMode(JSContext* cx, AbstractFramePtr frame, ResumeMode resumeMode, HandleValue rv, Handle exnStack) { RootedValue rval(cx, rv); // The value passed in here is unwrapped and has no guarantees about what // compartment it may be associated with, so we explicitly wrap it into the // debuggee compartment. if (!cx->compartment()->wrap(cx, &rval)) { return false; } if (!AdjustGeneratorResumptionValue(cx, frame, resumeMode, &rval)) { return false; } switch (resumeMode) { case ResumeMode::Continue: break; case ResumeMode::Throw: // If we have a stack from the original throw, use it instead of // associating the throw with the current execution point. if (exnStack) { cx->setPendingException(rval, exnStack); } else { cx->setPendingException(rval, ShouldCaptureStack::Always); } return false; case ResumeMode::Terminate: cx->clearPendingException(); return false; case ResumeMode::Return: PropagateForcedReturn(cx, frame, rval); return false; default: MOZ_CRASH("bad Debugger::onEnterFrame resume mode"); } return true; } static bool ApplyFrameResumeMode(JSContext* cx, AbstractFramePtr frame, ResumeMode resumeMode, HandleValue rval) { Rooted nullStack(cx); return ApplyFrameResumeMode(cx, frame, resumeMode, rval, nullStack); } bool js::ValueToStableChars(JSContext* cx, const char* fnname, HandleValue value, AutoStableStringChars& stableChars) { if (!value.isString()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE, fnname, "string", InformalValueTypeName(value)); return false; } Rooted linear(cx, value.toString()->ensureLinear(cx)); if (!linear) { return false; } if (!stableChars.initTwoByte(cx, linear)) { return false; } return true; } bool EvalOptions::setFilename(JSContext* cx, const char* filename) { JS::UniqueChars copy; if (filename) { copy = DuplicateString(cx, filename); if (!copy) { return false; } } filename_ = std::move(copy); return true; } bool js::ParseEvalOptions(JSContext* cx, HandleValue value, EvalOptions& options) { if (!value.isObject()) { return true; } RootedObject opts(cx, &value.toObject()); RootedValue v(cx); if (!JS_GetProperty(cx, opts, "url", &v)) { return false; } if (!v.isUndefined()) { RootedString url_str(cx, ToString(cx, v)); if (!url_str) { return false; } UniqueChars url_bytes = JS_EncodeStringToUTF8(cx, url_str); if (!url_bytes) { return false; } if (!options.setFilename(cx, url_bytes.get())) { return false; } } if (!JS_GetProperty(cx, opts, "lineNumber", &v)) { return false; } if (!v.isUndefined()) { uint32_t lineno; if (!ToUint32(cx, v, &lineno)) { return false; } options.setLineno(lineno); } if (!JS_GetProperty(cx, opts, "hideFromDebugger", &v)) { return false; } options.setHideFromDebugger(ToBoolean(v)); return true; } /*** Breakpoints ************************************************************/ bool BreakpointSite::isEmpty() const { return breakpoints.isEmpty(); } void BreakpointSite::trace(JSTracer* trc) { for (auto p = breakpoints.begin(); p; p++) { p->trace(trc); } } void BreakpointSite::finalize(JS::GCContext* gcx) { while (!breakpoints.isEmpty()) { breakpoints.begin()->delete_(gcx); } } Breakpoint* BreakpointSite::firstBreakpoint() const { if (isEmpty()) { return nullptr; } return &(*breakpoints.begin()); } bool BreakpointSite::hasBreakpoint(Breakpoint* toFind) { const BreakpointList::Iterator bp(toFind); for (auto p = breakpoints.begin(); p; p++) { if (p == bp) { return true; } } return false; } Breakpoint::Breakpoint(Debugger* debugger, HandleObject wrappedDebugger, BreakpointSite* site, HandleObject handler) : debugger(debugger), wrappedDebugger(wrappedDebugger), site(site), handler(handler) { MOZ_ASSERT(UncheckedUnwrap(wrappedDebugger) == debugger->object); MOZ_ASSERT(handler->compartment() == wrappedDebugger->compartment()); debugger->breakpoints.pushBack(this); site->breakpoints.pushBack(this); } void Breakpoint::trace(JSTracer* trc) { TraceEdge(trc, &wrappedDebugger, "breakpoint owner"); TraceEdge(trc, &handler, "breakpoint handler"); } void Breakpoint::delete_(JS::GCContext* gcx) { debugger->breakpoints.remove(this); site->breakpoints.remove(this); gc::Cell* cell = site->owningCell(); gcx->delete_(cell, this, MemoryUse::Breakpoint); } void Breakpoint::remove(JS::GCContext* gcx) { BreakpointSite* savedSite = site; delete_(gcx); savedSite->destroyIfEmpty(gcx); } Breakpoint* Breakpoint::nextInDebugger() { return debuggerLink.mNext; } Breakpoint* Breakpoint::nextInSite() { return siteLink.mNext; } JSBreakpointSite::JSBreakpointSite(JSScript* script, jsbytecode* pc) : script(script), pc(pc) { MOZ_ASSERT(!DebugAPI::hasBreakpointsAt(script, pc)); } void JSBreakpointSite::remove(JS::GCContext* gcx) { DebugScript::destroyBreakpointSite(gcx, script, pc); } void JSBreakpointSite::trace(JSTracer* trc) { BreakpointSite::trace(trc); TraceEdge(trc, &script, "breakpoint script"); } void JSBreakpointSite::delete_(JS::GCContext* gcx) { BreakpointSite::finalize(gcx); gcx->delete_(script, this, MemoryUse::BreakpointSite); } gc::Cell* JSBreakpointSite::owningCell() { return script; } Realm* JSBreakpointSite::realm() const { return script->realm(); } WasmBreakpointSite::WasmBreakpointSite(WasmInstanceObject* instanceObject_, uint32_t offset_) : instanceObject(instanceObject_), offset(offset_) { MOZ_ASSERT(instanceObject_); MOZ_ASSERT(instanceObject_->instance().debugEnabled()); } void WasmBreakpointSite::trace(JSTracer* trc) { BreakpointSite::trace(trc); TraceEdge(trc, &instanceObject, "breakpoint Wasm instance"); } void WasmBreakpointSite::remove(JS::GCContext* gcx) { instanceObject->instance().destroyBreakpointSite(gcx, offset); } void WasmBreakpointSite::delete_(JS::GCContext* gcx) { BreakpointSite::finalize(gcx); gcx->delete_(instanceObject, this, MemoryUse::BreakpointSite); } gc::Cell* WasmBreakpointSite::owningCell() { return instanceObject; } Realm* WasmBreakpointSite::realm() const { return instanceObject->realm(); } /*** Debugger hook dispatch *************************************************/ Debugger::Debugger(JSContext* cx, NativeObject* dbg) : object(dbg), debuggees(cx->zone()), uncaughtExceptionHook(nullptr), allowUnobservedAsmJS(false), allowUnobservedWasm(false), collectCoverageInfo(false), observedGCs(cx->zone()), allocationsLog(cx), trackingAllocationSites(false), allocationSamplingProbability(1.0), maxAllocationsLogLength(DEFAULT_MAX_LOG_LENGTH), allocationsLogOverflowed(false), frames(cx->zone()), generatorFrames(cx), scripts(cx), sources(cx), objects(cx), environments(cx), wasmInstanceScripts(cx), wasmInstanceSources(cx) { cx->check(dbg); cx->runtime()->debuggerList().insertBack(this); } template static void RemoveDebuggerEntry( mozilla::DoublyLinkedList& list, Debugger* dbg) { // The "probably" here is because there could technically be multiple lists // with this type signature and theoretically the debugger could be an entry // in a different one. That is not actually possible however because there // is only one list the debugger could be in. if (list.ElementProbablyInList(dbg)) { list.remove(dbg); } } Debugger::~Debugger() { MOZ_ASSERT(debuggees.empty()); allocationsLog.clear(); // Breakpoints should hold us alive, so any breakpoints remaining must be set // in dying JSScripts. We should clean them up, but this never asserts. I'm // not sure why. MOZ_ASSERT(breakpoints.isEmpty()); // We don't have to worry about locking here since Debugger is not // background finalized. JSContext* cx = TlsContext.get(); RemoveDebuggerEntry(cx->runtime()->onNewGlobalObjectWatchers(), this); RemoveDebuggerEntry(cx->runtime()->onGarbageCollectionWatchers(), this); } #ifdef DEBUG /* static */ bool Debugger::isChildJSObject(JSObject* obj) { return obj->getClass() == &DebuggerFrame::class_ || obj->getClass() == &DebuggerScript::class_ || obj->getClass() == &DebuggerSource::class_ || obj->getClass() == &DebuggerObject::class_ || obj->getClass() == &DebuggerEnvironment::class_; } #endif bool Debugger::hasMemory() const { return object->getReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE).isObject(); } DebuggerMemory& Debugger::memory() const { MOZ_ASSERT(hasMemory()); return object->getReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE) .toObject() .as(); } /*** Debugger accessors *******************************************************/ bool Debugger::getFrame(JSContext* cx, const FrameIter& iter, MutableHandleValue vp) { Rooted result(cx); if (!Debugger::getFrame(cx, iter, &result)) { return false; } vp.setObject(*result); return true; } bool Debugger::getFrame(JSContext* cx, MutableHandle result) { RootedObject proto( cx, &object->getReservedSlot(JSSLOT_DEBUG_FRAME_PROTO).toObject()); Rooted debugger(cx, object); // Since there is no frame/generator data to associate with this frame, this // will create a new, "terminated" Debugger.Frame object. Rooted frame( cx, DebuggerFrame::create(cx, proto, debugger, nullptr, nullptr)); if (!frame) { return false; } result.set(frame); return true; } bool Debugger::getFrame(JSContext* cx, const FrameIter& iter, MutableHandle result) { AbstractFramePtr referent = iter.abstractFramePtr(); MOZ_ASSERT_IF(referent.hasScript(), !referent.script()->selfHosted()); FrameMap::AddPtr p = frames.lookupForAdd(referent); if (!p) { Rooted genObj(cx); if (referent.isGeneratorFrame()) { if (referent.isFunctionFrame()) { AutoRealm ar(cx, referent.callee()); genObj = GetGeneratorObjectForFrame(cx, referent); } else { MOZ_ASSERT(referent.isModuleFrame()); AutoRealm ar(cx, referent.script()->module()); genObj = GetGeneratorObjectForFrame(cx, referent); } // If this frame has a generator associated with it, but no on-stack // Debugger.Frame object was found, there should not be a suspended // Debugger.Frame either because otherwise slowPathOnResumeFrame would // have already populated the "frames" map with a Debugger.Frame. MOZ_ASSERT_IF(genObj, !generatorFrames.has(genObj)); // If the frame's generator is closed, there is no way to associate the // generator with the frame successfully because there is no way to // get the generator's callee script, and even if we could, having it // there would in no way affect the behavior of the frame. if (genObj && genObj->isClosed()) { genObj = nullptr; } // If no AbstractGeneratorObject exists yet, we create a Debugger.Frame // below anyway, and Debugger::onNewGenerator() will associate it // with the AbstractGeneratorObject later when we hit JSOp::Generator. } // Create and populate the Debugger.Frame object. RootedObject proto( cx, &object->getReservedSlot(JSSLOT_DEBUG_FRAME_PROTO).toObject()); Rooted debugger(cx, object); Rooted frame( cx, DebuggerFrame::create(cx, proto, debugger, &iter, genObj)); if (!frame) { return false; } auto terminateDebuggerFrameGuard = MakeScopeExit([&] { terminateDebuggerFrame(cx->gcContext(), this, frame, referent); }); if (genObj) { DependentAddPtr genPtr(cx, generatorFrames, genObj); if (!genPtr.add(cx, generatorFrames, genObj, frame)) { return false; } } if (!ensureExecutionObservabilityOfFrame(cx, referent)) { return false; } if (!frames.add(p, referent, frame)) { ReportOutOfMemory(cx); return false; } terminateDebuggerFrameGuard.release(); } result.set(p->value()); return true; } bool Debugger::getFrame(JSContext* cx, Handle genObj, MutableHandle result) { // To create a Debugger.Frame for a running generator, we'd also need a // FrameIter for its stack frame. We could make this work by searching the // stack for the generator's frame, but for the moment, we only need this // function to handle generators we've found on promises' reaction records, // which should always be suspended. MOZ_ASSERT(genObj->isSuspended()); // Do we have an existing Debugger.Frame for this generator? DependentAddPtr p(cx, generatorFrames, genObj); if (p) { MOZ_ASSERT(&p->value()->unwrappedGenerator() == genObj); result.set(p->value()); return true; } // Create a new Debugger.Frame. RootedObject proto( cx, &object->getReservedSlot(JSSLOT_DEBUG_FRAME_PROTO).toObject()); Rooted debugger(cx, object); result.set(DebuggerFrame::create(cx, proto, debugger, nullptr, genObj)); if (!result) { return false; } if (!p.add(cx, generatorFrames, genObj, result)) { terminateDebuggerFrame(cx->gcContext(), this, result, NullFramePtr()); return false; } return true; } static bool DebuggerExists( GlobalObject* global, const std::function& predicate) { // The GC analysis can't determine that the predicate can't GC, so let it know // explicitly. JS::AutoSuppressGCAnalysis nogc; for (Realm::DebuggerVectorEntry& entry : global->getDebuggers(nogc)) { // Callbacks should not create new references to the debugger, so don't // use a barrier. This allows this method to be called during GC. if (predicate(entry.dbg.unbarrieredGet())) { return true; } } return false; } /* static */ bool Debugger::hasLiveHook(GlobalObject* global, Hook which) { return DebuggerExists(global, [=](Debugger* dbg) { return dbg->getHook(which); }); } /* static */ bool DebugAPI::debuggerObservesAllExecution(GlobalObject* global) { return DebuggerExists( global, [=](Debugger* dbg) { return dbg->observesAllExecution(); }); } /* static */ bool DebugAPI::debuggerObservesCoverage(GlobalObject* global) { return DebuggerExists(global, [=](Debugger* dbg) { return dbg->observesCoverage(); }); } /* static */ bool DebugAPI::debuggerObservesAsmJS(GlobalObject* global) { return DebuggerExists(global, [=](Debugger* dbg) { return dbg->observesAsmJS(); }); } /* static */ bool DebugAPI::debuggerObservesWasm(GlobalObject* global) { return DebuggerExists(global, [=](Debugger* dbg) { return dbg->observesWasm(); }); } /* static */ bool DebugAPI::hasExceptionUnwindHook(GlobalObject* global) { return Debugger::hasLiveHook(global, Debugger::OnExceptionUnwind); } /* static */ bool DebugAPI::hasDebuggerStatementHook(GlobalObject* global) { return Debugger::hasLiveHook(global, Debugger::OnDebuggerStatement); } template bool DebuggerList::init(JSContext* cx) { // Determine which debuggers will receive this event, and in what order. // Make a copy of the list, since the original is mutable and we will be // calling into arbitrary JS. Handle global = cx->global(); JS::AutoAssertNoGC nogc; for (Realm::DebuggerVectorEntry& entry : global->getDebuggers(nogc)) { Debugger* dbg = entry.dbg; if (dbg->isHookCallAllowed(cx) && hookIsEnabled(dbg)) { if (!debuggers.append(ObjectValue(*dbg->toJSObject()))) { return false; } } } return true; } template template bool DebuggerList::dispatchHook(JSContext* cx, FireHookFun fireHook) { // Preserve the debuggee's microtask event queue while we run the hooks, so // the debugger's microtask checkpoints don't run from the debuggee's // microtasks, and vice versa. JS::AutoDebuggerJobQueueInterruption adjqi; if (!adjqi.init(cx)) { return false; } // Deliver the event to each debugger, checking again to make sure it // should still be delivered. Handle global = cx->global(); for (Value* p = debuggers.begin(); p != debuggers.end(); p++) { Debugger* dbg = Debugger::fromJSObject(&p->toObject()); EnterDebuggeeNoExecute nx(cx, *dbg, adjqi); if (dbg->debuggees.has(global) && hookIsEnabled(dbg)) { bool result = dbg->enterDebuggerHook(cx, [&]() -> bool { return fireHook(dbg); }); adjqi.runJobs(); if (!result) { return false; } } } return true; } template template void DebuggerList::dispatchQuietHook(JSContext* cx, FireHookFun fireHook) { bool result = dispatchHook(cx, [&](Debugger* dbg) -> bool { return fireHook(dbg); }); // dispatchHook may fail due to OOM. This OOM is not handlable at the // callsites of dispatchQuietHook in the engine. if (!result) { cx->clearPendingException(); } } template template bool DebuggerList::dispatchResumptionHook( JSContext* cx, AbstractFramePtr frame, FireHookFun fireHook) { ResumeMode resumeMode = ResumeMode::Continue; RootedValue rval(cx); return dispatchHook(cx, [&](Debugger* dbg) -> bool { return fireHook(dbg, resumeMode, &rval); }) && ApplyFrameResumeMode(cx, frame, resumeMode, rval); } JSObject* Debugger::getHook(Hook hook) const { MOZ_ASSERT(hook >= 0 && hook < HookCount); const Value& v = object->getReservedSlot(JSSLOT_DEBUG_HOOK_START + std::underlying_type_t(hook)); return v.isUndefined() ? nullptr : &v.toObject(); } bool Debugger::hasAnyLiveHooks() const { // A onNewGlobalObject hook does not hold its Debugger live, so its behavior // is nondeterministic. This behavior is not satisfying, but it is at least // documented. if (getHook(OnDebuggerStatement) || getHook(OnExceptionUnwind) || getHook(OnNewScript) || getHook(OnEnterFrame)) { return true; } return false; } /* static */ bool DebugAPI::slowPathOnEnterFrame(JSContext* cx, AbstractFramePtr frame) { return Debugger::dispatchResumptionHook( cx, frame, [frame](Debugger* dbg) -> bool { return dbg->observesFrame(frame) && dbg->observesEnterFrame(); }, [&](Debugger* dbg, ResumeMode& resumeMode, MutableHandleValue vp) -> bool { return dbg->fireEnterFrame(cx, resumeMode, vp); }); } /* static */ bool DebugAPI::slowPathOnResumeFrame(JSContext* cx, AbstractFramePtr frame) { // Don't count on this method to be called every time a generator is // resumed! This is called only if the frame's debuggee bit is set, // i.e. the script has breakpoints or the frame is stepping. MOZ_ASSERT(frame.isGeneratorFrame()); MOZ_ASSERT(frame.isDebuggee()); Rooted genObj( cx, GetGeneratorObjectForFrame(cx, frame)); MOZ_ASSERT(genObj); // If there is an OOM, we mark all of the Debugger.Frame objects terminated // because we want to ensure that none of the frames are in a partially // initialized state where they are in "generatorFrames" but not "frames". auto terminateDebuggerFramesGuard = MakeScopeExit([&] { Debugger::terminateDebuggerFrames(cx, frame); MOZ_ASSERT(!DebugAPI::inFrameMaps(frame)); }); // For each debugger, if there is an existing Debugger.Frame object for the // resumed `frame`, update it with the new frame pointer and make sure the // frame is observable. FrameIter iter(cx); MOZ_ASSERT(iter.abstractFramePtr() == frame); { JS::AutoAssertNoGC nogc; for (Realm::DebuggerVectorEntry& entry : frame.global()->getDebuggers(nogc)) { Debugger* dbg = entry.dbg; if (Debugger::GeneratorWeakMap::Ptr generatorEntry = dbg->generatorFrames.lookup(genObj)) { DebuggerFrame* frameObj = generatorEntry->value(); MOZ_ASSERT(&frameObj->unwrappedGenerator() == genObj); if (!dbg->frames.putNew(frame, frameObj)) { ReportOutOfMemory(cx); return false; } if (!frameObj->resume(iter)) { return false; } } } } terminateDebuggerFramesGuard.release(); return slowPathOnEnterFrame(cx, frame); } /* static */ NativeResumeMode DebugAPI::slowPathOnNativeCall(JSContext* cx, const CallArgs& args, CallReason reason) { // "onNativeCall" only works consistently in the context of an explicit eval // (or a function call via DebuggerObject.call/apply) that has set the // "insideDebuggerEvaluationWithOnNativeCallHook" state // on the JSContext, so we fast-path this hook to bail right away if that is // not currently set. If this flag is set to a _different_ debugger, the // standard "isHookCallAllowed" debugger logic will apply and only hooks on // that debugger will be callable. if (!cx->insideDebuggerEvaluationWithOnNativeCallHook) { return NativeResumeMode::Continue; } DebuggerList debuggerList(cx, [](Debugger* dbg) -> bool { return dbg->getHook(Debugger::OnNativeCall); }); if (!debuggerList.init(cx)) { return NativeResumeMode::Abort; } if (debuggerList.empty()) { return NativeResumeMode::Continue; } // The onNativeCall hook is fired when self hosted functions are called, // and any other self hosted function or C++ native that is directly called // by the self hosted function is considered to be part of the same // native call, except for the following 4 cases: // // * callContentFunction and constructContentFunction, // which uses CallReason::CallContent // * Function.prototype.call and Function.prototype.apply, // which uses CallReason::FunCall // * Getter call which uses CallReason::Getter // * Setter call which uses CallReason::Setter // // We check this only after checking that debuggerList has items in order // to avoid unnecessary calls to cx->currentScript(), which can be expensive // when the top frame is in jitcode. JSScript* script = cx->currentScript(); if (script && script->selfHosted() && reason != CallReason::CallContent && reason != CallReason::FunCall && reason != CallReason::Getter && reason != CallReason::Setter) { return NativeResumeMode::Continue; } RootedValue rval(cx); ResumeMode resumeMode = ResumeMode::Continue; bool result = debuggerList.dispatchHook(cx, [&](Debugger* dbg) -> bool { return dbg->fireNativeCall(cx, args, reason, resumeMode, &rval); }); if (!result) { return NativeResumeMode::Abort; } // Hook must follow normal native function conventions and not return // primitive values. if (resumeMode == ResumeMode::Return) { if (args.isConstructing() && !rval.isObject()) { JS_ReportErrorASCII( cx, "onNativeCall hook must return an object for constructor call"); return NativeResumeMode::Abort; } } // The value is not in any particular compartment, so it needs to be // explicitly wrapped into the debuggee compartment. if (!cx->compartment()->wrap(cx, &rval)) { return NativeResumeMode::Abort; } switch (resumeMode) { case ResumeMode::Continue: break; case ResumeMode::Throw: cx->setPendingException(rval, ShouldCaptureStack::Always); return NativeResumeMode::Abort; case ResumeMode::Terminate: cx->clearPendingException(); return NativeResumeMode::Abort; case ResumeMode::Return: args.rval().set(rval); return NativeResumeMode::Override; } return NativeResumeMode::Continue; } /* * RAII class to mark a generator as "running" temporarily while running * debugger code. * * When Debugger::slowPathOnLeaveFrame is called for a frame that is yielding * or awaiting, its generator is in the "suspended" state. Letting script * observe this state, with the generator on stack yet also reenterable, would * be bad, so we mark it running while we fire events. */ class MOZ_RAII AutoSetGeneratorRunning { int32_t resumeIndex_; AsyncGeneratorObject::State asyncGenState_; Rooted genObj_; public: AutoSetGeneratorRunning(JSContext* cx, Handle genObj) : resumeIndex_(0), asyncGenState_(static_cast(0)), genObj_(cx, genObj) { if (genObj) { if (!genObj->isClosed() && !genObj->isBeforeInitialYield() && genObj->isSuspended()) { // Yielding or awaiting. resumeIndex_ = genObj->resumeIndex(); genObj->setRunning(); // Async generators have additionally bookkeeping which must be // adjusted when switching over to the running state. if (genObj->is()) { auto* generator = &genObj->as(); asyncGenState_ = generator->state(); generator->setExecuting(); } } else { // Returning or throwing. The generator is already closed, if // it was ever exposed at all. genObj_ = nullptr; } } } ~AutoSetGeneratorRunning() { if (genObj_) { MOZ_ASSERT(genObj_->isRunning()); genObj_->setResumeIndex(resumeIndex_); if (genObj_->is()) { genObj_->as().setState(asyncGenState_); } } } }; /* * Handle leaving a frame with debuggers watching. |frameOk| indicates whether * the frame is exiting normally or abruptly. Set |cx|'s exception and/or * |cx->fp()|'s return value, and return a new success value. */ /* static */ bool DebugAPI::slowPathOnLeaveFrame(JSContext* cx, AbstractFramePtr frame, const jsbytecode* pc, bool frameOk) { MOZ_ASSERT_IF(!frame.isWasmDebugFrame(), pc); mozilla::DebugOnly> debuggeeGlobal = cx->global(); // These are updated below, but consulted by the cleanup code we register now, // so declare them here, initialized to quiescent values. Rooted completion(cx); bool success = false; auto frameMapsGuard = MakeScopeExit([&] { // Clean up all Debugger.Frame instances on exit. On suspending, pass the // flag that says to leave those frames `.live`. Note that if the completion // is a suspension but success is false, the generator gets closed, not // suspended. if (success && completion.get().suspending()) { Debugger::suspendGeneratorDebuggerFrames(cx, frame); } else { Debugger::terminateDebuggerFrames(cx, frame); } }); // The onPop handler and associated clean up logic should not run multiple // times on the same frame. If slowPathOnLeaveFrame has already been // called, the frame will not be present in the Debugger frame maps. Rooted frames(cx); if (!Debugger::getDebuggerFrames(frame, &frames)) { // There is at least one match Debugger.Frame we failed to process, so drop // the pending exception and raise an out-of-memory instead. if (!frameOk) { cx->clearPendingException(); } ReportOutOfMemory(cx); return false; } if (frames.empty()) { return frameOk; } // Convert current exception state into a Completion and clear exception off // of the JSContext. completion = Completion::fromJSFramePop(cx, frame, pc, frameOk); ResumeMode resumeMode = ResumeMode::Continue; RootedValue rval(cx); { // Preserve the debuggee's microtask event queue while we run the hooks, so // the debugger's microtask checkpoints don't run from the debuggee's // microtasks, and vice versa. JS::AutoDebuggerJobQueueInterruption adjqi; if (!adjqi.init(cx)) { return false; } // This path can be hit via unwinding the stack due to over-recursion or // OOM. In those cases, don't fire the frames' onPop handlers, because // invoking JS will only trigger the same condition. See // slowPathOnExceptionUnwind. if (!cx->isThrowingOverRecursed() && !cx->isThrowingOutOfMemory()) { Rooted genObj( cx, frame.isGeneratorFrame() ? GetGeneratorObjectForFrame(cx, frame) : nullptr); // For each Debugger.Frame, fire its onPop handler, if any. for (size_t i = 0; i < frames.length(); i++) { Handle frameobj = frames[i]; Debugger* dbg = frameobj->owner(); EnterDebuggeeNoExecute nx(cx, *dbg, adjqi); // Removing a global from a Debugger's debuggee set kills all of that // Debugger's D.Fs in that global. This means that one D.F's onPop can // kill the next D.F. So we have to check whether frameobj is still "on // the stack". if (frameobj->isOnStack() && frameobj->onPopHandler()) { OnPopHandler* handler = frameobj->onPopHandler(); bool result = dbg->enterDebuggerHook(cx, [&]() -> bool { ResumeMode nextResumeMode = ResumeMode::Continue; RootedValue nextValue(cx); // Call the onPop handler. bool success; { // Mark the generator as running, to prevent reentrance. // // At certain points in a generator's lifetime, // GetGeneratorObjectForFrame can return null even when the // generator exists, but at those points the generator has not yet // been exposed to JavaScript, so reentrance isn't possible // anyway. So there's no harm done if this has no effect in that // case. AutoSetGeneratorRunning asgr(cx, genObj); success = handler->onPop(cx, frameobj, completion, nextResumeMode, &nextValue); } return dbg->processParsedHandlerResult(cx, frame, pc, success, nextResumeMode, nextValue, resumeMode, &rval); }); adjqi.runJobs(); if (!result) { return false; } // At this point, we are back in the debuggee compartment, and // any error has been wrapped up as a completion value. MOZ_ASSERT(!cx->isExceptionPending()); } } } } completion.get().updateFromHookResult(resumeMode, rval); // Now that we've run all the handlers, extract the final resumption mode. */ ResumeMode completionResumeMode; RootedValue completionValue(cx); Rooted completionStack(cx); completion.get().toResumeMode(completionResumeMode, &completionValue, &completionStack); // If we are returning the original value used to create the completion, then // we don't want to treat the resumption value as a Return completion, because // that would cause us to apply AdjustGeneratorResumptionValue to the // already-adjusted value that the generator actually returned. if (resumeMode == ResumeMode::Continue && completionResumeMode == ResumeMode::Return) { completionResumeMode = ResumeMode::Continue; } if (!ApplyFrameResumeMode(cx, frame, completionResumeMode, completionValue, completionStack)) { if (!cx->isPropagatingForcedReturn()) { // If this is an exception or termination, we just propagate that along. return false; } // Since we are leaving the frame here, we can convert a forced return // into a normal return right away. cx->clearPropagatingForcedReturn(); } success = true; return true; } /* static */ bool DebugAPI::slowPathOnNewGenerator(JSContext* cx, AbstractFramePtr frame, Handle genObj) { // This is called from JSOp::Generator, after default parameter expressions // are evaluated and well after onEnterFrame, so Debugger.Frame objects for // `frame` may already have been exposed to debugger code. The // AbstractGeneratorObject for this generator call, though, has just been // created. It must be associated with any existing Debugger.Frames. // Initializing frames with their associated generator is critical to the // functionality of the debugger, so if there is an OOM, we want to // cleanly terminate all of the frames. auto terminateDebuggerFramesGuard = MakeScopeExit([&] { Debugger::terminateDebuggerFrames(cx, frame); }); bool ok = true; gc::AutoSuppressGC nogc(cx); Debugger::forEachOnStackDebuggerFrame( frame, nogc, [&](Debugger* dbg, DebuggerFrame* frameObjPtr) { if (!ok) { return; } Rooted frameObj(cx, frameObjPtr); AutoRealm ar(cx, frameObj); if (!DebuggerFrame::setGeneratorInfo(cx, frameObj, genObj)) { // This leaves `genObj` and `frameObj` unassociated. It's OK // because we won't pause again with this generator on the stack: // the caller will immediately discard `genObj` and unwind `frame`. ok = false; return; } DependentAddPtr genPtr( cx, dbg->generatorFrames, genObj); if (!genPtr.add(cx, dbg->generatorFrames, genObj, frameObj)) { ok = false; } }); if (!ok) { return false; } terminateDebuggerFramesGuard.release(); return true; } /* static */ bool DebugAPI::slowPathOnDebuggerStatement(JSContext* cx, AbstractFramePtr frame) { return Debugger::dispatchResumptionHook( cx, frame, [](Debugger* dbg) -> bool { return dbg->getHook(Debugger::OnDebuggerStatement); }, [&](Debugger* dbg, ResumeMode& resumeMode, MutableHandleValue vp) -> bool { return dbg->fireDebuggerStatement(cx, resumeMode, vp); }); } /* static */ bool DebugAPI::slowPathOnExceptionUnwind(JSContext* cx, AbstractFramePtr frame) { // Invoking more JS on an over-recursed stack or after OOM is only going // to result in more of the same error. if (cx->isThrowingOverRecursed() || cx->isThrowingOutOfMemory()) { return true; } // The Debugger API mustn't muck with frames from self-hosted scripts. if (frame.hasScript() && frame.script()->selfHosted()) { return true; } DebuggerList debuggerList(cx, [](Debugger* dbg) -> bool { return dbg->getHook(Debugger::OnExceptionUnwind); }); if (!debuggerList.init(cx)) { return false; } if (debuggerList.empty()) { return true; } // We save and restore the exception once up front to avoid having to do it // for each 'onExceptionUnwind' hook that has been registered, and we also // only do it if the debuggerList contains items in order to avoid extra work. RootedValue exc(cx); Rooted stack(cx, cx->getPendingExceptionStack()); if (!cx->getPendingException(&exc)) { return false; } cx->clearPendingException(); bool result = debuggerList.dispatchResumptionHook( cx, frame, [&](Debugger* dbg, ResumeMode& resumeMode, MutableHandleValue vp) -> bool { return dbg->fireExceptionUnwind(cx, exc, resumeMode, vp); }); if (!result) { return false; } cx->setPendingException(exc, stack); return true; } // TODO: Remove Remove this function when all properties/methods returning a /// DebuggerEnvironment have been given a C++ interface (bug 1271649). bool Debugger::wrapEnvironment(JSContext* cx, Handle env, MutableHandleValue rval) { if (!env) { rval.setNull(); return true; } Rooted envobj(cx); if (!wrapEnvironment(cx, env, &envobj)) { return false; } rval.setObject(*envobj); return true; } bool Debugger::wrapEnvironment(JSContext* cx, Handle env, MutableHandle result) { MOZ_ASSERT(env); // DebuggerEnv should only wrap a debug scope chain obtained (transitively) // from GetDebugEnvironmentFor(Frame|Function). MOZ_ASSERT(!IsSyntacticEnvironment(env)); DependentAddPtr p(cx, environments, env); if (p) { result.set(&p->value()->as()); } else { // Create a new Debugger.Environment for env. RootedObject proto( cx, &object->getReservedSlot(JSSLOT_DEBUG_ENV_PROTO).toObject()); Rooted debugger(cx, object); Rooted envobj( cx, DebuggerEnvironment::create(cx, proto, env, debugger)); if (!envobj) { return false; } if (!p.add(cx, environments, env, envobj)) { // We need to destroy the edge to the referent, to avoid trying to trace // it during untimely collections. envobj->clearReferent(); return false; } result.set(envobj); } return true; } bool Debugger::wrapDebuggeeValue(JSContext* cx, MutableHandleValue vp) { cx->check(object.get()); if (vp.isObject()) { RootedObject obj(cx, &vp.toObject()); Rooted dobj(cx); if (!wrapDebuggeeObject(cx, obj, &dobj)) { return false; } vp.setObject(*dobj); } else if (vp.isMagic()) { Rooted optObj(cx, NewPlainObject(cx)); if (!optObj) { return false; } // We handle three sentinel values: missing arguments // (JS_MISSING_ARGUMENTS), optimized out slots (JS_OPTIMIZED_OUT), // and uninitialized bindings (JS_UNINITIALIZED_LEXICAL). // // Other magic values should not have escaped. PropertyName* name; switch (vp.whyMagic()) { case JS_MISSING_ARGUMENTS: name = cx->names().missingArguments; break; case JS_OPTIMIZED_OUT: name = cx->names().optimizedOut; break; case JS_UNINITIALIZED_LEXICAL: name = cx->names().uninitialized; break; default: MOZ_CRASH("Unsupported magic value escaped to Debugger"); } RootedValue trueVal(cx, BooleanValue(true)); if (!DefineDataProperty(cx, optObj, name, trueVal)) { return false; } vp.setObject(*optObj); } else if (!cx->compartment()->wrap(cx, vp)) { vp.setUndefined(); return false; } return true; } bool Debugger::wrapNullableDebuggeeObject( JSContext* cx, HandleObject obj, MutableHandle result) { if (!obj) { result.set(nullptr); return true; } return wrapDebuggeeObject(cx, obj, result); } bool Debugger::wrapDebuggeeObject(JSContext* cx, HandleObject obj, MutableHandle result) { MOZ_ASSERT(obj); DependentAddPtr p(cx, objects, obj); if (p) { result.set(&p->value()->as()); } else { // Create a new Debugger.Object for obj. Rooted debugger(cx, object); RootedObject proto( cx, &object->getReservedSlot(JSSLOT_DEBUG_OBJECT_PROTO).toObject()); Rooted dobj( cx, DebuggerObject::create(cx, proto, obj, debugger)); if (!dobj) { return false; } if (!p.add(cx, objects, obj, dobj)) { // We need to destroy the edge to the referent, to avoid trying to trace // it during untimely collections. dobj->clearReferent(); return false; } result.set(dobj); } return true; } static DebuggerObject* ToNativeDebuggerObject(JSContext* cx, MutableHandleObject obj) { if (!obj->is()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE, "Debugger", "Debugger.Object", obj->getClass()->name); return nullptr; } return &obj->as(); } bool Debugger::unwrapDebuggeeObject(JSContext* cx, MutableHandleObject obj) { DebuggerObject* ndobj = ToNativeDebuggerObject(cx, obj); if (!ndobj) { return false; } if (ndobj->owner() != Debugger::fromJSObject(object)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_WRONG_OWNER, "Debugger.Object"); return false; } obj.set(ndobj->referent()); return true; } bool Debugger::unwrapDebuggeeValue(JSContext* cx, MutableHandleValue vp) { cx->check(object.get(), vp); if (vp.isObject()) { RootedObject dobj(cx, &vp.toObject()); if (!unwrapDebuggeeObject(cx, &dobj)) { return false; } vp.setObject(*dobj); } return true; } static bool CheckArgCompartment(JSContext* cx, JSObject* obj, JSObject* arg, const char* methodname, const char* propname) { if (arg->compartment() != obj->compartment()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_COMPARTMENT_MISMATCH, methodname, propname); return false; } return true; } static bool CheckArgCompartment(JSContext* cx, JSObject* obj, HandleValue v, const char* methodname, const char* propname) { if (v.isObject()) { return CheckArgCompartment(cx, obj, &v.toObject(), methodname, propname); } return true; } bool Debugger::unwrapPropertyDescriptor( JSContext* cx, HandleObject obj, MutableHandle 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.hasGetter()) { RootedObject get(cx, desc.getter()); if (get) { if (!unwrapDebuggeeObject(cx, &get)) { return false; } if (!CheckArgCompartment(cx, obj, get, "defineProperty", "get")) { return false; } } desc.setGetter(get); } if (desc.hasSetter()) { RootedObject set(cx, desc.setter()); if (set) { if (!unwrapDebuggeeObject(cx, &set)) { return false; } if (!CheckArgCompartment(cx, obj, set, "defineProperty", "set")) { return false; } } desc.setSetter(set); } return true; } /*** Debuggee resumption values and debugger error handling *****************/ static bool GetResumptionProperty(JSContext* cx, HandleObject obj, Handle name, ResumeMode namedMode, ResumeMode& resumeMode, MutableHandleValue vp, int* hits) { bool found; if (!HasProperty(cx, obj, name, &found)) { return false; } if (found) { ++*hits; resumeMode = namedMode; if (!GetProperty(cx, obj, obj, name, vp)) { return false; } } return true; } bool js::ParseResumptionValue(JSContext* cx, HandleValue rval, ResumeMode& resumeMode, MutableHandleValue vp) { if (rval.isUndefined()) { resumeMode = ResumeMode::Continue; vp.setUndefined(); return true; } if (rval.isNull()) { resumeMode = ResumeMode::Terminate; vp.setUndefined(); return true; } int hits = 0; if (rval.isObject()) { RootedObject obj(cx, &rval.toObject()); if (!GetResumptionProperty(cx, obj, cx->names().return_, ResumeMode::Return, resumeMode, vp, &hits)) { return false; } if (!GetResumptionProperty(cx, obj, cx->names().throw_, ResumeMode::Throw, resumeMode, vp, &hits)) { return false; } } if (hits != 1) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_BAD_RESUMPTION); return false; } return true; } static bool CheckResumptionValue(JSContext* cx, AbstractFramePtr frame, const jsbytecode* pc, ResumeMode resumeMode, MutableHandleValue vp) { // Only forced returns from a frame need to be validated because forced // throw values behave just like debuggee `throw` statements. Since // forced-return is all custom logic within SpiderMonkey itself, we need // our own custom validation for it to conform with what is expected. if (resumeMode != ResumeMode::Return || !frame) { return true; } // This replicates the ECMA spec's behavior for [[Construct]] in derived // class constructors (section 9.2.2 of ECMA262-2020), where returning a // non-undefined primitive causes an exception tobe thrown. if (frame.debuggerNeedsCheckPrimitiveReturn() && vp.isPrimitive()) { if (!vp.isUndefined()) { ReportValueError(cx, JSMSG_BAD_DERIVED_RETURN, JSDVG_IGNORE_STACK, vp, nullptr); return false; } RootedValue thisv(cx); { AutoRealm ar(cx, frame.environmentChain()); if (!GetThisValueForDebuggerFrameMaybeOptimizedOut(cx, frame, pc, &thisv)) { return false; } } if (thisv.isMagic(JS_UNINITIALIZED_LEXICAL)) { return ThrowUninitializedThis(cx); } MOZ_ASSERT(!thisv.isMagic()); if (!cx->compartment()->wrap(cx, &thisv)) { return false; } vp.set(thisv); } // Check for forcing return from a generator before the initial yield. This // is not supported because some engine-internal code assumes a call to a // generator will return a GeneratorObject; see bug 1477084. if (frame.isFunctionFrame() && frame.callee()->isGenerator()) { Rooted genObj(cx); { AutoRealm ar(cx, frame.callee()); genObj = GetGeneratorObjectForFrame(cx, frame); } if (!genObj || genObj->isBeforeInitialYield()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_FORCED_RETURN_DISALLOWED); return false; } } return true; } // Last-minute sanity adjustments to resumption. // // This is called last, as we leave the debugger. It must happen outside the // control of the uncaughtExceptionHook, because this code assumes we won't // change our minds and continue execution--we must not close the generator // object unless we're really going to force-return. [[nodiscard]] static bool AdjustGeneratorResumptionValue( JSContext* cx, AbstractFramePtr frame, ResumeMode& resumeMode, MutableHandleValue vp) { if (resumeMode != ResumeMode::Return && resumeMode != ResumeMode::Throw) { return true; } if (!frame) { return true; } // Async modules need to be handled separately, as they do not have a callee. // frame.callee will throw if it is called on a moduleFrame. bool isAsyncModule = frame.isModuleFrame() && frame.script()->isAsync(); if (!frame.isFunctionFrame() && !isAsyncModule) { return true; } // Treat `{return: }` like a `return` statement. Simulate what the // debuggee would do for an ordinary `return` statement, using a few bytecode // instructions. It's simpler to do the work manually than to count on that // bytecode sequence existing in the debuggee, somehow jump to it, and then // avoid re-entering the debugger from it. // // Similarly treat `{throw: }` like a `throw` statement. // // Note: Async modules use the same handling as async functions. if (frame.isFunctionFrame() && frame.callee()->isGenerator()) { // Throw doesn't require any special processing for (async) generators. if (resumeMode == ResumeMode::Throw) { return true; } // Forcing return from a (possibly async) generator. Rooted genObj( cx, GetGeneratorObjectForFrame(cx, frame)); // We already went through CheckResumptionValue, which would have replaced // this invalid resumption value with an error if we were trying to force // return before the initial yield. MOZ_RELEASE_ASSERT(genObj && !genObj->isBeforeInitialYield()); // 1. `return ` creates and returns a new object, // `{value: , done: true}`. // // For non-async generators, the iterator result object is created in // bytecode, so we have to simulate that here. For async generators, our // C++ implementation of AsyncGeneratorResolve will do this. So don't do it // twice: if (!genObj->is()) { PlainObject* pair = CreateIterResultObject(cx, vp, true); if (!pair) { return false; } vp.setObject(*pair); } // 2. The generator must be closed. genObj->setClosed(); // Async generators have additionally bookkeeping which must be adjusted // when switching over to the closed state. if (genObj->is()) { genObj->as().setCompleted(); } } else if (isAsyncModule || frame.callee()->isAsync()) { if (AbstractGeneratorObject* genObj = GetGeneratorObjectForFrame(cx, frame)) { // Throw doesn't require any special processing for async functions when // the internal generator object is already present. if (resumeMode == ResumeMode::Throw) { return true; } Rooted generator( cx, &genObj->as()); // 1. `return ` fulfills and returns the async function's promise. Rooted promise(cx, generator->promise()); if (promise->state() == JS::PromiseState::Pending) { if (!AsyncFunctionResolve(cx, generator, vp, AsyncFunctionResolveKind::Fulfill)) { return false; } } vp.setObject(*promise); // 2. The generator must be closed. generator->setClosed(); } else { // We're before entering the actual function code. // 1. `throw ` creates a promise rejected with the value *vp. // 1. `return ` creates a promise resolved with the value *vp. JSObject* promise = resumeMode == ResumeMode::Throw ? PromiseObject::unforgeableReject(cx, vp) : PromiseObject::unforgeableResolve(cx, vp); if (!promise) { return false; } vp.setObject(*promise); // 2. Return normally in both cases. resumeMode = ResumeMode::Return; } } return true; } bool Debugger::processParsedHandlerResult(JSContext* cx, AbstractFramePtr frame, const jsbytecode* pc, bool success, ResumeMode resumeMode, HandleValue value, ResumeMode& resultMode, MutableHandleValue vp) { RootedValue rootValue(cx, value); if (!success || !prepareResumption(cx, frame, pc, resumeMode, &rootValue)) { RootedValue exceptionRv(cx); if (!callUncaughtExceptionHandler(cx, &exceptionRv) || !ParseResumptionValue(cx, exceptionRv, resumeMode, &rootValue) || !prepareResumption(cx, frame, pc, resumeMode, &rootValue)) { return false; } } // Since debugger hooks accumulate into the same final value handle, we // use that to throw if multiple hooks try to set a resumption value. if (resumeMode != ResumeMode::Continue) { if (resultMode != ResumeMode::Continue) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_RESUMPTION_CONFLICT); return false; } vp.set(rootValue); resultMode = resumeMode; } return true; } bool Debugger::processHandlerResult(JSContext* cx, bool success, HandleValue rv, AbstractFramePtr frame, jsbytecode* pc, ResumeMode& resultMode, MutableHandleValue vp) { ResumeMode resumeMode = ResumeMode::Continue; RootedValue value(cx); if (success) { success = ParseResumptionValue(cx, rv, resumeMode, &value); } return processParsedHandlerResult(cx, frame, pc, success, resumeMode, value, resultMode, vp); } bool Debugger::prepareResumption(JSContext* cx, AbstractFramePtr frame, const jsbytecode* pc, ResumeMode& resumeMode, MutableHandleValue vp) { return unwrapDebuggeeValue(cx, vp) && CheckResumptionValue(cx, frame, pc, resumeMode, vp); } bool Debugger::callUncaughtExceptionHandler(JSContext* cx, MutableHandleValue vp) { // Uncaught exceptions arise from Debugger code, and so we must already be in // an NX section. This also establishes that we are already within the scope // of an AutoDebuggerJobQueueInterruption object. MOZ_ASSERT(EnterDebuggeeNoExecute::isLockedInStack(cx, *this)); if (cx->isExceptionPending() && uncaughtExceptionHook) { RootedValue exc(cx); if (!cx->getPendingException(&exc)) { return false; } cx->clearPendingException(); RootedValue fval(cx, ObjectValue(*uncaughtExceptionHook)); if (js::Call(cx, fval, object, exc, vp)) { return true; } } return false; } bool Debugger::handleUncaughtException(JSContext* cx) { RootedValue rv(cx); return callUncaughtExceptionHandler(cx, &rv); } void Debugger::reportUncaughtException(JSContext* cx) { // Uncaught exceptions arise from Debugger code, and so we must already be // in an NX section. MOZ_ASSERT(EnterDebuggeeNoExecute::isLockedInStack(cx, *this)); if (cx->isExceptionPending()) { // We want to report the pending exception, but we want to let the // embedding handle it however it wants to. So pretend like we're // starting a new script execution on our current compartment (which // is the debugger compartment, so reported errors won't get // reported to various onerror handlers in debuggees) and as part of // that "execution" simply throw our exception so the embedding can // deal. RootedValue exn(cx); if (cx->getPendingException(&exn)) { // Clear the exception, because ReportErrorToGlobal will assert that // we don't have one. cx->clearPendingException(); ReportErrorToGlobal(cx, cx->global(), exn); } // And if not, or if PrepareScriptEnvironmentAndInvoke somehow left an // exception on cx (which it totally shouldn't do), just give up. cx->clearPendingException(); } } /*** Debuggee completion values *********************************************/ /* static */ Completion Completion::fromJSResult(JSContext* cx, bool ok, const Value& rv) { MOZ_ASSERT_IF(ok, !cx->isExceptionPending()); if (ok) { return Completion(Return(rv)); } if (!cx->isExceptionPending()) { return Completion(Terminate()); } RootedValue exception(cx); Rooted stack(cx, cx->getPendingExceptionStack()); bool getSucceeded = cx->getPendingException(&exception); cx->clearPendingException(); if (!getSucceeded) { return Completion(Terminate()); } return Completion(Throw(exception, stack)); } /* static */ Completion Completion::fromJSFramePop(JSContext* cx, AbstractFramePtr frame, const jsbytecode* pc, bool ok) { // Only Wasm frames get a null pc. MOZ_ASSERT_IF(!frame.isWasmDebugFrame(), pc); // If this isn't a generator suspension, then that's already handled above. if (!ok || !frame.isGeneratorFrame()) { return fromJSResult(cx, ok, frame.returnValue()); } // A generator is being suspended or returning. // Since generators are never wasm, we can assume pc is not nullptr, and // that analyzing bytecode is meaningful. MOZ_ASSERT(!frame.isWasmDebugFrame()); // If we're leaving successfully at a yield opcode, we're probably // suspending; the `isClosed()` check detects a debugger forced return from // an `onStep` handler, which looks almost the same. // // GetGeneratorObjectForFrame can return nullptr even when a generator // object does exist, if the frame is paused between the Generator and // SetAliasedVar opcodes. But by checking the opcode first we eliminate that // possibility, so it's fine to call genObj->isClosed(). Rooted generatorObj( cx, GetGeneratorObjectForFrame(cx, frame)); switch (JSOp(*pc)) { case JSOp::InitialYield: MOZ_ASSERT(!generatorObj->isClosed()); return Completion(InitialYield(generatorObj)); case JSOp::Yield: MOZ_ASSERT(!generatorObj->isClosed()); return Completion(Yield(generatorObj, frame.returnValue())); case JSOp::Await: MOZ_ASSERT(!generatorObj->isClosed()); return Completion(Await(generatorObj, frame.returnValue())); default: return Completion(Return(frame.returnValue())); } } void Completion::trace(JSTracer* trc) { variant.match([=](auto& var) { var.trace(trc); }); } struct MOZ_STACK_CLASS Completion::BuildValueMatcher { JSContext* cx; Debugger* dbg; MutableHandleValue result; BuildValueMatcher(JSContext* cx, Debugger* dbg, MutableHandleValue result) : cx(cx), dbg(dbg), result(result) { cx->check(dbg->toJSObject()); } bool operator()(const Completion::Return& ret) { Rooted obj(cx, newObject()); RootedValue retval(cx, ret.value); if (!obj || !wrap(&retval) || !add(obj, cx->names().return_, retval)) { return false; } result.setObject(*obj); return true; } bool operator()(const Completion::Throw& thr) { Rooted obj(cx, newObject()); RootedValue exc(cx, thr.exception); if (!obj || !wrap(&exc) || !add(obj, cx->names().throw_, exc)) { return false; } if (thr.stack) { RootedValue stack(cx, ObjectValue(*thr.stack)); if (!wrapStack(&stack) || !add(obj, cx->names().stack, stack)) { return false; } } result.setObject(*obj); return true; } bool operator()(const Completion::Terminate& term) { result.setNull(); return true; } bool operator()(const Completion::InitialYield& initialYield) { Rooted obj(cx, newObject()); RootedValue gen(cx, ObjectValue(*initialYield.generatorObject)); if (!obj || !wrap(&gen) || !add(obj, cx->names().return_, gen) || !add(obj, cx->names().yield, TrueHandleValue) || !add(obj, cx->names().initial, TrueHandleValue)) { return false; } result.setObject(*obj); return true; } bool operator()(const Completion::Yield& yield) { Rooted obj(cx, newObject()); RootedValue iteratorResult(cx, yield.iteratorResult); if (!obj || !wrap(&iteratorResult) || !add(obj, cx->names().return_, iteratorResult) || !add(obj, cx->names().yield, TrueHandleValue)) { return false; } result.setObject(*obj); return true; } bool operator()(const Completion::Await& await) { Rooted obj(cx, newObject()); RootedValue awaitee(cx, await.awaitee); if (!obj || !wrap(&awaitee) || !add(obj, cx->names().return_, awaitee) || !add(obj, cx->names().await, TrueHandleValue)) { return false; } result.setObject(*obj); return true; } private: NativeObject* newObject() const { return NewPlainObject(cx); } bool add(Handle obj, PropertyName* name, HandleValue value) const { return NativeDefineDataProperty(cx, obj, name, value, JSPROP_ENUMERATE); } bool wrap(MutableHandleValue v) const { return dbg->wrapDebuggeeValue(cx, v); } // Saved stacks are wrapped for direct consumption by debugger code. bool wrapStack(MutableHandleValue stack) const { return cx->compartment()->wrap(cx, stack); } }; bool Completion::buildCompletionValue(JSContext* cx, Debugger* dbg, MutableHandleValue result) const { return variant.match(BuildValueMatcher(cx, dbg, result)); } void Completion::updateFromHookResult(ResumeMode resumeMode, HandleValue value) { switch (resumeMode) { case ResumeMode::Continue: // No change to how we'll resume. break; case ResumeMode::Throw: // Since this is a new exception, the stack for the old one may not apply. // If we extend resumption values to specify stacks, we could revisit // this. variant = Variant(Throw(value, nullptr)); break; case ResumeMode::Terminate: variant = Variant(Terminate()); break; case ResumeMode::Return: variant = Variant(Return(value)); break; default: MOZ_CRASH("invalid resumeMode value"); } } struct MOZ_STACK_CLASS Completion::ToResumeModeMatcher { MutableHandleValue value; MutableHandle exnStack; ToResumeModeMatcher(MutableHandleValue value, MutableHandle exnStack) : value(value), exnStack(exnStack) {} ResumeMode operator()(const Return& ret) { value.set(ret.value); return ResumeMode::Return; } ResumeMode operator()(const Throw& thr) { value.set(thr.exception); exnStack.set(thr.stack); return ResumeMode::Throw; } ResumeMode operator()(const Terminate& term) { value.setUndefined(); return ResumeMode::Terminate; } ResumeMode operator()(const InitialYield& initialYield) { value.setObject(*initialYield.generatorObject); return ResumeMode::Return; } ResumeMode operator()(const Yield& yield) { value.set(yield.iteratorResult); return ResumeMode::Return; } ResumeMode operator()(const Await& await) { value.set(await.awaitee); return ResumeMode::Return; } }; void Completion::toResumeMode(ResumeMode& resumeMode, MutableHandleValue value, MutableHandle exnStack) const { resumeMode = variant.match(ToResumeModeMatcher(value, exnStack)); } /*** Firing debugger hooks **************************************************/ static bool CallMethodIfPresent(JSContext* cx, HandleObject obj, const char* name, size_t argc, Value* argv, MutableHandleValue rval) { rval.setUndefined(); JSAtom* atom = Atomize(cx, name, strlen(name)); if (!atom) { return false; } RootedId id(cx, AtomToId(atom)); RootedValue fval(cx); if (!GetProperty(cx, obj, obj, id, &fval)) { return false; } if (!IsCallable(fval)) { return true; } InvokeArgs args(cx); if (!args.init(cx, argc)) { return false; } for (size_t i = 0; i < argc; i++) { args[i].set(argv[i]); } rval.setObject(*obj); // overwritten by successful Call return js::Call(cx, fval, rval, args, rval); } bool Debugger::fireDebuggerStatement(JSContext* cx, ResumeMode& resumeMode, MutableHandleValue vp) { RootedObject hook(cx, getHook(OnDebuggerStatement)); MOZ_ASSERT(hook); MOZ_ASSERT(hook->isCallable()); ScriptFrameIter iter(cx); RootedValue scriptFrame(cx); if (!getFrame(cx, iter, &scriptFrame)) { return false; } RootedValue fval(cx, ObjectValue(*hook)); RootedValue rv(cx); bool ok = js::Call(cx, fval, object, scriptFrame, &rv); return processHandlerResult(cx, ok, rv, iter.abstractFramePtr(), iter.pc(), resumeMode, vp); } bool Debugger::fireExceptionUnwind(JSContext* cx, HandleValue exc, ResumeMode& resumeMode, MutableHandleValue vp) { RootedObject hook(cx, getHook(OnExceptionUnwind)); MOZ_ASSERT(hook); MOZ_ASSERT(hook->isCallable()); RootedValue scriptFrame(cx); RootedValue wrappedExc(cx, exc); FrameIter iter(cx); if (!getFrame(cx, iter, &scriptFrame) || !wrapDebuggeeValue(cx, &wrappedExc)) { return false; } RootedValue fval(cx, ObjectValue(*hook)); RootedValue rv(cx); bool ok = js::Call(cx, fval, object, scriptFrame, wrappedExc, &rv); return processHandlerResult(cx, ok, rv, iter.abstractFramePtr(), iter.pc(), resumeMode, vp); } bool Debugger::fireEnterFrame(JSContext* cx, ResumeMode& resumeMode, MutableHandleValue vp) { RootedObject hook(cx, getHook(OnEnterFrame)); MOZ_ASSERT(hook); MOZ_ASSERT(hook->isCallable()); RootedValue scriptFrame(cx); FrameIter iter(cx); #if DEBUG // Assert that the hook won't be able to re-enter the generator. if (iter.hasScript() && JSOp(*iter.pc()) == JSOp::AfterYield) { AutoRealm ar(cx, iter.script()); auto* genObj = GetGeneratorObjectForFrame(cx, iter.abstractFramePtr()); MOZ_ASSERT(genObj->isRunning()); } #endif if (!getFrame(cx, iter, &scriptFrame)) { return false; } RootedValue fval(cx, ObjectValue(*hook)); RootedValue rv(cx); bool ok = js::Call(cx, fval, object, scriptFrame, &rv); return processHandlerResult(cx, ok, rv, iter.abstractFramePtr(), iter.pc(), resumeMode, vp); } bool Debugger::fireNativeCall(JSContext* cx, const CallArgs& args, CallReason reason, ResumeMode& resumeMode, MutableHandleValue vp) { RootedObject hook(cx, getHook(OnNativeCall)); MOZ_ASSERT(hook); MOZ_ASSERT(hook->isCallable()); RootedValue fval(cx, ObjectValue(*hook)); RootedValue calleeval(cx, args.calleev()); if (!wrapDebuggeeValue(cx, &calleeval)) { return false; } JSAtom* reasonAtom = nullptr; switch (reason) { case CallReason::Call: reasonAtom = cx->names().call; break; case CallReason::CallContent: reasonAtom = cx->names().call; break; case CallReason::FunCall: reasonAtom = cx->names().call; break; case CallReason::Getter: reasonAtom = cx->names().get; break; case CallReason::Setter: reasonAtom = cx->names().set; break; } MOZ_ASSERT(AtomIsMarked(cx->zone(), reasonAtom)); RootedValue reasonval(cx, StringValue(reasonAtom)); RootedValue rv(cx); bool ok = js::Call(cx, fval, object, calleeval, reasonval, &rv); return processHandlerResult(cx, ok, rv, NullFramePtr(), nullptr, resumeMode, vp); } bool Debugger::fireNewScript(JSContext* cx, Handle scriptReferent) { RootedObject hook(cx, getHook(OnNewScript)); MOZ_ASSERT(hook); MOZ_ASSERT(hook->isCallable()); JSObject* dsobj = wrapVariantReferent(cx, scriptReferent); if (!dsobj) { return false; } RootedValue fval(cx, ObjectValue(*hook)); RootedValue dsval(cx, ObjectValue(*dsobj)); RootedValue rv(cx); return js::Call(cx, fval, object, dsval, &rv) || handleUncaughtException(cx); } bool 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()); JSObject* dataObj = gcData->toJSObject(cx); if (!dataObj) { return false; } RootedValue fval(cx, ObjectValue(*hook)); RootedValue dataVal(cx, ObjectValue(*dataObj)); RootedValue rv(cx); return js::Call(cx, fval, object, dataVal, &rv) || handleUncaughtException(cx); } template /* static */ void Debugger::dispatchQuietHook(JSContext* cx, HookIsEnabledFun hookIsEnabled, FireHookFun fireHook) { DebuggerList debuggerList(cx, hookIsEnabled); if (!debuggerList.init(cx)) { // init may fail due to OOM. This OOM is not handlable at the // callsites of dispatchQuietHook in the engine. cx->clearPendingException(); return; } debuggerList.dispatchQuietHook(cx, fireHook); } template /* static */ bool Debugger::dispatchResumptionHook(JSContext* cx, AbstractFramePtr frame, HookIsEnabledFun hookIsEnabled, FireHookFun fireHook) { DebuggerList debuggerList(cx, hookIsEnabled); if (!debuggerList.init(cx)) { return false; } return debuggerList.dispatchResumptionHook(cx, frame, fireHook); } // Maximum length for source URLs that can be remembered. static const size_t SourceURLMaxLength = 1024; // Maximum number of source URLs that can be remembered in a realm. static const size_t SourceURLRealmLimit = 100; static bool RememberSourceURL(JSContext* cx, HandleScript script) { cx->check(script); // Sources introduced dynamically are not remembered. if (script->sourceObject()->unwrappedIntroductionScript()) { return true; } const char* filename = script->filename(); if (!filename || strnlen(filename, SourceURLMaxLength + 1) > SourceURLMaxLength) { return true; } Rooted holder(cx, script->global().getSourceURLsHolder()); if (!holder) { holder = NewDenseEmptyArray(cx); if (!holder) { return false; } script->global().setSourceURLsHolder(holder); } if (holder->length() >= SourceURLRealmLimit) { return true; } RootedString filenameString(cx, AtomizeUTF8Chars(cx, filename, strlen(filename))); if (!filenameString) { return false; } // The source URLs holder never escapes to script, so we can treat it as a // newborn array for the purpose of adding elements. return NewbornArrayPush(cx, holder, StringValue(filenameString)); } void DebugAPI::onNewScript(JSContext* cx, HandleScript script) { if (!script->realm()->isDebuggee()) { // Remember the URLs associated with scripts in non-system realms, // in case the debugger is attached later. if (!script->realm()->isSystem()) { if (!RememberSourceURL(cx, script)) { cx->clearPendingException(); } } return; } Debugger::dispatchQuietHook( cx, [script](Debugger* dbg) -> bool { return dbg->observesNewScript() && dbg->observesScript(script); }, [&](Debugger* dbg) -> bool { BaseScript* base = script.get(); Rooted scriptReferent(cx, base); return dbg->fireNewScript(cx, scriptReferent); }); } void DebugAPI::slowPathOnNewWasmInstance( JSContext* cx, Handle wasmInstance) { Debugger::dispatchQuietHook( cx, [wasmInstance](Debugger* dbg) -> bool { return dbg->observesNewScript() && dbg->observesGlobal(&wasmInstance->global()); }, [&](Debugger* dbg) -> bool { Rooted scriptReferent(cx, wasmInstance.get()); return dbg->fireNewScript(cx, scriptReferent); }); } /* static */ bool DebugAPI::onTrap(JSContext* cx) { FrameIter iter(cx); JS::AutoSaveExceptionState savedExc(cx); Rooted global(cx); BreakpointSite* site; bool isJS; // true when iter.hasScript(), false when iter.isWasm() jsbytecode* pc; // valid when isJS == true uint32_t bytecodeOffset; // valid when isJS == false if (iter.hasScript()) { RootedScript script(cx, iter.script()); MOZ_ASSERT(script->isDebuggee()); global.set(&script->global()); isJS = true; pc = iter.pc(); bytecodeOffset = 0; site = DebugScript::getBreakpointSite(script, pc); } else { MOZ_ASSERT(iter.isWasm()); global.set(&iter.wasmInstance()->object()->global()); isJS = false; pc = nullptr; bytecodeOffset = iter.wasmBytecodeOffset(); site = iter.wasmInstance()->debug().getBreakpointSite(bytecodeOffset); } // Build list of breakpoint handlers. // // This does not need to be rooted: since the JSScript/WasmInstance is on the // stack, the Breakpoints will not be GC'd. However, they may be deleted, and // we check for that case below. Vector triggered(cx); for (Breakpoint* bp = site->firstBreakpoint(); bp; bp = bp->nextInSite()) { if (!triggered.append(bp)) { return false; } } ResumeMode resumeMode = ResumeMode::Continue; RootedValue rval(cx); if (triggered.length() > 0) { // Preserve the debuggee's microtask event queue while we run the hooks, so // the debugger's microtask checkpoints don't run from the debuggee's // microtasks, and vice versa. JS::AutoDebuggerJobQueueInterruption adjqi; if (!adjqi.init(cx)) { return false; } for (Breakpoint* bp : triggered) { // 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 debugging // global. // // 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; if (dbg->debuggees.has(global)) { EnterDebuggeeNoExecute nx(cx, *dbg, adjqi); bool result = dbg->enterDebuggerHook(cx, [&]() -> bool { RootedValue scriptFrame(cx); if (!dbg->getFrame(cx, iter, &scriptFrame)) { return false; } // Re-wrap the breakpoint's handler for the Debugger's compartment. // When the handler and the Debugger are in the same compartment (the // usual case), this actually unwraps it, but there's no requirement // that they be in the same compartment, so we can't be sure. Rooted handler(cx, bp->handler); if (!cx->compartment()->wrap(cx, &handler)) { return false; } RootedValue rv(cx); bool ok = CallMethodIfPresent(cx, handler, "hit", 1, scriptFrame.address(), &rv); return dbg->processHandlerResult(cx, ok, rv, iter.abstractFramePtr(), iter.pc(), resumeMode, &rval); }); adjqi.runJobs(); if (!result) { return false; } // Calling JS code invalidates site. Reload it. if (isJS) { site = DebugScript::getBreakpointSite(iter.script(), pc); } else { site = iter.wasmInstance()->debug().getBreakpointSite(bytecodeOffset); } } } } if (!ApplyFrameResumeMode(cx, iter.abstractFramePtr(), resumeMode, rval)) { savedExc.drop(); return false; } return true; } /* static */ bool DebugAPI::onSingleStep(JSContext* cx) { FrameIter 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). JS::AutoSaveExceptionState savedExc(cx); // Build list of Debugger.Frame instances referring to this frame with // onStep handlers. Rooted frames(cx); if (!Debugger::getDebuggerFrames(iter.abstractFramePtr(), &frames)) { ReportOutOfMemory(cx); return false; } #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. if (iter.hasScript()) { uint32_t liveStepperCount = 0; uint32_t suspendedStepperCount = 0; JSScript* trappingScript = iter.script(); JS::AutoAssertNoGC nogc; for (Realm::DebuggerVectorEntry& entry : cx->global()->getDebuggers(nogc)) { Debugger* dbg = entry.dbg; for (Debugger::FrameMap::Range r = dbg->frames.all(); !r.empty(); r.popFront()) { AbstractFramePtr frame = r.front().key(); NativeObject* frameobj = r.front().value(); if (frame.isWasmDebugFrame()) { continue; } if (frame.script() == trappingScript && !frameobj->getReservedSlot(DebuggerFrame::ONSTEP_HANDLER_SLOT) .isUndefined()) { liveStepperCount++; } } // Also count hooks set on suspended generator frames. for (Debugger::GeneratorWeakMap::Range r = dbg->generatorFrames.all(); !r.empty(); r.popFront()) { AbstractGeneratorObject& genObj = *r.front().key(); DebuggerFrame& frameObj = *r.front().value(); MOZ_ASSERT(&frameObj.unwrappedGenerator() == &genObj); // Live Debugger.Frames were already counted in dbg->frames loop. if (frameObj.isOnStack()) { continue; } // A closed generator no longer has a callee so it will not be able to // compare with the trappingScript. if (genObj.isClosed()) { continue; } // If a frame isn't live, but it has an entry in generatorFrames, // it had better be suspended. MOZ_ASSERT(genObj.isSuspended()); if (genObj.callee().hasBaseScript() && genObj.callee().baseScript() == trappingScript && !frameObj.getReservedSlot(DebuggerFrame::ONSTEP_HANDLER_SLOT) .isUndefined()) { suspendedStepperCount++; } } } MOZ_ASSERT(liveStepperCount + suspendedStepperCount == DebugScript::getStepperCount(trappingScript)); } #endif RootedValue rval(cx); ResumeMode resumeMode = ResumeMode::Continue; if (frames.length() > 0) { // Preserve the debuggee's microtask event queue while we run the hooks, so // the debugger's microtask checkpoints don't run from the debuggee's // microtasks, and vice versa. JS::AutoDebuggerJobQueueInterruption adjqi; if (!adjqi.init(cx)) { return false; } // Call onStep for frames that have the handler set. for (size_t i = 0; i < frames.length(); i++) { Handle frame = frames[i]; OnStepHandler* handler = frame->onStepHandler(); if (!handler) { continue; } Debugger* dbg = frame->owner(); EnterDebuggeeNoExecute nx(cx, *dbg, adjqi); bool result = dbg->enterDebuggerHook(cx, [&]() -> bool { ResumeMode nextResumeMode = ResumeMode::Continue; RootedValue nextValue(cx); bool success = handler->onStep(cx, frame, nextResumeMode, &nextValue); return dbg->processParsedHandlerResult( cx, iter.abstractFramePtr(), iter.pc(), success, nextResumeMode, nextValue, resumeMode, &rval); }); adjqi.runJobs(); if (!result) { return false; } } } if (!ApplyFrameResumeMode(cx, iter.abstractFramePtr(), resumeMode, rval)) { savedExc.drop(); return false; } return true; } bool Debugger::fireNewGlobalObject(JSContext* cx, Handle global) { RootedObject hook(cx, getHook(OnNewGlobalObject)); MOZ_ASSERT(hook); MOZ_ASSERT(hook->isCallable()); RootedValue wrappedGlobal(cx, ObjectValue(*global)); if (!wrapDebuggeeValue(cx, &wrappedGlobal)) { return false; } // onNewGlobalObject is infallible, and thus is only allowed to return // undefined as a resumption value. If it returns anything else, we throw. // And if that happens, or if the hook itself throws, we invoke the // uncaughtExceptionHook so that we never leave an exception pending on the // cx. This allows JS_NewGlobalObject to avoid handling failures from // debugger hooks. RootedValue rv(cx); RootedValue fval(cx, ObjectValue(*hook)); bool ok = js::Call(cx, fval, object, wrappedGlobal, &rv); if (ok && !rv.isUndefined()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_RESUMPTION_VALUE_DISALLOWED); ok = false; } return ok || handleUncaughtException(cx); } void DebugAPI::slowPathOnNewGlobalObject(JSContext* cx, Handle global) { MOZ_ASSERT(!cx->runtime()->onNewGlobalObjectWatchers().isEmpty()); if (global->realm()->creationOptions().invisibleToDebugger()) { return; } // Make a copy of the runtime's onNewGlobalObjectWatchers before running the // handlers. Since one Debugger's handler can disable another's, the list // can be mutated while we're walking it. RootedObjectVector watchers(cx); for (auto& dbg : cx->runtime()->onNewGlobalObjectWatchers()) { MOZ_ASSERT(dbg.observesNewGlobalObject()); JSObject* obj = dbg.object; JS::ExposeObjectToActiveJS(obj); if (!watchers.append(obj)) { if (cx->isExceptionPending()) { cx->clearPendingException(); } return; } } // Preserve the debuggee's microtask event queue while we run the hooks, so // the debugger's microtask checkpoints don't run from the debuggee's // microtasks, and vice versa. JS::AutoDebuggerJobQueueInterruption adjqi; if (!adjqi.init(cx)) { cx->clearPendingException(); return; } for (size_t i = 0; i < watchers.length(); i++) { Debugger* dbg = Debugger::fromJSObject(watchers[i]); EnterDebuggeeNoExecute nx(cx, *dbg, adjqi); if (dbg->observesNewGlobalObject()) { bool result = dbg->enterDebuggerHook( cx, [&]() -> bool { return dbg->fireNewGlobalObject(cx, global); }); adjqi.runJobs(); if (!result) { // Like other quiet hooks using dispatchQuietHook, this hook // silently ignores all errors that propagate out of it and aren't // already handled by the hook error reporting. cx->clearPendingException(); break; } } } MOZ_ASSERT(!cx->isExceptionPending()); } /* static */ void DebugAPI::slowPathNotifyParticipatesInGC(uint64_t majorGCNumber, Realm::DebuggerVector& dbgs, const JS::AutoRequireNoGC& nogc) { for (Realm::DebuggerVector::Range r = dbgs.all(); !r.empty(); r.popFront()) { if (!r.front().dbg.unbarrieredGet()->debuggeeIsBeingCollected( majorGCNumber)) { #ifdef DEBUG fprintf(stderr, "OOM while notifying observing Debuggers of a GC: The " "onGarbageCollection\n" "hook will not be fired for this GC for some Debuggers!\n"); #endif return; } } } /* static */ Maybe DebugAPI::allocationSamplingProbability(GlobalObject* global) { JS::AutoAssertNoGC nogc; Realm::DebuggerVector& dbgs = global->getDebuggers(nogc); if (dbgs.empty()) { return Nothing(); } DebugOnly begin = dbgs.begin(); double probability = 0; bool foundAnyDebuggers = false; for (auto p = dbgs.begin(); p < dbgs.end(); p++) { // The set of debuggers had better not change while we're iterating, // such that the vector gets reallocated. MOZ_ASSERT(dbgs.begin() == begin); // Use unbarrieredGet() to prevent triggering read barrier while collecting, // this is safe as long as dbgp does not escape. Debugger* dbgp = p->dbg.unbarrieredGet(); if (dbgp->trackingAllocationSites) { foundAnyDebuggers = true; probability = std::max(dbgp->allocationSamplingProbability, probability); } } return foundAnyDebuggers ? Some(probability) : Nothing(); } /* static */ bool DebugAPI::slowPathOnLogAllocationSite(JSContext* cx, HandleObject obj, Handle frame, mozilla::TimeStamp when, Realm::DebuggerVector& dbgs, const gc::AutoSuppressGC& nogc) { MOZ_ASSERT(!dbgs.empty()); mozilla::DebugOnly begin = dbgs.begin(); // GC is suppressed so we can iterate over the debuggers; appendAllocationSite // calls Compartment::wrap, and thus could GC. for (auto p = dbgs.begin(); p < dbgs.end(); p++) { // The set of debuggers had better not change while we're iterating, // such that the vector gets reallocated. MOZ_ASSERT(dbgs.begin() == begin); if (p->dbg->trackingAllocationSites && !p->dbg->appendAllocationSite(cx, obj, frame, when)) { return false; } } return true; } bool Debugger::isDebuggeeUnbarriered(const Realm* realm) const { MOZ_ASSERT(realm); return realm->isDebuggee() && debuggees.has(realm->unsafeUnbarrieredMaybeGlobal()); } bool Debugger::appendAllocationSite(JSContext* cx, HandleObject obj, Handle frame, mozilla::TimeStamp when) { MOZ_ASSERT(trackingAllocationSites); AutoRealm ar(cx, object); RootedObject wrappedFrame(cx, frame); if (!cx->compartment()->wrap(cx, &wrappedFrame)) { 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, size, inNursery)) { ReportOutOfMemory(cx); return false; } if (allocationsLog.length() > maxAllocationsLogLength) { allocationsLog.popFront(); MOZ_ASSERT(allocationsLog.length() == maxAllocationsLogLength); allocationsLogOverflowed = true; } return true; } bool Debugger::firePromiseHook(JSContext* cx, Hook hook, HandleObject promise) { MOZ_ASSERT(hook == OnNewPromise || hook == OnPromiseSettled); RootedObject hookObj(cx, getHook(hook)); MOZ_ASSERT(hookObj); MOZ_ASSERT(hookObj->isCallable()); RootedValue dbgObj(cx, ObjectValue(*promise)); if (!wrapDebuggeeValue(cx, &dbgObj)) { return false; } // Like onNewGlobalObject, the Promise hooks are infallible and the comments // in |Debugger::fireNewGlobalObject| apply here as well. RootedValue fval(cx, ObjectValue(*hookObj)); RootedValue rv(cx); bool ok = js::Call(cx, fval, object, dbgObj, &rv); if (ok && !rv.isUndefined()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_RESUMPTION_VALUE_DISALLOWED); ok = false; } return ok || handleUncaughtException(cx); } /* static */ void Debugger::slowPathPromiseHook(JSContext* cx, Hook hook, Handle promise) { MOZ_ASSERT(hook == OnNewPromise || hook == OnPromiseSettled); if (hook == OnPromiseSettled) { // We should be in the right compartment, but for simplicity always enter // the promise's realm below. cx->check(promise); } AutoRealm ar(cx, promise); Debugger::dispatchQuietHook( cx, [hook](Debugger* dbg) -> bool { return dbg->getHook(hook); }, [&](Debugger* dbg) -> bool { return dbg->firePromiseHook(cx, hook, promise); }); } /* static */ void DebugAPI::slowPathOnNewPromise(JSContext* cx, Handle promise) { Debugger::slowPathPromiseHook(cx, Debugger::OnNewPromise, promise); } /* static */ void DebugAPI::slowPathOnPromiseSettled(JSContext* cx, Handle promise) { Debugger::slowPathPromiseHook(cx, Debugger::OnPromiseSettled, promise); } /*** Debugger code invalidation for observing execution *********************/ class MOZ_RAII ExecutionObservableRealms : public DebugAPI::ExecutionObservableSet { HashSet realms_; HashSet zones_; public: explicit ExecutionObservableRealms(JSContext* cx) : realms_(cx), zones_(cx) {} bool add(Realm* realm) { return realms_.put(realm) && zones_.put(realm->zone()); } using RealmRange = HashSet::Range; const HashSet* realms() const { return &realms_; } const HashSet* zones() const override { return &zones_; } bool shouldRecompileOrInvalidate(JSScript* script) const override { return script->hasBaselineScript() && realms_.has(script->realm()); } bool shouldMarkAsDebuggee(FrameIter& iter) const override { // AbstractFramePtr can't refer to non-remateralized Ion frames or // non-debuggee wasm frames, so if iter refers to one such, we know we // don't match. return iter.hasUsableAbstractFramePtr() && realms_.has(iter.realm()); } }; // 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 DebugAPI::ExecutionObservableSet { AbstractFramePtr frame_; public: explicit ExecutionObservableFrame(AbstractFramePtr frame) : frame_(frame) {} Zone* singleZone() const override { // We never inline across realms, let alone across zones, so // frames_'s script's zone is the only one of interest. return frame_.script()->zone(); } JSScript* singleScriptForZoneInvalidation() const override { MOZ_CRASH( "ExecutionObservableFrame shouldn't need zone-wide invalidation."); return nullptr; } bool shouldRecompileOrInvalidate(JSScript* script) const override { // 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 (frame_.hasScript() && script == frame_.script()) { return true; } return frame_.isRematerializedFrame() && script == frame_.asRematerializedFrame()->outerScript(); } bool shouldMarkAsDebuggee(FrameIter& iter) const override { // AbstractFramePtr can't refer to non-remateralized Ion frames or // non-debuggee wasm 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_; } }; class MOZ_RAII ExecutionObservableScript : public DebugAPI::ExecutionObservableSet { RootedScript script_; public: ExecutionObservableScript(JSContext* cx, JSScript* script) : script_(cx, script) {} Zone* singleZone() const override { return script_->zone(); } JSScript* singleScriptForZoneInvalidation() const override { return script_; } bool shouldRecompileOrInvalidate(JSScript* script) const override { return script->hasBaselineScript() && script == script_; } bool shouldMarkAsDebuggee(FrameIter& iter) const override { // 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. // // Also AbstractFramePtr can't refer to non-debuggee wasm frames, so if // iter refers to one such, we know we don't match. return iter.hasUsableAbstractFramePtr() && !iter.isWasm() && iter.abstractFramePtr().script() == script_; } }; /* static */ bool Debugger::updateExecutionObservabilityOfFrames( JSContext* cx, const DebugAPI::ExecutionObservableSet& obs, IsObserving observing) { AutoSuppressProfilerSampling suppressProfilerSampling(cx); if (!jit::RecompileOnStackBaselineScriptsForDebugMode(cx, obs, observing)) { return false; } AbstractFramePtr oldestEnabledFrame; for (AllFramesIter iter(cx); !iter.done(); ++iter) { if (obs.shouldMarkAsDebuggee(iter)) { if (observing) { if (!iter.abstractFramePtr().isDebuggee()) { oldestEnabledFrame = iter.abstractFramePtr(); oldestEnabledFrame.setIsDebuggee(); } if (iter.abstractFramePtr().isWasmDebugFrame()) { iter.abstractFramePtr().asWasmDebugFrame()->observe(cx); } } else { #ifdef DEBUG // Debugger.Frame lifetimes are managed by the debug epilogue, // so in general it's unsafe to unmark a frame if it has a // Debugger.Frame associated with it. MOZ_ASSERT(!DebugAPI::inFrameMaps(iter.abstractFramePtr())); #endif iter.abstractFramePtr().unsetIsDebuggee(); } } } // See comment in unsetPrevUpToDateUntil. if (oldestEnabledFrame) { AutoRealm ar(cx, oldestEnabledFrame.environmentChain()); DebugEnvironments::unsetPrevUpToDateUntil(cx, oldestEnabledFrame); } return true; } static inline void MarkJitScriptActiveIfObservable( JSScript* script, const DebugAPI::ExecutionObservableSet& obs) { if (obs.shouldRecompileOrInvalidate(script)) { script->jitScript()->setActive(); } } static bool AppendAndInvalidateScript(JSContext* cx, Zone* zone, JSScript* script, jit::RecompileInfoVector& invalid, Vector& scripts) { // Enter the script's realm as AddPendingInvalidation attempts to // cancel off-thread compilations, whose books are kept on the // script's realm. MOZ_ASSERT(script->zone() == zone); AutoRealm ar(cx, script); AddPendingInvalidation(invalid, script); return scripts.append(script); } static bool UpdateExecutionObservabilityOfScriptsInZone( JSContext* cx, Zone* zone, const DebugAPI::ExecutionObservableSet& obs, Debugger::IsObserving observing) { using namespace js::jit; AutoSuppressProfilerSampling suppressProfilerSampling(cx); JS::GCContext* gcx = cx->gcContext(); Vector scripts(cx); // Iterate through observable scripts, invalidating their Ion scripts and // appending them to a vector for discarding their baseline scripts later. { RecompileInfoVector invalid; if (JSScript* script = obs.singleScriptForZoneInvalidation()) { if (obs.shouldRecompileOrInvalidate(script)) { if (!AppendAndInvalidateScript(cx, zone, script, invalid, scripts)) { return false; } } } else { for (auto base = zone->cellIter(); !base.done(); base.next()) { if (!base->hasJitScript()) { continue; } JSScript* script = base->asJSScript(); if (obs.shouldRecompileOrInvalidate(script)) { if (!AppendAndInvalidateScript(cx, zone, script, invalid, scripts)) { return false; } } } } Invalidate(cx, invalid); } // Code below this point must be infallible to ensure the active bit of // BaselineScripts is in a consistent state. // // Mark active baseline scripts in the observable set so that they don't // get discarded. They will be recompiled. for (JitActivationIterator actIter(cx); !actIter.done(); ++actIter) { if (actIter->compartment()->zone() != zone) { continue; } for (OnlyJSJitFrameIter iter(actIter); !iter.done(); ++iter) { const JSJitFrameIter& frame = iter.frame(); switch (frame.type()) { case FrameType::BaselineJS: MarkJitScriptActiveIfObservable(frame.script(), obs); break; case FrameType::IonJS: MarkJitScriptActiveIfObservable(frame.script(), obs); for (InlineFrameIterator inlineIter(cx, &frame); inlineIter.more(); ++inlineIter) { MarkJitScriptActiveIfObservable(inlineIter.script(), obs); } break; default:; } } } // Iterate through the scripts again and finish discarding // BaselineScripts. This must be done as a separate phase as we can only // discard the BaselineScript on scripts that have no IonScript. for (size_t i = 0; i < scripts.length(); i++) { MOZ_ASSERT_IF(scripts[i]->isDebuggee(), observing); if (!scripts[i]->jitScript()->active()) { FinishDiscardBaselineScript(gcx, scripts[i]); } scripts[i]->jitScript()->resetActive(); } // Iterate through all wasm instances to find ones that need to be updated. for (RealmsInZoneIter r(zone); !r.done(); r.next()) { for (wasm::Instance* instance : r->wasm.instances()) { if (!instance->debugEnabled()) { continue; } bool enableTrap = observing == Debugger::Observing; instance->debug().ensureEnterFrameTrapsState(cx, instance, enableTrap); } } return true; } /* static */ bool Debugger::updateExecutionObservabilityOfScripts( JSContext* cx, const DebugAPI::ExecutionObservableSet& obs, IsObserving observing) { if (Zone* zone = obs.singleZone()) { return UpdateExecutionObservabilityOfScriptsInZone(cx, zone, obs, observing); } using ZoneRange = DebugAPI::ExecutionObservableSet::ZoneRange; for (ZoneRange r = obs.zones()->all(); !r.empty(); r.popFront()) { if (!UpdateExecutionObservabilityOfScriptsInZone(cx, r.front(), obs, observing)) { return false; } } return true; } template /* static */ void Debugger::forEachOnStackDebuggerFrame(AbstractFramePtr frame, const JS::AutoRequireNoGC& nogc, FrameFn fn) { for (Realm::DebuggerVectorEntry& entry : frame.global()->getDebuggers(nogc)) { Debugger* dbg = entry.dbg; if (FrameMap::Ptr frameEntry = dbg->frames.lookup(frame)) { fn(dbg, frameEntry->value()); } } } template /* static */ void Debugger::forEachOnStackOrSuspendedDebuggerFrame( JSContext* cx, AbstractFramePtr frame, const JS::AutoRequireNoGC& nogc, FrameFn fn) { Rooted genObj( cx, frame.isGeneratorFrame() ? GetGeneratorObjectForFrame(cx, frame) : nullptr); for (Realm::DebuggerVectorEntry& entry : frame.global()->getDebuggers(nogc)) { Debugger* dbg = entry.dbg; DebuggerFrame* frameObj = nullptr; if (FrameMap::Ptr frameEntry = dbg->frames.lookup(frame)) { frameObj = frameEntry->value(); } else if (GeneratorWeakMap::Ptr frameEntry = dbg->generatorFrames.lookup(genObj)) { frameObj = frameEntry->value(); } if (frameObj) { fn(dbg, frameObj); } } } /* static */ bool Debugger::getDebuggerFrames(AbstractFramePtr frame, MutableHandle frames) { bool hadOOM = false; JS::AutoAssertNoGC nogc; forEachOnStackDebuggerFrame(frame, nogc, [&](Debugger*, DebuggerFrame* frameobj) { if (!hadOOM && !frames.append(frameobj)) { hadOOM = true; } }); return !hadOOM; } /* static */ bool Debugger::updateExecutionObservability( JSContext* cx, DebugAPI::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 DebugAPI::ensureExecutionObservabilityOfOsrFrame( JSContext* cx, AbstractFramePtr osrSourceFrame) { MOZ_ASSERT(osrSourceFrame.isDebuggee()); if (osrSourceFrame.script()->hasBaselineScript() && osrSourceFrame.script()->baselineScript()->hasDebugInstrumentation()) { return true; } ExecutionObservableFrame obs(osrSourceFrame); return Debugger::updateExecutionObservabilityOfFrames(cx, obs, Observing); } /* static */ bool Debugger::ensureExecutionObservabilityOfFrame(JSContext* cx, AbstractFramePtr frame) { MOZ_ASSERT_IF(frame.hasScript() && frame.script()->isDebuggee(), frame.isDebuggee()); MOZ_ASSERT_IF(frame.isWasmDebugFrame(), frame.wasmInstance()->debugEnabled()); if (frame.isDebuggee()) { return true; } ExecutionObservableFrame obs(frame); return updateExecutionObservabilityOfFrames(cx, obs, Observing); } /* static */ bool Debugger::ensureExecutionObservabilityOfRealm(JSContext* cx, Realm* realm) { if (realm->debuggerObservesAllExecution()) { return true; } ExecutionObservableRealms obs(cx); if (!obs.add(realm)) { return false; } realm->updateDebuggerObservesAllExecution(); return updateExecutionObservability(cx, obs, Observing); } /* static */ bool Debugger::hookObservesAllExecution(Hook which) { return which == OnEnterFrame; } Debugger::IsObserving Debugger::observesAllExecution() const { if (!!getHook(OnEnterFrame)) { return Observing; } return NotObserving; } Debugger::IsObserving Debugger::observesAsmJS() const { if (!allowUnobservedAsmJS) { return Observing; } return NotObserving; } Debugger::IsObserving Debugger::observesWasm() const { if (!allowUnobservedWasm) { return Observing; } return NotObserving; } Debugger::IsObserving Debugger::observesCoverage() const { if (collectCoverageInfo) { return Observing; } return NotObserving; } Debugger::IsObserving Debugger::observesNativeCalls() const { if (getHook(Debugger::OnNativeCall)) { 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) { ExecutionObservableRealms obs(cx); for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) { GlobalObject* global = r.front(); JS::Realm* realm = global->realm(); if (realm->debuggerObservesAllExecution() == observing) { continue; } // It's expensive to eagerly invalidate and recompile a realm, // so add the realm to the set only if we are observing. if (observing && !obs.add(realm)) { return false; } } if (!updateExecutionObservability(cx, obs, observing)) { return false; } using RealmRange = ExecutionObservableRealms::RealmRange; for (RealmRange r = obs.realms()->all(); !r.empty(); r.popFront()) { r.front()->updateDebuggerObservesAllExecution(); } return true; } bool Debugger::updateObservesCoverageOnDebuggees(JSContext* cx, IsObserving observing) { ExecutionObservableRealms obs(cx); for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) { GlobalObject* global = r.front(); Realm* realm = global->realm(); if (realm->debuggerObservesCoverage() == observing) { continue; } // Invalidate and recompile a realm to add or remove PCCounts // increments. We have to eagerly invalidate, as otherwise we might have // dangling pointers to freed PCCounts. if (!obs.add(realm)) { 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 (FrameIter iter(cx); !iter.done(); ++iter) { if (obs.shouldMarkAsDebuggee(iter)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_NOT_IDLE); return false; } } if (!updateExecutionObservability(cx, obs, observing)) { return false; } // All realms can safely be toggled, and all scripts will be recompiled. // Thus we can update each realm accordingly. using RealmRange = ExecutionObservableRealms::RealmRange; for (RealmRange r = obs.realms()->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(); Realm* realm = global->realm(); if (realm->debuggerObservesAsmJS() == observing) { continue; } realm->updateDebuggerObservesAsmJS(); } } void Debugger::updateObservesWasmOnDebuggees(IsObserving observing) { for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) { GlobalObject* global = r.front(); Realm* realm = global->realm(); if (realm->debuggerObservesWasm() == observing) { continue; } realm->updateDebuggerObservesWasm(); } } /*** Allocations Tracking ***************************************************/ /* static */ bool Debugger::cannotTrackAllocations(const GlobalObject& global) { auto existingCallback = global.realm()->getAllocationMetadataBuilder(); return existingCallback && existingCallback != &SavedStacks::metadataBuilder; } /* static */ bool DebugAPI::isObservedByDebuggerTrackingAllocations( const GlobalObject& debuggee) { JS::AutoAssertNoGC nogc; for (Realm::DebuggerVectorEntry& entry : debuggee.getDebuggers(nogc)) { // Use unbarrieredGet() to prevent triggering read barrier while // collecting, this is safe as long as dbg does not escape. Debugger* dbg = entry.dbg.unbarrieredGet(); if (dbg->trackingAllocationSites) { return true; } } return false; } /* static */ bool Debugger::addAllocationsTracking(JSContext* cx, Handle debuggee) { // Precondition: the given global object is being observed by at least one // Debugger that is tracking allocations. MOZ_ASSERT(DebugAPI::isObservedByDebuggerTrackingAllocations(*debuggee)); if (Debugger::cannotTrackAllocations(*debuggee)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_OBJECT_METADATA_CALLBACK_ALREADY_SET); return false; } debuggee->realm()->setAllocationMetadataBuilder( &SavedStacks::metadataBuilder); debuggee->realm()->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 (DebugAPI::isObservedByDebuggerTrackingAllocations(global)) { global.realm()->chooseAllocationSamplingProbability(); return; } if (!global.realm()->runtimeFromMainThread()->recordAllocationCallback) { // Something like the Gecko Profiler could request from the the JS runtime // to record allocations. If it is recording allocations, then do not // destroy the allocation metadata builder at this time. global.realm()->forgetAllocationMetadataBuilder(); } } bool Debugger::addAllocationsTrackingForAllDebuggees(JSContext* cx) { MOZ_ASSERT(trackingAllocationSites); // We don't want to end up in a state where we added allocations // tracking to some of our debuggees, but failed to do so for // others. Before attempting to start tracking allocations in *any* of // our debuggees, ensure that we will be able to track allocations for // *all* of our debuggees. for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) { if (Debugger::cannotTrackAllocations(*r.front().get())) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_OBJECT_METADATA_CALLBACK_ALREADY_SET); return false; } } Rooted g(cx); for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) { // This should always succeed, since we already checked for the // error case above. g = r.front().get(); MOZ_ALWAYS_TRUE(Debugger::addAllocationsTracking(cx, g)); } return true; } void Debugger::removeAllocationsTrackingForAllDebuggees() { for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) { Debugger::removeAllocationsTracking(*r.front().get()); } allocationsLog.clear(); } /*** Debugger JSObjects *****************************************************/ template inline void Debugger::forEachWeakMap(const F& f) { f(generatorFrames); f(objects); f(environments); f(scripts); f(sources); f(wasmInstanceScripts); f(wasmInstanceSources); } void Debugger::traceCrossCompartmentEdges(JSTracer* trc) { forEachWeakMap( [trc](auto& weakMap) { weakMap.traceCrossCompartmentEdges(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 five cross-compartment WeakMaps: objects, scripts, * lazy 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 into zones that are being compacted. */ /* static */ void DebugAPI::traceCrossCompartmentEdges(JSTracer* trc) { MOZ_ASSERT(JS::RuntimeHeapIsMajorCollecting()); JSRuntime* rt = trc->runtime(); gc::State state = rt->gc.state(); for (Debugger* dbg : rt->debuggerList()) { Zone* zone = MaybeForwarded(dbg->object.get())->zone(); if (!zone->isCollecting() || state == gc::State::Compact) { dbg->traceCrossCompartmentEdges(trc); } } } #ifdef DEBUG static bool RuntimeHasDebugger(JSRuntime* rt, Debugger* dbg) { for (Debugger* d : rt->debuggerList()) { if (d == dbg) { return true; } } return false; } /* static */ bool DebugAPI::edgeIsInDebuggerWeakmap(JSRuntime* rt, JSObject* src, JS::GCCellPtr dst) { if (!Debugger::isChildJSObject(src)) { return false; } if (src->is()) { DebuggerFrame* frame = &src->as(); Debugger* dbg = frame->owner(); MOZ_ASSERT(RuntimeHasDebugger(rt, dbg)); if (dst.is()) { // The generatorFrames map is not keyed on the associated JSScript. Get // the key from the source object and check everything matches. AbstractGeneratorObject* genObj = &frame->unwrappedGenerator(); return frame->generatorScript() == &dst.as() && dbg->generatorFrames.hasEntry(genObj, src); } return dst.is() && dst.as().is() && dbg->generatorFrames.hasEntry( &dst.as().as(), src); } if (src->is()) { Debugger* dbg = src->as().owner(); MOZ_ASSERT(RuntimeHasDebugger(rt, dbg)); return dst.is() && dbg->objects.hasEntry(&dst.as(), src); } if (src->is()) { Debugger* dbg = src->as().owner(); MOZ_ASSERT(RuntimeHasDebugger(rt, dbg)); return dst.is() && dbg->environments.hasEntry(&dst.as(), src); } if (src->is()) { Debugger* dbg = src->as().owner(); MOZ_ASSERT(RuntimeHasDebugger(rt, dbg)); return src->as().getReferent().match( [=](BaseScript* script) { return dst.is() && script == &dst.as() && dbg->scripts.hasEntry(script, src); }, [=](WasmInstanceObject* instance) { return dst.is() && instance == &dst.as() && dbg->wasmInstanceScripts.hasEntry(instance, src); }); } if (src->is()) { Debugger* dbg = src->as().owner(); MOZ_ASSERT(RuntimeHasDebugger(rt, dbg)); return src->as().getReferent().match( [=](ScriptSourceObject* sso) { return dst.is() && sso == &dst.as() && dbg->sources.hasEntry(sso, src); }, [=](WasmInstanceObject* instance) { return dst.is() && instance == &dst.as() && dbg->wasmInstanceSources.hasEntry(instance, src); }); } MOZ_ASSERT_UNREACHABLE("Unhandled cross-compartment edge"); } #endif /* See comments in DebugAPI.h. */ void DebugAPI::traceFramesWithLiveHooks(JSTracer* tracer) { JSRuntime* rt = tracer->runtime(); // Note that we must loop over all Debuggers here, not just those known to be // reachable from JavaScript. The existence of hooks set on a Debugger.Frame // for a live stack frame makes the Debuger.Frame (and hence its Debugger) // reachable. for (Debugger* dbg : rt->debuggerList()) { // Callback tracers set their own traversal boundaries, but otherwise we're // only interested in Debugger.Frames participating in the collection. if (!dbg->zone()->isGCMarking() && !tracer->isCallbackTracer()) { continue; } for (Debugger::FrameMap::Range r = dbg->frames.all(); !r.empty(); r.popFront()) { HeapPtr& frameobj = r.front().value(); MOZ_ASSERT(frameobj->isOnStack()); if (frameobj->hasAnyHooks()) { TraceEdge(tracer, &frameobj, "Debugger.Frame with live hooks"); } } } } void DebugAPI::slowPathTraceGeneratorFrame(JSTracer* tracer, AbstractGeneratorObject* generator) { MOZ_ASSERT(generator->realm()->isDebuggee()); // Ignore generic tracers. // // There are two kinds of generic tracers we need to bar: MovingTracers used // by compacting GC; and CompartmentCheckTracers. // // MovingTracers are used by the compacting GC to update pointers to objects // that have been moved: the MovingTracer checks each outgoing pointer to see // if it refers to a forwarding pointer, and if so, updates the pointer stored // in the object. // // Generator objects are background finalized, so the compacting GC assumes it // can update their pointers in the background as well. Since we treat // generator objects as having an owning edge to their Debugger.Frame objects, // a helper thread trying to update a generator object will end up calling // this function. However, it is verboten to do weak map lookups (e.g., in // Debugger::generatorFrames) off the main thread, since StableCellHasher // must consult the Zone to find the key's unique id. // // Fortunately, it's not necessary for compacting GC to worry about that edge // in the first place: the edge isn't a literal pointer stored on the // generator object, it's only inferred from the realm's debuggee status and // its Debuggers' generatorFrames weak maps. Those get relocated when the // Debugger itself is visited, so compacting GC can just ignore this edge. // // CompartmentCheckTracers walk the graph and verify that all // cross-compartment edges are recorded in the cross-compartment wrapper // tables. But edges between Debugger.Foo objects and their referents are not // in the CCW tables, so a CrossCompartmentCheckTracers also calls // DebugAPI::edgeIsInDebuggerWeakmap to see if a given cross-compartment edge // is accounted for there. However, edgeIsInDebuggerWeakmap only handles // debugger -> debuggee edges, so it won't recognize the edge we're // potentially traversing here, from a generator object to its Debugger.Frame. // // But since the purpose of this function is to retrieve such edges, if they // exist, from the very tables that edgeIsInDebuggerWeakmap would consult, // we're at no risk of reporting edges that they do not cover. So we can // safely hide the edges from CompartmentCheckTracers. // // We can't quite recognize MovingTracers and CompartmentCheckTracers // precisely, but they're both generic tracers, so we just show them all the // door. This means the generator -> Debugger.Frame edge is going to be // invisible to some traversals. We'll cope with that when it's a problem. if (!tracer->isMarkingTracer()) { return; } mozilla::Maybe lock; GCMarker* marker = GCMarker::fromTracer(tracer); if (marker->isParallelMarking()) { // Synchronise access to generatorFrames. lock.emplace(marker->runtime()); } JS::AutoAssertNoGC nogc; for (Realm::DebuggerVectorEntry& entry : generator->realm()->getDebuggers(nogc)) { Debugger* dbg = entry.dbg.unbarrieredGet(); if (Debugger::GeneratorWeakMap::Ptr entry = dbg->generatorFrames.lookupUnbarriered(generator)) { HeapPtr& frameObj = entry->value(); if (frameObj->hasAnyHooks()) { // See comment above. TraceCrossCompartmentEdge(tracer, generator, &frameObj, "Debugger.Frame with hooks for generator"); } } } } /* static */ void DebugAPI::traceAllForMovingGC(JSTracer* trc) { JSRuntime* rt = trc->runtime(); for (Debugger* dbg : rt->debuggerList()) { dbg->traceForMovingGC(trc); } } /* * Trace all debugger-owned GC things unconditionally. This is used during * compacting GC and in minor GC: the minor GC cannot apply the weak constraints * of the full GC because it visits only part of the heap. */ void Debugger::traceForMovingGC(JSTracer* trc) { trace(trc); for (WeakGlobalObjectSet::Enum e(debuggees); !e.empty(); e.popFront()) { TraceEdge(trc, &e.mutableFront(), "Global Object"); } } /* static */ void Debugger::traceObject(JSTracer* trc, JSObject* obj) { if (Debugger* dbg = Debugger::fromJSObject(obj)) { dbg->trace(trc); } } void Debugger::trace(JSTracer* trc) { TraceEdge(trc, &object, "Debugger Object"); TraceNullableEdge(trc, &uncaughtExceptionHook, "hooks"); // Mark Debugger.Frame objects. Since the Debugger is reachable, JS could call // getNewestFrame and then walk the stack, so these are all reachable from JS. // // Note that if a Debugger.Frame has hooks set, it must be retained even if // its Debugger is unreachable, since JS could observe that its hooks did not // fire. That case is handled by DebugAPI::traceFrames. // // (We have weakly-referenced Debugger.Frame objects as well, for suspended // generator frames; these are traced via generatorFrames just below.) for (FrameMap::Range r = frames.all(); !r.empty(); r.popFront()) { HeapPtr& frameobj = r.front().value(); TraceEdge(trc, &frameobj, "live Debugger.Frame"); MOZ_ASSERT(frameobj->isOnStack()); } allocationsLog.trace(trc); forEachWeakMap([trc](auto& weakMap) { weakMap.trace(trc); }); } /* static */ void DebugAPI::traceFromRealm(JSTracer* trc, Realm* realm) { JS::AutoAssertNoGC nogc; for (Realm::DebuggerVectorEntry& entry : realm->getDebuggers(nogc)) { TraceEdge(trc, &entry.debuggerLink, "realm debugger"); } } /* static */ void DebugAPI::sweepAll(JS::GCContext* gcx) { JSRuntime* rt = gcx->runtime(); Debugger* next; for (Debugger* dbg = rt->debuggerList().getFirst(); dbg; dbg = next) { next = dbg->getNext(); // Debugger.Frames for generator calls bump the JSScript's // generatorObserverCount, so the JIT will instrument the code to notify // Debugger when the generator is resumed. When a Debugger.Frame gets GC'd, // generatorObserverCount needs to be decremented. It's much easier to do // this when we know that all parties involved - the Debugger.Frame, the // generator object, and the JSScript - have not yet been finalized. // // Since DebugAPI::sweepAll is called after everything is marked, but before // anything has been finalized, this is the perfect place to drop the count. if (dbg->zone()->isGCSweeping()) { for (Debugger::GeneratorWeakMap::Enum e(dbg->generatorFrames); !e.empty(); e.popFront()) { DebuggerFrame* frameObj = e.front().value(); if (IsAboutToBeFinalizedUnbarriered(frameObj)) { // If the DebuggerFrame is being finalized, that means either: // 1) It is not present in "frames". // 2) The Debugger itself is also being finalized. // // In the first case, passing the frame is not necessary because there // isn't a frame entry to clear, and in the second case, // removeDebuggeeGlobal below will iterate and remove the entries // anyway, so things will be cleaned up properly. Debugger::terminateDebuggerFrame(gcx, dbg, frameObj, NullFramePtr(), nullptr, &e); } } } // Detach dying debuggers and debuggees from each other. Since this // requires access to both objects it must be done before either // object is finalized. bool debuggerDying = IsAboutToBeFinalized(dbg->object); for (WeakGlobalObjectSet::Enum e(dbg->debuggees); !e.empty(); e.popFront()) { GlobalObject* global = e.front().unbarrieredGet(); if (debuggerDying || IsAboutToBeFinalizedUnbarriered(global)) { dbg->removeDebuggeeGlobal(gcx, e.front().unbarrieredGet(), &e, Debugger::FromSweep::Yes); } } if (debuggerDying) { gcx->delete_(dbg->object, dbg, MemoryUse::Debugger); } dbg = next; } } static inline bool SweepZonesInSameGroup(Zone* a, Zone* b) { // Ensure two zones are swept in the same sweep group by adding an edge // between them in each direction. return a->addSweepGroupEdgeTo(b) && b->addSweepGroupEdgeTo(a); } /* static */ bool DebugAPI::findSweepGroupEdges(JSRuntime* rt) { // Ensure that debuggers and their debuggees are finalized in the same group // by adding edges in both directions for debuggee zones. These are weak // references that are not in the cross compartment wrapper map. for (Debugger* dbg : rt->debuggerList()) { Zone* debuggerZone = dbg->object->zone(); if (!debuggerZone->isGCMarking()) { continue; } for (auto e = dbg->debuggeeZones.all(); !e.empty(); e.popFront()) { Zone* debuggeeZone = e.front(); if (!debuggeeZone->isGCMarking()) { continue; } if (!SweepZonesInSameGroup(debuggerZone, debuggeeZone)) { return false; } } } return true; } template bool DebuggerWeakMap::findSweepGroupEdges() { Zone* debuggerZone = zone(); MOZ_ASSERT(debuggerZone->isGCMarking()); for (Enum e(*this); !e.empty(); e.popFront()) { MOZ_ASSERT(e.front().value()->zone() == debuggerZone); Zone* keyZone = e.front().key()->zone(); if (keyZone->isGCMarking() && !SweepZonesInSameGroup(debuggerZone, keyZone)) { return false; } } // Add in edges for delegates, if relevant for the key type. return Base::findSweepGroupEdges(); } const JSClassOps DebuggerInstanceObject::classOps_ = { nullptr, // addProperty nullptr, // delProperty nullptr, // enumerate nullptr, // newEnumerate nullptr, // resolve nullptr, // mayResolve nullptr, // finalize nullptr, // call nullptr, // construct Debugger::traceObject, // trace }; const JSClass DebuggerInstanceObject::class_ = { "Debugger", JSCLASS_HAS_RESERVED_SLOTS(Debugger::JSSLOT_DEBUG_COUNT), &classOps_}; static_assert(Debugger::JSSLOT_DEBUG_PROTO_START == 0, "DebuggerPrototypeObject only needs slots for the proto objects"); const JSClass DebuggerPrototypeObject::class_ = { "DebuggerPrototype", JSCLASS_HAS_RESERVED_SLOTS(Debugger::JSSLOT_DEBUG_PROTO_STOP)}; static Debugger* Debugger_fromThisValue(JSContext* cx, const CallArgs& args, const char* fnname) { JSObject* thisobj = RequireObject(cx, args.thisv()); if (!thisobj) { return nullptr; } if (!thisobj->is()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, "Debugger", fnname, thisobj->getClass()->name); return nullptr; } Debugger* dbg = Debugger::fromJSObject(thisobj); MOZ_ASSERT(dbg); return dbg; } struct MOZ_STACK_CLASS Debugger::CallData { JSContext* cx; const CallArgs& args; Debugger* dbg; CallData(JSContext* cx, const CallArgs& args, Debugger* dbg) : cx(cx), args(args), dbg(dbg) {} bool getOnDebuggerStatement(); bool setOnDebuggerStatement(); bool getOnExceptionUnwind(); bool setOnExceptionUnwind(); bool getOnNewScript(); bool setOnNewScript(); bool getOnEnterFrame(); bool setOnEnterFrame(); bool getOnNativeCall(); bool setOnNativeCall(); bool getOnNewGlobalObject(); bool setOnNewGlobalObject(); bool getOnNewPromise(); bool setOnNewPromise(); bool getOnPromiseSettled(); bool setOnPromiseSettled(); bool getUncaughtExceptionHook(); bool setUncaughtExceptionHook(); bool getAllowUnobservedAsmJS(); bool setAllowUnobservedAsmJS(); bool getAllowUnobservedWasm(); bool setAllowUnobservedWasm(); bool getCollectCoverageInfo(); bool setCollectCoverageInfo(); bool getMemory(); bool addDebuggee(); bool addAllGlobalsAsDebuggees(); bool removeDebuggee(); bool removeAllDebuggees(); bool hasDebuggee(); bool getDebuggees(); bool getNewestFrame(); bool clearAllBreakpoints(); bool findScripts(); bool findSources(); bool findObjects(); bool findAllGlobals(); bool findSourceURLs(); bool makeGlobalObjectReference(); bool adoptDebuggeeValue(); bool adoptFrame(); bool adoptSource(); bool enableAsyncStack(); bool disableAsyncStack(); bool enableUnlimitedStacksCapturing(); bool disableUnlimitedStacksCapturing(); using Method = bool (CallData::*)(); template static bool ToNative(JSContext* cx, unsigned argc, Value* vp); }; template /* static */ bool Debugger::CallData::ToNative(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); Debugger* dbg = Debugger_fromThisValue(cx, args, "method"); if (!dbg) { return false; } CallData data(cx, args, dbg); return (data.*MyMethod)(); } /* static */ bool Debugger::getHookImpl(JSContext* cx, const CallArgs& args, Debugger& dbg, Hook which) { MOZ_ASSERT(which >= 0 && which < HookCount); args.rval().set(dbg.object->getReservedSlot( JSSLOT_DEBUG_HOOK_START + std::underlying_type_t(which))); return true; } /* static */ bool Debugger::setHookImpl(JSContext* cx, const CallArgs& args, Debugger& dbg, Hook which) { MOZ_ASSERT(which >= 0 && which < HookCount); if (!args.requireAtLeast(cx, "Debugger.setHook", 1)) { return false; } if (args[0].isObject()) { if (!args[0].toObject().isCallable()) { return ReportIsNotFunction(cx, args[0], args.length() - 1); } } else if (!args[0].isUndefined()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_CALLABLE_OR_UNDEFINED); return false; } uint32_t slot = JSSLOT_DEBUG_HOOK_START + std::underlying_type_t(which); RootedValue oldHook(cx, dbg.object->getReservedSlot(slot)); dbg.object->setReservedSlot(slot, args[0]); if (hookObservesAllExecution(which)) { if (!dbg.updateObservesAllExecutionOnDebuggees( cx, dbg.observesAllExecution())) { dbg.object->setReservedSlot(slot, oldHook); return false; } } Rooted debuggeeLink(cx, dbg.getDebuggeeLink()); if (dbg.hasAnyLiveHooks()) { debuggeeLink->setLinkSlot(dbg); } else { debuggeeLink->clearLinkSlot(); } args.rval().setUndefined(); return true; } /* static */ bool Debugger::getGarbageCollectionHook(JSContext* cx, const CallArgs& args, Debugger& dbg) { return getHookImpl(cx, args, dbg, OnGarbageCollection); } /* static */ bool Debugger::setGarbageCollectionHook(JSContext* cx, const CallArgs& args, Debugger& dbg) { Rooted oldHook(cx, dbg.getHook(OnGarbageCollection)); if (!setHookImpl(cx, args, dbg, OnGarbageCollection)) { // We want to maintain the invariant that the hook is always set when the // Debugger is in the runtime's list, and vice-versa, so if we return early // and don't adjust the watcher list below, we need to be sure that the // hook didn't change. MOZ_ASSERT(dbg.getHook(OnGarbageCollection) == oldHook); return false; } // Add or remove ourselves from the runtime's list of Debuggers that care // about garbage collection. JSObject* newHook = dbg.getHook(OnGarbageCollection); if (!oldHook && newHook) { cx->runtime()->onGarbageCollectionWatchers().pushBack(&dbg); } else if (oldHook && !newHook) { cx->runtime()->onGarbageCollectionWatchers().remove(&dbg); } return true; } bool Debugger::CallData::getOnDebuggerStatement() { return getHookImpl(cx, args, *dbg, OnDebuggerStatement); } bool Debugger::CallData::setOnDebuggerStatement() { return setHookImpl(cx, args, *dbg, OnDebuggerStatement); } bool Debugger::CallData::getOnExceptionUnwind() { return getHookImpl(cx, args, *dbg, OnExceptionUnwind); } bool Debugger::CallData::setOnExceptionUnwind() { return setHookImpl(cx, args, *dbg, OnExceptionUnwind); } bool Debugger::CallData::getOnNewScript() { return getHookImpl(cx, args, *dbg, OnNewScript); } bool Debugger::CallData::setOnNewScript() { return setHookImpl(cx, args, *dbg, OnNewScript); } bool Debugger::CallData::getOnNewPromise() { return getHookImpl(cx, args, *dbg, OnNewPromise); } bool Debugger::CallData::setOnNewPromise() { return setHookImpl(cx, args, *dbg, OnNewPromise); } bool Debugger::CallData::getOnPromiseSettled() { return getHookImpl(cx, args, *dbg, OnPromiseSettled); } bool Debugger::CallData::setOnPromiseSettled() { return setHookImpl(cx, args, *dbg, OnPromiseSettled); } bool Debugger::CallData::getOnEnterFrame() { return getHookImpl(cx, args, *dbg, OnEnterFrame); } bool Debugger::CallData::setOnEnterFrame() { return setHookImpl(cx, args, *dbg, OnEnterFrame); } bool Debugger::CallData::getOnNativeCall() { return getHookImpl(cx, args, *dbg, OnNativeCall); } bool Debugger::CallData::setOnNativeCall() { return setHookImpl(cx, args, *dbg, OnNativeCall); } bool Debugger::CallData::getOnNewGlobalObject() { return getHookImpl(cx, args, *dbg, OnNewGlobalObject); } bool Debugger::CallData::setOnNewGlobalObject() { 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. JSObject* newHook = dbg->getHook(OnNewGlobalObject); if (!oldHook && newHook) { cx->runtime()->onNewGlobalObjectWatchers().pushBack(dbg); } else if (oldHook && !newHook) { cx->runtime()->onNewGlobalObjectWatchers().remove(dbg); } return true; } bool Debugger::CallData::getUncaughtExceptionHook() { args.rval().setObjectOrNull(dbg->uncaughtExceptionHook); return true; } bool Debugger::CallData::setUncaughtExceptionHook() { if (!args.requireAtLeast(cx, "Debugger.set uncaughtExceptionHook", 1)) { return false; } if (!args[0].isNull() && (!args[0].isObject() || !args[0].toObject().isCallable())) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ASSIGN_FUNCTION_OR_NULL, "uncaughtExceptionHook"); return false; } dbg->uncaughtExceptionHook = args[0].toObjectOrNull(); args.rval().setUndefined(); return true; } bool Debugger::CallData::getAllowUnobservedAsmJS() { args.rval().setBoolean(dbg->allowUnobservedAsmJS); return true; } bool Debugger::CallData::setAllowUnobservedAsmJS() { if (!args.requireAtLeast(cx, "Debugger.set allowUnobservedAsmJS", 1)) { return false; } dbg->allowUnobservedAsmJS = ToBoolean(args[0]); for (WeakGlobalObjectSet::Range r = dbg->debuggees.all(); !r.empty(); r.popFront()) { GlobalObject* global = r.front(); Realm* realm = global->realm(); realm->updateDebuggerObservesAsmJS(); } args.rval().setUndefined(); return true; } bool Debugger::CallData::getAllowUnobservedWasm() { args.rval().setBoolean(dbg->allowUnobservedWasm); return true; } bool Debugger::CallData::setAllowUnobservedWasm() { if (!args.requireAtLeast(cx, "Debugger.set allowUnobservedWasm", 1)) { return false; } dbg->allowUnobservedWasm = ToBoolean(args[0]); for (WeakGlobalObjectSet::Range r = dbg->debuggees.all(); !r.empty(); r.popFront()) { GlobalObject* global = r.front(); Realm* realm = global->realm(); realm->updateDebuggerObservesWasm(); } args.rval().setUndefined(); return true; } bool Debugger::CallData::getCollectCoverageInfo() { args.rval().setBoolean(dbg->collectCoverageInfo); return true; } bool Debugger::CallData::setCollectCoverageInfo() { if (!args.requireAtLeast(cx, "Debugger.set collectCoverageInfo", 1)) { return false; } dbg->collectCoverageInfo = ToBoolean(args[0]); IsObserving observing = dbg->collectCoverageInfo ? Observing : NotObserving; if (!dbg->updateObservesCoverageOnDebuggees(cx, observing)) { return false; } args.rval().setUndefined(); return true; } bool Debugger::CallData::getMemory() { Value memoryValue = dbg->object->getReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE); if (!memoryValue.isObject()) { RootedObject memory(cx, DebuggerMemory::create(cx, dbg)); if (!memory) { return false; } memoryValue = ObjectValue(*memory); } args.rval().set(memoryValue); return true; } /* * Given a value used to designate a global (there's quite a variety; see the * docs), return the actual designee. * * Note that this does not check whether the designee is marked "invisible to * Debugger" or not; different callers need to handle invisible-to-Debugger * globals in different ways. */ GlobalObject* Debugger::unwrapDebuggeeArgument(JSContext* cx, const Value& v) { if (!v.isObject()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, "argument", "not a global object"); return nullptr; } RootedObject obj(cx, &v.toObject()); // If it's a Debugger.Object belonging to this debugger, dereference that. if (obj->getClass() == &DebuggerObject::class_) { RootedValue rv(cx, v); if (!unwrapDebuggeeValue(cx, &rv)) { return nullptr; } obj = &rv.toObject(); } // If we have a cross-compartment wrapper, dereference as far as is secure. // // Since we're dealing with globals, we may have a WindowProxy here. So we // have to make sure to do a dynamic unwrap, and we want to unwrap the // WindowProxy too, if we have one. obj = CheckedUnwrapDynamic(obj, cx, /* stopAtWindowProxy = */ false); if (!obj) { ReportAccessDenied(cx); return nullptr; } // If that didn't produce a global object, it's an error. if (!obj->is()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, "argument", "not a global object"); return nullptr; } return &obj->as(); } bool Debugger::CallData::addDebuggee() { if (!args.requireAtLeast(cx, "Debugger.addDebuggee", 1)) { return false; } Rooted global(cx, dbg->unwrapDebuggeeArgument(cx, args[0])); if (!global) { return false; } if (!dbg->addDebuggeeGlobal(cx, global)) { return false; } RootedValue v(cx, ObjectValue(*global)); if (!dbg->wrapDebuggeeValue(cx, &v)) { return false; } args.rval().set(v); return true; } bool Debugger::CallData::addAllGlobalsAsDebuggees() { for (CompartmentsIter comp(cx->runtime()); !comp.done(); comp.next()) { if (comp == dbg->object->compartment()) { continue; } for (RealmsInCompartmentIter r(comp); !r.done(); r.next()) { if (r->creationOptions().invisibleToDebugger()) { continue; } r->compartment()->gcState.scheduledForDestruction = false; GlobalObject* global = r->maybeGlobal(); if (global) { Rooted rg(cx, global); if (!dbg->addDebuggeeGlobal(cx, rg)) { return false; } } } } args.rval().setUndefined(); return true; } bool Debugger::CallData::removeDebuggee() { if (!args.requireAtLeast(cx, "Debugger.removeDebuggee", 1)) { return false; } Rooted global(cx, dbg->unwrapDebuggeeArgument(cx, args[0])); if (!global) { return false; } ExecutionObservableRealms obs(cx); if (dbg->debuggees.has(global)) { dbg->removeDebuggeeGlobal(cx->gcContext(), global, nullptr, FromSweep::No); // Only update the realm if there are no Debuggers left, as it's // expensive to check if no other Debugger has a live script or frame // hook on any of the current on-stack debuggee frames. if (!global->hasDebuggers() && !obs.add(global->realm())) { return false; } if (!updateExecutionObservability(cx, obs, NotObserving)) { return false; } } args.rval().setUndefined(); return true; } bool Debugger::CallData::removeAllDebuggees() { ExecutionObservableRealms obs(cx); for (WeakGlobalObjectSet::Enum e(dbg->debuggees); !e.empty(); e.popFront()) { Rooted global(cx, e.front()); dbg->removeDebuggeeGlobal(cx->gcContext(), global, &e, FromSweep::No); // See note about adding to the observable set in removeDebuggee. if (!global->hasDebuggers() && !obs.add(global->realm())) { return false; } } if (!updateExecutionObservability(cx, obs, NotObserving)) { return false; } args.rval().setUndefined(); return true; } bool Debugger::CallData::hasDebuggee() { if (!args.requireAtLeast(cx, "Debugger.hasDebuggee", 1)) { return false; } GlobalObject* global = dbg->unwrapDebuggeeArgument(cx, args[0]); if (!global) { return false; } args.rval().setBoolean(!!dbg->debuggees.lookup(global)); return true; } bool Debugger::CallData::getDebuggees() { // Obtain the list of debuggees before wrapping each debuggee, as a GC could // update the debuggees set while we are iterating it. unsigned count = dbg->debuggees.count(); RootedValueVector debuggees(cx); if (!debuggees.resize(count)) { return false; } unsigned i = 0; { JS::AutoCheckCannotGC nogc; for (WeakGlobalObjectSet::Enum e(dbg->debuggees); !e.empty(); e.popFront()) { debuggees[i++].setObject(*e.front().get()); } } Rooted arrobj(cx, NewDenseFullyAllocatedArray(cx, count)); if (!arrobj) { return false; } arrobj->ensureDenseInitializedLength(0, count); for (i = 0; i < count; i++) { RootedValue v(cx, debuggees[i]); if (!dbg->wrapDebuggeeValue(cx, &v)) { return false; } arrobj->setDenseElement(i, v); } args.rval().setObject(*arrobj); return true; } bool Debugger::CallData::getNewestFrame() { // Since there may be multiple contexts, use AllFramesIter. for (AllFramesIter i(cx); !i.done(); ++i) { if (dbg->observesFrame(i)) { // Ensure that Ion frames are rematerialized. Only rematerialized // Ion frames may be used as AbstractFramePtrs. if (i.isIon() && !i.ensureHasRematerializedFrame(cx)) { return false; } AbstractFramePtr frame = i.abstractFramePtr(); FrameIter iter(i.activation()->cx()); while (!iter.hasUsableAbstractFramePtr() || iter.abstractFramePtr() != frame) { ++iter; } return dbg->getFrame(cx, iter, args.rval()); } } args.rval().setNull(); return true; } bool Debugger::CallData::clearAllBreakpoints() { JS::GCContext* gcx = cx->gcContext(); Breakpoint* nextbp; for (Breakpoint* bp = dbg->firstBreakpoint(); bp; bp = nextbp) { nextbp = bp->nextInDebugger(); bp->remove(gcx); } MOZ_ASSERT(!dbg->firstBreakpoint()); return true; } /* static */ bool Debugger::construct(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // Check that the arguments, if any, are cross-compartment wrappers. for (unsigned i = 0; i < args.length(); i++) { JSObject* argobj = RequireObject(cx, args[i]); if (!argobj) { return false; } if (!argobj->is()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_CCW_REQUIRED, "Debugger"); return false; } } // Get Debugger.prototype. RootedValue v(cx); RootedObject callee(cx, &args.callee()); if (!GetProperty(cx, callee, callee, cx->names().prototype, &v)) { return false; } Rooted proto(cx, &v.toObject().as()); MOZ_ASSERT(proto->is()); // Make the new Debugger object. Each one has a reference to // Debugger.{Frame,Object,Script,Memory}.prototype in reserved slots. The // rest of the reserved slots are for hooks; they default to undefined. Rooted obj( cx, NewTenuredObjectWithGivenProto(cx, proto)); if (!obj) { return false; } for (unsigned slot = JSSLOT_DEBUG_PROTO_START; slot < JSSLOT_DEBUG_PROTO_STOP; slot++) { obj->setReservedSlot(slot, proto->getReservedSlot(slot)); } obj->setReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE, NullValue()); Rooted livenessLink( cx, NewObjectWithGivenProto(cx, nullptr)); if (!livenessLink) { return false; } obj->setReservedSlot(JSSLOT_DEBUG_DEBUGGEE_LINK, ObjectValue(*livenessLink)); Debugger* debugger; { // Construct the underlying C++ object. auto dbg = cx->make_unique(cx, obj.get()); if (!dbg) { return false; } // The object owns the released pointer. debugger = dbg.release(); InitReservedSlot(obj, JSSLOT_DEBUG_DEBUGGER, debugger, MemoryUse::Debugger); } // Add the initial debuggees, if any. for (unsigned i = 0; i < args.length(); i++) { JSObject& wrappedObj = args[i].toObject().as().private_().toObject(); Rooted debuggee(cx, &wrappedObj.nonCCWGlobal()); if (!debugger->addDebuggeeGlobal(cx, debuggee)) { return false; } } args.rval().setObject(*obj); return true; } bool Debugger::addDebuggeeGlobal(JSContext* cx, Handle global) { if (debuggees.has(global)) { return true; } // Callers should generally be unable to get a reference to a debugger- // invisible global in order to pass it to addDebuggee. But this is possible // with certain testing aides we expose in the shell, so just make addDebuggee // throw in that case. Realm* debuggeeRealm = global->realm(); if (debuggeeRealm->creationOptions().invisibleToDebugger()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_CANT_DEBUG_GLOBAL); return false; } // Debugger and debuggee must be in different compartments. if (debuggeeRealm->compartment() == object->compartment()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_SAME_COMPARTMENT); return false; } // Check for cycles. If global's realm is reachable from this Debugger // object's realm by following debuggee-to-debugger links, then adding // global would create a cycle. (Typically nobody is debugging the // debugger, in which case we zip through this code without looping.) Vector visited(cx); if (!visited.append(object->realm())) { return false; } for (size_t i = 0; i < visited.length(); i++) { Realm* realm = visited[i]; if (realm == debuggeeRealm) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_LOOP); return false; } // Find all realms containing debuggers debugging realm's global object. // Add those realms to visited. if (realm->isDebuggee()) { JS::AutoAssertNoGC nogc; for (Realm::DebuggerVectorEntry& entry : realm->getDebuggers(nogc)) { Realm* next = entry.dbg->object->realm(); if (std::find(visited.begin(), visited.end(), next) == visited.end()) { if (!visited.append(next)) { return false; } } } } } // For global to become this js::Debugger's debuggee: // // 1. this js::Debugger must be in global->getDebuggers(), // 2. global must be in this->debuggees, // 3. the debuggee's zone must be in this->debuggeeZones, // 4. if we are tracking allocations, the SavedStacksMetadataBuilder must be // installed for this realm, and // 5. Realm::isDebuggee()'s bit must be set. // // All five indications must be kept consistent. AutoRealm ar(cx, global); Zone* zone = global->zone(); RootedObject debuggeeLink(cx, getDebuggeeLink()); if (!cx->compartment()->wrap(cx, &debuggeeLink)) { return false; } // (1) JS::AutoAssertNoGC nogc; auto& globalDebuggers = global->getDebuggers(nogc); if (!globalDebuggers.append(Realm::DebuggerVectorEntry(this, debuggeeLink))) { ReportOutOfMemory(cx); return false; } auto globalDebuggersGuard = MakeScopeExit([&] { globalDebuggers.popBack(); }); // (2) if (!debuggees.put(global)) { ReportOutOfMemory(cx); return false; } auto debuggeesGuard = MakeScopeExit([&] { debuggees.remove(global); }); bool addingZoneRelation = !debuggeeZones.has(zone); // (3) if (addingZoneRelation && !debuggeeZones.put(zone)) { ReportOutOfMemory(cx); return false; } auto debuggeeZonesGuard = MakeScopeExit([&] { if (addingZoneRelation) { debuggeeZones.remove(zone); } }); // (4) if (trackingAllocationSites && !Debugger::addAllocationsTracking(cx, global)) { return false; } auto allocationsTrackingGuard = MakeScopeExit([&] { if (trackingAllocationSites) { Debugger::removeAllocationsTracking(*global); } }); // (5) AutoRestoreRealmDebugMode debugModeGuard(debuggeeRealm); debuggeeRealm->setIsDebuggee(); debuggeeRealm->updateDebuggerObservesAsmJS(); debuggeeRealm->updateDebuggerObservesWasm(); debuggeeRealm->updateDebuggerObservesCoverage(); if (observesAllExecution() && !ensureExecutionObservabilityOfRealm(cx, debuggeeRealm)) { return false; } globalDebuggersGuard.release(); debuggeesGuard.release(); debuggeeZonesGuard.release(); allocationsTrackingGuard.release(); debugModeGuard.release(); return true; } void Debugger::recomputeDebuggeeZoneSet() { AutoEnterOOMUnsafeRegion oomUnsafe; debuggeeZones.clear(); for (auto range = debuggees.all(); !range.empty(); range.popFront()) { if (!debuggeeZones.put(range.front().unbarrieredGet()->zone())) { oomUnsafe.crash("Debugger::removeDebuggeeGlobal"); } } } template static T* findDebuggerInVector(Debugger* dbg, Vector* vec) { T* p; for (p = vec->begin(); p != vec->end(); p++) { if (p->dbg == dbg) { break; } } MOZ_ASSERT(p != vec->end()); return p; } void Debugger::removeDebuggeeGlobal(JS::GCContext* gcx, GlobalObject* global, WeakGlobalObjectSet::Enum* debugEnum, FromSweep fromSweep) { // The caller might have found global by enumerating this->debuggees; if // so, use HashSet::Enum::removeFront rather than HashSet::remove below, // to avoid invalidating the live enumerator. MOZ_ASSERT(debuggees.has(global)); MOZ_ASSERT(debuggeeZones.has(global->zone())); MOZ_ASSERT_IF(debugEnum, debugEnum->front().unbarrieredGet() == global); // Clear this global's generators from generatorFrames as well. // // This method can be called either from script (dbg.removeDebuggee) or during // GC sweeping, because the Debugger, debuggee global, or both are being GC'd. // // When called from script, it's okay to iterate over generatorFrames and // touch its keys and values (even when an incremental GC is in progress). // When called from GC, it's not okay; the keys and values may be dying. But // in that case, we can actually just skip the loop entirely! If the Debugger // is going away, it doesn't care about the state of its generatorFrames // table, and the Debugger.Frame finalizer will fix up the generator observer // counts. if (fromSweep == FromSweep::No) { for (GeneratorWeakMap::Enum e(generatorFrames); !e.empty(); e.popFront()) { AbstractGeneratorObject& genObj = *e.front().key(); if (&genObj.global() == global) { terminateDebuggerFrame(gcx, this, e.front().value(), NullFramePtr(), nullptr, &e); } } } for (FrameMap::Enum e(frames); !e.empty(); e.popFront()) { AbstractFramePtr frame = e.front().key(); if (frame.hasGlobal(global)) { terminateDebuggerFrame(gcx, this, e.front().value(), frame, &e); } } JS::AutoAssertNoGC nogc; auto& globalDebuggersVector = global->getDebuggers(nogc); // The relation must be removed from up to three places: // globalDebuggersVector and debuggees for sure, and possibly the // compartment's debuggee set. // // The debuggee zone set is recomputed on demand. This avoids refcounting // and in practice we have relatively few debuggees that tend to all be in // the same zone. If after recomputing the debuggee zone set, this global's // zone is not in the set, then we must remove ourselves from the zone's // vector of observing debuggers. globalDebuggersVector.erase( findDebuggerInVector(this, &globalDebuggersVector)); if (debugEnum) { debugEnum->removeFront(); } else { debuggees.remove(global); } recomputeDebuggeeZoneSet(); // Remove all breakpoints for the debuggee. Breakpoint* nextbp; for (Breakpoint* bp = firstBreakpoint(); bp; bp = nextbp) { nextbp = bp->nextInDebugger(); if (bp->site->realm() == global->realm()) { bp->remove(gcx); } } MOZ_ASSERT_IF(debuggees.empty(), !firstBreakpoint()); // If we are tracking allocation sites, we need to remove the object // metadata callback from this global's realm. if (trackingAllocationSites) { Debugger::removeAllocationsTracking(*global); } if (!global->realm()->hasDebuggers()) { global->realm()->unsetIsDebuggee(); } else { global->realm()->updateDebuggerObservesAllExecution(); global->realm()->updateDebuggerObservesAsmJS(); global->realm()->updateDebuggerObservesWasm(); global->realm()->updateDebuggerObservesCoverage(); } } class MOZ_STACK_CLASS Debugger::QueryBase { protected: QueryBase(JSContext* cx, Debugger* dbg) : cx(cx), debugger(dbg), iterMarker(&cx->runtime()->gc), realms(cx->zone()) {} // The context in which we should do our work. JSContext* cx; // The debugger for which we conduct queries. Debugger* debugger; // Require the set of realms to stay fixed while the query is alive. gc::AutoEnterIteration iterMarker; using RealmSet = HashSet, ZoneAllocPolicy>; // A script must be in one of these realms to match the query. RealmSet realms; // Indicates whether OOM has occurred while matching. bool oom = false; bool addRealm(Realm* realm) { return realms.put(realm); } // Arrange for this query to match only scripts that run in |global|. bool matchSingleGlobal(GlobalObject* global) { MOZ_ASSERT(realms.count() == 0); if (!addRealm(global->realm())) { ReportOutOfMemory(cx); return false; } return true; } // Arrange for this ScriptQuery to match all scripts running in debuggee // globals. bool matchAllDebuggeeGlobals() { MOZ_ASSERT(realms.count() == 0); // Build our realm set from the debugger's set of debuggee globals. for (WeakGlobalObjectSet::Range r = debugger->debuggees.all(); !r.empty(); r.popFront()) { if (!addRealm(r.front()->realm())) { ReportOutOfMemory(cx); return false; } } return true; } }; /* * A class for parsing 'findScripts' query arguments and searching for * scripts that match the criteria they represent. */ class MOZ_STACK_CLASS Debugger::ScriptQuery : public Debugger::QueryBase { public: /* Construct a ScriptQuery to use matching scripts for |dbg|. */ ScriptQuery(JSContext* cx, Debugger* dbg) : QueryBase(cx, dbg), url(cx), displayURLString(cx), source(cx, AsVariant(static_cast(nullptr))), scriptVector(cx, BaseScriptVector(cx)), partialMatchVector(cx, BaseScriptVector(cx)), wasmInstanceVector(cx, WasmInstanceObjectVector(cx)) {} /* * Parse the query object |query|, and prepare to match only the scripts * it specifies. */ bool parseQuery(HandleObject query) { // Check for a 'global' property, which limits the results to those // scripts scoped to a particular global object. RootedValue global(cx); if (!GetProperty(cx, query, query, cx->names().global, &global)) { return false; } if (global.isUndefined()) { if (!matchAllDebuggeeGlobals()) { return false; } } else { GlobalObject* globalObject = debugger->unwrapDebuggeeArgument(cx, global); if (!globalObject) { return false; } // If the given global isn't a debuggee, just leave the set of // acceptable globals empty; we'll return no scripts. if (debugger->debuggees.has(globalObject)) { if (!matchSingleGlobal(globalObject)) { return false; } } } // Check for a 'url' property. if (!GetProperty(cx, query, query, cx->names().url, &url)) { return false; } if (!url.isUndefined() && !url.isString()) { JS_ReportErrorNumberASCII( cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, "query object's 'url' property", "neither undefined nor a string"); return false; } // Check for a 'source' property RootedValue debuggerSource(cx); if (!GetProperty(cx, query, query, cx->names().source, &debuggerSource)) { return false; } if (!debuggerSource.isUndefined()) { if (!debuggerSource.isObject() || !debuggerSource.toObject().is()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, "query object's 'source' property", "not undefined nor a Debugger.Source object"); return false; } DebuggerSource& debuggerSourceObj = debuggerSource.toObject().as(); // If it does have an owner, it should match the Debugger we're // calling findScripts on. It would work fine even if it didn't, // but mixing Debugger.Sources is probably a sign of confusion. if (debuggerSourceObj.owner() != debugger) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_WRONG_OWNER, "Debugger.Source"); return false; } hasSource = true; source = debuggerSourceObj.getReferent(); } // Check for a 'displayURL' property. RootedValue displayURL(cx); if (!GetProperty(cx, query, query, cx->names().displayURL, &displayURL)) { return false; } if (!displayURL.isUndefined() && !displayURL.isString()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, "query object's 'displayURL' property", "neither undefined nor a string"); return false; } if (displayURL.isString()) { displayURLString = displayURL.toString()->ensureLinear(cx); if (!displayURLString) { return false; } } // Check for a 'line' property. RootedValue lineProperty(cx); if (!GetProperty(cx, query, query, cx->names().line, &lineProperty)) { return false; } if (lineProperty.isUndefined()) { hasLine = false; } else if (lineProperty.isNumber()) { if (displayURL.isUndefined() && url.isUndefined() && !hasSource) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_QUERY_LINE_WITHOUT_URL); return false; } double doubleLine = lineProperty.toNumber(); uint32_t uintLine = (uint32_t)doubleLine; if (doubleLine <= 0 || uintLine != doubleLine) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_BAD_LINE); return false; } hasLine = true; line = uintLine; } else { JS_ReportErrorNumberASCII( cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, "query object's 'line' property", "neither undefined nor an integer"); return false; } // Check for an 'innermost' property. PropertyName* innermostName = cx->names().innermost; RootedValue innermostProperty(cx); if (!GetProperty(cx, query, query, innermostName, &innermostProperty)) { return false; } innermost = ToBoolean(innermostProperty); if (innermost) { // Technically, we need only check hasLine, but this is clearer. if ((displayURL.isUndefined() && url.isUndefined() && !hasSource) || !hasLine) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_QUERY_INNERMOST_WITHOUT_LINE_URL); return false; } } return true; } /* Set up this ScriptQuery appropriately for a missing query argument. */ bool omittedQuery() { url.setUndefined(); hasLine = false; innermost = false; displayURLString = nullptr; return matchAllDebuggeeGlobals(); } /* * Search all relevant realms and the stack for scripts matching * this query, and append the matching scripts to |scriptVector|. */ bool findScripts() { if (!prepareQuery()) { return false; } Realm* singletonRealm = nullptr; if (realms.count() == 1) { singletonRealm = realms.all().front(); } // Search each realm for debuggee scripts. MOZ_ASSERT(scriptVector.empty()); MOZ_ASSERT(partialMatchVector.empty()); oom = false; IterateScripts(cx, singletonRealm, this, considerScript); if (oom) { ReportOutOfMemory(cx); return false; } // If we are filtering by line number, the lazy BaseScripts were not checked // yet since they do not implement `GetScriptLineExtent`. Instead we revisit // each result script and delazify its children and add any matching ones to // the results list. MOZ_ASSERT(hasLine || partialMatchVector.empty()); Rooted script(cx); RootedFunction fun(cx); while (!partialMatchVector.empty()) { script = partialMatchVector.popCopy(); // As a performance optimization, we can skip scripts that are definitely // out-of-bounds for the target line. This was checked before adding to // the partialMatchVector, but the bound may have improved since then. if (script->extent().sourceEnd <= sourceOffsetLowerBound) { continue; } MOZ_ASSERT(script->isFunction()); MOZ_ASSERT(script->isReadyForDelazification()); fun = script->function(); // Ignore any delazification placeholder functions. These should not be // exposed to debugger in any way. if (fun->isGhost()) { continue; } // Delazify script. JSScript* compiledScript = GetOrCreateFunctionScript(cx, fun); if (!compiledScript) { return false; } // If target line isn't in script, we are done with it. if (!scriptIsLineMatch(compiledScript)) { continue; } // Add script to results now that we've completed checks. if (!scriptVector.append(compiledScript)) { return false; } // If script was a leaf we are done with it. This is an optional // optimization to avoid inspecting the `gcthings` list below. if (!script->hasInnerFunctions()) { continue; } // Now add inner scripts to `partialMatchVector` work list to determine if // they are matches. Note that out IterateScripts callback ignored them // already since they did not have a compiled parent at the time. for (JS::GCCellPtr thing : script->gcthings()) { if (!thing.is() || !thing.as().is()) { continue; } JSFunction* fun = &thing.as().as(); if (!fun->hasBaseScript()) { continue; } BaseScript* inner = fun->baseScript(); MOZ_ASSERT(inner); if (!inner) { // If the function doesn't have script, ignore it. continue; } if (!scriptIsPartialLineMatch(inner)) { continue; } // Add the matching inner script to the back of the results queue // where it will be processed recursively. if (!partialMatchVector.append(inner)) { return false; } } } // If this is an 'innermost' query, we want to filter the results again to // only return the innermost script for each realm. To do this we build a // hashmap to track innermost and then recreate the `scriptVector` with the // results that remain in the hashmap. if (innermost) { using RealmToScriptMap = GCHashMap>; Rooted innermostForRealm(cx, cx); // Visit each candidate script and find innermost in each realm. for (BaseScript* script : scriptVector) { Realm* realm = script->realm(); RealmToScriptMap::AddPtr p = innermostForRealm.lookupForAdd(realm); if (p) { // Is our newly found script deeper than the last one we found? BaseScript* incumbent = p->value(); if (script->asJSScript()->innermostScope()->chainLength() > incumbent->asJSScript()->innermostScope()->chainLength()) { p->value() = script; } } else { // This is the first matching script we've encountered for this // realm, so it is thus the innermost such script. if (!innermostForRealm.add(p, realm, script)) { return false; } } } // Reset the results vector. scriptVector.clear(); // Re-add only the innermost scripts to the results. for (RealmToScriptMap::Range r = innermostForRealm.all(); !r.empty(); r.popFront()) { if (!scriptVector.append(r.front().value())) { return false; } } } // TODO: Until such time that wasm modules are real ES6 modules, // unconditionally consider all wasm toplevel instance scripts. for (WeakGlobalObjectSet::Range r = debugger->allDebuggees(); !r.empty(); r.popFront()) { for (wasm::Instance* instance : r.front()->realm()->wasm.instances()) { consider(instance->object()); if (oom) { ReportOutOfMemory(cx); return false; } } } return true; } Handle foundScripts() const { return scriptVector; } Handle foundWasmInstances() const { return wasmInstanceVector; } private: /* If this is a string, matching scripts have urls equal to it. */ RootedValue url; /* url as a C string. */ UniqueChars urlCString; /* If this is a string, matching scripts' sources have displayURLs equal to * it. */ Rooted displayURLString; /* * If this is a source referent, matching scripts will have sources equal * to this instance. Ideally we'd use a Maybe here, but Maybe interacts * very badly with Rooted's LIFO invariant. */ bool hasSource = false; Rooted source; /* True if the query contained a 'line' property. */ bool hasLine = false; /* The line matching scripts must cover. */ uint32_t line = 0; // As a performance optimization (and to avoid delazifying as many scripts), // we would like to know the source offset of the target line. // // Since we do not have a simple way to compute this precisely, we instead // track a lower-bound of the offset value. As we collect SourceExtent // examples with (line,column) <-> sourceStart mappings, we can improve the // bound. The target line is within the range [sourceOffsetLowerBound, Inf). // // NOTE: Using a SourceExtent for updating the bound happens independently of // if the script matches the target line or not in the in the end. mutable uint32_t sourceOffsetLowerBound = 0; /* True if the query has an 'innermost' property whose value is true. */ bool innermost = false; /* * Accumulate the scripts in an Rooted instead of creating * the JS array as we go, because we mustn't allocate JS objects or GC while * we use the CellIter. */ Rooted scriptVector; /* * While in the CellIter we may find BaseScripts that need to be compiled * before the query can be fully checked. Since we cannot compile while under * CellIter we accumulate them here instead. * * This occurs when matching line numbers since `GetScriptLineExtent` cannot * be computed without bytecode existing. */ Rooted partialMatchVector; /* * Like above, but for wasm modules. */ Rooted wasmInstanceVector; /* * Given that parseQuery or omittedQuery has been called, prepare to match * scripts. Set urlCString and displayURLChars as appropriate. */ bool prepareQuery() { // Compute urlCString and displayURLChars, if a url or displayURL was // given respectively. if (url.isString()) { Rooted str(cx, url.toString()); urlCString = JS_EncodeStringToUTF8(cx, str); if (!urlCString) { return false; } } return true; } void updateSourceOffsetLowerBound(const SourceExtent& extent) { // We trying to find the offset of (target-line, 0) so just ignore any // extents on target line to keep things simple. MOZ_ASSERT(extent.lineno <= line); if (extent.lineno == line) { return; } // The extent.sourceStart position is now definitely *before* the target // line, so update sourceOffsetLowerBound if extent.sourceStart is a tighter // bound. if (extent.sourceStart > sourceOffsetLowerBound) { sourceOffsetLowerBound = extent.sourceStart; } } // A partial match is a script that starts before the target line, but may or // may not end before it. If we can prove the script definitely ends before // the target line, we may return false here. bool scriptIsPartialLineMatch(BaseScript* script) { const SourceExtent& extent = script->extent(); // Check that start of script is before or on target line. if (extent.lineno > line) { return false; } // Use the implicit (line, column) <-> sourceStart mapping from the // SourceExtent to update our bounds on possible matches. We call this // without knowing if the script is a match or not. updateSourceOffsetLowerBound(script->extent()); // As an optional performance optimization, we rule out any script that ends // before the lower-bound on where target line exists. return extent.sourceEnd > sourceOffsetLowerBound; } // True if any part of script source is on the target line. bool scriptIsLineMatch(JSScript* script) { MOZ_ASSERT(scriptIsPartialLineMatch(script)); uint32_t lineCount = GetScriptLineExtent(script); return (script->lineno() + lineCount > line); } static void considerScript(JSRuntime* rt, void* data, BaseScript* script, const JS::AutoRequireNoGC& nogc) { ScriptQuery* self = static_cast(data); self->consider(script, nogc); } template [[nodiscard]] bool commonFilter(T script, const JS::AutoRequireNoGC& nogc) { if (urlCString) { bool gotFilename = false; if (script->filename() && strcmp(script->filename(), urlCString.get()) == 0) { gotFilename = true; } bool gotSourceURL = false; if (!gotFilename && script->scriptSource()->introducerFilename() && strcmp(script->scriptSource()->introducerFilename(), urlCString.get()) == 0) { gotSourceURL = true; } if (!gotFilename && !gotSourceURL) { return false; } } if (displayURLString) { if (!script->scriptSource() || !script->scriptSource()->hasDisplayURL()) { return false; } const char16_t* s = script->scriptSource()->displayURL(); if (CompareChars(s, js_strlen(s), displayURLString) != 0) { return false; } } if (hasSource && !(source.is() && source.as()->source() == script->scriptSource())) { return false; } return true; } /* * If |script| matches this query, append it to |scriptVector|. Set |oom| if * an out of memory condition occurred. */ void consider(BaseScript* script, const JS::AutoRequireNoGC& nogc) { if (oom || script->selfHosted()) { return; } Realm* realm = script->realm(); if (!realms.has(realm)) { return; } if (!commonFilter(script, nogc)) { return; } bool partial = false; if (hasLine) { if (!scriptIsPartialLineMatch(script)) { return; } if (script->hasBytecode()) { // Check if line is within script (or any of its inner scripts). if (!scriptIsLineMatch(script->asJSScript())) { return; } } else { // GetScriptLineExtent is not available on lazy scripts so instead to // the partial match list for be compiled and reprocessed later. We only // add scripts that are ready for delazification and they may in turn // process their inner functions. if (!script->isReadyForDelazification()) { return; } partial = true; } } // If innermost filter is required, we collect everything that matches the // line number and filter at the end of `findScripts`. MOZ_ASSERT_IF(innermost, hasLine); Rooted& vec = partial ? partialMatchVector : scriptVector; if (!vec.append(script)) { oom = true; } } /* * If |instanceObject| matches this query, append it to |wasmInstanceVector|. * Set |oom| if an out of memory condition occurred. */ void consider(WasmInstanceObject* instanceObject) { if (oom) { return; } if (hasSource && source != AsVariant(instanceObject)) { return; } if (!wasmInstanceVector.append(instanceObject)) { oom = true; } } }; bool Debugger::CallData::findScripts() { ScriptQuery query(cx, dbg); if (args.length() >= 1) { RootedObject queryObject(cx, RequireObject(cx, args[0])); if (!queryObject || !query.parseQuery(queryObject)) { return false; } } else { if (!query.omittedQuery()) { return false; } } if (!query.findScripts()) { return false; } Handle scripts(query.foundScripts()); Handle wasmInstances(query.foundWasmInstances()); size_t resultLength = scripts.length() + wasmInstances.length(); Rooted result(cx, NewDenseFullyAllocatedArray(cx, resultLength)); if (!result) { return false; } result->ensureDenseInitializedLength(0, resultLength); for (size_t i = 0; i < scripts.length(); i++) { JSObject* scriptObject = dbg->wrapScript(cx, scripts[i]); if (!scriptObject) { return false; } result->setDenseElement(i, ObjectValue(*scriptObject)); } size_t wasmStart = scripts.length(); for (size_t i = 0; i < wasmInstances.length(); i++) { JSObject* scriptObject = dbg->wrapWasmScript(cx, wasmInstances[i]); if (!scriptObject) { return false; } result->setDenseElement(wasmStart + i, ObjectValue(*scriptObject)); } args.rval().setObject(*result); return true; } /* * A class for searching sources for 'findSources'. */ class MOZ_STACK_CLASS Debugger::SourceQuery : public Debugger::QueryBase { public: using SourceSet = JS::GCHashSet, ZoneAllocPolicy>; SourceQuery(JSContext* cx, Debugger* dbg) : QueryBase(cx, dbg), sources(cx, SourceSet(cx->zone())) {} bool findSources() { if (!matchAllDebuggeeGlobals()) { return false; } Realm* singletonRealm = nullptr; if (realms.count() == 1) { singletonRealm = realms.all().front(); } // Search each realm for debuggee scripts. MOZ_ASSERT(sources.empty()); oom = false; IterateScripts(cx, singletonRealm, this, considerScript); if (oom) { ReportOutOfMemory(cx); return false; } // TODO: Until such time that wasm modules are real ES6 modules, // unconditionally consider all wasm toplevel instance scripts. for (WeakGlobalObjectSet::Range r = debugger->allDebuggees(); !r.empty(); r.popFront()) { for (wasm::Instance* instance : r.front()->realm()->wasm.instances()) { consider(instance->object()); if (oom) { ReportOutOfMemory(cx); return false; } } } return true; } Handle foundSources() const { return sources; } private: Rooted sources; static void considerScript(JSRuntime* rt, void* data, BaseScript* script, const JS::AutoRequireNoGC& nogc) { SourceQuery* self = static_cast(data); self->consider(script, nogc); } void consider(BaseScript* script, const JS::AutoRequireNoGC& nogc) { if (oom || script->selfHosted()) { return; } Realm* realm = script->realm(); if (!realms.has(realm)) { return; } ScriptSourceObject* source = script->sourceObject(); if (!sources.put(source)) { oom = true; } } void consider(WasmInstanceObject* instanceObject) { if (oom) { return; } if (!sources.put(instanceObject)) { oom = true; } } }; static inline DebuggerSourceReferent AsSourceReferent(JSObject* obj) { if (obj->is()) { return AsVariant(&obj->as()); } return AsVariant(&obj->as()); } bool Debugger::CallData::findSources() { SourceQuery query(cx, dbg); if (!query.findSources()) { return false; } Handle sources(query.foundSources()); size_t resultLength = sources.count(); Rooted result(cx, NewDenseFullyAllocatedArray(cx, resultLength)); if (!result) { return false; } result->ensureDenseInitializedLength(0, resultLength); size_t i = 0; for (auto iter = sources.get().iter(); !iter.done(); iter.next()) { Rooted sourceReferent(cx, AsSourceReferent(iter.get())); RootedObject sourceObject(cx, dbg->wrapVariantReferent(cx, sourceReferent)); if (!sourceObject) { return false; } result->setDenseElement(i, ObjectValue(*sourceObject)); i++; } args.rval().setObject(*result); return true; } /* * A class for parsing 'findObjects' query arguments and searching for objects * that match the criteria they represent. */ class MOZ_STACK_CLASS Debugger::ObjectQuery { public: /* Construct an ObjectQuery to use matching scripts for |dbg|. */ ObjectQuery(JSContext* cx, Debugger* dbg) : objects(cx), cx(cx), dbg(dbg), className(cx) {} /* The vector that we are accumulating results in. */ RootedObjectVector objects; /* The set of debuggee compartments. */ JS::CompartmentSet debuggeeCompartments; /* * Parse the query object |query|, and prepare to match only the objects it * specifies. */ bool parseQuery(HandleObject query) { // Check for the 'class' property RootedValue cls(cx); if (!GetProperty(cx, query, query, cx->names().class_, &cls)) { return false; } if (!cls.isUndefined()) { if (!cls.isString()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, "query object's 'class' property", "neither undefined nor a string"); return false; } JSLinearString* str = cls.toString()->ensureLinear(cx); if (!str) { return false; } if (!StringIsAscii(str)) { JS_ReportErrorNumberASCII( cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, "query object's 'class' property", "not a string containing only ASCII characters"); return false; } className = cls; } return true; } /* Set up this ObjectQuery appropriately for a missing query argument. */ void omittedQuery() { className.setUndefined(); } /* * Traverse the heap to find all relevant objects and add them to the * provided vector. */ bool findObjects() { if (!prepareQuery()) { return false; } for (WeakGlobalObjectSet::Range r = dbg->allDebuggees(); !r.empty(); r.popFront()) { if (!debuggeeCompartments.put(r.front()->compartment())) { ReportOutOfMemory(cx); return false; } } { // We can't tolerate the GC moving things around while we're // searching the heap. Check that nothing we do causes a GC. RootedObject dbgObj(cx, dbg->object); JS::ubi::RootList rootList(cx); auto [ok, nogc] = rootList.init(dbgObj); if (!ok) { ReportOutOfMemory(cx); return false; } Traversal traversal(cx, *this, nogc); traversal.wantNames = false; return traversal.addStart(JS::ubi::Node(&rootList)) && traversal.traverse(); } } /* * |ubi::Node::BreadthFirst| interface. */ class NodeData {}; using Traversal = JS::ubi::BreadthFirst; bool operator()(Traversal& traversal, JS::ubi::Node origin, const JS::ubi::Edge& edge, NodeData*, bool first) { if (!first) { return true; } JS::ubi::Node referent = edge.referent; // Only follow edges within our set of debuggee compartments; we don't // care about the heap's subgraphs outside of our debuggee compartments, // so we abandon the referent. Either (1) there is not a path from this // non-debuggee node back to a node in our debuggee compartments, and we // don't need to follow edges to or from this node, or (2) there does // exist some path from this non-debuggee node back to a node in our // debuggee compartments. However, if that were true, then the incoming // cross compartment edge back into a debuggee compartment is already // listed as an edge in the RootList we started traversal with, and // therefore we don't need to follow edges to or from this non-debuggee // node. JS::Compartment* comp = referent.compartment(); if (comp && !debuggeeCompartments.has(comp)) { traversal.abandonReferent(); return true; } // If the referent has an associated realm and it's not a debuggee // realm, skip it. Don't abandonReferent() here like above: realms // within a compartment can reference each other without going through // cross-compartment wrappers. Realm* realm = referent.realm(); if (realm && !dbg->isDebuggeeUnbarriered(realm)) { return true; } // If the referent is an object and matches our query's restrictions, // add it to the vector accumulating results. Skip objects that should // never be exposed to JS, like EnvironmentObjects and internal // functions. if (!referent.is() || referent.exposeToJS().isUndefined()) { return true; } JSObject* obj = referent.as(); if (!className.isUndefined()) { const char* objClassName = obj->getClass()->name; if (strcmp(objClassName, classNameCString.get()) != 0) { return true; } } return objects.append(obj); } private: /* The context in which we should do our work. */ JSContext* cx; /* The debugger for which we conduct queries. */ Debugger* dbg; /* * If this is non-null, matching objects will have a class whose name is * this property. */ RootedValue className; /* The className member, as a C string. */ UniqueChars classNameCString; /* * Given that either omittedQuery or parseQuery has been called, prepare the * query for matching objects. */ bool prepareQuery() { if (className.isString()) { classNameCString = JS_EncodeStringToASCII(cx, className.toString()); if (!classNameCString) { return false; } } return true; } }; bool Debugger::CallData::findObjects() { ObjectQuery query(cx, dbg); if (args.length() >= 1) { RootedObject queryObject(cx, RequireObject(cx, args[0])); if (!queryObject || !query.parseQuery(queryObject)) { return false; } } else { query.omittedQuery(); } if (!query.findObjects()) { return false; } // Returning internal objects (such as self-hosting intrinsics) to JS is not // fuzzing-safe. We still want to call parseQuery/findObjects when fuzzing so // just clear the Vector here. if (fuzzingSafe) { query.objects.clear(); } size_t length = query.objects.length(); Rooted result(cx, NewDenseFullyAllocatedArray(cx, length)); if (!result) { return false; } result->ensureDenseInitializedLength(0, length); for (size_t i = 0; i < length; i++) { RootedValue debuggeeVal(cx, ObjectValue(*query.objects[i])); if (!dbg->wrapDebuggeeValue(cx, &debuggeeVal)) { return false; } result->setDenseElement(i, debuggeeVal); } args.rval().setObject(*result); return true; } bool Debugger::CallData::findAllGlobals() { RootedObjectVector globals(cx); { // Accumulate the list of globals before wrapping them, because // wrapping can GC and collect realms from under us, while iterating. JS::AutoCheckCannotGC nogc; for (RealmsIter r(cx->runtime()); !r.done(); r.next()) { if (r->creationOptions().invisibleToDebugger()) { continue; } if (!r->hasInitializedGlobal()) { continue; } if (JS::RealmBehaviorsRef(r).isNonLive()) { continue; } r->compartment()->gcState.scheduledForDestruction = false; GlobalObject* global = r->maybeGlobal(); // We pulled |global| out of nowhere, so it's possible that it was // marked gray by XPConnect. Since we're now exposing it to JS code, // we need to mark it black. JS::ExposeObjectToActiveJS(global); if (!globals.append(global)) { return false; } } } RootedObject result(cx, NewDenseEmptyArray(cx)); if (!result) { return false; } for (size_t i = 0; i < globals.length(); i++) { RootedValue globalValue(cx, ObjectValue(*globals[i])); if (!dbg->wrapDebuggeeValue(cx, &globalValue)) { return false; } if (!NewbornArrayPush(cx, result, globalValue)) { return false; } } args.rval().setObject(*result); return true; } bool Debugger::CallData::findSourceURLs() { RootedObject result(cx, NewDenseEmptyArray(cx)); if (!result) { return false; } for (WeakGlobalObjectSet::Range r = dbg->allDebuggees(); !r.empty(); r.popFront()) { RootedObject holder(cx, r.front()->getSourceURLsHolder()); if (holder) { for (size_t i = 0; i < holder->as().length(); i++) { Value v = holder->as().getDenseElement(i); // The value is an atom and doesn't need wrapping, but the holder may be // in another zone and the atom must be marked when we create a // reference in this zone. MOZ_ASSERT(v.isString() && v.toString()->isAtom()); cx->markAtomValue(v); if (!NewbornArrayPush(cx, result, v)) { return false; } } } } args.rval().setObject(*result); return true; } bool Debugger::CallData::makeGlobalObjectReference() { if (!args.requireAtLeast(cx, "Debugger.makeGlobalObjectReference", 1)) { return false; } Rooted global(cx, dbg->unwrapDebuggeeArgument(cx, args[0])); if (!global) { return false; } // If we create a D.O referring to a global in an invisible realm, // then from it we can reach function objects, scripts, environments, etc., // none of which we're ever supposed to see. if (global->realm()->creationOptions().invisibleToDebugger()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_INVISIBLE_COMPARTMENT); return false; } args.rval().setObject(*global); return dbg->wrapDebuggeeValue(cx, args.rval()); } bool Debugger::isCompilableUnit(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); if (!args.requireAtLeast(cx, "Debugger.isCompilableUnit", 1)) { return false; } if (!args[0].isString()) { JS_ReportErrorNumberASCII( cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE, "Debugger.isCompilableUnit", "string", InformalValueTypeName(args[0])); return false; } JSString* str = args[0].toString(); size_t length = str->length(); AutoStableStringChars chars(cx); if (!chars.initTwoByte(cx, str)) { return false; } bool result = true; AutoReportFrontendContext fc(cx, AutoReportFrontendContext::Warning::Suppress); CompileOptions options(cx); Rooted input(cx, frontend::CompilationInput(options)); if (!input.get().initForGlobal(&fc)) { return false; } LifoAllocScope allocScope(&cx->tempLifoAlloc()); frontend::NoScopeBindingCache scopeCache; frontend::CompilationState compilationState(&fc, allocScope, input.get()); if (!compilationState.init(&fc, &scopeCache)) { return false; } frontend::Parser parser( &fc, options, chars.twoByteChars(), length, /* foldConstants = */ true, compilationState, /* syntaxParser = */ nullptr); if (!parser.checkOptions() || !parser.parse()) { // We ran into an error. If it was because we ran out of memory we report // it in the usual way. if (fc.hadOutOfMemory()) { return false; } // If it was because we ran out of source, we return false so our caller // knows to try to collect more [source]. if (parser.isUnexpectedEOF()) { result = false; } fc.clearAutoReport(); } args.rval().setBoolean(result); return true; } bool Debugger::CallData::adoptDebuggeeValue() { if (!args.requireAtLeast(cx, "Debugger.adoptDebuggeeValue", 1)) { return false; } RootedValue v(cx, args[0]); if (v.isObject()) { RootedObject obj(cx, &v.toObject()); DebuggerObject* ndobj = ToNativeDebuggerObject(cx, &obj); if (!ndobj) { return false; } obj.set(ndobj->referent()); v = ObjectValue(*obj); if (!dbg->wrapDebuggeeValue(cx, &v)) { return false; } } args.rval().set(v); return true; } class DebuggerAdoptSourceMatcher { JSContext* cx_; Debugger* dbg_; public: explicit DebuggerAdoptSourceMatcher(JSContext* cx, Debugger* dbg) : cx_(cx), dbg_(dbg) {} using ReturnType = DebuggerSource*; ReturnType match(Handle source) { if (source->compartment() == cx_->compartment()) { JS_ReportErrorASCII(cx_, "Source is in the same compartment as this debugger"); return nullptr; } return dbg_->wrapSource(cx_, source); } ReturnType match(Handle wasmInstance) { if (wasmInstance->compartment() == cx_->compartment()) { JS_ReportErrorASCII( cx_, "WasmInstance is in the same compartment as this debugger"); return nullptr; } return dbg_->wrapWasmSource(cx_, wasmInstance); } }; bool Debugger::CallData::adoptFrame() { if (!args.requireAtLeast(cx, "Debugger.adoptFrame", 1)) { return false; } RootedObject obj(cx, RequireObject(cx, args[0])); if (!obj) { return false; } obj = UncheckedUnwrap(obj); if (!obj->is()) { JS_ReportErrorASCII(cx, "Argument is not a Debugger.Frame"); return false; } RootedValue objVal(cx, ObjectValue(*obj)); Rooted frameObj(cx, DebuggerFrame::check(cx, objVal)); if (!frameObj) { return false; } Rooted adoptedFrame(cx); if (frameObj->isOnStack()) { FrameIter iter = frameObj->getFrameIter(cx); if (!dbg->observesFrame(iter)) { JS_ReportErrorASCII(cx, "Debugger.Frame's global is not a debuggee"); return false; } if (!dbg->getFrame(cx, iter, &adoptedFrame)) { return false; } } else if (frameObj->isSuspended()) { Rooted gen(cx, &frameObj->unwrappedGenerator()); if (!dbg->observesGlobal(&gen->global())) { JS_ReportErrorASCII(cx, "Debugger.Frame's global is not a debuggee"); return false; } if (!dbg->getFrame(cx, gen, &adoptedFrame)) { return false; } } else { if (!dbg->getFrame(cx, &adoptedFrame)) { return false; } } args.rval().setObject(*adoptedFrame); return true; } bool Debugger::CallData::adoptSource() { if (!args.requireAtLeast(cx, "Debugger.adoptSource", 1)) { return false; } RootedObject obj(cx, RequireObject(cx, args[0])); if (!obj) { return false; } obj = UncheckedUnwrap(obj); if (!obj->is()) { JS_ReportErrorASCII(cx, "Argument is not a Debugger.Source"); return false; } Rooted sourceObj(cx, &obj->as()); if (!sourceObj->getReferentRawObject()) { JS_ReportErrorASCII(cx, "Argument is Debugger.Source.prototype"); return false; } Rooted referent(cx, sourceObj->getReferent()); DebuggerAdoptSourceMatcher matcher(cx, dbg); DebuggerSource* res = referent.match(matcher); if (!res) { return false; } args.rval().setObject(*res); return true; } bool Debugger::CallData::enableAsyncStack() { if (!args.requireAtLeast(cx, "Debugger.enableAsyncStack", 1)) { return false; } Rooted global(cx, dbg->unwrapDebuggeeArgument(cx, args[0])); if (!global) { return false; } global->realm()->isAsyncStackCapturingEnabled = true; args.rval().setUndefined(); return true; } bool Debugger::CallData::disableAsyncStack() { if (!args.requireAtLeast(cx, "Debugger.disableAsyncStack", 1)) { return false; } Rooted global(cx, dbg->unwrapDebuggeeArgument(cx, args[0])); if (!global) { return false; } global->realm()->isAsyncStackCapturingEnabled = false; args.rval().setUndefined(); return true; } bool Debugger::CallData::enableUnlimitedStacksCapturing() { if (!args.requireAtLeast(cx, "Debugger.enableUnlimitedStacksCapturing", 1)) { return false; } Rooted global(cx, dbg->unwrapDebuggeeArgument(cx, args[0])); if (!global) { return false; } global->realm()->isUnlimitedStacksCapturingEnabled = true; args.rval().setUndefined(); return true; } bool Debugger::CallData::disableUnlimitedStacksCapturing() { if (!args.requireAtLeast(cx, "Debugger.disableUnlimitedStacksCapturing", 1)) { return false; } Rooted global(cx, dbg->unwrapDebuggeeArgument(cx, args[0])); if (!global) { return false; } global->realm()->isUnlimitedStacksCapturingEnabled = false; args.rval().setUndefined(); return true; } const JSPropertySpec Debugger::properties[] = { JS_DEBUG_PSGS("onDebuggerStatement", getOnDebuggerStatement, setOnDebuggerStatement), JS_DEBUG_PSGS("onExceptionUnwind", getOnExceptionUnwind, setOnExceptionUnwind), JS_DEBUG_PSGS("onNewScript", getOnNewScript, setOnNewScript), JS_DEBUG_PSGS("onNewPromise", getOnNewPromise, setOnNewPromise), JS_DEBUG_PSGS("onPromiseSettled", getOnPromiseSettled, setOnPromiseSettled), JS_DEBUG_PSGS("onEnterFrame", getOnEnterFrame, setOnEnterFrame), JS_DEBUG_PSGS("onNativeCall", getOnNativeCall, setOnNativeCall), JS_DEBUG_PSGS("onNewGlobalObject", getOnNewGlobalObject, setOnNewGlobalObject), JS_DEBUG_PSGS("uncaughtExceptionHook", getUncaughtExceptionHook, setUncaughtExceptionHook), JS_DEBUG_PSGS("allowUnobservedAsmJS", getAllowUnobservedAsmJS, setAllowUnobservedAsmJS), JS_DEBUG_PSGS("allowUnobservedWasm", getAllowUnobservedWasm, setAllowUnobservedWasm), JS_DEBUG_PSGS("collectCoverageInfo", getCollectCoverageInfo, setCollectCoverageInfo), JS_DEBUG_PSG("memory", getMemory), JS_STRING_SYM_PS(toStringTag, "Debugger", JSPROP_READONLY), JS_PS_END}; const JSFunctionSpec Debugger::methods[] = { JS_DEBUG_FN("addDebuggee", addDebuggee, 1), JS_DEBUG_FN("addAllGlobalsAsDebuggees", addAllGlobalsAsDebuggees, 0), JS_DEBUG_FN("removeDebuggee", removeDebuggee, 1), JS_DEBUG_FN("removeAllDebuggees", removeAllDebuggees, 0), JS_DEBUG_FN("hasDebuggee", hasDebuggee, 1), JS_DEBUG_FN("getDebuggees", getDebuggees, 0), JS_DEBUG_FN("getNewestFrame", getNewestFrame, 0), JS_DEBUG_FN("clearAllBreakpoints", clearAllBreakpoints, 0), JS_DEBUG_FN("findScripts", findScripts, 1), JS_DEBUG_FN("findSources", findSources, 1), JS_DEBUG_FN("findObjects", findObjects, 1), JS_DEBUG_FN("findAllGlobals", findAllGlobals, 0), JS_DEBUG_FN("findSourceURLs", findSourceURLs, 0), JS_DEBUG_FN("makeGlobalObjectReference", makeGlobalObjectReference, 1), JS_DEBUG_FN("adoptDebuggeeValue", adoptDebuggeeValue, 1), JS_DEBUG_FN("adoptFrame", adoptFrame, 1), JS_DEBUG_FN("adoptSource", adoptSource, 1), JS_DEBUG_FN("enableAsyncStack", enableAsyncStack, 1), JS_DEBUG_FN("disableAsyncStack", disableAsyncStack, 1), JS_DEBUG_FN("enableUnlimitedStacksCapturing", enableUnlimitedStacksCapturing, 1), JS_DEBUG_FN("disableUnlimitedStacksCapturing", disableUnlimitedStacksCapturing, 1), JS_FS_END}; const JSFunctionSpec Debugger::static_methods[]{ JS_FN("isCompilableUnit", Debugger::isCompilableUnit, 1, 0), JS_FS_END}; DebuggerScript* Debugger::newDebuggerScript( JSContext* cx, Handle referent) { cx->check(object.get()); RootedObject proto( cx, &object->getReservedSlot(JSSLOT_DEBUG_SCRIPT_PROTO).toObject()); MOZ_ASSERT(proto); Rooted debugger(cx, object); return DebuggerScript::create(cx, proto, referent, debugger); } template typename Map::WrapperType* Debugger::wrapVariantReferent( JSContext* cx, Map& map, Handle referent) { cx->check(object); Handle untaggedReferent = referent.template as(); MOZ_ASSERT(cx->compartment() != untaggedReferent->compartment()); DependentAddPtr p(cx, map, untaggedReferent); if (!p) { typename Map::WrapperType* wrapper = newVariantWrapper(cx, referent); if (!wrapper) { return nullptr; } if (!p.add(cx, map, untaggedReferent, wrapper)) { // We need to destroy the edge to the referent, to avoid trying to trace // it during untimely collections. wrapper->clearReferent(); return nullptr; } } return &p->value()->template as(); } DebuggerScript* Debugger::wrapVariantReferent( JSContext* cx, Handle referent) { if (referent.is()) { return wrapVariantReferent(cx, scripts, referent); } return wrapVariantReferent(cx, wasmInstanceScripts, referent); } DebuggerScript* Debugger::wrapScript(JSContext* cx, Handle script) { Rooted referent(cx, DebuggerScriptReferent(script.get())); return wrapVariantReferent(cx, referent); } DebuggerScript* Debugger::wrapWasmScript( JSContext* cx, Handle wasmInstance) { Rooted referent(cx, wasmInstance.get()); return wrapVariantReferent(cx, referent); } DebuggerSource* Debugger::newDebuggerSource( JSContext* cx, Handle referent) { cx->check(object.get()); RootedObject proto( cx, &object->getReservedSlot(JSSLOT_DEBUG_SOURCE_PROTO).toObject()); MOZ_ASSERT(proto); Rooted debugger(cx, object); return DebuggerSource::create(cx, proto, referent, debugger); } DebuggerSource* Debugger::wrapVariantReferent( JSContext* cx, Handle referent) { DebuggerSource* obj; if (referent.is()) { obj = wrapVariantReferent(cx, sources, referent); } else { obj = wrapVariantReferent(cx, wasmInstanceSources, referent); } MOZ_ASSERT_IF(obj, obj->getReferent() == referent); return obj; } DebuggerSource* Debugger::wrapSource(JSContext* cx, Handle source) { Rooted referent(cx, source.get()); return wrapVariantReferent(cx, referent); } DebuggerSource* Debugger::wrapWasmSource( JSContext* cx, Handle wasmInstance) { Rooted referent(cx, wasmInstance.get()); return wrapVariantReferent(cx, referent); } bool Debugger::observesFrame(AbstractFramePtr frame) const { if (frame.isWasmDebugFrame()) { return observesWasm(frame.wasmInstance()); } return observesScript(frame.script()); } bool Debugger::observesFrame(const FrameIter& iter) const { // Skip frames not yet fully initialized during their prologue. if (iter.isInterp() && iter.isFunctionFrame()) { const Value& thisVal = iter.interpFrame()->thisArgument(); if (thisVal.isMagic() && thisVal.whyMagic() == JS_IS_CONSTRUCTING) { return false; } } if (iter.isWasm()) { // Skip frame of wasm instances we cannot observe. if (!iter.wasmDebugEnabled()) { return false; } return observesWasm(iter.wasmInstance()); } return observesScript(iter.script()); } bool Debugger::observesScript(JSScript* script) const { // Don't ever observe self-hosted scripts: the Debugger API can break // self-hosted invariants. return observesGlobal(&script->global()) && !script->selfHosted(); } bool Debugger::observesWasm(wasm::Instance* instance) const { if (!instance->debugEnabled()) { return false; } return observesGlobal(&instance->object()->global()); } /* static */ bool Debugger::replaceFrameGuts(JSContext* cx, AbstractFramePtr from, AbstractFramePtr to, ScriptFrameIter& iter) { MOZ_ASSERT(from != to); // Rekey missingScopes to maintain Debugger.Environment identity and // forward liveScopes to point to the new frame. DebugEnvironments::forwardLiveFrame(cx, from, to); // If we hit an OOM anywhere in here, we need to make sure there aren't any // Debugger.Frame objects left partially-initialized. auto terminateDebuggerFramesOnExit = MakeScopeExit([&] { terminateDebuggerFrames(cx, from); terminateDebuggerFrames(cx, to); MOZ_ASSERT(!DebugAPI::inFrameMaps(from)); MOZ_ASSERT(!DebugAPI::inFrameMaps(to)); }); // Forward live Debugger.Frame objects. Rooted frames(cx); if (!getDebuggerFrames(from, &frames)) { // An OOM here means that all Debuggers' frame maps still contain // entries for 'from' and no entries for 'to'. Since the 'from' frame // will be gone, they are removed by terminateDebuggerFramesOnExit // above. ReportOutOfMemory(cx); return false; } for (size_t i = 0; i < frames.length(); i++) { Handle frameobj = frames[i]; Debugger* dbg = frameobj->owner(); // Update frame object's ScriptFrameIter::data pointer. if (!frameobj->replaceFrameIterData(cx, iter)) { return false; } // Add the frame object with |to| as key. if (!dbg->frames.putNew(to, frameobj)) { ReportOutOfMemory(cx); return false; } // Remove the old frame entry after all fallible operations are completed // so that an OOM will be able to clean up properly. dbg->frames.remove(from); } // All frames successfuly replaced, cancel the rollback. terminateDebuggerFramesOnExit.release(); MOZ_ASSERT(!DebugAPI::inFrameMaps(from)); MOZ_ASSERT_IF(!frames.empty(), DebugAPI::inFrameMaps(to)); return true; } /* static */ bool DebugAPI::inFrameMaps(AbstractFramePtr frame) { bool foundAny = false; JS::AutoAssertNoGC nogc; Debugger::forEachOnStackDebuggerFrame( frame, nogc, [&](Debugger*, DebuggerFrame* frameobj) { foundAny = true; }); return foundAny; } /* static */ void Debugger::suspendGeneratorDebuggerFrames(JSContext* cx, AbstractFramePtr frame) { JS::GCContext* gcx = cx->gcContext(); JS::AutoAssertNoGC nogc; forEachOnStackDebuggerFrame( frame, nogc, [&](Debugger* dbg, DebuggerFrame* dbgFrame) { dbg->frames.remove(frame); #if DEBUG MOZ_ASSERT(dbgFrame->hasGeneratorInfo()); AbstractGeneratorObject& genObj = dbgFrame->unwrappedGenerator(); GeneratorWeakMap::Ptr p = dbg->generatorFrames.lookup(&genObj); MOZ_ASSERT(p); MOZ_ASSERT(p->value() == dbgFrame); #endif dbgFrame->suspend(gcx); }); } /* static */ void Debugger::terminateDebuggerFrames(JSContext* cx, AbstractFramePtr frame) { JS::GCContext* gcx = cx->gcContext(); JS::AutoAssertNoGC nogc; forEachOnStackOrSuspendedDebuggerFrame( cx, frame, nogc, [&](Debugger* dbg, DebuggerFrame* dbgFrame) { Debugger::terminateDebuggerFrame(gcx, dbg, dbgFrame, frame); }); // If this is an eval frame, then from the debugger's perspective the // script is about to be destroyed. Remove any breakpoints in it. if (frame.isEvalFrame()) { RootedScript script(cx, frame.script()); DebugScript::clearBreakpointsIn(cx->gcContext(), script, nullptr, nullptr); } } /* static */ void Debugger::terminateDebuggerFrame( JS::GCContext* gcx, Debugger* dbg, DebuggerFrame* dbgFrame, AbstractFramePtr frame, FrameMap::Enum* maybeFramesEnum, GeneratorWeakMap::Enum* maybeGeneratorFramesEnum) { // If we were not passed the frame, either we are destroying a frame early // on before it was inserted into the "frames" list, or else we are // terminating a frame from "generatorFrames" and the "frames" entries will // be cleaned up later on with a second call to this function. MOZ_ASSERT_IF(!frame, !maybeFramesEnum); MOZ_ASSERT_IF(!frame, dbgFrame->hasGeneratorInfo()); MOZ_ASSERT_IF(!dbgFrame->hasGeneratorInfo(), !maybeGeneratorFramesEnum); if (frame) { if (maybeFramesEnum) { maybeFramesEnum->removeFront(); } else { dbg->frames.remove(frame); } } if (dbgFrame->hasGeneratorInfo()) { if (maybeGeneratorFramesEnum) { maybeGeneratorFramesEnum->removeFront(); } else { dbg->generatorFrames.remove(&dbgFrame->unwrappedGenerator()); } } dbgFrame->terminate(gcx, frame); } DebuggerDebuggeeLink* Debugger::getDebuggeeLink() { return &object->getReservedSlot(JSSLOT_DEBUG_DEBUGGEE_LINK) .toObject() .as(); } void DebuggerDebuggeeLink::setLinkSlot(Debugger& dbg) { setReservedSlot(DEBUGGER_LINK_SLOT, ObjectValue(*dbg.toJSObject())); } void DebuggerDebuggeeLink::clearLinkSlot() { setReservedSlot(DEBUGGER_LINK_SLOT, UndefinedValue()); } const JSClass DebuggerDebuggeeLink::class_ = { "DebuggerDebuggeeLink", JSCLASS_HAS_RESERVED_SLOTS(RESERVED_SLOTS)}; /* static */ bool DebugAPI::handleBaselineOsr(JSContext* cx, InterpreterFrame* from, jit::BaselineFrame* to) { ScriptFrameIter iter(cx); MOZ_ASSERT(iter.abstractFramePtr() == to); return Debugger::replaceFrameGuts(cx, from, to, iter); } /* static */ bool DebugAPI::handleIonBailout(JSContext* cx, jit::RematerializedFrame* from, jit::BaselineFrame* to) { // When we return to a bailed-out Ion real frame, we must update all // Debugger.Frames that refer to its inline frames. However, since we // can't pop individual inline frames off the stack (we can only pop the // real frame that contains them all, as a unit), we cannot assume that // the frame we're dealing with is the top frame. Advance the iterator // across any inlined frames younger than |to|, the baseline frame // reconstructed during bailout from the Ion frame corresponding to // |from|. ScriptFrameIter iter(cx); while (iter.abstractFramePtr() != to) { ++iter; } return Debugger::replaceFrameGuts(cx, from, to, iter); } /* static */ void DebugAPI::handleUnrecoverableIonBailoutError( JSContext* cx, jit::RematerializedFrame* frame) { // Ion bailout can fail due to overrecursion. In such cases we cannot // honor any further Debugger hooks on the frame, and need to ensure that // its Debugger.Frame entry is cleaned up. Debugger::terminateDebuggerFrames(cx, frame); } /*** JS::dbg::Builder *******************************************************/ Builder::Builder(JSContext* cx, js::Debugger* debugger) : debuggerObject(cx, debugger->toJSObject().get()), debugger(debugger) {} #if DEBUG void Builder::assertBuilt(JSObject* obj) { // We can't use assertSameCompartment here, because that is always keyed to // some JSContext's current compartment, whereas BuiltThings can be // constructed and assigned to without respect to any particular context; // the only constraint is that they should be in their debugger's compartment. MOZ_ASSERT_IF(obj, debuggerObject->compartment() == obj->compartment()); } #endif bool Builder::Object::definePropertyToTrusted(JSContext* cx, const char* name, JS::MutableHandleValue trusted) { // We should have checked for false Objects before calling this. MOZ_ASSERT(value); JSAtom* atom = Atomize(cx, name, strlen(name)); if (!atom) { return false; } RootedId id(cx, AtomToId(atom)); return DefineDataProperty(cx, value, id, trusted); } bool Builder::Object::defineProperty(JSContext* cx, const char* name, JS::HandleValue propval_) { AutoRealm ar(cx, debuggerObject()); RootedValue propval(cx, propval_); if (!debugger()->wrapDebuggeeValue(cx, &propval)) { return false; } return definePropertyToTrusted(cx, name, &propval); } bool Builder::Object::defineProperty(JSContext* cx, const char* name, JS::HandleObject propval_) { RootedValue propval(cx, ObjectOrNullValue(propval_)); return defineProperty(cx, name, propval); } bool Builder::Object::defineProperty(JSContext* cx, const char* name, Builder::Object& propval_) { AutoRealm ar(cx, debuggerObject()); RootedValue propval(cx, ObjectOrNullValue(propval_.value)); return definePropertyToTrusted(cx, name, &propval); } Builder::Object Builder::newObject(JSContext* cx) { AutoRealm ar(cx, debuggerObject); Rooted obj(cx, NewPlainObject(cx)); // If the allocation failed, this will return a false Object, as the spec // promises. return Object(cx, *this, obj); } /*** JS::dbg::AutoEntryMonitor **********************************************/ AutoEntryMonitor::AutoEntryMonitor(JSContext* cx) : cx_(cx), savedMonitor_(cx->entryMonitor) { cx->entryMonitor = this; } AutoEntryMonitor::~AutoEntryMonitor() { cx_->entryMonitor = savedMonitor_; } /*** Glue *******************************************************************/ extern JS_PUBLIC_API bool JS_DefineDebuggerObject(JSContext* cx, HandleObject obj) { Rooted debugCtor(cx), debugProto(cx), frameProto(cx), scriptProto(cx), sourceProto(cx), objectProto(cx), envProto(cx), memoryProto(cx); RootedObject debuggeeWouldRunProto(cx); RootedValue debuggeeWouldRunCtor(cx); Handle global = obj.as(); debugProto = InitClass(cx, global, &DebuggerPrototypeObject::class_, nullptr, "Debugger", Debugger::construct, 1, Debugger::properties, Debugger::methods, nullptr, Debugger::static_methods, debugCtor.address()); if (!debugProto) { return false; } frameProto = DebuggerFrame::initClass(cx, global, debugCtor); if (!frameProto) { return false; } scriptProto = DebuggerScript::initClass(cx, global, debugCtor); if (!scriptProto) { return false; } sourceProto = DebuggerSource::initClass(cx, global, debugCtor); if (!sourceProto) { return false; } objectProto = DebuggerObject::initClass(cx, global, debugCtor); if (!objectProto) { return false; } envProto = DebuggerEnvironment::initClass(cx, global, debugCtor); if (!envProto) { return false; } memoryProto = InitClass( cx, debugCtor, nullptr, nullptr, "Memory", DebuggerMemory::construct, 0, DebuggerMemory::properties, DebuggerMemory::methods, nullptr, nullptr); if (!memoryProto) { return false; } debuggeeWouldRunProto = GlobalObject::getOrCreateCustomErrorPrototype( cx, global, JSEXN_DEBUGGEEWOULDRUN); if (!debuggeeWouldRunProto) { return false; } debuggeeWouldRunCtor = ObjectValue(global->getConstructor(JSProto_DebuggeeWouldRun)); RootedId debuggeeWouldRunId( cx, NameToId(ClassName(JSProto_DebuggeeWouldRun, cx))); if (!DefineDataProperty(cx, debugCtor, debuggeeWouldRunId, debuggeeWouldRunCtor, 0)) { return false; } debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_FRAME_PROTO, ObjectValue(*frameProto)); debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_OBJECT_PROTO, ObjectValue(*objectProto)); debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_SCRIPT_PROTO, ObjectValue(*scriptProto)); debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_SOURCE_PROTO, ObjectValue(*sourceProto)); debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_ENV_PROTO, ObjectValue(*envProto)); debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_MEMORY_PROTO, ObjectValue(*memoryProto)); return true; } JS_PUBLIC_API bool JS::dbg::IsDebugger(JSObject& obj) { /* We only care about debugger objects, so CheckedUnwrapStatic is OK. */ JSObject* unwrapped = CheckedUnwrapStatic(&obj); if (!unwrapped || !unwrapped->is()) { return false; } MOZ_ASSERT(js::Debugger::fromJSObject(unwrapped)); return true; } JS_PUBLIC_API bool JS::dbg::GetDebuggeeGlobals( JSContext* cx, JSObject& dbgObj, MutableHandleObjectVector vector) { MOZ_ASSERT(IsDebugger(dbgObj)); /* Since we know we have a debugger object, CheckedUnwrapStatic is fine. */ js::Debugger* dbg = js::Debugger::fromJSObject(CheckedUnwrapStatic(&dbgObj)); if (!vector.reserve(vector.length() + dbg->debuggees.count())) { JS_ReportOutOfMemory(cx); return false; } for (WeakGlobalObjectSet::Range r = dbg->allDebuggees(); !r.empty(); r.popFront()) { vector.infallibleAppend(static_cast(r.front())); } return true; } #ifdef DEBUG /* static */ bool Debugger::isDebuggerCrossCompartmentEdge(JSObject* obj, const gc::Cell* target) { MOZ_ASSERT(target); const gc::Cell* referent = nullptr; if (obj->is()) { referent = obj->as().getReferentCell(); } else if (obj->is()) { referent = obj->as().getReferentRawObject(); } else if (obj->is()) { referent = obj->as().referent(); } else if (obj->is()) { referent = obj->as().referent(); } return referent == target; } static void CheckDebuggeeThingRealm(Realm* realm, bool invisibleOk) { MOZ_ASSERT_IF(!invisibleOk, !realm->creationOptions().invisibleToDebugger()); } void js::CheckDebuggeeThing(BaseScript* script, bool invisibleOk) { CheckDebuggeeThingRealm(script->realm(), invisibleOk); } void js::CheckDebuggeeThing(JSObject* obj, bool invisibleOk) { if (Realm* realm = JS::GetObjectRealmOrNull(obj)) { CheckDebuggeeThingRealm(realm, invisibleOk); } } #endif // DEBUG /*** JS::dbg::GarbageCollectionEvent ****************************************/ namespace JS { namespace dbg { /* static */ GarbageCollectionEvent::Ptr GarbageCollectionEvent::Create( JSRuntime* rt, ::js::gcstats::Statistics& stats, uint64_t gcNumber) { auto data = MakeUnique(gcNumber); if (!data) { return nullptr; } data->nonincrementalReason = stats.nonincrementalReason(); for (auto& slice : stats.slices()) { if (!data->reason) { // There is only one GC reason for the whole cycle, but for legacy // reasons this data is stored and replicated on each slice. Each // slice used to have its own GCReason, but now they are all the // same. data->reason = ExplainGCReason(slice.reason); MOZ_ASSERT(data->reason); } if (!data->collections.growBy(1)) { return nullptr; } data->collections.back().startTimestamp = slice.start; data->collections.back().endTimestamp = slice.end; } return data; } static bool DefineStringProperty(JSContext* cx, HandleObject obj, PropertyName* propName, const char* strVal) { RootedValue val(cx, UndefinedValue()); if (strVal) { JSAtom* atomized = Atomize(cx, strVal, strlen(strVal)); if (!atomized) { return false; } val = StringValue(atomized); } return DefineDataProperty(cx, obj, propName, val); } JSObject* GarbageCollectionEvent::toJSObject(JSContext* cx) const { RootedObject obj(cx, NewPlainObject(cx)); RootedValue gcCycleNumberVal(cx, NumberValue(majorGCNumber_)); if (!obj || !DefineStringProperty(cx, obj, cx->names().nonincrementalReason, nonincrementalReason) || !DefineStringProperty(cx, obj, cx->names().reason, reason) || !DefineDataProperty(cx, obj, cx->names().gcCycleNumber, gcCycleNumberVal)) { return nullptr; } Rooted slicesArray(cx, NewDenseEmptyArray(cx)); if (!slicesArray) { return nullptr; } TimeStamp originTime = TimeStamp::ProcessCreation(); size_t idx = 0; for (auto range = collections.all(); !range.empty(); range.popFront()) { Rooted collectionObj(cx, NewPlainObject(cx)); if (!collectionObj) { return nullptr; } RootedValue start(cx), end(cx); start = NumberValue( (range.front().startTimestamp - originTime).ToMilliseconds()); end = NumberValue((range.front().endTimestamp - originTime).ToMilliseconds()); if (!DefineDataProperty(cx, collectionObj, cx->names().startTimestamp, start) || !DefineDataProperty(cx, collectionObj, cx->names().endTimestamp, end)) { return nullptr; } RootedValue collectionVal(cx, ObjectValue(*collectionObj)); if (!DefineDataElement(cx, slicesArray, idx++, collectionVal)) { return nullptr; } } RootedValue slicesValue(cx, ObjectValue(*slicesArray)); if (!DefineDataProperty(cx, obj, cx->names().collections, slicesValue)) { return nullptr; } return obj; } JS_PUBLIC_API bool FireOnGarbageCollectionHookRequired(JSContext* cx) { AutoCheckCannotGC noGC; for (auto& dbg : cx->runtime()->onGarbageCollectionWatchers()) { MOZ_ASSERT(dbg.getHook(Debugger::OnGarbageCollection)); if (dbg.observedGC(cx->runtime()->gc.majorGCCount())) { return true; } } return false; } JS_PUBLIC_API bool FireOnGarbageCollectionHook( JSContext* cx, JS::dbg::GarbageCollectionEvent::Ptr&& data) { RootedObjectVector triggered(cx); { // We had better not GC (and potentially get a dangling Debugger // pointer) while finding all Debuggers observing a debuggee that // participated in this GC. AutoCheckCannotGC noGC; for (auto& dbg : cx->runtime()->onGarbageCollectionWatchers()) { MOZ_ASSERT(dbg.getHook(Debugger::OnGarbageCollection)); if (dbg.observedGC(data->majorGCNumber())) { if (!triggered.append(dbg.object)) { JS_ReportOutOfMemory(cx); return false; } } } } for (; !triggered.empty(); triggered.popBack()) { Debugger* dbg = Debugger::fromJSObject(triggered.back()); if (dbg->getHook(Debugger::OnGarbageCollection)) { (void)dbg->enterDebuggerHook(cx, [&]() -> bool { return dbg->fireOnGarbageCollectionHook(cx, data); }); MOZ_ASSERT(!cx->isExceptionPending()); } } return true; } } // namespace dbg } // namespace JS