summaryrefslogtreecommitdiffstats
path: root/accessible/windows/msaa/Compatibility.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'accessible/windows/msaa/Compatibility.cpp')
-rw-r--r--accessible/windows/msaa/Compatibility.cpp413
1 files changed, 413 insertions, 0 deletions
diff --git a/accessible/windows/msaa/Compatibility.cpp b/accessible/windows/msaa/Compatibility.cpp
new file mode 100644
index 0000000000..43eb407f75
--- /dev/null
+++ b/accessible/windows/msaa/Compatibility.cpp
@@ -0,0 +1,413 @@
+/* -*- 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 == 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);
+ }
+ }
+}
+
+#if !defined(HAVE_64BIT_BUILD)
+
+static bool ReadCOMRegDefaultString(const nsString& aRegPath,
+ nsAString& aOutBuf) {
+ aOutBuf.Truncate();
+
+ nsAutoString fullyQualifiedRegPath;
+ fullyQualifiedRegPath.AppendLiteral(u"SOFTWARE\\Classes\\");
+ fullyQualifiedRegPath.Append(aRegPath);
+
+ // Get the required size and type of the registry value.
+ // We expect either REG_SZ or REG_EXPAND_SZ.
+ DWORD type;
+ DWORD bufLen = 0;
+ LONG result = ::RegGetValue(HKEY_LOCAL_MACHINE, fullyQualifiedRegPath.get(),
+ nullptr, RRF_RT_ANY, &type, nullptr, &bufLen);
+ if (result != ERROR_SUCCESS || (type != REG_SZ && type != REG_EXPAND_SZ)) {
+ return false;
+ }
+
+ // Now obtain the value
+ DWORD flags = type == REG_SZ ? RRF_RT_REG_SZ : RRF_RT_REG_EXPAND_SZ;
+
+ aOutBuf.SetLength((bufLen + 1) / sizeof(char16_t));
+
+ result =
+ ::RegGetValue(HKEY_LOCAL_MACHINE, fullyQualifiedRegPath.get(), nullptr,
+ flags, nullptr, aOutBuf.BeginWriting(), &bufLen);
+ if (result != ERROR_SUCCESS) {
+ aOutBuf.Truncate();
+ return false;
+ }
+
+ // Truncate terminator
+ aOutBuf.Truncate((bufLen + 1) / sizeof(char16_t) - 1);
+ return true;
+}
+
+static bool IsSystemOleAcc(nsCOMPtr<nsIFile>& aFile) {
+ // Use FOLDERID_SystemX86 so that Windows doesn't give us a redirected
+ // system32 if we're a 32-bit process running on a 64-bit OS. This is
+ // necessary because the values that we are reading from the registry
+ // are not redirected; they reference SysWOW64 directly.
+ PWSTR systemPath = nullptr;
+ HRESULT hr =
+ ::SHGetKnownFolderPath(FOLDERID_SystemX86, 0, nullptr, &systemPath);
+ if (FAILED(hr)) {
+ return false;
+ }
+
+ nsCOMPtr<nsIFile> oleAcc;
+ nsresult rv = NS_NewLocalFile(nsDependentString(systemPath), false,
+ getter_AddRefs(oleAcc));
+
+ ::CoTaskMemFree(systemPath);
+ systemPath = nullptr;
+
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ rv = oleAcc->Append(u"oleacc.dll"_ns);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ bool isEqual;
+ rv = oleAcc->Equals(aFile, &isEqual);
+ return NS_SUCCEEDED(rv) && isEqual;
+}
+
+static bool IsTypelibPreferred() {
+ // If IAccessible's Proxy/Stub CLSID is kUniversalMarshalerClsid, then any
+ // external a11y clients are expecting to use a typelib.
+ constexpr auto kUniversalMarshalerClsid =
+ u"{00020424-0000-0000-C000-000000000046}"_ns;
+
+ constexpr auto kIAccessiblePSClsidPath =
+ "Interface\\{618736E0-3C3D-11CF-810C-00AA00389B71}"
+ u"\\ProxyStubClsid32"_ns;
+
+ nsAutoString psClsid;
+ if (!ReadCOMRegDefaultString(kIAccessiblePSClsidPath, psClsid)) {
+ return false;
+ }
+
+ return psClsid.Equals(kUniversalMarshalerClsid,
+ nsCaseInsensitiveStringComparator);
+}
+
+static bool IsIAccessibleTypelibRegistered() {
+ // The system default IAccessible typelib is always registered with version
+ // 1.1, under the neutral locale (LCID 0).
+ constexpr auto kIAccessibleTypelibRegPath =
+ u"TypeLib\\{1EA4DBF0-3C3B-11CF-810C-00AA00389B71}\\1.1\\0\\win32"_ns;
+
+ nsAutoString typelibPath;
+ if (!ReadCOMRegDefaultString(kIAccessibleTypelibRegPath, typelibPath)) {
+ return false;
+ }
+
+ nsCOMPtr<nsIFile> libTestFile;
+ nsresult rv =
+ NS_NewLocalFile(typelibPath, false, getter_AddRefs(libTestFile));
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ return IsSystemOleAcc(libTestFile);
+}
+
+static bool IsIAccessiblePSRegistered() {
+ constexpr auto kIAccessiblePSRegPath =
+ u"CLSID\\{03022430-ABC4-11D0-BDE2-00AA001A1953}\\InProcServer32"_ns;
+
+ nsAutoString proxyStubPath;
+ if (!ReadCOMRegDefaultString(kIAccessiblePSRegPath, proxyStubPath)) {
+ return false;
+ }
+
+ nsCOMPtr<nsIFile> libTestFile;
+ nsresult rv =
+ NS_NewLocalFile(proxyStubPath, false, getter_AddRefs(libTestFile));
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ return IsSystemOleAcc(libTestFile);
+}
+
+static bool UseIAccessibleProxyStub() {
+ // If a typelib is preferred then external clients are expecting to use
+ // typelib marshaling, so we should use that whenever available.
+ if (IsTypelibPreferred() && IsIAccessibleTypelibRegistered()) {
+ return false;
+ }
+
+ // Otherwise we try the proxy/stub
+ if (IsIAccessiblePSRegistered()) {
+ return true;
+ }
+
+ // If we reach this point then something is seriously wrong with the
+ // IAccessible configuration in the computer's registry. Let's annotate this
+ // so that we can easily determine this condition during crash analysis.
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::IAccessibleConfig, "NoSystemTypeLibOrPS"_ns);
+ return false;
+}
+
+#endif // !defined(HAVE_64BIT_BUILD)
+
+uint16_t Compatibility::GetActCtxResourceId() {
+#if defined(HAVE_64BIT_BUILD)
+ // The manifest for 64-bit Windows is embedded with resource ID 64.
+ return 64;
+#else
+ // The manifest for 32-bit Windows is embedded with resource ID 32.
+ // Beginning with Windows 10 Creators Update, 32-bit builds always use the
+ // 64-bit manifest. Older builds of Windows may or may not require the 64-bit
+ // manifest: UseIAccessibleProxyStub() determines the course of action.
+ if (mozilla::IsWin10CreatorsUpdateOrLater() || UseIAccessibleProxyStub()) {
+ return 64;
+ }
+
+ return 32;
+#endif // defined(HAVE_64BIT_BUILD)
+}
+
+// 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;
+ }
+ }
+}