blob: 5b00d8e225d4875f95063eb055c46cfc61d1aa5a [file] [log] [blame]
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
* vim: set ts=8 sts=4 et sw=4 tw=99:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "vm/Debugger-inl.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/TypeTraits.h"
#include "jscntxt.h"
#include "jscompartment.h"
#include "jsfriendapi.h"
#include "jshashutil.h"
#include "jsnum.h"
#include "jsobj.h"
#include "jswrapper.h"
#include "frontend/BytecodeCompiler.h"
#include "gc/Marking.h"
#include "jit/BaselineDebugModeOSR.h"
#include "jit/BaselineJIT.h"
#include "jit/JSONSpewer.h"
#include "jit/MIRGraph.h"
#include "js/GCAPI.h"
#include "js/UbiNodeBreadthFirst.h"
#include "js/Vector.h"
#include "vm/ArgumentsObject.h"
#include "vm/DebuggerMemory.h"
#include "vm/SPSProfiler.h"
#include "vm/TraceLogging.h"
#include "vm/WrapperObject.h"
#include "jsgcinlines.h"
#include "jsobjinlines.h"
#include "jsopcodeinlines.h"
#include "jsscriptinlines.h"
#include "vm/NativeObject-inl.h"
#include "vm/Stack-inl.h"
using namespace js;
using JS::dbg::AutoEntryMonitor;
using JS::dbg::Builder;
using js::frontend::IsIdentifier;
using mozilla::ArrayLength;
using mozilla::DebugOnly;
using mozilla::MakeScopeExit;
using mozilla::Maybe;
using mozilla::UniquePtr;
/*** Forward declarations ************************************************************************/
extern const Class DebuggerFrame_class;
enum {
JSSLOT_DEBUGFRAME_OWNER,
JSSLOT_DEBUGFRAME_ARGUMENTS,
JSSLOT_DEBUGFRAME_ONSTEP_HANDLER,
JSSLOT_DEBUGFRAME_ONPOP_HANDLER,
JSSLOT_DEBUGFRAME_COUNT
};
extern const Class DebuggerArguments_class;
enum {
JSSLOT_DEBUGARGUMENTS_FRAME,
JSSLOT_DEBUGARGUMENTS_COUNT
};
extern const Class DebuggerEnv_class;
enum {
JSSLOT_DEBUGENV_OWNER,
JSSLOT_DEBUGENV_COUNT
};
extern const Class DebuggerObject_class;
enum {
JSSLOT_DEBUGOBJECT_OWNER,
JSSLOT_DEBUGOBJECT_COUNT
};
extern const Class DebuggerScript_class;
enum {
JSSLOT_DEBUGSCRIPT_OWNER,
JSSLOT_DEBUGSCRIPT_COUNT
};
extern const Class DebuggerSource_class;
enum {
JSSLOT_DEBUGSOURCE_OWNER,
JSSLOT_DEBUGSOURCE_TEXT,
JSSLOT_DEBUGSOURCE_COUNT
};
void DebuggerObject_trace(JSTracer* trc, JSObject* obj);
void DebuggerEnv_trace(JSTracer* trc, JSObject* obj);
void DebuggerScript_trace(JSTracer* trc, JSObject* obj);
void DebuggerSource_trace(JSTracer* trc, JSObject* obj);
/*** Utils ***************************************************************************************/
static inline bool
EnsureFunctionHasScript(JSContext* cx, HandleFunction fun)
{
if (fun->isInterpretedLazy()) {
AutoCompartment ac(cx, fun);
return !!fun->getOrCreateScript(cx);
}
return true;
}
static inline JSScript*
GetOrCreateFunctionScript(JSContext* cx, HandleFunction fun)
{
MOZ_ASSERT(fun->isInterpreted());
if (!EnsureFunctionHasScript(cx, fun))
return nullptr;
return fun->nonLazyScript();
}
static bool
ValueToIdentifier(JSContext* cx, HandleValue v, MutableHandleId id)
{
if (!ValueToId<CanGC>(cx, v, id))
return false;
if (!JSID_IS_ATOM(id) || !IsIdentifier(JSID_TO_ATOM(id))) {
RootedValue val(cx, v);
ReportValueErrorFlags(cx, JSREPORT_ERROR, JSMSG_UNEXPECTED_TYPE,
JSDVG_SEARCH_STACK, val, nullptr, "not an identifier",
nullptr);
return false;
}
return true;
}
/*
* A range of all the Debugger.Frame objects for a particular AbstractFramePtr.
*
* FIXME This checks only current debuggers, so it relies on a hack in
* Debugger::removeDebuggeeGlobal to make sure only current debuggers
* have Frame objects with .live === true.
*/
class Debugger::FrameRange
{
AbstractFramePtr frame;
/* The debuggers in |fp|'s compartment, or nullptr if there are none. */
GlobalObject::DebuggerVector* debuggers;
/*
* The index of the front Debugger.Frame's debugger in debuggers.
* nextDebugger < debuggerCount if and only if the range is not empty.
*/
size_t debuggerCount, nextDebugger;
/*
* If the range is not empty, this is front Debugger.Frame's entry in its
* debugger's frame table.
*/
FrameMap::Ptr entry;
public:
/*
* Return a range containing all Debugger.Frame instances referring to
* |fp|. |global| is |fp|'s global object; if nullptr or omitted, we
* compute it ourselves from |fp|.
*
* We keep an index into the compartment's debugger list, and a
* FrameMap::Ptr into the current debugger's frame map. Thus, if the set of
* debuggers in |fp|'s compartment changes, this range becomes invalid.
* Similarly, if stack frames are added to or removed from frontDebugger(),
* then the range's front is invalid until popFront is called.
*/
explicit FrameRange(AbstractFramePtr frame, GlobalObject* global = nullptr)
: frame(frame)
{
nextDebugger = 0;
/* Find our global, if we were not given one. */
if (!global)
global = &frame.script()->global();
/* The frame and global must match. */
MOZ_ASSERT(&frame.script()->global() == global);
/* Find the list of debuggers we'll iterate over. There may be none. */
debuggers = global->getDebuggers();
if (debuggers) {
debuggerCount = debuggers->length();
findNext();
} else {
debuggerCount = 0;
}
}
bool empty() const {
return nextDebugger >= debuggerCount;
}
NativeObject* frontFrame() const {
MOZ_ASSERT(!empty());
return entry->value();
}
Debugger* frontDebugger() const {
MOZ_ASSERT(!empty());
return (*debuggers)[nextDebugger];
}
/*
* Delete the front frame from its Debugger's frame map. After this call,
* the range's front is invalid until popFront is called.
*/
void removeFrontFrame() const {
MOZ_ASSERT(!empty());
frontDebugger()->frames.remove(entry);
}
void popFront() {
MOZ_ASSERT(!empty());
nextDebugger++;
findNext();
}
private:
/*
* Either make this range refer to the first appropriate Debugger.Frame at
* or after nextDebugger, or make it empty.
*/
void findNext() {
while (!empty()) {
Debugger* dbg = (*debuggers)[nextDebugger];
entry = dbg->frames.lookup(frame);
if (entry)
break;
nextDebugger++;
}
}
};
/*** Breakpoints *********************************************************************************/
BreakpointSite::BreakpointSite(JSScript* script, jsbytecode* pc)
: script(script), pc(pc), enabledCount(0)
{
MOZ_ASSERT(!script->hasBreakpointsAt(pc));
JS_INIT_CLIST(&breakpoints);
}
void
BreakpointSite::recompile(FreeOp* fop)
{
if (script->hasBaselineScript())
script->baselineScript()->toggleDebugTraps(script, pc);
}
void
BreakpointSite::inc(FreeOp* fop)
{
enabledCount++;
if (enabledCount == 1)
recompile(fop);
}
void
BreakpointSite::dec(FreeOp* fop)
{
MOZ_ASSERT(enabledCount > 0);
enabledCount--;
if (enabledCount == 0)
recompile(fop);
}
void
BreakpointSite::destroyIfEmpty(FreeOp* fop)
{
if (JS_CLIST_IS_EMPTY(&breakpoints))
script->destroyBreakpointSite(fop, pc);
}
Breakpoint*
BreakpointSite::firstBreakpoint() const
{
if (JS_CLIST_IS_EMPTY(&breakpoints))
return nullptr;
return Breakpoint::fromSiteLinks(JS_NEXT_LINK(&breakpoints));
}
bool
BreakpointSite::hasBreakpoint(Breakpoint* bp)
{
for (Breakpoint* p = firstBreakpoint(); p; p = p->nextInSite())
if (p == bp)
return true;
return false;
}
Breakpoint::Breakpoint(Debugger* debugger, BreakpointSite* site, JSObject* handler)
: debugger(debugger), site(site), handler(handler)
{
MOZ_ASSERT(handler->compartment() == debugger->object->compartment());
JS_APPEND_LINK(&debuggerLinks, &debugger->breakpoints);
JS_APPEND_LINK(&siteLinks, &site->breakpoints);
}
Breakpoint*
Breakpoint::fromDebuggerLinks(JSCList* links)
{
return (Breakpoint*) ((unsigned char*) links - offsetof(Breakpoint, debuggerLinks));
}
Breakpoint*
Breakpoint::fromSiteLinks(JSCList* links)
{
return (Breakpoint*) ((unsigned char*) links - offsetof(Breakpoint, siteLinks));
}
void
Breakpoint::destroy(FreeOp* fop)
{
if (debugger->enabled)
site->dec(fop);
JS_REMOVE_LINK(&debuggerLinks);
JS_REMOVE_LINK(&siteLinks);
site->destroyIfEmpty(fop);
fop->delete_(this);
}
Breakpoint*
Breakpoint::nextInDebugger()
{
JSCList* link = JS_NEXT_LINK(&debuggerLinks);
return (link == &debugger->breakpoints) ? nullptr : fromDebuggerLinks(link);
}
Breakpoint*
Breakpoint::nextInSite()
{
JSCList* link = JS_NEXT_LINK(&siteLinks);
return (link == &site->breakpoints) ? nullptr : fromSiteLinks(link);
}
/*** Debugger hook dispatch **********************************************************************/
Debugger::Debugger(JSContext* cx, NativeObject* dbg)
: object(dbg),
uncaughtExceptionHook(nullptr),
enabled(true),
allowUnobservedAsmJS(false),
collectCoverageInfo(false),
observedGCs(cx),
tenurePromotionsLog(cx),
trackingTenurePromotions(false),
maxTenurePromotionsLogLength(DEFAULT_MAX_LOG_LENGTH),
tenurePromotionsLogOverflowed(false),
allocationsLog(cx),
trackingAllocationSites(false),
allocationSamplingProbability(1.0),
maxAllocationsLogLength(DEFAULT_MAX_LOG_LENGTH),
allocationsLogOverflowed(false),
frames(cx->runtime()),
scripts(cx),
sources(cx),
objects(cx),
environments(cx),
#ifdef NIGHTLY_BUILD
traceLoggerLastDrainedSize(0),
traceLoggerLastDrainedIteration(0),
#endif
traceLoggerScriptedCallsLastDrainedSize(0),
traceLoggerScriptedCallsLastDrainedIteration(0)
{
assertSameCompartment(cx, dbg);
cx->runtime()->debuggerList.insertBack(this);
JS_INIT_CLIST(&breakpoints);
JS_INIT_CLIST(&onNewGlobalObjectWatchersLink);
}
Debugger::~Debugger()
{
MOZ_ASSERT_IF(debuggees.initialized(), debuggees.empty());
allocationsLog.clear();
tenurePromotionsLog.clear();
/*
* Since the inactive state for this link is a singleton cycle, it's always
* safe to apply JS_REMOVE_LINK to it, regardless of whether we're in the list or not.
*
* We don't have to worry about locking here since Debugger is not
* background finalized.
*/
JS_REMOVE_LINK(&onNewGlobalObjectWatchersLink);
}
bool
Debugger::init(JSContext* cx)
{
bool ok = debuggees.init() &&
debuggeeZones.init() &&
frames.init() &&
scripts.init() &&
sources.init() &&
objects.init() &&
observedGCs.init() &&
environments.init();
if (!ok)
ReportOutOfMemory(cx);
return ok;
}
JS_STATIC_ASSERT(unsigned(JSSLOT_DEBUGFRAME_OWNER) == unsigned(JSSLOT_DEBUGSCRIPT_OWNER));
JS_STATIC_ASSERT(unsigned(JSSLOT_DEBUGFRAME_OWNER) == unsigned(JSSLOT_DEBUGSOURCE_OWNER));
JS_STATIC_ASSERT(unsigned(JSSLOT_DEBUGFRAME_OWNER) == unsigned(JSSLOT_DEBUGOBJECT_OWNER));
JS_STATIC_ASSERT(unsigned(JSSLOT_DEBUGFRAME_OWNER) == unsigned(JSSLOT_DEBUGENV_OWNER));
/* static */ Debugger*
Debugger::fromChildJSObject(JSObject* obj)
{
MOZ_ASSERT(obj->getClass() == &DebuggerFrame_class ||
obj->getClass() == &DebuggerScript_class ||
obj->getClass() == &DebuggerSource_class ||
obj->getClass() == &DebuggerObject_class ||
obj->getClass() == &DebuggerEnv_class);
JSObject* dbgobj = &obj->as<NativeObject>().getReservedSlot(JSSLOT_DEBUGOBJECT_OWNER).toObject();
return fromJSObject(dbgobj);
}
bool
Debugger::hasMemory() const
{
return object->getReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE).isObject();
}
DebuggerMemory&
Debugger::memory() const
{
MOZ_ASSERT(hasMemory());
return object->getReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE).toObject().as<DebuggerMemory>();
}
bool
Debugger::getScriptFrameWithIter(JSContext* cx, AbstractFramePtr frame,
const ScriptFrameIter* maybeIter, MutableHandleValue vp)
{
MOZ_ASSERT_IF(maybeIter, maybeIter->abstractFramePtr() == frame);
MOZ_ASSERT(!frame.script()->selfHosted());
FrameMap::AddPtr p = frames.lookupForAdd(frame);
if (!p) {
/* Create and populate the Debugger.Frame object. */
RootedObject proto(cx, &object->getReservedSlot(JSSLOT_DEBUG_FRAME_PROTO).toObject());
RootedNativeObject frameobj(cx, NewNativeObjectWithGivenProto(cx, &DebuggerFrame_class,
proto));
if (!frameobj)
return false;
// Eagerly copy ScriptFrameIter data if we've already walked the
// stack.
if (maybeIter) {
AbstractFramePtr data = maybeIter->copyDataAsAbstractFramePtr();
if (!data)
return false;
frameobj->setPrivate(data.raw());
} else {
frameobj->setPrivate(frame.raw());
}
frameobj->setReservedSlot(JSSLOT_DEBUGFRAME_OWNER, ObjectValue(*object));
if (!ensureExecutionObservabilityOfFrame(cx, frame))
return false;
if (!frames.add(p, frame, frameobj)) {
ReportOutOfMemory(cx);
return false;
}
}
vp.setObject(*p->value());
return true;
}
/* static */ bool
Debugger::hasLiveHook(GlobalObject* global, Hook which)
{
if (GlobalObject::DebuggerVector* debuggers = global->getDebuggers()) {
for (Debugger** p = debuggers->begin(); p != debuggers->end(); p++) {
Debugger* dbg = *p;
if (dbg->enabled && dbg->getHook(which))
return true;
}
}
return false;
}
JSObject*
Debugger::getHook(Hook hook) const
{
MOZ_ASSERT(hook >= 0 && hook < HookCount);
const Value& v = object->getReservedSlot(JSSLOT_DEBUG_HOOK_START + hook);
return v.isUndefined() ? nullptr : &v.toObject();
}
bool
Debugger::hasAnyLiveHooks() const
{
if (!enabled)
return false;
if (getHook(OnDebuggerStatement) ||
getHook(OnExceptionUnwind) ||
getHook(OnNewScript) ||
getHook(OnEnterFrame))
{
return true;
}
/* If any breakpoints are in live scripts, return true. */
for (Breakpoint* bp = firstBreakpoint(); bp; bp = bp->nextInDebugger()) {
if (IsMarkedUnbarriered(&bp->site->script))
return true;
}
for (FrameMap::Range r = frames.all(); !r.empty(); r.popFront()) {
NativeObject* frameObj = r.front().value();
if (!frameObj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined() ||
!frameObj->getReservedSlot(JSSLOT_DEBUGFRAME_ONPOP_HANDLER).isUndefined())
return true;
}
return false;
}
/* static */ JSTrapStatus
Debugger::slowPathOnEnterFrame(JSContext* cx, AbstractFramePtr frame)
{
RootedValue rval(cx);
JSTrapStatus status = dispatchHook(
cx,
[frame](Debugger* dbg) -> bool {
return dbg->observesFrame(frame) && dbg->observesEnterFrame();
},
[&](Debugger* dbg) -> JSTrapStatus {
return dbg->fireEnterFrame(cx, frame, &rval);
});
switch (status) {
case JSTRAP_CONTINUE:
break;
case JSTRAP_THROW:
cx->setPendingException(rval);
break;
case JSTRAP_ERROR:
cx->clearPendingException();
break;
case JSTRAP_RETURN:
frame.setReturnValue(rval);
break;
default:
MOZ_CRASH("bad Debugger::onEnterFrame JSTrapStatus value");
}
return status;
}
static void
DebuggerFrame_maybeDecrementFrameScriptStepModeCount(FreeOp* fop, AbstractFramePtr frame,
NativeObject* frameobj);
static void
DebuggerFrame_freeScriptFrameIterData(FreeOp* fop, JSObject* obj);
/*
* Handle leaving a frame with debuggers watching. |frameOk| indicates whether
* the frame is exiting normally or abruptly. Set |cx|'s exception and/or
* |cx->fp()|'s return value, and return a new success value.
*/
/* static */ bool
Debugger::slowPathOnLeaveFrame(JSContext* cx, AbstractFramePtr frame, bool frameOk)
{
Handle<GlobalObject*> global = cx->global();
// The onPop handler and associated clean up logic should not run multiple
// times on the same frame. If slowPathOnLeaveFrame has already been
// called, the frame will not be present in the Debugger frame maps.
FrameRange frameRange(frame, global);
if (frameRange.empty())
return frameOk;
auto frameMapsGuard = MakeScopeExit([&] {
// Clean up all Debugger.Frame instances. This call creates a fresh
// FrameRange, as one debugger's onPop handler could have caused another
// debugger to create its own Debugger.Frame instance.
removeFromFrameMapsAndClearBreakpointsIn(cx, frame);
});
/* Save the frame's completion value. */
JSTrapStatus status;
RootedValue value(cx);
Debugger::resultToCompletion(cx, frameOk, frame.returnValue(), &status, &value);
// This path can be hit via unwinding the stack due to over-recursion or
// OOM. In those cases, don't fire the frames' onPop handlers, because
// invoking JS will only trigger the same condition. See
// slowPathOnExceptionUnwind.
if (!cx->isThrowingOverRecursed() && !cx->isThrowingOutOfMemory()) {
/* Build a list of the recipients. */
AutoObjectVector frames(cx);
for (; !frameRange.empty(); frameRange.popFront()) {
if (!frames.append(frameRange.frontFrame())) {
cx->clearPendingException();
return false;
}
}
/* For each Debugger.Frame, fire its onPop handler, if any. */
for (JSObject** p = frames.begin(); p != frames.end(); p++) {
RootedNativeObject frameobj(cx, &(*p)->as<NativeObject>());
Debugger* dbg = Debugger::fromChildJSObject(frameobj);
if (dbg->enabled &&
!frameobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONPOP_HANDLER).isUndefined()) {
RootedValue handler(cx, frameobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONPOP_HANDLER));
Maybe<AutoCompartment> ac;
ac.emplace(cx, dbg->object);
RootedValue completion(cx);
if (!dbg->newCompletionValue(cx, status, value, &completion)) {
status = dbg->handleUncaughtException(ac, false);
break;
}
/* Call the onPop handler. */
RootedValue rval(cx);
bool hookOk = Invoke(cx, ObjectValue(*frameobj), handler, 1, completion.address(),
&rval);
RootedValue nextValue(cx);
JSTrapStatus nextStatus = dbg->parseResumptionValue(ac, hookOk, rval, &nextValue);
/*
* At this point, we are back in the debuggee compartment, and any error has
* been wrapped up as a completion value.
*/
MOZ_ASSERT(cx->compartment() == global->compartment());
MOZ_ASSERT(!cx->isExceptionPending());
/* JSTRAP_CONTINUE means "make no change". */
if (nextStatus != JSTRAP_CONTINUE) {
status = nextStatus;
value = nextValue;
}
}
}
}
/* Establish (status, value) as our resumption value. */
switch (status) {
case JSTRAP_RETURN:
frame.setReturnValue(value);
return true;
case JSTRAP_THROW:
cx->setPendingException(value);
return false;
case JSTRAP_ERROR:
MOZ_ASSERT(!cx->isExceptionPending());
return false;
default:
MOZ_CRASH("bad final trap status");
}
}
/* static */ JSTrapStatus
Debugger::slowPathOnDebuggerStatement(JSContext* cx, AbstractFramePtr frame)
{
RootedValue rval(cx);
JSTrapStatus status = dispatchHook(
cx,
[](Debugger* dbg) -> bool { return dbg->getHook(OnDebuggerStatement); },
[&](Debugger* dbg) -> JSTrapStatus {
return dbg->fireDebuggerStatement(cx, &rval);
});
switch (status) {
case JSTRAP_CONTINUE:
case JSTRAP_ERROR:
break;
case JSTRAP_RETURN:
frame.setReturnValue(rval);
break;
case JSTRAP_THROW:
cx->setPendingException(rval);
break;
default:
MOZ_CRASH("Invalid onDebuggerStatement trap status");
}
return status;
}
/* static */ JSTrapStatus
Debugger::slowPathOnExceptionUnwind(JSContext* cx, AbstractFramePtr frame)
{
// Invoking more JS on an over-recursed stack or after OOM is only going
// to result in more of the same error.
if (cx->isThrowingOverRecursed() || cx->isThrowingOutOfMemory())
return JSTRAP_CONTINUE;
// The Debugger API mustn't muck with frames from self-hosted scripts.
if (frame.script()->selfHosted())
return JSTRAP_CONTINUE;
RootedValue rval(cx);
JSTrapStatus status = dispatchHook(
cx,
[](Debugger* dbg) -> bool { return dbg->getHook(OnExceptionUnwind); },
[&](Debugger* dbg) -> JSTrapStatus {
return dbg->fireExceptionUnwind(cx, &rval);
});
switch (status) {
case JSTRAP_CONTINUE:
break;
case JSTRAP_THROW:
cx->setPendingException(rval);
break;
case JSTRAP_ERROR:
cx->clearPendingException();
break;
case JSTRAP_RETURN:
cx->clearPendingException();
frame.setReturnValue(rval);
break;
default:
MOZ_CRASH("Invalid onExceptionUnwind trap status");
}
return status;
}
bool
Debugger::wrapEnvironment(JSContext* cx, Handle<Env*> env, MutableHandleValue rval)
{
if (!env) {
rval.setNull();
return true;
}
/*
* DebuggerEnv should only wrap a debug scope chain obtained (transitively)
* from GetDebugScopeFor(Frame|Function).
*/
MOZ_ASSERT(!IsSyntacticScope(env));
NativeObject* envobj;
DependentAddPtr<ObjectWeakMap> p(cx, environments, env);
if (p) {
envobj = &p->value()->as<NativeObject>();
} else {
/* Create a new Debugger.Environment for env. */
RootedObject proto(cx, &object->getReservedSlot(JSSLOT_DEBUG_ENV_PROTO).toObject());
envobj = NewNativeObjectWithGivenProto(cx, &DebuggerEnv_class, proto,
TenuredObject);
if (!envobj)
return false;
envobj->setPrivateGCThing(env);
envobj->setReservedSlot(JSSLOT_DEBUGENV_OWNER, ObjectValue(*object));
if (!p.add(cx, environments, env, envobj))
return false;
CrossCompartmentKey key(CrossCompartmentKey::DebuggerEnvironment, object, env);
if (!object->compartment()->putWrapper(cx, key, ObjectValue(*envobj))) {
environments.remove(env);
ReportOutOfMemory(cx);
return false;
}
}
rval.setObject(*envobj);
return true;
}
bool
Debugger::wrapDebuggeeValue(JSContext* cx, MutableHandleValue vp)
{
assertSameCompartment(cx, object.get());
if (vp.isObject()) {
RootedObject obj(cx, &vp.toObject());
if (obj->is<JSFunction>()) {
MOZ_ASSERT(!IsInternalFunctionObject(*obj));
RootedFunction fun(cx, &obj->as<JSFunction>());
if (!EnsureFunctionHasScript(cx, fun))
return false;
}
DependentAddPtr<ObjectWeakMap> p(cx, objects, obj);
if (p) {
vp.setObject(*p->value());
} else {
/* Create a new Debugger.Object for obj. */
RootedObject proto(cx, &object->getReservedSlot(JSSLOT_DEBUG_OBJECT_PROTO).toObject());
NativeObject* dobj =
NewNativeObjectWithGivenProto(cx, &DebuggerObject_class, proto,
TenuredObject);
if (!dobj)
return false;
dobj->setPrivateGCThing(obj);
dobj->setReservedSlot(JSSLOT_DEBUGOBJECT_OWNER, ObjectValue(*object));
if (!p.add(cx, objects, obj, dobj))
return false;
if (obj->compartment() != object->compartment()) {
CrossCompartmentKey key(CrossCompartmentKey::DebuggerObject, object, obj);
if (!object->compartment()->putWrapper(cx, key, ObjectValue(*dobj))) {
objects.remove(obj);
ReportOutOfMemory(cx);
return false;
}
}
vp.setObject(*dobj);
}
} else if (vp.isMagic()) {
RootedPlainObject optObj(cx, NewBuiltinClassInstance<PlainObject>(cx));
if (!optObj)
return false;
// We handle three sentinel values: missing arguments (overloading
// JS_OPTIMIZED_ARGUMENTS), optimized out slots (JS_OPTIMIZED_OUT),
// and uninitialized bindings (JS_UNINITIALIZED_LEXICAL).
//
// Other magic values should not have escaped.
PropertyName* name;
switch (vp.whyMagic()) {
case JS_OPTIMIZED_ARGUMENTS: name = cx->names().missingArguments; break;
case JS_OPTIMIZED_OUT: name = cx->names().optimizedOut; break;
case JS_UNINITIALIZED_LEXICAL: name = cx->names().uninitialized; break;
default: MOZ_CRASH("Unsupported magic value escaped to Debugger");
}
RootedValue trueVal(cx, BooleanValue(true));
if (!DefineProperty(cx, optObj, name, trueVal))
return false;
vp.setObject(*optObj);
} else if (!cx->compartment()->wrap(cx, vp)) {
vp.setUndefined();
return false;
}
return true;
}
bool
Debugger::unwrapDebuggeeObject(JSContext* cx, MutableHandleObject obj)
{
if (obj->getClass() != &DebuggerObject_class) {
JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE,
"Debugger", "Debugger.Object", obj->getClass()->name);
return false;
}
NativeObject* ndobj = &obj->as<NativeObject>();
Value owner = ndobj->getReservedSlot(JSSLOT_DEBUGOBJECT_OWNER);
if (owner.isUndefined() || &owner.toObject() != object) {
JS_ReportErrorNumber(cx, GetErrorMessage, nullptr,
owner.isUndefined()
? JSMSG_DEBUG_OBJECT_PROTO
: JSMSG_DEBUG_OBJECT_WRONG_OWNER);
return false;
}
obj.set(static_cast<JSObject*>(ndobj->getPrivate()));
return true;
}
bool
Debugger::unwrapDebuggeeValue(JSContext* cx, MutableHandleValue vp)
{
assertSameCompartment(cx, object.get(), vp);
if (vp.isObject()) {
RootedObject dobj(cx, &vp.toObject());
if (!unwrapDebuggeeObject(cx, &dobj))
return false;
vp.setObject(*dobj);
}
return true;
}
static bool
CheckArgCompartment(JSContext* cx, JSObject* obj, JSObject* arg,
const char* methodname, const char* propname)
{
if (arg->compartment() != obj->compartment()) {
JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_COMPARTMENT_MISMATCH,
methodname, propname);
return false;
}
return true;
}
static bool
CheckArgCompartment(JSContext* cx, JSObject* obj, HandleValue v,
const char* methodname, const char* propname)
{
if (v.isObject())
return CheckArgCompartment(cx, obj, &v.toObject(), methodname, propname);
return true;
}
bool
Debugger::unwrapPropertyDescriptor(JSContext* cx, HandleObject obj,
MutableHandle<PropertyDescriptor> desc)
{
if (desc.hasValue()) {
RootedValue value(cx, desc.value());
if (!unwrapDebuggeeValue(cx, &value) ||
!CheckArgCompartment(cx, obj, value, "defineProperty", "value"))
{
return false;
}
desc.setValue(value);
}
if (desc.hasGetterObject()) {
RootedObject get(cx, desc.getterObject());
if (get) {
if (!unwrapDebuggeeObject(cx, &get))
return false;
if (!CheckArgCompartment(cx, obj, get, "defineProperty", "get"))
return false;
}
desc.setGetterObject(get);
}
if (desc.hasSetterObject()) {
RootedObject set(cx, desc.setterObject());
if (set) {
if (!unwrapDebuggeeObject(cx, &set))
return false;
if (!CheckArgCompartment(cx, obj, set, "defineProperty", "set"))
return false;
}
desc.setSetterObject(set);
}
return true;
}
namespace {
class MOZ_STACK_CLASS ReportExceptionClosure : public ScriptEnvironmentPreparer::Closure
{
public:
explicit ReportExceptionClosure(RootedValue& exn)
: exn_(exn)
{
}
bool operator()(JSContext* cx) override
{
cx->setPendingException(exn_);
return false;
}
private:
RootedValue& exn_;
};
} // anonymous namespace
JSTrapStatus
Debugger::handleUncaughtExceptionHelper(Maybe<AutoCompartment>& ac,
MutableHandleValue* vp, bool callHook)
{
JSContext* cx = ac->context()->asJSContext();
if (cx->isExceptionPending()) {
if (callHook && uncaughtExceptionHook) {
RootedValue exc(cx);
if (!cx->getPendingException(&exc))
return JSTRAP_ERROR;
cx->clearPendingException();
RootedValue fval(cx, ObjectValue(*uncaughtExceptionHook));
RootedValue rv(cx);
if (Invoke(cx, ObjectValue(*object), fval, 1, exc.address(), &rv))
return vp ? parseResumptionValue(ac, true, rv, *vp, false) : JSTRAP_CONTINUE;
}
if (cx->isExceptionPending()) {
/*
* We want to report the pending exception, but we want to let the
* embedding handle it however it wants to. So pretend like we're
* starting a new script execution on our current compartment (which
* is the debugger compartment, so reported errors won't get
* reported to various onerror handlers in debuggees) and as part of
* that "execution" simply throw our exception so the embedding can
* deal.
*/
RootedValue exn(cx);
if (cx->getPendingException(&exn)) {
/*
* Clear the exception, because
* PrepareScriptEnvironmentAndInvoke will assert that we don't
* have one.
*/
cx->clearPendingException();
ReportExceptionClosure reportExn(exn);
PrepareScriptEnvironmentAndInvoke(cx, cx->global(), reportExn);
}
/*
* And if not, or if PrepareScriptEnvironmentAndInvoke somehow left
* an exception on cx (which it totally shouldn't do), just give
* up.
*/
cx->clearPendingException();
}
}
ac.reset();
return JSTRAP_ERROR;
}
JSTrapStatus
Debugger::handleUncaughtException(Maybe<AutoCompartment>& ac, MutableHandleValue vp, bool callHook)
{
return handleUncaughtExceptionHelper(ac, &vp, callHook);
}
JSTrapStatus
Debugger::handleUncaughtException(Maybe<AutoCompartment>& ac, bool callHook)
{
return handleUncaughtExceptionHelper(ac, nullptr, callHook);
}
/* static */ void
Debugger::resultToCompletion(JSContext* cx, bool ok, const Value& rv,
JSTrapStatus* status, MutableHandleValue value)
{
MOZ_ASSERT_IF(ok, !cx->isExceptionPending());
if (ok) {
*status = JSTRAP_RETURN;
value.set(rv);
} else if (cx->isExceptionPending()) {
*status = JSTRAP_THROW;
if (!cx->getPendingException(value))
*status = JSTRAP_ERROR;
cx->clearPendingException();
} else {
*status = JSTRAP_ERROR;
value.setUndefined();
}
}
bool
Debugger::newCompletionValue(JSContext* cx, JSTrapStatus status, Value value_,
MutableHandleValue result)
{
/*
* We must be in the debugger's compartment, since that's where we want
* to construct the completion value.
*/
assertSameCompartment(cx, object.get());
RootedId key(cx);
RootedValue value(cx, value_);
switch (status) {
case JSTRAP_RETURN:
key = NameToId(cx->names().return_);
break;
case JSTRAP_THROW:
key = NameToId(cx->names().throw_);
break;
case JSTRAP_ERROR:
result.setNull();
return true;
default:
MOZ_CRASH("bad status passed to Debugger::newCompletionValue");
}
/* Common tail for JSTRAP_RETURN and JSTRAP_THROW. */
RootedPlainObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx));
if (!obj ||
!wrapDebuggeeValue(cx, &value) ||
!NativeDefineProperty(cx, obj, key, value, nullptr, nullptr, JSPROP_ENUMERATE))
{
return false;
}
result.setObject(*obj);
return true;
}
bool
Debugger::receiveCompletionValue(Maybe<AutoCompartment>& ac, bool ok,
HandleValue val,
MutableHandleValue vp)
{
JSContext* cx = ac->context()->asJSContext();
JSTrapStatus status;
RootedValue value(cx);
resultToCompletion(cx, ok, val, &status, &value);
ac.reset();
return newCompletionValue(cx, status, value, vp);
}
static bool
GetStatusProperty(JSContext* cx, HandleObject obj, HandlePropertyName name, JSTrapStatus status,
JSTrapStatus* statusOut, MutableHandleValue vp, int* hits)
{
bool found;
if (!HasProperty(cx, obj, name, &found))
return false;
if (found) {
++*hits;
*statusOut = status;
if (!GetProperty(cx, obj, obj, name, vp))
return false;
}
return true;
}
static bool
ParseResumptionValueAsObject(JSContext* cx, HandleValue rv, JSTrapStatus* statusp,
MutableHandleValue vp)
{
int hits = 0;
if (rv.isObject()) {
RootedObject obj(cx, &rv.toObject());
if (!GetStatusProperty(cx, obj, cx->names().return_, JSTRAP_RETURN, statusp, vp, &hits))
return false;
if (!GetStatusProperty(cx, obj, cx->names().throw_, JSTRAP_THROW, statusp, vp, &hits))
return false;
}
if (hits != 1) {
JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_BAD_RESUMPTION);
return false;
}
return true;
}
JSTrapStatus
Debugger::parseResumptionValue(Maybe<AutoCompartment>& ac, bool ok, const Value& rv, MutableHandleValue vp,
bool callHook)
{
vp.setUndefined();
if (!ok)
return handleUncaughtException(ac, vp, callHook);
if (rv.isUndefined()) {
ac.reset();
return JSTRAP_CONTINUE;
}
if (rv.isNull()) {
ac.reset();
return JSTRAP_ERROR;
}
JSContext* cx = ac->context()->asJSContext();
JSTrapStatus status = JSTRAP_CONTINUE;
RootedValue v(cx);
RootedValue rvRoot(cx, rv);
if (!ParseResumptionValueAsObject(cx, rvRoot, &status, &v) ||
!unwrapDebuggeeValue(cx, &v))
{
return handleUncaughtException(ac, vp, callHook);
}
ac.reset();
if (!cx->compartment()->wrap(cx, &v)) {
vp.setUndefined();
return JSTRAP_ERROR;
}
vp.set(v);
return status;
}
static bool
CallMethodIfPresent(JSContext* cx, HandleObject obj, const char* name, int argc, Value* argv,
MutableHandleValue rval)
{
rval.setUndefined();
JSAtom* atom = Atomize(cx, name, strlen(name));
if (!atom)
return false;
RootedId id(cx, AtomToId(atom));
RootedValue fval(cx);
return GetProperty(cx, obj, obj, id, &fval) &&
(!IsCallable(fval) || Invoke(cx, ObjectValue(*obj), fval, argc, argv, rval));
}
JSTrapStatus
Debugger::fireDebuggerStatement(JSContext* cx, MutableHandleValue vp)
{
RootedObject hook(cx, getHook(OnDebuggerStatement));
MOZ_ASSERT(hook);
MOZ_ASSERT(hook->isCallable());
Maybe<AutoCompartment> ac;
ac.emplace(cx, object);
ScriptFrameIter iter(cx);
RootedValue scriptFrame(cx);
if (!getScriptFrame(cx, iter, &scriptFrame))
return handleUncaughtException(ac, false);
RootedValue rv(cx);
bool ok = Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, scriptFrame.address(), &rv);
return parseResumptionValue(ac, ok, rv, vp);
}
JSTrapStatus
Debugger::fireExceptionUnwind(JSContext* cx, MutableHandleValue vp)
{
RootedObject hook(cx, getHook(OnExceptionUnwind));
MOZ_ASSERT(hook);
MOZ_ASSERT(hook->isCallable());
RootedValue exc(cx);
if (!cx->getPendingException(&exc))
return JSTRAP_ERROR;
cx->clearPendingException();
Maybe<AutoCompartment> ac;
ac.emplace(cx, object);
JS::AutoValueArray<2> argv(cx);
argv[0].setUndefined();
argv[1].set(exc);
ScriptFrameIter iter(cx);
if (!getScriptFrame(cx, iter, argv[0]) || !wrapDebuggeeValue(cx, argv[1]))
return handleUncaughtException(ac, false);
RootedValue rv(cx);
bool ok = Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 2, argv.begin(), &rv);
JSTrapStatus st = parseResumptionValue(ac, ok, rv, vp);
if (st == JSTRAP_CONTINUE)
cx->setPendingException(exc);
return st;
}
JSTrapStatus
Debugger::fireEnterFrame(JSContext* cx, AbstractFramePtr frame, MutableHandleValue vp)
{
RootedObject hook(cx, getHook(OnEnterFrame));
MOZ_ASSERT(hook);
MOZ_ASSERT(hook->isCallable());
Maybe<AutoCompartment> ac;
ac.emplace(cx, object);
RootedValue scriptFrame(cx);
if (!getScriptFrame(cx, frame, &scriptFrame))
return handleUncaughtException(ac, false);
RootedValue rv(cx);
bool ok = Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, scriptFrame.address(), &rv);
return parseResumptionValue(ac, ok, rv, vp);
}
void
Debugger::fireNewScript(JSContext* cx, HandleScript script)
{
RootedObject hook(cx, getHook(OnNewScript));
MOZ_ASSERT(hook);
MOZ_ASSERT(hook->isCallable());
Maybe<AutoCompartment> ac;
ac.emplace(cx, object);
JSObject* dsobj = wrapScript(cx, script);
if (!dsobj) {
handleUncaughtException(ac, false);
return;
}
RootedValue scriptObject(cx, ObjectValue(*dsobj));
RootedValue rv(cx);
if (!Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, scriptObject.address(), &rv))
handleUncaughtException(ac, true);
}
void
Debugger::fireOnGarbageCollectionHook(JSContext* cx,
const JS::dbg::GarbageCollectionEvent::Ptr& gcData)
{
MOZ_ASSERT(observedGC(gcData->majorGCNumber()));
observedGCs.remove(gcData->majorGCNumber());
RootedObject hook(cx, getHook(OnGarbageCollection));
MOZ_ASSERT(hook);
MOZ_ASSERT(hook->isCallable());
Maybe<AutoCompartment> ac;
ac.emplace(cx, object);
JSObject* dataObj = gcData->toJSObject(cx);
if (!dataObj) {
handleUncaughtException(ac, false);
return;
}
RootedValue dataVal(cx, ObjectValue(*dataObj));
RootedValue rv(cx);
if (!Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, dataVal.address(), &rv))
handleUncaughtException(ac, true);
}
JSTrapStatus
Debugger::fireOnIonCompilationHook(JSContext* cx, Handle<ScriptVector> scripts, LSprinter& graph)
{
RootedObject hook(cx, getHook(OnIonCompilation));
MOZ_ASSERT(hook);
MOZ_ASSERT(hook->isCallable());
Maybe<AutoCompartment> ac;
ac.emplace(cx, object);
// Copy the vector of scripts to a JS Array of Debugger.Script
RootedObject tmpObj(cx);
RootedValue tmpVal(cx);
AutoValueVector dbgScripts(cx);
for (size_t i = 0; i < scripts.length(); i++) {
tmpObj = wrapScript(cx, scripts[i]);
if (!tmpObj)
return handleUncaughtException(ac, false);
tmpVal.setObject(*tmpObj);
if (!dbgScripts.append(tmpVal))
return handleUncaughtException(ac, false);
}
RootedObject dbgScriptsArray(cx, JS_NewArrayObject(cx, dbgScripts));
if (!dbgScriptsArray)
return handleUncaughtException(ac, false);
// Copy the JSON compilation graph to a JS String which is allocated as part
// of the Debugger compartment.
Sprinter jsonPrinter(cx);
if (!jsonPrinter.init())
return handleUncaughtException(ac, false);
graph.exportInto(jsonPrinter);
if (jsonPrinter.hadOutOfMemory())
return handleUncaughtException(ac, false);
RootedString json(cx, JS_NewStringCopyZ(cx, jsonPrinter.string()));
if (!json)
return handleUncaughtException(ac, false);
// Create a JS Object which has the array of scripts, and the string of the
// JSON graph.
const char* names[] = { "scripts", "json" };
JS::AutoValueArray<2> values(cx);
values[0].setObject(*dbgScriptsArray);
values[1].setString(json);
RootedObject obj(cx, JS_NewObject(cx, nullptr));
if (!obj)
return handleUncaughtException(ac, false);
MOZ_ASSERT(mozilla::ArrayLength(names) == values.length());
for (size_t i = 0; i < mozilla::ArrayLength(names); i++) {
if (!JS_DefineProperty(cx, obj, names[i], values[i], JSPROP_ENUMERATE, nullptr, nullptr))
return handleUncaughtException(ac, false);
}
// Call Debugger.onIonCompilation hook.
JS::AutoValueArray<1> argv(cx);
argv[0].setObject(*obj);
RootedValue rv(cx);
if (!Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, argv.begin(), &rv))
return handleUncaughtException(ac, true);
return JSTRAP_CONTINUE;
}
template <typename HookIsEnabledFun /* bool (Debugger*) */,
typename FireHookFun /* JSTrapStatus (Debugger*) */>
/* static */ JSTrapStatus
Debugger::dispatchHook(JSContext* cx, HookIsEnabledFun hookIsEnabled, FireHookFun fireHook)
{
/*
* Determine which debuggers will receive this event, and in what order.
* Make a copy of the list, since the original is mutable and we will be
* calling into arbitrary JS.
*
* Note: In the general case, 'triggered' contains references to objects in
* different compartments--every compartment *except* this one.
*/
AutoValueVector triggered(cx);
Handle<GlobalObject*> global = cx->global();
if (GlobalObject::DebuggerVector* debuggers = global->getDebuggers()) {
for (Debugger** p = debuggers->begin(); p != debuggers->end(); p++) {
Debugger* dbg = *p;
if (dbg->enabled && hookIsEnabled(dbg)) {
if (!triggered.append(ObjectValue(*dbg->toJSObject())))
return JSTRAP_ERROR;
}
}
}
/*
* Deliver the event to each debugger, checking again to make sure it
* should still be delivered.
*/
for (Value* p = triggered.begin(); p != triggered.end(); p++) {
Debugger* dbg = Debugger::fromJSObject(&p->toObject());
if (dbg->debuggees.has(global) && dbg->enabled && hookIsEnabled(dbg)) {
JSTrapStatus st = fireHook(dbg);
if (st != JSTRAP_CONTINUE)
return st;
}
}
return JSTRAP_CONTINUE;
}
void
Debugger::slowPathOnNewScript(JSContext* cx, HandleScript script)
{
JSTrapStatus status = dispatchHook(
cx,
[script](Debugger* dbg) -> bool {
return dbg->observesNewScript() && dbg->observesScript(script);
},
[&](Debugger* dbg) -> JSTrapStatus {
dbg->fireNewScript(cx, script);
return JSTRAP_CONTINUE;
});
if (status == JSTRAP_ERROR) {
ReportOutOfMemory(cx);
return;
}
MOZ_ASSERT(status == JSTRAP_CONTINUE);
}
/* static */ JSTrapStatus
Debugger::onTrap(JSContext* cx, MutableHandleValue vp)
{
ScriptFrameIter iter(cx);
RootedScript script(cx, iter.script());
MOZ_ASSERT(script->isDebuggee());
Rooted<GlobalObject*> scriptGlobal(cx, &script->global());
jsbytecode* pc = iter.pc();
BreakpointSite* site = script->getBreakpointSite(pc);
JSOp op = JSOp(*pc);
/* Build list of breakpoint handlers. */
Vector<Breakpoint*> triggered(cx);
for (Breakpoint* bp = site->firstBreakpoint(); bp; bp = bp->nextInSite()) {
if (!triggered.append(bp))
return JSTRAP_ERROR;
}
for (Breakpoint** p = triggered.begin(); p != triggered.end(); p++) {
Breakpoint* bp = *p;
/* Handlers can clear breakpoints. Check that bp still exists. */
if (!site || !site->hasBreakpoint(bp))
continue;
/*
* There are two reasons we have to check whether dbg is enabled and
* debugging scriptGlobal.
*
* One is just that one breakpoint handler can disable other Debuggers
* or remove debuggees.
*
* The other has to do with non-compile-and-go scripts, which have no
* specific global--until they are executed. Only now do we know which
* global the script is running against.
*/
Debugger* dbg = bp->debugger;
bool hasDebuggee = dbg->enabled && dbg->debuggees.has(scriptGlobal);
if (hasDebuggee) {
Maybe<AutoCompartment> ac;
ac.emplace(cx, dbg->object);
RootedValue scriptFrame(cx);
if (!dbg->getScriptFrame(cx, iter, &scriptFrame))
return dbg->handleUncaughtException(ac, false);
RootedValue rv(cx);
Rooted<JSObject*> handler(cx, bp->handler);
bool ok = CallMethodIfPresent(cx, handler, "hit", 1, scriptFrame.address(), &rv);
JSTrapStatus st = dbg->parseResumptionValue(ac, ok, rv, vp, true);
if (st != JSTRAP_CONTINUE)
return st;
/* Calling JS code invalidates site. Reload it. */
site = script->getBreakpointSite(pc);
}
}
/* By convention, return the true op to the interpreter in vp. */
vp.setInt32(op);
return JSTRAP_CONTINUE;
}
/* static */ JSTrapStatus
Debugger::onSingleStep(JSContext* cx, MutableHandleValue vp)
{
ScriptFrameIter iter(cx);
/*
* We may be stepping over a JSOP_EXCEPTION, that pushes the context's
* pending exception for a 'catch' clause to handle. Don't let the
* onStep handlers mess with that (other than by returning a resumption
* value).
*/
RootedValue exception(cx, UndefinedValue());
bool exceptionPending = cx->isExceptionPending();
if (exceptionPending) {
if (!cx->getPendingException(&exception))
return JSTRAP_ERROR;
cx->clearPendingException();
}
/*
* Build list of Debugger.Frame instances referring to this frame with
* onStep handlers.
*/
AutoObjectVector frames(cx);
for (FrameRange r(iter.abstractFramePtr()); !r.empty(); r.popFront()) {
NativeObject* frame = r.frontFrame();
if (!frame->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined() &&
!frames.append(frame))
{
return JSTRAP_ERROR;
}
}
#ifdef DEBUG
/*
* Validate the single-step count on this frame's script, to ensure that
* we're not receiving traps we didn't ask for. Even when frames is
* non-empty (and thus we know this trap was requested), do the check
* anyway, to make sure the count has the correct non-zero value.
*
* The converse --- ensuring that we do receive traps when we should --- can
* be done with unit tests.
*/
{
uint32_t stepperCount = 0;
JSScript* trappingScript = iter.script();
GlobalObject* global = cx->global();
if (GlobalObject::DebuggerVector* debuggers = global->getDebuggers()) {
for (Debugger** p = debuggers->begin(); p != debuggers->end(); p++) {
Debugger* dbg = *p;
for (FrameMap::Range r = dbg->frames.all(); !r.empty(); r.popFront()) {
AbstractFramePtr frame = r.front().key();
NativeObject* frameobj = r.front().value();
if (frame.script() == trappingScript &&
!frameobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined())
{
stepperCount++;
}
}
}
}
MOZ_ASSERT(stepperCount == trappingScript->stepModeCount());
}
#endif
/* Call all the onStep handlers we found. */
for (JSObject** p = frames.begin(); p != frames.end(); p++) {
RootedNativeObject frame(cx, &(*p)->as<NativeObject>());
Debugger* dbg = Debugger::fromChildJSObject(frame);
Maybe<AutoCompartment> ac;
ac.emplace(cx, dbg->object);
const Value& handler = frame->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER);
RootedValue rval(cx);
bool ok = Invoke(cx, ObjectValue(*frame), handler, 0, nullptr, &rval);
JSTrapStatus st = dbg->parseResumptionValue(ac, ok, rval, vp);
if (st != JSTRAP_CONTINUE)
return st;
}
vp.setUndefined();
if (exceptionPending)
cx->setPendingException(exception);
return JSTRAP_CONTINUE;
}
JSTrapStatus
Debugger::fireNewGlobalObject(JSContext* cx, Handle<GlobalObject*> global, MutableHandleValue vp)
{
RootedObject hook(cx, getHook(OnNewGlobalObject));
MOZ_ASSERT(hook);
MOZ_ASSERT(hook->isCallable());
Maybe<AutoCompartment> ac;
ac.emplace(cx, object);
RootedValue wrappedGlobal(cx, ObjectValue(*global));
if (!wrapDebuggeeValue(cx, &wrappedGlobal))
return handleUncaughtException(ac, false);
RootedValue rv(cx);
// onNewGlobalObject is infallible, and thus is only allowed to return
// undefined as a resumption value. If it returns anything else, we throw.
// And if that happens, or if the hook itself throws, we invoke the
// uncaughtExceptionHook so that we never leave an exception pending on the
// cx. This allows JS_NewGlobalObject to avoid handling failures from debugger
// hooks.
bool ok = Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, wrappedGlobal.address(), &rv);
if (ok && !rv.isUndefined()) {
JS_ReportErrorNumber(cx, GetErrorMessage, nullptr,
JSMSG_DEBUG_RESUMPTION_VALUE_DISALLOWED);
ok = false;
}
// NB: Even though we don't care about what goes into it, we have to pass vp
// to handleUncaughtException so that it parses resumption values from the
// uncaughtExceptionHook and tells the caller whether we should execute the
// rest of the onNewGlobalObject hooks or not.
JSTrapStatus status = ok ? JSTRAP_CONTINUE
: handleUncaughtException(ac, vp, true);
MOZ_ASSERT(!cx->isExceptionPending());
return status;
}
void
Debugger::slowPathOnNewGlobalObject(JSContext* cx, Handle<GlobalObject*> global)
{
MOZ_ASSERT(!JS_CLIST_IS_EMPTY(&cx->runtime()->onNewGlobalObjectWatchers));
if (global->compartment()->options().invisibleToDebugger())
return;
/*
* Make a copy of the runtime's onNewGlobalObjectWatchers before running the
* handlers. Since one Debugger's handler can disable another's, the list
* can be mutated while we're walking it.
*/
AutoObjectVector watchers(cx);
for (JSCList* link = JS_LIST_HEAD(&cx->runtime()->onNewGlobalObjectWatchers);
link != &cx->runtime()->onNewGlobalObjectWatchers;
link = JS_NEXT_LINK(link))
{
Debugger* dbg = fromOnNewGlobalObjectWatchersLink(link);
MOZ_ASSERT(dbg->observesNewGlobalObject());
JSObject* obj = dbg->object;
JS::ExposeObjectToActiveJS(obj);
if (!watchers.append(obj))
return;
}
JSTrapStatus status = JSTRAP_CONTINUE;
RootedValue value(cx);
for (size_t i = 0; i < watchers.length(); i++) {
Debugger* dbg = fromJSObject(watchers[i]);
// We disallow resumption values from onNewGlobalObject hooks, because we
// want the debugger hooks for global object creation to be infallible.
// But if an onNewGlobalObject hook throws, and the uncaughtExceptionHook
// decides to raise an error, we want to at least avoid invoking the rest
// of the onNewGlobalObject handlers in the list (not for any super
// compelling reason, just because it seems like the right thing to do).
// So we ignore whatever comes out in |value|, but break out of the loop
// if a non-success trap status is returned.
if (dbg->observesNewGlobalObject()) {
status = dbg->fireNewGlobalObject(cx, global, &value);
if (status != JSTRAP_CONTINUE && status != JSTRAP_RETURN)
break;
}
}
MOZ_ASSERT(!cx->isExceptionPending());
}
/* static */ bool
Debugger::slowPathOnLogAllocationSite(JSContext* cx, HandleObject obj, HandleSavedFrame frame,
double when, GlobalObject::DebuggerVector& dbgs)
{
MOZ_ASSERT(!dbgs.empty());
mozilla::DebugOnly<Debugger**> begin = dbgs.begin();
for (Debugger** dbgp = dbgs.begin(); dbgp < dbgs.end(); dbgp++) {
// The set of debuggers had better not change while we're iterating,
// such that the vector gets reallocated.
MOZ_ASSERT(dbgs.begin() == begin);
if ((*dbgp)->trackingAllocationSites &&
(*dbgp)->enabled &&
!(*dbgp)->appendAllocationSite(cx, obj, frame, when))
{
return false;
}
}
return true;
}
/* static */ void
Debugger::slowPathOnIonCompilation(JSContext* cx, Handle<ScriptVector> scripts, LSprinter& graph)
{
JSTrapStatus status = dispatchHook(
cx,
[](Debugger* dbg) -> bool { return dbg->getHook(OnIonCompilation); },
[&](Debugger* dbg) -> JSTrapStatus {
(void) dbg->fireOnIonCompilationHook(cx, scripts, graph);
return JSTRAP_CONTINUE;
});
if (status == JSTRAP_ERROR) {
cx->clearPendingException();
return;
}
MOZ_ASSERT(status == JSTRAP_CONTINUE);
}
bool
Debugger::isDebuggeeUnbarriered(const JSCompartment* compartment) const
{
MOZ_ASSERT(compartment);
return compartment->isDebuggee() && debuggees.has(compartment->unsafeUnbarrieredMaybeGlobal());
}
Debugger::TenurePromotionsLogEntry::TenurePromotionsLogEntry(JSRuntime* rt, JSObject& obj, double when)
: className(obj.getClass()->name),
when(when),
frame(getObjectAllocationSite(obj)),
size(JS::ubi::Node(&obj).size(rt->debuggerMallocSizeOf))
{ }
void
Debugger::logTenurePromotion(JSRuntime* rt, JSObject& obj, double when)
{
AutoEnterOOMUnsafeRegion oomUnsafe;
if (!tenurePromotionsLog.emplaceBack(rt, obj, when))
oomUnsafe.crash("Debugger::logTenurePromotion");
if (tenurePromotionsLog.length() > maxTenurePromotionsLogLength) {
if (!tenurePromotionsLog.popFront())
oomUnsafe.crash("Debugger::logTenurePromotion");
MOZ_ASSERT(tenurePromotionsLog.length() == maxTenurePromotionsLogLength);
tenurePromotionsLogOverflowed = true;
}
}
bool
Debugger::appendAllocationSite(JSContext* cx, HandleObject obj, HandleSavedFrame frame,
double when)
{
MOZ_ASSERT(trackingAllocationSites && enabled);
AutoCompartment ac(cx, object);
RootedObject wrappedFrame(cx, frame);
if (!cx->compartment()->wrap(cx, &wrappedFrame))
return false;
RootedAtom ctorName(cx);
{
AutoCompartment ac(cx, obj);
if (!obj->constructorDisplayAtom(cx, &ctorName))
return false;
}
auto className = obj->getClass()->name;
auto size = JS::ubi::Node(obj.get()).size(cx->runtime()->debuggerMallocSizeOf);
auto inNursery = gc::IsInsideNursery(obj);
if (!allocationsLog.emplaceBack(wrappedFrame, when, className, ctorName, size, inNursery)) {
ReportOutOfMemory(cx);
return false;
}
if (allocationsLog.length() > maxAllocationsLogLength) {
if (!allocationsLog.popFront()) {
ReportOutOfMemory(cx);
return false;
}
MOZ_ASSERT(allocationsLog.length() == maxAllocationsLogLength);
allocationsLogOverflowed = true;
}
return true;
}
JSTrapStatus
Debugger::firePromiseHook(JSContext* cx, Hook hook, HandleObject promise, MutableHandleValue vp)
{
MOZ_ASSERT(hook == OnNewPromise || hook == OnPromiseSettled);
RootedObject hookObj(cx, getHook(hook));
MOZ_ASSERT(hookObj);
MOZ_ASSERT(hookObj->isCallable());
Maybe<AutoCompartment> ac;
ac.emplace(cx, object);
RootedValue dbgObj(cx, ObjectValue(*promise));
if (!wrapDebuggeeValue(cx, &dbgObj))
return handleUncaughtException(ac, false);
// Like onNewGlobalObject, the Promise hooks are infallible and the comments
// in |Debugger::fireNewGlobalObject| apply here as well.
RootedValue rv(cx);
bool ok = Invoke(cx, ObjectValue(*object), ObjectValue(*hookObj), 1, dbgObj.address(), &rv);
if (ok && !rv.isUndefined()) {
JS_ReportErrorNumber(cx, GetErrorMessage, nullptr,
JSMSG_DEBUG_RESUMPTION_VALUE_DISALLOWED);
ok = false;
}
JSTrapStatus status = ok ? JSTRAP_CONTINUE
: handleUncaughtException(ac, vp, true);
MOZ_ASSERT(!cx->isExceptionPending());
return status;
}
/* static */ void
Debugger::slowPathPromiseHook(JSContext* cx, Hook hook, HandleObject promise)
{
MOZ_ASSERT(hook == OnNewPromise || hook == OnPromiseSettled);
RootedValue rval(cx);
JSTrapStatus status = dispatchHook(
cx,
[hook](Debugger* dbg) -> bool { return dbg->getHook(hook); },
[&](Debugger* dbg) -> JSTrapStatus {
(void) dbg->firePromiseHook(cx, hook, promise, &rval);
return JSTRAP_CONTINUE;
});
if (status == JSTRAP_ERROR) {
// The dispatch hook function might fail to append into the list of
// Debuggers which are watching for the hook.
cx->clearPendingException();
return;
}
// Promise hooks are infallible and we ignore errors from uncaught
// exceptions by design.
MOZ_ASSERT(status == JSTRAP_CONTINUE);
}
/*** Debugger code invalidation for observing execution ******************************************/
class MOZ_RAII ExecutionObservableCompartments : public Debugger::ExecutionObservableSet
{
HashSet<JSCompartment*> compartments_;
HashSet<Zone*> zones_;
public:
explicit ExecutionObservableCompartments(JSContext* cx
MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
: compartments_(cx),
zones_(cx)
{
MOZ_GUARD_OBJECT_NOTIFIER_INIT;
}
bool init() { return compartments_.init() && zones_.init(); }
bool add(JSCompartment* comp) { return compartments_.put(comp) && zones_.put(comp->zone()); }
typedef HashSet<JSCompartment*>::Range CompartmentRange;
const HashSet<JSCompartment*>* compartments() const { return &compartments_; }
const HashSet<Zone*>* zones() const { return &zones_; }
bool shouldRecompileOrInvalidate(JSScript* script) const {
return script->hasBaselineScript() && compartments_.has(script->compartment());
}
bool shouldMarkAsDebuggee(ScriptFrameIter& iter) const {
// AbstractFramePtr can't refer to non-remateralized Ion frames, so if
// iter refers to one such, we know we don't match.
return iter.hasUsableAbstractFramePtr() && compartments_.has(iter.compartment());
}
MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
};
// Given a particular AbstractFramePtr F that has become observable, this
// represents the stack frames that need to be bailed out or marked as
// debuggees, and the scripts that need to be recompiled, taking inlining into
// account.
class MOZ_RAII ExecutionObservableFrame : public Debugger::ExecutionObservableSet
{
AbstractFramePtr frame_;
public:
explicit ExecutionObservableFrame(AbstractFramePtr frame
MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
: frame_(frame)
{
MOZ_GUARD_OBJECT_NOTIFIER_INIT;
}
Zone* singleZone() const {
// We never inline across compartments, let alone across zones, so
// frames_'s script's zone is the only one of interest.
return frame_.script()->compartment()->zone();
}
JSScript* singleScriptForZoneInvalidation() const {
MOZ_CRASH("ExecutionObservableFrame shouldn't need zone-wide invalidation.");
return nullptr;
}
bool shouldRecompileOrInvalidate(JSScript* script) const {
// Normally, *this represents exactly one script: the one frame_ is
// running.
//
// However, debug-mode OSR uses *this for both invalidating Ion frames,
// and recompiling the Baseline scripts that those Ion frames will bail
// out into. Suppose frame_ is an inline frame, executing a copy of its
// JSScript, S_inner, that has been inlined into the IonScript of some
// other JSScript, S_outer. We must match S_outer, to decide which Ion
// frame to invalidate; and we must match S_inner, to decide which
// Baseline script to recompile.
//
// Note that this does not, by design, invalidate *all* inliners of
// frame_.script(), as only frame_ is made observable, not
// frame_.script().
if (!script->hasBaselineScript())
return false;
if (script == frame_.script())
return true;
return frame_.isRematerializedFrame() &&
script == frame_.asRematerializedFrame()->outerScript();
}
bool shouldMarkAsDebuggee(ScriptFrameIter& iter) const {
// AbstractFramePtr can't refer to non-remateralized Ion frames, so if
// iter refers to one such, we know we don't match.
//
// We never use this 'has' overload for frame invalidation, only for
// frame debuggee marking; so this overload doesn't need a parallel to
// the just-so inlining logic above.
return iter.hasUsableAbstractFramePtr() && iter.abstractFramePtr() == frame_;
}
MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
};
class MOZ_RAII ExecutionObservableScript : public Debugger::ExecutionObservableSet
{
RootedScript script_;
public:
ExecutionObservableScript(JSContext* cx, JSScript* script
MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
: script_(cx, script)
{
MOZ_GUARD_OBJECT_NOTIFIER_INIT;
}
Zone* singleZone() const { return script_->compartment()->zone(); }
JSScript* singleScriptForZoneInvalidation() const { return script_; }
bool shouldRecompileOrInvalidate(JSScript* script) const {
return script->hasBaselineScript() && script == script_;
}
bool shouldMarkAsDebuggee(ScriptFrameIter& iter) const {
// AbstractFramePtr can't refer to non-remateralized Ion frames, and
// while a non-rematerialized Ion frame may indeed be running script_,
// we cannot mark them as debuggees until they bail out.
//
// Upon bailing out, any newly constructed Baseline frames that came
// from Ion frames with scripts that are isDebuggee() is marked as
// debuggee. This is correct in that the only other way a frame may be
// marked as debuggee is via Debugger.Frame reflection, which would
// have rematerialized any Ion frames.
return iter.hasUsableAbstractFramePtr() && iter.abstractFramePtr().script() == script_;
}
MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
};
/* static */ bool
Debugger::updateExecutionObservabilityOfFrames(JSContext* cx, const ExecutionObservableSet& obs,
IsObserving observing)
{
AutoSuppressProfilerSampling suppressProfilerSampling(cx);
{
jit::JitContext jctx(cx, nullptr);
if (!jit::RecompileOnStackBaselineScriptsForDebugMode(cx, obs, observing)) {
ReportOutOfMemory(cx);
return false;
}
}
AbstractFramePtr oldestEnabledFrame;
for (ScriptFrameIter iter(cx, ScriptFrameIter::ALL_CONTEXTS,
ScriptFrameIter::GO_THROUGH_SAVED);
!iter.done();
++iter)
{
if (obs.shouldMarkAsDebuggee(iter)) {
if (observing) {
if (!iter.abstractFramePtr().isDebuggee()) {
oldestEnabledFrame = iter.abstractFramePtr();
oldestEnabledFrame.setIsDebuggee();
}
} else {
#ifdef DEBUG
// Debugger.Frame lifetimes are managed by the debug epilogue,
// so in general it's unsafe to unmark a frame if it has a
// Debugger.Frame associated with it.
FrameRange r(iter.abstractFramePtr());
MOZ_ASSERT(r.empty());
#endif
iter.abstractFramePtr().unsetIsDebuggee();
}
}
}
// See comment in unsetPrevUpToDateUntil.
if (oldestEnabledFrame) {
AutoCompartment ac(cx, oldestEnabledFrame.compartment());
DebugScopes::unsetPrevUpToDateUntil(cx, oldestEnabledFrame);
}
return true;
}
static inline void
MarkBaselineScriptActiveIfObservable(JSScript* script, const Debugger::ExecutionObservableSet& obs)
{
if (obs.shouldRecompileOrInvalidate(script))
script->baselineScript()->setActive();
}
static bool
AppendAndInvalidateScript(JSContext* cx, Zone* zone, JSScript* script, Vector<JSScript*>& scripts)
{
// Enter the script's compartment as addPendingRecompile attempts to
// cancel off-thread compilations, whose books are kept on the
// script's compartment.
MOZ_ASSERT(script->compartment()->zone() == zone);
AutoCompartment ac(cx, script->compartment());
zone->types.addPendingRecompile(cx, script);
return scripts.append(script);
}
static bool
UpdateExecutionObservabilityOfScriptsInZone(JSContext* cx, Zone* zone,
const Debugger::ExecutionObservableSet& obs,
Debugger::IsObserving observing)
{
using namespace js::jit;
// See note in js::ReleaseAllJITCode.
cx->runtime()->gc.evictNursery();
AutoSuppressProfilerSampling suppressProfilerSampling(cx);
JSRuntime* rt = cx->runtime();
FreeOp* fop = cx->runtime()->defaultFreeOp();
// Mark active baseline scripts in the observable set so that they don't
// get discarded. They will be recompiled.
for (JitActivationIterator actIter(rt); !actIter.done(); ++actIter) {
if (actIter->compartment()->zone() != zone)
continue;
for (JitFrameIterator iter(actIter); !iter.done(); ++iter) {
switch (iter.type()) {
case JitFrame_BaselineJS:
MarkBaselineScriptActiveIfObservable(iter.script(), obs);
break;
case JitFrame_IonJS:
MarkBaselineScriptActiveIfObservable(iter.script(), obs);
for (InlineFrameIterator inlineIter(rt, &iter); inlineIter.more(); ++inlineIter)
MarkBaselineScriptActiveIfObservable(inlineIter.script(), obs);
break;
default:;
}
}
}
Vector<JSScript*> scripts(cx);
// Iterate through observable scripts, invalidating their Ion scripts and
// appending them to a vector for discarding their baseline scripts later.
{
AutoEnterAnalysis enter(fop, zone);
if (JSScript* script = obs.singleScriptForZoneInvalidation()) {
if (obs.shouldRecompileOrInvalidate(script)) {
if (!AppendAndInvalidateScript(cx, zone, script, scripts))
return false;
}
} else {
for (gc::ZoneCellIter iter(zone, gc::AllocKind::SCRIPT); !iter.done(); iter.next()) {
JSScript* script = iter.get<JSScript>();
if (obs.shouldRecompileOrInvalidate(script) &&
!gc::IsAboutToBeFinalizedUnbarriered(&script))
{
if (!AppendAndInvalidateScript(cx, zone, script, scripts))
return false;
}
}
}
}
// Iterate through the scripts again and finish discarding
// BaselineScripts. This must be done as a separate phase as we can only
// discard the BaselineScript on scripts that have no IonScript.
for (size_t i = 0; i < scripts.length(); i++) {
MOZ_ASSERT_IF(scripts[i]->isDebuggee(), observing);
FinishDiscardBaselineScript(fop, scripts[i]);
}
return true;
}
/* static */ bool
Debugger::updateExecutionObservabilityOfScripts(JSContext* cx, const ExecutionObservableSet& obs,
IsObserving observing)
{
if (Zone* zone = obs.singleZone())
return UpdateExecutionObservabilityOfScriptsInZone(cx, zone, obs, observing);
typedef ExecutionObservableSet::ZoneRange ZoneRange;
for (ZoneRange r = obs.zones()->all(); !r.empty(); r.popFront()) {
if (!UpdateExecutionObservabilityOfScriptsInZone(cx, r.front(), obs, observing))
return false;
}
return true;
}
/* static */ bool
Debugger::updateExecutionObservability(JSContext* cx, ExecutionObservableSet& obs,
IsObserving observing)
{
if (!obs.singleZone() && obs.zones()->empty())
return true;
// Invalidate scripts first so we can set the needsArgsObj flag on scripts
// before patching frames.
return updateExecutionObservabilityOfScripts(cx, obs, observing) &&
updateExecutionObservabilityOfFrames(cx, obs, observing);
}
/* static */ bool
Debugger::ensureExecutionObservabilityOfScript(JSContext* cx, JSScript* script)
{
if (script->isDebuggee())
return true;
ExecutionObservableScript obs(cx, script);
return updateExecutionObservability(cx, obs, Observing);
}
/* static */ bool
Debugger::ensureExecutionObservabilityOfOsrFrame(JSContext* cx, InterpreterFrame* frame)
{
MOZ_ASSERT(frame->isDebuggee());
if (frame->script()->hasBaselineScript() &&
frame->script()->baselineScript()->hasDebugInstrumentation())
{
return true;
}
ExecutionObservableFrame obs(frame);
return updateExecutionObservabilityOfFrames(cx, obs, Observing);
}
/* static */ bool
Debugger::ensureExecutionObservabilityOfFrame(JSContext* cx, AbstractFramePtr frame)
{
MOZ_ASSERT_IF(frame.script()->isDebuggee(), frame.isDebuggee());
if (frame.isDebuggee())
return true;
ExecutionObservableFrame obs(frame);
return updateExecutionObservabilityOfFrames(cx, obs, Observing);
}
/* static */ bool
Debugger::ensureExecutionObservabilityOfCompartment(JSContext* cx, JSCompartment* comp)
{
if (comp->debuggerObservesAllExecution())
return true;
ExecutionObservableCompartments obs(cx);
if (!obs.init() || !obs.add(comp))
return false;
comp->updateDebuggerObservesAllExecution();
return updateExecutionObservability(cx, obs, Observing);
}
/* static */ bool
Debugger::hookObservesAllExecution(Hook which)
{
return which == OnEnterFrame;
}
Debugger::IsObserving
Debugger::observesAllExecution() const
{
if (enabled && !!getHook(OnEnterFrame))
return Observing;
return NotObserving;
}
Debugger::IsObserving
Debugger::observesAsmJS() const
{
if (enabled && !allowUnobservedAsmJS)
return Observing;
return NotObserving;
}
Debugger::IsObserving
Debugger::observesCoverage() const
{
if (enabled && collectCoverageInfo)
return Observing;
return NotObserving;
}
// Toggle whether this Debugger's debuggees observe all execution. This is
// called when a hook that observes all execution is set or unset. See
// hookObservesAllExecution.
bool
Debugger::updateObservesAllExecutionOnDebuggees(JSContext* cx, IsObserving observing)
{
ExecutionObservableCompartments obs(cx);
if (!obs.init())
return false;
for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) {
GlobalObject* global = r.front();
JSCompartment* comp = global->compartment();
if (comp->debuggerObservesAllExecution() == observing)
continue;
// It's expensive to eagerly invalidate and recompile a compartment,
// so add the compartment to the set only if we are observing.
if (observing && !obs.add(comp))
return false;
comp->updateDebuggerObservesAllExecution();
}
return updateExecutionObservability(cx, obs, observing);
}
bool
Debugger::updateObservesCoverageOnDebuggees(JSContext* cx, IsObserving observing)
{
ExecutionObservableCompartments obs(cx);
if (!obs.init())
return false;
for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) {
GlobalObject* global = r.front();
JSCompartment* comp = global->compartment();
if (comp->debuggerObservesCoverage() == observing)
continue;
// Invalidate and recompile a compartment to add or remove PCCounts
// increments. We have to eagerly invalidate, as otherwise we might have
// dangling pointers to freed PCCounts.
if (!obs.add(comp))
return false;
}
// If any frame on the stack belongs to the debuggee, then we cannot update
// the ScriptCounts, because this would imply to invalidate a Debugger.Frame
// to recompile it with/without ScriptCount support.
for (ScriptFrameIter iter(cx, ScriptFrameIter::ALL_CONTEXTS,
ScriptFrameIter::GO_THROUGH_SAVED);
!iter.done();
++iter)
{
if (obs.shouldMarkAsDebuggee(iter)) {
JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_NOT_IDLE);
return false;
}
}
if (!updateExecutionObservability(cx, obs, observing))
return false;
// All compartments can safely be toggled, and all scripts will be
// recompiled. Thus we can update each compartment accordingly.
typedef ExecutionObservableCompartments::CompartmentRange CompartmentRange;
for (CompartmentRange r = obs.compartments()->all(); !r.empty(); r.popFront())
r.front()->updateDebuggerObservesCoverage();
return true;
}
void
Debugger::updateObservesAsmJSOnDebuggees(IsObserving observing)
{
for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) {
GlobalObject* global = r.front();
JSCompartment* comp = global->compartment();
if (comp->debuggerObservesAsmJS() == observing)
continue;
comp->updateDebuggerObservesAsmJS();
}
}
/*** Allocations Tracking *************************************************************************/
/* static */ bool
Debugger::cannotTrackAllocations(const GlobalObject& global)
{
auto existingCallback = global.compartment()->getObjectMetadataCallback();
return existingCallback && existingCallback != SavedStacksMetadataCallback;
}
/* static */ bool
Debugger::isObservedByDebuggerTrackingAllocations(const GlobalObject& debuggee)
{
if (auto* v = debuggee.getDebuggers()) {
Debugger** p;
for (p = v->begin(); p != v->end(); p++) {
if ((*p)->trackingAllocationSites && (*p)->enabled) {
return true;
}
}
}
return false;
}
/* static */ bool
Debugger::addAllocationsTracking(JSContext* cx, Handle<GlobalObject*> debuggee)
{
// Precondition: the given global object is being observed by at least one
// Debugger that is tracking allocations.
MOZ_ASSERT(isObservedByDebuggerTrackingAllocations(*debuggee));
if (Debugger::cannotTrackAllocations(*debuggee)) {
JS_ReportErrorNumber(cx, GetErrorMessage, nullptr,
JSMSG_OBJECT_METADATA_CALLBACK_ALREADY_SET);
return false;
}
debuggee->compartment()->setObjectMetadataCallback(SavedStacksMetadataCallback);
debuggee->compartment()->chooseAllocationSamplingProbability();
return true;
}
/* static */ void
Debugger::removeAllocationsTracking(GlobalObject& global)
{
// If there are still Debuggers that are observing allocations, we cannot
// remove the metadata callback yet. Recompute the sampling probability
// based on the remaining debuggers' needs.
if (isObservedByDebuggerTrackingAllocations(global)) {
global.compartment()->chooseAllocationSamplingProbability();
return;
}
global.compartment()->forgetObjectMetadataCallback();
}
bool
Debugger::addAllocationsTrackingForAllDebuggees(JSContext* cx)
{
MOZ_ASSERT(trackingAllocationSites);
// We don't want to end up in a state where we added allocations
// tracking to some of our debuggees, but failed to do so for
// others. Before attempting to start tracking allocations in *any* of
// our debuggees, ensure that we will be able to track allocations for
// *all* of our debuggees.
for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) {
if (Debugger::cannotTrackAllocations(*r.front().get())) {
JS_ReportErrorNumber(cx, GetErrorMessage, nullptr,
JSMSG_OBJECT_METADATA_CALLBACK_ALREADY_SET);
return false;
}
}
Rooted<GlobalObject*> g(cx);
for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) {
// This should always succeed, since we already checked for the
// error case above.
g = r.front().get();
MOZ_ALWAYS_TRUE(Debugger::addAllocationsTracking(cx, g));
}
return true;
}
void
Debugger::removeAllocationsTrackingForAllDebuggees()
{
for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront())
Debugger::removeAllocationsTracking(*r.front().get());
allocationsLog.clear();
}
/*** Debugger JSObjects **************************************************************************/
void
Debugger::markCrossCompartmentEdges(JSTracer* trc)
{
objects.markCrossCompartmentEdges<DebuggerObject_trace>(trc);
environments.markCrossCompartmentEdges<DebuggerEnv_trace>(trc);
scripts.markCrossCompartmentEdges<DebuggerScript_trace>(trc);
sources.markCrossCompartmentEdges<DebuggerSource_trace>(trc);
// Because we don't have access to a `cx` inside
// `Debugger::logTenurePromotion`, we can't hold onto CCWs inside the log,
// and instead have unwrapped cross-compartment edges. We need to be sure to
// mark those here.
TenurePromotionsLog::trace(&tenurePromotionsLog, trc);
}
/*
* Ordinarily, WeakMap keys and values are marked because at some point it was
* discovered that the WeakMap was live; that is, some object containing the
* WeakMap was marked during mark phase.
*
* However, during zone GC, we have to do something about cross-compartment
* edges in non-GC'd compartments. Since the source may be live, we
* conservatively assume it is and mark the edge.
*
* Each Debugger object keeps four cross-compartment WeakMaps: objects, scripts,
* script source objects, and environments. They have the property that all
* their values are in the same compartment as the Debugger object, but we have
* to mark the keys and the private pointer in the wrapper object.
*
* We must scan all Debugger objects regardless of whether they *currently* have
* any debuggees in a compartment being GC'd, because the WeakMap entries
* persist even when debuggees are removed.
*
* This happens during the initial mark phase, not iterative marking, because
* all the edges being reported here are strong references.
*
* This method is also used during compacting GC to update cross compartment
* pointers in zones that are not currently being compacted.
*/
/* static */ void
Debugger::markIncomingCrossCompartmentEdges(JSTracer* trc)
{
JSRuntime* rt = trc->runtime();
gc::State state = rt->gc.state();
MOZ_ASSERT(state == gc::MARK_ROOTS || state == gc::COMPACT);
for (Debugger* dbg : rt->debuggerList) {
Zone* zone = dbg->object->zone();
if ((state == gc::MARK_ROOTS && !zone->isCollecting()) ||
(state == gc::COMPACT && !zone->isGCCompacting()))
{
dbg->markCrossCompartmentEdges(trc);
}
}
}
/*
* This method has two tasks:
* 1. Mark Debugger objects that are unreachable except for debugger hooks that
* may yet be called.
* 2. Mark breakpoint handlers.
*
* This happens during the iterative part of the GC mark phase. This method
* returns true if it has to mark anything; GC calls it repeatedly until it
* returns false.
*/
/* static */ bool
Debugger::markAllIteratively(GCMarker* trc)
{
bool markedAny = false;
/*
* Find all Debugger objects in danger of GC. This code is a little
* convoluted since the easiest way to find them is via their debuggees.
*/
JSRuntime* rt = trc->runtime();
for (CompartmentsIter c(rt, SkipAtoms); !c.done(); c.next()) {
if (c->isDebuggee()) {
GlobalObject* global = c->unsafeUnbarrieredMaybeGlobal();
if (!IsMarkedUnbarriered(&global))
continue;
/*
* Every debuggee has at least one debugger, so in this case
* getDebuggers can't return nullptr.
*/
const GlobalObject::DebuggerVector* debuggers = global->getDebuggers();
MOZ_ASSERT(debuggers);
for (Debugger * const* p = debuggers->begin(); p != debuggers->end(); p++) {
Debugger* dbg = *p;
/*
* dbg is a Debugger with at least one debuggee. Check three things:
* - dbg is actually in a compartment that is being marked
* - it isn't already marked
* - it actually has hooks that might be called
*/
HeapPtrNativeObject& dbgobj = dbg->toJSObjectRef();
if (!dbgobj->zone()->isGCMarking())
continue;
bool dbgMarked = IsMarked(&dbgobj);
if (!dbgMarked && dbg->hasAnyLiveHooks()) {
/*
* obj could be reachable only via its live, enabled
* debugger hooks, which may yet be called.
*/
TraceEdge(trc, &dbgobj, "enabled Debugger");
markedAny = true;
dbgMarked = true;
}
if (dbgMarked) {
/* Search for breakpoints to mark. */
for (Breakpoint* bp = dbg->firstBreakpoint(); bp; bp = bp->nextInDebugger()) {
if (IsMarkedUnbarriered(&bp->site->script)) {
/*
* The debugger and the script are both live.
* Therefore the breakpoint handler is live.
*/
if (!IsMarked(&bp->getHandlerRef())) {
TraceEdge(trc, &bp->getHandlerRef(), "breakpoint handler");
markedAny = true;
}
}
}
}
}
}
}
return markedAny;
}
/*
* Mark all debugger-owned GC things unconditionally. This is used by the minor
* GC: the minor GC cannot apply the weak constraints of the full GC because it
* visits only part of the heap.
*/
/* static */ void
Debugger::markAll(JSTracer* trc)
{
JSRuntime* rt = trc->runtime();
for (Debugger* dbg : rt->debuggerList) {
for (WeakGlobalObjectSet::Enum e(dbg->debuggees); !e.empty(); e.popFront())
TraceManuallyBarrieredEdge(trc, e.mutableFront().unsafeGet(), "Global Object");
HeapPtrNativeObject& dbgobj = dbg->toJSObjectRef();
TraceEdge(trc, &dbgobj, "Debugger Object");
dbg->scripts.trace(trc);
dbg->sources.trace(trc);
dbg->objects.trace(trc);
dbg->environments.trace(trc);
for (Breakpoint* bp = dbg->firstBreakpoint(); bp; bp = bp->nextInDebugger()) {
TraceManuallyBarrieredEdge(trc, &bp->site->script, "breakpoint script");
TraceEdge(trc, &bp->getHandlerRef(), "breakpoint handler");
}
}
}
/* static */ void
Debugger::traceObject(JSTracer* trc, JSObject* obj)
{
if (Debugger* dbg = Debugger::fromJSObject(obj))
dbg->trace(trc);
}
void
Debugger::trace(JSTracer* trc)
{
if (uncaughtExceptionHook)
TraceEdge(trc, &uncaughtExceptionHook, "hooks");
/*
* Mark Debugger.Frame objects. These are all reachable from JS, because the
* corresponding JS frames are still on the stack.
*
* (Once we support generator frames properly, we will need
* weakly-referenced Debugger.Frame objects as well, for suspended generator
* frames.)
*/
for (FrameMap::Range r = frames.all(); !r.empty(); r.popFront()) {
RelocatablePtrNativeObject& frameobj = r.front().value();
MOZ_ASSERT(MaybeForwarded(frameobj.get())->getPrivate());
TraceEdge(trc, &frameobj, "live Debugger.Frame");
}
AllocationsLog::trace(&allocationsLog, trc);
TenurePromotionsLog::trace(&tenurePromotionsLog, trc);
/* Trace the weak map from JSScript instances to Debugger.Script objects. */
scripts.trace(trc);
/* Trace the referent ->Debugger.Source weak map */
sources.trace(trc);
/* Trace the referent -> Debugger.Object weak map. */
objects.trace(trc);
/* Trace the referent -> Debugger.Environment weak map. */
environments.trace(trc);
}
/* static */ void
Debugger::sweepAll(FreeOp* fop)
{
JSRuntime* rt = fop->runtime();
for (Debugger* dbg : rt->debuggerList) {
if (IsAboutToBeFinalized(&dbg->object)) {
/*
* dbg is being GC'd. Detach it from its debuggees. The debuggee
* might be GC'd too. Since detaching requires access to both
* objects, this must be done before finalize time.
*/
for (WeakGlobalObjectSet::Enum e(dbg->debuggees); !e.empty(); e.popFront())
dbg->removeDebuggeeGlobal(fop, e.front().unbarrieredGet(), &e);
}
}
}
/* static */ void
Debugger::detachAllDebuggersFromGlobal(FreeOp* fop, GlobalObject* global)
{
const GlobalObject::DebuggerVector* debuggers = global->getDebuggers();
MOZ_ASSERT(!debuggers->empty());
while (!debuggers->empty())
debuggers->back()->removeDebuggeeGlobal(fop, global, nullptr);
}
/* static */ void
Debugger::findZoneEdges(Zone* zone, js::gc::ComponentFinder<Zone>& finder)
{
/*
* For debugger cross compartment wrappers, add edges in the opposite
* direction to those already added by JSCompartment::findOutgoingEdges.
* This ensure that debuggers and their debuggees are finalized in the same
* group.
*/
for (Debugger* dbg : zone->runtimeFromMainThread()->debuggerList) {
Zone* w = dbg->object->zone();
if (w == zone || !w->isGCMarking())
continue;
if (dbg->debuggeeZones.has(zone) ||
dbg->scripts.hasKeyInZone(zone) ||
dbg->sources.hasKeyInZone(zone) ||
dbg->objects.hasKeyInZone(zone) ||
dbg->environments.hasKeyInZone(zone))
{
finder.addEdgeTo(w);
}
}
}
/* static */ void
Debugger::finalize(FreeOp* fop, JSObject* obj)
{
Debugger* dbg = fromJSObject(obj);
if (!dbg)
return;
fop->delete_(dbg);
}
const Class Debugger::jsclass = {
"Debugger",
JSCLASS_HAS_PRIVATE |
JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUG_COUNT),
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, Debugger::finalize,
nullptr, /* call */
nullptr, /* hasInstance */
nullptr, /* construct */
Debugger::traceObject
};
/* static */ Debugger*
Debugger::fromThisValue(JSContext* cx, const CallArgs& args, const char* fnname)
{
JSObject* thisobj = NonNullObject(cx, args.thisv());
if (!thisobj)
return nullptr;
if (thisobj->getClass() != &Debugger::jsclass) {
JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO,
"Debugger", fnname, thisobj->getClass()->name);
return nullptr;
}
/*
* Forbid Debugger.prototype, which is of the Debugger JSClass but isn't
* really a Debugger object. The prototype object is distinguished by
* having a nullptr private value.
*/
Debugger* dbg = fromJSObject(thisobj);
if (!dbg) {
JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO,
"Debugger", fnname, "prototype object");
}
return dbg;
}
#define THIS_DEBUGGER(cx, argc, vp, fnname, args, dbg) \
CallArgs args = CallArgsFromVp(argc, vp); \
Debugger* dbg = Debugger::fromThisValue(cx, args, fnname); \
if (!dbg) \
return false
/* static */ bool
Debugger::getEnabled(JSContext* cx, unsigned argc, Value* vp)
{
THIS_DEBUGGER(cx, argc, vp, "get enabled", args, dbg);
args.rval().setBoolean(dbg->enabled);
return true;
}
/* static */ bool
Debugger::setEnabled(JSContext* cx, unsigned argc, Value* vp)
{
THIS_DEBUGGER(cx, argc, vp, "set enabled", args, dbg);
if (!args.requireAtLeast(cx, "Debugger.set enabled", 1))
return false;
bool wasEnabled = dbg->enabled;
dbg->enabled = ToBoolean(args[0]);
if (wasEnabled != dbg->enabled) {
if (dbg->trackingAllocationSites) {
if (wasEnabled) {
dbg->removeAllocationsTrackingForAllDebuggees();
} else {
if (!dbg->addAllocationsTrackingForAllDebuggees(cx)) {
dbg->enabled = false;
return false;
}
}
}
for (Breakpoint* bp = dbg->firstBreakpoint(); bp; bp = bp->nextInDebugger()) {
if (!wasEnabled)
bp->site->inc(cx->runtime()->defaultFreeOp());
else
bp->site->dec(cx->runtime()->defaultFreeOp());
}
/*
* Add or remove ourselves from the runtime's list of Debuggers
* that care about new globals.
*/
if (dbg->getHook(OnNewGlobalObject)) {
if (!wasEnabled) {
/* If we were not enabled, the link should be a singleton list. */
MOZ_ASSERT(JS_CLIST_IS_EMPTY(&dbg->onNewGlobalObjectWatchersLink));
JS_APPEND_LINK(&dbg->onNewGlobalObjectWatchersLink,
&cx->runtime()->onNewGlobalObjectWatchers);
} else {
/* If we were enabled, the link should be inserted in the list. */
MOZ_ASSERT(!JS_CLIST_IS_EMPTY(&dbg->onNewGlobalObjectWatchersLink));
JS_REMOVE_AND_INIT_LINK(&dbg->onNewGlobalObjectWatchersLink);
}
}
// Ensure the compartment is observable if we are re-enabling a
// Debugger with hooks that observe all execution.
if (!dbg->updateObservesAllExecutionOnDebuggees(cx, dbg->observesAllExecution()))
return false;
// Note: To toogle code coverage, we currently need to have no live
// stack frame, thus the coverage does not depend on the enabled flag.
dbg->updateObservesAsmJSOnDebuggees(dbg->observesAsmJS());
}
args.rval().setUndefined();
return true;
}
/* static */ bool
Debugger::getHookImpl(JSContext* cx, CallArgs& args, Debugger& dbg, Hook which)
{
MOZ_ASSERT(which >= 0 && which < HookCount);
args.rval().set(dbg.object->getReservedSlot(JSSLOT_DEBUG_HOOK_START + which));
return true;
}
/* static */ bool
Debugger::setHookImpl(JSContext* cx, CallArgs& args, Debugger& dbg, Hook which)
{
MOZ_ASSERT(which >= 0 && which < HookCount);
if (!args.requireAtLeast(cx, "Debugger.setHook", 1))
return false;
if (args[0].isObject()) {
if (!args[0].toObject().isCallable())
return ReportIsNotFunction(cx, args[0], args.length() - 1);
} else if (!args[0].isUndefined()) {
JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_NOT_CALLABLE_OR_UNDEFINED);
return false;
}
dbg.object->setReservedSlot(JSSLOT_DEBUG_HOOK_START + which, args[0]);
if (hookObservesAllExecution(which)) {
if (!dbg.updateObservesAllExecutionOnDebuggees(cx, dbg.observesAllExecution()))
return false;
}
args.rval().setUndefined();
return true;
}
/* static */ bool
Debugger::getOnDebuggerStatement(JSContext* cx, unsigned argc, Value* vp)
{
THIS_DEBUGGER(cx, argc, vp, "(get onDebuggerStatement)", args, dbg);
return getHookImpl(cx, args, *dbg, OnDebuggerStatement);
}
/* static */ bool
Debugger::setOnDebuggerStatement(JSContext* cx, unsigned argc, Value* vp)
{
THIS_DEBUGGER(cx, argc, vp, "(set onDebuggerStatement)", args, dbg);
return setHookImpl(cx, args, *dbg, OnDebuggerStatement);
}
/* static */ bool
Debugger::getOnExceptionUnwind(JSContext* cx, unsigned argc, Value* vp)
{
THIS_DEBUGGER(cx, argc, vp, "(get onExceptionUnwind)", args, dbg);
return getHookImpl(cx, args, *dbg, OnExceptionUnwind);
}
/* static */ bool
Debugger::setOnExceptionUnwind(JSContext* cx, unsigned argc, Value* vp)
{
THIS_DEBUGGER(cx, argc, vp, "(set onExceptionUnwind)", args, dbg);
return setHookImpl(cx, args, *dbg, OnExceptionUnwind);
}
/* static */ bool
Debugger::getOnNewScript(JSContext* cx, unsigned argc, Value* vp)
{
THIS_DEBUGGER(cx, argc, vp, "(get onNewScript)", args, dbg);
return getHookImpl(cx, args, *dbg, OnNewScript);
}
/* static */ bool
Debugger::setOnNewScript(JSContext* cx, unsigned argc, Value* vp)
{
THIS_DEBUGGER(cx, argc, vp, "(set onNewScript)", args, dbg);
return setHookImpl(cx, args, *dbg, OnNewScript);
}
/* static */ bool
Debugger::getOnNewPromise(JSContext* cx, unsigned argc, Value* vp)
{
THIS_DEBUGGER(cx, argc, vp, "(get onNewPromise)", args, dbg);
return getHookImpl(cx, args, *dbg, OnNewPromise);
}
/* static */ bool
Debugger::setOnNewPromise(JSContext* cx, unsigned argc, Value* vp)
{
THIS_DEBUGGER(cx, argc, vp, "(set onNewPromise)", args, dbg);
return setHookImpl(cx, args, *dbg, OnNewPromise);
}
/* static */ bool
Debugger::getOnPromiseSettled(JSContext* cx, unsigned argc, Value* vp)
{
THIS_DEBUGGER(cx, argc, vp, "(get onPromiseSettled)", args, dbg);
return getHookImpl(cx, args, *dbg, OnPromiseSettled);
}
/* static */ bool
Debugger::setOnPromiseSettled(JSContext* cx, unsigned argc, Value* vp)
{
THIS_DEBUGGER(cx, argc, vp, "(set onPromiseSettled)", args, dbg);
return setHookImpl(cx, args, *dbg, OnPromiseSettled);
}
/* static */ bool
Debugger::getOnEnterFrame(JSContext* cx, unsigned argc, Value* vp)
{
THIS_DEBUGGER(cx, argc, vp, "(get onEnterFrame)", args, dbg);
return getHookImpl(cx, args, *dbg, OnEnterFrame);
}
/* static */ bool
Debugger::setOnEnterFrame(JSContext* cx, unsigned argc, Value* vp)
{
THIS_DEBUGGER(cx, argc, vp, "(set onEnterFrame)", args, dbg);
return setHookImpl(cx, args, *dbg, OnEnterFrame);
}
/* static */ bool
Debugger::getOnNewGlobalObject(JSContext* cx, unsigned argc, Value* vp)
{
THIS_DEBUGGER(cx, argc, vp, "(get onNewGlobalObject)", args, dbg);
return getHookImpl(cx, args, *dbg, OnNewGlobalObject);
}
/* static */ bool
Debugger::setOnNewGlobalObject(JSContext* cx, unsigned argc, Value* vp)
{
THIS_DEBUGGER(cx, argc, vp, "setOnNewGlobalObject", args, dbg);
RootedObject oldHook(cx, dbg->getHook(OnNewGlobalObject));
if (!setHookImpl(cx, args, *dbg, OnNewGlobalObject))
return false;
/*
* Add or remove ourselves from the runtime's list of Debuggers that
* care about new globals.
*/
if (dbg->enabled) {