Skip to content

AX: On macOS with site isolation enabled, VoiceOver cannot traverse into iframes because UIElementsForSearchPredicate only searches within the same process#58826

Merged
webkit-commit-queue merged 1 commit intoWebKit:mainfrom
twilco:eng/AX-On-macOS-with-site-isolation-enabled-VoiceOver-cannot-traverse-into-iframes-because-UIElementsForSearchPredicate-only-searches-the-local-process
Feb 27, 2026
Merged

AX: On macOS with site isolation enabled, VoiceOver cannot traverse into iframes because UIElementsForSearchPredicate only searches within the same process#58826
webkit-commit-queue merged 1 commit intoWebKit:mainfrom
twilco:eng/AX-On-macOS-with-site-isolation-enabled-VoiceOver-cannot-traverse-into-iframes-because-UIElementsForSearchPredicate-only-searches-the-local-process

Conversation

@twilco
Copy link
Contributor

@twilco twilco commented Feb 17, 2026

c1e1677

AX: On macOS with site isolation enabled, VoiceOver cannot traverse into iframes because UIElementsForSearchPredicate only searches within the same process
https://bugs.webkit.org/show_bug.cgi?id=307994
rdar://170487304

Reviewed by Joshua Hoffman.

This change implements cross-process accessibility search to support VoiceOver
navigation across site-isolated iframes. When a search encounters a remote frame,
it sends async IPC to the child process to continue the search, then continues its
search in its own process. The parent process is also queried via async IPC,
since the search may require moving beyond this frame (e.g. asking for the next
element at the last element in an iframe). When children + parent processes return
results, we merge them back in tree order, resolving AccessibilityRemoteToken results into
NSAccessibilityRemoteUIElements as necessary.

The search uses a single absolute deadline established when the top-level search begins,
rather than giving each nested frame its own independent timeout. When dispatching to child
frames, the remaining time until the deadline (minus IPC overhead buffer) is passed along,
ensuring deeply nested frame hierarchies share the original timeout budget. This prevents
timeout multiplication where N levels of nesting would otherwise allow N × timeout total
wait time, while still guaranteeing each frame gets at least a minimum search duration.

Other key changes:
  - Add AXCrossProcessSearch.{h,cpp} with coordination infrastructure for parallel
    IPC dispatch and cascading timeouts (described above) across nested frames

  - Refactor AXSearchManager to return AccessibilitySearchResultStream that records
    both local results and remote frame placeholders in tree order

  - Add ChromeClient methods for performAccessibilitySearchInRemoteFrame() and
    continueAccessibilitySearchFromChildFrame() to enable bidirectional search.

  - Add uiElementsForSearchPredicate() test helper for verifying multi-result searches.

  - Track accessibility enabled state in WebPageCreationParameters so newly-created
    web processes for site-isolated frames start with accessibility being enabled.
    This was required to make my three new tests pass because accessibility must be
    enabled by the time they got a cross-process accessibility search request.

  - Searched frames are tracked by ID and will not be searched multiple times.

New tests were added to exercise various interesting scenarios:
  - http/tests/site-isolation/accessibility/cross-process-search-headings.html
   * Ensures proper merging of local and remote results.
  - http/tests/site-isolation/accessibility/cross-process-search-nested-iframes.html
   * Verifies search progresses through multiple layers of iframes.
  - http/tests/site-isolation/accessibility/cross-process-search-traversal.html
   * Ensures that we can traverse all the way forwards and back through
     the all content on the page.

