blob: 2bd7c419cef55cbb20ff9d1d35b1e2f3f7b577f9 [file] [log] [blame]
// 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),
cached_interface_data_deleter_(&cached_interface_data_),
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_));
opaque_root_tracker_.reset(new OpaqueRootTracker(
context_, referenced_objects_.get(), wrapper_factory_.get()));
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_);
JSExceptionState* previous_exception_state = JS_SaveExceptionState(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;
JS_RestoreExceptionState(context_, previous_exception_state);
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_);
JSExceptionState* previous_exception_state = JS_SaveExceptionState(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);
}
JS_RestoreExceptionState(context_, previous_exception_state);
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");
}
InterfaceData* MozjsGlobalEnvironment::GetInterfaceData(intptr_t key) {
CachedInterfaceData::iterator it = cached_interface_data_.find(key);
if (it != cached_interface_data_.end()) {
return it->second;
}
return NULL;
}
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 create the OpaqueRootState the
// first time we enter. Also, only verify that |visisted_wrappables_| is
// empty in this case.
// TODO: Opaque root logic is a special case of tracing wrappables, and
// should be removed.
garbage_collection_count_++;
if (garbage_collection_count_ == 1) {
if (global_object_proxy_) {
DCHECK(!opaque_root_state_);
JSAutoRequest auto_request(context_);
JSAutoCompartment auto_compartment(context_, global_object_proxy_);
// Get the current state of opaque root relationships. Keep this object
// alive for the duration of the GC phase to ensure that reachability
// between roots and reachable objects is maintained.
opaque_root_state_ = opaque_root_tracker_->GetCurrentOpaqueRootState();
}
DCHECK_EQ(visited_wrappables_.size(), 0);
}
}
void MozjsGlobalEnvironment::EndGarbageCollection() {
// Reset opaque root reachability relationships. Also reset
// |visisted_wrappables_|.
// TODO: Opaque root logic is a special case of tracing wrappables, and
// should be removed.
garbage_collection_count_--;
DCHECK_GE(garbage_collection_count_, 0);
if (garbage_collection_count_ == 0) {
opaque_root_state_.reset(NULL);
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::CacheInterfaceData(intptr_t key,
InterfaceData* interface_data) {
std::pair<CachedInterfaceData::iterator, bool> pib =
cached_interface_data_.insert(std::make_pair(key, interface_data));
DCHECK(pib.second);
}
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 (CachedInterfaceData::iterator it =
global_object_environment->cached_interface_data_.begin();
it != global_object_environment->cached_interface_data_.end(); ++it) {
InterfaceData* data = it->second;
// Check whether prototype and interface object for this interface have been
// created yet or not before attempting to trace them.
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