Branch: refs/heads/main
  Home:   https://github.com/WebKit/WebKit
  Commit: a11b943d3bbeb317297b9bef6a6b0aaa2e628be4
      
https://github.com/WebKit/WebKit/commit/a11b943d3bbeb317297b9bef6a6b0aaa2e628be4
  Author: Ryosuke Niwa <[email protected]>
  Date:   2026-09-15 (Tue, 15 Sep 2026)

  Changed paths:
    M Source/WebKit/UIProcess/WebEditCommandProxy.cpp
    M Source/WebKit/UIProcess/WebEditCommandProxy.h
    M Source/WebKit/UIProcess/WebPageProxy.cpp
    M Source/WebKit/UIProcess/WebPageProxy.h
    M Source/WebKit/UIProcess/WebPageProxy.messages.in
    M Source/WebKit/WebProcess/WebCoreSupport/WebEditorClient.cpp
    M Source/WebKit/WebProcess/WebPage/WebPage.cpp
    M Source/WebKit/WebProcess/WebPage/WebPage.h
    M Source/WebKit/WebProcess/WebPage/WebPage.messages.in
    M Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SiteIsolation.mm

  Log Message:
  -----------
  [Site Isolation] Undo and redo do not work for edits made in a cross-origin 
iframe
https://bugs.webkit.org/show_bug.cgi?id=324199
rdar://168324268

Reviewed by Sihui Liu.

Undo state is split across processes: the UI process owns the platform undo 
stack (NSUndoManager on Cocoa,
DefaultUndoController elsewhere), while each WebUndoStep lives in the WebPage 
of the web content process whose Editor
created it. The UI process kept no record of which process registered a given 
step, so under site isolation every
message that has to reach the owner of a step was sent to the main frame's 
process instead.

WebEditCommandProxy::unapply() and reapply() sent WebPage::UnapplyEditCommand 
and ReapplyEditCommand to
legacyMainFrameProcess() with webPageIDInMainFrameProcess(), so a step 
registered by a cross-origin iframe was looked
up in the wrong WebPage's m_undoStepMap and silently dropped. 
removeEditCommand() sent DidRemoveEditCommand the same
way, so a subframe process never dropped its m_undoStepMap entry and never got 
didRemoveFromUndoManager().
executeUndoRedo() drained all of m_pendingUndoRedo into the synchronous reply 
of whichever process asked, handing one
process steps belonging to another. And since WebUndoStepID is an 
ObjectIdentifier generated from a per-process
counter, identifiers collide across processes, so removePendingUndoRedo() 
matching on the identifier alone could
retire the wrong entry.

The only reason undo appeared to work at all is the synchronous ExecuteUndoRedo 
reply, which exists so that
document.execCommand('undo') updates the DOM before it returns. When the frame 
that asked happened to own the step,
that reply applied it and the misdirected asynchronous message was a harmless 
no-op. Undo triggered from another
frame's process, or from the platform undo manager (the Edit menu, which has no 
synchronous reply at all), was lost:
the command moved to the redo stack in the UI process while the content was 
never reverted.

Record the registering process and the PageIdentifier its WebPage is registered 
under on WebEditCommandProxy, taken
from the IPC::Connection that registerEditCommandForUndo() already receives, 
and send Unapply/ReapplyEditCommand and
DidRemoveEditCommand there. Caching that identifier rather than recomputing it 
in unapply()/reapply() is deliberate:
webPageIDInProcess() falls back to m_webPageID when the given process has no 
RemotePageProxy, and a
WebEditCommandProxy can outlive its remote page because the platform undo stack 
goes on holding WKEditCommands until
something clears it. Recomputing would then address a subframe process with the 
main frame WebPage's identifier, which
names no WebPage in that process, instead of the identifier the step was 
actually registered under.

unapply() and reapply() bail out unless the owning process can still be sent to 
and still hosts a WebPage under that
identifier, which hasWebPageInProcess() answers. Without that they would move 
the command to the redo stack and enable
Redo in the Edit menu for an operation that cannot happen: after the iframe's 
process crashes and the frame is replaced
by a placeholder, or after its RemotePageProxy and WebPage are gone while the 
process is kept alive by another page, in
which case the web process drops the message silently and the asynchronous 
reply handler never runs. Checking this
lazily rather than invalidating the commands when the remote page goes away is 
deliberate.
RemotePageProxy::disconnect() calls isNoLongerAssociatedWithRemotePage() 
unconditionally, but the ProvisionalPageProxy
m_shouldReuseMainFrame path clears the drawing area first precisely so that no 
PageClose is sent and the WebPage
survives in that process under the same identifier, so invalidating there would 
permanently kill live commands if the
provisional navigation were then cancelled. The lazy check recovers on its own 
once that window closes.

Give each unapply or reapply request a sequence number, counted per process, 
and keep the request in
m_pendingUndoRedo until the web process acknowledges it. executeUndoRedo() 
hands the calling process every request
still outstanding to it, in send order, so that all of them, including the ones 
this undo or redo just produced, are
applied before its execCommand() returns. Steps owned by another process are 
left to their own asynchronous message,
which is the only copy they get. WebPage applies a request only if its sequence 
is at or past
m_nextUndoRedoSequenceToApply, which discards whichever copy arrives second.

