accessible/windows/msaa/Compatibility.cpp
author Mozilla Releng Treescript <release+treescript@mozilla.org>
Fri, 20 May 2022 19:04:40 +0000
changeset 618368 3b0030f76f1ee69138d29f114d5221f21c55e7e5
parent 615654 38403cbd82e61b2fe9bcb89a6e9dc09f59872c19
permissions -rw-r--r--
no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD de -> 1d458b9f128b124a25679e755f61139726a77caf dsb -> d3afb988e0d6b558a8a590658da39eb15b2f0b18 et -> 77d8cd6a89d77a9356bc597e476cd2cc764cd607 hsb -> a67aba9c3b33f8bc097d742bd70ea98ca80a1335 hu -> ccf9d887f4eb7e4bac53afde57598686a1e255b9 hye -> ed3709ec02d90cca3ebf3237ea6d6411353e2559 it -> d544065a45d9b684beb7f60bbeaf30fd1a696e69 ja -> 17e2f87f08c519add504e8f4265e0f7b6524a34a ja-JP-mac -> 0e30fddf50953f46b80dbc99825270568b1a0d65 oc -> e114f06272b1c9377d19ca013aa6a51509cb719a pt-PT -> f7bf8255666f78337b22b3206516736daf222d40 zh-CN -> ff1ee767ed480c30d6aaefa00c65c8f62477cc18

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=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 "Compatibility.h"

#include "mozilla/WindowsVersion.h"
#include "mozilla/WinHeaderOnlyUtils.h"
#include "nsExceptionHandler.h"
#include "nsIXULRuntime.h"
#include "nsPrintfCString.h"
#include "nsUnicharUtils.h"
#include "nsWindowsDllInterceptor.h"
#include "nsWinUtils.h"
#include "Statistics.h"

#include "mozilla/Preferences.h"

#include <shlobj.h>

using namespace mozilla;
using namespace mozilla::a11y;

/**
 * String versions of consumer flags. See GetHumanReadableConsumersStr.
 */
static const wchar_t* ConsumerStringMap[CONSUMERS_ENUM_LEN + 1] = {
    L"NVDA",    L"JAWS",         L"OLDJAWS",       L"WE",       L"DOLPHIN",
    L"SEROTEK", L"COBRA",        L"ZOOMTEXT",      L"KAZAGURU", L"YOUDAO",
    L"UNKNOWN", L"UIAUTOMATION", L"VISPEROSHARED", L"\0"};

bool Compatibility::IsModuleVersionLessThan(HMODULE aModuleHandle,
                                            unsigned long long aVersion) {
  LauncherResult<ModuleVersion> version = GetModuleVersion(aModuleHandle);
  if (version.isErr()) {
    return true;
  }

  return version.unwrap() < aVersion;
}

////////////////////////////////////////////////////////////////////////////////
// Compatibility
////////////////////////////////////////////////////////////////////////////////

static WindowsDllInterceptor sUser32Interceptor;
static WindowsDllInterceptor::FuncHookType<decltype(&InSendMessageEx)>
    sInSendMessageExStub;
static bool sInSendMessageExHackEnabled = false;
static PVOID sVectoredExceptionHandler = nullptr;

#if defined(_MSC_VER)
#  include <intrin.h>
#  pragma intrinsic(_ReturnAddress)
#  define RETURN_ADDRESS() _ReturnAddress()
#elif defined(__GNUC__) || defined(__clang__)
#  define RETURN_ADDRESS() \
    __builtin_extract_return_addr(__builtin_return_address(0))
#endif

static inline bool IsCurrentThreadInBlockingMessageSend(
    const DWORD aStateBits) {
  // From the MSDN docs for InSendMessageEx
  return (aStateBits & (ISMEX_REPLIED | ISMEX_SEND)) == ISMEX_SEND;
}

/**
 * COM assumes that if you're invoking a proxy from an STA thread while
 * InSendMessageEx reports that the calling thread is blocked, that you'll
 * deadlock your own process. It returns the RPC_E_CANTCALLOUT_ININPUTSYNCCALL
 * error code. This is not actually true in our case: we are calling into
 * the multithreaded apartment via ALPC. In this hook, we check to see if the
 * caller is COM, and if so, we lie to it.
 *
 * This hack is necessary for ATs who invoke COM proxies from within
 * WH_CALLWNDPROC hooks, WinEvent hooks, or a WndProc handling a sent
 * (as opposed to posted) message.
 */
static DWORD WINAPI InSendMessageExHook(LPVOID lpReserved) {
  MOZ_ASSERT(XRE_IsParentProcess());
  DWORD result = sInSendMessageExStub(lpReserved);
  if (NS_IsMainThread() && sInSendMessageExHackEnabled &&
      IsCurrentThreadInBlockingMessageSend(result)) {
    // We want to take a strong reference to the dll so that it is never
    // unloaded/reloaded from this point forward, hence we use LoadLibrary
    // and not GetModuleHandle.
    static const HMODULE comModule = []() -> HMODULE {
      HMODULE module = LoadLibraryW(L"combase.dll");
      if (!module) {
        // combase is not present on Windows 7, so we fall back to ole32 there
        module = LoadLibraryW(L"ole32.dll");
      }

      return module;
    }();

    MOZ_ASSERT(comModule);
    if (!comModule) {
      return result;
    }

    // Check if InSendMessageEx is being called from code within comModule
    HMODULE callingModule;
    if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
                              GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
                          reinterpret_cast<LPCWSTR>(RETURN_ADDRESS()),
                          &callingModule) &&
        callingModule == comModule) {
      result = ISMEX_NOTIFY;
    }
  }
  return result;
}

