diff options
Diffstat (limited to 'js/src/builtin/Eval.cpp')
-rw-r--r-- | js/src/builtin/Eval.cpp | 485 |
1 files changed, 485 insertions, 0 deletions
diff --git a/js/src/builtin/Eval.cpp b/js/src/builtin/Eval.cpp new file mode 100644 index 000000000..87321ba04 --- /dev/null +++ b/js/src/builtin/Eval.cpp @@ -0,0 +1,485 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * 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 "jscntxt.h" +#include "jshashutil.h" + +#include "frontend/BytecodeCompiler.h" +#include "vm/Debugger.h" +#include "vm/GlobalObject.h" +#include "vm/JSONParser.h" + +#include "vm/Interpreter-inl.h" + +using namespace js; + +using mozilla::AddToHash; +using mozilla::HashString; +using mozilla::RangedPtr; + +using JS::AutoCheckCannotGC; + +// 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) +{ + // Make sure there are no inner objects which might use the wrong parent + // and/or call scope by reusing the previous eval's script. + return script->isDirectEvalInFunction() && + !script->hasSingletons() && + !script->hasObjects(); +} + +/* static */ HashNumber +EvalCacheHashPolicy::hash(const EvalCacheLookup& l) +{ + AutoCheckCannotGC nogc; + uint32_t hash = l.str->hasLatin1Chars() + ? HashString(l.str->latin1Chars(nogc), l.str->length()) + : HashString(l.str->twoByteChars(nogc), l.str->length()); + return AddToHash(hash, l.callerScript.get(), l.version, l.pc); +} + +/* static */ bool +EvalCacheHashPolicy::match(const EvalCacheEntry& cacheEntry, const EvalCacheLookup& l) +{ + JSScript* script = cacheEntry.script; + + MOZ_ASSERT(IsEvalCacheCandidate(script)); + + return EqualStrings(cacheEntry.str, l.str) && + cacheEntry.callerScript == l.callerScript && + script->getVersion() == l.version && + 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_.version = cx_->findVersion(); + lookup_.pc = pc; + p_.emplace(cx_, cx_->caches.evalCache, lookup_); + if (*p_) { + script_ = (*p_)->script; + p_->remove(cx_, cx_->caches.evalCache, lookup_); + script_->uncacheForEval(); + } + } + + void setNewScript(JSScript* script) { + // JSScript::initFromEmitter has already called js_CallNewScriptHook. + MOZ_ASSERT(!script_ && script); + script_ = script; + script_->setActiveEval(); + } + + bool foundScript() { + return !!script_; + } + + HandleScript script() { + MOZ_ASSERT(script_); + return script_; + } +}; + +enum EvalJSONResult { + EvalJSON_Failure, + EvalJSON_Success, + EvalJSON_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 && + ((chars[0] == '[' && chars[length - 1] == ']') || + (chars[0] == '(' && chars[length - 1] == ')'))) + { + // Remarkably, JavaScript syntax is not a superset of JSON syntax: + // strings in JavaScript cannot contain the Unicode line and paragraph + // terminator characters U+2028 and U+2029, but strings in JSON can. + // Rather than force the JSON parser to handle this quirk when used by + // eval, we simply don't use the JSON parser when either character + // appears in the provided string. See bug 657367. + if (sizeof(CharT) > 1) { + for (RangedPtr<const CharT> cp = chars.begin() + 1, end = chars.end() - 1; + cp < end; + cp++) + { + char16_t c = *cp; + if (c == 0x2028 || c == 0x2029) + return false; + } + } + + return true; + } + return false; +} + +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::NoError)); + if (!parser.parse(rval)) + return EvalJSON_Failure; + + return rval.isUndefined() ? EvalJSON_NotJSON : EvalJSON_Success; +} + +static EvalJSONResult +TryEvalJSON(JSContext* cx, JSLinearString* str, MutableHandleValue rval) +{ + if (str->hasLatin1Chars()) { + AutoCheckCannotGC nogc; + if (!EvalStringMightBeJSON(str->latin1Range(nogc))) + return EvalJSON_NotJSON; + } else { + AutoCheckCannotGC nogc; + if (!EvalStringMightBeJSON(str->twoByteRange(nogc))) + return EvalJSON_NotJSON; + } + + AutoStableStringChars linearChars(cx); + if (!linearChars.init(cx, str)) + return EvalJSON_Failure; + + return linearChars.isLatin1() + ? ParseEvalStringAsJSON(cx, linearChars.latin1Range(), rval) + : ParseEvalStringAsJSON(cx, linearChars.twoByteRange(), rval); +} + +enum EvalType { DIRECT_EVAL, INDIRECT_EVAL }; + +// 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); + + Rooted<GlobalObject*> envGlobal(cx, &env->global()); + if (!GlobalObject::isRuntimeCodeGenEnabled(cx, envGlobal)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_CSP_BLOCKED_EVAL); + return false; + } + + // ES5 15.1.2.1 step 1. + if (!v.isString()) { + vp.set(v); + return true; + } + RootedString str(cx, v.toString()); + + // ES5 15.1.2.1 steps 2-8. + + // 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 != EvalJSON_NotJSON) + return ejr == EvalJSON_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; + DescribeScriptedCallerForCompilation(cx, &maybeScript, &filename, &lineno, &pcOffset, + &mutedErrors, + evalType == DIRECT_EVAL + ? CALLED_FROM_JSOP_EVAL + : NOT_CALLED_FROM_JSOP_EVAL); + + 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) + .maybeMakeStrictMode(evalType == DIRECT_EVAL && IsStrictEvalPC(pc)); + + if (introducerFilename) { + options.setFileAndLine(filename, 1); + options.setIntroductionInfo(introducerFilename, "eval", lineno, maybeScript, pcOffset); + } else { + options.setFileAndLine("eval", 1); + options.setIntroductionType("eval"); + } + + AutoStableStringChars linearChars(cx); + if (!linearChars.initTwoByte(cx, linearStr)) + return false; + + const char16_t* chars = linearChars.twoByteRange().begin().get(); + SourceBufferHolder::Ownership ownership = linearChars.maybeGiveOwnershipToCaller() + ? SourceBufferHolder::GiveOwnership + : SourceBufferHolder::NoOwnership; + SourceBufferHolder srcBuf(chars, linearStr->length(), ownership); + JSScript* compiled = frontend::CompileEvalScript(cx, cx->tempLifoAlloc(), + env, enclosing, + options, srcBuf); + if (!compiled) + return false; + + esg.setNewScript(compiled); + } + + // Look up the newTarget from the frame iterator. + Value newTargetVal = NullValue(); + return ExecuteKernel(cx, esg.script(), *env, newTargetVal, + NullFramePtr() /* evalInFrame */, vp.address()); +} + +bool +js::DirectEvalStringFromIon(JSContext* cx, + HandleObject env, HandleScript callerScript, + HandleValue newTargetValue, HandleString str, + jsbytecode* pc, MutableHandleValue vp) +{ + AssertInnerizedEnvironmentChain(cx, *env); + + Rooted<GlobalObject*> envGlobal(cx, &env->global()); + if (!GlobalObject::isRuntimeCodeGenEnabled(cx, envGlobal)) { + 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 != EvalJSON_NotJSON) + return ejr == EvalJSON_Success; + + EvalScriptGuard esg(cx); + + esg.lookupInEvalCache(linearStr, callerScript, pc); + + if (!esg.foundScript()) { + RootedScript maybeScript(cx); + const char* filename; + unsigned lineno; + bool mutedErrors; + uint32_t pcOffset; + DescribeScriptedCallerForCompilation(cx, &maybeScript, &filename, &lineno, &pcOffset, + &mutedErrors, CALLED_FROM_JSOP_EVAL); + + const char* introducerFilename = filename; + if (maybeScript && maybeScript->scriptSource()->introducerFilename()) + introducerFilename = maybeScript->scriptSource()->introducerFilename(); + + RootedScope enclosing(cx, callerScript->innermostScope(pc)); + + CompileOptions options(cx); + options.setIsRunOnce(true) + .setNoScriptRval(false) + .setMutedErrors(mutedErrors) + .maybeMakeStrictMode(IsStrictEvalPC(pc)); + + if (introducerFilename) { + options.setFileAndLine(filename, 1); + options.setIntroductionInfo(introducerFilename, "eval", lineno, maybeScript, pcOffset); + } else { + options.setFileAndLine("eval", 1); + options.setIntroductionType("eval"); + } + + AutoStableStringChars linearChars(cx); + if (!linearChars.initTwoByte(cx, linearStr)) + return false; + + const char16_t* chars = linearChars.twoByteRange().begin().get(); + SourceBufferHolder::Ownership ownership = linearChars.maybeGiveOwnershipToCaller() + ? SourceBufferHolder::GiveOwnership + : SourceBufferHolder::NoOwnership; + SourceBufferHolder srcBuf(chars, linearStr->length(), ownership); + JSScript* compiled = frontend::CompileEvalScript(cx, cx->tempLifoAlloc(), + env, enclosing, + options, srcBuf); + if (!compiled) + return false; + + esg.setNewScript(compiled); + } + + return ExecuteKernel(cx, esg.script(), *env, newTargetValue, + NullFramePtr() /* evalInFrame */, vp.address()); +} + +bool +js::IndirectEval(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<GlobalObject*> global(cx, &args.callee().global()); + RootedObject globalLexical(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.compartment() == caller.script()->compartment()); + + 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; +} + +JS_FRIEND_API(bool) +js::ExecuteInGlobalAndReturnScope(JSContext* cx, HandleObject global, HandleScript scriptArg, + MutableHandleObject envArg) +{ + CHECK_REQUEST(cx); + assertSameCompartment(cx, global); + MOZ_ASSERT(global->is<GlobalObject>()); + MOZ_RELEASE_ASSERT(scriptArg->hasNonSyntacticScope()); + + RootedScript script(cx, scriptArg); + Rooted<GlobalObject*> globalRoot(cx, &global->as<GlobalObject>()); + if (script->compartment() != cx->compartment()) { + script = CloneGlobalScript(cx, ScopeKind::NonSyntactic, script); + if (!script) + return false; + + Debugger::onNewScript(cx, script); + } + + Rooted<EnvironmentObject*> env(cx, NonSyntacticVariablesObject::create(cx)); + if (!env) + return false; + + // Unlike the non-syntactic scope chain API used by the subscript loader, + // this API creates a fresh block scope each time. + env = LexicalEnvironmentObject::createNonSyntactic(cx, env); + if (!env) + return false; + + RootedValue rval(cx); + if (!ExecuteKernel(cx, script, *env, UndefinedValue(), + NullFramePtr() /* evalInFrame */, rval.address())) + { + return false; + } + + envArg.set(env); + return true; +} |