Acknowledgements arrive in send order, so applied requests are always a prefix 
of sent ones, outstanding requests are a
contiguous suffix, and the returned run is contiguous in that process's 
sequence space. The reply therefore only needs
the first sequence and the rest follow from position, which keeps the reply's 
shape unchanged apart from replacing the
page-wide undoVersion with a per-process firstSequence. There is an assertion 
on the contiguity in the drain loop.

This is what the undoVersion watermark could not do, in either direction. It 
was bumped again in executeUndoRedo() and
the reply carried one version for the whole batch, so applying the batch pushed 
the web process's watermark past the
versions of older outstanding entries. Draining every entry then applied a step 
twice whenever the web process had
already applied it and its asynchronous reply had been overtaken by the sync 
request, which is dispatched out of
SyncMessageState and so can be handled first. Returning only the entries 
produced by the current call is not a fix
either: the watermark would then drop an older asynchronous message that had 
not been applied yet and lose that undo
outright. Nor can the reply simply be skipped when something is outstanding, 
since a synchronous reply is handled by
processIncomingSyncReply() and overtakes queued messages, so its steps would be 
unapplied ahead of older ones, and
EditCommandComposition::unapply() run out of undo stack order corrupts the 
document rather than merely duplicating an
edit. Sequence numbers per request avoid all three: every copy is identified 
individually, so order comes from the
reply and duplicates are dropped on arrival, and execCommand() stays 
synchronous in every case.

Added four API tests: undo and redo of an edit made in a cross-origin iframe 
driven from the main frame's process, the
same driven from the platform undo manager (the path with no synchronous reply 
to fall back on), undo of steps
registered by two processes on a single undo stack, checked to unapply in order 
and each in its own process, and undo
after the iframe's process is killed, which must not leave Redo enabled. The 
reordering window itself is a timing race
with no deterministic trigger and is not covered by a test.

Tests: SiteIsolation.UndoAndRedoEditInCrossOriginIframeFromMainFrame
       SiteIsolation.UndoAndRedoEditInCrossOriginIframeFromPlatformUndoManager
       SiteIsolation.UndoAfterCrossOriginIframeProcessCrashesDoesNotOfferRedo
       SiteIsolation.UndoEditsRegisteredByMultipleProcesses

* Source/WebKit/UIProcess/WebEditCommandProxy.cpp:
(WebKit::WebEditCommandProxy::WebEditCommandProxy):
(WebKit::WebEditCommandProxy::process):
(WebKit::WebEditCommandProxy::unapply):
(WebKit::WebEditCommandProxy::reapply):
* Source/WebKit/UIProcess/WebEditCommandProxy.h:
(WebKit::WebEditCommandProxy::create):
(WebKit::WebEditCommandProxy::pageIDInProcess):
(WebKit::WebEditCommandProxy::invalidate):
* Source/WebKit/UIProcess/WebPageProxy.cpp:
(WebKit::WebPageProxy::registerEditCommandForUndo):
(WebKit::WebPageProxy::executeUndoRedo):
(WebKit::WebPageProxy::addPendingUndoRedo):
(WebKit::WebPageProxy::removePendingUndoRedo):
(WebKit::WebPageProxy::removeEditCommand):
(WebKit::WebPageProxy::hasWebPageInProcess):
* Source/WebKit/UIProcess/WebPageProxy.h:
* Source/WebKit/UIProcess/WebPageProxy.messages.in:
* Source/WebKit/WebProcess/WebCoreSupport/WebEditorClient.cpp:
(WebKit::applyUndoRedo):
(WebKit::WebEditorClient::undo):
(WebKit::WebEditorClient::redo):
* Source/WebKit/WebProcess/WebPage/WebPage.cpp:
(WebKit::WebPage::unapplyEditCommand):
(WebKit::WebPage::reapplyEditCommand):
* Source/WebKit/WebProcess/WebPage/WebPage.h:
* Source/WebKit/WebProcess/WebPage/WebPage.messages.in:
* Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SiteIsolation.mm:
(TestWebKitAPI::insertTextInFrame):
(TestWebKitAPI::waitForTextContentInFrame):
(TestWebKitAPI::TEST(SiteIsolation, 
UndoAndRedoEditInCrossOriginIframeFromMainFrame)):
(TestWebKitAPI::TEST(SiteIsolation, 
UndoAndRedoEditInCrossOriginIframeFromPlatformUndoManager)):
(TestWebKitAPI::TEST(SiteIsolation, 
UndoAfterCrossOriginIframeProcessCrashesDoesNotOfferRedo)):
(TestWebKitAPI::TEST(SiteIsolation, UndoEditsRegisteredByMultipleProcesses)):

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



To unsubscribe from these emails, change your notification settings at 
https://github.com/WebKit/WebKit/settings/notifications

Reply via email to