Raw File
JavaScriptShared.cpp
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * vim: set ts=4 sw=2 et 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 "JavaScriptShared.h"
#include "mozilla/dom/BindingUtils.h"
#include "mozilla/dom/CPOWManagerGetter.h"
#include "mozilla/dom/BrowserChild.h"
#include "jsfriendapi.h"
#include "js/Symbol.h"
#include "xpcprivate.h"
#include "WrapperFactory.h"
#include "mozilla/Preferences.h"

using namespace js;
using namespace JS;
using namespace mozilla;
using namespace mozilla::jsipc;

IdToObjectMap::IdToObjectMap() : table_(SystemAllocPolicy(), 32) {}

void IdToObjectMap::trace(JSTracer* trc, uint64_t minimumId) {
  for (Table::Range r(table_.all()); !r.empty(); r.popFront()) {
    if (r.front().key().serialNumber() >= minimumId) {
      JS::TraceEdge(trc, &r.front().value(), "ipc-object");
    }
  }
}

void IdToObjectMap::sweep() {
  for (Table::Enum e(table_); !e.empty(); e.popFront()) {
    JS::Heap<JSObject*>* objp = &e.front().value();
    JS_UpdateWeakPointerAfterGC(objp);
    if (!*objp) {
      e.removeFront();
    }
  }
}

JSObject* IdToObjectMap::find(ObjectId id) {
  Table::Ptr p = table_.lookup(id);
  if (!p) {
    return nullptr;
  }
  return p->value();
}

JSObject* IdToObjectMap::findPreserveColor(ObjectId id) {
  Table::Ptr p = table_.lookup(id);
  if (!p) {
    return nullptr;
  }
  return p->value().unbarrieredGet();
}

bool IdToObjectMap::add(ObjectId id, JSObject* obj) {
  return table_.put(id, obj);
}

void IdToObjectMap::remove(ObjectId id) { table_.remove(id); }

void IdToObjectMap::clear() { table_.clear(); }

bool IdToObjectMap::empty() const { return table_.empty(); }

#ifdef DEBUG
bool IdToObjectMap::has(const ObjectId& id, const JSObject* obj) const {
  auto p = table_.lookup(id);
  if (!p) {
    return false;
  }
  return p->value() == obj;
}
#endif

ObjectToIdMap::ObjectToIdMap() : table_(SystemAllocPolicy(), 32) {}

void ObjectToIdMap::trace(JSTracer* trc) { table_.trace(trc); }

void ObjectToIdMap::sweep() { table_.sweep(); }

ObjectId ObjectToIdMap::find(JSObject* obj) {
  Table::Ptr p = table_.lookup(obj);
  if (!p) {
    return ObjectId::nullId();
  }
  return p->value();
}

bool ObjectToIdMap::add(JSContext* cx, JSObject* obj, ObjectId id) {
  return table_.put(obj, id);
}

void ObjectToIdMap::remove(JSObject* obj) { table_.remove(obj); }

void ObjectToIdMap::clear() { table_.clear(); }

bool JavaScriptShared::sLoggingInitialized;
bool JavaScriptShared::sLoggingEnabledByEnvVar;
bool JavaScriptShared::sStackLoggingEnabledByEnvVar;

JavaScriptShared::JavaScriptShared()
    : refcount_(1), nextSerialNumber_(1), nextCPOWNumber_(1) {
  if (!sLoggingInitialized) {
    sLoggingInitialized = true;

    if (PR_GetEnv("MOZ_CPOW_LOG")) {
      sLoggingEnabledByEnvVar = true;
      sStackLoggingEnabledByEnvVar =
          !!strstr(PR_GetEnv("MOZ_CPOW_LOG"), "stacks");
    }
  }
}

JavaScriptShared::~JavaScriptShared() { MOZ_RELEASE_ASSERT(cpows_.empty()); }

void JavaScriptShared::decref() {
  refcount_--;
  if (!refcount_) {
    delete this;
  }
}

void JavaScriptShared::incref() { refcount_++; }

bool JavaScriptShared::convertIdToGeckoString(JSContext* cx, JS::HandleId id,
                                              nsString* to) {
  RootedValue idval(cx);
  if (!JS_IdToValue(cx, id, &idval)) {
    return false;
  }

  RootedString str(cx, ToString(cx, idval));
  if (!str) {
    return false;
  }

  return AssignJSString(cx, *to, str);
}

