js/src/builtin/WeakRefObject.cpp
author William Durand <wdurand@mozilla.com>
Sat, 23 Oct 2021 15:10:40 +0000
changeset 596797 226ea4af4493332137b513239b5c4506ee14dffe
parent 596454 e1ef1c085097d633ccbb68f84bf1eded12c49a70
permissions -rw-r--r--
Bug 1736684 - Part 1: Add test coverage for the search-detection built-in add-on (client side). r=rpl This patch adds a new test file containing a test case to cover the two main client side scenarios. Differential Revision: https://phabricator.services.mozilla.com/D129050

/* -*- 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 "builtin/WeakRefObject.h"

#include "mozilla/Maybe.h"

#include "jsapi.h"

#include "js/friend/ErrorMessages.h"  // js::GetErrorMessage, JSMSG_*
#include "vm/GlobalObject.h"
#include "vm/JSContext.h"

#include "gc/PrivateIterators-inl.h"
#include "vm/JSObject-inl.h"
#include "vm/NativeObject-inl.h"

namespace js {

/* static */
bool WeakRefObject::construct(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);

  // https://tc39.es/proposal-weakrefs/#sec-weak-ref-constructor
  // The WeakRef constructor is not intended to be called as a function and will
  // throw an exception when called in that manner.
  if (!ThrowIfNotConstructing(cx, args, "WeakRef")) {
    return false;
  }

  // https://tc39.es/proposal-weakrefs/#sec-weak-ref-target
  // 1. If NewTarget is undefined, throw a TypeError exception.
  // 2. If Type(target) is not Object, throw a TypeError exception.
  if (!args.get(0).isObject()) {
    ReportNotObject(cx, args.get(0));
    return false;
  }

  // 3. Let weakRef be ? OrdinaryCreateFromConstructor(NewTarget,
  //    "%WeakRefPrototype%", « [[Target]] »).
  RootedObject proto(cx);
  if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_WeakRef, &proto)) {
    return false;
  }

  Rooted<WeakRefObject*> weakRef(
      cx, NewObjectWithClassProto<WeakRefObject>(cx, proto));
  if (!weakRef) {
    return false;
  }

  RootedObject target(cx);
  target = CheckedUnwrapDynamic(&args[0].toObject(), cx);
  if (!target) {
    ReportAccessDenied(cx);
    return false;
  }

  // If the target is a DOM wrapper, preserve it.
  if (!preserveDOMWrapper(cx, target)) {
    return false;
  }

  // Wrap the weakRef into the target's compartment.
  RootedObject wrappedWeakRef(cx, weakRef);
  AutoRealm ar(cx, target);
  if (!JS_WrapObject(cx, &wrappedWeakRef)) {
    return false;
  }

  if (JS_IsDeadWrapper(wrappedWeakRef)) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEAD_OBJECT);
    return false;
  }

  // 4. Perfom ! KeepDuringJob(target).
  if (!target->zone()->keepDuringJob(target)) {
    ReportOutOfMemory(cx);
    return false;
  };

  // Add an entry to the per-zone maps from target JS object to a list of weak
  // ref objects.
  gc::GCRuntime* gc = &cx->runtime()->gc;
  if (!gc->registerWeakRef(target, wrappedWeakRef)) {
    ReportOutOfMemory(cx);
    return false;
  };

  // 5. Set weakRef.[[Target]] to target.
  weakRef->setReservedSlotGCThingAsPrivate(TargetSlot, target);

  // 6. Return weakRef.
  args.rval().setObject(*weakRef);
  return true;
}

/* static */
bool WeakRefObject::preserveDOMWrapper(JSContext* cx, HandleObject obj) {
  if (!MaybePreserveDOMWrapper(cx, obj)) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_BAD_WEAKREF_TARGET);
    return false;
  }

  return true;
}

/* static */
void WeakRefObject::trace(JSTracer* trc, JSObject* obj) {
  WeakRefObject* weakRef = &obj->as<WeakRefObject>();

  if (trc->traceWeakEdges()) {
    JSObject* target = weakRef->target();
    if (target) {
      TraceManuallyBarrieredEdge(trc, &target, "WeakRefObject::target");
      weakRef->setTargetUnbarriered(target);
    }
  }
}