* LayoutTests/accessibility/ax-object-destroyed-on-reload-expected.txt:
* LayoutTests/accessibility/ax-object-destroyed-on-reload.html:
* LayoutTests/http/tests/site-isolation/accessibility/cross-process-search-headings-expected.txt: Added.
* LayoutTests/http/tests/site-isolation/accessibility/cross-process-search-headings.html: Added.
* LayoutTests/http/tests/site-isolation/accessibility/cross-process-search-nested-iframes-expected.txt: Added.
* LayoutTests/http/tests/site-isolation/accessibility/cross-process-search-nested-iframes.html: Added.
* LayoutTests/http/tests/site-isolation/accessibility/cross-process-search-traversal-expected.txt: Added.
* LayoutTests/http/tests/site-isolation/accessibility/cross-process-search-traversal.html: Added.
* LayoutTests/http/tests/site-isolation/accessibility/resources/iframe-with-headings.html: Added.
* LayoutTests/http/tests/site-isolation/accessibility/resources/nested-iframe-inner.html: Added.
* LayoutTests/http/tests/site-isolation/accessibility/resources/nested-iframe-middle.html: Added.
* LayoutTests/platform/ios/accessibility/ax-object-destroyed-on-reload-expected.txt:
* Source/WebCore/Headers.cmake:
* Source/WebCore/Sources.txt:
* Source/WebCore/WebCore.xcodeproj/project.pbxproj:
* Source/WebCore/accessibility/AXAnnouncementTypes.h: Added.
* Source/WebCore/accessibility/AXCoreObject.cpp:
(WebCore::AXCoreObject::partialOrder):
(WebCore::AXCoreObject::findMatchingObjectsWithin):
* Source/WebCore/accessibility/AXCoreObject.h:
* Source/WebCore/accessibility/AXCrossProcessSearch.cpp: Added.
(WebCore::canDoRemoteSearch):
(WebCore::spinRunLoopUntil):
(WebCore::AXCrossProcessSearchCoordinator::waitWithTimeout):
(WebCore::mergeStreamResults):
(WebCore::computeRemainingTimeout):
(WebCore::dispatchRemoteFrameSearch):
(WebCore::performCrossProcessSearch):
(WebCore::performSearchWithCrossProcessCoordination):
(WebCore::mergeParentSearchResults):
(WebCore::ParentFrameSearchContext::signal):
(WebCore::ParentFrameSearchContext::waitWithTimeout):
(WebCore::ParentFrameSearchContext::markParentDispatched):
(WebCore::ParentFrameSearchContext::didDispatchParent const):
(WebCore::ParentFrameSearchContext::setParentTokens):
(WebCore::ParentFrameSearchContext::takeParentTokens):
(WebCore::performSearchWithParentCoordination):
* Source/WebCore/accessibility/AXCrossProcessSearch.h: Added.
(WebCore::AXCrossProcessSearchCoordinator::create):
(WebCore::AXCrossProcessSearchCoordinator::addPendingRequest):
(WebCore::AXCrossProcessSearchCoordinator::markSearchComplete):
(WebCore::AXCrossProcessSearchCoordinator::responseReceived):
(WebCore::AXCrossProcessSearchCoordinator::storeRemoteResults):
(WebCore::AXCrossProcessSearchCoordinator::takeRemoteResults):
(WebCore::AXCrossProcessSearchCoordinator::markFrameAsSearched):
(WebCore::AXCrossProcessSearchCoordinator::checkCompletion):
* Source/WebCore/accessibility/AXLiveRegionManager.h:
(): Deleted.
* Source/WebCore/accessibility/AXLogger.cpp:
(WebCore::operator<<):
(WebCore::streamAXCoreObject):
* Source/WebCore/accessibility/AXObjectCache.cpp:
(WebCore::AXObjectCache::getOrCreateSlow):
* Source/WebCore/accessibility/AXObjectCache.h:
(WebCore::AccessibilityRemoteToken::AccessibilityRemoteToken): Deleted.
* Source/WebCore/accessibility/AXRemoteFrame.h:
* Source/WebCore/accessibility/AXSearchManager.cpp:
(WebCore::AXSearchManager::findMatchingObjectsAsStream):
(WebCore::AXSearchManager::findMatchingObjectsInternalAsStream):
(WebCore::AXSearchManager::findMatchingRange):
(WebCore::AXSearchManager::matchWithResultsLimit): Deleted.
(WebCore::AXSearchManager::findMatchingObjectsInternal): Deleted.
* Source/WebCore/accessibility/AXSearchManager.h:
(WebCore::AccessibilitySearchCriteriaIPC::AccessibilitySearchCriteriaIPC):
(WebCore::AccessibilitySearchCriteriaIPC::toSearchCriteria const):
(WebCore::SearchResultEntry::localResult):
(WebCore::SearchResultEntry::remoteFrame):
(WebCore::SearchResultEntry::isLocalResult const):
(WebCore::SearchResultEntry::isRemoteFrame const):
(WebCore::SearchResultEntry::objectIfLocalResult const):
(WebCore::SearchResultEntry::frameID const):
(WebCore::SearchResultEntry::streamIndex const):
(WebCore::SearchResultEntry::SearchResultEntry):
(WebCore::AccessibilitySearchResultStream::appendLocalResult):
(WebCore::AccessibilitySearchResultStream::appendRemoteFrame):
(WebCore::AccessibilitySearchResultStream::entries const):
(WebCore::AccessibilitySearchResultStream::entryCount const):
(WebCore::AccessibilitySearchResultStream::setResultsLimit):
(WebCore::AccessibilitySearchResultStream::resultsLimit const):
(WebCore::AccessibilitySearchResultStream::nextIndex const):
(WebCore::AccessibilitySearchResult::local):
(WebCore::AccessibilitySearchResult::remote):
(WebCore::AccessibilitySearchResult::isLocal const):
(WebCore::AccessibilitySearchResult::isRemote const):
(WebCore::AccessibilitySearchResult::objectIfLocalResult const):
(WebCore::AccessibilitySearchResult::remoteToken const):
(WebCore::AccessibilitySearchResult::AccessibilitySearchResult):
(WebCore::AXSearchManager::findMatchingObjects): Deleted.
* Source/WebCore/accessibility/AXStitchUtilities.h:
* Source/WebCore/accessibility/AccessibilityMockObject.h:
* Source/WebCore/accessibility/AccessibilityNodeObject.cpp:
* Source/WebCore/accessibility/AccessibilityObject.cpp:
(WebCore::AccessibilityObject::findMatchingObjectsWithin):
(WebCore::AccessibilityObject::findMatchingObjects): Deleted.
* Source/WebCore/accessibility/AccessibilityObject.h:
* Source/WebCore/accessibility/AccessibilityRemoteToken.h: Added.
(WebCore::AccessibilityRemoteToken::AccessibilityRemoteToken):
* Source/WebCore/accessibility/AccessibilityRenderObject.cpp:
* Source/WebCore/accessibility/AccessibilityScrollView.cpp:
(WebCore::AccessibilityScrollView::detachRemoteParts):
* Source/WebCore/accessibility/AccessibilityScrollView.h:
* Source/WebCore/accessibility/AccessibilitySearchCriteriaIPC.h: Added.
* Source/WebCore/accessibility/ios/WebAccessibilityObjectWrapperIOS.mm:
(-[WebAccessibilityObjectWrapper accessibilityFindMatchingObjects:]):
* Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.cpp:
(WebCore::AXIsolatedObject::findMatchingObjects): Deleted.
* Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.h:
* Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.cpp:
(WebCore::AXIsolatedTree::applyPendingChangesLocked):
* Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.h:
* Source/WebCore/accessibility/isolatedtree/mac/AXIsolatedObjectMac.mm:
(WebCore::appendPlatformProperties):
* Source/WebCore/accessibility/mac/WebAccessibilityObjectWrapperMac.mm:
(remoteElementFromToken):
(searchResultsToNSArray):
(performSearchWithRemoteFrames):
(-[WebAccessibilityObjectWrapper _accessibilityHitTestResolvingRemoteFrame:callback:]):
(-[WebAccessibilityObjectWrapper accessibilityAttributeValue:forParameter:]):
* Source/WebCore/page/ChromeClient.cpp:
(WebCore::ChromeClient::performAccessibilitySearchInRemoteFrame):
(WebCore::ChromeClient::continueAccessibilitySearchFromChildFrame):
* Source/WebCore/page/ChromeClient.h:
* Source/WebCore/page/FrameTree.h:
* Source/WebKit/Scripts/webkit/messages.py:
(headers_for_type):
* Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in:
* Source/WebKit/UIProcess/WebPageProxy.cpp:
(WebKit::WebPageProxy::performAccessibilitySearchInRemoteFrame):
(WebKit::WebPageProxy::continueAccessibilitySearchFromChildFrame):
* Source/WebKit/UIProcess/WebPageProxy.h:
* Source/WebKit/UIProcess/WebPageProxy.messages.in:
* Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp:
(WebKit::WebChromeClient::performAccessibilitySearchInRemoteFrame):
(WebKit::WebChromeClient::continueAccessibilitySearchFromChildFrame):
* Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.h:
* Source/WebKit/WebProcess/WebPage/Cocoa/WebPageCocoa.mm:
(WebKit::tokenFromWrapper):
(WebKit::convertSearchResultsToRemoteTokens):
(WebKit::WebPage::performAccessibilitySearchInRemoteFrame):
(WebKit::WebPage::continueAccessibilitySearchInParentFrame):
* Source/WebKit/WebProcess/WebPage/WebPage.h:
* Source/WebKit/WebProcess/WebPage/WebPage.messages.in:
* Tools/WebKitTestRunner/InjectedBundle/AccessibilityUIElement.cpp:
(WTR::AccessibilityUIElement::uiElementsForSearchPredicate):
* Tools/WebKitTestRunner/InjectedBundle/AccessibilityUIElement.h:
* Tools/WebKitTestRunner/InjectedBundle/Bindings/AccessibilityUIElement.idl:
* Tools/WebKitTestRunner/InjectedBundle/atspi/AccessibilityUIElementAtspi.cpp:
(WTR::AccessibilityUIElementAtspi::uiElementsForSearchPredicate):
* Tools/WebKitTestRunner/InjectedBundle/atspi/AccessibilityUIElementAtspi.h:
* Tools/WebKitTestRunner/InjectedBundle/ios/AccessibilityUIElementIOS.h:
* Tools/WebKitTestRunner/InjectedBundle/ios/AccessibilityUIElementIOS.mm:
(WTR::AccessibilityUIElementIOS::uiElementsForSearchPredicate):
* Tools/WebKitTestRunner/InjectedBundle/mac/AccessibilityUIElementMac.h:
* Tools/WebKitTestRunner/InjectedBundle/mac/AccessibilityUIElementMac.mm:
(WTR::AccessibilityUIElementMac::attributeValue const):
(WTR::AccessibilityUIElementMac::allAttributes):
(WTR::AccessibilityUIElementMac::setBoolAttributeValue):
(WTR::AccessibilityUIElementMac::setValue):
(WTR::AccessibilityUIElementMac::isAttributeSupported):
(WTR::AccessibilityUIElementMac::isValid const):
(WTR::AccessibilityUIElementMac::uiElementsForSearchPredicate):
(WTR::AccessibilityUIElementMac::setSelectedTextRange):
(WTR::AccessibilityUIElementMac::invokeCustomActionAtIndex):
(WTR::AccessibilityUIElementMac::setSelectedTextMarkerRange):
(WTR::AccessibilityUIElementMac::setSelectedChild const):
(WTR::AccessibilityUIElementMac::setSelectedChildAtIndex const):
(WTR::AccessibilityUIElementMac::removeSelectionAtIndex const):
(WTR::AccessibilityUIElementMac::takeFocus):
(WTR::AccessibilityUIElementMac::resetSelectedTextMarkerRange):
* Tools/WebKitTestRunner/InjectedBundle/playstation/AccessibilityUIElementPlayStation.cpp:
(WTR::AccessibilityUIElementPlayStation::uiElementsForSearchPredicate):
* Tools/WebKitTestRunner/InjectedBundle/playstation/AccessibilityUIElementPlayStation.h:
* Tools/WebKitTestRunner/InjectedBundle/win/AccessibilityUIElementWin.cpp:
(WTR::AccessibilityUIElementWin::uiElementsForSearchPredicate):
* Tools/WebKitTestRunner/InjectedBundle/win/AccessibilityUIElementWin.h:

