/* -*- 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 "Compatibility.h" #include "mozilla/a11y/Platform.h" #include "mozilla/UniquePtrExtensions.h" #include "mozilla/UniquePtr.h" #include "mozilla/WindowsVersion.h" #include "nsReadableUtils.h" #include "nsString.h" #include "nsTHashSet.h" #include "nsWindowsHelpers.h" #include "NtUndoc.h" using namespace mozilla; struct ByteArrayDeleter { void operator()(void* aBuf) { delete[] reinterpret_cast(aBuf); } }; typedef UniquePtr ObjDirInfoPtr; // ComparatorFnT returns true to continue searching, or else false to indicate // search completion. template static bool FindNamedObject(const ComparatorFnT& aComparator) { // We want to enumerate every named kernel object in our session. We do this // by opening a directory object using a path constructed using the session // id under which our process resides. DWORD sessionId; if (!::ProcessIdToSessionId(::GetCurrentProcessId(), &sessionId)) { return false; } nsAutoString path; path.AppendPrintf("\\Sessions\\%lu\\BaseNamedObjects", sessionId); UNICODE_STRING baseNamedObjectsName; ::RtlInitUnicodeString(&baseNamedObjectsName, path.get()); OBJECT_ATTRIBUTES attributes; InitializeObjectAttributes(&attributes, &baseNamedObjectsName, 0, nullptr, nullptr); HANDLE rawBaseNamedObjects; NTSTATUS ntStatus = ::NtOpenDirectoryObject( &rawBaseNamedObjects, DIRECTORY_QUERY | DIRECTORY_TRAVERSE, &attributes); if (!NT_SUCCESS(ntStatus)) { return false; } nsAutoHandle baseNamedObjects(rawBaseNamedObjects); ULONG context = 0, returnedLen; ULONG objDirInfoBufLen = 1024 * sizeof(OBJECT_DIRECTORY_INFORMATION); ObjDirInfoPtr objDirInfo(reinterpret_cast( new std::byte[objDirInfoBufLen])); // Now query that directory object for every named object that it contains. BOOL firstCall = TRUE; do { ntStatus = ::NtQueryDirectoryObject(baseNamedObjects, objDirInfo.get(), objDirInfoBufLen, FALSE, firstCall, &context, &returnedLen); #if defined(HAVE_64BIT_BUILD) if (!NT_SUCCESS(ntStatus)) { return false; } #else if (ntStatus == STATUS_BUFFER_TOO_SMALL) { // This case only occurs on 32-bit builds running atop WOW64. // (See https://bugzilla.mozilla.org/show_bug.cgi?id=1423999#c3) objDirInfo.reset(reinterpret_cast( new std::byte[returnedLen])); objDirInfoBufLen = returnedLen; continue; } else if (!NT_SUCCESS(ntStatus)) { return false; } #endif // NtQueryDirectoryObject gave us an array of OBJECT_DIRECTORY_INFORMATION // structures whose final entry is zeroed out. OBJECT_DIRECTORY_INFORMATION* curDir = objDirInfo.get(); while (curDir->mName.Length && curDir->mTypeName.Length) { // We use nsDependentSubstring here because UNICODE_STRINGs are not // guaranteed to be null-terminated. nsDependentSubstring objName(curDir->mName.Buffer, curDir->mName.Length / sizeof(wchar_t)); nsDependentSubstring typeName(curDir->mTypeName.Buffer, curDir->mTypeName.Length / sizeof(wchar_t)); if (!aComparator(objName, typeName)) { return true; } ++curDir; } firstCall = FALSE; } while (ntStatus == STATUS_MORE_ENTRIES); return false; } // ComparatorFnT returns true to continue searching, or else false to indicate // search completion. template static bool FindHandle(const ComparatorFnT& aComparator) { NTSTATUS ntStatus; // First we must query for a list of all the open handles in the system. UniquePtr handleInfoBuf; ULONG handleInfoBufLen = sizeof(SYSTEM_HANDLE_INFORMATION_EX) + 1024 * sizeof(SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX); // We must query for handle information in a loop, since we are effectively // asking the kernel to take a snapshot of all the handles on the system; // the size of the required buffer may fluctuate between successive calls. while (true) { // These allocations can be hundreds of megabytes on some computers, so // we should use fallible new here. handleInfoBuf = MakeUniqueFallible(handleInfoBufLen); if (!handleInfoBuf) { return false; } ntStatus = ::NtQuerySystemInformation( (SYSTEM_INFORMATION_CLASS)SystemExtendedHandleInformation, handleInfoBuf.get(), handleInfoBufLen, &handleInfoBufLen); if (ntStatus == STATUS_INFO_LENGTH_MISMATCH) { continue; } if (!NT_SUCCESS(ntStatus)) { return false; } break; } auto handleInfo = reinterpret_cast(handleInfoBuf.get()); for (ULONG index = 0; index < handleInfo->mHandleCount; ++index) { SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX& info = handleInfo->mHandles[index]; HANDLE handle = reinterpret_cast(info.mHandle); if (!aComparator(info, handle)) { return true; } } return false; } class GetUiaClientPidsWin11 { public: static void Run(nsTArray& aPids); private: struct HandleAndPid { explicit HandleAndPid(HANDLE aHandle) : mHandle(aHandle) {} HANDLE mHandle; ULONG mPid = 0; }; // Some local testing showed that we get around 40 handles when Firefox has // been started with a few tabs open for ~30 seconds before starting a UIA // client. That might increase with a longer duration, more tabs, etc., so // allow for some extra. using HandlesAndPids = AutoTArray; struct ThreadData { explicit ThreadData(HandlesAndPids& aHandlesAndPids) : mHandlesAndPids(aHandlesAndPids) {} HandlesAndPids& mHandlesAndPids; // Keeps track of the current index in mHandlesAndPids that is being // queried. When the thread is (re)started, it starts querying from this // index. size_t mCurrentIndex = 0; }; static DWORD WINAPI QueryThreadProc(LPVOID aParameter) { // WARNING! Because this thread may be terminated unexpectedly due to a // hang, it must not do anything which acquires resources, allocates memory, // non-atomically modifies state, etc. It may not get a chance to clean up. auto& data = *(ThreadData*)aParameter; for (; data.mCurrentIndex < data.mHandlesAndPids.Length(); ++data.mCurrentIndex) { auto& entry = data.mHandlesAndPids[data.mCurrentIndex]; // Counter-intuitively, for UIA pipes, we're the client and the remote // process is the server. ::GetNamedPipeServerProcessId(entry.mHandle, &entry.mPid); } return 0; }; }; void GetUiaClientPidsWin11::Run(nsTArray& aPids) { // 1. Get all handles of interest in our process. HandlesAndPids handlesAndPids; const DWORD ourPid = ::GetCurrentProcessId(); FindHandle([&](auto aInfo, auto aHandle) { // UIA pipes always have granted access 0x0012019F. Pipes with this access // can still hang, but this at least narrows down the handles we need to // check. if (aInfo.mPid == ourPid && aInfo.mGrantedAccess == 0x0012019F) { handlesAndPids.AppendElement(HandleAndPid(aHandle)); } return true; }); // 2. UIA creates a named pipe between the client and server processes. We // want to find our handle to those pipes (if any). For all named pipes, get // the process id of the remote end. We must use a background thread to query // pipes because this can hang on some pipes and there's no way to prevent // this other than terminating the thread. See bug 1899211 for more details. ThreadData threadData(handlesAndPids); while (threadData.mCurrentIndex < handlesAndPids.Length()) { // We use CreateThread here rather than Gecko's threading support because // we may terminate this thread and we must be certain it hasn't acquired // any resources which need to be cleaned up. nsAutoHandle thread(::CreateThread(nullptr, 0, QueryThreadProc, (LPVOID)&threadData, 0, nullptr)); if (!thread) { return; } if (::WaitForSingleObject(thread, 50) == WAIT_OBJECT_0) { // We're done querying the handles. MOZ_ASSERT(threadData.mCurrentIndex == handlesAndPids.Length()); break; } // The thread hung. Terminate it. ::TerminateThread(thread, 1); // The thread probably hung on threadData.mCurrentIndex, so skip this // handle. In the next iteration of this loop, we'll create another thread // and resume from that point. This could result in us skipping a handle if // the thread didn't actually hang, but took too long and was terminated // after incrementing but before querying the handle. At worst, we might // miss a UIA client in this case, but this should be very rare and it's an // acceptable compromise to avoid a main thread hang. ++threadData.mCurrentIndex; } // 3. Now that we have pids for all named pipes, get the name of those handles // and check whether they are UIA pipes. We can't do this in the thread above // because it allocates memory and that might not get cleaned up if the thread // is terminated. for (auto& entry : handlesAndPids) { if (!entry.mPid) { continue; // Not a named pipe. } ULONG objNameBufLen; NTSTATUS ntStatus = ::NtQueryObject( entry.mHandle, (OBJECT_INFORMATION_CLASS)ObjectNameInformation, nullptr, 0, &objNameBufLen); if (ntStatus != STATUS_INFO_LENGTH_MISMATCH) { continue; } auto objNameBuf = MakeUnique(objNameBufLen); ntStatus = ::NtQueryObject(entry.mHandle, (OBJECT_INFORMATION_CLASS)ObjectNameInformation, objNameBuf.get(), objNameBufLen, &objNameBufLen); if (!NT_SUCCESS(ntStatus)) { continue; } auto objNameInfo = reinterpret_cast(objNameBuf.get()); if (!objNameInfo->Name.Length) { continue; } nsDependentString objName(objNameInfo->Name.Buffer, objNameInfo->Name.Length / sizeof(wchar_t)); if (StringBeginsWith(objName, u"\\Device\\NamedPipe\\UIA_PIPE_"_ns)) { aPids.AppendElement(entry.mPid); } } } static DWORD GetUiaClientPidWin10() { // UIA creates a section of the form "HOOK_SHMEM_%08lx_%08lx_%08lx_%08lx" constexpr auto kStrHookShmem = u"HOOK_SHMEM_"_ns; // The second %08lx is the thread id. nsAutoString sectionThread; sectionThread.AppendPrintf("_%08lx_", ::GetCurrentThreadId()); // This is the number of characters from the end of the section name where // the sectionThread substring begins. constexpr size_t sectionThreadRPos = 27; // This is the length of sectionThread. constexpr size_t sectionThreadLen = 10; // Find any named Section that matches the naming convention of the UIA shared // memory. There can only be one of these at a time, since this only exists // while UIA is processing a request and it can only process a single request // on a single thread. nsAutoHandle section; auto objectComparator = [&](const nsDependentSubstring& aName, const nsDependentSubstring& aType) -> bool { if (aType.Equals(u"Section"_ns) && FindInReadable(kStrHookShmem, aName) && Substring(aName, aName.Length() - sectionThreadRPos, sectionThreadLen) == sectionThread) { // Get a handle to this section so we can get its kernel object and // use that to find the handle for this section in the remote process. section.own(::OpenFileMapping(GENERIC_READ, FALSE, PromiseFlatString(aName).get())); return false; } return true; }; if (!FindNamedObject(objectComparator) || !section) { return 0; } // Now, find the kernel object associated with our section, the handle in the // remote process associated with that kernel object and thus the remote // process id. NTSTATUS ntStatus; const DWORD ourPid = ::GetCurrentProcessId(); Maybe kernelObject; static Maybe sectionObjTypeIndex; nsTHashSet nonSectionObjTypes; nsTHashMap objMap; DWORD remotePid = 0; FindHandle([&](auto aInfo, auto aHandle) { // The mapping of the aInfo.mObjectTypeIndex field depends on the // underlying OS kernel. As we scan through the handle list, we record the // type indices such that we may use those values to skip over handles that // refer to non-section objects. if (sectionObjTypeIndex) { // If we know the type index for Sections, that's the fastest check... if (sectionObjTypeIndex.value() != aInfo.mObjectTypeIndex) { // Not a section return true; } } else if (nonSectionObjTypes.Contains( static_cast(aInfo.mObjectTypeIndex))) { // Otherwise we check whether or not the object type is definitely _not_ // a Section... return true; } else if (ourPid == aInfo.mPid) { // Otherwise we need to issue some system calls to find out the object // type corresponding to the current handle's type index. ULONG objTypeBufLen; ntStatus = ::NtQueryObject(aHandle, ObjectTypeInformation, nullptr, 0, &objTypeBufLen); if (ntStatus != STATUS_INFO_LENGTH_MISMATCH) { return true; } auto objTypeBuf = MakeUnique(objTypeBufLen); ntStatus = ::NtQueryObject(aHandle, ObjectTypeInformation, objTypeBuf.get(), objTypeBufLen, &objTypeBufLen); if (!NT_SUCCESS(ntStatus)) { return true; } auto objType = reinterpret_cast(objTypeBuf.get()); // Now we check whether the object's type name matches "Section" nsDependentSubstring objTypeName( objType->TypeName.Buffer, objType->TypeName.Length / sizeof(wchar_t)); if (!objTypeName.Equals(u"Section"_ns)) { nonSectionObjTypes.Insert( static_cast(aInfo.mObjectTypeIndex)); return true; } sectionObjTypeIndex = Some(aInfo.mObjectTypeIndex); } // At this point we know that aInfo references a Section object. // Now we can do some actual tests on it. if (ourPid != aInfo.mPid) { if (kernelObject && kernelObject.value() == aInfo.mObject) { // The kernel objects match -- we have found the remote pid! remotePid = aInfo.mPid; return false; } // An object that is not ours. Since we do not yet know which kernel // object we're interested in, we'll save the current object for later. objMap.InsertOrUpdate(aInfo.mObject, aInfo.mPid); } else if (aHandle == section.get()) { // This is the file mapping that we opened above. We save this mObject // in order to compare to Section objects opened by other processes. kernelObject = Some(aInfo.mObject); } return true; }); if (remotePid) { return remotePid; } if (!kernelObject) { return 0; } // If we reach here, we found kernelObject *after* we saw the remote process's // copy. Now we must look it up in objMap. if (objMap.Get(kernelObject.value(), &remotePid)) { return remotePid; } return 0; } namespace mozilla { namespace a11y { void Compatibility::GetUiaClientPids(nsTArray& aPids) { if (!::GetModuleHandleW(L"uiautomationcore.dll")) { // UIAutomationCore isn't loaded, so there is no UIA client. return; } if (IsWin11OrLater()) { GetUiaClientPidsWin11::Run(aPids); } else { if (DWORD pid = GetUiaClientPidWin10()) { aPids.AppendElement(pid); } } } } // namespace a11y } // namespace mozilla