Bug 1334550 - Part 1 - Proxy moz-extension protocol requests to the parent process; r=jimm,mayhemer
authorHaik Aftandilian <haftandilian@mozilla.com>
Fri, 23 Jun 2017 17:10:54 -0700
changeset 366153 69cb0b4c0b2be646cfd682876fc65d2ddc8ecc6e
parent 366152 ab348cba6c56cf0dfb4acaccee1bfa262ef3b2d8
child 366154 3358bef7a047d347e576bbb83d1461b3776fc5fb
push id32098
push usercbook@mozilla.com
push dateWed, 28 Jun 2017 11:20:51 +0000
treeherdermozilla-central@6190181ff409 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjimm, mayhemer
bugs1334550
milestone56.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1334550 - Part 1 - Proxy moz-extension protocol requests to the parent process; r=jimm,mayhemer Changes ExtensionProtocolHandler to use remote streams for moz-extension loads of file and JAR URI's to allow for filesystem read-access sandboxing. Adds messaging to PNecko to allow child processes to request an input stream or file descriptor for moz-extension URI's. Add ExtensionProtocolHandler singleton so that NeckoParent can call methods directly and ExtensionProtocolHandler::NewFD can use a new member variable |mFileOpenerThread| to open files. Adds FileDescriptorFile, a limited implementation of nsIFile that wraps a file descriptor, to be sideloaded into nsJARChannels so that extension JAR files can be read using a file descriptor without accessing the filesystem directly. MozReview-Commit-ID: 1pcnIpjz2yR
modules/libjar/nsIJARChannel.idl
modules/libjar/nsJARChannel.cpp
modules/libjar/nsJARChannel.h
modules/libpref/init/all.js
netwerk/build/nsNetModule.cpp
netwerk/ipc/NeckoParent.cpp
netwerk/ipc/NeckoParent.h
netwerk/ipc/PNecko.ipdl
netwerk/protocol/res/ExtensionProtocolHandler.cpp
netwerk/protocol/res/ExtensionProtocolHandler.h
netwerk/protocol/res/moz.build
xpcom/io/FileDescriptorFile.cpp
xpcom/io/FileDescriptorFile.h
xpcom/io/moz.build
--- a/modules/libjar/nsIJARChannel.idl
+++ b/modules/libjar/nsIJARChannel.idl
@@ -16,17 +16,20 @@ interface nsIJARChannel : nsIChannel
      * by the server for a remote JAR is not of an expected type).  Scripting,
      * redirects, and plugins should be disabled when loading from this
      * channel.
      */
     [infallible] readonly attribute boolean isUnsafe;
 
     /**
      * Returns the JAR file.  May be null if the jar is remote.
+     * Setting the JAR file is optional and overrides the JAR
+     * file used for local file JARs. Setting the JAR file after
+     * the channel has been opened is not permitted.
      */
-    readonly attribute nsIFile jarFile;
+    attribute nsIFile jarFile;
 
     /**
      * Returns the zip entry if the file is synchronously accessible.
      * This will work even without opening the channel.
      */
     readonly attribute nsIZipEntry zipEntry;
 };