bool JavaScriptShared::convertGeckoStringToId(JSContext* cx,
                                              const nsString& from,
                                              JS::MutableHandleId to) {
  RootedString str(cx,
                   JS_NewUCStringCopyN(cx, from.BeginReading(), from.Length()));
  if (!str) {
    return false;
  }

  return JS_StringToId(cx, str, to);
}

bool JavaScriptShared::toVariant(JSContext* cx, JS::HandleValue from,
                                 JSVariant* to) {
  switch (JS_TypeOfValue(cx, from)) {
    case JSTYPE_UNDEFINED:
      *to = UndefinedVariant();
      return true;

    case JSTYPE_OBJECT:
    case JSTYPE_FUNCTION: {
      RootedObject obj(cx, from.toObjectOrNull());
      if (!obj) {
        MOZ_ASSERT(from.isNull());
        *to = NullVariant();
        return true;
      }

      Maybe<nsID> id = xpc::JSValue2ID(cx, from);
      if (id) {
        JSIID iid;
        ConvertID(*id, &iid);
        *to = iid;
        return true;
      }

      ObjectVariant objVar;
      if (!toObjectVariant(cx, obj, &objVar)) {
        return false;
      }
      *to = objVar;
      return true;
    }

    case JSTYPE_SYMBOL: {
      RootedSymbol sym(cx, from.toSymbol());

      SymbolVariant symVar;
      if (!toSymbolVariant(cx, sym, &symVar)) {
        return false;
      }
      *to = symVar;
      return true;
    }

    case JSTYPE_STRING: {
      nsAutoJSString autoStr;
      if (!autoStr.init(cx, from)) {
        return false;
      }
      *to = autoStr;
      return true;
    }

    case JSTYPE_NUMBER:
      if (from.isInt32()) {
        *to = double(from.toInt32());
      } else {
        *to = from.toDouble();
      }
      return true;

    case JSTYPE_BOOLEAN:
      *to = from.toBoolean();
      return true;

    default:
      MOZ_ASSERT(false);
      return false;
  }
}

bool JavaScriptShared::fromVariant(JSContext* cx, const JSVariant& from,
                                   MutableHandleValue to) {
  switch (from.type()) {
    case JSVariant::TUndefinedVariant:
      to.set(UndefinedValue());
      return true;

    case JSVariant::TNullVariant:
      to.set(NullValue());
      return true;

    case JSVariant::TObjectVariant: {
      JSObject* obj = fromObjectVariant(cx, from.get_ObjectVariant());
      if (!obj) {
        return false;
      }
      to.set(ObjectValue(*obj));
      return true;
    }

    case JSVariant::TSymbolVariant: {
      Symbol* sym = fromSymbolVariant(cx, from.get_SymbolVariant());
      if (!sym) {
        return false;
      }
      to.setSymbol(sym);
      return true;
    }

    case JSVariant::Tdouble:
      to.set(JS_NumberValue(from.get_double()));
      return true;

    case JSVariant::Tbool:
      to.setBoolean(from.get_bool());
      return true;

    case JSVariant::TnsString: {
      const nsString& old = from.get_nsString();
      JSString* str = JS_NewUCStringCopyN(cx, old.BeginReading(), old.Length());
      if (!str) {
        return false;
      }
      to.set(StringValue(str));
      return true;
    }

    case JSVariant::TJSIID: {
      nsID iid;
      const JSIID& id = from.get_JSIID();
      ConvertID(id, &iid);
      return xpc::ID2JSValue(cx, iid, to);
    }

    default:
      MOZ_CRASH("NYI");
      return false;
  }
}

bool JavaScriptShared::toJSIDVariant(JSContext* cx, HandleId from,
                                     JSIDVariant* to) {
  if (JSID_IS_STRING(from)) {
    nsAutoJSString autoStr;
    if (!autoStr.init(cx, JSID_TO_STRING(from))) {
      return false;
    }
    *to = autoStr;
    return true;
  }
  if (JSID_IS_INT(from)) {
    *to = JSID_TO_INT(from);
    return true;
  }
  if (JSID_IS_SYMBOL(from)) {
    SymbolVariant symVar;
    if (!toSymbolVariant(cx, JSID_TO_SYMBOL(from), &symVar)) {
      return false;
    }
    *to = symVar;
    return true;
  }
  MOZ_ASSERT(false);
  return false;
}

