| // Copyright 2016 Google Inc. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| #include "cobalt/script/mozjs-45/mozjs_global_environment.h" |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "base/lazy_instance.h" |
| #include "base/stringprintf.h" |
| #include "cobalt/base/polymorphic_downcast.h" |
| #include "cobalt/script/mozjs-45/conversion_helpers.h" |
| #include "cobalt/script/mozjs-45/embedded_resources.h" |
| #include "cobalt/script/mozjs-45/mozjs_exception_state.h" |
| #include "cobalt/script/mozjs-45/mozjs_script_value_factory.h" |
| #include "cobalt/script/mozjs-45/mozjs_source_code.h" |
| #include "cobalt/script/mozjs-45/mozjs_wrapper_handle.h" |
| #include "cobalt/script/mozjs-45/proxy_handler.h" |
| #include "cobalt/script/mozjs-45/referenced_object_map.h" |
| #include "cobalt/script/mozjs-45/util/exception_helpers.h" |
| #include "nb/memory_scope.h" |
| #include "third_party/mozjs-45/js/public/Initialization.h" |
| #include "third_party/mozjs-45/js/src/jsfriendapi.h" |
| #include "third_party/mozjs-45/js/src/jsfun.h" |
| #include "third_party/mozjs-45/js/src/jsobj.h" |
| |
| // A note on CompartmentOptions and Principals. |
| // These concepts are an integral part of Gecko's security implementation, but |
| // we don't need to be concerned with this for Cobalt. |
| // |
| // Compartments are separate regions of memory. Each global object has a |
| // separate compartment. Since Cobalt will use a different JSRuntime for each |
| // thread and each global object cannot access another directly anyways, this |
| // shouldn't be an issue even in our development workflow. |
| // If we support multiple windows in some form, a cross-compartment wrapper is |
| // needed to access another global object. If they are same-origin this is |
| // simpler. |
| // Principals (or Security principals) are used to determine the security |
| // relationship between two compartments. Since Cobalt does not have multiple |
| // global objects there is no need to define Security principals. Even so, |
| // it we'd likely want to implement such a security policy in an engine |
| // independent way anyways. |
| // See https://developer.mozilla.org/en-US/docs/Mozilla/Gecko/Script_security |
| |
| namespace cobalt { |
| namespace script { |
| namespace mozjs { |
| namespace { |
| |
| // Class definition for global object with no bindings. |
| JSClass simple_global_class = {"global", JSCLASS_GLOBAL_FLAGS, |
| NULL, NULL, |
| NULL, NULL, |
| NULL, NULL, |
| NULL, NULL, |
| NULL, NULL, |
| NULL, JS_GlobalObjectTraceHook}; |
| |
| // 8192 is the recommended default value. |
| const uint32_t kStackChunkSize = 8192; |
| |
| // This is the default in the spidermonkey shell. |
| const uint32_t kMaxCodeCacheBytes = 16 * 1024 * 1024; |
| |
| // DOM proxies have an extra slot for the expando object at index |
| // kJSProxySlotExpando. |
| // The expando object is a plain JSObject whose properties correspond to |
| // "expandos" (custom properties set by the script author). |
| // The exact value stored in the kJSProxySlotExpando slot depends on whether |
| // the interface is annotated with the [OverrideBuiltins] extended attribute. |
| const uint32_t kJSProxySlotExpando = 0; |
| |
| // The DOMProxyShadowsCheck function will be called to check if the property for |
| // id should be gotten from the prototype, or if there is an own property that |
| // shadows it. |
| js::DOMProxyShadowsResult DOMProxyShadowsCheck(JSContext* context, |
| JS::HandleObject proxy, |
| JS::HandleId id) { |
| DCHECK(IsProxy(proxy)); |
| JS::Value value = js::GetProxyExtra(proxy, kJSProxySlotExpando); |
| DCHECK(value.isUndefined() || value.isObject()); |
| |
| // [OverrideBuiltins] extended attribute is not supported. |
| NOTIMPLEMENTED(); |
| |
| // If DoesntShadow is returned then the slot at listBaseExpandoSlot should |
| // either be undefined or point to an expando object that would contain the |
| // own property. |
| return js::DoesntShadow; |
| } |
| |
| class MozjsStubHandler : public ProxyHandler { |
| public: |
| MozjsStubHandler() |
| : ProxyHandler(indexed_property_hooks, named_property_hooks) {} |
| |
| private: |
| static NamedPropertyHooks named_property_hooks; |
| static IndexedPropertyHooks indexed_property_hooks; |
| }; |
| |
| ProxyHandler::NamedPropertyHooks MozjsStubHandler::named_property_hooks = { |
| NULL, NULL, NULL, NULL, NULL, |
| }; |
| ProxyHandler::IndexedPropertyHooks MozjsStubHandler::indexed_property_hooks = { |
| NULL, NULL, NULL, NULL, NULL, |
| }; |
| |
| static base::LazyInstance<MozjsStubHandler> proxy_handler; |
| } // namespace |
| |
| MozjsGlobalEnvironment::MozjsGlobalEnvironment( |
| JSRuntime* runtime, const JavaScriptEngine::Options& options) |
| : context_(NULL), |
| garbage_collection_count_(0), |
| context_destructor_(&context_), |
| environment_settings_(NULL), |
| last_error_message_(NULL), |
| eval_enabled_(false) { |
| TRACK_MEMORY_SCOPE("Javascript"); |
| context_ = JS_NewContext(runtime, kStackChunkSize); |
| DCHECK(context_); |
| // Set a pointer to this class inside the JSContext. |
| JS_SetContextPrivate(context_, this); |
| |
| JS_SetGCParameterForThread(context_, JSGC_MAX_CODE_CACHE_BYTES, |
| kMaxCodeCacheBytes); |
| |
| wrapper_factory_.reset(new WrapperFactory(context_)); |
| script_value_factory_.reset(new MozjsScriptValueFactory(this)); |
| referenced_objects_.reset(new ReferencedObjectMap(context_)); |
| |
| JS_AddExtraGCRootsTracer(runtime, TraceFunction, this); |
| } |
| |
| MozjsGlobalEnvironment::~MozjsGlobalEnvironment() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| JS_RemoveExtraGCRootsTracer(JS_GetRuntime(context_), TraceFunction, this); |
| } |
| |
| void MozjsGlobalEnvironment::CreateGlobalObject() { |
| TRACK_MEMORY_SCOPE("Javascript"); |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| DCHECK(!global_object_proxy_); |
| |
| // The global object is automatically rooted unless the |
| // JSOPTION_UNROOTED_GLOBAL option is set. |
| JSAutoRequest auto_request(context_); |
| |
| // NULL JSPrincipals and default JS::CompartmentOptions. See the comment |
| // above for rationale. |
| JS::RootedObject global_object( |
| context_, JS_NewGlobalObject(context_, &simple_global_class, NULL, |
| JS::FireOnNewGlobalHook)); |
| DCHECK(global_object); |
| |
| // Initialize standard JS constructors prototypes and top-level functions such |
| // as Object, isNan, etc. |
| JSAutoCompartment auto_compartment(context_, global_object); |
| bool success = JS_InitStandardClasses(context_, global_object); |
| DCHECK(success); |
| |
| JS::RootedObject proxy( |
| context_, ProxyHandler::NewProxy(context_, proxy_handler.Pointer(), |
| global_object, NULL)); |
| global_object_proxy_ = proxy; |
| |
| EvaluateAutomatics(); |
| } |
| |
| bool MozjsGlobalEnvironment::EvaluateScript( |
| const scoped_refptr<SourceCode>& source_code, |
| std::string* out_result_utf8) { |
| TRACK_MEMORY_SCOPE("Javascript"); |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| |
| JSAutoRequest auto_request(context_); |
| JSAutoCompartment auto_compartment(context_, global_object_proxy_); |
| JS::AutoSaveExceptionState auto_save_exception_state(context_); |
| JS::RootedValue result_value(context_); |
| |
| std::string error_message; |
| last_error_message_ = &error_message; |
| |
| bool success = EvaluateScriptInternal(source_code, &result_value); |
| if (out_result_utf8) { |
| if (success) { |
| MozjsExceptionState exception_state(context_); |
| FromJSValue(context_, result_value, kNoConversionFlags, &exception_state, |
| out_result_utf8); |
| } else if (last_error_message_) { |
| *out_result_utf8 = *last_error_message_; |
| } else { |
| DLOG(ERROR) << "Script execution failed."; |
| } |
| } |
| last_error_message_ = NULL; |
| return success; |
| } |
| |
| bool MozjsGlobalEnvironment::EvaluateScript( |
| const scoped_refptr<SourceCode>& source_code, |
| const scoped_refptr<Wrappable>& owning_object, |
| base::optional<OpaqueHandleHolder::Reference>* out_opaque_handle) { |
| TRACK_MEMORY_SCOPE("Javascript"); |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| JSAutoRequest auto_request(context_); |
| JSAutoCompartment auto_compartment(context_, global_object_proxy_); |
| JS::AutoSaveExceptionState auto_save_exception_state(context_); |
| JS::RootedValue result_value(context_); |
| bool success = EvaluateScriptInternal(source_code, &result_value); |
| if (success && out_opaque_handle) { |
| JS::RootedObject js_object(context_); |
| JS_ValueToObject(context_, result_value, &js_object); |
| MozjsObjectHandleHolder mozjs_object_holder(js_object, context_, |
| wrapper_factory()); |
| out_opaque_handle->emplace(owning_object.get(), mozjs_object_holder); |
| } |
| return success; |
| } |
| |
| bool MozjsGlobalEnvironment::EvaluateScriptInternal( |
| const scoped_refptr<SourceCode>& source_code, |
| JS::MutableHandleValue out_result) { |
| TRACK_MEMORY_SCOPE("Javascript"); |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| DCHECK(global_object_proxy_); |
| MozjsSourceCode* mozjs_source_code = |
| base::polymorphic_downcast<MozjsSourceCode*>(source_code.get()); |
| |
| const std::string& script = mozjs_source_code->source_utf8(); |
| const base::SourceLocation location = mozjs_source_code->location(); |
| |
| JS::RootedObject global_object( |
| context_, js::GetProxyTargetObject(global_object_proxy_)); |
| |
| size_t length = script.size(); |
| char16_t* inflated_buffer = |
| UTF8CharsToNewTwoByteCharsZ( |
| context_, JS::UTF8Chars(script.c_str(), length), &length) |
| .get(); |
| |
| bool success = false; |
| if (inflated_buffer) { |
| JS::CompileOptions options(context_); |
| options.setFileAndLine(location.file_path.c_str(), location.line_number); |
| success = |
| JS::Evaluate(context_, options, inflated_buffer, length, out_result); |
| js_free(inflated_buffer); |
| } else { |
| DLOG(ERROR) << "Malformed UTF-8 script."; |
| } |
| |
| return success; |
| } |
| |
| void MozjsGlobalEnvironment::EvaluateEmbeddedScript( |
| const unsigned char* data, size_t size, const char* filename) { |
| TRACK_MEMORY_SCOPE("Javascript"); |
| std::string source(reinterpret_cast<const char*>(data), size); |
| scoped_refptr<SourceCode> source_code = |
| new MozjsSourceCode(source, base::SourceLocation(filename, 1, 1)); |
| std::string result; |
| bool success = EvaluateScript(source_code, &result); |
| if (!success) { |
| DLOG(FATAL) << result; |
| } |
| } |
| |
| std::vector<StackFrame> MozjsGlobalEnvironment::GetStackTrace(int max_frames) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| return util::GetStackTrace(context_, max_frames); |
| } |
| |
| void MozjsGlobalEnvironment::PreventGarbageCollection( |
| const scoped_refptr<Wrappable>& wrappable) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| JSAutoRequest auto_request(context_); |
| JSAutoCompartment auto_compartment(context_, global_object_proxy_); |
| WrapperPrivate* wrapper_private = |
| WrapperPrivate::GetFromWrappable(wrappable, context_, wrapper_factory()); |
| JS::RootedObject proxy(context_, wrapper_private->js_object_proxy()); |
| kept_alive_objects_.insert(CachedWrapperMultiMap::value_type( |
| wrappable.get(), JS::Heap<JSObject*>(proxy))); |
| } |
| |
| void MozjsGlobalEnvironment::AllowGarbageCollection( |
| const scoped_refptr<Wrappable>& wrappable) { |
| TRACK_MEMORY_SCOPE("Javascript"); |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| CachedWrapperMultiMap::iterator it = |
| kept_alive_objects_.find(wrappable.get()); |
| DCHECK(it != kept_alive_objects_.end()); |
| if (it != kept_alive_objects_.end()) { |
| kept_alive_objects_.erase(it); |
| } |
| } |
| |
| void MozjsGlobalEnvironment::DisableEval(const std::string& message) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| eval_disabled_message_.emplace(message); |
| eval_enabled_ = false; |
| } |
| |
| void MozjsGlobalEnvironment::EnableEval() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| eval_disabled_message_ = base::nullopt; |
| eval_enabled_ = true; |
| } |
| |
| void MozjsGlobalEnvironment::DisableJit() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| JS::RuntimeOptionsRef(context_) |
| .setBaseline(false) |
| .setIon(false) |
| .setAsmJS(false) |
| .setNativeRegExp(false); |
| } |
| |
| void MozjsGlobalEnvironment::SetReportEvalCallback( |
| const base::Closure& report_eval) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| report_eval_ = report_eval; |
| } |
| |
| void MozjsGlobalEnvironment::Bind(const std::string& identifier, |
| const scoped_refptr<Wrappable>& impl) { |
| TRACK_MEMORY_SCOPE("Javascript"); |
| JSAutoRequest auto_request(context_); |
| JSAutoCompartment auto_compartment(context_, global_object_proxy_); |
| |
| JS::RootedObject wrapper_proxy(context_, |
| wrapper_factory_->GetWrapperProxy(impl)); |
| JS::RootedObject global_object( |
| context_, js::GetProxyTargetObject(global_object_proxy_)); |
| |
| JS::RootedValue wrapper_value(context_); |
| wrapper_value.setObject(*wrapper_proxy); |
| |
| bool success = JS_SetProperty(context_, global_object, identifier.c_str(), |
| wrapper_value); |
| |
| DCHECK(success); |
| } |
| |
| ScriptValueFactory* MozjsGlobalEnvironment::script_value_factory() { |
| DCHECK(script_value_factory_); |
| return script_value_factory_.get(); |
| } |
| |
| void MozjsGlobalEnvironment::EvaluateAutomatics() { |
| EvaluateEmbeddedScript( |
| MozjsEmbeddedResources::promise_min_js, |
| sizeof(MozjsEmbeddedResources::promise_min_js), |
| "promise.min.js"); |
| EvaluateEmbeddedScript( |
| MozjsEmbeddedResources::byte_length_queuing_strategy_js, |
| sizeof(MozjsEmbeddedResources::byte_length_queuing_strategy_js), |
| "byte_length_queuing_strategy.js"); |
| EvaluateEmbeddedScript( |
| MozjsEmbeddedResources::count_queuing_strategy_js, |
| sizeof(MozjsEmbeddedResources::count_queuing_strategy_js), |
| "count_queuing_strategy.js"); |
| EvaluateEmbeddedScript( |
| MozjsEmbeddedResources::readable_stream_js, |
| sizeof(MozjsEmbeddedResources::readable_stream_js), |
| "readable_stream.js"); |
| EvaluateEmbeddedScript( |
| MozjsEmbeddedResources::fetch_js, |
| sizeof(MozjsEmbeddedResources::fetch_js), |
| "fetch.js"); |
| } |
| |
| InterfaceData* MozjsGlobalEnvironment::GetInterfaceData(int key) { |
| DCHECK_GE(key, 0); |
| if (key >= cached_interface_data_.size()) { |
| cached_interface_data_.resize(key + 1); |
| } |
| return &cached_interface_data_[key]; |
| } |
| |
| void MozjsGlobalEnvironment::DoSweep() { |
| TRACK_MEMORY_SCOPE("Javascript"); |
| weak_object_manager_.SweepUnmarkedObjects(); |
| // Remove NULL references after sweeping weak references. |
| referenced_objects_->RemoveNullReferences(); |
| } |
| |
| void MozjsGlobalEnvironment::BeginGarbageCollection() { |
| TRACK_MEMORY_SCOPE("Javascript"); |
| // It's possible that a GC could be triggered from within the |
| // BeginGarbageCollection callback. Only verify that |visisted_wrappables_| |
| // is empty the first time we enter. |
| garbage_collection_count_++; |
| |
| if (garbage_collection_count_ == 1) { |
| DCHECK_EQ(visited_wrappables_.size(), 0); |
| } |
| } |
| |
| void MozjsGlobalEnvironment::EndGarbageCollection() { |
| // Reset |visisted_wrappables_|. |
| garbage_collection_count_--; |
| DCHECK_GE(garbage_collection_count_, 0); |
| if (garbage_collection_count_ == 0) { |
| visited_wrappables_.clear(); |
| } |
| } |
| |
| MozjsGlobalEnvironment* MozjsGlobalEnvironment::GetFromContext( |
| JSContext* context) { |
| MozjsGlobalEnvironment* global_proxy = |
| static_cast<MozjsGlobalEnvironment*>(JS_GetContextPrivate(context)); |
| DCHECK(global_proxy); |
| return global_proxy; |
| } |
| |
| void MozjsGlobalEnvironment::SetGlobalObjectProxyAndWrapper( |
| JS::HandleObject global_object_proxy, |
| const scoped_refptr<Wrappable>& wrappable) { |
| DCHECK(!global_object_proxy_); |
| DCHECK(global_object_proxy); |
| DCHECK(JS_IsGlobalObject(js::GetProxyTargetObject(global_object_proxy))); |
| DCHECK(IsProxy(global_object_proxy)); |
| |
| global_object_proxy_ = global_object_proxy; |
| |
| // Global object cached wrapper is not set as usual object. |
| // Set the global object cached wrapper, so we can get the object proxy |
| // through wrapper handle. |
| WrapperPrivate* wrapper_private = |
| WrapperPrivate::GetFromProxyObject(context_, global_object_proxy); |
| |
| DCHECK(wrapper_private); |
| |
| scoped_ptr<Wrappable::WeakWrapperHandle> object_handle( |
| new MozjsWrapperHandle(wrapper_private)); |
| SetCachedWrapper(wrappable.get(), object_handle.Pass()); |
| } |
| |
| void MozjsGlobalEnvironment::ReportError(const char* message, |
| JSErrorReport* report) { |
| std::string error_message; |
| if (report->errorNumber == JSMSG_CSP_BLOCKED_EVAL) { |
| error_message = eval_disabled_message_.value_or(message); |
| } else { |
| error_message = message; |
| } |
| |
| if (last_error_message_) { |
| *last_error_message_ = error_message; |
| } else { |
| const char* filename = report->filename ? report->filename : "(none)"; |
| LOG(ERROR) << "JS Error: " << filename << ":" << report->lineno << ":" |
| << report->column << ": " << error_message; |
| } |
| } |
| |
| void MozjsGlobalEnvironment::TraceFunction(JSTracer* trace, void* data) { |
| MozjsGlobalEnvironment* global_object_environment = |
| reinterpret_cast<MozjsGlobalEnvironment*>(data); |
| if (global_object_environment->global_object_proxy_) { |
| JS_CallObjectTracer(trace, &global_object_environment->global_object_proxy_, |
| "MozjsGlobalEnvironment"); |
| } |
| |
| for (int i = 0; i < global_object_environment->cached_interface_data_.size(); |
| i++) { |
| InterfaceData& data = global_object_environment->cached_interface_data_[i]; |
| if (data.prototype) { |
| JS_CallObjectTracer(trace, &data.prototype, "MozjsGlobalEnvironment"); |
| } |
| if (data.interface_object) { |
| JS_CallObjectTracer(trace, &data.interface_object, |
| "MozjsGlobalEnvironment"); |
| } |
| } |
| |
| for (CachedWrapperMultiMap::iterator it = |
| global_object_environment->kept_alive_objects_.begin(); |
| it != global_object_environment->kept_alive_objects_.end(); ++it) { |
| JS_CallObjectTracer(trace, &it->second, "MozjsGlobalEnvironment"); |
| } |
| } |
| |
| bool MozjsGlobalEnvironment::CheckEval(JSContext* context) { |
| TRACK_MEMORY_SCOPE("Javascript"); |
| MozjsGlobalEnvironment* global_object_proxy = GetFromContext(context); |
| DCHECK(global_object_proxy); |
| if (!global_object_proxy->report_eval_.is_null()) { |
| global_object_proxy->report_eval_.Run(); |
| } |
| return global_object_proxy->eval_enabled_; |
| } |
| |
| } // namespace mozjs |
| } // namespace script |
| } // namespace cobalt |