--- a/modules/libjar/nsJARChannel.cpp
+++ b/modules/libjar/nsJARChannel.cpp
@@ -341,16 +341,22 @@ nsJARChannel::LookupFile(bool aAllowAsyn
         return rv;
 
     // The name of the JAR entry must not contain URL-escaped characters:
     // we're moving from URL domain to a filename domain here. nsStandardURL
     // does basic escaping by default, which breaks reading zipped files which
     // have e.g. spaces in their filenames.
     NS_UnescapeURL(mJarEntry);
 
+    if (mJarFileOverride) {
+        mJarFile = mJarFileOverride;
+        LOG(("nsJARChannel::LookupFile [this=%p] Overriding mJarFile\n", this));
+        return NS_OK;
+    }
+
     // try to get a nsIFile directly from the url, which will often succeed.
     {
         nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(mJarBaseURI);
         if (fileURL)
             fileURL->GetFile(getter_AddRefs(mJarFile));
     }
 
     // try to handle a nested jar
@@ -880,16 +886,27 @@ nsJARChannel::GetIsUnsafe(bool *isUnsafe
 NS_IMETHODIMP
 nsJARChannel::GetJarFile(nsIFile **aFile)
 {
     NS_IF_ADDREF(*aFile = mJarFile);
     return NS_OK;
 }
 
 NS_IMETHODIMP
+nsJARChannel::SetJarFile(nsIFile *aFile)
+{
+    if (mOpened) {
+        return NS_ERROR_IN_PROGRESS;
+    }
+    mJarFileOverride = aFile;
+    return NS_OK;
+}
+
+
+NS_IMETHODIMP
 nsJARChannel::GetZipEntry(nsIZipEntry **aZipEntry)
 {
     nsresult rv = LookupFile(false);
     if (NS_FAILED(rv))
         return rv;
 
     if (!mJarFile)
         return NS_ERROR_NOT_AVAILABLE;
--- a/modules/libjar/nsJARChannel.h
+++ b/modules/libjar/nsJARChannel.h
@@ -46,16 +46,18 @@ public:
     NS_DECL_NSISTREAMLISTENER
     NS_DECL_NSITHREADRETARGETABLEREQUEST
     NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER
 
     nsJARChannel();
 
     nsresult Init(nsIURI *uri);
 
+    void SetFile(nsIFile *file);
+
 private:
     virtual ~nsJARChannel();
 
     nsresult CreateJarInput(nsIZipReaderCache *, nsJARInputThunk **);
     nsresult LookupFile(bool aAllowAsync);
     nsresult OpenLocalFile();
     void NotifyError(nsresult aError);
     void FireOnProgress(uint64_t aProgress);
@@ -93,16 +95,17 @@ private:
     bool                            mIsUnsafe;
 
     mozilla::net::MemoryDownloader::Data mTempMem;
     nsCOMPtr<nsIInputStreamPump>    mPump;
     // mRequest is only non-null during OnStartRequest, so we'll have a pointer
     // to the request if we get called back via RetargetDeliveryTo.
     nsCOMPtr<nsIRequest>            mRequest;
     nsCOMPtr<nsIFile>               mJarFile;
+    nsCOMPtr<nsIFile>               mJarFileOverride;
     nsCOMPtr<nsIURI>                mJarBaseURI;
     nsCString                       mJarEntry;
     nsCString                       mInnerJarEntry;
 
     // True if this channel should not download any remote files.
     bool                            mBlockRemoteFiles;
 };
 
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4869,16 +4869,18 @@ pref("extensions.allow-non-mpc-extension
 pref("extensions.webextensions.keepStorageOnUninstall", false);
 pref("extensions.webextensions.keepUuidOnUninstall", false);
 // Redirect basedomain used by identity api
 pref("extensions.webextensions.identity.redirectDomain", "extensions.allizom.org");
 // Whether or not webextension themes are supported.
 pref("extensions.webextensions.themes.enabled", false);
 pref("extensions.webextensions.themes.icons.enabled", false);
 pref("extensions.webextensions.remote", false);
+// Whether or not the moz-extension resource loads are remoted
+pref("extensions.webextensions.protocol.remote", true);
 
 pref("layers.popups.compositing.enabled", false);
 
 // Report Site Issue button
 pref("extensions.webcompat-reporter.newIssueEndpoint", "https://webcompat.com/issues/new");
 #if defined(MOZ_DEV_EDITION) || defined(NIGHTLY_BUILD)
 pref("extensions.webcompat-reporter.enabled", true);
 #else
--- a/netwerk/build/nsNetModule.cpp
+++ b/netwerk/build/nsNetModule.cpp
@@ -305,17 +305,18 @@ NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(Name
 #ifdef NECKO_PROTOCOL_res
 // resource
 #include "nsResProtocolHandler.h"
 #include "ExtensionProtocolHandler.h"
 #include "SubstitutingProtocolHandler.h"
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsResProtocolHandler, Init)
 
 namespace mozilla {
-NS_GENERIC_FACTORY_CONSTRUCTOR(ExtensionProtocolHandler)
+NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(ExtensionProtocolHandler,
+    ExtensionProtocolHandler::GetSingleton)
 NS_GENERIC_FACTORY_CONSTRUCTOR(SubstitutingURL)
 } // namespace mozilla
 #endif
 
 #ifdef NECKO_PROTOCOL_device
 #include "nsDeviceProtocolHandler.h"
 typedef mozilla::net::nsDeviceProtocolHandler nsDeviceProtocolHandler;
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsDeviceProtocolHandler)
--- a/netwerk/ipc/NeckoParent.cpp
+++ b/netwerk/ipc/NeckoParent.cpp
@@ -3,16 +3,18 @@
 
 /* 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 "necko-config.h"
 #include "nsHttp.h"
 #include "mozilla/BasePrincipal.h"
+#include "mozilla/ipc/IPCStreamUtils.h"
+#include "mozilla/net/ExtensionProtocolHandler.h"
 #include "mozilla/net/NeckoParent.h"
 #include "mozilla/net/HttpChannelParent.h"
 #include "mozilla/net/CookieServiceParent.h"
 #include "mozilla/net/WyciwygChannelParent.h"
 #include "mozilla/net/FTPChannelParent.h"
 #include "mozilla/net/WebSocketChannelParent.h"
 #include "mozilla/net/WebSocketEventListenerParent.h"
 #include "mozilla/net/DataChannelParent.h"
@@ -33,16 +35,17 @@
 #include "mozilla/dom/ContentParent.h"
 #include "mozilla/dom/TabContext.h"
 #include "mozilla/dom/TabParent.h"
 #include "mozilla/dom/network/TCPSocketParent.h"
 #include "mozilla/dom/network/TCPServerSocketParent.h"
 #include "mozilla/dom/network/UDPSocketParent.h"
 #include "mozilla/dom/workers/ServiceWorkerManager.h"
 #include "mozilla/LoadContext.h"
+#include "mozilla/MozPromise.h"
 #include "nsPrintfCString.h"
 #include "nsHTMLDNSPrefetch.h"
 #include "nsEscape.h"
 #include "SerializedLoadContext.h"
 #include "nsAuthInformationHolder.h"
 #include "nsIAuthPromptCallback.h"
 #include "ContentPrincipal.h"
 #include "nsINetworkPredictor.h"
@@ -57,18 +60,20 @@ using mozilla::dom::TabContext;
 using mozilla::dom::TabParent;
 using mozilla::net::PTCPSocketParent;
 using mozilla::dom::TCPSocketParent;
 using mozilla::net::PTCPServerSocketParent;
 using mozilla::dom::TCPServerSocketParent;
 using mozilla::net::PUDPSocketParent;
 using mozilla::dom::UDPSocketParent;
 using mozilla::dom::workers::ServiceWorkerManager;
+using mozilla::ipc::AutoIPCStream;
 using mozilla::ipc::OptionalPrincipalInfo;
 using mozilla::ipc::PrincipalInfo;
+using mozilla::ipc::LoadInfoArgsToLoadInfo;
 using IPC::SerializedLoadContext;
 
 namespace mozilla {
 namespace net {
 
 // C++ file contents
 NeckoParent::NeckoParent()
 {
@@ -950,10 +955,106 @@ NeckoParent::RecvNotifyCurrentTopLevelOu
 {
   if (NS_FAILED(NS_NotifyCurrentTopLevelOuterContentWindowId(aWindowId))) {
     NS_WARNING("NS_NotifyCurrentTopLevelOuterContentWindowId failed!");
   }
 
   return IPC_OK();
 }
 
+mozilla::ipc::IPCResult
+NeckoParent::RecvGetExtensionStream(const URIParams& aURI,
+                                    const LoadInfoArgs& aLoadInfo,
+                                    GetExtensionStreamResolver&& aResolve)
+{
+  nsCOMPtr<nsIURI> deserializedURI = DeserializeURI(aURI);
+  if (!deserializedURI) {
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  nsCOMPtr<nsILoadInfo> deserializedLoadInfo;
+  nsresult rv;
+  rv = LoadInfoArgsToLoadInfo(aLoadInfo, getter_AddRefs(deserializedLoadInfo));
+  if (NS_FAILED(rv)) {
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  RefPtr<ExtensionProtocolHandler> ph(ExtensionProtocolHandler::GetSingleton());
+  MOZ_ASSERT(ph);
+
+  // Ask the ExtensionProtocolHandler to give us a new input stream for
+  // this URI. The request comes from an ExtensionProtocolHandler in the
+  // child process, but is not guaranteed to be a valid moz-extension URI,
+  // and not guaranteed to represent a resource that the child should be
+  // allowed to access. The ExtensionProtocolHandler is responsible for
+  // validating the request. Specifically, only URI's for local files that
+  // an extension is allowed to access via moz-extension URI's should be
+  // accepted.
+  AutoIPCStream autoStream;
+  nsCOMPtr<nsIInputStream> inputStream;
+  bool terminateSender = true;
+  auto inputStreamOrReason = ph->NewStream(deserializedURI,
+                                           deserializedLoadInfo,
+                                           &terminateSender);
+  if (inputStreamOrReason.isOk()) {
+    inputStream = inputStreamOrReason.unwrap();
+    ContentParent* contentParent = static_cast<ContentParent*>(Manager());
+    Unused << autoStream.Serialize(inputStream, contentParent);
+  }
+
+  // If NewStream failed, we send back an invalid stream to the child so
+  // it can handle the error. MozPromise rejection is reserved for channel
+  // errors/disconnects.
+  aResolve(autoStream.TakeOptionalValue());
+
+  if (terminateSender) {
+    return IPC_FAIL_NO_REASON(this);
+  } else {
+    return IPC_OK();
+  }
+}
+
+mozilla::ipc::IPCResult
+NeckoParent::RecvGetExtensionFD(const URIParams& aURI,
+                                const OptionalLoadInfoArgs& aLoadInfo,
+                                GetExtensionFDResolver&& aResolve)
+{
+  nsCOMPtr<nsIURI> deserializedURI = DeserializeURI(aURI);
+  if (!deserializedURI) {
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  nsCOMPtr<nsILoadInfo> deserializedLoadInfo;
+  nsresult rv;
+  rv = LoadInfoArgsToLoadInfo(aLoadInfo, getter_AddRefs(deserializedLoadInfo));
+  if (NS_FAILED(rv)) {
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  RefPtr<ExtensionProtocolHandler> ph(ExtensionProtocolHandler::GetSingleton());
+  MOZ_ASSERT(ph);
+
+  // Ask the ExtensionProtocolHandler to give us a new input stream for
+  // this URI. The request comes from an ExtensionProtocolHandler in the
+  // child process, but is not guaranteed to be a valid moz-extension URI,
+  // and not guaranteed to represent a resource that the child should be
+  // allowed to access. The ExtensionProtocolHandler is responsible for
+  // validating the request. Specifically, only URI's for local files that
+  // an extension is allowed to access via moz-extension URI's should be
+  // accepted.
+  bool terminateSender = true;
+  auto result = ph->NewFD(deserializedURI, deserializedLoadInfo,
+                          &terminateSender, aResolve);
+
+  if (result.isErr() && terminateSender) {
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  if (result.isErr()) {
+    FileDescriptor invalidFD;
+    aResolve(invalidFD);
+  }
+
+  return IPC_OK();
+}
+
 } // namespace net
 } // namespace mozilla
--- a/netwerk/ipc/NeckoParent.h
+++ b/netwerk/ipc/NeckoParent.h
@@ -231,14 +231,25 @@ protected:
                                                 const ipc::OptionalURIParams& aSourceURI,
                                                 const PredictorPredictReason& aReason,
                                                 const OriginAttributes& aOriginAttributes) override;
   virtual mozilla::ipc::IPCResult RecvPredReset() override;
 
   virtual mozilla::ipc::IPCResult RecvRemoveRequestContext(const uint64_t& rcid) override;
 
   virtual mozilla::ipc::IPCResult RecvNotifyCurrentTopLevelOuterContentWindowId(const uint64_t& aWindowId) override;
+
+  /* WebExtensions */
+  virtual mozilla::ipc::IPCResult
+    RecvGetExtensionStream(const URIParams& aURI,
+                           const LoadInfoArgs& aLoadInfo,
+                           GetExtensionStreamResolver&& aResolve) override;
+
+  virtual mozilla::ipc::IPCResult
+    RecvGetExtensionFD(const URIParams& aURI,
+                       const OptionalLoadInfoArgs& aLoadInfo,
+                       GetExtensionFDResolver&& aResolve) override;
 };
 
 } // namespace net
 } // namespace mozilla
 
 #endif // mozilla_net_NeckoParent_h
