/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "debugger/Environment-inl.h" #include "mozilla/Assertions.h" // for AssertionConditionType #include "mozilla/Maybe.h" // for Maybe, Some, Nothing #include "mozilla/Vector.h" // for Vector #include // for strlen, size_t #include // for move #include "debugger/Debugger.h" // for Env, Debugger, ValueToIdentifier #include "debugger/Object.h" // for DebuggerObject #include "debugger/Script.h" // for DebuggerScript #include "frontend/BytecodeCompiler.h" // for IsIdentifier #include "gc/Tracer.h" // for TraceManuallyBarrieredCrossCompartmentEdge #include "js/CallArgs.h" // for CallArgs #include "js/friend/ErrorMessages.h" // for GetErrorMessage, JSMSG_* #include "js/HeapAPI.h" // for IsInsideNursery #include "js/RootingAPI.h" // for Rooted, MutableHandle #include "vm/Compartment.h" // for Compartment #include "vm/JSAtom.h" // for Atomize #include "vm/JSContext.h" // for JSContext #include "vm/JSFunction.h" // for JSFunction #include "vm/JSObject.h" // for JSObject, RequireObject, #include "vm/NativeObject.h" // for NativeObject, JSObject::is #include "vm/Realm.h" // for AutoRealm, ErrorCopier #include "vm/Scope.h" // for ScopeKind, ScopeKindString #include "vm/StringType.h" // for JSAtom #include "vm/Compartment-inl.h" // for Compartment::wrap #include "vm/EnvironmentObject-inl.h" // for JSObject::enclosingEnvironment #include "vm/JSObject-inl.h" // for IsInternalFunctionObject, NewObjectWithGivenProtoAndKind #include "vm/ObjectOperations-inl.h" // for HasProperty, GetProperty #include "vm/Realm-inl.h" // for AutoRealm::AutoRealm namespace js { class GlobalObject; } using namespace js; using js::frontend::IsIdentifier; using mozilla::Maybe; using mozilla::Nothing; using mozilla::Some; const JSClassOps DebuggerEnvironment::classOps_ = { nullptr, // addProperty nullptr, // delProperty nullptr, // enumerate nullptr, // newEnumerate nullptr, // resolve nullptr, // mayResolve nullptr, // finalize nullptr, // call nullptr, // construct CallTraceMethod, // trace }; const JSClass DebuggerEnvironment::class_ = { "Environment", JSCLASS_HAS_RESERVED_SLOTS(DebuggerEnvironment::RESERVED_SLOTS), &classOps_}; void DebuggerEnvironment::trace(JSTracer* trc) { // There is a barrier on private pointers, so the Unbarriered marking // is okay. if (Env* referent = maybeReferent()) { TraceManuallyBarrieredCrossCompartmentEdge(trc, this, &referent, "Debugger.Environment referent"); if (referent != maybeReferent()) { setReservedSlotGCThingAsPrivateUnbarriered(ENV_SLOT, referent); } } } static DebuggerEnvironment* DebuggerEnvironment_checkThis( JSContext* cx, const CallArgs& args) { JSObject* thisobj = RequireObject(cx, args.thisv()); if (!thisobj) { return nullptr; } if (!thisobj->is()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, "Debugger.Environment", "method", thisobj->getClass()->name); return nullptr; } return &thisobj->as(); } struct MOZ_STACK_CLASS DebuggerEnvironment::CallData { JSContext* cx; const CallArgs& args; Handle environment; CallData(JSContext* cx, const CallArgs& args, Handle env) : cx(cx), args(args), environment(env) {} bool typeGetter(); bool scopeKindGetter(); bool parentGetter(); bool objectGetter(); bool calleeScriptGetter(); bool inspectableGetter(); bool optimizedOutGetter(); bool namesMethod(); bool findMethod(); bool getVariableMethod(); bool setVariableMethod(); using Method = bool (CallData::*)(); template static bool ToNative(JSContext* cx, unsigned argc, Value* vp); }; template /* static */ bool DebuggerEnvironment::CallData::ToNative(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); Rooted environment( cx, DebuggerEnvironment_checkThis(cx, args)); if (!environment) { return false; } CallData data(cx, args, environment); return (data.*MyMethod)(); } /* static */ bool DebuggerEnvironment::construct(JSContext* cx, unsigned argc, Value* vp) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, "Debugger.Environment"); return false; } static bool IsDeclarative(Env* env) { return env->is() && env->as().isForDeclarative(); } template static bool IsDebugEnvironmentWrapper(Env* env) { return env->is() && env->as().environment().is(); } bool DebuggerEnvironment::CallData::typeGetter() { if (!environment->requireDebuggee(cx)) { return false; } DebuggerEnvironmentType type = environment->type(); const char* s; switch (type) { case DebuggerEnvironmentType::Declarative: s = "declarative"; break; case DebuggerEnvironmentType::With: s = "with"; break; case DebuggerEnvironmentType::Object: s = "object"; break; } JSAtom* str = Atomize(cx, s, strlen(s)); if (!str) { return false; } args.rval().setString(str); return true; } bool DebuggerEnvironment::CallData::scopeKindGetter() { if (!environment->requireDebuggee(cx)) { return false; } Maybe kind = environment->scopeKind(); if (kind.isSome()) { const char* s = ScopeKindString(*kind); JSAtom* str = Atomize(cx, s, strlen(s)); if (!str) { return false; } args.rval().setString(str); } else { args.rval().setNull(); } return true; } bool DebuggerEnvironment::CallData::parentGetter() { if (!environment->requireDebuggee(cx)) { return false; } Rooted result(cx); if (!environment->getParent(cx, &result)) { return false; } args.rval().setObjectOrNull(result); return true; } bool DebuggerEnvironment::CallData::objectGetter() { if (!environment->requireDebuggee(cx)) { return false; } if (environment->type() == DebuggerEnvironmentType::Declarative) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_NO_ENV_OBJECT); return false; } Rooted result(cx); if (!environment->getObject(cx, &result)) { return false; } args.rval().setObject(*result); return true; } bool DebuggerEnvironment::CallData::calleeScriptGetter() { if (!environment->requireDebuggee(cx)) { return false; } Rooted result(cx); if (!environment->getCalleeScript(cx, &result)) { return false; } args.rval().setObjectOrNull(result); return true; } bool DebuggerEnvironment::CallData::inspectableGetter() { args.rval().setBoolean(environment->isDebuggee()); return true; } bool DebuggerEnvironment::CallData::optimizedOutGetter() { args.rval().setBoolean(environment->isOptimized()); return true; } bool DebuggerEnvironment::CallData::namesMethod() { if (!environment->requireDebuggee(cx)) { return false; } RootedIdVector ids(cx); if (!DebuggerEnvironment::getNames(cx, environment, &ids)) { return false; } JSObject* obj = IdVectorToArray(cx, ids); if (!obj) { return false; } args.rval().setObject(*obj); return true; } bool DebuggerEnvironment::CallData::findMethod() { if (!args.requireAtLeast(cx, "Debugger.Environment.find", 1)) { return false; } if (!environment->requireDebuggee(cx)) { return false; } RootedId id(cx); if (!ValueToIdentifier(cx, args[0], &id)) { return false; } Rooted result(cx); if (!DebuggerEnvironment::find(cx, environment, id, &result)) { return false; } args.rval().setObjectOrNull(result); return true; } bool DebuggerEnvironment::CallData::getVariableMethod() { if (!args.requireAtLeast(cx, "Debugger.Environment.getVariable", 1)) { return false; } if (!environment->requireDebuggee(cx)) { return false; } RootedId id(cx); if (!ValueToIdentifier(cx, args[0], &id)) { return false; } return DebuggerEnvironment::getVariable(cx, environment, id, args.rval()); } bool DebuggerEnvironment::CallData::setVariableMethod() { if (!args.requireAtLeast(cx, "Debugger.Environment.setVariable", 2)) { return false; } if (!environment->requireDebuggee(cx)) { return false; } RootedId id(cx); if (!ValueToIdentifier(cx, args[0], &id)) { return false; } if (!DebuggerEnvironment::setVariable(cx, environment, id, args[1])) { return false; } args.rval().setUndefined(); return true; } bool DebuggerEnvironment::requireDebuggee(JSContext* cx) const { if (!isDebuggee()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_NOT_DEBUGGEE, "Debugger.Environment", "environment"); return false; } return true; } const JSPropertySpec DebuggerEnvironment::properties_[] = { JS_DEBUG_PSG("type", typeGetter), JS_DEBUG_PSG("scopeKind", scopeKindGetter), JS_DEBUG_PSG("parent", parentGetter), JS_DEBUG_PSG("object", objectGetter), JS_DEBUG_PSG("calleeScript", calleeScriptGetter), JS_DEBUG_PSG("inspectable", inspectableGetter), JS_DEBUG_PSG("optimizedOut", optimizedOutGetter), JS_PS_END}; const JSFunctionSpec DebuggerEnvironment::methods_[] = { JS_DEBUG_FN("names", namesMethod, 0), JS_DEBUG_FN("find", findMethod, 1), JS_DEBUG_FN("getVariable", getVariableMethod, 1), JS_DEBUG_FN("setVariable", setVariableMethod, 2), JS_FS_END}; /* static */ NativeObject* DebuggerEnvironment::initClass(JSContext* cx, Handle global, HandleObject dbgCtor) { return InitClass(cx, dbgCtor, nullptr, nullptr, "Environment", construct, 0, properties_, methods_, nullptr, nullptr); } /* static */ DebuggerEnvironment* DebuggerEnvironment::create( JSContext* cx, HandleObject proto, HandleObject referent, Handle debugger) { DebuggerEnvironment* obj = IsInsideNursery(referent) ? NewObjectWithGivenProto(cx, proto) : NewTenuredObjectWithGivenProto(cx, proto); if (!obj) { return nullptr; } obj->setReservedSlotGCThingAsPrivate(ENV_SLOT, referent); obj->setReservedSlot(OWNER_SLOT, ObjectValue(*debugger)); return obj; } /* static */ DebuggerEnvironmentType DebuggerEnvironment::type() const { // Don't bother switching compartments just to check env's type. if (IsDeclarative(referent())) { return DebuggerEnvironmentType::Declarative; } if (IsDebugEnvironmentWrapper(referent())) { return DebuggerEnvironmentType::With; } return DebuggerEnvironmentType::Object; } mozilla::Maybe DebuggerEnvironment::scopeKind() const { if (!referent()->is()) { return Nothing(); } EnvironmentObject& env = referent()->as().environment(); Scope* scope = GetEnvironmentScope(env); return scope ? Some(scope->kind()) : Nothing(); } bool DebuggerEnvironment::getParent( JSContext* cx, MutableHandle result) const { // Don't bother switching compartments just to get env's parent. Rooted parent(cx, referent()->enclosingEnvironment()); if (!parent) { result.set(nullptr); return true; } return owner()->wrapEnvironment(cx, parent, result); } bool DebuggerEnvironment::getObject( JSContext* cx, MutableHandle result) const { MOZ_ASSERT(type() != DebuggerEnvironmentType::Declarative); // Don't bother switching compartments just to get env's object. RootedObject object(cx); if (IsDebugEnvironmentWrapper(referent())) { object.set(&referent() ->as() .environment() .as() .object()); } else if (IsDebugEnvironmentWrapper( referent())) { object.set(&referent() ->as() .environment() .as()); } else { object.set(referent()); MOZ_ASSERT(!object->is()); } return owner()->wrapDebuggeeObject(cx, object, result); } bool DebuggerEnvironment::getCalleeScript( JSContext* cx, MutableHandle result) const { if (!referent()->is()) { result.set(nullptr); return true; } JSObject& scope = referent()->as().environment(); if (!scope.is()) { result.set(nullptr); return true; } Rooted script(cx, scope.as().callee().baseScript()); DebuggerScript* scriptObject = owner()->wrapScript(cx, script); if (!scriptObject) { return false; } result.set(scriptObject); return true; } bool DebuggerEnvironment::isDebuggee() const { MOZ_ASSERT(referent()); MOZ_ASSERT(!referent()->is()); return owner()->observesGlobal(&referent()->nonCCWGlobal()); } bool DebuggerEnvironment::isOptimized() const { return referent()->is() && referent()->as().isOptimizedOut(); } /* static */ bool DebuggerEnvironment::getNames(JSContext* cx, Handle environment, MutableHandleIdVector result) { MOZ_ASSERT(environment->isDebuggee()); MOZ_ASSERT(result.empty()); Rooted referent(cx, environment->referent()); { Maybe ar; ar.emplace(cx, referent); ErrorCopier ec(ar); if (!GetPropertyKeys(cx, referent, JSITER_HIDDEN, result)) { return false; } } result.eraseIf([](PropertyKey key) { return !key.isAtom() || !IsIdentifier(key.toAtom()); }); for (size_t i = 0; i < result.length(); ++i) { cx->markAtom(result[i].toAtom()); } return true; } /* static */ bool DebuggerEnvironment::find(JSContext* cx, Handle environment, HandleId id, MutableHandle result) { MOZ_ASSERT(environment->isDebuggee()); Rooted env(cx, environment->referent()); Debugger* dbg = environment->owner(); { Maybe ar; ar.emplace(cx, env); cx->markId(id); // This can trigger resolve hooks. ErrorCopier ec(ar); for (; env; env = env->enclosingEnvironment()) { bool found; if (!HasProperty(cx, env, id, &found)) { return false; } if (found) { break; } } } if (!env) { result.set(nullptr); return true; } return dbg->wrapEnvironment(cx, env, result); } /* static */ bool DebuggerEnvironment::getVariable(JSContext* cx, Handle environment, HandleId id, MutableHandleValue result) { MOZ_ASSERT(environment->isDebuggee()); Rooted referent(cx, environment->referent()); Debugger* dbg = environment->owner(); { Maybe ar; ar.emplace(cx, referent); cx->markId(id); // This can trigger getters. ErrorCopier ec(ar); bool found; if (!HasProperty(cx, referent, id, &found)) { return false; } if (!found) { result.setUndefined(); return true; } // For DebugEnvironmentProxys, we get sentinel values for optimized out // slots and arguments instead of throwing (the default behavior). // // See wrapDebuggeeValue for how the sentinel values are wrapped. if (referent->is()) { Rooted env( cx, &referent->as()); if (!DebugEnvironmentProxy::getMaybeSentinelValue(cx, env, id, result)) { return false; } } else { if (!GetProperty(cx, referent, referent, id, result)) { return false; } } } // When we've faked up scope chain objects for optimized-out scopes, // declarative environments may contain internal JSFunction objects, which // we shouldn't expose to the user. if (result.isObject()) { RootedObject obj(cx, &result.toObject()); if (obj->is() && IsInternalFunctionObject(obj->as())) result.setMagic(JS_OPTIMIZED_OUT); } return dbg->wrapDebuggeeValue(cx, result); } /* static */ bool DebuggerEnvironment::setVariable(JSContext* cx, Handle environment, HandleId id, HandleValue value_) { MOZ_ASSERT(environment->isDebuggee()); Rooted referent(cx, environment->referent()); Debugger* dbg = environment->owner(); RootedValue value(cx, value_); if (!dbg->unwrapDebuggeeValue(cx, &value)) { return false; } { Maybe ar; ar.emplace(cx, referent); if (!cx->compartment()->wrap(cx, &value)) { return false; } cx->markId(id); // This can trigger setters. ErrorCopier ec(ar); // Make sure the environment actually has the specified binding. bool found; if (!HasProperty(cx, referent, id, &found)) { return false; } if (!found) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_VARIABLE_NOT_FOUND); return false; } // Just set the property. if (!SetProperty(cx, referent, id, value)) { return false; } } return true; }