bool JavaScriptShared::fromJSIDVariant(JSContext* cx, const JSIDVariant& from,
                                       MutableHandleId to) {
  switch (from.type()) {
    case JSIDVariant::TSymbolVariant: {
      Symbol* sym = fromSymbolVariant(cx, from.get_SymbolVariant());
      if (!sym) {
        return false;
      }
      to.set(SYMBOL_TO_JSID(sym));
      return true;
    }

    case JSIDVariant::TnsString:
      return convertGeckoStringToId(cx, from.get_nsString(), to);

    case JSIDVariant::Tint32_t:
      to.set(INT_TO_JSID(from.get_int32_t()));
      return true;

    default:
      return false;
  }
}

bool JavaScriptShared::toSymbolVariant(JSContext* cx, JS::Symbol* symArg,
                                       SymbolVariant* symVarp) {
  RootedSymbol sym(cx, symArg);
  MOZ_ASSERT(sym);

  SymbolCode code = GetSymbolCode(sym);
  if (static_cast<uint32_t>(code) < WellKnownSymbolLimit) {
    *symVarp = WellKnownSymbol(static_cast<uint32_t>(code));
    return true;
  }
  if (code == SymbolCode::InSymbolRegistry) {
    nsAutoJSString autoStr;
    if (!autoStr.init(cx, GetSymbolDescription(sym))) {
      return false;
    }
    *symVarp = RegisteredSymbol(autoStr);
    return true;
  }

  JS_ReportErrorASCII(cx, "unique symbol can't be used with CPOW");
  return false;
}

JS::Symbol* JavaScriptShared::fromSymbolVariant(JSContext* cx,
                                                const SymbolVariant& symVar) {
  switch (symVar.type()) {
    case SymbolVariant::TWellKnownSymbol: {
      uint32_t which = symVar.get_WellKnownSymbol().which();
      if (which < WellKnownSymbolLimit) {
        return GetWellKnownSymbol(cx, static_cast<SymbolCode>(which));
      }
      MOZ_ASSERT(false, "bad child data");
      return nullptr;
    }

    case SymbolVariant::TRegisteredSymbol: {
      nsString key = symVar.get_RegisteredSymbol().key();
      RootedString str(cx, JS_NewUCStringCopyN(cx, key.get(), key.Length()));
      if (!str) {
        return nullptr;
      }
      return GetSymbolFor(cx, str);
    }

    default:
      return nullptr;
  }
}

/* static */
void JavaScriptShared::ConvertID(const nsID& from, JSIID* to) {
  to->m0() = from.m0;
  to->m1() = from.m1;
  to->m2() = from.m2;
  to->m3_0() = from.m3[0];
  to->m3_1() = from.m3[1];
  to->m3_2() = from.m3[2];
  to->m3_3() = from.m3[3];
  to->m3_4() = from.m3[4];
  to->m3_5() = from.m3[5];
  to->m3_6() = from.m3[6];
  to->m3_7() = from.m3[7];
}

/* static */
void JavaScriptShared::ConvertID(const JSIID& from, nsID* to) {
  to->m0 = from.m0();
  to->m1 = from.m1();
  to->m2 = from.m2();
  to->m3[0] = from.m3_0();
  to->m3[1] = from.m3_1();
  to->m3[2] = from.m3_2();
  to->m3[3] = from.m3_3();
  to->m3[4] = from.m3_4();
  to->m3[5] = from.m3_5();
  to->m3[6] = from.m3_6();
  to->m3[7] = from.m3_7();
}

JSObject* JavaScriptShared::findCPOWById(const ObjectId& objId) {
  JSObject* obj = findCPOWByIdPreserveColor(objId);
  if (obj) {
    JS::ExposeObjectToActiveJS(obj);
  }
  return obj;
}

JSObject* JavaScriptShared::findCPOWByIdPreserveColor(const ObjectId& objId) {
  JSObject* obj = cpows_.findPreserveColor(objId);
  if (!obj) {
    return nullptr;
  }

  if (js::gc::EdgeNeedsSweepUnbarriered(&obj)) {
    cpows_.remove(objId);
    return nullptr;
  }

  return obj;
}