Canonical link: https://commits.webkit.org/308321@main

2562f37

Misc iOS, visionOS, tvOS & watchOS macOS Linux Windows Apple Internal
✅ 🧪 style ✅ 🛠 ios ✅ 🛠 mac ✅ 🛠 wpe ✅ 🛠 win ⏳ 🛠 ios-apple
✅ 🧪 bindings ✅ 🛠 ios-sim ✅ 🛠 mac-AS-debug ✅ 🧪 wpe-wk2 ✅ 🧪 win-tests ⏳ 🛠 mac-apple
✅ 🧪 webkitperl ✅ 🧪 ios-wk2 ✅ 🧪 api-mac ✅ 🧪 api-wpe ⏳ 🛠 vision-apple
✅ 🧪 webkitpy ✅ 🧪 ios-wk2-wpt ✅ 🧪 api-mac-debug ✅ 🛠 gtk3-libwebrtc
✅ 🧪 api-ios ✅ 🧪 mac-wk1 ✅ 🛠 gtk
❌ 🛠 ios-safer-cpp ✅ 🧪 mac-wk2 ✅ 🧪 gtk-wk2
✅ 🛠 vision ✅ 🧪 mac-AS-debug-wk2 ✅ 🧪 api-gtk
✅ 🛠 🧪 merge ✅ 🛠 vision-sim ✅ 🧪 mac-wk2-stress ✅ 🛠 playstation
✅ 🧪 vision-wk2 ✅ 🧪 mac-intel-wk2
✅ 🛠 tv ✅ 🛠 mac-safer-cpp
✅ 🛠 tv-sim
✅ 🛠 watch
✅ 🛠 watch-sim