--- a/netwerk/ipc/PNecko.ipdl
+++ b/netwerk/ipc/PNecko.ipdl
@@ -23,16 +23,17 @@ include protocol PDataChannel;
 include protocol PTransportProvider;
 include protocol PChildToParentStream; //FIXME: bug #792908
 include protocol PParentToChildStream; //FIXME: bug #792908
 include protocol PStunAddrsRequest;
 include protocol PFileChannel;
 
 include protocol PRtspController;
 include protocol PRtspChannel;
+include IPCStream;
 include URIParams;
 include NeckoChannelParams;
 include PBrowserOrId;
 include protocol PAltDataOutputStream;
 
 using class IPC::SerializedLoadContext from "SerializedLoadContext.h";
 using mozilla::dom::TabId from "mozilla/dom/ipc/IdType.h";
 using class IPC::Principal from "mozilla/dom/PermissionMessageUtils.h";
@@ -122,16 +123,24 @@ parent:
   async RemoveRequestContext(uint64_t rcid);
 
   async PAltDataOutputStream(nsCString type, PHttpChannel channel);
 
   async PStunAddrsRequest();
 
   prio(high) async NotifyCurrentTopLevelOuterContentWindowId(uint64_t windowId);
 
+  /**
+   * WebExtension-specific remote resource loading
+   */
+  async GetExtensionStream(URIParams uri, LoadInfoArgs loadInfo) returns
+                          (OptionalIPCStream stream);
+  async GetExtensionFD(URIParams uri, OptionalLoadInfoArgs loadInfo) returns
+                      (FileDescriptor fd);
+
 child:
   /*
    * Bring up the http auth prompt for a nested remote mozbrowser.
    * NestedFrameId is the id corresponding to the PBrowser.  It is the same id
    * that was passed to the PBrowserOrId param in to the PHttpChannel constructor
    */
   async AsyncAuthPromptForNestedFrame(TabId nestedFrameId, nsCString uri,
                                       nsString realm, uint64_t callbackId);
