bwiwov
bwiwov

Reputation: 51

CoUninitialize error: Access violation reading location 0x00000008

The code below spawns a thread that waits for 5 seconds before iterating (recursively) over all the accessibles (widgets) in the foreground application.

If (during the 5 second delay) I switch to a Windows 10 Metro app (like Calc or Edge) then the call to CoUninitialize in the main thread will result in an access violation. Why?

#include <future>
#include <chrono>

#include <windows.h>
#include <oleacc.h>
#pragma comment(lib,"Oleacc.lib")

// Adapted from https://msdn.microsoft.com/en-us/library/windows/desktop/dd317975%28v=vs.85%29.aspx
HRESULT WalkTreeWithAccessibleChildren(IAccessible* pAcc, int depth)
{
  HRESULT hr;
  long childCount;
  long returnCount;

  if (!pAcc)
  {
    return E_INVALIDARG;
  }
  hr = pAcc->get_accChildCount(&childCount);
  if (FAILED(hr))
  {
    return hr;
  };
  if (childCount == 0)
  {
    return S_FALSE;
  }
  VARIANT* pArray = new VARIANT[childCount];
  hr = AccessibleChildren(pAcc, 0L, childCount, pArray, &returnCount);
  if (FAILED(hr))
  {
    return hr;
  };

  // Iterate through children.
  for (int x = 0; x < returnCount; x++)
  {
    VARIANT vtChild = pArray[x];
    // If it's an accessible object, get the IAccessible, and recurse.
    if (vtChild.vt == VT_DISPATCH)
    {
      IDispatch* pDisp = vtChild.pdispVal;
      IAccessible* pChild = NULL;
      hr = pDisp->QueryInterface(IID_IAccessible, (void**)&pChild);
      if (hr == S_OK)
      {
        WalkTreeWithAccessibleChildren(pChild, depth + 1);
        pChild->Release();
      }
      pDisp->Release();
    }
  }
  delete[] pArray;
  return S_OK;
}

int main(int argc, char *argv[])
{
  CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);

  auto future = std::async(std::launch::async,
    []
    {
      // Switch to a Windows 10 Metro app like the Calculator or Edge.
      std::this_thread::sleep_for(std::chrono::milliseconds(5000));

      auto hwnd = GetForegroundWindow();
      if (!hwnd) abort();

      CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
      IAccessible* pAcc = NULL;
      HRESULT hr = AccessibleObjectFromWindow(hwnd, OBJID_CLIENT, IID_IAccessible, (void**)&pAcc);
      if (hr == S_OK) {
        WalkTreeWithAccessibleChildren(pAcc, 0);
        pAcc->Release();
      }
      CoUninitialize();
    }
  );
  future.wait();

  CoUninitialize();
}

The error message is:

Unhandled exception at 0x7722B9E7 (combase.dll) in Test.exe: 0xC0000005: Access violation reading location 0x00000008.

Upvotes: 4

Views: 1790

Answers (1)

bwiwov
bwiwov

Reputation: 51

As per @RemyLebeau's recommendation, I added code to check the return value of CoInitialize(nullptr, COINIT_APARTMENTTHREADED) in the lambda. Turns out it was failing with 0x80010106 (Cannot change thread mode after it is set). It failed with this value even if I shuffled the code around and made the call at the very beginning of the lambda. This suggests that MSVS's implementation of std::async actually creates a multi-threaded apartment in the thread before calling the lambda (wtf!). In the end I was able to avoid this problem by using WINAPI directly (i.e. CreateThread) which doesn't suffer from this behavior. This fix alone though, was not sufficient to prevent the access violations.

I haven't yet discovered a way to properly fix the access violation but I have discovered several hacks that prevent it from happening:

  1. Create a window and show it. Note: Creating the window in itself is not sufficient, it actually needs to be visible.
  2. Configure CoInitializeEx in the main thread as COINIT_MULTITHREADED. Note: Configuring CoInitializeEx in the worker thread as COINIT_MULTITHREADED does not help.
  3. Do the enumeration in the main thread and forgo a worker thread altogether.
  4. Wait >15 seconds after the worker thread completes before calling CoUninitialize.
  5. Insert an extra (unmatched) call to CoInitialize, to ensure that the reference count never drops to 0 and therefore COM is never really uninitialized.

