mozJSSubScriptLoader.cpp (16334B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "mozJSSubScriptLoader.h" 8 #include "js/experimental/JSStencil.h" 9 #include "mozJSModuleLoader.h" 10 #include "mozJSLoaderUtils.h" 11 12 #include "nsIURI.h" 13 #include "nsIIOService.h" 14 #include "nsIChannel.h" 15 #include "nsIInputStream.h" 16 #include "nsNetCID.h" 17 #include "nsNetUtil.h" 18 19 #include "jsapi.h" 20 #include "jsfriendapi.h" 21 #include "xpcprivate.h" // xpc::OptionsBase 22 #include "js/CompilationAndEvaluation.h" // JS::Compile 23 #include "js/CompileOptions.h" // JS::ReadOnlyCompileOptions, JS::DecodeOptions 24 #include "js/EnvironmentChain.h" // JS::EnvironmentChain 25 #include "js/friend/JSMEnvironment.h" // JS::ExecuteInJSMEnvironment, JS::IsJSMEnvironment 26 #include "js/SourceText.h" // JS::Source{Ownership,Text} 27 #include "js/Wrapper.h" 28 29 #include "mozilla/ContentPrincipal.h" 30 #include "mozilla/ExtensionPolicyService.h" 31 #include "mozilla/dom/ScriptLoader.h" 32 #include "mozilla/ProfilerLabels.h" 33 #include "mozilla/ProfilerMarkers.h" 34 #include "mozilla/ScriptPreloader.h" 35 #include "mozilla/SystemPrincipal.h" 36 #include "mozilla/scache/StartupCache.h" 37 #include "mozilla/scache/StartupCacheUtils.h" 38 #include "mozilla/Utf8.h" // mozilla::Utf8Unit 39 #include "nsContentUtils.h" 40 #include "nsContentSecurityUtils.h" 41 #include "nsString.h" 42 43 using namespace mozilla::scache; 44 using namespace JS; 45 using namespace xpc; 46 using namespace mozilla; 47 using namespace mozilla::dom; 48 49 class MOZ_STACK_CLASS LoadSubScriptOptions : public OptionsBase { 50 public: 51 explicit LoadSubScriptOptions(JSContext* cx = xpc_GetSafeJSContext(), 52 JSObject* options = nullptr) 53 : OptionsBase(cx, options), 54 target(cx), 55 ignoreCache(false), 56 wantReturnValue(false) {} 57 58 virtual bool Parse() override { 59 return ParseObject("target", &target) && 60 ParseBoolean("ignoreCache", &ignoreCache) && 61 ParseBoolean("wantReturnValue", &wantReturnValue); 62 } 63 64 RootedObject target; 65 bool ignoreCache; 66 bool wantReturnValue; 67 }; 68 69 /* load() error msgs, XXX localize? */ 70 #define LOAD_ERROR_NOSERVICE "Error creating IO Service." 71 #define LOAD_ERROR_NOURI "Error creating URI (invalid URL scheme?)" 72 #define LOAD_ERROR_NOSTREAM "Error opening input stream (invalid filename?)" 73 #define LOAD_ERROR_NOCONTENT "ContentLength not available (not a local URL?)" 74 #define LOAD_ERROR_BADCHARSET "Error converting to specified charset" 75 #define LOAD_ERROR_NOSPEC "Failed to get URI spec. This is bad." 76 #define LOAD_ERROR_CONTENTTOOBIG "ContentLength is too large" 77 78 mozJSSubScriptLoader::mozJSSubScriptLoader() = default; 79 80 mozJSSubScriptLoader::~mozJSSubScriptLoader() = default; 81 82 NS_IMPL_ISUPPORTS(mozJSSubScriptLoader, mozIJSSubScriptLoader) 83 84 #define JSSUB_CACHE_PREFIX(aScopeType, aCompilationTarget) \ 85 "jssubloader/" aScopeType "/" aCompilationTarget 86 87 static void SubscriptCachePath(JSContext* cx, nsIURI* uri, 88 JS::HandleObject targetObj, 89 nsACString& cachePath) { 90 // StartupCache must distinguish between non-syntactic vs global when 91 // computing the cache key. 92 if (!JS_IsGlobalObject(targetObj)) { 93 PathifyURI(JSSUB_CACHE_PREFIX("non-syntactic", "script"), uri, cachePath); 94 } else { 95 PathifyURI(JSSUB_CACHE_PREFIX("global", "script"), uri, cachePath); 96 } 97 } 98 99 static void ReportError(JSContext* cx, const nsACString& msg) { 100 NS_ConvertUTF8toUTF16 ucMsg(msg); 101 102 RootedValue exn(cx); 103 if (xpc::NonVoidStringToJsval(cx, ucMsg, &exn)) { 104 JS_SetPendingException(cx, exn); 105 } 106 } 107 108 static void ReportError(JSContext* cx, const char* origMsg, nsIURI* uri) { 109 if (!uri) { 110 ReportError(cx, nsDependentCString(origMsg)); 111 return; 112 } 113 114 nsAutoCString spec; 115 nsresult rv = uri->GetSpec(spec); 116 if (NS_FAILED(rv)) { 117 spec.AssignLiteral("(unknown)"); 118 } 119 120 nsAutoCString msg(origMsg); 121 msg.AppendLiteral(": "); 122 msg.Append(spec); 123 ReportError(cx, msg); 124 } 125 126 static bool EvalStencil(JSContext* cx, HandleObject targetObj, 127 HandleObject loadScope, MutableHandleValue retval, 128 nsIURI* uri, bool storeIntoStartupCache, 129 bool storeIntoPreloadCache, JS::Stencil* stencil) { 130 MOZ_ASSERT(!js::IsWrapper(targetObj)); 131 132 JS::InstantiateOptions options; 133 JS::RootedScript script(cx, 134 JS::InstantiateGlobalStencil(cx, options, stencil)); 135 if (!script) { 136 return false; 137 } 138 139 if (JS_IsGlobalObject(targetObj)) { 140 if (!JS_ExecuteScript(cx, script, retval)) { 141 return false; 142 } 143 } else if (JS::IsJSMEnvironment(targetObj)) { 144 if (!JS::ExecuteInJSMEnvironment(cx, script, targetObj)) { 145 return false; 146 } 147 retval.setUndefined(); 148 } else { 149 JS::EnvironmentChain envChain(cx, JS::SupportUnscopables::No); 150 if (!envChain.append(targetObj)) { 151 return false; 152 } 153 if (!loadScope) { 154 // A null loadScope means we are cross-realm. In this case, we should 155 // check the target isn't in the JSM loader shared-global or we will 156 // contaminate all JSMs in the realm. 157 // 158 // NOTE: If loadScope is already a shared-global JSM, we can't 159 // determine which JSM the target belongs to and have to assume it 160 // is in our JSM. 161 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED 162 JSObject* targetGlobal = JS::GetNonCCWObjectGlobal(targetObj); 163 MOZ_DIAGNOSTIC_ASSERT( 164 !mozJSModuleLoader::Get()->IsLoaderGlobal(targetGlobal), 165 "Don't load subscript into target in a shared-global JSM"); 166 #endif 167 if (!JS_ExecuteScript(cx, envChain, script, retval)) { 168 return false; 169 } 170 } else if (JS_IsGlobalObject(loadScope)) { 171 if (!JS_ExecuteScript(cx, envChain, script, retval)) { 172 return false; 173 } 174 } else { 175 MOZ_ASSERT(JS::IsJSMEnvironment(loadScope)); 176 if (!JS::ExecuteInJSMEnvironment(cx, script, loadScope, envChain)) { 177 return false; 178 } 179 retval.setUndefined(); 180 } 181 } 182 183 JSAutoRealm rar(cx, targetObj); 184 if (!JS_WrapValue(cx, retval)) { 185 return false; 186 } 187 188 if (script && (storeIntoStartupCache || storeIntoPreloadCache)) { 189 nsAutoCString cachePath; 190 SubscriptCachePath(cx, uri, targetObj, cachePath); 191 192 nsCString uriStr; 193 if (storeIntoPreloadCache && NS_SUCCEEDED(uri->GetSpec(uriStr))) { 194 ScriptPreloader::GetSingleton().NoteStencil(uriStr, cachePath, stencil); 195 } 196 197 if (storeIntoStartupCache) { 198 JSAutoRealm ar(cx, script); 199 WriteCachedStencil(StartupCache::GetSingleton(), cachePath, cx, stencil); 200 } 201 } 202 203 return true; 204 } 205 206 bool mozJSSubScriptLoader::ReadStencil( 207 JS::Stencil** stencilOut, nsIURI* uri, JSContext* cx, 208 const JS::ReadOnlyCompileOptions& options, nsIIOService* serv, 209 bool useCompilationScope) { 210 // We create a channel and call SetContentType, to avoid expensive MIME type 211 // lookups (bug 632490). 212 nsCOMPtr<nsIChannel> chan; 213 nsCOMPtr<nsIInputStream> instream; 214 nsresult rv; 215 rv = NS_NewChannel(getter_AddRefs(chan), uri, 216 nsContentUtils::GetSystemPrincipal(), 217 nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, 218 nsIContentPolicy::TYPE_OTHER, 219 nullptr, // nsICookieJarSettings 220 nullptr, // PerformanceStorage 221 nullptr, // aLoadGroup 222 nullptr, // aCallbacks 223 nsIRequest::LOAD_NORMAL, serv); 224 225 if (NS_SUCCEEDED(rv)) { 226 chan->SetContentType("application/javascript"_ns); 227 rv = chan->Open(getter_AddRefs(instream)); 228 } 229 230 if (NS_FAILED(rv)) { 231 ReportError(cx, LOAD_ERROR_NOSTREAM, uri); 232 return false; 233 } 234 235 int64_t len = -1; 236 237 rv = chan->GetContentLength(&len); 238 if (NS_FAILED(rv)) { 239 ReportError(cx, LOAD_ERROR_NOCONTENT, uri); 240 return false; 241 } 242 243 if (len > INT32_MAX) { 244 ReportError(cx, LOAD_ERROR_CONTENTTOOBIG, uri); 245 return false; 246 } 247 248 nsCString buf; 249 rv = NS_ReadInputStreamToString(instream, buf, len); 250 NS_ENSURE_SUCCESS(rv, false); 251 252 if (len < 0) { 253 len = buf.Length(); 254 } 255 256 Maybe<JSAutoRealm> ar; 257 258 // Note that when using the ScriptPreloader cache with loadSubScript, there 259 // will be a side-effect of keeping the global that the script was compiled 260 // for alive. See note above in EvalScript(). 261 // 262 // This will compile the script in XPConnect compilation scope. When the 263 // script is evaluated, it will be cloned into the target scope to be 264 // executed, avoiding leaks on the first session when we don't have a 265 // startup cache. 266 if (useCompilationScope) { 267 ar.emplace(cx, xpc::CompilationScope()); 268 } 269 270 JS::SourceText<Utf8Unit> srcBuf; 271 if (!srcBuf.init(cx, buf.get(), len, JS::SourceOwnership::Borrowed)) { 272 return false; 273 } 274 275 RefPtr<JS::Stencil> stencil = 276 JS::CompileGlobalScriptToStencil(cx, options, srcBuf); 277 stencil.forget(stencilOut); 278 return *stencilOut; 279 } 280 281 NS_IMETHODIMP 282 mozJSSubScriptLoader::LoadSubScript(const nsAString& url, HandleValue target, 283 JSContext* cx, MutableHandleValue retval) { 284 /* 285 * Loads a local url, referring to UTF-8-encoded data, and evals it into the 286 * current cx. Synchronous. ChromeUtils.compileScript() should be used for 287 * async loads. 288 * url: The url to load. Must be local so that it can be loaded 289 * synchronously. 290 * targetObj: Optional object to eval the script onto (defaults to context 291 * global) 292 * returns: Whatever jsval the script pointed to by the url returns. 293 * Should ONLY (O N L Y !) be called from JavaScript code. 294 */ 295 LoadSubScriptOptions options(cx); 296 options.target = target.isObject() ? &target.toObject() : nullptr; 297 return DoLoadSubScriptWithOptions(url, options, cx, retval); 298 } 299 300 NS_IMETHODIMP 301 mozJSSubScriptLoader::LoadSubScriptWithOptions(const nsAString& url, 302 HandleValue optionsVal, 303 JSContext* cx, 304 MutableHandleValue retval) { 305 if (!optionsVal.isObject()) { 306 return NS_ERROR_INVALID_ARG; 307 } 308 309 LoadSubScriptOptions options(cx, &optionsVal.toObject()); 310 if (!options.Parse()) { 311 return NS_ERROR_INVALID_ARG; 312 } 313 314 return DoLoadSubScriptWithOptions(url, options, cx, retval); 315 } 316 317 static bool CheckAllowedURI(JSContext* aCx, nsIURI* aURI) { 318 // Trusted schemes like moz-src: are always ok. 319 if (nsContentSecurityUtils::IsTrustedScheme(aURI)) { 320 return true; 321 } 322 323 // TODO(Bug 1974213) Block file: and jar: schemes. 324 // TODO(Bug 1976115) experiment_apis scripts are run from jar:file: URL 325 // instead of moz-extension:-URL 326 if (aURI->SchemeIs("file") || aURI->SchemeIs("jar")) { 327 return true; 328 } 329 330 // TODO(Bug 1974691) Don't load subscripts from un-privileged moz-extension: 331 if (aURI->SchemeIs("moz-extension")) { 332 return true; 333 } 334 335 ReportError(aCx, "Trying to load untrusted URI.", aURI); 336 return false; 337 } 338 339 nsresult mozJSSubScriptLoader::DoLoadSubScriptWithOptions( 340 const nsAString& url, LoadSubScriptOptions& options, JSContext* cx, 341 MutableHandleValue retval) { 342 nsresult rv = NS_OK; 343 RootedObject targetObj(cx); 344 RootedObject loadScope(cx); 345 mozJSModuleLoader* loader = mozJSModuleLoader::Get(); 346 loader->FindTargetObject(cx, &loadScope); 347 348 if (options.target) { 349 targetObj = options.target; 350 } else { 351 targetObj = loadScope; 352 } 353 354 targetObj = JS_FindCompilationScope(cx, targetObj); 355 if (!targetObj || !loadScope) { 356 return NS_ERROR_FAILURE; 357 } 358 359 MOZ_ASSERT(!js::IsWrapper(targetObj), "JS_FindCompilationScope must unwrap"); 360 361 if (js::GetNonCCWObjectRealm(loadScope) != 362 js::GetNonCCWObjectRealm(targetObj)) { 363 loadScope = nullptr; 364 } 365 366 /* load up the url. From here on, failures are reflected as ``custom'' 367 * js exceptions */ 368 nsCOMPtr<nsIURI> uri; 369 nsAutoCString uriStr; 370 nsAutoCString scheme; 371 372 // Figure out who's calling us 373 JS::AutoFilename filename; 374 if (!JS::DescribeScriptedCaller(&filename, cx)) { 375 // No scripted frame means we don't know who's calling, bail. 376 return NS_ERROR_FAILURE; 377 } 378 379 JSAutoRealm ar(cx, targetObj); 380 381 nsCOMPtr<nsIIOService> serv = do_GetService(NS_IOSERVICE_CONTRACTID); 382 if (!serv) { 383 ReportError(cx, nsLiteralCString(LOAD_ERROR_NOSERVICE)); 384 return NS_OK; 385 } 386 387 NS_LossyConvertUTF16toASCII asciiUrl(url); 388 const nsDependentCSubstring profilerUrl = 389 Substring(asciiUrl, 0, std::min(size_t(128), asciiUrl.Length())); 390 AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING_NONSENSITIVE( 391 "mozJSSubScriptLoader::DoLoadSubScriptWithOptions", OTHER, profilerUrl); 392 AUTO_PROFILER_MARKER_TEXT("SubScript", JS, 393 MarkerOptions(MarkerStack::Capture(), 394 MarkerInnerWindowIdFromJSContext(cx)), 395 profilerUrl); 396 397 // Make sure to explicitly create the URI, since we'll need the 398 // canonicalized spec. 399 rv = NS_NewURI(getter_AddRefs(uri), asciiUrl); 400 if (NS_FAILED(rv)) { 401 ReportError(cx, nsLiteralCString(LOAD_ERROR_NOURI)); 402 return NS_OK; 403 } 404 405 rv = uri->GetSpec(uriStr); 406 if (NS_FAILED(rv)) { 407 ReportError(cx, nsLiteralCString(LOAD_ERROR_NOSPEC)); 408 return NS_OK; 409 } 410 411 if (!CheckAllowedURI(cx, uri)) { 412 return NS_OK; 413 } 414 415 // Suppress caching if we're compiling as content 416 bool useCompilationScope = false; 417 auto* principal = BasePrincipal::Cast(GetObjectPrincipal(targetObj)); 418 bool isSystem = principal->Is<SystemPrincipal>(); 419 if (!isSystem && principal->Is<ContentPrincipal>()) { 420 nsAutoCString scheme; 421 principal->GetScheme(scheme); 422 423 // We want to enable caching for scripts with Activity Stream's 424 // codebase URLs. 425 if (scheme.EqualsLiteral("about")) { 426 nsAutoCString filePath; 427 principal->GetFilePath(filePath); 428 429 useCompilationScope = filePath.EqualsLiteral("home") || 430 filePath.EqualsLiteral("newtab") || 431 filePath.EqualsLiteral("welcome"); 432 isSystem = true; 433 } 434 } 435 bool ignoreCache = options.ignoreCache || !isSystem; 436 437 StartupCache* cache = ignoreCache ? nullptr : StartupCache::GetSingleton(); 438 439 nsAutoCString cachePath; 440 SubscriptCachePath(cx, uri, targetObj, cachePath); 441 442 JS::DecodeOptions decodeOptions; 443 ScriptPreloader::FillDecodeOptionsForCachedStencil(decodeOptions); 444 445 RefPtr<JS::Stencil> stencil; 446 if (!options.ignoreCache) { 447 if (!options.wantReturnValue) { 448 // NOTE: If we need the return value, we cannot use ScriptPreloader. 449 stencil = ScriptPreloader::GetSingleton().GetCachedStencil( 450 cx, decodeOptions, cachePath); 451 } 452 if (!stencil && cache) { 453 rv = ReadCachedStencil(cache, cachePath, cx, decodeOptions, 454 getter_AddRefs(stencil)); 455 if (NS_FAILED(rv) || !stencil) { 456 JS_ClearPendingException(cx); 457 } 458 } 459 } 460 461 bool storeIntoStartupCache = false; 462 if (!stencil) { 463 // Store into startup cache only when the script isn't come from any cache. 464 storeIntoStartupCache = cache; 465 466 JS::CompileOptions compileOptions(cx); 467 ScriptPreloader::FillCompileOptionsForCachedStencil(compileOptions); 468 compileOptions.setFileAndLine(uriStr.get(), 1); 469 compileOptions.setNonSyntacticScope(!JS_IsGlobalObject(targetObj)); 470 471 if (options.wantReturnValue) { 472 compileOptions.setNoScriptRval(false); 473 } 474 475 if (!ReadStencil(getter_AddRefs(stencil), uri, cx, compileOptions, serv, 476 useCompilationScope)) { 477 return NS_OK; 478 } 479 480 #ifdef DEBUG 481 // The above shouldn't touch any options for instantiation. 482 JS::InstantiateOptions instantiateOptions(compileOptions); 483 instantiateOptions.assertDefault(); 484 #endif 485 } 486 487 // As a policy choice, we don't store scripts that want return values 488 // into the preload cache. 489 bool storeIntoPreloadCache = !ignoreCache && !options.wantReturnValue; 490 491 (void)EvalStencil(cx, targetObj, loadScope, retval, uri, 492 storeIntoStartupCache, storeIntoPreloadCache, stencil); 493 return NS_OK; 494 }