--- a/netwerk/protocol/res/ExtensionProtocolHandler.cpp
+++ b/netwerk/protocol/res/ExtensionProtocolHandler.cpp
@@ -1,39 +1,359 @@
 /* -*- 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 "ExtensionProtocolHandler.h"
 
+#include "mozilla/AbstractThread.h"
+#include "mozilla/ClearOnShutdown.h"
 #include "mozilla/ExtensionPolicyService.h"
+#include "mozilla/FileUtils.h"
+#include "mozilla/ipc/IPCStreamUtils.h"
+#include "mozilla/ipc/URIParams.h"
+#include "mozilla/ipc/URIUtils.h"
+#include "mozilla/net/NeckoChild.h"
+#include "mozilla/RefPtr.h"
+
+#include "FileDescriptor.h"
+#include "FileDescriptorFile.h"
+#include "LoadInfo.h"
+#include "nsContentUtils.h"
 #include "nsServiceManagerUtils.h"
+#include "nsIFile.h"
+#include "nsIFileChannel.h"
+#include "nsIFileStreams.h"
+#include "nsIFileURL.h"
+#include "nsIJARChannel.h"
 #include "nsIURL.h"
 #include "nsIChannel.h"
+#include "nsIInputStreamPump.h"
+#include "nsIJARURI.h"
 #include "nsIStreamListener.h"
+#include "nsIThread.h"
 #include "nsIInputStream.h"
 #include "nsIOutputStream.h"
 #include "nsIStreamConverterService.h"
 #include "nsNetUtil.h"
-#include "LoadInfo.h"
+#include "prio.h"
 #include "SimpleChannel.h"
 
+#if defined(XP_WIN)
+#include "nsILocalFileWin.h"
+#endif
+
+#define EXTENSION_SCHEME "moz-extension"
+using mozilla::ipc::FileDescriptor;
+using OptionalIPCStream = mozilla::ipc::OptionalIPCStream;
+
 namespace mozilla {
+
+template <>
+class MOZ_MUST_USE_TYPE GenericErrorResult<nsresult>
+{
+  nsresult mErrorValue;
+
+  template<typename V, typename E2> friend class Result;
+
+public:
+  explicit GenericErrorResult(nsresult aErrorValue) : mErrorValue(aErrorValue) {}
+
+  operator nsresult() { return mErrorValue; }
+};
+
 namespace net {
 
 using extensions::URLInfo;
 
+StaticRefPtr<ExtensionProtocolHandler> ExtensionProtocolHandler::sSingleton;
+
+static inline Result<Ok, nsresult>
+WrapNSResult(PRStatus aRv)
+{
+    if (aRv != PR_SUCCESS) {
+        return Err(NS_ERROR_FAILURE);
+    }
+    return Ok();
+}
+
+static inline Result<Ok, nsresult>
+WrapNSResult(nsresult aRv)
+{
+    if (NS_FAILED(aRv)) {
+        return Err(aRv);
+    }
+    return Ok();
+}
+
+#define NS_TRY(expr) MOZ_TRY(WrapNSResult(expr))
+
+/**
+ * Helper class used with SimpleChannel to asynchronously obtain an input
+ * stream or file descriptor from the parent for a remote moz-extension load
+ * from the child.
+ */
+class ExtensionStreamGetter : public RefCounted<ExtensionStreamGetter>
+{
+  public:
+    // To use when getting a remote input stream for a resource
+    // in an unpacked extension.
+    ExtensionStreamGetter(nsIURI* aURI, nsILoadInfo* aLoadInfo)
+      : mURI(aURI)
+      , mLoadInfo(aLoadInfo)
+      , mIsJarChannel(false)
+    {
+      MOZ_ASSERT(aURI);
+      MOZ_ASSERT(aLoadInfo);
+    }
+
+    // To use when getting an FD for a packed extension JAR file
+    // in order to load a resource.
+    ExtensionStreamGetter(nsIURI* aURI, nsILoadInfo* aLoadInfo,
+                          already_AddRefed<nsIJARChannel>&& aJarChannel,
+                          nsIFile* aJarFile)
+      : mURI(aURI)
+      , mLoadInfo(aLoadInfo)
+      , mJarChannel(Move(aJarChannel))
+      , mJarFile(aJarFile)
+      , mIsJarChannel(true)
+    {
+      MOZ_ASSERT(aURI);
+      MOZ_ASSERT(aLoadInfo);
+      MOZ_ASSERT(mJarChannel);
+      MOZ_ASSERT(aJarFile);
+    }
+
+    ~ExtensionStreamGetter() {}
+
+    // Get an input stream or file descriptor from the parent asynchronously.
+    Result<Ok, nsresult> GetAsync(nsIStreamListener* aListener,
+                                  nsIChannel* aChannel);
+
+    // Handle an input stream being returned from the parent
+    void OnStream(nsIInputStream* aStream);
+
+    // Handle file descriptor being returned from the parent
+    void OnFD(const FileDescriptor& aFD);
+
+    MOZ_DECLARE_REFCOUNTED_TYPENAME(ExtensionStreamGetter)
+
+  private:
+    nsCOMPtr<nsIURI> mURI;
+    nsCOMPtr<nsILoadInfo> mLoadInfo;
+    nsCOMPtr<nsIJARChannel> mJarChannel;
+    nsCOMPtr<nsIFile> mJarFile;
+    nsCOMPtr<nsIStreamListener> mListener;
+    nsCOMPtr<nsIChannel> mChannel;
+    bool mIsJarChannel;
+};
+
+class ExtensionJARFileOpener : public nsISupports
+{
+public:
+  ExtensionJARFileOpener(nsIFile* aFile,
+                         NeckoParent::GetExtensionFDResolver& aResolve) :
+    mFile(aFile),
+    mResolve(aResolve)
+  {
+    MOZ_ASSERT(aFile);
+    MOZ_ASSERT(aResolve);
+  }
+
+  NS_IMETHOD OpenFile()
+  {
+    MOZ_ASSERT(!NS_IsMainThread());
+    AutoFDClose prFileDesc;
+
+#if defined(XP_WIN)
+    nsresult rv;
+    nsCOMPtr<nsILocalFileWin> winFile = do_QueryInterface(mFile, &rv);
+    MOZ_ASSERT(winFile);
+    if (NS_SUCCEEDED(rv)) {
+      rv = winFile->OpenNSPRFileDescShareDelete(PR_RDONLY, 0,
+                                                &prFileDesc.rwget());
+    }
+#else
+    nsresult rv = mFile->OpenNSPRFileDesc(PR_RDONLY, 0, &prFileDesc.rwget());
+#endif /* XP_WIN */
+
+    if (NS_SUCCEEDED(rv)) {
+      mFD = FileDescriptor(FileDescriptor::PlatformHandleType(
+                           PR_FileDesc2NativeHandle(prFileDesc)));
+    }
+
+    nsCOMPtr<nsIRunnable> event =
+      mozilla::NewRunnableMethod("ExtensionJarFileFDResolver",
+        this, &ExtensionJARFileOpener::SendBackFD);
+
+    rv = NS_DispatchToMainThread(event, nsIEventTarget::DISPATCH_NORMAL);
+    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToMainThread");
+    return NS_OK;
+  }
+
+  NS_IMETHOD SendBackFD()
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+    mResolve(mFD);
+    return NS_OK;
+  }
+
+  NS_DECL_THREADSAFE_ISUPPORTS
+
+private:
+  virtual ~ExtensionJARFileOpener() {}
+
+  nsCOMPtr<nsIFile> mFile;
+  NeckoParent::GetExtensionFDResolver mResolve;
+  FileDescriptor mFD;
+};
+
+NS_IMPL_ISUPPORTS(ExtensionJARFileOpener, nsISupports)
+
+// The amount of time, in milliseconds, that the file opener thread will remain
+// allocated after it is used. This value chosen because to match other uses
+// of LazyIdleThread.
+#define DEFAULT_THREAD_TIMEOUT_MS 30000
+
+// Request an FD or input stream from the parent.
+Result<Ok, nsresult>
+ExtensionStreamGetter::GetAsync(nsIStreamListener* aListener,
+                                nsIChannel* aChannel)
+{
+  MOZ_ASSERT(IsNeckoChild());
+
+  mListener = aListener;
+  mChannel = aChannel;
+
+  // Serialize the URI to send to parent
+  mozilla::ipc::URIParams uri;
+  SerializeURI(mURI, uri);
+
+  // Serialize the LoadInfo to send to parent
+  OptionalLoadInfoArgs loadInfo;
+  NS_TRY(mozilla::ipc::LoadInfoToLoadInfoArgs(mLoadInfo, &loadInfo));
+
+  RefPtr<ExtensionStreamGetter> self = this;
+  if (mIsJarChannel) {
+    // Request an FD for this moz-extension URI
+    gNeckoChild->SendGetExtensionFD(uri, loadInfo)->Then(
+      AbstractThread::MainThread(),
+      __func__,
+      [self] (const FileDescriptor& fd) {
+        self->OnFD(fd);
+      },
+      [self] (const mozilla::ipc::PromiseRejectReason) {
+        self->OnFD(FileDescriptor());
+      }
+    );
+    return Ok();
+  }
+
+  // Request an input stream for this moz-extension URI
+  gNeckoChild->SendGetExtensionStream(uri, loadInfo)->Then(
+    AbstractThread::MainThread(),
+    __func__,
+    [self] (const OptionalIPCStream& stream) {
+      nsCOMPtr<nsIInputStream> inputStream;
+      if (stream.type() == OptionalIPCStream::OptionalIPCStream::TIPCStream) {
+        inputStream = ipc::DeserializeIPCStream(stream);
+      }
+      self->OnStream(inputStream);
+    },
+    [self] (const mozilla::ipc::PromiseRejectReason) {
+      self->OnStream(nullptr);
+    }
+  );
+  return Ok();
+}
+
+// Handle an input stream sent from the parent.
+void
+ExtensionStreamGetter::OnStream(nsIInputStream* aStream)
+{
+  MOZ_ASSERT(IsNeckoChild());
+  MOZ_ASSERT(mListener);
+
+  // We must keep an owning reference to the listener
+  // until we pass it on to AsyncRead.
+  nsCOMPtr<nsIStreamListener> listener = mListener.forget();
+
+  MOZ_ASSERT(mChannel);
+
+  if (!aStream) {
+    // The parent didn't send us back a stream.
+    listener->OnStartRequest(mChannel, nullptr);
+    listener->OnStopRequest(mChannel, nullptr, NS_ERROR_FILE_ACCESS_DENIED);
+    mChannel->Cancel(NS_BINDING_ABORTED);
+    return;
+  }
+
+  nsCOMPtr<nsIInputStreamPump> pump;
+  nsresult rv = NS_NewInputStreamPump(getter_AddRefs(pump), aStream);
+  if (NS_FAILED(rv)) {
+    mChannel->Cancel(NS_BINDING_ABORTED);
+    return;
+  }
+
+  rv = pump->AsyncRead(listener, nullptr);
+  if (NS_FAILED(rv)) {
+    mChannel->Cancel(NS_BINDING_ABORTED);
+  }
+}
+
+// Handle an FD sent from the parent.
+void
+ExtensionStreamGetter::OnFD(const FileDescriptor& aFD)
+{
+  MOZ_ASSERT(IsNeckoChild());
+  MOZ_ASSERT(mListener);
+  MOZ_ASSERT(mChannel);
+
+  if (!aFD.IsValid()) {
+    OnStream(nullptr);
+    return;
+  }
+
+  // We must keep an owning reference to the listener
+  // until we pass it on to AsyncOpen2.
+  nsCOMPtr<nsIStreamListener> listener = mListener.forget();
+
+  RefPtr<FileDescriptorFile> fdFile = new FileDescriptorFile(aFD, mJarFile);
+  mJarChannel->SetJarFile(fdFile);
+  nsresult rv = mJarChannel->AsyncOpen2(listener);
+  if (NS_FAILED(rv)) {
+    mChannel->Cancel(NS_BINDING_ABORTED);
+  }
+}
+
 NS_IMPL_QUERY_INTERFACE(ExtensionProtocolHandler, nsISubstitutingProtocolHandler,
                         nsIProtocolHandler, nsIProtocolHandlerWithDynamicFlags,
                         nsISupportsWeakReference)
 NS_IMPL_ADDREF_INHERITED(ExtensionProtocolHandler, SubstitutingProtocolHandler)
 NS_IMPL_RELEASE_INHERITED(ExtensionProtocolHandler, SubstitutingProtocolHandler)
 
+already_AddRefed<ExtensionProtocolHandler>
+ExtensionProtocolHandler::GetSingleton()
+{
+  if (!sSingleton) {
+    sSingleton = new ExtensionProtocolHandler();
+    ClearOnShutdown(&sSingleton);
+  }
+  return do_AddRef(sSingleton.get());
+}
+
+ExtensionProtocolHandler::ExtensionProtocolHandler()
+  : SubstitutingProtocolHandler(EXTENSION_SCHEME)
+{
+  mUseRemoteFileChannels = IsNeckoChild() &&
+    Preferences::GetBool("extensions.webextensions.protocol.remote");
+}
+
 static inline ExtensionPolicyService&
 EPS()
 {
   return ExtensionPolicyService::GetSingleton();
 }
 
 nsresult
 ExtensionProtocolHandler::GetFlagsForURI(nsIURI* aURI, uint32_t* aFlags)
@@ -72,36 +392,68 @@ ExtensionProtocolHandler::ResolveSpecial
   if (aPathname.EqualsLiteral("/_generated_background_page.html")) {
     Unused << EPS().GetGeneratedBackgroundPageUrl(aHost, aResult);
     return !aResult.IsEmpty();
   }
 
   return false;
 }
 