@twilco twilco requested review from a team and cdumez as code owners February 17, 2026 01:32
@twilco twilco self-assigned this Feb 17, 2026
@twilco twilco added the Accessibility For bugs related to accessibility. label Feb 17, 2026
@twilco twilco force-pushed the eng/AX-On-macOS-with-site-isolation-enabled-VoiceOver-cannot-traverse-into-iframes-because-UIElementsForSearchPredicate-only-searches-the-local-process branch from 3852830 to 0dd5954 Compare February 17, 2026 01:36
@webkit-ews-buildbot webkit-ews-buildbot added the merging-blocked Applied to prevent a change from being merged label Feb 17, 2026
@twilco twilco force-pushed the eng/AX-On-macOS-with-site-isolation-enabled-VoiceOver-cannot-traverse-into-iframes-because-UIElementsForSearchPredicate-only-searches-the-local-process branch from 0dd5954 to b2753a3 Compare February 17, 2026 02:40
@webkit-ews-buildbot
Copy link
Collaborator

Safer C++ Build #81085 (b2753a3)

❌ Found 4 failing files with 4 issues. Please address these issues before landing. See WebKit Guidelines for Safer C++ Programming.
(cc @rniwa)

@twilco twilco force-pushed the eng/AX-On-macOS-with-site-isolation-enabled-VoiceOver-cannot-traverse-into-iframes-because-UIElementsForSearchPredicate-only-searches-the-local-process branch from b2753a3 to 5dc6d38 Compare February 17, 2026 15:55
Copy link
Contributor

@minorninth minorninth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! A few ideas and questions, but no architectural concerns, this looks like a very comprehensive design

