summaryrefslogtreecommitdiffstats
path: root/js/src/debugger
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/debugger')
-rw-r--r--js/src/debugger/DebugAPI-inl.h180
-rw-r--r--js/src/debugger/DebugAPI.h414
-rw-r--r--js/src/debugger/DebugScript.cpp411
-rw-r--r--js/src/debugger/DebugScript.h161
-rw-r--r--js/src/debugger/Debugger-inl.h32
-rw-r--r--js/src/debugger/Debugger.cpp7059
-rw-r--r--js/src/debugger/Debugger.h1631
-rw-r--r--js/src/debugger/DebuggerMemory.cpp440
-rw-r--r--js/src/debugger/DebuggerMemory.h39
-rw-r--r--js/src/debugger/Environment-inl.h25
-rw-r--r--js/src/debugger/Environment.cpp664
-rw-r--r--js/src/debugger/Environment.h97
-rw-r--r--js/src/debugger/Frame-inl.h27
-rw-r--r--js/src/debugger/Frame.cpp1950
-rw-r--r--js/src/debugger/Frame.h300
-rw-r--r--js/src/debugger/NoExecute.cpp90
-rw-r--r--js/src/debugger/NoExecute.h94
-rw-r--r--js/src/debugger/Object-inl.h41
-rw-r--r--js/src/debugger/Object.cpp2689
-rw-r--r--js/src/debugger/Object.h221
-rw-r--r--js/src/debugger/Script-inl.h54
-rw-r--r--js/src/debugger/Script.cpp2419
-rw-r--r--js/src/debugger/Script.h85
-rw-r--r--js/src/debugger/Source.cpp658
-rw-r--r--js/src/debugger/Source.h62
-rw-r--r--js/src/debugger/moz.build31
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",
+]