| File: | Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/Source/WebCore/accessibility/AXCrossProcessSearch.cpp |
| Warning: | line 97, column 23 Local variable 'object' is uncounted and unsafe |
Press '?' to see keyboard shortcuts
Keyboard shortcuts:
| 1 | /* |
| 2 | * Copyright (C) 2025 Apple Inc. All rights reserved. |
| 3 | * |
| 4 | * Redistribution and use in source and binary forms, with or without |
| 5 | * modification, are permitted provided that the following conditions |
| 6 | * are met: |
| 7 | * 1. Redistributions of source code must retain the above copyright |
| 8 | * notice, this list of conditions and the following disclaimer. |
| 9 | * 2. Redistributions in binary form must reproduce the above copyright |
| 10 | * notice, this list of conditions and the following disclaimer in the |
| 11 | * documentation and/or other materials provided with the distribution. |
| 12 | * |
| 13 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY |
| 14 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| 15 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| 16 | * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| 17 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| 18 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| 19 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON |
| 20 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 21 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
| 22 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 23 | */ |
| 24 | |
| 25 | #include "config.h" |
| 26 | #include "AXCrossProcessSearch.h" |
| 27 | |
| 28 | #include <WebCore/AXCoreObject.h> |
| 29 | #include <WebCore/AXObjectCache.h> |
| 30 | #include <WebCore/AXTreeStoreInlines.h> |
| 31 | #include <WebCore/Chrome.h> |
| 32 | #include <WebCore/ChromeClient.h> |
| 33 | #include <WebCore/LocalFrame.h> |
| 34 | #include <WebCore/Page.h> |
| 35 | #include <wtf/MainThread.h> |
| 36 | #include <wtf/MonotonicTime.h> |
| 37 | #include <wtf/RefCounted.h> |
| 38 | #include <wtf/StdLibExtras.h> |
| 39 | #include <wtf/threads/BinarySemaphore.h> |
| 40 | |
| 41 | #if PLATFORM(COCOA)(defined 1 && 1) |
| 42 | #include <CoreFoundation/CFRunLoop.h> |
| 43 | #endif |
| 44 | |
| 45 | namespace WebCore { |
| 46 | |
| 47 | // Spins the run loop on the main thread while waiting for a condition to become true. |
| 48 | template<typename Predicate> |
| 49 | static DidTimeout spinRunLoopUntil(Predicate&& isComplete, Seconds timeout) |
| 50 | { |
| 51 | AX_ASSERT(isMainThread())((void)0); |
| 52 | |
| 53 | auto deadline = MonotonicTime::now() + timeout; |
| 54 | while (MonotonicTime::now() < deadline) { |
| 55 | if (isComplete()) |
| 56 | return DidTimeout::No; |
| 57 | #if PLATFORM(COCOA)(defined 1 && 1) |
| 58 | CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.02, true); |
| 59 | #else |
| 60 | Thread::yield(); |
| 61 | #endif |
| 62 | } |
| 63 | return isComplete() ? DidTimeout::No : DidTimeout::Yes; |
| 64 | } |
| 65 | |
| 66 | DidTimeout AXCrossProcessSearchCoordinator::waitWithTimeout(Seconds timeout) |
| 67 | { |
| 68 | auto isComplete = [this] { |
| 69 | return m_searchComplete.load(std::memory_order_acquire) && !m_pendingCount.load(std::memory_order_acquire); |
| 70 | }; |
| 71 | |
| 72 | // If search is already complete with no pending requests, return immediately. |
| 73 | if (isComplete()) |
| 74 | return DidTimeout::No; |
| 75 | |
| 76 | if (isMainThread()) { |
| 77 | // On the main thread, we can't block on a semaphore because IPC callbacks |
| 78 | // need to run on the main thread. Instead, spin the run loop. |
| 79 | return spinRunLoopUntil(isComplete, timeout); |
| 80 | } |
| 81 | |
| 82 | // On background threads (e.g., the accessibility thread), we can safely |
| 83 | // block on the semaphore. |
| 84 | return m_semaphore.waitFor(timeout) ? DidTimeout::No : DidTimeout::Yes; |
| 85 | } |
| 86 | |
| 87 | // Helper to merge stream entries into AccessibilitySearchResults. |
| 88 | // If coordinator is provided, also pulls in remote results for RemoteFrame entries. |
| 89 | static AccessibilitySearchResults mergeStreamResults(const Vector<SearchResultEntry>& entries, unsigned limit, AXCrossProcessSearchCoordinator* coordinator) |
| 90 | { |
| 91 | AccessibilitySearchResults results; |
| 92 | for (const auto& entry : entries) { |
| 93 | if (results.size() >= limit) |
| 94 | break; |
| 95 | |
| 96 | if (entry.isLocalResult()) { |
| 97 | if (auto* object = entry.object()) |
Local variable 'object' is uncounted and unsafe | |
| 98 | results.append(AccessibilitySearchResult::local(*object)); |
| 99 | } else if (coordinator) { |
| 100 | // The search result was from an AXRemoteFrame we contain. Pull |
| 101 | // AccessibilityRemoteTokens from the search coordinator and |
| 102 | // convert them into results. |
| 103 | auto tokens = coordinator->takeRemoteResults(entry.streamIndex()); |
| 104 | for (auto& token : tokens) { |
| 105 | if (results.size() >= limit) |
| 106 | break; |
| 107 | results.append(AccessibilitySearchResult::remote(WTF::move(token))); |
| 108 | } |
| 109 | } |
| 110 | } |
| 111 | return results; |
| 112 | } |
| 113 | |
| 114 | #if PLATFORM(MAC)(defined 1 && 1) |
| 115 | // Computes remaining timeout from an absolute deadline, accounting for IPC overhead. |
| 116 | // Returns at least crossProcessSearchMinimumTimeout to ensure deeply nested frames |
| 117 | // always get some time to search. |
| 118 | static Seconds computeRemainingTimeout(std::optional<MonotonicTime> deadline) |
| 119 | { |
| 120 | if (!deadline) |
| 121 | return crossProcessSearchTimeout; |
| 122 | |
| 123 | auto remaining = *deadline - MonotonicTime::now() - crossProcessSearchIPCOverhead; |
| 124 | return std::max(crossProcessSearchMinimumTimeout, remaining); |
| 125 | } |
| 126 | |
| 127 | // Dispatches an IPC request to search a remote frame. |
| 128 | // The coordinator's responseReceived() will be called when the response arrives (or on failure). |
| 129 | static void dispatchRemoteFrameSearch(Ref<AXCrossProcessSearchCoordinator> coordinator, FrameIdentifier frameID, AccessibilitySearchCriteriaIPC criteria, size_t streamIndex, AXTreeID treeID) |
| 130 | { |
| 131 | ensureOnMainThread([coordinator = WTF::move(coordinator), frameID, criteria = WTF::move(criteria), streamIndex, treeID]() mutable { |
| 132 | AX_ASSERT(isMainThread())((void)0); |
| 133 | |
| 134 | WeakPtr cache = AXTreeStore<AXObjectCache>::axObjectCacheForID(treeID); |
| 135 | RefPtr page = cache ? cache->page() : nullptr; |
| 136 | if (!page) { |
| 137 | coordinator->responseReceived(); |
| 138 | return; |
| 139 | } |
| 140 | |
| 141 | page->chrome().client().performAccessibilitySearchInRemoteFrame(frameID, criteria, |
| 142 | [coordinator = WTF::move(coordinator), streamIndex](Vector<AccessibilityRemoteToken>&& tokens) mutable { |
| 143 | coordinator->storeRemoteResults(streamIndex, WTF::move(tokens)); |
| 144 | coordinator->responseReceived(); |
| 145 | }); |
| 146 | }); |
| 147 | } |
| 148 | #endif // PLATFORM(MAC) |
| 149 | |
| 150 | AccessibilitySearchResults performCrossProcessSearch(AccessibilitySearchResultStream&& stream, const AccessibilitySearchCriteriaIPC& criteriaForIPC, std::optional<AXTreeID> treeID, unsigned originalLimit, std::optional<FrameIdentifier> requestingFrameID) |
| 151 | { |
| 152 | #if !PLATFORM(MAC)(defined 1 && 1) |
| 153 | // On non-Mac platforms, return only local results without cross-process coordination. |
| 154 | // We do this because NSAccessibilityRemoteUIElement on macOS allows providing a reference |
| 155 | // to an actual element (i.e. a WebAccessibilityObjectWrapper), whereas AXRemoteElement doesn't |
| 156 | // seem to. If we solve that problem, this code can be made to work on iOS. |
| 157 | UNUSED_PARAM(criteriaForIPC)(void)criteriaForIPC; |
| 158 | UNUSED_PARAM(treeID)(void)treeID; |
| 159 | UNUSED_PARAM(requestingFrameID)(void)requestingFrameID; |
| 160 | return mergeStreamResults(stream.entries(), originalLimit, nullptr); |
| 161 | #else |
| 162 | if (!treeID) { |
| 163 | // No tree ID means we can't coordinate with remote frames. |
| 164 | return mergeStreamResults(stream.entries(), originalLimit, nullptr); |
| 165 | } |
| 166 | |
| 167 | // Calculate how many results to request from each remote frame. |
| 168 | // We need to account for local results that precede each remote frame in tree order. |
| 169 | Vector<std::pair<const SearchResultEntry*, unsigned>> remoteFrameRequests; |
| 170 | unsigned localCountSoFar = 0; |
| 171 | for (const auto& entry : stream.entries()) { |
| 172 | if (entry.isLocalResult()) |
| 173 | ++localCountSoFar; |
| 174 | else { |
| 175 | // For this remote frame, request enough to potentially fill remaining quota. |
| 176 | unsigned remaining = originalLimit > localCountSoFar ? originalLimit - localCountSoFar : 0; |
| 177 | // If we've already filled our quota with local results before this remote frame, |
| 178 | // we don't need to query it. |
| 179 | if (remaining > 0) |
| 180 | remoteFrameRequests.append({ &entry, remaining }); |
| 181 | } |
| 182 | } |
| 183 | |
| 184 | if (remoteFrameRequests.isEmpty()) { |
| 185 | // All remote frames were skipped because local results filled the quota. |
| 186 | return mergeStreamResults(stream.entries(), originalLimit, nullptr); |
| 187 | } |
| 188 | |
| 189 | // We have remote frames to query. Create a coordinator for synchronization. |
| 190 | Ref coordinator = AXCrossProcessSearchCoordinator::create(); |
| 191 | |
| 192 | if (requestingFrameID) { |
| 193 | // Pre-populate with requesting frame to prevent re-searching it. |
| 194 | coordinator->markFrameAsSearched(*requestingFrameID); |
| 195 | } |
| 196 | |
| 197 | // Dispatch IPC for each remote frame. |
| 198 | for (const auto& [entry, maxResults] : remoteFrameRequests) { |
| 199 | if (!entry->frameID()) { |
| 200 | // No frame ID, nothing to dispatch. |
| 201 | continue; |
| 202 | } |
| 203 | |
| 204 | // Skip frames we've already searched. |
| 205 | if (!coordinator->markFrameAsSearched(*entry->frameID())) |
| 206 | continue; |
| 207 | |
| 208 | coordinator->addPendingRequest(); |
| 209 | |
| 210 | auto slotCriteria = criteriaForIPC; |
| 211 | slotCriteria.resultsLimit = maxResults; |
| 212 | |
| 213 | dispatchRemoteFrameSearch(coordinator.copyRef(), *entry->frameID(), WTF::move(slotCriteria), entry->streamIndex(), *treeID); |
| 214 | } |
| 215 | |
| 216 | // Mark search complete (all remote frames have been dispatched). |
| 217 | coordinator->markSearchComplete(); |
| 218 | |
| 219 | // Wait for all responses using the cascading timeout (remaining time from deadline). |
| 220 | coordinator->waitWithTimeout(computeRemainingTimeout(criteriaForIPC.deadline)); |
| 221 | |
| 222 | // Merge results in tree order. |
| 223 | return mergeStreamResults(stream.entries(), originalLimit, coordinator.ptr()); |
| 224 | #endif // PLATFORM(MAC) |
| 225 | } |
| 226 | |
| 227 | AccessibilitySearchResults performSearchWithCrossProcessCoordination(AXCoreObject& anchorObject, AccessibilitySearchCriteria&& criteria) |
| 228 | { |
| 229 | #if !PLATFORM(MAC)(defined 1 && 1) |
| 230 | // On non-Mac platforms, just do a local search without cross-process coordination. |
| 231 | criteria.anchorObject = &anchorObject; |
| 232 | auto stream = AXSearchManager().findMatchingObjectsAsStream(WTF::move(criteria)); |
| 233 | return mergeStreamResults(stream.entries(), criteria.resultsLimit, nullptr); |
| 234 | #else |
| 235 | unsigned originalLimit = criteria.resultsLimit; |
| 236 | auto criteriaForIPC = AccessibilitySearchCriteriaIPC(criteria); |
| 237 | std::optional treeID = anchorObject.treeID(); |
| 238 | |
| 239 | // If no deadline has been set, set one now. This establishes the timeout budget |
| 240 | // for the entire search tree, ensuring nested frames share the same deadline. |
| 241 | if (!criteriaForIPC.deadline) |
| 242 | criteriaForIPC.deadline = MonotonicTime::now() + crossProcessSearchTimeout; |
| 243 | |
| 244 | if (!treeID) { |
| 245 | // No tree ID means we can't coordinate with remote frames. |
| 246 | criteria.anchorObject = &anchorObject; |
| 247 | auto stream = AXSearchManager().findMatchingObjectsAsStream(WTF::move(criteria)); |
| 248 | return mergeStreamResults(stream.entries(), originalLimit, nullptr); |
| 249 | } |
| 250 | |
| 251 | // Create coordinator upfront for eager IPC dispatch. |
| 252 | Ref coordinator = AXCrossProcessSearchCoordinator::create(); |
| 253 | |
| 254 | // Callback invoked when a remote frame is encountered during search. |
| 255 | // Dispatches IPC immediately so remote search runs in parallel with local search. |
| 256 | auto remoteFrameCallback = [&coordinator, &criteriaForIPC, originalLimit, treeID](FrameIdentifier frameID, size_t streamIndex, unsigned localResultCount) { |
| 257 | // Skip frames we've already searched. |
| 258 | if (!coordinator->markFrameAsSearched(frameID)) |
| 259 | return; |
| 260 | |
| 261 | // Calculate how many results we need from this remote frame. |
| 262 | unsigned remaining = originalLimit > localResultCount ? originalLimit - localResultCount : 0; |
| 263 | if (!remaining) { |
| 264 | // Local results already filled quota, skip this remote frame. |
| 265 | return; |
| 266 | } |
| 267 | |
| 268 | coordinator->addPendingRequest(); |
| 269 | |
| 270 | auto slotCriteria = criteriaForIPC; |
| 271 | slotCriteria.resultsLimit = remaining; |
| 272 | |
| 273 | dispatchRemoteFrameSearch(coordinator.copyRef(), frameID, WTF::move(slotCriteria), streamIndex, *treeID); |
| 274 | }; |
| 275 | |
| 276 | criteria.anchorObject = &anchorObject; |
| 277 | auto stream = AXSearchManager().findMatchingObjectsAsStream(WTF::move(criteria), WTF::move(remoteFrameCallback)); |
| 278 | |
| 279 | // Mark search complete so coordinator knows all remote frames have been encountered. |
| 280 | coordinator->markSearchComplete(); |
| 281 | |
| 282 | // Wait for all responses using the cascading timeout (remaining time from deadline). |
| 283 | coordinator->waitWithTimeout(computeRemainingTimeout(criteriaForIPC.deadline)); |
| 284 | |
| 285 | // Merge results in tree order. |
| 286 | return mergeStreamResults(stream.entries(), originalLimit, coordinator.ptr()); |
| 287 | #endif // PLATFORM(MAC) |
| 288 | } |
| 289 | |
| 290 | AccessibilitySearchResults mergeParentSearchResults(AccessibilitySearchResults&& localResults, Vector<AccessibilityRemoteToken>&& parentTokens, bool isForwardSearch, unsigned limit) |
| 291 | { |
| 292 | if (parentTokens.isEmpty()) |
| 293 | return WTF::move(localResults); |
| 294 | |
| 295 | if (isForwardSearch) { |
| 296 | // Forward search: local results first, then parent results (elements after the frame). |
| 297 | for (auto& token : parentTokens) { |
| 298 | if (localResults.size() >= limit) |
| 299 | break; |
| 300 | localResults.append(AccessibilitySearchResult::remote(WTF::move(token))); |
| 301 | } |
| 302 | return WTF::move(localResults); |
| 303 | } |
| 304 | |
| 305 | // Backward search: parent results first (elements before the frame), then local results. |
| 306 | AccessibilitySearchResults mergedResults; |
| 307 | unsigned localCount = localResults.size(); |
| 308 | for (auto& token : parentTokens) { |
| 309 | if (mergedResults.size() + localCount >= limit) |
| 310 | break; |
| 311 | mergedResults.append(AccessibilitySearchResult::remote(WTF::move(token))); |
| 312 | } |
| 313 | mergedResults.appendVector(WTF::move(localResults)); |
| 314 | return mergedResults; |
| 315 | } |
| 316 | |
| 317 | #if PLATFORM(MAC)(defined 1 && 1) |
| 318 | // Ref-counted class for safe parent search coordination across threads. |
| 319 | // This prevents use-after-free when the calling thread times out and returns |
| 320 | // before the IPC callback completes. |
| 321 | class ParentSearchContext : public RefCounted<ParentSearchContext> { |
| 322 | WTF_MAKE_NONCOPYABLE(ParentSearchContext)ParentSearchContext(const ParentSearchContext&) = delete; ParentSearchContext& operator=(const ParentSearchContext &) = delete;; |
| 323 | WTF_MAKE_TZONE_ALLOCATED_INLINE(ParentSearchContext)public: public: using HeapRef = ::bmalloc::api::HeapRef; using TZoneDescriptor = ::bmalloc::api::TZoneDescriptor; using TZoneMallocFallback = ::bmalloc::api::TZoneMallocFallback; using CompactAllocationMode = ::bmalloc::CompactAllocationMode; static constexpr bool usesTZoneHeap () { return true; } static constexpr unsigned inheritedSizeClass () { return ::bmalloc::TZone::sizeClass<ParentSearchContext >(); } static constexpr unsigned inheritedAlignment() { return ::bmalloc::TZone::alignment<ParentSearchContext>(); } __attribute__ ((always_inline)) inline void* operator new(size_t, void* p) { return p; } __attribute__((always_inline)) inline void* operator new[](size_t, void* p) { return p; } void* operator new[](size_t size) = delete; void operator delete[](void* p) = delete; __attribute__ ((always_inline)) inline void* operator new(size_t, NotNullTag , void* location) { ((void)0); return location; } void* operator new(size_t size) { static HeapRef s_heapRef; static const TZoneSpecification s_heapSpec = { &s_heapRef, TZoneSpecification::encodeSize <ParentSearchContext>(), TZoneSpecification::encodeAlignment <ParentSearchContext>(), TZoneSpecification::encodeCategory <ParentSearchContext>(), ::bmalloc::api::compactAllocationMode <ParentSearchContext>(), TZoneSpecification::encodeDescriptor <ParentSearchContext>(), }; if (!s_heapRef || size != sizeof (ParentSearchContext)) { [[unlikely]] if constexpr (::bmalloc ::api::compactAllocationMode<ParentSearchContext>() == CompactAllocationMode ::Compact) return ::bmalloc::api::tzoneAllocateCompactSlow(size , s_heapSpec); return ::bmalloc::api::tzoneAllocateNonCompactSlow (size, s_heapSpec); } ; if constexpr (::bmalloc::api::compactAllocationMode <ParentSearchContext>() == CompactAllocationMode::Compact ) return ::bmalloc::api::tzoneAllocateCompact(s_heapRef); return ::bmalloc::api::tzoneAllocateNonCompact(s_heapRef); } __attribute__ ((always_inline)) inline void operator delete(void* p) { ::bmalloc ::api::tzoneFree(p); } __attribute__((always_inline)) inline static void freeAfterDestruction(void* p) { ::bmalloc::api::tzoneFree (p); } using WTFIsFastMallocAllocated = int; private: using __makeBtzoneMallocedInlineMacroSemicolonifier __attribute__((unused)) = int; |
| 324 | public: |
| 325 | ParentSearchContext() = default; |
| 326 | |
| 327 | void signal() |
| 328 | { |
| 329 | if (m_shouldSignal.exchange(false, std::memory_order_acq_rel)) |
| 330 | m_semaphore.signal(); |
| 331 | } |
| 332 | |
| 333 | DidTimeout waitWithTimeout(Seconds timeout) |
| 334 | { |
| 335 | DidTimeout didTimeout; |
| 336 | if (isMainThread()) { |
| 337 | // On the main thread, we can't block on a semaphore because IPC callbacks |
| 338 | // need to run on the main thread. Instead, spin the run loop. |
| 339 | auto isComplete = [this] { |
| 340 | return !m_shouldSignal.load(std::memory_order_acquire); |
| 341 | }; |
| 342 | didTimeout = spinRunLoopUntil(isComplete, timeout); |
| 343 | } else |
| 344 | didTimeout = m_semaphore.waitFor(timeout) ? DidTimeout::No : DidTimeout::Yes; |
| 345 | |
| 346 | if (didTimeout == DidTimeout::Yes) |
| 347 | m_shouldSignal.exchange(false, std::memory_order_acq_rel); |
| 348 | return didTimeout; |
| 349 | } |
| 350 | |
| 351 | void markParentDispatched() { m_dispatchedParent.store(true, std::memory_order_release); } |
| 352 | bool didDispatchParent() const { return m_dispatchedParent.load(std::memory_order_acquire); } |
| 353 | |
| 354 | void setParentTokens(Vector<AccessibilityRemoteToken>&& tokens) |
| 355 | { |
| 356 | Locker locker { m_lock }; |
| 357 | m_parentTokens = WTF::move(tokens); |
| 358 | } |
| 359 | |
| 360 | Vector<AccessibilityRemoteToken> takeParentTokens() |
| 361 | { |
| 362 | Locker locker { m_lock }; |
| 363 | return std::exchange(m_parentTokens, { }); |
| 364 | } |
| 365 | |
| 366 | private: |
| 367 | BinarySemaphore m_semaphore; |
| 368 | std::atomic<bool> m_shouldSignal { true }; |
| 369 | std::atomic<bool> m_dispatchedParent { false }; |
| 370 | Lock m_lock; |
| 371 | Vector<AccessibilityRemoteToken> m_parentTokens WTF_GUARDED_BY_LOCK(m_lock)__attribute__((guarded_by(m_lock))); |
| 372 | }; |
| 373 | #endif // PLATFORM(MAC) |
| 374 | |
| 375 | AccessibilitySearchResults performSearchWithParentCoordination(AXCoreObject& anchorObject, AccessibilitySearchCriteria&& criteria, std::optional<FrameIdentifier> currentFrameID) |
| 376 | { |
| 377 | #if !PLATFORM(MAC)(defined 1 && 1) |
| 378 | UNUSED_PARAM(currentFrameID)(void)currentFrameID; |
| 379 | return performSearchWithCrossProcessCoordination(anchorObject, WTF::move(criteria)); |
| 380 | #else |
| 381 | // Save original parameters for parent coordination. |
| 382 | unsigned originalLimit = criteria.resultsLimit; |
| 383 | bool isForward = criteria.searchDirection == AccessibilitySearchDirection::Next; |
| 384 | auto criteriaForParent = AccessibilitySearchCriteriaIPC(criteria); |
| 385 | std::optional treeID = anchorObject.treeID(); |
| 386 | |
| 387 | // If no deadline has been set, set one now. This establishes the timeout budget |
| 388 | // for the entire search tree, ensuring nested frames share the same deadline. |
| 389 | if (!criteriaForParent.deadline) |
| 390 | criteriaForParent.deadline = MonotonicTime::now() + crossProcessSearchTimeout; |
| 391 | |
| 392 | // Use ref-counted context to safely coordinate between threads. |
| 393 | Ref context = adoptRef(*new ParentSearchContext); |
| 394 | |
| 395 | if (treeID) { |
| 396 | ensureOnMainThread([context, criteriaForParent, treeID, currentFrameID]() mutable { |
| 397 | WeakPtr cache = AXTreeStore<AXObjectCache>::axObjectCacheForID(*treeID); |
| 398 | RefPtr document = cache ? cache->document() : nullptr; |
| 399 | RefPtr frame = document ? document->frame() : nullptr; |
| 400 | RefPtr page = frame ? frame->page() : nullptr; |
| 401 | |
| 402 | if (!frame || !page || frame->isMainFrame() || !page->settings().siteIsolationEnabled()) { |
| 403 | // Not in a child frame, or site isolation is disabled (so no cross-process coordination needed). |
| 404 | context->signal(); |
| 405 | return; |
| 406 | } |
| 407 | |
| 408 | context->markParentDispatched(); |
| 409 | |
| 410 | // Use the provided frameID if available, otherwise use the frame's own ID. |
| 411 | FrameIdentifier frameIDToUse = currentFrameID.value_or(frame->frameID()); |
| 412 | |
| 413 | // Request full limit from parent - we'll truncate during merge. |
| 414 | page->chrome().client().continueAccessibilitySearchFromChildFrame(frameIDToUse, criteriaForParent, |
| 415 | [context](Vector<AccessibilityRemoteToken>&& tokens) mutable { |
| 416 | context->setParentTokens(WTF::move(tokens)); |
| 417 | context->signal(); |
| 418 | }); |
| 419 | }); |
| 420 | } |
| 421 | |
| 422 | // Perform local + nested remote frame search (runs in parallel with parent search). |
| 423 | auto searchResults = performSearchWithCrossProcessCoordination(anchorObject, WTF::move(criteria)); |
| 424 | |
| 425 | // Wait for parent search to complete using the cascading timeout. |
| 426 | if (treeID) |
| 427 | context->waitWithTimeout(computeRemainingTimeout(criteriaForParent.deadline)); |
| 428 | |
| 429 | // Merge parent results with local results based on search direction. |
| 430 | if (context->didDispatchParent()) |
| 431 | searchResults = mergeParentSearchResults(WTF::move(searchResults), context->takeParentTokens(), isForward, originalLimit); |
| 432 | |
| 433 | return searchResults; |
| 434 | #endif // PLATFORM(MAC) |
| 435 | } |
| 436 | |
| 437 | } // namespace WebCore |