Source code

Revision control

Other Tools

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim: set ts=8 sts=2 et sw=2 tw=80:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "builtin/Eval.h"
#include "mozilla/HashFunctions.h"
#include "mozilla/Range.h"
#include "ds/LifoAlloc.h"
#include "frontend/BytecodeCompilation.h"
#include "frontend/CompilationInfo.h"
#include "gc/HashUtil.h"
#include "js/SourceText.h"
#include "js/StableStringChars.h"
#include "vm/GlobalObject.h"
#include "vm/JSContext.h"
#include "vm/JSONParser.h"
#include "debugger/DebugAPI-inl.h"
#include "vm/Interpreter-inl.h"
using namespace js;
using mozilla::AddToHash;
using mozilla::HashString;
using mozilla::RangedPtr;
using JS::AutoCheckCannotGC;
using JS::AutoStableStringChars;
using JS::CompileOptions;
using JS::SourceOwnership;
using JS::SourceText;
// We should be able to assert this for *any* fp->environmentChain().
static void AssertInnerizedEnvironmentChain(JSContext* cx, JSObject& env) {
#ifdef DEBUG
RootedObject obj(cx);
for (obj = &env; obj; obj = obj->enclosingEnvironment()) {
MOZ_ASSERT(!IsWindowProxy(obj));
}
#endif
}
static bool IsEvalCacheCandidate(JSScript* script) {
if (!script->isDirectEvalInFunction()) {
return false;
}
// Make sure there are no inner objects (which may be used directly by script
// and clobbered) or inner functions (which may have wrong scope).
for (JS::GCCellPtr gcThing : script->gcthings()) {
if (gcThing.is<JSObject>()) {
return false;
}
}
return true;
}
/* static */
HashNumber EvalCacheHashPolicy::hash(const EvalCacheLookup& l) {
HashNumber hash = HashStringChars(l.str);
return AddToHash(hash, l.callerScript.get(), l.pc);
}
/* static */
bool EvalCacheHashPolicy::match(const EvalCacheEntry& cacheEntry,
const EvalCacheLookup& l) {
MOZ_ASSERT(IsEvalCacheCandidate(cacheEntry.script));
return EqualStrings(cacheEntry.str, l.str) &&
cacheEntry.callerScript == l.callerScript && cacheEntry.pc == l.pc;
}
// Add the script to the eval cache when EvalKernel is finished
class EvalScriptGuard {
JSContext* cx_;
Rooted<JSScript*> script_;
/* These fields are only valid if lookup_.str is non-nullptr. */
EvalCacheLookup lookup_;
mozilla::Maybe<DependentAddPtr<EvalCache>> p_;
RootedLinearString lookupStr_;
public:
explicit EvalScriptGuard(JSContext* cx)
: cx_(cx), script_(cx), lookup_(cx), lookupStr_(cx) {}
~EvalScriptGuard() {
if (script_ && !cx_->isExceptionPending()) {
script_->cacheForEval();
EvalCacheEntry cacheEntry = {lookupStr_, script_, lookup_.callerScript,
lookup_.pc};
lookup_.str = lookupStr_;
if (lookup_.str && IsEvalCacheCandidate(script_)) {
// Ignore failure to add cache entry.
if (!p_->add(cx_, cx_->caches().evalCache, lookup_, cacheEntry)) {
cx_->recoverFromOutOfMemory();
}
}
}
}
void lookupInEvalCache(JSLinearString* str, JSScript* callerScript,
jsbytecode* pc) {
lookupStr_ = str;
lookup_.str = str;
lookup_.callerScript = callerScript;
lookup_.pc = pc;
p_.emplace(cx_, cx_->caches().evalCache, lookup_);
if (*p_) {
script_ = (*p_)->script;
p_->remove(cx_, cx_->caches().evalCache, lookup_);
}
}
void setNewScript(JSScript* script) {
// JSScript::fullyInitFromStencil has already called js_CallNewScriptHook.
MOZ_ASSERT(!script_ && script);
script_ = script;
}
bool foundScript() { return !!script_; }
HandleScript script() {
MOZ_ASSERT(script_);
return script_;
}
};
enum class EvalJSONResult { Failure, Success, NotJSON };
template <typename CharT>
static bool EvalStringMightBeJSON(const mozilla::Range<const CharT> chars) {
// If the eval string starts with '(' or '[' and ends with ')' or ']', it
// may be JSON. Try the JSON parser first because it's much faster. If
// the eval string isn't JSON, JSON parsing will probably fail quickly, so
// little time will be lost.
size_t length = chars.length();
if (length < 2) {
return false;
}
// It used to be that strings in JavaScript forbid U+2028 LINE SEPARATOR
// and U+2029 PARAGRAPH SEPARATOR, so something like
//
// eval("['" + "\u2028" + "']");
//
// i.e. an array containing a string with a line separator in it, *would*
// be JSON but *would not* be valid JavaScript. Handing such a string to
// the JSON parser would then fail to recognize a syntax error. As of
// contain these two code points, so it's safe to JSON-parse eval strings
// that contain them.
CharT first = chars[0], last = chars[length - 1];
return (first == '[' && last == ']') || (first == '(' && last == ')');
}
template <typename CharT>
static EvalJSONResult ParseEvalStringAsJSON(
JSContext* cx, const mozilla::Range<const CharT> chars,
MutableHandleValue rval) {
size_t len = chars.length();
MOZ_ASSERT((chars[0] == '(' && chars[len - 1] == ')') ||
(chars[0] == '[' && chars[len - 1] == ']'));
auto jsonChars = (chars[0] == '[') ? chars
: mozilla::Range<const CharT>(
chars.begin().get() + 1U, len - 2);
Rooted<JSONParser<CharT>> parser(
cx, JSONParser<CharT>(cx, jsonChars,
JSONParserBase::ParseType::AttemptForEval));
if (!parser.parse(rval)) {
return EvalJSONResult::Failure;
}
return rval.isUndefined() ? EvalJSONResult::NotJSON : EvalJSONResult::Success;
}
static EvalJSONResult TryEvalJSON(JSContext* cx, JSLinearString* str,
MutableHandleValue rval) {
if (str->hasLatin1Chars()) {
AutoCheckCannotGC nogc;
if (!EvalStringMightBeJSON(str->latin1Range(nogc))) {
return EvalJSONResult::NotJSON;
}
} else {
AutoCheckCannotGC nogc;
if (!EvalStringMightBeJSON(str->twoByteRange(nogc))) {
return EvalJSONResult::NotJSON;
}
}
AutoStableStringChars linearChars(cx);
if (!linearChars.init(cx, str)) {
return EvalJSONResult::Failure;
}
return linearChars.isLatin1()
? ParseEvalStringAsJSON(cx, linearChars.latin1Range(), rval)
: ParseEvalStringAsJSON(cx, linearChars.twoByteRange(), rval);
}
enum EvalType { DIRECT_EVAL, INDIRECT_EVAL };
// 18.2.1.1 PerformEval
//
// Common code implementing direct and indirect eval.
//
// Evaluate call.argv[2], if it is a string, in the context of the given calling
// frame, with the provided scope chain, with the semantics of either a direct
// or indirect eval (see ES5 10.4.2). If this is an indirect eval, env
// must be a global object.
//
// On success, store the completion value in call.rval and return true.
static bool EvalKernel(JSContext* cx, HandleValue v, EvalType evalType,
AbstractFramePtr caller, HandleObject env,
jsbytecode* pc, MutableHandleValue vp) {
MOZ_ASSERT((evalType == INDIRECT_EVAL) == !caller);
MOZ_ASSERT((evalType == INDIRECT_EVAL) == !pc);
MOZ_ASSERT_IF(evalType == INDIRECT_EVAL, IsGlobalLexicalEnvironment(env));
AssertInnerizedEnvironmentChain(cx, *env);
// Step 2.
if (!v.isString()) {
vp.set(v);
return true;
}
// Steps 3-4.
RootedString str(cx, v.toString());
if (!GlobalObject::isRuntimeCodeGenEnabled(cx, str, cx->global())) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_CSP_BLOCKED_EVAL);
return false;
}
// Step 5 ff.
// Per ES5, indirect eval runs in the global scope. (eval is specified this
// way so that the compiler can make assumptions about what bindings may or
// may not exist in the current frame if it doesn't see 'eval'.)
MOZ_ASSERT_IF(evalType != DIRECT_EVAL,
cx->global() == &env->as<LexicalEnvironmentObject>().global());
RootedLinearString linearStr(cx, str->ensureLinear(cx));
if (!linearStr) {
return false;
}
RootedScript callerScript(cx, caller ? caller.script() : nullptr);
EvalJSONResult ejr = TryEvalJSON(cx, linearStr, vp);
if (ejr != EvalJSONResult::NotJSON) {
return ejr == EvalJSONResult::Success;
}
EvalScriptGuard esg(cx);
if (evalType == DIRECT_EVAL && caller.isFunctionFrame()) {
esg.lookupInEvalCache(linearStr, callerScript, pc);
}
if (!esg.foundScript()) {
RootedScript maybeScript(cx);
unsigned lineno;
const char* filename;
bool mutedErrors;
uint32_t pcOffset;
if (evalType == DIRECT_EVAL) {
DescribeScriptedCallerForDirectEval(cx, callerScript, pc, &filename,
&lineno, &pcOffset, &mutedErrors);
maybeScript = callerScript;
} else {
DescribeScriptedCallerForCompilation(cx, &maybeScript, &filename, &lineno,
&pcOffset, &mutedErrors);
}
const char* introducerFilename = filename;
if (maybeScript && maybeScript->scriptSource()->introducerFilename()) {
introducerFilename = maybeScript->scriptSource()->introducerFilename();
}
RootedScope enclosing(cx);
if (evalType == DIRECT_EVAL) {
enclosing = callerScript->innermostScope(pc);
} else {
enclosing = &cx->global()->emptyGlobalScope();
}
CompileOptions options(cx);
options.setIsRunOnce(true)
.setNoScriptRval(false)
.setMutedErrors(mutedErrors)
.setScriptOrModule(maybeScript);
if (evalType == DIRECT_EVAL && IsStrictEvalPC(pc)) {
options.setForceStrictMode();
}
if (introducerFilename) {
options.setFileAndLine(filename, 1);
options.setIntroductionInfo(introducerFilename, "eval", lineno,
maybeScript, pcOffset);
} else {
options.setFileAndLine("eval", 1);
options.setIntroductionType("eval");
}
options.setNonSyntacticScope(
enclosing->hasOnChain(ScopeKind::NonSyntactic));
AutoStableStringChars linearChars(cx);
if (!linearChars.initTwoByte(cx, linearStr)) {
return false;
}
SourceText<char16_t> srcBuf;
const char16_t* chars = linearChars.twoByteRange().begin().get();
SourceOwnership ownership = linearChars.maybeGiveOwnershipToCaller()
? SourceOwnership::TakeOwnership
: SourceOwnership::Borrowed;
if (!srcBuf.init(cx, chars, linearStr->length(), ownership)) {
return false;
}
LifoAllocScope allocScope(&cx->tempLifoAlloc());
frontend::CompilationInfo compilationInfo(cx, allocScope, options,
enclosing, env);
if (!compilationInfo.init(cx)) {
return false;
}
uint32_t len = srcBuf.length();
SourceExtent extent = SourceExtent::makeGlobalExtent(len);
frontend::EvalSharedContext evalsc(cx, compilationInfo, enclosing,
compilationInfo.directives, extent);
RootedScript compiled(
cx, frontend::CompileEvalScript(compilationInfo, evalsc, srcBuf));
if (!compiled) {
return false;
}
esg.setNewScript(compiled);
}
// If this is a direct eval we need to use the caller's newTarget.
RootedValue newTargetVal(cx);
if (esg.script()->isDirectEvalInFunction()) {
newTargetVal = caller.newTarget();
}
return ExecuteKernel(cx, esg.script(), env, newTargetVal,
NullFramePtr() /* evalInFrame */, vp);
}
bool js::DirectEvalStringFromIon(JSContext* cx, HandleObject env,
HandleScript callerScript,
HandleValue newTargetValue, HandleString str,
jsbytecode* pc, MutableHandleValue vp) {
AssertInnerizedEnvironmentChain(cx, *env);
if (!GlobalObject::isRuntimeCodeGenEnabled(cx, str, cx->global())) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_CSP_BLOCKED_EVAL);
return false;
}
// ES5 15.1.2.1 steps 2-8.
RootedLinearString linearStr(cx, str->ensureLinear(cx));
if (!linearStr) {
return false;
}
EvalJSONResult ejr = TryEvalJSON(cx, linearStr, vp);
if (ejr != EvalJSONResult::NotJSON) {
return ejr == EvalJSONResult::Success;
}
EvalScriptGuard esg(cx);
esg.lookupInEvalCache(linearStr, callerScript, pc);
if (!esg.foundScript()) {
const char* filename;
unsigned lineno;
bool mutedErrors;
uint32_t pcOffset;
DescribeScriptedCallerForDirectEval(cx, callerScript, pc, &filename,
&lineno, &pcOffset, &mutedErrors);
const char* introducerFilename = filename;
if (callerScript->scriptSource()->introducerFilename()) {
introducerFilename = callerScript->scriptSource()->introducerFilename();
}
RootedScope enclosing(cx, callerScript->innermostScope(pc));
CompileOptions options(cx);
options.setIsRunOnce(true);
options.setNoScriptRval(false);
options.setMutedErrors(mutedErrors);
if (IsStrictEvalPC(pc)) {
options.setForceStrictMode();
}
if (introducerFilename) {
options.setFileAndLine(filename, 1);
options.setIntroductionInfo(introducerFilename, "eval", lineno,
callerScript, pcOffset);
} else {
options.setFileAndLine("eval", 1);
options.setIntroductionType("eval");
}
options.setNonSyntacticScope(
enclosing->hasOnChain(ScopeKind::NonSyntactic));
AutoStableStringChars linearChars(cx);
if (!linearChars.initTwoByte(cx, linearStr)) {
return false;
}
SourceText<char16_t> srcBuf;
const char16_t* chars = linearChars.twoByteRange().begin().get();
SourceOwnership ownership = linearChars.maybeGiveOwnershipToCaller()
? SourceOwnership::TakeOwnership
: SourceOwnership::Borrowed;
if (!srcBuf.init(cx, chars, linearStr->length(), ownership)) {
return false;
}
LifoAllocScope allocScope(&cx->tempLifoAlloc());
frontend::CompilationInfo compilationInfo(cx, allocScope, options,
enclosing, env);
if (!compilationInfo.init(cx)) {
return false;
}
uint32_t len = srcBuf.length();
SourceExtent extent = SourceExtent::makeGlobalExtent(len);
frontend::EvalSharedContext evalsc(cx, compilationInfo, enclosing,
compilationInfo.directives, extent);
JSScript* compiled =
frontend::CompileEvalScript(compilationInfo, evalsc, srcBuf);
if (!compiled) {
return false;
}
esg.setNewScript(compiled);
}
return ExecuteKernel(cx, esg.script(), env, newTargetValue,
NullFramePtr() /* evalInFrame */, vp);
}
bool js::IndirectEval(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
RootedObject globalLexical(cx, &cx->global()->lexicalEnvironment());
// Note we'll just pass |undefined| here, then return it directly (or throw
// if runtime codegen is disabled), if no argument is provided.
return EvalKernel(cx, args.get(0), INDIRECT_EVAL, NullFramePtr(),
globalLexical, nullptr, args.rval());
}
bool js::DirectEval(JSContext* cx, HandleValue v, MutableHandleValue vp) {
// Direct eval can assume it was called from an interpreted or baseline frame.
ScriptFrameIter iter(cx);
AbstractFramePtr caller = iter.abstractFramePtr();
MOZ_ASSERT(JSOp(*iter.pc()) == JSOp::Eval ||
JSOp(*iter.pc()) == JSOp::StrictEval ||
JSOp(*iter.pc()) == JSOp::SpreadEval ||
JSOp(*iter.pc()) == JSOp::StrictSpreadEval);
MOZ_ASSERT(caller.realm() == caller.script()->realm());
RootedObject envChain(cx, caller.environmentChain());
return EvalKernel(cx, v, DIRECT_EVAL, caller, envChain, iter.pc(), vp);
}
bool js::IsAnyBuiltinEval(JSFunction* fun) {
return fun->maybeNative() == IndirectEval;
}
static bool ExecuteInExtensibleLexicalEnvironment(JSContext* cx,
HandleScript scriptArg,
HandleObject env) {
CHECK_THREAD(cx);
cx->check(env);
MOZ_ASSERT(IsExtensibleLexicalEnvironment(env));
MOZ_RELEASE_ASSERT(scriptArg->hasNonSyntacticScope());
RootedScript script(cx, scriptArg);
if (script->realm() != cx->realm()) {
script = CloneGlobalScript(cx, ScopeKind::NonSyntactic, script);
if (!script) {
return false;
}
}
RootedValue rval(cx);
return ExecuteKernel(cx, script, env, UndefinedHandleValue,
NullFramePtr() /* evalInFrame */, &rval);
}
JS_FRIEND_API bool js::ExecuteInFrameScriptEnvironment(
JSContext* cx, HandleObject objArg, HandleScript scriptArg,
MutableHandleObject envArg) {
RootedObject varEnv(cx, NonSyntacticVariablesObject::create(cx));
if (!varEnv) {
return false;
}
RootedObjectVector envChain(cx);
if (!envChain.append(objArg)) {
return false;
}
RootedObject env(cx);
if (!js::CreateObjectsForEnvironmentChain(cx, envChain, varEnv, &env)) {
return false;
}
// Create lexical environment with |this| == objArg, which should be a Gecko
// MessageManager.
// NOTE: This is required behavior for Gecko FrameScriptLoader, where some
// callers try to bind methods from the message manager in their scope chain
// to |this|, and will fail if it is not bound to a message manager.
ObjectRealm& realm = ObjectRealm::get(varEnv);
env =
realm.getOrCreateNonSyntacticLexicalEnvironment(cx, env, varEnv, objArg);
if (!env) {
return false;
}
if (!ExecuteInExtensibleLexicalEnvironment(cx, scriptArg, env)) {
return false;
}
envArg.set(env);
return true;
}
JS_FRIEND_API JSObject* js::NewJSMEnvironment(JSContext* cx) {
RootedObject varEnv(cx, NonSyntacticVariablesObject::create(cx));
if (!varEnv) {
return nullptr;
}
// Force LexicalEnvironmentObject to be created.
ObjectRealm& realm = ObjectRealm::get(varEnv);
MOZ_ASSERT(!realm.getNonSyntacticLexicalEnvironment(varEnv));
if (!realm.getOrCreateNonSyntacticLexicalEnvironment(cx, varEnv)) {
return nullptr;
}
return varEnv;
}
JS_FRIEND_API bool js::ExecuteInJSMEnvironment(JSContext* cx,
HandleScript scriptArg,
HandleObject varEnv) {
RootedObjectVector emptyChain(cx);
return ExecuteInJSMEnvironment(cx, scriptArg, varEnv, emptyChain);
}
JS_FRIEND_API bool js::ExecuteInJSMEnvironment(JSContext* cx,
HandleScript scriptArg,
HandleObject varEnv,
HandleObjectVector targetObj) {
cx->check(varEnv);
MOZ_ASSERT(
ObjectRealm::get(varEnv).getNonSyntacticLexicalEnvironment(varEnv));
MOZ_DIAGNOSTIC_ASSERT(scriptArg->noScriptRval());
RootedObject env(cx, JS_ExtensibleLexicalEnvironment(varEnv));
// If the Gecko subscript loader specifies target objects, we need to add
// them to the environment. These are added after the NSVO environment.
if (!targetObj.empty()) {
// The environment chain will be as follows:
// GlobalObject / BackstagePass
// LexicalEnvironmentObject[this=global]
// NonSyntacticVariablesObject (the JSMEnvironment)
// LexicalEnvironmentObject[this=nsvo]
// WithEnvironmentObject[target=targetObj]
// LexicalEnvironmentObject[this=targetObj] (*)
//
// (*) This environment intercepts JSOp::GlobalThis.
// Wrap the target objects in WithEnvironments.
if (!js::CreateObjectsForEnvironmentChain(cx, targetObj, env, &env)) {
return false;
}
// See CreateNonSyntacticEnvironmentChain
if (!JSObject::setQualifiedVarObj(cx, env)) {
return false;
}
// Create an extensible LexicalEnvironmentObject for target object
env = ObjectRealm::get(env).getOrCreateNonSyntacticLexicalEnvironment(cx,
env);
if (!env) {
return false;
}
}
return ExecuteInExtensibleLexicalEnvironment(cx, scriptArg, env);
}
JS_FRIEND_API JSObject* js::GetJSMEnvironmentOfScriptedCaller(JSContext* cx) {
FrameIter iter(cx);
if (iter.done()) {
return nullptr;
}
// WASM frames don't always provide their environment, but we also shouldn't
// expect to see any calling into here.
MOZ_RELEASE_ASSERT(!iter.isWasm());
RootedObject env(cx, iter.environmentChain(cx));
while (env && !env->is<NonSyntacticVariablesObject>()) {
env = env->enclosingEnvironment();
}
return env;
}
JS_FRIEND_API bool js::IsJSMEnvironment(JSObject* obj) {
// NOTE: This also returns true if the NonSyntacticVariablesObject was
// created for reasons other than the JSM loader.
return obj->is<NonSyntacticVariablesObject>();
}