-static inline Result<Ok, nsresult>
-WrapNSResult(nsresult aRv)
+// For file or JAR URI's, substitute in a remote channel.
+Result<Ok, nsresult>
+ExtensionProtocolHandler::SubstituteRemoteChannel(nsIURI* aURI,
+                                                  nsILoadInfo* aLoadInfo,
+                                                  nsIChannel** aRetVal)
 {
-  if (NS_FAILED(aRv)) {
-    return Err(aRv);
+  MOZ_ASSERT(IsNeckoChild());
+  NS_TRY(aURI ? NS_OK : NS_ERROR_INVALID_ARG);
+  NS_TRY(aLoadInfo ? NS_OK : NS_ERROR_INVALID_ARG);
+
+  nsAutoCString unResolvedSpec;
+  NS_TRY(aURI->GetSpec(unResolvedSpec));
+
+  nsAutoCString resolvedSpec;
+  NS_TRY(ResolveURI(aURI, resolvedSpec));
+
+  // Use the target URI scheme to determine if this is a packed or unpacked
+  // extension URI. For unpacked extensions, we'll request an input stream
+  // from the parent. For a packed extension, we'll request a file descriptor
+  // for the JAR file.
+  nsAutoCString scheme;
+  NS_TRY(net_ExtractURLScheme(resolvedSpec, scheme));
+
+  if (scheme.EqualsLiteral("file")) {
+    // Unpacked extension
+    SubstituteRemoteFileChannel(aURI, aLoadInfo, resolvedSpec, aRetVal);
+    return Ok();
   }
+
+  if (scheme.EqualsLiteral("jar")) {
+    // Packed extension
+    return SubstituteRemoteJarChannel(aURI, aLoadInfo, resolvedSpec, aRetVal);
+  }
+
+  // Only unpacked resource files and JAR files are remoted.
+  // No other moz-extension loads should be reading from the filesystem.
   return Ok();
 }
 
-#define NS_TRY(expr) MOZ_TRY(WrapNSResult(expr))
-
 nsresult
 ExtensionProtocolHandler::SubstituteChannel(nsIURI* aURI,
                                             nsILoadInfo* aLoadInfo,
                                             nsIChannel** result)
 {
   nsresult rv;
   nsCOMPtr<nsIURL> url = do_QueryInterface(aURI, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
+  if (mUseRemoteFileChannels) {
+    MOZ_TRY(SubstituteRemoteChannel(aURI, aLoadInfo, result));
+  }
+
   nsAutoCString ext;
   rv = url->GetFileExtension(ext);
   NS_ENSURE_SUCCESS(rv, rv);
 
   if (!ext.LowerCaseEqualsLiteral("css")) {
     return NS_OK;
   }
 
@@ -141,12 +493,247 @@ ExtensionProtocolHandler::SubstituteChan
     (*result)->SetLoadInfo(loadInfo);
   }
 
   channel.swap(*result);
 
   return NS_OK;
 }
 
+Result<nsCOMPtr<nsIInputStream>, nsresult>
+ExtensionProtocolHandler::NewStream(nsIURI* aChildURI,
+                                    nsILoadInfo* aChildLoadInfo,
+                                    bool* aTerminateSender)
+{
+  MOZ_ASSERT(!IsNeckoChild());
+  NS_TRY(aChildURI ? NS_OK : NS_ERROR_INVALID_ARG);
+  NS_TRY(aChildLoadInfo ? NS_OK : NS_ERROR_INVALID_ARG);
+  NS_TRY(aTerminateSender ? NS_OK : NS_ERROR_INVALID_ARG);
+
+  *aTerminateSender = true;
+  nsresult rv;
+
+  // We should never receive a URI that isn't for a moz-extension because
+  // these requests ordinarily come from the child's ExtensionProtocolHandler.
+  // Ensure this request is for a moz-extension URI. A rogue child process
+  // could send us any URI.
+  bool isExtScheme = false;
+  if (NS_FAILED(aChildURI->SchemeIs(EXTENSION_SCHEME, &isExtScheme)) ||
+      !isExtScheme) {
+    return Err(NS_ERROR_UNKNOWN_PROTOCOL);
+  }
+
+  // For errors after this point, we want to propagate the error to
+  // the child, but we don't force the child to be terminated because
+  // the error is likely to be due to a bug in the extension.
+  *aTerminateSender = false;
+
+  /*
+   * Make sure there is a substitution installed for the host found
+   * in the child's request URI and make sure the host resolves to
+   * a directory.
+   */
+
+  nsAutoCString host;
+  NS_TRY(aChildURI->GetAsciiHost(host));
+
+  // Lookup the directory this host string resolves to
+  nsCOMPtr<nsIURI> baseURI;
+  NS_TRY(GetSubstitution(host, getter_AddRefs(baseURI)));
+
+  // The result should be a file URL for the extension base dir
+  nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(baseURI, &rv);
+  NS_TRY(rv);
+
+  nsCOMPtr<nsIFile> extensionDir;
+  NS_TRY(fileURL->GetFile(getter_AddRefs(extensionDir)));
+
+  bool isDirectory = false;
+  NS_TRY(extensionDir->IsDirectory(&isDirectory));
+  if (!isDirectory) {
+    // The host should map to a directory for unpacked extensions
+    return Err(NS_ERROR_FILE_NOT_DIRECTORY);
+  }
+
+  /*
+   * Now get a channel for the resolved child URI and make sure the
+   * channel is a file channel.
+   */
+
+  nsCOMPtr<nsIPrincipal> childPrincipal;
+  NS_TRY(aChildLoadInfo->GetLoadingPrincipal(getter_AddRefs(childPrincipal)));
+  if (nsContentUtils::IsSystemPrincipal(childPrincipal)) {
+    return Err(NS_ERROR_FILE_ACCESS_DENIED);
+  }
+
+  nsCOMPtr<nsIChannel> channel;
+  NS_TRY(NS_NewChannelInternal(getter_AddRefs(channel),
+                               aChildURI,
+                               aChildLoadInfo));
+
+  // Channel should be a file channel. It should never be a JAR
+  // channel because we only request remote streams for unpacked
+  // extension resource loads where the URI resolves to a file.
+  nsCOMPtr<nsIFileChannel> fileChannel = do_QueryInterface(channel, &rv);
+  NS_TRY(rv);
+
+  nsCOMPtr<nsIFile> requestedFile;
+  NS_TRY(fileChannel->GetFile(getter_AddRefs(requestedFile)));
+
+  /*
+   * Make sure the file we resolved to is within the extension directory.
+   */
+
+  // Normalize paths for sane comparisons. nsIFile::Contains depends on
+  // it for reliable subpath checks.
+  NS_TRY(extensionDir->Normalize());
+  NS_TRY(requestedFile->Normalize());
+
+  bool isResourceFromExtensionDir = false;
+  NS_TRY(extensionDir->Contains(requestedFile, &isResourceFromExtensionDir));
+  if (!isResourceFromExtensionDir) {
+    return Err(NS_ERROR_FILE_ACCESS_DENIED);
+  }
+
+  nsCOMPtr<nsIInputStream> inputStream;
+  NS_TRY(NS_NewLocalFileInputStream(getter_AddRefs(inputStream),
+                                    requestedFile,
+                                    PR_RDONLY,
+                                    -1,
+                                    nsIFileInputStream::DEFER_OPEN));
+
+  return inputStream;
+}
+
+Result<Ok, nsresult>
+ExtensionProtocolHandler::NewFD(nsIURI* aChildURI,
+                                nsILoadInfo* aChildLoadInfo,
+                                bool* aTerminateSender,
+                                NeckoParent::GetExtensionFDResolver& aResolve)
+{
+  MOZ_ASSERT(!IsNeckoChild());
+  NS_TRY(aChildURI ? NS_OK : NS_ERROR_INVALID_ARG);
+  NS_TRY(aChildLoadInfo ? NS_OK : NS_ERROR_INVALID_ARG);
+  NS_TRY(aTerminateSender ? NS_OK : NS_ERROR_INVALID_ARG);
+
+  *aTerminateSender = true;
+  nsresult rv;
+
+  // Ensure this is a moz-extension URI
+  bool isExtScheme = false;
+  if (NS_FAILED(aChildURI->SchemeIs(EXTENSION_SCHEME, &isExtScheme)) ||
+      !isExtScheme) {
+    return Err(NS_ERROR_UNKNOWN_PROTOCOL);
+  }
+
+  // For errors after this point, we want to propagate the error to
+  // the child, but we don't force the child to be terminated.
+  *aTerminateSender = false;
+
+  nsAutoCString host;
+  NS_TRY(aChildURI->GetAsciiHost(host));
+
+  // We expect the host string to map to a JAR file because the URI
+  // should refer to a web accessible resource for an enabled extension.
+  nsCOMPtr<nsIURI> subURI;
+  NS_TRY(GetSubstitution(host, getter_AddRefs(subURI)));
+
+  nsCOMPtr<nsIJARURI> jarURI = do_QueryInterface(subURI, &rv);
+  NS_TRY(rv);
+
+  nsCOMPtr<nsIURI> innerFileURI;
+  NS_TRY(jarURI->GetJARFile(getter_AddRefs(innerFileURI)));
+
+  nsCOMPtr<nsIFileURL> innerFileURL = do_QueryInterface(innerFileURI, &rv);
+  NS_TRY(rv);
+
+  nsCOMPtr<nsIFile> jarFile;
+  NS_TRY(innerFileURL->GetFile(getter_AddRefs(jarFile)));
+
+  if (!mFileOpenerThread) {
+    mFileOpenerThread =
+      new LazyIdleThread(DEFAULT_THREAD_TIMEOUT_MS,
+                         NS_LITERAL_CSTRING("ExtensionProtocolHandler"));
+  }
+
+  RefPtr<ExtensionJARFileOpener> fileOpener =
+    new ExtensionJARFileOpener(jarFile, aResolve);
+
+  nsCOMPtr<nsIRunnable> event =
+    mozilla::NewRunnableMethod("ExtensionJarFileOpener",
+        fileOpener, &ExtensionJARFileOpener::OpenFile);
+
+  NS_TRY(mFileOpenerThread->Dispatch(event, nsIEventTarget::DISPATCH_NORMAL));
+
+  return Ok();
+}
+
+static void
+NewSimpleChannel(nsIURI* aURI,
+                 nsILoadInfo* aLoadinfo,
+                 ExtensionStreamGetter* aStreamGetter,
+                 nsIChannel** aRetVal)
+{
+  nsCOMPtr<nsIChannel> channel = NS_NewSimpleChannel(
+    aURI, aLoadinfo, aStreamGetter,
+    [] (nsIStreamListener* listener, nsIChannel* channel,
+        ExtensionStreamGetter* getter) -> RequestOrReason {
+      MOZ_TRY(getter->GetAsync(listener, channel));
+      return RequestOrReason(nullptr);
+
+    });
+  channel.swap(*aRetVal);
+}
+
+void
+ExtensionProtocolHandler::SubstituteRemoteFileChannel(nsIURI* aURI,
+                                                      nsILoadInfo* aLoadinfo,
+                                                      nsACString& aResolvedFileSpec,
+                                                      nsIChannel** aRetVal)
+{
+  MOZ_ASSERT(IsNeckoChild());
+
+  RefPtr<ExtensionStreamGetter> streamGetter =
+    new ExtensionStreamGetter(aURI, aLoadinfo);
+
+  NewSimpleChannel(aURI, aLoadinfo, streamGetter, aRetVal);
+}
+
+Result<Ok, nsresult>
+ExtensionProtocolHandler::SubstituteRemoteJarChannel(nsIURI* aURI,
+                                                     nsILoadInfo* aLoadinfo,
+                                                     nsACString& aResolvedSpec,
+                                                     nsIChannel** aRetVal)
+{
+  MOZ_ASSERT(IsNeckoChild());
+  nsresult rv;
+
+  // Build a JAR URI for this jar:file:// URI and use it to extract the
+  // inner file URI.
+  nsCOMPtr<nsIURI> uri;
+  NS_TRY(NS_NewURI(getter_AddRefs(uri), aResolvedSpec));
+
+  nsCOMPtr<nsIJARURI> jarURI = do_QueryInterface(uri, &rv);
+  NS_TRY(rv);
+
+  nsCOMPtr<nsIURI> innerFileURI;
+  NS_TRY(jarURI->GetJARFile(getter_AddRefs(innerFileURI)));
+
+  nsCOMPtr<nsIFileURL> innerFileURL = do_QueryInterface(innerFileURI, &rv);
+  NS_TRY(rv);
+
+  nsCOMPtr<nsIFile> jarFile;
+  NS_TRY(innerFileURL->GetFile(getter_AddRefs(jarFile)));
+
+  nsCOMPtr<nsIJARChannel> jarChannel = do_QueryInterface(*aRetVal, &rv);
+  NS_TRY(rv);
+
+  RefPtr<ExtensionStreamGetter> streamGetter =
+    new ExtensionStreamGetter(aURI, aLoadinfo, jarChannel.forget(), jarFile);
+
+  NewSimpleChannel(aURI, aLoadinfo, streamGetter, aRetVal);
+  return Ok();
+}
+
 #undef NS_TRY
 
 } // namespace net
 } // namespace mozilla
