#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* workaround C Ruby <= 2.x problems w/ clang in C++ mode */ #if defined(ENGINE_IS_CRUBY) && \ RUBY_API_VERSION_MAJOR == 2 && RUBY_API_VERSION_MINOR <= 6 # define MR_METHOD_FUNC(fn) RUBY_METHOD_FUNC(fn) #else # define MR_METHOD_FUNC(fn) fn #endif using namespace v8; typedef struct { const char* data; int raw_size; } SnapshotInfo; class IsolateInfo { public: Isolate* isolate; ArrayBuffer::Allocator* allocator; StartupData* startup_data; bool interrupted; bool added_gc_cb; pid_t pid; VALUE mutex; class Lock { VALUE &mutex; public: Lock(VALUE &mutex) : mutex(mutex) { rb_mutex_lock(mutex); } ~Lock() { rb_mutex_unlock(mutex); } }; IsolateInfo() : isolate(nullptr), allocator(nullptr), startup_data(nullptr), interrupted(false), added_gc_cb(false), pid(getpid()), refs_count(0) { VALUE cMutex = rb_const_get(rb_cThread, rb_intern("Mutex")); mutex = rb_class_new_instance(0, nullptr, cMutex); } ~IsolateInfo(); void init(SnapshotInfo* snapshot_info = nullptr); void mark() { rb_gc_mark(mutex); } Lock createLock() { Lock lock(mutex); return lock; } void hold() { refs_count++; } void release() { if (--refs_count <= 0) { delete this; } } int refs() { return refs_count; } static void* operator new(size_t size) { return ruby_xmalloc(size); } static void operator delete(void *block) { xfree(block); } private: // how many references to this isolate exist // we can't rely on Ruby's GC for this, because Ruby could destroy the // isolate before destroying the contexts that depend on them. We'd need to // keep a list of linked contexts in the isolate to destroy those first when // isolate destruction was requested. Keeping such a list would require // notification from the context VALUEs when they are constructed and // destroyed. With a ref count, those notifications are still needed, but // we keep a simple int rather than a list of pointers. std::atomic_int refs_count; }; typedef struct { IsolateInfo* isolate_info; Persistent* context; } ContextInfo; typedef struct { bool parsed; bool executed; bool terminated; bool json; Persistent* value; Persistent* message; Persistent* backtrace; } EvalResult; typedef struct { ContextInfo* context_info; Local* eval; Local* filename; useconds_t timeout; EvalResult* result; size_t max_memory; size_t marshal_stackdepth; } EvalParams; typedef struct { ContextInfo *context_info; char *function_name; int argc; bool error; Local fun; Local *argv; EvalResult result; size_t max_memory; size_t marshal_stackdepth; } FunctionCall; class IsolateData { public: enum Flag { // first flags are bitfield // max count: sizeof(uintptr_t) * 8 IN_GVL, // whether we are inside of ruby gvl or not DO_TERMINATE, // terminate as soon as possible MEM_SOFTLIMIT_REACHED, // we've hit the memory soft limit MEM_SOFTLIMIT_MAX, // maximum memory value MARSHAL_STACKDEPTH_REACHED, // we've hit our max stack depth MARSHAL_STACKDEPTH_VALUE, // current stackdepth MARSHAL_STACKDEPTH_MAX, // maximum stack depth during marshal }; static void Init(Isolate *isolate) { // zero out all fields in the bitfield isolate->SetData(0, 0); } static uintptr_t Get(Isolate *isolate, Flag flag) { Bitfield u = { reinterpret_cast(isolate->GetData(0)) }; switch (flag) { case IN_GVL: return u.IN_GVL; case DO_TERMINATE: return u.DO_TERMINATE; case MEM_SOFTLIMIT_REACHED: return u.MEM_SOFTLIMIT_REACHED; case MEM_SOFTLIMIT_MAX: return static_cast(u.MEM_SOFTLIMIT_MAX) << 10; case MARSHAL_STACKDEPTH_REACHED: return u.MARSHAL_STACKDEPTH_REACHED; case MARSHAL_STACKDEPTH_VALUE: return u.MARSHAL_STACKDEPTH_VALUE; case MARSHAL_STACKDEPTH_MAX: return u.MARSHAL_STACKDEPTH_MAX; } // avoid compiler warning return u.IN_GVL; } static void Set(Isolate *isolate, Flag flag, uintptr_t value) { Bitfield u = { reinterpret_cast(isolate->GetData(0)) }; switch (flag) { case IN_GVL: u.IN_GVL = value; break; case DO_TERMINATE: u.DO_TERMINATE = value; break; case MEM_SOFTLIMIT_REACHED: u.MEM_SOFTLIMIT_REACHED = value; break; // drop least significant 10 bits 'store memory amount in kb' case MEM_SOFTLIMIT_MAX: u.MEM_SOFTLIMIT_MAX = value >> 10; break; case MARSHAL_STACKDEPTH_REACHED: u.MARSHAL_STACKDEPTH_REACHED = value; break; case MARSHAL_STACKDEPTH_VALUE: u.MARSHAL_STACKDEPTH_VALUE = value; break; case MARSHAL_STACKDEPTH_MAX: u.MARSHAL_STACKDEPTH_MAX = value; break; } isolate->SetData(0, reinterpret_cast(u.dataPtr)); } private: struct Bitfield { // WARNING: this would explode on platforms below 64 bit ptrs // compiler will fail here, making it clear for them. // Additionally, using the other part of the union to reinterpret the // memory is undefined behavior according to spec, but is / has been stable // across major compilers for decades. static_assert(sizeof(uintptr_t) >= sizeof(uint64_t), "mini_racer not supported on this platform. ptr size must be at least 64 bit."); union { uint64_t dataPtr: 64; // order in this struct matters. For cpu performance keep larger subobjects // aligned on their boundaries (8 16 32), try not to straddle struct { size_t MEM_SOFTLIMIT_MAX:22; bool IN_GVL:1; bool DO_TERMINATE:1; bool MEM_SOFTLIMIT_REACHED:1; bool MARSHAL_STACKDEPTH_REACHED:1; uint8_t :0; // align to next 8bit bound size_t MARSHAL_STACKDEPTH_VALUE:10; uint8_t :0; // align to next 8bit bound size_t MARSHAL_STACKDEPTH_MAX:10; }; }; }; }; struct StackCounter { static void Reset(Isolate* isolate) { if (IsolateData::Get(isolate, IsolateData::MARSHAL_STACKDEPTH_MAX) > 0) { IsolateData::Set(isolate, IsolateData::MARSHAL_STACKDEPTH_VALUE, 0); IsolateData::Set(isolate, IsolateData::MARSHAL_STACKDEPTH_REACHED, false); } } static void SetMax(Isolate* isolate, size_t marshalMaxStackDepth) { if (marshalMaxStackDepth > 0) { IsolateData::Set(isolate, IsolateData::MARSHAL_STACKDEPTH_MAX, marshalMaxStackDepth); IsolateData::Set(isolate, IsolateData::MARSHAL_STACKDEPTH_VALUE, 0); IsolateData::Set(isolate, IsolateData::MARSHAL_STACKDEPTH_REACHED, false); } } StackCounter(Isolate* isolate) { this->isActive = IsolateData::Get(isolate, IsolateData::MARSHAL_STACKDEPTH_MAX) > 0; if (this->isActive) { this->isolate = isolate; this->IncDepth(1); } } bool IsTooDeep() { if (!this->IsActive()) { return false; } size_t depth = IsolateData::Get(this->isolate, IsolateData::MARSHAL_STACKDEPTH_VALUE); size_t maxDepth = IsolateData::Get(this->isolate, IsolateData::MARSHAL_STACKDEPTH_MAX); if (depth > maxDepth) { IsolateData::Set(this->isolate, IsolateData::MARSHAL_STACKDEPTH_REACHED, true); return true; } return false; } bool IsActive() { return this->isActive && !IsolateData::Get(this->isolate, IsolateData::DO_TERMINATE); } ~StackCounter() { if (this->IsActive()) { this->IncDepth(-1); } } private: Isolate* isolate; bool isActive; void IncDepth(int direction) { int inc = direction > 0 ? 1 : -1; size_t depth = IsolateData::Get(this->isolate, IsolateData::MARSHAL_STACKDEPTH_VALUE); // don't decrement past 0 if (inc > 0 || depth > 0) { depth += inc; } IsolateData::Set(this->isolate, IsolateData::MARSHAL_STACKDEPTH_VALUE, depth); } }; static VALUE rb_cContext; static VALUE rb_cSnapshot; static VALUE rb_cIsolate; static VALUE rb_eScriptTerminatedError; static VALUE rb_eV8OutOfMemoryError; static VALUE rb_eParseError; static VALUE rb_eScriptRuntimeError; static VALUE rb_cJavaScriptFunction; static VALUE rb_eSnapshotError; static VALUE rb_ePlatformAlreadyInitializedError; static VALUE rb_mJSON; static VALUE rb_cFailedV8Conversion; static VALUE rb_cDateTime = Qnil; static std::unique_ptr current_platform = NULL; static std::mutex platform_lock; static pthread_attr_t *thread_attr_p; static std::atomic_int ruby_exiting(0); static bool single_threaded = false; static void mark_context(void *); static void deallocate(void *); static size_t context_memsize(const void *); static const rb_data_type_t context_type = { "mini_racer/context_info", { mark_context, deallocate, context_memsize } }; static void deallocate_snapshot(void *); static size_t snapshot_memsize(const void *); static const rb_data_type_t snapshot_type = { "mini_racer/snapshot_info", { NULL, deallocate_snapshot, snapshot_memsize } }; static void mark_isolate(void *); static void deallocate_isolate(void *); static size_t isolate_memsize(const void *); static const rb_data_type_t isolate_type = { "mini_racer/isolate_info", { mark_isolate, deallocate_isolate, isolate_memsize } }; static VALUE rb_platform_set_flag_as_str(VALUE _klass, VALUE flag_as_str) { bool platform_already_initialized = false; Check_Type(flag_as_str, T_STRING); platform_lock.lock(); if (current_platform == NULL) { if (!strcmp(RSTRING_PTR(flag_as_str), "--single_threaded")) { single_threaded = true; } V8::SetFlagsFromString(RSTRING_PTR(flag_as_str), RSTRING_LENINT(flag_as_str)); } else { platform_already_initialized = true; } platform_lock.unlock(); // important to raise outside of the lock if (platform_already_initialized) { rb_raise(rb_ePlatformAlreadyInitializedError, "The V8 platform is already initialized"); } return Qnil; } static void init_v8() { // no need to wait for the lock if already initialized if (current_platform != NULL) return; platform_lock.lock(); if (current_platform == NULL) { V8::InitializeICU(); if (single_threaded) { current_platform = platform::NewSingleThreadedDefaultPlatform(); } else { current_platform = platform::NewDefaultPlatform(); } V8::InitializePlatform(current_platform.get()); V8::Initialize(); } platform_lock.unlock(); } static void gc_callback(Isolate *isolate, GCType type, GCCallbackFlags flags) { if (IsolateData::Get(isolate, IsolateData::MEM_SOFTLIMIT_REACHED)) { return; } size_t softlimit = IsolateData::Get(isolate, IsolateData::MEM_SOFTLIMIT_MAX); HeapStatistics stats; isolate->GetHeapStatistics(&stats); size_t used = stats.used_heap_size(); if(used > softlimit) { IsolateData::Set(isolate, IsolateData::MEM_SOFTLIMIT_REACHED, true); isolate->TerminateExecution(); } } // to be called with active lock and scope static void prepare_result(MaybeLocal v8res, TryCatch& trycatch, Isolate* isolate, Local context, EvalResult& evalRes /* out */) { // just don't touch .parsed evalRes.terminated = false; evalRes.json = false; evalRes.value = nullptr; evalRes.message = nullptr; evalRes.backtrace = nullptr; evalRes.executed = !v8res.IsEmpty(); if (evalRes.executed) { // arrays and objects get converted to json Local local_value = v8res.ToLocalChecked(); if ((local_value->IsObject() || local_value->IsArray()) && !local_value->IsDate() && !local_value->IsFunction()) { MaybeLocal ml = context->Global()->Get( context, String::NewFromUtf8Literal(isolate, "JSON")); if (ml.IsEmpty()) { // exception evalRes.executed = false; } else { Local JSON = ml.ToLocalChecked().As(); Local stringify = JSON->Get( context, v8::String::NewFromUtf8Literal(isolate, "stringify")) .ToLocalChecked().As(); Local object = local_value->ToObject(context).ToLocalChecked(); const unsigned argc = 1; Local argv[argc] = { object }; MaybeLocal maybe_json = stringify->Call(context, JSON, argc, argv); Local json; if (!maybe_json.ToLocal(&json)) { evalRes.executed = false; } else { // JSON.stringify() returns undefined for inputs that // are exotic objects, like WASM function or string refs evalRes.json = !json->IsUndefined(); Persistent* persistent = new Persistent(); persistent->Reset(isolate, json); evalRes.value = persistent; } } } else { Persistent* persistent = new Persistent(); persistent->Reset(isolate, local_value); evalRes.value = persistent; } } if (!evalRes.executed || !evalRes.parsed) { if (trycatch.HasCaught()) { if (!trycatch.Exception()->IsNull()) { evalRes.message = new Persistent(); Local message = trycatch.Message(); char buf[1000]; int len, line, column; if (!message->GetLineNumber(context).To(&line)) { line = 0; } if (!message->GetStartColumn(context).To(&column)) { column = 0; } len = snprintf(buf, sizeof(buf), "%s at %s:%i:%i", *String::Utf8Value(isolate, message->Get()), *String::Utf8Value(isolate, message->GetScriptResourceName()->ToString(context).ToLocalChecked()), line, column); if ((size_t) len >= sizeof(buf)) { len = sizeof(buf) - 1; buf[len] = '\0'; } Local v8_message = String::NewFromUtf8(isolate, buf, NewStringType::kNormal, len).ToLocalChecked(); evalRes.message->Reset(isolate, v8_message); } else if(trycatch.HasTerminated()) { evalRes.terminated = true; evalRes.message = new Persistent(); Local tmp = String::NewFromUtf8Literal(isolate, "JavaScript was terminated (either by timeout or explicitly)"); evalRes.message->Reset(isolate, tmp); } if (!trycatch.StackTrace(context).IsEmpty()) { evalRes.backtrace = new Persistent(); evalRes.backtrace->Reset(isolate, trycatch.StackTrace(context).ToLocalChecked()->ToString(context).ToLocalChecked()); } } } } static void* nogvl_context_eval(void* arg) { EvalParams* eval_params = (EvalParams*)arg; EvalResult* result = eval_params->result; IsolateInfo* isolate_info = eval_params->context_info->isolate_info; Isolate* isolate = isolate_info->isolate; Isolate::Scope isolate_scope(isolate); HandleScope handle_scope(isolate); TryCatch trycatch(isolate); Local context = eval_params->context_info->context->Get(isolate); Context::Scope context_scope(context); v8::ScriptOrigin *origin = NULL; IsolateData::Init(isolate); if (eval_params->max_memory > 0) { IsolateData::Set(isolate, IsolateData::MEM_SOFTLIMIT_MAX, eval_params->max_memory); if (!isolate_info->added_gc_cb) { isolate->AddGCEpilogueCallback(gc_callback); isolate_info->added_gc_cb = true; } } MaybeLocal