/* static */
void WeakRefObject::finalize(JSFreeOp* fop, JSObject* obj) {
  // The target is cleared when the target's zone is swept and that always
  // happens before this object is finalized because of the CCW from the target
  // zone to this object. If the CCW is nuked, the target is cleared in
  // NotifyGCNukeWrapper().
  MOZ_ASSERT(!obj->as<WeakRefObject>().target());
}

const JSClassOps WeakRefObject::classOps_ = {
    nullptr,   // addProperty
    nullptr,   // delProperty
    nullptr,   // enumerate
    nullptr,   // newEnumerate
    nullptr,   // resolve
    nullptr,   // mayResolve
    finalize,  // finalize
    nullptr,   // call
    nullptr,   // hasInstance
    nullptr,   // construct
    trace,     // trace
};

const ClassSpec WeakRefObject::classSpec_ = {
    GenericCreateConstructor<WeakRefObject::construct, 1,
                             gc::AllocKind::FUNCTION>,
    GenericCreatePrototype<WeakRefObject>,
    nullptr,
    nullptr,
    WeakRefObject::methods,
    WeakRefObject::properties,
};

const JSClass WeakRefObject::class_ = {
    "WeakRef",
    JSCLASS_HAS_RESERVED_SLOTS(SlotCount) |
        JSCLASS_HAS_CACHED_PROTO(JSProto_WeakRef) | JSCLASS_FOREGROUND_FINALIZE,
    &classOps_, &classSpec_};

const JSClass WeakRefObject::protoClass_ = {
    // https://tc39.es/proposal-weakrefs/#sec-weak-ref.prototype
    // https://tc39.es/proposal-weakrefs/#sec-properties-of-the-weak-ref-prototype-object
    "WeakRef.prototype", JSCLASS_HAS_CACHED_PROTO(JSProto_WeakRef),
    JS_NULL_CLASS_OPS, &classSpec_};

const JSPropertySpec WeakRefObject::properties[] = {
    JS_STRING_SYM_PS(toStringTag, "WeakRef", JSPROP_READONLY), JS_PS_END};

const JSFunctionSpec WeakRefObject::methods[] = {JS_FN("deref", deref, 0, 0),
                                                 JS_FS_END};

/* static */
bool WeakRefObject::deref(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);

  // https://tc39.es/proposal-weakrefs/#sec-weak-ref.prototype.deref
  // 1. Let weakRef be the this value.
  // 2. If Type(weakRef) is not Object, throw a TypeError exception.
  // 3. If weakRef does not have a [[Target]] internal slot, throw a TypeError
  //    exception.
  if (!args.thisv().isObject() ||
      !args.thisv().toObject().is<WeakRefObject>()) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_NOT_A_WEAK_REF,
                              "Receiver of WeakRef.deref call");
    return false;
  }

  Rooted<WeakRefObject*> weakRef(cx,
                                 &args.thisv().toObject().as<WeakRefObject>());

  // We need to perform a read barrier, which may clear the target.
  readBarrier(cx, weakRef);

  // 4. Let target be the value of weakRef.[[Target]].
  // 5. If target is not empty,
  //    a. Perform ! KeepDuringJob(target).
  //    b. Return target.
  // 6. Return undefined.
  if (!weakRef->target()) {
    args.rval().setUndefined();
    return true;
  }

  RootedObject target(cx, weakRef->target());
  if (!target->zone()->keepDuringJob(target)) {
    return false;
  }

  // Target should be wrapped into the current realm before returning it.
  RootedObject wrappedTarget(cx, target);
  if (!JS_WrapObject(cx, &wrappedTarget)) {
    return false;
  }

  args.rval().setObject(*wrappedTarget);
  return true;
}

void WeakRefObject::setTargetUnbarriered(JSObject* target) {
  setReservedSlotGCThingAsPrivateUnbarriered(TargetSlot, target);
}

void WeakRefObject::clearTarget() {
  clearReservedSlotGCThingAsPrivate(TargetSlot);
}

