current commit
261007.7 · quic: give each BindingData its own allocator state
#include "guard.h"
#ifndef OPENSSL_NO_QUIC
#include <async_wrap-inl.h>
#include <base_object-inl.h>
#include <env-inl.h>
#include <memory_tracker-inl.h>
#include <nghttp3/nghttp3.h>
#include <ngtcp2/ngtcp2.h>
#include <node.h>
#include <node_errors.h>
#include <node_external_reference.h>
#include <node_mem-inl.h>
#include <node_realm-inl.h>
#include <node_sockaddr-inl.h>
#include <v8.h>
#include "application.h"
#include "bindingdata.h"
#include "session.h"
#include "session_manager.h"
#include "streams.h"

namespace node {

using mem::kReserveSizeAndAlign;
using v8::Array;
using v8::DictionaryTemplate;
using v8::Function;
using v8::FunctionTemplate;
using v8::HandleScope;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

namespace quic {

// ============================================================================
// QUIC allocator.
//
// ngtcp2 and nghttp3 keep a pointer to their allocator struct in every object
// they allocate, and nghttp3 rcbufs backing V8 external strings can be freed
// after the BindingData is gone. A QuicAllocState is therefore deleted only
// once its BindingData has been destroyed and its last allocation freed.
//
// The allocation functions use the same prepended-size-header scheme as
// NgLibMemoryManager (node_mem-inl.h) so that frees always know the
// allocation size regardless of whether BindingData is still around.

struct QuicAllocState {
  BindingData* binding;
  size_t live_allocations = 0;
  ngtcp2_mem ngtcp2 = {};
  nghttp3_mem nghttp3 = {};