// Token used to identify accessibility objects across process boundaries.
// On Mac, this is an opaque byte array from NSAccessibilityRemoteUIElement.
// On other platforms, it's a UUID and process ID pair.
struct AccessibilityRemoteToken {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for abstracting this!


// FIXME: This should be replaced by AXDirection (or vice versa).
enum class AccessibilitySearchDirection {
enum class AccessibilitySearchDirection : uint8_t {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: This might make sense if AccessibilitySearchDirection is in a packed struct and saves a significant number of bytes, but could actually hurt performance in other cases (adds a truncation/masking instruction every time a value that's not already uint8 is assigned).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally makes sense! The motivation for doing this is because we are now sending this over IPC between web content processes, and having an explicitly defined size is required for that.


// Spins the run loop on the main thread while waiting for a condition to become true.
template<typename Predicate>
static DidTimeout spinRunLoopUntil(Predicate&& isComplete, Seconds timeout)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems totally reasonable for now, but should we add a comment or file a radar about updating this to exit the run loop immediately when the condition is hit, enabling using a longer timeout rather than polling?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment!

// convert them into results.
auto tokens = coordinator->takeRemoteResults(entry.streamIndex());
for (auto& token : tokens) {
if (results.size() >= limit)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels weirdly asymmetric to be checking the size at the top of the outer for loop, and also at the top of the inner for loop.

What about instead, just check after each append:

for (const auto& entry : entries) {
    if (RefPtr object = entry.objectIfLocalResult()) {
      results.append(...);
      if (results.size() >= limit)
        break;
    } else if (coordinator) {
      ...
      for (auto& token : tokens) {
        results.append(...);
        if (results.size() >= limit)
          break;
      }
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your suggestion would break out of the inner for loop, but not the outer one, which is why the code is structured as-is. Happy to adjust though if you have another idea!

return crossProcessSearchTimeout;

auto remaining = *deadline - MonotonicTime::now() - crossProcessSearchIPCOverhead;
return std::max(crossProcessSearchMinimumTimeout, remaining);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's say a process receives the IPC super late, after the query timed out. We should probably just skip the search in that case, otherwise when a process finally becomes unblocked it could waste time servicing a long queue of expired queries.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! Will fix in latest commit.

criteriaForIPC.deadline = MonotonicTime::now() + crossProcessSearchTimeout;

if (!treeID) {
// No tree ID means we can't coordinate with remote frames.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be worth merging this with the non-Mac case? Like basically have a canDoRemoteSearch flag that's always false on non-Mac, but if there's no treeID, Mac could set that flag - then have a single fallback block.

Another idea is that for debugging we might want to have a flag to disable remote search.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

}

#if PLATFORM(MAC)
// Ref-counted class for safe parent search coordination across threads.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does Parent here mean "Parent Frame"? Or does it mean the caller of the IPC? I get the issue with preventing UAF if the callback comes after the timeout, but it's not clear what "parent" here refers to, it feels like it should actually relate to whoever initiated the search.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does mean "Parent frame" — it's the context we send to the parent frame via IPC to continue the search from where we, a child frame, are. I will rename this to ParentFrameSearchContext since maybe that could help avoid confusion?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also improved the code comment to help better describe what's happening here.

if (auto token = tokenFromWrapper(protect(object->wrapper())))
results.append(WTF::move(*token));
} else if (result.isRemote())
results.append(*result.remoteToken());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thought: it probably wouldn't be that wasteful to store a tiny bit more info here other than the remote token - you could also return the role of the result, for example, enabling tests to provide more info than just a bunch of remote tokens.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I explored this. I agree that it wouldn't be overly wasteful or costly to do it, but the mechanics of piping the information through ended up being a bit complicated. I think a better option may just be porting these tests to be client tests in a later PR, where we can get the role for free.

@webkit-ews-buildbot
Copy link
Collaborator

Safer C++ Build #81154 (5dc6d38)

❌ Found 2 failing files with 2 issues. Please address these issues before landing. See WebKit Guidelines for Safer C++ Programming.
(cc @rniwa)

twilco added a commit to twilco/WebKit that referenced this pull request Feb 18, 2026
https://bugs.webkit.org/show_bug.cgi?id=308161
rdar://170668752

Reviewed by NOBODY (OOPS!).

When accessibility is enabled via enableAccessibilityForAllProcesses,
set a flag that this has happened so that we can eagerly also enable
accessibility in any new processes that are created (e.g. via site
isolated iframes). This is important because otherwise the new web
content process may reject accessibility state synchronization from
other processes, or reject accessibility requests that stared in another
web content process and moved to this one (i.e. for WebKit#58826
which propagates UIElementsForSearchPredicate requests between processes).

* Source/WebKit/Shared/WebPageCreationParameters.h:
* Source/WebKit/Shared/WebPageCreationParameters.serialization.in:
* Source/WebKit/UIProcess/WebPageProxy.cpp:
(WebKit::WebPageProxy::enableAccessibilityForAllProcesses):
(WebKit::WebPageProxy::creationParameters):
* Source/WebKit/UIProcess/WebPageProxy.h:
* Source/WebKit/WebProcess/InjectedBundle/API/c/WKBundleFrame.cpp:
(_WKAccessibilityRootObjectForTesting):
@twilco twilco force-pushed the eng/AX-On-macOS-with-site-isolation-enabled-VoiceOver-cannot-traverse-into-iframes-because-UIElementsForSearchPredicate-only-searches-the-local-process branch from 5dc6d38 to ff88ed5 Compare February 18, 2026 23:09
@twilco twilco force-pushed the eng/AX-On-macOS-with-site-isolation-enabled-VoiceOver-cannot-traverse-into-iframes-because-UIElementsForSearchPredicate-only-searches-the-local-process branch from ff88ed5 to 69a20d9 Compare February 18, 2026 23:13
@twilco twilco force-pushed the eng/AX-On-macOS-with-site-isolation-enabled-VoiceOver-cannot-traverse-into-iframes-because-UIElementsForSearchPredicate-only-searches-the-local-process branch from 69a20d9 to d165df5 Compare February 19, 2026 05:03
webkit-commit-queue pushed a commit that referenced this pull request Feb 19, 2026
https://bugs.webkit.org/show_bug.cgi?id=308161
rdar://170668752

Reviewed by Joshua Hoffman.

When accessibility is enabled via enableAccessibilityForAllProcesses,
set a flag that this has happened so that we can eagerly also enable
accessibility in any new processes that are created (e.g. via site
isolated iframes). This is important because otherwise the new web
content process may reject accessibility state synchronization from
other processes, or reject accessibility requests that stared in another
web content process and moved to this one (i.e. for #58826
which propagates UIElementsForSearchPredicate requests between processes).

* Source/WebKit/Shared/WebPageCreationParameters.h:
* Source/WebKit/Shared/WebPageCreationParameters.serialization.in:
* Source/WebKit/UIProcess/WebPageProxy.cpp:
(WebKit::WebPageProxy::enableAccessibilityForAllProcesses):
(WebKit::WebPageProxy::creationParameters):
* Source/WebKit/UIProcess/WebPageProxy.h:
* Source/WebKit/WebProcess/InjectedBundle/API/c/WKBundleFrame.cpp:
(_WKAccessibilityRootObjectForTesting):

Canonical link: https://commits.webkit.org/307836@main
@twilco twilco force-pushed the eng/AX-On-macOS-with-site-isolation-enabled-VoiceOver-cannot-traverse-into-iframes-because-UIElementsForSearchPredicate-only-searches-the-local-process branch from d165df5 to 71d544f Compare February 26, 2026 16:04
@webkit-ews-buildbot
Copy link
Collaborator

iOS Safer C++ Build #1174 (71d544f)

❌ Found 3 failing files with 3 issues. Please address these issues before landing. See WebKit Guidelines for Safer C++ Programming.
(cc @rniwa)

@rniwa
Copy link
Member

rniwa commented Feb 26, 2026

only searches the local process

I'd rephrase that to:

only searches within the same process

it's not like we have "the" local process. It's that we're doing the search within a single process.

@webkit-ews-buildbot
Copy link
Collaborator

macOS Safer C++ Build #82882 (71d544f)

❌ Found 1 failing file with 1 issue. Please address these issues before landing. See WebKit Guidelines for Safer C++ Programming.
(cc @rniwa)

@twilco twilco changed the title AX: On macOS with site isolation enabled, VoiceOver cannot traverse into iframes because UIElementsForSearchPredicate only searches the local process AX: On macOS with site isolation enabled, VoiceOver cannot traverse into iframes because UIElementsForSearchPredicate only searches within the same process Feb 26, 2026
@twilco twilco force-pushed the eng/AX-On-macOS-with-site-isolation-enabled-VoiceOver-cannot-traverse-into-iframes-because-UIElementsForSearchPredicate-only-searches-the-local-process branch from 71d544f to 2562f37 Compare February 26, 2026 18:59
@twilco
Copy link
Contributor Author

twilco commented Feb 26, 2026

only searches the local process

I'd rephrase that to:

only searches within the same process

it's not like we have "the" local process. It's that we're doing the search within a single process.

Fixed, thanks!

@webkit-ews-buildbot
Copy link
Collaborator

iOS Safer C++ Build #1214 (2562f37)

❌ Found 1 failing file with 1 issue. Please address these issues before landing. See WebKit Guidelines for Safer C++ Programming.
(cc @rniwa)

@twilco
Copy link
Contributor Author

twilco commented Feb 27, 2026

iOS safer-cpp warning is for code I didn't touch in this PR, so going to merge.

@twilco twilco added merge-queue Applied to send a pull request to merge-queue and removed merging-blocked Applied to prevent a change from being merged labels Feb 27, 2026
…nto iframes because UIElementsForSearchPredicate only searches within the same process

https://bugs.webkit.org/show_bug.cgi?id=307994
rdar://170487304

Reviewed by Joshua Hoffman.

This change implements cross-process accessibility search to support VoiceOver
navigation across site-isolated iframes. When a search encounters a remote frame,
it sends async IPC to the child process to continue the search, then continues its
search in its own process. The parent process is also queried via async IPC,
since the search may require moving beyond this frame (e.g. asking for the next
element at the last element in an iframe). When children + parent processes return
results, we merge them back in tree order, resolving AccessibilityRemoteToken results into
NSAccessibilityRemoteUIElements as necessary.

The search uses a single absolute deadline established when the top-level search begins,
rather than giving each nested frame its own independent timeout. When dispatching to child
frames, the remaining time until the deadline (minus IPC overhead buffer) is passed along,
ensuring deeply nested frame hierarchies share the original timeout budget. This prevents
timeout multiplication where N levels of nesting would otherwise allow N × timeout total
wait time, while still guaranteeing each frame gets at least a minimum search duration.

Other key changes:
  - Add AXCrossProcessSearch.{h,cpp} with coordination infrastructure for parallel
    IPC dispatch and cascading timeouts (described above) across nested frames

  - Refactor AXSearchManager to return AccessibilitySearchResultStream that records
    both local results and remote frame placeholders in tree order

  - Add ChromeClient methods for performAccessibilitySearchInRemoteFrame() and
    continueAccessibilitySearchFromChildFrame() to enable bidirectional search.

  - Add uiElementsForSearchPredicate() test helper for verifying multi-result searches.

  - Track accessibility enabled state in WebPageCreationParameters so newly-created
    web processes for site-isolated frames start with accessibility being enabled.
    This was required to make my three new tests pass because accessibility must be
    enabled by the time they got a cross-process accessibility search request.

  - Searched frames are tracked by ID and will not be searched multiple times.

New tests were added to exercise various interesting scenarios:
  - http/tests/site-isolation/accessibility/cross-process-search-headings.html
   * Ensures proper merging of local and remote results.
  - http/tests/site-isolation/accessibility/cross-process-search-nested-iframes.html
   * Verifies search progresses through multiple layers of iframes.
  - http/tests/site-isolation/accessibility/cross-process-search-traversal.html
   * Ensures that we can traverse all the way forwards and back through
     the all content on the page.

* LayoutTests/accessibility/ax-object-destroyed-on-reload-expected.txt:
* LayoutTests/accessibility/ax-object-destroyed-on-reload.html:
* LayoutTests/http/tests/site-isolation/accessibility/cross-process-search-headings-expected.txt: Added.
* LayoutTests/http/tests/site-isolation/accessibility/cross-process-search-headings.html: Added.
* LayoutTests/http/tests/site-isolation/accessibility/cross-process-search-nested-iframes-expected.txt: Added.
* LayoutTests/http/tests/site-isolation/accessibility/cross-process-search-nested-iframes.html: Added.
* LayoutTests/http/tests/site-isolation/accessibility/cross-process-search-traversal-expected.txt: Added.
* LayoutTests/http/tests/site-isolation/accessibility/cross-process-search-traversal.html: Added.
* LayoutTests/http/tests/site-isolation/accessibility/resources/iframe-with-headings.html: Added.
* LayoutTests/http/tests/site-isolation/accessibility/resources/nested-iframe-inner.html: Added.
* LayoutTests/http/tests/site-isolation/accessibility/resources/nested-iframe-middle.html: Added.
* LayoutTests/platform/ios/accessibility/ax-object-destroyed-on-reload-expected.txt:
* Source/WebCore/Headers.cmake:
* Source/WebCore/Sources.txt:
* Source/WebCore/WebCore.xcodeproj/project.pbxproj:
* Source/WebCore/accessibility/AXAnnouncementTypes.h: Added.
* Source/WebCore/accessibility/AXCoreObject.cpp:
(WebCore::AXCoreObject::partialOrder):
(WebCore::AXCoreObject::findMatchingObjectsWithin):
* Source/WebCore/accessibility/AXCoreObject.h:
* Source/WebCore/accessibility/AXCrossProcessSearch.cpp: Added.
(WebCore::canDoRemoteSearch):
(WebCore::spinRunLoopUntil):
(WebCore::AXCrossProcessSearchCoordinator::waitWithTimeout):
(WebCore::mergeStreamResults):
(WebCore::computeRemainingTimeout):
(WebCore::dispatchRemoteFrameSearch):
(WebCore::performCrossProcessSearch):
(WebCore::performSearchWithCrossProcessCoordination):
(WebCore::mergeParentSearchResults):
(WebCore::ParentFrameSearchContext::signal):
(WebCore::ParentFrameSearchContext::waitWithTimeout):
(WebCore::ParentFrameSearchContext::markParentDispatched):
(WebCore::ParentFrameSearchContext::didDispatchParent const):
(WebCore::ParentFrameSearchContext::setParentTokens):
(WebCore::ParentFrameSearchContext::takeParentTokens):
(WebCore::performSearchWithParentCoordination):
* Source/WebCore/accessibility/AXCrossProcessSearch.h: Added.
(WebCore::AXCrossProcessSearchCoordinator::create):
(WebCore::AXCrossProcessSearchCoordinator::addPendingRequest):
(WebCore::AXCrossProcessSearchCoordinator::markSearchComplete):
(WebCore::AXCrossProcessSearchCoordinator::responseReceived):
(WebCore::AXCrossProcessSearchCoordinator::storeRemoteResults):
(WebCore::AXCrossProcessSearchCoordinator::takeRemoteResults):
(WebCore::AXCrossProcessSearchCoordinator::markFrameAsSearched):
(WebCore::AXCrossProcessSearchCoordinator::checkCompletion):
* Source/WebCore/accessibility/AXLiveRegionManager.h:
(): Deleted.
* Source/WebCore/accessibility/AXLogger.cpp:
(WebCore::operator<<):
(WebCore::streamAXCoreObject):
* Source/WebCore/accessibility/AXObjectCache.cpp:
(WebCore::AXObjectCache::getOrCreateSlow):
* Source/WebCore/accessibility/AXObjectCache.h:
(WebCore::AccessibilityRemoteToken::AccessibilityRemoteToken): Deleted.
* Source/WebCore/accessibility/AXRemoteFrame.h:
* Source/WebCore/accessibility/AXSearchManager.cpp:
(WebCore::AXSearchManager::findMatchingObjectsAsStream):
(WebCore::AXSearchManager::findMatchingObjectsInternalAsStream):
(WebCore::AXSearchManager::findMatchingRange):
(WebCore::AXSearchManager::matchWithResultsLimit): Deleted.
(WebCore::AXSearchManager::findMatchingObjectsInternal): Deleted.
* Source/WebCore/accessibility/AXSearchManager.h:
(WebCore::AccessibilitySearchCriteriaIPC::AccessibilitySearchCriteriaIPC):
(WebCore::AccessibilitySearchCriteriaIPC::toSearchCriteria const):
(WebCore::SearchResultEntry::localResult):
(WebCore::SearchResultEntry::remoteFrame):
(WebCore::SearchResultEntry::isLocalResult const):
(WebCore::SearchResultEntry::isRemoteFrame const):
(WebCore::SearchResultEntry::objectIfLocalResult const):
(WebCore::SearchResultEntry::frameID const):
(WebCore::SearchResultEntry::streamIndex const):
(WebCore::SearchResultEntry::SearchResultEntry):
(WebCore::AccessibilitySearchResultStream::appendLocalResult):
(WebCore::AccessibilitySearchResultStream::appendRemoteFrame):
(WebCore::AccessibilitySearchResultStream::entries const):
(WebCore::AccessibilitySearchResultStream::entryCount const):
(WebCore::AccessibilitySearchResultStream::setResultsLimit):
(WebCore::AccessibilitySearchResultStream::resultsLimit const):
(WebCore::AccessibilitySearchResultStream::nextIndex const):
(WebCore::AccessibilitySearchResult::local):
(WebCore::AccessibilitySearchResult::remote):
(WebCore::AccessibilitySearchResult::isLocal const):
(WebCore::AccessibilitySearchResult::isRemote const):
(WebCore::AccessibilitySearchResult::objectIfLocalResult const):
(WebCore::AccessibilitySearchResult::remoteToken const):
(WebCore::AccessibilitySearchResult::AccessibilitySearchResult):
(WebCore::AXSearchManager::findMatchingObjects): Deleted.
* Source/WebCore/accessibility/AXStitchUtilities.h:
* Source/WebCore/accessibility/AccessibilityMockObject.h:
* Source/WebCore/accessibility/AccessibilityNodeObject.cpp:
* Source/WebCore/accessibility/AccessibilityObject.cpp:
(WebCore::AccessibilityObject::findMatchingObjectsWithin):
(WebCore::AccessibilityObject::findMatchingObjects): Deleted.
* Source/WebCore/accessibility/AccessibilityObject.h:
* Source/WebCore/accessibility/AccessibilityRemoteToken.h: Added.
(WebCore::AccessibilityRemoteToken::AccessibilityRemoteToken):
* Source/WebCore/accessibility/AccessibilityRenderObject.cpp:
* Source/WebCore/accessibility/AccessibilityScrollView.cpp:
(WebCore::AccessibilityScrollView::detachRemoteParts):
* Source/WebCore/accessibility/AccessibilityScrollView.h:
* Source/WebCore/accessibility/AccessibilitySearchCriteriaIPC.h: Added.
* Source/WebCore/accessibility/ios/WebAccessibilityObjectWrapperIOS.mm:
(-[WebAccessibilityObjectWrapper accessibilityFindMatchingObjects:]):
* Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.cpp:
(WebCore::AXIsolatedObject::findMatchingObjects): Deleted.
* Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.h:
* Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.cpp:
(WebCore::AXIsolatedTree::applyPendingChangesLocked):
* Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.h:
* Source/WebCore/accessibility/isolatedtree/mac/AXIsolatedObjectMac.mm:
(WebCore::appendPlatformProperties):
* Source/WebCore/accessibility/mac/WebAccessibilityObjectWrapperMac.mm:
(remoteElementFromToken):
(searchResultsToNSArray):
(performSearchWithRemoteFrames):
(-[WebAccessibilityObjectWrapper _accessibilityHitTestResolvingRemoteFrame:callback:]):
(-[WebAccessibilityObjectWrapper accessibilityAttributeValue:forParameter:]):
* Source/WebCore/page/ChromeClient.cpp:
(WebCore::ChromeClient::performAccessibilitySearchInRemoteFrame):
(WebCore::ChromeClient::continueAccessibilitySearchFromChildFrame):
* Source/WebCore/page/ChromeClient.h:
* Source/WebCore/page/FrameTree.h:
* Source/WebKit/Scripts/webkit/messages.py:
(headers_for_type):
* Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in:
* Source/WebKit/UIProcess/WebPageProxy.cpp:
(WebKit::WebPageProxy::performAccessibilitySearchInRemoteFrame):
(WebKit::WebPageProxy::continueAccessibilitySearchFromChildFrame):
* Source/WebKit/UIProcess/WebPageProxy.h:
* Source/WebKit/UIProcess/WebPageProxy.messages.in:
* Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp:
(WebKit::WebChromeClient::performAccessibilitySearchInRemoteFrame):
(WebKit::WebChromeClient::continueAccessibilitySearchFromChildFrame):
* Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.h:
* Source/WebKit/WebProcess/WebPage/Cocoa/WebPageCocoa.mm:
(WebKit::tokenFromWrapper):
(WebKit::convertSearchResultsToRemoteTokens):
(WebKit::WebPage::performAccessibilitySearchInRemoteFrame):
(WebKit::WebPage::continueAccessibilitySearchInParentFrame):
* Source/WebKit/WebProcess/WebPage/WebPage.h:
* Source/WebKit/WebProcess/WebPage/WebPage.messages.in:
* Tools/WebKitTestRunner/InjectedBundle/AccessibilityUIElement.cpp:
(WTR::AccessibilityUIElement::uiElementsForSearchPredicate):
* Tools/WebKitTestRunner/InjectedBundle/AccessibilityUIElement.h:
* Tools/WebKitTestRunner/InjectedBundle/Bindings/AccessibilityUIElement.idl:
* Tools/WebKitTestRunner/InjectedBundle/atspi/AccessibilityUIElementAtspi.cpp:
(WTR::AccessibilityUIElementAtspi::uiElementsForSearchPredicate):
* Tools/WebKitTestRunner/InjectedBundle/atspi/AccessibilityUIElementAtspi.h:
* Tools/WebKitTestRunner/InjectedBundle/ios/AccessibilityUIElementIOS.h:
* Tools/WebKitTestRunner/InjectedBundle/ios/AccessibilityUIElementIOS.mm:
(WTR::AccessibilityUIElementIOS::uiElementsForSearchPredicate):
* Tools/WebKitTestRunner/InjectedBundle/mac/AccessibilityUIElementMac.h:
* Tools/WebKitTestRunner/InjectedBundle/mac/AccessibilityUIElementMac.mm:
(WTR::AccessibilityUIElementMac::attributeValue const):
(WTR::AccessibilityUIElementMac::allAttributes):
(WTR::AccessibilityUIElementMac::setBoolAttributeValue):
(WTR::AccessibilityUIElementMac::setValue):
(WTR::AccessibilityUIElementMac::isAttributeSupported):
(WTR::AccessibilityUIElementMac::isValid const):
(WTR::AccessibilityUIElementMac::uiElementsForSearchPredicate):
(WTR::AccessibilityUIElementMac::setSelectedTextRange):
(WTR::AccessibilityUIElementMac::invokeCustomActionAtIndex):
(WTR::AccessibilityUIElementMac::setSelectedTextMarkerRange):
(WTR::AccessibilityUIElementMac::setSelectedChild const):
(WTR::AccessibilityUIElementMac::setSelectedChildAtIndex const):
(WTR::AccessibilityUIElementMac::removeSelectionAtIndex const):
(WTR::AccessibilityUIElementMac::takeFocus):
(WTR::AccessibilityUIElementMac::resetSelectedTextMarkerRange):
* Tools/WebKitTestRunner/InjectedBundle/playstation/AccessibilityUIElementPlayStation.cpp:
(WTR::AccessibilityUIElementPlayStation::uiElementsForSearchPredicate):
* Tools/WebKitTestRunner/InjectedBundle/playstation/AccessibilityUIElementPlayStation.h:
* Tools/WebKitTestRunner/InjectedBundle/win/AccessibilityUIElementWin.cpp:
(WTR::AccessibilityUIElementWin::uiElementsForSearchPredicate):
* Tools/WebKitTestRunner/InjectedBundle/win/AccessibilityUIElementWin.h:

Canonical link: https://commits.webkit.org/308321@main
@webkit-commit-queue webkit-commit-queue force-pushed the eng/AX-On-macOS-with-site-isolation-enabled-VoiceOver-cannot-traverse-into-iframes-because-UIElementsForSearchPredicate-only-searches-the-local-process branch from 2562f37 to c1e1677 Compare February 27, 2026 06:06
@webkit-commit-queue
Copy link
Collaborator

Committed 308321@main (c1e1677): https://commits.webkit.org/308321@main

Reviewed commits have been landed. Closing PR #58826 and removing active labels.

@webkit-commit-queue webkit-commit-queue merged commit c1e1677 into WebKit:main Feb 27, 2026
@webkit-commit-queue webkit-commit-queue removed the merge-queue Applied to send a pull request to merge-queue label Feb 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Accessibility For bugs related to accessibility.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants