diff options
Diffstat (limited to '')
-rw-r--r-- | js/src/builtin/Eval.cpp | 547 |
1 files changed, 547 insertions, 0 deletions
diff --git a/js/src/builtin/Eval.cpp b/js/src/builtin/Eval.cpp new file mode 100644 index 0000000000..28c3317d41 --- /dev/null +++ b/js/src/builtin/Eval.cpp @@ -0,0 +1,547 @@ +/* -*- 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 "frontend/BytecodeCompilation.h" +#include "gc/HashUtil.h" +#include "js/CompilationAndEvaluation.h" +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/friend/JSMEnvironment.h" // JS::NewJSMEnvironment, JS::ExecuteInJSMEnvironment, JS::GetJSMEnvironmentOfScriptedCaller, JS::IsJSMEnvironment +#include "js/friend/WindowProxy.h" // js::IsWindowProxy +#include "js/SourceText.h" +#include "js/StableStringChars.h" +#include "vm/EnvironmentObject.h" +#include "vm/FrameIter.h" +#include "vm/GlobalObject.h" +#include "vm/Interpreter.h" +#include "vm/JSContext.h" +#include "vm/JSONParser.h" + +#include "gc/Marking-inl.h" +#include "vm/EnvironmentObject-inl.h" +#include "vm/JSContext-inl.h" +#include "vm/Stack-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_; + + Rooted<JSLinearString*> 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 + // <https://tc39.github.io/proposal-json-superset/> JavaScript strings may + // 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, + JSONParser<CharT>::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 v, 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 the global lexical environment. +// +// 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 (!cx->isRuntimeCodeGenEnabled(JS::RuntimeCode::JS, str)) { + 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<GlobalLexicalEnvironmentObject>().global()); + + Rooted<JSLinearString*> 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(); + } + + Rooted<Scope*> 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) + .setDeferDebugMetadata(); + + RootedScript introScript(cx); + + if (evalType == DIRECT_EVAL && IsStrictEvalPC(pc)) { + options.setForceStrictMode(); + } + + if (introducerFilename) { + options.setFileAndLine(filename, 1); + options.setIntroductionInfo(introducerFilename, "eval", lineno, pcOffset); + introScript = maybeScript; + } 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; + if (!srcBuf.initMaybeBorrowed(cx, linearChars)) { + return false; + } + + RootedScript script( + cx, frontend::CompileEvalScript(cx, options, srcBuf, enclosing, env)); + if (!script) { + return false; + } + + RootedValue undefValue(cx); + JS::InstantiateOptions instantiateOptions(options); + if (!JS::UpdateDebugMetadata(cx, script, instantiateOptions, undefValue, + nullptr, introScript, maybeScript)) { + return false; + } + + esg.setNewScript(script); + } + + return ExecuteKernel(cx, esg.script(), env, 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, + Handle<ExtensibleLexicalEnvironmentObject*> env) { + CHECK_THREAD(cx); + cx->check(env); + cx->check(scriptArg); + MOZ_RELEASE_ASSERT(scriptArg->hasNonSyntacticScope()); + + RootedValue rval(cx); + return ExecuteKernel(cx, scriptArg, env, NullFramePtr() /* evalInFrame */, + &rval); +} + +JS_PUBLIC_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); + Rooted<NonSyntacticLexicalEnvironmentObject*> lexicalEnv( + cx, + realm.getOrCreateNonSyntacticLexicalEnvironment(cx, env, varEnv, objArg)); + if (!lexicalEnv) { + return false; + } + + if (!ExecuteInExtensibleLexicalEnvironment(cx, scriptArg, lexicalEnv)) { + return false; + } + + envArg.set(lexicalEnv); + return true; +} + +JS_PUBLIC_API JSObject* JS::NewJSMEnvironment(JSContext* cx) { + RootedObject varEnv(cx, NonSyntacticVariablesObject::create(cx)); + if (!varEnv) { + return nullptr; + } + + // Force the NonSyntacticLexicalEnvironmentObject to be created. + ObjectRealm& realm = ObjectRealm::get(varEnv); + MOZ_ASSERT(!realm.getNonSyntacticLexicalEnvironment(varEnv)); + if (!realm.getOrCreateNonSyntacticLexicalEnvironment(cx, varEnv)) { + return nullptr; + } + + return varEnv; +} + +JS_PUBLIC_API bool JS::ExecuteInJSMEnvironment(JSContext* cx, + HandleScript scriptArg, + HandleObject varEnv) { + RootedObjectVector emptyChain(cx); + return ExecuteInJSMEnvironment(cx, scriptArg, varEnv, emptyChain); +} + +JS_PUBLIC_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()); + + Rooted<ExtensibleLexicalEnvironmentObject*> env( + cx, ExtensibleLexicalEnvironmentObject::forVarEnvironment(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 + // GlobalLexicalEnvironmentObject[this=global] + // NonSyntacticVariablesObject (the JSMEnvironment) + // NonSyntacticLexicalEnvironmentObject[this=nsvo] + // WithEnvironmentObject[target=targetObj] + // NonSyntacticLexicalEnvironmentObject[this=targetObj] (*) + // + // (*) This environment intercepts JSOp::GlobalThis. + + // Wrap the target objects in WithEnvironments. + RootedObject envChain(cx); + if (!js::CreateObjectsForEnvironmentChain(cx, targetObj, env, &envChain)) { + return false; + } + + // See CreateNonSyntacticEnvironmentChain + if (!JSObject::setQualifiedVarObj(cx, envChain)) { + return false; + } + + // Create an extensible lexical environment for the target object. + env = ObjectRealm::get(envChain).getOrCreateNonSyntacticLexicalEnvironment( + cx, envChain); + if (!env) { + return false; + } + } + + return ExecuteInExtensibleLexicalEnvironment(cx, scriptArg, env); +} + +JS_PUBLIC_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_PUBLIC_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>(); +} + +#ifdef JSGC_HASH_TABLE_CHECKS +void RuntimeCaches::checkEvalCacheAfterMinorGC() { + JSContext* cx = TlsContext.get(); + for (auto r = evalCache.all(); !r.empty(); r.popFront()) { + const EvalCacheEntry& entry = r.front(); + CheckGCThingAfterMovingGC(entry.str); + EvalCacheLookup lookup(cx); + lookup.str = entry.str; + lookup.callerScript = entry.callerScript; + lookup.pc = entry.pc; + auto ptr = evalCache.lookup(lookup); + MOZ_RELEASE_ASSERT(ptr.found() && &*ptr == &r.front()); + } +} +#endif |