blob: 086ef3757d9313a8d4a6d90bc5589450451e215c [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 "cobalt/script/mozjs-45/util/stack_trace_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);
#if defined(COBALT_GC_ZEAL)
// Set zeal level to "Collect when every frequency allocations." See
// https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/JSAPI_reference/JS_SetGCZeal
// for other valid options.
const uint8_t kZealLevel = 2;
// TODO: Decrease this value as bugs that it discovers are fixed.
const uint32_t kZealFrequency = 2000;
JS_SetGCZeal(context_, kZealLevel, kZealFrequency);
#endif
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();
if (!inflated_buffer) {
DLOG(ERROR) << "Malformed UTF-8 script.";
return false;
}
JS::CompileOptions options(context_);
options.setFileAndLine(location.file_path.c_str(), location.line_number);
bool success =
JS::Evaluate(context_, options, inflated_buffer, length, out_result);
if (!success && context_->isExceptionPending()) {
JS_ReportPendingException(context_);
}
js_free(inflated_buffer);
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());
nb::RewindableVector<StackFrame> stack_frames;
util::GetStackTrace(context_, max_frames, &stack_frames);
return stack_frames.InternalData();
}
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::SetReportErrorCallback(
const ReportErrorCallback& report_error_callback) {
DCHECK(thread_checker_.CalledOnValidThread());
report_error_callback_ = report_error_callback;
}
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) {
JS::RootedValue exception(context_);
::JS_GetPendingException(context_, &exception);
// Note: we must do this before running any more code on context.
::JS_ClearPendingException(context_);
// Populate the error report.
ErrorReport error_report;
if (report->errorNumber == JSMSG_CSP_BLOCKED_EVAL) {
error_report.message = eval_disabled_message_.value_or(message);
} else {
error_report.message = message;
}
error_report.filename =
report->filename ? report->filename : "<internal exception>";
error_report.line_number = report->lineno;
error_report.column_number = report->column;
// Let error object be the object that represents the error: in the case of
// an uncaught exception, that would be the object that was thrown; in the
// case of a JavaScript error that would be an Error object. If there is no
// corresponding object, then the null value must be used instead.
// https://www.w3.org/TR/html5/webappapis.html#runtime-script-errors
if (exception.isObject()) {
error_report.error.reset(
new MozjsValueHandleHolder(exception, context_, wrapper_factory()));
}
error_report.is_muted = report->isMuted;
// If this isn't simply a warning, and the error wasn't caused by JS running
// out of memory (in which case the callback will fail as well), then run
// the callback. In the case that it returns that the script was handled,
// simply return; the error should only be reported to the user if it wasn't
// handled.
if (!JSREPORT_IS_WARNING(report->flags) &&
report->errorNumber != JSMSG_OUT_OF_MEMORY &&
!report_error_callback_.is_null() &&
report_error_callback_.Run(error_report)) {
return;
}
// If the error is not handled, then the error may be reported to the user.
// https://www.w3.org/TR/html5/webappapis.html#runtime-script-errors-in-documents
if (last_error_message_) {
*last_error_message_ = error_report.message;
} else {
LOG(ERROR) << "JS Error: " << error_report.filename << ":"
<< error_report.line_number << ":" << error_report.column_number
<< ": " << error_report.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