--- a/netwerk/protocol/res/ExtensionProtocolHandler.h
+++ b/netwerk/protocol/res/ExtensionProtocolHandler.h
@@ -1,48 +1,167 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
 /* 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 ExtensionProtocolHandler_h___
 #define ExtensionProtocolHandler_h___
 
+#include "mozilla/net/NeckoParent.h"
+#include "mozilla/LazyIdleThread.h"
 #include "SubstitutingProtocolHandler.h"
-#include "nsWeakReference.h"
 
 namespace mozilla {
 namespace net {
 
 class ExtensionProtocolHandler final : public nsISubstitutingProtocolHandler,
                                        public nsIProtocolHandlerWithDynamicFlags,
                                        public SubstitutingProtocolHandler,
                                        public nsSupportsWeakReference
 {
 public:
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_NSIPROTOCOLHANDLERWITHDYNAMICFLAGS
   NS_FORWARD_NSIPROTOCOLHANDLER(SubstitutingProtocolHandler::)
   NS_FORWARD_NSISUBSTITUTINGPROTOCOLHANDLER(SubstitutingProtocolHandler::)
 
-  ExtensionProtocolHandler() : SubstitutingProtocolHandler("moz-extension") {}
+  static already_AddRefed<ExtensionProtocolHandler> GetSingleton();
+
+  /**
+   * To be called in the parent process to obtain an input stream for a
+   * a web accessible resource from an unpacked WebExtension dir.
+   *
+   * @param aChildURI a moz-extension URI sent from the child that refers
+   *        to a web accessible resource file in an enabled unpacked extension
+   * @param aChildLoadInfo the loadinfo for the request sent from the child
+   * @param aTerminateSender out param set to true when the params are invalid
+   *        and indicate the child should be terminated. If |aChildURI| is
+   *        not a moz-extension URI, the child is in an invalid state and
+   *        should be terminated.
+   * @return NS_OK with |aTerminateSender| set to false on success. On
+   *         failure, returns an error and sets |aTerminateSender| to indicate
+   *         whether or not the child process should be terminated.
+   *         A moz-extension URI from the child that doesn't resolve to a
+   *         resource file within the extension could be the result of a bug
+   *         in the extension and doesn't result in |aTerminateSender| being
+   *         set to true.
+   */
+  Result<nsCOMPtr<nsIInputStream>, nsresult> NewStream(nsIURI* aChildURI,
+                                                       nsILoadInfo* aChildLoadInfo,
+                                                       bool* aTerminateSender);
+
+  /**
+   * To be called in the parent process to obtain a file descriptor for an
+   * enabled WebExtension JAR file.
+   *
+   * @param aChildURI a moz-extension URI sent from the child that refers
+   *        to a web accessible resource file in an enabled unpacked extension
+   * @param aChildLoadInfo the loadinfo for the request sent from the child
+   * @param aTerminateSender out param set to true when the params are invalid
+   *        and indicate the child should be terminated. If |aChildURI| is
+   *        not a moz-extension URI, the child is in an invalid state and
+   *        should be terminated.
+   * @param aPromise a promise that will be resolved asynchronously when the
+   *        file descriptor is available.
+   * @return NS_OK with |aTerminateSender| set to false on success. On
+   *         failure, returns an error and sets |aTerminateSender| to indicate
+   *         whether or not the child process should be terminated.
+   *         A moz-extension URI from the child that doesn't resolve to an
+   *         enabled WebExtension JAR could be the result of a bug in the
+   *         extension and doesn't result in |aTerminateSender| being
+   *         set to true.
+   */
+  Result<Ok, nsresult> NewFD(nsIURI* aChildURI,
+                             nsILoadInfo* aChildLoadInfo,
+                             bool* aTerminateSender,
+                             NeckoParent::GetExtensionFDResolver& aResolve);
 
 protected:
   ~ExtensionProtocolHandler() {}
 