/* static */
void WeakRefObject::readBarrier(JSContext* cx, Handle<WeakRefObject*> self) {
  RootedObject obj(cx, self->target());
  if (!obj) {
    return;
  }

  if (obj->getClass()->isDOMClass()) {
    // We preserved the target when the WeakRef was created. If it has since
    // been released then the DOM object it wraps has been collected, so clear
    // the target.
    MOZ_ASSERT(cx->runtime()->hasReleasedWrapperCallback);
    bool wasReleased = cx->runtime()->hasReleasedWrapperCallback(obj);
    if (wasReleased) {
      self->clearTarget();
      return;
    }
  }

  gc::ReadBarrier(obj.get());
}

namespace gc {

bool GCRuntime::registerWeakRef(HandleObject target, HandleObject weakRef) {
  MOZ_ASSERT(!IsCrossCompartmentWrapper(target));
  MOZ_ASSERT(UncheckedUnwrap(weakRef)->is<WeakRefObject>());
  MOZ_ASSERT(target->compartment() == weakRef->compartment());

  auto& map = target->zone()->weakRefMap();
  auto ptr = map.lookupForAdd(target);
  if (!ptr && !map.add(ptr, target, WeakRefHeapPtrVector(target->zone()))) {
    return false;
  }

  auto& refs = ptr->value();
  return refs.emplaceBack(weakRef);
}

bool GCRuntime::unregisterWeakRefWrapper(JSObject* wrapper) {
  WeakRefObject* weakRef =
      &UncheckedUnwrapWithoutExpose(wrapper)->as<WeakRefObject>();

  JSObject* target = weakRef->target();
  MOZ_ASSERT(target);

  bool removed = false;
  auto& map = target->zone()->weakRefMap();
  if (auto ptr = map.lookup(target)) {
    ptr->value().eraseIf([wrapper, &removed](JSObject* obj) {
      bool remove = obj == wrapper;
      if (remove) {
        removed = true;
      }
      return remove;
    });
  }

  return removed;
}

void GCRuntime::traceKeptObjects(JSTracer* trc) {
  for (GCZonesIter zone(this); !zone.done(); zone.next()) {
    zone->traceKeptObjects(trc);
  }
}

}  // namespace gc

static WeakRefObject* UnwrapWeakRef(JSObject* obj) {
  MOZ_ASSERT(!JS_IsDeadWrapper(obj));
  obj = UncheckedUnwrapWithoutExpose(obj);
  return &obj->as<WeakRefObject>();
}

void WeakRefMap::traceWeak(JSTracer* trc, gc::StoreBuffer* sbToLock) {
  mozilla::Maybe<typename Base::Enum> e;
  for (e.emplace(*this); !e->empty(); e->popFront()) {
    // If target is dying, clear the target field of all weakRefs, and remove
    // the entry from the map.
    auto result =
        TraceWeakEdge(trc, &e->front().mutableKey(), "WeakRef target");
    if (result.isDead()) {
      for (JSObject* obj : e->front().value()) {
        UnwrapWeakRef(obj)->clearTarget();
      }
      e->removeFront();
    } else {
      // Update the target field after compacting.
      e->front().value().traceWeak(trc, result.finalTarget());
    }
  }

  // Take store buffer lock while the Enum's destructor is called as this can
  // rehash/resize the table and access the store buffer.
  gc::AutoLockStoreBuffer lock(sbToLock);
  e.reset();
}

// Like GCVector::sweep, but this method will also update the target in every
// weakRef in this GCVector.
void WeakRefHeapPtrVector::traceWeak(JSTracer* trc, JSObject* target) {
  HeapPtrObject* src = begin();
  HeapPtrObject* dst = begin();
  while (src != end()) {
    auto result = TraceWeakEdge(trc, src, "WeakRef");
    if (result.isDead()) {
      UnwrapWeakRef(result.initialTarget())->clearTarget();
    } else {
      UnwrapWeakRef(result.finalTarget())->setTargetUnbarriered(target);

      if (src != dst) {
        *dst = std::move(*src);
      }
      dst++;
    }
    src++;
  }

  MOZ_ASSERT(dst <= end());
  shrinkBy(end() - dst);
}

}  // namespace js