diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /js/xpconnect/loader | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/xpconnect/loader')
32 files changed, 9016 insertions, 0 deletions
diff --git a/js/xpconnect/loader/AutoMemMap.cpp b/js/xpconnect/loader/AutoMemMap.cpp new file mode 100644 index 0000000000..f8c75ea445 --- /dev/null +++ b/js/xpconnect/loader/AutoMemMap.cpp @@ -0,0 +1,157 @@ +/* -*- 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 "AutoMemMap.h" +#include "ScriptPreloader-inl.h" + +#include "mozilla/Unused.h" +#include "mozilla/ipc/FileDescriptor.h" +#include "nsIFile.h" + +#include <private/pprio.h> + +namespace mozilla { +namespace loader { + +using namespace mozilla::ipc; + +AutoMemMap::~AutoMemMap() { reset(); } + +FileDescriptor AutoMemMap::cloneFileDescriptor() const { + if (fd.get()) { + auto handle = + FileDescriptor::PlatformHandleType(PR_FileDesc2NativeHandle(fd.get())); + return FileDescriptor(handle); + } + return FileDescriptor(); +} + +Result<Ok, nsresult> AutoMemMap::init(nsIFile* file, int flags, int mode, + PRFileMapProtect prot) { + MOZ_ASSERT(!fd); + + MOZ_TRY(file->OpenNSPRFileDesc(flags, mode, &fd.rwget())); + + return initInternal(prot); +} + +Result<Ok, nsresult> AutoMemMap::init(const FileDescriptor& file, + PRFileMapProtect prot, size_t maybeSize) { + MOZ_ASSERT(!fd); + if (!file.IsValid()) { + return Err(NS_ERROR_INVALID_ARG); + } + + auto handle = file.ClonePlatformHandle(); + + fd = PR_ImportFile(PROsfd(handle.get())); + if (!fd) { + return Err(NS_ERROR_FAILURE); + } + Unused << handle.release(); + + return initInternal(prot, maybeSize); +} + +Result<Ok, nsresult> AutoMemMap::initInternal(PRFileMapProtect prot, + size_t maybeSize) { + MOZ_ASSERT(!fileMap); + MOZ_ASSERT(!addr); + + if (maybeSize > 0) { + // Some OSes' shared memory objects can't be stat()ed, either at + // all (Android) or without loosening the sandbox (Mac) so just + // use the size. + size_ = maybeSize; + } else { + // But if we don't have the size, assume it's a regular file and + // ask for it. + PRFileInfo64 fileInfo; + MOZ_TRY(PR_GetOpenFileInfo64(fd.get(), &fileInfo)); + + if (fileInfo.size > UINT32_MAX) { + return Err(NS_ERROR_INVALID_ARG); + } + size_ = fileInfo.size; + } + + fileMap = PR_CreateFileMap(fd, 0, prot); + if (!fileMap) { + return Err(NS_ERROR_FAILURE); + } + + addr = PR_MemMap(fileMap, 0, size_); + if (!addr) { + return Err(NS_ERROR_FAILURE); + } + + return Ok(); +} + +#ifdef XP_WIN + +Result<Ok, nsresult> AutoMemMap::initWithHandle(const FileDescriptor& file, + size_t size, + PRFileMapProtect prot) { + MOZ_ASSERT(!fd); + MOZ_ASSERT(!handle_); + if (!file.IsValid()) { + return Err(NS_ERROR_INVALID_ARG); + } + + handle_ = file.ClonePlatformHandle().release(); + + MOZ_ASSERT(!addr); + + size_ = size; + + addr = MapViewOfFile( + handle_, prot == PR_PROT_READONLY ? FILE_MAP_READ : FILE_MAP_ALL_ACCESS, + 0, 0, size); + if (!addr) { + return Err(NS_ERROR_FAILURE); + } + + return Ok(); +} + +FileDescriptor AutoMemMap::cloneHandle() const { + return FileDescriptor(handle_); +} + +#else + +Result<Ok, nsresult> AutoMemMap::initWithHandle(const FileDescriptor& file, + size_t size, + PRFileMapProtect prot) { + MOZ_DIAGNOSTIC_ASSERT(size > 0); + return init(file, prot, size); +} + +FileDescriptor AutoMemMap::cloneHandle() const { return cloneFileDescriptor(); } + +#endif + +void AutoMemMap::reset() { + if (addr && !persistent_) { + Unused << NS_WARN_IF(PR_MemUnmap(addr, size()) != PR_SUCCESS); + addr = nullptr; + } + if (fileMap) { + Unused << NS_WARN_IF(PR_CloseFileMap(fileMap) != PR_SUCCESS); + fileMap = nullptr; + } +#ifdef XP_WIN + if (handle_) { + CloseHandle(handle_); + handle_ = nullptr; + } +#endif + fd.dispose(); +} + +} // namespace loader +} // namespace mozilla diff --git a/js/xpconnect/loader/AutoMemMap.h b/js/xpconnect/loader/AutoMemMap.h new file mode 100644 index 0000000000..54180d09e3 --- /dev/null +++ b/js/xpconnect/loader/AutoMemMap.h @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#ifndef loader_AutoMemMap_h +#define loader_AutoMemMap_h + +#include "mozilla/FileUtils.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/RangedPtr.h" +#include "mozilla/Result.h" + +#include <prio.h> + +class nsIFile; + +namespace mozilla { +namespace ipc { +class FileDescriptor; +} + +namespace loader { + +class AutoMemMap { + typedef mozilla::ipc::FileDescriptor FileDescriptor; + + public: + AutoMemMap() = default; + + ~AutoMemMap(); + + Result<Ok, nsresult> init(nsIFile* file, int flags = PR_RDONLY, int mode = 0, + PRFileMapProtect prot = PR_PROT_READONLY); + + Result<Ok, nsresult> init(const FileDescriptor& file, + PRFileMapProtect prot = PR_PROT_READONLY, + size_t maybeSize = 0); + + // Initializes the mapped memory with a shared memory handle. On + // Unix-like systems, this is identical to the above init() method. On + // Windows, the FileDescriptor must be a handle for a file mapping, + // rather than a file descriptor. + Result<Ok, nsresult> initWithHandle(const FileDescriptor& file, size_t size, + PRFileMapProtect prot = PR_PROT_READONLY); + + void reset(); + + bool initialized() const { return addr; } + + uint32_t size() const { return size_; } + + template <typename T = void> + RangedPtr<T> get() { + MOZ_ASSERT(addr); + return {static_cast<T*>(addr), size_}; + } + + template <typename T = void> + const RangedPtr<T> get() const { + MOZ_ASSERT(addr); + return {static_cast<T*>(addr), size_}; + } + + size_t nonHeapSizeOfExcludingThis() { return size_; } + + FileDescriptor cloneFileDescriptor() const; + FileDescriptor cloneHandle() const; + + // Makes this mapping persistent. After calling this, the mapped memory + // will remained mapped, even after this instance is destroyed. + void setPersistent() { persistent_ = true; } + + private: + Result<Ok, nsresult> initInternal(PRFileMapProtect prot, + size_t maybeSize = 0); + + AutoFDClose fd; + PRFileMap* fileMap = nullptr; + +#ifdef XP_WIN + // We can't include windows.h in this header, since it gets included + // by some binding headers (which are explicitly incompatible with + // windows.h). So we can't use the HANDLE type here. + void* handle_ = nullptr; +#endif + + uint32_t size_ = 0; + void* addr = nullptr; + + bool persistent_ = 0; + + AutoMemMap(const AutoMemMap&) = delete; + void operator=(const AutoMemMap&) = delete; +}; + +} // namespace loader +} // namespace mozilla + +#endif // loader_AutoMemMap_h diff --git a/js/xpconnect/loader/ChromeScriptLoader.cpp b/js/xpconnect/loader/ChromeScriptLoader.cpp new file mode 100644 index 0000000000..22035d75b0 --- /dev/null +++ b/js/xpconnect/loader/ChromeScriptLoader.cpp @@ -0,0 +1,379 @@ +/* -*- 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 "PrecompiledScript.h" + +#include "nsIIncrementalStreamLoader.h" +#include "nsIURI.h" +#include "nsIChannel.h" +#include "nsNetUtil.h" +#include "nsThreadUtils.h" + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "js/CompilationAndEvaluation.h" +#include "js/experimental/JSStencil.h" // JS::CompileGlobalScriptToStencil, JS::InstantiateGlobalStencil, JS::OffThreadCompileToStencil +#include "js/SourceText.h" +#include "js/Utility.h" + +#include "mozilla/Attributes.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/dom/ChromeUtils.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/ScriptLoader.h" +#include "mozilla/HoldDropJSObjects.h" +#include "nsCCUncollectableMarker.h" +#include "nsCycleCollectionParticipant.h" +#include "nsGlobalWindowInner.h" + +using namespace JS; +using namespace mozilla; +using namespace mozilla::dom; + +class AsyncScriptCompiler final : public nsIIncrementalStreamLoaderObserver, + public Runnable { + public: + // Note: References to this class are never held by cycle-collected objects. + // If at any point a reference is returned to a caller, please update this + // class to implement cycle collection. + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIINCREMENTALSTREAMLOADEROBSERVER + NS_DECL_NSIRUNNABLE + + AsyncScriptCompiler(JSContext* aCx, nsIGlobalObject* aGlobal, + const nsACString& aURL, Promise* aPromise) + : mozilla::Runnable("AsyncScriptCompiler"), + mOptions(aCx), + mURL(aURL), + mGlobalObject(aGlobal), + mPromise(aPromise), + mToken(nullptr), + mScriptLength(0) {} + + [[nodiscard]] nsresult Start(JSContext* aCx, + const CompileScriptOptionsDictionary& aOptions, + nsIPrincipal* aPrincipal); + + inline void SetToken(JS::OffThreadToken* aToken) { mToken = aToken; } + + protected: + virtual ~AsyncScriptCompiler() { + if (mPromise->State() == Promise::PromiseState::Pending) { + mPromise->MaybeReject(NS_ERROR_FAILURE); + } + } + + private: + void Reject(JSContext* aCx); + void Reject(JSContext* aCx, const char* aMxg); + + bool StartCompile(JSContext* aCx); + void FinishCompile(JSContext* aCx); + void Finish(JSContext* aCx, RefPtr<JS::Stencil> aStencil); + + OwningCompileOptions mOptions; + nsCString mURL; + nsCOMPtr<nsIGlobalObject> mGlobalObject; + RefPtr<Promise> mPromise; + nsString mCharset; + JS::OffThreadToken* mToken; + UniquePtr<Utf8Unit[], JS::FreePolicy> mScriptText; + size_t mScriptLength; +}; + +NS_IMPL_QUERY_INTERFACE_INHERITED(AsyncScriptCompiler, Runnable, + nsIIncrementalStreamLoaderObserver) +NS_IMPL_ADDREF_INHERITED(AsyncScriptCompiler, Runnable) +NS_IMPL_RELEASE_INHERITED(AsyncScriptCompiler, Runnable) + +nsresult AsyncScriptCompiler::Start( + JSContext* aCx, const CompileScriptOptionsDictionary& aOptions, + nsIPrincipal* aPrincipal) { + mCharset = aOptions.mCharset; + + CompileOptions options(aCx); + options.setFile(mURL.get()).setNoScriptRval(!aOptions.mHasReturnValue); + + if (!aOptions.mLazilyParse) { + options.setForceFullParse(); + } + + if (NS_WARN_IF(!mOptions.copy(aCx, options))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), mURL); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIChannel> channel; + rv = NS_NewChannel( + getter_AddRefs(channel), uri, aPrincipal, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_INTERNAL_CHROMEUTILS_COMPILED_SCRIPT); + NS_ENSURE_SUCCESS(rv, rv); + + // allow deprecated HTTP request from SystemPrincipal + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + loadInfo->SetAllowDeprecatedSystemRequests(true); + nsCOMPtr<nsIIncrementalStreamLoader> loader; + rv = NS_NewIncrementalStreamLoader(getter_AddRefs(loader), this); + NS_ENSURE_SUCCESS(rv, rv); + + return channel->AsyncOpen(loader); +} + +static void OffThreadScriptLoaderCallback(JS::OffThreadToken* aToken, + void* aCallbackData) { + RefPtr<AsyncScriptCompiler> scriptCompiler = + dont_AddRef(static_cast<AsyncScriptCompiler*>(aCallbackData)); + + scriptCompiler->SetToken(aToken); + + SchedulerGroup::Dispatch(TaskCategory::Other, scriptCompiler.forget()); +} + +bool AsyncScriptCompiler::StartCompile(JSContext* aCx) { + JS::SourceText<Utf8Unit> srcBuf; + if (!srcBuf.init(aCx, std::move(mScriptText), mScriptLength)) { + return false; + } + + if (JS::CanCompileOffThread(aCx, mOptions, mScriptLength)) { + if (!JS::CompileToStencilOffThread(aCx, mOptions, srcBuf, + OffThreadScriptLoaderCallback, + static_cast<void*>(this))) { + return false; + } + + NS_ADDREF(this); + return true; + } + + RefPtr<Stencil> stencil = + JS::CompileGlobalScriptToStencil(aCx, mOptions, srcBuf); + if (!stencil) { + return false; + } + + Finish(aCx, stencil); + return true; +} + +NS_IMETHODIMP +AsyncScriptCompiler::Run() { + AutoJSAPI jsapi; + if (jsapi.Init(mGlobalObject)) { + FinishCompile(jsapi.cx()); + } else { + jsapi.Init(); + JS::CancelOffThreadToken(jsapi.cx(), mToken); + + mPromise->MaybeReject(NS_ERROR_FAILURE); + } + + return NS_OK; +} + +void AsyncScriptCompiler::FinishCompile(JSContext* aCx) { + RefPtr<JS::Stencil> stencil = JS::FinishOffThreadStencil(aCx, mToken); + if (stencil) { + Finish(aCx, stencil); + } else { + Reject(aCx); + } +} + +void AsyncScriptCompiler::Finish(JSContext* aCx, RefPtr<JS::Stencil> aStencil) { + RefPtr<PrecompiledScript> result = + new PrecompiledScript(mGlobalObject, aStencil, mOptions); + + mPromise->MaybeResolve(result); +} + +void AsyncScriptCompiler::Reject(JSContext* aCx) { + RootedValue value(aCx, JS::UndefinedValue()); + if (JS_GetPendingException(aCx, &value)) { + JS_ClearPendingException(aCx); + } + mPromise->MaybeReject(value); +} + +void AsyncScriptCompiler::Reject(JSContext* aCx, const char* aMsg) { + nsAutoString msg; + msg.AppendASCII(aMsg); + msg.AppendLiteral(": "); + AppendUTF8toUTF16(mURL, msg); + + RootedValue exn(aCx); + if (xpc::NonVoidStringToJsval(aCx, msg, &exn)) { + JS_SetPendingException(aCx, exn); + } + + Reject(aCx); +} + +NS_IMETHODIMP +AsyncScriptCompiler::OnIncrementalData(nsIIncrementalStreamLoader* aLoader, + nsISupports* aContext, + uint32_t aDataLength, + const uint8_t* aData, + uint32_t* aConsumedData) { + return NS_OK; +} + +NS_IMETHODIMP +AsyncScriptCompiler::OnStreamComplete(nsIIncrementalStreamLoader* aLoader, + nsISupports* aContext, nsresult aStatus, + uint32_t aLength, const uint8_t* aBuf) { + AutoJSAPI jsapi; + if (!jsapi.Init(mGlobalObject)) { + mPromise->MaybeReject(NS_ERROR_FAILURE); + return NS_OK; + } + + JSContext* cx = jsapi.cx(); + + if (NS_FAILED(aStatus)) { + Reject(cx, "Unable to load script"); + return NS_OK; + } + + nsresult rv = ScriptLoader::ConvertToUTF8( + nullptr, aBuf, aLength, mCharset, nullptr, mScriptText, mScriptLength); + if (NS_FAILED(rv)) { + Reject(cx, "Unable to decode script"); + return NS_OK; + } + + if (!StartCompile(cx)) { + Reject(cx); + } + + return NS_OK; +} + +namespace mozilla { +namespace dom { + +/* static */ +already_AddRefed<Promise> ChromeUtils::CompileScript( + GlobalObject& aGlobal, const nsAString& aURL, + const CompileScriptOptionsDictionary& aOptions, ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + MOZ_ASSERT(global); + + RefPtr<Promise> promise = Promise::Create(global, aRv); + if (aRv.Failed()) { + return nullptr; + } + + NS_ConvertUTF16toUTF8 url(aURL); + RefPtr<AsyncScriptCompiler> compiler = + new AsyncScriptCompiler(aGlobal.Context(), global, url, promise); + + nsresult rv = compiler->Start(aGlobal.Context(), aOptions, + aGlobal.GetSubjectPrincipal()); + if (NS_FAILED(rv)) { + promise->MaybeReject(rv); + } + + return promise.forget(); +} + +PrecompiledScript::PrecompiledScript(nsISupports* aParent, + RefPtr<JS::Stencil> aStencil, + JS::ReadOnlyCompileOptions& aOptions) + : mParent(aParent), + mStencil(aStencil), + mURL(aOptions.filename()), + mHasReturnValue(!aOptions.noScriptRval) { + MOZ_ASSERT(aParent); + MOZ_ASSERT(aStencil); +#ifdef DEBUG + JS::InstantiateOptions options(aOptions); + options.assertDefault(); +#endif +}; + +void PrecompiledScript::ExecuteInGlobal(JSContext* aCx, HandleObject aGlobal, + const ExecuteInGlobalOptions& aOptions, + MutableHandleValue aRval, + ErrorResult& aRv) { + { + RootedObject targetObj(aCx, JS_FindCompilationScope(aCx, aGlobal)); + // Use AutoEntryScript for its ReportException method call. + // This will ensure notified any exception happening in the content script + // directly to the console, so that exceptions are flagged with the right + // innerWindowID. It helps these exceptions to appear in the page's web + // console. + AutoEntryScript aes(targetObj, "pre-compiled-script execution"); + JSContext* cx = aes.cx(); + + // See assertion in constructor. + JS::InstantiateOptions options; + Rooted<JSScript*> script( + cx, JS::InstantiateGlobalStencil(cx, options, mStencil)); + if (!script) { + aRv.NoteJSContextException(aCx); + return; + } + + if (!JS_ExecuteScript(cx, script, aRval)) { + JS::RootedValue exn(cx); + if (aOptions.mReportExceptions) { + // Note that ReportException will consume the exception. + aes.ReportException(); + } else { + // Set the exception on our caller's cx. + aRv.MightThrowJSException(); + aRv.StealExceptionFromJSContext(cx); + } + return; + } + } + + JS_WrapValue(aCx, aRval); +} + +void PrecompiledScript::GetUrl(nsAString& aUrl) { CopyUTF8toUTF16(mURL, aUrl); } + +bool PrecompiledScript::HasReturnValue() { return mHasReturnValue; } + +JSObject* PrecompiledScript::WrapObject(JSContext* aCx, + HandleObject aGivenProto) { + return PrecompiledScript_Binding::Wrap(aCx, this, aGivenProto); +} + +bool PrecompiledScript::IsBlackForCC(bool aTracingNeeded) { + return (nsCCUncollectableMarker::sGeneration && HasKnownLiveWrapper() && + (!aTracingNeeded || HasNothingToTrace(this))); +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PrecompiledScript, mParent) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PrecompiledScript) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(PrecompiledScript) + return tmp->IsBlackForCC(false); +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_END + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_BEGIN(PrecompiledScript) + return tmp->IsBlackForCC(true); +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(PrecompiledScript) + return tmp->IsBlackForCC(false); +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(PrecompiledScript) +NS_IMPL_CYCLE_COLLECTING_RELEASE(PrecompiledScript) + +} // namespace dom +} // namespace mozilla diff --git a/js/xpconnect/loader/ComponentModuleLoader.cpp b/js/xpconnect/loader/ComponentModuleLoader.cpp new file mode 100644 index 0000000000..9f293fdcc0 --- /dev/null +++ b/js/xpconnect/loader/ComponentModuleLoader.cpp @@ -0,0 +1,272 @@ +/* -*- 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 "ComponentModuleLoader.h" + +#include "nsISupportsImpl.h" + +#include "js/loader/ModuleLoadRequest.h" +#include "js/RootingAPI.h" // JS::Rooted +#include "js/PropertyAndElement.h" // JS_SetProperty +#include "js/Value.h" // JS::Value, JS::NumberValue +#include "mozJSModuleLoader.h" + +using namespace JS::loader; + +namespace mozilla { +namespace loader { + +////////////////////////////////////////////////////////////// +// ComponentScriptLoader +////////////////////////////////////////////////////////////// + +NS_IMPL_ISUPPORTS0(ComponentScriptLoader) + +nsIURI* ComponentScriptLoader::GetBaseURI() const { return nullptr; } + +void ComponentScriptLoader::ReportErrorToConsole(ScriptLoadRequest* aRequest, + nsresult aResult) const {} + +void ComponentScriptLoader::ReportWarningToConsole( + ScriptLoadRequest* aRequest, const char* aMessageName, + const nsTArray<nsString>& aParams) const {} + +nsresult ComponentScriptLoader::FillCompileOptionsForRequest( + JSContext* cx, ScriptLoadRequest* aRequest, JS::CompileOptions* aOptions, + JS::MutableHandle<JSScript*> aIntroductionScript) { + return NS_OK; +} + +////////////////////////////////////////////////////////////// +// ComponentModuleLoader +////////////////////////////////////////////////////////////// + +NS_IMPL_ADDREF_INHERITED(ComponentModuleLoader, JS::loader::ModuleLoaderBase) +NS_IMPL_RELEASE_INHERITED(ComponentModuleLoader, JS::loader::ModuleLoaderBase) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(ComponentModuleLoader, + JS::loader::ModuleLoaderBase, mLoadRequests) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ComponentModuleLoader) +NS_INTERFACE_MAP_END_INHERITING(JS::loader::ModuleLoaderBase) + +ComponentModuleLoader::ComponentModuleLoader( + ComponentScriptLoader* aScriptLoader, nsIGlobalObject* aGlobalObject) + : ModuleLoaderBase(aScriptLoader, aGlobalObject, new SyncEventTarget()) {} + +ComponentModuleLoader::~ComponentModuleLoader() { + MOZ_ASSERT(mLoadRequests.isEmpty()); +} + +already_AddRefed<ModuleLoadRequest> ComponentModuleLoader::CreateStaticImport( + nsIURI* aURI, ModuleLoadRequest* aParent) { + RefPtr<ComponentLoadContext> context = new ComponentLoadContext(); + RefPtr<ModuleLoadRequest> request = new ModuleLoadRequest( + aURI, aParent->mFetchOptions, dom::SRIMetadata(), aParent->mURI, context, + false, /* is top level */ + false, /* is dynamic import */ + this, aParent->mVisitedSet, aParent->GetRootModule()); + return request.forget(); +} + +already_AddRefed<ModuleLoadRequest> ComponentModuleLoader::CreateDynamicImport( + JSContext* aCx, nsIURI* aURI, LoadedScript* aMaybeActiveScript, + JS::Handle<JS::Value> aReferencingPrivate, JS::Handle<JSString*> aSpecifier, + JS::Handle<JSObject*> aPromise) { + return nullptr; // Not yet implemented. +} + +bool ComponentModuleLoader::CanStartLoad(ModuleLoadRequest* aRequest, + nsresult* aRvOut) { + return mozJSModuleLoader::IsTrustedScheme(aRequest->mURI); +} + +nsresult ComponentModuleLoader::StartFetch(ModuleLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->HasLoadContext()); + + aRequest->mBaseURL = aRequest->mURI; + + // Loading script source and compilation are intertwined in + // mozJSModuleLoader. Perform both operations here but only report load + // failures. Compilation failure is reported in CompileFetchedModule. + + dom::AutoJSAPI jsapi; + if (!jsapi.Init(GetGlobalObject())) { + return NS_ERROR_FAILURE; + } + + JSContext* cx = jsapi.cx(); + JS::RootedScript script(cx); + nsresult rv = + mozJSModuleLoader::LoadSingleModuleScript(this, cx, aRequest, &script); + MOZ_ASSERT_IF(jsapi.HasException(), NS_FAILED(rv)); + MOZ_ASSERT(bool(script) == NS_SUCCEEDED(rv)); + + // Check for failure to load script source and abort. + bool threwException = jsapi.HasException(); + if (NS_FAILED(rv) && !threwException) { + nsAutoCString uri; + nsresult rv2 = aRequest->mURI->GetSpec(uri); + NS_ENSURE_SUCCESS(rv2, rv2); + + JS_ReportErrorUTF8(cx, "Failed to load %s", PromiseFlatCString(uri).get()); + + // Remember the error for MaybeReportLoadError. + if (!mLoadException.initialized()) { + mLoadException.init(cx); + } + if (!jsapi.StealException(&mLoadException)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + if (mLoadException.isObject()) { + // Expose `nsresult`. + JS::Rooted<JS::Value> resultVal(cx, JS::NumberValue(uint32_t(rv))); + JS::Rooted<JSObject*> exceptionObj(cx, &mLoadException.toObject()); + if (!JS_SetProperty(cx, exceptionObj, "result", resultVal)) { + // Ignore the error and keep reporting the exception without the result + // property. + JS_ClearPendingException(cx); + } + } + + return rv; + } + + // Otherwise remember the results in this context so we can report them later. + ComponentLoadContext* context = aRequest->GetComponentLoadContext(); + context->mRv = rv; + if (threwException) { + context->mExceptionValue.init(cx); + if (!jsapi.StealException(&context->mExceptionValue)) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + if (script) { + context->mScript.init(cx); + context->mScript = script; + } + + mLoadRequests.AppendElement(aRequest); + + return NS_OK; +} + +nsresult ComponentModuleLoader::CompileFetchedModule( + JSContext* aCx, JS::Handle<JSObject*> aGlobal, JS::CompileOptions& aOptions, + ModuleLoadRequest* aRequest, JS::MutableHandle<JSObject*> aModuleOut) { + // Compilation already happened in StartFetch. Report the result here. + ComponentLoadContext* context = aRequest->GetComponentLoadContext(); + nsresult rv = context->mRv; + if (context->mScript) { + aModuleOut.set(JS::GetModuleObject(context->mScript)); + context->mScript = nullptr; + } + if (NS_FAILED(rv)) { + JS_SetPendingException(aCx, context->mExceptionValue); + context->mExceptionValue = JS::UndefinedValue(); + } + + MOZ_ASSERT(JS_IsExceptionPending(aCx) == NS_FAILED(rv)); + MOZ_ASSERT(bool(aModuleOut) == NS_SUCCEEDED(rv)); + + return rv; +} + +void ComponentModuleLoader::MaybeReportLoadError(JSContext* aCx) { + if (JS_IsExceptionPending(aCx)) { + // Do not override. + return; + } + + if (mLoadException.isUndefined()) { + return; + } + + JS_SetPendingException(aCx, mLoadException); + mLoadException = JS::UndefinedValue(); +} + +void ComponentModuleLoader::OnModuleLoadComplete(ModuleLoadRequest* aRequest) {} + +nsresult ComponentModuleLoader::ProcessRequests() { + // Work list to drive module loader since this is all synchronous. + while (!mLoadRequests.isEmpty()) { + RefPtr<ScriptLoadRequest> request = mLoadRequests.StealFirst(); + nsresult rv = OnFetchComplete(request->AsModuleRequest(), NS_OK); + if (NS_FAILED(rv)) { + mLoadRequests.CancelRequestsAndClear(); + return rv; + } + } + + return NS_OK; +} + +////////////////////////////////////////////////////////////// +// ComponentModuleLoader::SyncEventTarget +////////////////////////////////////////////////////////////// + +NS_IMPL_ADDREF(ComponentModuleLoader::SyncEventTarget) +NS_IMPL_RELEASE(ComponentModuleLoader::SyncEventTarget) + +NS_INTERFACE_MAP_BEGIN(ComponentModuleLoader::SyncEventTarget) + NS_INTERFACE_MAP_ENTRY(nsISerialEventTarget) + NS_INTERFACE_MAP_ENTRY(nsIEventTarget) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMETHODIMP +ComponentModuleLoader::SyncEventTarget::DispatchFromScript( + nsIRunnable* aRunnable, uint32_t aFlags) { + nsCOMPtr<nsIRunnable> event(aRunnable); + return Dispatch(event.forget(), aFlags); +} + +NS_IMETHODIMP +ComponentModuleLoader::SyncEventTarget::Dispatch( + already_AddRefed<nsIRunnable> aRunnable, uint32_t aFlags) { + MOZ_ASSERT(IsOnCurrentThreadInfallible()); + + nsCOMPtr<nsIRunnable> runnable(aRunnable); + runnable->Run(); + + return NS_OK; +} + +NS_IMETHODIMP +ComponentModuleLoader::SyncEventTarget::DelayedDispatch( + already_AddRefed<nsIRunnable>, uint32_t) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ComponentModuleLoader::SyncEventTarget::RegisterShutdownTask( + nsITargetShutdownTask* aTask) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ComponentModuleLoader::SyncEventTarget::UnregisterShutdownTask( + nsITargetShutdownTask* aTask) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ComponentModuleLoader::SyncEventTarget::IsOnCurrentThread( + bool* aIsOnCurrentThread) { + MOZ_ASSERT(aIsOnCurrentThread); + *aIsOnCurrentThread = IsOnCurrentThreadInfallible(); + return NS_OK; +} + +NS_IMETHODIMP_(bool) +ComponentModuleLoader::SyncEventTarget::IsOnCurrentThreadInfallible() { + return NS_IsMainThread(); +} + +} // namespace loader +} // namespace mozilla diff --git a/js/xpconnect/loader/ComponentModuleLoader.h b/js/xpconnect/loader/ComponentModuleLoader.h new file mode 100644 index 0000000000..7dd39dd342 --- /dev/null +++ b/js/xpconnect/loader/ComponentModuleLoader.h @@ -0,0 +1,119 @@ +/* -*- 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/. */ + +#ifndef mozilla_loader_ComponentModuleLoader_h +#define mozilla_loader_ComponentModuleLoader_h + +#include "js/loader/LoadContextBase.h" +#include "js/loader/ModuleLoaderBase.h" + +#include "SkipCheckForBrokenURLOrZeroSized.h" + +class mozJSModuleLoader; + +namespace mozilla { +namespace loader { + +class ComponentScriptLoader : public JS::loader::ScriptLoaderInterface { + public: + NS_DECL_ISUPPORTS + + private: + ~ComponentScriptLoader() = default; + + nsIURI* GetBaseURI() const override; + + void ReportErrorToConsole(ScriptLoadRequest* aRequest, + nsresult aResult) const override; + + void ReportWarningToConsole(ScriptLoadRequest* aRequest, + const char* aMessageName, + const nsTArray<nsString>& aParams) const override; + + nsresult FillCompileOptionsForRequest( + JSContext* cx, ScriptLoadRequest* aRequest, JS::CompileOptions* aOptions, + JS::MutableHandle<JSScript*> aIntroductionScript) override; +}; + +class ComponentModuleLoader : public JS::loader::ModuleLoaderBase { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ComponentModuleLoader, + JS::loader::ModuleLoaderBase) + + ComponentModuleLoader(ComponentScriptLoader* aScriptLoader, + nsIGlobalObject* aGlobalObject); + + [[nodiscard]] nsresult ProcessRequests(); + + void MaybeReportLoadError(JSContext* aCx); + + private: + // An event target that dispatches runnables by executing them + // immediately. This is used to drive mozPromise dispatch for + // ComponentModuleLoader. + class SyncEventTarget : public nsISerialEventTarget { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIEVENTTARGET_FULL + private: + virtual ~SyncEventTarget() = default; + }; + + ~ComponentModuleLoader(); + + already_AddRefed<ModuleLoadRequest> CreateStaticImport( + nsIURI* aURI, ModuleLoadRequest* aParent) override; + + already_AddRefed<ModuleLoadRequest> CreateDynamicImport( + JSContext* aCx, nsIURI* aURI, LoadedScript* aMaybeActiveScript, + JS::Handle<JS::Value> aReferencingPrivate, + JS::Handle<JSString*> aSpecifier, + JS::Handle<JSObject*> aPromise) override; + + bool CanStartLoad(ModuleLoadRequest* aRequest, nsresult* aRvOut) override; + + nsresult StartFetch(ModuleLoadRequest* aRequest) override; + + nsresult CompileFetchedModule( + JSContext* aCx, JS::Handle<JSObject*> aGlobal, + JS::CompileOptions& aOptions, ModuleLoadRequest* aRequest, + JS::MutableHandle<JSObject*> aModuleScript) override; + + void OnModuleLoadComplete(ModuleLoadRequest* aRequest) override; + + JS::loader::ScriptLoadRequestList mLoadRequests; + + // If any of module scripts failed to load, exception is set here until it's + // reported by MaybeReportLoadError. + JS::PersistentRooted<JS::Value> mLoadException; +}; + +// Data specific to ComponentModuleLoader that is associated with each load +// request. +class ComponentLoadContext : public JS::loader::LoadContextBase { + public: + ComponentLoadContext() + : LoadContextBase(JS::loader::ContextKind::Component) {} + + public: + // The result of compiling a module script. These fields are used temporarily + // before being passed to the module loader. + nsresult mRv; + + SkipCheckForBrokenURLOrZeroSized mSkipCheck; + + // The exception thrown during compiling a module script. These fields are + // used temporarily before being passed to the module loader. + JS::PersistentRooted<JS::Value> mExceptionValue; + + JS::PersistentRooted<JSScript*> mScript; +}; + +} // namespace loader +} // namespace mozilla + +#endif // mozilla_loader_ComponentModuleLoader_h diff --git a/js/xpconnect/loader/ComponentUtils.sys.mjs b/js/xpconnect/loader/ComponentUtils.sys.mjs new file mode 100644 index 0000000000..cfffb96ef7 --- /dev/null +++ b/js/xpconnect/loader/ComponentUtils.sys.mjs @@ -0,0 +1,33 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 sts=2 et filetype=javascript + * 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/. */ + +/** + * Deprecated utilities for JavaScript components loaded by the JS component + * loader. + */ + +const nsIFactoryQI = ChromeUtils.generateQI(["nsIFactory"]); + +export var ComponentUtils = { + /** + * Generates a singleton nsIFactory implementation that can be used as + * an argument to nsIComponentRegistrar.registerFactory. + * @param aServiceConstructor + * Constructor function of the component. + */ + generateSingletonFactory(aServiceConstructor) { + return { + _instance: null, + createInstance(aIID) { + if (this._instance === null) { + this._instance = new aServiceConstructor(); + } + return this._instance.QueryInterface(aIID); + }, + QueryInterface: nsIFactoryQI, + }; + }, +}; diff --git a/js/xpconnect/loader/IOBuffers.h b/js/xpconnect/loader/IOBuffers.h new file mode 100644 index 0000000000..e26b0c3bca --- /dev/null +++ b/js/xpconnect/loader/IOBuffers.h @@ -0,0 +1,148 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#ifndef IOBuffers_h +#define IOBuffers_h + +#include "mozilla/Assertions.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/EnumSet.h" +#include "mozilla/Range.h" +#include "mozilla/Span.h" +#include "nsString.h" +#include "nsTArray.h" + +namespace mozilla { +namespace loader { + +class OutputBuffer { + public: + OutputBuffer() {} + + uint8_t* write(size_t size) { + auto buf = data.AppendElements(size); + cursor_ += size; + return buf; + } + + void codeUint8(const uint8_t& val) { *write(sizeof val) = val; } + + template <typename T> + void codeUint8(const EnumSet<T>& val) { + // EnumSets are always represented as uint32_t values, so we need to + // assert that the value actually fits in a uint8 before writing it. + uint32_t value = val.serialize(); + codeUint8(CheckedUint8(value).value()); + } + + void codeUint16(const uint16_t& val) { + LittleEndian::writeUint16(write(sizeof val), val); + } + + void codeUint32(const uint32_t& val) { + LittleEndian::writeUint32(write(sizeof val), val); + } + + void codeString(const nsCString& str) { + auto len = CheckedUint16(str.Length()).value(); + + codeUint16(len); + memcpy(write(len), str.BeginReading(), len); + } + + size_t cursor() const { return cursor_; } + + uint8_t* Get() { return data.Elements(); } + + const uint8_t* Get() const { return data.Elements(); } + + private: + nsTArray<uint8_t> data; + size_t cursor_ = 0; +}; + +class InputBuffer { + public: + explicit InputBuffer(const Range<uint8_t>& buffer) : data(buffer) {} + + const uint8_t* read(size_t size) { + MOZ_ASSERT(checkCapacity(size)); + + auto buf = &data[cursor_]; + cursor_ += size; + return buf; + } + + bool codeUint8(uint8_t& val) { + if (checkCapacity(sizeof val)) { + val = *read(sizeof val); + } + return !error_; + } + + template <typename T> + bool codeUint8(EnumSet<T>& val) { + uint8_t value; + if (codeUint8(value)) { + val.deserialize(value); + } + return !error_; + } + + bool codeUint16(uint16_t& val) { + if (checkCapacity(sizeof val)) { + val = LittleEndian::readUint16(read(sizeof val)); + } + return !error_; + } + + bool codeUint32(uint32_t& val) { + if (checkCapacity(sizeof val)) { + val = LittleEndian::readUint32(read(sizeof val)); + } + return !error_; + } + + bool codeString(nsCString& str) { + uint16_t len; + if (codeUint16(len)) { + if (checkCapacity(len)) { + str.SetLength(len); + memcpy(str.BeginWriting(), read(len), len); + } + } + return !error_; + } + + bool error() { return error_; } + + bool finished() { return error_ || !remainingCapacity(); } + + size_t remainingCapacity() { return data.length() - cursor_; } + + size_t cursor() const { return cursor_; } + + const uint8_t* Get() const { return data.begin().get(); } + + private: + bool checkCapacity(size_t size) { + if (size > remainingCapacity()) { + error_ = true; + } + return !error_; + } + + bool error_ = false; + + public: + const Range<uint8_t>& data; + size_t cursor_ = 0; +}; + +} // namespace loader +} // namespace mozilla + +#endif // IOBuffers_h diff --git a/js/xpconnect/loader/JSMEnvironmentProxy.cpp b/js/xpconnect/loader/JSMEnvironmentProxy.cpp new file mode 100644 index 0000000000..6c4a532d44 --- /dev/null +++ b/js/xpconnect/loader/JSMEnvironmentProxy.cpp @@ -0,0 +1,260 @@ +/* -*- 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 "JSMEnvironmentProxy.h" + +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/Maybe.h" // mozilla::Maybe + +#include <stddef.h> // size_t + +#include "jsapi.h" // JS_HasExtensibleLexicalEnvironment, JS_ExtensibleLexicalEnvironment +#include "js/Class.h" // JS::ObjectOpResult +#include "js/ErrorReport.h" // JS_ReportOutOfMemory +#include "js/GCVector.h" // JS::RootedVector +#include "js/Id.h" // JS::PropertyKey +#include "js/PropertyAndElement.h" // JS::IdVector, JS_HasPropertyById, JS_HasOwnPropertyById, JS_GetPropertyById, JS_Enumerate +#include "js/PropertyDescriptor.h" // JS::PropertyDescriptor, JS_GetOwnPropertyDescriptorById +#include "js/PropertyDescriptor.h" // JS::PropertyDescriptor, JS_GetOwnPropertyDescriptorById +#include "js/Proxy.h" // js::ProxyOptions, js::NewProxyObject, js::GetProxyPrivate +#include "js/RootingAPI.h" // JS::Rooted, JS::Handle, JS::MutableHandle +#include "js/TypeDecls.h" // JSContext, JSObject, JS::MutableHandleVector +#include "js/Value.h" // JS::Value, JS::UndefinedValue, JS_UNINITIALIZED_LEXICAL +#include "js/friend/ErrorMessages.h" // JSMSG_* + +namespace mozilla { +namespace loader { + +struct JSMEnvironmentProxyHandler : public js::BaseProxyHandler { + JSMEnvironmentProxyHandler() : BaseProxyHandler(&gFamily, false) {} + + bool defineProperty(JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::Handle<JS::PropertyKey> aId, + JS::Handle<JS::PropertyDescriptor> aDesc, + JS::ObjectOpResult& aResult) const override { + return aResult.fail(JSMSG_CANT_DEFINE_PROP_OBJECT_NOT_EXTENSIBLE); + } + + bool getPrototype(JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::MutableHandle<JSObject*> aProtop) const override { + aProtop.set(nullptr); + return true; + } + + bool setPrototype(JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::Handle<JSObject*> aProto, + JS::ObjectOpResult& aResult) const override { + if (!aProto) { + return aResult.succeed(); + } + return aResult.failCantSetProto(); + } + + bool getPrototypeIfOrdinary( + JSContext* aCx, JS::Handle<JSObject*> aProxy, bool* aIsOrdinary, + JS::MutableHandle<JSObject*> aProtop) const override { + *aIsOrdinary = false; + return true; + } + + bool setImmutablePrototype(JSContext* aCx, JS::Handle<JSObject*> aProxy, + bool* aSucceeded) const override { + *aSucceeded = true; + return true; + } + + bool preventExtensions(JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::ObjectOpResult& aResult) const override { + aResult.succeed(); + return true; + } + + bool isExtensible(JSContext* aCx, JS::Handle<JSObject*> aProxy, + bool* aExtensible) const override { + *aExtensible = false; + return true; + } + + bool set(JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::Handle<JS::PropertyKey> aId, JS::Handle<JS::Value> aValue, + JS::Handle<JS::Value> aReceiver, + JS::ObjectOpResult& aResult) const override { + return aResult.failReadOnly(); + } + + bool delete_(JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::Handle<JS::PropertyKey> aId, + JS::ObjectOpResult& aResult) const override { + return aResult.failCantDelete(); + } + + bool getOwnPropertyDescriptor( + JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::Handle<JS::PropertyKey> aId, + JS::MutableHandle<mozilla::Maybe<JS::PropertyDescriptor>> aDesc) + const override; + bool has(JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::Handle<JS::PropertyKey> aId, bool* aBp) const override; + bool get(JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::Handle<JS::Value> aReceiver, JS::Handle<JS::PropertyKey> aId, + JS::MutableHandle<JS::Value> aVp) const override; + bool ownPropertyKeys( + JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::MutableHandleVector<JS::PropertyKey> aProps) const override; + + private: + static JSObject* getGlobal(JSContext* aCx, JS::Handle<JSObject*> aProxy) { + JS::Rooted<JSObject*> globalObj(aCx, + &js::GetProxyPrivate(aProxy).toObject()); + return globalObj; + } + + public: + static const char gFamily; + static const JSMEnvironmentProxyHandler gHandler; +}; + +const JSMEnvironmentProxyHandler JSMEnvironmentProxyHandler::gHandler; +const char JSMEnvironmentProxyHandler::gFamily = 0; + +JSObject* ResolveModuleObjectPropertyById(JSContext* aCx, + JS::Handle<JSObject*> aModObj, + JS::Handle<JS::PropertyKey> aId) { + if (JS_HasExtensibleLexicalEnvironment(aModObj)) { + JS::Rooted<JSObject*> lexical(aCx, + JS_ExtensibleLexicalEnvironment(aModObj)); + bool found; + if (!JS_HasOwnPropertyById(aCx, lexical, aId, &found)) { + return nullptr; + } + if (found) { + return lexical; + } + } + return aModObj; +} + +JSObject* ResolveModuleObjectProperty(JSContext* aCx, + JS::Handle<JSObject*> aModObj, + const char* aName) { + if (JS_HasExtensibleLexicalEnvironment(aModObj)) { + JS::RootedObject lexical(aCx, JS_ExtensibleLexicalEnvironment(aModObj)); + bool found; + if (!JS_HasOwnProperty(aCx, lexical, aName, &found)) { + return nullptr; + } + if (found) { + return lexical; + } + } + return aModObj; +} + +bool JSMEnvironmentProxyHandler::getOwnPropertyDescriptor( + JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::Handle<JS::PropertyKey> aId, + JS::MutableHandle<mozilla::Maybe<JS::PropertyDescriptor>> aDesc) const { + JS::Rooted<JSObject*> globalObj(aCx, getGlobal(aCx, aProxy)); + JS::Rooted<JSObject*> holder( + aCx, ResolveModuleObjectPropertyById(aCx, globalObj, aId)); + if (!JS_GetOwnPropertyDescriptorById(aCx, holder, aId, aDesc)) { + return false; + } + + if (aDesc.get().isNothing()) { + return true; + } + + JS::PropertyDescriptor& desc = *aDesc.get(); + + if (desc.hasValue()) { + if (desc.value().isMagic(JS_UNINITIALIZED_LEXICAL)) { + desc.setValue(JS::UndefinedValue()); + } + } + + desc.setConfigurable(false); + desc.setEnumerable(true); + if (!desc.isAccessorDescriptor()) { + desc.setWritable(false); + } + + return true; +} + +bool JSMEnvironmentProxyHandler::has(JSContext* aCx, + JS::Handle<JSObject*> aProxy, + JS::Handle<JS::PropertyKey> aId, + bool* aBp) const { + JS::Rooted<JSObject*> globalObj(aCx, getGlobal(aCx, aProxy)); + JS::Rooted<JSObject*> holder( + aCx, ResolveModuleObjectPropertyById(aCx, globalObj, aId)); + return JS_HasPropertyById(aCx, holder, aId, aBp); +} + +bool JSMEnvironmentProxyHandler::get(JSContext* aCx, + JS::Handle<JSObject*> aProxy, + JS::Handle<JS::Value> aReceiver, + JS::Handle<JS::PropertyKey> aId, + JS::MutableHandle<JS::Value> aVp) const { + JS::Rooted<JSObject*> globalObj(aCx, getGlobal(aCx, aProxy)); + JS::Rooted<JSObject*> holder( + aCx, ResolveModuleObjectPropertyById(aCx, globalObj, aId)); + if (!JS_GetPropertyById(aCx, holder, aId, aVp)) { + return false; + } + + if (aVp.isMagic(JS_UNINITIALIZED_LEXICAL)) { + aVp.setUndefined(); + } + + return true; +} + +bool JSMEnvironmentProxyHandler::ownPropertyKeys( + JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::MutableHandleVector<JS::PropertyKey> aProps) const { + JS::Rooted<JSObject*> globalObj(aCx, getGlobal(aCx, aProxy)); + JS::Rooted<JS::IdVector> globalIds(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, globalObj, &globalIds)) { + return false; + } + + for (size_t i = 0; i < globalIds.length(); i++) { + if (!aProps.append(globalIds[i])) { + JS_ReportOutOfMemory(aCx); + return false; + } + } + + JS::RootedObject lexicalEnv(aCx, JS_ExtensibleLexicalEnvironment(globalObj)); + JS::Rooted<JS::IdVector> lexicalIds(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, lexicalEnv, &lexicalIds)) { + return false; + } + + for (size_t i = 0; i < lexicalIds.length(); i++) { + if (!aProps.append(lexicalIds[i])) { + JS_ReportOutOfMemory(aCx); + return false; + } + } + + return true; +} + +JSObject* CreateJSMEnvironmentProxy(JSContext* aCx, + JS::Handle<JSObject*> aGlobalObj) { + js::ProxyOptions options; + options.setLazyProto(true); + + JS::Rooted<JS::Value> globalVal(aCx, JS::ObjectValue(*aGlobalObj)); + return NewProxyObject(aCx, &JSMEnvironmentProxyHandler::gHandler, globalVal, + nullptr, options); +} + +} // namespace loader +} // namespace mozilla diff --git a/js/xpconnect/loader/JSMEnvironmentProxy.h b/js/xpconnect/loader/JSMEnvironmentProxy.h new file mode 100644 index 0000000000..f45d7e3801 --- /dev/null +++ b/js/xpconnect/loader/JSMEnvironmentProxy.h @@ -0,0 +1,31 @@ +/* -*- 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/. */ + +#ifndef mozilla_loader_JSMEnvironmentProxy_h +#define mozilla_loader_JSMEnvironmentProxy_h + +#include "js/Id.h" // JS::PropertyKey +#include "js/TypeDecls.h" // JSContext, JSObject +#include "js/RootingAPI.h" // JS::Handle + +namespace mozilla { +namespace loader { + +JSObject* ResolveModuleObjectPropertyById(JSContext* aCx, + JS::Handle<JSObject*> aModObj, + JS::Handle<JS::PropertyKey> aId); + +JSObject* ResolveModuleObjectProperty(JSContext* aCx, + JS::Handle<JSObject*> aModObj, + const char* aName); + +JSObject* CreateJSMEnvironmentProxy(JSContext* aCx, + JS::Handle<JSObject*> aGlobalObj); + +} // namespace loader +} // namespace mozilla + +#endif // mozilla_loader_JSMEnvironmentProxy_h diff --git a/js/xpconnect/loader/ModuleEnvironmentProxy.cpp b/js/xpconnect/loader/ModuleEnvironmentProxy.cpp new file mode 100644 index 0000000000..d513bc6194 --- /dev/null +++ b/js/xpconnect/loader/ModuleEnvironmentProxy.cpp @@ -0,0 +1,238 @@ +/* -*- 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 "ModuleEnvironmentProxy.h" + +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/Maybe.h" // mozilla::Maybe + +#include <stddef.h> // size_t + +#include "js/Class.h" // JS::ObjectOpResult +#include "js/ErrorReport.h" // JS_ReportOutOfMemory +#include "js/GCVector.h" // JS::RootedVector +#include "js/Id.h" // JS::PropertyKey +#include "js/PropertyAndElement.h" // JS::IdVector, JS_HasPropertyById, JS_GetPropertyById, JS_Enumerate +#include "js/PropertyDescriptor.h" // JS::PropertyDescriptor, JS_GetOwnPropertyDescriptorById +#include "js/PropertyDescriptor.h" // JS::PropertyDescriptor, JS_GetOwnPropertyDescriptorById +#include "js/Proxy.h" // js::ProxyOptions, js::NewProxyObject, js::GetProxyPrivate +#include "js/RootingAPI.h" // JS::Rooted, JS::Handle, JS::MutableHandle +#include "js/TypeDecls.h" // JSContext, JSObject, JS::MutableHandleVector +#include "js/Value.h" // JS::Value +#include "js/friend/ErrorMessages.h" // JSMSG_* +#include "js/String.h" +#include "js/Modules.h" + +namespace mozilla { +namespace loader { + +struct ModuleEnvironmentProxyHandler : public js::BaseProxyHandler { + ModuleEnvironmentProxyHandler() : js::BaseProxyHandler(&gFamily, false) {} + + bool defineProperty(JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::Handle<JS::PropertyKey> aId, + JS::Handle<JS::PropertyDescriptor> aDesc, + JS::ObjectOpResult& aResult) const override { + return aResult.fail(JSMSG_CANT_DEFINE_PROP_OBJECT_NOT_EXTENSIBLE); + } + + bool getPrototype(JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::MutableHandle<JSObject*> aProtop) const override { + aProtop.set(nullptr); + return true; + } + + bool setPrototype(JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::Handle<JSObject*> aProto, + JS::ObjectOpResult& aResult) const override { + if (!aProto) { + return aResult.succeed(); + } + return aResult.failCantSetProto(); + } + + bool getPrototypeIfOrdinary( + JSContext* aCx, JS::Handle<JSObject*> aProxy, bool* aIsOrdinary, + JS::MutableHandle<JSObject*> aProtop) const override { + *aIsOrdinary = false; + return true; + } + + bool setImmutablePrototype(JSContext* aCx, JS::Handle<JSObject*> aProxy, + bool* aSucceeded) const override { + *aSucceeded = true; + return true; + } + + bool preventExtensions(JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::ObjectOpResult& aResult) const override { + aResult.succeed(); + return true; + } + + bool isExtensible(JSContext* aCx, JS::Handle<JSObject*> aProxy, + bool* aExtensible) const override { + *aExtensible = false; + return true; + } + + bool set(JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::Handle<JS::PropertyKey> aId, JS::Handle<JS::Value> aValue, + JS::Handle<JS::Value> aReceiver, + JS::ObjectOpResult& aResult) const override { + return aResult.failReadOnly(); + } + + bool delete_(JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::Handle<JS::PropertyKey> aId, + JS::ObjectOpResult& aResult) const override { + return aResult.failCantDelete(); + } + + bool getOwnPropertyDescriptor( + JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::Handle<JS::PropertyKey> aId, + JS::MutableHandle<mozilla::Maybe<JS::PropertyDescriptor>> aDesc) + const override; + bool has(JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::Handle<JS::PropertyKey> aId, bool* aBp) const override; + bool get(JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::Handle<JS::Value> receiver, JS::Handle<JS::PropertyKey> aId, + JS::MutableHandle<JS::Value> aVp) const override; + bool ownPropertyKeys( + JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::MutableHandleVector<JS::PropertyKey> aProps) const override; + + private: + static JSObject* getEnvironment(JS::Handle<JSObject*> aProxy) { + return &js::GetProxyPrivate(aProxy).toObject(); + } + + static bool equalsNamespace(JSContext* aCx, JS::Handle<JS::PropertyKey> aId, + bool* aMatch) { + if (!aId.isString()) { + *aMatch = false; + return true; + } + return JS_StringEqualsLiteral(aCx, aId.toString(), "*namespace*", aMatch); + } + + public: + static const char gFamily; + static const ModuleEnvironmentProxyHandler gHandler; +}; + +const ModuleEnvironmentProxyHandler ModuleEnvironmentProxyHandler::gHandler; +const char ModuleEnvironmentProxyHandler::gFamily = 0; + +bool ModuleEnvironmentProxyHandler::getOwnPropertyDescriptor( + JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::Handle<JS::PropertyKey> aId, + JS::MutableHandle<mozilla::Maybe<JS::PropertyDescriptor>> aDesc) const { + bool isNamespace; + if (!equalsNamespace(aCx, aId, &isNamespace)) { + return false; + } + if (isNamespace) { + aDesc.reset(); + return true; + } + + JS::Rooted<JSObject*> envObj(aCx, getEnvironment(aProxy)); + if (!JS_GetOwnPropertyDescriptorById(aCx, envObj, aId, aDesc)) { + return false; + } + + if (aDesc.get().isNothing()) { + return true; + } + + JS::PropertyDescriptor& desc = *aDesc.get(); + + desc.setConfigurable(false); + desc.setWritable(false); + desc.setEnumerable(true); + + return true; +} + +bool ModuleEnvironmentProxyHandler::has(JSContext* aCx, + JS::Handle<JSObject*> aProxy, + JS::Handle<JS::PropertyKey> aId, + bool* aBp) const { + bool isNamespace; + if (!equalsNamespace(aCx, aId, &isNamespace)) { + return false; + } + if (isNamespace) { + *aBp = false; + return true; + } + + JS::Rooted<JSObject*> envObj(aCx, getEnvironment(aProxy)); + return JS_HasOwnPropertyById(aCx, envObj, aId, aBp); +} + +bool ModuleEnvironmentProxyHandler::get( + JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::Handle<JS::Value> aReceiver, JS::Handle<JS::PropertyKey> aId, + JS::MutableHandle<JS::Value> aVp) const { + bool isNamespace; + if (!equalsNamespace(aCx, aId, &isNamespace)) { + return false; + } + if (isNamespace) { + aVp.setUndefined(); + return true; + } + + JS::Rooted<JSObject*> envObj(aCx, getEnvironment(aProxy)); + return JS_GetPropertyById(aCx, envObj, aId, aVp); +} + +bool ModuleEnvironmentProxyHandler::ownPropertyKeys( + JSContext* aCx, JS::Handle<JSObject*> aProxy, + JS::MutableHandleVector<JS::PropertyKey> aProps) const { + JS::Rooted<JSObject*> envObj(aCx, getEnvironment(aProxy)); + JS::Rooted<JS::IdVector> ids(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, envObj, &ids)) { + return false; + } + + for (size_t i = 0; i < ids.length(); i++) { + bool isNamespace; + if (!equalsNamespace(aCx, ids[i], &isNamespace)) { + return false; + } + if (isNamespace) { + continue; + } + if (!aProps.append(ids[i])) { + JS_ReportOutOfMemory(aCx); + return false; + } + } + + return true; +} + +JSObject* CreateModuleEnvironmentProxy(JSContext* aCx, + JS::Handle<JSObject*> aModuleObj) { + js::ProxyOptions options; + options.setLazyProto(true); + + JS::Rooted<JSObject*> envObj(aCx, JS::GetModuleEnvironment(aCx, aModuleObj)); + if (!envObj) { + return nullptr; + } + + JS::Rooted<JS::Value> envVal(aCx, JS::ObjectValue(*envObj)); + return NewProxyObject(aCx, &ModuleEnvironmentProxyHandler::gHandler, envVal, + nullptr, options); +} + +} // namespace loader +} // namespace mozilla diff --git a/js/xpconnect/loader/ModuleEnvironmentProxy.h b/js/xpconnect/loader/ModuleEnvironmentProxy.h new file mode 100644 index 0000000000..d59f6de5b3 --- /dev/null +++ b/js/xpconnect/loader/ModuleEnvironmentProxy.h @@ -0,0 +1,30 @@ +/* -*- 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/. */ + +#ifndef mozilla_loader_ModuleEnvironmentProxy_h +#define mozilla_loader_ModuleEnvironmentProxy_h + +#include "js/TypeDecls.h" // JSContext, JSObject +#include "js/RootingAPI.h" // JS::Handle + +namespace mozilla { +namespace loader { + +// Create an object that works in the same way as global object returned by +// `Cu.import`. This proxy exposes all global variables, including lexical +// variables. +// +// This is a temporary workaround to support not-in-tree code that depends on +// `Cu.import` return value. +// +// This will eventually be removed once ESM-ification finishes. +JSObject* CreateModuleEnvironmentProxy(JSContext* aCx, + JS::Handle<JSObject*> aModuleObj); + +} // namespace loader +} // namespace mozilla + +#endif // mozilla_loader_ModuleEnvironmentProxy_h diff --git a/js/xpconnect/loader/PScriptCache.ipdl b/js/xpconnect/loader/PScriptCache.ipdl new file mode 100644 index 0000000000..e22166c16d --- /dev/null +++ b/js/xpconnect/loader/PScriptCache.ipdl @@ -0,0 +1,36 @@ +/* -*- Mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 8 -*- */ +/* vim: set sw=4 ts=8 et tw=80 ft=cpp : */ +/* 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 protocol PContent; + +include "mozilla/loader/ScriptCacheActors.h"; + +using class mozilla::TimeStamp from "mozilla/TimeStamp.h"; +using struct mozilla::void_t from "mozilla/ipc/IPCCore.h"; + +namespace mozilla { +namespace loader { + +struct ScriptData { + nsCString url; + nsCString cachePath; + TimeStamp loadTime; + // This will be an empty array if script data is present in the previous + // session's cache. + uint8_t[] xdrData; +}; + +[ManualDealloc, ChildImpl="ScriptCacheChild", ParentImpl="ScriptCacheParent"] +protocol PScriptCache +{ + manager PContent; + +parent: + async __delete__(ScriptData[] scripts); +}; + +} // namespace loader +} // namespace mozilla diff --git a/js/xpconnect/loader/PrecompiledScript.h b/js/xpconnect/loader/PrecompiledScript.h new file mode 100644 index 0000000000..2b49c05373 --- /dev/null +++ b/js/xpconnect/loader/PrecompiledScript.h @@ -0,0 +1,63 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#ifndef mozilla_dom_PrecompiledScript_h +#define mozilla_dom_PrecompiledScript_h + +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/PrecompiledScriptBinding.h" +#include "mozilla/RefPtr.h" + +#include "js/experimental/JSStencil.h" +#include "js/TypeDecls.h" + +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsWrapperCache.h" + +namespace JS { +class ReadOnlyCompileOptions; +} + +namespace mozilla { +namespace dom { +class PrecompiledScript : public nsISupports, public nsWrapperCache { + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SKIPPABLE_WRAPPERCACHE_CLASS(PrecompiledScript) + + explicit PrecompiledScript(nsISupports* aParent, RefPtr<JS::Stencil> aStencil, + JS::ReadOnlyCompileOptions& aOptions); + + void ExecuteInGlobal(JSContext* aCx, JS::Handle<JSObject*> aGlobal, + const ExecuteInGlobalOptions& aOptions, + JS::MutableHandle<JS::Value> aRval, ErrorResult& aRv); + + void GetUrl(nsAString& aUrl); + + bool HasReturnValue(); + + nsISupports* GetParentObject() const { return mParent; } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + protected: + virtual ~PrecompiledScript() = default; + + private: + bool IsBlackForCC(bool aTracingNeeded); + + nsCOMPtr<nsISupports> mParent; + + RefPtr<JS::Stencil> mStencil; + nsCString mURL; + const bool mHasReturnValue; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_PrecompiledScript_h diff --git a/js/xpconnect/loader/ScriptCacheActors.cpp b/js/xpconnect/loader/ScriptCacheActors.cpp new file mode 100644 index 0000000000..9b44f0ffe6 --- /dev/null +++ b/js/xpconnect/loader/ScriptCacheActors.cpp @@ -0,0 +1,92 @@ +/* -*- 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 "mozilla/ScriptPreloader.h" +#include "ScriptPreloader-inl.h" +#include "mozilla/loader/ScriptCacheActors.h" + +#include "mozilla/dom/ContentParent.h" + +namespace mozilla { +namespace loader { + +void ScriptCacheChild::Init(const Maybe<FileDescriptor>& cacheFile, + bool wantCacheData) { + mWantCacheData = wantCacheData; + + auto& cache = ScriptPreloader::GetChildSingleton(); + Unused << cache.InitCache(cacheFile, this); + + if (!wantCacheData) { + // If the parent process isn't expecting any cache data from us, we're + // done. + Send__delete__(this, AutoTArray<ScriptData, 0>()); + } +} + +// Finalize the script cache for the content process, and send back data about +// any scripts executed up to this point. +void ScriptCacheChild::SendScriptsAndFinalize( + ScriptPreloader::ScriptHash& scripts) { + MOZ_ASSERT(mWantCacheData); + + AutoSafeJSAPI jsapi; + + auto matcher = ScriptPreloader::Match<ScriptPreloader::ScriptStatus::Saved>(); + + nsTArray<ScriptData> dataArray; + for (auto& script : IterHash(scripts, matcher)) { + if (!script->mSize && !script->XDREncode(jsapi.cx())) { + continue; + } + + auto data = dataArray.AppendElement(); + + data->url() = script->mURL; + data->cachePath() = script->mCachePath; + data->loadTime() = script->mLoadTime; + + if (script->HasBuffer()) { + auto& xdrData = script->Buffer(); + data->xdrData().AppendElements(xdrData.begin(), xdrData.length()); + script->FreeData(); + } + } + + Send__delete__(this, dataArray); +} + +void ScriptCacheChild::ActorDestroy(ActorDestroyReason aWhy) { + auto& cache = ScriptPreloader::GetChildSingleton(); + cache.mChildActor = nullptr; +} + +IPCResult ScriptCacheParent::Recv__delete__(nsTArray<ScriptData>&& scripts) { + if (!mWantCacheData && scripts.Length()) { + return IPC_FAIL(this, "UnexpectedScriptData"); + } + + // We don't want any more data from the process at this point. + mWantCacheData = false; + + // Merge the child's script data with the parent's. + auto parent = static_cast<dom::ContentParent*>(Manager()); + auto processType = + ScriptPreloader::GetChildProcessType(parent->GetRemoteType()); + + auto& cache = ScriptPreloader::GetChildSingleton(); + for (auto& script : scripts) { + cache.NoteStencil(script.url(), script.cachePath(), processType, + std::move(script.xdrData()), script.loadTime()); + } + + return IPC_OK(); +} + +void ScriptCacheParent::ActorDestroy(ActorDestroyReason aWhy) {} + +} // namespace loader +} // namespace mozilla diff --git a/js/xpconnect/loader/ScriptCacheActors.h b/js/xpconnect/loader/ScriptCacheActors.h new file mode 100644 index 0000000000..92148464ea --- /dev/null +++ b/js/xpconnect/loader/ScriptCacheActors.h @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#ifndef ScriptCache_h +#define ScriptCache_h + +#include "mozilla/ScriptPreloader.h" +#include "mozilla/loader/PScriptCacheChild.h" +#include "mozilla/loader/PScriptCacheParent.h" + +namespace mozilla { +namespace ipc { +class FileDescriptor; +} + +namespace loader { + +using mozilla::ipc::FileDescriptor; +using mozilla::ipc::IPCResult; + +class ScriptCacheParent final : public PScriptCacheParent { + friend class PScriptCacheParent; + + public: + explicit ScriptCacheParent(bool wantCacheData) + : mWantCacheData(wantCacheData) {} + + protected: + IPCResult Recv__delete__(nsTArray<ScriptData>&& scripts); + + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + private: + bool mWantCacheData; +}; + +class ScriptCacheChild final : public PScriptCacheChild { + friend class mozilla::ScriptPreloader; + + public: + ScriptCacheChild() = default; + + void Init(const Maybe<FileDescriptor>& cacheFile, bool wantCacheData); + + protected: + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + void SendScriptsAndFinalize(ScriptPreloader::ScriptHash& scripts); + + private: + bool mWantCacheData = false; +}; + +} // namespace loader +} // namespace mozilla + +#endif // ScriptCache_h diff --git a/js/xpconnect/loader/ScriptPreloader-inl.h b/js/xpconnect/loader/ScriptPreloader-inl.h new file mode 100644 index 0000000000..5908600616 --- /dev/null +++ b/js/xpconnect/loader/ScriptPreloader-inl.h @@ -0,0 +1,167 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#ifndef ScriptPreloader_inl_h +#define ScriptPreloader_inl_h + +#include "mozilla/Attributes.h" +#include "mozilla/Assertions.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/EnumSet.h" +#include "mozilla/Range.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/ScriptSettings.h" +#include "nsString.h" +#include "nsTArray.h" + +#include <prio.h> + +namespace mozilla { + +namespace loader { + +using mozilla::dom::AutoJSAPI; + +static inline Result<Ok, nsresult> Write(PRFileDesc* fd, const void* data, + int32_t len) { + if (PR_Write(fd, data, len) != len) { + return Err(NS_ERROR_FAILURE); + } + return Ok(); +} + +static inline Result<Ok, nsresult> WritePadding(PRFileDesc* fd, + uint8_t padding) { + static const char paddingBytes[8] = "PADBYTE"; + MOZ_DIAGNOSTIC_ASSERT(padding <= sizeof(paddingBytes)); + + if (padding == 0) { + return Ok(); + } + + if (PR_Write(fd, static_cast<const void*>(paddingBytes), padding) != + padding) { + return Err(NS_ERROR_FAILURE); + } + return Ok(); +} + +struct MOZ_RAII AutoSafeJSAPI : public AutoJSAPI { + AutoSafeJSAPI() { Init(); } +}; + +template <typename T> +struct Matcher; + +// Wraps the iterator for a nsTHashTable so that it may be used as a range +// iterator. Each iterator result acts as a smart pointer to the hash element, +// and has a Remove() method which will remove the element from the hash. +// +// It also accepts an optional Matcher instance against which to filter the +// elements which should be iterated over. +// +// Example: +// +// for (auto& elem : HashElemIter<HashType>(hash)) { +// if (elem->IsDead()) { +// elem.Remove(); +// } +// } +template <typename T> +class HashElemIter { + using Iterator = typename T::Iterator; + using ElemType = typename T::UserDataType; + + T& hash_; + Matcher<ElemType>* matcher_; + Iterator iter_; + + public: + explicit HashElemIter(T& hash, Matcher<ElemType>* matcher = nullptr) + : hash_(hash), matcher_(matcher), iter_(hash.Iter()) {} + + class Elem { + friend class HashElemIter<T>; + + HashElemIter<T>& iter_; + bool done_; + + Elem(HashElemIter& iter, bool done) : iter_(iter), done_(done) { + skipNonMatching(); + } + + Iterator& iter() { return iter_.iter_; } + + void skipNonMatching() { + if (iter_.matcher_) { + while (!done_ && !iter_.matcher_->Matches(get())) { + iter().Next(); + done_ = iter().Done(); + } + } + } + + public: + Elem& operator*() { return *this; } + + ElemType get() { + if (done_) { + return nullptr; + } + return iter().UserData(); + } + + const ElemType get() const { return const_cast<Elem*>(this)->get(); } + + ElemType operator->() { return get(); } + + const ElemType operator->() const { return get(); } + + operator ElemType() { return get(); } + + void Remove() { iter().Remove(); } + + Elem& operator++() { + MOZ_ASSERT(!done_); + + iter().Next(); + done_ = iter().Done(); + + skipNonMatching(); + return *this; + } + + bool operator!=(Elem& other) const { + return done_ != other.done_ || this->get() != other.get(); + } + }; + + Elem begin() { return Elem(*this, iter_.Done()); } + + Elem end() { return Elem(*this, true); } +}; + +template <typename T> +HashElemIter<T> IterHash(T& hash, + Matcher<typename T::UserDataType>* matcher = nullptr) { + return HashElemIter<T>(hash, matcher); +} + +template <typename T, typename F> +bool Find(T&& iter, F&& match) { + for (auto& elem : iter) { + if (match(elem)) { + return true; + } + } + return false; +} + +}; // namespace loader +}; // namespace mozilla + +#endif // ScriptPreloader_inl_h diff --git a/js/xpconnect/loader/ScriptPreloader.cpp b/js/xpconnect/loader/ScriptPreloader.cpp new file mode 100644 index 0000000000..22b788127c --- /dev/null +++ b/js/xpconnect/loader/ScriptPreloader.cpp @@ -0,0 +1,1319 @@ +/* -*- 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 "ScriptPreloader-inl.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Monitor.h" + +#include "mozilla/ScriptPreloader.h" +#include "mozilla/loader/ScriptCacheActors.h" + +#include "mozilla/URLPreloader.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Components.h" +#include "mozilla/FileUtils.h" +#include "mozilla/IOBuffers.h" +#include "mozilla/Logging.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Services.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/Document.h" +#include "mozilla/scache/StartupCache.h" + +#include "crc32c.h" +#include "js/CompileOptions.h" // JS::ReadOnlyCompileOptions +#include "js/experimental/JSStencil.h" +#include "js/Transcoding.h" +#include "MainThreadUtils.h" +#include "nsDebug.h" +#include "nsDirectoryServiceUtils.h" +#include "nsIFile.h" +#include "nsIObserverService.h" +#include "nsJSUtils.h" +#include "nsMemoryReporterManager.h" +#include "nsNetUtil.h" +#include "nsProxyRelease.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" +#include "xpcpublic.h" + +#define STARTUP_COMPLETE_TOPIC "browser-delayed-startup-finished" +#define DOC_ELEM_INSERTED_TOPIC "document-element-inserted" +#define CONTENT_DOCUMENT_LOADED_TOPIC "content-document-loaded" +#define CACHE_WRITE_TOPIC "browser-idle-startup-tasks-finished" +#define XPCOM_SHUTDOWN_TOPIC "xpcom-shutdown" +#define CACHE_INVALIDATE_TOPIC "startupcache-invalidate" + +// The maximum time we'll wait for a child process to finish starting up before +// we send its script data back to the parent. +constexpr uint32_t CHILD_STARTUP_TIMEOUT_MS = 8000; + +namespace mozilla { +namespace { +static LazyLogModule gLog("ScriptPreloader"); + +#define LOG(level, ...) MOZ_LOG(gLog, LogLevel::level, (__VA_ARGS__)) +} // namespace + +using mozilla::dom::AutoJSAPI; +using mozilla::dom::ContentChild; +using mozilla::dom::ContentParent; +using namespace mozilla::loader; +using mozilla::scache::StartupCache; + +using namespace JS; + +ProcessType ScriptPreloader::sProcessType; + +nsresult ScriptPreloader::CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) { + MOZ_COLLECT_REPORT( + "explicit/script-preloader/heap/saved-scripts", KIND_HEAP, UNITS_BYTES, + SizeOfHashEntries<ScriptStatus::Saved>(mScripts, MallocSizeOf), + "Memory used to hold the scripts which have been executed in this " + "session, and will be written to the startup script cache file."); + + MOZ_COLLECT_REPORT( + "explicit/script-preloader/heap/restored-scripts", KIND_HEAP, UNITS_BYTES, + SizeOfHashEntries<ScriptStatus::Restored>(mScripts, MallocSizeOf), + "Memory used to hold the scripts which have been restored from the " + "startup script cache file, but have not been executed in this session."); + + MOZ_COLLECT_REPORT("explicit/script-preloader/heap/other", KIND_HEAP, + UNITS_BYTES, ShallowHeapSizeOfIncludingThis(MallocSizeOf), + "Memory used by the script cache service itself."); + + // Since the mem-mapped cache file is mapped into memory, we want to report + // it as explicit memory somewhere. But since the child cache is shared + // between all processes, we don't want to report it as explicit memory for + // all of them. So we report it as explicit only in the parent process, and + // non-explicit everywhere else. + if (XRE_IsParentProcess()) { + MOZ_COLLECT_REPORT("explicit/script-preloader/non-heap/memmapped-cache", + KIND_NONHEAP, UNITS_BYTES, + mCacheData->nonHeapSizeOfExcludingThis(), + "The memory-mapped startup script cache file."); + } else { + MOZ_COLLECT_REPORT("script-preloader-memmapped-cache", KIND_NONHEAP, + UNITS_BYTES, mCacheData->nonHeapSizeOfExcludingThis(), + "The memory-mapped startup script cache file."); + } + + return NS_OK; +} + +StaticRefPtr<ScriptPreloader> ScriptPreloader::gScriptPreloader; +StaticRefPtr<ScriptPreloader> ScriptPreloader::gChildScriptPreloader; +UniquePtr<AutoMemMap> ScriptPreloader::gCacheData; +UniquePtr<AutoMemMap> ScriptPreloader::gChildCacheData; + +ScriptPreloader& ScriptPreloader::GetSingleton() { + if (!gScriptPreloader) { + if (XRE_IsParentProcess()) { + gCacheData = MakeUnique<AutoMemMap>(); + gScriptPreloader = new ScriptPreloader(gCacheData.get()); + gScriptPreloader->mChildCache = &GetChildSingleton(); + Unused << gScriptPreloader->InitCache(); + } else { + gScriptPreloader = &GetChildSingleton(); + } + } + + return *gScriptPreloader; +} + +// The child singleton is available in all processes, including the parent, and +// is used for scripts which are expected to be loaded into child processes +// (such as process and frame scripts), or scripts that have already been loaded +// into a child. The child caches are managed as follows: +// +// - Every startup, we open the cache file from the last session, move it to a +// new location, and begin pre-loading the scripts that are stored in it. There +// is a separate cache file for parent and content processes, but the parent +// process opens both the parent and content cache files. +// +// - Once startup is complete, we write a new cache file for the next session, +// containing only the scripts that were used during early startup, so we +// don't waste pre-loading scripts that may not be needed. +// +// - For content processes, opening and writing the cache file is handled in the +// parent process. The first content process of each type sends back the data +// for scripts that were loaded in early startup, and the parent merges them +// and writes them to a cache file. +// +// - Currently, content processes only benefit from the cache data written +// during the *previous* session. Ideally, new content processes should +// probably use the cache data written during this session if there was no +// previous cache file, but I'd rather do that as a follow-up. +ScriptPreloader& ScriptPreloader::GetChildSingleton() { + if (!gChildScriptPreloader) { + gChildCacheData = MakeUnique<AutoMemMap>(); + gChildScriptPreloader = new ScriptPreloader(gChildCacheData.get()); + if (XRE_IsParentProcess()) { + Unused << gChildScriptPreloader->InitCache(u"scriptCache-child"_ns); + } + } + + return *gChildScriptPreloader; +} + +/* static */ +void ScriptPreloader::DeleteSingleton() { + gScriptPreloader = nullptr; + gChildScriptPreloader = nullptr; +} + +/* static */ +void ScriptPreloader::DeleteCacheDataSingleton() { + MOZ_ASSERT(!gScriptPreloader); + MOZ_ASSERT(!gChildScriptPreloader); + + gCacheData = nullptr; + gChildCacheData = nullptr; +} + +void ScriptPreloader::InitContentChild(ContentParent& parent) { + auto& cache = GetChildSingleton(); + cache.mSaveMonitor.AssertOnWritingThread(); + + // We want startup script data from the first process of a given type. + // That process sends back its script data before it executes any + // untrusted code, and then we never accept further script data for that + // type of process for the rest of the session. + // + // The script data from each process type is merged with the data from the + // parent process's frame and process scripts, and shared between all + // content process types in the next session. + // + // Note that if the first process of a given type crashes or shuts down + // before sending us its script data, we silently ignore it, and data for + // that process type is not included in the next session's cache. This + // should be a sufficiently rare occurrence that it's not worth trying to + // handle specially. + auto processType = GetChildProcessType(parent.GetRemoteType()); + bool wantScriptData = !cache.mInitializedProcesses.contains(processType); + cache.mInitializedProcesses += processType; + + auto fd = cache.mCacheData->cloneFileDescriptor(); + // Don't send original cache data to new processes if the cache has been + // invalidated. + if (fd.IsValid() && !cache.mCacheInvalidated) { + Unused << parent.SendPScriptCacheConstructor(fd, wantScriptData); + } else { + Unused << parent.SendPScriptCacheConstructor(NS_ERROR_FILE_NOT_FOUND, + wantScriptData); + } +} + +ProcessType ScriptPreloader::GetChildProcessType(const nsACString& remoteType) { + if (remoteType == EXTENSION_REMOTE_TYPE) { + return ProcessType::Extension; + } + if (remoteType == PRIVILEGEDABOUT_REMOTE_TYPE) { + return ProcessType::PrivilegedAbout; + } + return ProcessType::Web; +} + +ScriptPreloader::ScriptPreloader(AutoMemMap* cacheData) + : mCacheData(cacheData), + mMonitor("[ScriptPreloader.mMonitor]"), + mSaveMonitor("[ScriptPreloader.mSaveMonitor]", this) { + // We do not set the process type for child processes here because the + // remoteType in ContentChild is not ready yet. + if (XRE_IsParentProcess()) { + sProcessType = ProcessType::Parent; + } + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + MOZ_RELEASE_ASSERT(obs); + + if (XRE_IsParentProcess()) { + // In the parent process, we want to freeze the script cache as soon + // as idle tasks for the first browser window have completed. + obs->AddObserver(this, STARTUP_COMPLETE_TOPIC, false); + obs->AddObserver(this, CACHE_WRITE_TOPIC, false); + } + + obs->AddObserver(this, XPCOM_SHUTDOWN_TOPIC, false); + obs->AddObserver(this, CACHE_INVALIDATE_TOPIC, false); +} + +ScriptPreloader::~ScriptPreloader() { Cleanup(); } + +void ScriptPreloader::Cleanup() { + mScripts.Clear(); + UnregisterWeakMemoryReporter(this); +} + +void ScriptPreloader::StartCacheWrite() { + MOZ_DIAGNOSTIC_ASSERT(!mSaveThread); + + Unused << NS_NewNamedThread("SaveScripts", getter_AddRefs(mSaveThread), this); + + nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier(); + barrier->AddBlocker(this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, + u""_ns); +} + +void ScriptPreloader::InvalidateCache() { + { + mMonitor.AssertNotCurrentThreadOwns(); + MonitorAutoLock mal(mMonitor); + + // Wait for pending off-thread parses to finish, since they depend on the + // memory allocated by our CachedScripts, and can't be canceled + // asynchronously. + FinishPendingParses(mal); + + // Pending scripts should have been cleared by the above, and new parses + // should not have been queued. + MOZ_ASSERT(mParsingScripts.empty()); + MOZ_ASSERT(mParsingSources.empty()); + MOZ_ASSERT(mPendingScripts.isEmpty()); + + mScripts.Clear(); + + // If we've already finished saving the cache at this point, start a new + // delayed save operation. This will write out an empty cache file in place + // of any cache file we've already written out this session, which will + // prevent us from falling back to the current session's cache file on the + // next startup. + if (mSaveComplete && !mSaveThread && mChildCache) { + mSaveComplete = false; + + StartCacheWrite(); + } + } + + { + MonitorSingleWriterAutoLock saveMonitorAutoLock(mSaveMonitor); + + mCacheInvalidated = true; + } + + // If we're waiting on a timeout to finish saving, interrupt it and just save + // immediately. + mSaveMonitor.NotifyAll(); +} + +nsresult ScriptPreloader::Observe(nsISupports* subject, const char* topic, + const char16_t* data) { + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (!strcmp(topic, STARTUP_COMPLETE_TOPIC)) { + obs->RemoveObserver(this, STARTUP_COMPLETE_TOPIC); + + MOZ_ASSERT(XRE_IsParentProcess()); + + mStartupFinished = true; + URLPreloader::GetSingleton().SetStartupFinished(); + } else if (!strcmp(topic, CACHE_WRITE_TOPIC)) { + obs->RemoveObserver(this, CACHE_WRITE_TOPIC); + + MOZ_ASSERT(mStartupFinished); + MOZ_ASSERT(XRE_IsParentProcess()); + + if (mChildCache && !mSaveComplete && !mSaveThread) { + StartCacheWrite(); + } + } else if (mContentStartupFinishedTopic.Equals(topic)) { + // If this is an uninitialized about:blank viewer or a chrome: document + // (which should always be an XBL binding document), ignore it. We don't + // have to worry about it loading malicious content. + if (nsCOMPtr<dom::Document> doc = do_QueryInterface(subject)) { + nsCOMPtr<nsIURI> uri = doc->GetDocumentURI(); + + if ((NS_IsAboutBlank(uri) && + doc->GetReadyStateEnum() == doc->READYSTATE_UNINITIALIZED) || + uri->SchemeIs("chrome")) { + return NS_OK; + } + } + FinishContentStartup(); + } else if (!strcmp(topic, "timer-callback")) { + FinishContentStartup(); + } else if (!strcmp(topic, XPCOM_SHUTDOWN_TOPIC)) { + // Wait for any pending parses to finish at this point, to avoid creating + // new stencils during destroying the JS runtime. + MonitorAutoLock mal(mMonitor); + FinishPendingParses(mal); + } else if (!strcmp(topic, CACHE_INVALIDATE_TOPIC)) { + InvalidateCache(); + } + + return NS_OK; +} + +void ScriptPreloader::FinishContentStartup() { + MOZ_ASSERT(XRE_IsContentProcess()); + +#ifdef DEBUG + if (mContentStartupFinishedTopic.Equals(CONTENT_DOCUMENT_LOADED_TOPIC)) { + MOZ_ASSERT(sProcessType == ProcessType::PrivilegedAbout); + } else { + MOZ_ASSERT(sProcessType != ProcessType::PrivilegedAbout); + } +#endif /* DEBUG */ + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + obs->RemoveObserver(this, mContentStartupFinishedTopic.get()); + + mSaveTimer = nullptr; + + mStartupFinished = true; + + if (mChildActor) { + mChildActor->SendScriptsAndFinalize(mScripts); + } + +#ifdef XP_WIN + // Record the amount of USS at startup. This is Windows-only for now, + // we could turn it on for Linux relatively cheaply. On macOS it can have + // a perf impact. Only record this for non-privileged processes because + // privileged processes record this value at a different time, leading to + // a higher value which skews the telemetry. + if (sProcessType != ProcessType::PrivilegedAbout) { + mozilla::Telemetry::Accumulate( + mozilla::Telemetry::MEMORY_UNIQUE_CONTENT_STARTUP, + nsMemoryReporterManager::ResidentUnique() / 1024); + } +#endif +} + +bool ScriptPreloader::WillWriteScripts() { + return !mDataPrepared && (XRE_IsParentProcess() || mChildActor); +} + +Result<nsCOMPtr<nsIFile>, nsresult> ScriptPreloader::GetCacheFile( + const nsAString& suffix) { + NS_ENSURE_TRUE(mProfD, Err(NS_ERROR_NOT_INITIALIZED)); + + nsCOMPtr<nsIFile> cacheFile; + MOZ_TRY(mProfD->Clone(getter_AddRefs(cacheFile))); + + MOZ_TRY(cacheFile->AppendNative("startupCache"_ns)); + Unused << cacheFile->Create(nsIFile::DIRECTORY_TYPE, 0777); + + MOZ_TRY(cacheFile->Append(mBaseName + suffix)); + + return std::move(cacheFile); +} + +static const uint8_t MAGIC[] = "mozXDRcachev003"; + +Result<Ok, nsresult> ScriptPreloader::OpenCache() { + if (StartupCache::GetIgnoreDiskCache()) { + return Err(NS_ERROR_ABORT); + } + + MOZ_TRY(NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(mProfD))); + + nsCOMPtr<nsIFile> cacheFile; + MOZ_TRY_VAR(cacheFile, GetCacheFile(u".bin"_ns)); + + bool exists; + MOZ_TRY(cacheFile->Exists(&exists)); + if (exists) { + MOZ_TRY(cacheFile->MoveTo(nullptr, mBaseName + u"-current.bin"_ns)); + } else { + MOZ_TRY(cacheFile->SetLeafName(mBaseName + u"-current.bin"_ns)); + MOZ_TRY(cacheFile->Exists(&exists)); + if (!exists) { + return Err(NS_ERROR_FILE_NOT_FOUND); + } + } + + MOZ_TRY(mCacheData->init(cacheFile)); + + return Ok(); +} + +// Opens the script cache file for this session, and initializes the script +// cache based on its contents. See WriteCache for details of the cache file. +Result<Ok, nsresult> ScriptPreloader::InitCache(const nsAString& basePath) { + mSaveMonitor.AssertOnWritingThread(); + mCacheInitialized = true; + mBaseName = basePath; + + RegisterWeakMemoryReporter(this); + + if (!XRE_IsParentProcess()) { + return Ok(); + } + + // Grab the compilation scope before initializing the URLPreloader, since + // it's not safe to run component loader code during its critical section. + AutoSafeJSAPI jsapi; + JS::RootedObject scope(jsapi.cx(), xpc::CompilationScope()); + + // Note: Code on the main thread *must not access Omnijar in any way* until + // this AutoBeginReading guard is destroyed. + URLPreloader::AutoBeginReading abr; + + MOZ_TRY(OpenCache()); + + return InitCacheInternal(scope); +} + +Result<Ok, nsresult> ScriptPreloader::InitCache( + const Maybe<ipc::FileDescriptor>& cacheFile, ScriptCacheChild* cacheChild) { + mSaveMonitor.AssertOnWritingThread(); + MOZ_ASSERT(XRE_IsContentProcess()); + + mCacheInitialized = true; + mChildActor = cacheChild; + sProcessType = + GetChildProcessType(dom::ContentChild::GetSingleton()->GetRemoteType()); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + MOZ_RELEASE_ASSERT(obs); + + if (sProcessType == ProcessType::PrivilegedAbout) { + // Since we control all of the documents loaded in the privileged + // content process, we can increase the window of active time for the + // ScriptPreloader to include the scripts that are loaded until the + // first document finishes loading. + mContentStartupFinishedTopic.AssignLiteral(CONTENT_DOCUMENT_LOADED_TOPIC); + } else { + // In the child process, we need to freeze the script cache before any + // untrusted code has been executed. The insertion of the first DOM + // document element may sometimes be earlier than is ideal, but at + // least it should always be safe. + mContentStartupFinishedTopic.AssignLiteral(DOC_ELEM_INSERTED_TOPIC); + } + obs->AddObserver(this, mContentStartupFinishedTopic.get(), false); + + RegisterWeakMemoryReporter(this); + + auto cleanup = MakeScopeExit([&] { + // If the parent is expecting cache data from us, make sure we send it + // before it writes out its cache file. For normal proceses, this isn't + // a concern, since they begin loading documents quite early. For the + // preloaded process, we may end up waiting a long time (or, indeed, + // never loading a document), so we need an additional timeout. + if (cacheChild) { + NS_NewTimerWithObserver(getter_AddRefs(mSaveTimer), this, + CHILD_STARTUP_TIMEOUT_MS, + nsITimer::TYPE_ONE_SHOT); + } + }); + + if (cacheFile.isNothing()) { + return Ok(); + } + + MOZ_TRY(mCacheData->init(cacheFile.ref())); + + return InitCacheInternal(); +} + +Result<Ok, nsresult> ScriptPreloader::InitCacheInternal( + JS::HandleObject scope) { + auto size = mCacheData->size(); + + uint32_t headerSize; + uint32_t crc; + if (size < sizeof(MAGIC) + sizeof(headerSize) + sizeof(crc)) { + return Err(NS_ERROR_UNEXPECTED); + } + + auto data = mCacheData->get<uint8_t>(); + MOZ_RELEASE_ASSERT(JS::IsTranscodingBytecodeAligned(data.get())); + + auto end = data + size; + + if (memcmp(MAGIC, data.get(), sizeof(MAGIC))) { + return Err(NS_ERROR_UNEXPECTED); + } + data += sizeof(MAGIC); + + headerSize = LittleEndian::readUint32(data.get()); + data += sizeof(headerSize); + + crc = LittleEndian::readUint32(data.get()); + data += sizeof(crc); + + if (data + headerSize > end) { + return Err(NS_ERROR_UNEXPECTED); + } + + if (crc != ComputeCrc32c(~0, data.get(), headerSize)) { + return Err(NS_ERROR_UNEXPECTED); + } + + { + auto cleanup = MakeScopeExit([&]() { mScripts.Clear(); }); + + LinkedList<CachedStencil> scripts; + + Range<uint8_t> header(data, data + headerSize); + data += headerSize; + + // Reconstruct alignment padding if required. + size_t currentOffset = data - mCacheData->get<uint8_t>(); + data += JS::AlignTranscodingBytecodeOffset(currentOffset) - currentOffset; + + InputBuffer buf(header); + + size_t offset = 0; + while (!buf.finished()) { + auto script = MakeUnique<CachedStencil>(*this, buf); + MOZ_RELEASE_ASSERT(script); + + auto scriptData = data + script->mOffset; + if (!JS::IsTranscodingBytecodeAligned(scriptData.get())) { + return Err(NS_ERROR_UNEXPECTED); + } + + if (scriptData + script->mSize > end) { + return Err(NS_ERROR_UNEXPECTED); + } + + // Make sure offsets match what we'd expect based on script ordering and + // size, as a basic sanity check. + if (script->mOffset != offset) { + return Err(NS_ERROR_UNEXPECTED); + } + offset += script->mSize; + + script->mXDRRange.emplace(scriptData, scriptData + script->mSize); + + // Don't pre-decode the script unless it was used in this process type + // during the previous session. + if (script->mOriginalProcessTypes.contains(CurrentProcessType())) { + scripts.insertBack(script.get()); + } else { + script->mReadyToExecute = true; + } + + const auto& cachePath = script->mCachePath; + mScripts.InsertOrUpdate(cachePath, std::move(script)); + } + + if (buf.error()) { + return Err(NS_ERROR_UNEXPECTED); + } + + mPendingScripts = std::move(scripts); + cleanup.release(); + } + + DecodeNextBatch(OFF_THREAD_FIRST_CHUNK_SIZE, scope); + return Ok(); +} + +void ScriptPreloader::PrepareCacheWriteInternal() { + MOZ_ASSERT(NS_IsMainThread()); + + mMonitor.AssertCurrentThreadOwns(); + + auto cleanup = MakeScopeExit([&]() { + if (mChildCache) { + mChildCache->PrepareCacheWrite(); + } + }); + + if (mDataPrepared) { + return; + } + + AutoSafeJSAPI jsapi; + JSAutoRealm ar(jsapi.cx(), xpc::PrivilegedJunkScope()); + bool found = false; + for (auto& script : IterHash(mScripts, Match<ScriptStatus::Saved>())) { + // Don't write any scripts that are also in the child cache. They'll be + // loaded from the child cache in that case, so there's no need to write + // them twice. + CachedStencil* childScript = + mChildCache ? mChildCache->mScripts.Get(script->mCachePath) : nullptr; + if (childScript && !childScript->mProcessTypes.isEmpty()) { + childScript->UpdateLoadTime(script->mLoadTime); + childScript->mProcessTypes += script->mProcessTypes; + script.Remove(); + continue; + } + + if (!(script->mProcessTypes == script->mOriginalProcessTypes)) { + // Note: EnumSet doesn't support operator!=, hence the weird form above. + found = true; + } + + if (!script->mSize && !script->XDREncode(jsapi.cx())) { + script.Remove(); + } + } + + if (!found) { + mSaveComplete = true; + return; + } + + mDataPrepared = true; +} + +void ScriptPreloader::PrepareCacheWrite() { + MonitorAutoLock mal(mMonitor); + + PrepareCacheWriteInternal(); +} + +// Writes out a script cache file for the scripts accessed during early +// startup in this session. The cache file is a little-endian binary file with +// the following format: +// +// - A uint32 containing the size of the header block. +// +// - A header entry for each file stored in the cache containing: +// - The URL that the script was originally read from. +// - Its cache key. +// - The offset of its XDR data within the XDR data block. +// - The size of its XDR data in the XDR data block. +// - A bit field describing which process types the script is used in. +// +// - A block of XDR data for the encoded scripts, with each script's data at +// an offset from the start of the block, as specified above. +Result<Ok, nsresult> ScriptPreloader::WriteCache() { + MOZ_ASSERT(!NS_IsMainThread()); + mSaveMonitor.AssertCurrentThreadOwns(); + + if (!mDataPrepared && !mSaveComplete) { + MonitorSingleWriterAutoUnlock mau(mSaveMonitor); + + NS_DispatchAndSpinEventLoopUntilComplete( + "ScriptPreloader::PrepareCacheWrite"_ns, + GetMainThreadSerialEventTarget(), + NewRunnableMethod("ScriptPreloader::PrepareCacheWrite", this, + &ScriptPreloader::PrepareCacheWrite)); + } + + if (mSaveComplete) { + // If we don't have anything we need to save, we're done. + return Ok(); + } + + nsCOMPtr<nsIFile> cacheFile; + MOZ_TRY_VAR(cacheFile, GetCacheFile(u"-new.bin"_ns)); + + bool exists; + MOZ_TRY(cacheFile->Exists(&exists)); + if (exists) { + MOZ_TRY(cacheFile->Remove(false)); + } + + { + AutoFDClose fd; + MOZ_TRY(cacheFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE, 0644, + &fd.rwget())); + + // We also need to hold mMonitor while we're touching scripts in + // mScripts, or they may be freed before we're done with them. + mMonitor.AssertNotCurrentThreadOwns(); + MonitorAutoLock mal(mMonitor); + + nsTArray<CachedStencil*> scripts; + for (auto& script : IterHash(mScripts, Match<ScriptStatus::Saved>())) { + scripts.AppendElement(script); + } + + // Sort scripts by load time, with async loaded scripts before sync scripts. + // Since async scripts are always loaded immediately at startup, it helps to + // have them stored contiguously. + scripts.Sort(CachedStencil::Comparator()); + + OutputBuffer buf; + size_t offset = 0; + for (auto script : scripts) { + script->mOffset = offset; + MOZ_DIAGNOSTIC_ASSERT( + JS::IsTranscodingBytecodeOffsetAligned(script->mOffset)); + script->Code(buf); + + offset += script->mSize; + MOZ_DIAGNOSTIC_ASSERT( + JS::IsTranscodingBytecodeOffsetAligned(script->mSize)); + } + + uint8_t headerSize[4]; + LittleEndian::writeUint32(headerSize, buf.cursor()); + + uint8_t crc[4]; + LittleEndian::writeUint32(crc, ComputeCrc32c(~0, buf.Get(), buf.cursor())); + + MOZ_TRY(Write(fd, MAGIC, sizeof(MAGIC))); + MOZ_TRY(Write(fd, headerSize, sizeof(headerSize))); + MOZ_TRY(Write(fd, crc, sizeof(crc))); + MOZ_TRY(Write(fd, buf.Get(), buf.cursor())); + + // Align the start of the scripts section to the transcode alignment. + size_t written = sizeof(MAGIC) + sizeof(headerSize) + buf.cursor(); + size_t padding = JS::AlignTranscodingBytecodeOffset(written) - written; + if (padding) { + MOZ_TRY(WritePadding(fd, padding)); + written += padding; + } + + for (auto script : scripts) { + MOZ_DIAGNOSTIC_ASSERT(JS::IsTranscodingBytecodeOffsetAligned(written)); + MOZ_TRY(Write(fd, script->Range().begin().get(), script->mSize)); + + written += script->mSize; + // We can only free the XDR data if the stencil isn't borrowing data from + // it. + if (script->mStencil && !JS::StencilIsBorrowed(script->mStencil)) { + script->FreeData(); + } + } + } + + MOZ_TRY(cacheFile->MoveTo(nullptr, mBaseName + u".bin"_ns)); + + return Ok(); +} + +nsresult ScriptPreloader::GetName(nsACString& aName) { + aName.AssignLiteral("ScriptPreloader"); + return NS_OK; +} + +// Runs in the mSaveThread thread, and writes out the cache file for the next +// session after a reasonable delay. +nsresult ScriptPreloader::Run() { + MonitorSingleWriterAutoLock mal(mSaveMonitor); + + // Ideally wait about 10 seconds before saving, to avoid unnecessary IO + // during early startup. But only if the cache hasn't been invalidated, + // since that can trigger a new write during shutdown, and we don't want to + // cause shutdown hangs. + if (!mCacheInvalidated) { + mal.Wait(TimeDuration::FromSeconds(10)); + } + + auto result = URLPreloader::GetSingleton().WriteCache(); + Unused << NS_WARN_IF(result.isErr()); + + result = WriteCache(); + Unused << NS_WARN_IF(result.isErr()); + + { + MonitorSingleWriterAutoLock lock(mChildCache->mSaveMonitor); + result = mChildCache->WriteCache(); + } + Unused << NS_WARN_IF(result.isErr()); + + NS_DispatchToMainThread( + NewRunnableMethod("ScriptPreloader::CacheWriteComplete", this, + &ScriptPreloader::CacheWriteComplete), + NS_DISPATCH_NORMAL); + return NS_OK; +} + +void ScriptPreloader::CacheWriteComplete() { + mSaveThread->AsyncShutdown(); + mSaveThread = nullptr; + mSaveComplete = true; + + nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier(); + barrier->RemoveBlocker(this); +} + +void ScriptPreloader::NoteStencil(const nsCString& url, + const nsCString& cachePath, + JS::Stencil* stencil, bool isRunOnce) { + if (!Active()) { + if (isRunOnce) { + if (auto script = mScripts.Get(cachePath)) { + script->mIsRunOnce = true; + script->MaybeDropStencil(); + } + } + return; + } + + // Don't bother trying to cache any URLs with cache-busting query + // parameters. + if (cachePath.FindChar('?') >= 0) { + return; + } + + // Don't bother caching files that belong to the mochitest harness. + constexpr auto mochikitPrefix = "chrome://mochikit/"_ns; + if (StringHead(url, mochikitPrefix.Length()) == mochikitPrefix) { + return; + } + + auto* script = + mScripts.GetOrInsertNew(cachePath, *this, url, cachePath, stencil); + if (isRunOnce) { + script->mIsRunOnce = true; + } + + if (!script->MaybeDropStencil() && !script->mStencil) { + MOZ_ASSERT(stencil); + script->mStencil = stencil; + script->mReadyToExecute = true; + } + + script->UpdateLoadTime(TimeStamp::Now()); + script->mProcessTypes += CurrentProcessType(); +} + +void ScriptPreloader::NoteStencil(const nsCString& url, + const nsCString& cachePath, + ProcessType processType, + nsTArray<uint8_t>&& xdrData, + TimeStamp loadTime) { + // After data has been prepared, there's no point in noting further scripts, + // since the cache either has already been written, or is about to be + // written. Any time prior to the data being prepared, we can safely mutate + // mScripts without locking. After that point, the save thread is free to + // access it, and we can't alter it without locking. + if (mDataPrepared) { + return; + } + + auto* script = + mScripts.GetOrInsertNew(cachePath, *this, url, cachePath, nullptr); + + if (!script->HasRange()) { + MOZ_ASSERT(!script->HasArray()); + + script->mSize = xdrData.Length(); + script->mXDRData.construct<nsTArray<uint8_t>>( + std::forward<nsTArray<uint8_t>>(xdrData)); + + auto& data = script->Array(); + script->mXDRRange.emplace(data.Elements(), data.Length()); + } + + if (!script->mSize && !script->mStencil) { + // If the content process is sending us an entry for a stencil + // which was in the cache at startup, it expects us to already have this + // script data, so it doesn't send it. + // + // However, the cache may have been invalidated at this point (usually + // due to the add-on manager installing or uninstalling a legacy + // extension during very early startup), which means we may no longer + // have an entry for this script. Since that means we have no data to + // write to the new cache, and no JSScript to generate it from, we need + // to discard this entry. + mScripts.Remove(cachePath); + return; + } + + script->UpdateLoadTime(loadTime); + script->mProcessTypes += processType; +} + +/* static */ +void ScriptPreloader::FillCompileOptionsForCachedStencil( + JS::CompileOptions& options) { + // Users of the cache do not require return values, so inform the JS parser in + // order for it to generate simpler bytecode. + options.setNoScriptRval(true); + + // The ScriptPreloader trades off having bytecode available but not source + // text. This means the JS syntax-only parser is not used. If `toString` is + // called on functions in these scripts, the source-hook will fetch it over, + // so using `toString` of functions should be avoided in chrome js. + options.setSourceIsLazy(true); +} + +/* static */ +void ScriptPreloader::FillDecodeOptionsForCachedStencil( + JS::DecodeOptions& options) { + // ScriptPreloader's XDR buffer is alive during the Stencil is alive. + // The decoded stencil can borrow from it. + // + // NOTE: The XDR buffer is alive during the entire browser lifetime only + // when it's mmapped. + options.borrowBuffer = true; +} + +already_AddRefed<JS::Stencil> ScriptPreloader::GetCachedStencil( + JSContext* cx, const JS::DecodeOptions& options, const nsCString& path) { + MOZ_RELEASE_ASSERT( + !(XRE_IsContentProcess() && !mCacheInitialized), + "ScriptPreloader must be initialized before getting cached " + "scripts in the content process."); + + // If a script is used by both the parent and the child, it's stored only + // in the child cache. + if (mChildCache) { + RefPtr<JS::Stencil> stencil = + mChildCache->GetCachedStencilInternal(cx, options, path); + if (stencil) { + Telemetry::AccumulateCategorical( + Telemetry::LABELS_SCRIPT_PRELOADER_REQUESTS::HitChild); + return stencil.forget(); + } + } + + RefPtr<JS::Stencil> stencil = GetCachedStencilInternal(cx, options, path); + Telemetry::AccumulateCategorical( + stencil ? Telemetry::LABELS_SCRIPT_PRELOADER_REQUESTS::Hit + : Telemetry::LABELS_SCRIPT_PRELOADER_REQUESTS::Miss); + return stencil.forget(); +} + +already_AddRefed<JS::Stencil> ScriptPreloader::GetCachedStencilInternal( + JSContext* cx, const JS::DecodeOptions& options, const nsCString& path) { + auto* cachedScript = mScripts.Get(path); + if (cachedScript) { + return WaitForCachedStencil(cx, options, cachedScript); + } + return nullptr; +} + +already_AddRefed<JS::Stencil> ScriptPreloader::WaitForCachedStencil( + JSContext* cx, const JS::DecodeOptions& options, CachedStencil* script) { + // Always check for finished operations so that we can move on to decoding the + // next batch as soon as possible after the pending batch is ready. If we wait + // until we hit an unfinished script, we wind up having at most one batch of + // buffered scripts, and occasionally under-running that buffer. + if (JS::OffThreadToken* token = mToken.exchange(nullptr)) { + FinishOffThreadDecode(token); + } + + if (!script->mReadyToExecute) { + LOG(Info, "Must wait for async script load: %s\n", script->mURL.get()); + auto start = TimeStamp::Now(); + + // If script is small enough, we'd rather recompile on main-thread than wait + // for a decode task to complete. + if (script->mSize < MAX_MAINTHREAD_DECODE_SIZE) { + LOG(Info, "Script is small enough to recompile on main thread\n"); + + script->mReadyToExecute = true; + Telemetry::ScalarAdd( + Telemetry::ScalarID::SCRIPT_PRELOADER_MAINTHREAD_RECOMPILE, 1); + } else { + MonitorAutoLock mal(mMonitor); + + // Process script batches until our target is found. + while (!script->mReadyToExecute) { + if (JS::OffThreadToken* token = mToken.exchange(nullptr)) { + MonitorAutoUnlock mau(mMonitor); + FinishOffThreadDecode(token); + } else { + MOZ_ASSERT(!mParsingScripts.empty()); + mWaitingForDecode = true; + mal.Wait(); + mWaitingForDecode = false; + } + } + } + + double waitedMS = (TimeStamp::Now() - start).ToMilliseconds(); + Telemetry::Accumulate(Telemetry::SCRIPT_PRELOADER_WAIT_TIME, int(waitedMS)); + LOG(Debug, "Waited %fms\n", waitedMS); + } + + return script->GetStencil(cx, options); +} + +/* static */ +void ScriptPreloader::OffThreadDecodeCallback(JS::OffThreadToken* token, + void* context) { + auto cache = static_cast<ScriptPreloader*>(context); + + // Make the token available to main-thread asynchronously. The lock below is + // used for Wait/Notify machinery and isn't needed to update the token itself. + MOZ_ALWAYS_FALSE(cache->mToken.exchange(token)); + + cache->mMonitor.AssertNotCurrentThreadOwns(); + MonitorAutoLock mal(cache->mMonitor); + + if (cache->mWaitingForDecode) { + // Wake up the blocked main thread. + mal.Notify(); + } else if (!cache->mFinishDecodeRunnablePending) { + // Issue a Runnable to ensure batches continue to decode even if the next + // WaitForCachedScript call has not happened yet. + cache->mFinishDecodeRunnablePending = true; + NS_DispatchToMainThread( + NewRunnableMethod("ScriptPreloader::DoFinishOffThreadDecode", cache, + &ScriptPreloader::DoFinishOffThreadDecode)); + } +} + +void ScriptPreloader::FinishPendingParses(MonitorAutoLock& aMal) { + mMonitor.AssertCurrentThreadOwns(); + + // Clear out scripts that we have not issued batch for yet. + mPendingScripts.clear(); + + // Process any pending decodes that are in flight. + while (!mParsingScripts.empty()) { + if (JS::OffThreadToken* token = mToken.exchange(nullptr)) { + MonitorAutoUnlock mau(mMonitor); + FinishOffThreadDecode(token); + } else { + mWaitingForDecode = true; + aMal.Wait(); + mWaitingForDecode = false; + } + } +} + +void ScriptPreloader::DoFinishOffThreadDecode() { + { + MonitorAutoLock mal(mMonitor); + mFinishDecodeRunnablePending = false; + } + + if (JS::OffThreadToken* token = mToken.exchange(nullptr)) { + FinishOffThreadDecode(token); + } +} + +void ScriptPreloader::FinishOffThreadDecode(JS::OffThreadToken* token) { + mMonitor.AssertNotCurrentThreadOwns(); + MOZ_ASSERT(token); + + auto cleanup = MakeScopeExit([&]() { + mParsingSources.clear(); + mParsingScripts.clear(); + + DecodeNextBatch(OFF_THREAD_CHUNK_SIZE); + }); + + AutoSafeJSAPI jsapi; + JSContext* cx = jsapi.cx(); + + JSAutoRealm ar(cx, xpc::CompilationScope()); + Vector<RefPtr<JS::Stencil>> stencils; + + // If this fails, we still need to mark the scripts as finished. Any that + // weren't successfully compiled in this operation (which should never + // happen under ordinary circumstances) will be re-decoded on the main + // thread, and raise the appropriate errors when they're executed. + // + // The exception from the off-thread decode operation will be reported when + // we pop the AutoJSAPI off the stack. + Unused << JS::FinishDecodeMultiStencilsOffThread(cx, token, &stencils); + + unsigned i = 0; + for (auto script : mParsingScripts) { + LOG(Debug, "Finished off-thread decode of %s\n", script->mURL.get()); + if (i < stencils.length()) { + script->mStencil = stencils[i++].forget(); + } + script->mReadyToExecute = true; + } +} + +void ScriptPreloader::DecodeNextBatch(size_t chunkSize, + JS::HandleObject scope) { + MOZ_ASSERT(mParsingSources.length() == 0); + MOZ_ASSERT(mParsingScripts.length() == 0); + + auto cleanup = MakeScopeExit([&]() { + mParsingScripts.clearAndFree(); + mParsingSources.clearAndFree(); + }); + + auto start = TimeStamp::Now(); + LOG(Debug, "Off-thread decoding scripts...\n"); + + size_t size = 0; + for (CachedStencil* next = mPendingScripts.getFirst(); next;) { + auto* script = next; + next = script->getNext(); + + MOZ_ASSERT(script->IsMemMapped()); + + // Skip any scripts that we decoded on the main thread rather than + // waiting for an off-thread operation to complete. + if (script->mReadyToExecute) { + script->remove(); + continue; + } + // If we have enough data for one chunk and this script would put us + // over our chunk size limit, we're done. + if (size > SMALL_SCRIPT_CHUNK_THRESHOLD && + size + script->mSize > chunkSize) { + break; + } + if (!mParsingScripts.append(script) || + !mParsingSources.emplaceBack(script->Range(), script->mURL.get(), 0)) { + break; + } + + LOG(Debug, "Beginning off-thread decode of script %s (%u bytes)\n", + script->mURL.get(), script->mSize); + + script->remove(); + size += script->mSize; + } + + if (size == 0 && mPendingScripts.isEmpty()) { + return; + } + + AutoSafeJSAPI jsapi; + JSContext* cx = jsapi.cx(); + JSAutoRealm ar(cx, scope ? scope : xpc::CompilationScope()); + + JS::CompileOptions options(cx); + FillCompileOptionsForCachedStencil(options); + + // All XDR buffers are mmapped and live longer than JS runtime. + // The bytecode can be borrowed from the buffer. + options.borrowBuffer = true; + options.usePinnedBytecode = true; + + JS::DecodeOptions decodeOptions(options); + + if (!JS::CanDecodeOffThread(cx, decodeOptions, size) || + !JS::DecodeMultiStencilsOffThread(cx, decodeOptions, mParsingSources, + OffThreadDecodeCallback, + static_cast<void*>(this))) { + // If we fail here, we don't move on to process the next batch, so make + // sure we don't have any other scripts left to process. + MOZ_ASSERT(mPendingScripts.isEmpty()); + for (auto script : mPendingScripts) { + script->mReadyToExecute = true; + } + + LOG(Info, "Can't decode %lu bytes of scripts off-thread", + (unsigned long)size); + for (auto script : mParsingScripts) { + script->mReadyToExecute = true; + } + return; + } + + cleanup.release(); + + LOG(Debug, "Initialized decoding of %u scripts (%u bytes) in %fms\n", + (unsigned)mParsingSources.length(), (unsigned)size, + (TimeStamp::Now() - start).ToMilliseconds()); +} + +ScriptPreloader::CachedStencil::CachedStencil(ScriptPreloader& cache, + InputBuffer& buf) + : mCache(cache) { + Code(buf); + + // Swap the mProcessTypes and mOriginalProcessTypes values, since we want to + // start with an empty set of processes loaded into for this session, and + // compare against last session's values later. + mOriginalProcessTypes = mProcessTypes; + mProcessTypes = {}; +} + +bool ScriptPreloader::CachedStencil::XDREncode(JSContext* cx) { + auto cleanup = MakeScopeExit([&]() { MaybeDropStencil(); }); + + mXDRData.construct<JS::TranscodeBuffer>(); + + JS::TranscodeResult code = JS::EncodeStencil(cx, mStencil, Buffer()); + if (code == JS::TranscodeResult::Ok) { + mXDRRange.emplace(Buffer().begin(), Buffer().length()); + mSize = Range().length(); + return true; + } + mXDRData.destroy(); + JS_ClearPendingException(cx); + return false; +} + +already_AddRefed<JS::Stencil> ScriptPreloader::CachedStencil::GetStencil( + JSContext* cx, const JS::DecodeOptions& options) { + MOZ_ASSERT(mReadyToExecute); + if (mStencil) { + return do_AddRef(mStencil); + } + + if (!HasRange()) { + // We've already executed the script, and thrown it away. But it wasn't + // in the cache at startup, so we don't have any data to decode. Give + // up. + return nullptr; + } + + // If we have no script at this point, the script was too small to decode + // off-thread, or it was needed before the off-thread compilation was + // finished, and is small enough to decode on the main thread rather than + // wait for the off-thread decoding to finish. In either case, we decode + // it synchronously the first time it's needed. + + auto start = TimeStamp::Now(); + LOG(Info, "Decoding stencil %s on main thread...\n", mURL.get()); + + RefPtr<JS::Stencil> stencil; + if (JS::DecodeStencil(cx, options, Range(), getter_AddRefs(stencil)) == + JS::TranscodeResult::Ok) { + // Lock the monitor here to avoid data races on mScript + // from other threads like the cache writing thread. + // + // It is possible that we could end up decoding the same + // script twice, because DecodeScript isn't being guarded + // by the monitor; however, to encourage off-thread decode + // to proceed for other scripts we don't hold the monitor + // while doing main thread decode, merely while updating + // mScript. + mCache.mMonitor.AssertNotCurrentThreadOwns(); + MonitorAutoLock mal(mCache.mMonitor); + + mStencil = stencil.forget(); + + if (mCache.mSaveComplete) { + // We can only free XDR data if the stencil isn't borrowing data out of + // it. + if (!JS::StencilIsBorrowed(mStencil)) { + FreeData(); + } + } + } + + LOG(Debug, "Finished decoding in %fms", + (TimeStamp::Now() - start).ToMilliseconds()); + + return do_AddRef(mStencil); +} + +// nsIAsyncShutdownBlocker + +nsresult ScriptPreloader::GetName(nsAString& aName) { + aName.AssignLiteral(u"ScriptPreloader: Saving bytecode cache"); + return NS_OK; +} + +nsresult ScriptPreloader::GetState(nsIPropertyBag** aState) { + *aState = nullptr; + return NS_OK; +} + +nsresult ScriptPreloader::BlockShutdown( + nsIAsyncShutdownClient* aBarrierClient) { + // If we're waiting on a timeout to finish saving, interrupt it and just save + // immediately. + mSaveMonitor.NotifyAll(); + return NS_OK; +} + +already_AddRefed<nsIAsyncShutdownClient> ScriptPreloader::GetShutdownBarrier() { + nsCOMPtr<nsIAsyncShutdownService> svc = components::AsyncShutdown::Service(); + MOZ_RELEASE_ASSERT(svc); + + nsCOMPtr<nsIAsyncShutdownClient> barrier; + Unused << svc->GetXpcomWillShutdown(getter_AddRefs(barrier)); + MOZ_RELEASE_ASSERT(barrier); + + return barrier.forget(); +} + +NS_IMPL_ISUPPORTS(ScriptPreloader, nsIObserver, nsIRunnable, nsIMemoryReporter, + nsINamed, nsIAsyncShutdownBlocker) + +#undef LOG + +} // namespace mozilla diff --git a/js/xpconnect/loader/ScriptPreloader.h b/js/xpconnect/loader/ScriptPreloader.h new file mode 100644 index 0000000000..e1868b6f40 --- /dev/null +++ b/js/xpconnect/loader/ScriptPreloader.h @@ -0,0 +1,543 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#ifndef ScriptPreloader_h +#define ScriptPreloader_h + +#include "mozilla/Atomics.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/EnumSet.h" +#include "mozilla/LinkedList.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/Maybe.h" +#include "mozilla/MaybeOneOf.h" +#include "mozilla/Monitor.h" +#include "mozilla/Range.h" +#include "mozilla/Vector.h" +#include "mozilla/Result.h" +#include "mozilla/loader/AutoMemMap.h" +#include "MainThreadUtils.h" +#include "nsClassHashtable.h" +#include "nsIAsyncShutdown.h" +#include "nsIFile.h" +#include "nsIMemoryReporter.h" +#include "nsIObserver.h" +#include "nsIThread.h" +#include "nsITimer.h" + +#include "js/CompileOptions.h" // JS::DecodeOptions +#include "js/experimental/JSStencil.h" +#include "js/GCAnnotations.h" // for JS_HAZ_NON_GC_POINTER +#include "js/RootingAPI.h" // for Handle, Heap +#include "js/Transcoding.h" // for TranscodeBuffer, TranscodeRange, TranscodeSources +#include "js/TypeDecls.h" // for HandleObject, HandleScript + +#include <prio.h> + +namespace JS { +class CompileOptions; +class OffThreadToken; +} // namespace JS + +namespace mozilla { +namespace dom { +class ContentParent; +} +namespace ipc { +class FileDescriptor; +} +namespace loader { +class InputBuffer; +class ScriptCacheChild; + +enum class ProcessType : uint8_t { + Uninitialized, + Parent, + Web, + Extension, + PrivilegedAbout, +}; + +template <typename T> +struct Matcher { + virtual bool Matches(T) = 0; +}; +} // namespace loader + +using namespace mozilla::loader; + +class ScriptPreloader : public nsIObserver, + public nsIMemoryReporter, + public nsIRunnable, + public nsINamed, + public nsIAsyncShutdownBlocker, + public SingleWriterLockOwner { + MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf) + + friend class mozilla::loader::ScriptCacheChild; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSIMEMORYREPORTER + NS_DECL_NSIRUNNABLE + NS_DECL_NSINAMED + NS_DECL_NSIASYNCSHUTDOWNBLOCKER + + private: + static StaticRefPtr<ScriptPreloader> gScriptPreloader; + static StaticRefPtr<ScriptPreloader> gChildScriptPreloader; + static UniquePtr<AutoMemMap> gCacheData; + static UniquePtr<AutoMemMap> gChildCacheData; + + public: + static ScriptPreloader& GetSingleton(); + static ScriptPreloader& GetChildSingleton(); + + static void DeleteSingleton(); + static void DeleteCacheDataSingleton(); + + static ProcessType GetChildProcessType(const nsACString& remoteType); + + // Fill some options that should be consistent across all scripts stored + // into preloader cache. + static void FillCompileOptionsForCachedStencil(JS::CompileOptions& options); + static void FillDecodeOptionsForCachedStencil(JS::DecodeOptions& options); + + bool OnWritingThread() const override { return NS_IsMainThread(); } + + // Retrieves the stencil with the given cache key from the cache. + // Returns null if the stencil is not cached. + already_AddRefed<JS::Stencil> GetCachedStencil( + JSContext* cx, const JS::DecodeOptions& options, const nsCString& path); + + // Notes the execution of a script with the given URL and cache key. + // Depending on the stage of startup, the script may be serialized and + // stored to the startup script cache. + // + // If isRunOnce is true, this script is expected to run only once per + // process per browser session. A cached instance will not be kept alive + // for repeated execution. + void NoteStencil(const nsCString& url, const nsCString& cachePath, + JS::Stencil* stencil, bool isRunOnce = false); + + // Notes the IPC arrival of the XDR data of a stencil compiled by some + // child process. See ScriptCacheChild::SendScriptsAndFinalize. + void NoteStencil(const nsCString& url, const nsCString& cachePath, + ProcessType processType, nsTArray<uint8_t>&& xdrData, + TimeStamp loadTime); + + // Initializes the script cache from the startup script cache file. + Result<Ok, nsresult> InitCache(const nsAString& = u"scriptCache"_ns); + + Result<Ok, nsresult> InitCache(const Maybe<ipc::FileDescriptor>& cacheFile, + ScriptCacheChild* cacheChild); + + bool Active() const { return mCacheInitialized && !mStartupFinished; } + + private: + Result<Ok, nsresult> InitCacheInternal(JS::Handle<JSObject*> scope = nullptr); + already_AddRefed<JS::Stencil> GetCachedStencilInternal( + JSContext* cx, const JS::DecodeOptions& options, const nsCString& path); + + public: + static ProcessType CurrentProcessType() { + MOZ_ASSERT(sProcessType != ProcessType::Uninitialized); + return sProcessType; + } + + static void InitContentChild(dom::ContentParent& parent); + + protected: + virtual ~ScriptPreloader(); + + private: + enum class ScriptStatus { + Restored, + Saved, + }; + + // Represents a cached script stencil, either initially read from the + // cache file, to be added to the next session's stencil cache file, or + // both. + // + // - Read from the cache, and being decoded off thread. In this case, + // mReadyToExecute is false, and mToken is null. + // - Off-thread decode has finished, but the stencil has not yet been + // executed. In this case, mReadyToExecute is true, and mToken has a + // non-null value. + // - Read from the cache, but too small or needed to immediately to be + // compiled off-thread. In this case, mReadyToExecute is true, and both + // mToken and mStencil are null. + // - Fully decoded, and ready to be added to the next session's cache + // file. In this case, mReadyToExecute is true, and mStencil is non-null. + // + // A stencil to be added to the next session's cache file always has a + // non-null mStencil value. If it was read from the last session's cache + // file, it also has a non-empty mXDRRange range, which will be stored in + // the next session's cache file. If it was compiled in this session, its + // mXDRRange will initially be empty, and its mXDRData buffer will be + // populated just before it is written to the cache file. + class CachedStencil : public LinkedListElement<CachedStencil> { + public: + CachedStencil(CachedStencil&&) = delete; + + CachedStencil(ScriptPreloader& cache, const nsCString& url, + const nsCString& cachePath, JS::Stencil* stencil) + : mCache(cache), + mURL(url), + mCachePath(cachePath), + mStencil(stencil), + mReadyToExecute(true), + mIsRunOnce(false) {} + + inline CachedStencil(ScriptPreloader& cache, InputBuffer& buf); + + ~CachedStencil() = default; + + ScriptStatus Status() const { + return mProcessTypes.isEmpty() ? ScriptStatus::Restored + : ScriptStatus::Saved; + } + + // For use with nsTArray::Sort. + // + // Orders scripts by script load time, so that scripts which are needed + // earlier are stored earlier, and scripts needed at approximately the + // same time are stored approximately contiguously. + struct Comparator { + bool Equals(const CachedStencil* a, const CachedStencil* b) const { + return a->mLoadTime == b->mLoadTime; + } + + bool LessThan(const CachedStencil* a, const CachedStencil* b) const { + return a->mLoadTime < b->mLoadTime; + } + }; + + struct StatusMatcher final : public Matcher<CachedStencil*> { + explicit StatusMatcher(ScriptStatus status) : mStatus(status) {} + + virtual bool Matches(CachedStencil* script) override { + return script->Status() == mStatus; + } + + const ScriptStatus mStatus; + }; + + void FreeData() { + // If the script data isn't mmapped, we need to release both it + // and the Range that points to it at the same time. + if (!IsMemMapped()) { + mXDRRange.reset(); + mXDRData.destroy(); + } + } + + void UpdateLoadTime(const TimeStamp& loadTime) { + if (mLoadTime.IsNull() || loadTime < mLoadTime) { + mLoadTime = loadTime; + } + } + + // Checks whether the cached JSScript for this entry will be needed + // again and, if not, drops it and returns true. This is the case for + // run-once scripts that do not still need to be encoded into the + // cache. + // + // If this method returns false, callers may set mScript to a cached + // JSScript instance for this entry. If it returns true, they should + // not. + bool MaybeDropStencil() { + if (mIsRunOnce && (HasRange() || !mCache.WillWriteScripts())) { + mStencil = nullptr; + return true; + } + return false; + } + + // Encodes this script into XDR data, and stores the result in mXDRData. + // Returns true on success, false on failure. + bool XDREncode(JSContext* cx); + + // Encodes or decodes this script, in the storage format required by the + // script cache file. + template <typename Buffer> + void Code(Buffer& buffer) { + buffer.codeString(mURL); + buffer.codeString(mCachePath); + buffer.codeUint32(mOffset); + buffer.codeUint32(mSize); + buffer.codeUint8(mProcessTypes); + } + + // Returns the XDR data generated for this script during this session. See + // mXDRData. + JS::TranscodeBuffer& Buffer() { + MOZ_ASSERT(HasBuffer()); + return mXDRData.ref<JS::TranscodeBuffer>(); + } + + bool HasBuffer() { return mXDRData.constructed<JS::TranscodeBuffer>(); } + + // Returns the read-only XDR data for this script. See mXDRRange. + const JS::TranscodeRange& Range() { + MOZ_ASSERT(HasRange()); + return mXDRRange.ref(); + } + + bool HasRange() { return mXDRRange.isSome(); } + + bool IsMemMapped() const { return mXDRData.empty(); } + + nsTArray<uint8_t>& Array() { + MOZ_ASSERT(HasArray()); + return mXDRData.ref<nsTArray<uint8_t>>(); + } + + bool HasArray() { return mXDRData.constructed<nsTArray<uint8_t>>(); } + + already_AddRefed<JS::Stencil> GetStencil(JSContext* cx, + const JS::DecodeOptions& options); + + size_t HeapSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) { + auto size = mallocSizeOf(this); + + if (HasArray()) { + size += Array().ShallowSizeOfExcludingThis(mallocSizeOf); + } else if (HasBuffer()) { + size += Buffer().sizeOfExcludingThis(mallocSizeOf); + } + + if (mStencil) { + size += JS::SizeOfStencil(mStencil, mallocSizeOf); + } + + // Note: mURL and mCachePath use the same string for scripts loaded + // by the message manager. The following statement avoids + // double-measuring in that case. + size += (mURL.SizeOfExcludingThisIfUnshared(mallocSizeOf) + + mCachePath.SizeOfExcludingThisEvenIfShared(mallocSizeOf)); + + return size; + } + + ScriptPreloader& mCache; + + // The URL from which this script was initially read and compiled. + nsCString mURL; + // A unique identifier for this script's filesystem location, used as a + // primary cache lookup value. + nsCString mCachePath; + + // The offset of this script in the cache file, from the start of the XDR + // data block. + uint32_t mOffset = 0; + // The size of this script's encoded XDR data. + uint32_t mSize = 0; + + TimeStamp mLoadTime{}; + + RefPtr<JS::Stencil> mStencil; + + // True if this script is ready to be executed. This means that either the + // off-thread portion of an off-thread decode has finished, or the script + // is too small to be decoded off-thread, and may be immediately decoded + // whenever it is first executed. + bool mReadyToExecute = false; + + // True if this script is expected to run once per process. If so, its + // JSScript instance will be dropped as soon as the script has + // executed and been encoded into the cache. + bool mIsRunOnce = false; + + // The set of processes in which this script has been used. + EnumSet<ProcessType> mProcessTypes{}; + + // The set of processes which the script was loaded into during the + // last session, as read from the cache file. + EnumSet<ProcessType> mOriginalProcessTypes{}; + + // The read-only XDR data for this script, which was either read from an + // existing cache file, or generated by encoding a script which was + // compiled during this session. + Maybe<JS::TranscodeRange> mXDRRange; + + // XDR data which was generated from a script compiled during this + // session, and will be written to the cache file. + // + // The format is JS::TranscodeBuffer if the script was XDR'd as part + // of this process, or nsTArray<> if the script was transfered by IPC + // from a child process. + MaybeOneOf<JS::TranscodeBuffer, nsTArray<uint8_t>> mXDRData; + } JS_HAZ_NON_GC_POINTER; + + template <ScriptStatus status> + static Matcher<CachedStencil*>* Match() { + static CachedStencil::StatusMatcher matcher{status}; + return &matcher; + } + + // There's a significant setup cost for each off-thread decode operation, + // so scripts are decoded in chunks to minimize the overhead. There's a + // careful balancing act in choosing the size of chunks, to minimize the + // number of decode operations, while also minimizing the number of buffer + // underruns that require the main thread to wait for a script to finish + // decoding. + // + // For the first chunk, we don't have much time between the start of the + // decode operation and the time the first script is needed, so that chunk + // needs to be fairly small. After the first chunk is finished, we have + // some buffered scripts to fall back on, and a lot more breathing room, + // so the chunks can be a bit bigger, but still not too big. + static constexpr int OFF_THREAD_FIRST_CHUNK_SIZE = 128 * 1024; + static constexpr int OFF_THREAD_CHUNK_SIZE = 512 * 1024; + + // Ideally, we want every chunk to be smaller than the chunk sizes + // specified above. However, if we have some number of small scripts + // followed by a huge script that would put us over the normal chunk size, + // we're better off processing them as a single chunk. + // + // In order to guarantee that the JS engine will process a chunk + // off-thread, it needs to be at least 100K (which is an implementation + // detail that can change at any time), so make sure that we always hit at + // least that size, with a bit of breathing room to be safe. + static constexpr int SMALL_SCRIPT_CHUNK_THRESHOLD = 128 * 1024; + + // The maximum size of scripts to re-decode on the main thread if off-thread + // decoding hasn't finished yet. In practice, we don't hit this very often, + // but when we do, re-decoding some smaller scripts on the main thread gives + // the background decoding a chance to catch up without blocking the main + // thread for quite as long. + static constexpr int MAX_MAINTHREAD_DECODE_SIZE = 50 * 1024; + + explicit ScriptPreloader(AutoMemMap* cacheData); + + void Cleanup(); + + void FinishPendingParses(MonitorAutoLock& aMal); + void InvalidateCache(); + + // Opens the cache file for reading. + Result<Ok, nsresult> OpenCache(); + + // Writes a new cache file to disk. Must not be called on the main thread. + Result<Ok, nsresult> WriteCache() MOZ_REQUIRES(mSaveMonitor); + + void StartCacheWrite(); + + // Prepares scripts for writing to the cache, serializing new scripts to + // XDR, and calculating their size-based offsets. + void PrepareCacheWrite(); + + void PrepareCacheWriteInternal(); + + void CacheWriteComplete(); + + void FinishContentStartup(); + + // Returns true if scripts added to the cache now will be encoded and + // written to the cache. If we've already encoded scripts for the cache + // write, or this is a content process which hasn't been asked to return + // script bytecode, this will return false. + bool WillWriteScripts(); + + // Returns a file pointer for the cache file with the given name in the + // current profile. + Result<nsCOMPtr<nsIFile>, nsresult> GetCacheFile(const nsAString& suffix); + + // Waits for the given cached script to finish compiling off-thread, or + // decodes it synchronously on the main thread, as appropriate. + already_AddRefed<JS::Stencil> WaitForCachedStencil( + JSContext* cx, const JS::DecodeOptions& options, CachedStencil* script); + + void DecodeNextBatch(size_t chunkSize, JS::Handle<JSObject*> scope = nullptr); + + static void OffThreadDecodeCallback(JS::OffThreadToken* token, void* context); + void FinishOffThreadDecode(JS::OffThreadToken* token); + void DoFinishOffThreadDecode(); + + already_AddRefed<nsIAsyncShutdownClient> GetShutdownBarrier(); + + size_t ShallowHeapSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) { + return (mallocSizeOf(this) + + mScripts.ShallowSizeOfExcludingThis(mallocSizeOf) + + mallocSizeOf(mSaveThread.get()) + mallocSizeOf(mProfD.get())); + } + + using ScriptHash = nsClassHashtable<nsCStringHashKey, CachedStencil>; + + template <ScriptStatus status> + static size_t SizeOfHashEntries(ScriptHash& scripts, + mozilla::MallocSizeOf mallocSizeOf) { + size_t size = 0; + for (auto elem : IterHash(scripts, Match<status>())) { + size += elem->HeapSizeOfIncludingThis(mallocSizeOf); + } + return size; + } + + ScriptHash mScripts; + + // True after we've shown the first window, and are no longer adding new + // scripts to the cache. + bool mStartupFinished = false; + + bool mCacheInitialized = false; + bool mSaveComplete = false; + bool mDataPrepared = false; + // May only be changed on the main thread, while `mSaveMonitor` is held. + bool mCacheInvalidated MOZ_GUARDED_BY(mSaveMonitor) = false; + + // The list of scripts that we read from the initial startup cache file, + // but have yet to initiate a decode task for. + LinkedList<CachedStencil> mPendingScripts; + + // The lists of scripts and their sources that make up the chunk currently + // being decoded in a background thread. + JS::TranscodeSources mParsingSources; + Vector<CachedStencil*> mParsingScripts; + + // The token for the completed off-thread decode task. + Atomic<JS::OffThreadToken*, ReleaseAcquire> mToken{nullptr}; + + // True if a runnable has been dispatched to the main thread to finish an + // off-thread decode operation. Access only while 'mMonitor' is held. + bool mFinishDecodeRunnablePending MOZ_GUARDED_BY(mMonitor) = false; + + // True is main-thread is blocked and we should notify with Monitor. Access + // only while `mMonitor` is held. + bool mWaitingForDecode MOZ_GUARDED_BY(mMonitor) = false; + + // The process type of the current process. + static ProcessType sProcessType; + + // The process types for which remote processes have been initialized, and + // are expected to send back script data. + EnumSet<ProcessType> mInitializedProcesses{}; + + RefPtr<ScriptPreloader> mChildCache; + ScriptCacheChild* mChildActor = nullptr; + + nsString mBaseName; + nsCString mContentStartupFinishedTopic; + + nsCOMPtr<nsIFile> mProfD; + nsCOMPtr<nsIThread> mSaveThread; + nsCOMPtr<nsITimer> mSaveTimer; + + // The mmapped cache data from this session's cache file. + // The instance is held by either `gCacheData` or `gChildCacheData` static + // fields, and its lifetime is guaranteed to be longer than ScriptPreloader + // instance. + AutoMemMap* mCacheData; + + Monitor mMonitor; + MonitorSingleWriter mSaveMonitor MOZ_ACQUIRED_BEFORE(mMonitor); +}; + +} // namespace mozilla + +#endif // ScriptPreloader_h diff --git a/js/xpconnect/loader/SkipCheckForBrokenURLOrZeroSized.h b/js/xpconnect/loader/SkipCheckForBrokenURLOrZeroSized.h new file mode 100644 index 0000000000..3a413b898f --- /dev/null +++ b/js/xpconnect/loader/SkipCheckForBrokenURLOrZeroSized.h @@ -0,0 +1,22 @@ +/* -*- 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/. */ + +#ifndef mozilla_loader_SkipCheckForBrokenURLOrZeroSized_h +#define mozilla_loader_SkipCheckForBrokenURLOrZeroSized_h + +#include <stdint.h> // uint8_t + +namespace mozilla { +namespace loader { + +// Represents the `aSkipCheckForBrokenURLOrZeroSized` parameter for +// `NS_NewChannel` function. +enum class SkipCheckForBrokenURLOrZeroSized : uint8_t { No, Yes }; + +} // namespace loader +} // namespace mozilla + +#endif // mozilla_loader_SkipCheckForBrokenURLOrZeroSized_h diff --git a/js/xpconnect/loader/URLPreloader.cpp b/js/xpconnect/loader/URLPreloader.cpp new file mode 100644 index 0000000000..c5bcbad5bb --- /dev/null +++ b/js/xpconnect/loader/URLPreloader.cpp @@ -0,0 +1,707 @@ +/* -*- 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 "ScriptPreloader-inl.h" +#include "mozilla/URLPreloader.h" +#include "mozilla/loader/AutoMemMap.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/FileUtils.h" +#include "mozilla/IOBuffers.h" +#include "mozilla/Logging.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Services.h" +#include "mozilla/Unused.h" +#include "mozilla/Vector.h" +#include "mozilla/scache/StartupCache.h" + +#include "crc32c.h" +#include "MainThreadUtils.h" +#include "nsPrintfCString.h" +#include "nsDebug.h" +#include "nsIFile.h" +#include "nsIFileURL.h" +#include "nsNetUtil.h" +#include "nsPromiseFlatString.h" +#include "nsProxyRelease.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" +#include "nsZipArchive.h" +#include "xpcpublic.h" + +namespace mozilla { +namespace { +static LazyLogModule gURLLog("URLPreloader"); + +#define LOG(level, ...) MOZ_LOG(gURLLog, LogLevel::level, (__VA_ARGS__)) + +template <typename T> +bool StartsWith(const T& haystack, const T& needle) { + return StringHead(haystack, needle.Length()) == needle; +} +} // anonymous namespace + +using namespace mozilla::loader; +using mozilla::scache::StartupCache; + +nsresult URLPreloader::CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) { + MOZ_COLLECT_REPORT("explicit/url-preloader/other", KIND_HEAP, UNITS_BYTES, + ShallowSizeOfIncludingThis(MallocSizeOf), + "Memory used by the URL preloader service itself."); + + for (const auto& elem : mCachedURLs.Values()) { + nsAutoCString pathName; + pathName.Append(elem->mPath); + // The backslashes will automatically be replaced with slashes in + // about:memory, without splitting each path component into a separate + // branch in the memory report tree. + pathName.ReplaceChar('/', '\\'); + + nsPrintfCString path("explicit/url-preloader/cached-urls/%s/[%s]", + elem->TypeString(), pathName.get()); + + aHandleReport->Callback( + ""_ns, path, KIND_HEAP, UNITS_BYTES, + elem->SizeOfIncludingThis(MallocSizeOf), + nsLiteralCString("Memory used to hold cache data for files which " + "have been read or pre-loaded during this session."), + aData); + } + + return NS_OK; +} + +// static +already_AddRefed<URLPreloader> URLPreloader::Create(bool* aInitialized) { + // The static APIs like URLPreloader::Read work in the child process because + // they fall back to a synchronous read. The actual preloader must be + // explicitly initialized, and this should only be done in the parent. + MOZ_RELEASE_ASSERT(XRE_IsParentProcess()); + + RefPtr<URLPreloader> preloader = new URLPreloader(); + if (preloader->InitInternal().isOk()) { + *aInitialized = true; + RegisterWeakMemoryReporter(preloader); + } else { + *aInitialized = false; + } + + return preloader.forget(); +} + +URLPreloader& URLPreloader::GetSingleton() { + if (!sSingleton) { + sSingleton = Create(&sInitialized); + ClearOnShutdown(&sSingleton); + } + + return *sSingleton; +} + +bool URLPreloader::sInitialized = false; + +StaticRefPtr<URLPreloader> URLPreloader::sSingleton; + +URLPreloader::~URLPreloader() { + if (sInitialized) { + UnregisterWeakMemoryReporter(this); + sInitialized = false; + } +} + +Result<Ok, nsresult> URLPreloader::InitInternal() { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + + if (Omnijar::HasOmnijar(Omnijar::GRE)) { + MOZ_TRY(Omnijar::GetURIString(Omnijar::GRE, mGREPrefix)); + } + if (Omnijar::HasOmnijar(Omnijar::APP)) { + MOZ_TRY(Omnijar::GetURIString(Omnijar::APP, mAppPrefix)); + } + + nsresult rv; + nsCOMPtr<nsIIOService> ios = do_GetIOService(&rv); + MOZ_TRY(rv); + + nsCOMPtr<nsIProtocolHandler> ph; + MOZ_TRY(ios->GetProtocolHandler("resource", getter_AddRefs(ph))); + + mResProto = do_QueryInterface(ph, &rv); + MOZ_TRY(rv); + + mChromeReg = services::GetChromeRegistry(); + if (!mChromeReg) { + return Err(NS_ERROR_UNEXPECTED); + } + + MOZ_TRY(NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(mProfD))); + + return Ok(); +} + +URLPreloader& URLPreloader::ReInitialize() { + MOZ_ASSERT(sSingleton); + sSingleton = nullptr; + sSingleton = Create(&sInitialized); + return *sSingleton; +} + +Result<nsCOMPtr<nsIFile>, nsresult> URLPreloader::GetCacheFile( + const nsAString& suffix) { + if (!mProfD) { + return Err(NS_ERROR_NOT_INITIALIZED); + } + + nsCOMPtr<nsIFile> cacheFile; + MOZ_TRY(mProfD->Clone(getter_AddRefs(cacheFile))); + + MOZ_TRY(cacheFile->AppendNative("startupCache"_ns)); + Unused << cacheFile->Create(nsIFile::DIRECTORY_TYPE, 0777); + + MOZ_TRY(cacheFile->Append(u"urlCache"_ns + suffix)); + + return std::move(cacheFile); +} + +static const uint8_t URL_MAGIC[] = "mozURLcachev003"; + +Result<nsCOMPtr<nsIFile>, nsresult> URLPreloader::FindCacheFile() { + if (StartupCache::GetIgnoreDiskCache()) { + return Err(NS_ERROR_ABORT); + } + + nsCOMPtr<nsIFile> cacheFile; + MOZ_TRY_VAR(cacheFile, GetCacheFile(u".bin"_ns)); + + bool exists; + MOZ_TRY(cacheFile->Exists(&exists)); + if (exists) { + MOZ_TRY(cacheFile->MoveTo(nullptr, u"urlCache-current.bin"_ns)); + } else { + MOZ_TRY(cacheFile->SetLeafName(u"urlCache-current.bin"_ns)); + MOZ_TRY(cacheFile->Exists(&exists)); + if (!exists) { + return Err(NS_ERROR_FILE_NOT_FOUND); + } + } + + return std::move(cacheFile); +} + +Result<Ok, nsresult> URLPreloader::WriteCache() { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(mStartupFinished); + + // The script preloader might call us a second time, if it has to re-write + // its cache after a cache flush. We don't care about cache flushes, since + // our cache doesn't store any file data, only paths. And we currently clear + // our cached file list after the first write, which means that a second + // write would (aside from breaking the invariant that we never touch + // mCachedURLs off-main-thread after the first write, and trigger a data + // race) mean we get no pre-loading on the next startup. + if (mCacheWritten) { + return Ok(); + } + mCacheWritten = true; + + LOG(Debug, "Writing cache..."); + + nsCOMPtr<nsIFile> cacheFile; + MOZ_TRY_VAR(cacheFile, GetCacheFile(u"-new.bin"_ns)); + + bool exists; + MOZ_TRY(cacheFile->Exists(&exists)); + if (exists) { + MOZ_TRY(cacheFile->Remove(false)); + } + + { + AutoFDClose fd; + MOZ_TRY(cacheFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE, 0644, + &fd.rwget())); + + nsTArray<URLEntry*> entries; + for (const auto& entry : mCachedURLs.Values()) { + if (entry->mReadTime) { + entries.AppendElement(entry.get()); + } + } + + entries.Sort(URLEntry::Comparator()); + + OutputBuffer buf; + for (auto entry : entries) { + entry->Code(buf); + } + + uint8_t headerSize[4]; + LittleEndian::writeUint32(headerSize, buf.cursor()); + + uint8_t crc[4]; + LittleEndian::writeUint32(crc, ComputeCrc32c(~0, buf.Get(), buf.cursor())); + + MOZ_TRY(Write(fd, URL_MAGIC, sizeof(URL_MAGIC))); + MOZ_TRY(Write(fd, headerSize, sizeof(headerSize))); + MOZ_TRY(Write(fd, crc, sizeof(crc))); + MOZ_TRY(Write(fd, buf.Get(), buf.cursor())); + } + + MOZ_TRY(cacheFile->MoveTo(nullptr, u"urlCache.bin"_ns)); + + NS_DispatchToMainThread( + NewRunnableMethod("URLPreloader::Cleanup", this, &URLPreloader::Cleanup)); + + return Ok(); +} + +void URLPreloader::Cleanup() { mCachedURLs.Clear(); } + +Result<Ok, nsresult> URLPreloader::ReadCache( + LinkedList<URLEntry>& pendingURLs) { + LOG(Debug, "Reading cache..."); + + nsCOMPtr<nsIFile> cacheFile; + MOZ_TRY_VAR(cacheFile, FindCacheFile()); + + AutoMemMap cache; + MOZ_TRY(cache.init(cacheFile)); + + auto size = cache.size(); + + uint32_t headerSize; + uint32_t crc; + if (size < sizeof(URL_MAGIC) + sizeof(headerSize) + sizeof(crc)) { + return Err(NS_ERROR_UNEXPECTED); + } + + auto data = cache.get<uint8_t>(); + auto end = data + size; + + if (memcmp(URL_MAGIC, data.get(), sizeof(URL_MAGIC))) { + return Err(NS_ERROR_UNEXPECTED); + } + data += sizeof(URL_MAGIC); + + headerSize = LittleEndian::readUint32(data.get()); + data += sizeof(headerSize); + + crc = LittleEndian::readUint32(data.get()); + data += sizeof(crc); + + if (data + headerSize > end) { + return Err(NS_ERROR_UNEXPECTED); + } + + if (crc != ComputeCrc32c(~0, data.get(), headerSize)) { + return Err(NS_ERROR_UNEXPECTED); + } + + { + mMonitor.AssertCurrentThreadOwns(); + + auto cleanup = MakeScopeExit([&]() { + while (auto* elem = pendingURLs.getFirst()) { + elem->remove(); + } + mCachedURLs.Clear(); + }); + + Range<uint8_t> header(data, data + headerSize); + data += headerSize; + + InputBuffer buf(header); + while (!buf.finished()) { + CacheKey key(buf); + + LOG(Debug, "Cached file: %s %s", key.TypeString(), key.mPath.get()); + + // Don't bother doing anything else if the key didn't load correctly. + // We're going to throw it out right away, and it is possible that this + // leads to pendingURLs getting into a weird state. + if (buf.error()) { + return Err(NS_ERROR_UNEXPECTED); + } + + auto entry = mCachedURLs.GetOrInsertNew(key, key); + entry->mResultCode = NS_ERROR_NOT_INITIALIZED; + + if (entry->isInList()) { +#ifdef NIGHTLY_BUILD + MOZ_DIAGNOSTIC_ASSERT(pendingURLs.contains(entry), + "Entry should be in pendingURLs"); + MOZ_DIAGNOSTIC_ASSERT(key.mPath.Length() > 0, + "Path should be non-empty"); + MOZ_DIAGNOSTIC_ASSERT(false, "Entry should be new and not in any list"); +#endif + return Err(NS_ERROR_UNEXPECTED); + } + + pendingURLs.insertBack(entry); + } + + MOZ_RELEASE_ASSERT(!buf.error(), + "We should have already bailed on an error"); + + cleanup.release(); + } + + return Ok(); +} + +void URLPreloader::BackgroundReadFiles() { + auto cleanup = MakeScopeExit([&]() { + auto lock = mReaderThread.Lock(); + auto& readerThread = lock.ref(); + NS_DispatchToMainThread(NewRunnableMethod( + "nsIThread::AsyncShutdown", readerThread, &nsIThread::AsyncShutdown)); + + readerThread = nullptr; + }); + + Vector<nsZipCursor> cursors; + LinkedList<URLEntry> pendingURLs; + { + MonitorAutoLock mal(mMonitor); + + if (ReadCache(pendingURLs).isErr()) { + mReaderInitialized = true; + mal.NotifyAll(); + return; + } + + int numZipEntries = 0; + for (auto entry : pendingURLs) { + if (entry->mType != entry->TypeFile) { + numZipEntries++; + } + } + MOZ_RELEASE_ASSERT(cursors.reserve(numZipEntries)); + + // Initialize the zip cursors for all files in Omnijar while the monitor + // is locked. Omnijar is not threadsafe, so the caller of + // AutoBeginReading guard must ensure that no code accesses Omnijar + // until this segment is done. Once the cursors have been initialized, + // the actual reading and decompression can safely be done off-thread, + // as is the case for thread-retargeted jar: channels. + for (auto entry : pendingURLs) { + if (entry->mType == entry->TypeFile) { + continue; + } + + RefPtr<nsZipArchive> zip = entry->Archive(); + if (!zip) { + MOZ_CRASH_UNSAFE_PRINTF( + "Failed to get Omnijar %s archive for entry (path: \"%s\")", + entry->TypeString(), entry->mPath.get()); + } + + auto item = zip->GetItem(entry->mPath.get()); + if (!item) { + entry->mResultCode = NS_ERROR_FILE_NOT_FOUND; + continue; + } + + size_t size = item->RealSize(); + + entry->mData.SetLength(size); + auto data = entry->mData.BeginWriting(); + + cursors.infallibleEmplaceBack(item, zip, reinterpret_cast<uint8_t*>(data), + size, true); + } + + mReaderInitialized = true; + mal.NotifyAll(); + } + + // Loop over the entries, read the file's contents, store them in the + // entry's mData pointer, and notify any waiting threads to check for + // completion. + uint32_t i = 0; + for (auto entry : pendingURLs) { + // If there is any other error code, the entry has already failed at + // this point, so don't bother trying to read it again. + if (entry->mResultCode != NS_ERROR_NOT_INITIALIZED) { + continue; + } + + nsresult rv = NS_OK; + + LOG(Debug, "Background reading %s file %s", entry->TypeString(), + entry->mPath.get()); + + if (entry->mType == entry->TypeFile) { + auto result = entry->Read(); + if (result.isErr()) { + rv = result.unwrapErr(); + } + } else { + auto& cursor = cursors[i++]; + + uint32_t len; + cursor.Copy(&len); + if (len != entry->mData.Length()) { + entry->mData.Truncate(); + rv = NS_ERROR_FAILURE; + } + } + + entry->mResultCode = rv; + mMonitor.NotifyAll(); + } + + // We're done reading pending entries, so clear the list. + pendingURLs.clear(); +} + +void URLPreloader::BeginBackgroundRead() { + auto lock = mReaderThread.Lock(); + auto& readerThread = lock.ref(); + if (!readerThread && !mReaderInitialized && sInitialized) { + nsresult rv; + rv = NS_NewNamedThread("BGReadURLs", getter_AddRefs(readerThread)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsCOMPtr<nsIRunnable> runnable = + NewRunnableMethod("URLPreloader::BackgroundReadFiles", this, + &URLPreloader::BackgroundReadFiles); + rv = readerThread->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + // If we can't launch the task, just destroy the thread + readerThread = nullptr; + return; + } + } +} + +Result<nsCString, nsresult> URLPreloader::ReadInternal(const CacheKey& key, + ReadType readType) { + if (mStartupFinished || !mReaderInitialized) { + URLEntry entry(key); + + return entry.Read(); + } + + auto entry = mCachedURLs.GetOrInsertNew(key, key); + + entry->UpdateUsedTime(); + + return entry->ReadOrWait(readType); +} + +Result<nsCString, nsresult> URLPreloader::ReadURIInternal(nsIURI* uri, + ReadType readType) { + CacheKey key; + MOZ_TRY_VAR(key, ResolveURI(uri)); + + return ReadInternal(key, readType); +} + +/* static */ Result<nsCString, nsresult> URLPreloader::Read(const CacheKey& key, + ReadType readType) { + // If we're being called before the preloader has been initialized (i.e., + // before the profile has been initialized), just fall back to a synchronous + // read. This happens when we're reading .ini and preference files that are + // needed to locate and initialize the profile. + if (!sInitialized) { + return URLEntry(key).Read(); + } + + return GetSingleton().ReadInternal(key, readType); +} + +/* static */ Result<nsCString, nsresult> URLPreloader::ReadURI( + nsIURI* uri, ReadType readType) { + if (!sInitialized) { + return Err(NS_ERROR_NOT_INITIALIZED); + } + + return GetSingleton().ReadURIInternal(uri, readType); +} + +/* static */ Result<nsCString, nsresult> URLPreloader::ReadFile( + nsIFile* file, ReadType readType) { + return Read(CacheKey(file), readType); +} + +/* static */ Result<nsCString, nsresult> URLPreloader::Read( + FileLocation& location, ReadType readType) { + if (location.IsZip()) { + if (location.GetBaseZip()) { + nsCString path; + location.GetPath(path); + return ReadZip(location.GetBaseZip(), path); + } + return URLEntry::ReadLocation(location); + } + + nsCOMPtr<nsIFile> file = location.GetBaseFile(); + return ReadFile(file, readType); +} + +/* static */ Result<nsCString, nsresult> URLPreloader::ReadZip( + nsZipArchive* zip, const nsACString& path, ReadType readType) { + // If the zip archive belongs to an Omnijar location, map it to a cache + // entry, and cache it as normal. Otherwise, simply read the entry + // synchronously, since other JAR archives are currently unsupported by the + // cache. + RefPtr<nsZipArchive> reader = Omnijar::GetReader(Omnijar::GRE); + if (zip == reader) { + CacheKey key(CacheKey::TypeGREJar, path); + return Read(key, readType); + } + + reader = Omnijar::GetReader(Omnijar::APP); + if (zip == reader) { + CacheKey key(CacheKey::TypeAppJar, path); + return Read(key, readType); + } + + // Not an Omnijar archive, so just read it directly. + FileLocation location(zip, PromiseFlatCString(path).BeginReading()); + return URLEntry::ReadLocation(location); +} + +Result<URLPreloader::CacheKey, nsresult> URLPreloader::ResolveURI(nsIURI* uri) { + nsCString spec; + nsCString scheme; + MOZ_TRY(uri->GetSpec(spec)); + MOZ_TRY(uri->GetScheme(scheme)); + + nsCOMPtr<nsIURI> resolved; + + // If the URI is a resource: or chrome: URI, first resolve it to the + // underlying URI that it wraps. + if (scheme.EqualsLiteral("resource")) { + MOZ_TRY(mResProto->ResolveURI(uri, spec)); + MOZ_TRY(NS_NewURI(getter_AddRefs(resolved), spec)); + } else if (scheme.EqualsLiteral("chrome")) { + MOZ_TRY(mChromeReg->ConvertChromeURL(uri, getter_AddRefs(resolved))); + MOZ_TRY(resolved->GetSpec(spec)); + } else { + resolved = uri; + } + MOZ_TRY(resolved->GetScheme(scheme)); + + // Try the GRE and App Omnijar prefixes. + if (mGREPrefix.Length() && StartsWith(spec, mGREPrefix)) { + return CacheKey(CacheKey::TypeGREJar, Substring(spec, mGREPrefix.Length())); + } + + if (mAppPrefix.Length() && StartsWith(spec, mAppPrefix)) { + return CacheKey(CacheKey::TypeAppJar, Substring(spec, mAppPrefix.Length())); + } + + // Try for a file URI. + if (scheme.EqualsLiteral("file")) { + nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(resolved); + MOZ_ASSERT(fileURL); + + nsCOMPtr<nsIFile> file; + MOZ_TRY(fileURL->GetFile(getter_AddRefs(file))); + + nsString path; + MOZ_TRY(file->GetPath(path)); + + return CacheKey(CacheKey::TypeFile, NS_ConvertUTF16toUTF8(path)); + } + + // Not a file or Omnijar URI, so currently unsupported. + return Err(NS_ERROR_INVALID_ARG); +} + +size_t URLPreloader::ShallowSizeOfIncludingThis( + mozilla::MallocSizeOf mallocSizeOf) { + return (mallocSizeOf(this) + + mAppPrefix.SizeOfExcludingThisEvenIfShared(mallocSizeOf) + + mGREPrefix.SizeOfExcludingThisEvenIfShared(mallocSizeOf) + + mCachedURLs.ShallowSizeOfExcludingThis(mallocSizeOf)); +} + +Result<FileLocation, nsresult> URLPreloader::CacheKey::ToFileLocation() { + if (mType == TypeFile) { + nsCOMPtr<nsIFile> file; + MOZ_TRY(NS_NewLocalFile(NS_ConvertUTF8toUTF16(mPath), false, + getter_AddRefs(file))); + return FileLocation(file); + } + + RefPtr<nsZipArchive> zip = Archive(); + return FileLocation(zip, mPath.get()); +} + +Result<nsCString, nsresult> URLPreloader::URLEntry::Read() { + FileLocation location; + MOZ_TRY_VAR(location, ToFileLocation()); + + MOZ_TRY_VAR(mData, ReadLocation(location)); + return mData; +} + +/* static */ Result<nsCString, nsresult> URLPreloader::URLEntry::ReadLocation( + FileLocation& location) { + FileLocation::Data data; + MOZ_TRY(location.GetData(data)); + + uint32_t size; + MOZ_TRY(data.GetSize(&size)); + + nsCString result; + result.SetLength(size); + MOZ_TRY(data.Copy(result.BeginWriting(), size)); + + return std::move(result); +} + +Result<nsCString, nsresult> URLPreloader::URLEntry::ReadOrWait( + ReadType readType) { + auto now = TimeStamp::Now(); + LOG(Info, "Reading %s\n", mPath.get()); + auto cleanup = MakeScopeExit([&]() { + LOG(Info, "Read in %fms\n", (TimeStamp::Now() - now).ToMilliseconds()); + }); + + if (mResultCode == NS_ERROR_NOT_INITIALIZED) { + MonitorAutoLock mal(GetSingleton().mMonitor); + + while (mResultCode == NS_ERROR_NOT_INITIALIZED) { + mal.Wait(); + } + } + + if (mResultCode == NS_OK && mData.IsVoid()) { + LOG(Info, "Reading synchronously...\n"); + return Read(); + } + + if (NS_FAILED(mResultCode)) { + return Err(mResultCode); + } + + nsCString res = mData; + + if (readType == Forget) { + mData.SetIsVoid(true); + } + return res; +} + +inline URLPreloader::CacheKey::CacheKey(InputBuffer& buffer) { + Code(buffer); + MOZ_DIAGNOSTIC_ASSERT( + mType == TypeAppJar || mType == TypeGREJar || mType == TypeFile, + "mType should be valid"); +} + +NS_IMPL_ISUPPORTS(URLPreloader, nsIMemoryReporter) + +#undef LOG + +} // namespace mozilla diff --git a/js/xpconnect/loader/URLPreloader.h b/js/xpconnect/loader/URLPreloader.h new file mode 100644 index 0000000000..2573fc89a2 --- /dev/null +++ b/js/xpconnect/loader/URLPreloader.h @@ -0,0 +1,318 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#ifndef URLPreloader_h +#define URLPreloader_h + +#include "mozilla/DataMutex.h" +#include "mozilla/FileLocation.h" +#include "mozilla/HashFunctions.h" +#include "mozilla/LinkedList.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/Monitor.h" +#include "mozilla/Omnijar.h" +#include "mozilla/Range.h" +#include "mozilla/Vector.h" +#include "mozilla/Result.h" +#include "nsClassHashtable.h" +#include "nsHashKeys.h" +#include "nsIChromeRegistry.h" +#include "nsIFile.h" +#include "nsIURI.h" +#include "nsIMemoryReporter.h" +#include "nsIResProtocolHandler.h" +#include "nsIThread.h" +#include "nsReadableUtils.h" + +class nsZipArchive; + +namespace mozilla { +namespace loader { +class InputBuffer; +} + +using namespace mozilla::loader; + +class ScriptPreloader; + +/** + * A singleton class to manage loading local URLs during startup, recording + * them, and pre-loading them during early startup in the next session. URLs + * that are not already loaded (or already being pre-loaded) when required are + * read synchronously from disk, and (if startup is not already complete) + * added to the pre-load list for the next session. + */ +class URLPreloader final : public nsIMemoryReporter { + MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf) + + URLPreloader() = default; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIMEMORYREPORTER + + static URLPreloader& GetSingleton(); + + // The type of read operation to perform. + enum ReadType { + // Read the file and then immediately forget its data. + Forget, + // Read the file and retain its data for the next caller. + Retain, + }; + + // Helpers to read the contents of files or JAR archive entries with various + // representations. If the preloader has not yet been initialized, or the + // given location is not supported by the cache, the entries will be read + // synchronously, and not stored in the cache. + static Result<nsCString, nsresult> Read(FileLocation& location, + ReadType readType = Forget); + + static Result<nsCString, nsresult> ReadURI(nsIURI* uri, + ReadType readType = Forget); + + static Result<nsCString, nsresult> ReadFile(nsIFile* file, + ReadType readType = Forget); + + static Result<nsCString, nsresult> ReadZip(nsZipArchive* archive, + const nsACString& path, + ReadType readType = Forget); + + void SetStartupFinished() { mStartupFinished = true; } + + private: + struct CacheKey; + + Result<nsCString, nsresult> ReadInternal(const CacheKey& key, + ReadType readType); + + Result<nsCString, nsresult> ReadURIInternal(nsIURI* uri, ReadType readType); + + Result<nsCString, nsresult> ReadFileInternal(nsIFile* file, + ReadType readType); + + static Result<nsCString, nsresult> Read(const CacheKey& key, + ReadType readType); + + static bool sInitialized; + + static mozilla::StaticRefPtr<URLPreloader> sSingleton; + + protected: + friend class AddonManagerStartup; + friend class ScriptPreloader; + + virtual ~URLPreloader(); + + Result<Ok, nsresult> WriteCache(); + + static URLPreloader& ReInitialize(); + + // Clear leftover entries after the cache has been written. + void Cleanup(); + + // Begins reading files off-thread, and ensures that initialization has + // completed before leaving the current scope. The caller *must* ensure that + // no code on the main thread access Omnijar, either directly or indirectly, + // for the lifetime of this guard object. + struct MOZ_RAII AutoBeginReading final { + AutoBeginReading() { GetSingleton().BeginBackgroundRead(); } + + ~AutoBeginReading() { + auto& reader = GetSingleton(); + + MonitorAutoLock mal(reader.mMonitor); + + while (!reader.mReaderInitialized && URLPreloader::sInitialized) { + mal.Wait(); + } + } + }; + + private: + // Represents a key for an entry in the URI cache, based on its file or JAR + // location. + struct CacheKey { + // The type of the entry. TypeAppJar and TypeGREJar entries are in the + // app-specific or toolkit Omnijar files, and are handled specially. + // TypeFile entries are plain files in the filesystem. + enum EntryType : uint8_t { + TypeAppJar, + TypeGREJar, + TypeFile, + }; + + CacheKey() = default; + CacheKey(const CacheKey& other) = default; + + CacheKey(EntryType type, const nsACString& path) + : mType(type), mPath(path) {} + + explicit CacheKey(nsIFile* file) : mType(TypeFile) { + nsString path; + MOZ_ALWAYS_SUCCEEDS(file->GetPath(path)); + MOZ_DIAGNOSTIC_ASSERT(path.Length() > 0); + CopyUTF16toUTF8(path, mPath); + } + + explicit inline CacheKey(InputBuffer& buffer); + + // Encodes or decodes the cache key for storage in a session cache file. + template <typename Buffer> + void Code(Buffer& buffer) { + buffer.codeUint8(*reinterpret_cast<uint8_t*>(&mType)); + buffer.codeString(mPath); + MOZ_DIAGNOSTIC_ASSERT(mPath.Length() > 0); + } + + uint32_t Hash() const { return HashGeneric(mType, HashString(mPath)); } + + bool operator==(const CacheKey& other) const { + return mType == other.mType && mPath == other.mPath; + } + + // Returns the Omnijar type for this entry. This may *only* be called + // for Omnijar entries. + Omnijar::Type OmnijarType() { + switch (mType) { + case TypeAppJar: + return Omnijar::APP; + case TypeGREJar: + return Omnijar::GRE; + default: + MOZ_CRASH("Unexpected entry type"); + return Omnijar::GRE; + } + } + + const char* TypeString() const { + switch (mType) { + case TypeAppJar: + return "AppJar"; + case TypeGREJar: + return "GREJar"; + case TypeFile: + return "File"; + } + MOZ_ASSERT_UNREACHABLE("no such type"); + return ""; + } + + already_AddRefed<nsZipArchive> Archive() { + return Omnijar::GetReader(OmnijarType()); + } + + Result<FileLocation, nsresult> ToFileLocation(); + + EntryType mType = TypeFile; + + // The path of the entry. For Type*Jar entries, this is the path within + // the Omnijar archive. For TypeFile entries, this is the full path to + // the file. + nsCString mPath{}; + }; + + // Represents an entry in the URI cache. + struct URLEntry final : public CacheKey, public LinkedListElement<URLEntry> { + MOZ_IMPLICIT URLEntry(const CacheKey& key) + : CacheKey(key), mData(VoidCString()) {} + + explicit URLEntry(nsIFile* file) : CacheKey(file) {} + + // For use with nsTArray::Sort. + // + // Sorts entries by the time they were initially read during this + // session. + struct Comparator final { + bool Equals(const URLEntry* a, const URLEntry* b) const { + return a->mReadTime == b->mReadTime; + } + + bool LessThan(const URLEntry* a, const URLEntry* b) const { + return a->mReadTime < b->mReadTime; + } + }; + + // Sets the first-used time of this file to the earlier of its current + // first-use time or the given timestamp. + void UpdateUsedTime(const TimeStamp& time = TimeStamp::Now()) { + if (!mReadTime || time < mReadTime) { + mReadTime = time; + } + } + + Result<nsCString, nsresult> Read(); + static Result<nsCString, nsresult> ReadLocation(FileLocation& location); + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const { + return (mallocSizeOf(this) + + mPath.SizeOfExcludingThisEvenIfShared(mallocSizeOf) + + mData.SizeOfExcludingThisEvenIfShared(mallocSizeOf)); + } + + // Reads the contents of the file referenced by this entry, or wait for + // an off-thread read operation to finish if it is currently pending, + // and return the file's contents. + Result<nsCString, nsresult> ReadOrWait(ReadType readType); + + nsCString mData; + + TimeStamp mReadTime{}; + + nsresult mResultCode = NS_OK; + }; + + // Resolves the given URI to a CacheKey, if the URI is cacheable. + Result<CacheKey, nsresult> ResolveURI(nsIURI* uri); + + static already_AddRefed<URLPreloader> Create(bool* aInitialized); + + Result<Ok, nsresult> InitInternal(); + + // Returns a file pointer to the (possibly nonexistent) cache file with the + // given suffix. + Result<nsCOMPtr<nsIFile>, nsresult> GetCacheFile(const nsAString& suffix); + // Finds the correct cache file to use for this session. + Result<nsCOMPtr<nsIFile>, nsresult> FindCacheFile(); + + Result<Ok, nsresult> ReadCache(LinkedList<URLEntry>& pendingURLs); + + void BackgroundReadFiles(); + void BeginBackgroundRead(); + + using HashType = nsClassHashtable<nsGenericHashKey<CacheKey>, URLEntry>; + + size_t ShallowSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf); + + bool mStartupFinished = false; + bool mReaderInitialized = false; + + // Only to be accessed from the cache write thread. + bool mCacheWritten = false; + + // The prefix URLs for files in the GRE and App omni jar archives. + nsCString mGREPrefix; + nsCString mAppPrefix; + + nsCOMPtr<nsIResProtocolHandler> mResProto; + nsCOMPtr<nsIChromeRegistry> mChromeReg; + nsCOMPtr<nsIFile> mProfD; + + // Note: We use a RefPtr rather than an nsCOMPtr here because the + // AssertNoQueryNeeded checks done by getter_AddRefs happen at a time that + // violate data access invariants. It's wrapped in a mutex because + // the reader thread needs to be able to null this out to terminate itself. + DataMutex<RefPtr<nsIThread>> mReaderThread{"ReaderThread"}; + + // A map of URL entries which have were either read this session, or read + // from the last session's cache file. + HashType mCachedURLs; + + Monitor mMonitor MOZ_UNANNOTATED{"[URLPreloader::mMutex]"}; +}; + +} // namespace mozilla + +#endif // URLPreloader_h diff --git a/js/xpconnect/loader/XPCOMUtils.sys.mjs b/js/xpconnect/loader/XPCOMUtils.sys.mjs new file mode 100644 index 0000000000..403b17e2be --- /dev/null +++ b/js/xpconnect/loader/XPCOMUtils.sys.mjs @@ -0,0 +1,580 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 sts=2 et filetype=javascript + * 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/. */ + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + +let global = Cu.getGlobalForObject({}); + +// Some global imports expose additional symbols; for example, +// `Cu.importGlobalProperties(["MessageChannel"])` imports `MessageChannel` +// and `MessagePort`. This table maps those extra symbols to the main +// import name. +const EXTRA_GLOBAL_NAME_TO_IMPORT_NAME = { + MessagePort: "MessageChannel", +}; + +/** + * Redefines the given property on the given object with the given + * value. This can be used to redefine getter properties which do not + * implement setters. + */ +function redefine(object, prop, value) { + Object.defineProperty(object, prop, { + configurable: true, + enumerable: true, + value, + writable: true, + }); + return value; +} + +export var XPCOMUtils = { + /** + * Defines a getter on a specified object that will be created upon first use. + * + * @param aObject + * The object to define the lazy getter on. + * @param aName + * The name of the getter to define on aObject. + * @param aLambda + * A function that returns what the getter should return. This will + * only ever be called once. + */ + defineLazyGetter(aObject, aName, aLambda) { + ChromeUtils.defineLazyGetter(aObject, aName, aLambda); + }, + + /** + * Defines a getter on a specified object for a script. The script will not + * be loaded until first use. + * + * @param aObject + * The object to define the lazy getter on. + * @param aNames + * The name of the getter to define on aObject for the script. + * This can be a string if the script exports only one symbol, + * or an array of strings if the script can be first accessed + * from several different symbols. + * @param aResource + * The URL used to obtain the script. + */ + defineLazyScriptGetter(aObject, aNames, aResource) { + if (!Array.isArray(aNames)) { + aNames = [aNames]; + } + for (let name of aNames) { + Object.defineProperty(aObject, name, { + get() { + XPCOMUtils._scriptloader.loadSubScript(aResource, aObject); + return aObject[name]; + }, + set(value) { + redefine(aObject, name, value); + }, + configurable: true, + enumerable: true, + }); + } + }, + + /** + * Overrides the scriptloader definition for tests to help with globals + * tracking. Should only be used for tests. + * + * @param {object} aObject + * The alternative script loader object to use. + */ + overrideScriptLoaderForTests(aObject) { + Cu.crashIfNotInAutomation(); + delete this._scriptloader; + this._scriptloader = aObject; + }, + + /** + * Defines a getter property on the given object for each of the given + * global names as accepted by Cu.importGlobalProperties. These + * properties are imported into the shared JSM module global, and then + * copied onto the given object, no matter which global the object + * belongs to. + * + * @param {object} aObject + * The object on which to define the properties. + * @param {string[]} aNames + * The list of global properties to define. + */ + defineLazyGlobalGetters(aObject, aNames) { + for (let name of aNames) { + this.defineLazyGetter(aObject, name, () => { + if (!(name in global)) { + let importName = EXTRA_GLOBAL_NAME_TO_IMPORT_NAME[name] || name; + // eslint-disable-next-line mozilla/reject-importGlobalProperties, no-unused-vars + Cu.importGlobalProperties([importName]); + } + return global[name]; + }); + } + }, + + /** + * Defines a getter on a specified object for a service. The service will not + * be obtained until first use. + * + * @param aObject + * The object to define the lazy getter on. + * @param aName + * The name of the getter to define on aObject for the service. + * @param aContract + * The contract used to obtain the service. + * @param aInterfaceName + * The name of the interface to query the service to. + */ + defineLazyServiceGetter(aObject, aName, aContract, aInterfaceName) { + this.defineLazyGetter(aObject, aName, () => { + if (aInterfaceName) { + return Cc[aContract].getService(Ci[aInterfaceName]); + } + return Cc[aContract].getService().wrappedJSObject; + }); + }, + + /** + * Defines a lazy service getter on a specified object for each + * property in the given object. + * + * @param aObject + * The object to define the lazy getter on. + * @param aServices + * An object with a property for each service to be + * imported, where the property name is the name of the + * symbol to define, and the value is a 1 or 2 element array + * containing the contract ID and, optionally, the interface + * name of the service, as passed to defineLazyServiceGetter. + */ + defineLazyServiceGetters(aObject, aServices) { + for (let [name, service] of Object.entries(aServices)) { + // Note: This is hot code, and cross-compartment array wrappers + // are not JIT-friendly to destructuring or spread operators, so + // we need to use indexed access instead. + this.defineLazyServiceGetter( + aObject, + name, + service[0], + service[1] || null + ); + } + }, + + /** + * Defines a getter on a specified object for a module. The module will not + * be imported until first use. The getter allows to execute setup and + * teardown code (e.g. to register/unregister to services) and accepts + * a proxy object which acts on behalf of the module until it is imported. + * + * @param aObject + * The object to define the lazy getter on. + * @param aName + * The name of the getter to define on aObject for the module. + * @param aResource + * The URL used to obtain the module. + * @param aSymbol + * The name of the symbol exported by the module. + * This parameter is optional and defaults to aName. + * @param aPreLambda + * A function that is executed when the proxy is set up. + * This will only ever be called once. + * @param aPostLambda + * A function that is executed when the module has been imported to + * run optional teardown procedures on the proxy object. + * This will only ever be called once. + * @param aProxy + * An object which acts on behalf of the module to be imported until + * the module has been imported. + */ + defineLazyModuleGetter( + aObject, + aName, + aResource, + aSymbol, + aPreLambda, + aPostLambda, + aProxy + ) { + if (arguments.length == 3) { + ChromeUtils.defineModuleGetter(aObject, aName, aResource); + return; + } + + let proxy = aProxy || {}; + + if (typeof aPreLambda === "function") { + aPreLambda.apply(proxy); + } + + this.defineLazyGetter(aObject, aName, () => { + var temp = {}; + try { + temp = ChromeUtils.import(aResource); + + if (typeof aPostLambda === "function") { + aPostLambda.apply(proxy); + } + } catch (ex) { + console.error("Failed to load module " + aResource + "."); + throw ex; + } + return temp[aSymbol || aName]; + }); + }, + + /** + * Defines a lazy module getter on a specified object for each + * property in the given object. + * + * @param aObject + * The object to define the lazy getter on. + * @param aModules + * An object with a property for each module property to be + * imported, where the property name is the name of the + * imported symbol and the value is the module URI. + */ + defineLazyModuleGetters(aObject, aModules) { + for (let [name, module] of Object.entries(aModules)) { + ChromeUtils.defineModuleGetter(aObject, name, module); + } + }, + + /** + * Defines a getter on a specified object for preference value. The + * preference is read the first time that the property is accessed, + * and is thereafter kept up-to-date using a preference observer. + * + * @param aObject + * The object to define the lazy getter on. + * @param aName + * The name of the getter property to define on aObject. + * @param aPreference + * The name of the preference to read. + * @param aDefaultPrefValue + * The default value to use, if the preference is not defined. + * This is the default value of the pref, before applying aTransform. + * @param aOnUpdate + * A function to call upon update. Receives as arguments + * `(aPreference, previousValue, newValue)` + * @param aTransform + * An optional function to transform the value. If provided, + * this function receives the new preference value as an argument + * and its return value is used by the getter. + */ + defineLazyPreferenceGetter( + aObject, + aName, + aPreference, + aDefaultPrefValue = null, + aOnUpdate = null, + aTransform = val => val + ) { + if (AppConstants.DEBUG && aDefaultPrefValue !== null) { + let prefType = Services.prefs.getPrefType(aPreference); + if (prefType != Ci.nsIPrefBranch.PREF_INVALID) { + // The pref may get defined after the lazy getter is called + // at which point the code here won't know the expected type. + let prefTypeForDefaultValue = { + boolean: Ci.nsIPrefBranch.PREF_BOOL, + number: Ci.nsIPrefBranch.PREF_INT, + string: Ci.nsIPrefBranch.PREF_STRING, + }[typeof aDefaultPrefValue]; + if (prefTypeForDefaultValue != prefType) { + throw new Error( + `Default value does not match preference type (Got ${prefTypeForDefaultValue}, expected ${prefType}) for ${aPreference}` + ); + } + } + } + + // Note: We need to keep a reference to this observer alive as long + // as aObject is alive. This means that all of our getters need to + // explicitly close over the variable that holds the object, and we + // cannot define a value in place of a getter after we read the + // preference. + let observer = { + QueryInterface: XPCU_lazyPreferenceObserverQI, + + value: undefined, + + observe(subject, topic, data) { + if (data == aPreference) { + if (aOnUpdate) { + let previous = this.value; + + // Fetch and cache value. + this.value = undefined; + let latest = lazyGetter(); + aOnUpdate(data, previous, latest); + } else { + // Empty cache, next call to the getter will cause refetch. + this.value = undefined; + } + } + }, + }; + + let defineGetter = get => { + Object.defineProperty(aObject, aName, { + configurable: true, + enumerable: true, + get, + }); + }; + + function lazyGetter() { + if (observer.value === undefined) { + let prefValue; + switch (Services.prefs.getPrefType(aPreference)) { + case Ci.nsIPrefBranch.PREF_STRING: + prefValue = Services.prefs.getStringPref(aPreference); + break; + + case Ci.nsIPrefBranch.PREF_INT: + prefValue = Services.prefs.getIntPref(aPreference); + break; + + case Ci.nsIPrefBranch.PREF_BOOL: + prefValue = Services.prefs.getBoolPref(aPreference); + break; + + case Ci.nsIPrefBranch.PREF_INVALID: + prefValue = aDefaultPrefValue; + break; + + default: + // This should never happen. + throw new Error( + `Error getting pref ${aPreference}; its value's type is ` + + `${Services.prefs.getPrefType(aPreference)}, which I don't ` + + `know how to handle.` + ); + } + + observer.value = aTransform(prefValue); + } + return observer.value; + } + + defineGetter(() => { + Services.prefs.addObserver(aPreference, observer, true); + + defineGetter(lazyGetter); + return lazyGetter(); + }); + }, + + /** + * Defines a non-writable property on an object. + */ + defineConstant(aObj, aName, aValue) { + Object.defineProperty(aObj, aName, { + value: aValue, + enumerable: true, + writable: false, + }); + }, + + /** + * Defines a proxy which acts as a lazy object getter that can be passed + * around as a reference, and will only be evaluated when something in + * that object gets accessed. + * + * The evaluation can be triggered by a function call, by getting or + * setting a property, calling this as a constructor, or enumerating + * the properties of this object (e.g. during an iteration). + * + * Please note that, even after evaluated, the object given to you + * remains being the proxy object (which forwards everything to the + * real object). This is important to correctly use these objects + * in pairs of add+remove listeners, for example. + * If your use case requires access to the direct object, you can + * get it through the untrap callback. + * + * @param aObject + * The object to define the lazy getter on. + * + * You can pass null to aObject if you just want to get this + * proxy through the return value. + * + * @param aName + * The name of the getter to define on aObject. + * + * @param aInitFuncOrResource + * A function or a module that defines what this object actually + * should be when it gets evaluated. This will only ever be called once. + * + * Short-hand: If you pass a string to this parameter, it will be treated + * as the URI of a module to be imported, and aName will be used as + * the symbol to retrieve from the module. + * + * @param aStubProperties + * In this parameter, you can provide an object which contains + * properties from the original object that, when accessed, will still + * prevent the entire object from being evaluated. + * + * These can be copies or simplified versions of the original properties. + * + * One example is to provide an alternative QueryInterface implementation + * to avoid the entire object from being evaluated when it's added as an + * observer (as addObserver calls object.QueryInterface(Ci.nsIObserver)). + * + * Once the object has been evaluated, the properties from the real + * object will be used instead of the ones provided here. + * + * @param aUntrapCallback + * A function that gets called once when the object has just been evaluated. + * You can use this to do some work (e.g. setting properties) that you need + * to do on this object but that can wait until it gets evaluated. + * + * Another use case for this is to use during code development to log when + * this object gets evaluated, to make sure you're not accidentally triggering + * it earlier than expected. + */ + defineLazyProxy( + aObject, + aName, + aInitFuncOrResource, + aStubProperties, + aUntrapCallback + ) { + let initFunc = aInitFuncOrResource; + + if (typeof aInitFuncOrResource == "string") { + initFunc = () => ChromeUtils.import(aInitFuncOrResource)[aName]; + } + + let handler = new LazyProxyHandler( + aName, + initFunc, + aStubProperties, + aUntrapCallback + ); + + /* + * We cannot simply create a lazy getter for the underlying + * object and pass it as the target of the proxy, because + * just passing it in `new Proxy` means it would get + * evaluated. Becase of this, a full handler needs to be + * implemented (the LazyProxyHandler). + * + * So, an empty object is used as the target, and the handler + * replaces it on every call with the real object. + */ + let proxy = new Proxy({}, handler); + + if (aObject) { + Object.defineProperty(aObject, aName, { + value: proxy, + enumerable: true, + writable: true, + }); + } + + return proxy; + }, +}; + +XPCOMUtils.defineLazyGetter(XPCOMUtils, "_scriptloader", () => { + return Services.scriptloader; +}); + +/** + * LazyProxyHandler + * This class implements the handler used + * in the proxy from defineLazyProxy. + * + * This handler forwards all calls to an underlying object, + * stored as `this.realObject`, which is obtained as the returned + * value from aInitFunc, which will be called on the first time + * time that it needs to be used (with an exception in the get() trap + * for the properties provided in the `aStubProperties` parameter). + */ + +class LazyProxyHandler { + constructor(aName, aInitFunc, aStubProperties, aUntrapCallback) { + this.pending = true; + this.name = aName; + this.initFuncOrResource = aInitFunc; + this.stubProperties = aStubProperties; + this.untrapCallback = aUntrapCallback; + } + + getObject() { + if (this.pending) { + this.realObject = this.initFuncOrResource.call(null); + + if (this.untrapCallback) { + this.untrapCallback.call(null, this.realObject); + this.untrapCallback = null; + } + + this.pending = false; + this.stubProperties = null; + } + return this.realObject; + } + + getPrototypeOf(target) { + return Reflect.getPrototypeOf(this.getObject()); + } + + setPrototypeOf(target, prototype) { + return Reflect.setPrototypeOf(this.getObject(), prototype); + } + + isExtensible(target) { + return Reflect.isExtensible(this.getObject()); + } + + preventExtensions(target) { + return Reflect.preventExtensions(this.getObject()); + } + + getOwnPropertyDescriptor(target, prop) { + return Reflect.getOwnPropertyDescriptor(this.getObject(), prop); + } + + defineProperty(target, prop, descriptor) { + return Reflect.defineProperty(this.getObject(), prop, descriptor); + } + + has(target, prop) { + return Reflect.has(this.getObject(), prop); + } + + get(target, prop, receiver) { + if ( + this.pending && + this.stubProperties && + Object.prototype.hasOwnProperty.call(this.stubProperties, prop) + ) { + return this.stubProperties[prop]; + } + return Reflect.get(this.getObject(), prop, receiver); + } + + set(target, prop, value, receiver) { + return Reflect.set(this.getObject(), prop, value, receiver); + } + + deleteProperty(target, prop) { + return Reflect.deleteProperty(this.getObject(), prop); + } + + ownKeys(target) { + return Reflect.ownKeys(this.getObject()); + } +} + +var XPCU_lazyPreferenceObserverQI = ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", +]); diff --git a/js/xpconnect/loader/moz.build b/js/xpconnect/loader/moz.build new file mode 100644 index 0000000000..44ec49c955 --- /dev/null +++ b/js/xpconnect/loader/moz.build @@ -0,0 +1,67 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +UNIFIED_SOURCES += [ + "AutoMemMap.cpp", + "ChromeScriptLoader.cpp", + "ComponentModuleLoader.cpp", + "JSMEnvironmentProxy.cpp", + "ModuleEnvironmentProxy.cpp", + "mozJSLoaderUtils.cpp", + "mozJSSubScriptLoader.cpp", + "nsImportModule.cpp", + "ScriptCacheActors.cpp", + "ScriptPreloader.cpp", + "URLPreloader.cpp", +] + +# mozJSModuleLoader.cpp cannot be built in unified mode because it uses +# windows.h +SOURCES += [ + "mozJSModuleLoader.cpp", +] + +IPDL_SOURCES += [ + "PScriptCache.ipdl", +] + +EXPORTS += ["nsImportModule.h"] + +EXPORTS.mozilla += [ + "AutoMemMap.h", + "IOBuffers.h", + "ScriptPreloader.h", + "URLPreloader.h", +] + +EXPORTS.mozilla.dom += [ + "PrecompiledScript.h", +] + +EXPORTS.mozilla.loader += [ + "AutoMemMap.h", + "ComponentModuleLoader.h", + "ScriptCacheActors.h", + "SkipCheckForBrokenURLOrZeroSized.h", +] + +EXTRA_JS_MODULES += [ + "ComponentUtils.sys.mjs", + "XPCOMUtils.sys.mjs", +] + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "../src", + "../wrappers", + "/dom/base", + "/js/loader", + "/xpcom/base/", + "/xpcom/io", # crc32c.h +] + +include("/ipc/chromium/chromium-config.mozbuild") diff --git a/js/xpconnect/loader/mozJSLoaderUtils.cpp b/js/xpconnect/loader/mozJSLoaderUtils.cpp new file mode 100644 index 0000000000..3f54e87d3b --- /dev/null +++ b/js/xpconnect/loader/mozJSLoaderUtils.cpp @@ -0,0 +1,75 @@ +/* -*- 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 "mozilla/scache/StartupCache.h" + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "js/CompileOptions.h" +#include "js/Transcoding.h" +#include "js/experimental/JSStencil.h" + +#include "mozilla/BasePrincipal.h" +#include "mozilla/Span.h" + +using namespace JS; +using namespace mozilla::scache; +using mozilla::UniquePtr; + +static nsresult HandleTranscodeResult(JSContext* cx, + JS::TranscodeResult result) { + if (result == JS::TranscodeResult::Ok) { + return NS_OK; + } + + if (result == JS::TranscodeResult::Throw) { + JS_ClearPendingException(cx); + return NS_ERROR_OUT_OF_MEMORY; + } + + MOZ_ASSERT(IsTranscodeFailureResult(result)); + return NS_ERROR_FAILURE; +} + +nsresult ReadCachedStencil(StartupCache* cache, nsACString& cachePath, + JSContext* cx, const JS::DecodeOptions& options, + JS::Stencil** stencilOut) { + MOZ_ASSERT(options.borrowBuffer); + MOZ_ASSERT(!options.usePinnedBytecode); + + const char* buf; + uint32_t len; + nsresult rv = + cache->GetBuffer(PromiseFlatCString(cachePath).get(), &buf, &len); + if (NS_FAILED(rv)) { + return rv; // don't warn since NOT_AVAILABLE is an ok error + } + + JS::TranscodeRange range(AsBytes(mozilla::Span(buf, len))); + JS::TranscodeResult code = JS::DecodeStencil(cx, options, range, stencilOut); + return HandleTranscodeResult(cx, code); +} + +nsresult WriteCachedStencil(StartupCache* cache, nsACString& cachePath, + JSContext* cx, JS::Stencil* stencil) { + JS::TranscodeBuffer buffer; + JS::TranscodeResult code = JS::EncodeStencil(cx, stencil, buffer); + if (code != JS::TranscodeResult::Ok) { + return HandleTranscodeResult(cx, code); + } + + size_t size = buffer.length(); + if (size > UINT32_MAX) { + return NS_ERROR_FAILURE; + } + + // Move the vector buffer into a unique pointer buffer. + mozilla::UniqueFreePtr<char[]> buf( + reinterpret_cast<char*>(buffer.extractOrCopyRawBuffer())); + nsresult rv = cache->PutBuffer(PromiseFlatCString(cachePath).get(), + std::move(buf), size); + return rv; +} diff --git a/js/xpconnect/loader/mozJSLoaderUtils.h b/js/xpconnect/loader/mozJSLoaderUtils.h new file mode 100644 index 0000000000..8c996c17be --- /dev/null +++ b/js/xpconnect/loader/mozJSLoaderUtils.h @@ -0,0 +1,30 @@ +/* -*- 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/. */ + +#ifndef mozJSLoaderUtils_h +#define mozJSLoaderUtils_h + +#include "nsString.h" + +#include "js/experimental/JSStencil.h" +#include "js/CompileOptions.h" // JS::DecodeOptions + +namespace mozilla { +namespace scache { +class StartupCache; +} // namespace scache +} // namespace mozilla + +nsresult ReadCachedStencil(mozilla::scache::StartupCache* cache, + nsACString& cachePath, JSContext* cx, + const JS::DecodeOptions& options, + JS::Stencil** stencilOut); + +nsresult WriteCachedStencil(mozilla::scache::StartupCache* cache, + nsACString& cachePath, JSContext* cx, + JS::Stencil* stencil); + +#endif /* mozJSLoaderUtils_h */ diff --git a/js/xpconnect/loader/mozJSModuleLoader.cpp b/js/xpconnect/loader/mozJSModuleLoader.cpp new file mode 100644 index 0000000000..50102f8770 --- /dev/null +++ b/js/xpconnect/loader/mozJSModuleLoader.cpp @@ -0,0 +1,1936 @@ +/* -*- 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 "mozilla/Attributes.h" +#include "mozilla/ArrayUtils.h" // mozilla::ArrayLength +#include "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include <cstdarg> + +#include "mozilla/Logging.h" +#ifdef ANDROID +# include <android/log.h> +#endif +#ifdef XP_WIN +# include <windows.h> +#endif + +#include "jsapi.h" +#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject +#include "js/CharacterEncoding.h" +#include "js/CompilationAndEvaluation.h" +#include "js/CompileOptions.h" // JS::CompileOptions +#include "js/ErrorReport.h" // JS_ReportErrorUTF8, JSErrorReport +#include "js/Exception.h" // JS_ErrorFromException +#include "js/friend/JSMEnvironment.h" // JS::ExecuteInJSMEnvironment, JS::GetJSMEnvironmentOfScriptedCaller, JS::NewJSMEnvironment +#include "js/friend/ErrorMessages.h" // JSMSG_* +#include "js/loader/ModuleLoadRequest.h" +#include "js/Object.h" // JS::GetCompartment +#include "js/Printf.h" +#include "js/PropertyAndElement.h" // JS_DefineFunctions, JS_DefineProperty, JS_Enumerate, JS_GetElement, JS_GetProperty, JS_GetPropertyById, JS_HasOwnProperty, JS_HasOwnPropertyById, JS_SetProperty, JS_SetPropertyById +#include "js/PropertySpec.h" +#include "js/SourceText.h" // JS::SourceText +#include "nsCOMPtr.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsIFile.h" +#include "mozJSModuleLoader.h" +#include "mozJSLoaderUtils.h" +#include "nsIFileURL.h" +#include "nsIJARURI.h" +#include "nsIChannel.h" +#include "nsNetUtil.h" +#include "nsJSUtils.h" +#include "xpcprivate.h" +#include "xpcpublic.h" +#include "nsContentUtils.h" +#include "nsXULAppAPI.h" +#include "WrapperFactory.h" +#include "JSMEnvironmentProxy.h" +#include "ModuleEnvironmentProxy.h" +#include "JSServices.h" + +#include "mozilla/scache/StartupCache.h" +#include "mozilla/scache/StartupCacheUtils.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/MacroForEach.h" +#include "mozilla/Preferences.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/ProfilerMarkers.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/ScriptPreloader.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/ReferrerPolicyBinding.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/Unused.h" + +using namespace mozilla; +using namespace mozilla::scache; +using namespace mozilla::loader; +using namespace xpc; +using namespace JS; + +#define JS_CACHE_PREFIX(aScopeType, aCompilationTarget) \ + "jsloader/" aScopeType "/" aCompilationTarget + +/** + * Buffer sizes for serialization and deserialization of scripts. + * FIXME: bug #411579 (tune this macro!) Last updated: Jan 2008 + */ +#define XPC_SERIALIZATION_BUFFER_SIZE (64 * 1024) +#define XPC_DESERIALIZATION_BUFFER_SIZE (12 * 8192) + +// MOZ_LOG=JSModuleLoader:5 +static LazyLogModule gJSCLLog("JSModuleLoader"); + +#define LOG(args) MOZ_LOG(gJSCLLog, mozilla::LogLevel::Debug, args) + +// Components.utils.import error messages +#define ERROR_SCOPE_OBJ "%s - Second argument must be an object." +#define ERROR_NO_TARGET_OBJECT "%s - Couldn't find target object for import." +#define ERROR_NOT_PRESENT "%s - EXPORTED_SYMBOLS is not present." +#define ERROR_NOT_AN_ARRAY "%s - EXPORTED_SYMBOLS is not an array." +#define ERROR_GETTING_ARRAY_LENGTH \ + "%s - Error getting array length of EXPORTED_SYMBOLS." +#define ERROR_ARRAY_ELEMENT "%s - EXPORTED_SYMBOLS[%d] is not a string." +#define ERROR_GETTING_SYMBOL "%s - Could not get symbol '%s'." +#define ERROR_SETTING_SYMBOL "%s - Could not set symbol '%s' on target object." +#define ERROR_UNINITIALIZED_SYMBOL \ + "%s - Symbol '%s' accessed before initialization. Cyclic import?" + +static constexpr char JSM_Suffix[] = ".jsm"; +static constexpr size_t JSM_SuffixLength = mozilla::ArrayLength(JSM_Suffix) - 1; +static constexpr char JSM_JS_Suffix[] = ".jsm.js"; +static constexpr size_t JSM_JS_SuffixLength = + mozilla::ArrayLength(JSM_JS_Suffix) - 1; +static constexpr char JS_Suffix[] = ".js"; +static constexpr size_t JS_SuffixLength = mozilla::ArrayLength(JS_Suffix) - 1; +static constexpr char MJS_Suffix[] = ".sys.mjs"; +static constexpr size_t MJS_SuffixLength = mozilla::ArrayLength(MJS_Suffix) - 1; + +static bool IsJSM(const nsACString& aLocation) { + if (aLocation.Length() < JSM_SuffixLength) { + return false; + } + const auto ext = Substring(aLocation, aLocation.Length() - JSM_SuffixLength); + return ext == JSM_Suffix; +} + +static bool IsJS(const nsACString& aLocation) { + if (aLocation.Length() < JS_SuffixLength) { + return false; + } + const auto ext = Substring(aLocation, aLocation.Length() - JS_SuffixLength); + return ext == JS_Suffix; +} + +static bool IsJSM_JS(const nsACString& aLocation) { + if (aLocation.Length() < JSM_JS_SuffixLength) { + return false; + } + const auto ext = + Substring(aLocation, aLocation.Length() - JSM_JS_SuffixLength); + return ext == JSM_JS_Suffix; +} + +static bool IsMJS(const nsACString& aLocation) { + if (aLocation.Length() < MJS_SuffixLength) { + return false; + } + const auto ext = Substring(aLocation, aLocation.Length() - MJS_SuffixLength); + return ext == MJS_Suffix; +} + +static void MJSToJSM(const nsACString& aLocation, nsAutoCString& aOut) { + MOZ_ASSERT(IsMJS(aLocation)); + aOut = Substring(aLocation, 0, aLocation.Length() - MJS_SuffixLength); + aOut += JSM_Suffix; +} + +static bool TryToMJS(const nsACString& aLocation, nsAutoCString& aOut) { + if (IsJSM(aLocation)) { + aOut = Substring(aLocation, 0, aLocation.Length() - JSM_SuffixLength); + aOut += MJS_Suffix; + return true; + } + + if (IsJSM_JS(aLocation)) { + aOut = Substring(aLocation, 0, aLocation.Length() - JSM_JS_SuffixLength); + aOut += MJS_Suffix; + return true; + } + + if (IsJS(aLocation)) { + aOut = Substring(aLocation, 0, aLocation.Length() - JS_SuffixLength); + aOut += MJS_Suffix; + return true; + } + + return false; +} + +static bool Dump(JSContext* cx, unsigned argc, Value* vp) { + if (!nsJSUtils::DumpEnabled()) { + return true; + } + + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() == 0) { + return true; + } + + RootedString str(cx, JS::ToString(cx, args[0])); + if (!str) { + return false; + } + + JS::UniqueChars utf8str = JS_EncodeStringToUTF8(cx, str); + if (!utf8str) { + return false; + } + + MOZ_LOG(nsContentUtils::DOMDumpLog(), mozilla::LogLevel::Debug, + ("[Backstage.Dump] %s", utf8str.get())); +#ifdef ANDROID + __android_log_print(ANDROID_LOG_INFO, "Gecko", "%s", utf8str.get()); +#endif +#ifdef XP_WIN + if (IsDebuggerPresent()) { + nsAutoJSString wstr; + if (!wstr.init(cx, str)) { + return false; + } + OutputDebugStringW(wstr.get()); + } +#endif + fputs(utf8str.get(), stdout); + fflush(stdout); + return true; +} + +static bool Debug(JSContext* cx, unsigned argc, Value* vp) { +#ifdef DEBUG + return Dump(cx, argc, vp); +#else + return true; +#endif +} + +static const JSFunctionSpec gGlobalFun[] = { + JS_FN("dump", Dump, 1, 0), JS_FN("debug", Debug, 1, 0), + JS_FN("atob", Atob, 1, 0), JS_FN("btoa", Btoa, 1, 0), JS_FS_END}; + +class MOZ_STACK_CLASS JSCLContextHelper { + public: + explicit JSCLContextHelper(JSContext* aCx); + ~JSCLContextHelper(); + + void reportErrorAfterPop(UniqueChars&& buf); + + private: + JSContext* mContext; + UniqueChars mBuf; + + // prevent copying and assignment + JSCLContextHelper(const JSCLContextHelper&) = delete; + const JSCLContextHelper& operator=(const JSCLContextHelper&) = delete; +}; + +static nsresult MOZ_FORMAT_PRINTF(2, 3) + ReportOnCallerUTF8(JSContext* callerContext, const char* format, ...) { + if (!callerContext) { + return NS_ERROR_FAILURE; + } + + va_list ap; + va_start(ap, format); + + UniqueChars buf = JS_vsmprintf(format, ap); + if (!buf) { + va_end(ap); + return NS_ERROR_OUT_OF_MEMORY; + } + + JS_ReportErrorUTF8(callerContext, "%s", buf.get()); + + va_end(ap); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(mozJSModuleLoader, nsIMemoryReporter) + +mozJSModuleLoader::mozJSModuleLoader() + : mImports(16), + mInProgressImports(16), + mFallbackImports(16), +#ifdef STARTUP_RECORDER_ENABLED + mImportStacks(16), +#endif + mLocations(16), + mInitialized(false), + mLoaderGlobal(dom::RootingCx()), + mServicesObj(dom::RootingCx()) { +} + +#define ENSURE_DEP(name) \ + { \ + nsresult rv = Ensure##name(); \ + NS_ENSURE_SUCCESS(rv, rv); \ + } +#define ENSURE_DEPS(...) MOZ_FOR_EACH(ENSURE_DEP, (), (__VA_ARGS__)); +#define BEGIN_ENSURE(self, ...) \ + { \ + if (m##self) return NS_OK; \ + ENSURE_DEPS(__VA_ARGS__); \ + } + +class MOZ_STACK_CLASS ModuleLoaderInfo { + public: + explicit ModuleLoaderInfo(const nsACString& aLocation, + SkipCheckForBrokenURLOrZeroSized aSkipCheck = + SkipCheckForBrokenURLOrZeroSized::No) + : mLocation(&aLocation), mIsModule(false), mSkipCheck(aSkipCheck) {} + explicit ModuleLoaderInfo(JS::loader::ModuleLoadRequest* aRequest) + : mLocation(nullptr), + mURI(aRequest->mURI), + mIsModule(true), + mSkipCheck(aRequest->GetComponentLoadContext()->mSkipCheck) {} + + SkipCheckForBrokenURLOrZeroSized getSkipCheckForBrokenURLOrZeroSized() const { + return mSkipCheck; + } + + void resetChannelWithCheckForBrokenURLOrZeroSized() { + MOZ_ASSERT(mSkipCheck == SkipCheckForBrokenURLOrZeroSized::Yes); + mSkipCheck = SkipCheckForBrokenURLOrZeroSized::No; + mScriptChannel = nullptr; + } + + nsIIOService* IOService() { + MOZ_ASSERT(mIOService); + return mIOService; + } + nsresult EnsureIOService() { + if (mIOService) { + return NS_OK; + } + nsresult rv; + mIOService = do_GetIOService(&rv); + return rv; + } + + nsIURI* URI() { + MOZ_ASSERT(mURI); + return mURI; + } + nsresult EnsureURI() { + BEGIN_ENSURE(URI, IOService); + MOZ_ASSERT(mLocation); + return mIOService->NewURI(*mLocation, nullptr, nullptr, + getter_AddRefs(mURI)); + } + + nsIChannel* ScriptChannel() { + MOZ_ASSERT(mScriptChannel); + return mScriptChannel; + } + nsresult EnsureScriptChannel() { + BEGIN_ENSURE(ScriptChannel, IOService, URI); + + // Skip check for missing URL when handling JSM-to-ESM fallback. + return NS_NewChannel( + getter_AddRefs(mScriptChannel), mURI, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_SCRIPT, + /* aCookieJarSettings = */ nullptr, + /* aPerformanceStorage = */ nullptr, + /* aLoadGroup = */ nullptr, /* aCallbacks = */ nullptr, + nsIRequest::LOAD_NORMAL, mIOService, /* aSandboxFlags = */ 0, + mSkipCheck == SkipCheckForBrokenURLOrZeroSized::Yes); + } + + nsIURI* ResolvedURI() { + MOZ_ASSERT(mResolvedURI); + return mResolvedURI; + } + nsresult EnsureResolvedURI() { + BEGIN_ENSURE(ResolvedURI, URI); + return ResolveURI(mURI, getter_AddRefs(mResolvedURI)); + } + + const nsACString& Key() { + MOZ_ASSERT(mLocation); + return *mLocation; + } + + [[nodiscard]] nsresult GetLocation(nsCString& aLocation) { + nsresult rv = EnsureURI(); + NS_ENSURE_SUCCESS(rv, rv); + return mURI->GetSpec(aLocation); + } + + bool IsModule() const { return mIsModule; } + + private: + const nsACString* mLocation; + nsCOMPtr<nsIIOService> mIOService; + nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsIChannel> mScriptChannel; + nsCOMPtr<nsIURI> mResolvedURI; + const bool mIsModule; + SkipCheckForBrokenURLOrZeroSized mSkipCheck; +}; + +template <typename... Args> +static nsresult ReportOnCallerUTF8(JSCLContextHelper& helper, + const char* format, ModuleLoaderInfo& info, + Args... args) { + nsCString location; + MOZ_TRY(info.GetLocation(location)); + + UniqueChars buf = JS_smprintf(format, location.get(), args...); + if (!buf) { + return NS_ERROR_OUT_OF_MEMORY; + } + + helper.reportErrorAfterPop(std::move(buf)); + return NS_ERROR_FAILURE; +} + +#undef BEGIN_ENSURE +#undef ENSURE_DEPS +#undef ENSURE_DEP + +mozJSModuleLoader::~mozJSModuleLoader() { + MOZ_ASSERT(!mInitialized, + "UnloadModules() was not explicitly called before cleaning up " + "mozJSModuleLoader"); + + if (mInitialized) { + UnloadModules(); + } +} + +StaticRefPtr<mozJSModuleLoader> mozJSModuleLoader::sSelf; +StaticRefPtr<mozJSModuleLoader> mozJSModuleLoader::sDevToolsLoader; + +void mozJSModuleLoader::FindTargetObject(JSContext* aCx, + MutableHandleObject aTargetObject) { + aTargetObject.set(JS::GetJSMEnvironmentOfScriptedCaller(aCx)); + + // The above could fail if the scripted caller is not a JSM (it could be a DOM + // scope, for instance). + // + // If the target object was not in the JSM shared global, return the global + // instead. This is needed when calling the subscript loader within a frame + // script, since it the FrameScript NSVO will have been found. + if (!aTargetObject || + !IsLoaderGlobal(JS::GetNonCCWObjectGlobal(aTargetObject))) { + aTargetObject.set(JS::GetScriptedCallerGlobal(aCx)); + + // Return nullptr if the scripted caller is in a different compartment. + if (JS::GetCompartment(aTargetObject) != js::GetContextCompartment(aCx)) { + aTargetObject.set(nullptr); + } + } +} + +void mozJSModuleLoader::InitStatics() { + MOZ_ASSERT(!sSelf); + sSelf = new mozJSModuleLoader(); + RegisterWeakMemoryReporter(sSelf); +} + +void mozJSModuleLoader::UnloadLoaders() { + if (sSelf) { + sSelf->Unload(); + } + if (sDevToolsLoader) { + sDevToolsLoader->Unload(); + } +} + +void mozJSModuleLoader::Unload() { + UnloadModules(); + + if (mModuleLoader) { + mModuleLoader->Shutdown(); + mModuleLoader = nullptr; + } +} + +void mozJSModuleLoader::ShutdownLoaders() { + MOZ_ASSERT(sSelf); + UnregisterWeakMemoryReporter(sSelf); + sSelf = nullptr; + + if (sDevToolsLoader) { + UnregisterWeakMemoryReporter(sDevToolsLoader); + sDevToolsLoader = nullptr; + } +} + +mozJSModuleLoader* mozJSModuleLoader::GetOrCreateDevToolsLoader() { + if (sDevToolsLoader) { + return sDevToolsLoader; + } + sDevToolsLoader = new mozJSModuleLoader(); + RegisterWeakMemoryReporter(sDevToolsLoader); + return sDevToolsLoader; +} + +// This requires that the keys be strings and the values be pointers. +template <class Key, class Data, class UserData, class Converter> +static size_t SizeOfTableExcludingThis( + const nsBaseHashtable<Key, Data, UserData, Converter>& aTable, + MallocSizeOf aMallocSizeOf) { + size_t n = aTable.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (const auto& entry : aTable) { + n += entry.GetKey().SizeOfExcludingThisIfUnshared(aMallocSizeOf); + n += entry.GetData()->SizeOfIncludingThis(aMallocSizeOf); + } + return n; +} + +#ifdef STARTUP_RECORDER_ENABLED +template <class Key, class Data, class UserData, class Converter> +static size_t SizeOfStringTableExcludingThis( + const nsBaseHashtable<Key, Data, UserData, Converter>& aTable, + MallocSizeOf aMallocSizeOf) { + size_t n = aTable.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (const auto& entry : aTable) { + n += entry.GetKey().SizeOfExcludingThisIfUnshared(aMallocSizeOf); + n += entry.GetData().SizeOfExcludingThisIfUnshared(aMallocSizeOf); + } + return n; +} +#endif + +size_t mozJSModuleLoader::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) { + size_t n = aMallocSizeOf(this); + n += SizeOfTableExcludingThis(mImports, aMallocSizeOf); + n += mLocations.ShallowSizeOfExcludingThis(aMallocSizeOf); + n += SizeOfTableExcludingThis(mInProgressImports, aMallocSizeOf); + n += SizeOfTableExcludingThis(mFallbackImports, aMallocSizeOf); +#ifdef STARTUP_RECORDER_ENABLED + n += SizeOfStringTableExcludingThis(mImportStacks, aMallocSizeOf); +#endif + return n; +} + +// Memory report paths are split on '/', with each module displayed as a +// separate layer of a visual tree. Any slashes which are meant to belong to a +// particular path module, rather than be used to build a hierarchy, therefore +// need to be replaced with backslashes, which are displayed as slashes in the +// UI. +// +// If `aAnonymize` is true, this function also attempts to translate any file: +// URLs to replace the path of the GRE directory with a placeholder containing +// no private information, and strips all other file: URIs of everything upto +// their last `/`. +static nsAutoCString MangleURL(const char* aURL, bool aAnonymize) { + nsAutoCString url(aURL); + + if (aAnonymize) { + static nsCString greDirURI; + if (greDirURI.IsEmpty()) { + nsCOMPtr<nsIFile> file; + Unused << NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(file)); + if (file) { + nsCOMPtr<nsIURI> uri; + NS_NewFileURI(getter_AddRefs(uri), file); + if (uri) { + uri->GetSpec(greDirURI); + RunOnShutdown([&]() { greDirURI.Truncate(0); }); + } + } + } + + url.ReplaceSubstring(greDirURI, "<GREDir>/"_ns); + + if (FindInReadable("file:"_ns, url)) { + if (StringBeginsWith(url, "jar:file:"_ns)) { + int32_t idx = url.RFindChar('!'); + url = "jar:file://<anonymized>!"_ns + Substring(url, idx); + } else { + int32_t idx = url.RFindChar('/'); + url = "file://<anonymized>/"_ns + Substring(url, idx); + } + } + } + + url.ReplaceChar('/', '\\'); + return url; +} + +NS_IMETHODIMP +mozJSModuleLoader::CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) { + for (const auto& entry : mImports.Values()) { + nsAutoCString path("js-module-loader/modules/"); + path.Append(MangleURL(entry->location, aAnonymize)); + + aHandleReport->Callback(""_ns, path, KIND_NONHEAP, UNITS_COUNT, 1, + "Loaded JS modules"_ns, aData); + } + + return NS_OK; +} + +void mozJSModuleLoader::CreateLoaderGlobal(JSContext* aCx, + const nsACString& aLocation, + MutableHandleObject aGlobal) { + auto backstagePass = MakeRefPtr<BackstagePass>(); + RealmOptions options; + auto& creationOptions = options.creationOptions(); + + creationOptions.setFreezeBuiltins(true).setNewCompartmentInSystemZone(); + if (IsDevToolsLoader()) { + creationOptions.setInvisibleToDebugger(true); + } + xpc::SetPrefableRealmOptions(options); + + // Defer firing OnNewGlobalObject until after the __URI__ property has + // been defined so the JS debugger can tell what module the global is + // for + RootedObject global(aCx); + +#ifdef DEBUG + // See mozJSModuleLoader::DefineJSServices. + mIsInitializingLoaderGlobal = true; +#endif + nsresult rv = xpc::InitClassesWithNewWrappedGlobal( + aCx, static_cast<nsIGlobalObject*>(backstagePass), + nsContentUtils::GetSystemPrincipal(), xpc::DONT_FIRE_ONNEWGLOBALHOOK, + options, &global); +#ifdef DEBUG + mIsInitializingLoaderGlobal = false; +#endif + NS_ENSURE_SUCCESS_VOID(rv); + + NS_ENSURE_TRUE_VOID(global); + + backstagePass->SetGlobalObject(global); + + JSAutoRealm ar(aCx, global); + if (!JS_DefineFunctions(aCx, global, gGlobalFun)) { + return; + } + + if (!CreateJSServices(aCx)) { + return; + } + + if (!DefineJSServices(aCx, global)) { + return; + } + + // Set the location information for the new global, so that tools like + // about:memory may use that information + xpc::SetLocationForGlobal(global, aLocation); + + MOZ_ASSERT(!mModuleLoader); + RefPtr<ComponentScriptLoader> scriptLoader = new ComponentScriptLoader; + mModuleLoader = new ComponentModuleLoader(scriptLoader, backstagePass); + backstagePass->InitModuleLoader(mModuleLoader); + + aGlobal.set(global); +} + +JSObject* mozJSModuleLoader::GetSharedGlobal(JSContext* aCx) { + if (!mLoaderGlobal) { + JS::RootedObject globalObj(aCx); + + CreateLoaderGlobal( + aCx, IsDevToolsLoader() ? "DevTools global"_ns : "shared JSM global"_ns, + &globalObj); + + // If we fail to create a module global this early, we're not going to + // get very far, so just bail out now. + MOZ_RELEASE_ASSERT(globalObj); + mLoaderGlobal = globalObj; + + // AutoEntryScript required to invoke debugger hook, which is a + // Gecko-specific concept at present. + dom::AutoEntryScript aes(globalObj, "module loader report global"); + JS_FireOnNewGlobalObject(aes.cx(), globalObj); + } + + return mLoaderGlobal; +} + +/* static */ +nsresult mozJSModuleLoader::LoadSingleModuleScript( + ComponentModuleLoader* aModuleLoader, JSContext* aCx, + JS::loader::ModuleLoadRequest* aRequest, MutableHandleScript aScriptOut) { + AUTO_PROFILER_MARKER_TEXT( + "ChromeUtils.importESModule static import", JS, + MarkerOptions(MarkerStack::Capture(), + MarkerInnerWindowIdFromJSContext(aCx)), + nsContentUtils::TruncatedURLForDisplay(aRequest->mURI)); + + ModuleLoaderInfo info(aRequest); + nsresult rv = info.EnsureResolvedURI(); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> sourceFile; + rv = GetSourceFile(info.ResolvedURI(), getter_AddRefs(sourceFile)); + NS_ENSURE_SUCCESS(rv, rv); + + bool realFile = LocationIsRealFile(aRequest->mURI); + + RootedScript script(aCx); + rv = GetScriptForLocation(aCx, info, sourceFile, realFile, aScriptOut); + NS_ENSURE_SUCCESS(rv, rv); + +#ifdef STARTUP_RECORDER_ENABLED + if (aModuleLoader == sSelf->mModuleLoader) { + sSelf->RecordImportStack(aCx, aRequest); + } else { + MOZ_ASSERT(sDevToolsLoader); + MOZ_ASSERT(aModuleLoader == sDevToolsLoader->mModuleLoader); + sDevToolsLoader->RecordImportStack(aCx, aRequest); + } +#endif + + return NS_OK; +} + +/* static */ +nsresult mozJSModuleLoader::GetSourceFile(nsIURI* aResolvedURI, + nsIFile** aSourceFileOut) { + // Get the JAR if there is one. + nsCOMPtr<nsIJARURI> jarURI; + nsresult rv = NS_OK; + jarURI = do_QueryInterface(aResolvedURI, &rv); + nsCOMPtr<nsIFileURL> baseFileURL; + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIURI> baseURI; + while (jarURI) { + jarURI->GetJARFile(getter_AddRefs(baseURI)); + jarURI = do_QueryInterface(baseURI, &rv); + } + baseFileURL = do_QueryInterface(baseURI, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } else { + baseFileURL = do_QueryInterface(aResolvedURI, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + return baseFileURL->GetFile(aSourceFileOut); +} + +/* static */ +bool mozJSModuleLoader::LocationIsRealFile(nsIURI* aURI) { + // We need to be extra careful checking for URIs pointing to files. + // EnsureFile may not always get called, especially on resource URIs so we + // need to call GetFile to make sure this is a valid file. + nsresult rv = NS_OK; + nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(aURI, &rv); + nsCOMPtr<nsIFile> testFile; + if (NS_SUCCEEDED(rv)) { + fileURL->GetFile(getter_AddRefs(testFile)); + } + + return bool(testFile); +} + +JSObject* mozJSModuleLoader::PrepareObjectForLocation(JSContext* aCx, + nsIFile* aModuleFile, + nsIURI* aURI, + bool aRealFile) { + RootedObject globalObj(aCx, GetSharedGlobal(aCx)); + NS_ENSURE_TRUE(globalObj, nullptr); + JSAutoRealm ar(aCx, globalObj); + + // |thisObj| is the object we set properties on for a particular .jsm. + RootedObject thisObj(aCx, JS::NewJSMEnvironment(aCx)); + NS_ENSURE_TRUE(thisObj, nullptr); + + if (aRealFile) { + if (XRE_IsParentProcess()) { + RootedObject locationObj(aCx); + + nsresult rv = nsXPConnect::XPConnect()->WrapNative( + aCx, thisObj, aModuleFile, NS_GET_IID(nsIFile), + locationObj.address()); + NS_ENSURE_SUCCESS(rv, nullptr); + NS_ENSURE_TRUE(locationObj, nullptr); + + if (!JS_DefineProperty(aCx, thisObj, "__LOCATION__", locationObj, 0)) { + return nullptr; + } + } + } + + // Expose the URI from which the script was imported through a special + // variable that we insert into the JSM. + nsAutoCString nativePath; + NS_ENSURE_SUCCESS(aURI->GetSpec(nativePath), nullptr); + + RootedString exposedUri( + aCx, JS_NewStringCopyN(aCx, nativePath.get(), nativePath.Length())); + NS_ENSURE_TRUE(exposedUri, nullptr); + + if (!JS_DefineProperty(aCx, thisObj, "__URI__", exposedUri, 0)) { + return nullptr; + } + + return thisObj; +} + +static mozilla::Result<nsCString, nsresult> ReadScript( + ModuleLoaderInfo& aInfo) { + MOZ_TRY(aInfo.EnsureScriptChannel()); + + nsCOMPtr<nsIInputStream> scriptStream; + MOZ_TRY(aInfo.ScriptChannel()->Open(getter_AddRefs(scriptStream))); + + uint64_t len64; + uint32_t bytesRead; + + MOZ_TRY(scriptStream->Available(&len64)); + NS_ENSURE_TRUE(len64 < UINT32_MAX, Err(NS_ERROR_FILE_TOO_BIG)); + NS_ENSURE_TRUE(len64, Err(NS_ERROR_FAILURE)); + uint32_t len = (uint32_t)len64; + + /* malloc an internal buf the size of the file */ + nsCString str; + if (!str.SetLength(len, fallible)) { + return Err(NS_ERROR_OUT_OF_MEMORY); + } + + /* read the file in one swoop */ + MOZ_TRY(scriptStream->Read(str.BeginWriting(), len, &bytesRead)); + if (bytesRead != len) { + return Err(NS_BASE_STREAM_OSERROR); + } + + return std::move(str); +} + +nsresult mozJSModuleLoader::ObjectForLocation( + ModuleLoaderInfo& aInfo, nsIFile* aModuleFile, MutableHandleObject aObject, + MutableHandleScript aTableScript, char** aLocation, + bool aPropagateExceptions, MutableHandleValue aException) { + MOZ_ASSERT(NS_IsMainThread(), "Must be on main thread."); + + dom::AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + + nsresult rv = aInfo.EnsureURI(); + NS_ENSURE_SUCCESS(rv, rv); + + bool realFile = LocationIsRealFile(aInfo.URI()); + + RootedObject obj( + cx, PrepareObjectForLocation(cx, aModuleFile, aInfo.URI(), realFile)); + NS_ENSURE_TRUE(obj, NS_ERROR_FAILURE); + MOZ_ASSERT(!JS_IsGlobalObject(obj)); + + JSAutoRealm ar(cx, obj); + + RootedScript script(cx); + rv = GetScriptForLocation(cx, aInfo, aModuleFile, realFile, &script, + aLocation); + if (NS_FAILED(rv)) { + // Propagate the exception, if one exists. Also, don't leave the stale + // exception on this context. + if (aPropagateExceptions && jsapi.HasException()) { + if (!jsapi.StealException(aException)) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + + return rv; + } + + // Assign aObject here so that it's available to recursive imports. + // See bug 384168. + aObject.set(obj); + + aTableScript.set(script); + + { // Scope for AutoEntryScript + AutoAllowLegacyScriptExecution exemption; + + // We're going to run script via JS_ExecuteScript, so we need an + // AutoEntryScript. This is Gecko-specific and not in any spec. + dom::AutoEntryScript aes(CurrentGlobalOrNull(cx), + "module loader load module"); + JSContext* aescx = aes.cx(); + + bool executeOk = false; + if (JS_IsGlobalObject(obj)) { + JS::RootedValue rval(cx); + executeOk = JS_ExecuteScript(aescx, script, &rval); + } else { + executeOk = JS::ExecuteInJSMEnvironment(aescx, script, obj); + } + + if (!executeOk) { + if (aPropagateExceptions && aes.HasException()) { + // Ignore return value because we're returning an error code + // anyway. + Unused << aes.StealException(aException); + } + aObject.set(nullptr); + aTableScript.set(nullptr); + return NS_ERROR_FAILURE; + } + } + + return rv; +} + +/* static */ +nsresult mozJSModuleLoader::GetScriptForLocation( + JSContext* aCx, ModuleLoaderInfo& aInfo, nsIFile* aModuleFile, + bool aUseMemMap, MutableHandleScript aScriptOut, char** aLocationOut) { + // JS compilation errors are returned via an exception on the context. + MOZ_ASSERT(!JS_IsExceptionPending(aCx)); + + aScriptOut.set(nullptr); + + nsAutoCString nativePath; + nsresult rv = aInfo.URI()->GetSpec(nativePath); + NS_ENSURE_SUCCESS(rv, rv); + + // Before compiling the script, first check to see if we have it in + // the preloader cache or the startupcache. Note: as a rule, preloader cache + // errors and startupcache errors are not fatal to loading the script, since + // we can always slow-load. + + bool storeIntoStartupCache = false; + StartupCache* cache = StartupCache::GetSingleton(); + + aInfo.EnsureResolvedURI(); + + nsAutoCString cachePath; + if (aInfo.IsModule()) { + rv = PathifyURI(JS_CACHE_PREFIX("non-syntactic", "module"), + aInfo.ResolvedURI(), cachePath); + } else { + rv = PathifyURI(JS_CACHE_PREFIX("non-syntactic", "script"), + aInfo.ResolvedURI(), cachePath); + } + NS_ENSURE_SUCCESS(rv, rv); + + JS::DecodeOptions decodeOptions; + ScriptPreloader::FillDecodeOptionsForCachedStencil(decodeOptions); + + RefPtr<JS::Stencil> stencil = + ScriptPreloader::GetSingleton().GetCachedStencil(aCx, decodeOptions, + cachePath); + + if (!stencil && cache) { + ReadCachedStencil(cache, cachePath, aCx, decodeOptions, + getter_AddRefs(stencil)); + if (!stencil) { + JS_ClearPendingException(aCx); + + storeIntoStartupCache = true; + } + } + + if (stencil) { + LOG(("Successfully loaded %s from cache\n", nativePath.get())); + } else { + // The script wasn't in the cache , so compile it now. + LOG(("Slow loading %s\n", nativePath.get())); + + CompileOptions options(aCx); + ScriptPreloader::FillCompileOptionsForCachedStencil(options); + options.setFileAndLine(nativePath.get(), 1); + if (aInfo.IsModule()) { + options.setModule(); + // Top level await is not supported in synchronously loaded modules. + options.topLevelAwait = false; + + // Make all top-level `vars` available in `ModuleEnvironmentObject`. + options.deoptimizeModuleGlobalVars = true; + } else { + options.setForceStrictMode(); + options.setNonSyntacticScope(true); + } + + // If we can no longer write to caches, we should stop using lazy sources + // and instead let normal syntax parsing occur. This can occur in content + // processes after the ScriptPreloader is flushed where we can read but no + // longer write. + if (!storeIntoStartupCache && !ScriptPreloader::GetSingleton().Active()) { + options.setSourceIsLazy(false); + } + + if (aUseMemMap) { + AutoMemMap map; + MOZ_TRY(map.init(aModuleFile)); + + // Note: exceptions will get handled further down; + // don't early return for them here. + auto buf = map.get<char>(); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + if (srcBuf.init(aCx, buf.get(), map.size(), + JS::SourceOwnership::Borrowed)) { + stencil = CompileStencil(aCx, options, srcBuf, aInfo.IsModule()); + } + } else { + nsCString str; + MOZ_TRY_VAR(str, ReadScript(aInfo)); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + if (srcBuf.init(aCx, str.get(), str.Length(), + JS::SourceOwnership::Borrowed)) { + stencil = CompileStencil(aCx, options, srcBuf, aInfo.IsModule()); + } + } + +#ifdef DEBUG + // The above shouldn't touch any options for instantiation. + JS::InstantiateOptions instantiateOptions(options); + instantiateOptions.assertDefault(); +#endif + + if (!stencil) { + return NS_ERROR_FAILURE; + } + } + + aScriptOut.set(InstantiateStencil(aCx, stencil, aInfo.IsModule())); + if (!aScriptOut) { + return NS_ERROR_FAILURE; + } + + // ScriptPreloader::NoteScript needs to be called unconditionally, to + // reflect the usage into the next session's cache. + ScriptPreloader::GetSingleton().NoteStencil(nativePath, cachePath, stencil); + + // Write to startup cache only when we didn't have any cache for the script + // and compiled it. + if (storeIntoStartupCache) { + MOZ_ASSERT(stencil); + + // We successfully compiled the script, so cache it. + rv = WriteCachedStencil(cache, cachePath, aCx, stencil); + + // Don't treat failure to write as fatal, since we might be working + // with a read-only cache. + if (NS_SUCCEEDED(rv)) { + LOG(("Successfully wrote to cache\n")); + } else { + LOG(("Failed to write to cache\n")); + } + } + + /* Owned by ModuleEntry. Freed when we remove from the table. */ + if (aLocationOut) { + *aLocationOut = ToNewCString(nativePath, mozilla::fallible); + if (!*aLocationOut) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + + return NS_OK; +} + +void mozJSModuleLoader::UnloadModules() { + mInitialized = false; + + if (mLoaderGlobal) { + MOZ_ASSERT(JS_HasExtensibleLexicalEnvironment(mLoaderGlobal)); + JS::RootedObject lexicalEnv(dom::RootingCx(), + JS_ExtensibleLexicalEnvironment(mLoaderGlobal)); + JS_SetAllNonReservedSlotsToUndefined(lexicalEnv); + JS_SetAllNonReservedSlotsToUndefined(mLoaderGlobal); + mLoaderGlobal = nullptr; + } + mServicesObj = nullptr; + +#ifdef STARTUP_RECORDER_ENABLED + mImportStacks.Clear(); +#endif + mFallbackImports.Clear(); + mInProgressImports.Clear(); + mImports.Clear(); + mLocations.Clear(); +} + +/* static */ +already_AddRefed<Stencil> mozJSModuleLoader::CompileStencil( + JSContext* aCx, const JS::CompileOptions& aOptions, + JS::SourceText<mozilla::Utf8Unit>& aSource, bool aIsModule) { + if (aIsModule) { + return CompileModuleScriptToStencil(aCx, aOptions, aSource); + } + + return CompileGlobalScriptToStencil(aCx, aOptions, aSource); +} + +/* static */ +JSScript* mozJSModuleLoader::InstantiateStencil(JSContext* aCx, + JS::Stencil* aStencil, + bool aIsModule) { + JS::InstantiateOptions instantiateOptions; + + if (aIsModule) { + RootedObject module(aCx); + module = JS::InstantiateModuleStencil(aCx, instantiateOptions, aStencil); + if (!module) { + return nullptr; + } + + return JS::GetModuleScript(module); + } + + return JS::InstantiateGlobalStencil(aCx, instantiateOptions, aStencil); +} + +nsresult mozJSModuleLoader::ImportInto(const nsACString& registryLocation, + HandleValue targetValArg, JSContext* cx, + uint8_t optionalArgc, + MutableHandleValue retval) { + MOZ_ASSERT(nsContentUtils::IsCallerChrome()); + + RootedValue targetVal(cx, targetValArg); + RootedObject targetObject(cx, nullptr); + + if (optionalArgc) { + // The caller passed in the optional second argument. Get it. + if (targetVal.isObject()) { + // If we're passing in something like a content DOM window, chances + // are the caller expects the properties to end up on the object + // proper and not on the Xray holder. This is dubious, but can be used + // during testing. Given that dumb callers can already leak JSMs into + // content by passing a raw content JS object (where Xrays aren't + // possible), we aim for consistency here. Waive xray. + if (WrapperFactory::IsXrayWrapper(&targetVal.toObject()) && + !WrapperFactory::WaiveXrayAndWrap(cx, &targetVal)) { + return NS_ERROR_FAILURE; + } + targetObject = &targetVal.toObject(); + } else if (!targetVal.isNull()) { + // If targetVal isNull(), we actually want to leave targetObject null. + // Not doing so breaks |make package|. + return ReportOnCallerUTF8(cx, ERROR_SCOPE_OBJ, + PromiseFlatCString(registryLocation).get()); + } + } else { + FindTargetObject(cx, &targetObject); + if (!targetObject) { + return ReportOnCallerUTF8(cx, ERROR_NO_TARGET_OBJECT, + PromiseFlatCString(registryLocation).get()); + } + } + + js::AssertSameCompartment(cx, targetObject); + + RootedObject global(cx); + nsresult rv = ImportInto(registryLocation, targetObject, cx, &global); + + if (global) { + if (!JS_WrapObject(cx, &global)) { + NS_ERROR("can't wrap return value"); + return NS_ERROR_FAILURE; + } + + retval.setObject(*global); + } + return rv; +} + +nsresult mozJSModuleLoader::IsModuleLoaded(const nsACString& aLocation, + bool* retval) { + MOZ_ASSERT(nsContentUtils::IsCallerChrome()); + + mInitialized = true; + ModuleLoaderInfo info(aLocation); + if (mImports.Get(info.Key())) { + *retval = true; + return NS_OK; + } + + if (mModuleLoader) { + nsAutoCString mjsLocation; + if (!TryToMJS(aLocation, mjsLocation)) { + *retval = false; + return NS_OK; + } + + ModuleLoaderInfo mjsInfo(mjsLocation); + + nsresult rv = mjsInfo.EnsureURI(); + NS_ENSURE_SUCCESS(rv, rv); + + if (mModuleLoader->IsModuleFetched(mjsInfo.URI())) { + *retval = true; + return NS_OK; + } + } + + *retval = false; + return NS_OK; +} + +nsresult mozJSModuleLoader::IsJSModuleLoaded(const nsACString& aLocation, + bool* retval) { + MOZ_ASSERT(nsContentUtils::IsCallerChrome()); + + mInitialized = true; + ModuleLoaderInfo info(aLocation); + if (mImports.Get(info.Key())) { + *retval = true; + return NS_OK; + } + + *retval = false; + return NS_OK; +} + +nsresult mozJSModuleLoader::IsESModuleLoaded(const nsACString& aLocation, + bool* retval) { + MOZ_ASSERT(nsContentUtils::IsCallerChrome()); + + mInitialized = true; + ModuleLoaderInfo info(aLocation); + + nsresult rv = info.EnsureURI(); + NS_ENSURE_SUCCESS(rv, rv); + + if (mModuleLoader->IsModuleFetched(info.URI())) { + *retval = true; + return NS_OK; + } + + *retval = false; + return NS_OK; +} + +void mozJSModuleLoader::GetLoadedModules(nsTArray<nsCString>& aLoadedModules) { + aLoadedModules.SetCapacity(mImports.Count()); + for (const auto& data : mImports.Values()) { + aLoadedModules.AppendElement(data->location); + } +} + +nsresult mozJSModuleLoader::GetLoadedESModules( + nsTArray<nsCString>& aLoadedModules) { + return mModuleLoader->GetFetchedModuleURLs(aLoadedModules); +} + +nsresult mozJSModuleLoader::GetLoadedJSAndESModules( + nsTArray<nsCString>& aLoadedModules) { + GetLoadedModules(aLoadedModules); + + nsTArray<nsCString> modules; + nsresult rv = GetLoadedESModules(modules); + NS_ENSURE_SUCCESS(rv, rv); + + for (const auto& location : modules) { + if (IsMJS(location)) { + nsAutoCString jsmLocation; + // NOTE: Unconditionally convert to *.jsm. This doesn't cover *.js case + // but given `Cu.loadedModules` is rarely used for system modules, + // this won't cause much compat issue. + MJSToJSM(location, jsmLocation); + aLoadedModules.AppendElement(jsmLocation); + } + } + + return NS_OK; +} + +#ifdef STARTUP_RECORDER_ENABLED +void mozJSModuleLoader::RecordImportStack(JSContext* aCx, + const nsACString& aLocation) { + if (!Preferences::GetBool("browser.startup.record", false)) { + return; + } + + mImportStacks.InsertOrUpdate( + aLocation, xpc_PrintJSStack(aCx, false, false, false).get()); +} + +void mozJSModuleLoader::RecordImportStack( + JSContext* aCx, JS::loader::ModuleLoadRequest* aRequest) { + if (!Preferences::GetBool("browser.startup.record", false)) { + return; + } + + nsAutoCString location; + nsresult rv = aRequest->mURI->GetSpec(location); + if (NS_FAILED(rv)) { + return; + } + + auto recordJSStackOnly = [&]() { + mImportStacks.InsertOrUpdate( + location, xpc_PrintJSStack(aCx, false, false, false).get()); + }; + + if (aRequest->IsTopLevel()) { + recordJSStackOnly(); + return; + } + + nsAutoCString importerSpec; + rv = aRequest->mReferrer->GetSpec(importerSpec); + if (NS_FAILED(rv)) { + recordJSStackOnly(); + return; + } + + ModuleLoaderInfo importerInfo(importerSpec); + auto importerStack = mImportStacks.Lookup(importerInfo.Key()); + if (!importerStack) { + // The importer's stack is not collected, possibly due to OOM. + recordJSStackOnly(); + return; + } + + nsAutoCString stack; + + stack += "* import [\""; + stack += importerSpec; + stack += "\"]\n"; + stack += *importerStack; + + mImportStacks.InsertOrUpdate(location, stack); +} +#endif + +nsresult mozJSModuleLoader::GetModuleImportStack(const nsACString& aLocation, + nsACString& retval) { +#ifdef STARTUP_RECORDER_ENABLED + MOZ_ASSERT(nsContentUtils::IsCallerChrome()); + + // When querying the DevTools loader, it may not be initialized yet + if (!mInitialized) { + return NS_ERROR_FAILURE; + } + + ModuleLoaderInfo info(aLocation); + auto str = mImportStacks.Lookup(info.Key()); + if (!str) { + return NS_ERROR_FAILURE; + } + + retval = *str; + return NS_OK; +#else + return NS_ERROR_NOT_IMPLEMENTED; +#endif +} + +nsresult mozJSModuleLoader::ImportInto(const nsACString& aLocation, + HandleObject targetObj, JSContext* cx, + MutableHandleObject vp) { + vp.set(nullptr); + + JS::RootedObject exports(cx); + MOZ_TRY(Import(cx, aLocation, vp, &exports, !targetObj)); + + if (targetObj) { + JS::Rooted<JS::IdVector> ids(cx, JS::IdVector(cx)); + if (!JS_Enumerate(cx, exports, &ids)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + JS::RootedValue value(cx); + JS::RootedId id(cx); + for (jsid idVal : ids) { + id = idVal; + if (!JS_GetPropertyById(cx, exports, id, &value) || + !JS_SetPropertyById(cx, targetObj, id, value)) { + return NS_ERROR_FAILURE; + } + } + } + + return NS_OK; +} + +nsresult mozJSModuleLoader::ExtractExports(JSContext* aCx, + ModuleLoaderInfo& aInfo, + ModuleEntry* aMod, + JS::MutableHandleObject aExports) { + // cxhelper must be created before jsapi, so that jsapi is destroyed and + // pops any context it has pushed before we report to the caller context. + JSCLContextHelper cxhelper(aCx); + + // Even though we are calling JS_SetPropertyById on targetObj, we want + // to ensure that we never run script here, so we use an AutoJSAPI and + // not an AutoEntryScript. + dom::AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + JSAutoRealm ar(cx, aMod->obj); + + RootedValue symbols(cx); + { + RootedObject obj( + cx, ResolveModuleObjectProperty(cx, aMod->obj, "EXPORTED_SYMBOLS")); + if (!obj || !JS_GetProperty(cx, obj, "EXPORTED_SYMBOLS", &symbols)) { + return ReportOnCallerUTF8(cxhelper, ERROR_NOT_PRESENT, aInfo); + } + } + + bool isArray; + if (!JS::IsArrayObject(cx, symbols, &isArray)) { + return NS_ERROR_FAILURE; + } + if (!isArray) { + return ReportOnCallerUTF8(cxhelper, ERROR_NOT_AN_ARRAY, aInfo); + } + + RootedObject symbolsObj(cx, &symbols.toObject()); + + // Iterate over symbols array, installing symbols on targetObj: + + uint32_t symbolCount = 0; + if (!JS::GetArrayLength(cx, symbolsObj, &symbolCount)) { + return ReportOnCallerUTF8(cxhelper, ERROR_GETTING_ARRAY_LENGTH, aInfo); + } + +#ifdef DEBUG + nsAutoCString logBuffer; +#endif + + aExports.set(JS_NewPlainObject(cx)); + if (!aExports) { + return NS_ERROR_OUT_OF_MEMORY; + } + + bool missing = false; + + RootedValue value(cx); + RootedId symbolId(cx); + RootedObject symbolHolder(cx); + for (uint32_t i = 0; i < symbolCount; ++i) { + if (!JS_GetElement(cx, symbolsObj, i, &value) || !value.isString() || + !JS_ValueToId(cx, value, &symbolId)) { + return ReportOnCallerUTF8(cxhelper, ERROR_ARRAY_ELEMENT, aInfo, i); + } + + symbolHolder = ResolveModuleObjectPropertyById(cx, aMod->obj, symbolId); + if (!symbolHolder || + !JS_GetPropertyById(cx, symbolHolder, symbolId, &value)) { + RootedString symbolStr(cx, symbolId.toString()); + JS::UniqueChars bytes = JS_EncodeStringToUTF8(cx, symbolStr); + if (!bytes) { + return NS_ERROR_FAILURE; + } + return ReportOnCallerUTF8(cxhelper, ERROR_GETTING_SYMBOL, aInfo, + bytes.get()); + } + + // It's possible |value| is the uninitialized lexical MagicValue when + // there's a cyclic import: const obj = ChromeUtils.import("parent.jsm"). + if (value.isMagic(JS_UNINITIALIZED_LEXICAL)) { + RootedString symbolStr(cx, symbolId.toString()); + JS::UniqueChars bytes = JS_EncodeStringToUTF8(cx, symbolStr); + if (!bytes) { + return NS_ERROR_FAILURE; + } + return ReportOnCallerUTF8(cxhelper, ERROR_UNINITIALIZED_SYMBOL, aInfo, + bytes.get()); + } + + if (value.isUndefined()) { + missing = true; + } + + if (!JS_SetPropertyById(cx, aExports, symbolId, value)) { + RootedString symbolStr(cx, symbolId.toString()); + JS::UniqueChars bytes = JS_EncodeStringToUTF8(cx, symbolStr); + if (!bytes) { + return NS_ERROR_FAILURE; + } + return ReportOnCallerUTF8(cxhelper, ERROR_GETTING_SYMBOL, aInfo, + bytes.get()); + } +#ifdef DEBUG + if (i == 0) { + logBuffer.AssignLiteral("Installing symbols [ "); + } + JS::UniqueChars bytes = JS_EncodeStringToLatin1(cx, symbolId.toString()); + if (!!bytes) { + logBuffer.Append(bytes.get()); + } + logBuffer.Append(' '); + if (i == symbolCount - 1) { + nsCString location; + MOZ_TRY(aInfo.GetLocation(location)); + LOG(("%s] from %s\n", logBuffer.get(), location.get())); + } +#endif + } + + // Don't cache the exports object if any of its exported symbols are + // missing. If the module hasn't finished loading yet, they may be + // defined the next time we try to import it. + if (!missing) { + aMod->exports = aExports; + } + return NS_OK; +} + +/* static */ +bool mozJSModuleLoader::IsTrustedScheme(nsIURI* aURI) { + return aURI->SchemeIs("resource") || aURI->SchemeIs("chrome"); +} + +nsresult mozJSModuleLoader::Import(JSContext* aCx, const nsACString& aLocation, + JS::MutableHandleObject aModuleGlobal, + JS::MutableHandleObject aModuleExports, + bool aIgnoreExports) { + mInitialized = true; + + AUTO_PROFILER_MARKER_TEXT( + "ChromeUtils.import", JS, + MarkerOptions(MarkerStack::Capture(), + MarkerInnerWindowIdFromJSContext(aCx)), + Substring(aLocation, 0, std::min(size_t(128), aLocation.Length()))); + + // The JSM may already be ESM-ified, and in that case the load is expected + // to fail. Suppress the error message, the crash, and also the telemetry + // event for the failure. + // + // If this load fails, it will be redirected to `.sys.mjs` URL + // in TryFallbackToImportESModule, and if the redirect also fails, + // the load is performed again below, with the check enabled. + ModuleLoaderInfo info(aLocation, SkipCheckForBrokenURLOrZeroSized::Yes); + + nsresult rv; + ModuleEntry* mod; + UniquePtr<ModuleEntry> newEntry; + if (!mImports.Get(info.Key(), &mod) && + !mInProgressImports.Get(info.Key(), &mod)) { + // We're trying to import a new JSM, but we're late in shutdown and this + // will likely not succeed and might even crash, so fail here. + if (PastShutdownPhase(ShutdownPhase::XPCOMShutdownFinal)) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + // If we've hit file-not-found and fallback was successful, + // return the cached data. + bool aFound; + rv = TryCachedFallbackToImportESModule( + aCx, aLocation, aModuleGlobal, aModuleExports, aIgnoreExports, &aFound); + NS_ENSURE_SUCCESS(rv, rv); + if (aFound) { + return NS_OK; + } + + newEntry = MakeUnique<ModuleEntry>(RootingContext::get(aCx)); + + // Note: This implies EnsureURI(). + MOZ_TRY(info.EnsureResolvedURI()); + + // Reject imports from untrusted sources. + if (!IsTrustedScheme(info.URI())) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + nsCOMPtr<nsIFile> sourceFile; + rv = GetSourceFile(info.ResolvedURI(), getter_AddRefs(sourceFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = info.ResolvedURI()->GetSpec(newEntry->resolvedURL); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString* existingPath; + if (mLocations.Get(newEntry->resolvedURL, &existingPath) && + *existingPath != info.Key()) { + return NS_ERROR_UNEXPECTED; + } + + mLocations.InsertOrUpdate(newEntry->resolvedURL, + MakeUnique<nsCString>(info.Key())); + + RootedValue exception(aCx); + { + mInProgressImports.InsertOrUpdate(info.Key(), newEntry.get()); + auto cleanup = + MakeScopeExit([&]() { mInProgressImports.Remove(info.Key()); }); + + rv = ObjectForLocation(info, sourceFile, &newEntry->obj, + &newEntry->thisObjectKey, &newEntry->location, + true, &exception); + } + + if (NS_FAILED(rv)) { + mLocations.Remove(newEntry->resolvedURL); + if (!exception.isUndefined()) { + // An exception was thrown during compilation. Propagate it + // out to our caller so they can report it. + bool isModuleSyntaxError = false; + + if (exception.isObject()) { + JS::Rooted<JSObject*> exceptionObj(aCx, &exception.toObject()); + JSAutoRealm ar(aCx, exceptionObj); + JSErrorReport* report = JS_ErrorFromException(aCx, exceptionObj); + if (report) { + switch (report->errorNumber) { + case JSMSG_IMPORT_DECL_AT_TOP_LEVEL: + case JSMSG_EXPORT_DECL_AT_TOP_LEVEL: + // If the exception is related to module syntax, it's most + // likely because of misuse of API. + // Provide better error message. + isModuleSyntaxError = true; + + JS_ReportErrorUTF8(aCx, + "ChromeUtils.import is called against " + "an ES module script (%s). Please use " + "ChromeUtils.importESModule instead " + "(SyntaxError: %s)", + aLocation.BeginReading(), + report->message().c_str()); + break; + default: + break; + } + } + } + + if (!isModuleSyntaxError) { + if (!JS_WrapValue(aCx, &exception)) { + return NS_ERROR_OUT_OF_MEMORY; + } + JS_SetPendingException(aCx, exception); + } + + return NS_ERROR_FAILURE; + } + + if (rv == NS_ERROR_FILE_NOT_FOUND || rv == NS_ERROR_FILE_ACCESS_DENIED) { + // NS_ERROR_FILE_ACCESS_DENIED happens if the access is blocked by + // sandbox. + rv = TryFallbackToImportESModule(aCx, aLocation, aModuleGlobal, + aModuleExports, aIgnoreExports); + + if (rv == NS_ERROR_FILE_NOT_FOUND || + rv == NS_ERROR_FILE_ACCESS_DENIED) { + // Both JSM and ESM are not found, with the check inside necko + // skipped (See EnsureScriptChannel and mSkipCheck). + // + // Perform the load again with the check enabled, so that + // logging, crash-on-autonation, and telemetry event happen. + if (NS_SUCCEEDED(info.EnsureURI()) && + !LocationIsRealFile(info.URI())) { + info.resetChannelWithCheckForBrokenURLOrZeroSized(); + (void)ReadScript(info); + } + } + + return rv; + } + + // Something failed, but we don't know what it is, guess. + return NS_ERROR_FILE_NOT_FOUND; + } + +#ifdef STARTUP_RECORDER_ENABLED + RecordImportStack(aCx, aLocation); +#endif + + mod = newEntry.get(); + } + + MOZ_ASSERT(mod->obj, "Import table contains entry with no object"); + JS::RootedObject globalProxy(aCx); + { + JSAutoRealm ar(aCx, mod->obj); + + globalProxy = CreateJSMEnvironmentProxy(aCx, mod->obj); + if (!globalProxy) { + return NS_ERROR_FAILURE; + } + } + if (!JS_WrapObject(aCx, &globalProxy)) { + return NS_ERROR_FAILURE; + } + aModuleGlobal.set(globalProxy); + + JS::RootedObject exports(aCx, mod->exports); + if (!exports && !aIgnoreExports) { + MOZ_TRY(ExtractExports(aCx, info, mod, &exports)); + } + + if (exports && !JS_WrapObject(aCx, &exports)) { + mLocations.Remove(newEntry->resolvedURL); + return NS_ERROR_FAILURE; + } + aModuleExports.set(exports); + + // Cache this module for later + if (newEntry) { + mImports.InsertOrUpdate(info.Key(), std::move(newEntry)); + } + + return NS_OK; +} + +nsresult mozJSModuleLoader::TryFallbackToImportESModule( + JSContext* aCx, const nsACString& aLocation, + JS::MutableHandleObject aModuleGlobal, + JS::MutableHandleObject aModuleExports, bool aIgnoreExports) { + nsAutoCString mjsLocation; + if (!TryToMJS(aLocation, mjsLocation)) { + return NS_ERROR_FILE_NOT_FOUND; + } + + JS::RootedObject moduleNamespace(aCx); + // The fallback can fail if the URL was not for ESMified JSM. Suppress the + // error message, the crash, and also the telemetry event for the failure. + nsresult rv = ImportESModule(aCx, mjsLocation, &moduleNamespace, + SkipCheckForBrokenURLOrZeroSized::Yes); + if (rv == NS_ERROR_FILE_NOT_FOUND || rv == NS_ERROR_FILE_ACCESS_DENIED) { + // The error for ESModule shouldn't be exposed if the file does not exist, + // or the access is blocked by sandbox. + if (JS_IsExceptionPending(aCx)) { + JS_ClearPendingException(aCx); + } + } + NS_ENSURE_SUCCESS(rv, rv); + + JS::RootedObject globalProxy(aCx); + { + JSAutoRealm ar(aCx, moduleNamespace); + + JS::RootedObject moduleObject( + aCx, JS::GetModuleForNamespace(aCx, moduleNamespace)); + if (!moduleObject) { + return NS_ERROR_FAILURE; + } + + globalProxy = CreateModuleEnvironmentProxy(aCx, moduleObject); + if (!globalProxy) { + return NS_ERROR_FAILURE; + } + + // Cache the redirect to use in subsequent imports. + ModuleLoaderInfo info(aLocation); + auto newEntry = MakeUnique<FallbackModuleEntry>(RootingContext::get(aCx)); + newEntry->globalProxy = globalProxy; + newEntry->moduleNamespace = moduleNamespace; + mFallbackImports.InsertOrUpdate(info.Key(), std::move(newEntry)); + } + + if (!JS_WrapObject(aCx, &globalProxy)) { + return NS_ERROR_FAILURE; + } + aModuleGlobal.set(globalProxy); + + if (!aIgnoreExports) { + JS::RootedObject exports(aCx, moduleNamespace); + if (!JS_WrapObject(aCx, &exports)) { + return NS_ERROR_FAILURE; + } + aModuleExports.set(exports); + } + + return NS_OK; +} + +nsresult mozJSModuleLoader::TryCachedFallbackToImportESModule( + JSContext* aCx, const nsACString& aLocation, + JS::MutableHandleObject aModuleGlobal, + JS::MutableHandleObject aModuleExports, bool aIgnoreExports, bool* aFound) { + ModuleLoaderInfo info(aLocation); + FallbackModuleEntry* fallbackMod; + if (!mFallbackImports.Get(info.Key(), &fallbackMod)) { + *aFound = false; + return NS_OK; + } + + JS::RootedObject globalProxy(aCx, fallbackMod->globalProxy); + if (!JS_WrapObject(aCx, &globalProxy)) { + return NS_ERROR_FAILURE; + } + aModuleGlobal.set(globalProxy); + + if (!aIgnoreExports) { + JS::RootedObject exports(aCx, fallbackMod->moduleNamespace); + if (!JS_WrapObject(aCx, &exports)) { + return NS_ERROR_FAILURE; + } + aModuleExports.set(exports); + } + + *aFound = true; + return NS_OK; +} + +nsresult mozJSModuleLoader::ImportESModule( + JSContext* aCx, const nsACString& aLocation, + JS::MutableHandleObject aModuleNamespace, + SkipCheckForBrokenURLOrZeroSized + aSkipCheck /* = SkipCheckForBrokenURLOrZeroSized::No */) { + using namespace JS::loader; + + mInitialized = true; + + // Called from ChromeUtils::ImportESModule. + nsCString str(aLocation); + + AUTO_PROFILER_MARKER_TEXT( + "ChromeUtils.importESModule", JS, + MarkerOptions(MarkerStack::Capture(), + MarkerInnerWindowIdFromJSContext(aCx)), + Substring(aLocation, 0, std::min(size_t(128), aLocation.Length()))); + + RootedObject globalObj(aCx, GetSharedGlobal(aCx)); + NS_ENSURE_TRUE(globalObj, NS_ERROR_FAILURE); + MOZ_ASSERT(xpc::Scriptability::Get(globalObj).Allowed()); + + // The module loader should be instantiated when fetching the shared global + MOZ_ASSERT(mModuleLoader); + + JSAutoRealm ar(aCx, globalObj); + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aLocation); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrincipal> principal = + mModuleLoader->GetGlobalObject()->PrincipalOrNull(); + MOZ_ASSERT(principal); + + RefPtr<ScriptFetchOptions> options = new ScriptFetchOptions( + CORS_NONE, dom::ReferrerPolicy::No_referrer, principal); + + RefPtr<ComponentLoadContext> context = new ComponentLoadContext(); + context->mSkipCheck = aSkipCheck; + + RefPtr<VisitedURLSet> visitedSet = + ModuleLoadRequest::NewVisitedSetForTopLevelImport(uri); + + RefPtr<ModuleLoadRequest> request = new ModuleLoadRequest( + uri, options, dom::SRIMetadata(), + /* aReferrer = */ nullptr, context, + /* aIsTopLevel = */ true, + /* aIsDynamicImport = */ false, mModuleLoader, visitedSet, nullptr); + + rv = request->StartModuleLoad(); + if (NS_FAILED(rv)) { + mModuleLoader->MaybeReportLoadError(aCx); + return rv; + } + + rv = mModuleLoader->ProcessRequests(); + if (NS_FAILED(rv)) { + mModuleLoader->MaybeReportLoadError(aCx); + return rv; + } + + MOZ_ASSERT(request->IsReadyToRun()); + if (!request->mModuleScript) { + mModuleLoader->MaybeReportLoadError(aCx); + return NS_ERROR_FAILURE; + } + + // All modules are loaded. MaybeReportLoadError isn't necessary from here. + + if (!request->InstantiateModuleGraph()) { + return NS_ERROR_FAILURE; + } + + rv = mModuleLoader->EvaluateModuleInContext(aCx, request, + JS::ThrowModuleErrorsSync); + NS_ENSURE_SUCCESS(rv, rv); + if (JS_IsExceptionPending(aCx)) { + return NS_ERROR_FAILURE; + } + + RefPtr<ModuleScript> moduleScript = request->mModuleScript; + JS::Rooted<JSObject*> module(aCx, moduleScript->ModuleRecord()); + aModuleNamespace.set(JS::GetModuleNamespace(aCx, module)); + + return NS_OK; +} + +nsresult mozJSModuleLoader::Unload(const nsACString& aLocation) { + if (!mInitialized) { + return NS_OK; + } + + ModuleLoaderInfo info(aLocation); + + ModuleEntry* mod; + if (mImports.Get(info.Key(), &mod)) { + mLocations.Remove(mod->resolvedURL); + mImports.Remove(info.Key()); + } + + // If this is the last module to be unloaded, we will leak mLoaderGlobal + // until UnloadModules is called. So be it. + + return NS_OK; +} + +bool mozJSModuleLoader::CreateJSServices(JSContext* aCx) { + JSObject* services = NewJSServices(aCx); + if (!services) { + return false; + } + + mServicesObj = services; + return true; +} + +bool mozJSModuleLoader::DefineJSServices(JSContext* aCx, + JS::Handle<JSObject*> aGlobal) { + if (!mServicesObj) { + // This function is called whenever creating a new global that needs + // `Services`, including the loader's shared global. + // + // This function is no-op if it's called during creating the loader's + // shared global. + // + // See also CreateAndDefineJSServices. + MOZ_ASSERT(!mLoaderGlobal); + MOZ_ASSERT(mIsInitializingLoaderGlobal); + return true; + } + + JS::Rooted<JS::Value> services(aCx, ObjectValue(*mServicesObj)); + if (!JS_WrapValue(aCx, &services)) { + return false; + } + + JS::Rooted<JS::PropertyKey> servicesId( + aCx, XPCJSContext::Get()->GetStringID(XPCJSContext::IDX_SERVICES)); + return JS_DefinePropertyById(aCx, aGlobal, servicesId, services, 0); +} + +size_t mozJSModuleLoader::ModuleEntry::SizeOfIncludingThis( + MallocSizeOf aMallocSizeOf) const { + size_t n = aMallocSizeOf(this); + n += aMallocSizeOf(location); + + return n; +} + +//---------------------------------------------------------------------- + +JSCLContextHelper::JSCLContextHelper(JSContext* aCx) + : mContext(aCx), mBuf(nullptr) {} + +JSCLContextHelper::~JSCLContextHelper() { + if (mBuf) { + JS_ReportErrorUTF8(mContext, "%s", mBuf.get()); + } +} + +void JSCLContextHelper::reportErrorAfterPop(UniqueChars&& buf) { + MOZ_ASSERT(!mBuf, "Already called reportErrorAfterPop"); + mBuf = std::move(buf); +} diff --git a/js/xpconnect/loader/mozJSModuleLoader.h b/js/xpconnect/loader/mozJSModuleLoader.h new file mode 100644 index 0000000000..e5a114a193 --- /dev/null +++ b/js/xpconnect/loader/mozJSModuleLoader.h @@ -0,0 +1,264 @@ +/* -*- 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/. */ + +#ifndef mozJSModuleLoader_h +#define mozJSModuleLoader_h + +#include "ComponentModuleLoader.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/FileLocation.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/StaticPtr.h" +#include "nsIMemoryReporter.h" +#include "nsISupports.h" +#include "nsIURI.h" +#include "nsClassHashtable.h" +#include "jsapi.h" +#include "js/experimental/JSStencil.h" +#include "SkipCheckForBrokenURLOrZeroSized.h" + +#include "xpcpublic.h" + +class nsIFile; +class ModuleLoaderInfo; + +namespace mozilla { +class ScriptPreloader; +} // namespace mozilla + +namespace JS::loader { +class ModuleLoadRequest; +} // namespace JS::loader + +#if defined(NIGHTLY_BUILD) || defined(MOZ_DEV_EDITION) || defined(DEBUG) +# define STARTUP_RECORDER_ENABLED +#endif + +class mozJSModuleLoader final : public nsIMemoryReporter { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMEMORYREPORTER + + // Returns the list of all JSMs. + void GetLoadedModules(nsTArray<nsCString>& aLoadedModules); + + // Returns the list of all ESMs. + nsresult GetLoadedESModules(nsTArray<nsCString>& aLoadedModules); + + // Returns the list of all JSMs and ESMs. + nsresult GetLoadedJSAndESModules(nsTArray<nsCString>& aLoadedModules); + + nsresult GetModuleImportStack(const nsACString& aLocation, + nsACString& aRetval); + + void FindTargetObject(JSContext* aCx, JS::MutableHandleObject aTargetObject); + + static void InitStatics(); + static void UnloadLoaders(); + static void ShutdownLoaders(); + + static mozJSModuleLoader* Get() { + MOZ_ASSERT(sSelf, "Should have already created the module loader"); + return sSelf; + } + + static mozJSModuleLoader* GetDevToolsLoader() { return sDevToolsLoader; } + static mozJSModuleLoader* GetOrCreateDevToolsLoader(); + + nsresult ImportInto(const nsACString& aResourceURI, + JS::HandleValue aTargetObj, JSContext* aCx, uint8_t aArgc, + JS::MutableHandleValue aRetval); + + // Load a JSM. + nsresult Import(JSContext* aCx, const nsACString& aResourceURI, + JS::MutableHandleObject aModuleGlobal, + JS::MutableHandleObject aModuleExports, + bool aIgnoreExports = false); + + // Load an ES6 module and all its dependencies. + nsresult ImportESModule( + JSContext* aCx, const nsACString& aResourceURI, + JS::MutableHandleObject aModuleNamespace, + mozilla::loader::SkipCheckForBrokenURLOrZeroSized aSkipCheck = + mozilla::loader::SkipCheckForBrokenURLOrZeroSized::No); + + // Fallback from Import to ImportESModule. + nsresult TryFallbackToImportESModule(JSContext* aCx, + const nsACString& aResourceURI, + JS::MutableHandleObject aModuleGlobal, + JS::MutableHandleObject aModuleExports, + bool aIgnoreExports); + + // If the request was handled by fallback before, fills the output and + // sets *aFound to true and returns NS_OK. + // If the request wasn't yet handled by fallback, sets *Found to false + // and returns NS_OK. + nsresult TryCachedFallbackToImportESModule( + JSContext* aCx, const nsACString& aResourceURI, + JS::MutableHandleObject aModuleGlobal, + JS::MutableHandleObject aModuleExports, bool aIgnoreExports, + bool* aFound); + +#ifdef STARTUP_RECORDER_ENABLED + void RecordImportStack(JSContext* aCx, const nsACString& aLocation); + void RecordImportStack(JSContext* aCx, + JS::loader::ModuleLoadRequest* aRequest); +#endif + + nsresult Unload(const nsACString& aResourceURI); + nsresult IsModuleLoaded(const nsACString& aResourceURI, bool* aRetval); + nsresult IsJSModuleLoaded(const nsACString& aResourceURI, bool* aRetval); + nsresult IsESModuleLoaded(const nsACString& aResourceURI, bool* aRetval); + bool IsLoaderGlobal(JSObject* aObj) { return mLoaderGlobal == aObj; } + bool IsDevToolsLoader() const { return this == sDevToolsLoader; } + + // Public methods for use from ComponentModuleLoader. + static bool IsTrustedScheme(nsIURI* aURI); + static nsresult LoadSingleModuleScript( + mozilla::loader::ComponentModuleLoader* aModuleLoader, JSContext* aCx, + JS::loader::ModuleLoadRequest* aRequest, + JS::MutableHandleScript aScriptOut); + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf); + + bool DefineJSServices(JSContext* aCx, JS::Handle<JSObject*> aGlobal); + + protected: + mozJSModuleLoader(); + ~mozJSModuleLoader(); + + friend class XPCJSRuntime; + + private: + static mozilla::StaticRefPtr<mozJSModuleLoader> sSelf; + static mozilla::StaticRefPtr<mozJSModuleLoader> sDevToolsLoader; + + void Unload(); + void UnloadModules(); + + void CreateLoaderGlobal(JSContext* aCx, const nsACString& aLocation, + JS::MutableHandleObject aGlobal); + void CreateDevToolsLoaderGlobal(JSContext* aCx, const nsACString& aLocation, + JS::MutableHandleObject aGlobal); + + bool CreateJSServices(JSContext* aCx); + + JSObject* GetSharedGlobal(JSContext* aCx); + + static nsresult GetSourceFile(nsIURI* aResolvedURI, nsIFile** aSourceFileOut); + + static bool LocationIsRealFile(nsIURI* aURI); + + JSObject* PrepareObjectForLocation(JSContext* aCx, nsIFile* aModuleFile, + nsIURI* aURI, bool aRealFile); + + nsresult ObjectForLocation(ModuleLoaderInfo& aInfo, nsIFile* aModuleFile, + JS::MutableHandleObject aObject, + JS::MutableHandleScript aTableScript, + char** aLocation, bool aCatchException, + JS::MutableHandleValue aException); + + // Get the script for a given location, either from a cached stencil or by + // compiling it from source. + static nsresult GetScriptForLocation(JSContext* aCx, ModuleLoaderInfo& aInfo, + nsIFile* aModuleFile, bool aUseMemMap, + JS::MutableHandleScript aScriptOut, + char** aLocationOut = nullptr); + + static already_AddRefed<JS::Stencil> CompileStencil( + JSContext* aCx, const JS::CompileOptions& aOptions, + JS::SourceText<mozilla::Utf8Unit>& aSource, bool aIsModule); + static JSScript* InstantiateStencil(JSContext* aCx, JS::Stencil* aStencil, + bool aIsModule); + + nsresult ImportInto(const nsACString& aLocation, JS::HandleObject targetObj, + JSContext* callercx, JS::MutableHandleObject vp); + + class ModuleEntry { + public: + explicit ModuleEntry(JS::RootingContext* aRootingCx) + : obj(aRootingCx), exports(aRootingCx), thisObjectKey(aRootingCx) { + location = nullptr; + } + + ~ModuleEntry() { Clear(); } + + void Clear() { + if (obj) { + if (JS_HasExtensibleLexicalEnvironment(obj)) { + JS::RootedObject lexicalEnv(mozilla::dom::RootingCx(), + JS_ExtensibleLexicalEnvironment(obj)); + JS_SetAllNonReservedSlotsToUndefined(lexicalEnv); + } + JS_SetAllNonReservedSlotsToUndefined(obj); + obj = nullptr; + thisObjectKey = nullptr; + } + + if (location) { + free(location); + } + + obj = nullptr; + thisObjectKey = nullptr; + location = nullptr; + } + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + JS::PersistentRootedObject obj; + JS::PersistentRootedObject exports; + JS::PersistentRootedScript thisObjectKey; + char* location; + nsCString resolvedURL; + }; + + class FallbackModuleEntry { + public: + explicit FallbackModuleEntry(JS::RootingContext* aRootingCx) + : globalProxy(aRootingCx), moduleNamespace(aRootingCx) {} + + ~FallbackModuleEntry() { Clear(); } + + void Clear() { + globalProxy = nullptr; + moduleNamespace = nullptr; + } + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this); + } + + JS::PersistentRootedObject globalProxy; + JS::PersistentRootedObject moduleNamespace; + }; + + nsresult ExtractExports(JSContext* aCx, ModuleLoaderInfo& aInfo, + ModuleEntry* aMod, JS::MutableHandleObject aExports); + + nsClassHashtable<nsCStringHashKey, ModuleEntry> mImports; + nsTHashMap<nsCStringHashKey, ModuleEntry*> mInProgressImports; + nsClassHashtable<nsCStringHashKey, FallbackModuleEntry> mFallbackImports; +#ifdef STARTUP_RECORDER_ENABLED + nsTHashMap<nsCStringHashKey, nsCString> mImportStacks; +#endif + + // A map of on-disk file locations which are loaded as modules to the + // pre-resolved URIs they were loaded from. Used to prevent the same file + // from being loaded separately, from multiple URLs. + nsClassHashtable<nsCStringHashKey, nsCString> mLocations; + + bool mInitialized; +#ifdef DEBUG + bool mIsInitializingLoaderGlobal = false; +#endif + JS::PersistentRooted<JSObject*> mLoaderGlobal; + JS::PersistentRooted<JSObject*> mServicesObj; + + RefPtr<mozilla::loader::ComponentModuleLoader> mModuleLoader; +}; + +#endif diff --git a/js/xpconnect/loader/mozJSSubScriptLoader.cpp b/js/xpconnect/loader/mozJSSubScriptLoader.cpp new file mode 100644 index 0000000000..33192bff29 --- /dev/null +++ b/js/xpconnect/loader/mozJSSubScriptLoader.cpp @@ -0,0 +1,476 @@ +/* -*- 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 "mozJSSubScriptLoader.h" +#include "js/experimental/JSStencil.h" +#include "mozJSModuleLoader.h" +#include "mozJSLoaderUtils.h" + +#include "nsIURI.h" +#include "nsIIOService.h" +#include "nsIChannel.h" +#include "nsIInputStream.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "xpcprivate.h" // xpc::OptionsBase +#include "js/CompilationAndEvaluation.h" // JS::Compile +#include "js/CompileOptions.h" // JS::ReadOnlyCompileOptions, JS::DecodeOptions +#include "js/friend/JSMEnvironment.h" // JS::ExecuteInJSMEnvironment, JS::IsJSMEnvironment +#include "js/SourceText.h" // JS::Source{Ownership,Text} +#include "js/Wrapper.h" + +#include "mozilla/ContentPrincipal.h" +#include "mozilla/dom/ScriptLoader.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/ProfilerMarkers.h" +#include "mozilla/ScriptPreloader.h" +#include "mozilla/SystemPrincipal.h" +#include "mozilla/scache/StartupCache.h" +#include "mozilla/scache/StartupCacheUtils.h" +#include "mozilla/Unused.h" +#include "mozilla/Utf8.h" // mozilla::Utf8Unit +#include "nsContentUtils.h" +#include "nsString.h" + +using namespace mozilla::scache; +using namespace JS; +using namespace xpc; +using namespace mozilla; +using namespace mozilla::dom; + +class MOZ_STACK_CLASS LoadSubScriptOptions : public OptionsBase { + public: + explicit LoadSubScriptOptions(JSContext* cx = xpc_GetSafeJSContext(), + JSObject* options = nullptr) + : OptionsBase(cx, options), + target(cx), + ignoreCache(false), + wantReturnValue(false) {} + + virtual bool Parse() override { + return ParseObject("target", &target) && + ParseBoolean("ignoreCache", &ignoreCache) && + ParseBoolean("wantReturnValue", &wantReturnValue); + } + + RootedObject target; + bool ignoreCache; + bool wantReturnValue; +}; + +/* load() error msgs, XXX localize? */ +#define LOAD_ERROR_NOSERVICE "Error creating IO Service." +#define LOAD_ERROR_NOURI "Error creating URI (invalid URL scheme?)" +#define LOAD_ERROR_NOSCHEME "Failed to get URI scheme. This is bad." +#define LOAD_ERROR_URI_NOT_LOCAL "Trying to load a non-local URI." +#define LOAD_ERROR_NOSTREAM "Error opening input stream (invalid filename?)" +#define LOAD_ERROR_NOCONTENT "ContentLength not available (not a local URL?)" +#define LOAD_ERROR_BADCHARSET "Error converting to specified charset" +#define LOAD_ERROR_NOSPEC "Failed to get URI spec. This is bad." +#define LOAD_ERROR_CONTENTTOOBIG "ContentLength is too large" + +mozJSSubScriptLoader::mozJSSubScriptLoader() = default; + +mozJSSubScriptLoader::~mozJSSubScriptLoader() = default; + +NS_IMPL_ISUPPORTS(mozJSSubScriptLoader, mozIJSSubScriptLoader) + +#define JSSUB_CACHE_PREFIX(aScopeType, aCompilationTarget) \ + "jssubloader/" aScopeType "/" aCompilationTarget + +static void SubscriptCachePath(JSContext* cx, nsIURI* uri, + JS::HandleObject targetObj, + nsACString& cachePath) { + // StartupCache must distinguish between non-syntactic vs global when + // computing the cache key. + if (!JS_IsGlobalObject(targetObj)) { + PathifyURI(JSSUB_CACHE_PREFIX("non-syntactic", "script"), uri, cachePath); + } else { + PathifyURI(JSSUB_CACHE_PREFIX("global", "script"), uri, cachePath); + } +} + +static void ReportError(JSContext* cx, const nsACString& msg) { + NS_ConvertUTF8toUTF16 ucMsg(msg); + + RootedValue exn(cx); + if (xpc::NonVoidStringToJsval(cx, ucMsg, &exn)) { + JS_SetPendingException(cx, exn); + } +} + +static void ReportError(JSContext* cx, const char* origMsg, nsIURI* uri) { + if (!uri) { + ReportError(cx, nsDependentCString(origMsg)); + return; + } + + nsAutoCString spec; + nsresult rv = uri->GetSpec(spec); + if (NS_FAILED(rv)) { + spec.AssignLiteral("(unknown)"); + } + + nsAutoCString msg(origMsg); + msg.AppendLiteral(": "); + msg.Append(spec); + ReportError(cx, msg); +} + +static bool EvalStencil(JSContext* cx, HandleObject targetObj, + HandleObject loadScope, MutableHandleValue retval, + nsIURI* uri, bool storeIntoStartupCache, + bool storeIntoPreloadCache, JS::Stencil* stencil) { + MOZ_ASSERT(!js::IsWrapper(targetObj)); + + JS::InstantiateOptions options; + JS::RootedScript script(cx, + JS::InstantiateGlobalStencil(cx, options, stencil)); + if (!script) { + return false; + } + + if (JS_IsGlobalObject(targetObj)) { + if (!JS_ExecuteScript(cx, script, retval)) { + return false; + } + } else if (JS::IsJSMEnvironment(targetObj)) { + if (!JS::ExecuteInJSMEnvironment(cx, script, targetObj)) { + return false; + } + retval.setUndefined(); + } else { + JS::RootedObjectVector envChain(cx); + if (!envChain.append(targetObj)) { + return false; + } + if (!loadScope) { + // A null loadScope means we are cross-realm. In this case, we should + // check the target isn't in the JSM loader shared-global or we will + // contaminate all JSMs in the realm. + // + // NOTE: If loadScope is already a shared-global JSM, we can't + // determine which JSM the target belongs to and have to assume it + // is in our JSM. +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + JSObject* targetGlobal = JS::GetNonCCWObjectGlobal(targetObj); + MOZ_DIAGNOSTIC_ASSERT( + !mozJSModuleLoader::Get()->IsLoaderGlobal(targetGlobal), + "Don't load subscript into target in a shared-global JSM"); +#endif + if (!JS_ExecuteScript(cx, envChain, script, retval)) { + return false; + } + } else if (JS_IsGlobalObject(loadScope)) { + if (!JS_ExecuteScript(cx, envChain, script, retval)) { + return false; + } + } else { + MOZ_ASSERT(JS::IsJSMEnvironment(loadScope)); + if (!JS::ExecuteInJSMEnvironment(cx, script, loadScope, envChain)) { + return false; + } + retval.setUndefined(); + } + } + + JSAutoRealm rar(cx, targetObj); + if (!JS_WrapValue(cx, retval)) { + return false; + } + + if (script && (storeIntoStartupCache || storeIntoPreloadCache)) { + nsAutoCString cachePath; + SubscriptCachePath(cx, uri, targetObj, cachePath); + + nsCString uriStr; + if (storeIntoPreloadCache && NS_SUCCEEDED(uri->GetSpec(uriStr))) { + ScriptPreloader::GetSingleton().NoteStencil(uriStr, cachePath, stencil); + } + + if (storeIntoStartupCache) { + JSAutoRealm ar(cx, script); + WriteCachedStencil(StartupCache::GetSingleton(), cachePath, cx, stencil); + } + } + + return true; +} + +bool mozJSSubScriptLoader::ReadStencil( + JS::Stencil** stencilOut, nsIURI* uri, JSContext* cx, + const JS::ReadOnlyCompileOptions& options, nsIIOService* serv, + bool useCompilationScope) { + // We create a channel and call SetContentType, to avoid expensive MIME type + // lookups (bug 632490). + nsCOMPtr<nsIChannel> chan; + nsCOMPtr<nsIInputStream> instream; + nsresult rv; + rv = NS_NewChannel(getter_AddRefs(chan), uri, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER, + nullptr, // nsICookieJarSettings + nullptr, // PerformanceStorage + nullptr, // aLoadGroup + nullptr, // aCallbacks + nsIRequest::LOAD_NORMAL, serv); + + if (NS_SUCCEEDED(rv)) { + chan->SetContentType("application/javascript"_ns); + rv = chan->Open(getter_AddRefs(instream)); + } + + if (NS_FAILED(rv)) { + ReportError(cx, LOAD_ERROR_NOSTREAM, uri); + return false; + } + + int64_t len = -1; + + rv = chan->GetContentLength(&len); + if (NS_FAILED(rv)) { + ReportError(cx, LOAD_ERROR_NOCONTENT, uri); + return false; + } + + if (len > INT32_MAX) { + ReportError(cx, LOAD_ERROR_CONTENTTOOBIG, uri); + return false; + } + + nsCString buf; + rv = NS_ReadInputStreamToString(instream, buf, len); + NS_ENSURE_SUCCESS(rv, false); + + if (len < 0) { + len = buf.Length(); + } + + Maybe<JSAutoRealm> ar; + + // Note that when using the ScriptPreloader cache with loadSubScript, there + // will be a side-effect of keeping the global that the script was compiled + // for alive. See note above in EvalScript(). + // + // This will compile the script in XPConnect compilation scope. When the + // script is evaluated, it will be cloned into the target scope to be + // executed, avoiding leaks on the first session when we don't have a + // startup cache. + if (useCompilationScope) { + ar.emplace(cx, xpc::CompilationScope()); + } + + JS::SourceText<Utf8Unit> srcBuf; + if (!srcBuf.init(cx, buf.get(), len, JS::SourceOwnership::Borrowed)) { + return false; + } + + RefPtr<JS::Stencil> stencil = + JS::CompileGlobalScriptToStencil(cx, options, srcBuf); + stencil.forget(stencilOut); + return *stencilOut; +} + +NS_IMETHODIMP +mozJSSubScriptLoader::LoadSubScript(const nsAString& url, HandleValue target, + JSContext* cx, MutableHandleValue retval) { + /* + * Loads a local url, referring to UTF-8-encoded data, and evals it into the + * current cx. Synchronous. ChromeUtils.compileScript() should be used for + * async loads. + * url: The url to load. Must be local so that it can be loaded + * synchronously. + * targetObj: Optional object to eval the script onto (defaults to context + * global) + * returns: Whatever jsval the script pointed to by the url returns. + * Should ONLY (O N L Y !) be called from JavaScript code. + */ + LoadSubScriptOptions options(cx); + options.target = target.isObject() ? &target.toObject() : nullptr; + return DoLoadSubScriptWithOptions(url, options, cx, retval); +} + +NS_IMETHODIMP +mozJSSubScriptLoader::LoadSubScriptWithOptions(const nsAString& url, + HandleValue optionsVal, + JSContext* cx, + MutableHandleValue retval) { + if (!optionsVal.isObject()) { + return NS_ERROR_INVALID_ARG; + } + + LoadSubScriptOptions options(cx, &optionsVal.toObject()); + if (!options.Parse()) { + return NS_ERROR_INVALID_ARG; + } + + return DoLoadSubScriptWithOptions(url, options, cx, retval); +} + +nsresult mozJSSubScriptLoader::DoLoadSubScriptWithOptions( + const nsAString& url, LoadSubScriptOptions& options, JSContext* cx, + MutableHandleValue retval) { + nsresult rv = NS_OK; + RootedObject targetObj(cx); + RootedObject loadScope(cx); + mozJSModuleLoader* loader = mozJSModuleLoader::Get(); + loader->FindTargetObject(cx, &loadScope); + + if (options.target) { + targetObj = options.target; + } else { + targetObj = loadScope; + } + + targetObj = JS_FindCompilationScope(cx, targetObj); + if (!targetObj || !loadScope) { + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT(!js::IsWrapper(targetObj), "JS_FindCompilationScope must unwrap"); + + if (js::GetNonCCWObjectRealm(loadScope) != + js::GetNonCCWObjectRealm(targetObj)) { + loadScope = nullptr; + } + + /* load up the url. From here on, failures are reflected as ``custom'' + * js exceptions */ + nsCOMPtr<nsIURI> uri; + nsAutoCString uriStr; + nsAutoCString scheme; + + // Figure out who's calling us + JS::AutoFilename filename; + if (!JS::DescribeScriptedCaller(cx, &filename)) { + // No scripted frame means we don't know who's calling, bail. + return NS_ERROR_FAILURE; + } + + JSAutoRealm ar(cx, targetObj); + + nsCOMPtr<nsIIOService> serv = do_GetService(NS_IOSERVICE_CONTRACTID); + if (!serv) { + ReportError(cx, nsLiteralCString(LOAD_ERROR_NOSERVICE)); + return NS_OK; + } + + NS_LossyConvertUTF16toASCII asciiUrl(url); + const nsDependentCSubstring profilerUrl = + Substring(asciiUrl, 0, std::min(size_t(128), asciiUrl.Length())); + AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING_NONSENSITIVE( + "mozJSSubScriptLoader::DoLoadSubScriptWithOptions", OTHER, profilerUrl); + AUTO_PROFILER_MARKER_TEXT("SubScript", JS, + MarkerOptions(MarkerStack::Capture(), + MarkerInnerWindowIdFromJSContext(cx)), + profilerUrl); + + // Make sure to explicitly create the URI, since we'll need the + // canonicalized spec. + rv = NS_NewURI(getter_AddRefs(uri), asciiUrl); + if (NS_FAILED(rv)) { + ReportError(cx, nsLiteralCString(LOAD_ERROR_NOURI)); + return NS_OK; + } + + rv = uri->GetSpec(uriStr); + if (NS_FAILED(rv)) { + ReportError(cx, nsLiteralCString(LOAD_ERROR_NOSPEC)); + return NS_OK; + } + + rv = uri->GetScheme(scheme); + if (NS_FAILED(rv)) { + ReportError(cx, LOAD_ERROR_NOSCHEME, uri); + return NS_OK; + } + + // Suppress caching if we're compiling as content or if we're loading a + // blob: URI. + bool useCompilationScope = false; + auto* principal = BasePrincipal::Cast(GetObjectPrincipal(targetObj)); + bool isSystem = principal->Is<SystemPrincipal>(); + if (!isSystem && principal->Is<ContentPrincipal>()) { + nsAutoCString scheme; + principal->GetScheme(scheme); + + // We want to enable caching for scripts with Activity Stream's + // codebase URLs. + if (scheme.EqualsLiteral("about")) { + nsAutoCString filePath; + principal->GetFilePath(filePath); + + useCompilationScope = filePath.EqualsLiteral("home") || + filePath.EqualsLiteral("newtab") || + filePath.EqualsLiteral("welcome"); + isSystem = true; + } + } + bool ignoreCache = + options.ignoreCache || !isSystem || scheme.EqualsLiteral("blob"); + + StartupCache* cache = ignoreCache ? nullptr : StartupCache::GetSingleton(); + + nsAutoCString cachePath; + SubscriptCachePath(cx, uri, targetObj, cachePath); + + JS::DecodeOptions decodeOptions; + ScriptPreloader::FillDecodeOptionsForCachedStencil(decodeOptions); + + RefPtr<JS::Stencil> stencil; + if (!options.ignoreCache) { + if (!options.wantReturnValue) { + // NOTE: If we need the return value, we cannot use ScriptPreloader. + stencil = ScriptPreloader::GetSingleton().GetCachedStencil( + cx, decodeOptions, cachePath); + } + if (!stencil && cache) { + rv = ReadCachedStencil(cache, cachePath, cx, decodeOptions, + getter_AddRefs(stencil)); + if (NS_FAILED(rv) || !stencil) { + JS_ClearPendingException(cx); + } + } + } + + bool storeIntoStartupCache = false; + if (!stencil) { + // Store into startup cache only when the script isn't come from any cache. + storeIntoStartupCache = cache; + + JS::CompileOptions compileOptions(cx); + ScriptPreloader::FillCompileOptionsForCachedStencil(compileOptions); + compileOptions.setFileAndLine(uriStr.get(), 1); + compileOptions.setNonSyntacticScope(!JS_IsGlobalObject(targetObj)); + + if (options.wantReturnValue) { + compileOptions.setNoScriptRval(false); + } + + if (!ReadStencil(getter_AddRefs(stencil), uri, cx, compileOptions, serv, + useCompilationScope)) { + return NS_OK; + } + +#ifdef DEBUG + // The above shouldn't touch any options for instantiation. + JS::InstantiateOptions instantiateOptions(compileOptions); + instantiateOptions.assertDefault(); +#endif + } + + // As a policy choice, we don't store scripts that want return values + // into the preload cache. + bool storeIntoPreloadCache = !ignoreCache && !options.wantReturnValue; + + Unused << EvalStencil(cx, targetObj, loadScope, retval, uri, + storeIntoStartupCache, storeIntoPreloadCache, stencil); + return NS_OK; +} diff --git a/js/xpconnect/loader/mozJSSubScriptLoader.h b/js/xpconnect/loader/mozJSSubScriptLoader.h new file mode 100644 index 0000000000..01909da10c --- /dev/null +++ b/js/xpconnect/loader/mozJSSubScriptLoader.h @@ -0,0 +1,50 @@ +/* -*- 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 "nsCOMPtr.h" +#include "mozIJSSubScriptLoader.h" + +#include "js/experimental/JSStencil.h" +#include "js/CompileOptions.h" // JS::ReadOnlyCompileOptions + +class nsIPrincipal; +class nsIURI; +class LoadSubScriptOptions; + +#define MOZ_JSSUBSCRIPTLOADER_CID \ + { /* 829814d6-1dd2-11b2-8e08-82fa0a339b00 */ \ + 0x929814d6, 0x1dd2, 0x11b2, { \ + 0x8e, 0x08, 0x82, 0xfa, 0x0a, 0x33, 0x9b, 0x00 \ + } \ + } + +class nsIIOService; + +class mozJSSubScriptLoader : public mozIJSSubScriptLoader { + public: + mozJSSubScriptLoader(); + + // all the interface method declarations... + NS_DECL_ISUPPORTS + NS_DECL_MOZIJSSUBSCRIPTLOADER + + private: + virtual ~mozJSSubScriptLoader(); + + bool ReadStencil(JS::Stencil** stencilOut, nsIURI* uri, JSContext* cx, + const JS::ReadOnlyCompileOptions& options, + nsIIOService* serv, bool useCompilationScope); + + nsresult ReadScriptAsync(nsIURI* uri, JS::HandleObject targetObj, + JS::HandleObject loadScope, nsIIOService* serv, + bool wantReturnValue, bool cache, + JS::MutableHandleValue retval); + + nsresult DoLoadSubScriptWithOptions(const nsAString& url, + LoadSubScriptOptions& options, + JSContext* cx, + JS::MutableHandleValue retval); +}; diff --git a/js/xpconnect/loader/nsImportModule.cpp b/js/xpconnect/loader/nsImportModule.cpp new file mode 100644 index 0000000000..a313c44388 --- /dev/null +++ b/js/xpconnect/loader/nsImportModule.cpp @@ -0,0 +1,113 @@ +/* -*- 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 "nsImportModule.h" + +#include "mozilla/dom/ScriptSettings.h" +#include "mozJSModuleLoader.h" +#include "nsContentUtils.h" +#include "nsExceptionHandler.h" +#include "nsPrintfCString.h" +#include "xpcpublic.h" +#include "xpcprivate.h" +#include "js/PropertyAndElement.h" // JS_GetProperty + +using mozilla::dom::AutoJSAPI; + +namespace mozilla { +namespace loader { + +static void AnnotateCrashReportWithJSException(JSContext* aCx, + const char* aURI) { + JS::RootedValue exn(aCx); + if (JS_GetPendingException(aCx, &exn)) { + JS_ClearPendingException(aCx); + + JSAutoRealm ar(aCx, xpc::PrivilegedJunkScope()); + JS_WrapValue(aCx, &exn); + + nsAutoCString file; + uint32_t line; + uint32_t column; + nsAutoString msg; + nsContentUtils::ExtractErrorValues(aCx, exn, file, &line, &column, msg); + + nsPrintfCString errorString("Failed to load module \"%s\": %s:%u:%u: %s", + aURI, file.get(), line, column, + NS_ConvertUTF16toUTF8(msg).get()); + + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::JSModuleLoadError, errorString); + } +} + +nsresult ImportModule(const char* aURI, const char* aExportName, + const nsIID& aIID, void** aResult, bool aInfallible) { + AutoJSAPI jsapi; + MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); + JSContext* cx = jsapi.cx(); + + JS::RootedObject global(cx); + JS::RootedObject exports(cx); + nsresult rv = mozJSModuleLoader::Get()->Import(cx, nsDependentCString(aURI), + &global, &exports); + if (NS_WARN_IF(NS_FAILED(rv))) { + if (aInfallible) { + AnnotateCrashReportWithJSException(cx, aURI); + + MOZ_CRASH_UNSAFE_PRINTF("Failed to load critical module \"%s\"", aURI); + } + return rv; + } + + if (aExportName) { + JS::RootedValue namedExport(cx); + if (!JS_GetProperty(cx, exports, aExportName, &namedExport)) { + return NS_ERROR_FAILURE; + } + if (!namedExport.isObject()) { + return NS_ERROR_XPC_BAD_CONVERT_JS; + } + exports.set(&namedExport.toObject()); + } + + return nsXPConnect::XPConnect()->WrapJS(cx, exports, aIID, aResult); +} + +nsresult ImportESModule(const char* aURI, const char* aExportName, + const nsIID& aIID, void** aResult, bool aInfallible) { + AutoJSAPI jsapi; + MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); + JSContext* cx = jsapi.cx(); + + JS::RootedObject moduleNamespace(cx); + nsresult rv = mozJSModuleLoader::Get()->ImportESModule( + cx, nsDependentCString(aURI), &moduleNamespace); + if (NS_WARN_IF(NS_FAILED(rv))) { + if (aInfallible) { + AnnotateCrashReportWithJSException(cx, aURI); + + MOZ_CRASH_UNSAFE_PRINTF("Failed to load critical module \"%s\"", aURI); + } + return rv; + } + + if (aExportName) { + JS::RootedValue namedExport(cx); + if (!JS_GetProperty(cx, moduleNamespace, aExportName, &namedExport)) { + return NS_ERROR_FAILURE; + } + if (!namedExport.isObject()) { + return NS_ERROR_XPC_BAD_CONVERT_JS; + } + moduleNamespace.set(&namedExport.toObject()); + } + + return nsXPConnect::XPConnect()->WrapJS(cx, moduleNamespace, aIID, aResult); +} + +} // namespace loader +} // namespace mozilla diff --git a/js/xpconnect/loader/nsImportModule.h b/js/xpconnect/loader/nsImportModule.h new file mode 100644 index 0000000000..31f6f8c7c1 --- /dev/null +++ b/js/xpconnect/loader/nsImportModule.h @@ -0,0 +1,240 @@ +/* -*- 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/. */ + +#ifndef nsImportModule_h +#define nsImportModule_h + +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" + +#include "nsCOMPtr.h" +#include "mozilla/RefPtr.h" + +namespace mozilla { +namespace loader { + +nsresult ImportModule(const char* aURI, const char* aExportName, + const nsIID& aIID, void** aResult, bool aInfallible); + +nsresult ImportESModule(const char* aURI, const char* aExportName, + const nsIID& aIID, void** aResult, bool aInfallible); + +} // namespace loader +} // namespace mozilla + +class MOZ_STACK_CLASS nsImportModule final : public nsCOMPtr_helper { + public: + nsImportModule(const char* aURI, const char* aExportName, nsresult* aErrorPtr, + bool aInfallible) + : mURI(aURI), + mExportName(aExportName), + mErrorPtr(aErrorPtr), + mInfallible(aInfallible) { + MOZ_ASSERT_IF(mErrorPtr, !mInfallible); + } + + virtual nsresult NS_FASTCALL operator()(const nsIID& aIID, + void** aResult) const override { + nsresult rv = ::mozilla::loader::ImportModule(mURI, mExportName, aIID, + aResult, mInfallible); + if (mErrorPtr) { + *mErrorPtr = rv; + } + return rv; + } + + private: + const char* mURI; + const char* mExportName; + nsresult* mErrorPtr; + bool mInfallible; +}; + +/** + * These helpers make it considerably easier for C++ code to import a JS module + * and wrap it in an appropriately-defined XPIDL interface for its exports. + * Typical usage is something like: + * + * Foo.jsm: + * + * var EXPORTED_SYMBOLS = ["foo"]; + * + * function foo(bar) { + * return bar.toString(); + * } + * + * mozIFoo.idl: + * + * interface mozIFoo : nsISupports { + * AString foo(double meh); + * } + * + * Thing.cpp: + * + * nsCOMPtr<mozIFoo> foo = do_ImportModule( + * "resource://meh/Foo.jsm"); + * + * MOZ_TRY(foo->Foo(42)); + * + * For JS modules which export all fields within a single named object, a second + * argument can be passed naming that object. + * + * Foo.jsm: + * + * var EXPORTED_SYMBOLS = ["Foo"]; + * + * var Foo = { + * function foo(bar) { + * return bar.toString(); + * } + * }; + * + * Thing.cpp: + * + * nsCOMPtr<mozIFoo> foo = do_ImportModule( + * "resource:://meh/Foo.jsm", "Foo"); + */ + +template <size_t N> +inline nsImportModule do_ImportModule(const char (&aURI)[N]) { + return {aURI, nullptr, nullptr, /* infallible */ true}; +} + +template <size_t N> +inline nsImportModule do_ImportModule(const char (&aURI)[N], + const mozilla::fallible_t&) { + return {aURI, nullptr, nullptr, /* infallible */ false}; +} + +template <size_t N> +inline nsImportModule do_ImportModule(const char (&aURI)[N], nsresult* aRv) { + return {aURI, nullptr, aRv, /* infallible */ false}; +} + +template <size_t N, size_t N2> +inline nsImportModule do_ImportModule(const char (&aURI)[N], + const char (&aExportName)[N2]) { + return {aURI, aExportName, nullptr, /* infallible */ true}; +} + +template <size_t N, size_t N2> +inline nsImportModule do_ImportModule(const char (&aURI)[N], + const char (&aExportName)[N2], + const mozilla::fallible_t&) { + return {aURI, aExportName, nullptr, /* infallible */ false}; +} + +template <size_t N, size_t N2> +inline nsImportModule do_ImportModule(const char (&aURI)[N], + const char (&aExportName)[N2], + nsresult* aRv) { + return {aURI, aExportName, aRv, /* infallible */ false}; +} + +class MOZ_STACK_CLASS nsImportESModule final : public nsCOMPtr_helper { + public: + nsImportESModule(const char* aURI, const char* aExportName, + nsresult* aErrorPtr, bool aInfallible) + : mURI(aURI), + mExportName(aExportName), + mErrorPtr(aErrorPtr), + mInfallible(aInfallible) { + MOZ_ASSERT_IF(mErrorPtr, !mInfallible); + } + + virtual nsresult NS_FASTCALL operator()(const nsIID& aIID, + void** aResult) const override { + nsresult rv = ::mozilla::loader::ImportESModule(mURI, mExportName, aIID, + aResult, mInfallible); + if (mErrorPtr) { + *mErrorPtr = rv; + } + return rv; + } + + private: + const char* mURI; + const char* mExportName; + nsresult* mErrorPtr; + bool mInfallible; +}; + +/** + * Usage with exported name: + * + * Foo.sys.mjs: + * + * export function foo(bar) { + * return bar.toString(); + * } + * + * mozIFoo.idl: + * + * interface mozIFoo : nsISupports { + * AString foo(double meh); + * } + * + * Thing.cpp: + * + * nsCOMPtr<mozIFoo> foo = do_ImportESModule( + * "resource://meh/Foo.sys.mjs"); + * + * MOZ_TRY(foo->Foo(42)); + * + * Usage with a single named object: + * + * Foo.sys.mjs: + * + * export var Foo = { + * function foo(bar) { + * return bar.toString(); + * } + * }; + * + * Thing.cpp: + * + * nsCOMPtr<mozIFoo> foo = do_ImportESModule( + * "resource:://meh/Foo.sys.mjs", "Foo"); + */ + +template <size_t N> +inline nsImportESModule do_ImportESModule(const char (&aURI)[N]) { + return {aURI, nullptr, nullptr, /* infallible */ true}; +} + +template <size_t N> +inline nsImportESModule do_ImportESModule(const char (&aURI)[N], + const mozilla::fallible_t&) { + return {aURI, nullptr, nullptr, /* infallible */ false}; +} + +template <size_t N> +inline nsImportESModule do_ImportESModule(const char (&aURI)[N], + nsresult* aRv) { + return {aURI, nullptr, aRv, /* infallible */ false}; +} + +template <size_t N, size_t N2> +inline nsImportESModule do_ImportESModule(const char (&aURI)[N], + const char (&aExportName)[N2]) { + return {aURI, aExportName, nullptr, /* infallible */ true}; +} + +template <size_t N, size_t N2> +inline nsImportESModule do_ImportESModule(const char (&aURI)[N], + const char (&aExportName)[N2], + const mozilla::fallible_t&) { + return {aURI, aExportName, nullptr, /* infallible */ false}; +} + +template <size_t N, size_t N2> +inline nsImportESModule do_ImportESModule(const char (&aURI)[N], + const char (&aExportName)[N2], + nsresult* aRv) { + return {aURI, aExportName, aRv, /* infallible */ false}; +} + +#endif // defined nsImportModule_h diff --git a/js/xpconnect/loader/script_cache.py b/js/xpconnect/loader/script_cache.py new file mode 100755 index 0000000000..bd3a746fcf --- /dev/null +++ b/js/xpconnect/loader/script_cache.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +# 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/. + +import io +import os +import struct +import sys + +MAGIC = b"mozXDRcachev002\0" + + +def usage(): + print( + """Usage: script_cache.py <file.bin> ... + + Decodes and prints out the contents of a startup script cache file + (e.g., startupCache/scriptCache.bin) in human-readable form.""" + ) + + sys.exit(1) + + +class ProcessTypes: + Uninitialized = 0 + Parent = 1 + Web = 2 + Extension = 3 + Privileged = 4 + + def __init__(self, val): + self.val = val + + def __str__(self): + res = [] + if self.val & (1 << self.Uninitialized): + raise Exception("Uninitialized process type") + if self.val & (1 << self.Parent): + res.append("Parent") + if self.val & (1 << self.Web): + res.append("Web") + if self.val & (1 << self.Extension): + res.append("Extension") + if self.val & (1 << self.Privileged): + res.append("Privileged") + return "|".join(res) + + +class InputBuffer(object): + def __init__(self, data): + self.data = data + self.offset = 0 + + @property + def remaining(self): + return len(self.data) - self.offset + + def unpack(self, fmt): + res = struct.unpack_from(fmt, self.data, self.offset) + self.offset += struct.calcsize(fmt) + return res + + def unpack_str(self): + (size,) = self.unpack("<H") + res = self.data[self.offset : self.offset + size].decode("utf-8") + self.offset += size + return res + + +if len(sys.argv) < 2 or not os.path.exists(sys.argv[1]): + usage() + +for filename in sys.argv[1:]: + with io.open(filename, "rb") as f: + magic = f.read(len(MAGIC)) + if magic != MAGIC: + raise Exception("Bad magic number") + + (hdrSize,) = struct.unpack("<I", f.read(4)) + + hdr = InputBuffer(f.read(hdrSize)) + + i = 0 + while hdr.remaining: + i += 1 + print("{}: {}".format(i, hdr.unpack_str())) + print(" Key: {}".format(hdr.unpack_str())) + print(" Offset: {:>9,}".format(*hdr.unpack("<I"))) + print(" Size: {:>9,}".format(*hdr.unpack("<I"))) + print(" Processes: {}".format(ProcessTypes(*hdr.unpack("B")))) + print("") |