JSObject* JavaScriptShared::findObjectById(JSContext* cx,
                                           const ObjectId& objId) {
  RootedObject obj(cx, objects_.find(objId));
  if (!obj) {
    JS_ReportErrorASCII(cx, "operation not possible on dead CPOW");
    return nullptr;
  }

  // Each process has a dedicated compartment for CPOW targets. All CPOWs
  // from the other process point to objects in this scope. From there, they
  // can access objects in other compartments using cross-compartment
  // wrappers.
  JSAutoRealm ar(cx, scopeForTargetObjects());
  if (objId.hasXrayWaiver()) {
    obj = js::ToWindowProxyIfWindow(obj);
    MOZ_ASSERT(obj);
    if (!xpc::WrapperFactory::WaiveXrayAndWrap(cx, &obj)) {
      return nullptr;
    }
  } else {
    if (!JS_WrapObject(cx, &obj)) {
      return nullptr;
    }
  }
  return obj;
}

static const uint64_t UnknownPropertyOp = 1;

bool JavaScriptShared::fromDescriptor(JSContext* cx,
                                      Handle<PropertyDescriptor> desc,
                                      PPropertyDescriptor* out) {
  out->attrs() = desc.attributes();
  if (!toVariant(cx, desc.value(), &out->value())) {
    return false;
  }

  if (!toObjectOrNullVariant(cx, desc.object(), &out->obj())) {
    return false;
  }

  if (!desc.getter()) {
    out->getter() = 0;
  } else if (desc.hasGetterObject()) {
    JSObject* getter = desc.getterObject();
    ObjectVariant objVar;
    if (!toObjectVariant(cx, getter, &objVar)) {
      return false;
    }
    out->getter() = objVar;
  } else {
    out->getter() = UnknownPropertyOp;
  }

  if (!desc.setter()) {
    out->setter() = 0;
  } else if (desc.hasSetterObject()) {
    JSObject* setter = desc.setterObject();
    ObjectVariant objVar;
    if (!toObjectVariant(cx, setter, &objVar)) {
      return false;
    }
    out->setter() = objVar;
  } else {
    out->setter() = UnknownPropertyOp;
  }

  return true;
}

bool UnknownPropertyStub(JSContext* cx, HandleObject obj, HandleId id,
                         MutableHandleValue vp) {
  JS_ReportErrorASCII(cx, "getter could not be wrapped via CPOWs");
  return false;
}

bool UnknownStrictPropertyStub(JSContext* cx, HandleObject obj, HandleId id,
                               HandleValue v, ObjectOpResult& result) {
  JS_ReportErrorASCII(cx, "setter could not be wrapped via CPOWs");
  return false;
}

bool JavaScriptShared::toDescriptor(JSContext* cx,
                                    const PPropertyDescriptor& in,
                                    MutableHandle<PropertyDescriptor> out) {
  out.setAttributes(in.attrs());
  if (!fromVariant(cx, in.value(), out.value())) {
    return false;
  }
  out.object().set(fromObjectOrNullVariant(cx, in.obj()));

  if (in.getter().type() == GetterSetter::Tuint64_t &&
      !in.getter().get_uint64_t()) {
    out.setGetter(nullptr);
  } else if (in.attrs() & JSPROP_GETTER) {
    Rooted<JSObject*> getter(cx);
    getter = fromObjectVariant(cx, in.getter().get_ObjectVariant());
    if (!getter) {
      return false;
    }
    out.setGetter(JS_DATA_TO_FUNC_PTR(JSGetterOp, getter.get()));
  } else {
    out.setGetter(UnknownPropertyStub);
  }

  if (in.setter().type() == GetterSetter::Tuint64_t &&
      !in.setter().get_uint64_t()) {
    out.setSetter(nullptr);
  } else if (in.attrs() & JSPROP_SETTER) {
    Rooted<JSObject*> setter(cx);
    setter = fromObjectVariant(cx, in.setter().get_ObjectVariant());
    if (!setter) {
      return false;
    }
    out.setSetter(JS_DATA_TO_FUNC_PTR(JSSetterOp, setter.get()));
  } else {
    out.setSetter(UnknownStrictPropertyStub);
  }

  return true;
}

bool JavaScriptShared::toObjectOrNullVariant(JSContext* cx, JSObject* obj,
                                             ObjectOrNullVariant* objVarp) {
  if (!obj) {
    *objVarp = NullVariant();
    return true;
  }

  ObjectVariant objVar;
  if (!toObjectVariant(cx, obj, &objVar)) {
    return false;
  }

  *objVarp = objVar;
  return true;
}