+private:
+  explicit ExtensionProtocolHandler();
+
   MOZ_MUST_USE bool ResolveSpecialCases(const nsACString& aHost,
                                         const nsACString& aPath,
                                         const nsACString& aPathname,
                                         nsACString& aResult) override;
 
   // |result| is an inout param.  On entry to this function, *result
   // is expected to be non-null and already addrefed.  This function
   // may release the object stored in *result on entry and write
   // a new pointer to an already addrefed channel to *result.
   virtual MOZ_MUST_USE nsresult SubstituteChannel(nsIURI* uri,
                                                   nsILoadInfo* aLoadInfo,
                                                   nsIChannel** result) override;
+
+  /**
+   * For moz-extension URI's that resolve to file or JAR URI's, replaces
+   * the provided channel with a channel that will proxy the load to the
+   * parent process. For moz-extension URI's that resolve to other types
+   * of URI's (not file or JAR), the provide channel is not replaced and
+   * NS_OK is returned.
+   *
+   * @param aURI the moz-extension URI
+   * @param aLoadInfo the loadinfo for the request
+   * @param aRetVal in/out channel param referring to the channel that
+   *        might need to be substituted with a remote channel.
+   * @return NS_OK if the channel does not need to be substituted or
+   *         or the replacement channel was created successfully.
+   *         Otherwise returns an error.
+   */
+  Result<Ok, nsresult> SubstituteRemoteChannel(nsIURI* aURI,
+                                               nsILoadInfo* aLoadInfo,
+                                               nsIChannel** aRetVal);
+
+  /**
+   * Replaces a file channel with a remote file channel for loading a
+   * web accessible resource for an unpacked extension from the parent.
+   *
+   * @param aURI the moz-extension URI
+   * @param aLoadInfo the loadinfo for the request
+   * @param aResolvedFileSpec the resolved URI spec for the file.
+   * @param aRetVal in/out param referring to the new remote channel.
+   *        The reference to the input param file channel is dropped and
+   *        replaced with a reference to a new channel that remotes
+   *        the file access. The new channel encapsulates a request to
+   *        the parent for an IPCStream for the file.
+   */
+  void SubstituteRemoteFileChannel(nsIURI* aURI,
+                                   nsILoadInfo* aLoadinfo,
+                                   nsACString& aResolvedFileSpec,
+                                   nsIChannel** aRetVal);
+
+  /**
+   * Replaces a JAR channel with a remote JAR channel for loading a
+   * an extension JAR file from the parent.
+   *
+   * @param aURI the moz-extension URI
+   * @param aLoadInfo the loadinfo for the request
+   * @param aResolvedFileSpec the resolved URI spec for the file.
+   * @param aRetVal in/out param referring to the new remote channel.
+   *        The input param JAR channel is replaced with a new channel
+   *        that remotes the JAR file access. The new channel encapsulates
+   *        a request to the parent for the JAR file FD.
+   */
+  Result<Ok, nsresult> SubstituteRemoteJarChannel(nsIURI* aURI,
+                                                  nsILoadInfo* aLoadinfo,
+                                                  nsACString& aResolvedSpec,
+                                                  nsIChannel** aRetVal);
+
+  // Used for opening JAR files off the main thread when we just need to
+  // obtain a file descriptor to send back to the child.
+  RefPtr<mozilla::LazyIdleThread> mFileOpenerThread;
+
+  // To allow parent IPDL actors to invoke methods on this handler when
+  // handling moz-extension requests from the child.
+  static StaticRefPtr<ExtensionProtocolHandler> sSingleton;
+
+  // Set to true when this instance of the handler must proxy loads of
+  // extension web-accessible resources to the parent process.
+  bool mUseRemoteFileChannels;
 };
 
 } // namespace net
 } // namespace mozilla
 
 #endif /* ExtensionProtocolHandler_h___ */
--- a/netwerk/protocol/res/moz.build
+++ b/netwerk/protocol/res/moz.build
@@ -6,16 +6,21 @@
 
 XPIDL_SOURCES += [
     'nsIResProtocolHandler.idl',
     'nsISubstitutingProtocolHandler.idl',
 ]
 
 XPIDL_MODULE = 'necko_res'
 
+EXPORTS.mozilla.net += [
+    'ExtensionProtocolHandler.h',
+    'SubstitutingProtocolHandler.h',
+]
+
 UNIFIED_SOURCES += [
     'ExtensionProtocolHandler.cpp',
     'nsResProtocolHandler.cpp',
     'SubstitutingProtocolHandler.cpp',
 ]
 
 include('/ipc/chromium/chromium-config.mozbuild')
 
