Files
sousa-gecko/dom/script/ScriptLoadHandler.cpp
T
Tooru Fujisawa b1578b8a63 Bug 2036849 - Part 8: Rewrite the IsTextSource and IsSerializedStencil accessors for the ScriptLoadRequest to avoid accidental use after converting to cache. r=nbp,dom-worker-reviewers,asuth
LoadedScript::{IsTextSource,IsSerializedStencil} accessors can return
false once it's converted to cache.
Most of the consumers need the flag to branch on the server response, before
converting to cache.
Add assertions to avoid accidental use.

Differential Revision: https://phabricator.services.mozilla.com/D299926
2026-05-13 17:09:56 +00:00

623 lines
21 KiB
C++

/* 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 <stdlib.h>
#include <utility>
#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 <typename Unit>
nsresult ScriptDecoder::DecodeRawDataHelper(
JS::loader::ScriptLoadRequest* aRequest, const uint8_t* aData,
uint32_t aDataLength, bool aEndOfStream) {
CheckedInt<size_t> needed =
ScriptDecoding<Unit>::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<Unit>& scriptText =
aRequest->ScriptText<Unit>();
uint32_t haveRead = scriptText.length();
CheckedInt<uint32_t> capacity = haveRead;
capacity += needed.value();
if (!capacity.isValid() || !scriptText.resize(capacity.value())) {
return NS_ERROR_OUT_OF_MEMORY;
}
size_t written = ScriptDecoding<Unit>::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<char16_t>(aRequest, aData, aDataLength,
aEndOfStream);
}
return DecodeRawDataHelper<Utf8Unit>(aRequest, aData, aDataLength,
aEndOfStream);
}
ScriptLoadHandler::ScriptLoadHandler(
ScriptLoader* aScriptLoader, JS::loader::ScriptLoadRequest* aRequest,
UniquePtr<SRICheckDataVerifier>&& aSRIDataVerifier)
: mScriptLoader(aScriptLoader),
mRequest(aRequest),
mSRIDataVerifier(std::move(aSRIDataVerifier)),
mSRIStatus(NS_OK) {
MOZ_ASSERT(aRequest->IsUnknownDataType());
MOZ_ASSERT(!aRequest->IsRetrievedFromMemoryCache());
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<nsIRequest> channelRequest;
aLoader->GetRequest(getter_AddRefs(channelRequest));
nsCOMPtr<nsIChannel> 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->IsRetrievedAsSerializedStencil() && firstTime) {
PerfStats::RecordMeasurementStart(PerfStats::Metric::JSBC_IO_Read);
}
if (mRequest->IsFetchedAsTextSource()) {
#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->IsRetrievedAsSerializedStencil());
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<ScriptDecoder>(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<ScriptDecoder>(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<ScriptDecoder>(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<ScriptLoader::PreloadInfo>::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<ScriptDecoder>(encoding, ScriptDecoder::BOMHandling::Ignore);
return true;
}
// Get the charset from the charset of the document.
if (mScriptLoader->mDocument) {
encoding = mScriptLoader->mDocument->GetDocumentCharacterSet();
mDecoder =
MakeUnique<ScriptDecoder>(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<ScriptDecoder>(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->IsRetrievedFromMemoryCache());
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<nsIHttpChannel> 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<nsICacheInfoChannel> 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<nsIRequest> channelRequest;
aLoader->GetRequest(getter_AddRefs(channelRequest));
nsCOMPtr<nsIChannel> 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<IntegrityPolicyWAICT> 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<const uint8_t> data{aData};
// We have to use the pre-redirect URL for the check.
nsCOMPtr<nsIURI> 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->IsRetrievedAsSerializedStencil() && !firstMessage) {
// if firstMessage, then entire stream is in aData, and PerfStats would
// measure 0 time
PerfStats::RecordMeasurementEnd(PerfStats::Metric::JSBC_IO_Read);
}
if (mRequest->IsFetchedAsTextSource()) {
DebugOnly<bool> 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->IsRetrievedAsSerializedStencil());
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<uint8_t> 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