CompatibilityUIA.cpp (16341B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "Compatibility.h" 8 9 #include "mozilla/a11y/Platform.h" 10 #include "mozilla/UniquePtrExtensions.h" 11 #include "mozilla/UniquePtr.h" 12 #include "mozilla/WindowsVersion.h" 13 #include "nsReadableUtils.h" 14 #include "nsString.h" 15 #include "nsTHashSet.h" 16 #include "nsWindowsHelpers.h" 17 18 #include "NtUndoc.h" 19 20 using namespace mozilla; 21 22 struct ByteArrayDeleter { 23 void operator()(void* aBuf) { delete[] reinterpret_cast<std::byte*>(aBuf); } 24 }; 25 26 typedef UniquePtr<OBJECT_DIRECTORY_INFORMATION, ByteArrayDeleter> ObjDirInfoPtr; 27 28 // ComparatorFnT returns true to continue searching, or else false to indicate 29 // search completion. 30 template <typename ComparatorFnT> 31 static bool FindNamedObject(const ComparatorFnT& aComparator) { 32 // We want to enumerate every named kernel object in our session. We do this 33 // by opening a directory object using a path constructed using the session 34 // id under which our process resides. 35 DWORD sessionId; 36 if (!::ProcessIdToSessionId(::GetCurrentProcessId(), &sessionId)) { 37 return false; 38 } 39 40 nsAutoString path; 41 path.AppendPrintf("\\Sessions\\%lu\\BaseNamedObjects", sessionId); 42 43 UNICODE_STRING baseNamedObjectsName; 44 ::RtlInitUnicodeString(&baseNamedObjectsName, path.get()); 45 46 OBJECT_ATTRIBUTES attributes; 47 InitializeObjectAttributes(&attributes, &baseNamedObjectsName, 0, nullptr, 48 nullptr); 49 50 HANDLE rawBaseNamedObjects; 51 NTSTATUS ntStatus = ::NtOpenDirectoryObject( 52 &rawBaseNamedObjects, DIRECTORY_QUERY | DIRECTORY_TRAVERSE, &attributes); 53 if (!NT_SUCCESS(ntStatus)) { 54 return false; 55 } 56 57 nsAutoHandle baseNamedObjects(rawBaseNamedObjects); 58 59 ULONG context = 0, returnedLen; 60 61 ULONG objDirInfoBufLen = 1024 * sizeof(OBJECT_DIRECTORY_INFORMATION); 62 ObjDirInfoPtr objDirInfo(reinterpret_cast<OBJECT_DIRECTORY_INFORMATION*>( 63 new std::byte[objDirInfoBufLen])); 64 65 // Now query that directory object for every named object that it contains. 66 67 BOOL firstCall = TRUE; 68 69 do { 70 ntStatus = ::NtQueryDirectoryObject(baseNamedObjects, objDirInfo.get(), 71 objDirInfoBufLen, FALSE, firstCall, 72 &context, &returnedLen); 73 #if defined(HAVE_64BIT_BUILD) 74 if (!NT_SUCCESS(ntStatus)) { 75 return false; 76 } 77 #else 78 if (ntStatus == STATUS_BUFFER_TOO_SMALL) { 79 // This case only occurs on 32-bit builds running atop WOW64. 80 // (See https://bugzilla.mozilla.org/show_bug.cgi?id=1423999#c3) 81 objDirInfo.reset(reinterpret_cast<OBJECT_DIRECTORY_INFORMATION*>( 82 new std::byte[returnedLen])); 83 objDirInfoBufLen = returnedLen; 84 continue; 85 } else if (!NT_SUCCESS(ntStatus)) { 86 return false; 87 } 88 #endif 89 90 // NtQueryDirectoryObject gave us an array of OBJECT_DIRECTORY_INFORMATION 91 // structures whose final entry is zeroed out. 92 OBJECT_DIRECTORY_INFORMATION* curDir = objDirInfo.get(); 93 while (curDir->mName.Length && curDir->mTypeName.Length) { 94 // We use nsDependentSubstring here because UNICODE_STRINGs are not 95 // guaranteed to be null-terminated. 96 nsDependentSubstring objName(curDir->mName.Buffer, 97 curDir->mName.Length / sizeof(wchar_t)); 98 nsDependentSubstring typeName(curDir->mTypeName.Buffer, 99 curDir->mTypeName.Length / sizeof(wchar_t)); 100 101 if (!aComparator(objName, typeName)) { 102 return true; 103 } 104 105 ++curDir; 106 } 107 108 firstCall = FALSE; 109 } while (ntStatus == STATUS_MORE_ENTRIES); 110 111 return false; 112 } 113 114 // ComparatorFnT returns true to continue searching, or else false to indicate 115 // search completion. 116 template <typename ComparatorFnT> 117 static bool FindHandle(const ComparatorFnT& aComparator) { 118 NTSTATUS ntStatus; 119 // First we must query for a list of all the open handles in the system. 120 UniquePtr<std::byte[]> handleInfoBuf; 121 ULONG handleInfoBufLen = sizeof(SYSTEM_HANDLE_INFORMATION_EX) + 122 1024 * sizeof(SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX); 123 // We must query for handle information in a loop, since we are effectively 124 // asking the kernel to take a snapshot of all the handles on the system; 125 // the size of the required buffer may fluctuate between successive calls. 126 while (true) { 127 // These allocations can be hundreds of megabytes on some computers, so 128 // we should use fallible new here. 129 handleInfoBuf = MakeUniqueFallible<std::byte[]>(handleInfoBufLen); 130 if (!handleInfoBuf) { 131 return false; 132 } 133 ntStatus = ::NtQuerySystemInformation( 134 (SYSTEM_INFORMATION_CLASS)SystemExtendedHandleInformation, 135 handleInfoBuf.get(), handleInfoBufLen, &handleInfoBufLen); 136 if (ntStatus == STATUS_INFO_LENGTH_MISMATCH) { 137 continue; 138 } 139 if (!NT_SUCCESS(ntStatus)) { 140 return false; 141 } 142 break; 143 } 144 145 auto handleInfo = 146 reinterpret_cast<SYSTEM_HANDLE_INFORMATION_EX*>(handleInfoBuf.get()); 147 for (ULONG index = 0; index < handleInfo->mHandleCount; ++index) { 148 SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX& info = handleInfo->mHandles[index]; 149 HANDLE handle = reinterpret_cast<HANDLE>(info.mHandle); 150 if (!aComparator(info, handle)) { 151 return true; 152 } 153 } 154 return false; 155 } 156 157 class GetUiaClientPidsWin11 { 158 public: 159 static void Run(nsTArray<DWORD>& aPids); 160 161 private: 162 struct HandleAndPid { 163 explicit HandleAndPid(HANDLE aHandle) : mHandle(aHandle) {} 164 HANDLE mHandle; 165 ULONG mPid = 0; 166 }; 167 // Some local testing showed that we get around 40 handles when Firefox has 168 // been started with a few tabs open for ~30 seconds before starting a UIA 169 // client. That might increase with a longer duration, more tabs, etc., so 170 // allow for some extra. 171 using HandlesAndPids = AutoTArray<HandleAndPid, 128>; 172 173 struct ThreadData { 174 explicit ThreadData(HandlesAndPids& aHandlesAndPids) 175 : mHandlesAndPids(aHandlesAndPids) {} 176 HandlesAndPids& mHandlesAndPids; 177 // Keeps track of the current index in mHandlesAndPids that is being 178 // queried. When the thread is (re)started, it starts querying from this 179 // index. 180 size_t mCurrentIndex = 0; 181 }; 182 183 static DWORD WINAPI QueryThreadProc(LPVOID aParameter) { 184 // WARNING! Because this thread may be terminated unexpectedly due to a 185 // hang, it must not do anything which acquires resources, allocates memory, 186 // non-atomically modifies state, etc. It may not get a chance to clean up. 187 auto& data = *(ThreadData*)aParameter; 188 for (; data.mCurrentIndex < data.mHandlesAndPids.Length(); 189 ++data.mCurrentIndex) { 190 auto& entry = data.mHandlesAndPids[data.mCurrentIndex]; 191 // Counter-intuitively, for UIA pipes, we're the client and the remote 192 // process is the server. 193 ::GetNamedPipeServerProcessId(entry.mHandle, &entry.mPid); 194 } 195 return 0; 196 }; 197 }; 198 199 void GetUiaClientPidsWin11::Run(nsTArray<DWORD>& aPids) { 200 // 1. Get all handles of interest in our process. 201 HandlesAndPids handlesAndPids; 202 const DWORD ourPid = ::GetCurrentProcessId(); 203 FindHandle([&](auto aInfo, auto aHandle) { 204 // UIA pipes always have granted access 0x0012019F. Pipes with this access 205 // can still hang, but this at least narrows down the handles we need to 206 // check. 207 if (aInfo.mPid == ourPid && aInfo.mGrantedAccess == 0x0012019F) { 208 handlesAndPids.AppendElement(HandleAndPid(aHandle)); 209 } 210 return true; 211 }); 212 213 // 2. UIA creates a named pipe between the client and server processes. We 214 // want to find our handle to those pipes (if any). For all named pipes, get 215 // the process id of the remote end. We must use a background thread to query 216 // pipes because this can hang on some pipes and there's no way to prevent 217 // this other than terminating the thread. See bug 1899211 for more details. 218 ThreadData threadData(handlesAndPids); 219 while (threadData.mCurrentIndex < handlesAndPids.Length()) { 220 // We use CreateThread here rather than Gecko's threading support because 221 // we may terminate this thread and we must be certain it hasn't acquired 222 // any resources which need to be cleaned up. 223 nsAutoHandle thread(::CreateThread(nullptr, 0, QueryThreadProc, 224 (LPVOID)&threadData, 0, nullptr)); 225 if (!thread) { 226 return; 227 } 228 if (::WaitForSingleObject(thread, 50) == WAIT_OBJECT_0) { 229 // We're done querying the handles. 230 MOZ_ASSERT(threadData.mCurrentIndex == handlesAndPids.Length()); 231 break; 232 } 233 // The thread hung. Terminate it. 234 ::TerminateThread(thread, 1); 235 // The thread probably hung on threadData.mCurrentIndex, so skip this 236 // handle. In the next iteration of this loop, we'll create another thread 237 // and resume from that point. This could result in us skipping a handle if 238 // the thread didn't actually hang, but took too long and was terminated 239 // after incrementing but before querying the handle. At worst, we might 240 // miss a UIA client in this case, but this should be very rare and it's an 241 // acceptable compromise to avoid a main thread hang. 242 ++threadData.mCurrentIndex; 243 } 244 245 // 3. Now that we have pids for all named pipes, get the name of those handles 246 // and check whether they are UIA pipes. We can't do this in the thread above 247 // because it allocates memory and that might not get cleaned up if the thread 248 // is terminated. 249 for (auto& entry : handlesAndPids) { 250 if (!entry.mPid) { 251 continue; // Not a named pipe. 252 } 253 ULONG objNameBufLen; 254 NTSTATUS ntStatus = ::NtQueryObject( 255 entry.mHandle, (OBJECT_INFORMATION_CLASS)ObjectNameInformation, nullptr, 256 0, &objNameBufLen); 257 if (ntStatus != STATUS_INFO_LENGTH_MISMATCH) { 258 continue; 259 } 260 auto objNameBuf = MakeUnique<std::byte[]>(objNameBufLen); 261 ntStatus = ::NtQueryObject(entry.mHandle, 262 (OBJECT_INFORMATION_CLASS)ObjectNameInformation, 263 objNameBuf.get(), objNameBufLen, &objNameBufLen); 264 if (!NT_SUCCESS(ntStatus)) { 265 continue; 266 } 267 auto objNameInfo = 268 reinterpret_cast<OBJECT_NAME_INFORMATION*>(objNameBuf.get()); 269 if (!objNameInfo->Name.Length) { 270 continue; 271 } 272 nsDependentString objName(objNameInfo->Name.Buffer, 273 objNameInfo->Name.Length / sizeof(wchar_t)); 274 if (StringBeginsWith(objName, u"\\Device\\NamedPipe\\UIA_PIPE_"_ns)) { 275 aPids.AppendElement(entry.mPid); 276 } 277 } 278 } 279 280 static DWORD GetUiaClientPidWin10() { 281 // UIA creates a section of the form "HOOK_SHMEM_%08lx_%08lx_%08lx_%08lx" 282 constexpr auto kStrHookShmem = u"HOOK_SHMEM_"_ns; 283 // The second %08lx is the thread id. 284 nsAutoString sectionThread; 285 sectionThread.AppendPrintf("_%08lx_", ::GetCurrentThreadId()); 286 // This is the number of characters from the end of the section name where 287 // the sectionThread substring begins. 288 constexpr size_t sectionThreadRPos = 27; 289 // This is the length of sectionThread. 290 constexpr size_t sectionThreadLen = 10; 291 // Find any named Section that matches the naming convention of the UIA shared 292 // memory. There can only be one of these at a time, since this only exists 293 // while UIA is processing a request and it can only process a single request 294 // on a single thread. 295 nsAutoHandle section; 296 auto objectComparator = [&](const nsDependentSubstring& aName, 297 const nsDependentSubstring& aType) -> bool { 298 if (aType.Equals(u"Section"_ns) && FindInReadable(kStrHookShmem, aName) && 299 Substring(aName, aName.Length() - sectionThreadRPos, 300 sectionThreadLen) == sectionThread) { 301 // Get a handle to this section so we can get its kernel object and 302 // use that to find the handle for this section in the remote process. 303 section.own(::OpenFileMapping(GENERIC_READ, FALSE, 304 PromiseFlatString(aName).get())); 305 return false; 306 } 307 return true; 308 }; 309 if (!FindNamedObject(objectComparator) || !section) { 310 return 0; 311 } 312 313 // Now, find the kernel object associated with our section, the handle in the 314 // remote process associated with that kernel object and thus the remote 315 // process id. 316 NTSTATUS ntStatus; 317 const DWORD ourPid = ::GetCurrentProcessId(); 318 Maybe<PVOID> kernelObject; 319 static Maybe<USHORT> sectionObjTypeIndex; 320 nsTHashSet<uint32_t> nonSectionObjTypes; 321 nsTHashMap<nsVoidPtrHashKey, DWORD> objMap; 322 DWORD remotePid = 0; 323 FindHandle([&](auto aInfo, auto aHandle) { 324 // The mapping of the aInfo.mObjectTypeIndex field depends on the 325 // underlying OS kernel. As we scan through the handle list, we record the 326 // type indices such that we may use those values to skip over handles that 327 // refer to non-section objects. 328 if (sectionObjTypeIndex) { 329 // If we know the type index for Sections, that's the fastest check... 330 if (sectionObjTypeIndex.value() != aInfo.mObjectTypeIndex) { 331 // Not a section 332 return true; 333 } 334 } else if (nonSectionObjTypes.Contains( 335 static_cast<uint32_t>(aInfo.mObjectTypeIndex))) { 336 // Otherwise we check whether or not the object type is definitely _not_ 337 // a Section... 338 return true; 339 } else if (ourPid == aInfo.mPid) { 340 // Otherwise we need to issue some system calls to find out the object 341 // type corresponding to the current handle's type index. 342 ULONG objTypeBufLen; 343 ntStatus = ::NtQueryObject(aHandle, ObjectTypeInformation, nullptr, 0, 344 &objTypeBufLen); 345 if (ntStatus != STATUS_INFO_LENGTH_MISMATCH) { 346 return true; 347 } 348 auto objTypeBuf = MakeUnique<std::byte[]>(objTypeBufLen); 349 ntStatus = 350 ::NtQueryObject(aHandle, ObjectTypeInformation, objTypeBuf.get(), 351 objTypeBufLen, &objTypeBufLen); 352 if (!NT_SUCCESS(ntStatus)) { 353 return true; 354 } 355 auto objType = 356 reinterpret_cast<PUBLIC_OBJECT_TYPE_INFORMATION*>(objTypeBuf.get()); 357 // Now we check whether the object's type name matches "Section" 358 nsDependentSubstring objTypeName( 359 objType->TypeName.Buffer, objType->TypeName.Length / sizeof(wchar_t)); 360 if (!objTypeName.Equals(u"Section"_ns)) { 361 nonSectionObjTypes.Insert( 362 static_cast<uint32_t>(aInfo.mObjectTypeIndex)); 363 return true; 364 } 365 sectionObjTypeIndex = Some(aInfo.mObjectTypeIndex); 366 } 367 368 // At this point we know that aInfo references a Section object. 369 // Now we can do some actual tests on it. 370 if (ourPid != aInfo.mPid) { 371 if (kernelObject && kernelObject.value() == aInfo.mObject) { 372 // The kernel objects match -- we have found the remote pid! 373 remotePid = aInfo.mPid; 374 return false; 375 } 376 // An object that is not ours. Since we do not yet know which kernel 377 // object we're interested in, we'll save the current object for later. 378 objMap.InsertOrUpdate(aInfo.mObject, aInfo.mPid); 379 } else if (aHandle == section.get()) { 380 // This is the file mapping that we opened above. We save this mObject 381 // in order to compare to Section objects opened by other processes. 382 kernelObject = Some(aInfo.mObject); 383 } 384 return true; 385 }); 386 387 if (remotePid) { 388 return remotePid; 389 } 390 if (!kernelObject) { 391 return 0; 392 } 393 394 // If we reach here, we found kernelObject *after* we saw the remote process's 395 // copy. Now we must look it up in objMap. 396 if (objMap.Get(kernelObject.value(), &remotePid)) { 397 return remotePid; 398 } 399 400 return 0; 401 } 402 403 namespace mozilla { 404 namespace a11y { 405 406 void Compatibility::GetUiaClientPids(nsTArray<DWORD>& aPids) { 407 if (!::GetModuleHandleW(L"uiautomationcore.dll")) { 408 // UIAutomationCore isn't loaded, so there is no UIA client. 409 return; 410 } 411 if (IsWin11OrLater()) { 412 GetUiaClientPidsWin11::Run(aPids); 413 } else { 414 if (DWORD pid = GetUiaClientPidWin10()) { 415 aPids.AppendElement(pid); 416 } 417 } 418 } 419 420 } // namespace a11y 421 } // namespace mozilla