diff options
Diffstat (limited to '')
26 files changed, 19874 insertions, 0 deletions
diff --git a/js/src/debugger/DebugAPI-inl.h b/js/src/debugger/DebugAPI-inl.h new file mode 100644 index 0000000000..3d63e37dad --- /dev/null +++ b/js/src/debugger/DebugAPI-inl.h @@ -0,0 +1,180 @@ +/* -*- 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/. */ + +#ifndef debugger_DebugAPI_inl_h +#define debugger_DebugAPI_inl_h + +#include "debugger/DebugAPI.h" + +#include "vm/GeneratorObject.h" +#include "vm/PromiseObject.h" // js::PromiseObject + +#include "vm/Stack-inl.h" + +namespace js { + +/* static */ +bool DebugAPI::stepModeEnabled(JSScript* script) { + return script->hasDebugScript() && stepModeEnabledSlow(script); +} + +/* static */ +bool DebugAPI::hasBreakpointsAt(JSScript* script, jsbytecode* pc) { + return script->hasDebugScript() && hasBreakpointsAtSlow(script, pc); +} + +/* static */ +bool DebugAPI::hasAnyBreakpointsOrStepMode(JSScript* script) { + return script->hasDebugScript(); +} + +/* static */ +void DebugAPI::onNewGlobalObject(JSContext* cx, Handle<GlobalObject*> global) { + MOZ_ASSERT(!global->realm()->firedOnNewGlobalObject); +#ifdef DEBUG + global->realm()->firedOnNewGlobalObject = true; +#endif + if (!cx->runtime()->onNewGlobalObjectWatchers().isEmpty()) { + slowPathOnNewGlobalObject(cx, global); + } +} + +/* static */ +void DebugAPI::notifyParticipatesInGC(GlobalObject* global, + uint64_t majorGCNumber) { + Realm::DebuggerVector& dbgs = global->getDebuggers(); + if (!dbgs.empty()) { + slowPathNotifyParticipatesInGC(majorGCNumber, dbgs); + } +} + +/* static */ +bool DebugAPI::onLogAllocationSite(JSContext* cx, JSObject* obj, + Handle<SavedFrame*> frame, + mozilla::TimeStamp when) { + Realm::DebuggerVector& dbgs = cx->global()->getDebuggers(); + if (dbgs.empty()) { + return true; + } + RootedObject hobj(cx, obj); + return slowPathOnLogAllocationSite(cx, hobj, frame, when, dbgs); +} + +/* static */ +bool DebugAPI::onLeaveFrame(JSContext* cx, AbstractFramePtr frame, + const jsbytecode* pc, bool ok) { + MOZ_ASSERT_IF(frame.isInterpreterFrame(), + frame.asInterpreterFrame() == cx->interpreterFrame()); + MOZ_ASSERT_IF(frame.hasScript() && frame.script()->isDebuggee(), + frame.isDebuggee()); + /* Traps must be cleared from eval frames, see slowPathOnLeaveFrame. */ + mozilla::DebugOnly<bool> evalTraps = + frame.isEvalFrame() && frame.script()->hasDebugScript(); + MOZ_ASSERT_IF(evalTraps, frame.isDebuggee()); + if (frame.isDebuggee()) { + ok = slowPathOnLeaveFrame(cx, frame, pc, ok); + } + MOZ_ASSERT(!inFrameMaps(frame)); + return ok; +} + +/* static */ +bool DebugAPI::onNewGenerator(JSContext* cx, AbstractFramePtr frame, + Handle<AbstractGeneratorObject*> genObj) { + if (frame.isDebuggee()) { + return slowPathOnNewGenerator(cx, frame, genObj); + } + return true; +} + +/* static */ +bool DebugAPI::checkNoExecute(JSContext* cx, HandleScript script) { + if (!cx->realm()->isDebuggee() || !cx->noExecuteDebuggerTop) { + return true; + } + return slowPathCheckNoExecute(cx, script); +} + +/* static */ +bool DebugAPI::onEnterFrame(JSContext* cx, AbstractFramePtr frame) { + MOZ_ASSERT_IF(frame.hasScript() && frame.script()->isDebuggee(), + frame.isDebuggee()); + if (MOZ_UNLIKELY(frame.isDebuggee())) { + return slowPathOnEnterFrame(cx, frame); + } + return true; +} + +/* static */ +bool DebugAPI::onResumeFrame(JSContext* cx, AbstractFramePtr frame) { + MOZ_ASSERT_IF(frame.hasScript() && frame.script()->isDebuggee(), + frame.isDebuggee()); + if (MOZ_UNLIKELY(frame.isDebuggee())) { + return slowPathOnResumeFrame(cx, frame); + } + return true; +} + +/* static */ +NativeResumeMode DebugAPI::onNativeCall(JSContext* cx, const CallArgs& args, + CallReason reason) { + if (MOZ_UNLIKELY(cx->realm()->isDebuggee())) { + return slowPathOnNativeCall(cx, args, reason); + } + + return NativeResumeMode::Continue; +} + +/* static */ +bool DebugAPI::onDebuggerStatement(JSContext* cx, AbstractFramePtr frame) { + if (MOZ_UNLIKELY(cx->realm()->isDebuggee())) { + return slowPathOnDebuggerStatement(cx, frame); + } + + return true; +} + +/* static */ +bool DebugAPI::onExceptionUnwind(JSContext* cx, AbstractFramePtr frame) { + if (MOZ_UNLIKELY(cx->realm()->isDebuggee())) { + return slowPathOnExceptionUnwind(cx, frame); + } + return true; +} + +/* static */ +void DebugAPI::onNewWasmInstance(JSContext* cx, + Handle<WasmInstanceObject*> wasmInstance) { + if (cx->realm()->isDebuggee()) { + slowPathOnNewWasmInstance(cx, wasmInstance); + } +} + +/* static */ +void DebugAPI::onNewPromise(JSContext* cx, Handle<PromiseObject*> promise) { + if (MOZ_UNLIKELY(cx->realm()->isDebuggee())) { + slowPathOnNewPromise(cx, promise); + } +} + +/* static */ +void DebugAPI::onPromiseSettled(JSContext* cx, Handle<PromiseObject*> promise) { + if (MOZ_UNLIKELY(promise->realm()->isDebuggee())) { + slowPathOnPromiseSettled(cx, promise); + } +} + +/* static */ +void DebugAPI::traceGeneratorFrame(JSTracer* tracer, + AbstractGeneratorObject* generator) { + if (MOZ_UNLIKELY(generator->realm()->isDebuggee())) { + slowPathTraceGeneratorFrame(tracer, generator); + } +} + +} // namespace js + +#endif /* debugger_DebugAPI_inl_h */ diff --git a/js/src/debugger/DebugAPI.h b/js/src/debugger/DebugAPI.h new file mode 100644 index 0000000000..9b3356ad86 --- /dev/null +++ b/js/src/debugger/DebugAPI.h @@ -0,0 +1,414 @@ +/* -*- 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/. */ + +#ifndef debugger_DebugAPI_h +#define debugger_DebugAPI_h + +#include "vm/GlobalObject.h" +#include "vm/Interpreter.h" +#include "vm/JSContext.h" +#include "vm/Realm.h" + +namespace js { + +// This file contains the API which SpiderMonkey should use to interact with any +// active Debuggers. + +class AbstractGeneratorObject; +class DebugScriptMap; +class PromiseObject; + +/** + * DebugAPI::onNativeCall allows the debugger to call callbacks just before + * some native functions are to be executed. It also allows the hooks + * themselves to affect the result of the call. This enum represents the + * various affects that DebugAPI::onNativeCall may perform. + */ +enum class NativeResumeMode { + /** + * If the debugger hook did not return a value to manipulate the result of + * the native call, execution can continue unchanged. + * + * Continue indicates that the native function should execute normally. + */ + Continue, + + /** + * If the debugger hook returned an explicit return value that is meant to + * take the place of the native call's result, execution of the native + * function needs to be skipped in favor of the explicit result. + * + * Override indicates that the native function should be skipped and that + * the debugger has already stored the return value into the CallArgs. + */ + Override, + + /** + * If the debugger hook returns an explicit termination or an explicit + * thrown exception, execution of the native function needs to be skipped + * in favor of handling the error condition. + * + * Abort indicates that the native function should be skipped and that + * execution should be terminated. The debugger may or may not have set a + * pending exception. + */ + Abort, +}; + +class DebugScript; +class DebuggerVector; + +class DebugAPI { + public: + friend class Debugger; + + /*** Methods for interaction with the GC. ***********************************/ + + /* + * Trace (inferred) owning edges from stack frames to Debugger.Frames, as part + * of root marking. + * + * Even if a Debugger.Frame for a live stack frame is entirely unreachable + * from JS, if it has onStep or onPop hooks set, then collecting it would have + * observable side effects - namely, the hooks would fail to run. The effect + * is the same as if the stack frame held an owning edge to its + * Debugger.Frame. + * + * Debugger.Frames must also be retained if the Debugger to which they belong + * is reachable, even if they have no hooks set, but we handle that elsewhere; + * this function is only concerned with the inferred roots from stack frames + * to Debugger.Frames that have hooks set. + */ + static void traceFramesWithLiveHooks(JSTracer* tracer); + + /* + * Trace (inferred) owning edges from generator objects to Debugger.Frames. + * + * Even if a Debugger.Frame for a live suspended generator object is entirely + * unreachable from JS, if it has onStep or onPop hooks set, then collecting + * it would have observable side effects - namely, the hooks would fail to run + * if the generator is resumed. The effect is the same as if the generator + * object held an owning edge to its Debugger.Frame. + */ + static inline void traceGeneratorFrame(JSTracer* tracer, + AbstractGeneratorObject* generator); + + // Trace cross compartment edges in all debuggers relevant to the current GC. + static void traceCrossCompartmentEdges(JSTracer* tracer); + + // Trace all debugger-owned GC things unconditionally, during a moving GC. + static void traceAllForMovingGC(JSTracer* trc); + + // Trace the debug script map. Called as part of tracing a zone's roots. + static void traceDebugScriptMap(JSTracer* trc, DebugScriptMap* map); + + static void traceFromRealm(JSTracer* trc, Realm* realm); + + // The garbage collector calls this after everything has been marked, but + // before anything has been finalized. We use this to clear Debugger / + // debuggee edges at a point where the parties concerned are all still + // initialized. This does not update edges to moved GC things which is handled + // via the other trace methods. + static void sweepAll(JS::GCContext* gcx); + + // Add sweep group edges due to the presence of any debuggers. + [[nodiscard]] static bool findSweepGroupEdges(JSRuntime* rt); + + // Remove the debugging information associated with a script. + static void removeDebugScript(JS::GCContext* gcx, JSScript* script); + + // Delete a Zone's debug script map. Called when a zone is destroyed. + static void deleteDebugScriptMap(DebugScriptMap* map); + + // Validate the debugging information in a script after a moving GC> +#ifdef JSGC_HASH_TABLE_CHECKS + static void checkDebugScriptAfterMovingGC(DebugScript* ds); +#endif + +#ifdef DEBUG + static bool edgeIsInDebuggerWeakmap(JSRuntime* rt, JSObject* src, + JS::GCCellPtr dst); +#endif + + /*** Methods for querying script breakpoint state. **************************/ + + // Query information about whether any debuggers are observing a script. + static inline bool stepModeEnabled(JSScript* script); + static inline bool hasBreakpointsAt(JSScript* script, jsbytecode* pc); + static inline bool hasAnyBreakpointsOrStepMode(JSScript* script); + + /*** Methods for interacting with the JITs. *********************************/ + + // Update Debugger frames when an interpreter frame is replaced with a + // baseline frame. + [[nodiscard]] static bool handleBaselineOsr(JSContext* cx, + InterpreterFrame* from, + jit::BaselineFrame* to); + + // Update Debugger frames when an Ion frame bails out and is replaced with a + // baseline frame. + [[nodiscard]] static bool handleIonBailout(JSContext* cx, + jit::RematerializedFrame* from, + jit::BaselineFrame* to); + + // Detach any Debugger frames from an Ion frame after an error occurred while + // it bailed out. + static void handleUnrecoverableIonBailoutError( + JSContext* cx, jit::RematerializedFrame* frame); + + // When doing on-stack-replacement of a debuggee interpreter frame with a + // baseline frame, ensure that the resulting frame can be observed by the + // debugger. + [[nodiscard]] static bool ensureExecutionObservabilityOfOsrFrame( + JSContext* cx, AbstractFramePtr osrSourceFrame); + + // Describes a set of scripts or frames whose execution observability can + // change due to debugger activity. + class ExecutionObservableSet { + public: + using ZoneRange = HashSet<Zone*>::Range; + + virtual Zone* singleZone() const { return nullptr; } + virtual JSScript* singleScriptForZoneInvalidation() const { + return nullptr; + } + virtual const HashSet<Zone*>* zones() const { return nullptr; } + + virtual bool shouldRecompileOrInvalidate(JSScript* script) const = 0; + virtual bool shouldMarkAsDebuggee(FrameIter& iter) const = 0; + }; + + // This enum is converted to and compare with bool values; NotObserving + // must be 0 and Observing must be 1. + enum IsObserving { NotObserving = 0, Observing = 1 }; + + /*** Methods for calling installed debugger handlers. ***********************/ + + // Called when a new script becomes accessible to debuggers. + static void onNewScript(JSContext* cx, HandleScript script); + + // Called when a new wasm instance becomes accessible to debuggers. + static inline void onNewWasmInstance( + JSContext* cx, Handle<WasmInstanceObject*> wasmInstance); + + /* + * Announce to the debugger that the context has entered a new JavaScript + * frame, |frame|. Call whatever hooks have been registered to observe new + * frames. + */ + [[nodiscard]] static inline bool onEnterFrame(JSContext* cx, + AbstractFramePtr frame); + + /* + * Like onEnterFrame, but for resuming execution of a generator or async + * function. `frame` is a new baseline or interpreter frame, but abstractly + * it can be identified with a particular generator frame that was + * suspended earlier. + * + * There is no separate user-visible Debugger.onResumeFrame hook; this + * fires .onEnterFrame (again, since we're re-entering the frame). + * + * Unfortunately, the interpreter and the baseline JIT arrange for this to + * be called in different ways. The interpreter calls it from JSOp::Resume, + * immediately after pushing the resumed frame; the JIT calls it from + * JSOp::AfterYield, just after the generator resumes. The difference + * should not be user-visible. + */ + [[nodiscard]] static inline bool onResumeFrame(JSContext* cx, + AbstractFramePtr frame); + + static inline NativeResumeMode onNativeCall(JSContext* cx, + const CallArgs& args, + CallReason reason); + + /* + * Announce to the debugger a |debugger;| statement on has been + * encountered on the youngest JS frame on |cx|. Call whatever hooks have + * been registered to observe this. + * + * Note that this method is called for all |debugger;| statements, + * regardless of the frame's debuggee-ness. + */ + [[nodiscard]] static inline bool onDebuggerStatement(JSContext* cx, + AbstractFramePtr frame); + + /* + * Announce to the debugger that an exception has been thrown and propagated + * to |frame|. Call whatever hooks have been registered to observe this. + */ + [[nodiscard]] static inline bool onExceptionUnwind(JSContext* cx, + AbstractFramePtr frame); + + /* + * Announce to the debugger that the thread has exited a JavaScript frame, + * |frame|. If |ok| is true, the frame is returning normally; if |ok| is + * false, the frame is throwing an exception or terminating. + * + * Change cx's current exception and |frame|'s return value to reflect the + * changes in behavior the hooks request, if any. Return the new error/success + * value. + * + * This function may be called twice for the same outgoing frame; only the + * first call has any effect. (Permitting double calls simplifies some + * cases where an onPop handler's resumption value changes a return to a + * throw, or vice versa: we can redirect to a complete copy of the + * alternative path, containing its own call to onLeaveFrame.) + */ + [[nodiscard]] static inline bool onLeaveFrame(JSContext* cx, + AbstractFramePtr frame, + const jsbytecode* pc, bool ok); + + // Call any breakpoint handlers for the current scripted location. + [[nodiscard]] static bool onTrap(JSContext* cx); + + // Call any stepping handlers for the current scripted location. + [[nodiscard]] static bool onSingleStep(JSContext* cx); + + // Notify any Debugger instances observing this promise's global that a new + // promise was allocated. + static inline void onNewPromise(JSContext* cx, + Handle<PromiseObject*> promise); + + // Notify any Debugger instances observing this promise's global that the + // promise has settled (ie, it has either been fulfilled or rejected). Note + // that this is *not* equivalent to the promise resolution (ie, the promise's + // fate getting locked in) because you can resolve a promise with another + // pending promise, in which case neither promise has settled yet. + // + // This should never be called on the same promise more than once, because a + // promise can only make the transition from unsettled to settled once. + static inline void onPromiseSettled(JSContext* cx, + Handle<PromiseObject*> promise); + + // Notify any Debugger instances that a new global object has been created. + static inline void onNewGlobalObject(JSContext* cx, + Handle<GlobalObject*> global); + + /*** Methods for querying installed debugger handlers. **********************/ + + // Whether any debugger is observing execution in a global. + static bool debuggerObservesAllExecution(GlobalObject* global); + + // Whether any debugger is observing JS execution coverage in a global. + static bool debuggerObservesCoverage(GlobalObject* global); + + // Whether any Debugger is observing asm.js execution in a global. + static bool debuggerObservesAsmJS(GlobalObject* global); + + // Whether any Debugger is observing WebAssembly execution in a global. + static bool debuggerObservesWasm(GlobalObject* global); + + /* + * Return true if the given global is being observed by at least one + * Debugger that is tracking allocations. + */ + static bool isObservedByDebuggerTrackingAllocations( + const GlobalObject& debuggee); + + // If any debuggers are tracking allocations for a global, return the + // probability that a given allocation should be tracked. Nothing otherwise. + static mozilla::Maybe<double> allocationSamplingProbability( + GlobalObject* global); + + // Whether any debugger is observing exception unwinds in a realm. + static bool hasExceptionUnwindHook(GlobalObject* global); + + // Whether any debugger is observing debugger statements in a realm. + static bool hasDebuggerStatementHook(GlobalObject* global); + + /*** Assorted methods for interacting with the runtime. *********************/ + + // Checks if the current compartment is allowed to execute code. + [[nodiscard]] static inline bool checkNoExecute(JSContext* cx, + HandleScript script); + + /* + * Announce to the debugger that a generator object has been created, + * via JSOp::Generator. + * + * This does not fire user hooks, but it's needed for debugger bookkeeping. + */ + [[nodiscard]] static inline bool onNewGenerator( + JSContext* cx, AbstractFramePtr frame, + Handle<AbstractGeneratorObject*> genObj); + + // If necessary, record an object that was just allocated for any observing + // debuggers. + [[nodiscard]] static inline bool onLogAllocationSite( + JSContext* cx, JSObject* obj, Handle<SavedFrame*> frame, + mozilla::TimeStamp when); + + // Announce to the debugger that a global object is being collected by the + // specified major GC. + static inline void notifyParticipatesInGC(GlobalObject* global, + uint64_t majorGCNumber); + + private: + static bool stepModeEnabledSlow(JSScript* script); + static bool hasBreakpointsAtSlow(JSScript* script, jsbytecode* pc); + static void slowPathOnNewGlobalObject(JSContext* cx, + Handle<GlobalObject*> global); + static void slowPathNotifyParticipatesInGC(uint64_t majorGCNumber, + JS::Realm::DebuggerVector& dbgs); + [[nodiscard]] static bool slowPathOnLogAllocationSite( + JSContext* cx, HandleObject obj, Handle<SavedFrame*> frame, + mozilla::TimeStamp when, JS::Realm::DebuggerVector& dbgs); + [[nodiscard]] static bool slowPathOnLeaveFrame(JSContext* cx, + AbstractFramePtr frame, + const jsbytecode* pc, bool ok); + [[nodiscard]] static bool slowPathOnNewGenerator( + JSContext* cx, AbstractFramePtr frame, + Handle<AbstractGeneratorObject*> genObj); + [[nodiscard]] static bool slowPathCheckNoExecute(JSContext* cx, + HandleScript script); + [[nodiscard]] static bool slowPathOnEnterFrame(JSContext* cx, + AbstractFramePtr frame); + [[nodiscard]] static bool slowPathOnResumeFrame(JSContext* cx, + AbstractFramePtr frame); + static NativeResumeMode slowPathOnNativeCall(JSContext* cx, + const CallArgs& args, + CallReason reason); + [[nodiscard]] static bool slowPathOnDebuggerStatement(JSContext* cx, + AbstractFramePtr frame); + [[nodiscard]] static bool slowPathOnExceptionUnwind(JSContext* cx, + AbstractFramePtr frame); + static void slowPathOnNewWasmInstance( + JSContext* cx, Handle<WasmInstanceObject*> wasmInstance); + static void slowPathOnNewPromise(JSContext* cx, + Handle<PromiseObject*> promise); + static void slowPathOnPromiseSettled(JSContext* cx, + Handle<PromiseObject*> promise); + static bool inFrameMaps(AbstractFramePtr frame); + static void slowPathTraceGeneratorFrame(JSTracer* tracer, + AbstractGeneratorObject* generator); +}; + +// Suppresses all debuggee NX checks, i.e., allow all execution. Used to allow +// certain whitelisted operations to execute code. +// +// WARNING +// WARNING Do not use this unless you know what you are doing! +// WARNING +class AutoSuppressDebuggeeNoExecuteChecks { + EnterDebuggeeNoExecute** stack_; + EnterDebuggeeNoExecute* prev_; + + public: + explicit AutoSuppressDebuggeeNoExecuteChecks(JSContext* cx) { + stack_ = &cx->noExecuteDebuggerTop.ref(); + prev_ = *stack_; + *stack_ = nullptr; + } + + ~AutoSuppressDebuggeeNoExecuteChecks() { + MOZ_ASSERT(!*stack_); + *stack_ = prev_; + } +}; + +} /* namespace js */ + +#endif /* debugger_DebugAPI_h */ diff --git a/js/src/debugger/DebugScript.cpp b/js/src/debugger/DebugScript.cpp new file mode 100644 index 0000000000..610784c228 --- /dev/null +++ b/js/src/debugger/DebugScript.cpp @@ -0,0 +1,411 @@ +/* -*- 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/DebugScript.h" + +#include "mozilla/Assertions.h" // for AssertionConditionType +#include "mozilla/HashTable.h" // for HashMapEntry, HashTable<>::Ptr, HashMap +#include "mozilla/UniquePtr.h" // for UniquePtr + +#include <utility> // for std::move + +#include "debugger/DebugAPI.h" // for DebugAPI +#include "debugger/Debugger.h" // for JSBreakpointSite, Breakpoint +#include "gc/Cell.h" // for TenuredCell +#include "gc/GCContext.h" // for JS::GCContext +#include "gc/GCEnum.h" // for MemoryUse, MemoryUse::BreakpointSite +#include "gc/Marking.h" // for IsAboutToBeFinalized +#include "gc/Zone.h" // for Zone +#include "gc/ZoneAllocator.h" // for AddCellMemory +#include "jit/BaselineJIT.h" // for BaselineScript +#include "vm/BytecodeIterator.h" // for AllBytecodesIterable +#include "vm/JSContext.h" // for JSContext +#include "vm/JSScript.h" // for JSScript, DebugScriptMap +#include "vm/NativeObject.h" // for NativeObject +#include "vm/Realm.h" // for Realm, AutoRealm +#include "vm/Runtime.h" // for ReportOutOfMemory +#include "vm/Stack.h" // for ActivationIterator, Activation + +#include "gc/GC-inl.h" // for ZoneCellIter +#include "gc/GCContext-inl.h" // for JS::GCContext::free_ +#include "gc/Marking-inl.h" // for CheckGCThingAfterMovingGC +#include "gc/WeakMap-inl.h" // for WeakMap::remove +#include "vm/BytecodeIterator-inl.h" // for AllBytecodesIterable +#include "vm/JSContext-inl.h" // for JSContext::check +#include "vm/JSObject-inl.h" // for NewObjectWithGivenProto +#include "vm/JSScript-inl.h" // for JSScript::hasBaselineScript +#include "vm/Realm-inl.h" // for AutoRealm::AutoRealm + +namespace js { + +const JSClass DebugScriptObject::class_ = { + "DebugScriptObject", + JSCLASS_HAS_RESERVED_SLOTS(SlotCount) | JSCLASS_BACKGROUND_FINALIZE, + &classOps_, JS_NULL_CLASS_SPEC}; + +const JSClassOps DebugScriptObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + DebugScriptObject::finalize, // finalize + nullptr, // call + nullptr, // construct + DebugScriptObject::trace, // trace +}; + +/* static */ +DebugScriptObject* DebugScriptObject::create(JSContext* cx, + UniqueDebugScript debugScript, + size_t nbytes) { + auto* object = NewObjectWithGivenProto<DebugScriptObject>(cx, nullptr); + if (!object) { + return nullptr; + } + + object->initReservedSlot(ScriptSlot, PrivateValue(debugScript.release())); + AddCellMemory(object, nbytes, MemoryUse::ScriptDebugScript); + + return object; +} + +DebugScript* DebugScriptObject::debugScript() const { + return maybePtrFromReservedSlot<DebugScript>(ScriptSlot); +} + +/* static */ +void DebugScriptObject::trace(JSTracer* trc, JSObject* obj) { + DebugScript* debugScript = obj->as<DebugScriptObject>().debugScript(); + if (debugScript) { + debugScript->trace(trc); + } +} + +/* static */ +void DebugScriptObject::finalize(JS::GCContext* gcx, JSObject* obj) { + DebugScriptObject* object = &obj->as<DebugScriptObject>(); + DebugScript* debugScript = object->debugScript(); + if (debugScript) { + debugScript->delete_(gcx, object); + } +} + +/* static */ +DebugScript* DebugScript::get(JSScript* script) { + MOZ_ASSERT(script->hasDebugScript()); + DebugScriptMap* map = script->zone()->debugScriptMap; + MOZ_ASSERT(map); + DebugScriptMap::Ptr p = map->lookupUnbarriered(script); + MOZ_ASSERT(p); + return p->value().get()->as<DebugScriptObject>().debugScript(); +} + +/* static */ +DebugScript* DebugScript::getOrCreate(JSContext* cx, HandleScript script) { + cx->check(script); + + if (script->hasDebugScript()) { + return get(script); + } + + size_t nbytes = allocSize(script->length()); + UniqueDebugScript debug( + reinterpret_cast<DebugScript*>(cx->pod_calloc<uint8_t>(nbytes))); + if (!debug) { + return nullptr; + } + + debug->codeLength = script->length(); + + Rooted<DebugScriptObject*> object( + cx, DebugScriptObject::create(cx, std::move(debug), nbytes)); + if (!object) { + return nullptr; + } + + /* Create zone's debugScriptMap if necessary. */ + Zone* zone = script->zone(); + MOZ_ASSERT(cx->zone() == zone); + if (!zone->debugScriptMap) { + DebugScriptMap* map = cx->new_<DebugScriptMap>(cx); + if (!map) { + return nullptr; + } + + zone->debugScriptMap = map; + } + + MOZ_ASSERT(script->hasBytecode()); + + if (!zone->debugScriptMap->putNew(script.get(), object.get())) { + ReportOutOfMemory(cx); + return nullptr; + } + + // It is safe to set this: we can't fail after this point. + script->setHasDebugScript(true); + + /* + * Ensure that any Interpret() instances running on this script have + * interrupts enabled. The interrupts must stay enabled until the + * debug state is destroyed. + */ + for (ActivationIterator iter(cx); !iter.done(); ++iter) { + if (iter->isInterpreter()) { + iter->asInterpreter()->enableInterruptsIfRunning(script); + } + } + + return object->debugScript(); +} + +/* static */ +JSBreakpointSite* DebugScript::getBreakpointSite(JSScript* script, + jsbytecode* pc) { + uint32_t offset = script->pcToOffset(pc); + return script->hasDebugScript() ? get(script)->breakpoints[offset] : nullptr; +} + +/* static */ +JSBreakpointSite* DebugScript::getOrCreateBreakpointSite(JSContext* cx, + HandleScript script, + jsbytecode* pc) { + AutoRealm ar(cx, script); + + DebugScript* debug = getOrCreate(cx, script); + if (!debug) { + return nullptr; + } + + JSBreakpointSite*& site = debug->breakpoints[script->pcToOffset(pc)]; + + if (!site) { + site = cx->new_<JSBreakpointSite>(script, pc); + if (!site) { + return nullptr; + } + debug->numSites++; + AddCellMemory(script, sizeof(JSBreakpointSite), MemoryUse::BreakpointSite); + + if (script->hasBaselineScript()) { + script->baselineScript()->toggleDebugTraps(script, pc); + } + } + + return site; +} + +/* static */ +void DebugScript::destroyBreakpointSite(JS::GCContext* gcx, JSScript* script, + jsbytecode* pc) { + DebugScript* debug = get(script); + JSBreakpointSite*& site = debug->breakpoints[script->pcToOffset(pc)]; + MOZ_ASSERT(site); + MOZ_ASSERT(site->isEmpty()); + + site->delete_(gcx); + site = nullptr; + + debug->numSites--; + if (!debug->needed()) { + DebugAPI::removeDebugScript(gcx, script); + } + + if (script->hasBaselineScript()) { + script->baselineScript()->toggleDebugTraps(script, pc); + } +} + +/* static */ +void DebugScript::clearBreakpointsIn(JS::GCContext* gcx, JSScript* script, + Debugger* dbg, JSObject* handler) { + MOZ_ASSERT(script); + // Breakpoints hold wrappers in the script's compartment for the handler. Make + // sure we don't try to search for the unwrapped handler. + MOZ_ASSERT_IF(handler, script->compartment() == handler->compartment()); + + if (!script->hasDebugScript()) { + return; + } + + AllBytecodesIterable iter(script); + for (BytecodeLocation loc : iter) { + JSBreakpointSite* site = getBreakpointSite(script, loc.toRawBytecode()); + if (site) { + Breakpoint* nextbp; + for (Breakpoint* bp = site->firstBreakpoint(); bp; bp = nextbp) { + nextbp = bp->nextInSite(); + if ((!dbg || bp->debugger == dbg) && + (!handler || bp->getHandler() == handler)) { + bp->remove(gcx); + } + } + } + } +} + +#ifdef DEBUG +/* static */ +uint32_t DebugScript::getStepperCount(JSScript* script) { + return script->hasDebugScript() ? get(script)->stepperCount : 0; +} +#endif // DEBUG + +/* static */ +bool DebugScript::incrementStepperCount(JSContext* cx, HandleScript script) { + cx->check(script); + MOZ_ASSERT(cx->realm()->isDebuggee()); + + AutoRealm ar(cx, script); + + DebugScript* debug = getOrCreate(cx, script); + if (!debug) { + return false; + } + + debug->stepperCount++; + + if (debug->stepperCount == 1) { + if (script->hasBaselineScript()) { + script->baselineScript()->toggleDebugTraps(script, nullptr); + } + } + + return true; +} + +/* static */ +void DebugScript::decrementStepperCount(JS::GCContext* gcx, JSScript* script) { + DebugScript* debug = get(script); + MOZ_ASSERT(debug); + MOZ_ASSERT(debug->stepperCount > 0); + + debug->stepperCount--; + + if (debug->stepperCount == 0) { + if (script->hasBaselineScript()) { + script->baselineScript()->toggleDebugTraps(script, nullptr); + } + + if (!debug->needed()) { + DebugAPI::removeDebugScript(gcx, script); + } + } +} + +/* static */ +bool DebugScript::incrementGeneratorObserverCount(JSContext* cx, + HandleScript script) { + cx->check(script); + MOZ_ASSERT(cx->realm()->isDebuggee()); + + AutoRealm ar(cx, script); + + DebugScript* debug = getOrCreate(cx, script); + if (!debug) { + return false; + } + + debug->generatorObserverCount++; + + // It is our caller's responsibility, before bumping the generator observer + // count, to make sure that the baseline code includes the necessary + // JSOp::AfterYield instrumentation by calling + // {ensure,update}ExecutionObservabilityOfScript. + MOZ_ASSERT_IF(script->hasBaselineScript(), + script->baselineScript()->hasDebugInstrumentation()); + + return true; +} + +/* static */ +void DebugScript::decrementGeneratorObserverCount(JS::GCContext* gcx, + JSScript* script) { + DebugScript* debug = get(script); + MOZ_ASSERT(debug); + MOZ_ASSERT(debug->generatorObserverCount > 0); + + debug->generatorObserverCount--; + + if (!debug->needed()) { + DebugAPI::removeDebugScript(gcx, script); + } +} + +void DebugScript::trace(JSTracer* trc) { + for (size_t i = 0; i < codeLength; i++) { + JSBreakpointSite* site = breakpoints[i]; + if (site) { + site->trace(trc); + } + } +} + +/* static */ +void DebugAPI::removeDebugScript(JS::GCContext* gcx, JSScript* script) { + if (script->hasDebugScript()) { + if (IsAboutToBeFinalizedUnbarriered(script)) { + // The script is dying and all breakpoint data will be cleaned up. + return; + } + + DebugScriptMap* map = script->zone()->debugScriptMap; + MOZ_ASSERT(map); + DebugScriptMap::Ptr p = map->lookupUnbarriered(script); + MOZ_ASSERT(p); + map->remove(p); + script->setHasDebugScript(false); + + // The DebugScript will be destroyed at the next GC when its owning + // DebugScriptObject dies. + } +} + +void DebugScript::delete_(JS::GCContext* gcx, DebugScriptObject* owner) { + for (size_t i = 0; i < codeLength; i++) { + JSBreakpointSite* site = breakpoints[i]; + if (site) { + site->delete_(gcx); + } + } + + gcx->free_(owner, this, allocSize(codeLength), MemoryUse::ScriptDebugScript); +} + +#ifdef JSGC_HASH_TABLE_CHECKS +/* static */ +void DebugAPI::checkDebugScriptAfterMovingGC(DebugScript* ds) { + for (uint32_t i = 0; i < ds->numSites; i++) { + JSBreakpointSite* site = ds->breakpoints[i]; + if (site) { + CheckGCThingAfterMovingGC(site->script.get()); + } + } +} +#endif // JSGC_HASH_TABLE_CHECKS + +/* static */ +bool DebugAPI::stepModeEnabledSlow(JSScript* script) { + return DebugScript::get(script)->stepperCount > 0; +} + +/* static */ +bool DebugAPI::hasBreakpointsAtSlow(JSScript* script, jsbytecode* pc) { + JSBreakpointSite* site = DebugScript::getBreakpointSite(script, pc); + return !!site; +} + +/* static */ +void DebugAPI::traceDebugScriptMap(JSTracer* trc, DebugScriptMap* map) { + map->trace(trc); +} + +/* static */ +void DebugAPI::deleteDebugScriptMap(DebugScriptMap* map) { js_delete(map); } + +} // namespace js diff --git a/js/src/debugger/DebugScript.h b/js/src/debugger/DebugScript.h new file mode 100644 index 0000000000..176ea3b80c --- /dev/null +++ b/js/src/debugger/DebugScript.h @@ -0,0 +1,161 @@ +/* -*- 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/. */ + +#ifndef dbg_DebugScript_h +#define dbg_DebugScript_h + +#include <stddef.h> // for offsetof +#include <stddef.h> // for size_t +#include <stdint.h> // for uint32_t + +#include "jstypes.h" + +#include "gc/WeakMap.h" +#include "vm/NativeObject.h" + +namespace JS { +class JS_PUBLIC_API Realm; +} + +namespace js { + +class JSBreakpointSite; +class Debugger; +class DebugScriptObject; + +// DebugScript manages the internal debugger state for a JSScript, which may be +// associated with multiple Debuggers. +class DebugScript { + friend class DebugAPI; + friend class DebugScriptObject; + + /* + * If this is a generator script, this is the number of Debugger.Frames + * referring to calls to this generator, whether live or suspended. Closed + * generators do not contribute a count. + * + * When greater than zero, this script should be compiled with debug + * instrumentation to call Debugger::onResumeFrame at each resumption site, so + * that Debugger can reconnect any extant Debugger.Frames with the new + * concrete frame. + */ + uint32_t generatorObserverCount; + + /* + * The number of Debugger.Frame objects that refer to frames running this + * script and that have onStep handlers. When nonzero, the interpreter and JIT + * must arrange to call Debugger::onSingleStep before each bytecode, or at + * least at some useful granularity. + */ + uint32_t stepperCount; + + /* + * The size of the script as reported by BaseScript::length. This is the + * length of the DebugScript::breakpoints array, below. + */ + size_t codeLength; + + /* + * Number of breakpoint sites at opcodes in the script. This is the number + * of populated entries in DebugScript::breakpoints. + */ + uint32_t numSites; + + /* + * Breakpoints set in our script. For speed and simplicity, this array is + * parallel to script->code(): the JSBreakpointSite for the opcode at + * script->code()[offset] is debugScript->breakpoints[offset]. + */ + JSBreakpointSite* breakpoints[1]; + + /* + * True if this DebugScript carries any useful information. If false, it + * should be removed from its JSScript. + */ + bool needed() const { + return generatorObserverCount > 0 || stepperCount > 0 || numSites > 0; + } + + static size_t allocSize(size_t codeLength) { + return offsetof(DebugScript, breakpoints) + + codeLength * sizeof(JSBreakpointSite*); + } + + void trace(JSTracer* trc); + void delete_(JS::GCContext* gcx, DebugScriptObject* owner); + + static DebugScript* get(JSScript* script); + static DebugScript* getOrCreate(JSContext* cx, HandleScript script); + + public: + static JSBreakpointSite* getBreakpointSite(JSScript* script, jsbytecode* pc); + static JSBreakpointSite* getOrCreateBreakpointSite(JSContext* cx, + HandleScript script, + jsbytecode* pc); + static void destroyBreakpointSite(JS::GCContext* gcx, JSScript* script, + jsbytecode* pc); + + static void clearBreakpointsIn(JS::GCContext* gcx, JSScript* script, + Debugger* dbg, JSObject* handler); + +#ifdef DEBUG + static uint32_t getStepperCount(JSScript* script); +#endif + + /* + * Increment or decrement the single-step count. If the count is non-zero + * then the script is in single-step mode. + * + * Only incrementing is fallible, as it could allocate a DebugScript. + */ + [[nodiscard]] static bool incrementStepperCount(JSContext* cx, + HandleScript script); + static void decrementStepperCount(JS::GCContext* gcx, JSScript* script); + + /* + * Increment or decrement the generator observer count. If the count is + * non-zero then the script reports resumptions to the debugger. + * + * Only incrementing is fallible, as it could allocate a DebugScript. + */ + [[nodiscard]] static bool incrementGeneratorObserverCount( + JSContext* cx, HandleScript script); + static void decrementGeneratorObserverCount(JS::GCContext* gcx, + JSScript* script); +}; + +using UniqueDebugScript = js::UniquePtr<DebugScript, JS::FreePolicy>; + +// A JSObject that wraps a DebugScript, so we can use it as the value in a +// WeakMap. This object owns the DebugScript and is responsible for deleting it. +class DebugScriptObject : public NativeObject { + public: + static const JSClass class_; + + enum { ScriptSlot, SlotCount }; + + static DebugScriptObject* create(JSContext* cx, UniqueDebugScript debugScript, + size_t nbytes); + + DebugScript* debugScript() const; + + private: + static const JSClassOps classOps_; + + static void trace(JSTracer* trc, JSObject* obj); + static void finalize(JS::GCContext* gcx, JSObject* obj); +}; + +// A weak map from JSScripts to DebugScriptObjects. +class DebugScriptMap + : public WeakMap<HeapPtr<JSScript*>, HeapPtr<DebugScriptObject*>> { + public: + explicit DebugScriptMap(JSContext* cx) : WeakMap(cx) {} +}; + +} /* namespace js */ + +#endif /* dbg_DebugScript_h */ diff --git a/js/src/debugger/Debugger-inl.h b/js/src/debugger/Debugger-inl.h new file mode 100644 index 0000000000..ad12ea2f9a --- /dev/null +++ b/js/src/debugger/Debugger-inl.h @@ -0,0 +1,32 @@ +/* -*- 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/. */ + +#ifndef debugger_Debugger_inl_h +#define debugger_Debugger_inl_h + +#include "debugger/Debugger.h" // for Debugger, ResumeMode + +#include "mozilla/Assertions.h" // for AssertionConditionType + +#include "vm/JSObject.h" // for JSObject +#include "vm/NativeObject.h" // for NativeObject, JSObject::is + +/* static */ inline js::Debugger* js::Debugger::fromJSObject( + const JSObject* obj) { + MOZ_ASSERT(obj->is<DebuggerInstanceObject>()); + auto* dbg = &obj->as<DebuggerInstanceObject>(); + return dbg->maybePtrFromReservedSlot<Debugger>(JSSLOT_DEBUG_DEBUGGER); +} + +inline bool js::Debugger::isHookCallAllowed(JSContext* cx) const { + // If we are evaluating inside of an eval on a debugger that has an + // onNativeCall hook, we want to _only_ call the hooks attached to that + // specific debugger. + return !cx->insideDebuggerEvaluationWithOnNativeCallHook || + this == cx->insideDebuggerEvaluationWithOnNativeCallHook; +} + +#endif /* debugger_Debugger_inl_h */ diff --git a/js/src/debugger/Debugger.cpp b/js/src/debugger/Debugger.cpp new file mode 100644 index 0000000000..293310305b --- /dev/null +++ b/js/src/debugger/Debugger.cpp @@ -0,0 +1,7059 @@ +/* -*- 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 <algorithm> // for std::find, std::max +#include <functional> // for function +#include <stddef.h> // for size_t +#include <stdint.h> // for uint32_t, uint64_t, int32_t +#include <string.h> // for strlen, strcmp +#include <type_traits> // for std::underlying_type_t +#include <utility> // 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/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<SavedFrame*> 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<SavedFrame*> 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<JSLinearString*> linear(cx, value.toString()->ensureLinear(cx)); + if (!linear) { + return false; + } + if (!stableChars.initTwoByte(cx, linear)) { + return false; + } + return true; +} + +bool EvalOptions::setFilename(JSContext* cx, const char* filename) { + JS::UniqueChars copy; + if (filename) { + copy = DuplicateString(cx, filename); + if (!copy) { + return false; + } + } + + filename_ = std::move(copy); + return true; +} + +bool js::ParseEvalOptions(JSContext* cx, HandleValue value, + EvalOptions& options) { + if (!value.isObject()) { + return true; + } + + RootedObject opts(cx, &value.toObject()); + + RootedValue v(cx); + if (!JS_GetProperty(cx, opts, "url", &v)) { + return false; + } + if (!v.isUndefined()) { + RootedString url_str(cx, ToString<CanGC>(cx, v)); + if (!url_str) { + return false; + } + UniqueChars url_bytes = JS_EncodeStringToLatin1(cx, url_str); + if (!url_bytes) { + return false; + } + if (!options.setFilename(cx, url_bytes.get())) { + return false; + } + } + + if (!JS_GetProperty(cx, opts, "lineNumber", &v)) { + return false; + } + if (!v.isUndefined()) { + uint32_t lineno; + if (!ToUint32(cx, v, &lineno)) { + return false; + } + options.setLineno(lineno); + } + + 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 <typename ElementAccess> +static void RemoveDebuggerEntry( + mozilla::DoublyLinkedList<Debugger, ElementAccess>& 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<DebuggerMemory>(); +} + +/*** Debugger accessors *******************************************************/ + +bool Debugger::getFrame(JSContext* cx, const FrameIter& iter, + MutableHandleValue vp) { + Rooted<DebuggerFrame*> result(cx); + if (!Debugger::getFrame(cx, iter, &result)) { + return false; + } + vp.setObject(*result); + return true; +} + +bool Debugger::getFrame(JSContext* cx, MutableHandle<DebuggerFrame*> result) { + RootedObject proto( + cx, &object->getReservedSlot(JSSLOT_DEBUG_FRAME_PROTO).toObject()); + Rooted<NativeObject*> 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<DebuggerFrame*> 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<DebuggerFrame*> result) { + AbstractFramePtr referent = iter.abstractFramePtr(); + MOZ_ASSERT_IF(referent.hasScript(), !referent.script()->selfHosted()); + + FrameMap::AddPtr p = frames.lookupForAdd(referent); + if (!p) { + Rooted<AbstractGeneratorObject*> 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<NativeObject*> debugger(cx, object); + + Rooted<DebuggerFrame*> 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<GeneratorWeakMap> genPtr(cx, generatorFrames, genObj); + if (!genPtr.add(cx, generatorFrames, genObj, frame)) { + return false; + } + } + + if (!ensureExecutionObservabilityOfFrame(cx, referent)) { + return false; + } + + if (!frames.add(p, referent, frame)) { + ReportOutOfMemory(cx); + return false; + } + + terminateDebuggerFrameGuard.release(); + } + + result.set(p->value()); + return true; +} + +bool Debugger::getFrame(JSContext* cx, Handle<AbstractGeneratorObject*> genObj, + MutableHandle<DebuggerFrame*> result) { + // To create a Debugger.Frame for a running generator, we'd also need a + // FrameIter for its stack frame. We could make this work by searching the + // stack for the generator's frame, but for the moment, we only need this + // function to handle generators we've found on promises' reaction records, + // which should always be suspended. + MOZ_ASSERT(genObj->isSuspended()); + + // Do we have an existing Debugger.Frame for this generator? + DependentAddPtr<GeneratorWeakMap> p(cx, generatorFrames, genObj); + if (p) { + MOZ_ASSERT(&p->value()->unwrappedGenerator() == genObj); + result.set(p->value()); + return true; + } + + // Create a new Debugger.Frame. + RootedObject proto( + cx, &object->getReservedSlot(JSSLOT_DEBUG_FRAME_PROTO).toObject()); + Rooted<NativeObject*> 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<bool(Debugger* dbg)>& predicate) { + // The GC analysis can't determine that the predicate can't GC, so let it know + // explicitly. + JS::AutoSuppressGCAnalysis nogc; + + for (Realm::DebuggerVectorEntry& entry : global->getDebuggers()) { + // Callbacks should not create new references to the debugger, so don't + // use a barrier. This allows this method to be called during GC. + if (predicate(entry.dbg.unbarrieredGet())) { + return true; + } + } + return false; +} + +/* static */ +bool Debugger::hasLiveHook(GlobalObject* global, Hook which) { + return DebuggerExists(global, + [=](Debugger* dbg) { return dbg->getHook(which); }); +} + +/* static */ +bool DebugAPI::debuggerObservesAllExecution(GlobalObject* global) { + return DebuggerExists( + global, [=](Debugger* dbg) { return dbg->observesAllExecution(); }); +} + +/* static */ +bool DebugAPI::debuggerObservesCoverage(GlobalObject* global) { + return DebuggerExists(global, + [=](Debugger* dbg) { return dbg->observesCoverage(); }); +} + +/* static */ +bool DebugAPI::debuggerObservesAsmJS(GlobalObject* global) { + return DebuggerExists(global, + [=](Debugger* dbg) { return dbg->observesAsmJS(); }); +} + +/* static */ +bool DebugAPI::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 <typename HookIsEnabledFun /* bool (Debugger*) */> +bool DebuggerList<HookIsEnabledFun>::init(JSContext* cx) { + // Determine which debuggers will receive this event, and in what order. + // Make a copy of the list, since the original is mutable and we will be + // calling into arbitrary JS. + Handle<GlobalObject*> global = cx->global(); + for (Realm::DebuggerVectorEntry& entry : global->getDebuggers()) { + Debugger* dbg = entry.dbg; + if (dbg->isHookCallAllowed(cx) && hookIsEnabled(dbg)) { + if (!debuggers.append(ObjectValue(*dbg->toJSObject()))) { + return false; + } + } + } + return true; +} + +template <typename HookIsEnabledFun /* bool (Debugger*) */> +template <typename FireHookFun /* bool (Debugger*) */> +bool DebuggerList<HookIsEnabledFun>::dispatchHook(JSContext* cx, + FireHookFun fireHook) { + // Preserve the debuggee's microtask event queue while we run the hooks, so + // the debugger's microtask checkpoints don't run from the debuggee's + // microtasks, and vice versa. + JS::AutoDebuggerJobQueueInterruption adjqi; + if (!adjqi.init(cx)) { + return false; + } + + // Deliver the event to each debugger, checking again to make sure it + // should still be delivered. + Handle<GlobalObject*> global = cx->global(); + for (Value* p = debuggers.begin(); p != debuggers.end(); p++) { + Debugger* dbg = Debugger::fromJSObject(&p->toObject()); + EnterDebuggeeNoExecute nx(cx, *dbg, adjqi); + if (dbg->debuggees.has(global) && hookIsEnabled(dbg)) { + bool result = + dbg->enterDebuggerHook(cx, [&]() -> bool { return fireHook(dbg); }); + adjqi.runJobs(); + if (!result) { + return false; + } + } + } + return true; +} + +template <typename HookIsEnabledFun /* bool (Debugger*) */> +template <typename FireHookFun /* bool (Debugger*) */> +void DebuggerList<HookIsEnabledFun>::dispatchQuietHook(JSContext* cx, + FireHookFun fireHook) { + bool result = + dispatchHook(cx, [&](Debugger* dbg) -> bool { return fireHook(dbg); }); + + // dispatchHook may fail due to OOM. This OOM is not handlable at the + // callsites of dispatchQuietHook in the engine. + if (!result) { + cx->clearPendingException(); + } +} + +template <typename HookIsEnabledFun /* bool (Debugger*) */> +template <typename FireHookFun /* bool (Debugger*, ResumeMode&, MutableHandleValue vp) */> +bool DebuggerList<HookIsEnabledFun>::dispatchResumptionHook( + JSContext* cx, AbstractFramePtr frame, FireHookFun fireHook) { + ResumeMode resumeMode = ResumeMode::Continue; + RootedValue rval(cx); + return dispatchHook(cx, + [&](Debugger* dbg) -> bool { + return fireHook(dbg, resumeMode, &rval); + }) && + ApplyFrameResumeMode(cx, frame, resumeMode, rval); +} + +JSObject* Debugger::getHook(Hook hook) const { + MOZ_ASSERT(hook >= 0 && hook < HookCount); + const Value& v = object->getReservedSlot(JSSLOT_DEBUG_HOOK_START + + std::underlying_type_t<Hook>(hook)); + return v.isUndefined() ? nullptr : &v.toObject(); +} + +bool Debugger::hasAnyLiveHooks() const { + // A onNewGlobalObject hook does not hold its Debugger live, so its behavior + // is nondeterministic. This behavior is not satisfying, but it is at least + // documented. + if (getHook(OnDebuggerStatement) || getHook(OnExceptionUnwind) || + getHook(OnNewScript) || getHook(OnEnterFrame)) { + return true; + } + + return false; +} + +/* static */ +bool DebugAPI::slowPathOnEnterFrame(JSContext* cx, AbstractFramePtr frame) { + return Debugger::dispatchResumptionHook( + cx, frame, + [frame](Debugger* dbg) -> bool { + return dbg->observesFrame(frame) && dbg->observesEnterFrame(); + }, + [&](Debugger* dbg, ResumeMode& resumeMode, MutableHandleValue vp) + -> bool { return dbg->fireEnterFrame(cx, resumeMode, vp); }); +} + +/* static */ +bool DebugAPI::slowPathOnResumeFrame(JSContext* cx, AbstractFramePtr frame) { + // Don't count on this method to be called every time a generator is + // resumed! This is called only if the frame's debuggee bit is set, + // i.e. the script has breakpoints or the frame is stepping. + MOZ_ASSERT(frame.isGeneratorFrame()); + MOZ_ASSERT(frame.isDebuggee()); + + Rooted<AbstractGeneratorObject*> genObj( + cx, GetGeneratorObjectForFrame(cx, frame)); + MOZ_ASSERT(genObj); + + // If there is an OOM, we mark all of the Debugger.Frame objects terminated + // because we want to ensure that none of the frames are in a partially + // initialized state where they are in "generatorFrames" but not "frames". + auto terminateDebuggerFramesGuard = MakeScopeExit([&] { + Debugger::terminateDebuggerFrames(cx, frame); + + MOZ_ASSERT(!DebugAPI::inFrameMaps(frame)); + }); + + // For each debugger, if there is an existing Debugger.Frame object for the + // resumed `frame`, update it with the new frame pointer and make sure the + // frame is observable. + FrameIter iter(cx); + MOZ_ASSERT(iter.abstractFramePtr() == frame); + for (Realm::DebuggerVectorEntry& entry : frame.global()->getDebuggers()) { + Debugger* dbg = entry.dbg; + if (Debugger::GeneratorWeakMap::Ptr generatorEntry = + dbg->generatorFrames.lookup(genObj)) { + DebuggerFrame* frameObj = generatorEntry->value(); + MOZ_ASSERT(&frameObj->unwrappedGenerator() == genObj); + if (!dbg->frames.putNew(frame, frameObj)) { + ReportOutOfMemory(cx); + return false; + } + if (!frameObj->resume(iter)) { + return false; + } + } + } + + terminateDebuggerFramesGuard.release(); + + return slowPathOnEnterFrame(cx, frame); +} + +/* static */ +NativeResumeMode DebugAPI::slowPathOnNativeCall(JSContext* cx, + const CallArgs& args, + CallReason reason) { + // "onNativeCall" only works consistently in the context of an explicit eval + // (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 2 cases: + // + // * callContentFunction and constructContentFunction, + // which uses CallReason::CallContent + // * Function.prototype.call and Function.prototype.apply, + // which uses CallReason::FunCall + // + // 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) { + 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<AbstractGeneratorObject*> genObj_; + + public: + AutoSetGeneratorRunning(JSContext* cx, + Handle<AbstractGeneratorObject*> genObj) + : resumeIndex_(0), + asyncGenState_(static_cast<AsyncGeneratorObject::State>(0)), + genObj_(cx, genObj) { + if (genObj) { + if (!genObj->isClosed() && !genObj->isBeforeInitialYield() && + genObj->isSuspended()) { + // Yielding or awaiting. + resumeIndex_ = genObj->resumeIndex(); + genObj->setRunning(); + + // Async generators have additionally bookkeeping which must be + // adjusted when switching over to the running state. + if (genObj->is<AsyncGeneratorObject>()) { + auto* generator = &genObj->as<AsyncGeneratorObject>(); + 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<AsyncGeneratorObject>()) { + genObj_->as<AsyncGeneratorObject>().setState(asyncGenState_); + } + } + } +}; + +/* + * Handle leaving a frame with debuggers watching. |frameOk| indicates whether + * the frame is exiting normally or abruptly. Set |cx|'s exception and/or + * |cx->fp()|'s return value, and return a new success value. + */ +/* static */ +bool DebugAPI::slowPathOnLeaveFrame(JSContext* cx, AbstractFramePtr frame, + const jsbytecode* pc, bool frameOk) { + MOZ_ASSERT_IF(!frame.isWasmDebugFrame(), pc); + + mozilla::DebugOnly<Handle<GlobalObject*>> debuggeeGlobal = cx->global(); + + // These are updated below, but consulted by the cleanup code we register now, + // so declare them here, initialized to quiescent values. + Rooted<Completion> completion(cx); + bool success = false; + + auto frameMapsGuard = MakeScopeExit([&] { + // Clean up all Debugger.Frame instances on exit. On suspending, pass the + // flag that says to leave those frames `.live`. Note that if the completion + // is a suspension but success is false, the generator gets closed, not + // suspended. + if (success && completion.get().suspending()) { + Debugger::suspendGeneratorDebuggerFrames(cx, frame); + } else { + Debugger::terminateDebuggerFrames(cx, frame); + } + }); + + // The onPop handler and associated clean up logic should not run multiple + // times on the same frame. If slowPathOnLeaveFrame has already been + // called, the frame will not be present in the Debugger frame maps. + Rooted<Debugger::DebuggerFrameVector> frames(cx); + 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<AbstractGeneratorObject*> genObj( + cx, frame.isGeneratorFrame() ? GetGeneratorObjectForFrame(cx, frame) + : nullptr); + + // For each Debugger.Frame, fire its onPop handler, if any. + for (size_t i = 0; i < frames.length(); i++) { + Handle<DebuggerFrame*> 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<SavedFrame*> completionStack(cx); + completion.get().toResumeMode(completionResumeMode, &completionValue, + &completionStack); + + // If we are returning the original value used to create the completion, then + // we don't want to treat the resumption value as a Return completion, because + // that would cause us to apply AdjustGeneratorResumptionValue to the + // already-adjusted value that the generator actually returned. + if (resumeMode == ResumeMode::Continue && + completionResumeMode == ResumeMode::Return) { + completionResumeMode = ResumeMode::Continue; + } + + if (!ApplyFrameResumeMode(cx, frame, completionResumeMode, completionValue, + completionStack)) { + if (!cx->isPropagatingForcedReturn()) { + // If this is an exception or termination, we just propagate that along. + return false; + } + + // Since we are leaving the frame here, we can convert a forced return + // into a normal return right away. + cx->clearPropagatingForcedReturn(); + } + success = true; + return true; +} + +/* static */ +bool DebugAPI::slowPathOnNewGenerator(JSContext* cx, AbstractFramePtr frame, + Handle<AbstractGeneratorObject*> genObj) { + // This is called from JSOp::Generator, after default parameter expressions + // are evaluated and well after onEnterFrame, so Debugger.Frame objects for + // `frame` may already have been exposed to debugger code. The + // AbstractGeneratorObject for this generator call, though, has just been + // created. It must be associated with any existing Debugger.Frames. + + // Initializing frames with their associated generator is critical to the + // functionality of the debugger, so if there is an OOM, we want to + // cleanly terminate all of the frames. + auto terminateDebuggerFramesGuard = + MakeScopeExit([&] { Debugger::terminateDebuggerFrames(cx, frame); }); + + bool ok = true; + Debugger::forEachOnStackDebuggerFrame( + frame, [&](Debugger* dbg, DebuggerFrame* frameObjPtr) { + if (!ok) { + return; + } + + Rooted<DebuggerFrame*> 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<Debugger::GeneratorWeakMap> genPtr( + cx, dbg->generatorFrames, genObj); + if (!genPtr.add(cx, dbg->generatorFrames, genObj, frameObj)) { + ok = false; + } + }); + + if (!ok) { + return false; + } + + terminateDebuggerFramesGuard.release(); + return true; +} + +/* static */ +bool DebugAPI::slowPathOnDebuggerStatement(JSContext* cx, + AbstractFramePtr frame) { + return Debugger::dispatchResumptionHook( + cx, frame, + [](Debugger* dbg) -> bool { + return dbg->getHook(Debugger::OnDebuggerStatement); + }, + [&](Debugger* dbg, ResumeMode& resumeMode, MutableHandleValue vp) + -> bool { return dbg->fireDebuggerStatement(cx, resumeMode, vp); }); +} + +/* static */ +bool DebugAPI::slowPathOnExceptionUnwind(JSContext* cx, + AbstractFramePtr frame) { + // Invoking more JS on an over-recursed stack or after OOM is only going + // to result in more of the same error. + if (cx->isThrowingOverRecursed() || cx->isThrowingOutOfMemory()) { + return true; + } + + // The Debugger API mustn't muck with frames from self-hosted scripts. + if (frame.hasScript() && frame.script()->selfHosted()) { + return true; + } + + DebuggerList debuggerList(cx, [](Debugger* dbg) -> bool { + return dbg->getHook(Debugger::OnExceptionUnwind); + }); + + if (!debuggerList.init(cx)) { + return false; + } + + if (debuggerList.empty()) { + return true; + } + + // We save and restore the exception once up front to avoid having to do it + // for each 'onExceptionUnwind' hook that has been registered, and we also + // only do it if the debuggerList contains items in order to avoid extra work. + RootedValue exc(cx); + Rooted<SavedFrame*> stack(cx, cx->getPendingExceptionStack()); + if (!cx->getPendingException(&exc)) { + return false; + } + cx->clearPendingException(); + + bool result = debuggerList.dispatchResumptionHook( + cx, frame, + [&](Debugger* dbg, ResumeMode& resumeMode, + MutableHandleValue vp) -> bool { + return dbg->fireExceptionUnwind(cx, exc, resumeMode, vp); + }); + if (!result) { + return false; + } + + cx->setPendingException(exc, stack); + return true; +} + +// TODO: Remove Remove this function when all properties/methods returning a +/// DebuggerEnvironment have been given a C++ interface (bug 1271649). +bool Debugger::wrapEnvironment(JSContext* cx, Handle<Env*> env, + MutableHandleValue rval) { + if (!env) { + rval.setNull(); + return true; + } + + Rooted<DebuggerEnvironment*> envobj(cx); + + if (!wrapEnvironment(cx, env, &envobj)) { + return false; + } + + rval.setObject(*envobj); + return true; +} + +bool Debugger::wrapEnvironment(JSContext* cx, Handle<Env*> env, + MutableHandle<DebuggerEnvironment*> result) { + MOZ_ASSERT(env); + + // DebuggerEnv should only wrap a debug scope chain obtained (transitively) + // from GetDebugEnvironmentFor(Frame|Function). + MOZ_ASSERT(!IsSyntacticEnvironment(env)); + + DependentAddPtr<EnvironmentWeakMap> p(cx, environments, env); + if (p) { + result.set(&p->value()->as<DebuggerEnvironment>()); + } else { + // Create a new Debugger.Environment for env. + RootedObject proto( + cx, &object->getReservedSlot(JSSLOT_DEBUG_ENV_PROTO).toObject()); + Rooted<NativeObject*> debugger(cx, object); + + Rooted<DebuggerEnvironment*> 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<DebuggerObject*> dobj(cx); + + if (!wrapDebuggeeObject(cx, obj, &dobj)) { + return false; + } + + vp.setObject(*dobj); + } else if (vp.isMagic()) { + Rooted<PlainObject*> 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<DebuggerObject*> result) { + if (!obj) { + result.set(nullptr); + return true; + } + + return wrapDebuggeeObject(cx, obj, result); +} + +bool Debugger::wrapDebuggeeObject(JSContext* cx, HandleObject obj, + MutableHandle<DebuggerObject*> result) { + MOZ_ASSERT(obj); + + DependentAddPtr<ObjectWeakMap> p(cx, objects, obj); + if (p) { + result.set(&p->value()->as<DebuggerObject>()); + } else { + // Create a new Debugger.Object for obj. + Rooted<NativeObject*> debugger(cx, object); + RootedObject proto( + cx, &object->getReservedSlot(JSSLOT_DEBUG_OBJECT_PROTO).toObject()); + Rooted<DebuggerObject*> 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<DebuggerObject>()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_EXPECTED_TYPE, "Debugger", + "Debugger.Object", obj->getClass()->name); + return nullptr; + } + + return &obj->as<DebuggerObject>(); +} + +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<PropertyDescriptor> desc) { + if (desc.hasValue()) { + RootedValue value(cx, desc.value()); + if (!unwrapDebuggeeValue(cx, &value) || + !CheckArgCompartment(cx, obj, value, "defineProperty", "value")) { + return false; + } + desc.setValue(value); + } + + if (desc.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<PropertyName*> 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<AbstractGeneratorObject*> genObj(cx); + { + AutoRealm ar(cx, frame.callee()); + genObj = GetGeneratorObjectForFrame(cx, frame); + } + + if (!genObj || genObj->isBeforeInitialYield()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_FORCED_RETURN_DISALLOWED); + return false; + } + } + + return true; +} + +// Last-minute sanity adjustments to resumption. +// +// This is called last, as we leave the debugger. It must happen outside the +// control of the uncaughtExceptionHook, because this code assumes we won't +// change our minds and continue execution--we must not close the generator +// object unless we're really going to force-return. +[[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: <value>}` like a `return` statement. Simulate what the + // debuggee would do for an ordinary `return` statement, using a few bytecode + // instructions. It's simpler to do the work manually than to count on that + // bytecode sequence existing in the debuggee, somehow jump to it, and then + // avoid re-entering the debugger from it. + // + // Similarly treat `{throw: <value>}` like a `throw` statement. + // + // 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<AbstractGeneratorObject*> genObj( + cx, GetGeneratorObjectForFrame(cx, frame)); + + // We already went through CheckResumptionValue, which would have replaced + // this invalid resumption value with an error if we were trying to force + // return before the initial yield. + MOZ_RELEASE_ASSERT(genObj && !genObj->isBeforeInitialYield()); + + // 1. `return <value>` creates and returns a new object, + // `{value: <value>, done: true}`. + // + // For non-async generators, the iterator result object is created in + // bytecode, so we have to simulate that here. For async generators, our + // C++ implementation of AsyncGeneratorResolve will do this. So don't do it + // twice: + if (!genObj->is<AsyncGeneratorObject>()) { + PlainObject* pair = CreateIterResultObject(cx, vp, true); + if (!pair) { + return false; + } + vp.setObject(*pair); + } + + // 2. The generator must be closed. + genObj->setClosed(); + + // Async generators have additionally bookkeeping which must be adjusted + // when switching over to the closed state. + if (genObj->is<AsyncGeneratorObject>()) { + genObj->as<AsyncGeneratorObject>().setCompleted(); + } + } else if (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<AsyncFunctionGeneratorObject*> generator( + cx, &genObj->as<AsyncFunctionGeneratorObject>()); + + // 1. `return <value>` fulfills and returns the async function's promise. + Rooted<PromiseObject*> 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 <value>` creates a promise rejected with the value *vp. + // 1. `return <value>` creates a promise resolved with the value *vp. + JSObject* promise = resumeMode == ResumeMode::Throw + ? PromiseObject::unforgeableReject(cx, vp) + : PromiseObject::unforgeableResolve(cx, vp); + if (!promise) { + return false; + } + vp.setObject(*promise); + + // 2. Return normally in both cases. + resumeMode = ResumeMode::Return; + } + } + + return true; +} + +bool Debugger::processParsedHandlerResult(JSContext* cx, AbstractFramePtr frame, + 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<SavedFrame*> 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<AbstractGeneratorObject*> 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<NativeObject*> 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<NativeObject*> 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<NativeObject*> 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<NativeObject*> 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<NativeObject*> 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<NativeObject*> 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<SavedFrame*> exnStack; + ToResumeModeMatcher(MutableHandleValue value, + MutableHandle<SavedFrame*> 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<SavedFrame*> 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<DebuggerScriptReferent> 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 <typename HookIsEnabledFun /* bool (Debugger*) */, + typename FireHookFun /* bool (Debugger*) */> +/* static */ +void Debugger::dispatchQuietHook(JSContext* cx, HookIsEnabledFun hookIsEnabled, + FireHookFun fireHook) { + DebuggerList<HookIsEnabledFun> 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 <typename HookIsEnabledFun /* bool (Debugger*) */, typename FireHookFun /* bool (Debugger*, ResumeMode&, MutableHandleValue vp) */> +/* static */ +bool Debugger::dispatchResumptionHook(JSContext* cx, AbstractFramePtr frame, + HookIsEnabledFun hookIsEnabled, + FireHookFun fireHook) { + DebuggerList<HookIsEnabledFun> 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<ArrayObject*> 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, JS_AtomizeString(cx, 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<DebuggerScriptReferent> scriptReferent(cx, base); + return dbg->fireNewScript(cx, scriptReferent); + }); +} + +void DebugAPI::slowPathOnNewWasmInstance( + JSContext* cx, Handle<WasmInstanceObject*> wasmInstance) { + Debugger::dispatchQuietHook( + cx, + [wasmInstance](Debugger* dbg) -> bool { + return dbg->observesNewScript() && + dbg->observesGlobal(&wasmInstance->global()); + }, + [&](Debugger* dbg) -> bool { + Rooted<DebuggerScriptReferent> scriptReferent(cx, wasmInstance.get()); + return dbg->fireNewScript(cx, scriptReferent); + }); +} + +/* static */ +bool DebugAPI::onTrap(JSContext* cx) { + FrameIter iter(cx); + JS::AutoSaveExceptionState savedExc(cx); + Rooted<GlobalObject*> 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<Breakpoint*> 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<JSObject*> 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<Debugger::DebuggerFrameVector> 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(); + for (Realm::DebuggerVectorEntry& entry : cx->global()->getDebuggers()) { + 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<DebuggerFrame*> 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<GlobalObject*> 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<GlobalObject*> 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) { + 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<double> DebugAPI::allocationSamplingProbability(GlobalObject* global) { + Realm::DebuggerVector& dbgs = global->getDebuggers(); + if (dbgs.empty()) { + return Nothing(); + } + + DebugOnly<Realm::DebuggerVectorEntry*> 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<SavedFrame*> frame, + mozilla::TimeStamp when, + Realm::DebuggerVector& dbgs) { + MOZ_ASSERT(!dbgs.empty()); + mozilla::DebugOnly<Realm::DebuggerVectorEntry*> begin = dbgs.begin(); + + // Root all the Debuggers while we're iterating over them; + // appendAllocationSite calls Compartment::wrap, and thus can GC. + // + // SpiderMonkey protocol is generally for the caller to prove that it has + // rooted the stuff it's asking you to operate on (i.e. by passing a + // Handle), but in this case, we're iterating over a global's list of + // Debuggers, and globals only hold their Debuggers weakly. + Rooted<GCVector<JSObject*>> activeDebuggers(cx, GCVector<JSObject*>(cx)); + for (auto p = dbgs.begin(); p < dbgs.end(); p++) { + if (!activeDebuggers.append(p->dbg->object)) { + return 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); + + 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<SavedFrame*> 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<PromiseObject*> 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<PromiseObject*> promise) { + Debugger::slowPathPromiseHook(cx, Debugger::OnNewPromise, promise); +} + +/* static */ +void DebugAPI::slowPathOnPromiseSettled(JSContext* cx, + Handle<PromiseObject*> promise) { + Debugger::slowPathPromiseHook(cx, Debugger::OnPromiseSettled, promise); +} + +/*** Debugger code invalidation for observing execution *********************/ + +class MOZ_RAII ExecutionObservableRealms + : public DebugAPI::ExecutionObservableSet { + HashSet<Realm*> realms_; + HashSet<Zone*> 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<Realm*>::Range; + const HashSet<Realm*>* realms() const { return &realms_; } + + const HashSet<Zone*>* 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<JSScript*>& 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<JSScript*> 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<BaseScript>(); !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 <typename FrameFn> +/* static */ +void Debugger::forEachOnStackDebuggerFrame(AbstractFramePtr frame, FrameFn fn) { + for (Realm::DebuggerVectorEntry& entry : frame.global()->getDebuggers()) { + Debugger* dbg = entry.dbg; + if (FrameMap::Ptr frameEntry = dbg->frames.lookup(frame)) { + fn(dbg, frameEntry->value()); + } + } +} + +template <typename FrameFn> +/* static */ +void Debugger::forEachOnStackOrSuspendedDebuggerFrame(JSContext* cx, + AbstractFramePtr frame, + FrameFn fn) { + Rooted<AbstractGeneratorObject*> genObj( + cx, frame.isGeneratorFrame() ? GetGeneratorObjectForFrame(cx, frame) + : nullptr); + + for (Realm::DebuggerVectorEntry& entry : frame.global()->getDebuggers()) { + 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<DebuggerFrameVector> frames) { + bool hadOOM = false; + forEachOnStackDebuggerFrame(frame, [&](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) { + for (Realm::DebuggerVectorEntry& entry : debuggee.getDebuggers()) { + // 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<GlobalObject*> 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<GlobalObject*> g(cx); + for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); + r.popFront()) { + // This should always succeed, since we already checked for the + // error case above. + g = r.front().get(); + MOZ_ALWAYS_TRUE(Debugger::addAllocationsTracking(cx, g)); + } + + return true; +} + +void Debugger::removeAllocationsTrackingForAllDebuggees() { + for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); + r.popFront()) { + Debugger::removeAllocationsTracking(*r.front().get()); + } + + allocationsLog.clear(); +} + +/*** Debugger JSObjects *****************************************************/ + +template <typename F> +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>()) { + DebuggerFrame* frame = &src->as<DebuggerFrame>(); + Debugger* dbg = frame->owner(); + MOZ_ASSERT(RuntimeHasDebugger(rt, dbg)); + + if (dst.is<BaseScript>()) { + // 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<BaseScript>() && + dbg->generatorFrames.hasEntry(genObj, src); + } + return dst.is<JSObject>() && + dst.as<JSObject>().is<AbstractGeneratorObject>() && + dbg->generatorFrames.hasEntry( + &dst.as<JSObject>().as<AbstractGeneratorObject>(), src); + } + if (src->is<DebuggerObject>()) { + Debugger* dbg = src->as<DebuggerObject>().owner(); + MOZ_ASSERT(RuntimeHasDebugger(rt, dbg)); + return dst.is<JSObject>() && + dbg->objects.hasEntry(&dst.as<JSObject>(), src); + } + if (src->is<DebuggerEnvironment>()) { + Debugger* dbg = src->as<DebuggerEnvironment>().owner(); + MOZ_ASSERT(RuntimeHasDebugger(rt, dbg)); + return dst.is<JSObject>() && + dbg->environments.hasEntry(&dst.as<JSObject>(), src); + } + if (src->is<DebuggerScript>()) { + Debugger* dbg = src->as<DebuggerScript>().owner(); + MOZ_ASSERT(RuntimeHasDebugger(rt, dbg)); + + return src->as<DebuggerScript>().getReferent().match( + [=](BaseScript* script) { + return dst.is<BaseScript>() && script == &dst.as<BaseScript>() && + dbg->scripts.hasEntry(script, src); + }, + [=](WasmInstanceObject* instance) { + return dst.is<JSObject>() && instance == &dst.as<JSObject>() && + dbg->wasmInstanceScripts.hasEntry(instance, src); + }); + } + if (src->is<DebuggerSource>()) { + Debugger* dbg = src->as<DebuggerSource>().owner(); + MOZ_ASSERT(RuntimeHasDebugger(rt, dbg)); + + return src->as<DebuggerSource>().getReferent().match( + [=](ScriptSourceObject* sso) { + return dst.is<JSObject>() && sso == &dst.as<JSObject>() && + dbg->sources.hasEntry(sso, src); + }, + [=](WasmInstanceObject* instance) { + return dst.is<JSObject>() && instance == &dst.as<JSObject>() && + 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<DebuggerFrame*>& 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 MovableCellHasher + // 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<AutoLockGC> lock; + GCMarker* marker = GCMarker::fromTracer(tracer); + if (marker->isParallelMarking()) { + // Synchronise access to generatorFrames. + lock.emplace(marker->runtime()); + } + + for (Realm::DebuggerVectorEntry& entry : generator->realm()->getDebuggers()) { + Debugger* dbg = entry.dbg.unbarrieredGet(); + + if (Debugger::GeneratorWeakMap::Ptr entry = + dbg->generatorFrames.lookupUnbarriered(generator)) { + HeapPtr<DebuggerFrame*>& 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<DebuggerFrame*>& 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) { + for (Realm::DebuggerVectorEntry& entry : realm->getDebuggers()) { + 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 <class UnbarrieredKey, class Wrapper, bool InvisibleKeysOk> +bool DebuggerWeakMap<UnbarrieredKey, Wrapper, + InvisibleKeysOk>::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<DebuggerInstanceObject>()) { + 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(); + + using Method = bool (CallData::*)(); + + template <Method MyMethod> + static bool ToNative(JSContext* cx, unsigned argc, Value* vp); +}; + +template <Debugger::CallData::Method MyMethod> +/* 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<Hook>(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<Hook>(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<DebuggerDebuggeeLink*> 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<JSObject*> 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<GlobalObject>()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_UNEXPECTED_TYPE, "argument", + "not a global object"); + return nullptr; + } + + return &obj->as<GlobalObject>(); +} + +bool Debugger::CallData::addDebuggee() { + if (!args.requireAtLeast(cx, "Debugger.addDebuggee", 1)) { + return false; + } + Rooted<GlobalObject*> global(cx, dbg->unwrapDebuggeeArgument(cx, args[0])); + if (!global) { + return false; + } + + if (!dbg->addDebuggeeGlobal(cx, global)) { + return false; + } + + RootedValue v(cx, ObjectValue(*global)); + if (!dbg->wrapDebuggeeValue(cx, &v)) { + return false; + } + args.rval().set(v); + return true; +} + +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<GlobalObject*> 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<GlobalObject*> 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->getDebuggers().empty() && !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<GlobalObject*> global(cx, e.front()); + dbg->removeDebuggeeGlobal(cx->gcContext(), global, &e, FromSweep::No); + + // See note about adding to the observable set in removeDebuggee. + if (global->getDebuggers().empty() && !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<ArrayObject*> 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<CrossCompartmentWrapperObject>()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_CCW_REQUIRED, "Debugger"); + return false; + } + } + + // Get Debugger.prototype. + RootedValue v(cx); + RootedObject callee(cx, &args.callee()); + if (!GetProperty(cx, callee, callee, cx->names().prototype, &v)) { + return false; + } + Rooted<NativeObject*> proto(cx, &v.toObject().as<NativeObject>()); + MOZ_ASSERT(proto->is<DebuggerPrototypeObject>()); + + // 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<DebuggerInstanceObject*> obj( + cx, NewTenuredObjectWithGivenProto<DebuggerInstanceObject>(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<NativeObject*> livenessLink( + cx, NewObjectWithGivenProto<DebuggerDebuggeeLink>(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<Debugger>(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<ProxyObject>().private_().toObject(); + Rooted<GlobalObject*> debuggee(cx, &wrappedObj.nonCCWGlobal()); + if (!debugger->addDebuggeeGlobal(cx, debuggee)) { + return false; + } + } + + args.rval().setObject(*obj); + return true; +} + +bool Debugger::addDebuggeeGlobal(JSContext* cx, Handle<GlobalObject*> global) { + if (debuggees.has(global)) { + return true; + } + + // Callers should generally be unable to get a reference to a debugger- + // invisible global in order to pass it to addDebuggee. But this is possible + // with certain testing aides we expose in the shell, so just make addDebuggee + // throw in that case. + 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<Realm*> 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()) { + for (Realm::DebuggerVectorEntry& entry : realm->getDebuggers()) { + 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) + auto& globalDebuggers = global->getDebuggers(); + 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 <typename T, typename AP> +static T* findDebuggerInVector(Debugger* dbg, Vector<T, 0, AP>* 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); + } + } + + auto& globalDebuggersVector = global->getDebuggers(); + + // The relation must be removed from up to three places: + // globalDebuggersVector and debuggees for sure, and possibly the + // compartment's debuggee set. + // + // The debuggee zone set is recomputed on demand. This avoids refcounting + // and in practice we have relatively few debuggees that tend to all be in + // the same zone. If after recomputing the debuggee zone set, this global's + // zone is not in the set, then we must remove ourselves from the zone's + // vector of observing debuggers. + globalDebuggersVector.erase( + findDebuggerInVector(this, &globalDebuggersVector)); + + if (debugEnum) { + debugEnum->removeFront(); + } else { + debuggees.remove(global); + } + + recomputeDebuggeeZoneSet(); + + // 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()->getDebuggers().empty()) { + 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<Realm*, DefaultHasher<Realm*>, 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<ScriptSourceObject*>(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<DebuggerSource>()) { + 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<DebuggerSource>(); + + // 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<BaseScript*> 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<JSObject>() || !thing.as<JSObject>().is<JSFunction>()) { + continue; + } + JSFunction* fun = &thing.as<JSObject>().as<JSFunction>(); + 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<Realm*, BaseScript*, DefaultHasher<Realm*>>; + + Rooted<RealmToScriptMap> 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<BaseScriptVector> foundScripts() const { return scriptVector; } + + Handle<WasmInstanceObjectVector> 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<JSLinearString*> 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<DebuggerSourceReferent> 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<BaseScriptVector> instead of creating + * the JS array as we go, because we mustn't allocate JS objects or GC while + * we use the CellIter. + */ + Rooted<BaseScriptVector> 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<BaseScriptVector> partialMatchVector; + + /* + * Like above, but for wasm modules. + */ + Rooted<WasmInstanceObjectVector> 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()) { + urlCString = JS_EncodeStringToLatin1(cx, url.toString()); + 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<ScriptQuery*>(data); + self->consider(script, nogc); + } + + template <typename T> + [[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<ScriptSourceObject*>() && + source.as<ScriptSourceObject*>()->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<BaseScriptVector>& 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<BaseScriptVector> scripts(query.foundScripts()); + Handle<WasmInstanceObjectVector> wasmInstances(query.foundWasmInstances()); + + size_t resultLength = scripts.length() + wasmInstances.length(); + Rooted<ArrayObject*> 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<JSObject*, js::MovableCellHasher<JSObject*>, + 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<SourceSet> foundSources() const { return sources; } + + private: + Rooted<SourceSet> sources; + + static void considerScript(JSRuntime* rt, void* data, BaseScript* script, + const JS::AutoRequireNoGC& nogc) { + SourceQuery* self = static_cast<SourceQuery*>(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<ScriptSourceObject>()) { + return AsVariant(&obj->as<ScriptSourceObject>()); + } + return AsVariant(&obj->as<WasmInstanceObject>()); +} + +bool Debugger::CallData::findSources() { + SourceQuery query(cx, dbg); + if (!query.findSources()) { + return false; + } + + Handle<SourceQuery::SourceSet> sources(query.foundSources()); + + size_t resultLength = sources.count(); + Rooted<ArrayObject*> 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<DebuggerSourceReferent> 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<ObjectQuery>; + 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<JSObject>() || referent.exposeToJS().isUndefined()) { + return true; + } + + JSObject* obj = referent.as<JSObject>(); + + 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<ArrayObject*> 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<ArrayObject>().length(); i++) { + Value v = holder->as<ArrayObject>().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<GlobalObject*> global(cx, dbg->unwrapDebuggeeArgument(cx, args[0])); + if (!global) { + return false; + } + + // If we create a D.O referring to a global in an invisible 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<frontend::CompilationInput> input(cx, + frontend::CompilationInput(options)); + if (!input.get().initForGlobal(cx, &fc)) { + return false; + } + + LifoAllocScope allocScope(&cx->tempLifoAlloc()); + frontend::NoScopeBindingCache scopeCache; + frontend::CompilationState compilationState(cx, &fc, allocScope, input.get()); + if (!compilationState.init(cx, &fc, &scopeCache)) { + return false; + } + + frontend::Parser<frontend::FullParseHandler, char16_t> parser( + cx, &fc, cx->stackLimitForCurrentPrincipal(), 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<ScriptSourceObject*> 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<WasmInstanceObject*> 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<DebuggerFrame>()) { + JS_ReportErrorASCII(cx, "Argument is not a Debugger.Frame"); + return false; + } + + RootedValue objVal(cx, ObjectValue(*obj)); + Rooted<DebuggerFrame*> frameObj(cx, DebuggerFrame::check(cx, objVal)); + if (!frameObj) { + return false; + } + + Rooted<DebuggerFrame*> 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<AbstractGeneratorObject*> 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<DebuggerSource>()) { + JS_ReportErrorASCII(cx, "Argument is not a Debugger.Source"); + return false; + } + + Rooted<DebuggerSource*> sourceObj(cx, &obj->as<DebuggerSource>()); + if (!sourceObj->getReferentRawObject()) { + JS_ReportErrorASCII(cx, "Argument is Debugger.Source.prototype"); + return false; + } + + Rooted<DebuggerSourceReferent> 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<GlobalObject*> 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<GlobalObject*> global(cx, dbg->unwrapDebuggeeArgument(cx, args[0])); + if (!global) { + return false; + } + + global->realm()->isAsyncStackCapturingEnabled = 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_FS_END}; + +const JSFunctionSpec Debugger::static_methods[]{ + JS_FN("isCompilableUnit", Debugger::isCompilableUnit, 1, 0), JS_FS_END}; + +DebuggerScript* Debugger::newDebuggerScript( + JSContext* cx, Handle<DebuggerScriptReferent> referent) { + cx->check(object.get()); + + RootedObject proto( + cx, &object->getReservedSlot(JSSLOT_DEBUG_SCRIPT_PROTO).toObject()); + MOZ_ASSERT(proto); + Rooted<NativeObject*> debugger(cx, object); + + return DebuggerScript::create(cx, proto, referent, debugger); +} + +template <typename ReferentType, typename Map> +typename Map::WrapperType* Debugger::wrapVariantReferent( + JSContext* cx, Map& map, + Handle<typename Map::WrapperType::ReferentVariant> referent) { + cx->check(object); + + Handle<ReferentType*> untaggedReferent = + referent.template as<ReferentType*>(); + MOZ_ASSERT(cx->compartment() != untaggedReferent->compartment()); + + DependentAddPtr<Map> 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<typename Map::WrapperType>(); +} + +DebuggerScript* Debugger::wrapVariantReferent( + JSContext* cx, Handle<DebuggerScriptReferent> referent) { + if (referent.is<BaseScript*>()) { + return wrapVariantReferent<BaseScript>(cx, scripts, referent); + } + + return wrapVariantReferent<WasmInstanceObject>(cx, wasmInstanceScripts, + referent); +} + +DebuggerScript* Debugger::wrapScript(JSContext* cx, + Handle<BaseScript*> script) { + Rooted<DebuggerScriptReferent> referent(cx, + DebuggerScriptReferent(script.get())); + return wrapVariantReferent(cx, referent); +} + +DebuggerScript* Debugger::wrapWasmScript( + JSContext* cx, Handle<WasmInstanceObject*> wasmInstance) { + Rooted<DebuggerScriptReferent> referent(cx, wasmInstance.get()); + return wrapVariantReferent(cx, referent); +} + +DebuggerSource* Debugger::newDebuggerSource( + JSContext* cx, Handle<DebuggerSourceReferent> referent) { + cx->check(object.get()); + + RootedObject proto( + cx, &object->getReservedSlot(JSSLOT_DEBUG_SOURCE_PROTO).toObject()); + MOZ_ASSERT(proto); + Rooted<NativeObject*> debugger(cx, object); + return DebuggerSource::create(cx, proto, referent, debugger); +} + +DebuggerSource* Debugger::wrapVariantReferent( + JSContext* cx, Handle<DebuggerSourceReferent> referent) { + DebuggerSource* obj; + if (referent.is<ScriptSourceObject*>()) { + obj = wrapVariantReferent<ScriptSourceObject>(cx, sources, referent); + } else { + obj = wrapVariantReferent<WasmInstanceObject>(cx, wasmInstanceSources, + referent); + } + MOZ_ASSERT_IF(obj, obj->getReferent() == referent); + return obj; +} + +DebuggerSource* Debugger::wrapSource(JSContext* cx, + Handle<ScriptSourceObject*> source) { + Rooted<DebuggerSourceReferent> referent(cx, source.get()); + return wrapVariantReferent(cx, referent); +} + +DebuggerSource* Debugger::wrapWasmSource( + JSContext* cx, Handle<WasmInstanceObject*> wasmInstance) { + Rooted<DebuggerSourceReferent> 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<DebuggerFrameVector> 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<DebuggerFrame*> 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; + Debugger::forEachOnStackDebuggerFrame( + frame, [&](Debugger*, DebuggerFrame* frameobj) { foundAny = true; }); + return foundAny; +} + +/* static */ +void Debugger::suspendGeneratorDebuggerFrames(JSContext* cx, + AbstractFramePtr frame) { + JS::GCContext* gcx = cx->gcContext(); + forEachOnStackDebuggerFrame( + frame, [&](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(); + + forEachOnStackOrSuspendedDebuggerFrame( + cx, frame, [&](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<DebuggerDebuggeeLink>(); +} + +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<PlainObject*> 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<NativeObject*> debugCtor(cx), debugProto(cx), frameProto(cx), + scriptProto(cx), sourceProto(cx), objectProto(cx), envProto(cx), + memoryProto(cx); + RootedObject debuggeeWouldRunProto(cx); + RootedValue debuggeeWouldRunCtor(cx); + Handle<GlobalObject*> global = obj.as<GlobalObject>(); + + 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<DebuggerInstanceObject>()) { + 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<JSObject*>(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<DebuggerScript>()) { + referent = obj->as<DebuggerScript>().getReferentCell(); + } else if (obj->is<DebuggerSource>()) { + referent = obj->as<DebuggerSource>().getReferentRawObject(); + } else if (obj->is<DebuggerObject>()) { + referent = obj->as<DebuggerObject>().referent(); + } else if (obj->is<DebuggerEnvironment>()) { + referent = obj->as<DebuggerEnvironment>().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<GarbageCollectionEvent>(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<ArrayObject*> 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<PlainObject*> 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 diff --git a/js/src/debugger/Debugger.h b/js/src/debugger/Debugger.h new file mode 100644 index 0000000000..3af7575806 --- /dev/null +++ b/js/src/debugger/Debugger.h @@ -0,0 +1,1631 @@ +/* -*- 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/. */ + +#ifndef debugger_Debugger_h +#define debugger_Debugger_h + +#include "mozilla/Assertions.h" // for MOZ_ASSERT_HELPER1 +#include "mozilla/Attributes.h" // for MOZ_RAII +#include "mozilla/DoublyLinkedList.h" // for DoublyLinkedListElement +#include "mozilla/HashTable.h" // for HashSet, DefaultHasher (ptr only) +#include "mozilla/LinkedList.h" // for LinkedList (ptr only) +#include "mozilla/Maybe.h" // for Maybe, Nothing +#include "mozilla/Range.h" // for Range +#include "mozilla/Result.h" // for Result +#include "mozilla/TimeStamp.h" // for TimeStamp +#include "mozilla/Variant.h" // for Variant + +#include <stddef.h> // for size_t +#include <stdint.h> // for uint32_t, uint64_t, uintptr_t +#include <utility> // for std::move + +#include "jstypes.h" // for JS_GC_ZEAL +#include "NamespaceImports.h" // for Value, HandleObject + +#include "debugger/DebugAPI.h" // for DebugAPI +#include "debugger/Object.h" // for DebuggerObject +#include "ds/TraceableFifo.h" // for TraceableFifo +#include "gc/Barrier.h" // +#include "gc/Tracer.h" // for TraceNullableEdge, TraceEdge +#include "gc/WeakMap.h" // for WeakMap +#include "gc/ZoneAllocator.h" // for ZoneAllocPolicy +#include "js/Debug.h" // JS_DefineDebuggerObject +#include "js/GCAPI.h" // for GarbageCollectionEvent +#include "js/GCVariant.h" // for GCVariant +#include "js/Proxy.h" // for PropertyDescriptor +#include "js/RootingAPI.h" // for Handle +#include "js/TracingAPI.h" // for TraceRoot +#include "js/Wrapper.h" // for UncheckedUnwrap +#include "proxy/DeadObjectProxy.h" // for IsDeadProxyObject +#include "vm/GeneratorObject.h" // for AbstractGeneratorObject +#include "vm/GlobalObject.h" // for GlobalObject +#include "vm/JSContext.h" // for JSContext +#include "vm/JSObject.h" // for JSObject +#include "vm/JSScript.h" // for JSScript, ScriptSourceObject +#include "vm/NativeObject.h" // for NativeObject +#include "vm/Runtime.h" // for JSRuntime +#include "vm/SavedFrame.h" // for SavedFrame +#include "vm/Stack.h" // for AbstractFramePtr, FrameIter +#include "vm/StringType.h" // for JSAtom +#include "wasm/WasmJS.h" // for WasmInstanceObject + +class JS_PUBLIC_API JSFunction; + +namespace JS { +class JS_PUBLIC_API AutoStableStringChars; +class JS_PUBLIC_API Compartment; +class JS_PUBLIC_API Realm; +class JS_PUBLIC_API Zone; +} /* namespace JS */ + +namespace js { +class AutoRealm; +class CrossCompartmentKey; +class Debugger; +class DebuggerEnvironment; +class PromiseObject; +namespace gc { +struct Cell; +} /* namespace gc */ +namespace wasm { +class Instance; +} /* namespace wasm */ +} /* namespace js */ + +/* + * Windows 3.x used a cooperative multitasking model, with a Yield macro that + * let you relinquish control to other cooperative threads. Microsoft replaced + * it with an empty macro long ago. We should be free to use it in our code. + */ +#undef Yield + +namespace js { + +class Breakpoint; +class DebuggerFrame; +class DebuggerScript; +class DebuggerSource; +class DebuggerMemory; +class ScriptedOnStepHandler; +class ScriptedOnPopHandler; +class DebuggerDebuggeeLink; + +/** + * Tells how the JS engine should resume debuggee execution after firing a + * debugger hook. Most debugger hooks get to choose how the debuggee proceeds; + * see js/src/doc/Debugger/Conventions.md under "Resumption Values". + * + * Debugger::processHandlerResult() translates between JavaScript values and + * this enum. + */ +enum class ResumeMode { + /** + * The debuggee should continue unchanged. + * + * This corresponds to a resumption value of `undefined`. + */ + Continue, + + /** + * Throw an exception in the debuggee. + * + * This corresponds to a resumption value of `{throw: <value>}`. + */ + Throw, + + /** + * Terminate the debuggee, as if it had been cancelled via the "slow + * script" ribbon. + * + * This corresponds to a resumption value of `null`. + */ + Terminate, + + /** + * Force the debuggee to return from the current frame. + * + * This corresponds to a resumption value of `{return: <value>}`. + */ + Return, +}; + +/** + * A completion value, describing how some sort of JavaScript evaluation + * completed. This is used to tell an onPop handler what's going on with the + * frame, and to report the outcome of call, apply, setProperty, and getProperty + * operations. + * + * Local variables of type Completion should be held in Rooted locations, + * and passed using Handle and MutableHandle. + */ +class Completion { + public: + struct Return { + explicit Return(const Value& value) : value(value) {} + Value value; + + void trace(JSTracer* trc) { + JS::TraceRoot(trc, &value, "js::Completion::Return::value"); + } + }; + + struct Throw { + Throw(const Value& exception, SavedFrame* stack) + : exception(exception), stack(stack) {} + Value exception; + SavedFrame* stack; + + void trace(JSTracer* trc) { + JS::TraceRoot(trc, &exception, "js::Completion::Throw::exception"); + JS::TraceRoot(trc, &stack, "js::Completion::Throw::stack"); + } + }; + + struct Terminate { + void trace(JSTracer* trc) {} + }; + + struct InitialYield { + explicit InitialYield(AbstractGeneratorObject* generatorObject) + : generatorObject(generatorObject) {} + AbstractGeneratorObject* generatorObject; + + void trace(JSTracer* trc) { + JS::TraceRoot(trc, &generatorObject, + "js::Completion::InitialYield::generatorObject"); + } + }; + + struct Yield { + Yield(AbstractGeneratorObject* generatorObject, const Value& iteratorResult) + : generatorObject(generatorObject), iteratorResult(iteratorResult) {} + AbstractGeneratorObject* generatorObject; + Value iteratorResult; + + void trace(JSTracer* trc) { + JS::TraceRoot(trc, &generatorObject, + "js::Completion::Yield::generatorObject"); + JS::TraceRoot(trc, &iteratorResult, + "js::Completion::Yield::iteratorResult"); + } + }; + + struct Await { + Await(AbstractGeneratorObject* generatorObject, const Value& awaitee) + : generatorObject(generatorObject), awaitee(awaitee) {} + AbstractGeneratorObject* generatorObject; + Value awaitee; + + void trace(JSTracer* trc) { + JS::TraceRoot(trc, &generatorObject, + "js::Completion::Await::generatorObject"); + JS::TraceRoot(trc, &awaitee, "js::Completion::Await::awaitee"); + } + }; + + // The JS::Result macros want to assign to an existing variable, so having a + // default constructor is handy. + Completion() : variant(Terminate()) {} + + // Construct a completion from a specific variant. + // + // Unfortunately, using a template here would prevent the implicit definitions + // of the copy and move constructor and assignment operators, which is icky. + explicit Completion(Return&& variant) + : variant(std::forward<Return>(variant)) {} + explicit Completion(Throw&& variant) + : variant(std::forward<Throw>(variant)) {} + explicit Completion(Terminate&& variant) + : variant(std::forward<Terminate>(variant)) {} + explicit Completion(InitialYield&& variant) + : variant(std::forward<InitialYield>(variant)) {} + explicit Completion(Yield&& variant) + : variant(std::forward<Yield>(variant)) {} + explicit Completion(Await&& variant) + : variant(std::forward<Await>(variant)) {} + + // Capture a JavaScript operation result as a Completion value. This clears + // any exception and stack from cx, taking ownership of them itself. + static Completion fromJSResult(JSContext* cx, bool ok, const Value& rv); + + // Construct a completion given an AbstractFramePtr that is being popped. This + // clears any exception and stack from cx, taking ownership of them itself. + static Completion fromJSFramePop(JSContext* cx, AbstractFramePtr frame, + const jsbytecode* pc, bool ok); + + template <typename V> + bool is() const { + return variant.template is<V>(); + } + + template <typename V> + V& as() { + return variant.template as<V>(); + } + + template <typename V> + const V& as() const { + return variant.template as<V>(); + } + + void trace(JSTracer* trc); + + /* True if this completion is a suspension of a generator or async call. */ + bool suspending() const { + return variant.is<InitialYield>() || variant.is<Yield>() || + variant.is<Await>(); + } + + /* Set `result` to a Debugger API completion value describing this completion. + */ + bool buildCompletionValue(JSContext* cx, Debugger* dbg, + MutableHandleValue result) const; + + /* + * Set `resumeMode`, `value`, and `exnStack` to values describing this + * completion. + */ + void toResumeMode(ResumeMode& resumeMode, MutableHandleValue value, + MutableHandle<SavedFrame*> exnStack) const; + /* + * Given a `ResumeMode` and value (typically derived from a resumption value + * returned by a Debugger hook), update this completion as requested. + */ + void updateFromHookResult(ResumeMode resumeMode, HandleValue value); + + private: + using Variant = + mozilla::Variant<Return, Throw, Terminate, InitialYield, Yield, Await>; + struct BuildValueMatcher; + struct ToResumeModeMatcher; + + Variant variant; +}; + +typedef HashSet<WeakHeapPtr<GlobalObject*>, + MovableCellHasher<WeakHeapPtr<GlobalObject*>>, ZoneAllocPolicy> + WeakGlobalObjectSet; + +#ifdef DEBUG +extern void CheckDebuggeeThing(BaseScript* script, bool invisibleOk); + +extern void CheckDebuggeeThing(JSObject* obj, bool invisibleOk); +#endif + +/* + * [SMDOC] Cross-compartment weakmap entries for Debugger API objects + * + * The Debugger API creates objects like Debugger.Object, Debugger.Script, + * Debugger.Environment, etc. to refer to things in the debuggee. Each Debugger + * gets at most one Debugger.Mumble for each referent: Debugger.Mumbles are + * unique per referent per Debugger. This is accomplished by storing the + * debugger objects in a DebuggerWeakMap, using the debuggee thing as the key. + * + * Since a Debugger and its debuggee must be in different compartments, a + * Debugger.Mumble's pointer to its referent is a cross-compartment edge, from + * the debugger's compartment into the debuggee compartment. Like any other sort + * of cross-compartment edge, the GC needs to be able to find all of these edges + * readily. The GC therefore consults the debugger's weakmap tables as + * necessary. This allows the garbage collector to easily find edges between + * debuggee object compartments and debugger compartments when calculating the + * zone sweep groups. + * + * The current implementation results in all debuggee object compartments being + * swept in the same group as the debugger. This is a conservative approach, and + * compartments may be unnecessarily grouped. However this results in a simpler + * and faster implementation. + */ + +/* + * A weakmap from GC thing keys to JSObject values that supports the keys being + * in different compartments to the values. All values must be in the same + * compartment. + * + * If InvisibleKeysOk is true, then the map can have keys in invisible-to- + * debugger compartments. If it is false, we assert that such entries are never + * created. + * + * Note that keys in these weakmaps can be in any compartment, debuggee or not, + * because they are not deleted when a compartment is no longer a debuggee: the + * values need to maintain object identity across add/remove/add + * transitions. (Frames are an exception to the rule. Existing Debugger.Frame + * objects are killed if their realm is removed as a debugger; if the realm + * beacomes a debuggee again later, new Frame objects are created.) + */ +template <class Referent, class Wrapper, bool InvisibleKeysOk = false> +class DebuggerWeakMap : private WeakMap<HeapPtr<Referent*>, HeapPtr<Wrapper*>> { + private: + using Key = HeapPtr<Referent*>; + using Value = HeapPtr<Wrapper*>; + + JS::Compartment* compartment; + + public: + typedef WeakMap<Key, Value> Base; + using ReferentType = Referent; + using WrapperType = Wrapper; + + explicit DebuggerWeakMap(JSContext* cx) + : Base(cx), compartment(cx->compartment()) {} + + public: + // Expose those parts of HashMap public interface that are used by Debugger + // methods. + + using Entry = typename Base::Entry; + using Ptr = typename Base::Ptr; + using AddPtr = typename Base::AddPtr; + using Range = typename Base::Range; + using Lookup = typename Base::Lookup; + + // Expose WeakMap public interface. + + using Base::all; + using Base::has; + using Base::lookup; + using Base::lookupForAdd; + using Base::lookupUnbarriered; + using Base::remove; + using Base::trace; + using Base::zone; +#ifdef DEBUG + using Base::hasEntry; +#endif + + class Enum : public Base::Enum { + public: + explicit Enum(DebuggerWeakMap& map) : Base::Enum(map) {} + }; + + template <typename KeyInput, typename ValueInput> + bool relookupOrAdd(AddPtr& p, const KeyInput& k, const ValueInput& v) { + MOZ_ASSERT(v->compartment() == this->compartment); +#ifdef DEBUG + CheckDebuggeeThing(k, InvisibleKeysOk); +#endif + MOZ_ASSERT(!Base::has(k)); + bool ok = Base::relookupOrAdd(p, k, v); + return ok; + } + + public: + void traceCrossCompartmentEdges(JSTracer* tracer) { + for (Enum e(*this); !e.empty(); e.popFront()) { + TraceEdge(tracer, &e.front().mutableKey(), "Debugger WeakMap key"); + e.front().value()->trace(tracer); + } + } + + bool findSweepGroupEdges() override; + + private: +#ifdef JS_GC_ZEAL + // Let the weak map marking verifier know that this map can + // contain keys in other zones. + virtual bool allowKeysInOtherZones() const override { return true; } +#endif +}; + +class LeaveDebuggeeNoExecute; + +class MOZ_RAII EvalOptions { + JS::UniqueChars filename_; + unsigned lineno_ = 1; + bool hideFromDebugger_ = false; + + public: + EvalOptions() = default; + ~EvalOptions() = default; + const char* filename() const { return filename_.get(); } + unsigned lineno() const { return lineno_; } + bool hideFromDebugger() const { return hideFromDebugger_; } + [[nodiscard]] bool setFilename(JSContext* cx, const char* filename); + void setLineno(unsigned lineno) { lineno_ = lineno; } + void setHideFromDebugger(bool hide) { hideFromDebugger_ = hide; } +}; + +/* + * Env is the type of what ECMA-262 calls "lexical environments" (the records + * that represent scopes and bindings). See vm/EnvironmentObject.h. + * + * This is JSObject rather than js::EnvironmentObject because GlobalObject and + * some proxies, despite not being in the EnvironmentObject class hierarchy, + * can be in environment chains. + */ +using Env = JSObject; + +// The referent of a Debugger.Script. +// +// - For most scripts, we point at their BaseScript. +// +// - For Web Assembly instances for which we are presenting a script-like +// interface, we point at their WasmInstanceObject. +// +// The DebuggerScript object itself simply stores a Cell* in its private +// pointer, but when we're working with that pointer in C++ code, we'd rather +// not pass around a Cell* and be constantly asserting that, yes, this really +// does point to something okay. Instead, we immediately build an instance of +// this type from the Cell* and use that instead, so we can benefit from +// Variant's static checks. +typedef mozilla::Variant<BaseScript*, WasmInstanceObject*> + DebuggerScriptReferent; + +// The referent of a Debugger.Source. +// +// - For most sources, this is a ScriptSourceObject. +// +// - For Web Assembly instances for which we are presenting a source-like +// interface, we point at their WasmInstanceObject. +// +// The DebuggerSource object actually simply stores a Cell* in its private +// pointer. See the comments for DebuggerScriptReferent for the rationale for +// this type. +typedef mozilla::Variant<ScriptSourceObject*, WasmInstanceObject*> + DebuggerSourceReferent; + +template <typename HookIsEnabledFun /* bool (Debugger*) */> +class MOZ_RAII DebuggerList { + private: + // Note: In the general case, 'debuggers' contains references to objects in + // different compartments--every compartment *except* the debugger's. + RootedValueVector debuggers; + HookIsEnabledFun hookIsEnabled; + + public: + /** + * The hook function will be called during `init()` to build the list of + * active debuggers, and again during dispatch to validate that the hook is + * still active for the given debugger. + */ + DebuggerList(JSContext* cx, HookIsEnabledFun hookIsEnabled) + : debuggers(cx), hookIsEnabled(hookIsEnabled) {} + + [[nodiscard]] bool init(JSContext* cx); + + bool empty() { return debuggers.empty(); } + + template <typename FireHookFun /* ResumeMode (Debugger*) */> + bool dispatchHook(JSContext* cx, FireHookFun fireHook); + + template <typename FireHookFun /* void (Debugger*) */> + void dispatchQuietHook(JSContext* cx, FireHookFun fireHook); + + template <typename FireHookFun /* bool (Debugger*, ResumeMode&, MutableHandleValue) */> + [[nodiscard]] bool dispatchResumptionHook(JSContext* cx, + AbstractFramePtr frame, + FireHookFun fireHook); +}; + +// The Debugger.prototype object. +class DebuggerPrototypeObject : public NativeObject { + public: + static const JSClass class_; +}; + +class DebuggerInstanceObject : public NativeObject { + private: + static const JSClassOps classOps_; + + public: + static const JSClass class_; +}; + +class Debugger : private mozilla::LinkedListElement<Debugger> { + friend class DebugAPI; + friend class Breakpoint; + friend class DebuggerFrame; + friend class DebuggerMemory; + friend class DebuggerInstanceObject; + + template <typename> + friend class DebuggerList; + friend struct JSRuntime::GlobalObjectWatchersLinkAccess<Debugger>; + friend struct JSRuntime::GarbageCollectionWatchersLinkAccess<Debugger>; + friend class SavedStacks; + friend class ScriptedOnStepHandler; + friend class ScriptedOnPopHandler; + friend class mozilla::LinkedListElement<Debugger>; + friend class mozilla::LinkedList<Debugger>; + friend bool(::JS_DefineDebuggerObject)(JSContext* cx, JS::HandleObject obj); + friend bool(::JS::dbg::IsDebugger)(JSObject&); + friend bool(::JS::dbg::GetDebuggeeGlobals)(JSContext*, JSObject&, + MutableHandleObjectVector); + friend bool JS::dbg::FireOnGarbageCollectionHookRequired(JSContext* cx); + friend bool JS::dbg::FireOnGarbageCollectionHook( + JSContext* cx, JS::dbg::GarbageCollectionEvent::Ptr&& data); + + public: + enum Hook { + OnDebuggerStatement, + OnExceptionUnwind, + OnNewScript, + OnEnterFrame, + OnNativeCall, + OnNewGlobalObject, + OnNewPromise, + OnPromiseSettled, + OnGarbageCollection, + HookCount + }; + enum { + JSSLOT_DEBUG_PROTO_START, + JSSLOT_DEBUG_FRAME_PROTO = JSSLOT_DEBUG_PROTO_START, + JSSLOT_DEBUG_ENV_PROTO, + JSSLOT_DEBUG_OBJECT_PROTO, + JSSLOT_DEBUG_SCRIPT_PROTO, + JSSLOT_DEBUG_SOURCE_PROTO, + JSSLOT_DEBUG_MEMORY_PROTO, + JSSLOT_DEBUG_PROTO_STOP, + JSSLOT_DEBUG_DEBUGGER = JSSLOT_DEBUG_PROTO_STOP, + JSSLOT_DEBUG_HOOK_START, + JSSLOT_DEBUG_HOOK_STOP = JSSLOT_DEBUG_HOOK_START + HookCount, + JSSLOT_DEBUG_MEMORY_INSTANCE = JSSLOT_DEBUG_HOOK_STOP, + JSSLOT_DEBUG_DEBUGGEE_LINK, + JSSLOT_DEBUG_COUNT + }; + + // Bring DebugAPI::IsObserving into the Debugger namespace. + using IsObserving = DebugAPI::IsObserving; + static const IsObserving Observing = DebugAPI::Observing; + static const IsObserving NotObserving = DebugAPI::NotObserving; + + // Return true if the given realm is a debuggee of this debugger, + // false otherwise. + bool isDebuggeeUnbarriered(const Realm* realm) const; + + // Return true if this Debugger observed a debuggee that participated in the + // GC identified by the given GC number. Return false otherwise. + // May return false negatives if we have hit OOM. + bool observedGC(uint64_t majorGCNumber) const { + return observedGCs.has(majorGCNumber); + } + + // Notify this Debugger that one or more of its debuggees is participating + // in the GC identified by the given GC number. + bool debuggeeIsBeingCollected(uint64_t majorGCNumber) { + return observedGCs.put(majorGCNumber); + } + + static SavedFrame* getObjectAllocationSite(JSObject& obj); + + struct AllocationsLogEntry { + AllocationsLogEntry(HandleObject frame, mozilla::TimeStamp when, + const char* className, size_t size, bool inNursery) + : frame(frame), + when(when), + className(className), + size(size), + inNursery(inNursery) { + MOZ_ASSERT_IF(frame, UncheckedUnwrap(frame)->is<SavedFrame>() || + IsDeadProxyObject(frame)); + } + + HeapPtr<JSObject*> frame; + mozilla::TimeStamp when; + const char* className; + size_t size; + bool inNursery; + + void trace(JSTracer* trc) { + TraceNullableEdge(trc, &frame, "Debugger::AllocationsLogEntry::frame"); + } + }; + + private: + HeapPtr<NativeObject*> object; /* The Debugger object. Strong reference. */ + WeakGlobalObjectSet + debuggees; /* Debuggee globals. Cross-compartment weak references. */ + JS::ZoneSet debuggeeZones; /* Set of zones that we have debuggees in. */ + HeapPtr<JSObject*> uncaughtExceptionHook; /* Strong reference. */ + bool allowUnobservedAsmJS; + bool allowUnobservedWasm; + + // Whether to enable code coverage on the Debuggee. + bool collectCoverageInfo; + + template <typename T> + struct DebuggerLinkAccess { + static mozilla::DoublyLinkedListElement<T>& Get(T* aThis) { + return aThis->debuggerLink; + } + }; + + // List of all js::Breakpoints in this debugger. + using BreakpointList = + mozilla::DoublyLinkedList<js::Breakpoint, + DebuggerLinkAccess<js::Breakpoint>>; + BreakpointList breakpoints; + + // The set of GC numbers for which one or more of this Debugger's observed + // debuggees participated in. + using GCNumberSet = + HashSet<uint64_t, DefaultHasher<uint64_t>, ZoneAllocPolicy>; + GCNumberSet observedGCs; + + using AllocationsLog = js::TraceableFifo<AllocationsLogEntry>; + + AllocationsLog allocationsLog; + bool trackingAllocationSites; + double allocationSamplingProbability; + size_t maxAllocationsLogLength; + bool allocationsLogOverflowed; + + static const size_t DEFAULT_MAX_LOG_LENGTH = 5000; + + [[nodiscard]] bool appendAllocationSite(JSContext* cx, HandleObject obj, + Handle<SavedFrame*> frame, + mozilla::TimeStamp when); + + /* + * Recompute the set of debuggee zones based on the set of debuggee globals. + */ + void recomputeDebuggeeZoneSet(); + + /* + * Return true if there is an existing object metadata callback for the + * given global's compartment that will prevent our instrumentation of + * allocations. + */ + static bool cannotTrackAllocations(const GlobalObject& global); + + /* + * Add allocations tracking for objects allocated within the given + * debuggee's compartment. The given debuggee global must be observed by at + * least one Debugger that is tracking allocations. + */ + [[nodiscard]] static bool addAllocationsTracking( + JSContext* cx, Handle<GlobalObject*> debuggee); + + /* + * Remove allocations tracking for objects allocated within the given + * global's compartment. This is a no-op if there are still Debuggers + * observing this global and who are tracking allocations. + */ + static void removeAllocationsTracking(GlobalObject& global); + + /* + * Add or remove allocations tracking for all debuggees. + */ + [[nodiscard]] bool addAllocationsTrackingForAllDebuggees(JSContext* cx); + void removeAllocationsTrackingForAllDebuggees(); + + /* + * If this Debugger has a onNewGlobalObject handler, then + * this link is inserted into the list headed by + * JSRuntime::onNewGlobalObjectWatchers. + */ + mozilla::DoublyLinkedListElement<Debugger> onNewGlobalObjectWatchersLink; + + /* + * If this Debugger has a onGarbageCollection handler, then + * this link is inserted into the list headed by + * JSRuntime::onGarbageCollectionWatchers. + */ + mozilla::DoublyLinkedListElement<Debugger> onGarbageCollectionWatchersLink; + + /* + * Map from stack frames that are currently on the stack to Debugger.Frame + * instances. + * + * The keys are always live stack frames. We drop them from this map as + * soon as they leave the stack (see slowPathOnLeaveFrame) and in + * removeDebuggee. + * + * We don't trace the keys of this map (the frames are on the stack and + * thus necessarily live), but we do trace the values. It's like a WeakMap + * that way, but since stack frames are not gc-things, the implementation + * has to be different. + */ + typedef HashMap<AbstractFramePtr, HeapPtr<DebuggerFrame*>, + DefaultHasher<AbstractFramePtr>, ZoneAllocPolicy> + FrameMap; + FrameMap frames; + + /* + * Map from generator objects to their Debugger.Frame instances. + * + * When a Debugger.Frame is created for a generator frame, it is added to + * this map and remains there for the lifetime of the generator, whether + * that frame is on the stack at the moment or not. This is in addition to + * the entry in `frames` that exists as long as the generator frame is on + * the stack. + * + * We need to keep the Debugger.Frame object alive to deliver it to the + * onEnterFrame handler on resume, and to retain onStep and onPop hooks. + * + * An entry is present in this table when: + * - both the debuggee generator object and the Debugger.Frame object exists + * - the debuggee generator object belongs to a realm that is a debuggee of + * the Debugger.Frame's owner. + * + * regardless of whether the frame is currently suspended. (This list is + * meant to explain why we update the table in the particular places where + * we do so.) + * + * An entry in this table exists if and only if the Debugger.Frame's + * GENERATOR_INFO_SLOT is set. + */ + typedef DebuggerWeakMap<AbstractGeneratorObject, DebuggerFrame> + GeneratorWeakMap; + GeneratorWeakMap generatorFrames; + + // An ephemeral map from BaseScript* to Debugger.Script instances. + using ScriptWeakMap = DebuggerWeakMap<BaseScript, DebuggerScript>; + ScriptWeakMap scripts; + + using BaseScriptVector = JS::GCVector<BaseScript*>; + + // The map from debuggee source script objects to their Debugger.Source + // instances. + typedef DebuggerWeakMap<ScriptSourceObject, DebuggerSource, true> + SourceWeakMap; + SourceWeakMap sources; + + // The map from debuggee objects to their Debugger.Object instances. + typedef DebuggerWeakMap<JSObject, DebuggerObject> ObjectWeakMap; + ObjectWeakMap objects; + + // The map from debuggee Envs to Debugger.Environment instances. + typedef DebuggerWeakMap<JSObject, DebuggerEnvironment> EnvironmentWeakMap; + EnvironmentWeakMap environments; + + // The map from WasmInstanceObjects to synthesized Debugger.Script + // instances. + typedef DebuggerWeakMap<WasmInstanceObject, DebuggerScript> + WasmInstanceScriptWeakMap; + WasmInstanceScriptWeakMap wasmInstanceScripts; + + // The map from WasmInstanceObjects to synthesized Debugger.Source + // instances. + typedef DebuggerWeakMap<WasmInstanceObject, DebuggerSource> + WasmInstanceSourceWeakMap; + WasmInstanceSourceWeakMap wasmInstanceSources; + + class QueryBase; + class ScriptQuery; + class SourceQuery; + class ObjectQuery; + + enum class FromSweep { No, Yes }; + + [[nodiscard]] bool addDebuggeeGlobal(JSContext* cx, + Handle<GlobalObject*> obj); + void removeDebuggeeGlobal(JS::GCContext* gcx, GlobalObject* global, + WeakGlobalObjectSet::Enum* debugEnum, + FromSweep fromSweep); + + /* + * Handle the result of a hook that is expected to return a resumption + * value <https://wiki.mozilla.org/Debugger#Resumption_Values>. This is + * called when we return from a debugging hook to debuggee code. + * + * If `success` is false, the hook failed. If an exception is pending in + * ar.context(), attempt to handle it via the uncaught exception hook, + * otherwise report it to the AutoRealm's global. + * + * If `success` is true, there must be no exception pending in ar.context(). + * `rv` may be: + * + * undefined - Set `resultMode` to `ResumeMode::Continue` to continue + * execution normally. + * + * {return: value} or {throw: value} - Call unwrapDebuggeeValue to + * unwrap `value`. Store the result in `vp` and set `resultMode` to + * `ResumeMode::Return` or `ResumeMode::Throw`. The interpreter + * will force the current frame to return or throw an exception. + * + * null - Set `resultMode` to `ResumeMode::Terminate` to terminate the + * debuggee with an uncatchable error. + * + * anything else - Make a new TypeError the pending exception and + * attempt to handle it with the uncaught exception handler. + */ + [[nodiscard]] bool processHandlerResult( + JSContext* cx, bool success, HandleValue rv, AbstractFramePtr frame, + jsbytecode* pc, ResumeMode& resultMode, MutableHandleValue vp); + + [[nodiscard]] bool processParsedHandlerResult( + JSContext* cx, AbstractFramePtr frame, const jsbytecode* pc, bool success, + ResumeMode resumeMode, HandleValue value, ResumeMode& resultMode, + MutableHandleValue vp); + + /** + * Given a resumption return value from a hook, parse and validate it based + * on the given frame, and split the result into a ResumeMode and Value. + */ + [[nodiscard]] bool prepareResumption(JSContext* cx, AbstractFramePtr frame, + const jsbytecode* pc, + ResumeMode& resumeMode, + MutableHandleValue vp); + + /** + * If there is a pending exception and a handler, call the handler with the + * exception so that it can attempt to resolve the error. + */ + [[nodiscard]] bool callUncaughtExceptionHandler(JSContext* cx, + MutableHandleValue vp); + + /** + * If the context has a pending exception, report it to the current global. + */ + void reportUncaughtException(JSContext* cx); + + /* + * Call the uncaught exception handler if there is one, returning true + * if it handled the error, or false otherwise. + */ + [[nodiscard]] bool handleUncaughtException(JSContext* cx); + + GlobalObject* unwrapDebuggeeArgument(JSContext* cx, const Value& v); + + static void traceObject(JSTracer* trc, JSObject* obj); + + void trace(JSTracer* trc); + + void traceForMovingGC(JSTracer* trc); + void traceCrossCompartmentEdges(JSTracer* tracer); + + private: + template <typename F> + void forEachWeakMap(const F& f); + + [[nodiscard]] static bool getHookImpl(JSContext* cx, const CallArgs& args, + Debugger& dbg, Hook which); + [[nodiscard]] static bool setHookImpl(JSContext* cx, const CallArgs& args, + Debugger& dbg, Hook which); + + [[nodiscard]] static bool getGarbageCollectionHook(JSContext* cx, + const CallArgs& args, + Debugger& dbg); + [[nodiscard]] static bool setGarbageCollectionHook(JSContext* cx, + const CallArgs& args, + Debugger& dbg); + + static bool isCompilableUnit(JSContext* cx, unsigned argc, Value* vp); + static bool recordReplayProcessKind(JSContext* cx, unsigned argc, Value* vp); + static bool construct(JSContext* cx, unsigned argc, Value* vp); + + struct CallData; + + static const JSPropertySpec properties[]; + static const JSFunctionSpec methods[]; + static const JSFunctionSpec static_methods[]; + + /** + * Suspend the DebuggerFrame, clearing on-stack data but leaving it linked + * with the AbstractGeneratorObject so it can be re-used later. + */ + static void suspendGeneratorDebuggerFrames(JSContext* cx, + AbstractFramePtr frame); + + /** + * Terminate the DebuggerFrame, clearing all data associated with the frame + * so that it cannot be used to introspect stack frame data. + */ + static void terminateDebuggerFrames(JSContext* cx, AbstractFramePtr frame); + + /** + * Terminate a given DebuggerFrame, removing all internal state and all + * references to the frame from the Debugger itself. If the frame is being + * terminated while 'frames' or 'generatorFrames' are being iterated, pass a + * pointer to the iteration Enum to remove the entry and ensure that iteration + * behaves properly. + * + * The AbstractFramePtr may be omited in a call so long as it is either + * called again later with the correct 'frame', or the frame itself has never + * had on-stack data or a 'frames' entry and has never had an onStep handler. + */ + static void terminateDebuggerFrame( + JS::GCContext* gcx, Debugger* dbg, DebuggerFrame* dbgFrame, + AbstractFramePtr frame, FrameMap::Enum* maybeFramesEnum = nullptr, + GeneratorWeakMap::Enum* maybeGeneratorFramesEnum = nullptr); + + static bool updateExecutionObservabilityOfFrames( + JSContext* cx, const DebugAPI::ExecutionObservableSet& obs, + IsObserving observing); + static bool updateExecutionObservabilityOfScripts( + JSContext* cx, const DebugAPI::ExecutionObservableSet& obs, + IsObserving observing); + static bool updateExecutionObservability( + JSContext* cx, DebugAPI::ExecutionObservableSet& obs, + IsObserving observing); + + template <typename FrameFn /* void (Debugger*, DebuggerFrame*) */> + static void forEachOnStackDebuggerFrame(AbstractFramePtr frame, FrameFn fn); + template <typename FrameFn /* void (Debugger*, DebuggerFrame*) */> + static void forEachOnStackOrSuspendedDebuggerFrame(JSContext* cx, + AbstractFramePtr frame, + FrameFn fn); + + /* + * Return a vector containing all Debugger.Frame instances referring to + * |frame|. |global| is |frame|'s global object; if nullptr or omitted, we + * compute it ourselves from |frame|. + */ + using DebuggerFrameVector = GCVector<DebuggerFrame*, 0, SystemAllocPolicy>; + [[nodiscard]] static bool getDebuggerFrames( + AbstractFramePtr frame, MutableHandle<DebuggerFrameVector> frames); + + public: + // Public for DebuggerScript::setBreakpoint. + [[nodiscard]] static bool ensureExecutionObservabilityOfScript( + JSContext* cx, JSScript* script); + + // Whether the Debugger instance needs to observe all non-AOT JS + // execution of its debugees. + IsObserving observesAllExecution() const; + + // Whether the Debugger instance needs to observe AOT-compiled asm.js + // execution of its debuggees. + IsObserving observesAsmJS() const; + + // Whether the Debugger instance needs to observe compiled Wasm + // execution of its debuggees. + IsObserving observesWasm() const; + + // Whether the Debugger instance needs to observe coverage of any JavaScript + // execution. + IsObserving observesCoverage() const; + + // Whether the Debugger instance needs to observe native call invocations. + IsObserving observesNativeCalls() const; + + private: + [[nodiscard]] static bool ensureExecutionObservabilityOfFrame( + JSContext* cx, AbstractFramePtr frame); + [[nodiscard]] static bool ensureExecutionObservabilityOfRealm( + JSContext* cx, JS::Realm* realm); + + static bool hookObservesAllExecution(Hook which); + + [[nodiscard]] bool updateObservesAllExecutionOnDebuggees( + JSContext* cx, IsObserving observing); + [[nodiscard]] bool updateObservesCoverageOnDebuggees(JSContext* cx, + IsObserving observing); + void updateObservesAsmJSOnDebuggees(IsObserving observing); + void updateObservesWasmOnDebuggees(IsObserving observing); + + JSObject* getHook(Hook hook) const; + bool hasAnyLiveHooks() const; + inline bool isHookCallAllowed(JSContext* cx) const; + + static void slowPathPromiseHook(JSContext* cx, Hook hook, + Handle<PromiseObject*> promise); + + template <typename HookIsEnabledFun /* bool (Debugger*) */, + typename FireHookFun /* void (Debugger*) */> + static void dispatchQuietHook(JSContext* cx, HookIsEnabledFun hookIsEnabled, + FireHookFun fireHook); + template < + typename HookIsEnabledFun /* bool (Debugger*) */, typename FireHookFun /* bool (Debugger*, ResumeMode&, MutableHandleValue) */> + [[nodiscard]] static bool dispatchResumptionHook( + JSContext* cx, AbstractFramePtr frame, HookIsEnabledFun hookIsEnabled, + FireHookFun fireHook); + + template <typename RunImpl /* bool () */> + [[nodiscard]] bool enterDebuggerHook(JSContext* cx, RunImpl runImpl) { + if (!isHookCallAllowed(cx)) { + return true; + } + + AutoRealm ar(cx, object); + + if (!runImpl()) { + // We do not want errors within one hook to effect errors in other hooks, + // so the only errors that we allow to propagate out of a debugger hook + // are OOM errors and general terminations. + if (!cx->isExceptionPending() || cx->isThrowingOutOfMemory()) { + return false; + } + + reportUncaughtException(cx); + } + MOZ_ASSERT(!cx->isExceptionPending()); + return true; + } + + [[nodiscard]] bool fireDebuggerStatement(JSContext* cx, + ResumeMode& resumeMode, + MutableHandleValue vp); + [[nodiscard]] bool fireExceptionUnwind(JSContext* cx, HandleValue exc, + ResumeMode& resumeMode, + MutableHandleValue vp); + [[nodiscard]] bool fireEnterFrame(JSContext* cx, ResumeMode& resumeMode, + MutableHandleValue vp); + [[nodiscard]] bool fireNativeCall(JSContext* cx, const CallArgs& args, + CallReason reason, ResumeMode& resumeMode, + MutableHandleValue vp); + [[nodiscard]] bool fireNewGlobalObject(JSContext* cx, + Handle<GlobalObject*> global); + [[nodiscard]] bool firePromiseHook(JSContext* cx, Hook hook, + HandleObject promise); + + DebuggerScript* newVariantWrapper(JSContext* cx, + Handle<DebuggerScriptReferent> referent) { + return newDebuggerScript(cx, referent); + } + DebuggerSource* newVariantWrapper(JSContext* cx, + Handle<DebuggerSourceReferent> referent) { + return newDebuggerSource(cx, referent); + } + + /* + * Helper function to help wrap Debugger objects whose referents may be + * variants. Currently Debugger.Script and Debugger.Source referents may + * be variants. + * + * Prefer using wrapScript, wrapWasmScript, wrapSource, and wrapWasmSource + * whenever possible. + */ + template <typename ReferentType, typename Map> + typename Map::WrapperType* wrapVariantReferent( + JSContext* cx, Map& map, + Handle<typename Map::WrapperType::ReferentVariant> referent); + DebuggerScript* wrapVariantReferent(JSContext* cx, + Handle<DebuggerScriptReferent> referent); + DebuggerSource* wrapVariantReferent(JSContext* cx, + Handle<DebuggerSourceReferent> referent); + + /* + * Allocate and initialize a Debugger.Script instance whose referent is + * |referent|. + */ + DebuggerScript* newDebuggerScript(JSContext* cx, + Handle<DebuggerScriptReferent> referent); + + /* + * Allocate and initialize a Debugger.Source instance whose referent is + * |referent|. + */ + DebuggerSource* newDebuggerSource(JSContext* cx, + Handle<DebuggerSourceReferent> referent); + + /* + * Receive a "new script" event from the engine. A new script was compiled + * or deserialized. + */ + [[nodiscard]] bool fireNewScript( + JSContext* cx, Handle<DebuggerScriptReferent> scriptReferent); + + /* + * Receive a "garbage collection" event from the engine. A GC cycle with the + * given data was recently completed. + */ + [[nodiscard]] bool fireOnGarbageCollectionHook( + JSContext* cx, const JS::dbg::GarbageCollectionEvent::Ptr& gcData); + + inline Breakpoint* firstBreakpoint() const; + + [[nodiscard]] static bool replaceFrameGuts(JSContext* cx, + AbstractFramePtr from, + AbstractFramePtr to, + ScriptFrameIter& iter); + + public: + Debugger(JSContext* cx, NativeObject* dbg); + ~Debugger(); + + inline const js::HeapPtr<NativeObject*>& toJSObject() const; + inline js::HeapPtr<NativeObject*>& toJSObjectRef(); + static inline Debugger* fromJSObject(const JSObject* obj); + +#ifdef DEBUG + static bool isChildJSObject(JSObject* obj); +#endif + + Zone* zone() const { return toJSObject()->zone(); } + + bool hasMemory() const; + DebuggerMemory& memory() const; + + WeakGlobalObjectSet::Range allDebuggees() const { return debuggees.all(); } + +#ifdef DEBUG + static bool isDebuggerCrossCompartmentEdge(JSObject* obj, + const js::gc::Cell* cell); +#endif + + static bool hasLiveHook(GlobalObject* global, Hook which); + + /*** Functions for use by Debugger.cpp. *********************************/ + + inline bool observesEnterFrame() const; + inline bool observesNewScript() const; + inline bool observesNewGlobalObject() const; + inline bool observesGlobal(GlobalObject* global) const; + bool observesFrame(AbstractFramePtr frame) const; + bool observesFrame(const FrameIter& iter) const; + bool observesScript(JSScript* script) const; + bool observesWasm(wasm::Instance* instance) const; + + /* + * If env is nullptr, call vp->setNull() and return true. Otherwise, find + * or create a Debugger.Environment object for the given Env. On success, + * store the Environment object in *vp and return true. + */ + [[nodiscard]] bool wrapEnvironment(JSContext* cx, Handle<Env*> env, + MutableHandleValue vp); + [[nodiscard]] bool wrapEnvironment( + JSContext* cx, Handle<Env*> env, + MutableHandle<DebuggerEnvironment*> result); + + /* + * Like cx->compartment()->wrap(cx, vp), but for the debugger realm. + * + * Preconditions: *vp is a value from a debuggee realm; cx is in the + * debugger's compartment. + * + * If *vp is an object, this produces a (new or existing) Debugger.Object + * wrapper for it. Otherwise this is the same as Compartment::wrap. + * + * If *vp is a magic JS_OPTIMIZED_OUT value, this produces a plain object + * of the form { optimizedOut: true }. + * + * If *vp is a magic JS_MISSING_ARGUMENTS value signifying missing + * arguments, this produces a plain object of the form { missingArguments: + * true }. + * + * If *vp is a magic JS_UNINITIALIZED_LEXICAL value signifying an + * unaccessible uninitialized binding, this produces a plain object of the + * form { uninitialized: true }. + */ + [[nodiscard]] bool wrapDebuggeeValue(JSContext* cx, MutableHandleValue vp); + [[nodiscard]] bool wrapDebuggeeObject(JSContext* cx, HandleObject obj, + MutableHandle<DebuggerObject*> result); + [[nodiscard]] bool wrapNullableDebuggeeObject( + JSContext* cx, HandleObject obj, MutableHandle<DebuggerObject*> result); + + /* + * Unwrap a Debug.Object, without rewrapping it for any particular debuggee + * compartment. + * + * Preconditions: cx is in the debugger compartment. *vp is a value in that + * compartment. (*vp should be a "debuggee value", meaning it is the + * debugger's reflection of a value in the debuggee.) + * + * If *vp is a Debugger.Object, store the referent in *vp. Otherwise, if *vp + * is an object, throw a TypeError, because it is not a debuggee + * value. Otherwise *vp is a primitive, so leave it alone. + * + * When passing values from the debuggee to the debugger: + * enter debugger compartment; + * call wrapDebuggeeValue; // compartment- and debugger-wrapping + * + * When passing values from the debugger to the debuggee: + * call unwrapDebuggeeValue; // debugger-unwrapping + * enter debuggee realm; + * call cx->compartment()->wrap; // compartment-rewrapping + * + * (Extreme nerd sidebar: Unwrapping happens in two steps because there are + * two different kinds of symmetry at work: regardless of which direction + * we're going, we want any exceptions to be created and thrown in the + * debugger compartment--mirror symmetry. But compartment wrapping always + * happens in the target compartment--rotational symmetry.) + */ + [[nodiscard]] bool unwrapDebuggeeValue(JSContext* cx, MutableHandleValue vp); + [[nodiscard]] bool unwrapDebuggeeObject(JSContext* cx, + MutableHandleObject obj); + [[nodiscard]] bool unwrapPropertyDescriptor( + JSContext* cx, HandleObject obj, MutableHandle<PropertyDescriptor> desc); + + /* + * Store the Debugger.Frame object for iter in *vp/result. + * + * If this Debugger does not already have a Frame object for the frame + * `iter` points to, a new Frame object is created, and `iter`'s private + * data is copied into it. + */ + [[nodiscard]] bool getFrame(JSContext* cx, const FrameIter& iter, + MutableHandleValue vp); + [[nodiscard]] bool getFrame(JSContext* cx, + MutableHandle<DebuggerFrame*> result); + [[nodiscard]] bool getFrame(JSContext* cx, const FrameIter& iter, + MutableHandle<DebuggerFrame*> result); + [[nodiscard]] bool getFrame(JSContext* cx, + Handle<AbstractGeneratorObject*> genObj, + MutableHandle<DebuggerFrame*> result); + + /* + * Return the Debugger.Script object for |script|, or create a new one if + * needed. The context |cx| must be in the debugger realm; |script| must be + * a script in a debuggee realm. + */ + DebuggerScript* wrapScript(JSContext* cx, Handle<BaseScript*> script); + + /* + * Return the Debugger.Script object for |wasmInstance| (the toplevel + * script), synthesizing a new one if needed. The context |cx| must be in + * the debugger compartment; |wasmInstance| must be a WasmInstanceObject in + * the debuggee realm. + */ + DebuggerScript* wrapWasmScript(JSContext* cx, + Handle<WasmInstanceObject*> wasmInstance); + + /* + * Return the Debugger.Source object for |source|, or create a new one if + * needed. The context |cx| must be in the debugger compartment; |source| + * must be a script source object in a debuggee realm. + */ + DebuggerSource* wrapSource(JSContext* cx, + js::Handle<ScriptSourceObject*> source); + + /* + * Return the Debugger.Source object for |wasmInstance| (the entire module), + * synthesizing a new one if needed. The context |cx| must be in the + * debugger compartment; |wasmInstance| must be a WasmInstanceObject in the + * debuggee realm. + */ + DebuggerSource* wrapWasmSource(JSContext* cx, + Handle<WasmInstanceObject*> wasmInstance); + + DebuggerDebuggeeLink* getDebuggeeLink(); + + private: + Debugger(const Debugger&) = delete; + Debugger& operator=(const Debugger&) = delete; +}; + +// Specialize InternalBarrierMethods so we can have WeakHeapPtr<Debugger*>. +template <> +struct InternalBarrierMethods<Debugger*> { + static bool isMarkable(Debugger* dbg) { return dbg->toJSObject(); } + + static void postBarrier(Debugger** vp, Debugger* prev, Debugger* next) {} + + static void readBarrier(Debugger* dbg) { + InternalBarrierMethods<JSObject*>::readBarrier(dbg->toJSObject()); + } + +#ifdef DEBUG + static void assertThingIsNotGray(Debugger* dbg) {} +#endif +}; + +/** + * This class exists for one specific reason. If a given Debugger object is in + * a state where: + * + * a) nothing in the system has a reference to the object + * b) the debugger is currently attached to a live debuggee + * c) the debugger has hooks like 'onEnterFrame' + * + * then we don't want the GC to delete the Debugger, because the system could + * still call the hooks. This means we need to ensure that, whenever the global + * gets marked, the Debugger will get marked as well. Critically, we _only_ + * want that to happen if the debugger has hooks. If it doesn't, then GCing + * the debugger is the right think to do. + * + * Note that there are _other_ cases where the debugger may be held live, but + * those are not addressed by this case. + * + * To accomplish this, we use a bit of roundabout link approach. Both the + * Debugger and the debuggees can reach the link object: + * + * Debugger -> DebuggerDebuggeeLink <- CCW <- Debuggee Global #1 + * | | ^ ^---<- CCW <- Debuggee Global #2 + * \--<<-optional-<<--/ \------<- CCW <- Debuggee Global #3 + * + * and critically, the Debugger is able to conditionally add or remove the link + * going from the DebuggerDebuggeeLink _back_ to the Debugger. When this link + * exists, the GC can trace all the way from the global to the Debugger, + * meaning that any Debugger with this link will be kept alive as long as any + * of its debuggees are alive. + */ +class DebuggerDebuggeeLink : public NativeObject { + private: + enum { + DEBUGGER_LINK_SLOT, + RESERVED_SLOTS, + }; + + public: + static const JSClass class_; + + void setLinkSlot(Debugger& dbg); + void clearLinkSlot(); +}; + +/* + * A Handler represents a Debugger API reflection object's handler function, + * like a Debugger.Frame's onStep handler. These handler functions are called by + * the Debugger API to notify the user of certain events. For each event type, + * we define a separate subclass of Handler. + * + * When a reflection object accepts a Handler, it calls its 'hold' method; and + * if the Handler is replaced by another, or the reflection object is finalized, + * the reflection object calls the Handler's 'drop' method. The reflection + * object does not otherwise manage the Handler's lifetime, say, by calling its + * destructor or freeing its memory. A simple Handler implementation might have + * an empty 'hold' method, and have its 'drop' method delete the Handler. A more + * complex Handler might process many kinds of events, and thus inherit from + * many Handler subclasses and be held by many reflection objects + * simultaneously; a handler like this could use 'hold' and 'drop' to manage a + * reference count. + * + * To support SpiderMonkey's memory use tracking, 'hold' and 'drop' also require + * a pointer to the owning reflection object, so that the Holder implementation + * can properly report changes in ownership to functions using the + * js::gc::MemoryUse categories. + */ +struct Handler { + virtual ~Handler() = default; + + /* + * If this Handler is a reference to a callable JSObject, return that + * JSObject. Otherwise, this method returns nullptr. + * + * The JavaScript getters for handler properties on reflection objects use + * this method to obtain the callable the handler represents. When a Handler's + * 'object' method returns nullptr, that handler is simply not visible to + * JavaScript. + */ + virtual JSObject* object() const = 0; + + /* Report that this Handler is now held by owner. See comment above. */ + virtual void hold(JSObject* owner) = 0; + + /* Report that this Handler is no longer held by owner. See comment above. */ + virtual void drop(JS::GCContext* gcx, JSObject* owner) = 0; + + /* + * Trace the reference to the handler. This method will be called by the + * reflection object holding this Handler whenever the former is traced. + */ + virtual void trace(JSTracer* tracer) = 0; + + /* Allocation size in bytes for memory accounting purposes. */ + virtual size_t allocSize() const = 0; +}; + +class JSBreakpointSite; +class WasmBreakpointSite; + +/** + * Breakpoint GC rules: + * + * BreakpointSites and Breakpoints are owned by the code in which they are set. + * Tracing a JSScript or WasmInstance traces all BreakpointSites set in it, + * which traces all Breakpoints; and if the code is garbage collected, the + * BreakpointSite and the Breakpoints set at it are freed as well. Doing so is + * not observable to JS, since the handlers would never fire, and there is no + * way to enumerate all breakpoints without specifying a specific script, in + * which case it must not have been GC'd. + * + * Although BreakpointSites and Breakpoints are not GC things, they should be + * treated as belonging to the code's compartment. This means that the + * BreakpointSite concrete subclasses' pointers to the code are not + * cross-compartment references, but a Breakpoint's pointers to its handler and + * owning Debugger are cross-compartment references, and go through + * cross-compartment wrappers. + */ + +/** + * A location in a JSScript or WasmInstance at which we have breakpoints. A + * BreakpointSite owns a linked list of all the breakpoints set at its location. + * In general, this list contains breakpoints set by multiple Debuggers in + * various compartments. + * + * BreakpointSites are created only as needed, for locations at which + * breakpoints are currently set. When the last breakpoint is removed from a + * location, the BreakpointSite is removed as well. + * + * This is an abstract base class, with subclasses specialized for the different + * sorts of code a breakpoint might be set in. JSBreakpointSite manages sites in + * JSScripts, and WasmBreakpointSite manages sites in WasmInstances. + */ +class BreakpointSite { + friend class DebugAPI; + friend class Breakpoint; + friend class Debugger; + + private: + template <typename T> + struct SiteLinkAccess { + static mozilla::DoublyLinkedListElement<T>& Get(T* aThis) { + return aThis->siteLink; + } + }; + + // List of all js::Breakpoints at this instruction. + using BreakpointList = + mozilla::DoublyLinkedList<js::Breakpoint, SiteLinkAccess<js::Breakpoint>>; + BreakpointList breakpoints; + + protected: + BreakpointSite() = default; + virtual ~BreakpointSite() = default; + void finalize(JS::GCContext* gcx); + virtual gc::Cell* owningCell() = 0; + + public: + Breakpoint* firstBreakpoint() const; + bool hasBreakpoint(Breakpoint* bp); + + bool isEmpty() const; + virtual void trace(JSTracer* trc); + virtual void remove(JS::GCContext* gcx) = 0; + void destroyIfEmpty(JS::GCContext* gcx) { + if (isEmpty()) { + remove(gcx); + } + } + virtual Realm* realm() const = 0; +}; + +/* + * A breakpoint set at a given BreakpointSite, indicating the owning debugger + * and the handler object. A Breakpoint is a member of two linked lists: its + * owning debugger's list and its site's list. + */ +class Breakpoint { + friend class DebugAPI; + friend class Debugger; + friend class BreakpointSite; + + public: + /* Our owning debugger. */ + Debugger* const debugger; + + /** + * A cross-compartment wrapper for our owning debugger's object, a CCW in the + * code's compartment to the Debugger object in its own compartment. Holding + * this lets the GC know about the effective cross-compartment reference from + * the code to the debugger; see "Breakpoint GC Rules", above. + * + * This is almost redundant with the `debugger` field, except that we need + * access to our owning `Debugger` regardless of the relative privilege levels + * of debugger and debuggee, regardless of whether we're in the midst of a GC, + * and so on - unwrapping is just too entangled. + */ + const HeapPtr<JSObject*> wrappedDebugger; + + /* The site at which we're inserted. */ + BreakpointSite* const site; + + private: + /** + * The breakpoint handler object, via a cross-compartment wrapper in the + * code's compartment. + * + * Although eventually we would like this to be a `js::Handler` instance, for + * now it is just cross-compartment wrapper for the JS object supplied to + * `setBreakpoint`, hopefully with a callable `hit` property. + */ + const HeapPtr<JSObject*> handler; + + /** + * Link elements for each list this breakpoint can be in. + */ + mozilla::DoublyLinkedListElement<Breakpoint> debuggerLink; + mozilla::DoublyLinkedListElement<Breakpoint> siteLink; + + void trace(JSTracer* trc); + + public: + Breakpoint(Debugger* debugger, HandleObject wrappedDebugger, + BreakpointSite* site, HandleObject handler); + + enum MayDestroySite { False, True }; + + /** + * Unlink this breakpoint from its Debugger's and and BreakpointSite's lists, + * and free its memory. + * + * This is the low-level primitive shared by breakpoint removal and script + * finalization code. It is only concerned with cleaning up this Breakpoint; + * it does not check for now-empty BreakpointSites, unneeded DebugScripts, or + * the like. + */ + void delete_(JS::GCContext* gcx); + + /** + * Remove this breakpoint. Unlink it from its Debugger's and BreakpointSite's + * lists, and if the BreakpointSite is now empty, clean that up and update JIT + * code as necessary. + */ + void remove(JS::GCContext* gcx); + + Breakpoint* nextInDebugger(); + Breakpoint* nextInSite(); + JSObject* getHandler() const { return handler; } +}; + +class JSBreakpointSite : public BreakpointSite { + public: + const HeapPtr<JSScript*> script; + jsbytecode* const pc; + + public: + JSBreakpointSite(JSScript* script, jsbytecode* pc); + + void trace(JSTracer* trc) override; + void delete_(JS::GCContext* gcx); + void remove(JS::GCContext* gcx) override; + Realm* realm() const override; + + private: + gc::Cell* owningCell() override; +}; + +class WasmBreakpointSite : public BreakpointSite { + public: + const HeapPtr<WasmInstanceObject*> instanceObject; + uint32_t offset; + + public: + WasmBreakpointSite(WasmInstanceObject* instanceObject, uint32_t offset); + + void trace(JSTracer* trc) override; + void delete_(JS::GCContext* gcx); + void remove(JS::GCContext* gcx) override; + Realm* realm() const override; + + private: + gc::Cell* owningCell() override; +}; + +Breakpoint* Debugger::firstBreakpoint() const { + if (breakpoints.isEmpty()) { + return nullptr; + } + return &(*breakpoints.begin()); +} + +const js::HeapPtr<NativeObject*>& Debugger::toJSObject() const { + MOZ_ASSERT(object); + return object; +} + +js::HeapPtr<NativeObject*>& Debugger::toJSObjectRef() { + MOZ_ASSERT(object); + return object; +} + +bool Debugger::observesEnterFrame() const { return getHook(OnEnterFrame); } + +bool Debugger::observesNewScript() const { return getHook(OnNewScript); } + +bool Debugger::observesNewGlobalObject() const { + return getHook(OnNewGlobalObject); +} + +bool Debugger::observesGlobal(GlobalObject* global) const { + WeakHeapPtr<GlobalObject*> debuggee(global); + return debuggees.has(debuggee); +} + +[[nodiscard]] bool ReportObjectRequired(JSContext* cx); + +JSObject* IdVectorToArray(JSContext* cx, HandleIdVector ids); +bool IsInterpretedNonSelfHostedFunction(JSFunction* fun); +JSScript* GetOrCreateFunctionScript(JSContext* cx, HandleFunction fun); +ArrayObject* GetFunctionParameterNamesArray(JSContext* cx, HandleFunction fun); +bool ValueToIdentifier(JSContext* cx, HandleValue v, MutableHandleId id); +bool ValueToStableChars(JSContext* cx, const char* fnname, HandleValue value, + JS::AutoStableStringChars& stableChars); +bool ParseEvalOptions(JSContext* cx, HandleValue value, EvalOptions& options); + +Result<Completion> DebuggerGenericEval( + JSContext* cx, const mozilla::Range<const char16_t> chars, + HandleObject bindings, const EvalOptions& options, Debugger* dbg, + HandleObject envArg, FrameIter* iter); + +bool ParseResumptionValue(JSContext* cx, HandleValue rval, + ResumeMode& resumeMode, MutableHandleValue vp); + +#define JS_DEBUG_PSG(Name, Getter) \ + JS_PSG(Name, CallData::ToNative<&CallData::Getter>, 0) + +#define JS_DEBUG_PSGS(Name, Getter, Setter) \ + JS_PSGS(Name, CallData::ToNative<&CallData::Getter>, \ + CallData::ToNative<&CallData::Setter>, 0) + +#define JS_DEBUG_FN(Name, Method, NumArgs) \ + JS_FN(Name, CallData::ToNative<&CallData::Method>, NumArgs, 0) + +} /* namespace js */ + +#endif /* debugger_Debugger_h */ diff --git a/js/src/debugger/DebuggerMemory.cpp b/js/src/debugger/DebuggerMemory.cpp new file mode 100644 index 0000000000..61fbb7053d --- /dev/null +++ b/js/src/debugger/DebuggerMemory.cpp @@ -0,0 +1,440 @@ +/* -*- 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/DebuggerMemory.h" + +#include "mozilla/Maybe.h" +#include "mozilla/Vector.h" + +#include <stdlib.h> +#include <utility> + +#include "jsapi.h" + +#include "builtin/MapObject.h" +#include "debugger/Debugger.h" +#include "gc/Marking.h" +#include "js/AllocPolicy.h" +#include "js/Debug.h" +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/PropertySpec.h" +#include "js/TracingAPI.h" +#include "js/UbiNode.h" +#include "js/UbiNodeCensus.h" +#include "js/Utility.h" +#include "vm/GlobalObject.h" +#include "vm/JSContext.h" +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/Realm.h" +#include "vm/SavedStacks.h" + +#include "debugger/Debugger-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +using mozilla::Maybe; +using mozilla::Nothing; + +/* static */ +DebuggerMemory* DebuggerMemory::create(JSContext* cx, Debugger* dbg) { + Value memoryProtoValue = + dbg->object->getReservedSlot(Debugger::JSSLOT_DEBUG_MEMORY_PROTO); + RootedObject memoryProto(cx, &memoryProtoValue.toObject()); + Rooted<DebuggerMemory*> memory( + cx, NewObjectWithGivenProto<DebuggerMemory>(cx, memoryProto)); + if (!memory) { + return nullptr; + } + + dbg->object->setReservedSlot(Debugger::JSSLOT_DEBUG_MEMORY_INSTANCE, + ObjectValue(*memory)); + memory->setReservedSlot(JSSLOT_DEBUGGER, ObjectValue(*dbg->object)); + + return memory; +} + +Debugger* DebuggerMemory::getDebugger() { + const Value& dbgVal = getReservedSlot(JSSLOT_DEBUGGER); + return Debugger::fromJSObject(&dbgVal.toObject()); +} + +/* static */ +bool DebuggerMemory::construct(JSContext* cx, unsigned argc, Value* vp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, + "Debugger.Source"); + return false; +} + +/* static */ const JSClass DebuggerMemory::class_ = { + "Memory", JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_COUNT)}; + +/* static */ +DebuggerMemory* DebuggerMemory::checkThis(JSContext* cx, CallArgs& args) { + const Value& thisValue = args.thisv(); + + if (!thisValue.isObject()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_OBJECT_REQUIRED, + InformalValueTypeName(thisValue)); + return nullptr; + } + + JSObject& thisObject = thisValue.toObject(); + if (!thisObject.is<DebuggerMemory>()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INCOMPATIBLE_PROTO, class_.name, "method", + thisObject.getClass()->name); + return nullptr; + } + + return &thisObject.as<DebuggerMemory>(); +} + +struct MOZ_STACK_CLASS DebuggerMemory::CallData { + JSContext* cx; + const CallArgs& args; + + Handle<DebuggerMemory*> memory; + + CallData(JSContext* cx, const CallArgs& args, Handle<DebuggerMemory*> memory) + : cx(cx), args(args), memory(memory) {} + + // Accessor properties of Debugger.Memory.prototype. + + bool setTrackingAllocationSites(); + bool getTrackingAllocationSites(); + bool setMaxAllocationsLogLength(); + bool getMaxAllocationsLogLength(); + bool setAllocationSamplingProbability(); + bool getAllocationSamplingProbability(); + bool getAllocationsLogOverflowed(); + bool getOnGarbageCollection(); + bool setOnGarbageCollection(); + + // Function properties of Debugger.Memory.prototype. + + bool takeCensus(); + bool drainAllocationsLog(); + + using Method = bool (CallData::*)(); + + template <Method MyMethod> + static bool ToNative(JSContext* cx, unsigned argc, Value* vp); +}; + +template <DebuggerMemory::CallData::Method MyMethod> +/* static */ +bool DebuggerMemory::CallData::ToNative(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<DebuggerMemory*> memory(cx, DebuggerMemory::checkThis(cx, args)); + if (!memory) { + return false; + } + + CallData data(cx, args, memory); + return (data.*MyMethod)(); +} + +static bool undefined(const CallArgs& args) { + args.rval().setUndefined(); + return true; +} + +bool DebuggerMemory::CallData::setTrackingAllocationSites() { + if (!args.requireAtLeast(cx, "(set trackingAllocationSites)", 1)) { + return false; + } + + Debugger* dbg = memory->getDebugger(); + bool enabling = ToBoolean(args[0]); + + if (enabling == dbg->trackingAllocationSites) { + return undefined(args); + } + + dbg->trackingAllocationSites = enabling; + + if (enabling) { + if (!dbg->addAllocationsTrackingForAllDebuggees(cx)) { + dbg->trackingAllocationSites = false; + return false; + } + } else { + dbg->removeAllocationsTrackingForAllDebuggees(); + } + + return undefined(args); +} + +bool DebuggerMemory::CallData::getTrackingAllocationSites() { + args.rval().setBoolean(memory->getDebugger()->trackingAllocationSites); + return true; +} + +bool DebuggerMemory::CallData::drainAllocationsLog() { + Debugger* dbg = memory->getDebugger(); + + if (!dbg->trackingAllocationSites) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_TRACKING_ALLOCATIONS, + "drainAllocationsLog"); + return false; + } + + size_t length = dbg->allocationsLog.length(); + + Rooted<ArrayObject*> result(cx, NewDenseFullyAllocatedArray(cx, length)); + if (!result) { + return false; + } + result->ensureDenseInitializedLength(0, length); + + for (size_t i = 0; i < length; i++) { + Rooted<PlainObject*> obj(cx, NewPlainObject(cx)); + if (!obj) { + return false; + } + + // Don't pop the AllocationsLogEntry yet. The queue's links are followed + // by the GC to find the AllocationsLogEntry, but are not barriered, so + // we must edit them with great care. Use the queue entry in place, and + // then pop and delete together. + Debugger::AllocationsLogEntry& entry = dbg->allocationsLog.front(); + + RootedValue frame(cx, ObjectOrNullValue(entry.frame)); + if (!DefineDataProperty(cx, obj, cx->names().frame, frame)) { + return false; + } + + double when = + (entry.when - mozilla::TimeStamp::ProcessCreation()).ToMilliseconds(); + RootedValue timestampValue(cx, NumberValue(when)); + if (!DefineDataProperty(cx, obj, cx->names().timestamp, timestampValue)) { + return false; + } + + RootedString className( + cx, Atomize(cx, entry.className, strlen(entry.className))); + if (!className) { + return false; + } + RootedValue classNameValue(cx, StringValue(className)); + if (!DefineDataProperty(cx, obj, cx->names().class_, classNameValue)) { + return false; + } + + RootedValue size(cx, NumberValue(entry.size)); + if (!DefineDataProperty(cx, obj, cx->names().size, size)) { + return false; + } + + RootedValue inNursery(cx, BooleanValue(entry.inNursery)); + if (!DefineDataProperty(cx, obj, cx->names().inNursery, inNursery)) { + return false; + } + + result->setDenseElement(i, ObjectValue(*obj)); + + // Pop the front queue entry, and delete it immediately, so that the GC + // sees the AllocationsLogEntry's HeapPtr barriers run atomically with + // the change to the graph (the queue link). + dbg->allocationsLog.popFront(); + } + + dbg->allocationsLogOverflowed = false; + args.rval().setObject(*result); + return true; +} + +bool DebuggerMemory::CallData::getMaxAllocationsLogLength() { + args.rval().setInt32(memory->getDebugger()->maxAllocationsLogLength); + return true; +} + +bool DebuggerMemory::CallData::setMaxAllocationsLogLength() { + if (!args.requireAtLeast(cx, "(set maxAllocationsLogLength)", 1)) { + return false; + } + + int32_t max; + if (!ToInt32(cx, args[0], &max)) { + return false; + } + + if (max < 1) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "(set maxAllocationsLogLength)'s parameter", "not a positive integer"); + return false; + } + + Debugger* dbg = memory->getDebugger(); + dbg->maxAllocationsLogLength = max; + + while (dbg->allocationsLog.length() > dbg->maxAllocationsLogLength) { + dbg->allocationsLog.popFront(); + } + + args.rval().setUndefined(); + return true; +} + +bool DebuggerMemory::CallData::getAllocationSamplingProbability() { + args.rval().setDouble(memory->getDebugger()->allocationSamplingProbability); + return true; +} + +bool DebuggerMemory::CallData::setAllocationSamplingProbability() { + if (!args.requireAtLeast(cx, "(set allocationSamplingProbability)", 1)) { + return false; + } + + double probability; + if (!ToNumber(cx, args[0], &probability)) { + return false; + } + + // Careful! This must also reject NaN. + if (!(0.0 <= probability && probability <= 1.0)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_UNEXPECTED_TYPE, + "(set allocationSamplingProbability)'s parameter", + "not a number between 0 and 1"); + return false; + } + + Debugger* dbg = memory->getDebugger(); + if (dbg->allocationSamplingProbability != probability) { + dbg->allocationSamplingProbability = probability; + + // If this is a change any debuggees would observe, have all debuggee + // realms recompute their sampling probabilities. + if (dbg->trackingAllocationSites) { + for (auto r = dbg->debuggees.all(); !r.empty(); r.popFront()) { + r.front()->realm()->chooseAllocationSamplingProbability(); + } + } + } + + args.rval().setUndefined(); + return true; +} + +bool DebuggerMemory::CallData::getAllocationsLogOverflowed() { + args.rval().setBoolean(memory->getDebugger()->allocationsLogOverflowed); + return true; +} + +bool DebuggerMemory::CallData::getOnGarbageCollection() { + return Debugger::getGarbageCollectionHook(cx, args, *memory->getDebugger()); +} + +bool DebuggerMemory::CallData::setOnGarbageCollection() { + return Debugger::setGarbageCollectionHook(cx, args, *memory->getDebugger()); +} + +/* Debugger.Memory.prototype.takeCensus */ + +JS_PUBLIC_API void JS::dbg::SetDebuggerMallocSizeOf( + JSContext* cx, mozilla::MallocSizeOf mallocSizeOf) { + cx->runtime()->debuggerMallocSizeOf = mallocSizeOf; +} + +JS_PUBLIC_API mozilla::MallocSizeOf JS::dbg::GetDebuggerMallocSizeOf( + JSContext* cx) { + return cx->runtime()->debuggerMallocSizeOf; +} + +using JS::ubi::Census; +using JS::ubi::CountBasePtr; +using JS::ubi::CountTypePtr; + +// The takeCensus function works in three phases: +// +// 1) We examine the 'breakdown' property of our 'options' argument, and +// use that to build a CountType tree. +// +// 2) We create a count node for the root of our CountType tree, and then walk +// the heap, counting each node we find, expanding our tree of counts as we +// go. +// +// 3) We walk the tree of counts and produce JavaScript objects reporting the +// accumulated results. +bool DebuggerMemory::CallData::takeCensus() { + Census census(cx); + CountTypePtr rootType; + + RootedObject options(cx); + if (args.get(0).isObject()) { + options = &args[0].toObject(); + } + + if (!JS::ubi::ParseCensusOptions(cx, census, options, rootType)) { + return false; + } + + JS::ubi::RootedCount rootCount(cx, rootType->makeCount()); + if (!rootCount) { + ReportOutOfMemory(cx); + return false; + } + JS::ubi::CensusHandler handler(census, rootCount, + cx->runtime()->debuggerMallocSizeOf); + + Debugger* dbg = memory->getDebugger(); + RootedObject dbgObj(cx, dbg->object); + + // Populate our target set of debuggee zones. + for (WeakGlobalObjectSet::Range r = dbg->allDebuggees(); !r.empty(); + r.popFront()) { + if (!census.targetZones.put(r.front()->zone())) { + ReportOutOfMemory(cx); + return false; + } + } + + { + JS::ubi::RootList rootList(cx); + auto [ok, nogc] = rootList.init(dbgObj); + if (!ok) { + ReportOutOfMemory(cx); + return false; + } + + JS::ubi::CensusTraversal traversal(cx, handler, nogc); + traversal.wantNames = false; + + if (!traversal.addStart(JS::ubi::Node(&rootList)) || + !traversal.traverse()) { + ReportOutOfMemory(cx); + return false; + } + } + + return handler.report(cx, args.rval()); +} + +/* Debugger.Memory property and method tables. */ + +/* static */ const JSPropertySpec DebuggerMemory::properties[] = { + JS_DEBUG_PSGS("trackingAllocationSites", getTrackingAllocationSites, + setTrackingAllocationSites), + JS_DEBUG_PSGS("maxAllocationsLogLength", getMaxAllocationsLogLength, + setMaxAllocationsLogLength), + JS_DEBUG_PSGS("allocationSamplingProbability", + getAllocationSamplingProbability, + setAllocationSamplingProbability), + JS_DEBUG_PSG("allocationsLogOverflowed", getAllocationsLogOverflowed), + JS_DEBUG_PSGS("onGarbageCollection", getOnGarbageCollection, + setOnGarbageCollection), + JS_PS_END}; + +/* static */ const JSFunctionSpec DebuggerMemory::methods[] = { + JS_DEBUG_FN("drainAllocationsLog", drainAllocationsLog, 0), + JS_DEBUG_FN("takeCensus", takeCensus, 0), JS_FS_END}; diff --git a/js/src/debugger/DebuggerMemory.h b/js/src/debugger/DebuggerMemory.h new file mode 100644 index 0000000000..a8b3a19cdd --- /dev/null +++ b/js/src/debugger/DebuggerMemory.h @@ -0,0 +1,39 @@ +/* -*- 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/. */ + +#ifndef debugger_DebuggerMemory_h +#define debugger_DebuggerMemory_h + +#include "js/Class.h" +#include "js/Value.h" +#include "vm/JSContext.h" +#include "vm/JSObject.h" + +namespace js { + +class DebuggerMemory : public NativeObject { + friend class Debugger; + + static DebuggerMemory* checkThis(JSContext* cx, CallArgs& args); + + Debugger* getDebugger(); + + public: + static DebuggerMemory* create(JSContext* cx, Debugger* dbg); + + enum { JSSLOT_DEBUGGER, JSSLOT_COUNT }; + + static bool construct(JSContext* cx, unsigned argc, Value* vp); + static const JSClass class_; + static const JSPropertySpec properties[]; + static const JSFunctionSpec methods[]; + + struct CallData; +}; + +} /* namespace js */ + +#endif /* debugger_DebuggerMemory_h */ diff --git a/js/src/debugger/Environment-inl.h b/js/src/debugger/Environment-inl.h new file mode 100644 index 0000000000..721897ccf9 --- /dev/null +++ b/js/src/debugger/Environment-inl.h @@ -0,0 +1,25 @@ +/* -*- 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/. */ + +#ifndef debugger_Environment_inl_h +#define debugger_Environment_inl_h + +#include "debugger/Environment.h" // for DebuggerEnvironment + +#include "jstypes.h" // for JS_PUBLIC_API +#include "NamespaceImports.h" // for Value +#include "debugger/Debugger.h" // for Debugger + +#include "debugger/Debugger-inl.h" // for Debugger::fromJSObject + +class JS_PUBLIC_API JSObject; + +inline js::Debugger* js::DebuggerEnvironment::owner() const { + JSObject* dbgobj = &getReservedSlot(OWNER_SLOT).toObject(); + return Debugger::fromJSObject(dbgobj); +} + +#endif /* debugger_Environment_inl_h */ diff --git a/js/src/debugger/Environment.cpp b/js/src/debugger/Environment.cpp new file mode 100644 index 0000000000..dbb6851e3a --- /dev/null +++ b/js/src/debugger/Environment.cpp @@ -0,0 +1,664 @@ +/* -*- 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/Environment-inl.h" + +#include "mozilla/Assertions.h" // for AssertionConditionType +#include "mozilla/Maybe.h" // for Maybe, Some, Nothing +#include "mozilla/Vector.h" // for Vector + +#include <string.h> // for strlen, size_t +#include <utility> // for move + +#include "debugger/Debugger.h" // for Env, Debugger, ValueToIdentifier +#include "debugger/Object.h" // for DebuggerObject +#include "debugger/Script.h" // for DebuggerScript +#include "frontend/BytecodeCompiler.h" // for IsIdentifier +#include "gc/Tracer.h" // for TraceManuallyBarrieredCrossCompartmentEdge +#include "js/CallArgs.h" // for CallArgs +#include "js/friend/ErrorMessages.h" // for GetErrorMessage, JSMSG_* +#include "js/HeapAPI.h" // for IsInsideNursery +#include "js/RootingAPI.h" // for Rooted, MutableHandle +#include "vm/Compartment.h" // for Compartment +#include "vm/JSAtom.h" // for Atomize +#include "vm/JSContext.h" // for JSContext +#include "vm/JSFunction.h" // for JSFunction +#include "vm/JSObject.h" // for JSObject, RequireObject, +#include "vm/NativeObject.h" // for NativeObject, JSObject::is +#include "vm/Realm.h" // for AutoRealm, ErrorCopier +#include "vm/Scope.h" // for ScopeKind, ScopeKindString +#include "vm/StringType.h" // for JSAtom + +#include "vm/Compartment-inl.h" // for Compartment::wrap +#include "vm/EnvironmentObject-inl.h" // for JSObject::enclosingEnvironment +#include "vm/JSObject-inl.h" // for IsInternalFunctionObject, NewObjectWithGivenProtoAndKind +#include "vm/ObjectOperations-inl.h" // for HasProperty, GetProperty +#include "vm/Realm-inl.h" // for AutoRealm::AutoRealm + +namespace js { +class GlobalObject; +} + +using namespace js; + +using js::frontend::IsIdentifier; +using mozilla::Maybe; +using mozilla::Nothing; +using mozilla::Some; + +const JSClassOps DebuggerEnvironment::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + nullptr, // finalize + nullptr, // call + nullptr, // construct + CallTraceMethod<DebuggerEnvironment>, // trace +}; + +const JSClass DebuggerEnvironment::class_ = { + "Environment", + JSCLASS_HAS_RESERVED_SLOTS(DebuggerEnvironment::RESERVED_SLOTS), + &classOps_}; + +void DebuggerEnvironment::trace(JSTracer* trc) { + // There is a barrier on private pointers, so the Unbarriered marking + // is okay. + if (Env* referent = maybeReferent()) { + TraceManuallyBarrieredCrossCompartmentEdge(trc, this, &referent, + "Debugger.Environment referent"); + if (referent != maybeReferent()) { + setReservedSlotGCThingAsPrivateUnbarriered(ENV_SLOT, referent); + } + } +} + +static DebuggerEnvironment* DebuggerEnvironment_checkThis( + JSContext* cx, const CallArgs& args) { + JSObject* thisobj = RequireObject(cx, args.thisv()); + if (!thisobj) { + return nullptr; + } + if (!thisobj->is<DebuggerEnvironment>()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INCOMPATIBLE_PROTO, "Debugger.Environment", + "method", thisobj->getClass()->name); + return nullptr; + } + + return &thisobj->as<DebuggerEnvironment>(); +} + +struct MOZ_STACK_CLASS DebuggerEnvironment::CallData { + JSContext* cx; + const CallArgs& args; + + Handle<DebuggerEnvironment*> environment; + + CallData(JSContext* cx, const CallArgs& args, + Handle<DebuggerEnvironment*> env) + : cx(cx), args(args), environment(env) {} + + bool typeGetter(); + bool scopeKindGetter(); + bool parentGetter(); + bool objectGetter(); + bool calleeScriptGetter(); + bool inspectableGetter(); + bool optimizedOutGetter(); + + bool namesMethod(); + bool findMethod(); + bool getVariableMethod(); + bool setVariableMethod(); + + using Method = bool (CallData::*)(); + + template <Method MyMethod> + static bool ToNative(JSContext* cx, unsigned argc, Value* vp); +}; + +template <DebuggerEnvironment::CallData::Method MyMethod> +/* static */ +bool DebuggerEnvironment::CallData::ToNative(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<DebuggerEnvironment*> environment( + cx, DebuggerEnvironment_checkThis(cx, args)); + if (!environment) { + return false; + } + + CallData data(cx, args, environment); + return (data.*MyMethod)(); +} + +/* static */ +bool DebuggerEnvironment::construct(JSContext* cx, unsigned argc, Value* vp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, + "Debugger.Environment"); + return false; +} + +static bool IsDeclarative(Env* env) { + return env->is<DebugEnvironmentProxy>() && + env->as<DebugEnvironmentProxy>().isForDeclarative(); +} + +template <typename T> +static bool IsDebugEnvironmentWrapper(Env* env) { + return env->is<DebugEnvironmentProxy>() && + env->as<DebugEnvironmentProxy>().environment().is<T>(); +} + +bool DebuggerEnvironment::CallData::typeGetter() { + if (!environment->requireDebuggee(cx)) { + return false; + } + + DebuggerEnvironmentType type = environment->type(); + + const char* s; + switch (type) { + case DebuggerEnvironmentType::Declarative: + s = "declarative"; + break; + case DebuggerEnvironmentType::With: + s = "with"; + break; + case DebuggerEnvironmentType::Object: + s = "object"; + break; + } + + JSAtom* str = Atomize(cx, s, strlen(s)); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +bool DebuggerEnvironment::CallData::scopeKindGetter() { + if (!environment->requireDebuggee(cx)) { + return false; + } + + Maybe<ScopeKind> kind = environment->scopeKind(); + if (kind.isSome()) { + const char* s = ScopeKindString(*kind); + JSAtom* str = Atomize(cx, s, strlen(s)); + if (!str) { + return false; + } + args.rval().setString(str); + } else { + args.rval().setNull(); + } + + return true; +} + +bool DebuggerEnvironment::CallData::parentGetter() { + if (!environment->requireDebuggee(cx)) { + return false; + } + + Rooted<DebuggerEnvironment*> result(cx); + if (!environment->getParent(cx, &result)) { + return false; + } + + args.rval().setObjectOrNull(result); + return true; +} + +bool DebuggerEnvironment::CallData::objectGetter() { + if (!environment->requireDebuggee(cx)) { + return false; + } + + if (environment->type() == DebuggerEnvironmentType::Declarative) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_NO_ENV_OBJECT); + return false; + } + + Rooted<DebuggerObject*> result(cx); + if (!environment->getObject(cx, &result)) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +bool DebuggerEnvironment::CallData::calleeScriptGetter() { + if (!environment->requireDebuggee(cx)) { + return false; + } + + Rooted<DebuggerScript*> result(cx); + if (!environment->getCalleeScript(cx, &result)) { + return false; + } + + args.rval().setObjectOrNull(result); + return true; +} + +bool DebuggerEnvironment::CallData::inspectableGetter() { + args.rval().setBoolean(environment->isDebuggee()); + return true; +} + +bool DebuggerEnvironment::CallData::optimizedOutGetter() { + args.rval().setBoolean(environment->isOptimized()); + return true; +} + +bool DebuggerEnvironment::CallData::namesMethod() { + if (!environment->requireDebuggee(cx)) { + return false; + } + + RootedIdVector ids(cx); + if (!DebuggerEnvironment::getNames(cx, environment, &ids)) { + return false; + } + + JSObject* obj = IdVectorToArray(cx, ids); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +bool DebuggerEnvironment::CallData::findMethod() { + if (!args.requireAtLeast(cx, "Debugger.Environment.find", 1)) { + return false; + } + + if (!environment->requireDebuggee(cx)) { + return false; + } + + RootedId id(cx); + if (!ValueToIdentifier(cx, args[0], &id)) { + return false; + } + + Rooted<DebuggerEnvironment*> result(cx); + if (!DebuggerEnvironment::find(cx, environment, id, &result)) { + return false; + } + + args.rval().setObjectOrNull(result); + return true; +} + +bool DebuggerEnvironment::CallData::getVariableMethod() { + if (!args.requireAtLeast(cx, "Debugger.Environment.getVariable", 1)) { + return false; + } + + if (!environment->requireDebuggee(cx)) { + return false; + } + + RootedId id(cx); + if (!ValueToIdentifier(cx, args[0], &id)) { + return false; + } + + return DebuggerEnvironment::getVariable(cx, environment, id, args.rval()); +} + +bool DebuggerEnvironment::CallData::setVariableMethod() { + if (!args.requireAtLeast(cx, "Debugger.Environment.setVariable", 2)) { + return false; + } + + if (!environment->requireDebuggee(cx)) { + return false; + } + + RootedId id(cx); + if (!ValueToIdentifier(cx, args[0], &id)) { + return false; + } + + if (!DebuggerEnvironment::setVariable(cx, environment, id, args[1])) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +bool DebuggerEnvironment::requireDebuggee(JSContext* cx) const { + if (!isDebuggee()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_NOT_DEBUGGEE, "Debugger.Environment", + "environment"); + + return false; + } + + return true; +} + +const JSPropertySpec DebuggerEnvironment::properties_[] = { + JS_DEBUG_PSG("type", typeGetter), + JS_DEBUG_PSG("scopeKind", scopeKindGetter), + JS_DEBUG_PSG("parent", parentGetter), + JS_DEBUG_PSG("object", objectGetter), + JS_DEBUG_PSG("calleeScript", calleeScriptGetter), + JS_DEBUG_PSG("inspectable", inspectableGetter), + JS_DEBUG_PSG("optimizedOut", optimizedOutGetter), + JS_PS_END}; + +const JSFunctionSpec DebuggerEnvironment::methods_[] = { + JS_DEBUG_FN("names", namesMethod, 0), JS_DEBUG_FN("find", findMethod, 1), + JS_DEBUG_FN("getVariable", getVariableMethod, 1), + JS_DEBUG_FN("setVariable", setVariableMethod, 2), JS_FS_END}; + +/* static */ +NativeObject* DebuggerEnvironment::initClass(JSContext* cx, + Handle<GlobalObject*> global, + HandleObject dbgCtor) { + return InitClass(cx, dbgCtor, nullptr, nullptr, "Environment", construct, 0, + properties_, methods_, nullptr, nullptr); +} + +/* static */ +DebuggerEnvironment* DebuggerEnvironment::create( + JSContext* cx, HandleObject proto, HandleObject referent, + Handle<NativeObject*> debugger) { + DebuggerEnvironment* obj = + IsInsideNursery(referent) + ? NewObjectWithGivenProto<DebuggerEnvironment>(cx, proto) + : NewTenuredObjectWithGivenProto<DebuggerEnvironment>(cx, proto); + if (!obj) { + return nullptr; + } + + obj->setReservedSlotGCThingAsPrivate(ENV_SLOT, referent); + obj->setReservedSlot(OWNER_SLOT, ObjectValue(*debugger)); + + return obj; +} + +/* static */ +DebuggerEnvironmentType DebuggerEnvironment::type() const { + // Don't bother switching compartments just to check env's type. + if (IsDeclarative(referent())) { + return DebuggerEnvironmentType::Declarative; + } + if (IsDebugEnvironmentWrapper<WithEnvironmentObject>(referent())) { + return DebuggerEnvironmentType::With; + } + return DebuggerEnvironmentType::Object; +} + +mozilla::Maybe<ScopeKind> DebuggerEnvironment::scopeKind() const { + if (!referent()->is<DebugEnvironmentProxy>()) { + return Nothing(); + } + EnvironmentObject& env = + referent()->as<DebugEnvironmentProxy>().environment(); + Scope* scope = GetEnvironmentScope(env); + return scope ? Some(scope->kind()) : Nothing(); +} + +bool DebuggerEnvironment::getParent( + JSContext* cx, MutableHandle<DebuggerEnvironment*> result) const { + // Don't bother switching compartments just to get env's parent. + Rooted<Env*> parent(cx, referent()->enclosingEnvironment()); + if (!parent) { + result.set(nullptr); + return true; + } + + return owner()->wrapEnvironment(cx, parent, result); +} + +bool DebuggerEnvironment::getObject( + JSContext* cx, MutableHandle<DebuggerObject*> result) const { + MOZ_ASSERT(type() != DebuggerEnvironmentType::Declarative); + + // Don't bother switching compartments just to get env's object. + RootedObject object(cx); + if (IsDebugEnvironmentWrapper<WithEnvironmentObject>(referent())) { + object.set(&referent() + ->as<DebugEnvironmentProxy>() + .environment() + .as<WithEnvironmentObject>() + .object()); + } else if (IsDebugEnvironmentWrapper<NonSyntacticVariablesObject>( + referent())) { + object.set(&referent() + ->as<DebugEnvironmentProxy>() + .environment() + .as<NonSyntacticVariablesObject>()); + } else { + object.set(referent()); + MOZ_ASSERT(!object->is<DebugEnvironmentProxy>()); + } + + return owner()->wrapDebuggeeObject(cx, object, result); +} + +bool DebuggerEnvironment::getCalleeScript( + JSContext* cx, MutableHandle<DebuggerScript*> result) const { + if (!referent()->is<DebugEnvironmentProxy>()) { + result.set(nullptr); + return true; + } + + JSObject& scope = referent()->as<DebugEnvironmentProxy>().environment(); + if (!scope.is<CallObject>()) { + result.set(nullptr); + return true; + } + + Rooted<BaseScript*> script(cx, scope.as<CallObject>().callee().baseScript()); + + DebuggerScript* scriptObject = owner()->wrapScript(cx, script); + if (!scriptObject) { + return false; + } + + result.set(scriptObject); + return true; +} + +bool DebuggerEnvironment::isDebuggee() const { + MOZ_ASSERT(referent()); + MOZ_ASSERT(!referent()->is<EnvironmentObject>()); + + return owner()->observesGlobal(&referent()->nonCCWGlobal()); +} + +bool DebuggerEnvironment::isOptimized() const { + return referent()->is<DebugEnvironmentProxy>() && + referent()->as<DebugEnvironmentProxy>().isOptimizedOut(); +} + +/* static */ +bool DebuggerEnvironment::getNames(JSContext* cx, + Handle<DebuggerEnvironment*> environment, + MutableHandleIdVector result) { + MOZ_ASSERT(environment->isDebuggee()); + MOZ_ASSERT(result.empty()); + + Rooted<Env*> referent(cx, environment->referent()); + { + Maybe<AutoRealm> ar; + ar.emplace(cx, referent); + + ErrorCopier ec(ar); + if (!GetPropertyKeys(cx, referent, JSITER_HIDDEN, result)) { + return false; + } + } + + result.eraseIf([](PropertyKey key) { + return !key.isAtom() || !IsIdentifier(key.toAtom()); + }); + + for (size_t i = 0; i < result.length(); ++i) { + cx->markAtom(result[i].toAtom()); + } + + return true; +} + +/* static */ +bool DebuggerEnvironment::find(JSContext* cx, + Handle<DebuggerEnvironment*> environment, + HandleId id, + MutableHandle<DebuggerEnvironment*> result) { + MOZ_ASSERT(environment->isDebuggee()); + + Rooted<Env*> env(cx, environment->referent()); + Debugger* dbg = environment->owner(); + + { + Maybe<AutoRealm> ar; + ar.emplace(cx, env); + + cx->markId(id); + + // This can trigger resolve hooks. + ErrorCopier ec(ar); + for (; env; env = env->enclosingEnvironment()) { + bool found; + if (!HasProperty(cx, env, id, &found)) { + return false; + } + if (found) { + break; + } + } + } + + if (!env) { + result.set(nullptr); + return true; + } + + return dbg->wrapEnvironment(cx, env, result); +} + +/* static */ +bool DebuggerEnvironment::getVariable(JSContext* cx, + Handle<DebuggerEnvironment*> environment, + HandleId id, MutableHandleValue result) { + MOZ_ASSERT(environment->isDebuggee()); + + Rooted<Env*> referent(cx, environment->referent()); + Debugger* dbg = environment->owner(); + + { + Maybe<AutoRealm> ar; + ar.emplace(cx, referent); + + cx->markId(id); + + // This can trigger getters. + ErrorCopier ec(ar); + + bool found; + if (!HasProperty(cx, referent, id, &found)) { + return false; + } + if (!found) { + result.setUndefined(); + return true; + } + + // For DebugEnvironmentProxys, we get sentinel values for optimized out + // slots and arguments instead of throwing (the default behavior). + // + // See wrapDebuggeeValue for how the sentinel values are wrapped. + if (referent->is<DebugEnvironmentProxy>()) { + Rooted<DebugEnvironmentProxy*> env( + cx, &referent->as<DebugEnvironmentProxy>()); + if (!DebugEnvironmentProxy::getMaybeSentinelValue(cx, env, id, result)) { + return false; + } + } else { + if (!GetProperty(cx, referent, referent, id, result)) { + return false; + } + } + } + + // When we've faked up scope chain objects for optimized-out scopes, + // declarative environments may contain internal JSFunction objects, which + // we shouldn't expose to the user. + if (result.isObject()) { + RootedObject obj(cx, &result.toObject()); + if (obj->is<JSFunction>() && + IsInternalFunctionObject(obj->as<JSFunction>())) + result.setMagic(JS_OPTIMIZED_OUT); + } + + return dbg->wrapDebuggeeValue(cx, result); +} + +/* static */ +bool DebuggerEnvironment::setVariable(JSContext* cx, + Handle<DebuggerEnvironment*> environment, + HandleId id, HandleValue value_) { + MOZ_ASSERT(environment->isDebuggee()); + + Rooted<Env*> referent(cx, environment->referent()); + Debugger* dbg = environment->owner(); + + RootedValue value(cx, value_); + if (!dbg->unwrapDebuggeeValue(cx, &value)) { + return false; + } + + { + Maybe<AutoRealm> ar; + ar.emplace(cx, referent); + if (!cx->compartment()->wrap(cx, &value)) { + return false; + } + cx->markId(id); + + // This can trigger setters. + ErrorCopier ec(ar); + + // Make sure the environment actually has the specified binding. + bool found; + if (!HasProperty(cx, referent, id, &found)) { + return false; + } + if (!found) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_VARIABLE_NOT_FOUND); + return false; + } + + // Just set the property. + if (!SetProperty(cx, referent, id, value)) { + return false; + } + } + + return true; +} diff --git a/js/src/debugger/Environment.h b/js/src/debugger/Environment.h new file mode 100644 index 0000000000..a4186e07a8 --- /dev/null +++ b/js/src/debugger/Environment.h @@ -0,0 +1,97 @@ +/* -*- 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/. */ + +#ifndef debugger_Environment_h +#define debugger_Environment_h + +#include "mozilla/Assertions.h" // for AssertionConditionType, MOZ_ASSERT +#include "mozilla/Maybe.h" // for Maybe + +#include "jstypes.h" // for JS_PUBLIC_API +#include "NamespaceImports.h" // for Value, HandleId, HandleObject +#include "debugger/Debugger.h" // for Env +#include "js/PropertySpec.h" // for JSFunctionSpec, JSPropertySpec +#include "js/RootingAPI.h" // for Handle, MutableHandle +#include "vm/NativeObject.h" // for NativeObject +#include "vm/Scope.h" // for ScopeKind + +class JS_PUBLIC_API JSObject; +struct JS_PUBLIC_API JSContext; +class JSTracer; + +namespace js { + +class GlobalObject; + +enum class DebuggerEnvironmentType { Declarative, With, Object }; + +class DebuggerEnvironment : public NativeObject { + public: + enum { ENV_SLOT, OWNER_SLOT, RESERVED_SLOTS }; + + static const JSClass class_; + + static NativeObject* initClass(JSContext* cx, Handle<GlobalObject*> global, + HandleObject dbgCtor); + static DebuggerEnvironment* create(JSContext* cx, HandleObject proto, + HandleObject referent, + Handle<NativeObject*> debugger); + + void trace(JSTracer* trc); + + DebuggerEnvironmentType type() const; + mozilla::Maybe<ScopeKind> scopeKind() const; + [[nodiscard]] bool getParent( + JSContext* cx, MutableHandle<DebuggerEnvironment*> result) const; + [[nodiscard]] bool getObject(JSContext* cx, + MutableHandle<DebuggerObject*> result) const; + [[nodiscard]] bool getCalleeScript( + JSContext* cx, MutableHandle<DebuggerScript*> result) const; + bool isDebuggee() const; + bool isOptimized() const; + + [[nodiscard]] static bool getNames(JSContext* cx, + Handle<DebuggerEnvironment*> environment, + MutableHandleIdVector result); + [[nodiscard]] static bool find(JSContext* cx, + Handle<DebuggerEnvironment*> environment, + HandleId id, + MutableHandle<DebuggerEnvironment*> result); + [[nodiscard]] static bool getVariable( + JSContext* cx, Handle<DebuggerEnvironment*> environment, HandleId id, + MutableHandleValue result); + [[nodiscard]] static bool setVariable( + JSContext* cx, Handle<DebuggerEnvironment*> environment, HandleId id, + HandleValue value); + + Debugger* owner() const; + + Env* maybeReferent() const { return maybePtrFromReservedSlot<Env>(ENV_SLOT); } + + Env* referent() const { + Env* env = maybeReferent(); + MOZ_ASSERT(env); + return env; + } + + void clearReferent() { clearReservedSlotGCThingAsPrivate(ENV_SLOT); } + + private: + static const JSClassOps classOps_; + + static const JSPropertySpec properties_[]; + static const JSFunctionSpec methods_[]; + + bool requireDebuggee(JSContext* cx) const; + + [[nodiscard]] static bool construct(JSContext* cx, unsigned argc, Value* vp); + + struct CallData; +}; + +} /* namespace js */ + +#endif /* debugger_Environment_h */ diff --git a/js/src/debugger/Frame-inl.h b/js/src/debugger/Frame-inl.h new file mode 100644 index 0000000000..6cb595d44c --- /dev/null +++ b/js/src/debugger/Frame-inl.h @@ -0,0 +1,27 @@ +/* -*- 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/. */ + +#ifndef debugger_Frame_inl_h +#define debugger_Frame_inl_h + +#include "debugger/Frame.h" // for DebuggerFrame + +#include "mozilla/Assertions.h" // for AssertionConditionType, MOZ_ASSERT + +#include "NamespaceImports.h" // for Value + +inline bool js::DebuggerFrame::hasGeneratorInfo() const { + return !getReservedSlot(GENERATOR_INFO_SLOT).isUndefined(); +} + +inline js::DebuggerFrame::GeneratorInfo* js::DebuggerFrame::generatorInfo() + const { + MOZ_ASSERT(hasGeneratorInfo()); + return static_cast<GeneratorInfo*>( + getReservedSlot(GENERATOR_INFO_SLOT).toPrivate()); +} + +#endif /* debugger_Frame_inl_h */ diff --git a/js/src/debugger/Frame.cpp b/js/src/debugger/Frame.cpp new file mode 100644 index 0000000000..1ae529be78 --- /dev/null +++ b/js/src/debugger/Frame.cpp @@ -0,0 +1,1950 @@ +/* -*- 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/Frame-inl.h" + +#include "mozilla/Assertions.h" // for AssertionConditionType +#include "mozilla/HashTable.h" // for HashMapEntry +#include "mozilla/Maybe.h" // for Maybe +#include "mozilla/Range.h" // for Range +#include "mozilla/RangedPtr.h" // for RangedPtr +#include "mozilla/Result.h" // for Result +#include "mozilla/ScopeExit.h" // for MakeScopeExit, ScopeExit +#include "mozilla/ThreadLocal.h" // for ThreadLocal +#include "mozilla/Vector.h" // for Vector + +#include <stddef.h> // for size_t +#include <stdint.h> // for int32_t +#include <string.h> // for strlen +#include <utility> // for std::move + +#include "jsnum.h" // for Int32ToString + +#include "builtin/Array.h" // for NewDenseCopiedArray +#include "debugger/Debugger.h" // for Completion, Debugger +#include "debugger/DebugScript.h" +#include "debugger/Environment.h" // for DebuggerEnvironment +#include "debugger/NoExecute.h" // for LeaveDebuggeeNoExecute +#include "debugger/Object.h" // for DebuggerObject +#include "debugger/Script.h" // for DebuggerScript +#include "frontend/BytecodeCompilation.h" // for CompileEvalScript +#include "frontend/FrontendContext.h" // for AutoReportFrontendContext +#include "gc/Barrier.h" // for HeapPtr +#include "gc/GC.h" // for MemoryUse +#include "gc/GCContext.h" // for JS::GCContext +#include "gc/Marking.h" // for IsAboutToBeFinalized +#include "gc/Tracer.h" // for TraceCrossCompartmentEdge +#include "gc/ZoneAllocator.h" // for AddCellMemory +#include "jit/JSJitFrameIter.h" // for InlineFrameIterator +#include "jit/RematerializedFrame.h" // for RematerializedFrame +#include "js/CallArgs.h" // for CallArgs +#include "js/friend/ErrorMessages.h" // for GetErrorMessage, JSMSG_* +#include "js/Object.h" // for SetReservedSlot +#include "js/Proxy.h" // for PrivateValue +#include "js/RootingAPI.h" // for Handle +#include "js/SourceText.h" // for SourceText, SourceOwnership +#include "js/StableStringChars.h" // for AutoStableStringChars +#include "vm/ArgumentsObject.h" // for ArgumentsObject +#include "vm/ArrayObject.h" // for ArrayObject +#include "vm/AsyncFunction.h" // for AsyncFunctionGeneratorObject +#include "vm/AsyncIteration.h" // for AsyncGeneratorObject +#include "vm/BytecodeUtil.h" // for JSDVG_SEARCH_STACK +#include "vm/Compartment.h" // for Compartment +#include "vm/EnvironmentObject.h" // for IsGlobalLexicalEnvironment +#include "vm/GeneratorObject.h" // for AbstractGeneratorObject +#include "vm/GlobalObject.h" // for GlobalObject +#include "vm/Interpreter.h" // for Call, ExecuteKernel +#include "vm/JSAtom.h" // for Atomize +#include "vm/JSContext.h" // for JSContext, ReportValueError +#include "vm/JSFunction.h" // for JSFunction, NewNativeFunction +#include "vm/JSObject.h" // for JSObject, RequireObject +#include "vm/JSScript.h" // for JSScript +#include "vm/NativeObject.h" // for NativeDefineDataProperty +#include "vm/Realm.h" // for AutoRealm +#include "vm/Runtime.h" // for JSAtomState +#include "vm/Scope.h" // for PositionalFormalParameterIter +#include "vm/Stack.h" // for AbstractFramePtr, FrameIter +#include "vm/StringType.h" // for PropertyName, JSString +#include "wasm/WasmDebug.h" // for DebugState +#include "wasm/WasmDebugFrame.h" // for DebugFrame +#include "wasm/WasmInstance.h" // for Instance +#include "wasm/WasmJS.h" // for WasmInstanceObject + +#include "debugger/Debugger-inl.h" // for Debugger::fromJSObject +#include "gc/WeakMap-inl.h" // for WeakMap::remove +#include "vm/Compartment-inl.h" // for Compartment::wrap +#include "vm/JSContext-inl.h" // for JSContext::check +#include "vm/JSObject-inl.h" // for NewObjectWithGivenProto +#include "vm/NativeObject-inl.h" // for NativeObject::global +#include "vm/ObjectOperations-inl.h" // for GetProperty +#include "vm/Realm-inl.h" // for AutoRealm::AutoRealm +#include "vm/Stack-inl.h" // for AbstractFramePtr::script + +namespace js { +namespace jit { +class JitFrameLayout; +} /* namespace jit */ +} /* namespace js */ + +using namespace js; + +using JS::AutoStableStringChars; +using JS::CompileOptions; +using JS::SourceOwnership; +using JS::SourceText; +using mozilla::MakeScopeExit; +using mozilla::Maybe; + +ScriptedOnStepHandler::ScriptedOnStepHandler(JSObject* object) + : object_(object) { + MOZ_ASSERT(object_->isCallable()); +} + +JSObject* ScriptedOnStepHandler::object() const { return object_; } + +void ScriptedOnStepHandler::hold(JSObject* owner) { + AddCellMemory(owner, allocSize(), MemoryUse::DebuggerOnStepHandler); +} + +void ScriptedOnStepHandler::drop(JS::GCContext* gcx, JSObject* owner) { + gcx->delete_(owner, this, allocSize(), MemoryUse::DebuggerOnStepHandler); +} + +void ScriptedOnStepHandler::trace(JSTracer* tracer) { + TraceEdge(tracer, &object_, "OnStepHandlerFunction.object"); +} + +bool ScriptedOnStepHandler::onStep(JSContext* cx, Handle<DebuggerFrame*> frame, + ResumeMode& resumeMode, + MutableHandleValue vp) { + RootedValue fval(cx, ObjectValue(*object_)); + RootedValue rval(cx); + if (!js::Call(cx, fval, frame, &rval)) { + return false; + } + + return ParseResumptionValue(cx, rval, resumeMode, vp); +}; + +size_t ScriptedOnStepHandler::allocSize() const { return sizeof(*this); } + +ScriptedOnPopHandler::ScriptedOnPopHandler(JSObject* object) : object_(object) { + MOZ_ASSERT(object->isCallable()); +} + +JSObject* ScriptedOnPopHandler::object() const { return object_; } + +void ScriptedOnPopHandler::hold(JSObject* owner) { + AddCellMemory(owner, allocSize(), MemoryUse::DebuggerOnPopHandler); +} + +void ScriptedOnPopHandler::drop(JS::GCContext* gcx, JSObject* owner) { + gcx->delete_(owner, this, allocSize(), MemoryUse::DebuggerOnPopHandler); +} + +void ScriptedOnPopHandler::trace(JSTracer* tracer) { + TraceEdge(tracer, &object_, "OnStepHandlerFunction.object"); +} + +bool ScriptedOnPopHandler::onPop(JSContext* cx, Handle<DebuggerFrame*> frame, + const Completion& completion, + ResumeMode& resumeMode, + MutableHandleValue vp) { + Debugger* dbg = frame->owner(); + + RootedValue completionValue(cx); + if (!completion.buildCompletionValue(cx, dbg, &completionValue)) { + return false; + } + + RootedValue fval(cx, ObjectValue(*object_)); + RootedValue rval(cx); + if (!js::Call(cx, fval, frame, completionValue, &rval)) { + return false; + } + + return ParseResumptionValue(cx, rval, resumeMode, vp); +}; + +size_t ScriptedOnPopHandler::allocSize() const { return sizeof(*this); } + +js::Debugger* js::DebuggerFrame::owner() const { + JSObject* dbgobj = &getReservedSlot(OWNER_SLOT).toObject(); + return Debugger::fromJSObject(dbgobj); +} + +const JSClassOps DebuggerFrame::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + finalize, // finalize + nullptr, // call + nullptr, // construct + CallTraceMethod<DebuggerFrame>, // trace +}; + +const JSClass DebuggerFrame::class_ = { + "Frame", + JSCLASS_HAS_RESERVED_SLOTS(RESERVED_SLOTS) | + // We require foreground finalization so we can destruct GeneratorInfo's + // HeapPtrs. + JSCLASS_FOREGROUND_FINALIZE, + &DebuggerFrame::classOps_}; + +enum { JSSLOT_DEBUGARGUMENTS_FRAME, JSSLOT_DEBUGARGUMENTS_COUNT }; + +const JSClass DebuggerArguments::class_ = { + "Arguments", JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUGARGUMENTS_COUNT)}; + +bool DebuggerFrame::resume(const FrameIter& iter) { + FrameIter::Data* data = iter.copyData(); + if (!data) { + return false; + } + setFrameIterData(data); + return true; +} + +bool DebuggerFrame::hasAnyHooks() const { + return !getReservedSlot(ONSTEP_HANDLER_SLOT).isUndefined() || + !getReservedSlot(ONPOP_HANDLER_SLOT).isUndefined(); +} + +/* static */ +NativeObject* DebuggerFrame::initClass(JSContext* cx, + Handle<GlobalObject*> global, + HandleObject dbgCtor) { + return InitClass(cx, dbgCtor, nullptr, nullptr, "Frame", construct, 0, + properties_, methods_, nullptr, nullptr); +} + +/* static */ +DebuggerFrame* DebuggerFrame::create( + JSContext* cx, HandleObject proto, Handle<NativeObject*> debugger, + const FrameIter* maybeIter, + Handle<AbstractGeneratorObject*> maybeGenerator) { + Rooted<DebuggerFrame*> frame( + cx, NewObjectWithGivenProto<DebuggerFrame>(cx, proto)); + if (!frame) { + return nullptr; + } + + frame->setReservedSlot(OWNER_SLOT, ObjectValue(*debugger)); + + if (maybeIter) { + FrameIter::Data* data = maybeIter->copyData(); + if (!data) { + return nullptr; + } + + frame->setFrameIterData(data); + } + + if (maybeGenerator) { + if (!DebuggerFrame::setGeneratorInfo(cx, frame, maybeGenerator)) { + frame->freeFrameIterData(cx->gcContext()); + return nullptr; + } + } + + return frame; +} + +/** + * Information held by a DebuggerFrame about a generator/async call. A + * Debugger.Frame's GENERATOR_INFO_SLOT, if set, holds a PrivateValue pointing + * to one of these. + * + * This is created and attached as soon as a generator object is created for a + * debuggee generator/async frame, retained across suspensions and resumptions, + * and cleared when the generator call ends permanently. + * + * It may seem like this information might belong in ordinary reserved slots on + * the DebuggerFrame object. But that isn't possible: + * + * 1) Slots cannot contain cross-compartment references directly. + * 2) Ordinary cross-compartment wrappers aren't good enough, because the + * debugger must create its own magic entries in the wrapper table for the GC + * to get zone collection groups right. + * 3) Even if we make debugger wrapper table entries by hand, hiding + * cross-compartment edges as PrivateValues doesn't call post-barriers, and + * the generational GC won't update our pointer when the generator object + * gets tenured. + * + * Yes, officer, I definitely knew all this in advance and designed it this way + * the first time. + * + * Note that it is not necessary to have a second cross-compartment wrapper + * table entry to cover the pointer to the generator's script. The wrapper table + * entries play two roles: they help the GC put a debugger zone in the same zone + * group as its debuggee, and they serve as roots when collecting the debuggee + * zone, but not the debugger zone. Since an AbstractGeneratorObject holds a + * strong reference to its callee's script (via the callee), and the AGO and the + * script are always in the same compartment, it suffices to add a + * cross-compartment wrapper table entry for the Debugger.Frame -> AGO edge. + */ +class DebuggerFrame::GeneratorInfo { + // An unwrapped cross-compartment reference to the generator object. + // + // Always an object. + // + // This cannot be GCPtr because we are not always destructed during sweeping; + // a Debugger.Frame's generator is also cleared when the generator returns + // permanently. + const HeapPtr<Value> unwrappedGenerator_; + + // A cross-compartment reference to the generator's script. + const HeapPtr<JSScript*> generatorScript_; + + public: + GeneratorInfo(Handle<AbstractGeneratorObject*> unwrappedGenerator, + HandleScript generatorScript) + : unwrappedGenerator_(ObjectValue(*unwrappedGenerator)), + generatorScript_(generatorScript) {} + + // Trace a rooted instance of this class, e.g. a Rooted<GeneratorInfo>. + void trace(JSTracer* tracer) { + TraceRoot(tracer, &unwrappedGenerator_, "Debugger.Frame generator object"); + TraceRoot(tracer, &generatorScript_, "Debugger.Frame generator script"); + } + // Trace a GeneratorInfo from a DebuggerFrame object. + void trace(JSTracer* tracer, DebuggerFrame& frameObj) { + TraceCrossCompartmentEdge(tracer, &frameObj, &unwrappedGenerator_, + "Debugger.Frame generator object"); + TraceCrossCompartmentEdge(tracer, &frameObj, &generatorScript_, + "Debugger.Frame generator script"); + } + + AbstractGeneratorObject& unwrappedGenerator() const { + return unwrappedGenerator_.toObject().as<AbstractGeneratorObject>(); + } + + JSScript* generatorScript() { return generatorScript_; } + + bool isGeneratorScriptAboutToBeFinalized() { + return IsAboutToBeFinalized(generatorScript_); + } +}; + +bool js::DebuggerFrame::isSuspended() const { + return hasGeneratorInfo() && + generatorInfo()->unwrappedGenerator().isSuspended(); +} + +js::AbstractGeneratorObject& js::DebuggerFrame::unwrappedGenerator() const { + return generatorInfo()->unwrappedGenerator(); +} + +#ifdef DEBUG +JSScript* js::DebuggerFrame::generatorScript() const { + return generatorInfo()->generatorScript(); +} +#endif + +/* static */ +bool DebuggerFrame::setGeneratorInfo(JSContext* cx, + Handle<DebuggerFrame*> frame, + Handle<AbstractGeneratorObject*> genObj) { + cx->check(frame); + + MOZ_ASSERT(!frame->hasGeneratorInfo()); + MOZ_ASSERT(!genObj->isClosed()); + + // When we initialize the generator information, we do not need to adjust + // the stepper increment, because either it was already incremented when + // the step hook was added, or we're setting this into on a new DebuggerFrame + // that has not yet had the chance for a hook to be added to it. + MOZ_ASSERT_IF(frame->onStepHandler(), frame->frameIterData()); + MOZ_ASSERT_IF(!frame->frameIterData(), !frame->onStepHandler()); + + // There are two relations we must establish: + // + // 1) The DebuggerFrame must point to the AbstractGeneratorObject. + // + // 2) The generator's script's observer count must be bumped. + + RootedScript script(cx, genObj->callee().nonLazyScript()); + Rooted<UniquePtr<GeneratorInfo>> info( + cx, cx->make_unique<GeneratorInfo>(genObj, script)); + if (!info) { + return false; + } + + AutoRealm ar(cx, script); + + // All frames running a debuggee script must themselves be marked as + // debuggee frames. Bumping a script's generator observer count makes it a + // debuggee, so we need to mark all frames on the stack running it as + // debuggees as well, not just this one. This call takes care of all that. + if (!Debugger::ensureExecutionObservabilityOfScript(cx, script)) { + return false; + } + + if (!DebugScript::incrementGeneratorObserverCount(cx, script)) { + return false; + } + + InitReservedSlot(frame, GENERATOR_INFO_SLOT, info.release(), + MemoryUse::DebuggerFrameGeneratorInfo); + return true; +} + +void DebuggerFrame::terminate(JS::GCContext* gcx, AbstractFramePtr frame) { + if (frameIterData()) { + // If no frame pointer was provided to decrement the stepper counter, + // then we must be terminating a generator, otherwise the stepper count + // would have no way to synchronize properly. + MOZ_ASSERT_IF(!frame, hasGeneratorInfo()); + + freeFrameIterData(gcx); + if (frame && !hasGeneratorInfo() && onStepHandler()) { + // If we are terminating a non-generator frame that had a step handler, + // we need to decrement the counter to keep things in sync. + decrementStepperCounter(gcx, frame); + } + } + + if (!hasGeneratorInfo()) { + return; + } + + GeneratorInfo* info = generatorInfo(); + + // 3) The generator's script's observer count must be dropped. + // + // For ordinary calls, Debugger.Frame objects drop the script's stepper count + // when the frame is popped, but for generators, they leave the stepper count + // incremented across suspensions. This means that, whereas ordinary calls + // never need to drop the stepper count from the D.F finalizer, generator + // calls may. + if (!info->isGeneratorScriptAboutToBeFinalized()) { + JSScript* generatorScript = info->generatorScript(); + DebugScript::decrementGeneratorObserverCount(gcx, generatorScript); + if (onStepHandler()) { + // If we are terminating a generator frame that had a step handler, + // we need to decrement the counter to keep things in sync. + decrementStepperCounter(gcx, generatorScript); + } + } + + // 1) The DebuggerFrame must no longer point to the AbstractGeneratorObject. + setReservedSlot(GENERATOR_INFO_SLOT, UndefinedValue()); + gcx->delete_(this, info, MemoryUse::DebuggerFrameGeneratorInfo); +} + +void DebuggerFrame::suspend(JS::GCContext* gcx) { + // There must be generator info because otherwise this would be the same + // overall behavior as terminate() except that here we do not properly + // adjust stepper counts. + MOZ_ASSERT(hasGeneratorInfo()); + + freeFrameIterData(gcx); +} + +/* static */ +bool DebuggerFrame::getCallee(JSContext* cx, Handle<DebuggerFrame*> frame, + MutableHandle<DebuggerObject*> result) { + RootedObject callee(cx); + if (frame->isOnStack()) { + AbstractFramePtr referent = DebuggerFrame::getReferent(frame); + if (referent.isFunctionFrame()) { + callee = referent.callee(); + } + } else { + MOZ_ASSERT(frame->isSuspended()); + + callee = &frame->generatorInfo()->unwrappedGenerator().callee(); + } + + return frame->owner()->wrapNullableDebuggeeObject(cx, callee, result); +} + +/* static */ +bool DebuggerFrame::getIsConstructing(JSContext* cx, + Handle<DebuggerFrame*> frame, + bool& result) { + if (frame->isOnStack()) { + FrameIter iter = frame->getFrameIter(cx); + + result = iter.isFunctionFrame() && iter.isConstructing(); + } else { + MOZ_ASSERT(frame->isSuspended()); + + // Generators and async functions can't be constructed. + result = false; + } + return true; +} + +static void UpdateFrameIterPc(FrameIter& iter) { + if (iter.abstractFramePtr().isWasmDebugFrame()) { + // Wasm debug frames don't need their pc updated -- it's null. + return; + } + + if (iter.abstractFramePtr().isRematerializedFrame()) { +#ifdef DEBUG + // Rematerialized frames don't need their pc updated. The reason we + // need to update pc is because we might get the same Debugger.Frame + // object for multiple re-entries into debugger code from debuggee + // code. This reentrancy is not possible with rematerialized frames, + // because when returning to debuggee code, we would have bailed out + // to baseline. + // + // We walk the stack to assert that it doesn't need updating. + jit::RematerializedFrame* frame = + iter.abstractFramePtr().asRematerializedFrame(); + jit::JitFrameLayout* jsFrame = (jit::JitFrameLayout*)frame->top(); + jit::JitActivation* activation = iter.activation()->asJit(); + + JSContext* cx = TlsContext.get(); + MOZ_ASSERT(cx == activation->cx()); + + ActivationIterator activationIter(cx); + while (activationIter.activation() != activation) { + ++activationIter; + } + + OnlyJSJitFrameIter jitIter(activationIter); + while (!jitIter.frame().isIonJS() || jitIter.frame().jsFrame() != jsFrame) { + ++jitIter; + } + + jit::InlineFrameIterator ionInlineIter(cx, &jitIter.frame()); + while (ionInlineIter.frameNo() != frame->frameNo()) { + ++ionInlineIter; + } + + MOZ_ASSERT(ionInlineIter.pc() == iter.pc()); +#endif + return; + } + + iter.updatePcQuadratic(); +} + +/* static */ +bool DebuggerFrame::getEnvironment(JSContext* cx, Handle<DebuggerFrame*> frame, + MutableHandle<DebuggerEnvironment*> result) { + Debugger* dbg = frame->owner(); + Rooted<Env*> env(cx); + + if (frame->isOnStack()) { + FrameIter iter = frame->getFrameIter(cx); + + { + AutoRealm ar(cx, iter.abstractFramePtr().environmentChain()); + UpdateFrameIterPc(iter); + env = GetDebugEnvironmentForFrame(cx, iter.abstractFramePtr(), iter.pc()); + } + } else { + MOZ_ASSERT(frame->isSuspended()); + + AbstractGeneratorObject& genObj = + frame->generatorInfo()->unwrappedGenerator(); + JSScript* script = frame->generatorInfo()->generatorScript(); + + { + AutoRealm ar(cx, &genObj.environmentChain()); + env = GetDebugEnvironmentForSuspendedGenerator(cx, script, genObj); + } + } + + if (!env) { + return false; + } + + return dbg->wrapEnvironment(cx, env, result); +} + +/* static */ +bool DebuggerFrame::getOffset(JSContext* cx, Handle<DebuggerFrame*> frame, + size_t& result) { + if (frame->isOnStack()) { + FrameIter iter = frame->getFrameIter(cx); + + AbstractFramePtr referent = DebuggerFrame::getReferent(frame); + if (referent.isWasmDebugFrame()) { + iter.wasmUpdateBytecodeOffset(); + result = iter.wasmBytecodeOffset(); + } else { + JSScript* script = iter.script(); + UpdateFrameIterPc(iter); + jsbytecode* pc = iter.pc(); + result = script->pcToOffset(pc); + } + } else { + MOZ_ASSERT(frame->isSuspended()); + + AbstractGeneratorObject& genObj = + frame->generatorInfo()->unwrappedGenerator(); + JSScript* script = frame->generatorInfo()->generatorScript(); + result = script->resumeOffsets()[genObj.resumeIndex()]; + } + return true; +} + +/* static */ +bool DebuggerFrame::getOlder(JSContext* cx, Handle<DebuggerFrame*> frame, + MutableHandle<DebuggerFrame*> result) { + if (frame->isOnStack()) { + Debugger* dbg = frame->owner(); + FrameIter iter = frame->getFrameIter(cx); + + while (true) { + Activation& activation = *iter.activation(); + ++iter; + + // If the parent frame crosses an explicit async stack boundary, we + // treat that as an indication to stop traversing sync frames, so that + // the on-stack Debugger.Frame instances align with what you would + // see in a stringified stack trace. + if (iter.activation() != &activation && activation.asyncStack() && + activation.asyncCallIsExplicit()) { + break; + } + + // If there is no parent frame, we're done. + if (iter.done()) { + break; + } + + if (dbg->observesFrame(iter)) { + if (iter.isIon() && !iter.ensureHasRematerializedFrame(cx)) { + return false; + } + return dbg->getFrame(cx, iter, result); + } + } + } else { + MOZ_ASSERT(frame->isSuspended()); + + // If the frame is suspended, there is no older frame. + } + + result.set(nullptr); + return true; +} + +/* static */ +bool DebuggerFrame::getAsyncPromise(JSContext* cx, Handle<DebuggerFrame*> frame, + MutableHandle<DebuggerObject*> result) { + MOZ_ASSERT(frame->isOnStack() || frame->isSuspended()); + + if (!frame->hasGeneratorInfo()) { + // An on-stack frame may not have an associated generator yet when the + // frame is initially entered. + result.set(nullptr); + return true; + } + + RootedObject resultObject(cx); + AbstractGeneratorObject& generator = frame->unwrappedGenerator(); + if (generator.is<AsyncFunctionGeneratorObject>()) { + resultObject = generator.as<AsyncFunctionGeneratorObject>().promise(); + } else if (generator.is<AsyncGeneratorObject>()) { + Rooted<AsyncGeneratorObject*> asyncGen( + cx, &generator.as<AsyncGeneratorObject>()); + // In initial function execution, there is no promise. + if (!asyncGen->isQueueEmpty()) { + resultObject = AsyncGeneratorObject::peekRequest(asyncGen)->promise(); + } + } else { + MOZ_CRASH("Unknown async generator type"); + } + + return frame->owner()->wrapNullableDebuggeeObject(cx, resultObject, result); +} + +/* static */ +bool DebuggerFrame::getThis(JSContext* cx, Handle<DebuggerFrame*> frame, + MutableHandleValue result) { + Debugger* dbg = frame->owner(); + + if (frame->isOnStack()) { + if (!requireScriptReferent(cx, frame)) { + return false; + } + FrameIter iter = frame->getFrameIter(cx); + + { + AbstractFramePtr frame = iter.abstractFramePtr(); + AutoRealm ar(cx, frame.environmentChain()); + + UpdateFrameIterPc(iter); + + if (!GetThisValueForDebuggerFrameMaybeOptimizedOut(cx, frame, iter.pc(), + result)) { + return false; + } + } + } else { + MOZ_ASSERT(frame->isSuspended()); + + AbstractGeneratorObject& genObj = + frame->generatorInfo()->unwrappedGenerator(); + AutoRealm ar(cx, &genObj); + JSScript* script = frame->generatorInfo()->generatorScript(); + + if (!GetThisValueForDebuggerSuspendedGeneratorMaybeOptimizedOut( + cx, genObj, script, result)) { + return false; + } + } + + return dbg->wrapDebuggeeValue(cx, result); +} + +/* static */ +DebuggerFrameType DebuggerFrame::getType(Handle<DebuggerFrame*> frame) { + if (frame->isOnStack()) { + AbstractFramePtr referent = DebuggerFrame::getReferent(frame); + + // Indirect eval frames are both isGlobalFrame() and isEvalFrame(), so the + // order of checks here is significant. + if (referent.isEvalFrame()) { + return DebuggerFrameType::Eval; + } + + if (referent.isGlobalFrame()) { + return DebuggerFrameType::Global; + } + + if (referent.isFunctionFrame()) { + return DebuggerFrameType::Call; + } + + if (referent.isModuleFrame()) { + return DebuggerFrameType::Module; + } + + if (referent.isWasmDebugFrame()) { + return DebuggerFrameType::WasmCall; + } + } else { + MOZ_ASSERT(frame->isSuspended()); + + return DebuggerFrameType::Call; + } + + MOZ_CRASH("Unknown frame type"); +} + +/* static */ +DebuggerFrameImplementation DebuggerFrame::getImplementation( + Handle<DebuggerFrame*> frame) { + AbstractFramePtr referent = DebuggerFrame::getReferent(frame); + + if (referent.isBaselineFrame()) { + return DebuggerFrameImplementation::Baseline; + } + + if (referent.isRematerializedFrame()) { + return DebuggerFrameImplementation::Ion; + } + + if (referent.isWasmDebugFrame()) { + return DebuggerFrameImplementation::Wasm; + } + + return DebuggerFrameImplementation::Interpreter; +} + +/* + * If succesful, transfers the ownership of the given `handler` to this + * Debugger.Frame. Note that on failure, the ownership of `handler` is not + * transferred, and the caller is responsible for cleaning it up. + */ +/* static */ +bool DebuggerFrame::setOnStepHandler(JSContext* cx, + Handle<DebuggerFrame*> frame, + UniquePtr<OnStepHandler> handlerArg) { + // Handler has never been successfully associated with the frame so allow + // UniquePtr to delete it rather than calling drop() if we return early from + // this method.. + Rooted<UniquePtr<OnStepHandler>> handler(cx, std::move(handlerArg)); + + OnStepHandler* prior = frame->onStepHandler(); + if (handler.get() == prior) { + return true; + } + + JS::GCContext* gcx = cx->gcContext(); + if (frame->isOnStack()) { + AbstractFramePtr referent = DebuggerFrame::getReferent(frame); + + // Adjust execution observability and step counts on whatever code (JS or + // Wasm) this frame is running. + if (handler && !prior) { + if (!frame->incrementStepperCounter(cx, referent)) { + return false; + } + } else if (!handler && prior) { + frame->decrementStepperCounter(cx->gcContext(), referent); + } + } else if (frame->isSuspended()) { + RootedScript script(cx, frame->generatorInfo()->generatorScript()); + + if (handler && !prior) { + if (!frame->incrementStepperCounter(cx, script)) { + return false; + } + } else if (!handler && prior) { + frame->decrementStepperCounter(cx->gcContext(), script); + } + } else { + // If the frame is entirely dead, we still allow setting the onStep + // handler, but it has no effect. + } + + // Now that the stepper counts and observability are set correctly, we can + // actually switch the handler. + if (prior) { + prior->drop(gcx, frame); + } + + if (handler) { + handler->hold(frame); + frame->setReservedSlot(ONSTEP_HANDLER_SLOT, + PrivateValue(handler.get().release())); + } else { + frame->setReservedSlot(ONSTEP_HANDLER_SLOT, UndefinedValue()); + } + + return true; +} + +bool DebuggerFrame::incrementStepperCounter(JSContext* cx, + AbstractFramePtr referent) { + if (!referent.isWasmDebugFrame()) { + RootedScript script(cx, referent.script()); + return incrementStepperCounter(cx, script); + } + + wasm::Instance* instance = referent.asWasmDebugFrame()->instance(); + wasm::DebugFrame* wasmFrame = referent.asWasmDebugFrame(); + // Single stepping toggled off->on. + if (!instance->debug().incrementStepperCount(cx, instance, + wasmFrame->funcIndex())) { + return false; + } + + return true; +} + +bool DebuggerFrame::incrementStepperCounter(JSContext* cx, + HandleScript script) { + // Single stepping toggled off->on. + AutoRealm ar(cx, script); + // Ensure observability *before* incrementing the step mode count. + // Calling this function after calling incrementStepperCount + // will make it a no-op. + if (!Debugger::ensureExecutionObservabilityOfScript(cx, script)) { + return false; + } + if (!DebugScript::incrementStepperCount(cx, script)) { + return false; + } + + return true; +} + +void DebuggerFrame::decrementStepperCounter(JS::GCContext* gcx, + AbstractFramePtr referent) { + if (!referent.isWasmDebugFrame()) { + decrementStepperCounter(gcx, referent.script()); + return; + } + + wasm::Instance* instance = referent.asWasmDebugFrame()->instance(); + wasm::DebugFrame* wasmFrame = referent.asWasmDebugFrame(); + // Single stepping toggled on->off. + instance->debug().decrementStepperCount(gcx, instance, + wasmFrame->funcIndex()); +} + +void DebuggerFrame::decrementStepperCounter(JS::GCContext* gcx, + JSScript* script) { + // Single stepping toggled on->off. + DebugScript::decrementStepperCount(gcx, script); +} + +/* static */ +bool DebuggerFrame::getArguments(JSContext* cx, Handle<DebuggerFrame*> frame, + MutableHandle<DebuggerArguments*> result) { + Value argumentsv = frame->getReservedSlot(ARGUMENTS_SLOT); + if (!argumentsv.isUndefined()) { + result.set(argumentsv.isObject() + ? &argumentsv.toObject().as<DebuggerArguments>() + : nullptr); + return true; + } + + AbstractFramePtr referent = DebuggerFrame::getReferent(frame); + + Rooted<DebuggerArguments*> arguments(cx); + if (referent.hasArgs()) { + Rooted<GlobalObject*> global(cx, &frame->global()); + RootedObject proto(cx, GlobalObject::getOrCreateArrayPrototype(cx, global)); + if (!proto) { + return false; + } + arguments = DebuggerArguments::create(cx, proto, frame); + if (!arguments) { + return false; + } + } else { + arguments = nullptr; + } + + result.set(arguments); + frame->setReservedSlot(ARGUMENTS_SLOT, ObjectOrNullValue(result)); + return true; +} + +/* + * Evaluate |chars[0..length-1]| in the environment |env|, treating that + * source as appearing starting at |lineno| in |filename|. Store the return + * value in |*rval|. Use |thisv| as the 'this' value. + * + * If |frame| is non-nullptr, evaluate as for a direct eval in that frame; |env| + * must be either |frame|'s DebugScopeObject, or some extension of that + * environment; either way, |frame|'s scope is where newly declared variables + * go. In this case, |frame| must have a computed 'this' value, equal to + * |thisv|. + */ +static bool EvaluateInEnv(JSContext* cx, Handle<Env*> env, + AbstractFramePtr frame, + mozilla::Range<const char16_t> chars, + const EvalOptions& evalOptions, + MutableHandleValue rval) { + cx->check(env, frame); + + CompileOptions options(cx); + const char* filename = + evalOptions.filename() ? evalOptions.filename() : "debugger eval code"; + options.setIsRunOnce(true) + .setNoScriptRval(false) + .setFileAndLine(filename, evalOptions.lineno()) + .setHideScriptFromDebugger(evalOptions.hideFromDebugger()) + .setIntroductionType("debugger eval") + /* Do not perform the Javascript filename validation security check for + * javascript executions sent through the debugger. Besides making up + * a filename for these codepaths, we must allow arbitrary JS execution + * for the Browser toolbox to function. */ + .setSkipFilenameValidation(true) + /* Don't lazy parse. We need full-parsing to correctly support bytecode + * emission for private fields/methods. See EmitterScope::lookupPrivate. + */ + .setForceFullParse(); + + if (frame && frame.hasScript() && frame.script()->strict()) { + options.setForceStrictMode(); + } + + SourceText<char16_t> srcBuf; + if (!srcBuf.init(cx, chars.begin().get(), chars.length(), + SourceOwnership::Borrowed)) { + return false; + } + + RootedScript callerScript( + cx, frame && frame.hasScript() ? frame.script() : nullptr); + RootedScript script(cx); + + ScopeKind scopeKind; + if (IsGlobalLexicalEnvironment(env)) { + scopeKind = ScopeKind::Global; + } else { + scopeKind = ScopeKind::NonSyntactic; + options.setNonSyntacticScope(true); + } + + if (frame) { + MOZ_ASSERT(scopeKind == ScopeKind::NonSyntactic); + Rooted<Scope*> scope(cx, + GlobalScope::createEmpty(cx, ScopeKind::NonSyntactic)); + if (!scope) { + return false; + } + + script = frontend::CompileEvalScript(cx, options, srcBuf, scope, env); + if (!script) { + return false; + } + } else { + // Do not consider executeInGlobal{WithBindings} as an eval, but instead + // as executing a series of statements at the global level. This is to + // circumvent the fresh lexical scope that all eval have, so that the + // users of executeInGlobal, like the web console, may add new bindings to + // the global scope. + + MOZ_ASSERT(scopeKind == ScopeKind::Global || + scopeKind == ScopeKind::NonSyntactic); + + AutoReportFrontendContext fc(cx); + script = frontend::CompileGlobalScript(cx, &fc, + cx->stackLimitForCurrentPrincipal(), + options, srcBuf, scopeKind); + if (!script) { + return false; + } + } + + return ExecuteKernel(cx, script, env, frame, rval); +} + +Result<Completion> js::DebuggerGenericEval( + JSContext* cx, const mozilla::Range<const char16_t> chars, + HandleObject bindings, const EvalOptions& options, Debugger* dbg, + HandleObject envArg, FrameIter* iter) { + // Either we're specifying the frame, or a global. + MOZ_ASSERT_IF(iter, !envArg); + MOZ_ASSERT_IF(!iter, envArg && IsGlobalLexicalEnvironment(envArg)); + + // Gather keys and values of bindings, if any. This must be done in the + // debugger compartment, since that is where any exceptions must be thrown. + RootedIdVector keys(cx); + RootedValueVector values(cx); + if (bindings) { + if (!GetPropertyKeys(cx, bindings, JSITER_OWNONLY, &keys) || + !values.growBy(keys.length())) { + return cx->alreadyReportedError(); + } + for (size_t i = 0; i < keys.length(); i++) { + MutableHandleValue valp = values[i]; + if (!GetProperty(cx, bindings, bindings, keys[i], valp) || + !dbg->unwrapDebuggeeValue(cx, valp)) { + return cx->alreadyReportedError(); + } + } + } + + Maybe<AutoRealm> ar; + if (iter) { + ar.emplace(cx, iter->environmentChain(cx)); + } else { + ar.emplace(cx, envArg); + } + + Rooted<Env*> env(cx); + if (iter) { + env = GetDebugEnvironmentForFrame(cx, iter->abstractFramePtr(), iter->pc()); + if (!env) { + return cx->alreadyReportedError(); + } + } else { + env = envArg; + } + + // If evalWithBindings, create the inner environment. + if (bindings) { + Rooted<PlainObject*> nenv(cx, NewPlainObjectWithProto(cx, nullptr)); + if (!nenv) { + return cx->alreadyReportedError(); + } + RootedId id(cx); + for (size_t i = 0; i < keys.length(); i++) { + id = keys[i]; + cx->markId(id); + MutableHandleValue val = values[i]; + if (!cx->compartment()->wrap(cx, val) || + !NativeDefineDataProperty(cx, nenv, id, val, 0)) { + return cx->alreadyReportedError(); + } + } + + RootedObjectVector envChain(cx); + if (!envChain.append(nenv)) { + return cx->alreadyReportedError(); + } + + RootedObject newEnv(cx); + if (!CreateObjectsForEnvironmentChain(cx, envChain, env, &newEnv)) { + return cx->alreadyReportedError(); + } + + env = newEnv; + } + + // Note whether we are in an evaluation that might invoke the OnNativeCall + // hook, so that the JITs will be disabled. + AutoNoteDebuggerEvaluationWithOnNativeCallHook noteEvaluation( + cx, dbg->observesNativeCalls() ? dbg : nullptr); + + // Run the code and produce the completion value. + LeaveDebuggeeNoExecute nnx(cx); + RootedValue rval(cx); + AbstractFramePtr frame = iter ? iter->abstractFramePtr() : NullFramePtr(); + + bool ok = EvaluateInEnv(cx, env, frame, chars, options, &rval); + Rooted<Completion> completion(cx, Completion::fromJSResult(cx, ok, rval)); + ar.reset(); + return completion.get(); +} + +/* static */ +Result<Completion> DebuggerFrame::eval(JSContext* cx, + Handle<DebuggerFrame*> frame, + mozilla::Range<const char16_t> chars, + HandleObject bindings, + const EvalOptions& options) { + MOZ_ASSERT(frame->isOnStack()); + + Debugger* dbg = frame->owner(); + FrameIter iter = frame->getFrameIter(cx); + + UpdateFrameIterPc(iter); + + return DebuggerGenericEval(cx, chars, bindings, options, dbg, nullptr, &iter); +} + +bool DebuggerFrame::isOnStack() const { + // Note: this is equivalent to checking frameIterData() != nullptr, but works + // also when called from the trace hook during a moving GC. + return !getFixedSlot(FRAME_ITER_SLOT).isUndefined(); +} + +OnStepHandler* DebuggerFrame::onStepHandler() const { + return maybePtrFromReservedSlot<OnStepHandler>(ONSTEP_HANDLER_SLOT); +} + +OnPopHandler* DebuggerFrame::onPopHandler() const { + return maybePtrFromReservedSlot<OnPopHandler>(ONPOP_HANDLER_SLOT); +} + +void DebuggerFrame::setOnPopHandler(JSContext* cx, OnPopHandler* handler) { + OnPopHandler* prior = onPopHandler(); + if (handler == prior) { + return; + } + + JS::GCContext* gcx = cx->gcContext(); + + if (prior) { + prior->drop(gcx, this); + } + + if (handler) { + setReservedSlot(ONPOP_HANDLER_SLOT, PrivateValue(handler)); + handler->hold(this); + } else { + setReservedSlot(ONPOP_HANDLER_SLOT, UndefinedValue()); + } +} + +FrameIter::Data* DebuggerFrame::frameIterData() const { + return maybePtrFromReservedSlot<FrameIter::Data>(FRAME_ITER_SLOT); +} + +/* static */ +AbstractFramePtr DebuggerFrame::getReferent(Handle<DebuggerFrame*> frame) { + FrameIter iter(*frame->frameIterData()); + return iter.abstractFramePtr(); +} + +FrameIter DebuggerFrame::getFrameIter(JSContext* cx) { + FrameIter::Data* data = frameIterData(); + MOZ_ASSERT(data); + MOZ_ASSERT(data->cx_ == cx); + + return FrameIter(*data); +} + +/* static */ +bool DebuggerFrame::requireScriptReferent(JSContext* cx, + Handle<DebuggerFrame*> frame) { + AbstractFramePtr referent = DebuggerFrame::getReferent(frame); + if (!referent.hasScript()) { + RootedValue frameobj(cx, ObjectValue(*frame)); + ReportValueError(cx, JSMSG_DEBUG_BAD_REFERENT, JSDVG_SEARCH_STACK, frameobj, + nullptr, "a script frame"); + return false; + } + return true; +} + +void DebuggerFrame::setFrameIterData(FrameIter::Data* data) { + MOZ_ASSERT(data); + MOZ_ASSERT(!frameIterData()); + InitReservedSlot(this, FRAME_ITER_SLOT, data, + MemoryUse::DebuggerFrameIterData); +} + +void DebuggerFrame::freeFrameIterData(JS::GCContext* gcx) { + if (FrameIter::Data* data = frameIterData()) { + gcx->delete_(this, data, MemoryUse::DebuggerFrameIterData); + setReservedSlot(FRAME_ITER_SLOT, UndefinedValue()); + } +} + +bool DebuggerFrame::replaceFrameIterData(JSContext* cx, const FrameIter& iter) { + FrameIter::Data* data = iter.copyData(); + if (!data) { + return false; + } + freeFrameIterData(cx->gcContext()); + setFrameIterData(data); + return true; +} + +/* static */ +void DebuggerFrame::finalize(JS::GCContext* gcx, JSObject* obj) { + MOZ_ASSERT(gcx->onMainThread()); + + DebuggerFrame& frameobj = obj->as<DebuggerFrame>(); + + // Connections between dying Debugger.Frames and their + // AbstractGeneratorObjects, as well as the frame's stack data should have + // been by a call to terminate() from sweepAll or some other place. + MOZ_ASSERT(!frameobj.hasGeneratorInfo()); + MOZ_ASSERT(!frameobj.frameIterData()); + OnStepHandler* onStepHandler = frameobj.onStepHandler(); + if (onStepHandler) { + onStepHandler->drop(gcx, &frameobj); + } + OnPopHandler* onPopHandler = frameobj.onPopHandler(); + if (onPopHandler) { + onPopHandler->drop(gcx, &frameobj); + } +} + +void DebuggerFrame::trace(JSTracer* trc) { + OnStepHandler* onStepHandler = this->onStepHandler(); + if (onStepHandler) { + onStepHandler->trace(trc); + } + OnPopHandler* onPopHandler = this->onPopHandler(); + if (onPopHandler) { + onPopHandler->trace(trc); + } + + if (hasGeneratorInfo()) { + generatorInfo()->trace(trc, *this); + } +} + +/* static */ +DebuggerFrame* DebuggerFrame::check(JSContext* cx, HandleValue thisv) { + JSObject* thisobj = RequireObject(cx, thisv); + if (!thisobj) { + return nullptr; + } + if (!thisobj->is<DebuggerFrame>()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INCOMPATIBLE_PROTO, "Debugger.Frame", + "method", thisobj->getClass()->name); + return nullptr; + } + + return &thisobj->as<DebuggerFrame>(); +} + +struct MOZ_STACK_CLASS DebuggerFrame::CallData { + JSContext* cx; + const CallArgs& args; + + Handle<DebuggerFrame*> frame; + + CallData(JSContext* cx, const CallArgs& args, Handle<DebuggerFrame*> frame) + : cx(cx), args(args), frame(frame) {} + + bool argumentsGetter(); + bool calleeGetter(); + bool constructingGetter(); + bool environmentGetter(); + bool generatorGetter(); + bool asyncPromiseGetter(); + bool olderSavedFrameGetter(); + bool liveGetter(); + bool onStackGetter(); + bool terminatedGetter(); + bool offsetGetter(); + bool olderGetter(); + bool getScript(); + bool thisGetter(); + bool typeGetter(); + bool implementationGetter(); + bool onStepGetter(); + bool onStepSetter(); + bool onPopGetter(); + bool onPopSetter(); + bool evalMethod(); + bool evalWithBindingsMethod(); + + using Method = bool (CallData::*)(); + + template <Method MyMethod> + static bool ToNative(JSContext* cx, unsigned argc, Value* vp); + + bool ensureOnStack() const; + bool ensureOnStackOrSuspended() const; +}; + +template <DebuggerFrame::CallData::Method MyMethod> +/* static */ +bool DebuggerFrame::CallData::ToNative(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<DebuggerFrame*> frame(cx, DebuggerFrame::check(cx, args.thisv())); + if (!frame) { + return false; + } + + CallData data(cx, args, frame); + return (data.*MyMethod)(); +} + +static bool EnsureOnStack(JSContext* cx, Handle<DebuggerFrame*> frame) { + MOZ_ASSERT(frame); + if (!frame->isOnStack()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_NOT_ON_STACK, "Debugger.Frame"); + return false; + } + + return true; +} +static bool EnsureOnStackOrSuspended(JSContext* cx, + Handle<DebuggerFrame*> frame) { + MOZ_ASSERT(frame); + if (!frame->isOnStack() && !frame->isSuspended()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_NOT_ON_STACK_OR_SUSPENDED, + "Debugger.Frame"); + return false; + } + + return true; +} + +bool DebuggerFrame::CallData::ensureOnStack() const { + return EnsureOnStack(cx, frame); +} +bool DebuggerFrame::CallData::ensureOnStackOrSuspended() const { + return EnsureOnStackOrSuspended(cx, frame); +} + +bool DebuggerFrame::CallData::typeGetter() { + if (!ensureOnStackOrSuspended()) { + return false; + } + + DebuggerFrameType type = DebuggerFrame::getType(frame); + + JSString* str; + switch (type) { + case DebuggerFrameType::Eval: + str = cx->names().eval; + break; + case DebuggerFrameType::Global: + str = cx->names().global; + break; + case DebuggerFrameType::Call: + str = cx->names().call; + break; + case DebuggerFrameType::Module: + str = cx->names().module; + break; + case DebuggerFrameType::WasmCall: + str = cx->names().wasmcall; + break; + default: + MOZ_CRASH("bad DebuggerFrameType value"); + } + + args.rval().setString(str); + return true; +} + +bool DebuggerFrame::CallData::implementationGetter() { + if (!ensureOnStack()) { + return false; + } + + DebuggerFrameImplementation implementation = + DebuggerFrame::getImplementation(frame); + + const char* s; + switch (implementation) { + case DebuggerFrameImplementation::Baseline: + s = "baseline"; + break; + case DebuggerFrameImplementation::Ion: + s = "ion"; + break; + case DebuggerFrameImplementation::Interpreter: + s = "interpreter"; + break; + case DebuggerFrameImplementation::Wasm: + s = "wasm"; + break; + default: + MOZ_CRASH("bad DebuggerFrameImplementation value"); + } + + JSAtom* str = Atomize(cx, s, strlen(s)); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +bool DebuggerFrame::CallData::environmentGetter() { + if (!ensureOnStackOrSuspended()) { + return false; + } + + Rooted<DebuggerEnvironment*> result(cx); + if (!DebuggerFrame::getEnvironment(cx, frame, &result)) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +bool DebuggerFrame::CallData::calleeGetter() { + if (!ensureOnStackOrSuspended()) { + return false; + } + + Rooted<DebuggerObject*> result(cx); + if (!DebuggerFrame::getCallee(cx, frame, &result)) { + return false; + } + + args.rval().setObjectOrNull(result); + return true; +} + +bool DebuggerFrame::CallData::generatorGetter() { + JS_ReportErrorASCII(cx, + "Debugger.Frame.prototype.generator has been removed. " + "Use frame.script.isGeneratorFunction instead."); + return false; +} + +bool DebuggerFrame::CallData::constructingGetter() { + if (!ensureOnStackOrSuspended()) { + return false; + } + + bool result; + if (!DebuggerFrame::getIsConstructing(cx, frame, result)) { + return false; + } + + args.rval().setBoolean(result); + return true; +} + +bool DebuggerFrame::CallData::asyncPromiseGetter() { + if (!ensureOnStackOrSuspended()) { + return false; + } + + RootedScript script(cx); + if (frame->isOnStack()) { + FrameIter iter = frame->getFrameIter(cx); + AbstractFramePtr framePtr = iter.abstractFramePtr(); + + if (!framePtr.isWasmDebugFrame()) { + script = framePtr.script(); + } + } else { + MOZ_ASSERT(frame->isSuspended()); + script = frame->generatorInfo()->generatorScript(); + } + // The async promise value is only provided for async functions and + // async generator functions. + if (!script || !script->isAsync()) { + args.rval().setUndefined(); + return true; + } + + Rooted<DebuggerObject*> result(cx); + if (!DebuggerFrame::getAsyncPromise(cx, frame, &result)) { + return false; + } + + args.rval().setObjectOrNull(result); + return true; +} + +bool DebuggerFrame::CallData::olderSavedFrameGetter() { + if (!ensureOnStackOrSuspended()) { + return false; + } + + Rooted<SavedFrame*> result(cx); + if (!DebuggerFrame::getOlderSavedFrame(cx, frame, &result)) { + return false; + } + + args.rval().setObjectOrNull(result); + return true; +} + +/* static */ +bool DebuggerFrame::getOlderSavedFrame(JSContext* cx, + Handle<DebuggerFrame*> frame, + MutableHandle<SavedFrame*> result) { + if (frame->isOnStack()) { + Debugger* dbg = frame->owner(); + FrameIter iter = frame->getFrameIter(cx); + + while (true) { + Activation& activation = *iter.activation(); + ++iter; + + // If the parent frame crosses an explicit async stack boundary, or we + // have hit the end of the synchronous frames, we want to switch over + // to using SavedFrames. + if (iter.activation() != &activation && activation.asyncStack() && + (activation.asyncCallIsExplicit() || iter.done())) { + const char* cause = activation.asyncCause(); + Rooted<JSAtom*> causeAtom(cx, + AtomizeUTF8Chars(cx, cause, strlen(cause))); + if (!causeAtom) { + return false; + } + Rooted<SavedFrame*> stackObj(cx, activation.asyncStack()); + + return cx->realm()->savedStacks().copyAsyncStack( + cx, stackObj, causeAtom, result, mozilla::Nothing()); + } + + // If there are no more parent frames, we're done. + if (iter.done()) { + break; + } + + // If we hit another frame that we observe, then there is no saved + // frame that we'd want to return. + if (dbg->observesFrame(iter)) { + break; + } + } + } else { + MOZ_ASSERT(frame->isSuspended()); + } + + result.set(nullptr); + return true; +} + +bool DebuggerFrame::CallData::thisGetter() { + if (!ensureOnStackOrSuspended()) { + return false; + } + + return DebuggerFrame::getThis(cx, frame, args.rval()); +} + +bool DebuggerFrame::CallData::olderGetter() { + if (!ensureOnStackOrSuspended()) { + return false; + } + + Rooted<DebuggerFrame*> result(cx); + if (!DebuggerFrame::getOlder(cx, frame, &result)) { + return false; + } + + args.rval().setObjectOrNull(result); + return true; +} + +// The getter used for each element of frame.arguments. +// See DebuggerFrame::getArguments. +static bool DebuggerArguments_getArg(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + int32_t i = args.callee().as<JSFunction>().getExtendedSlot(0).toInt32(); + + // Check that the this value is an Arguments object. + RootedObject argsobj(cx, RequireObject(cx, args.thisv())); + if (!argsobj) { + return false; + } + if (argsobj->getClass() != &DebuggerArguments::class_) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INCOMPATIBLE_PROTO, "Arguments", + "getArgument", argsobj->getClass()->name); + return false; + } + + RootedValue framev(cx, argsobj->as<NativeObject>().getReservedSlot( + JSSLOT_DEBUGARGUMENTS_FRAME)); + Rooted<DebuggerFrame*> thisobj(cx, DebuggerFrame::check(cx, framev)); + if (!thisobj || !EnsureOnStack(cx, thisobj)) { + return false; + } + + FrameIter iter = thisobj->getFrameIter(cx); + AbstractFramePtr frame = iter.abstractFramePtr(); + + // TODO handle wasm frame arguments -- they are not yet reflectable. + MOZ_ASSERT(!frame.isWasmDebugFrame(), "a wasm frame args"); + + // Since getters can be extracted and applied to other objects, + // there is no guarantee this object has an ith argument. + MOZ_ASSERT(i >= 0); + RootedValue arg(cx); + RootedScript script(cx); + if (unsigned(i) < frame.numActualArgs()) { + script = frame.script(); + if (unsigned(i) < frame.numFormalArgs()) { + for (PositionalFormalParameterIter fi(script); fi; fi++) { + if (fi.argumentSlot() == unsigned(i)) { + // We might've been called before the CallObject was + // created. + if (fi.closedOver() && frame.hasInitialEnvironment()) { + arg = frame.callObj().aliasedBinding(fi); + } else { + arg = frame.unaliasedActual(i, DONT_CHECK_ALIASING); + } + break; + } + } + } else if (script->argsObjAliasesFormals() && frame.hasArgsObj()) { + arg = frame.argsObj().arg(i); + } else { + arg = frame.unaliasedActual(i, DONT_CHECK_ALIASING); + } + } else { + arg.setUndefined(); + } + + if (!thisobj->owner()->wrapDebuggeeValue(cx, &arg)) { + return false; + } + args.rval().set(arg); + return true; +} + +/* static */ +DebuggerArguments* DebuggerArguments::create(JSContext* cx, HandleObject proto, + Handle<DebuggerFrame*> frame) { + AbstractFramePtr referent = DebuggerFrame::getReferent(frame); + + Rooted<DebuggerArguments*> obj( + cx, NewObjectWithGivenProto<DebuggerArguments>(cx, proto)); + if (!obj) { + return nullptr; + } + + JS::SetReservedSlot(obj, FRAME_SLOT, ObjectValue(*frame)); + + MOZ_ASSERT(referent.numActualArgs() <= 0x7fffffff); + unsigned fargc = referent.numActualArgs(); + RootedValue fargcVal(cx, Int32Value(fargc)); + if (!NativeDefineDataProperty(cx, obj, cx->names().length, fargcVal, + JSPROP_PERMANENT | JSPROP_READONLY)) { + return nullptr; + } + + Rooted<jsid> id(cx); + for (unsigned i = 0; i < fargc; i++) { + RootedFunction getobj(cx); + getobj = NewNativeFunction(cx, DebuggerArguments_getArg, 0, nullptr, + gc::AllocKind::FUNCTION_EXTENDED); + if (!getobj) { + return nullptr; + } + id = PropertyKey::Int(i); + if (!NativeDefineAccessorProperty(cx, obj, id, getobj, nullptr, + JSPROP_ENUMERATE)) { + return nullptr; + } + getobj->setExtendedSlot(0, Int32Value(i)); + } + + return obj; +} + +bool DebuggerFrame::CallData::argumentsGetter() { + if (!ensureOnStack()) { + return false; + } + + Rooted<DebuggerArguments*> result(cx); + if (!DebuggerFrame::getArguments(cx, frame, &result)) { + return false; + } + + args.rval().setObjectOrNull(result); + return true; +} + +bool DebuggerFrame::CallData::getScript() { + if (!ensureOnStackOrSuspended()) { + return false; + } + + Rooted<DebuggerScript*> scriptObject(cx); + + Debugger* debug = frame->owner(); + if (frame->isOnStack()) { + FrameIter iter = frame->getFrameIter(cx); + AbstractFramePtr framePtr = iter.abstractFramePtr(); + + if (framePtr.isWasmDebugFrame()) { + Rooted<WasmInstanceObject*> instance(cx, + framePtr.wasmInstance()->object()); + scriptObject = debug->wrapWasmScript(cx, instance); + } else { + RootedScript script(cx, framePtr.script()); + scriptObject = debug->wrapScript(cx, script); + } + } else { + MOZ_ASSERT(frame->isSuspended()); + RootedScript script(cx, frame->generatorInfo()->generatorScript()); + scriptObject = debug->wrapScript(cx, script); + } + if (!scriptObject) { + return false; + } + + args.rval().setObject(*scriptObject); + return true; +} + +bool DebuggerFrame::CallData::offsetGetter() { + if (!ensureOnStackOrSuspended()) { + return false; + } + + size_t result; + if (!DebuggerFrame::getOffset(cx, frame, result)) { + return false; + } + + args.rval().setNumber(double(result)); + return true; +} + +bool DebuggerFrame::CallData::liveGetter() { + JS_ReportErrorASCII( + cx, "Debugger.Frame.prototype.live has been renamed to .onStack"); + return false; +} + +bool DebuggerFrame::CallData::onStackGetter() { + args.rval().setBoolean(frame->isOnStack()); + return true; +} + +bool DebuggerFrame::CallData::terminatedGetter() { + args.rval().setBoolean(!frame->isOnStack() && !frame->isSuspended()); + return true; +} + +static bool IsValidHook(const Value& v) { + return v.isUndefined() || (v.isObject() && v.toObject().isCallable()); +} + +bool DebuggerFrame::CallData::onStepGetter() { + OnStepHandler* handler = frame->onStepHandler(); + RootedValue value( + cx, handler ? ObjectOrNullValue(handler->object()) : UndefinedValue()); + MOZ_ASSERT(IsValidHook(value)); + args.rval().set(value); + return true; +} + +bool DebuggerFrame::CallData::onStepSetter() { + if (!args.requireAtLeast(cx, "Debugger.Frame.set onStep", 1)) { + return false; + } + if (!IsValidHook(args[0])) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_CALLABLE_OR_UNDEFINED); + return false; + } + + UniquePtr<ScriptedOnStepHandler> handler; + if (!args[0].isUndefined()) { + handler = cx->make_unique<ScriptedOnStepHandler>(&args[0].toObject()); + if (!handler) { + return false; + } + } + + if (!DebuggerFrame::setOnStepHandler(cx, frame, std::move(handler))) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +bool DebuggerFrame::CallData::onPopGetter() { + OnPopHandler* handler = frame->onPopHandler(); + RootedValue value( + cx, handler ? ObjectValue(*handler->object()) : UndefinedValue()); + MOZ_ASSERT(IsValidHook(value)); + args.rval().set(value); + return true; +} + +bool DebuggerFrame::CallData::onPopSetter() { + if (!args.requireAtLeast(cx, "Debugger.Frame.set onPop", 1)) { + return false; + } + if (!IsValidHook(args[0])) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_CALLABLE_OR_UNDEFINED); + return false; + } + + ScriptedOnPopHandler* handler = nullptr; + if (!args[0].isUndefined()) { + handler = cx->new_<ScriptedOnPopHandler>(&args[0].toObject()); + if (!handler) { + return false; + } + } + + frame->setOnPopHandler(cx, handler); + + args.rval().setUndefined(); + return true; +} + +bool DebuggerFrame::CallData::evalMethod() { + if (!ensureOnStack()) { + return false; + } + + if (!args.requireAtLeast(cx, "Debugger.Frame.prototype.eval", 1)) { + return false; + } + + AutoStableStringChars stableChars(cx); + if (!ValueToStableChars(cx, "Debugger.Frame.prototype.eval", args[0], + stableChars)) { + return false; + } + mozilla::Range<const char16_t> chars = stableChars.twoByteRange(); + + EvalOptions options; + if (!ParseEvalOptions(cx, args.get(1), options)) { + return false; + } + + Rooted<Completion> comp(cx); + JS_TRY_VAR_OR_RETURN_FALSE( + cx, comp, DebuggerFrame::eval(cx, frame, chars, nullptr, options)); + return comp.get().buildCompletionValue(cx, frame->owner(), args.rval()); +} + +bool DebuggerFrame::CallData::evalWithBindingsMethod() { + if (!ensureOnStack()) { + return false; + } + + if (!args.requireAtLeast(cx, "Debugger.Frame.prototype.evalWithBindings", + 2)) { + return false; + } + + AutoStableStringChars stableChars(cx); + if (!ValueToStableChars(cx, "Debugger.Frame.prototype.evalWithBindings", + args[0], stableChars)) { + return false; + } + mozilla::Range<const char16_t> chars = stableChars.twoByteRange(); + + RootedObject bindings(cx, RequireObject(cx, args[1])); + if (!bindings) { + return false; + } + + EvalOptions options; + if (!ParseEvalOptions(cx, args.get(2), options)) { + return false; + } + + Rooted<Completion> comp(cx); + JS_TRY_VAR_OR_RETURN_FALSE( + cx, comp, DebuggerFrame::eval(cx, frame, chars, bindings, options)); + return comp.get().buildCompletionValue(cx, frame->owner(), args.rval()); +} + +/* static */ +bool DebuggerFrame::construct(JSContext* cx, unsigned argc, Value* vp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, + "Debugger.Frame"); + return false; +} + +const JSPropertySpec DebuggerFrame::properties_[] = { + JS_DEBUG_PSG("arguments", argumentsGetter), + JS_DEBUG_PSG("callee", calleeGetter), + JS_DEBUG_PSG("constructing", constructingGetter), + JS_DEBUG_PSG("environment", environmentGetter), + JS_DEBUG_PSG("generator", generatorGetter), + JS_DEBUG_PSG("live", liveGetter), + JS_DEBUG_PSG("onStack", onStackGetter), + JS_DEBUG_PSG("terminated", terminatedGetter), + JS_DEBUG_PSG("offset", offsetGetter), + JS_DEBUG_PSG("older", olderGetter), + JS_DEBUG_PSG("olderSavedFrame", olderSavedFrameGetter), + JS_DEBUG_PSG("script", getScript), + JS_DEBUG_PSG("this", thisGetter), + JS_DEBUG_PSG("asyncPromise", asyncPromiseGetter), + JS_DEBUG_PSG("type", typeGetter), + JS_DEBUG_PSG("implementation", implementationGetter), + JS_DEBUG_PSGS("onStep", onStepGetter, onStepSetter), + JS_DEBUG_PSGS("onPop", onPopGetter, onPopSetter), + JS_PS_END}; + +const JSFunctionSpec DebuggerFrame::methods_[] = { + JS_DEBUG_FN("eval", evalMethod, 1), + JS_DEBUG_FN("evalWithBindings", evalWithBindingsMethod, 1), JS_FS_END}; + +JSObject* js::IdVectorToArray(JSContext* cx, HandleIdVector ids) { + if (MOZ_UNLIKELY(ids.length() > UINT32_MAX)) { + ReportAllocationOverflow(cx); + return nullptr; + } + + Rooted<ArrayObject*> arr(cx, NewDenseFullyAllocatedArray(cx, ids.length())); + if (!arr) { + return nullptr; + } + + arr->ensureDenseInitializedLength(0, ids.length()); + + for (size_t i = 0, len = ids.length(); i < len; i++) { + jsid id = ids[i]; + Value v; + if (id.isInt()) { + JSString* str = Int32ToString<CanGC>(cx, id.toInt()); + if (!str) { + return nullptr; + } + v = StringValue(str); + } else if (id.isAtom()) { + v = StringValue(id.toAtom()); + } else if (id.isSymbol()) { + v = SymbolValue(id.toSymbol()); + } else { + MOZ_CRASH("IdVector must contain only string, int, and Symbol jsids"); + } + + arr->initDenseElement(i, v); + } + + return arr; +} diff --git a/js/src/debugger/Frame.h b/js/src/debugger/Frame.h new file mode 100644 index 0000000000..675accbcf7 --- /dev/null +++ b/js/src/debugger/Frame.h @@ -0,0 +1,300 @@ +/* -*- 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/. */ + +#ifndef debugger_Frame_h +#define debugger_Frame_h + +#include "mozilla/Maybe.h" // for Maybe +#include "mozilla/Range.h" // for Range +#include "mozilla/Result.h" // for Result + +#include <stddef.h> // for size_t + +#include "NamespaceImports.h" // for Value, MutableHandleValue, HandleObject +#include "debugger/DebugAPI.h" // for ResumeMode +#include "debugger/Debugger.h" // for ResumeMode, Handler, Debugger +#include "gc/Barrier.h" // for HeapPtr +#include "vm/FrameIter.h" // for FrameIter +#include "vm/JSObject.h" // for JSObject +#include "vm/NativeObject.h" // for NativeObject +#include "vm/Stack.h" // for AbstractFramePtr + +struct JS_PUBLIC_API JSContext; + +namespace js { + +class AbstractGeneratorObject; +class GlobalObject; + +/* + * An OnStepHandler represents a handler function that is called when a small + * amount of progress is made in a frame. + */ +struct OnStepHandler : Handler { + /* + * If we have made a small amount of progress in a frame, this method is + * called with the frame as argument. If succesful, this method should + * return true, with `resumeMode` and `vp` set to a resumption value + * specifiying how execution should continue. + */ + virtual bool onStep(JSContext* cx, Handle<DebuggerFrame*> frame, + ResumeMode& resumeMode, MutableHandleValue vp) = 0; +}; + +class ScriptedOnStepHandler final : public OnStepHandler { + public: + explicit ScriptedOnStepHandler(JSObject* object); + virtual JSObject* object() const override; + virtual void hold(JSObject* owner) override; + virtual void drop(JS::GCContext* gcx, JSObject* owner) override; + virtual void trace(JSTracer* tracer) override; + virtual size_t allocSize() const override; + virtual bool onStep(JSContext* cx, Handle<DebuggerFrame*> frame, + ResumeMode& resumeMode, MutableHandleValue vp) override; + + private: + const HeapPtr<JSObject*> object_; +}; + +/* + * An OnPopHandler represents a handler function that is called just before a + * frame is popped. + */ +struct OnPopHandler : Handler { + /* + * The given `frame` is about to be popped; `completion` explains why. + * + * When this method returns true, it must set `resumeMode` and `vp` to a + * resumption value specifying how execution should continue. + * + * When this method returns false, it should set an exception on `cx`. + */ + virtual bool onPop(JSContext* cx, Handle<DebuggerFrame*> frame, + const Completion& completion, ResumeMode& resumeMode, + MutableHandleValue vp) = 0; +}; + +class ScriptedOnPopHandler final : public OnPopHandler { + public: + explicit ScriptedOnPopHandler(JSObject* object); + virtual JSObject* object() const override; + virtual void hold(JSObject* owner) override; + virtual void drop(JS::GCContext* gcx, JSObject* owner) override; + virtual void trace(JSTracer* tracer) override; + virtual size_t allocSize() const override; + virtual bool onPop(JSContext* cx, Handle<DebuggerFrame*> frame, + const Completion& completion, ResumeMode& resumeMode, + MutableHandleValue vp) override; + + private: + const HeapPtr<JSObject*> object_; +}; + +enum class DebuggerFrameType { Eval, Global, Call, Module, WasmCall }; + +enum class DebuggerFrameImplementation { Interpreter, Baseline, Ion, Wasm }; + +class DebuggerArguments : public NativeObject { + public: + static const JSClass class_; + + static DebuggerArguments* create(JSContext* cx, HandleObject proto, + Handle<DebuggerFrame*> frame); + + private: + enum { FRAME_SLOT }; + + static const unsigned RESERVED_SLOTS = 1; +}; + +class DebuggerFrame : public NativeObject { + friend class DebuggerArguments; + friend class ScriptedOnStepHandler; + friend class ScriptedOnPopHandler; + + public: + static const JSClass class_; + + enum { + FRAME_ITER_SLOT = 0, + OWNER_SLOT, + ARGUMENTS_SLOT, + ONSTEP_HANDLER_SLOT, + ONPOP_HANDLER_SLOT, + + // If this is a frame for a generator call, and the generator object has + // been created (which doesn't happen until after default argument + // evaluation and destructuring), then this is a PrivateValue pointing to a + // GeneratorInfo struct that points to the call's AbstractGeneratorObject. + // This allows us to implement Debugger.Frame methods even while the call is + // suspended, and we have no FrameIter::Data. + // + // While Debugger::generatorFrames maps an AbstractGeneratorObject to its + // Debugger.Frame, this link represents the reverse relation, from a + // Debugger.Frame to its generator object. This slot is set if and only if + // there is a corresponding entry in generatorFrames. + GENERATOR_INFO_SLOT, + + RESERVED_SLOTS, + }; + + void trace(JSTracer* trc); + + static NativeObject* initClass(JSContext* cx, Handle<GlobalObject*> global, + HandleObject dbgCtor); + static DebuggerFrame* create(JSContext* cx, HandleObject proto, + Handle<NativeObject*> debugger, + const FrameIter* maybeIter, + Handle<AbstractGeneratorObject*> maybeGenerator); + + [[nodiscard]] static bool getArguments( + JSContext* cx, Handle<DebuggerFrame*> frame, + MutableHandle<DebuggerArguments*> result); + [[nodiscard]] static bool getCallee(JSContext* cx, + Handle<DebuggerFrame*> frame, + MutableHandle<DebuggerObject*> result); + [[nodiscard]] static bool getIsConstructing(JSContext* cx, + Handle<DebuggerFrame*> frame, + bool& result); + [[nodiscard]] static bool getEnvironment( + JSContext* cx, Handle<DebuggerFrame*> frame, + MutableHandle<DebuggerEnvironment*> result); + [[nodiscard]] static bool getOffset(JSContext* cx, + Handle<DebuggerFrame*> frame, + size_t& result); + [[nodiscard]] static bool getOlder(JSContext* cx, + Handle<DebuggerFrame*> frame, + MutableHandle<DebuggerFrame*> result); + [[nodiscard]] static bool getAsyncPromise( + JSContext* cx, Handle<DebuggerFrame*> frame, + MutableHandle<DebuggerObject*> result); + [[nodiscard]] static bool getOlderSavedFrame( + JSContext* cx, Handle<DebuggerFrame*> frame, + MutableHandle<SavedFrame*> result); + [[nodiscard]] static bool getThis(JSContext* cx, Handle<DebuggerFrame*> frame, + MutableHandleValue result); + static DebuggerFrameType getType(Handle<DebuggerFrame*> frame); + static DebuggerFrameImplementation getImplementation( + Handle<DebuggerFrame*> frame); + [[nodiscard]] static bool setOnStepHandler(JSContext* cx, + Handle<DebuggerFrame*> frame, + UniquePtr<OnStepHandler> handler); + + [[nodiscard]] static JS::Result<Completion> eval( + JSContext* cx, Handle<DebuggerFrame*> frame, + mozilla::Range<const char16_t> chars, HandleObject bindings, + const EvalOptions& options); + + [[nodiscard]] static DebuggerFrame* check(JSContext* cx, HandleValue thisv); + + bool isOnStack() const; + + bool isSuspended() const; + + OnStepHandler* onStepHandler() const; + OnPopHandler* onPopHandler() const; + void setOnPopHandler(JSContext* cx, OnPopHandler* handler); + + inline bool hasGeneratorInfo() const; + + // If hasGeneratorInfo(), return an direct cross-compartment reference to this + // Debugger.Frame's generator object. + AbstractGeneratorObject& unwrappedGenerator() const; + +#ifdef DEBUG + JSScript* generatorScript() const; +#endif + + /* + * Associate the generator object genObj with this Debugger.Frame. This + * association allows the Debugger.Frame to track the generator's execution + * across suspensions and resumptions, and to implement some methods even + * while the generator is suspended. + * + * The context `cx` must be in the Debugger.Frame's realm, and `genObj` must + * be in a debuggee realm. + * + * Technically, the generator activation need not actually be on the stack + * right now; it's okay to call this method on a Debugger.Frame that has no + * ScriptFrameIter::Data at present. However, this function has no way to + * verify that genObj really is the generator associated with the call for + * which this Debugger.Frame was originally created, so it's best to make the + * association while the call is on the stack, and the relationships are easy + * to discern. + */ + [[nodiscard]] static bool setGeneratorInfo( + JSContext* cx, Handle<DebuggerFrame*> frame, + Handle<AbstractGeneratorObject*> genObj); + + /* + * Undo the effects of a prior call to setGenerator. + * + * If provided, owner must be the Debugger to which this Debugger.Frame + * belongs; remove this frame's entry from its generatorFrames map, and clean + * up its cross-compartment wrapper table entry. The owner must be passed + * unless this method is being called from the Debugger.Frame's finalizer. (In + * that case, the owner is not reliably available, and is not actually + * necessary.) + * + * If maybeGeneratorFramesEnum is non-null, use it to remove this frame's + * entry from the Debugger's generatorFrames weak map. In this case, this + * function will not otherwise disturb generatorFrames. Passing the enum + * allows this function to be used while iterating over generatorFrames. + */ + void clearGeneratorInfo(JS::GCContext* gcx); + + /* + * Called after a generator/async frame is resumed, before exposing this + * Debugger.Frame object to any hooks. + */ + bool resume(const FrameIter& iter); + + bool hasAnyHooks() const; + + Debugger* owner() const; + + private: + static const JSClassOps classOps_; + + static const JSPropertySpec properties_[]; + static const JSFunctionSpec methods_[]; + + static void finalize(JS::GCContext* gcx, JSObject* obj); + + static AbstractFramePtr getReferent(Handle<DebuggerFrame*> frame); + [[nodiscard]] static bool requireScriptReferent(JSContext* cx, + Handle<DebuggerFrame*> frame); + + [[nodiscard]] static bool construct(JSContext* cx, unsigned argc, Value* vp); + + struct CallData; + + [[nodiscard]] bool incrementStepperCounter(JSContext* cx, + AbstractFramePtr referent); + [[nodiscard]] bool incrementStepperCounter(JSContext* cx, + HandleScript script); + void decrementStepperCounter(JS::GCContext* gcx, JSScript* script); + void decrementStepperCounter(JS::GCContext* gcx, AbstractFramePtr referent); + + FrameIter::Data* frameIterData() const; + void setFrameIterData(FrameIter::Data*); + void freeFrameIterData(JS::GCContext* gcx); + + public: + FrameIter getFrameIter(JSContext* cx); + + void terminate(JS::GCContext* gcx, AbstractFramePtr frame); + void suspend(JS::GCContext* gcx); + + [[nodiscard]] bool replaceFrameIterData(JSContext* cx, const FrameIter&); + + class GeneratorInfo; + inline GeneratorInfo* generatorInfo() const; +}; + +} /* namespace js */ + +#endif /* debugger_Frame_h */ diff --git a/js/src/debugger/NoExecute.cpp b/js/src/debugger/NoExecute.cpp new file mode 100644 index 0000000000..a881ea3a77 --- /dev/null +++ b/js/src/debugger/NoExecute.cpp @@ -0,0 +1,90 @@ +/* -*- 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/NoExecute.h" + +#include "mozilla/Sprintf.h" // for SprintfLiteral + +#include <stdio.h> // for fprintf, stdout + +#include "debugger/Debugger.h" // for Debugger +#include "js/friend/DumpFunctions.h" // for DumpBacktrace +#include "js/friend/ErrorMessages.h" // for GetErrorMessage, JSMSG_DEBUGGEE_WOULD_RUN +#include "js/Promise.h" // for AutoDebuggerJobQueueInterruption +#include "js/RootingAPI.h" // for Handle +#include "vm/JSContext.h" // for ProtectedDataContextArg, JSContext +#include "vm/JSScript.h" // for JSScript +#include "vm/Realm.h" // for AutoRealm, Realm +#include "vm/Warnings.h" // for WarnNumberLatin1 + +#include "vm/Realm-inl.h" // for AutoRealm::AutoRealm + +using namespace js; + +EnterDebuggeeNoExecute::EnterDebuggeeNoExecute( + JSContext* cx, Debugger& dbg, + const JS::AutoDebuggerJobQueueInterruption& adjqiProof) + : dbg_(dbg), unlocked_(nullptr), reported_(false) { + MOZ_ASSERT(adjqiProof.initialized()); + stack_ = &cx->noExecuteDebuggerTop.ref(); + prev_ = *stack_; + *stack_ = this; +} + +#ifdef DEBUG +/* static */ +bool EnterDebuggeeNoExecute::isLockedInStack(JSContext* cx, Debugger& dbg) { + for (EnterDebuggeeNoExecute* it = cx->noExecuteDebuggerTop; it; + it = it->prev_) { + if (&it->debugger() == &dbg) { + return !it->unlocked_; + } + } + return false; +} +#endif + +/* static */ +EnterDebuggeeNoExecute* EnterDebuggeeNoExecute::findInStack(JSContext* cx) { + Realm* debuggee = cx->realm(); + for (EnterDebuggeeNoExecute* it = cx->noExecuteDebuggerTop; it; + it = it->prev_) { + Debugger& dbg = it->debugger(); + if (!it->unlocked_ && dbg.observesGlobal(debuggee->maybeGlobal())) { + return it; + } + } + return nullptr; +} + +/* static */ +bool EnterDebuggeeNoExecute::reportIfFoundInStack(JSContext* cx, + HandleScript script) { + if (EnterDebuggeeNoExecute* nx = findInStack(cx)) { + bool warning = !cx->options().throwOnDebuggeeWouldRun(); + if (!warning || !nx->reported_) { + AutoRealm ar(cx, nx->debugger().toJSObject()); + nx->reported_ = true; + if (cx->options().dumpStackOnDebuggeeWouldRun()) { + fprintf(stdout, "Dumping stack for DebuggeeWouldRun:\n"); + DumpBacktrace(cx); + } + const char* filename = script->filename() ? script->filename() : "(none)"; + char linenoStr[15]; + SprintfLiteral(linenoStr, "%u", script->lineno()); + // FIXME: filename should be UTF-8 (bug 987069). + if (warning) { + return WarnNumberLatin1(cx, JSMSG_DEBUGGEE_WOULD_RUN, filename, + linenoStr); + } + + JS_ReportErrorNumberLatin1(cx, GetErrorMessage, nullptr, + JSMSG_DEBUGGEE_WOULD_RUN, filename, linenoStr); + return false; + } + } + return true; +} diff --git a/js/src/debugger/NoExecute.h b/js/src/debugger/NoExecute.h new file mode 100644 index 0000000000..6c9fa3374d --- /dev/null +++ b/js/src/debugger/NoExecute.h @@ -0,0 +1,94 @@ +/* -*- 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/. */ + +#ifndef debugger_NoExecute_h +#define debugger_NoExecute_h + +#include "mozilla/Assertions.h" // for AssertionConditionType, MOZ_ASSERT +#include "mozilla/Attributes.h" // for MOZ_RAII + +#include "NamespaceImports.h" // for HandleScript +#include "js/Promise.h" // for JS::AutoDebuggerJobQueueInterruption + +namespace js { + +class Debugger; +class LeaveDebuggeeNoExecute; + +// Prevents all the debuggeee compartments of a given Debugger from executing +// scripts. Attempts to run script will throw an +// instance of Debugger.DebuggeeWouldRun from the topmost locked Debugger's +// compartment. +class MOZ_RAII EnterDebuggeeNoExecute { + friend class LeaveDebuggeeNoExecute; + + Debugger& dbg_; + EnterDebuggeeNoExecute** stack_; + EnterDebuggeeNoExecute* prev_; + + // Non-nullptr when unlocked temporarily by a LeaveDebuggeeNoExecute. + LeaveDebuggeeNoExecute* unlocked_; + + // When DebuggeeWouldRun is a warning instead of an error, whether we've + // reported a warning already. + bool reported_; + + public: + // Mark execution in dbg's debuggees as forbidden, for the lifetime of this + // object. Require an AutoDebuggerJobQueueInterruption in scope. + explicit EnterDebuggeeNoExecute( + JSContext* cx, Debugger& dbg, + const JS::AutoDebuggerJobQueueInterruption& adjqiProof); + + ~EnterDebuggeeNoExecute() { + MOZ_ASSERT(*stack_ == this); + *stack_ = prev_; + } + + Debugger& debugger() const { return dbg_; } + +#ifdef DEBUG + static bool isLockedInStack(JSContext* cx, Debugger& dbg); +#endif + + // Given a JSContext entered into a debuggee realm, find the lock + // that locks it. Returns nullptr if not found. + static EnterDebuggeeNoExecute* findInStack(JSContext* cx); + + // Given a JSContext entered into a debuggee compartment, report a + // warning or an error if there is a lock that locks it. + static bool reportIfFoundInStack(JSContext* cx, HandleScript script); +}; + +// Given a JSContext entered into a debuggee compartment, if it is in +// an NX section, unlock the topmost EnterDebuggeeNoExecute instance. +// +// Does nothing if debuggee is not in an NX section. For example, this +// situation arises when invocation functions are called without entering +// Debugger code, e.g., calling D.O.p.executeInGlobal or D.O.p.apply. +class MOZ_RAII LeaveDebuggeeNoExecute { + EnterDebuggeeNoExecute* prevLocked_; + + public: + explicit LeaveDebuggeeNoExecute(JSContext* cx) + : prevLocked_(EnterDebuggeeNoExecute::findInStack(cx)) { + if (prevLocked_) { + MOZ_ASSERT(!prevLocked_->unlocked_); + prevLocked_->unlocked_ = this; + } + } + + ~LeaveDebuggeeNoExecute() { + if (prevLocked_) { + MOZ_ASSERT(prevLocked_->unlocked_ == this); + prevLocked_->unlocked_ = nullptr; + } + } +}; + +} /* namespace js */ + +#endif /* debugger_NoExecute_h */ diff --git a/js/src/debugger/Object-inl.h b/js/src/debugger/Object-inl.h new file mode 100644 index 0000000000..927f6603fa --- /dev/null +++ b/js/src/debugger/Object-inl.h @@ -0,0 +1,41 @@ +/* -*- 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/. */ + +#ifndef debugger_Object_inl_h +#define debugger_Object_inl_h + +#include "debugger/Object.h" // for DebuggerObject + +#include "mozilla/Assertions.h" // for AssertionConditionType, MOZ_ASSERT + +#include "NamespaceImports.h" // for Value + +#include "debugger/Debugger.h" // for Debugger +#include "js/Wrapper.h" // for CheckedUnwrapStatic +#include "vm/JSObject.h" // for JSObject +#include "vm/PromiseObject.h" // for js::PromiseObject + +#include "debugger/Debugger-inl.h" // for Debugger::fromJSObject + +inline js::Debugger* js::DebuggerObject::owner() const { + JSObject* dbgobj = &getReservedSlot(OWNER_SLOT).toObject(); + return Debugger::fromJSObject(dbgobj); +} + +inline js::PromiseObject* js::DebuggerObject::promise() const { + MOZ_ASSERT(isPromise()); + + JSObject* referent = this->referent(); + if (IsCrossCompartmentWrapper(referent)) { + // We know we have a Promise here, so CheckedUnwrapStatic is fine. + referent = CheckedUnwrapStatic(referent); + MOZ_ASSERT(referent); + } + + return &referent->as<PromiseObject>(); +} + +#endif /* debugger_Object_inl_h */ diff --git a/js/src/debugger/Object.cpp b/js/src/debugger/Object.cpp new file mode 100644 index 0000000000..b6d2c54e7a --- /dev/null +++ b/js/src/debugger/Object.cpp @@ -0,0 +1,2689 @@ +/* -*- 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/Object-inl.h" + +#include "mozilla/Maybe.h" // for Maybe, Nothing, Some +#include "mozilla/Range.h" // for Range +#include "mozilla/Result.h" // for Result +#include "mozilla/Vector.h" // for Vector + +#include <algorithm> +#include <string.h> // for size_t, strlen +#include <type_traits> // for remove_reference<>::type +#include <utility> // for move + +#include "jsapi.h" // for CallArgs, RootedObject, Rooted + +#include "builtin/Array.h" // for NewDenseCopiedArray +#include "builtin/Promise.h" // for PromiseReactionRecordBuilder +#include "debugger/Debugger.h" // for Completion, Debugger +#include "debugger/Frame.h" // for DebuggerFrame +#include "debugger/NoExecute.h" // for LeaveDebuggeeNoExecute +#include "debugger/Script.h" // for DebuggerScript +#include "debugger/Source.h" // for DebuggerSource +#include "gc/Tracer.h" // for TraceManuallyBarrieredCrossCompartmentEdge +#include "js/CompilationAndEvaluation.h" // for Compile +#include "js/Conversions.h" // for ToObject +#include "js/friend/ErrorMessages.h" // for GetErrorMessage, JSMSG_* +#include "js/friend/WindowProxy.h" // for IsWindow, IsWindowProxy, ToWindowIfWindowProxy +#include "js/HeapAPI.h" // for IsInsideNursery +#include "js/Promise.h" // for PromiseState +#include "js/PropertyAndElement.h" // for JS_GetProperty +#include "js/Proxy.h" // for PropertyDescriptor +#include "js/SourceText.h" // for SourceText +#include "js/StableStringChars.h" // for AutoStableStringChars +#include "js/String.h" // for JS::StringHasLatin1Chars +#include "proxy/ScriptedProxyHandler.h" // for ScriptedProxyHandler +#include "vm/ArgumentsObject.h" // for ARGS_LENGTH_MAX +#include "vm/ArrayObject.h" // for ArrayObject +#include "vm/AsyncFunction.h" // for AsyncGeneratorObject +#include "vm/AsyncIteration.h" // for AsyncFunctionGeneratorObject +#include "vm/BytecodeUtil.h" // for JSDVG_SEARCH_STACK +#include "vm/Compartment.h" // for Compartment +#include "vm/EnvironmentObject.h" // for GetDebugEnvironmentForFunction +#include "vm/ErrorObject.h" // for JSObject::is, ErrorObject +#include "vm/GeneratorObject.h" // for AbstractGeneratorObject +#include "vm/GlobalObject.h" // for JSObject::is, GlobalObject +#include "vm/Interpreter.h" // for Call +#include "vm/JSAtom.h" // for Atomize +#include "vm/JSContext.h" // for JSContext, ReportValueError +#include "vm/JSFunction.h" // for JSFunction +#include "vm/JSObject.h" // for GenericObject, NewObjectKind +#include "vm/JSScript.h" // for JSScript +#include "vm/NativeObject.h" // for NativeObject, JSObject::is +#include "vm/ObjectOperations.h" // for DefineProperty +#include "vm/PlainObject.h" // for js::PlainObject +#include "vm/PromiseObject.h" // for js::PromiseObject +#include "vm/Realm.h" // for AutoRealm, ErrorCopier, Realm +#include "vm/Runtime.h" // for JSAtomState +#include "vm/SavedFrame.h" // for SavedFrame +#include "vm/Scope.h" // for PositionalFormalParameterIter +#include "vm/SelfHosting.h" // for GetClonedSelfHostedFunctionName +#include "vm/Shape.h" // for Shape +#include "vm/Stack.h" // for InvokeArgs +#include "vm/StringType.h" // for JSAtom, PropertyName +#include "vm/WellKnownAtom.h" // for js_apply_str +#include "vm/WrapperObject.h" // for JSObject::is, WrapperObject + +#include "vm/Compartment-inl.h" // for Compartment::wrap +#include "vm/JSObject-inl.h" // for GetObjectClassName, InitClass, NewObjectWithGivenProtoAndKind, ToPropertyKey +#include "vm/NativeObject-inl.h" // for NativeObject::global +#include "vm/ObjectOperations-inl.h" // for DeleteProperty, GetProperty +#include "vm/Realm-inl.h" // for AutoRealm::AutoRealm + +using namespace js; + +using JS::AutoStableStringChars; +using mozilla::Maybe; +using mozilla::Nothing; +using mozilla::Some; + +const JSClassOps DebuggerObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + nullptr, // finalize + nullptr, // call + nullptr, // construct + CallTraceMethod<DebuggerObject>, // trace +}; + +const JSClass DebuggerObject::class_ = { + "Object", JSCLASS_HAS_RESERVED_SLOTS(RESERVED_SLOTS), &classOps_}; + +void DebuggerObject::trace(JSTracer* trc) { + // There is a barrier on private pointers, so the Unbarriered marking + // is okay. + if (JSObject* referent = maybeReferent()) { + TraceManuallyBarrieredCrossCompartmentEdge(trc, this, &referent, + "Debugger.Object referent"); + if (referent != maybeReferent()) { + setReservedSlotGCThingAsPrivateUnbarriered(OBJECT_SLOT, referent); + } + } +} + +static DebuggerObject* DebuggerObject_checkThis(JSContext* cx, + const CallArgs& args) { + JSObject* thisobj = RequireObject(cx, args.thisv()); + if (!thisobj) { + return nullptr; + } + if (!thisobj->is<DebuggerObject>()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INCOMPATIBLE_PROTO, "Debugger.Object", + "method", thisobj->getClass()->name); + return nullptr; + } + + return &thisobj->as<DebuggerObject>(); +} + +/* static */ +bool DebuggerObject::construct(JSContext* cx, unsigned argc, Value* vp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, + "Debugger.Object"); + return false; +} + +struct MOZ_STACK_CLASS DebuggerObject::CallData { + JSContext* cx; + const CallArgs& args; + + Handle<DebuggerObject*> object; + RootedObject referent; + + CallData(JSContext* cx, const CallArgs& args, Handle<DebuggerObject*> obj) + : cx(cx), args(args), object(obj), referent(cx, obj->referent()) {} + + // JSNative properties + bool callableGetter(); + bool isBoundFunctionGetter(); + bool isArrowFunctionGetter(); + bool isAsyncFunctionGetter(); + bool isClassConstructorGetter(); + bool isGeneratorFunctionGetter(); + bool protoGetter(); + bool classGetter(); + bool nameGetter(); + bool displayNameGetter(); + bool parameterNamesGetter(); + bool scriptGetter(); + bool environmentGetter(); + bool boundTargetFunctionGetter(); + bool boundThisGetter(); + bool boundArgumentsGetter(); + bool allocationSiteGetter(); + bool isErrorGetter(); + bool errorMessageNameGetter(); + bool errorNotesGetter(); + bool errorLineNumberGetter(); + bool errorColumnNumberGetter(); + bool isProxyGetter(); + bool proxyTargetGetter(); + bool proxyHandlerGetter(); + bool isPromiseGetter(); + bool promiseStateGetter(); + bool promiseValueGetter(); + bool promiseReasonGetter(); + bool promiseLifetimeGetter(); + bool promiseTimeToResolutionGetter(); + bool promiseAllocationSiteGetter(); + bool promiseResolutionSiteGetter(); + bool promiseIDGetter(); + bool promiseDependentPromisesGetter(); + + // JSNative methods + bool isExtensibleMethod(); + bool isSealedMethod(); + bool isFrozenMethod(); + bool getPropertyMethod(); + bool setPropertyMethod(); + bool getOwnPropertyNamesMethod(); + bool getOwnPropertyNamesLengthMethod(); + bool getOwnPropertySymbolsMethod(); + bool getOwnPrivatePropertiesMethod(); + bool getOwnPropertyDescriptorMethod(); + bool preventExtensionsMethod(); + bool sealMethod(); + bool freezeMethod(); + bool definePropertyMethod(); + bool definePropertiesMethod(); + bool deletePropertyMethod(); + bool callMethod(); + bool applyMethod(); + bool asEnvironmentMethod(); + bool forceLexicalInitializationByNameMethod(); + bool executeInGlobalMethod(); + bool executeInGlobalWithBindingsMethod(); + bool createSource(); + bool makeDebuggeeValueMethod(); + bool makeDebuggeeNativeFunctionMethod(); + bool isSameNativeMethod(); + bool unsafeDereferenceMethod(); + bool unwrapMethod(); + bool getPromiseReactionsMethod(); + + using Method = bool (CallData::*)(); + + template <Method MyMethod> + static bool ToNative(JSContext* cx, unsigned argc, Value* vp); +}; + +template <DebuggerObject::CallData::Method MyMethod> +/* static */ +bool DebuggerObject::CallData::ToNative(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<DebuggerObject*> obj(cx, DebuggerObject_checkThis(cx, args)); + if (!obj) { + return false; + } + + CallData data(cx, args, obj); + return (data.*MyMethod)(); +} + +bool DebuggerObject::CallData::callableGetter() { + args.rval().setBoolean(object->isCallable()); + return true; +} + +bool DebuggerObject::CallData::isBoundFunctionGetter() { + if (!object->isDebuggeeFunction()) { + args.rval().setUndefined(); + return true; + } + + args.rval().setBoolean(object->isBoundFunction()); + return true; +} + +bool DebuggerObject::CallData::isArrowFunctionGetter() { + if (!object->isDebuggeeFunction()) { + args.rval().setUndefined(); + return true; + } + + args.rval().setBoolean(object->isArrowFunction()); + return true; +} + +bool DebuggerObject::CallData::isAsyncFunctionGetter() { + if (!object->isDebuggeeFunction()) { + args.rval().setUndefined(); + return true; + } + + args.rval().setBoolean(object->isAsyncFunction()); + return true; +} + +bool DebuggerObject::CallData::isGeneratorFunctionGetter() { + if (!object->isDebuggeeFunction()) { + args.rval().setUndefined(); + return true; + } + + args.rval().setBoolean(object->isGeneratorFunction()); + return true; +} + +bool DebuggerObject::CallData::isClassConstructorGetter() { + if (!object->isDebuggeeFunction()) { + args.rval().setUndefined(); + return true; + } + + args.rval().setBoolean(object->isClassConstructor()); + return true; +} + +bool DebuggerObject::CallData::protoGetter() { + Rooted<DebuggerObject*> result(cx); + if (!DebuggerObject::getPrototypeOf(cx, object, &result)) { + return false; + } + + args.rval().setObjectOrNull(result); + return true; +} + +bool DebuggerObject::CallData::classGetter() { + RootedString result(cx); + if (!DebuggerObject::getClassName(cx, object, &result)) { + return false; + } + + args.rval().setString(result); + return true; +} + +bool DebuggerObject::CallData::nameGetter() { + if (!object->isFunction()) { + args.rval().setUndefined(); + return true; + } + + RootedString result(cx, object->name(cx)); + if (result) { + args.rval().setString(result); + } else { + args.rval().setUndefined(); + } + return true; +} + +bool DebuggerObject::CallData::displayNameGetter() { + if (!object->isFunction()) { + args.rval().setUndefined(); + return true; + } + + RootedString result(cx, object->displayName(cx)); + if (result) { + args.rval().setString(result); + } else { + args.rval().setUndefined(); + } + return true; +} + +bool DebuggerObject::CallData::parameterNamesGetter() { + if (!object->isDebuggeeFunction()) { + args.rval().setUndefined(); + return true; + } + + RootedFunction referent(cx, &object->referent()->as<JSFunction>()); + + ArrayObject* arr = GetFunctionParameterNamesArray(cx, referent); + if (!arr) { + return false; + } + + args.rval().setObject(*arr); + return true; +} + +bool DebuggerObject::CallData::scriptGetter() { + Debugger* dbg = object->owner(); + + if (!referent->is<JSFunction>()) { + args.rval().setUndefined(); + return true; + } + + RootedFunction fun(cx, &referent->as<JSFunction>()); + if (!IsInterpretedNonSelfHostedFunction(fun)) { + args.rval().setUndefined(); + return true; + } + + RootedScript script(cx, GetOrCreateFunctionScript(cx, fun)); + if (!script) { + return false; + } + + // Only hand out debuggee scripts. + if (!dbg->observesScript(script)) { + args.rval().setNull(); + return true; + } + + Rooted<DebuggerScript*> scriptObject(cx, dbg->wrapScript(cx, script)); + if (!scriptObject) { + return false; + } + + args.rval().setObject(*scriptObject); + return true; +} + +bool DebuggerObject::CallData::environmentGetter() { + Debugger* dbg = object->owner(); + + // Don't bother switching compartments just to check obj's type and get its + // env. + if (!referent->is<JSFunction>()) { + args.rval().setUndefined(); + return true; + } + + RootedFunction fun(cx, &referent->as<JSFunction>()); + if (!IsInterpretedNonSelfHostedFunction(fun)) { + args.rval().setUndefined(); + return true; + } + + // Only hand out environments of debuggee functions. + if (!dbg->observesGlobal(&fun->global())) { + args.rval().setNull(); + return true; + } + + Rooted<Env*> env(cx); + { + AutoRealm ar(cx, fun); + env = GetDebugEnvironmentForFunction(cx, fun); + if (!env) { + return false; + } + } + + return dbg->wrapEnvironment(cx, env, args.rval()); +} + +bool DebuggerObject::CallData::boundTargetFunctionGetter() { + if (!object->isDebuggeeFunction() || !object->isBoundFunction()) { + args.rval().setUndefined(); + return true; + } + + Rooted<DebuggerObject*> result(cx); + if (!DebuggerObject::getBoundTargetFunction(cx, object, &result)) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +bool DebuggerObject::CallData::boundThisGetter() { + if (!object->isDebuggeeFunction() || !object->isBoundFunction()) { + args.rval().setUndefined(); + return true; + } + + return DebuggerObject::getBoundThis(cx, object, args.rval()); +} + +bool DebuggerObject::CallData::boundArgumentsGetter() { + if (!object->isDebuggeeFunction() || !object->isBoundFunction()) { + args.rval().setUndefined(); + return true; + } + + Rooted<ValueVector> result(cx, ValueVector(cx)); + if (!DebuggerObject::getBoundArguments(cx, object, &result)) { + return false; + } + + RootedObject obj(cx, + NewDenseCopiedArray(cx, result.length(), result.begin())); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +bool DebuggerObject::CallData::allocationSiteGetter() { + RootedObject result(cx); + if (!DebuggerObject::getAllocationSite(cx, object, &result)) { + return false; + } + + args.rval().setObjectOrNull(result); + return true; +} + +// Returns the "name" field (see js/public/friend/ErrorNumbers.msg), which may +// be used as a unique identifier, for any error object with a JSErrorReport or +// undefined if the object has no JSErrorReport. +bool DebuggerObject::CallData::errorMessageNameGetter() { + RootedString result(cx); + if (!DebuggerObject::getErrorMessageName(cx, object, &result)) { + return false; + } + + if (result) { + args.rval().setString(result); + } else { + args.rval().setUndefined(); + } + return true; +} + +bool DebuggerObject::CallData::isErrorGetter() { + args.rval().setBoolean(object->isError()); + return true; +} + +bool DebuggerObject::CallData::errorNotesGetter() { + return DebuggerObject::getErrorNotes(cx, object, args.rval()); +} + +bool DebuggerObject::CallData::errorLineNumberGetter() { + return DebuggerObject::getErrorLineNumber(cx, object, args.rval()); +} + +bool DebuggerObject::CallData::errorColumnNumberGetter() { + return DebuggerObject::getErrorColumnNumber(cx, object, args.rval()); +} + +bool DebuggerObject::CallData::isProxyGetter() { + args.rval().setBoolean(object->isScriptedProxy()); + return true; +} + +bool DebuggerObject::CallData::proxyTargetGetter() { + if (!object->isScriptedProxy()) { + args.rval().setUndefined(); + return true; + } + + Rooted<DebuggerObject*> result(cx); + if (!DebuggerObject::getScriptedProxyTarget(cx, object, &result)) { + return false; + } + + args.rval().setObjectOrNull(result); + return true; +} + +bool DebuggerObject::CallData::proxyHandlerGetter() { + if (!object->isScriptedProxy()) { + args.rval().setUndefined(); + return true; + } + Rooted<DebuggerObject*> result(cx); + if (!DebuggerObject::getScriptedProxyHandler(cx, object, &result)) { + return false; + } + + args.rval().setObjectOrNull(result); + return true; +} + +bool DebuggerObject::CallData::isPromiseGetter() { + args.rval().setBoolean(object->isPromise()); + return true; +} + +bool DebuggerObject::CallData::promiseStateGetter() { + if (!DebuggerObject::requirePromise(cx, object)) { + return false; + } + + RootedValue result(cx); + switch (object->promiseState()) { + case JS::PromiseState::Pending: + result.setString(cx->names().pending); + break; + case JS::PromiseState::Fulfilled: + result.setString(cx->names().fulfilled); + break; + case JS::PromiseState::Rejected: + result.setString(cx->names().rejected); + break; + } + + args.rval().set(result); + return true; +} + +bool DebuggerObject::CallData::promiseValueGetter() { + if (!DebuggerObject::requirePromise(cx, object)) { + return false; + } + + if (object->promiseState() != JS::PromiseState::Fulfilled) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_PROMISE_NOT_FULFILLED); + return false; + } + + return DebuggerObject::getPromiseValue(cx, object, args.rval()); + ; +} + +bool DebuggerObject::CallData::promiseReasonGetter() { + if (!DebuggerObject::requirePromise(cx, object)) { + return false; + } + + if (object->promiseState() != JS::PromiseState::Rejected) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_PROMISE_NOT_REJECTED); + return false; + } + + return DebuggerObject::getPromiseReason(cx, object, args.rval()); +} + +bool DebuggerObject::CallData::promiseLifetimeGetter() { + if (!DebuggerObject::requirePromise(cx, object)) { + return false; + } + + args.rval().setNumber(object->promiseLifetime()); + return true; +} + +bool DebuggerObject::CallData::promiseTimeToResolutionGetter() { + if (!DebuggerObject::requirePromise(cx, object)) { + return false; + } + + if (object->promiseState() == JS::PromiseState::Pending) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_PROMISE_NOT_RESOLVED); + return false; + } + + args.rval().setNumber(object->promiseTimeToResolution()); + return true; +} + +static PromiseObject* EnsurePromise(JSContext* cx, HandleObject referent) { + // We only care about promises, so CheckedUnwrapStatic is OK. + RootedObject obj(cx, CheckedUnwrapStatic(referent)); + if (!obj) { + ReportAccessDenied(cx); + return nullptr; + } + if (!obj->is<PromiseObject>()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_EXPECTED_TYPE, "Debugger", "Promise", + obj->getClass()->name); + return nullptr; + } + return &obj->as<PromiseObject>(); +} + +bool DebuggerObject::CallData::promiseAllocationSiteGetter() { + Rooted<PromiseObject*> promise(cx, EnsurePromise(cx, referent)); + if (!promise) { + return false; + } + + RootedObject allocSite(cx, promise->allocationSite()); + if (!allocSite) { + args.rval().setNull(); + return true; + } + + if (!cx->compartment()->wrap(cx, &allocSite)) { + return false; + } + args.rval().set(ObjectValue(*allocSite)); + return true; +} + +bool DebuggerObject::CallData::promiseResolutionSiteGetter() { + Rooted<PromiseObject*> promise(cx, EnsurePromise(cx, referent)); + if (!promise) { + return false; + } + + if (promise->state() == JS::PromiseState::Pending) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_PROMISE_NOT_RESOLVED); + return false; + } + + RootedObject resolutionSite(cx, promise->resolutionSite()); + if (!resolutionSite) { + args.rval().setNull(); + return true; + } + + if (!cx->compartment()->wrap(cx, &resolutionSite)) { + return false; + } + args.rval().set(ObjectValue(*resolutionSite)); + return true; +} + +bool DebuggerObject::CallData::promiseIDGetter() { + Rooted<PromiseObject*> promise(cx, EnsurePromise(cx, referent)); + if (!promise) { + return false; + } + + args.rval().setNumber(double(promise->getID())); + return true; +} + +bool DebuggerObject::CallData::promiseDependentPromisesGetter() { + Debugger* dbg = object->owner(); + + Rooted<PromiseObject*> promise(cx, EnsurePromise(cx, referent)); + if (!promise) { + return false; + } + + Rooted<GCVector<Value>> values(cx, GCVector<Value>(cx)); + { + JSAutoRealm ar(cx, promise); + if (!promise->dependentPromises(cx, &values)) { + return false; + } + } + for (size_t i = 0; i < values.length(); i++) { + if (!dbg->wrapDebuggeeValue(cx, values[i])) { + return false; + } + } + Rooted<ArrayObject*> promises(cx); + if (values.length() == 0) { + promises = NewDenseEmptyArray(cx); + } else { + promises = NewDenseCopiedArray(cx, values.length(), values[0].address()); + } + if (!promises) { + return false; + } + args.rval().setObject(*promises); + return true; +} + +bool DebuggerObject::CallData::isExtensibleMethod() { + bool result; + if (!DebuggerObject::isExtensible(cx, object, result)) { + return false; + } + + args.rval().setBoolean(result); + return true; +} + +bool DebuggerObject::CallData::isSealedMethod() { + bool result; + if (!DebuggerObject::isSealed(cx, object, result)) { + return false; + } + + args.rval().setBoolean(result); + return true; +} + +bool DebuggerObject::CallData::isFrozenMethod() { + bool result; + if (!DebuggerObject::isFrozen(cx, object, result)) { + return false; + } + + args.rval().setBoolean(result); + return true; +} + +bool DebuggerObject::CallData::getOwnPropertyNamesMethod() { + RootedIdVector ids(cx); + if (!DebuggerObject::getOwnPropertyNames(cx, object, &ids)) { + return false; + } + + JSObject* obj = IdVectorToArray(cx, ids); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +bool DebuggerObject::CallData::getOwnPropertyNamesLengthMethod() { + size_t ownPropertiesLength; + if (!DebuggerObject::getOwnPropertyNamesLength(cx, object, + &ownPropertiesLength)) { + return false; + } + + args.rval().setNumber(ownPropertiesLength); + return true; +} + +bool DebuggerObject::CallData::getOwnPropertySymbolsMethod() { + RootedIdVector ids(cx); + if (!DebuggerObject::getOwnPropertySymbols(cx, object, &ids)) { + return false; + } + + JSObject* obj = IdVectorToArray(cx, ids); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +bool DebuggerObject::CallData::getOwnPrivatePropertiesMethod() { + RootedIdVector ids(cx); + if (!DebuggerObject::getOwnPrivateProperties(cx, object, &ids)) { + return false; + } + + JSObject* obj = IdVectorToArray(cx, ids); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +bool DebuggerObject::CallData::getOwnPropertyDescriptorMethod() { + RootedId id(cx); + if (!ToPropertyKey(cx, args.get(0), &id)) { + return false; + } + + Rooted<Maybe<PropertyDescriptor>> desc(cx); + if (!DebuggerObject::getOwnPropertyDescriptor(cx, object, id, &desc)) { + return false; + } + + return JS::FromPropertyDescriptor(cx, desc, args.rval()); +} + +bool DebuggerObject::CallData::preventExtensionsMethod() { + if (!DebuggerObject::preventExtensions(cx, object)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +bool DebuggerObject::CallData::sealMethod() { + if (!DebuggerObject::seal(cx, object)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +bool DebuggerObject::CallData::freezeMethod() { + if (!DebuggerObject::freeze(cx, object)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +bool DebuggerObject::CallData::definePropertyMethod() { + if (!args.requireAtLeast(cx, "Debugger.Object.defineProperty", 2)) { + return false; + } + + RootedId id(cx); + if (!ToPropertyKey(cx, args[0], &id)) { + return false; + } + + Rooted<PropertyDescriptor> desc(cx); + if (!ToPropertyDescriptor(cx, args[1], false, &desc)) { + return false; + } + + if (!DebuggerObject::defineProperty(cx, object, id, desc)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +bool DebuggerObject::CallData::definePropertiesMethod() { + if (!args.requireAtLeast(cx, "Debugger.Object.defineProperties", 1)) { + return false; + } + + RootedValue arg(cx, args[0]); + RootedObject props(cx, ToObject(cx, arg)); + if (!props) { + return false; + } + RootedIdVector ids(cx); + Rooted<PropertyDescriptorVector> descs(cx, PropertyDescriptorVector(cx)); + if (!ReadPropertyDescriptors(cx, props, false, &ids, &descs)) { + return false; + } + Rooted<IdVector> ids2(cx, IdVector(cx)); + if (!ids2.append(ids.begin(), ids.end())) { + return false; + } + + if (!DebuggerObject::defineProperties(cx, object, ids2, descs)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +/* + * This does a non-strict delete, as a matter of API design. The case where the + * property is non-configurable isn't necessarily exceptional here. + */ +bool DebuggerObject::CallData::deletePropertyMethod() { + RootedId id(cx); + if (!ToPropertyKey(cx, args.get(0), &id)) { + return false; + } + + ObjectOpResult result; + if (!DebuggerObject::deleteProperty(cx, object, id, result)) { + return false; + } + + args.rval().setBoolean(result.ok()); + return true; +} + +bool DebuggerObject::CallData::callMethod() { + RootedValue thisv(cx, args.get(0)); + + Rooted<ValueVector> nargs(cx, ValueVector(cx)); + if (args.length() >= 2) { + if (!nargs.growBy(args.length() - 1)) { + return false; + } + for (size_t i = 1; i < args.length(); ++i) { + nargs[i - 1].set(args[i]); + } + } + + Rooted<Maybe<Completion>> completion( + cx, DebuggerObject::call(cx, object, thisv, nargs)); + if (!completion.get()) { + return false; + } + + return completion->buildCompletionValue(cx, object->owner(), args.rval()); +} + +bool DebuggerObject::CallData::getPropertyMethod() { + Debugger* dbg = object->owner(); + + RootedId id(cx); + if (!ToPropertyKey(cx, args.get(0), &id)) { + return false; + } + + RootedValue receiver(cx, + args.length() < 2 ? ObjectValue(*object) : args.get(1)); + + Rooted<Completion> comp(cx); + JS_TRY_VAR_OR_RETURN_FALSE(cx, comp, getProperty(cx, object, id, receiver)); + return comp.get().buildCompletionValue(cx, dbg, args.rval()); +} + +bool DebuggerObject::CallData::setPropertyMethod() { + Debugger* dbg = object->owner(); + + RootedId id(cx); + if (!ToPropertyKey(cx, args.get(0), &id)) { + return false; + } + + RootedValue value(cx, args.get(1)); + + RootedValue receiver(cx, + args.length() < 3 ? ObjectValue(*object) : args.get(2)); + + Rooted<Completion> comp(cx); + JS_TRY_VAR_OR_RETURN_FALSE(cx, comp, + setProperty(cx, object, id, value, receiver)); + return comp.get().buildCompletionValue(cx, dbg, args.rval()); +} + +bool DebuggerObject::CallData::applyMethod() { + RootedValue thisv(cx, args.get(0)); + + Rooted<ValueVector> nargs(cx, ValueVector(cx)); + if (args.length() >= 2 && !args[1].isNullOrUndefined()) { + if (!args[1].isObject()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_APPLY_ARGS, js_apply_str); + return false; + } + + RootedObject argsobj(cx, &args[1].toObject()); + + uint64_t argc = 0; + if (!GetLengthProperty(cx, argsobj, &argc)) { + return false; + } + argc = std::min(argc, uint64_t(ARGS_LENGTH_MAX)); + + if (!nargs.growBy(argc) || !GetElements(cx, argsobj, argc, nargs.begin())) { + return false; + } + } + + Rooted<Maybe<Completion>> completion( + cx, DebuggerObject::call(cx, object, thisv, nargs)); + if (!completion.get()) { + return false; + } + + return completion->buildCompletionValue(cx, object->owner(), args.rval()); +} + +static void EnterDebuggeeObjectRealm(JSContext* cx, Maybe<AutoRealm>& ar, + JSObject* referent) { + // |referent| may be a cross-compartment wrapper and CCWs normally + // shouldn't be used with AutoRealm, but here we use an arbitrary realm for + // now because we don't really have another option. + ar.emplace(cx, referent->maybeCCWRealm()->maybeGlobal()); +} + +static bool RequireGlobalObject(JSContext* cx, HandleValue dbgobj, + HandleObject referent) { + RootedObject obj(cx, referent); + + if (!obj->is<GlobalObject>()) { + const char* isWrapper = ""; + const char* isWindowProxy = ""; + + // Help the poor programmer by pointing out wrappers around globals... + if (obj->is<WrapperObject>()) { + obj = js::UncheckedUnwrap(obj); + isWrapper = "a wrapper around "; + } + + // ... and WindowProxies around Windows. + if (IsWindowProxy(obj)) { + obj = ToWindowIfWindowProxy(obj); + isWindowProxy = "a WindowProxy referring to "; + } + + if (obj->is<GlobalObject>()) { + ReportValueError(cx, JSMSG_DEBUG_WRAPPER_IN_WAY, JSDVG_SEARCH_STACK, + dbgobj, nullptr, isWrapper, isWindowProxy); + } else { + ReportValueError(cx, JSMSG_DEBUG_BAD_REFERENT, JSDVG_SEARCH_STACK, dbgobj, + nullptr, "a global object"); + } + return false; + } + + return true; +} + +bool DebuggerObject::CallData::asEnvironmentMethod() { + Debugger* dbg = object->owner(); + + if (!RequireGlobalObject(cx, args.thisv(), referent)) { + return false; + } + + Rooted<Env*> env(cx); + { + AutoRealm ar(cx, referent); + env = GetDebugEnvironmentForGlobalLexicalEnvironment(cx); + if (!env) { + return false; + } + } + + return dbg->wrapEnvironment(cx, env, args.rval()); +} + +// Lookup a binding on the referent's global scope and change it to undefined +// if it is an uninitialized lexical, otherwise do nothing. The method's +// JavaScript return value is true _only_ when an uninitialized lexical has been +// altered, otherwise it is false. +bool DebuggerObject::CallData::forceLexicalInitializationByNameMethod() { + if (!args.requireAtLeast( + cx, "Debugger.Object.prototype.forceLexicalInitializationByName", + 1)) { + return false; + } + + if (!DebuggerObject::requireGlobal(cx, object)) { + return false; + } + + RootedId id(cx); + if (!ValueToIdentifier(cx, args[0], &id)) { + return false; + } + + bool result; + if (!DebuggerObject::forceLexicalInitializationByName(cx, object, id, + result)) { + return false; + } + + args.rval().setBoolean(result); + return true; +} + +bool DebuggerObject::CallData::executeInGlobalMethod() { + if (!args.requireAtLeast(cx, "Debugger.Object.prototype.executeInGlobal", + 1)) { + return false; + } + + if (!DebuggerObject::requireGlobal(cx, object)) { + return false; + } + + AutoStableStringChars stableChars(cx); + if (!ValueToStableChars(cx, "Debugger.Object.prototype.executeInGlobal", + args[0], stableChars)) { + return false; + } + mozilla::Range<const char16_t> chars = stableChars.twoByteRange(); + + EvalOptions options; + if (!ParseEvalOptions(cx, args.get(1), options)) { + return false; + } + + Rooted<Completion> comp(cx); + JS_TRY_VAR_OR_RETURN_FALSE( + cx, comp, + DebuggerObject::executeInGlobal(cx, object, chars, nullptr, options)); + return comp.get().buildCompletionValue(cx, object->owner(), args.rval()); +} + +bool DebuggerObject::CallData::executeInGlobalWithBindingsMethod() { + if (!args.requireAtLeast( + cx, "Debugger.Object.prototype.executeInGlobalWithBindings", 2)) { + return false; + } + + if (!DebuggerObject::requireGlobal(cx, object)) { + return false; + } + + AutoStableStringChars stableChars(cx); + if (!ValueToStableChars( + cx, "Debugger.Object.prototype.executeInGlobalWithBindings", args[0], + stableChars)) { + return false; + } + mozilla::Range<const char16_t> chars = stableChars.twoByteRange(); + + RootedObject bindings(cx, RequireObject(cx, args[1])); + if (!bindings) { + return false; + } + + EvalOptions options; + if (!ParseEvalOptions(cx, args.get(2), options)) { + return false; + } + + Rooted<Completion> comp(cx); + JS_TRY_VAR_OR_RETURN_FALSE( + cx, comp, + DebuggerObject::executeInGlobal(cx, object, chars, bindings, options)); + return comp.get().buildCompletionValue(cx, object->owner(), args.rval()); +} + +// Copy a narrow or wide string to a vector, appending a null terminator. +template <typename T> +static bool CopyStringToVector(JSContext* cx, JSString* str, Vector<T>& chars) { + JSLinearString* linear = str->ensureLinear(cx); + if (!linear) { + return false; + } + if (!chars.appendN(0, linear->length() + 1)) { + return false; + } + CopyChars(chars.begin(), *linear); + return true; +} + +bool DebuggerObject::CallData::createSource() { + if (!args.requireAtLeast(cx, "Debugger.Object.prototype.createSource", 1)) { + return false; + } + + if (!DebuggerObject::requireGlobal(cx, object)) { + return false; + } + + Debugger* dbg = object->owner(); + if (!dbg->isDebuggeeUnbarriered(referent->as<GlobalObject>().realm())) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_NOT_DEBUGGEE, "Debugger.Object", + "global"); + return false; + } + + RootedObject options(cx, ToObject(cx, args[0])); + if (!options) { + return false; + } + + RootedValue v(cx); + if (!JS_GetProperty(cx, options, "text", &v)) { + return false; + } + + RootedString text(cx, ToString<CanGC>(cx, v)); + if (!text) { + return false; + } + + if (!JS_GetProperty(cx, options, "url", &v)) { + return false; + } + + RootedString url(cx, ToString<CanGC>(cx, v)); + if (!url) { + return false; + } + + if (!JS_GetProperty(cx, options, "startLine", &v)) { + return false; + } + + uint32_t startLine; + if (!ToUint32(cx, v, &startLine)) { + return false; + } + + if (!JS_GetProperty(cx, options, "sourceMapURL", &v)) { + return false; + } + + RootedString sourceMapURL(cx); + if (!v.isUndefined()) { + sourceMapURL = ToString<CanGC>(cx, v); + if (!sourceMapURL) { + return false; + } + } + + if (!JS_GetProperty(cx, options, "isScriptElement", &v)) { + return false; + } + + bool isScriptElement = ToBoolean(v); + + JS::CompileOptions compileOptions(cx); + compileOptions.lineno = startLine; + + if (!JS::StringHasLatin1Chars(url)) { + JS_ReportErrorASCII(cx, "URL must be a narrow string"); + return false; + } + + Vector<Latin1Char> urlChars(cx); + if (!CopyStringToVector(cx, url, urlChars)) { + return false; + } + compileOptions.setFile((const char*)urlChars.begin()); + + Vector<char16_t> sourceMapURLChars(cx); + if (sourceMapURL) { + if (!CopyStringToVector(cx, sourceMapURL, sourceMapURLChars)) { + return false; + } + compileOptions.setSourceMapURL(sourceMapURLChars.begin()); + } + + if (isScriptElement) { + // The introduction type must be a statically allocated string. + compileOptions.setIntroductionType("inlineScript"); + } + + AutoStableStringChars linearChars(cx); + if (!linearChars.initTwoByte(cx, text)) { + return false; + } + JS::SourceText<char16_t> srcBuf; + if (!srcBuf.initMaybeBorrowed(cx, linearChars)) { + return false; + } + + RootedScript script(cx); + { + AutoRealm ar(cx, referent); + script = JS::Compile(cx, compileOptions, srcBuf); + if (!script) { + return false; + } + } + + Rooted<ScriptSourceObject*> sso(cx, script->sourceObject()); + RootedObject wrapped(cx, dbg->wrapSource(cx, sso)); + if (!wrapped) { + return false; + } + + args.rval().setObject(*wrapped); + return true; +} + +bool DebuggerObject::CallData::makeDebuggeeValueMethod() { + if (!args.requireAtLeast(cx, "Debugger.Object.prototype.makeDebuggeeValue", + 1)) { + return false; + } + + return DebuggerObject::makeDebuggeeValue(cx, object, args[0], args.rval()); +} + +bool DebuggerObject::CallData::makeDebuggeeNativeFunctionMethod() { + if (!args.requireAtLeast( + cx, "Debugger.Object.prototype.makeDebuggeeNativeFunction", 1)) { + return false; + } + + return DebuggerObject::makeDebuggeeNativeFunction(cx, object, args[0], + args.rval()); +} + +bool DebuggerObject::CallData::isSameNativeMethod() { + if (!args.requireAtLeast(cx, "Debugger.Object.prototype.isSameNative", 1)) { + return false; + } + + return DebuggerObject::isSameNative(cx, object, args[0], args.rval()); +} + +bool DebuggerObject::CallData::unsafeDereferenceMethod() { + RootedObject result(cx); + if (!DebuggerObject::unsafeDereference(cx, object, &result)) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +bool DebuggerObject::CallData::unwrapMethod() { + Rooted<DebuggerObject*> result(cx); + if (!DebuggerObject::unwrap(cx, object, &result)) { + return false; + } + + args.rval().setObjectOrNull(result); + return true; +} + +struct DebuggerObject::PromiseReactionRecordBuilder + : js::PromiseReactionRecordBuilder { + Debugger* dbg; + Handle<ArrayObject*> records; + + PromiseReactionRecordBuilder(Debugger* dbg, Handle<ArrayObject*> records) + : dbg(dbg), records(records) {} + + bool then(JSContext* cx, HandleObject resolve, HandleObject reject, + HandleObject result) override { + Rooted<PlainObject*> record(cx, NewPlainObject(cx)); + if (!record) { + return false; + } + + if (!setIfNotNull(cx, record, cx->names().resolve, resolve) || + !setIfNotNull(cx, record, cx->names().reject, reject) || + !setIfNotNull(cx, record, cx->names().result, result)) { + return false; + } + + return push(cx, record); + } + + bool direct(JSContext* cx, Handle<PromiseObject*> unwrappedPromise) override { + RootedValue v(cx, ObjectValue(*unwrappedPromise)); + return dbg->wrapDebuggeeValue(cx, &v) && push(cx, v); + } + + bool asyncFunction( + JSContext* cx, + Handle<AsyncFunctionGeneratorObject*> unwrappedGenerator) override { + return pushGenerator(cx, unwrappedGenerator); + } + + bool asyncGenerator( + JSContext* cx, + Handle<AsyncGeneratorObject*> unwrappedGenerator) override { + return pushGenerator(cx, unwrappedGenerator); + } + + private: + bool push(JSContext* cx, HandleObject record) { + RootedValue recordVal(cx, ObjectValue(*record)); + return push(cx, recordVal); + } + + bool push(JSContext* cx, HandleValue recordVal) { + return NewbornArrayPush(cx, records, recordVal); + } + + bool pushGenerator(JSContext* cx, + Handle<AbstractGeneratorObject*> unwrappedGenerator) { + Rooted<DebuggerFrame*> frame(cx); + return dbg->getFrame(cx, unwrappedGenerator, &frame) && push(cx, frame); + } + + bool setIfNotNull(JSContext* cx, Handle<PlainObject*> obj, + Handle<PropertyName*> name, HandleObject prop) { + if (!prop) { + return true; + } + + RootedValue v(cx, ObjectValue(*prop)); + if (!dbg->wrapDebuggeeValue(cx, &v) || + !DefineDataProperty(cx, obj, name, v)) { + return false; + } + + return true; + } +}; + +bool DebuggerObject::CallData::getPromiseReactionsMethod() { + Debugger* dbg = object->owner(); + + Rooted<PromiseObject*> unwrappedPromise(cx, EnsurePromise(cx, referent)); + if (!unwrappedPromise) { + return false; + } + + Rooted<ArrayObject*> holder(cx, NewDenseEmptyArray(cx)); + if (!holder) { + return false; + } + + PromiseReactionRecordBuilder builder(dbg, holder); + if (!unwrappedPromise->forEachReactionRecord(cx, builder)) { + return false; + } + + args.rval().setObject(*builder.records); + return true; +} + +const JSPropertySpec DebuggerObject::properties_[] = { + JS_DEBUG_PSG("callable", callableGetter), + JS_DEBUG_PSG("isBoundFunction", isBoundFunctionGetter), + JS_DEBUG_PSG("isArrowFunction", isArrowFunctionGetter), + JS_DEBUG_PSG("isGeneratorFunction", isGeneratorFunctionGetter), + JS_DEBUG_PSG("isAsyncFunction", isAsyncFunctionGetter), + JS_DEBUG_PSG("isClassConstructor", isClassConstructorGetter), + JS_DEBUG_PSG("proto", protoGetter), + JS_DEBUG_PSG("class", classGetter), + JS_DEBUG_PSG("name", nameGetter), + JS_DEBUG_PSG("displayName", displayNameGetter), + JS_DEBUG_PSG("parameterNames", parameterNamesGetter), + JS_DEBUG_PSG("script", scriptGetter), + JS_DEBUG_PSG("environment", environmentGetter), + JS_DEBUG_PSG("boundTargetFunction", boundTargetFunctionGetter), + JS_DEBUG_PSG("boundThis", boundThisGetter), + JS_DEBUG_PSG("boundArguments", boundArgumentsGetter), + JS_DEBUG_PSG("allocationSite", allocationSiteGetter), + JS_DEBUG_PSG("isError", isErrorGetter), + JS_DEBUG_PSG("errorMessageName", errorMessageNameGetter), + JS_DEBUG_PSG("errorNotes", errorNotesGetter), + JS_DEBUG_PSG("errorLineNumber", errorLineNumberGetter), + JS_DEBUG_PSG("errorColumnNumber", errorColumnNumberGetter), + JS_DEBUG_PSG("isProxy", isProxyGetter), + JS_DEBUG_PSG("proxyTarget", proxyTargetGetter), + JS_DEBUG_PSG("proxyHandler", proxyHandlerGetter), + JS_PS_END}; + +const JSPropertySpec DebuggerObject::promiseProperties_[] = { + JS_DEBUG_PSG("isPromise", isPromiseGetter), + JS_DEBUG_PSG("promiseState", promiseStateGetter), + JS_DEBUG_PSG("promiseValue", promiseValueGetter), + JS_DEBUG_PSG("promiseReason", promiseReasonGetter), + JS_DEBUG_PSG("promiseLifetime", promiseLifetimeGetter), + JS_DEBUG_PSG("promiseTimeToResolution", promiseTimeToResolutionGetter), + JS_DEBUG_PSG("promiseAllocationSite", promiseAllocationSiteGetter), + JS_DEBUG_PSG("promiseResolutionSite", promiseResolutionSiteGetter), + JS_DEBUG_PSG("promiseID", promiseIDGetter), + JS_DEBUG_PSG("promiseDependentPromises", promiseDependentPromisesGetter), + JS_PS_END}; + +const JSFunctionSpec DebuggerObject::methods_[] = { + JS_DEBUG_FN("isExtensible", isExtensibleMethod, 0), + JS_DEBUG_FN("isSealed", isSealedMethod, 0), + JS_DEBUG_FN("isFrozen", isFrozenMethod, 0), + JS_DEBUG_FN("getProperty", getPropertyMethod, 0), + JS_DEBUG_FN("setProperty", setPropertyMethod, 0), + JS_DEBUG_FN("getOwnPropertyNames", getOwnPropertyNamesMethod, 0), + JS_DEBUG_FN("getOwnPropertyNamesLength", getOwnPropertyNamesLengthMethod, + 0), + JS_DEBUG_FN("getOwnPropertySymbols", getOwnPropertySymbolsMethod, 0), + JS_DEBUG_FN("getOwnPrivateProperties", getOwnPrivatePropertiesMethod, 0), + JS_DEBUG_FN("getOwnPropertyDescriptor", getOwnPropertyDescriptorMethod, 1), + JS_DEBUG_FN("preventExtensions", preventExtensionsMethod, 0), + JS_DEBUG_FN("seal", sealMethod, 0), + JS_DEBUG_FN("freeze", freezeMethod, 0), + JS_DEBUG_FN("defineProperty", definePropertyMethod, 2), + JS_DEBUG_FN("defineProperties", definePropertiesMethod, 1), + JS_DEBUG_FN("deleteProperty", deletePropertyMethod, 1), + JS_DEBUG_FN("call", callMethod, 0), + JS_DEBUG_FN("apply", applyMethod, 0), + JS_DEBUG_FN("asEnvironment", asEnvironmentMethod, 0), + JS_DEBUG_FN("forceLexicalInitializationByName", + forceLexicalInitializationByNameMethod, 1), + JS_DEBUG_FN("executeInGlobal", executeInGlobalMethod, 1), + JS_DEBUG_FN("executeInGlobalWithBindings", + executeInGlobalWithBindingsMethod, 2), + JS_DEBUG_FN("createSource", createSource, 1), + JS_DEBUG_FN("makeDebuggeeValue", makeDebuggeeValueMethod, 1), + JS_DEBUG_FN("makeDebuggeeNativeFunction", makeDebuggeeNativeFunctionMethod, + 1), + JS_DEBUG_FN("isSameNative", isSameNativeMethod, 1), + JS_DEBUG_FN("unsafeDereference", unsafeDereferenceMethod, 0), + JS_DEBUG_FN("unwrap", unwrapMethod, 0), + JS_DEBUG_FN("getPromiseReactions", getPromiseReactionsMethod, 0), + JS_FS_END}; + +/* static */ +NativeObject* DebuggerObject::initClass(JSContext* cx, + Handle<GlobalObject*> global, + HandleObject debugCtor) { + Rooted<NativeObject*> objectProto( + cx, InitClass(cx, debugCtor, nullptr, nullptr, "Object", construct, 0, + properties_, methods_, nullptr, nullptr)); + + if (!objectProto) { + return nullptr; + } + + if (!DefinePropertiesAndFunctions(cx, objectProto, promiseProperties_, + nullptr)) { + return nullptr; + } + + return objectProto; +} + +/* static */ +DebuggerObject* DebuggerObject::create(JSContext* cx, HandleObject proto, + HandleObject referent, + Handle<NativeObject*> debugger) { + DebuggerObject* obj = + IsInsideNursery(referent) + ? NewObjectWithGivenProto<DebuggerObject>(cx, proto) + : NewTenuredObjectWithGivenProto<DebuggerObject>(cx, proto); + if (!obj) { + return nullptr; + } + + obj->setReservedSlotGCThingAsPrivate(OBJECT_SLOT, referent); + obj->setReservedSlot(OWNER_SLOT, ObjectValue(*debugger)); + + return obj; +} + +bool DebuggerObject::isCallable() const { return referent()->isCallable(); } + +bool DebuggerObject::isFunction() const { return referent()->is<JSFunction>(); } + +bool DebuggerObject::isDebuggeeFunction() const { + return referent()->is<JSFunction>() && + owner()->observesGlobal(&referent()->as<JSFunction>().global()); +} + +bool DebuggerObject::isBoundFunction() const { + MOZ_ASSERT(isDebuggeeFunction()); + + return referent()->as<JSFunction>().isBoundFunction(); +} + +bool DebuggerObject::isArrowFunction() const { + MOZ_ASSERT(isDebuggeeFunction()); + + return referent()->as<JSFunction>().isArrow(); +} + +bool DebuggerObject::isAsyncFunction() const { + MOZ_ASSERT(isDebuggeeFunction()); + + return referent()->as<JSFunction>().isAsync(); +} + +bool DebuggerObject::isGeneratorFunction() const { + MOZ_ASSERT(isDebuggeeFunction()); + + return referent()->as<JSFunction>().isGenerator(); +} + +bool DebuggerObject::isClassConstructor() const { + MOZ_ASSERT(isDebuggeeFunction()); + + return referent()->as<JSFunction>().isClassConstructor(); +} + +bool DebuggerObject::isGlobal() const { return referent()->is<GlobalObject>(); } + +bool DebuggerObject::isScriptedProxy() const { + return js::IsScriptedProxy(referent()); +} + +bool DebuggerObject::isPromise() const { + JSObject* referent = this->referent(); + + if (IsCrossCompartmentWrapper(referent)) { + // We only care about promises, so CheckedUnwrapStatic is OK. + referent = CheckedUnwrapStatic(referent); + if (!referent) { + return false; + } + } + + return referent->is<PromiseObject>(); +} + +bool DebuggerObject::isError() const { + JSObject* referent = this->referent(); + + if (IsCrossCompartmentWrapper(referent)) { + // We only check for error classes, so CheckedUnwrapStatic is OK. + referent = CheckedUnwrapStatic(referent); + if (!referent) { + return false; + } + } + + return referent->is<ErrorObject>(); +} + +/* static */ +bool DebuggerObject::getClassName(JSContext* cx, Handle<DebuggerObject*> object, + MutableHandleString result) { + RootedObject referent(cx, object->referent()); + + const char* className; + { + Maybe<AutoRealm> ar; + EnterDebuggeeObjectRealm(cx, ar, referent); + className = GetObjectClassName(cx, referent); + } + + JSAtom* str = Atomize(cx, className, strlen(className)); + if (!str) { + return false; + } + + result.set(str); + return true; +} + +JSAtom* DebuggerObject::name(JSContext* cx) const { + MOZ_ASSERT(isFunction()); + + JSAtom* atom = referent()->as<JSFunction>().explicitName(); + if (atom) { + cx->markAtom(atom); + } + return atom; +} + +JSAtom* DebuggerObject::displayName(JSContext* cx) const { + MOZ_ASSERT(isFunction()); + + JSAtom* atom = referent()->as<JSFunction>().displayAtom(); + if (atom) { + cx->markAtom(atom); + } + return atom; +} + +JS::PromiseState DebuggerObject::promiseState() const { + return promise()->state(); +} + +double DebuggerObject::promiseLifetime() const { return promise()->lifetime(); } + +double DebuggerObject::promiseTimeToResolution() const { + MOZ_ASSERT(promiseState() != JS::PromiseState::Pending); + + return promise()->timeToResolution(); +} + +/* static */ +bool DebuggerObject::getBoundTargetFunction( + JSContext* cx, Handle<DebuggerObject*> object, + MutableHandle<DebuggerObject*> result) { + MOZ_ASSERT(object->isBoundFunction()); + + RootedFunction referent(cx, &object->referent()->as<JSFunction>()); + Debugger* dbg = object->owner(); + + RootedObject target(cx, referent->getBoundFunctionTarget()); + return dbg->wrapDebuggeeObject(cx, target, result); +} + +/* static */ +bool DebuggerObject::getBoundThis(JSContext* cx, Handle<DebuggerObject*> object, + MutableHandleValue result) { + MOZ_ASSERT(object->isBoundFunction()); + + RootedFunction referent(cx, &object->referent()->as<JSFunction>()); + Debugger* dbg = object->owner(); + + result.set(referent->getBoundFunctionThis()); + return dbg->wrapDebuggeeValue(cx, result); +} + +/* static */ +bool DebuggerObject::getBoundArguments(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandle<ValueVector> result) { + MOZ_ASSERT(object->isBoundFunction()); + + RootedFunction referent(cx, &object->referent()->as<JSFunction>()); + Debugger* dbg = object->owner(); + + size_t length = referent->getBoundFunctionArgumentCount(); + if (!result.resize(length)) { + return false; + } + for (size_t i = 0; i < length; i++) { + result[i].set(referent->getBoundFunctionArgument(i)); + if (!dbg->wrapDebuggeeValue(cx, result[i])) { + return false; + } + } + return true; +} + +/* static */ +SavedFrame* Debugger::getObjectAllocationSite(JSObject& obj) { + JSObject* metadata = GetAllocationMetadata(&obj); + if (!metadata) { + return nullptr; + } + + MOZ_ASSERT(!metadata->is<WrapperObject>()); + return metadata->is<SavedFrame>() ? &metadata->as<SavedFrame>() : nullptr; +} + +/* static */ +bool DebuggerObject::getAllocationSite(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandleObject result) { + RootedObject referent(cx, object->referent()); + + RootedObject allocSite(cx, Debugger::getObjectAllocationSite(*referent)); + if (!cx->compartment()->wrap(cx, &allocSite)) { + return false; + } + + result.set(allocSite); + return true; +} + +/* static */ +bool DebuggerObject::getErrorReport(JSContext* cx, HandleObject maybeError, + JSErrorReport*& report) { + JSObject* obj = maybeError; + if (IsCrossCompartmentWrapper(obj)) { + /* We only care about Error objects, so CheckedUnwrapStatic is OK. */ + obj = CheckedUnwrapStatic(obj); + } + + if (!obj) { + ReportAccessDenied(cx); + return false; + } + + if (!obj->is<ErrorObject>()) { + report = nullptr; + return true; + } + + report = obj->as<ErrorObject>().getErrorReport(); + return true; +} + +/* static */ +bool DebuggerObject::getErrorMessageName(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandleString result) { + RootedObject referent(cx, object->referent()); + JSErrorReport* report; + if (!getErrorReport(cx, referent, report)) { + return false; + } + + if (!report || !report->errorMessageName) { + result.set(nullptr); + return true; + } + + RootedString str(cx, JS_NewStringCopyZ(cx, report->errorMessageName)); + if (!str) { + return false; + } + result.set(str); + return true; +} + +/* static */ +bool DebuggerObject::getErrorNotes(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandleValue result) { + RootedObject referent(cx, object->referent()); + JSErrorReport* report; + if (!getErrorReport(cx, referent, report)) { + return false; + } + + if (!report) { + result.setUndefined(); + return true; + } + + RootedObject errorNotesArray(cx, CreateErrorNotesArray(cx, report)); + if (!errorNotesArray) { + return false; + } + + if (!cx->compartment()->wrap(cx, &errorNotesArray)) { + return false; + } + result.setObject(*errorNotesArray); + return true; +} + +/* static */ +bool DebuggerObject::getErrorLineNumber(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandleValue result) { + RootedObject referent(cx, object->referent()); + JSErrorReport* report; + if (!getErrorReport(cx, referent, report)) { + return false; + } + + if (!report) { + result.setUndefined(); + return true; + } + + result.setNumber(report->lineno); + return true; +} + +/* static */ +bool DebuggerObject::getErrorColumnNumber(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandleValue result) { + RootedObject referent(cx, object->referent()); + JSErrorReport* report; + if (!getErrorReport(cx, referent, report)) { + return false; + } + + if (!report) { + result.setUndefined(); + return true; + } + + result.setNumber(report->column); + return true; +} + +/* static */ +bool DebuggerObject::getPromiseValue(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandleValue result) { + MOZ_ASSERT(object->promiseState() == JS::PromiseState::Fulfilled); + + result.set(object->promise()->value()); + return object->owner()->wrapDebuggeeValue(cx, result); +} + +/* static */ +bool DebuggerObject::getPromiseReason(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandleValue result) { + MOZ_ASSERT(object->promiseState() == JS::PromiseState::Rejected); + + result.set(object->promise()->reason()); + return object->owner()->wrapDebuggeeValue(cx, result); +} + +/* static */ +bool DebuggerObject::isExtensible(JSContext* cx, Handle<DebuggerObject*> object, + bool& result) { + RootedObject referent(cx, object->referent()); + + Maybe<AutoRealm> ar; + EnterDebuggeeObjectRealm(cx, ar, referent); + + ErrorCopier ec(ar); + return IsExtensible(cx, referent, &result); +} + +/* static */ +bool DebuggerObject::isSealed(JSContext* cx, Handle<DebuggerObject*> object, + bool& result) { + RootedObject referent(cx, object->referent()); + + Maybe<AutoRealm> ar; + EnterDebuggeeObjectRealm(cx, ar, referent); + + ErrorCopier ec(ar); + return TestIntegrityLevel(cx, referent, IntegrityLevel::Sealed, &result); +} + +/* static */ +bool DebuggerObject::isFrozen(JSContext* cx, Handle<DebuggerObject*> object, + bool& result) { + RootedObject referent(cx, object->referent()); + + Maybe<AutoRealm> ar; + EnterDebuggeeObjectRealm(cx, ar, referent); + + ErrorCopier ec(ar); + return TestIntegrityLevel(cx, referent, IntegrityLevel::Frozen, &result); +} + +/* static */ +bool DebuggerObject::getPrototypeOf(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandle<DebuggerObject*> result) { + RootedObject referent(cx, object->referent()); + Debugger* dbg = object->owner(); + + RootedObject proto(cx); + { + Maybe<AutoRealm> ar; + EnterDebuggeeObjectRealm(cx, ar, referent); + if (!GetPrototype(cx, referent, &proto)) { + return false; + } + } + + return dbg->wrapNullableDebuggeeObject(cx, proto, result); +} + +/* static */ +bool DebuggerObject::getOwnPropertyNames(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandleIdVector result) { + MOZ_ASSERT(result.empty()); + + RootedObject referent(cx, object->referent()); + { + Maybe<AutoRealm> ar; + EnterDebuggeeObjectRealm(cx, ar, referent); + + ErrorCopier ec(ar); + if (!GetPropertyKeys(cx, referent, JSITER_OWNONLY | JSITER_HIDDEN, + result)) { + return false; + } + } + + for (size_t i = 0; i < result.length(); i++) { + cx->markId(result[i]); + } + + return true; +} + +/* static */ +bool DebuggerObject::getOwnPropertyNamesLength(JSContext* cx, + Handle<DebuggerObject*> object, + size_t* result) { + RootedObject referent(cx, object->referent()); + + RootedIdVector ids(cx); + { + Maybe<AutoRealm> ar; + EnterDebuggeeObjectRealm(cx, ar, referent); + + ErrorCopier ec(ar); + if (!GetPropertyKeys(cx, referent, JSITER_OWNONLY | JSITER_HIDDEN, &ids)) { + return false; + } + } + + *result = ids.length(); + return true; +} + +static bool GetSymbolPropertyKeys(JSContext* cx, Handle<DebuggerObject*> object, + JS::MutableHandleIdVector props, + bool includePrivate) { + RootedObject referent(cx, object->referent()); + + { + Maybe<AutoRealm> ar; + EnterDebuggeeObjectRealm(cx, ar, referent); + + ErrorCopier ec(ar); + + unsigned flags = + JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS | JSITER_SYMBOLSONLY; + if (includePrivate) { + flags = flags | JSITER_PRIVATE; + } + if (!GetPropertyKeys(cx, referent, flags, props)) { + return false; + } + } + + return true; +} + +/* static */ +bool DebuggerObject::getOwnPropertySymbols(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandleIdVector result) { + MOZ_ASSERT(result.empty()); + + if (!GetSymbolPropertyKeys(cx, object, result, false)) { + return false; + } + + for (size_t i = 0; i < result.length(); i++) { + cx->markAtom(result[i].toSymbol()); + } + + return true; +} + +/* static */ +bool DebuggerObject::getOwnPrivateProperties(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandleIdVector result) { + MOZ_ASSERT(result.empty()); + + if (!GetSymbolPropertyKeys(cx, object, result, true)) { + return false; + } + + result.eraseIf([](PropertyKey key) { + if (!key.isPrivateName()) { + return true; + } + // Private *methods* create a Private Brand, a special private name + // stamped onto the symbol, to indicate it is possible to execute private + // methods from the class on this object. We don't want to return such + // items here, so we check if we're dealing with a private property, e.g. + // the Symbol description starts with a "#" character. + JSAtom* privateDescription = key.toSymbol()->description(); + if (privateDescription->length() == 0) { + return true; + } + char16_t firstChar = privateDescription->latin1OrTwoByteChar(0); + return firstChar != '#'; + }); + + for (size_t i = 0; i < result.length(); i++) { + cx->markAtom(result[i].toSymbol()); + } + + return true; +} + +/* static */ +bool DebuggerObject::getOwnPropertyDescriptor( + JSContext* cx, Handle<DebuggerObject*> object, HandleId id, + MutableHandle<Maybe<PropertyDescriptor>> desc_) { + RootedObject referent(cx, object->referent()); + Debugger* dbg = object->owner(); + + // Bug: This can cause the debuggee to run! + { + Maybe<AutoRealm> ar; + EnterDebuggeeObjectRealm(cx, ar, referent); + + cx->markId(id); + + ErrorCopier ec(ar); + if (!GetOwnPropertyDescriptor(cx, referent, id, desc_)) { + return false; + } + } + + if (desc_.isSome()) { + Rooted<PropertyDescriptor> desc(cx, *desc_); + + if (desc.hasValue()) { + // Rewrap the debuggee values in desc for the debugger. + if (!dbg->wrapDebuggeeValue(cx, desc.value())) { + return false; + } + } + if (desc.hasGetter()) { + RootedValue get(cx, ObjectOrNullValue(desc.getter())); + if (!dbg->wrapDebuggeeValue(cx, &get)) { + return false; + } + desc.setGetter(get.toObjectOrNull()); + } + if (desc.hasSetter()) { + RootedValue set(cx, ObjectOrNullValue(desc.setter())); + if (!dbg->wrapDebuggeeValue(cx, &set)) { + return false; + } + desc.setSetter(set.toObjectOrNull()); + } + + desc_.set(mozilla::Some(desc.get())); + } + + return true; +} + +/* static */ +bool DebuggerObject::preventExtensions(JSContext* cx, + Handle<DebuggerObject*> object) { + RootedObject referent(cx, object->referent()); + + Maybe<AutoRealm> ar; + EnterDebuggeeObjectRealm(cx, ar, referent); + + ErrorCopier ec(ar); + return PreventExtensions(cx, referent); +} + +/* static */ +bool DebuggerObject::seal(JSContext* cx, Handle<DebuggerObject*> object) { + RootedObject referent(cx, object->referent()); + + Maybe<AutoRealm> ar; + EnterDebuggeeObjectRealm(cx, ar, referent); + + ErrorCopier ec(ar); + return SetIntegrityLevel(cx, referent, IntegrityLevel::Sealed); +} + +/* static */ +bool DebuggerObject::freeze(JSContext* cx, Handle<DebuggerObject*> object) { + RootedObject referent(cx, object->referent()); + + Maybe<AutoRealm> ar; + EnterDebuggeeObjectRealm(cx, ar, referent); + + ErrorCopier ec(ar); + return SetIntegrityLevel(cx, referent, IntegrityLevel::Frozen); +} + +/* static */ +bool DebuggerObject::defineProperty(JSContext* cx, + Handle<DebuggerObject*> object, HandleId id, + Handle<PropertyDescriptor> desc_) { + RootedObject referent(cx, object->referent()); + Debugger* dbg = object->owner(); + + Rooted<PropertyDescriptor> desc(cx, desc_); + if (!dbg->unwrapPropertyDescriptor(cx, referent, &desc)) { + return false; + } + JS_TRY_OR_RETURN_FALSE(cx, CheckPropertyDescriptorAccessors(cx, desc)); + + Maybe<AutoRealm> ar; + EnterDebuggeeObjectRealm(cx, ar, referent); + + if (!cx->compartment()->wrap(cx, &desc)) { + return false; + } + cx->markId(id); + + ErrorCopier ec(ar); + return DefineProperty(cx, referent, id, desc); +} + +/* static */ +bool DebuggerObject::defineProperties(JSContext* cx, + Handle<DebuggerObject*> object, + Handle<IdVector> ids, + Handle<PropertyDescriptorVector> descs_) { + RootedObject referent(cx, object->referent()); + Debugger* dbg = object->owner(); + + Rooted<PropertyDescriptorVector> descs(cx, PropertyDescriptorVector(cx)); + if (!descs.append(descs_.begin(), descs_.end())) { + return false; + } + for (size_t i = 0; i < descs.length(); i++) { + if (!dbg->unwrapPropertyDescriptor(cx, referent, descs[i])) { + return false; + } + JS_TRY_OR_RETURN_FALSE(cx, CheckPropertyDescriptorAccessors(cx, descs[i])); + } + + Maybe<AutoRealm> ar; + EnterDebuggeeObjectRealm(cx, ar, referent); + + for (size_t i = 0; i < descs.length(); i++) { + if (!cx->compartment()->wrap(cx, descs[i])) { + return false; + } + cx->markId(ids[i]); + } + + ErrorCopier ec(ar); + for (size_t i = 0; i < descs.length(); i++) { + if (!DefineProperty(cx, referent, ids[i], descs[i])) { + return false; + } + } + + return true; +} + +/* static */ +bool DebuggerObject::deleteProperty(JSContext* cx, + Handle<DebuggerObject*> object, HandleId id, + ObjectOpResult& result) { + RootedObject referent(cx, object->referent()); + + Maybe<AutoRealm> ar; + EnterDebuggeeObjectRealm(cx, ar, referent); + + cx->markId(id); + + ErrorCopier ec(ar); + return DeleteProperty(cx, referent, id, result); +} + +/* static */ +Result<Completion> DebuggerObject::getProperty(JSContext* cx, + Handle<DebuggerObject*> object, + HandleId id, + HandleValue receiver_) { + RootedObject referent(cx, object->referent()); + Debugger* dbg = object->owner(); + + // Unwrap Debugger.Objects. This happens in the debugger's compartment since + // that is where any exceptions must be reported. + RootedValue receiver(cx, receiver_); + if (!dbg->unwrapDebuggeeValue(cx, &receiver)) { + return cx->alreadyReportedError(); + } + + // Enter the debuggee compartment and rewrap all input value for that + // compartment. (Rewrapping always takes place in the destination + // compartment.) + Maybe<AutoRealm> ar; + EnterDebuggeeObjectRealm(cx, ar, referent); + if (!cx->compartment()->wrap(cx, &referent) || + !cx->compartment()->wrap(cx, &receiver)) { + return cx->alreadyReportedError(); + } + cx->markId(id); + + LeaveDebuggeeNoExecute nnx(cx); + + RootedValue result(cx); + bool ok = GetProperty(cx, referent, receiver, id, &result); + return Completion::fromJSResult(cx, ok, result); +} + +/* static */ +Result<Completion> DebuggerObject::setProperty(JSContext* cx, + Handle<DebuggerObject*> object, + HandleId id, HandleValue value_, + HandleValue receiver_) { + RootedObject referent(cx, object->referent()); + Debugger* dbg = object->owner(); + + // Unwrap Debugger.Objects. This happens in the debugger's compartment since + // that is where any exceptions must be reported. + RootedValue value(cx, value_); + RootedValue receiver(cx, receiver_); + if (!dbg->unwrapDebuggeeValue(cx, &value) || + !dbg->unwrapDebuggeeValue(cx, &receiver)) { + return cx->alreadyReportedError(); + } + + // Enter the debuggee compartment and rewrap all input value for that + // compartment. (Rewrapping always takes place in the destination + // compartment.) + Maybe<AutoRealm> ar; + EnterDebuggeeObjectRealm(cx, ar, referent); + if (!cx->compartment()->wrap(cx, &referent) || + !cx->compartment()->wrap(cx, &value) || + !cx->compartment()->wrap(cx, &receiver)) { + return cx->alreadyReportedError(); + } + cx->markId(id); + + LeaveDebuggeeNoExecute nnx(cx); + + ObjectOpResult opResult; + bool ok = SetProperty(cx, referent, id, value, receiver, opResult); + + return Completion::fromJSResult(cx, ok, BooleanValue(ok && opResult.ok())); +} + +/* static */ +Maybe<Completion> DebuggerObject::call(JSContext* cx, + Handle<DebuggerObject*> object, + HandleValue thisv_, + Handle<ValueVector> args) { + RootedObject referent(cx, object->referent()); + Debugger* dbg = object->owner(); + + if (!referent->isCallable()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INCOMPATIBLE_PROTO, "Debugger.Object", + "call", referent->getClass()->name); + return Nothing(); + } + + RootedValue calleev(cx, ObjectValue(*referent)); + + // Unwrap Debugger.Objects. This happens in the debugger's compartment since + // that is where any exceptions must be reported. + RootedValue thisv(cx, thisv_); + if (!dbg->unwrapDebuggeeValue(cx, &thisv)) { + return Nothing(); + } + Rooted<ValueVector> args2(cx, ValueVector(cx)); + if (!args2.append(args.begin(), args.end())) { + return Nothing(); + } + for (size_t i = 0; i < args2.length(); ++i) { + if (!dbg->unwrapDebuggeeValue(cx, args2[i])) { + return Nothing(); + } + } + + // Enter the debuggee compartment and rewrap all input value for that + // compartment. (Rewrapping always takes place in the destination + // compartment.) + Maybe<AutoRealm> ar; + EnterDebuggeeObjectRealm(cx, ar, referent); + if (!cx->compartment()->wrap(cx, &calleev) || + !cx->compartment()->wrap(cx, &thisv)) { + return Nothing(); + } + for (size_t i = 0; i < args2.length(); ++i) { + if (!cx->compartment()->wrap(cx, args2[i])) { + return Nothing(); + } + } + + // Note whether we are in an evaluation that might invoke the OnNativeCall + // hook, so that the JITs will be disabled. + AutoNoteDebuggerEvaluationWithOnNativeCallHook noteEvaluation( + cx, dbg->observesNativeCalls() ? dbg : nullptr); + + // Call the function. + LeaveDebuggeeNoExecute nnx(cx); + + RootedValue result(cx); + bool ok; + { + InvokeArgs invokeArgs(cx); + + ok = invokeArgs.init(cx, args2.length()); + if (ok) { + for (size_t i = 0; i < args2.length(); ++i) { + invokeArgs[i].set(args2[i]); + } + + ok = js::Call(cx, calleev, thisv, invokeArgs, &result); + } + } + + Rooted<Completion> completion(cx, Completion::fromJSResult(cx, ok, result)); + ar.reset(); + return Some(std::move(completion.get())); +} + +/* static */ +bool DebuggerObject::forceLexicalInitializationByName( + JSContext* cx, Handle<DebuggerObject*> object, HandleId id, bool& result) { + if (!id.isString()) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE, + "Debugger.Object.prototype.forceLexicalInitializationByName", "string", + InformalValueTypeName(IdToValue(id))); + return false; + } + + MOZ_ASSERT(object->isGlobal()); + + Rooted<GlobalObject*> referent(cx, &object->referent()->as<GlobalObject>()); + + // Shape::search can end up allocating a new BaseShape in Shape::cachify so + // we need to be in the right compartment here. + Maybe<AutoRealm> ar; + EnterDebuggeeObjectRealm(cx, ar, referent); + + RootedObject globalLexical(cx, &referent->lexicalEnvironment()); + RootedObject pobj(cx); + PropertyResult prop; + if (!LookupProperty(cx, globalLexical, id, &pobj, &prop)) { + return false; + } + + result = false; + if (prop.isFound()) { + MOZ_ASSERT(prop.isNativeProperty()); + PropertyInfo propInfo = prop.propertyInfo(); + Value v = globalLexical->as<NativeObject>().getSlot(propInfo.slot()); + if (propInfo.isDataProperty() && v.isMagic() && + v.whyMagic() == JS_UNINITIALIZED_LEXICAL) { + globalLexical->as<NativeObject>().setSlot(propInfo.slot(), + UndefinedValue()); + result = true; + } + } + + return true; +} + +/* static */ +Result<Completion> DebuggerObject::executeInGlobal( + JSContext* cx, Handle<DebuggerObject*> object, + mozilla::Range<const char16_t> chars, HandleObject bindings, + const EvalOptions& options) { + MOZ_ASSERT(object->isGlobal()); + + Rooted<GlobalObject*> referent(cx, &object->referent()->as<GlobalObject>()); + Debugger* dbg = object->owner(); + + RootedObject globalLexical(cx, &referent->lexicalEnvironment()); + return DebuggerGenericEval(cx, chars, bindings, options, dbg, globalLexical, + nullptr); +} + +/* static */ +bool DebuggerObject::makeDebuggeeValue(JSContext* cx, + Handle<DebuggerObject*> object, + HandleValue value_, + MutableHandleValue result) { + RootedObject referent(cx, object->referent()); + Debugger* dbg = object->owner(); + + RootedValue value(cx, value_); + + // Non-objects are already debuggee values. + if (value.isObject()) { + // Enter this Debugger.Object's referent's compartment, and wrap the + // argument as appropriate for references from there. + { + Maybe<AutoRealm> ar; + EnterDebuggeeObjectRealm(cx, ar, referent); + if (!cx->compartment()->wrap(cx, &value)) { + return false; + } + } + + // Back in the debugger's compartment, produce a new Debugger.Object + // instance referring to the wrapped argument. + if (!dbg->wrapDebuggeeValue(cx, &value)) { + return false; + } + } + + result.set(value); + return true; +} + +static JSFunction* EnsureNativeFunction(const Value& value, + bool allowExtended = true) { + if (!value.isObject() || !value.toObject().is<JSFunction>()) { + return nullptr; + } + + JSFunction* fun = &value.toObject().as<JSFunction>(); + if (!fun->isNativeFun() || (fun->isExtended() && !allowExtended)) { + return nullptr; + } + + return fun; +} + +/* static */ +bool DebuggerObject::makeDebuggeeNativeFunction(JSContext* cx, + Handle<DebuggerObject*> object, + HandleValue value, + MutableHandleValue result) { + RootedObject referent(cx, object->referent()); + Debugger* dbg = object->owner(); + + // The logic below doesn't work with extended functions, so do not allow them. + RootedFunction fun(cx, EnsureNativeFunction(value, + /* allowExtended */ false)); + if (!fun) { + JS_ReportErrorASCII(cx, "Need native function"); + return false; + } + + RootedValue newValue(cx); + { + Maybe<AutoRealm> ar; + EnterDebuggeeObjectRealm(cx, ar, referent); + + unsigned nargs = fun->nargs(); + Rooted<JSAtom*> name(cx, fun->displayAtom()); + if (name) { + cx->markAtom(name); + } + JSFunction* newFun = NewNativeFunction(cx, fun->native(), nargs, name); + if (!newFun) { + return false; + } + + newValue.setObject(*newFun); + } + + // Back in the debugger's compartment, produce a new Debugger.Object + // instance referring to the wrapped argument. + if (!dbg->wrapDebuggeeValue(cx, &newValue)) { + return false; + } + + result.set(newValue); + return true; +} + +static JSAtom* MaybeGetSelfHostedFunctionName(const Value& v) { + if (!v.isObject() || !v.toObject().is<JSFunction>()) { + return nullptr; + } + + JSFunction* fun = &v.toObject().as<JSFunction>(); + if (!fun->isSelfHostedBuiltin()) { + return nullptr; + } + + return GetClonedSelfHostedFunctionName(fun); +} + +/* static */ +bool DebuggerObject::isSameNative(JSContext* cx, Handle<DebuggerObject*> object, + HandleValue value, + MutableHandleValue result) { + RootedValue referentValue(cx, ObjectValue(*object->referent())); + + RootedValue nonCCWValue( + cx, value.isObject() ? ObjectValue(*UncheckedUnwrap(&value.toObject())) + : value); + + RootedFunction fun(cx, EnsureNativeFunction(nonCCWValue)); + if (!fun) { + Rooted<JSAtom*> selfHostedName(cx, + MaybeGetSelfHostedFunctionName(nonCCWValue)); + if (!selfHostedName) { + JS_ReportErrorASCII(cx, "Need native function"); + return false; + } + + result.setBoolean(selfHostedName == + MaybeGetSelfHostedFunctionName(referentValue)); + return true; + } + + RootedFunction referentFun(cx, EnsureNativeFunction(referentValue)); + result.setBoolean(referentFun && referentFun->native() == fun->native()); + return true; +} + +/* static */ +bool DebuggerObject::unsafeDereference(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandleObject result) { + RootedObject referent(cx, object->referent()); + + if (!cx->compartment()->wrap(cx, &referent)) { + return false; + } + + // Wrapping should return the WindowProxy. + MOZ_ASSERT(!IsWindow(referent)); + + result.set(referent); + return true; +} + +/* static */ +bool DebuggerObject::unwrap(JSContext* cx, Handle<DebuggerObject*> object, + MutableHandle<DebuggerObject*> result) { + RootedObject referent(cx, object->referent()); + Debugger* dbg = object->owner(); + + RootedObject unwrapped(cx, UnwrapOneCheckedStatic(referent)); + + // Don't allow unwrapping to create a D.O whose referent is in an + // invisible-to-Debugger compartment. (If our referent is a *wrapper* to such, + // and the wrapper is in a visible compartment, that's fine.) + if (unwrapped && unwrapped->compartment()->invisibleToDebugger()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_INVISIBLE_COMPARTMENT); + return false; + } + + return dbg->wrapNullableDebuggeeObject(cx, unwrapped, result); +} + +/* static */ +bool DebuggerObject::requireGlobal(JSContext* cx, + Handle<DebuggerObject*> object) { + if (!object->isGlobal()) { + RootedObject referent(cx, object->referent()); + + const char* isWrapper = ""; + const char* isWindowProxy = ""; + + // Help the poor programmer by pointing out wrappers around globals... + if (referent->is<WrapperObject>()) { + referent = js::UncheckedUnwrap(referent); + isWrapper = "a wrapper around "; + } + + // ... and WindowProxies around Windows. + if (IsWindowProxy(referent)) { + referent = ToWindowIfWindowProxy(referent); + isWindowProxy = "a WindowProxy referring to "; + } + + RootedValue dbgobj(cx, ObjectValue(*object)); + if (referent->is<GlobalObject>()) { + ReportValueError(cx, JSMSG_DEBUG_WRAPPER_IN_WAY, JSDVG_SEARCH_STACK, + dbgobj, nullptr, isWrapper, isWindowProxy); + } else { + ReportValueError(cx, JSMSG_DEBUG_BAD_REFERENT, JSDVG_SEARCH_STACK, dbgobj, + nullptr, "a global object"); + } + return false; + } + + return true; +} + +/* static */ +bool DebuggerObject::requirePromise(JSContext* cx, + Handle<DebuggerObject*> object) { + RootedObject referent(cx, object->referent()); + + if (IsCrossCompartmentWrapper(referent)) { + /* We only care about promises, so CheckedUnwrapStatic is OK. */ + referent = CheckedUnwrapStatic(referent); + if (!referent) { + ReportAccessDenied(cx); + return false; + } + } + + if (!referent->is<PromiseObject>()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_EXPECTED_TYPE, "Debugger", "Promise", + object->getClass()->name); + return false; + } + + return true; +} + +/* static */ +bool DebuggerObject::getScriptedProxyTarget( + JSContext* cx, Handle<DebuggerObject*> object, + MutableHandle<DebuggerObject*> result) { + MOZ_ASSERT(object->isScriptedProxy()); + RootedObject referent(cx, object->referent()); + Debugger* dbg = object->owner(); + RootedObject unwrapped(cx, js::GetProxyTargetObject(referent)); + + return dbg->wrapNullableDebuggeeObject(cx, unwrapped, result); +} + +/* static */ +bool DebuggerObject::getScriptedProxyHandler( + JSContext* cx, Handle<DebuggerObject*> object, + MutableHandle<DebuggerObject*> result) { + MOZ_ASSERT(object->isScriptedProxy()); + RootedObject referent(cx, object->referent()); + Debugger* dbg = object->owner(); + RootedObject unwrapped(cx, ScriptedProxyHandler::handlerObject(referent)); + return dbg->wrapNullableDebuggeeObject(cx, unwrapped, result); +} diff --git a/js/src/debugger/Object.h b/js/src/debugger/Object.h new file mode 100644 index 0000000000..5a5ea149e0 --- /dev/null +++ b/js/src/debugger/Object.h @@ -0,0 +1,221 @@ +/* -*- 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/. */ + +#ifndef debugger_Object_h +#define debugger_Object_h + +#include "mozilla/Assertions.h" // for AssertionConditionType, MOZ_ASSERT +#include "mozilla/Maybe.h" // for Maybe +#include "mozilla/Range.h" // for Range +#include "mozilla/Result.h" // for Result + +#include "jstypes.h" // for JS_PUBLIC_API +#include "NamespaceImports.h" // for Value, MutableHandleValue, HandleId + +#include "js/Promise.h" // for PromiseState +#include "js/Proxy.h" // for PropertyDescriptor +#include "vm/JSObject.h" // for JSObject (ptr only) +#include "vm/NativeObject.h" // for NativeObject + +class JS_PUBLIC_API JSAtom; +struct JS_PUBLIC_API JSContext; + +namespace js { + +class Completion; +class Debugger; +class EvalOptions; +class GlobalObject; +class PromiseObject; + +class DebuggerObject : public NativeObject { + public: + static const JSClass class_; + + static NativeObject* initClass(JSContext* cx, Handle<GlobalObject*> global, + HandleObject debugCtor); + static DebuggerObject* create(JSContext* cx, HandleObject proto, + HandleObject referent, + Handle<NativeObject*> debugger); + + void trace(JSTracer* trc); + + // Properties + [[nodiscard]] static bool getClassName(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandleString result); + [[nodiscard]] static bool getBoundTargetFunction( + JSContext* cx, Handle<DebuggerObject*> object, + MutableHandle<DebuggerObject*> result); + [[nodiscard]] static bool getBoundThis(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandleValue result); + [[nodiscard]] static bool getBoundArguments( + JSContext* cx, Handle<DebuggerObject*> object, + MutableHandle<ValueVector> result); + [[nodiscard]] static bool getAllocationSite(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandleObject result); + [[nodiscard]] static bool getErrorMessageName(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandleString result); + [[nodiscard]] static bool getErrorNotes(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandleValue result); + [[nodiscard]] static bool getErrorLineNumber(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandleValue result); + [[nodiscard]] static bool getErrorColumnNumber(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandleValue result); + [[nodiscard]] static bool getScriptedProxyTarget( + JSContext* cx, Handle<DebuggerObject*> object, + MutableHandle<DebuggerObject*> result); + [[nodiscard]] static bool getScriptedProxyHandler( + JSContext* cx, Handle<DebuggerObject*> object, + MutableHandle<DebuggerObject*> result); + [[nodiscard]] static bool getPromiseValue(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandleValue result); + [[nodiscard]] static bool getPromiseReason(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandleValue result); + + // Methods + [[nodiscard]] static bool isExtensible(JSContext* cx, + Handle<DebuggerObject*> object, + bool& result); + [[nodiscard]] static bool isSealed(JSContext* cx, + Handle<DebuggerObject*> object, + bool& result); + [[nodiscard]] static bool isFrozen(JSContext* cx, + Handle<DebuggerObject*> object, + bool& result); + [[nodiscard]] static JS::Result<Completion> getProperty( + JSContext* cx, Handle<DebuggerObject*> object, HandleId id, + HandleValue receiver); + [[nodiscard]] static JS::Result<Completion> setProperty( + JSContext* cx, Handle<DebuggerObject*> object, HandleId id, + HandleValue value, HandleValue receiver); + [[nodiscard]] static bool getPrototypeOf( + JSContext* cx, Handle<DebuggerObject*> object, + MutableHandle<DebuggerObject*> result); + [[nodiscard]] static bool getOwnPropertyNames(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandleIdVector result); + [[nodiscard]] static bool getOwnPropertyNamesLength( + JSContext* cx, Handle<DebuggerObject*> object, size_t* result); + [[nodiscard]] static bool getOwnPropertySymbols( + JSContext* cx, Handle<DebuggerObject*> object, + MutableHandleIdVector result); + [[nodiscard]] static bool getOwnPrivateProperties( + JSContext* cx, Handle<DebuggerObject*> object, + MutableHandleIdVector result); + [[nodiscard]] static bool getOwnPropertyDescriptor( + JSContext* cx, Handle<DebuggerObject*> object, HandleId id, + MutableHandle<mozilla::Maybe<PropertyDescriptor>> desc); + [[nodiscard]] static bool preventExtensions(JSContext* cx, + Handle<DebuggerObject*> object); + [[nodiscard]] static bool seal(JSContext* cx, Handle<DebuggerObject*> object); + [[nodiscard]] static bool freeze(JSContext* cx, + Handle<DebuggerObject*> object); + [[nodiscard]] static bool defineProperty(JSContext* cx, + Handle<DebuggerObject*> object, + HandleId id, + Handle<PropertyDescriptor> desc); + [[nodiscard]] static bool defineProperties( + JSContext* cx, Handle<DebuggerObject*> object, Handle<IdVector> ids, + Handle<PropertyDescriptorVector> descs); + [[nodiscard]] static bool deleteProperty(JSContext* cx, + Handle<DebuggerObject*> object, + HandleId id, ObjectOpResult& result); + [[nodiscard]] static mozilla::Maybe<Completion> call( + JSContext* cx, Handle<DebuggerObject*> object, HandleValue thisv, + Handle<ValueVector> args); + [[nodiscard]] static bool forceLexicalInitializationByName( + JSContext* cx, Handle<DebuggerObject*> object, HandleId id, bool& result); + [[nodiscard]] static JS::Result<Completion> executeInGlobal( + JSContext* cx, Handle<DebuggerObject*> object, + mozilla::Range<const char16_t> chars, HandleObject bindings, + const EvalOptions& options); + [[nodiscard]] static bool makeDebuggeeValue(JSContext* cx, + Handle<DebuggerObject*> object, + HandleValue value, + MutableHandleValue result); + [[nodiscard]] static bool makeDebuggeeNativeFunction( + JSContext* cx, Handle<DebuggerObject*> object, HandleValue value, + MutableHandleValue result); + [[nodiscard]] static bool isSameNative(JSContext* cx, + Handle<DebuggerObject*> object, + HandleValue value, + MutableHandleValue result); + [[nodiscard]] static bool unsafeDereference(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandleObject result); + [[nodiscard]] static bool unwrap(JSContext* cx, + Handle<DebuggerObject*> object, + MutableHandle<DebuggerObject*> result); + + // Infallible properties + bool isCallable() const; + bool isFunction() const; + bool isDebuggeeFunction() const; + bool isBoundFunction() const; + bool isArrowFunction() const; + bool isAsyncFunction() const; + bool isGeneratorFunction() const; + bool isClassConstructor() const; + bool isGlobal() const; + bool isScriptedProxy() const; + bool isPromise() const; + bool isError() const; + JSAtom* name(JSContext* cx) const; + JSAtom* displayName(JSContext* cx) const; + JS::PromiseState promiseState() const; + double promiseLifetime() const; + double promiseTimeToResolution() const; + + Debugger* owner() const; + + JSObject* maybeReferent() const { + return maybePtrFromReservedSlot<JSObject>(OBJECT_SLOT); + } + JSObject* referent() const { + JSObject* obj = maybeReferent(); + MOZ_ASSERT(obj); + return obj; + } + + void clearReferent() { clearReservedSlotGCThingAsPrivate(OBJECT_SLOT); } + + private: + enum { OBJECT_SLOT, OWNER_SLOT, RESERVED_SLOTS }; + + static const JSClassOps classOps_; + + static const JSPropertySpec properties_[]; + static const JSPropertySpec promiseProperties_[]; + static const JSFunctionSpec methods_[]; + + PromiseObject* promise() const; + + [[nodiscard]] static bool requireGlobal(JSContext* cx, + Handle<DebuggerObject*> object); + [[nodiscard]] static bool requirePromise(JSContext* cx, + Handle<DebuggerObject*> object); + [[nodiscard]] static bool construct(JSContext* cx, unsigned argc, Value* vp); + + struct CallData; + struct PromiseReactionRecordBuilder; + + [[nodiscard]] static bool getErrorReport(JSContext* cx, + HandleObject maybeError, + JSErrorReport*& report); +}; + +} /* namespace js */ + +#endif /* debugger_Object_h */ diff --git a/js/src/debugger/Script-inl.h b/js/src/debugger/Script-inl.h new file mode 100644 index 0000000000..36ac085ba6 --- /dev/null +++ b/js/src/debugger/Script-inl.h @@ -0,0 +1,54 @@ +/* -*- 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/. */ + +#ifndef debugger_Script_inl_h +#define debugger_Script_inl_h + +#include "debugger/Script.h" // for DebuggerScript + +#include "mozilla/Assertions.h" // for AssertionConditionType, MOZ_ASSERT +#include "mozilla/Variant.h" // for AsVariant + +#include <utility> // for move + +#include "jstypes.h" // for JS_PUBLIC_API +#include "debugger/Debugger.h" // for DebuggerScriptReferent +#include "gc/Cell.h" // for Cell +#include "vm/JSScript.h" // for BaseScript, JSScript +#include "vm/NativeObject.h" // for NativeObject +#include "wasm/WasmJS.h" // for WasmInstanceObject + +#include "debugger/Debugger-inl.h" // for Debugger::fromJSObject + +class JS_PUBLIC_API JSObject; + +inline js::Debugger* js::DebuggerScript::owner() const { + JSObject* dbgobj = &getReservedSlot(OWNER_SLOT).toObject(); + return Debugger::fromJSObject(dbgobj); +} + +js::gc::Cell* js::DebuggerScript::getReferentCell() const { + return maybePtrFromReservedSlot<gc::Cell>(SCRIPT_SLOT); +} + +js::DebuggerScriptReferent js::DebuggerScript::getReferent() const { + if (gc::Cell* cell = getReferentCell()) { + if (cell->is<BaseScript>()) { + return mozilla::AsVariant(cell->as<BaseScript>()); + } + MOZ_ASSERT(cell->is<JSObject>()); + return mozilla::AsVariant( + &static_cast<NativeObject*>(cell)->as<WasmInstanceObject>()); + } + return mozilla::AsVariant(static_cast<BaseScript*>(nullptr)); +} + +js::BaseScript* js::DebuggerScript::getReferentScript() const { + gc::Cell* cell = getReferentCell(); + return cell->as<BaseScript>(); +} + +#endif /* debugger_Script_inl_h */ diff --git a/js/src/debugger/Script.cpp b/js/src/debugger/Script.cpp new file mode 100644 index 0000000000..45895a4c4e --- /dev/null +++ b/js/src/debugger/Script.cpp @@ -0,0 +1,2419 @@ +/* -*- 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/Script-inl.h" + +#include "mozilla/Maybe.h" // for Some, Maybe +#include "mozilla/Span.h" // for Span +#include "mozilla/Vector.h" // for Vector + +#include <stddef.h> // for ptrdiff_t +#include <stdint.h> // for uint32_t, SIZE_MAX, int32_t + +#include "jsnum.h" // for ToNumber +#include "NamespaceImports.h" // for CallArgs, RootedValue + +#include "builtin/Array.h" // for NewDenseEmptyArray +#include "debugger/Debugger.h" // for DebuggerScriptReferent, Debugger +#include "debugger/DebugScript.h" // for DebugScript +#include "debugger/Source.h" // for DebuggerSource +#include "gc/GC.h" // for MemoryUse, MemoryUse::Breakpoint +#include "gc/Tracer.h" // for TraceManuallyBarrieredCrossCompartmentEdge +#include "gc/Zone.h" // for Zone +#include "gc/ZoneAllocator.h" // for AddCellMemory +#include "js/CallArgs.h" // for CallArgs, CallArgsFromVp +#include "js/friend/ErrorMessages.h" // for GetErrorMessage, JSMSG_* +#include "js/GCVariant.h" // for GCVariant +#include "js/HeapAPI.h" // for GCCellPtr +#include "js/RootingAPI.h" // for Rooted +#include "js/Wrapper.h" // for UncheckedUnwrap +#include "vm/ArrayObject.h" // for ArrayObject +#include "vm/BytecodeUtil.h" // for GET_JUMP_OFFSET +#include "vm/Compartment.h" // for JS::Compartment +#include "vm/EnvironmentObject.h" // for EnvironmentCoordinateNameSlow +#include "vm/GlobalObject.h" // for GlobalObject +#include "vm/JSContext.h" // for JSContext, ReportValueError +#include "vm/JSFunction.h" // for JSFunction +#include "vm/JSObject.h" // for RequireObject, JSObject +#include "vm/JSScript.h" // for BaseScript +#include "vm/ObjectOperations.h" // for DefineDataProperty, HasOwnProperty +#include "vm/PlainObject.h" // for js::PlainObject +#include "vm/Realm.h" // for AutoRealm +#include "vm/Runtime.h" // for JSAtomState, JSRuntime +#include "vm/StringType.h" // for NameToId, PropertyName, JSAtom +#include "wasm/WasmDebug.h" // for ExprLoc, DebugState +#include "wasm/WasmInstance.h" // for Instance +#include "wasm/WasmJS.h" // for WasmInstanceObject +#include "wasm/WasmTypeDecls.h" // for Bytes + +#include "vm/BytecodeUtil-inl.h" // for BytecodeRangeWithPosition +#include "vm/JSAtom-inl.h" // for ValueToId +#include "vm/JSObject-inl.h" // for NewBuiltinClassInstance, NewObjectWithGivenProto, NewTenuredObjectWithGivenProto +#include "vm/JSScript-inl.h" // for JSScript::global +#include "vm/ObjectOperations-inl.h" // for GetProperty +#include "vm/Realm-inl.h" // for AutoRealm::AutoRealm + +using namespace js; + +using mozilla::Maybe; +using mozilla::Some; + +const JSClassOps DebuggerScript::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + nullptr, // finalize + nullptr, // call + nullptr, // construct + CallTraceMethod<DebuggerScript>, // trace +}; + +const JSClass DebuggerScript::class_ = { + "Script", JSCLASS_HAS_RESERVED_SLOTS(RESERVED_SLOTS), &classOps_}; + +void DebuggerScript::trace(JSTracer* trc) { + // This comes from a private pointer, so no barrier needed. + gc::Cell* cell = getReferentCell(); + if (cell) { + if (cell->is<BaseScript>()) { + BaseScript* script = cell->as<BaseScript>(); + TraceManuallyBarrieredCrossCompartmentEdge( + trc, this, &script, "Debugger.Script script referent"); + if (script != cell->as<BaseScript>()) { + setReservedSlotGCThingAsPrivateUnbarriered(SCRIPT_SLOT, script); + } + } else { + JSObject* wasm = cell->as<JSObject>(); + TraceManuallyBarrieredCrossCompartmentEdge( + trc, this, &wasm, "Debugger.Script wasm referent"); + if (wasm != cell->as<JSObject>()) { + MOZ_ASSERT(wasm->is<WasmInstanceObject>()); + setReservedSlotGCThingAsPrivateUnbarriered(SCRIPT_SLOT, wasm); + } + } + } +} + +/* static */ +NativeObject* DebuggerScript::initClass(JSContext* cx, + Handle<GlobalObject*> global, + HandleObject debugCtor) { + return InitClass(cx, debugCtor, nullptr, nullptr, "Script", construct, 0, + properties_, methods_, nullptr, nullptr); +} + +/* static */ +DebuggerScript* DebuggerScript::create(JSContext* cx, HandleObject proto, + Handle<DebuggerScriptReferent> referent, + Handle<NativeObject*> debugger) { + DebuggerScript* scriptobj = + NewTenuredObjectWithGivenProto<DebuggerScript>(cx, proto); + if (!scriptobj) { + return nullptr; + } + + scriptobj->setReservedSlot(DebuggerScript::OWNER_SLOT, + ObjectValue(*debugger)); + referent.get().match([&](auto& scriptHandle) { + scriptobj->setReservedSlotGCThingAsPrivate(SCRIPT_SLOT, scriptHandle); + }); + + return scriptobj; +} + +static JSScript* DelazifyScript(JSContext* cx, Handle<BaseScript*> script) { + if (script->hasBytecode()) { + return script->asJSScript(); + } + MOZ_ASSERT(script->isFunction()); + + // JSFunction::getOrCreateScript requires an enclosing scope. This requires + // the enclosing script to be non-lazy. + if (script->hasEnclosingScript()) { + Rooted<BaseScript*> enclosingScript(cx, script->enclosingScript()); + if (!DelazifyScript(cx, enclosingScript)) { + return nullptr; + } + + if (!script->isReadyForDelazification()) { + // It didn't work! Delazifying the enclosing script still didn't + // delazify this script. This happens when the function + // corresponding to this script was removed by constant folding. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_OPTIMIZED_OUT_FUN); + return nullptr; + } + } + + MOZ_ASSERT(script->enclosingScope()); + + RootedFunction fun(cx, script->function()); + AutoRealm ar(cx, fun); + return JSFunction::getOrCreateScript(cx, fun); +} + +/* static */ +DebuggerScript* DebuggerScript::check(JSContext* cx, HandleValue v) { + JSObject* thisobj = RequireObject(cx, v); + if (!thisobj) { + return nullptr; + } + if (!thisobj->is<DebuggerScript>()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INCOMPATIBLE_PROTO, "Debugger.Script", + "method", thisobj->getClass()->name); + return nullptr; + } + + return &thisobj->as<DebuggerScript>(); +} + +struct MOZ_STACK_CLASS DebuggerScript::CallData { + JSContext* cx; + const CallArgs& args; + + Handle<DebuggerScript*> obj; + Rooted<DebuggerScriptReferent> referent; + RootedScript script; + + CallData(JSContext* cx, const CallArgs& args, Handle<DebuggerScript*> obj) + : cx(cx), + args(args), + obj(obj), + referent(cx, obj->getReferent()), + script(cx) {} + + [[nodiscard]] bool ensureScriptMaybeLazy() { + if (!referent.is<BaseScript*>()) { + ReportValueError(cx, JSMSG_DEBUG_BAD_REFERENT, JSDVG_SEARCH_STACK, + args.thisv(), nullptr, "a JS script"); + return false; + } + return true; + } + + [[nodiscard]] bool ensureScript() { + if (!ensureScriptMaybeLazy()) { + return false; + } + script = DelazifyScript(cx, referent.as<BaseScript*>()); + if (!script) { + return false; + } + return true; + } + + bool getIsGeneratorFunction(); + bool getIsAsyncFunction(); + bool getIsFunction(); + bool getIsModule(); + bool getDisplayName(); + bool getParameterNames(); + bool getUrl(); + bool getStartLine(); + bool getStartColumn(); + bool getLineCount(); + bool getSource(); + bool getSourceStart(); + bool getSourceLength(); + bool getMainOffset(); + bool getGlobal(); + bool getFormat(); + bool getChildScripts(); + bool getPossibleBreakpoints(); + bool getPossibleBreakpointOffsets(); + bool getOffsetMetadata(); + bool getOffsetLocation(); + bool getEffectfulOffsets(); + bool getAllOffsets(); + bool getAllColumnOffsets(); + bool getLineOffsets(); + bool setBreakpoint(); + bool getBreakpoints(); + bool clearBreakpoint(); + bool clearAllBreakpoints(); + bool isInCatchScope(); + bool getOffsetsCoverage(); + + using Method = bool (CallData::*)(); + + template <Method MyMethod> + static bool ToNative(JSContext* cx, unsigned argc, Value* vp); +}; + +template <DebuggerScript::CallData::Method MyMethod> +/* static */ +bool DebuggerScript::CallData::ToNative(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<DebuggerScript*> obj(cx, DebuggerScript::check(cx, args.thisv())); + if (!obj) { + return false; + } + + CallData data(cx, args, obj); + return (data.*MyMethod)(); +} + +bool DebuggerScript::CallData::getIsGeneratorFunction() { + if (!ensureScriptMaybeLazy()) { + return false; + } + args.rval().setBoolean(obj->getReferentScript()->isGenerator()); + return true; +} + +bool DebuggerScript::CallData::getIsAsyncFunction() { + if (!ensureScriptMaybeLazy()) { + return false; + } + args.rval().setBoolean(obj->getReferentScript()->isAsync()); + return true; +} + +bool DebuggerScript::CallData::getIsFunction() { + if (!ensureScriptMaybeLazy()) { + return false; + } + + args.rval().setBoolean(obj->getReferentScript()->function()); + return true; +} + +bool DebuggerScript::CallData::getIsModule() { + if (!ensureScriptMaybeLazy()) { + return false; + } + BaseScript* script = referent.as<BaseScript*>(); + + args.rval().setBoolean(script->isModule()); + return true; +} + +bool DebuggerScript::CallData::getDisplayName() { + if (!ensureScriptMaybeLazy()) { + return false; + } + JSFunction* func = obj->getReferentScript()->function(); + Debugger* dbg = obj->owner(); + + JSString* name = func ? func->displayAtom() : nullptr; + if (!name) { + args.rval().setUndefined(); + return true; + } + + RootedValue namev(cx, StringValue(name)); + if (!dbg->wrapDebuggeeValue(cx, &namev)) { + return false; + } + args.rval().set(namev); + return true; +} + +bool DebuggerScript::CallData::getParameterNames() { + if (!ensureScript()) { + return false; + } + + RootedFunction fun(cx, referent.as<BaseScript*>()->function()); + if (!fun) { + args.rval().setUndefined(); + return true; + } + + ArrayObject* arr = GetFunctionParameterNamesArray(cx, fun); + if (!arr) { + return false; + } + + args.rval().setObject(*arr); + return true; +} + +bool DebuggerScript::CallData::getUrl() { + if (!ensureScriptMaybeLazy()) { + return false; + } + + Rooted<BaseScript*> script(cx, referent.as<BaseScript*>()); + + if (script->filename()) { + JSString* str; + if (script->scriptSource()->introducerFilename()) { + str = NewStringCopyZ<CanGC>(cx, + script->scriptSource()->introducerFilename()); + } else { + str = NewStringCopyZ<CanGC>(cx, script->filename()); + } + if (!str) { + return false; + } + args.rval().setString(str); + } else { + args.rval().setNull(); + } + return true; +} + +bool DebuggerScript::CallData::getStartLine() { + args.rval().setNumber( + referent.get().match([](BaseScript*& s) { return s->lineno(); }, + [](WasmInstanceObject*&) { return (uint32_t)1; })); + return true; +} + +bool DebuggerScript::CallData::getStartColumn() { + args.rval().setNumber( + referent.get().match([](BaseScript*& s) { return s->column(); }, + [](WasmInstanceObject*&) { return (uint32_t)0; })); + return true; +} + +struct DebuggerScript::GetLineCountMatcher { + JSContext* cx_; + double totalLines; + + explicit GetLineCountMatcher(JSContext* cx) : cx_(cx), totalLines(0.0) {} + using ReturnType = bool; + + ReturnType match(Handle<BaseScript*> base) { + RootedScript script(cx_, DelazifyScript(cx_, base)); + if (!script) { + return false; + } + totalLines = double(GetScriptLineExtent(script)); + return true; + } + ReturnType match(Handle<WasmInstanceObject*> instanceObj) { + wasm::Instance& instance = instanceObj->instance(); + if (instance.debugEnabled()) { + totalLines = double(instance.debug().bytecode().length()); + } else { + totalLines = 0; + } + return true; + } +}; + +bool DebuggerScript::CallData::getLineCount() { + GetLineCountMatcher matcher(cx); + if (!referent.match(matcher)) { + return false; + } + args.rval().setNumber(matcher.totalLines); + return true; +} + +class DebuggerScript::GetSourceMatcher { + JSContext* cx_; + Debugger* dbg_; + + public: + GetSourceMatcher(JSContext* cx, Debugger* dbg) : cx_(cx), dbg_(dbg) {} + + using ReturnType = DebuggerSource*; + + ReturnType match(Handle<BaseScript*> script) { + Rooted<ScriptSourceObject*> source(cx_, script->sourceObject()); + return dbg_->wrapSource(cx_, source); + } + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { + return dbg_->wrapWasmSource(cx_, wasmInstance); + } +}; + +bool DebuggerScript::CallData::getSource() { + Debugger* dbg = obj->owner(); + + GetSourceMatcher matcher(cx, dbg); + Rooted<DebuggerSource*> sourceObject(cx, referent.match(matcher)); + if (!sourceObject) { + return false; + } + + args.rval().setObject(*sourceObject); + return true; +} + +bool DebuggerScript::CallData::getSourceStart() { + if (!ensureScriptMaybeLazy()) { + return false; + } + args.rval().setNumber(uint32_t(obj->getReferentScript()->sourceStart())); + return true; +} + +bool DebuggerScript::CallData::getSourceLength() { + if (!ensureScriptMaybeLazy()) { + return false; + } + args.rval().setNumber(uint32_t(obj->getReferentScript()->sourceLength())); + return true; +} + +bool DebuggerScript::CallData::getMainOffset() { + if (!ensureScript()) { + return false; + } + args.rval().setNumber(uint32_t(script->mainOffset())); + return true; +} + +bool DebuggerScript::CallData::getGlobal() { + if (!ensureScript()) { + return false; + } + Debugger* dbg = obj->owner(); + + RootedValue v(cx, ObjectValue(script->global())); + if (!dbg->wrapDebuggeeValue(cx, &v)) { + return false; + } + args.rval().set(v); + return true; +} + +bool DebuggerScript::CallData::getFormat() { + args.rval().setString(referent.get().match( + [this](BaseScript*&) { return cx->names().js.get(); }, + [this](WasmInstanceObject*&) { return cx->names().wasm.get(); })); + return true; +} + +static bool PushFunctionScript(JSContext* cx, Debugger* dbg, HandleFunction fun, + HandleObject array) { + // Ignore asm.js natives. + if (!IsInterpretedNonSelfHostedFunction(fun)) { + return true; + } + + Rooted<BaseScript*> script(cx, fun->baseScript()); + MOZ_ASSERT(script); + if (!script) { + // If the function doesn't have script, ignore it. + return true; + } + RootedObject wrapped(cx, dbg->wrapScript(cx, script)); + if (!wrapped) { + return false; + } + + return NewbornArrayPush(cx, array, ObjectValue(*wrapped)); +} + +static bool PushInnerFunctions(JSContext* cx, Debugger* dbg, HandleObject array, + mozilla::Span<const JS::GCCellPtr> gcThings) { + RootedFunction fun(cx); + + for (JS::GCCellPtr gcThing : gcThings) { + if (!gcThing.is<JSObject>()) { + continue; + } + + JSObject* obj = &gcThing.as<JSObject>(); + if (obj->is<JSFunction>()) { + fun = &obj->as<JSFunction>(); + + // Ignore any delazification placeholder functions. These should not be + // exposed to debugger in any way. + if (fun->isGhost()) { + continue; + } + + if (!PushFunctionScript(cx, dbg, fun, array)) { + return false; + } + } + } + + return true; +} + +bool DebuggerScript::CallData::getChildScripts() { + if (!ensureScriptMaybeLazy()) { + return false; + } + Debugger* dbg = obj->owner(); + + RootedObject result(cx, NewDenseEmptyArray(cx)); + if (!result) { + return false; + } + + Rooted<BaseScript*> script(cx, obj->getReferent().as<BaseScript*>()); + if (!PushInnerFunctions(cx, dbg, result, script->gcthings())) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +static bool ScriptOffset(JSContext* cx, const Value& v, size_t* offsetp) { + double d; + size_t off; + + bool ok = v.isNumber(); + if (ok) { + d = v.toNumber(); + off = size_t(d); + } + if (!ok || off != d) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_BAD_OFFSET); + return false; + } + *offsetp = off; + return true; +} + +static bool EnsureScriptOffsetIsValid(JSContext* cx, JSScript* script, + size_t offset) { + if (IsValidBytecodeOffset(cx, script, offset)) { + return true; + } + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_BAD_OFFSET); + return false; +} + +static bool IsGeneratorSlotInitialization(JSScript* script, size_t offset, + JSContext* cx) { + jsbytecode* pc = script->offsetToPC(offset); + if (JSOp(*pc) != JSOp::SetAliasedVar) { + return false; + } + + PropertyName* name = EnvironmentCoordinateNameSlow(script, pc); + return name == cx->names().dotGenerator; +} + +static bool EnsureBreakpointIsAllowed(JSContext* cx, JSScript* script, + size_t offset) { + // Disallow breakpoint for `JSOp::SetAliasedVar` after `JSOp::Generator`. + // Those 2 instructions are supposed to be atomic, and nothing should happen + // in between them. + // + // Hitting a breakpoint there breaks the assumption around the existence of + // the frame's `GeneratorInfo`. + // (see `DebugAPI::slowPathOnNewGenerator` and `DebuggerFrame::create`) + if (IsGeneratorSlotInitialization(script, offset, cx)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_BREAKPOINT_NOT_ALLOWED); + return false; + } + + return true; +} + +template <bool OnlyOffsets> +class DebuggerScript::GetPossibleBreakpointsMatcher { + JSContext* cx_; + MutableHandleObject result_; + + Maybe<size_t> minOffset; + Maybe<size_t> maxOffset; + + Maybe<size_t> minLine; + size_t minColumn; + Maybe<size_t> maxLine; + size_t maxColumn; + + bool passesQuery(size_t offset, size_t lineno, size_t colno) { + // [minOffset, maxOffset) - Inclusive minimum and exclusive maximum. + if ((minOffset && offset < *minOffset) || + (maxOffset && offset >= *maxOffset)) { + return false; + } + + if (minLine) { + if (lineno < *minLine || (lineno == *minLine && colno < minColumn)) { + return false; + } + } + + if (maxLine) { + if (lineno > *maxLine || (lineno == *maxLine && colno >= maxColumn)) { + return false; + } + } + + return true; + } + + bool maybeAppendEntry(size_t offset, size_t lineno, size_t colno, + bool isStepStart) { + if (!passesQuery(offset, lineno, colno)) { + return true; + } + + if (OnlyOffsets) { + if (!NewbornArrayPush(cx_, result_, NumberValue(offset))) { + return false; + } + + return true; + } + + Rooted<PlainObject*> entry(cx_, NewPlainObject(cx_)); + if (!entry) { + return false; + } + + RootedValue value(cx_, NumberValue(offset)); + if (!DefineDataProperty(cx_, entry, cx_->names().offset, value)) { + return false; + } + + value = NumberValue(lineno); + if (!DefineDataProperty(cx_, entry, cx_->names().lineNumber, value)) { + return false; + } + + value = NumberValue(colno); + if (!DefineDataProperty(cx_, entry, cx_->names().columnNumber, value)) { + return false; + } + + value = BooleanValue(isStepStart); + if (!DefineDataProperty(cx_, entry, cx_->names().isStepStart, value)) { + return false; + } + + if (!NewbornArrayPush(cx_, result_, ObjectValue(*entry))) { + return false; + } + return true; + } + + bool parseIntValue(HandleValue value, size_t* result) { + if (!value.isNumber()) { + return false; + } + + double doubleOffset = value.toNumber(); + if (doubleOffset < 0 || (unsigned int)doubleOffset != doubleOffset) { + return false; + } + + *result = doubleOffset; + return true; + } + + bool parseIntValue(HandleValue value, Maybe<size_t>* result) { + size_t result_; + if (!parseIntValue(value, &result_)) { + return false; + } + + *result = Some(result_); + return true; + } + + public: + explicit GetPossibleBreakpointsMatcher(JSContext* cx, + MutableHandleObject result) + : cx_(cx), + result_(result), + minOffset(), + maxOffset(), + minLine(), + minColumn(0), + maxLine(), + maxColumn(0) {} + + bool parseQuery(HandleObject query) { + RootedValue lineValue(cx_); + if (!GetProperty(cx_, query, query, cx_->names().line, &lineValue)) { + return false; + } + + RootedValue minLineValue(cx_); + if (!GetProperty(cx_, query, query, cx_->names().minLine, &minLineValue)) { + return false; + } + + RootedValue minColumnValue(cx_); + if (!GetProperty(cx_, query, query, cx_->names().minColumn, + &minColumnValue)) { + return false; + } + + RootedValue minOffsetValue(cx_); + if (!GetProperty(cx_, query, query, cx_->names().minOffset, + &minOffsetValue)) { + return false; + } + + RootedValue maxLineValue(cx_); + if (!GetProperty(cx_, query, query, cx_->names().maxLine, &maxLineValue)) { + return false; + } + + RootedValue maxColumnValue(cx_); + if (!GetProperty(cx_, query, query, cx_->names().maxColumn, + &maxColumnValue)) { + return false; + } + + RootedValue maxOffsetValue(cx_); + if (!GetProperty(cx_, query, query, cx_->names().maxOffset, + &maxOffsetValue)) { + return false; + } + + if (!minOffsetValue.isUndefined()) { + if (!parseIntValue(minOffsetValue, &minOffset)) { + JS_ReportErrorNumberASCII( + cx_, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "getPossibleBreakpoints' 'minOffset'", "not an integer"); + return false; + } + } + if (!maxOffsetValue.isUndefined()) { + if (!parseIntValue(maxOffsetValue, &maxOffset)) { + JS_ReportErrorNumberASCII( + cx_, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "getPossibleBreakpoints' 'maxOffset'", "not an integer"); + return false; + } + } + + if (!lineValue.isUndefined()) { + if (!minLineValue.isUndefined() || !maxLineValue.isUndefined()) { + JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr, + JSMSG_UNEXPECTED_TYPE, + "getPossibleBreakpoints' 'line'", + "not allowed alongside 'minLine'/'maxLine'"); + return false; + } + + size_t line; + if (!parseIntValue(lineValue, &line)) { + JS_ReportErrorNumberASCII( + cx_, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "getPossibleBreakpoints' 'line'", "not an integer"); + return false; + } + + // If no end column is given, we use the default of 0 and wrap to + // the next line. + minLine = Some(line); + maxLine = Some(line + (maxColumnValue.isUndefined() ? 1 : 0)); + } + + if (!minLineValue.isUndefined()) { + if (!parseIntValue(minLineValue, &minLine)) { + JS_ReportErrorNumberASCII( + cx_, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "getPossibleBreakpoints' 'minLine'", "not an integer"); + return false; + } + } + + if (!minColumnValue.isUndefined()) { + if (!minLine) { + JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr, + JSMSG_UNEXPECTED_TYPE, + "getPossibleBreakpoints' 'minColumn'", + "not allowed without 'line' or 'minLine'"); + return false; + } + + if (!parseIntValue(minColumnValue, &minColumn)) { + JS_ReportErrorNumberASCII( + cx_, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "getPossibleBreakpoints' 'minColumn'", "not an integer"); + return false; + } + } + + if (!maxLineValue.isUndefined()) { + if (!parseIntValue(maxLineValue, &maxLine)) { + JS_ReportErrorNumberASCII( + cx_, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "getPossibleBreakpoints' 'maxLine'", "not an integer"); + return false; + } + } + + if (!maxColumnValue.isUndefined()) { + if (!maxLine) { + JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr, + JSMSG_UNEXPECTED_TYPE, + "getPossibleBreakpoints' 'maxColumn'", + "not allowed without 'line' or 'maxLine'"); + return false; + } + + if (!parseIntValue(maxColumnValue, &maxColumn)) { + JS_ReportErrorNumberASCII( + cx_, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "getPossibleBreakpoints' 'maxColumn'", "not an integer"); + return false; + } + } + + return true; + } + + using ReturnType = bool; + ReturnType match(Handle<BaseScript*> base) { + RootedScript script(cx_, DelazifyScript(cx_, base)); + if (!script) { + return false; + } + + // Second pass: build the result array. + result_.set(NewDenseEmptyArray(cx_)); + if (!result_) { + return false; + } + + for (BytecodeRangeWithPosition r(cx_, script); !r.empty(); r.popFront()) { + if (!r.frontIsBreakablePoint()) { + continue; + } + + size_t offset = r.frontOffset(); + size_t lineno = r.frontLineNumber(); + size_t colno = r.frontColumnNumber(); + + if (!maybeAppendEntry(offset, lineno, colno, + r.frontIsBreakableStepPoint())) { + return false; + } + } + + return true; + } + ReturnType match(Handle<WasmInstanceObject*> instanceObj) { + wasm::Instance& instance = instanceObj->instance(); + + Vector<wasm::ExprLoc> offsets(cx_); + if (instance.debugEnabled() && + !instance.debug().getAllColumnOffsets(&offsets)) { + return false; + } + + result_.set(NewDenseEmptyArray(cx_)); + if (!result_) { + return false; + } + + for (uint32_t i = 0; i < offsets.length(); i++) { + size_t lineno = offsets[i].lineno; + size_t column = offsets[i].column; + size_t offset = offsets[i].offset; + if (!maybeAppendEntry(offset, lineno, column, true)) { + return false; + } + } + return true; + } +}; + +bool DebuggerScript::CallData::getPossibleBreakpoints() { + RootedObject result(cx); + GetPossibleBreakpointsMatcher<false> matcher(cx, &result); + if (args.length() >= 1 && !args[0].isUndefined()) { + RootedObject queryObject(cx, RequireObject(cx, args[0])); + if (!queryObject || !matcher.parseQuery(queryObject)) { + return false; + } + } + if (!referent.match(matcher)) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +bool DebuggerScript::CallData::getPossibleBreakpointOffsets() { + RootedObject result(cx); + GetPossibleBreakpointsMatcher<true> matcher(cx, &result); + if (args.length() >= 1 && !args[0].isUndefined()) { + RootedObject queryObject(cx, RequireObject(cx, args[0])); + if (!queryObject || !matcher.parseQuery(queryObject)) { + return false; + } + } + if (!referent.match(matcher)) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +class DebuggerScript::GetOffsetMetadataMatcher { + JSContext* cx_; + size_t offset_; + MutableHandle<PlainObject*> result_; + + public: + explicit GetOffsetMetadataMatcher(JSContext* cx, size_t offset, + MutableHandle<PlainObject*> result) + : cx_(cx), offset_(offset), result_(result) {} + using ReturnType = bool; + ReturnType match(Handle<BaseScript*> base) { + RootedScript script(cx_, DelazifyScript(cx_, base)); + if (!script) { + return false; + } + + if (!EnsureScriptOffsetIsValid(cx_, script, offset_)) { + return false; + } + + result_.set(NewPlainObject(cx_)); + if (!result_) { + return false; + } + + BytecodeRangeWithPosition r(cx_, script); + while (!r.empty() && r.frontOffset() < offset_) { + r.popFront(); + } + + RootedValue value(cx_, NumberValue(r.frontLineNumber())); + if (!DefineDataProperty(cx_, result_, cx_->names().lineNumber, value)) { + return false; + } + + value = NumberValue(r.frontColumnNumber()); + if (!DefineDataProperty(cx_, result_, cx_->names().columnNumber, value)) { + return false; + } + + value = BooleanValue(r.frontIsBreakablePoint()); + if (!DefineDataProperty(cx_, result_, cx_->names().isBreakpoint, value)) { + return false; + } + + value = BooleanValue(r.frontIsBreakableStepPoint()); + if (!DefineDataProperty(cx_, result_, cx_->names().isStepStart, value)) { + return false; + } + + return true; + } + ReturnType match(Handle<WasmInstanceObject*> instanceObj) { + wasm::Instance& instance = instanceObj->instance(); + if (!instance.debugEnabled()) { + JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr, + JSMSG_DEBUG_BAD_OFFSET); + return false; + } + + size_t lineno; + size_t column; + if (!instance.debug().getOffsetLocation(offset_, &lineno, &column)) { + JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr, + JSMSG_DEBUG_BAD_OFFSET); + return false; + } + + result_.set(NewPlainObject(cx_)); + if (!result_) { + return false; + } + + RootedValue value(cx_, NumberValue(lineno)); + if (!DefineDataProperty(cx_, result_, cx_->names().lineNumber, value)) { + return false; + } + + value = NumberValue(column); + if (!DefineDataProperty(cx_, result_, cx_->names().columnNumber, value)) { + return false; + } + + value.setBoolean(true); + if (!DefineDataProperty(cx_, result_, cx_->names().isBreakpoint, value)) { + return false; + } + + value.setBoolean(true); + if (!DefineDataProperty(cx_, result_, cx_->names().isStepStart, value)) { + return false; + } + + return true; + } +}; + +bool DebuggerScript::CallData::getOffsetMetadata() { + if (!args.requireAtLeast(cx, "Debugger.Script.getOffsetMetadata", 1)) { + return false; + } + size_t offset; + if (!ScriptOffset(cx, args[0], &offset)) { + return false; + } + + Rooted<PlainObject*> result(cx); + GetOffsetMetadataMatcher matcher(cx, offset, &result); + if (!referent.match(matcher)) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +namespace { + +/* + * FlowGraphSummary::populate(cx, script) computes a summary of script's + * control flow graph used by DebuggerScript_{getAllOffsets,getLineOffsets}. + * + * An instruction on a given line is an entry point for that line if it can be + * reached from (an instruction on) a different line. We distinguish between the + * following cases: + * - hasNoEdges: + * The instruction cannot be reached, so the instruction is not an entry + * point for the line it is on. + * - hasSingleEdge: + * The instruction can be reached from a single line. If this line is + * different from the line the instruction is on, the instruction is an + * entry point for that line. + * + * Similarly, an instruction on a given position (line/column pair) is an + * entry point for that position if it can be reached from (an instruction on) a + * different position. Again, we distinguish between the following cases: + * - hasNoEdges: + * The instruction cannot be reached, so the instruction is not an entry + * point for the position it is on. + * - hasSingleEdge: + * The instruction can be reached from a single position. If this line is + * different from the position the instruction is on, the instruction is + * an entry point for that position. + */ +class FlowGraphSummary { + public: + class Entry { + public: + static Entry createWithSingleEdge(size_t lineno, size_t column) { + return Entry(lineno, column); + } + + static Entry createWithMultipleEdgesFromSingleLine(size_t lineno) { + return Entry(lineno, SIZE_MAX); + } + + static Entry createWithMultipleEdgesFromMultipleLines() { + return Entry(SIZE_MAX, SIZE_MAX); + } + + Entry() : lineno_(SIZE_MAX), column_(0) {} + + bool hasNoEdges() const { + return lineno_ == SIZE_MAX && column_ != SIZE_MAX; + } + + bool hasSingleEdge() const { + return lineno_ != SIZE_MAX && column_ != SIZE_MAX; + } + + size_t lineno() const { return lineno_; } + + size_t column() const { return column_; } + + private: + Entry(size_t lineno, size_t column) : lineno_(lineno), column_(column) {} + + size_t lineno_; + size_t column_; + }; + + explicit FlowGraphSummary(JSContext* cx) : entries_(cx) {} + + Entry& operator[](size_t index) { return entries_[index]; } + + bool populate(JSContext* cx, JSScript* script) { + if (!entries_.growBy(script->length())) { + return false; + } + unsigned mainOffset = script->pcToOffset(script->main()); + entries_[mainOffset] = Entry::createWithMultipleEdgesFromMultipleLines(); + + size_t prevLineno = script->lineno(); + size_t prevColumn = 0; + JSOp prevOp = JSOp::Nop; + for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) { + size_t lineno = prevLineno; + size_t column = prevColumn; + JSOp op = r.frontOpcode(); + + if (BytecodeFallsThrough(prevOp)) { + addEdge(prevLineno, prevColumn, r.frontOffset()); + } + + // If we visit the branch target before we visit the + // branch op itself, just reuse the previous location. + // This is reasonable for the time being because this + // situation can currently only arise from loop heads, + // where this assumption holds. + if (BytecodeIsJumpTarget(op) && !entries_[r.frontOffset()].hasNoEdges()) { + lineno = entries_[r.frontOffset()].lineno(); + column = entries_[r.frontOffset()].column(); + } + + if (r.frontIsEntryPoint()) { + lineno = r.frontLineNumber(); + column = r.frontColumnNumber(); + } + + if (IsJumpOpcode(op)) { + addEdge(lineno, column, r.frontOffset() + GET_JUMP_OFFSET(r.frontPC())); + } else if (op == JSOp::TableSwitch) { + jsbytecode* const switchPC = r.frontPC(); + jsbytecode* pc = switchPC; + size_t offset = r.frontOffset(); + ptrdiff_t step = JUMP_OFFSET_LEN; + size_t defaultOffset = offset + GET_JUMP_OFFSET(pc); + pc += step; + addEdge(lineno, column, defaultOffset); + + int32_t low = GET_JUMP_OFFSET(pc); + pc += JUMP_OFFSET_LEN; + int ncases = GET_JUMP_OFFSET(pc) - low + 1; + pc += JUMP_OFFSET_LEN; + + for (int i = 0; i < ncases; i++) { + size_t target = script->tableSwitchCaseOffset(switchPC, i); + addEdge(lineno, column, target); + } + } else if (op == JSOp::Try) { + // As there is no literal incoming edge into the catch block, we + // make a fake one by copying the JSOp::Try location, as-if this + // was an incoming edge of the catch block. This is needed + // because we only report offsets of entry points which have + // valid incoming edges. + for (const TryNote& tn : script->trynotes()) { + if (tn.start == r.frontOffset() + JSOpLength_Try) { + uint32_t catchOffset = tn.start + tn.length; + if (tn.kind() == TryNoteKind::Catch || + tn.kind() == TryNoteKind::Finally) { + addEdge(lineno, column, catchOffset); + } + } + } + } + + prevLineno = lineno; + prevColumn = column; + prevOp = op; + } + + return true; + } + + private: + void addEdge(size_t sourceLineno, size_t sourceColumn, size_t targetOffset) { + if (entries_[targetOffset].hasNoEdges()) { + entries_[targetOffset] = + Entry::createWithSingleEdge(sourceLineno, sourceColumn); + } else if (entries_[targetOffset].lineno() != sourceLineno) { + entries_[targetOffset] = + Entry::createWithMultipleEdgesFromMultipleLines(); + } else if (entries_[targetOffset].column() != sourceColumn) { + entries_[targetOffset] = + Entry::createWithMultipleEdgesFromSingleLine(sourceLineno); + } + } + + Vector<Entry> entries_; +}; + +} /* anonymous namespace */ + +class DebuggerScript::GetOffsetLocationMatcher { + JSContext* cx_; + size_t offset_; + MutableHandle<PlainObject*> result_; + + public: + explicit GetOffsetLocationMatcher(JSContext* cx, size_t offset, + MutableHandle<PlainObject*> result) + : cx_(cx), offset_(offset), result_(result) {} + using ReturnType = bool; + ReturnType match(Handle<BaseScript*> base) { + RootedScript script(cx_, DelazifyScript(cx_, base)); + if (!script) { + return false; + } + + if (!EnsureScriptOffsetIsValid(cx_, script, offset_)) { + return false; + } + + FlowGraphSummary flowData(cx_); + if (!flowData.populate(cx_, script)) { + return false; + } + + result_.set(NewPlainObject(cx_)); + if (!result_) { + return false; + } + + BytecodeRangeWithPosition r(cx_, script); + while (!r.empty() && r.frontOffset() < offset_) { + r.popFront(); + } + + size_t offset = r.frontOffset(); + bool isEntryPoint = r.frontIsEntryPoint(); + + // Line numbers are only correctly defined on entry points. Thus looks + // either for the next valid offset in the flowData, being the last entry + // point flowing into the current offset, or for the next valid entry point. + while (!r.frontIsEntryPoint() && + !flowData[r.frontOffset()].hasSingleEdge()) { + r.popFront(); + MOZ_ASSERT(!r.empty()); + } + + // If this is an entry point, take the line number associated with the entry + // point, otherwise settle on the next instruction and take the incoming + // edge position. + size_t lineno; + size_t column; + if (r.frontIsEntryPoint()) { + lineno = r.frontLineNumber(); + column = r.frontColumnNumber(); + } else { + MOZ_ASSERT(flowData[r.frontOffset()].hasSingleEdge()); + lineno = flowData[r.frontOffset()].lineno(); + column = flowData[r.frontOffset()].column(); + } + + RootedValue value(cx_, NumberValue(lineno)); + if (!DefineDataProperty(cx_, result_, cx_->names().lineNumber, value)) { + return false; + } + + value = NumberValue(column); + if (!DefineDataProperty(cx_, result_, cx_->names().columnNumber, value)) { + return false; + } + + // The same entry point test that is used by getAllColumnOffsets. + isEntryPoint = (isEntryPoint && !flowData[offset].hasNoEdges() && + (flowData[offset].lineno() != r.frontLineNumber() || + flowData[offset].column() != r.frontColumnNumber())); + value.setBoolean(isEntryPoint); + if (!DefineDataProperty(cx_, result_, cx_->names().isEntryPoint, value)) { + return false; + } + + return true; + } + ReturnType match(Handle<WasmInstanceObject*> instanceObj) { + wasm::Instance& instance = instanceObj->instance(); + if (!instance.debugEnabled()) { + JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr, + JSMSG_DEBUG_BAD_OFFSET); + return false; + } + + size_t lineno; + size_t column; + if (!instance.debug().getOffsetLocation(offset_, &lineno, &column)) { + JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr, + JSMSG_DEBUG_BAD_OFFSET); + return false; + } + + result_.set(NewPlainObject(cx_)); + if (!result_) { + return false; + } + + RootedValue value(cx_, NumberValue(lineno)); + if (!DefineDataProperty(cx_, result_, cx_->names().lineNumber, value)) { + return false; + } + + value = NumberValue(column); + if (!DefineDataProperty(cx_, result_, cx_->names().columnNumber, value)) { + return false; + } + + value.setBoolean(true); + if (!DefineDataProperty(cx_, result_, cx_->names().isEntryPoint, value)) { + return false; + } + + return true; + } +}; + +bool DebuggerScript::CallData::getOffsetLocation() { + if (!args.requireAtLeast(cx, "Debugger.Script.getOffsetLocation", 1)) { + return false; + } + size_t offset; + if (!ScriptOffset(cx, args[0], &offset)) { + return false; + } + + Rooted<PlainObject*> result(cx); + GetOffsetLocationMatcher matcher(cx, offset, &result); + if (!referent.match(matcher)) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +// Return whether an opcode is considered effectful: it can have direct side +// effects that can be observed outside of the current frame. Opcodes are not +// effectful if they only modify the current frame's state, modify objects +// created by the current frame, or can potentially call other scripts or +// natives which could have side effects. +static bool BytecodeIsEffectful(JSScript* script, size_t offset) { + jsbytecode* pc = script->offsetToPC(offset); + JSOp op = JSOp(*pc); + switch (op) { + case JSOp::SetProp: + case JSOp::StrictSetProp: + case JSOp::SetPropSuper: + case JSOp::StrictSetPropSuper: + case JSOp::SetElem: + case JSOp::StrictSetElem: + case JSOp::SetElemSuper: + case JSOp::StrictSetElemSuper: + case JSOp::SetName: + case JSOp::StrictSetName: + case JSOp::SetGName: + case JSOp::StrictSetGName: + case JSOp::DelProp: + case JSOp::StrictDelProp: + case JSOp::DelElem: + case JSOp::StrictDelElem: + case JSOp::DelName: + case JSOp::SetAliasedVar: + case JSOp::InitHomeObject: + case JSOp::SetIntrinsic: + case JSOp::InitGLexical: + case JSOp::GlobalOrEvalDeclInstantiation: + case JSOp::SetFunName: + case JSOp::MutateProto: + case JSOp::DynamicImport: + case JSOp::InitialYield: + case JSOp::Yield: + return true; + + case JSOp::Nop: + case JSOp::NopDestructuring: + case JSOp::TryDestructuring: + case JSOp::Lineno: + case JSOp::JumpTarget: + case JSOp::Undefined: + case JSOp::JumpIfTrue: + case JSOp::JumpIfFalse: + case JSOp::Return: + case JSOp::RetRval: + case JSOp::And: + case JSOp::Or: + case JSOp::Coalesce: + case JSOp::Try: + case JSOp::Throw: + case JSOp::Goto: + case JSOp::TableSwitch: + case JSOp::Case: + case JSOp::Default: + case JSOp::BitNot: + case JSOp::BitAnd: + case JSOp::BitOr: + case JSOp::BitXor: + case JSOp::Lsh: + case JSOp::Rsh: + case JSOp::Ursh: + case JSOp::Add: + case JSOp::Sub: + case JSOp::Mul: + case JSOp::Div: + case JSOp::Mod: + case JSOp::Pow: + case JSOp::Pos: + case JSOp::ToNumeric: + case JSOp::Neg: + case JSOp::Inc: + case JSOp::Dec: + case JSOp::ToString: + case JSOp::Eq: + case JSOp::Ne: + case JSOp::StrictEq: + case JSOp::StrictNe: + case JSOp::Lt: + case JSOp::Le: + case JSOp::Gt: + case JSOp::Ge: + case JSOp::Double: + case JSOp::BigInt: + case JSOp::String: + case JSOp::Symbol: + case JSOp::Zero: + case JSOp::One: + case JSOp::Null: + case JSOp::Void: + case JSOp::Hole: + case JSOp::False: + case JSOp::True: + case JSOp::Arguments: + case JSOp::Rest: + case JSOp::GetArg: + case JSOp::SetArg: + case JSOp::GetLocal: + case JSOp::SetLocal: + case JSOp::ThrowSetConst: + case JSOp::CheckLexical: + case JSOp::CheckAliasedLexical: + case JSOp::InitLexical: + case JSOp::Uninitialized: + case JSOp::Pop: + case JSOp::PopN: + case JSOp::DupAt: + case JSOp::NewArray: + case JSOp::NewInit: + case JSOp::NewObject: + case JSOp::InitElem: + case JSOp::InitHiddenElem: + case JSOp::InitLockedElem: + case JSOp::InitElemInc: + case JSOp::InitElemArray: + case JSOp::InitProp: + case JSOp::InitLockedProp: + case JSOp::InitHiddenProp: + case JSOp::InitPropGetter: + case JSOp::InitHiddenPropGetter: + case JSOp::InitPropSetter: + case JSOp::InitHiddenPropSetter: + case JSOp::InitElemGetter: + case JSOp::InitHiddenElemGetter: + case JSOp::InitElemSetter: + case JSOp::InitHiddenElemSetter: + case JSOp::SpreadCall: + case JSOp::Call: + case JSOp::CallContent: + case JSOp::CallIgnoresRv: + case JSOp::CallIter: + case JSOp::CallContentIter: + case JSOp::New: + case JSOp::NewContent: + case JSOp::Eval: + case JSOp::StrictEval: + case JSOp::Int8: + case JSOp::Uint16: + case JSOp::ResumeKind: + case JSOp::GetGName: + case JSOp::GetName: + case JSOp::GetIntrinsic: + case JSOp::GetImport: + case JSOp::BindGName: + case JSOp::BindName: + case JSOp::BindVar: + case JSOp::Dup: + case JSOp::Dup2: + case JSOp::Swap: + case JSOp::Pick: + case JSOp::Unpick: + case JSOp::GetAliasedDebugVar: + case JSOp::GetAliasedVar: + case JSOp::Uint24: + case JSOp::Int32: + case JSOp::LoopHead: + case JSOp::GetElem: + case JSOp::Not: + case JSOp::FunctionThis: + case JSOp::GlobalThis: + case JSOp::NonSyntacticGlobalThis: + case JSOp::Callee: + case JSOp::EnvCallee: + case JSOp::SuperBase: + case JSOp::GetPropSuper: + case JSOp::GetElemSuper: + case JSOp::GetProp: + case JSOp::RegExp: + case JSOp::CallSiteObj: + case JSOp::Object: + case JSOp::Typeof: + case JSOp::TypeofExpr: + case JSOp::ToAsyncIter: + case JSOp::ToPropertyKey: + case JSOp::Lambda: + case JSOp::PushLexicalEnv: + case JSOp::PopLexicalEnv: + case JSOp::FreshenLexicalEnv: + case JSOp::RecreateLexicalEnv: + case JSOp::PushClassBodyEnv: + case JSOp::Iter: + case JSOp::MoreIter: + case JSOp::IsNoIter: + case JSOp::EndIter: + case JSOp::CloseIter: + case JSOp::IsNullOrUndefined: + case JSOp::In: + case JSOp::HasOwn: + case JSOp::CheckPrivateField: + case JSOp::NewPrivateName: + case JSOp::SetRval: + case JSOp::Instanceof: + case JSOp::DebugLeaveLexicalEnv: + case JSOp::Debugger: + case JSOp::ImplicitThis: + case JSOp::NewTarget: + case JSOp::CheckIsObj: + case JSOp::CheckObjCoercible: + case JSOp::DebugCheckSelfHosted: + case JSOp::IsConstructing: + case JSOp::OptimizeSpreadCall: + case JSOp::ImportMeta: + case JSOp::EnterWith: + case JSOp::LeaveWith: + case JSOp::SpreadNew: + case JSOp::SpreadEval: + case JSOp::StrictSpreadEval: + case JSOp::CheckClassHeritage: + case JSOp::FunWithProto: + case JSOp::ObjWithProto: + case JSOp::BuiltinObject: + case JSOp::CheckThis: + case JSOp::CheckReturn: + case JSOp::CheckThisReinit: + case JSOp::SuperFun: + case JSOp::SpreadSuperCall: + case JSOp::SuperCall: + case JSOp::PushVarEnv: + case JSOp::GetBoundName: + case JSOp::Exception: + case JSOp::IsGenClosing: + case JSOp::FinalYieldRval: + case JSOp::Resume: + case JSOp::CheckResumeKind: + case JSOp::AfterYield: + case JSOp::Await: + case JSOp::CanSkipAwait: + case JSOp::MaybeExtractAwaitValue: + case JSOp::Generator: + case JSOp::AsyncAwait: + case JSOp::AsyncResolve: + case JSOp::Finally: + case JSOp::GetRval: + case JSOp::ThrowMsg: + case JSOp::ForceInterpreter: +#ifdef ENABLE_RECORD_TUPLE + case JSOp::InitRecord: + case JSOp::AddRecordProperty: + case JSOp::AddRecordSpread: + case JSOp::FinishRecord: + case JSOp::InitTuple: + case JSOp::AddTupleElement: + case JSOp::FinishTuple: +#endif + return false; + + case JSOp::InitAliasedLexical: { + uint32_t hops = EnvironmentCoordinate(pc).hops(); + if (hops == 0) { + // Initializing aliased lexical in the current scope is almost same + // as JSOp::InitLexical. + return false; + } + + // Otherwise this can touch an environment outside of the current scope. + return true; + } + } + + MOZ_ASSERT_UNREACHABLE("Invalid opcode"); + return false; +} + +bool DebuggerScript::CallData::getEffectfulOffsets() { + if (!ensureScript()) { + return false; + } + + RootedObject result(cx, NewDenseEmptyArray(cx)); + if (!result) { + return false; + } + for (BytecodeRange r(cx, script); !r.empty(); r.popFront()) { + size_t offset = r.frontOffset(); + if (!BytecodeIsEffectful(script, offset)) { + continue; + } + + if (IsGeneratorSlotInitialization(script, offset, cx)) { + // This is engine-internal operation and not visible outside the + // currently executing frame. + // + // Also this offset is not allowed for setting breakpoint. + continue; + } + + if (!NewbornArrayPush(cx, result, NumberValue(offset))) { + return false; + } + } + + args.rval().setObject(*result); + return true; +} + +bool DebuggerScript::CallData::getAllOffsets() { + if (!ensureScript()) { + return false; + } + + // First pass: determine which offsets in this script are jump targets and + // which line numbers jump to them. + FlowGraphSummary flowData(cx); + if (!flowData.populate(cx, script)) { + return false; + } + + // Second pass: build the result array. + RootedObject result(cx, NewDenseEmptyArray(cx)); + if (!result) { + return false; + } + for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) { + if (!r.frontIsEntryPoint()) { + continue; + } + + size_t offset = r.frontOffset(); + size_t lineno = r.frontLineNumber(); + + // Make a note, if the current instruction is an entry point for the current + // line. + if (!flowData[offset].hasNoEdges() && flowData[offset].lineno() != lineno) { + // Get the offsets array for this line. + RootedObject offsets(cx); + RootedValue offsetsv(cx); + + RootedId id(cx, PropertyKey::Int(lineno)); + + bool found; + if (!HasOwnProperty(cx, result, id, &found)) { + return false; + } + if (found && !GetProperty(cx, result, result, id, &offsetsv)) { + return false; + } + + if (offsetsv.isObject()) { + offsets = &offsetsv.toObject(); + } else { + MOZ_ASSERT(offsetsv.isUndefined()); + + // Create an empty offsets array for this line. + // Store it in the result array. + RootedId id(cx); + RootedValue v(cx, NumberValue(lineno)); + offsets = NewDenseEmptyArray(cx); + if (!offsets || !PrimitiveValueToId<CanGC>(cx, v, &id)) { + return false; + } + + RootedValue value(cx, ObjectValue(*offsets)); + if (!DefineDataProperty(cx, result, id, value)) { + return false; + } + } + + // Append the current offset to the offsets array. + if (!NewbornArrayPush(cx, offsets, NumberValue(offset))) { + return false; + } + } + } + + args.rval().setObject(*result); + return true; +} + +class DebuggerScript::GetAllColumnOffsetsMatcher { + JSContext* cx_; + MutableHandleObject result_; + + bool appendColumnOffsetEntry(size_t lineno, size_t column, size_t offset) { + Rooted<PlainObject*> entry(cx_, NewPlainObject(cx_)); + if (!entry) { + return false; + } + + RootedValue value(cx_, NumberValue(lineno)); + if (!DefineDataProperty(cx_, entry, cx_->names().lineNumber, value)) { + return false; + } + + value = NumberValue(column); + if (!DefineDataProperty(cx_, entry, cx_->names().columnNumber, value)) { + return false; + } + + value = NumberValue(offset); + if (!DefineDataProperty(cx_, entry, cx_->names().offset, value)) { + return false; + } + + return NewbornArrayPush(cx_, result_, ObjectValue(*entry)); + } + + public: + explicit GetAllColumnOffsetsMatcher(JSContext* cx, MutableHandleObject result) + : cx_(cx), result_(result) {} + using ReturnType = bool; + ReturnType match(Handle<BaseScript*> base) { + RootedScript script(cx_, DelazifyScript(cx_, base)); + if (!script) { + return false; + } + + // First pass: determine which offsets in this script are jump targets + // and which positions jump to them. + FlowGraphSummary flowData(cx_); + if (!flowData.populate(cx_, script)) { + return false; + } + + // Second pass: build the result array. + result_.set(NewDenseEmptyArray(cx_)); + if (!result_) { + return false; + } + + for (BytecodeRangeWithPosition r(cx_, script); !r.empty(); r.popFront()) { + size_t lineno = r.frontLineNumber(); + size_t column = r.frontColumnNumber(); + size_t offset = r.frontOffset(); + + // Make a note, if the current instruction is an entry point for + // the current position. + if (r.frontIsEntryPoint() && !flowData[offset].hasNoEdges() && + (flowData[offset].lineno() != lineno || + flowData[offset].column() != column)) { + if (!appendColumnOffsetEntry(lineno, column, offset)) { + return false; + } + } + } + return true; + } + ReturnType match(Handle<WasmInstanceObject*> instanceObj) { + wasm::Instance& instance = instanceObj->instance(); + + Vector<wasm::ExprLoc> offsets(cx_); + if (instance.debugEnabled() && + !instance.debug().getAllColumnOffsets(&offsets)) { + return false; + } + + result_.set(NewDenseEmptyArray(cx_)); + if (!result_) { + return false; + } + + for (uint32_t i = 0; i < offsets.length(); i++) { + size_t lineno = offsets[i].lineno; + size_t column = offsets[i].column; + size_t offset = offsets[i].offset; + if (!appendColumnOffsetEntry(lineno, column, offset)) { + return false; + } + } + return true; + } +}; + +bool DebuggerScript::CallData::getAllColumnOffsets() { + RootedObject result(cx); + GetAllColumnOffsetsMatcher matcher(cx, &result); + if (!referent.match(matcher)) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +class DebuggerScript::GetLineOffsetsMatcher { + JSContext* cx_; + size_t lineno_; + MutableHandleObject result_; + + public: + explicit GetLineOffsetsMatcher(JSContext* cx, size_t lineno, + MutableHandleObject result) + : cx_(cx), lineno_(lineno), result_(result) {} + using ReturnType = bool; + ReturnType match(Handle<BaseScript*> base) { + RootedScript script(cx_, DelazifyScript(cx_, base)); + if (!script) { + return false; + } + + // First pass: determine which offsets in this script are jump targets and + // which line numbers jump to them. + FlowGraphSummary flowData(cx_); + if (!flowData.populate(cx_, script)) { + return false; + } + + result_.set(NewDenseEmptyArray(cx_)); + if (!result_) { + return false; + } + + // Second pass: build the result array. + for (BytecodeRangeWithPosition r(cx_, script); !r.empty(); r.popFront()) { + if (!r.frontIsEntryPoint()) { + continue; + } + + size_t offset = r.frontOffset(); + + // If the op at offset is an entry point, append offset to result. + if (r.frontLineNumber() == lineno_ && !flowData[offset].hasNoEdges() && + flowData[offset].lineno() != lineno_) { + if (!NewbornArrayPush(cx_, result_, NumberValue(offset))) { + return false; + } + } + } + + return true; + } + ReturnType match(Handle<WasmInstanceObject*> instanceObj) { + wasm::Instance& instance = instanceObj->instance(); + + Vector<uint32_t> offsets(cx_); + if (instance.debugEnabled() && + !instance.debug().getLineOffsets(lineno_, &offsets)) { + return false; + } + + result_.set(NewDenseEmptyArray(cx_)); + if (!result_) { + return false; + } + + for (uint32_t i = 0; i < offsets.length(); i++) { + if (!NewbornArrayPush(cx_, result_, NumberValue(offsets[i]))) { + return false; + } + } + return true; + } +}; + +bool DebuggerScript::CallData::getLineOffsets() { + if (!args.requireAtLeast(cx, "Debugger.Script.getLineOffsets", 1)) { + return false; + } + + // Parse lineno argument. + RootedValue linenoValue(cx, args[0]); + size_t lineno; + if (!ToNumber(cx, &linenoValue)) { + return false; + } + { + double d = linenoValue.toNumber(); + lineno = size_t(d); + if (lineno != d) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_BAD_LINE); + return false; + } + } + + RootedObject result(cx); + GetLineOffsetsMatcher matcher(cx, lineno, &result); + if (!referent.match(matcher)) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +struct DebuggerScript::SetBreakpointMatcher { + JSContext* cx_; + Debugger* dbg_; + size_t offset_; + RootedObject handler_; + RootedObject debuggerObject_; + + bool wrapCrossCompartmentEdges() { + if (!cx_->compartment()->wrap(cx_, &handler_) || + !cx_->compartment()->wrap(cx_, &debuggerObject_)) { + return false; + } + + // If the Debugger's compartment has killed incoming wrappers, we may not + // have gotten usable results from the 'wrap' calls. Treat it as a + // failure. + if (IsDeadProxyObject(handler_) || IsDeadProxyObject(debuggerObject_)) { + ReportAccessDenied(cx_); + return false; + } + + return true; + } + + public: + explicit SetBreakpointMatcher(JSContext* cx, Debugger* dbg, size_t offset, + HandleObject handler) + : cx_(cx), + dbg_(dbg), + offset_(offset), + handler_(cx, handler), + debuggerObject_(cx_, dbg_->toJSObject()) {} + + using ReturnType = bool; + + ReturnType match(Handle<BaseScript*> base) { + RootedScript script(cx_, DelazifyScript(cx_, base)); + if (!script) { + return false; + } + + if (!dbg_->observesScript(script)) { + JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr, + JSMSG_DEBUG_NOT_DEBUGGING); + return false; + } + + if (!EnsureScriptOffsetIsValid(cx_, script, offset_)) { + return false; + } + + if (!EnsureBreakpointIsAllowed(cx_, script, offset_)) { + return false; + } + + // Ensure observability *before* setting the breakpoint. If the script is + // not already a debuggee, trying to ensure observability after setting + // the breakpoint (and thus marking the script as a debuggee) will skip + // actually ensuring observability. + if (!dbg_->ensureExecutionObservabilityOfScript(cx_, script)) { + return false; + } + + // A Breakpoint belongs logically to its script's compartment, so its + // references to its Debugger and handler must be properly wrapped. + AutoRealm ar(cx_, script); + if (!wrapCrossCompartmentEdges()) { + return false; + } + + jsbytecode* pc = script->offsetToPC(offset_); + JSBreakpointSite* site = + DebugScript::getOrCreateBreakpointSite(cx_, script, pc); + if (!site) { + return false; + } + + if (!cx_->zone()->new_<Breakpoint>(dbg_, debuggerObject_, site, handler_)) { + site->destroyIfEmpty(cx_->runtime()->gcContext()); + return false; + } + AddCellMemory(script, sizeof(Breakpoint), MemoryUse::Breakpoint); + + return true; + } + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { + wasm::Instance& instance = wasmInstance->instance(); + if (!instance.debugEnabled() || + !instance.debug().hasBreakpointTrapAtOffset(offset_)) { + JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr, + JSMSG_DEBUG_BAD_OFFSET); + return false; + } + + // A Breakpoint belongs logically to its Instance's compartment, so its + // references to its Debugger and handler must be properly wrapped. + AutoRealm ar(cx_, wasmInstance); + if (!wrapCrossCompartmentEdges()) { + return false; + } + + WasmBreakpointSite* site = instance.getOrCreateBreakpointSite(cx_, offset_); + if (!site) { + return false; + } + + if (!cx_->zone()->new_<Breakpoint>(dbg_, debuggerObject_, site, handler_)) { + site->destroyIfEmpty(cx_->runtime()->gcContext()); + return false; + } + AddCellMemory(wasmInstance, sizeof(Breakpoint), MemoryUse::Breakpoint); + + return true; + } +}; + +bool DebuggerScript::CallData::setBreakpoint() { + if (!args.requireAtLeast(cx, "Debugger.Script.setBreakpoint", 2)) { + return false; + } + Debugger* dbg = obj->owner(); + + size_t offset; + if (!ScriptOffset(cx, args[0], &offset)) { + return false; + } + + RootedObject handler(cx, RequireObject(cx, args[1])); + if (!handler) { + return false; + } + + SetBreakpointMatcher matcher(cx, dbg, offset, handler); + if (!referent.match(matcher)) { + return false; + } + args.rval().setUndefined(); + return true; +} + +bool DebuggerScript::CallData::getBreakpoints() { + if (!ensureScript()) { + return false; + } + Debugger* dbg = obj->owner(); + + jsbytecode* pc; + if (args.length() > 0) { + size_t offset; + if (!ScriptOffset(cx, args[0], &offset) || + !EnsureScriptOffsetIsValid(cx, script, offset)) { + return false; + } + pc = script->offsetToPC(offset); + } else { + pc = nullptr; + } + + RootedObject arr(cx, NewDenseEmptyArray(cx)); + if (!arr) { + return false; + } + + for (unsigned i = 0; i < script->length(); i++) { + JSBreakpointSite* site = + DebugScript::getBreakpointSite(script, script->offsetToPC(i)); + if (!site) { + continue; + } + if (!pc || site->pc == pc) { + for (Breakpoint* bp = site->firstBreakpoint(); bp; + bp = bp->nextInSite()) { + if (bp->debugger == dbg) { + RootedObject handler(cx, bp->getHandler()); + if (!cx->compartment()->wrap(cx, &handler) || + !NewbornArrayPush(cx, arr, ObjectValue(*handler))) { + return false; + } + } + } + } + } + args.rval().setObject(*arr); + return true; +} + +class DebuggerScript::ClearBreakpointMatcher { + JSContext* cx_; + Debugger* dbg_; + RootedObject handler_; + + public: + ClearBreakpointMatcher(JSContext* cx, Debugger* dbg, JSObject* handler) + : cx_(cx), dbg_(dbg), handler_(cx, handler) {} + using ReturnType = bool; + + ReturnType match(Handle<BaseScript*> base) { + RootedScript script(cx_, DelazifyScript(cx_, base)); + if (!script) { + return false; + } + + // A Breakpoint belongs logically to its script's compartment, so it holds + // its handler via a cross-compartment wrapper. But the handler passed to + // `clearBreakpoint` is same-compartment with the Debugger. Wrap it here, + // so that `DebugScript::clearBreakpointsIn` gets the right value to + // search for. + AutoRealm ar(cx_, script); + if (!cx_->compartment()->wrap(cx_, &handler_)) { + return false; + } + + DebugScript::clearBreakpointsIn(cx_->runtime()->gcContext(), script, dbg_, + handler_); + return true; + } + ReturnType match(Handle<WasmInstanceObject*> instanceObj) { + wasm::Instance& instance = instanceObj->instance(); + if (!instance.debugEnabled()) { + return true; + } + + // A Breakpoint belongs logically to its instance's compartment, so it + // holds its handler via a cross-compartment wrapper. But the handler + // passed to `clearBreakpoint` is same-compartment with the Debugger. Wrap + // it here, so that `DebugState::clearBreakpointsIn` gets the right value + // to search for. + AutoRealm ar(cx_, instanceObj); + if (!cx_->compartment()->wrap(cx_, &handler_)) { + return false; + } + + instance.debug().clearBreakpointsIn(cx_->runtime()->gcContext(), + instanceObj, dbg_, handler_); + return true; + } +}; + +bool DebuggerScript::CallData::clearBreakpoint() { + if (!args.requireAtLeast(cx, "Debugger.Script.clearBreakpoint", 1)) { + return false; + } + Debugger* dbg = obj->owner(); + + JSObject* handler = RequireObject(cx, args[0]); + if (!handler) { + return false; + } + + ClearBreakpointMatcher matcher(cx, dbg, handler); + if (!referent.match(matcher)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +bool DebuggerScript::CallData::clearAllBreakpoints() { + Debugger* dbg = obj->owner(); + ClearBreakpointMatcher matcher(cx, dbg, nullptr); + if (!referent.match(matcher)) { + return false; + } + args.rval().setUndefined(); + return true; +} + +class DebuggerScript::IsInCatchScopeMatcher { + JSContext* cx_; + size_t offset_; + bool isInCatch_; + + public: + explicit IsInCatchScopeMatcher(JSContext* cx, size_t offset) + : cx_(cx), offset_(offset), isInCatch_(false) {} + using ReturnType = bool; + + inline bool isInCatch() const { return isInCatch_; } + + ReturnType match(Handle<BaseScript*> base) { + RootedScript script(cx_, DelazifyScript(cx_, base)); + if (!script) { + return false; + } + + if (!EnsureScriptOffsetIsValid(cx_, script, offset_)) { + return false; + } + + for (const TryNote& tn : script->trynotes()) { + if (tn.start <= offset_ && offset_ < tn.start + tn.length && + tn.kind() == TryNoteKind::Catch) { + isInCatch_ = true; + return true; + } + } + + isInCatch_ = false; + return true; + } + ReturnType match(Handle<WasmInstanceObject*> instance) { + isInCatch_ = false; + return true; + } +}; + +bool DebuggerScript::CallData::isInCatchScope() { + if (!args.requireAtLeast(cx, "Debugger.Script.isInCatchScope", 1)) { + return false; + } + + size_t offset; + if (!ScriptOffset(cx, args[0], &offset)) { + return false; + } + + IsInCatchScopeMatcher matcher(cx, offset); + if (!referent.match(matcher)) { + return false; + } + args.rval().setBoolean(matcher.isInCatch()); + return true; +} + +bool DebuggerScript::CallData::getOffsetsCoverage() { + if (!ensureScript()) { + return false; + } + + Debugger* dbg = obj->owner(); + if (dbg->observesCoverage() != Debugger::Observing) { + args.rval().setNull(); + return true; + } + + // If the script has no coverage information, then skip this and return null + // instead. + if (!script->hasScriptCounts()) { + args.rval().setNull(); + return true; + } + + ScriptCounts* sc = &script->getScriptCounts(); + + // If the main ever got visited, then assume that any code before main got + // visited once. + uint64_t hits = 0; + const PCCounts* counts = + sc->maybeGetPCCounts(script->pcToOffset(script->main())); + if (counts->numExec()) { + hits = 1; + } + + // Build an array of objects which are composed of 4 properties: + // - offset PC offset of the current opcode. + // - lineNumber Line of the current opcode. + // - columnNumber Column of the current opcode. + // - count Number of times the instruction got executed. + RootedObject result(cx, NewDenseEmptyArray(cx)); + if (!result) { + return false; + } + + RootedId offsetId(cx, NameToId(cx->names().offset)); + RootedId lineNumberId(cx, NameToId(cx->names().lineNumber)); + RootedId columnNumberId(cx, NameToId(cx->names().columnNumber)); + RootedId countId(cx, NameToId(cx->names().count)); + + RootedObject item(cx); + RootedValue offsetValue(cx); + RootedValue lineNumberValue(cx); + RootedValue columnNumberValue(cx); + RootedValue countValue(cx); + + // Iterate linearly over the bytecode. + for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) { + size_t offset = r.frontOffset(); + + // The beginning of each non-branching sequences of instruction set the + // number of execution of the current instruction and any following + // instruction. + counts = sc->maybeGetPCCounts(offset); + if (counts) { + hits = counts->numExec(); + } + + offsetValue.setNumber(double(offset)); + lineNumberValue.setNumber(double(r.frontLineNumber())); + columnNumberValue.setNumber(double(r.frontColumnNumber())); + countValue.setNumber(double(hits)); + + // Create a new object with the offset, line number, column number, the + // number of hit counts, and append it to the array. + item = NewPlainObjectWithProto(cx, nullptr); + if (!item || !DefineDataProperty(cx, item, offsetId, offsetValue) || + !DefineDataProperty(cx, item, lineNumberId, lineNumberValue) || + !DefineDataProperty(cx, item, columnNumberId, columnNumberValue) || + !DefineDataProperty(cx, item, countId, countValue) || + !NewbornArrayPush(cx, result, ObjectValue(*item))) { + return false; + } + + // If the current instruction has thrown, then decrement the hit counts + // with the number of throws. + counts = sc->maybeGetThrowCounts(offset); + if (counts) { + hits -= counts->numExec(); + } + } + + args.rval().setObject(*result); + return true; +} + +/* static */ +bool DebuggerScript::construct(JSContext* cx, unsigned argc, Value* vp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, + "Debugger.Script"); + return false; +} + +const JSPropertySpec DebuggerScript::properties_[] = { + JS_DEBUG_PSG("isGeneratorFunction", getIsGeneratorFunction), + JS_DEBUG_PSG("isAsyncFunction", getIsAsyncFunction), + JS_DEBUG_PSG("isFunction", getIsFunction), + JS_DEBUG_PSG("isModule", getIsModule), + JS_DEBUG_PSG("displayName", getDisplayName), + JS_DEBUG_PSG("parameterNames", getParameterNames), + JS_DEBUG_PSG("url", getUrl), + JS_DEBUG_PSG("startLine", getStartLine), + JS_DEBUG_PSG("startColumn", getStartColumn), + JS_DEBUG_PSG("lineCount", getLineCount), + JS_DEBUG_PSG("source", getSource), + JS_DEBUG_PSG("sourceStart", getSourceStart), + JS_DEBUG_PSG("sourceLength", getSourceLength), + JS_DEBUG_PSG("mainOffset", getMainOffset), + JS_DEBUG_PSG("global", getGlobal), + JS_DEBUG_PSG("format", getFormat), + JS_PS_END}; + +const JSFunctionSpec DebuggerScript::methods_[] = { + JS_DEBUG_FN("getChildScripts", getChildScripts, 0), + JS_DEBUG_FN("getPossibleBreakpoints", getPossibleBreakpoints, 0), + JS_DEBUG_FN("getPossibleBreakpointOffsets", getPossibleBreakpointOffsets, + 0), + JS_DEBUG_FN("setBreakpoint", setBreakpoint, 2), + JS_DEBUG_FN("getBreakpoints", getBreakpoints, 1), + JS_DEBUG_FN("clearBreakpoint", clearBreakpoint, 1), + JS_DEBUG_FN("clearAllBreakpoints", clearAllBreakpoints, 0), + JS_DEBUG_FN("isInCatchScope", isInCatchScope, 1), + JS_DEBUG_FN("getOffsetMetadata", getOffsetMetadata, 1), + JS_DEBUG_FN("getOffsetsCoverage", getOffsetsCoverage, 0), + JS_DEBUG_FN("getEffectfulOffsets", getEffectfulOffsets, 1), + + // The following APIs are deprecated due to their reliance on the + // under-defined 'entrypoint' concept. Make use of getPossibleBreakpoints, + // getPossibleBreakpointOffsets, or getOffsetMetadata instead. + JS_DEBUG_FN("getAllOffsets", getAllOffsets, 0), + JS_DEBUG_FN("getAllColumnOffsets", getAllColumnOffsets, 0), + JS_DEBUG_FN("getLineOffsets", getLineOffsets, 1), + JS_DEBUG_FN("getOffsetLocation", getOffsetLocation, 0), JS_FS_END}; diff --git a/js/src/debugger/Script.h b/js/src/debugger/Script.h new file mode 100644 index 0000000000..f6ba553724 --- /dev/null +++ b/js/src/debugger/Script.h @@ -0,0 +1,85 @@ +/* -*- 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/. */ + +#ifndef debugger_Script_h +#define debugger_Script_h + +#include "jstypes.h" // for JS_PUBLIC_API +#include "NamespaceImports.h" // for Value, HandleObject, CallArgs +#include "debugger/Debugger.h" // for DebuggerScriptReferent +#include "js/TypeDecls.h" // for Handle +#include "vm/NativeObject.h" // for NativeObject + +class JS_PUBLIC_API JSObject; +struct JSFunctionSpec; +struct JSPropertySpec; + +namespace js { + +class BaseScript; +class GlobalObject; + +namespace gc { +struct Cell; +} + +class DebuggerScript : public NativeObject { + public: + static const JSClass class_; + + enum { + SCRIPT_SLOT, + OWNER_SLOT, + + RESERVED_SLOTS, + }; + + static NativeObject* initClass(JSContext* cx, Handle<GlobalObject*> global, + HandleObject debugCtor); + static DebuggerScript* create(JSContext* cx, HandleObject proto, + Handle<DebuggerScriptReferent> referent, + Handle<NativeObject*> debugger); + + void trace(JSTracer* trc); + + using ReferentVariant = DebuggerScriptReferent; + + inline gc::Cell* getReferentCell() const; + inline js::BaseScript* getReferentScript() const; + inline DebuggerScriptReferent getReferent() const; + + void clearReferent() { clearReservedSlotGCThingAsPrivate(SCRIPT_SLOT); } + + static DebuggerScript* check(JSContext* cx, HandleValue v); + + static bool construct(JSContext* cx, unsigned argc, Value* vp); + + struct CallData; + + Debugger* owner() const; + + private: + static const JSClassOps classOps_; + + static const JSPropertySpec properties_[]; + static const JSFunctionSpec methods_[]; + + struct GetLineCountMatcher; + class GetSourceMatcher; + template <bool OnlyOffsets> + class GetPossibleBreakpointsMatcher; + class GetOffsetMetadataMatcher; + class GetOffsetLocationMatcher; + class GetAllColumnOffsetsMatcher; + class GetLineOffsetsMatcher; + struct SetBreakpointMatcher; + class ClearBreakpointMatcher; + class IsInCatchScopeMatcher; +}; + +} /* namespace js */ + +#endif /* debugger_Script_h */ diff --git a/js/src/debugger/Source.cpp b/js/src/debugger/Source.cpp new file mode 100644 index 0000000000..99b1f4698c --- /dev/null +++ b/js/src/debugger/Source.cpp @@ -0,0 +1,658 @@ +/* -*- 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/Source.h" + +#include "mozilla/Assertions.h" // for AssertionConditionType, MOZ_ASSERT +#include "mozilla/Maybe.h" // for Some, Maybe, Nothing +#include "mozilla/Variant.h" // for AsVariant, Variant + +#include <stdint.h> // for uint32_t +#include <string.h> // for memcpy +#include <utility> // for move + +#include "debugger/Debugger.h" // for DebuggerSourceReferent, Debugger +#include "debugger/Script.h" // for DebuggerScript +#include "frontend/FrontendContext.h" // for AutoReportFrontendContext +#include "gc/Tracer.h" // for TraceManuallyBarrieredCrossCompartmentEdge +#include "js/CompilationAndEvaluation.h" // for Compile +#include "js/ErrorReport.h" // for JS_ReportErrorASCII, JS_ReportErrorNumberASCII +#include "js/experimental/TypedData.h" // for JS_NewUint8Array +#include "js/friend/ErrorMessages.h" // for GetErrorMessage, JSMSG_* +#include "js/GCVariant.h" // for GCVariant +#include "js/SourceText.h" // for JS::SourceOwnership +#include "js/String.h" // for JS_CopyStringCharsZ +#include "vm/BytecodeUtil.h" // for JSDVG_SEARCH_STACK +#include "vm/JSContext.h" // for JSContext (ptr only) +#include "vm/JSObject.h" // for JSObject, RequireObject +#include "vm/JSScript.h" // for ScriptSource, ScriptSourceObject +#include "vm/StringType.h" // for NewStringCopyZ, JSString (ptr only) +#include "vm/TypedArrayObject.h" // for TypedArrayObject, JSObject::is +#include "wasm/WasmCode.h" // for Metadata +#include "wasm/WasmDebug.h" // for DebugState +#include "wasm/WasmInstance.h" // for Instance +#include "wasm/WasmJS.h" // for WasmInstanceObject +#include "wasm/WasmTypeDecls.h" // for Bytes, Rooted<WasmInstanceObject*> + +#include "debugger/Debugger-inl.h" // for Debugger::fromJSObject +#include "vm/JSObject-inl.h" // for InitClass +#include "vm/NativeObject-inl.h" // for NewTenuredObjectWithGivenProto +#include "wasm/WasmInstance-inl.h" + +namespace js { +class GlobalObject; +} + +using namespace js; + +using mozilla::AsVariant; +using mozilla::Maybe; +using mozilla::Nothing; +using mozilla::Some; + +const JSClassOps DebuggerSource::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + nullptr, // finalize + nullptr, // call + nullptr, // construct + CallTraceMethod<DebuggerSource>, // trace +}; + +const JSClass DebuggerSource::class_ = { + "Source", JSCLASS_HAS_RESERVED_SLOTS(RESERVED_SLOTS), &classOps_}; + +/* static */ +NativeObject* DebuggerSource::initClass(JSContext* cx, + Handle<GlobalObject*> global, + HandleObject debugCtor) { + return InitClass(cx, debugCtor, nullptr, nullptr, "Source", construct, 0, + properties_, methods_, nullptr, nullptr); +} + +/* static */ +DebuggerSource* DebuggerSource::create(JSContext* cx, HandleObject proto, + Handle<DebuggerSourceReferent> referent, + Handle<NativeObject*> debugger) { + Rooted<DebuggerSource*> sourceObj( + cx, NewTenuredObjectWithGivenProto<DebuggerSource>(cx, proto)); + if (!sourceObj) { + return nullptr; + } + sourceObj->setReservedSlot(OWNER_SLOT, ObjectValue(*debugger)); + referent.get().match([&](auto sourceHandle) { + sourceObj->setReservedSlotGCThingAsPrivate(SOURCE_SLOT, sourceHandle); + }); + + return sourceObj; +} + +Debugger* DebuggerSource::owner() const { + JSObject* dbgobj = &getReservedSlot(OWNER_SLOT).toObject(); + return Debugger::fromJSObject(dbgobj); +} + +// For internal use only. +NativeObject* DebuggerSource::getReferentRawObject() const { + return maybePtrFromReservedSlot<NativeObject>(SOURCE_SLOT); +} + +DebuggerSourceReferent DebuggerSource::getReferent() const { + if (NativeObject* referent = getReferentRawObject()) { + if (referent->is<ScriptSourceObject>()) { + return AsVariant(&referent->as<ScriptSourceObject>()); + } + return AsVariant(&referent->as<WasmInstanceObject>()); + } + return AsVariant(static_cast<ScriptSourceObject*>(nullptr)); +} + +void DebuggerSource::trace(JSTracer* trc) { + // There is a barrier on private pointers, so the Unbarriered marking + // is okay. + if (JSObject* referent = getReferentRawObject()) { + TraceManuallyBarrieredCrossCompartmentEdge(trc, this, &referent, + "Debugger.Source referent"); + if (referent != getReferentRawObject()) { + setReservedSlotGCThingAsPrivateUnbarriered(SOURCE_SLOT, referent); + } + } +} + +/* static */ +bool DebuggerSource::construct(JSContext* cx, unsigned argc, Value* vp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, + "Debugger.Source"); + return false; +} + +/* static */ +DebuggerSource* DebuggerSource::check(JSContext* cx, HandleValue thisv) { + JSObject* thisobj = RequireObject(cx, thisv); + if (!thisobj) { + return nullptr; + } + if (!thisobj->is<DebuggerSource>()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INCOMPATIBLE_PROTO, "Debugger.Source", + "method", thisobj->getClass()->name); + return nullptr; + } + + return &thisobj->as<DebuggerSource>(); +} + +struct MOZ_STACK_CLASS DebuggerSource::CallData { + JSContext* cx; + const CallArgs& args; + + Handle<DebuggerSource*> obj; + Rooted<DebuggerSourceReferent> referent; + + CallData(JSContext* cx, const CallArgs& args, Handle<DebuggerSource*> obj) + : cx(cx), args(args), obj(obj), referent(cx, obj->getReferent()) {} + + bool getText(); + bool getBinary(); + bool getURL(); + bool getStartLine(); + bool getId(); + bool getDisplayURL(); + bool getElementProperty(); + bool getIntroductionScript(); + bool getIntroductionOffset(); + bool getIntroductionType(); + bool setSourceMapURL(); + bool getSourceMapURL(); + bool reparse(); + + using Method = bool (CallData::*)(); + + template <Method MyMethod> + static bool ToNative(JSContext* cx, unsigned argc, Value* vp); +}; + +template <DebuggerSource::CallData::Method MyMethod> +/* static */ +bool DebuggerSource::CallData::ToNative(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<DebuggerSource*> obj(cx, DebuggerSource::check(cx, args.thisv())); + if (!obj) { + return false; + } + + CallData data(cx, args, obj); + return (data.*MyMethod)(); +} + +class DebuggerSourceGetTextMatcher { + JSContext* cx_; + + public: + explicit DebuggerSourceGetTextMatcher(JSContext* cx) : cx_(cx) {} + + using ReturnType = JSString*; + + ReturnType match(Handle<ScriptSourceObject*> sourceObject) { + ScriptSource* ss = sourceObject->source(); + bool hasSourceText; + if (!ScriptSource::loadSource(cx_, ss, &hasSourceText)) { + return nullptr; + } + if (!hasSourceText) { + return NewStringCopyZ<CanGC>(cx_, "[no source]"); + } + + if (ss->isFunctionBody()) { + return ss->functionBodyString(cx_); + } + + return ss->substring(cx_, 0, ss->length()); + } + + ReturnType match(Handle<WasmInstanceObject*> instanceObj) { + wasm::Instance& instance = instanceObj->instance(); + const char* msg; + if (!instance.debugEnabled()) { + msg = "Restart with developer tools open to view WebAssembly source."; + } else { + msg = "[debugger missing wasm binary-to-text conversion]"; + } + return NewStringCopyZ<CanGC>(cx_, msg); + } +}; + +bool DebuggerSource::CallData::getText() { + Value textv = obj->getReservedSlot(TEXT_SLOT); + if (!textv.isUndefined()) { + MOZ_ASSERT(textv.isString()); + args.rval().set(textv); + return true; + } + + DebuggerSourceGetTextMatcher matcher(cx); + JSString* str = referent.match(matcher); + if (!str) { + return false; + } + + args.rval().setString(str); + obj->setReservedSlot(TEXT_SLOT, args.rval()); + return true; +} + +bool DebuggerSource::CallData::getBinary() { + if (!referent.is<WasmInstanceObject*>()) { + ReportValueError(cx, JSMSG_DEBUG_BAD_REFERENT, JSDVG_SEARCH_STACK, + args.thisv(), nullptr, "a wasm source"); + return false; + } + + Rooted<WasmInstanceObject*> instanceObj(cx, + referent.as<WasmInstanceObject*>()); + wasm::Instance& instance = instanceObj->instance(); + + if (!instance.debugEnabled()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_NO_BINARY_SOURCE); + return false; + } + + const wasm::Bytes& bytecode = instance.debug().bytecode(); + RootedObject arr(cx, JS_NewUint8Array(cx, bytecode.length())); + if (!arr) { + return false; + } + + memcpy(arr->as<TypedArrayObject>().dataPointerUnshared(), bytecode.begin(), + bytecode.length()); + + args.rval().setObject(*arr); + return true; +} + +class DebuggerSourceGetURLMatcher { + JSContext* cx_; + + public: + explicit DebuggerSourceGetURLMatcher(JSContext* cx) : cx_(cx) {} + + using ReturnType = Maybe<JSString*>; + + ReturnType match(Handle<ScriptSourceObject*> sourceObject) { + ScriptSource* ss = sourceObject->source(); + MOZ_ASSERT(ss); + if (ss->filename()) { + JSString* str = NewStringCopyZ<CanGC>(cx_, ss->filename()); + return Some(str); + } + return Nothing(); + } + ReturnType match(Handle<WasmInstanceObject*> instanceObj) { + return Some(instanceObj->instance().createDisplayURL(cx_)); + } +}; + +bool DebuggerSource::CallData::getURL() { + DebuggerSourceGetURLMatcher matcher(cx); + Maybe<JSString*> str = referent.match(matcher); + if (str.isSome()) { + if (!*str) { + return false; + } + args.rval().setString(*str); + } else { + args.rval().setNull(); + } + return true; +} + +class DebuggerSourceGetStartLineMatcher { + public: + using ReturnType = uint32_t; + + ReturnType match(Handle<ScriptSourceObject*> sourceObject) { + ScriptSource* ss = sourceObject->source(); + return ss->startLine(); + } + ReturnType match(Handle<WasmInstanceObject*> instanceObj) { return 0; } +}; + +bool DebuggerSource::CallData::getStartLine() { + DebuggerSourceGetStartLineMatcher matcher; + uint32_t line = referent.match(matcher); + args.rval().setNumber(line); + return true; +} + +class DebuggerSourceGetIdMatcher { + public: + using ReturnType = uint32_t; + + ReturnType match(Handle<ScriptSourceObject*> sourceObject) { + ScriptSource* ss = sourceObject->source(); + return ss->id(); + } + ReturnType match(Handle<WasmInstanceObject*> instanceObj) { return 0; } +}; + +bool DebuggerSource::CallData::getId() { + DebuggerSourceGetIdMatcher matcher; + uint32_t id = referent.match(matcher); + args.rval().setNumber(id); + return true; +} + +struct DebuggerSourceGetDisplayURLMatcher { + using ReturnType = const char16_t*; + ReturnType match(Handle<ScriptSourceObject*> sourceObject) { + ScriptSource* ss = sourceObject->source(); + MOZ_ASSERT(ss); + return ss->hasDisplayURL() ? ss->displayURL() : nullptr; + } + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { + return wasmInstance->instance().metadata().displayURL(); + } +}; + +bool DebuggerSource::CallData::getDisplayURL() { + DebuggerSourceGetDisplayURLMatcher matcher; + if (const char16_t* displayURL = referent.match(matcher)) { + JSString* str = JS_NewUCStringCopyZ(cx, displayURL); + if (!str) { + return false; + } + args.rval().setString(str); + } else { + args.rval().setNull(); + } + return true; +} + +struct DebuggerSourceGetElementPropertyMatcher { + using ReturnType = Value; + ReturnType match(Handle<ScriptSourceObject*> sourceObject) { + return sourceObject->unwrappedElementAttributeName(); + } + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { + return UndefinedValue(); + } +}; + +bool DebuggerSource::CallData::getElementProperty() { + DebuggerSourceGetElementPropertyMatcher matcher; + args.rval().set(referent.match(matcher)); + return obj->owner()->wrapDebuggeeValue(cx, args.rval()); +} + +class DebuggerSourceGetIntroductionScriptMatcher { + JSContext* cx_; + Debugger* dbg_; + MutableHandleValue rval_; + + public: + DebuggerSourceGetIntroductionScriptMatcher(JSContext* cx, Debugger* dbg, + MutableHandleValue rval) + : cx_(cx), dbg_(dbg), rval_(rval) {} + + using ReturnType = bool; + + ReturnType match(Handle<ScriptSourceObject*> sourceObject) { + Rooted<BaseScript*> script(cx_, + sourceObject->unwrappedIntroductionScript()); + if (script) { + RootedObject scriptDO(cx_, dbg_->wrapScript(cx_, script)); + if (!scriptDO) { + return false; + } + rval_.setObject(*scriptDO); + } else { + rval_.setUndefined(); + } + return true; + } + + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { + RootedObject ds(cx_, dbg_->wrapWasmScript(cx_, wasmInstance)); + if (!ds) { + return false; + } + rval_.setObject(*ds); + return true; + } +}; + +bool DebuggerSource::CallData::getIntroductionScript() { + Debugger* dbg = obj->owner(); + DebuggerSourceGetIntroductionScriptMatcher matcher(cx, dbg, args.rval()); + return referent.match(matcher); +} + +struct DebuggerGetIntroductionOffsetMatcher { + using ReturnType = Value; + ReturnType match(Handle<ScriptSourceObject*> sourceObject) { + // Regardless of what's recorded in the ScriptSourceObject and + // ScriptSource, only hand out the introduction offset if we also have + // the script within which it applies. + ScriptSource* ss = sourceObject->source(); + if (ss->hasIntroductionOffset() && + sourceObject->unwrappedIntroductionScript()) { + return Int32Value(ss->introductionOffset()); + } + return UndefinedValue(); + } + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { + return UndefinedValue(); + } +}; + +bool DebuggerSource::CallData::getIntroductionOffset() { + DebuggerGetIntroductionOffsetMatcher matcher; + args.rval().set(referent.match(matcher)); + return true; +} + +struct DebuggerSourceGetIntroductionTypeMatcher { + using ReturnType = const char*; + ReturnType match(Handle<ScriptSourceObject*> sourceObject) { + ScriptSource* ss = sourceObject->source(); + MOZ_ASSERT(ss); + return ss->hasIntroductionType() ? ss->introductionType() : nullptr; + } + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { return "wasm"; } +}; + +bool DebuggerSource::CallData::getIntroductionType() { + DebuggerSourceGetIntroductionTypeMatcher matcher; + if (const char* introductionType = referent.match(matcher)) { + JSString* str = NewStringCopyZ<CanGC>(cx, introductionType); + if (!str) { + return false; + } + args.rval().setString(str); + } else { + args.rval().setUndefined(); + } + + return true; +} + +ScriptSourceObject* EnsureSourceObject(JSContext* cx, + Handle<DebuggerSource*> obj) { + if (!obj->getReferent().is<ScriptSourceObject*>()) { + RootedValue v(cx, ObjectValue(*obj)); + ReportValueError(cx, JSMSG_DEBUG_BAD_REFERENT, JSDVG_SEARCH_STACK, v, + nullptr, "a JS source"); + return nullptr; + } + return obj->getReferent().as<ScriptSourceObject*>(); +} + +bool DebuggerSource::CallData::setSourceMapURL() { + Rooted<ScriptSourceObject*> sourceObject(cx, EnsureSourceObject(cx, obj)); + if (!sourceObject) { + return false; + } + ScriptSource* ss = sourceObject->source(); + MOZ_ASSERT(ss); + + if (!args.requireAtLeast(cx, "set sourceMapURL", 1)) { + return false; + } + + JSString* str = ToString<CanGC>(cx, args[0]); + if (!str) { + return false; + } + + UniqueTwoByteChars chars = JS_CopyStringCharsZ(cx, str); + if (!chars) { + return false; + } + + AutoReportFrontendContext fc(cx); + if (!ss->setSourceMapURL(&fc, std::move(chars))) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +class DebuggerSourceGetSourceMapURLMatcher { + JSContext* cx_; + MutableHandleString result_; + + public: + explicit DebuggerSourceGetSourceMapURLMatcher(JSContext* cx, + MutableHandleString result) + : cx_(cx), result_(result) {} + + using ReturnType = bool; + ReturnType match(Handle<ScriptSourceObject*> sourceObject) { + ScriptSource* ss = sourceObject->source(); + MOZ_ASSERT(ss); + if (!ss->hasSourceMapURL()) { + result_.set(nullptr); + return true; + } + JSString* str = JS_NewUCStringCopyZ(cx_, ss->sourceMapURL()); + if (!str) { + return false; + } + result_.set(str); + return true; + } + ReturnType match(Handle<WasmInstanceObject*> instanceObj) { + wasm::Instance& instance = instanceObj->instance(); + if (!instance.debugEnabled()) { + result_.set(nullptr); + return true; + } + + RootedString str(cx_); + if (!instance.debug().getSourceMappingURL(cx_, &str)) { + return false; + } + + result_.set(str); + return true; + } +}; + +bool DebuggerSource::CallData::getSourceMapURL() { + RootedString result(cx); + DebuggerSourceGetSourceMapURLMatcher matcher(cx, &result); + if (!referent.match(matcher)) { + return false; + } + if (result) { + args.rval().setString(result); + } else { + args.rval().setNull(); + } + return true; +} + +template <typename Unit> +static JSScript* ReparseSource(JSContext* cx, Handle<ScriptSourceObject*> sso) { + AutoRealm ar(cx, sso); + ScriptSource* ss = sso->source(); + + JS::CompileOptions options(cx); + options.setHideScriptFromDebugger(true); + options.setFileAndLine(ss->filename(), ss->startLine()); + + UncompressedSourceCache::AutoHoldEntry holder; + + ScriptSource::PinnedUnits<Unit> units(cx, ss, holder, 0, ss->length()); + if (!units.get()) { + return nullptr; + } + + JS::SourceText<Unit> srcBuf; + if (!srcBuf.init(cx, units.get(), ss->length(), + JS::SourceOwnership::Borrowed)) { + return nullptr; + } + + return JS::Compile(cx, options, srcBuf); +} + +bool DebuggerSource::CallData::reparse() { + Rooted<ScriptSourceObject*> sourceObject(cx, EnsureSourceObject(cx, obj)); + if (!sourceObject) { + return false; + } + + if (!sourceObject->source()->hasSourceText()) { + JS_ReportErrorASCII(cx, "Source object missing text"); + return false; + } + + RootedScript script(cx); + if (sourceObject->source()->hasSourceType<mozilla::Utf8Unit>()) { + script = ReparseSource<mozilla::Utf8Unit>(cx, sourceObject); + } else { + script = ReparseSource<char16_t>(cx, sourceObject); + } + + if (!script) { + return false; + } + + Debugger* dbg = obj->owner(); + RootedObject scriptDO(cx, dbg->wrapScript(cx, script)); + if (!scriptDO) { + return false; + } + + args.rval().setObject(*scriptDO); + return true; +} + +const JSPropertySpec DebuggerSource::properties_[] = { + JS_DEBUG_PSG("text", getText), + JS_DEBUG_PSG("binary", getBinary), + JS_DEBUG_PSG("url", getURL), + JS_DEBUG_PSG("startLine", getStartLine), + JS_DEBUG_PSG("id", getId), + JS_DEBUG_PSG("displayURL", getDisplayURL), + JS_DEBUG_PSG("introductionScript", getIntroductionScript), + JS_DEBUG_PSG("introductionOffset", getIntroductionOffset), + JS_DEBUG_PSG("introductionType", getIntroductionType), + JS_DEBUG_PSG("elementAttributeName", getElementProperty), + JS_DEBUG_PSGS("sourceMapURL", getSourceMapURL, setSourceMapURL), + JS_PS_END}; + +const JSFunctionSpec DebuggerSource::methods_[] = { + JS_DEBUG_FN("reparse", reparse, 0), JS_FS_END}; diff --git a/js/src/debugger/Source.h b/js/src/debugger/Source.h new file mode 100644 index 0000000000..93ac410838 --- /dev/null +++ b/js/src/debugger/Source.h @@ -0,0 +1,62 @@ +/* -*- 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/. */ + +#ifndef dbg_Source_h +#define dbg_Source_h + +#include "NamespaceImports.h" // for Value, HandleObject, CallArgs +#include "debugger/Debugger.h" // for DebuggerSourceReferent +#include "vm/NativeObject.h" // for NativeObject + +namespace js { +class GlobalObject; +} + +namespace js { + +class DebuggerSource : public NativeObject { + public: + static const JSClass class_; + + enum { + SOURCE_SLOT, + OWNER_SLOT, + TEXT_SLOT, + RESERVED_SLOTS, + }; + + static NativeObject* initClass(JSContext* cx, Handle<GlobalObject*> global, + HandleObject debugCtor); + static DebuggerSource* create(JSContext* cx, HandleObject proto, + Handle<DebuggerSourceReferent> referent, + Handle<NativeObject*> debugger); + + void trace(JSTracer* trc); + + using ReferentVariant = DebuggerSourceReferent; + + NativeObject* getReferentRawObject() const; + DebuggerSourceReferent getReferent() const; + + void clearReferent() { clearReservedSlotGCThingAsPrivate(SOURCE_SLOT); } + + static DebuggerSource* check(JSContext* cx, HandleValue v); + static bool construct(JSContext* cx, unsigned argc, Value* vp); + + struct CallData; + + Debugger* owner() const; + + private: + static const JSClassOps classOps_; + + static const JSPropertySpec properties_[]; + static const JSFunctionSpec methods_[]; +}; + +} /* namespace js */ + +#endif /* dbg_Source_h */ diff --git a/js/src/debugger/moz.build b/js/src/debugger/moz.build new file mode 100644 index 0000000000..c8c162bcbd --- /dev/null +++ b/js/src/debugger/moz.build @@ -0,0 +1,31 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +# We give js/src/debugger its own moz.build file, separate from +# js/src/moz.build, so that the object file names don't conflict with those from +# other directories. For example, js/src/debugger/Object.cpp and +# js/src/builtin/Object.cpp had better not smash each other's .o files when +# unified sources are disabled. + +FINAL_LIBRARY = "js" + +# Includes should be relative to parent path +LOCAL_INCLUDES += ["!..", ".."] + +include("../js-config.mozbuild") +include("../js-cxxflags.mozbuild") + +UNIFIED_SOURCES = [ + "Debugger.cpp", + "DebuggerMemory.cpp", + "DebugScript.cpp", + "Environment.cpp", + "Frame.cpp", + "NoExecute.cpp", + "Object.cpp", + "Script.cpp", + "Source.cpp", +] |