Bug 1592116 - Mark Debuggers with hooks by tracing from the Realm. r=jimb

Differential Revision: https://phabricator.services.mozilla.com/D51226

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Logan Smyth 2019-11-27 00:48:00 +00:00
Родитель 678b770b5c
Коммит 6e176ae4f7
7 изменённых файлов: 158 добавлений и 116 удалений

Просмотреть файл

@ -105,23 +105,6 @@ class DebugAPI {
static inline void traceGeneratorFrame(JSTracer* tracer,
AbstractGeneratorObject* generator);
/*
* A Debugger object is live if:
* * the Debugger JSObject is live (Debugger::trace handles this case); OR
* * it is in the middle of dispatching an event (the event dispatching
* code roots it in this case); OR
* * it is debugging at least one live compartment, and at least one of the
* following is true:
* - it has a debugger hook installed
* - it has a breakpoint set on a live script
* - it has a watchpoint set on a live object.
*
* DebugAPI::markIteratively handles the last case. If it finds any Debugger
* objects that are definitely live but not yet marked, it marks them and
* returns true. If not, it returns false.
*/
static MOZ_MUST_USE bool markIteratively(GCMarker* marker);
// Trace cross compartment edges in all debuggers relevant to the current GC.
static void traceCrossCompartmentEdges(JSTracer* tracer);
@ -131,6 +114,8 @@ class DebugAPI {
// Trace debugging information for a JSScript.
static void traceDebugScript(JSTracer* trc, JSScript* script);
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

Просмотреть файл

@ -628,11 +628,10 @@ static bool DebuggerExists(
// explicitly.
JS::AutoSuppressGCAnalysis nogc;
Realm::DebuggerVector& debuggers = global->getDebuggers();
for (auto p = debuggers.begin(); p != debuggers.end(); p++) {
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(p->unbarrieredGet())) {
if (predicate(entry.dbg.unbarrieredGet())) {
return true;
}
}
@ -679,7 +678,7 @@ JSObject* Debugger::getHook(Hook hook) const {
return v.isUndefined() ? nullptr : &v.toObject();
}
bool Debugger::hasAnyLiveHooks(JSRuntime* rt) const {
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.
@ -743,11 +742,11 @@ ResumeMode DebugAPI::slowPathOnResumeFrame(JSContext* cx,
// 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.
Realm::DebuggerVector& debuggers = frame.global()->getDebuggers();
for (Debugger* dbg : debuggers) {
if (Debugger::GeneratorWeakMap::Ptr entry =
for (Realm::DebuggerVectorEntry& entry : frame.global()->getDebuggers()) {
Debugger* dbg = entry.dbg;
if (Debugger::GeneratorWeakMap::Ptr generatorEntry =
dbg->generatorFrames.lookup(genObj)) {
DebuggerFrame* frameObj = &entry->value()->as<DebuggerFrame>();
DebuggerFrame* frameObj = &generatorEntry->value()->as<DebuggerFrame>();
MOZ_ASSERT(&frameObj->unwrappedGenerator() == genObj);
if (!dbg->frames.putNew(frame, frameObj)) {
ReportOutOfMemory(cx);
@ -2218,9 +2217,8 @@ ResumeMode Debugger::dispatchHook(JSContext* cx, HookIsEnabledFun hookIsEnabled,
// different compartments--every compartment *except* this one.
RootedValueVector triggered(cx);
Handle<GlobalObject*> global = cx->global();
Realm::DebuggerVector& debuggers = global->getDebuggers();
for (auto p = debuggers.begin(); p != debuggers.end(); p++) {
Debugger* dbg = *p;
for (Realm::DebuggerVectorEntry& entry : global->getDebuggers()) {
Debugger* dbg = entry.dbg;
if (hookIsEnabled(dbg)) {
if (!triggered.append(ObjectValue(*dbg->toJSObject()))) {
return ResumeMode::Terminate;
@ -2495,10 +2493,8 @@ ResumeMode DebugAPI::onSingleStep(JSContext* cx, MutableHandleValue vp) {
uint32_t liveStepperCount = 0;
uint32_t suspendedStepperCount = 0;
JSScript* trappingScript = iter.script();
GlobalObject* global = cx->global();
Realm::DebuggerVector& debuggers = global->getDebuggers();
for (auto p = debuggers.begin(); p != debuggers.end(); p++) {
Debugger* dbg = *p;
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();
@ -2686,7 +2682,8 @@ void DebugAPI::slowPathOnNewGlobalObject(JSContext* cx,
void DebugAPI::slowPathNotifyParticipatesInGC(uint64_t majorGCNumber,
Realm::DebuggerVector& dbgs) {
for (Realm::DebuggerVector::Range r = dbgs.all(); !r.empty(); r.popFront()) {
if (!r.front().unbarrieredGet()->debuggeeIsBeingCollected(majorGCNumber)) {
if (!r.front().dbg.unbarrieredGet()->debuggeeIsBeingCollected(
majorGCNumber)) {
#ifdef DEBUG
fprintf(stderr,
"OOM while notifying observing Debuggers of a GC: The "
@ -2705,7 +2702,7 @@ Maybe<double> DebugAPI::allocationSamplingProbability(GlobalObject* global) {
return Nothing();
}
DebugOnly<WeakHeapPtr<Debugger*>*> begin = dbgs.begin();
DebugOnly<Realm::DebuggerVectorEntry*> begin = dbgs.begin();
double probability = 0;
bool foundAnyDebuggers = false;
@ -2715,7 +2712,7 @@ Maybe<double> DebugAPI::allocationSamplingProbability(GlobalObject* global) {
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->unbarrieredGet();
Debugger* dbgp = p->dbg.unbarrieredGet();
if (dbgp->trackingAllocationSites) {
foundAnyDebuggers = true;
@ -2732,7 +2729,7 @@ bool DebugAPI::slowPathOnLogAllocationSite(JSContext* cx, HandleObject obj,
mozilla::TimeStamp when,
Realm::DebuggerVector& dbgs) {
MOZ_ASSERT(!dbgs.empty());
mozilla::DebugOnly<WeakHeapPtr<Debugger*>*> begin = dbgs.begin();
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.
@ -2742,19 +2739,19 @@ bool DebugAPI::slowPathOnLogAllocationSite(JSContext* cx, HandleObject obj,
// 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 dbgp = dbgs.begin(); dbgp < dbgs.end(); dbgp++) {
if (!activeDebuggers.append((*dbgp)->object)) {
for (auto p = dbgs.begin(); p < dbgs.end(); p++) {
if (!activeDebuggers.append(p->dbg->object)) {
return false;
}
}
for (auto dbgp = dbgs.begin(); dbgp < dbgs.end(); dbgp++) {
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 ((*dbgp)->trackingAllocationSites &&
!(*dbgp)->appendAllocationSite(cx, obj, frame, when)) {
if (p->dbg->trackingAllocationSites &&
!p->dbg->appendAllocationSite(cx, obj, frame, when)) {
return false;
}
}
@ -3206,12 +3203,10 @@ bool Debugger::updateExecutionObservabilityOfScripts(
template <typename FrameFn>
/* static */
void Debugger::forEachDebuggerFrame(AbstractFramePtr frame, FrameFn fn) {
GlobalObject* global = frame.global();
Realm::DebuggerVector& debuggers = global->getDebuggers();
for (auto p = debuggers.begin(); p != debuggers.end(); p++) {
Debugger* dbg = *p;
if (FrameMap::Ptr entry = dbg->frames.lookup(frame)) {
fn(entry->value());
for (Realm::DebuggerVectorEntry& entry : frame.global()->getDebuggers()) {
Debugger* dbg = entry.dbg;
if (FrameMap::Ptr frameEntry = dbg->frames.lookup(frame)) {
fn(frameEntry->value());
}
}
}
@ -3430,11 +3425,10 @@ bool Debugger::cannotTrackAllocations(const GlobalObject& global) {
/* static */
bool DebugAPI::isObservedByDebuggerTrackingAllocations(
const GlobalObject& debuggee) {
auto& v = debuggee.getDebuggers();
for (auto p = v.begin(); p != v.end(); p++) {
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 = p->unbarrieredGet();
Debugger* dbg = entry.dbg.unbarrieredGet();
if (dbg->trackingAllocationSites) {
return true;
}
@ -3695,9 +3689,8 @@ void DebugAPI::slowPathTraceGeneratorFrame(JSTracer* tracer,
return;
}
Realm::DebuggerVector& debuggers = generator->realm()->getDebuggers();
for (js::WeakHeapPtr<Debugger*>& dbgWeak : debuggers) {
Debugger* dbg = dbgWeak.unbarrieredGet();
for (Realm::DebuggerVectorEntry& entry : generator->realm()->getDebuggers()) {
Debugger* dbg = entry.dbg.unbarrieredGet();
if (Debugger::GeneratorWeakMap::Ptr entry =
dbg->generatorFrames.lookupUnbarriered(generator)) {
@ -3713,59 +3706,6 @@ void DebugAPI::slowPathTraceGeneratorFrame(JSTracer* tracer,
}
}
/*
* This method has two tasks:
* 1. Mark Debugger objects that are unreachable except for debugger hooks
* that may yet be called.
* 2. Mark breakpoint handlers.
*
* This happens during the iterative part of the GC mark phase. This method
* returns true if it has to mark anything; GC calls it repeatedly until it
* returns false.
*/
/* static */
bool DebugAPI::markIteratively(GCMarker* marker) {
MOZ_ASSERT(JS::RuntimeHeapIsCollecting(),
"This method should be called during GC.");
bool markedAny = false;
// Find all Debugger objects in danger of GC. This code is a little
// convoluted since the easiest way to find them is via their debuggees.
JSRuntime* rt = marker->runtime();
for (RealmsIter r(rt); !r.done(); r.next()) {
if (r->isDebuggee()) {
GlobalObject* global = r->unsafeUnbarrieredMaybeGlobal();
if (!IsMarkedUnbarriered(rt, &global)) {
continue;
}
const Realm::DebuggerVector& debuggers = global->getDebuggers();
for (auto p = debuggers.begin(); p != debuggers.end(); p++) {
Debugger* dbg = p->unbarrieredGet();
// dbg is a Debugger with at least one debuggee. Check three things:
// - dbg is actually in a compartment that is being marked
// - it isn't already marked
// - it actually has hooks that might be called
GCPtrNativeObject& dbgobj = dbg->toJSObjectRef();
if (!dbgobj->zone()->isGCMarking()) {
continue;
}
bool dbgMarked = IsMarked(rt, &dbgobj);
if (!dbgMarked && dbg->hasAnyLiveHooks(rt)) {
// obj could be reachable only via its live, enabled
// debugger hooks, which may yet be called.
TraceEdge(marker, &dbgobj, "Debugger with live hooks");
markedAny = true;
dbgMarked = true;
}
}
}
}
return markedAny;
}
/* static */
void DebugAPI::traceAllForMovingGC(JSTracer* trc) {
JSRuntime* rt = trc->runtime();
@ -3819,6 +3759,13 @@ void Debugger::trace(JSTracer* 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(JSFreeOp* fop) {
JSRuntime* rt = fop->runtime();
@ -3873,8 +3820,8 @@ void Debugger::detachAllDebuggersFromGlobal(JSFreeOp* fop,
const Realm::DebuggerVector& debuggers = global->getDebuggers();
MOZ_ASSERT(!debuggers.empty());
while (!debuggers.empty()) {
debuggers.back()->removeDebuggeeGlobal(fop, global, nullptr,
Debugger::FromSweep::No);
debuggers.back().dbg->removeDebuggeeGlobal(fop, global, nullptr,
Debugger::FromSweep::No);
}
}
@ -4074,6 +4021,14 @@ bool Debugger::setHookImpl(JSContext* cx, const CallArgs& args, Debugger& dbg,
return false;
}
}
Rooted<DebuggerDebuggeeLink*> debuggeeLink(cx, dbg.getDebuggeeLink());
if (dbg.hasAnyLiveHooks()) {
debuggeeLink->setLinkSlot(dbg);
} else {
debuggeeLink->clearLinkSlot();
}
args.rval().setUndefined();
return true;
}
@ -4499,6 +4454,13 @@ bool Debugger::construct(JSContext* cx, unsigned argc, Value* vp) {
}
obj->setReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE, NullValue());
RootedNativeObject 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.
@ -4567,9 +4529,8 @@ bool Debugger::addDebuggeeGlobal(JSContext* cx, Handle<GlobalObject*> global) {
// Find all realms containing debuggers debugging realm's global object.
// Add those realms to visited.
if (realm->isDebuggee()) {
Realm::DebuggerVector& v = realm->getDebuggers();
for (auto p = v.begin(); p != v.end(); p++) {
Realm* next = (*p)->object->realm();
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;
@ -4593,9 +4554,14 @@ bool Debugger::addDebuggeeGlobal(JSContext* cx, Handle<GlobalObject*> global) {
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(this)) {
if (!globalDebuggers.append(Realm::DebuggerVectorEntry(this, debuggeeLink))) {
ReportOutOfMemory(cx);
return false;
}
@ -4665,7 +4631,7 @@ 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) {
if (p->dbg == dbg) {
break;
}
}
@ -6409,6 +6375,23 @@ void Debugger::removeFromFrameMapsAndClearBreakpointsIn(JSContext* cx,
}
}
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) {

Просмотреть файл

@ -92,6 +92,7 @@ class DebuggerSource;
class DebuggerMemory;
class ScriptedOnStepHandler;
class ScriptedOnPopHandler;
class DebuggerDebuggeeLink;
/**
* A completion value, describing how some sort of JavaScript evaluation
@ -483,6 +484,7 @@ class Debugger : private mozilla::LinkedListElement<Debugger> {
JSSLOT_DEBUG_HOOK_START = JSSLOT_DEBUG_PROTO_STOP,
JSSLOT_DEBUG_HOOK_STOP = JSSLOT_DEBUG_HOOK_START + HookCount,
JSSLOT_DEBUG_MEMORY_INSTANCE = JSSLOT_DEBUG_HOOK_STOP,
JSSLOT_DEBUG_DEBUGGEE_LINK,
JSSLOT_DEBUG_COUNT
};
@ -915,7 +917,7 @@ class Debugger : private mozilla::LinkedListElement<Debugger> {
void updateObservesAsmJSOnDebuggees(IsObserving observing);
JSObject* getHook(Hook hook) const;
bool hasAnyLiveHooks(JSRuntime* rt) const;
bool hasAnyLiveHooks() const;
static void slowPathPromiseHook(JSContext* cx, Hook hook,
Handle<PromiseObject*> promise);
@ -1149,11 +1151,57 @@ class Debugger : private mozilla::LinkedListElement<Debugger> {
DebuggerSource* wrapWasmSource(JSContext* cx,
Handle<WasmInstanceObject*> wasmInstance);
DebuggerDebuggeeLink* getDebuggeeLink();
private:
Debugger(const Debugger&) = delete;
Debugger& operator=(const Debugger&) = delete;
};
/**
* 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

Просмотреть файл

@ -4176,7 +4176,6 @@ void GCRuntime::markWeakReferences(gcstats::PhaseKind phase) {
markedAny |= WeakMapBase::markZoneIteratively(zone, &marker);
}
}
markedAny |= DebugAPI::markIteratively(&marker);
markedAny |= jit::JitRuntime::MarkJitcodeGlobalTableIteratively(&marker);
if (!markedAny) {

Просмотреть файл

@ -38,6 +38,10 @@
using namespace js;
Realm::DebuggerVectorEntry::DebuggerVectorEntry(js::Debugger* dbg_,
JSObject* link)
: dbg(dbg_), debuggerLink(link) {}
ObjectRealm::ObjectRealm(JS::Zone* zone)
: innerViews(zone, zone), iteratorCache(zone) {}
@ -269,6 +273,8 @@ void Realm::traceGlobal(JSTracer* trc) {
savedStacks_.trace(trc);
DebugAPI::traceFromRealm(trc, this);
// Atoms are always tenured.
if (!JS::RuntimeHeapIsMinorCollecting()) {
varNames_.trace(trc);

Просмотреть файл

@ -385,8 +385,26 @@ class JS::Realm : public JS::shadow::Realm {
unsigned enterRealmDepthIgnoringJit_ = 0;
public:
struct DebuggerVectorEntry {
// The debugger relies on iterating through the DebuggerVector to know what
// debuggers to notify about certain actions, which it does using this
// pointer. We need an explicit Debugger* because the JSObject* from
// the DebuggerDebuggeeLink to the Debugger is only set some of the time.
// This `Debugger*` pointer itself could also live on the
// DebuggerDebuggeeLink itself, but that would then require all of the
// places that iterate over the realm's DebuggerVector to also traverse
// the CCW which seems like it would be needlessly complicated.
js::WeakHeapPtr<js::Debugger*> dbg;
// This links to the debugger's DebuggerDebuggeeLink object, via a CCW.
// Tracing this link from the realm allows the debugger to define
// whether pieces of the debugger should be held live by a given realm.
js::HeapPtr<JSObject*> debuggerLink;
DebuggerVectorEntry(js::Debugger* dbg_, JSObject* link);
};
using DebuggerVector =
js::Vector<js::WeakHeapPtr<js::Debugger*>, 0, js::ZoneAllocPolicy>;
js::Vector<DebuggerVectorEntry, 0, js::ZoneAllocPolicy>;
private:
DebuggerVector debuggers_;

Просмотреть файл

@ -2586,6 +2586,9 @@ GlobalObject* JSRuntime::createSelfHostingGlobal(JSContext* cx) {
JS::RealmOptions options;
options.creationOptions().setNewCompartmentAndZone();
// Debugging the selfHosted zone is not supported because CCWs are not
// allowed in that zone.
options.creationOptions().setInvisibleToDebugger(true);
options.behaviors().setDiscardSource(true);
Realm* realm = NewRealm(cx, nullptr, options);