  void OnFreed() {
    CHECK_GT(live_allocations, 0);
    if (--live_allocations == 0 && binding == nullptr) delete this;
  }
};

namespace {

void* QuicRealloc(void* ptr, size_t size, void* user_data) {
  auto* state = static_cast<QuicAllocState*>(user_data);

  size_t previous_size = 0;
  char* original_ptr = nullptr;

  if (size > 0) size += kReserveSizeAndAlign;

  if (ptr != nullptr) {
    original_ptr = static_cast<char*>(ptr) - kReserveSizeAndAlign;
    previous_size = *reinterpret_cast<size_t*>(original_ptr);
    if (previous_size == 0) {
      char* ret = UncheckedRealloc(original_ptr, size);
      if (size == 0) {
        state->OnFreed();
        return nullptr;
      }
      if (ret != nullptr) ret += kReserveSizeAndAlign;
      return ret;
    }
  }

  if (state->binding) {
    state->binding->CheckAllocatedSize(previous_size);
  }

  char* mem = UncheckedRealloc(original_ptr, size);

  if (mem != nullptr) {
    const int64_t new_size = size - previous_size;
    if (state->binding) {
      state->binding->IncreaseAllocatedSize(new_size);
      state->binding->env()->external_memory_accounter()->Update(
          state->binding->env()->isolate(), new_size);
    }
    if (ptr == nullptr) state->live_allocations++;
    *reinterpret_cast<size_t*>(mem) = size;
    mem += kReserveSizeAndAlign;
  } else if (size == 0) {
    if (state->binding) {
      state->binding->DecreaseAllocatedSize(previous_size);
      state->binding->env()->external_memory_accounter()->Decrease(
          state->binding->env()->isolate(), previous_size);
    }
    if (ptr != nullptr) state->OnFreed();
  }
  return mem;
}

void* QuicMalloc(size_t size, void* user_data) {
  return QuicRealloc(nullptr, size, user_data);
}

void QuicFree(void* ptr, void* user_data) {
  if (ptr == nullptr) return;
  CHECK_NULL(QuicRealloc(ptr, 0, user_data));
}

void* QuicCalloc(size_t nmemb, size_t size, void* user_data) {
  size_t real_size = MultiplyWithOverflowCheck(nmemb, size);
  void* mem = QuicMalloc(real_size, user_data);
  if (mem != nullptr) memset(mem, 0, real_size);
  return mem;
}

// Thin wrappers with the correct function-pointer types for each
// library. The signatures happen to be identical today, but keeping
// them separate avoids ABI coupling between ngtcp2 and nghttp3.

void* Ngtcp2Malloc(size_t size, void* ud) {
  return QuicMalloc(size, ud);
}
void Ngtcp2Free(void* ptr, void* ud) {
  QuicFree(ptr, ud);
}
void* Ngtcp2Calloc(size_t n, size_t s, void* ud) {
  return QuicCalloc(n, s, ud);
}
void* Ngtcp2Realloc(void* ptr, size_t size, void* ud) {
  return QuicRealloc(ptr, size, ud);
}

void* Nghttp3Malloc(size_t size, void* ud) {
  return QuicMalloc(size, ud);
}
void Nghttp3Free(void* ptr, void* ud) {
  QuicFree(ptr, ud);
}
void* Nghttp3Calloc(size_t n, size_t s, void* ud) {
  return QuicCalloc(n, s, ud);
}
void* Nghttp3Realloc(void* ptr, size_t size, void* ud) {
  return QuicRealloc(ptr, size, ud);
}
}  // namespace

// ============================================================================
// CheckWrap / CheckWrapHandle

void CheckWrap::Start() {
  if (check_.data == nullptr) return;
  uv_check_start(&check_, OnCheck);
}

void CheckWrap::Stop() {
  if (check_.data == nullptr) return;
  uv_check_stop(&check_);
}

void CheckWrap::Close() {
  check_.data = nullptr;
  env_->CloseHandle(reinterpret_cast<uv_handle_t*>(&check_), CheckClosedCb);
}

void CheckWrap::Ref() {
  if (check_.data == nullptr) return;
  uv_ref(reinterpret_cast<uv_handle_t*>(&check_));
}

void CheckWrap::Unref() {
  if (check_.data == nullptr) return;
  uv_unref(reinterpret_cast<uv_handle_t*>(&check_));
}

void CheckWrap::OnCheck(uv_check_t* check) {
  CheckWrap* wrap = ContainerOf(&CheckWrap::check_, check);
  wrap->fn_();
}

void CheckWrap::CheckClosedCb(uv_handle_t* handle) {
  std::unique_ptr<CheckWrap> ptr(
      ContainerOf(&CheckWrap::check_, reinterpret_cast<uv_check_t*>(handle)));
}

void CheckWrapHandle::Start() {
  if (check_ != nullptr) check_->Start();
}

void CheckWrapHandle::Stop() {
  if (check_ != nullptr) check_->Stop();
}

void CheckWrapHandle::Close() {
  if (check_ != nullptr) {
    check_->env()->RemoveCleanupHook(CleanupHook, this);
    check_->Close();
  }
  check_ = nullptr;
}

void CheckWrapHandle::Ref() {
  if (check_ != nullptr) check_->Ref();
}

void CheckWrapHandle::Unref() {
  if (check_ != nullptr) check_->Unref();
}

void CheckWrapHandle::MemoryInfo(MemoryTracker* tracker) const {
  if (check_ != nullptr) tracker->TrackField("check", *check_);
}

void CheckWrapHandle::CleanupHook(void* data) {
  static_cast<CheckWrapHandle*>(data)->Close();
}

// ============================================================================

BindingData& BindingData::Get(Environment* env) {
  return *(env->principal_realm()->GetBindingData<BindingData>());
}

BindingData::~BindingData() {
  alloc_state_->binding = nullptr;
  if (alloc_state_->live_allocations == 0) delete alloc_state_;
  // flush_check_ is cleaned up by ~CheckWrapHandle() after the destructor
  // body completes. The inner CheckWrap (and its uv_check_t) will be freed
  // later by the uv_close callback, after CleanupHandles() runs uv_run().
  pending_flush_sessions_.clear();
}

ngtcp2_mem* BindingData::ngtcp2_allocator() {
  return &alloc_state_->ngtcp2;
}

nghttp3_mem* BindingData::nghttp3_allocator() {
  return &alloc_state_->nghttp3;
}

void BindingData::CheckAllocatedSize(size_t previous_size) const {
  CHECK_GE(current_ngtcp2_memory_, previous_size);
}

void BindingData::IncreaseAllocatedSize(size_t size) {
  CHECK_GE(current_ngtcp2_memory_ + size, current_ngtcp2_memory_);
  current_ngtcp2_memory_ += size;
}

void BindingData::DecreaseAllocatedSize(size_t size) {
  CHECK_LE(current_ngtcp2_memory_ - size, current_ngtcp2_memory_);
  current_ngtcp2_memory_ -= size;
}

// Forwards detailed(verbose) debugging information from nghttp3. Enabled using
// the NODE_DEBUG_NATIVE=NGHTTP3 category.
void nghttp3_debug_log(const char* fmt, va_list args) {
  auto isolate = Isolate::GetCurrent();
  if (isolate == nullptr) return;
  auto env = Environment::GetCurrent(isolate);
  if (env->enabled_debug_list()->enabled(DebugCategory::NGHTTP3)) {
    fprintf(stderr, "nghttp3 ");
    vfprintf(stderr, fmt, args);
  }
}

void BindingData::InitPerContext(Realm* realm, Local<Object> target) {
  nghttp3_set_debug_vprintf_callback(nghttp3_debug_log);
  SetMethod(realm->context(), target, "setCallbacks", SetCallbacks);
  SetMethod(realm->context(), target, "sendHeaders", SendHeaders);
  SetMethod(realm->context(), target, "setHeadersInterest", SetHeadersInterest);

  constexpr int QUIC_STREAM_HEADERS_KIND_HINTS =
      static_cast<uint8_t>(HeadersKind::HINTS);
  constexpr int QUIC_STREAM_HEADERS_KIND_INITIAL =
      static_cast<uint8_t>(HeadersKind::INITIAL);
  constexpr int QUIC_STREAM_HEADERS_KIND_TRAILING =
      static_cast<uint8_t>(HeadersKind::TRAILING);
  constexpr int QUIC_STREAM_HEADERS_FLAGS_NONE =
      static_cast<uint8_t>(HeadersFlags::NONE);
  constexpr int QUIC_STREAM_HEADERS_FLAGS_TERMINAL =
      static_cast<uint8_t>(HeadersFlags::TERMINAL);

  NODE_DEFINE_CONSTANT(target, QUIC_STREAM_HEADERS_KIND_HINTS);
  NODE_DEFINE_CONSTANT(target, QUIC_STREAM_HEADERS_KIND_INITIAL);
  NODE_DEFINE_CONSTANT(target, QUIC_STREAM_HEADERS_KIND_TRAILING);
  NODE_DEFINE_CONSTANT(target, QUIC_STREAM_HEADERS_FLAGS_NONE);
  NODE_DEFINE_CONSTANT(target, QUIC_STREAM_HEADERS_FLAGS_TERMINAL);

  Realm::GetCurrent(realm->context())->AddBindingData<BindingData>(target);
}

void BindingData::RegisterExternalReferences(
    ExternalReferenceRegistry* registry) {
  registry->Register(IllegalConstructor);
  registry->Register(SetCallbacks);
  registry->Register(SendHeaders);
  registry->Register(SetHeadersInterest);
}

JS_METHOD_IMPL(BindingData::SendHeaders) {
  Stream* stream;
  ASSIGN_OR_RETURN_UNWRAP(&stream, args[0]);
  CHECK(args[1]->IsUint32());  // Kind
  CHECK(args[2]->IsArray());   // Headers
  CHECK(args[3]->IsUint32());  // Flags

  HeadersKind kind = FromV8Value<HeadersKind>(args[1]);
  Local<Array> headers = args[2].As<Array>();
  HeadersFlags flags = FromV8Value<HeadersFlags>(args[3]);

  args.GetReturnValue().Set(stream->session().application().SendHeaders(
      *stream, kind, headers, flags));
}

JS_METHOD_IMPL(BindingData::SetHeadersInterest) {
  Stream* stream;
  ASSIGN_OR_RETURN_UNWRAP(&stream, args[0]);
  CHECK(args[1]->IsBoolean());
  CHECK(args[2]->IsBoolean());
  stream->session().application().SetHeadersInterest(
      *stream, args[1]->IsTrue(), args[2]->IsTrue());
}

BindingData::BindingData(Realm* realm, Local<Object> object)
    : BaseObject(realm, object),
      alloc_state_(new QuicAllocState{this}),
      flush_check_(env(), [this]() { OnFlushCheck(); }) {
  alloc_state_->ngtcp2 = {
      alloc_state_, Ngtcp2Malloc, Ngtcp2Free, Ngtcp2Calloc, Ngtcp2Realloc};
  alloc_state_->nghttp3 = {
      alloc_state_, Nghttp3Malloc, Nghttp3Free, Nghttp3Calloc, Nghttp3Realloc};
  MakeWeak();
  // Unref so the check handle doesn't keep the event loop alive on its own.
  flush_check_.Unref();
  // Ensure Clean() below is called before the tearing anything down.
  env()->cleanable_queue()->PushFront(this);
}

void BindingData::Clean() {
  // Make sure sessions are always properly destroyed. This does nothing in
  // a clean shutdown, but is required for cases like worker.terminate().
  if (session_manager_) session_manager_->DestroyAllSessions();
}

SessionManager& BindingData::session_manager() {
  if (!session_manager_) {
    session_manager_ = std::make_unique<SessionManager>();
  }
  return *session_manager_;
}

void BindingData::ScheduleSessionFlush(const BaseObjectPtr<Session>& session) {
  pending_flush_sessions_.push_back(session);
  if (!flush_check_started_) {
    flush_check_.Start();
    flush_check_started_ = true;
  }
}

void BindingData::OnFlushCheck() {
  if (pending_flush_sessions_.empty()) {
    flush_check_.Stop();
    flush_check_started_ = false;
    return;
  }

  HandleScope scope(env()->isolate());

  // Swap to a local vector before iterating. SendPendingData may trigger
  // MakeCallback which runs JS that could cause more packet receives via
  // re-entry (e.g., a stream data callback that synchronously writes to
  // another session). Any sessions added during the flush remain in
  // pending_flush_sessions_ and are picked up on the next check tick.
  auto sessions = std::move(pending_flush_sessions_);
  for (auto& session : sessions) {
    session->flags_.pending_flush = false;
    if (!session->is_destroyed()) {
      session->FlushPendingData();
    }
  }

  // If no new sessions were added during the flush, stop the check
  // to avoid per-tick callback overhead when idle.
  if (pending_flush_sessions_.empty()) {
    flush_check_.Stop();
    flush_check_started_ = false;
  }
}

void BindingData::MemoryInfo(MemoryTracker* tracker) const {
#define V(name, _) tracker->TrackField(#name, name##_callback());

  QUIC_JS_CALLBACKS(V)

#undef V

#define V(name, _) tracker->TrackField(#name, name##_string());

  QUIC_STRINGS(V)

#undef V
}

#define V(name)                                                                \
  void BindingData::set_##name##_constructor_template(                         \
      Local<FunctionTemplate> tmpl) {                                          \
    name##_constructor_template_.Reset(env()->isolate(), tmpl);                \
  }                                                                            \
  Local<FunctionTemplate> BindingData::name##_constructor_template() const {   \
    return PersistentToLocal::Default(env()->isolate(),                        \
                                      name##_constructor_template_);           \
  }

QUIC_CONSTRUCTORS(V)

#undef V

void BindingData::set_transport_params_template(
    Local<DictionaryTemplate> tmpl) {
  transport_params_template_.Reset(env()->isolate(), tmpl);
}

Local<DictionaryTemplate> BindingData::transport_params_template() const {
  return PersistentToLocal::Default(env()->isolate(),
                                    transport_params_template_);
}

void BindingData::set_application_options_template(
    Local<DictionaryTemplate> tmpl) {
  application_options_template_.Reset(env()->isolate(), tmpl);
}

Local<DictionaryTemplate> BindingData::application_options_template() const {
  return PersistentToLocal::Default(env()->isolate(),
                                    application_options_template_);
}

#define V(name, _)                                                             \
  void BindingData::set_##name##_callback(Local<Function> fn) {                \
    name##_callback_.Reset(env()->isolate(), fn);                              \
  }                                                                            \
  Local<Function> BindingData::name##_callback() const {                       \
    return PersistentToLocal::Default(env()->isolate(), name##_callback_);     \
  }

