/* 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 "SharedScriptCache.h" #include "ScriptLoadHandler.h" // ScriptLoadHandler #include "ScriptLoader.h" // ScriptLoader #include "ScriptTrace.h" // TRACE_FOR_TEST #include "js/RootingAPI.h" // JS::MutableHandle #include "js/Value.h" // JS::Value #include "js/experimental/CompileScript.h" // JS::FrontendContext, JS::NewFrontendContext, JS::DestroyFrontendContext #include "js/experimental/JSStencil.h" // JS::GetScriptSourceText #include "mozilla/HalTypes.h" // hal::CONTENT_PROCESS_ID_* #include "mozilla/Maybe.h" // Maybe, Some, Nothing #include "mozilla/TaskController.h" // TaskController, Task #include "mozilla/dom/ContentChild.h" // dom::ContentChild #include "mozilla/dom/ContentParent.h" // dom::ContentParent #include "mozilla/glean/DomMetrics.h" // mozilla::glean::dom::* #include "nsIMemoryReporter.h" // nsIMemoryReporter, MOZ_DEFINE_MALLOC_SIZE_OF, RegisterWeakMemoryReporter, UnregisterWeakMemoryReporter, MOZ_COLLECT_REPORT, KIND_HEAP, UNITS_BYTES #include "nsIPrefBranch.h" // nsIPrefBranch, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID #include "nsIPrefService.h" // NS_PREFSERVICE_CONTRACTID #include "nsIPrincipal.h" // nsIPrincipal #include "nsIPropertyBag2.h" // nsIPropertyBag2 #include "nsISupportsImpl.h" // NS_IMPL_ISUPPORTS #include "nsStringFwd.h" // nsACString namespace mozilla::dom { ScriptHashKey::ScriptHashKey( ScriptLoader* aLoader, const JS::loader::ScriptLoadRequest* aRequest, mozilla::dom::ReferrerPolicy aReferrerPolicy, const JS::loader::ScriptFetchOptions* aFetchOptions, const nsCOMPtr aURI) : PLDHashEntryHdr(), mURI(aURI), mPartitionPrincipal(aLoader->PartitionedPrincipal()), mLoaderPrincipal(aLoader->LoaderPrincipal()), mKind(aRequest->mKind), mCORSMode(aFetchOptions->mCORSMode), mReferrerPolicy(aReferrerPolicy) { if (mKind == JS::loader::ScriptKind::eClassic) { if (aRequest->GetScriptLoadContext()->HasScriptElement()) { aRequest->GetScriptLoadContext()->GetHintCharset(mHintCharset); } } MOZ_COUNT_CTOR(ScriptHashKey); } ScriptHashKey::ScriptHashKey(ScriptLoader* aLoader, const JS::loader::ScriptLoadRequest* aRequest, const JS::loader::LoadedScript* aLoadedScript) : ScriptHashKey(aLoader, aRequest, aLoadedScript->ReferrerPolicy(), aLoadedScript->GetFetchOptions(), aLoadedScript->GetURI()) { } ScriptHashKey::ScriptHashKey(const ScriptLoadData& aLoadData) : ScriptHashKey(aLoadData.CacheKey()) {} bool ScriptHashKey::KeyEquals(const ScriptHashKey& aKey) const { { bool eq; if (NS_FAILED(mURI->Equals(aKey.mURI, &eq)) || !eq) { return false; } } if (!mPartitionPrincipal->Equals(aKey.mPartitionPrincipal)) { return false; } // NOTE: mLoaderPrincipal is only for the SharedSubResourceCache logic, // not for comparison here. if (mKind != aKey.mKind) { return false; } if (mCORSMode != aKey.mCORSMode) { return false; } if (mReferrerPolicy != aKey.mReferrerPolicy) { return false; } // NOTE: module always use UTF-8. if (mKind == JS::loader::ScriptKind::eClassic) { if (mHintCharset != aKey.mHintCharset) { return false; } } return true; } void ScriptHashKey::ToStringForLookup(nsACString& aResult) { aResult.Truncate(); aResult.AppendLiteral("SharedScriptCache:"); switch (mKind) { case JS::loader::ScriptKind::eClassic: aResult.Append('c'); break; case JS::loader::ScriptKind::eModule: aResult.Append('m'); break; case JS::loader::ScriptKind::eEvent: aResult.Append('e'); break; case JS::loader::ScriptKind::eImportMap: aResult.Append('i'); break; } switch (mCORSMode) { case CORS_NONE: aResult.Append('n'); break; case CORS_ANONYMOUS: aResult.Append('a'); break; case CORS_USE_CREDENTIALS: aResult.Append('c'); break; } switch (mReferrerPolicy) { case ReferrerPolicy::_empty: aResult.Append('_'); break; case ReferrerPolicy::No_referrer: aResult.Append('n'); break; case ReferrerPolicy::No_referrer_when_downgrade: aResult.Append('d'); break; case ReferrerPolicy::Origin: aResult.Append('o'); break; case ReferrerPolicy::Origin_when_cross_origin: aResult.Append('c'); break; case ReferrerPolicy::Unsafe_url: aResult.Append('u'); break; case ReferrerPolicy::Same_origin: aResult.Append('s'); break; case ReferrerPolicy::Strict_origin: aResult.Append('S'); break; case ReferrerPolicy::Strict_origin_when_cross_origin: aResult.Append('C'); break; } nsAutoCString partitionPrincipal; BasePrincipal::Cast(mPartitionPrincipal)->ToJSON(partitionPrincipal); aResult.Append(partitionPrincipal); } /* static */ Maybe ScriptHashKey::FromStringsForLookup( const nsACString& aKey, const nsACString& aURI, const nsACString& aHintCharset) { if (aKey.Length() < 22) { return Nothing(); } if (Substring(aKey, 0, 18) != "SharedScriptCache:") { return Nothing(); } JS::loader::ScriptKind kind; char kindChar = aKey[18]; if (kindChar == 'c') { kind = JS::loader::ScriptKind::eClassic; } else if (kindChar == 'm') { kind = JS::loader::ScriptKind::eModule; } else if (kindChar == 'e') { kind = JS::loader::ScriptKind::eEvent; } else if (kindChar == 'i') { kind = JS::loader::ScriptKind::eImportMap; } else { return Nothing(); } CORSMode corsMode; char corsModeChar = aKey[19]; if (corsModeChar == 'n') { corsMode = CORS_NONE; } else if (corsModeChar == 'a') { corsMode = CORS_ANONYMOUS; } else if (corsModeChar == 'c') { corsMode = CORS_USE_CREDENTIALS; } else { return Nothing(); } mozilla::dom::ReferrerPolicy referrerPolicy; char referrerPolicyChar = aKey[20]; if (referrerPolicyChar == '_') { referrerPolicy = ReferrerPolicy::_empty; } else if (referrerPolicyChar == 'n') { referrerPolicy = ReferrerPolicy::No_referrer; } else if (referrerPolicyChar == 'd') { referrerPolicy = ReferrerPolicy::No_referrer_when_downgrade; } else if (referrerPolicyChar == 'o') { referrerPolicy = ReferrerPolicy::Origin; } else if (referrerPolicyChar == 'c') { referrerPolicy = ReferrerPolicy::Origin_when_cross_origin; } else if (referrerPolicyChar == 'u') { referrerPolicy = ReferrerPolicy::Unsafe_url; } else if (referrerPolicyChar == 's') { referrerPolicy = ReferrerPolicy::Same_origin; } else if (referrerPolicyChar == 'S') { referrerPolicy = ReferrerPolicy::Strict_origin; } else if (referrerPolicyChar == 'C') { referrerPolicy = ReferrerPolicy::Strict_origin_when_cross_origin; } else { return Nothing(); } nsCOMPtr partitionPrincipal = BasePrincipal::FromJSON(Substring(aKey, 21)); if (!partitionPrincipal) { return Nothing(); } nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), aURI); if (NS_FAILED(rv)) { return Nothing(); } return Some(ScriptHashKey(uri, partitionPrincipal, kind, corsMode, referrerPolicy, NS_ConvertUTF8toUTF16(aHintCharset))); } NS_IMPL_ISUPPORTS(ScriptLoadData, nsISupports) ScriptLoadData::ScriptLoadData(ScriptLoader* aLoader, JS::loader::ScriptLoadRequest* aRequest, JS::loader::LoadedScript* aLoadedScript) : mExpirationTime(aRequest->ExpirationTime()), mLoader(aLoader), mKey(aLoader, aRequest, aLoadedScript), mLoadedScript(aLoadedScript), mNetworkMetadata(aRequest->mNetworkMetadata) {} NS_IMPL_ISUPPORTS(SharedScriptCache, nsIMemoryReporter, nsIObserver) MOZ_DEFINE_MALLOC_SIZE_OF(SharedScriptCacheMallocSizeOf) SharedScriptCache::SharedScriptCache() = default; void SharedScriptCache::Init() { RegisterWeakMemoryReporter(this); // URL classification (tracking protection etc) are handled inside // nsHttpChannel. // The cache reflects the policy for whether to block or not, and once // the policy is modified, we should discard the cache, to avoid running // a cached script which is supposed to be blocked. auto ClearCache = [](const char*, void*) { Clear(); }; Preferences::RegisterPrefixCallback(ClearCache, "urlclassifier."); Preferences::RegisterCallback(ClearCache, "privacy.trackingprotection.enabled"); } SharedScriptCache::~SharedScriptCache() { UnregisterWeakMemoryReporter(this); } bool SharedScriptCache::ShouldIgnoreMemoryPressure() { // During the automated testing, we need to ignore the memory pressure, // in order to get the deterministic result. return !StaticPrefs:: dom_script_loader_experimental_navigation_cache_check_memory_pressure(); } void SharedScriptCache::LoadCompleted(SharedScriptCache* aCache, ScriptLoadData& aData) {} NS_IMETHODIMP SharedScriptCache::CollectReports(nsIHandleReportCallback* aHandleReport, nsISupports* aData, bool aAnonymize) { MOZ_COLLECT_REPORT("explicit/js-non-window/cache", KIND_HEAP, UNITS_BYTES, SharedScriptCacheMallocSizeOf(this) + SizeOfExcludingThis(SharedScriptCacheMallocSizeOf), "Memory used for SharedScriptCache to share script " "across documents"); return NS_OK; } /* static */ void SharedScriptCache::Clear(const Maybe& aChrome, const Maybe>& aPrincipal, const Maybe& aSchemelessSite, const Maybe& aPattern, const Maybe& aURL) { using ContentParent = dom::ContentParent; if (XRE_IsParentProcess()) { for (auto* cp : ContentParent::AllProcesses(ContentParent::eLive)) { (void)cp->SendClearScriptCache(aChrome, aPrincipal, aSchemelessSite, aPattern, aURL); } } if (sSingleton) { sSingleton->ClearInProcess(aChrome, aPrincipal, aSchemelessSite, aPattern, aURL); } } /* static */ void SharedScriptCache::Invalidate() { using ContentParent = dom::ContentParent; if (XRE_IsParentProcess()) { for (auto* cp : ContentParent::AllProcesses(ContentParent::eLive)) { (void)cp->SendInvalidateScriptCache(); } } if (sSingleton) { sSingleton->InvalidateInProcess(); } TRACE_FOR_TEST_0("memorycache:invalidate"); } /* static */ bool SharedScriptCache::GetCachedScriptSource( JSContext* aCx, const nsACString& aKey, const nsACString& aURI, const nsACString& aHintCharset, JS::MutableHandle aRetval) { if (!sSingleton) { aRetval.setUndefined(); return true; } Maybe maybeKey = ScriptHashKey::FromStringsForLookup(aKey, aURI, aHintCharset); if (!maybeKey) { aRetval.setUndefined(); return true; } JS::Stencil* stencil = nullptr; if (auto lookup = sSingleton->mComplete.Lookup(*maybeKey)) { JS::loader::LoadedScript* loadedScript = lookup.Data().mResource; // NOTE: We don't check the SRIMetadata here, because this is not a // request from