-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Expand file tree
/
Copy pathAXCrossProcessSearch.cpp
More file actions
509 lines (428 loc) · 20.8 KB
/
AXCrossProcessSearch.cpp
File metadata and controls
509 lines (428 loc) · 20.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
/*
* Copyright (C) 2026 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "AXCrossProcessSearch.h"
#include <WebCore/AXCoreObject.h>
#include <WebCore/AXObjectCache.h>
#include <WebCore/AXTreeStoreInlines.h>
#include <WebCore/Chrome.h>
#include <WebCore/ChromeClient.h>
#include <WebCore/LocalFrame.h>
#include <WebCore/Page.h>
#include <wtf/MainThread.h>
#include <wtf/MonotonicTime.h>
#include <wtf/RefCounted.h>
#include <wtf/StdLibExtras.h>
#include <wtf/threads/BinarySemaphore.h>
#if PLATFORM(COCOA)
#include <CoreFoundation/CFRunLoop.h>
#endif
#if PLATFORM(MAC)
#define PLATFORM_SUPPORTS_REMOTE_SEARCH 1
#else
#define PLATFORM_SUPPORTS_REMOTE_SEARCH 0
#endif
namespace WebCore {
static bool s_shouldMockParentSearchResults = false;
void setShouldMockParentSearchResultsForTesting(bool enabled)
{
s_shouldMockParentSearchResults = enabled;
}
bool shouldMockParentSearchResultsForTesting()
{
return s_shouldMockParentSearchResults;
}
static bool s_shouldMockChildFrameSearchResults = false;
void setShouldMockChildFrameSearchResultsForTesting(bool enabled)
{
s_shouldMockChildFrameSearchResults = enabled;
}
bool shouldMockChildFrameSearchResultsForTesting()
{
return s_shouldMockChildFrameSearchResults;
}
static bool NODELETE canDoRemoteSearch(const std::optional<AXTreeID>& treeID)
{
#if PLATFORM_SUPPORTS_REMOTE_SEARCH
return treeID.has_value();
#else
UNUSED_PARAM(treeID);
return false;
#endif // PLATFORM_SUPPORTS_REMOTE_SEARCH
}
// Spins the run loop on the main thread while waiting for a condition to become true.
// In the future, we could consider changing callers to implement a solution that doesn't
// require polling as done in this function, since polling can be inefficient.
template<typename Predicate>
static DidTimeout spinRunLoopUntil(Predicate&& isComplete, Seconds timeout)
{
AX_ASSERT(isMainThread());
auto deadline = MonotonicTime::now() + timeout;
while (MonotonicTime::now() < deadline) {
if (isComplete())
return DidTimeout::No;
#if PLATFORM(COCOA)
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.02, true);
#else
Thread::yield();
#endif
}
return isComplete() ? DidTimeout::No : DidTimeout::Yes;
}
DidTimeout AXCrossProcessSearchCoordinator::waitWithTimeout(Seconds timeout)
{
auto isComplete = [this] {
return m_searchComplete.load(std::memory_order_acquire) && !m_pendingCount.load(std::memory_order_acquire);
};
// If search is already complete with no pending requests, return immediately.
if (isComplete())
return DidTimeout::No;
if (isMainThread()) {
// On the main thread, we can't block on a semaphore because IPC callbacks
// need to run on the main thread. Instead, spin the run loop.
return spinRunLoopUntil(isComplete, timeout);
}
// On background threads (e.g., the accessibility thread), we can safely
// block on the semaphore.
return m_semaphore.waitFor(timeout) ? DidTimeout::No : DidTimeout::Yes;
}
// Helper to merge stream entries into AccessibilitySearchResults.
// If coordinator is provided, also pulls in remote results for RemoteFrame entries.
static AccessibilitySearchResults mergeStreamResults(const Vector<SearchResultEntry>& entries, unsigned limit, AXCrossProcessSearchCoordinator* coordinator)
{
AccessibilitySearchResults results;
for (const auto& entry : entries) {
if (results.size() >= limit)
break;
if (RefPtr object = entry.objectIfLocalResult())
results.append(AccessibilitySearchResult::local(object.releaseNonNull()));
else if (coordinator) {
// The search result was from an AXRemoteFrame we contain. Pull
// AccessibilityRemoteTokens from the search coordinator and
// convert them into results.
auto tokens = coordinator->takeRemoteResults(entry.streamIndex());
for (auto& token : tokens) {
if (results.size() >= limit)
break;
results.append(AccessibilitySearchResult::remote(WTF::move(token)));
}
}
}
return results;
}
#if PLATFORM_SUPPORTS_REMOTE_SEARCH
// Computes remaining timeout from an absolute deadline, accounting for IPC overhead.
// Returns std::nullopt if the deadline has already passed (so callers can skip the search).
// Returns at least crossProcessSearchMinimumTimeout to ensure deeply nested frames
// always get some time to search.
static std::optional<Seconds> computeRemainingTimeout(std::optional<MonotonicTime> deadline)
{
if (!deadline)
return crossProcessSearchTimeout;
auto remaining = *deadline - MonotonicTime::now() - crossProcessSearchIPCOverhead;
if (remaining <= 1_ms)
return std::nullopt;
return std::max(crossProcessSearchMinimumTimeout, remaining);
}
// Dispatches an IPC request to search a remote frame.
// The coordinator's responseReceived() will be called when the response arrives (or on failure).
static void dispatchRemoteFrameSearch(Ref<AXCrossProcessSearchCoordinator> coordinator, FrameIdentifier frameID, AccessibilitySearchCriteriaIPC criteria, size_t streamIndex, AXTreeID treeID)
{
ensureOnMainThread([coordinator = WTF::move(coordinator), frameID, criteria = WTF::move(criteria), streamIndex, treeID]() mutable {
AX_ASSERT(isMainThread());
WeakPtr cache = AXTreeStore<AXObjectCache>::axObjectCacheForID(treeID);
RefPtr page = cache ? cache->page() : nullptr;
if (!page) {
coordinator->responseReceived();
return;
}
page->chrome().client().performAccessibilitySearchInRemoteFrame(frameID, criteria,
[coordinator = WTF::move(coordinator), streamIndex](Vector<AccessibilityRemoteToken>&& tokens) mutable {
coordinator->storeRemoteResults(streamIndex, WTF::move(tokens));
coordinator->responseReceived();
});
});
}
#endif // PLATFORM_SUPPORTS_REMOTE_SEARCH
AccessibilitySearchResults performCrossProcessSearch(AccessibilitySearchResultStream&& stream, const AccessibilitySearchCriteriaIPC& criteriaForIPC, std::optional<AXTreeID> treeID, unsigned originalLimit, std::optional<FrameIdentifier> requestingFrameID)
{
if (!canDoRemoteSearch(treeID)) {
UNUSED_PARAM(criteriaForIPC);
UNUSED_PARAM(requestingFrameID);
return mergeStreamResults(stream.entries(), originalLimit, nullptr);
}
#if PLATFORM_SUPPORTS_REMOTE_SEARCH
// Calculate how many results to request from each remote frame.
// We need to account for local results that precede each remote frame in tree order.
Vector<std::pair<const SearchResultEntry*, unsigned>> remoteFrameRequests;
unsigned localCountSoFar = 0;
for (const auto& entry : stream.entries()) {
if (entry.isLocalResult())
++localCountSoFar;
else {
// For this remote frame, request enough to potentially fill remaining quota.
unsigned remaining = originalLimit > localCountSoFar ? originalLimit - localCountSoFar : 0;
// If we've already filled our quota with local results before this remote frame,
// we don't need to query it.
if (remaining > 0)
remoteFrameRequests.append({ &entry, remaining });
}
}
if (remoteFrameRequests.isEmpty()) {
// All remote frames were skipped because local results filled the quota.
return mergeStreamResults(stream.entries(), originalLimit, nullptr);
}
// We have remote frames to query. Create a coordinator for synchronization.
Ref coordinator = AXCrossProcessSearchCoordinator::create();
if (requestingFrameID) {
// Pre-populate with requesting frame to prevent re-searching it.
coordinator->markFrameAsSearched(*requestingFrameID);
}
// Dispatch IPC for each remote frame.
for (const auto& [entry, maxResults] : remoteFrameRequests) {
if (!entry->frameID()) {
// No frame ID, nothing to dispatch.
continue;
}
// Skip frames we've already searched.
if (!coordinator->markFrameAsSearched(*entry->frameID()))
continue;
coordinator->addPendingRequest();
auto slotCriteria = criteriaForIPC;
slotCriteria.resultsLimit = maxResults;
dispatchRemoteFrameSearch(coordinator.copyRef(), *entry->frameID(), WTF::move(slotCriteria), entry->streamIndex(), *treeID);
}
// Mark search complete (all remote frames have been dispatched).
coordinator->markSearchComplete();
// Wait for all responses using the cascading timeout (remaining time from deadline).
if (std::optional remainingTimeout = computeRemainingTimeout(criteriaForIPC.deadline))
coordinator->waitWithTimeout(*remainingTimeout);
// Merge results in tree order.
return mergeStreamResults(stream.entries(), originalLimit, coordinator.ptr());
#else
RELEASE_ASSERT_NOT_REACHED();
#endif // PLATFORM_SUPPORTS_REMOTE_SEARCH
}
AccessibilitySearchResults performSearchWithCrossProcessCoordination(AXCoreObject& anchorObject, AccessibilitySearchCriteria&& criteria)
{
unsigned originalLimit = criteria.resultsLimit;
std::optional treeID = anchorObject.treeID();
if (!canDoRemoteSearch(treeID)) {
criteria.anchorObject = &anchorObject;
auto stream = AXSearchManager().findMatchingObjectsAsStream(WTF::move(criteria));
return mergeStreamResults(stream.entries(), originalLimit, nullptr);
}
#if PLATFORM_SUPPORTS_REMOTE_SEARCH
auto criteriaForIPC = AccessibilitySearchCriteriaIPC(criteria);
// If no deadline has been set, set one now. This establishes the timeout budget
// for the entire search tree, ensuring nested frames share the same deadline.
if (!criteriaForIPC.deadline)
criteriaForIPC.deadline = MonotonicTime::now() + crossProcessSearchTimeout;
// Create coordinator upfront for eager IPC dispatch.
Ref coordinator = AXCrossProcessSearchCoordinator::create();
// Callback invoked when a remote frame is encountered during search.
// Dispatches IPC immediately so remote search runs in parallel with local search.
auto remoteFrameCallback = [&coordinator, &criteriaForIPC, originalLimit, treeID](FrameIdentifier frameID, size_t streamIndex, unsigned localResultCount) {
// Skip frames we've already searched.
if (!coordinator->markFrameAsSearched(frameID))
return;
// Calculate how many results we need from this remote frame.
unsigned remaining = originalLimit > localResultCount ? originalLimit - localResultCount : 0;
if (!remaining) {
// Local results already filled quota, skip this remote frame.
return;
}
if (shouldMockChildFrameSearchResultsForTesting()) [[unlikely]]
return;
coordinator->addPendingRequest();
auto slotCriteria = criteriaForIPC;
slotCriteria.resultsLimit = remaining;
dispatchRemoteFrameSearch(coordinator.copyRef(), frameID, WTF::move(slotCriteria), streamIndex, *treeID);
};
criteria.anchorObject = &anchorObject;
auto stream = AXSearchManager().findMatchingObjectsAsStream(WTF::move(criteria), WTF::move(remoteFrameCallback));
// Mark search complete so coordinator knows all remote frames have been encountered.
coordinator->markSearchComplete();
// Wait for all responses using the cascading timeout (remaining time from deadline).
if (std::optional remainingTimeout = computeRemainingTimeout(criteriaForIPC.deadline))
coordinator->waitWithTimeout(*remainingTimeout);
// Merge results in tree order.
auto results = mergeStreamResults(stream.entries(), originalLimit, coordinator.ptr());
if (shouldMockChildFrameSearchResultsForTesting()) [[unlikely]] {
// Testing: inject the anchor as a mock child frame result. Tests should
// not rely on this being any specific object — just that *something* is
// returned from the child frame search.
results.append(AccessibilitySearchResult::local(anchorObject));
}
return results;
#else
RELEASE_ASSERT_NOT_REACHED();
#endif // PLATFORM_SUPPORTS_REMOTE_SEARCH
}
AccessibilitySearchResults mergeParentSearchResults(AccessibilitySearchResults&& localResults, Vector<AccessibilityRemoteToken>&& parentTokens, bool isForwardSearch, unsigned limit)
{
if (parentTokens.isEmpty())
return WTF::move(localResults);
if (isForwardSearch) {
// Forward search: local results first, then parent results (elements after the frame).
for (auto& token : parentTokens) {
if (localResults.size() >= limit)
break;
localResults.append(AccessibilitySearchResult::remote(WTF::move(token)));
}
return WTF::move(localResults);
}
// Backward search: parent results first (elements before the frame), then local results.
AccessibilitySearchResults mergedResults;
unsigned localCount = localResults.size();
for (auto& token : parentTokens) {
if (mergedResults.size() + localCount >= limit)
break;
mergedResults.append(AccessibilitySearchResult::remote(WTF::move(token)));
}
mergedResults.appendVector(WTF::move(localResults));
return mergedResults;
}
#if PLATFORM_SUPPORTS_REMOTE_SEARCH
// Ref-counted context for coordinating search continuation into a parent frame.
// When a child frame's search needs results from its parent frame (e.g. elements
// before or after the iframe in tree order), this context manages the IPC roundtrip
// and prevents use-after-free if the calling thread times out before the callback.
class ParentFrameSearchContext : public RefCounted<ParentFrameSearchContext> {
WTF_MAKE_NONCOPYABLE(ParentFrameSearchContext);
WTF_MAKE_TZONE_ALLOCATED_INLINE(ParentFrameSearchContext);
public:
ParentFrameSearchContext() = default;
void signal()
{
if (m_shouldSignal.exchange(false, std::memory_order_acq_rel))
m_semaphore.signal();
}
DidTimeout waitWithTimeout(Seconds timeout)
{
DidTimeout didTimeout;
if (isMainThread()) {
// On the main thread, we can't block on a semaphore because IPC callbacks
// need to run on the main thread. Instead, spin the run loop.
auto isComplete = [this] {
return !m_shouldSignal.load(std::memory_order_acquire);
};
didTimeout = spinRunLoopUntil(isComplete, timeout);
} else
didTimeout = m_semaphore.waitFor(timeout) ? DidTimeout::No : DidTimeout::Yes;
if (didTimeout == DidTimeout::Yes)
m_shouldSignal.exchange(false, std::memory_order_acq_rel);
return didTimeout;
}
void NODELETE markParentDispatched() { m_dispatchedParent.store(true, std::memory_order_release); }
bool NODELETE didDispatchParent() const { return m_dispatchedParent.load(std::memory_order_acquire); }
void setParentTokens(Vector<AccessibilityRemoteToken>&& tokens)
{
Locker locker { m_lock };
m_parentTokens = WTF::move(tokens);
}
Vector<AccessibilityRemoteToken> takeParentTokens()
{
Locker locker { m_lock };
return std::exchange(m_parentTokens, { });
}
private:
BinarySemaphore m_semaphore;
std::atomic<bool> m_shouldSignal { true };
std::atomic<bool> m_dispatchedParent { false };
Lock m_lock;
Vector<AccessibilityRemoteToken> m_parentTokens WTF_GUARDED_BY_LOCK(m_lock);
};
#endif // PLATFORM_SUPPORTS_REMOTE_SEARCH
AccessibilitySearchResults performSearchWithParentCoordination(AXCoreObject& anchorObject, AccessibilitySearchCriteria&& criteria, std::optional<FrameIdentifier> currentFrameID)
{
std::optional treeID = anchorObject.treeID();
if (!canDoRemoteSearch(treeID)) {
UNUSED_PARAM(currentFrameID);
return performSearchWithCrossProcessCoordination(anchorObject, WTF::move(criteria));
}
#if PLATFORM_SUPPORTS_REMOTE_SEARCH
// Save original parameters for parent coordination.
unsigned originalLimit = criteria.resultsLimit;
bool isForward = criteria.searchDirection == AccessibilitySearchDirection::Next;
auto criteriaForParent = AccessibilitySearchCriteriaIPC(criteria);
// If no deadline has been set, set one now. This establishes the timeout budget
// for the entire search tree, ensuring nested frames share the same deadline.
if (!criteriaForParent.deadline)
criteriaForParent.deadline = MonotonicTime::now() + crossProcessSearchTimeout;
// Use ref-counted context to safely coordinate between threads.
Ref context = adoptRef(*new ParentFrameSearchContext);
ensureOnMainThread([context, criteriaForParent, treeID, currentFrameID]() mutable {
WeakPtr cache = AXTreeStore<AXObjectCache>::axObjectCacheForID(*treeID);
RefPtr document = cache ? cache->document() : nullptr;
RefPtr frame = document ? document->frame() : nullptr;
RefPtr page = frame ? frame->page() : nullptr;
if (!frame || !page || frame->isMainFrame() || !page->settings().siteIsolationEnabled() || criteriaForParent.immediateDescendantsOnly) {
// Not in a child frame, site isolation is disabled, or this is an
// immediateDescendantsOnly search. In the latter case, the search is scoped
// to a specific container in the child frame, so the parent frame has nothing
// to contribute — skip the parent search to match non-site-isolation behavior
// and avoid returning unrelated elements (like the parent's ScrollBar).
context->signal();
return;
}
context->markParentDispatched();
if (shouldMockParentSearchResultsForTesting()) [[unlikely]] {
// Testing: provide a mock parent result instead of dispatching
// real IPC (which deadlocks in the test runner). This lets tests
// verify that the parent search is correctly skipped for
// immediateDescendantsOnly searches (i.e. the if just above).
context->signal();
return;
}
// Use the provided frameID if available, otherwise use the frame's own ID.
FrameIdentifier frameIDToUse = currentFrameID.value_or(frame->frameID());
// Request full limit from parent - we'll truncate during merge.
page->chrome().client().continueAccessibilitySearchFromChildFrame(frameIDToUse, criteriaForParent,
[context](Vector<AccessibilityRemoteToken>&& tokens) mutable {
context->setParentTokens(WTF::move(tokens));
context->signal();
});
});
// Perform local + nested remote frame search (runs in parallel with parent search).
auto searchResults = performSearchWithCrossProcessCoordination(anchorObject, WTF::move(criteria));
// Wait for parent search to complete using the cascading timeout.
if (auto remainingTimeout = computeRemainingTimeout(criteriaForParent.deadline))
context->waitWithTimeout(*remainingTimeout);
// Merge parent results with local results based on search direction.
if (context->didDispatchParent()) {
searchResults = mergeParentSearchResults(WTF::move(searchResults), context->takeParentTokens(), isForward, originalLimit);
if (shouldMockParentSearchResultsForTesting()) [[unlikely]] {
// Inject the anchor as a mock parent result. Tests should not rely on
// this being any specific object — just that *something* is returned
// from the parent search.
searchResults.append(AccessibilitySearchResult::local(anchorObject));
}
}
return searchResults;
#else
RELEASE_ASSERT_NOT_REACHED();
#endif // PLATFORM_SUPPORTS_REMOTE_SEARCH
}
} // namespace WebCore