Bug 1263224: Adds COM interception that allows us to proxy COM invocations; r?jimm draft
authorAaron Klotz <aklotz@mozilla.com>
Wed, 27 Jul 2016 11:44:56 -0600
changeset 393337 452d849ba4812ebf1111c1bddfa49fc49613e498
parent 393336 5f5bd38f4d3bd68bfd1d0c39ee922d378ee41c1d
child 393338 990988c61a4c1a2f82796c4078f23ce8cb64c525
push id24290
push useraklotz@mozilla.com
push dateWed, 27 Jul 2016 18:19:09 +0000
reviewersjimm
bugs1263224
milestone50.0a1
Bug 1263224: Adds COM interception that allows us to proxy COM invocations; r?jimm MozReview-Commit-ID: 7dp0Jokg0X3
ipc/mscom/Interceptor.cpp
ipc/mscom/Interceptor.h
ipc/mscom/moz.build
new file mode 100644
--- /dev/null
+++ b/ipc/mscom/Interceptor.cpp
@@ -0,0 +1,293 @@
+/* -*- 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/. */
+
+#define INITGUID
+#include "mozilla/mscom/Interceptor.h"
+#include "mozilla/mscom/InterceptorLog.h"
+
+#include "mozilla/mscom/MainThreadInvoker.h"
+#include "mozilla/mscom/Registration.h"
+#include "mozilla/mscom/utils.h"
+#include "MainThreadUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/DebugOnly.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+namespace mscom {
+
+/* static */ HRESULT
+Interceptor::Create(STAUniquePtr<IUnknown>& aTarget, IInterceptorSink* aSink,
+                    REFIID aIid, void** aOutput)
+{
+  if (!aOutput) {
+    return E_INVALIDARG;
+  }
+  *aOutput = nullptr;
+  if (!aTarget || !aSink) {
+    return E_INVALIDARG;
+  }
+  Interceptor* intcpt = new Interceptor(aTarget, aSink);
+  HRESULT hr = intcpt->QueryInterface(aIid, aOutput);
+  static_cast<WeakReferenceSupport*>(intcpt)->Release();
+  return hr;
+}
+
+Interceptor::Interceptor(STAUniquePtr<IUnknown>& aTarget, IInterceptorSink* aSink)
+  : WeakReferenceSupport(WeakReferenceSupport::Flags::eDestroyOnMainThread)
+  , mTarget(Move(aTarget))
+  , mEventSink(aSink)
+  , mMutex("mozilla::mscom::Interceptor::mMutex")
+{
+  MOZ_ASSERT(aSink);
+  MOZ_ASSERT(!IsProxy(mTarget.get()));
+  RefPtr<IWeakReference> weakRef;
+  if (SUCCEEDED(GetWeakReference(getter_AddRefs(weakRef)))) {
+    aSink->SetInterceptor(weakRef);
+  }
+}
+
+Interceptor::~Interceptor()
+{
+  // This needs to run on the main thread because it releases target interface
+  // reference counts which may not be thread-safe.
+  MOZ_ASSERT(NS_IsMainThread());
+  for (uint32_t index = 0, len = mInterceptorMap.Length(); index < len; ++index) {
+    MapEntry& entry = mInterceptorMap[index];
+    entry.mInterceptor->Release();
+    entry.mTargetInterface->Release();
+  }
+}
+
+Interceptor::MapEntry*
+Interceptor::Lookup(REFIID aIid)
+{
+  mMutex.AssertCurrentThreadOwns();
+  for (uint32_t index = 0, len = mInterceptorMap.Length(); index < len; ++index) {
+    if (mInterceptorMap[index].mIID == aIid) {
+      return &mInterceptorMap[index];
+    }
+  }
+  return nullptr;
+}
+
+HRESULT
+Interceptor::GetTargetForIID(REFIID aIid, InterceptorTargetPtr& aTarget)
+{
+  MutexAutoLock lock(mMutex);
+  MapEntry* entry = Lookup(aIid);
+  if (entry) {
+    aTarget.reset(entry->mTargetInterface);
+    return S_OK;
+  }
+
+  return E_NOINTERFACE;
+}
+
+// CoGetInterceptor requires information from a typelib to be able to
+// generate its emulated vtable. If a typelib is unavailable,
+// CoGetInterceptor returns 0x80070002.
+static const HRESULT kFileNotFound = 0x80070002;
+
+HRESULT
+Interceptor::CreateInterceptor(REFIID aIid, IUnknown* aOuter, IUnknown** aOutput)
+{
+  // In order to aggregate, we *must* request IID_IUnknown as the initial
+  // interface for the interceptor, as that IUnknown is non-delegating.
+  // This is a fundamental rule for creating aggregated objects in COM.
+  HRESULT hr = ::CoGetInterceptor(aIid, aOuter, IID_IUnknown, (void**)aOutput);
+  if (hr != kFileNotFound) {
+    return hr;
+  }
+
+  // In the case that CoGetInterceptor returns kFileNotFound, we can try to
+  // explicitly load typelib data from our runtime registration facility and
+  // pass that into CoGetInterceptorFromTypeInfo.
+
+  RefPtr<ITypeInfo> typeInfo;
+  bool found = RegisteredProxy::Find(aIid, getter_AddRefs(typeInfo));
+  // If this assert fires then we have omitted registering the typelib for a
+  // required interface. To fix this, review our calls to mscom::RegisterProxy
+  // and mscom::RegisterTypelib, and add the additional typelib as necessary.
+  MOZ_ASSERT(found);
+  if (!found) {
+    return kFileNotFound;
+  }
+
+  hr = ::CoGetInterceptorFromTypeInfo(aIid, aOuter, typeInfo, IID_IUnknown,
+                                      (void**)aOutput);
+  // If this assert fires then the interceptor doesn't like something about
+  // the format of the typelib. One thing in particular that it doesn't like
+  // is complex types that contain unions.
+  MOZ_ASSERT(SUCCEEDED(hr));
+  return hr;
+}
+
+/**
+ * This method contains the core guts of the handling of QueryInterface calls
+ * that are delegated to us from the ICallInterceptor.
+ *
+ * @param aIid ID of the desired interface
+ * @param aOutInterceptor The resulting emulated vtable that corresponds to
+ * the interface specified by aIid.
+ */
+HRESULT
+Interceptor::GetInterceptorForIID(REFIID aIid, void** aOutInterceptor)
+{
+  if (!aOutInterceptor) {
+    return E_INVALIDARG;
+  }
+
+  RefPtr<IUnknown> unkInterceptor;
+  IUnknown* interfaceForQILog = nullptr;
+
+  // (1) Check to see if we already have an existing interceptor for aIid.
+
+  { // Scope for lock
+    MutexAutoLock lock(mMutex);
+    MapEntry* entry = Lookup(aIid);
+    if (entry) {
+      unkInterceptor = entry->mInterceptor;
+      interfaceForQILog = entry->mTargetInterface;
+    }
+  }
+
+  // (1a) A COM interceptor already exists for this interface, so all we need
+  // to do is run a QI on it.
+  if (unkInterceptor) {
+    // Technically we didn't actually execute a QI on the target interface, but
+    // for logging purposes we would like to record the fact that this interface
+    // was requested.
+    InterceptorLog::QI(S_OK, mTarget.get(), aIid, interfaceForQILog);
+
+    return unkInterceptor->QueryInterface(aIid, aOutInterceptor);
+  }
+
+  // (2) Obtain a new target interface.
+
+  // (2a) First, make sure that the target interface is available
+  // NB: We *MUST* query the correct interface! ICallEvents::Invoke casts its
+  // pvReceiver argument directly to the required interface! DO NOT assume
+  // that COM will use QI or upcast/downcast!
+  HRESULT hr;
+
+  STAUniquePtr<IUnknown> targetInterface;
+  IUnknown* rawTargetInterface = nullptr;
+  hr = QueryInterfaceTarget(aIid, (void**)&rawTargetInterface);
+  targetInterface.reset(rawTargetInterface);
+  InterceptorLog::QI(hr, mTarget.get(), aIid, targetInterface.get());
+  MOZ_ASSERT(SUCCEEDED(hr) || hr == E_NOINTERFACE);
+  if (FAILED(hr)) {
+    return hr;
+  }
+
+  // We *really* shouldn't be adding interceptors to proxies
+  MOZ_ASSERT(aIid != IID_IMarshal);
+
+  // (3) Create a new COM interceptor to that interface that delegates its
+  // IUnknown to |this|.
+
+  // Raise the refcount for stabilization purposes during aggregation
+  RefPtr<IUnknown> kungFuDeathGrip(static_cast<IUnknown*>(
+        static_cast<WeakReferenceSupport*>(this)));
+
+  hr = CreateInterceptor(aIid, kungFuDeathGrip, getter_AddRefs(unkInterceptor));
+  if (FAILED(hr)) {
+    return hr;
+  }
+
+  // (4) Obtain the interceptor's ICallInterceptor interface and register our
+  // event sink.
+  RefPtr<ICallInterceptor> interceptor;
+  hr = unkInterceptor->QueryInterface(IID_ICallInterceptor,
+                                      (void**)getter_AddRefs(interceptor));
+  if (FAILED(hr)) {
+    return hr;
+  }
+
+  hr = interceptor->RegisterSink(mEventSink);
+  if (FAILED(hr)) {
+    return hr;
+  }
+
+  // (5) Now that we have this new COM interceptor, insert it into the map.
+
+  { // Scope for lock
+    MutexAutoLock lock(mMutex);
+    // We might have raced with another thread, so first check that we don't
+    // already have an entry for this
+    MapEntry* entry = Lookup(aIid);
+    if (entry && entry->mInterceptor) {
+      unkInterceptor = entry->mInterceptor;
+    } else {
+      // We're inserting unkInterceptor into the map but we still want to hang
+      // onto it locally so that we can QI it below.
+      unkInterceptor->AddRef();
+      // OTOH we must not touch the refcount for the target interface
+      // because we are just moving it into the map and its refcounting might
+      // not be thread-safe.
+      IUnknown* rawTargetInterface = targetInterface.release();
+      mInterceptorMap.AppendElement(MapEntry(aIid,
+                                             unkInterceptor,
+                                             rawTargetInterface));
+    }
+  }
+
+  return unkInterceptor->QueryInterface(aIid, aOutInterceptor);
+}
+
+HRESULT
+Interceptor::QueryInterfaceTarget(REFIID aIid, void** aOutput)
+{
+  // NB: This QI needs to run on the main thread because the target object
+  // is probably Gecko code that is not thread-safe. Note that this main
+  // thread invocation is *synchronous*.
+  MainThreadInvoker invoker;
+  HRESULT hr;
+  auto runOnMainThread = [&]() -> void {
+    MOZ_ASSERT(NS_IsMainThread());
+    hr = mTarget->QueryInterface(aIid, aOutput);
+  };
+  if (!invoker.Invoke(NS_NewRunnableFunction(runOnMainThread))) {
+    return E_FAIL;
+  }
+  return hr;
+}
+
+HRESULT
+Interceptor::QueryInterface(REFIID riid, void** ppv)
+{
+  return WeakReferenceSupport::QueryInterface(riid, ppv);
+}
+
+HRESULT
+Interceptor::ThreadSafeQueryInterface(REFIID aIid, IUnknown** aOutInterface)
+{
+  if (aIid == IID_IInterceptor) {
+    *aOutInterface = static_cast<IInterceptor*>(this);
+    (*aOutInterface)->AddRef();
+    return S_OK;
+  }
+
+  return GetInterceptorForIID(aIid, (void**)aOutInterface);
+}
+
+ULONG
+Interceptor::AddRef()
+{
+  return WeakReferenceSupport::AddRef();
+}
+
+ULONG
+Interceptor::Release()
+{
+  return WeakReferenceSupport::Release();
+}
+
+} // namespace mscom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/ipc/mscom/Interceptor.h
@@ -0,0 +1,125 @@
+/* -*- 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_mscom_interceptor_h
+#define mozilla_mscom_interceptor_h
+
+#include "mozilla/Move.h"
+#include "mozilla/Mutex.h"
+#include "nsTArray.h"
+#include "mozilla/mscom/Ptr.h"
+#include "mozilla/mscom/WeakRef.h"
+#include "mozilla/RefPtr.h"
+
+#include <callobj.h>
+
+namespace mozilla {
+namespace mscom {
+
+// {8831EB53-A937-42BC-9921-B3E1121FDF86}
+DEFINE_GUID(IID_IInterceptorSink,
+0x8831eb53, 0xa937, 0x42bc, 0x99, 0x21, 0xb3, 0xe1, 0x12, 0x1f, 0xdf, 0x86);
+
+struct IInterceptorSink : public ICallFrameEvents
+{
+  virtual STDMETHODIMP SetInterceptor(IWeakReference* aInterceptor) = 0;
+};
+
+// {3710799B-ECA2-4165-B9B0-3FA1E4A9B230}
+DEFINE_GUID(IID_IInterceptor,
+0x3710799b, 0xeca2, 0x4165, 0xb9, 0xb0, 0x3f, 0xa1, 0xe4, 0xa9, 0xb2, 0x30);
+
+struct IInterceptor : public IUnknown
+{
+  virtual STDMETHODIMP GetTargetForIID(REFIID aIid, InterceptorTargetPtr& aTarget) = 0;
+  virtual STDMETHODIMP GetInterceptorForIID(REFIID aIid,
+                                            void** aOutInterceptor) = 0;
+};
+
+/**
+ * The COM interceptor is the core functionality in mscom that allows us to
+ * redirect method calls to different threads. It emulates the vtable of a
+ * target interface. When a call is made on this emulated vtable, the call is
+ * packaged up into an instance of the ICallFrame interface which may be passed
+ * to other contexts for execution.
+ *
+ * In order to accomplish this, COM itself provides the CoGetInterceptor
+ * function, which instantiates an ICallInterceptor. Note, however, that
+ * ICallInterceptor only works on a single interface; we need to be able to
+ * interpose QueryInterface calls so that we can instantiate a new
+ * ICallInterceptor for each new interface that is requested.
+ *
+ * We accomplish this by using COM aggregation, which means that the
+ * ICallInterceptor delegates its IUnknown implementation to its outer object
+ * (the mscom::Interceptor we implement and control).
+ */
+class Interceptor final : public WeakReferenceSupport
+                        , public IInterceptor
+{
+public:
+  static HRESULT Create(STAUniquePtr<IUnknown>& aTarget, IInterceptorSink* aSink,
+                        REFIID aIid, void** aOutput);
+
+  // IUnknown
+  STDMETHODIMP QueryInterface(REFIID riid, void** ppv) override;
+  STDMETHODIMP_(ULONG) AddRef() override;
+  STDMETHODIMP_(ULONG) Release() override;
+
+  // IInterceptor
+  STDMETHODIMP GetTargetForIID(REFIID aIid, InterceptorTargetPtr& aTarget) override;
+  STDMETHODIMP GetInterceptorForIID(REFIID aIid, void** aOutInterceptor) override;
+
+private:
+  struct MapEntry
+  {
+    MapEntry(REFIID aIid, IUnknown* aInterceptor, IUnknown* aTargetInterface)
+      : mIID(aIid)
+      , mInterceptor(aInterceptor)
+      , mTargetInterface(aTargetInterface)
+    {}
+    IID               mIID;
+    IUnknown*         mInterceptor;
+    IUnknown*         mTargetInterface;
+  };
+
+private:
+  Interceptor(STAUniquePtr<IUnknown>& aTarget, IInterceptorSink* aSink);
+  ~Interceptor();
+  MapEntry* Lookup(REFIID aIid);
+  HRESULT QueryInterfaceTarget(REFIID aIid, void** aOutput);
+  HRESULT ThreadSafeQueryInterface(REFIID aIid,
+                                   IUnknown** aOutInterface) override;
+  HRESULT CreateInterceptor(REFIID aIid, IUnknown* aOuter, IUnknown** aOutput);
+
+private:
+  STAUniquePtr<IUnknown>    mTarget;
+  RefPtr<IInterceptorSink>  mEventSink;
+  mozilla::Mutex            mMutex; // Guards mInterceptorMap
+  // Using a nsTArray since the # of interfaces is not going to be very high
+  nsTArray<MapEntry>        mInterceptorMap;
+};
+
+template <typename InterfaceT>
+inline HRESULT
+CreateInterceptor(STAUniquePtr<InterfaceT>& aTargetInterface,
+                  IInterceptorSink* aEventSink,
+                  InterfaceT** aOutInterface)
+{
+  if (!aTargetInterface || !aEventSink) {
+    return E_INVALIDARG;
+  }
+
+  REFIID iidTarget = __uuidof(aTargetInterface);
+
+  STAUniquePtr<IUnknown> targetUnknown(aTargetInterface.release());
+  return Interceptor::Create(targetUnknown, aEventSink, iidTarget,
+                             (void**)aOutInterface);
+}
+
+} // namespace mscom
+} // namespace mozilla
+
+#endif // mozilla_mscom_interceptor_h
--- a/ipc/mscom/moz.build
+++ b/ipc/mscom/moz.build
@@ -3,27 +3,29 @@
 # 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/.
 
 EXPORTS.mozilla.mscom += [
     'COMApartmentRegion.h',
     'COMPtrHolder.h',
     'EnsureMTA.h',
+    'Interceptor.h',
     'InterceptorLog.h',
     'MainThreadInvoker.h',
     'MainThreadRuntime.h',
     'ProxyStream.h',
     'Ptr.h',
     'Registration.h',
     'Utils.h',
     'WeakRef.h',
 ]
 
 SOURCES += [
+    'Interceptor.cpp',
     'Registration.cpp',
     'Utils.cpp',
     'WeakRef.cpp',
 ]
 
 UNIFIED_SOURCES += [
     'EnsureMTA.cpp',
     'InterceptorLog.cpp',