JSObject* JavaScriptShared::fromObjectOrNullVariant(
    JSContext* cx, const ObjectOrNullVariant& objVar) {
  if (objVar.type() == ObjectOrNullVariant::TNullVariant) {
    return nullptr;
  }

  return fromObjectVariant(cx, objVar.get_ObjectVariant());
}

CrossProcessCpowHolder::CrossProcessCpowHolder(
    dom::CPOWManagerGetter* managerGetter, const nsTArray<CpowEntry>& cpows)
    : js_(nullptr), cpows_(cpows), unwrapped_(false) {
  // Only instantiate the CPOW manager if we might need it later.
  if (cpows.Length()) {
    js_ = managerGetter->GetCPOWManager();
  }
}

CrossProcessCpowHolder::~CrossProcessCpowHolder() {
  if (cpows_.Length() && !unwrapped_) {
    // This should only happen if a message manager message
    // containing CPOWs gets ignored for some reason. We need to
    // unwrap every incoming CPOW in this process to ensure that
    // the corresponding part of the CPOW in the other process
    // will eventually be collected. The scope for this object
    // doesn't really matter, because it immediately becomes
    // garbage. Ignore this for middleman processes used when
    // recording or replaying, as they do not have a CPOW manager
    // and the message will also be received in the recording
    // process.
    if (recordreplay::IsMiddleman()) {
      return;
    }
    AutoJSAPI jsapi;
    if (!jsapi.Init(xpc::PrivilegedJunkScope())) {
      return;
    }
    JSContext* cx = jsapi.cx();
    JS::Rooted<JSObject*> cpows(cx);
    js_->Unwrap(cx, cpows_, &cpows);
  }
}

bool CrossProcessCpowHolder::ToObject(JSContext* cx,
                                      JS::MutableHandleObject objp) {
  unwrapped_ = true;

  if (!cpows_.Length()) {
    return true;
  }

  return js_->Unwrap(cx, cpows_, objp);
}

bool JavaScriptShared::Unwrap(JSContext* cx, const nsTArray<CpowEntry>& aCpows,
                              JS::MutableHandleObject objp) {
  // Middleman processes never operate on CPOWs.
  MOZ_ASSERT(!recordreplay::IsMiddleman());

  objp.set(nullptr);

  if (!aCpows.Length()) {
    return true;
  }

  RootedObject obj(cx, JS_NewPlainObject(cx));
  if (!obj) {
    return false;
  }

  RootedValue v(cx);
  RootedString str(cx);
  for (size_t i = 0; i < aCpows.Length(); i++) {
    const nsString& name = aCpows[i].name();

    if (!fromVariant(cx, aCpows[i].value(), &v)) {
      return false;
    }

    if (!JS_DefineUCProperty(cx, obj, name.BeginReading(), name.Length(), v,
                             JSPROP_ENUMERATE)) {
      return false;
    }
  }

  objp.set(obj);
  return true;
}

bool JavaScriptShared::Wrap(JSContext* cx, HandleObject aObj,
                            nsTArray<CpowEntry>* outCpows) {
  if (!aObj) {
    return true;
  }

  Rooted<IdVector> ids(cx, IdVector(cx));
  if (!JS_Enumerate(cx, aObj, &ids)) {
    return false;
  }

  RootedId id(cx);
  RootedValue v(cx);
  for (size_t i = 0; i < ids.length(); i++) {
    id = ids[i];

    nsString str;
    if (!convertIdToGeckoString(cx, id, &str)) {
      return false;
    }

    if (!JS_GetPropertyById(cx, aObj, id, &v)) {
      return false;
    }

    JSVariant var;
    if (!toVariant(cx, v, &var)) {
      return false;
    }

    outCpows->AppendElement(CpowEntry(str, var));
  }

  return true;
}

CPOWManager* mozilla::jsipc::CPOWManagerFor(PJavaScriptParent* aParent) {
  return static_cast<JavaScriptParent*>(aParent);
}

CPOWManager* mozilla::jsipc::CPOWManagerFor(PJavaScriptChild* aChild) {
  return static_cast<JavaScriptChild*>(aChild);
}
back to top