/* 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 "ScriptLoadHandler.h" #include #include #include "ScriptCompression.h" #include "ScriptLoader.h" #include "ScriptTrace.h" #include "js/Transcoding.h" #include "js/loader/ModuleLoadRequest.h" #include "js/loader/ScriptLoadRequest.h" #include "mozilla/Assertions.h" #include "mozilla/CheckedInt.h" #include "mozilla/DebugOnly.h" #include "mozilla/Encoding.h" #include "mozilla/Logging.h" #include "mozilla/PerfStats.h" #include "mozilla/ScopeExit.h" #include "mozilla/SharedSubResourceCache.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/StaticPrefs_javascript.h" #include "mozilla/Utf8.h" #include "mozilla/Vector.h" #include "mozilla/dom/CacheExpirationTime.h" #include "mozilla/dom/Document.h" #ifdef NIGHTLY_BUILD # include "mozilla/dom/IntegrityPolicyWAICT.h" # include "mozilla/dom/PolicyContainer.h" # include "mozilla/dom/ResourceHasher.h" # include "mozilla/dom/WAICTUtils.h" #endif #include "mozilla/dom/SRICheck.h" #include "mozilla/dom/ScriptDecoding.h" #include "nsCOMPtr.h" #include "nsContentUtils.h" #include "nsDebug.h" #include "nsIAsyncVerifyRedirectCallback.h" #include "nsICacheInfoChannel.h" #include "nsIChannel.h" #include "nsIHttpChannel.h" #include "nsIRequest.h" #include "nsIScriptElement.h" #include "nsIURI.h" #include "nsJSUtils.h" #include "nsMimeTypes.h" #include "nsString.h" #include "nsTArray.h" #include "zlib.h" namespace mozilla::dom { #ifdef NIGHTLY_BUILD using mozilla::waict::gWaictLog; #endif #undef LOG #define LOG(args) \ MOZ_LOG(ScriptLoader::gScriptLoaderLog, mozilla::LogLevel::Debug, args) #define LOG_ENABLED() \ MOZ_LOG_TEST(ScriptLoader::gScriptLoaderLog, mozilla::LogLevel::Debug) ScriptDecoder::ScriptDecoder(const Encoding* aEncoding, ScriptDecoder::BOMHandling handleBOM) { if (handleBOM == BOMHandling::Ignore) { mDecoder = aEncoding->NewDecoderWithoutBOMHandling(); } else { mDecoder = aEncoding->NewDecoderWithBOMRemoval(); } MOZ_ASSERT(mDecoder); } template nsresult ScriptDecoder::DecodeRawDataHelper( JS::loader::ScriptLoadRequest* aRequest, const uint8_t* aData, uint32_t aDataLength, bool aEndOfStream) { CheckedInt needed = ScriptDecoding::MaxBufferLength(mDecoder, aDataLength); if (!needed.isValid()) { return NS_ERROR_OUT_OF_MEMORY; } // Reference to the script source buffer which we will update. JS::loader::ScriptLoadRequest::ScriptTextBuffer& scriptText = aRequest->ScriptText(); uint32_t haveRead = scriptText.length(); CheckedInt capacity = haveRead; capacity += needed.value(); if (!capacity.isValid() || !scriptText.resize(capacity.value())) { return NS_ERROR_OUT_OF_MEMORY; } size_t written = ScriptDecoding::DecodeInto( mDecoder, Span(aData, aDataLength), Span(scriptText.begin() + haveRead, needed.value()), aEndOfStream); MOZ_ASSERT(written <= needed.value()); haveRead += written; MOZ_ASSERT(haveRead <= capacity.value(), "mDecoder produced more data than expected"); MOZ_ALWAYS_TRUE(scriptText.resize(haveRead)); aRequest->SetReceivedScriptTextLength(scriptText.length()); return NS_OK; } nsresult ScriptDecoder::DecodeRawData(JS::loader::ScriptLoadRequest* aRequest, const uint8_t* aData, uint32_t aDataLength, bool aEndOfStream) { if (aRequest->IsUTF16Text()) { return DecodeRawDataHelper(aRequest, aData, aDataLength, aEndOfStream); } return DecodeRawDataHelper(aRequest, aData, aDataLength, aEndOfStream); } ScriptLoadHandler::ScriptLoadHandler( ScriptLoader* aScriptLoader, JS::loader::ScriptLoadRequest* aRequest, UniquePtr&& aSRIDataVerifier) : mScriptLoader(aScriptLoader), mRequest(aRequest), mSRIDataVerifier(std::move(aSRIDataVerifier)), mSRIStatus(NS_OK) { MOZ_ASSERT(aRequest->IsUnknownDataType()); MOZ_ASSERT(aRequest->IsFetching()); } ScriptLoadHandler::~ScriptLoadHandler() = default; NS_IMPL_ISUPPORTS(ScriptLoadHandler, nsIIncrementalStreamLoaderObserver, nsIChannelEventSink, nsIInterfaceRequestor) NS_IMETHODIMP ScriptLoadHandler::OnStartRequest(nsIRequest* aRequest) { mRequest->SetMinimumExpirationTime( nsContentUtils::GetSubresourceCacheExpirationTime(aRequest, mRequest->URI())); #ifdef NIGHTLY_BUILD // Only create a ResourceHasher when we need to enforce WAICT. if (mScriptLoader->WAICTHandlesScripts()) { mResourceHasher = mozilla::dom::ResourceHasher::Init(); } #endif return NS_OK; } NS_IMETHODIMP ScriptLoadHandler::OnIncrementalData(nsIIncrementalStreamLoader* aLoader, nsISupports* aContext, uint32_t aDataLength, const uint8_t* aData, uint32_t* aConsumedLength) { nsCOMPtr channelRequest; aLoader->GetRequest(getter_AddRefs(channelRequest)); nsCOMPtr channel = do_QueryInterface(channelRequest); MOZ_ASSERT(channel, "StreamLoader must have a channel"); auto firstTime = !mPreloadStartNotified; if (!mPreloadStartNotified) { mPreloadStartNotified = true; mRequest->GetScriptLoadContext()->NotifyStart(channelRequest); } if (mRequest->IsCanceled()) { // If request cancelled, ignore any incoming data. *aConsumedLength = aDataLength; return NS_OK; } nsresult rv = NS_OK; if (mRequest->IsUnknownDataType()) { rv = EnsureKnownDataType(channel); NS_ENSURE_SUCCESS(rv, rv); } if (mRequest->IsSerializedStencil() && firstTime) { PerfStats::RecordMeasurementStart(PerfStats::Metric::JSBC_IO_Read); } if (mRequest->IsTextSource()) { #ifdef NIGHTLY_BUILD // If we have a resource hasher, update it with the new data. if (mResourceHasher) { rv = mResourceHasher->Update(aData, aDataLength); NS_ENSURE_SUCCESS(rv, rv); } #endif if (!EnsureDecoder(channel, aData, aDataLength, /* aEndOfStream = */ false)) { return NS_OK; } // Below we will/shall consume entire data chunk. *aConsumedLength = aDataLength; // Decoder has already been initialized. -- trying to decode all loaded // bytes. rv = mDecoder->DecodeRawData(mRequest, aData, aDataLength, /* aEndOfStream = */ false); NS_ENSURE_SUCCESS(rv, rv); // If SRI is required for this load, appending new bytes to the hash. if (mSRIDataVerifier && NS_SUCCEEDED(mSRIStatus)) { mSRIStatus = mSRIDataVerifier->Update(aDataLength, aData); } } else if (mRequest->IsWasmBytes()) { auto& wasmBytes = mRequest->WasmBytes(); if (!wasmBytes.append(aData, aDataLength)) { return NS_ERROR_OUT_OF_MEMORY; } *aConsumedLength = aDataLength; } else { MOZ_ASSERT(mRequest->IsSerializedStencil()); if (!mRequest->SRIAndSerializedStencil().append(aData, aDataLength)) { return NS_ERROR_OUT_OF_MEMORY; } *aConsumedLength = aDataLength; uint32_t sriLength = 0; rv = MaybeDecodeSRI(&sriLength); if (NS_FAILED(rv)) { return channelRequest->Cancel(mScriptLoader->RestartLoad(mRequest)); } if (sriLength) { mRequest->SetSRILength(sriLength); } } return rv; } bool ScriptLoadHandler::TrySetDecoder(nsIChannel* aChannel, const uint8_t* aData, uint32_t aDataLength, bool aEndOfStream) { MOZ_ASSERT(mDecoder == nullptr, "can't have a decoder already if we're trying to set one"); // JavaScript modules are always UTF-8. if (mRequest->IsModuleRequest()) { mDecoder = MakeUnique(UTF_8_ENCODING, ScriptDecoder::BOMHandling::Remove); return true; } // Determine if BOM check should be done. This occurs either // if end-of-stream has been reached, or at least 3 bytes have // been read from input. if (!aEndOfStream && (aDataLength < 3)) { return false; } // Do BOM detection. const Encoding* encoding; std::tie(encoding, std::ignore) = Encoding::ForBOM(Span(aData, aDataLength)); if (encoding) { mDecoder = MakeUnique(encoding, ScriptDecoder::BOMHandling::Remove); return true; } // BOM detection failed, check content stream for charset. nsAutoCString label; if (NS_SUCCEEDED(aChannel->GetContentCharset(label)) && (encoding = Encoding::ForLabel(label))) { mDecoder = MakeUnique(encoding, ScriptDecoder::BOMHandling::Ignore); return true; } // Check the hint charset from the script element or preload // request. nsAutoString hintCharset; if (!mRequest->GetScriptLoadContext()->IsPreload()) { mRequest->GetScriptLoadContext()->GetHintCharset(hintCharset); } else { nsTArray::index_type i = mScriptLoader->mPreloads.IndexOf( mRequest, 0, ScriptLoader::PreloadRequestComparator()); NS_ASSERTION(i != mScriptLoader->mPreloads.NoIndex, "Incorrect preload bookkeeping"); hintCharset = mScriptLoader->mPreloads[i].mCharset; } if ((encoding = Encoding::ForLabel(hintCharset))) { mDecoder = MakeUnique(encoding, ScriptDecoder::BOMHandling::Ignore); return true; } // Get the charset from the charset of the document. if (mScriptLoader->mDocument) { encoding = mScriptLoader->mDocument->GetDocumentCharacterSet(); mDecoder = MakeUnique(encoding, ScriptDecoder::BOMHandling::Ignore); return true; } // Curiously, there are various callers that don't pass aDocument. The // fallback in the old code was ISO-8859-1, which behaved like // windows-1252. mDecoder = MakeUnique(WINDOWS_1252_ENCODING, ScriptDecoder::BOMHandling::Ignore); return true; } nsresult ScriptLoadHandler::MaybeDecodeSRI(uint32_t* sriLength) { *sriLength = 0; if (!mSRIDataVerifier || mSRIDataVerifier->IsInvalid() || mSRIDataVerifier->IsComplete() || NS_FAILED(mSRIStatus)) { return NS_OK; } // Skip until the content is large enough to be decoded. JS::TranscodeBuffer& receivedData = mRequest->SRIAndSerializedStencil(); if (receivedData.length() <= mSRIDataVerifier->DataSummaryLength()) { return NS_OK; } mSRIStatus = mSRIDataVerifier->ImportDataSummary(receivedData.length(), receivedData.begin()); if (NS_FAILED(mSRIStatus)) { // We are unable to decode the hash contained in the alternate data which // contains the serialized Stencil, or it does not use the same algorithm. LOG( ("ScriptLoadHandler::MaybeDecodeSRI, failed to decode SRI, restart " "request")); return mSRIStatus; } *sriLength = mSRIDataVerifier->DataSummaryLength(); MOZ_ASSERT(*sriLength > 0); return NS_OK; } nsresult ScriptLoadHandler::EnsureKnownDataType(nsIChannel* aChannel) { MOZ_ASSERT(mRequest->IsUnknownDataType()); MOZ_ASSERT(mRequest->IsFetching()); #ifdef NIGHTLY_BUILD if (StaticPrefs::javascript_options_experimental_wasm_esm_integration()) { if (mRequest->IsModuleRequest()) { // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-single-module-script // Extract the content-type. If its essence is wasm, we'll attempt to // compile this module as a wasm module. (Steps 13.2, 13.6) nsCOMPtr httpChannel = do_QueryInterface(aChannel); if (httpChannel) { nsAutoCString mimeType; if (NS_SUCCEEDED(httpChannel->GetContentType(mimeType))) { if (nsContentUtils::HasWasmMimeTypeEssence( NS_ConvertUTF8toUTF16(mimeType))) { mRequest->AsModuleRequest()->SetHasWasmMimeTypeEssence(); mRequest->getLoadedScript()->SetWasmBytes(); return NS_OK; } } } } } #endif if (mRequest->mFetchSourceOnly) { mRequest->SetTextSource(mRequest->mLoadContext.get()); TRACE_FOR_TEST(mRequest, "load:source"); return NS_OK; } if (nsCOMPtr cic = do_QueryInterface(aChannel)) { nsAutoCString altDataType; cic->GetAlternativeDataType(altDataType); if (altDataType.Equals(ScriptLoader::BytecodeMimeTypeFor(mRequest))) { mRequest->SetSerializedStencil(); TRACE_FOR_TEST(mRequest, "load:diskcache"); return NS_OK; } MOZ_ASSERT(altDataType.IsEmpty()); } mRequest->SetTextSource(mRequest->mLoadContext.get()); TRACE_FOR_TEST(mRequest, "load:source"); MOZ_ASSERT(!mRequest->IsUnknownDataType()); MOZ_ASSERT(mRequest->IsFetching()); return NS_OK; } NS_IMETHODIMP ScriptLoadHandler::OnStreamComplete(nsIIncrementalStreamLoader* aLoader, nsISupports* aContext, nsresult aStatus, uint32_t aDataLength, const uint8_t* aData) { nsCOMPtr channelRequest; aLoader->GetRequest(getter_AddRefs(channelRequest)); nsCOMPtr channel = do_QueryInterface(channelRequest); #ifndef NIGHTLY_BUILD return DoOnStreamComplete(channel, aStatus, aDataLength, aData); #else if (!mResourceHasher) { return DoOnStreamComplete(channel, aStatus, aDataLength, aData); } nsresult rv = mResourceHasher->Update(aData, aDataLength); if (NS_FAILED(rv)) { MOZ_LOG(gWaictLog, LogLevel::Error, ("ScriptLoadHandler::OnStreamComplete: Failed to update resource " "hash\n")); return rv; } mResourceHasher->Finish(); nsAutoCString computedHash(mResourceHasher->GetHash()); if (computedHash.IsEmpty()) { MOZ_LOG_FMT( gWaictLog, LogLevel::Error, "ScriptLoadHandler::OnStreamComplete: Failed to compute resource hash"); return NS_ERROR_FAILURE; } RefPtr integrity = mScriptLoader->mDocument ? PolicyContainer::GetIntegrityPolicyWAICT( mScriptLoader->mDocument->GetPolicyContainer()) : nullptr; if (!integrity) { MOZ_LOG_FMT( gWaictLog, LogLevel::Error, "ScriptLoadHandler::OnStreamComplete: Could not get IntegrityPolicy"); return NS_ERROR_FAILURE; } integrity->WaitForManifestLoad()->Then( GetCurrentSerialEventTarget(), __func__, [self = RefPtr{this}, channel, integrity = RefPtr{integrity}, computedHash = nsCString(computedHash), aStatus, aDataLength, aData](bool) { MOZ_LOG_FMT(gWaictLog, LogLevel::Debug, "ScriptLoadHandler::OnStreamComplete: WaitForManifestLoad " "promise resolved"); // Using NS_SUCCESS_ADOPTED_DATA we are taking ownership of the data, so // we have to free it after DoOnStreamComplete completes. std::unique_ptr data{aData}; // We have to use the pre-redirect URL for the check. nsCOMPtr originalURI; channel->GetOriginalURI(getter_AddRefs(originalURI)); if (!integrity->MaybeCheckResourceIntegrity( originalURI, IntegrityPolicy::DestinationType::Script, computedHash)) { MOZ_LOG_FMT(gWaictLog, LogLevel::Warning, "ScriptLoadHandler::OnStreamComplete: Wrong script hash"); self->DoOnStreamComplete(channel, NS_ERROR_FAILURE, aDataLength, data.get()); return; } MOZ_LOG_FMT( gWaictLog, LogLevel::Debug, "ScriptLoadHandler::OnStreamComplete: Correct script hash :)"); self->DoOnStreamComplete(channel, aStatus, aDataLength, data.get()); }, [](bool) { MOZ_ASSERT_UNREACHABLE( "WaitForManifestLoad() promise should never be rejected"); }); return NS_SUCCESS_ADOPTED_DATA; #endif } nsresult ScriptLoadHandler::DoOnStreamComplete(nsIChannel* aChannel, nsresult aStatus, uint32_t aDataLength, const uint8_t* aData) { nsresult rv = NS_OK; if (LOG_ENABLED()) { nsAutoCString url; mRequest->URI()->GetAsciiSpec(url); LOG(("ScriptLoadRequest (%p): Stream complete (url = %s)", mRequest.get(), url.get())); } mRequest->mNetworkMetadata = new SubResourceNetworkMetadataHolder(aChannel); aChannel->SetNotificationCallbacks(nullptr); auto firstMessage = !mPreloadStartNotified; if (!mPreloadStartNotified) { mPreloadStartNotified = true; mRequest->GetScriptLoadContext()->NotifyStart(aChannel); } auto notifyStop = MakeScopeExit( [&] { mRequest->GetScriptLoadContext()->NotifyStop(aChannel, rv); }); if (!mRequest->IsCanceled()) { if (mRequest->IsUnknownDataType()) { rv = EnsureKnownDataType(aChannel); NS_ENSURE_SUCCESS(rv, rv); } if (mRequest->IsSerializedStencil() && !firstMessage) { // if firstMessage, then entire stream is in aData, and PerfStats would // measure 0 time PerfStats::RecordMeasurementEnd(PerfStats::Metric::JSBC_IO_Read); } if (mRequest->IsTextSource()) { DebugOnly encoderSet = EnsureDecoder(aChannel, aData, aDataLength, /* aEndOfStream = */ true); MOZ_ASSERT(encoderSet); rv = mDecoder->DecodeRawData(mRequest, aData, aDataLength, /* aEndOfStream = */ true); NS_ENSURE_SUCCESS(rv, rv); LOG(("ScriptLoadRequest (%p): Source length in code units = %u", mRequest.get(), unsigned(mRequest->ScriptTextLength()))); // If SRI is required for this load, appending new bytes to the hash. if (mSRIDataVerifier && NS_SUCCEEDED(mSRIStatus)) { mSRIStatus = mSRIDataVerifier->Update(aDataLength, aData); } } else if (mRequest->IsWasmBytes()) { auto& wasmBytes = mRequest->WasmBytes(); if (!wasmBytes.append(aData, aDataLength)) { return NS_ERROR_OUT_OF_MEMORY; } } else { MOZ_ASSERT(mRequest->IsSerializedStencil()); JS::TranscodeBuffer& buf = mRequest->SRIAndSerializedStencil(); if (!buf.append(aData, aDataLength)) { return NS_ERROR_OUT_OF_MEMORY; } LOG(("ScriptLoadRequest (%p): SRIAndSerializedStencil length = %u", mRequest.get(), unsigned(buf.length()))); // If we abort while decoding the SRI, we fallback on explicitly // requesting the source. Thus, we should not continue in // ScriptLoader::OnStreamComplete, which removes the request from the // waiting lists. // // We calculate the SRI length below. uint32_t unused; rv = MaybeDecodeSRI(&unused); if (NS_FAILED(rv)) { return aChannel->Cancel(mScriptLoader->RestartLoad(mRequest)); } // The serialized stencil always starts with the SRI hash, thus even if // there is no SRI data verifier instance, we still want to skip the hash. uint32_t sriLength; rv = SRICheckDataVerifier::DataSummaryLength(buf.length(), buf.begin(), &sriLength); if (NS_FAILED(rv)) { return aChannel->Cancel(mScriptLoader->RestartLoad(mRequest)); } mRequest->SetSRILength(sriLength); Vector compressed; // mRequest has the compressed data, but will be filled with the // uncompressed data compressed.swap(buf); if (!JS::loader::ScriptBytecodeDecompress( compressed, mRequest->GetSRILength(), buf)) { return NS_ERROR_UNEXPECTED; } } } // Everything went well, keep the CacheInfoChannel alive such that we can // later save the serialized stencil on the cache entry. // we have to mediate and use mRequest. rv = mScriptLoader->OnStreamComplete(aChannel, mRequest, aStatus, mSRIStatus, mSRIDataVerifier.get()); return rv; } NS_IMETHODIMP ScriptLoadHandler::GetInterface(const nsIID& aIID, void** aResult) { if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { return QueryInterface(aIID, aResult); } return NS_NOINTERFACE; } nsresult ScriptLoadHandler::AsyncOnChannelRedirect( nsIChannel* aOld, nsIChannel* aNew, uint32_t aFlags, nsIAsyncVerifyRedirectCallback* aCallback) { mRequest->SetMinimumExpirationTime( nsContentUtils::GetSubresourceCacheExpirationTime(aOld, mRequest->URI())); aCallback->OnRedirectVerifyCallback(NS_OK); return NS_OK; } #undef LOG_ENABLED #undef LOG } // namespace mozilla::dom