QUIC_JS_CALLBACKS(V)

#undef V

#define V(name, value)                                                         \
  Local<String> BindingData::name##_string() const {                           \
    if (name##_string_.IsEmpty())                                              \
      name##_string_.Set(env()->isolate(),                                     \
                         OneByteString(env()->isolate(), value));              \
    return name##_string_.Get(env()->isolate());                               \
  }

QUIC_STRINGS(V)

#undef V

#define V(name, value)                                                         \
  Local<String> BindingData::on_##name##_string() const {                      \
    if (on_##name##_string_.IsEmpty())                                         \
      on_##name##_string_.Set(                                                 \
          env()->isolate(),                                                    \
          FIXED_ONE_BYTE_STRING(env()->isolate(), "on" #value));               \
    return on_##name##_string_.Get(env()->isolate());                          \
  }

QUIC_JS_CALLBACKS(V)

#undef V

Local<String> BindingData::error_name_string(const char* name) {
  auto& slot = error_name_strings_[name];
  if (slot.IsEmpty()) {
    slot.Set(env()->isolate(), OneByteString(env()->isolate(), name));
  }
  return slot.Get(env()->isolate());
}

JS_METHOD_IMPL(BindingData::SetCallbacks) {
  auto env = Environment::GetCurrent(args);
  auto isolate = env->isolate();
  auto& state = Get(env);
  CHECK(args[0]->IsObject());
  Local<Object> obj = args[0].As<Object>();

#define V(name, key)                                                           \
  do {                                                                         \
    Local<Value> val;                                                          \
    if (!obj->Get(env->context(), state.on_##name##_string()).ToLocal(&val) || \
        !val->IsFunction()) {                                                  \
      return THROW_ERR_MISSING_ARGS(isolate, "Missing Callback: on" #key);     \
    }                                                                          \
    state.set_##name##_callback(val.As<Function>());                           \
  } while (0);

  QUIC_JS_CALLBACKS(V)

#undef V
}

NgTcp2CallbackScope::NgTcp2CallbackScope(Session* session) : session(session) {
  CHECK(!session->flags_.in_ngtcp2_callback_scope);
  session->flags_.in_ngtcp2_callback_scope = true;
}

NgTcp2CallbackScope::~NgTcp2CallbackScope() {
  session->flags_.in_ngtcp2_callback_scope = false;
  if (session->flags_.destroy_deferred) {
    session->flags_.destroy_deferred = false;
    session->Destroy();
  }
}

NgHttp3CallbackScope::NgHttp3CallbackScope(Session* session)
    : session(session) {
  CHECK(!session->flags_.in_nghttp3_callback_scope);
  session->flags_.in_nghttp3_callback_scope = true;
}

NgHttp3CallbackScope::~NgHttp3CallbackScope() {
  session->flags_.in_nghttp3_callback_scope = false;
  if (session->flags_.destroy_deferred) {
    session->flags_.destroy_deferred = false;
    session->Destroy();
  }
}

CallbackScopeBase::CallbackScopeBase(Environment* env)
    : env(env), context_scope(env->context()), try_catch(env->isolate()) {}

CallbackScopeBase::~CallbackScopeBase() {
  if (try_catch.HasCaught()) {
    if (!try_catch.HasTerminated() && env->can_call_into_js()) {
      errors::TriggerUncaughtException(env->isolate(), try_catch);
    } else {
      try_catch.ReThrow();
    }
  }
}

JS_METHOD_IMPL(IllegalConstructor) {
  THROW_ERR_ILLEGAL_CONSTRUCTOR(Environment::GetCurrent(args));
}

}  // namespace quic
}  // namespace node

#endif  // OPENSSL_NO_QUIC