Unfortunately hacks 1-3 are not feasible in the real-world code that this test-case is based on. I'm loathe to force the user to wait >15 seconds for the application to exit. Therefore right now I'm leaning towards hack #5.

Any resource leaks in the client itself are not that important since the process will exit and the resources will be reclaimed by the operating system (although it will frustrate any leak testing). What is important is that it causes the accessibility server (MicrosoftEdge.exe) to leak a few kB of memory most times I run the test case.

The revised code implements the CreateThread fix as well as all 5 'hacks'. At least one of the hacks must be enabled to prevent the access violation:

#define HACK 0 // Set this between 1-5 to enable one of the hacks.

#include <future>
#include <chrono>

#include <windows.h>
#include <oleacc.h>
#pragma comment(lib,"Oleacc.lib")

// Adapted from https://msdn.microsoft.com/en-us/library/windows/desktop/dd317975%28v=vs.85%29.aspx
HRESULT WalkTreeWithAccessibleChildren(IAccessible* pAcc, int depth)
{
  HRESULT hr;
  long childCount;
  long returnCount;

  if (!pAcc)
  {
    return E_INVALIDARG;
  }
  hr = pAcc->get_accChildCount(&childCount);
  if (FAILED(hr))
  {
    return hr;
  };
  if (childCount == 0)
  {
    return S_FALSE;
  }
  VARIANT* pArray = new VARIANT[childCount];
  hr = AccessibleChildren(pAcc, 0L, childCount, pArray, &returnCount);
  if (FAILED(hr))
  {
    delete[] pArray;
    return hr;
  };

  // Iterate through children.
  for (int x = 0; x < returnCount; x++)
  {
    VARIANT vtChild = pArray[x];
    // If it's an accessible object, get the IAccessible, and recurse.
    if (vtChild.vt == VT_DISPATCH)
    {
      IDispatch* pDisp = vtChild.pdispVal;
      IAccessible* pChild = NULL;
      hr = pDisp->QueryInterface(IID_IAccessible, (void**)&pChild);
      if (hr == S_OK)
      {
        WalkTreeWithAccessibleChildren(pChild, depth + 1);
        pChild->Release();
      }
      pDisp->Release();
    }
  }
  delete[] pArray;
  return S_OK;
}

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
  HRESULT result{};

  // Switch to a Windows 10 Metro app like the Calculator or Edge.
  std::this_thread::sleep_for(std::chrono::milliseconds(5000));

  auto hwnd = GetForegroundWindow();
  if (!hwnd) {
    abort();
  }

  result = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
  if (FAILED(result)) {
    abort();
  }

  IAccessible* pAcc = NULL;
  result = AccessibleObjectFromWindow(hwnd, OBJID_CLIENT, IID_IAccessible, (void**)&pAcc);
  if (result == S_OK) {
    WalkTreeWithAccessibleChildren(pAcc, 0);
    pAcc->Release();
  }

  CoUninitialize();

  return 0;
}

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
  _In_opt_ HINSTANCE hPrevInstance,
  _In_ LPTSTR    lpCmdLine,
  _In_ int       nCmdShow)
{
  HRESULT result{};
  DWORD dw{};

#if HACK == 1
  HWND hwnd = CreateWindowA("STATIC", nullptr, 0,
    CW_USEDEFAULT, CW_USEDEFAULT,
    CW_USEDEFAULT, CW_USEDEFAULT,
    0, 0, 0, nullptr);
  if (!hwnd) {
    abort();
  }
  ShowWindow(hwnd, SW_SHOWNORMAL);
#endif

  result = CoInitializeEx(nullptr,
#if HACK == 2
    COINIT_MULTITHREADED
#else
    COINIT_APARTMENTTHREADED
#endif
  );
  if (FAILED(result)) {
    abort();
  }

#if HACK == 3
  ThreadProc(nullptr);
#else
  HANDLE threadHandle = CreateThread(nullptr, 0, &ThreadProc, nullptr, 0, nullptr);
  if (!threadHandle) {
    auto error = GetLastError();
    abort();
  }

  dw = WaitForSingleObject(threadHandle, INFINITE);
  if (dw == WAIT_FAILED) {
    auto error = GetLastError();
    abort();
  }
#endif

#if HACK == 4
  std::this_thread::sleep_for(std::chrono::milliseconds(16000));
#endif

#if HACK == 5
  CoInitialize(nullptr);
#endif

  CoUninitialize();

  return 0;
}

Upvotes: 1

Related Questions