new file mode 100644
--- /dev/null
+++ b/xpcom/io/FileDescriptorFile.cpp
@@ -0,0 +1,486 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et 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 "FileDescriptorFile.h"
+
+#include "mozilla/ipc/FileDescriptorUtils.h"
+#include "mozilla/ipc/URIUtils.h"
+#include "mozilla/net/NeckoChild.h"
+#include "nsIFileURL.h"
+#include "nsNetUtil.h"
+#include "nsProxyRelease.h"
+#include "nsThreadUtils.h"
+#include "private/pprio.h"
+#include "SerializedLoadContext.h"
+
+namespace mozilla {
+namespace net {
+
+NS_IMPL_ISUPPORTS(FileDescriptorFile, nsIFile)
+
+LazyLogModule gFDFileLog("FDFile");
+#undef DBG
+#define DBG(...) MOZ_LOG(gFDFileLog, LogLevel::Debug, (__VA_ARGS__))
+
+FileDescriptorFile::FileDescriptorFile(const FileDescriptor& aFD,
+                                       nsIFile* aFile)
+{
+  MOZ_ASSERT(aFD.IsValid());
+  auto platformHandle = aFD.ClonePlatformHandle();
+  mFD = FileDescriptor(platformHandle.get());
+  mFile = aFile;
+}
+
+FileDescriptorFile::FileDescriptorFile(const FileDescriptorFile& aOther)
+{
+  auto platformHandle = aOther.mFD.ClonePlatformHandle();
+  mFD = FileDescriptor(platformHandle.get());
+  aOther.mFile->Clone(getter_AddRefs(mFile));
+}
+
+//-----------------------------------------------------------------------------
+// FileDescriptorFile::nsIFile functions that we override logic for
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+FileDescriptorFile::Clone(nsIFile **aFileOut)
+{
+  RefPtr<FileDescriptorFile> fdFile = new FileDescriptorFile(*this);
+  fdFile.forget(aFileOut);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::OpenNSPRFileDesc(int32_t aFlags, int32_t aMode,
+                                     PRFileDesc **aRetval)
+{
+  // Remove optional OS_READAHEAD flag so we test against PR_RDONLY
+  aFlags &= ~nsIFile::OS_READAHEAD;
+
+  // Remove optional/deprecated DELETE_ON_CLOSE flag
+  aFlags &= ~nsIFile::DELETE_ON_CLOSE;
+
+  // All other flags require write access to the file and
+  // this implementation only provides read access.
+  if (aFlags != PR_RDONLY) {
+    DBG("OpenNSPRFileDesc flags error (%" PRIu32 ")\n", aFlags);
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  if (!mFD.IsValid()) {
+    DBG("OpenNSPRFileDesc error: no file descriptor\n");
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  auto platformHandle = mFD.ClonePlatformHandle();
+  *aRetval = PR_ImportFile(PROsfd(platformHandle.release()));
+
+  if (!*aRetval) {
+    DBG("OpenNSPRFileDesc Clone failure\n");
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// FileDescriptorFile::nsIFile functions that we delegate to underlying nsIFile
+//-----------------------------------------------------------------------------
+
+nsresult
+FileDescriptorFile::GetLeafName(nsAString &aLeafName)
+{
+  return mFile->GetLeafName(aLeafName);
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::GetNativeLeafName(nsACString &aLeafName)
+{
+  return mFile->GetNativeLeafName(aLeafName);
+}
+
+nsresult
+FileDescriptorFile::GetTarget(nsAString &_retval)
+{
+  return mFile->GetTarget(_retval);
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::GetNativeTarget(nsACString &_retval)
+{
+  return mFile->GetNativeTarget(_retval);
+}
+
+nsresult
+FileDescriptorFile::GetPath(nsAString &_retval)
+{
+  return mFile->GetPath(_retval);
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::GetNativePath(nsACString &_retval)
+{
+  return mFile->GetNativePath(_retval);
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::Equals(nsIFile *inFile, bool *_retval)
+{
+  return mFile->Equals(inFile, _retval);
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::Contains(nsIFile *inFile, bool *_retval)
+{
+  return mFile->Contains(inFile, _retval);
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::GetParent(nsIFile **aParent)
+{
+  return mFile->GetParent(aParent);
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::GetFollowLinks(bool *aFollowLinks)
+{
+  return mFile->GetFollowLinks(aFollowLinks);
+}
+
+//-----------------------------------------------------------------------------
+// FileDescriptorFile::nsIFile functions that are not currently supported
+//-----------------------------------------------------------------------------
+
+nsresult
+FileDescriptorFile::Append(const nsAString &node)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::AppendNative(const nsACString &fragment)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::Normalize()
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::Create(uint32_t type, uint32_t permissions)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult
+FileDescriptorFile::SetLeafName(const nsAString &aLeafName)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::SetNativeLeafName(const nsACString &aLeafName)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult
+FileDescriptorFile::InitWithPath(const nsAString &filePath)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::InitWithNativePath(const nsACString &filePath)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::InitWithFile(nsIFile *aFile)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::SetFollowLinks(bool aFollowLinks)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult
+FileDescriptorFile::AppendRelativePath(const nsAString &node)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::AppendRelativeNativePath(const nsACString &fragment)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::GetPersistentDescriptor(nsACString &aPersistentDescriptor)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::SetPersistentDescriptor(const nsACString &aPersistentDescriptor)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::GetRelativeDescriptor(nsIFile *fromFile, nsACString& _retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::SetRelativeDescriptor(nsIFile *fromFile,
+                                   const nsACString& relativeDesc)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::GetRelativePath(nsIFile *fromFile, nsACString& _retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::SetRelativePath(nsIFile *fromFile,
+                                     const nsACString& relativePath)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult
+FileDescriptorFile::CopyTo(nsIFile *newParentDir, const nsAString &newName)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::CopyToNative(nsIFile *newParent, const nsACString &newName)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult
+FileDescriptorFile::CopyToFollowingLinks(nsIFile *newParentDir,
+                                  const nsAString &newName)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::CopyToFollowingLinksNative(nsIFile *newParent,
+                                        const nsACString &newName)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult
+FileDescriptorFile::MoveTo(nsIFile *newParentDir, const nsAString &newName)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::MoveToNative(nsIFile *newParent, const nsACString &newName)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::RenameTo(nsIFile *newParentDir, const nsAString &newName)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::RenameToNative(nsIFile *newParentDir, const nsACString &newName)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::Remove(bool recursive)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::GetPermissions(uint32_t *aPermissions)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::SetPermissions(uint32_t aPermissions)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::GetPermissionsOfLink(uint32_t *aPermissionsOfLink)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::SetPermissionsOfLink(uint32_t aPermissions)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::GetLastModifiedTime(PRTime *aLastModTime)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::SetLastModifiedTime(PRTime aLastModTime)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::GetLastModifiedTimeOfLink(PRTime *aLastModTimeOfLink)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::SetLastModifiedTimeOfLink(PRTime aLastModTimeOfLink)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::GetFileSize(int64_t *aFileSize)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::SetFileSize(int64_t aFileSize)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::GetFileSizeOfLink(int64_t *aFileSize)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::Exists(bool *_retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::IsWritable(bool *_retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::IsReadable(bool *_retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::IsExecutable(bool *_retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::IsHidden(bool *_retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::IsDirectory(bool *_retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::IsFile(bool *_retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::IsSymlink(bool *_retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::IsSpecial(bool *_retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::CreateUnique(uint32_t type, uint32_t attributes)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::GetDirectoryEntries(nsISimpleEnumerator **entries)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::OpenANSIFileDesc(const char *mode, FILE **_retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::Load(PRLibrary **_retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::GetDiskSpaceAvailable(int64_t *aDiskSpaceAvailable)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::Reveal()
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorFile::Launch()
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+} // namespace net
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/xpcom/io/FileDescriptorFile.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et 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 _FileDescriptorFile_h
+#define _FileDescriptorFile_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/ipc/FileDescriptor.h"
+#include "nsIFile.h"
+#include "nsIFileURL.h"
+#include "nsIURI.h"
+#include "private/pprio.h"
+
+namespace mozilla {
+namespace net {
+
+/**
+ * A limited implementation of nsIFile that wraps a FileDescriptor object
+ * allowing the file to be read from. Added to allow a child process to use
+ * an nsIFile object for a file it does not have access to on the filesystem
+ * but has been provided a FileDescriptor for from the parent. Many nsIFile
+ * methods are not implemented and this is not intended to be a general
+ * purpose file implementation.
+ */
+class FileDescriptorFile final : public nsIFile
+{
+  typedef mozilla::ipc::FileDescriptor FileDescriptor;
+
+public:
+  FileDescriptorFile(const FileDescriptor& aFD, nsIFile* aFile);
+
+  NS_DECL_THREADSAFE_ISUPPORTS
+  NS_DECL_NSIFILE
+
+private:
+  ~FileDescriptorFile()
+  {}
+
+  FileDescriptorFile(const FileDescriptorFile& other);
+
+  // regular nsIFile object, that we forward most calls to.
+  nsCOMPtr<nsIFile> mFile;
+  FileDescriptor mFD;
+};
+
+} // namespace net
+} // namespace mozilla
+
+#endif // _FileDescriptorFile_h
--- a/xpcom/io/moz.build
+++ b/xpcom/io/moz.build
@@ -56,16 +56,17 @@ else:
     EXPORTS += ['nsLocalFileUnix.h']
     SOURCES += [
         'nsLocalFileUnix.cpp',
     ]
 
 XPIDL_MODULE = 'xpcom_io'
 
 EXPORTS += [
+    'FileDescriptorFile.h',
     'nsAnonymousTemporaryFile.h',
     'nsAppDirectoryServiceDefs.h',
     'nsDirectoryService.h',
     'nsDirectoryServiceAtomList.h',
     'nsDirectoryServiceDefs.h',
     'nsDirectoryServiceUtils.h',
     'nsEscape.h',
     'nsLinebreakConverter.h',
@@ -87,16 +88,17 @@ EXPORTS.mozilla += [
     'SnappyCompressOutputStream.h',
     'SnappyFrameUtils.h',
     'SnappyUncompressInputStream.h',
 ]
 
 UNIFIED_SOURCES += [
     'Base64.cpp',
     'crc32c.c',
+    'FileDescriptorFile.cpp',
     'nsAnonymousTemporaryFile.cpp',
     'nsAppFileLocationProvider.cpp',
     'nsBinaryStream.cpp',
     'nsDirectoryService.cpp',
     'nsEscape.cpp',
     'nsInputStreamTee.cpp',
     'nsIOUtil.cpp',
     'nsLinebreakConverter.cpp',