static LONG CALLBACK
DetectInSendMessageExCompat(PEXCEPTION_POINTERS aExceptionInfo) {
  DWORD exceptionCode = aExceptionInfo->ExceptionRecord->ExceptionCode;
  if (exceptionCode == static_cast<DWORD>(RPC_E_CANTCALLOUT_ININPUTSYNCCALL) &&
      NS_IsMainThread()) {
    sInSendMessageExHackEnabled = true;
    // We don't need this exception handler anymore, so remove it
    if (RemoveVectoredExceptionHandler(sVectoredExceptionHandler)) {
      sVectoredExceptionHandler = nullptr;
    }
  }
  return EXCEPTION_CONTINUE_SEARCH;
}

uint32_t Compatibility::sConsumers = Compatibility::UNKNOWN;

/**
 * This function is safe to call multiple times.
 */
/* static */
void Compatibility::InitConsumers() {
  HMODULE jawsHandle = ::GetModuleHandleW(L"jhook");
  if (jawsHandle) {
    sConsumers |=
        IsModuleVersionLessThan(jawsHandle, MAKE_FILE_VERSION(19, 0, 0, 0))
            ? OLDJAWS
            : JAWS;
  }

  if (::GetModuleHandleW(L"gwm32inc")) sConsumers |= WE;

  if (::GetModuleHandleW(L"dolwinhk")) sConsumers |= DOLPHIN;

  if (::GetModuleHandleW(L"STSA32")) sConsumers |= SEROTEK;

  if (::GetModuleHandleW(L"nvdaHelperRemote")) sConsumers |= NVDA;

  if (::GetModuleHandleW(L"OsmHooks") || ::GetModuleHandleW(L"OsmHks64"))
    sConsumers |= COBRA;

  if (::GetModuleHandleW(L"WebFinderRemote")) sConsumers |= ZOOMTEXT;

  if (::GetModuleHandleW(L"Kazahook")) sConsumers |= KAZAGURU;

  if (::GetModuleHandleW(L"TextExtractorImpl32") ||
      ::GetModuleHandleW(L"TextExtractorImpl64"))
    sConsumers |= YOUDAO;

  if (::GetModuleHandleW(L"uiautomation") ||
      ::GetModuleHandleW(L"uiautomationcore"))
    sConsumers |= UIAUTOMATION;

  if (::GetModuleHandleW(L"AccEventCache")) {
    sConsumers |= VISPEROSHARED;
  }

  // If we have a known consumer remove the unknown bit.
  if (sConsumers != Compatibility::UNKNOWN)
    sConsumers &= ~Compatibility::UNKNOWN;
}

/* static */
bool Compatibility::HasKnownNonUiaConsumer() {
  InitConsumers();
  return sConsumers & ~(Compatibility::UNKNOWN | UIAUTOMATION);
}

void Compatibility::Init() {
  // Note we collect some AT statistics/telemetry here for convenience.
  InitConsumers();

  CrashReporter::AnnotateCrashReport(
      CrashReporter::Annotation::AccessibilityInProcClient,
      nsPrintfCString("0x%X", sConsumers));

  // Gather telemetry
  uint32_t temp = sConsumers;
  for (int i = 0; temp; i++) {
    if (temp & 0x1) statistics::A11yConsumers(i);

    temp >>= 1;
  }

  // Turn off new tab switching for Jaws and WE.
  if (sConsumers & (JAWS | OLDJAWS | WE)) {
    // Check to see if the pref for disallowing CtrlTab is already set. If so,
    // bail out (respect the user settings). If not, set it.
    if (!Preferences::HasUserValue("browser.ctrlTab.disallowForScreenReaders"))
      Preferences::SetBool("browser.ctrlTab.disallowForScreenReaders", true);
  }

  // If we have a consumer who is not NVDA, we enable detection for the
  // InSendMessageEx compatibility hack. NVDA does not require this.
  // We also skip UIA, as we see crashes there.
  if ((sConsumers & (~(UIAUTOMATION | NVDA))) && BrowserTabsRemoteAutostart()) {
    sUser32Interceptor.Init("user32.dll");
    sInSendMessageExStub.Set(sUser32Interceptor, "InSendMessageEx",
                             &InSendMessageExHook);

    // The vectored exception handler allows us to catch exceptions ahead of any
    // SEH handlers.
    if (!sVectoredExceptionHandler) {
      // We need to let ASan's ShadowExceptionHandler remain in the firstHandler
      // position, otherwise we'll get infinite recursion when our handler
      // faults on shadow memory.
      const ULONG firstHandler = FALSE;
      sVectoredExceptionHandler = AddVectoredExceptionHandler(
          firstHandler, &DetectInSendMessageExCompat);
    }
  }
}

// static
void Compatibility::GetHumanReadableConsumersStr(nsAString& aResult) {
  bool appened = false;
  uint32_t index = 0;
  for (uint32_t consumers = sConsumers; consumers; consumers = consumers >> 1) {
    if (consumers & 0x1) {
      if (appened) {
        aResult.AppendLiteral(",");
      }
      aResult.Append(ConsumerStringMap[index]);
      appened = true;
    }
    if (++index > CONSUMERS_ENUM_LEN) {
      break;
    }
  }
}