LibWebView: Keep UI history authoritative during races

Keep UI process session history authoritative across overlapping
fallback loads and traversals. WebContent can finish a superseded
history load with a live document matching the UI seed URL while its
local step, document state id, and Navigation API keys still describe
a temporary partial list.

Reconstruct the current entry around the UI-owned list in that case.
This avoids making the UI process adopt WebContent's incomplete
snapshot.

Track UI-started fallback loads by URL so unrelated navigations cannot
consume the pending seed state. Resolve deferred WebDriver completions
through the view registry so callbacks queued before a process swap do
not touch a destroyed view.

Add WebDriver coverage that waits for explicit UI/WebContent history
convergence after the relevant document events. The waits poll
observable history state instead of depending on timing.
This commit is contained in:
Andreas Kling 2026-06-16 13:03:40 +02:00 committed by Andreas Kling
parent 641404d6b3
commit bbeb2eb69c
4 changed files with 159 additions and 22 deletions

View file

@ -405,8 +405,20 @@ bool TraversableNavigable::replace_top_level_session_history_entries_from_ui_pro
&& active_entry_is_latest_entry
&& current_entry_url_matches_ui_seed;
// NB: UI-process fallback history loads can overlap when a newer traversal supersedes an older one before the
// older load has finished. Other engines give pending history loads an identity so the latest traversal
// stays authoritative; after the race has happened, WebContent can still have the latest live document in
// an incomplete local top-level list, for example [b, c] while the UI process is restoring [a, b, c] at c.
// If the live active entry has the UI seed's current URL, accept the UI-owned list around that document
// instead of making the UI process adopt the incomplete WebContent list. The WebContent step, document state
// id, and Navigation API identity are all process-local placeholders at this point, and are replaced by the
// UI-owned values below.
auto can_restore_current_entry_after_superseded_ui_history_load = entries_from_ui_process.size() > m_session_history_entries.size()
&& active_entry_is_latest_entry
&& current_entry_url_matches_ui_seed;
auto can_reconstruct_current_entry = allow_reconstructing_current_entry
&& (can_restore_fresh_ui_history_load || can_restore_preseeded_ui_history_load);
&& (can_restore_fresh_ui_history_load || can_restore_preseeded_ui_history_load || can_restore_current_entry_after_superseded_ui_history_load);
if (!latest_entry_matches_ui_seed && !can_reconstruct_current_entry)
return false;
}

View file

@ -183,7 +183,7 @@ void ViewImplementation::create_new_process_for_cross_site_navigation(URL::URL c
return false;
if (!m_pending_web_content_session_history_seed.should_send_entries)
return false;
if (m_loading_session_history_entry_from_ui_process)
if (m_session_history_entry_url_loading_from_ui_process.has_value())
return false;
if (m_session_history.current_step_to_restore_after_loading_top_level_entry().has_value())
return false;
@ -194,7 +194,7 @@ void ViewImplementation::create_new_process_for_cross_site_navigation(URL::URL c
if (ui_session_history_already_points_to_url) {
m_should_suppress_history_for_current_load = false;
m_should_suppress_history_for_next_load = false;
if (!m_loading_session_history_entry_from_ui_process)
if (!m_session_history_entry_url_loading_from_ui_process.has_value())
m_pending_web_content_session_history_seed.step_after_loading_top_level_entry = m_session_history.current_step_to_restore_after_loading_top_level_entry();
set_url(url);
auto seeded_replacement_process_before_load = seed_replacement_process_before_load_if_possible();
@ -204,7 +204,7 @@ void ViewImplementation::create_new_process_for_cross_site_navigation(URL::URL c
} else {
m_should_suppress_history_for_current_load = false;
m_should_suppress_history_for_next_load = false;
if (should_manage_session_history_in_ui_process() && !m_loading_session_history_entry_from_ui_process) {
if (should_manage_session_history_in_ui_process() && !m_session_history_entry_url_loading_from_ui_process.has_value()) {
if (m_session_history.current_entry()) {
m_pending_session_history_navigation = PendingSessionHistoryNavigation {
url,
@ -221,7 +221,7 @@ void ViewImplementation::create_new_process_for_cross_site_navigation(URL::URL c
m_current_web_content_session_history_matches_mirror = false;
update_navigation_action_state();
}
if (!m_loading_session_history_entry_from_ui_process)
if (!m_session_history_entry_url_loading_from_ui_process.has_value())
m_pending_web_content_session_history_seed.step_after_loading_top_level_entry = m_session_history.current_step_to_restore_after_loading_top_level_entry();
set_url(url);
auto seeded_replacement_process_before_load = seed_replacement_process_before_load_if_possible();
@ -282,9 +282,9 @@ void ViewImplementation::load(URL::URL const& url, Web::Bindings::NavigationHist
m_should_suppress_history_for_current_load = false;
m_should_suppress_history_for_next_load = false;
auto should_defer_ui_process_history_update = false;
if (!m_loading_session_history_entry_from_ui_process)
if (!m_session_history_entry_url_loading_from_ui_process.has_value())
abandon_pending_web_content_session_history_seed();
if (should_manage_session_history_in_ui_process() && !m_loading_session_history_entry_from_ui_process) {
if (should_manage_session_history_in_ui_process() && !m_session_history_entry_url_loading_from_ui_process.has_value()) {
m_pending_session_history_traversal.clear();
auto const* current_entry = m_session_history.current_entry();
auto is_javascript_navigation = url.scheme() == "javascript"sv;
@ -428,7 +428,7 @@ ViewImplementation::HistoryTraversalOutcome ViewImplementation::traverse_the_his
auto web_content_can_apply_traversal = !m_pending_web_content_session_history_seed.should_send_entries
&& !m_pending_web_content_session_history_seed.ignore_updates_until_seed
&& !m_pending_web_content_session_history_seed.waiting_for_ack
&& !m_loading_session_history_entry_from_ui_process
&& !m_session_history_entry_url_loading_from_ui_process.has_value()
&& !m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.has_value()
&& m_session_history.web_content_can_traverse_to(*target);
@ -1416,9 +1416,18 @@ void ViewImplementation::did_start_navigation(URL::URL const& url, Variant<Empty
if (m_should_suppress_history_for_next_load || m_should_suppress_history_for_current_load)
return;
if (m_loading_session_history_entry_from_ui_process) {
if (m_session_history_entry_url_loading_from_ui_process.has_value()) {
if (*m_session_history_entry_url_loading_from_ui_process != url) {
// NB: An earlier UI-process history fallback load can start after a newer back/forward request has
// superseded it. Chromium, WebKit, and Firefox all give pending history loads an identity so stale
// traversals cannot consume state belonging to the current one. Keep the UI-owned history authoritative
// here and wait for the load matching the latest requested entry.
dump_session_history("ignored-stale-ui-history-load-start"sv);
return;
}
auto should_keep_preseeded_web_content_history = m_pending_web_content_session_history_seed.waiting_for_ack || m_session_history.web_content_uses_ui_step_coordinates();
m_loading_session_history_entry_from_ui_process = false;
m_session_history_entry_url_loading_from_ui_process.clear();
if (!should_keep_preseeded_web_content_history) {
m_current_web_content_session_history_matches_mirror = false;
m_session_history.forget_web_content_state();
@ -1427,6 +1436,13 @@ void ViewImplementation::did_start_navigation(URL::URL const& url, Variant<Empty
return;
}
if (m_pending_web_content_session_history_seed.should_send_entries || m_pending_web_content_session_history_seed.ignore_updates_until_seed || m_pending_web_content_session_history_seed.waiting_for_ack) {
if (auto const* current_entry = m_session_history.current_entry(); current_entry && current_entry->url != url) {
dump_session_history("ignored-navigation-start-before-ui-history-seed"sv);
return;
}
}
if (m_is_showing_crash_page) {
m_is_showing_crash_page = false;
if (auto const* current_entry = m_session_history.current_entry(); current_entry && current_entry->url == url) {
@ -1491,8 +1507,8 @@ void ViewImplementation::did_cancel_navigation(URL::URL const& url)
return;
}
if (m_loading_session_history_entry_from_ui_process) {
m_loading_session_history_entry_from_ui_process = false;
if (m_session_history_entry_url_loading_from_ui_process.has_value() && *m_session_history_entry_url_loading_from_ui_process == url) {
m_session_history_entry_url_loading_from_ui_process.clear();
abandon_pending_web_content_session_history_seed();
m_current_web_content_session_history_matches_mirror = false;
m_session_history.forget_web_content_state();
@ -1561,7 +1577,7 @@ bool ViewImplementation::restore_pending_session_history_navigation(StringView r
m_webdriver_pending_navigation_completes_with_session_history_update = true;
load_current_session_history_entry_from_ui_process();
} else {
m_loading_session_history_entry_from_ui_process = false;
m_session_history_entry_url_loading_from_ui_process.clear();
abandon_pending_web_content_session_history_seed();
m_current_web_content_session_history_matches_mirror = m_session_history.web_content_history_matches_mirror();
m_webdriver_pending_navigation_url.clear();
@ -1579,6 +1595,7 @@ bool ViewImplementation::restore_pending_session_history_navigation(StringView r
void ViewImplementation::abandon_pending_web_content_session_history_seed()
{
m_session_history_entry_url_loading_from_ui_process.clear();
m_pending_web_content_session_history_seed.clear();
}
@ -1686,9 +1703,13 @@ void ViewImplementation::complete_webdriver_pending_navigation_if_url_matches(UR
for (auto const& request : m_pending_webdriver_navigation_completion_requests)
request_ids.unchecked_append(request.key);
auto view_id = m_view_id;
for (auto request_id : request_ids) {
Core::EventLoop::current().deferred_invoke([this, request_id] {
complete_webdriver_navigation_completion(request_id, JsonValue {});
Core::EventLoop::current().deferred_invoke([view_id, request_id] {
auto view = ViewImplementation::find_view_by_id(view_id);
if (!view.has_value())
return;
view->complete_webdriver_navigation_completion(request_id, JsonValue {});
});
}
}
@ -1701,7 +1722,7 @@ JsonValue ViewImplementation::webdriver_session_history() const
serialized.set("backButtonEnabled"sv, m_navigate_back_action->enabled());
serialized.set("forwardButtonEnabled"sv, m_navigate_forward_action->enabled());
serialized.set("webContentHistoryMatchesUI"sv, m_current_web_content_session_history_matches_mirror);
serialized.set("loadingSessionHistoryEntryFromUI"sv, m_loading_session_history_entry_from_ui_process);
serialized.set("loadingSessionHistoryEntryFromUI"sv, m_session_history_entry_url_loading_from_ui_process.has_value());
serialized.set("waitingToSeedWebContent"sv, m_pending_web_content_session_history_seed.should_send_entries);
serialized.set("waitingForWebContentSeedAck"sv, m_pending_web_content_session_history_seed.waiting_for_ack);
serialized.set("ignoringWebContentUpdatesUntilSeed"sv, m_pending_web_content_session_history_seed.ignore_updates_until_seed);
@ -1848,14 +1869,14 @@ void ViewImplementation::restore_current_session_history_entry_from_ui_process()
void ViewImplementation::load_current_session_history_entry_from_ui_process()
{
m_loading_session_history_entry_from_ui_process = true;
auto const* current_entry = m_session_history.current_entry();
if (!current_entry) {
m_session_history_entry_url_loading_from_ui_process = m_url;
client().async_load_url(page_id(), m_url, Web::Bindings::NavigationHistoryBehavior::Auto);
return;
}
m_session_history_entry_url_loading_from_ui_process = current_entry->url;
auto history_handling = m_pending_web_content_session_history_seed.waiting_for_ack || m_session_history.web_content_uses_ui_step_coordinates()
? Web::Bindings::NavigationHistoryBehavior::Replace
: Web::Bindings::NavigationHistoryBehavior::Auto;
@ -2156,7 +2177,7 @@ void ViewImplementation::did_reset_session_history_for_testing(Badge<WebContentC
m_current_web_content_session_history_matches_mirror = false;
m_pending_session_history_navigation.clear();
m_pending_session_history_traversal.clear();
m_loading_session_history_entry_from_ui_process = false;
m_session_history_entry_url_loading_from_ui_process.clear();
abandon_pending_web_content_session_history_seed();
m_webdriver_pending_navigation_url.clear();
m_webdriver_pending_navigation_completes_with_session_history_update = false;
@ -2202,7 +2223,7 @@ void ViewImplementation::dump_session_history(StringView reason, SessionHistoryD
m_url,
m_current_web_content_session_history_matches_mirror,
m_session_history.web_content_uses_ui_step_coordinates(),
m_loading_session_history_entry_from_ui_process,
m_session_history_entry_url_loading_from_ui_process.has_value(),
m_pending_web_content_session_history_seed.should_send_entries,
m_pending_web_content_session_history_seed.waiting_for_ack,
m_pending_web_content_session_history_seed.ignore_updates_until_seed,
@ -2254,7 +2275,7 @@ void ViewImplementation::handle_web_content_process_crash(LoadErrorPage load_err
m_backup_shared_image_buffer = nullptr;
if (should_manage_session_history_in_ui_process()) {
m_loading_session_history_entry_from_ui_process = false;
m_session_history_entry_url_loading_from_ui_process.clear();
prepare_to_seed_web_content_session_history_from_ui_process();
}

View file

@ -572,7 +572,7 @@ protected:
Optional<PendingSessionHistoryNavigation> m_pending_session_history_navigation;
Optional<PendingSessionHistoryTraversal> m_pending_session_history_traversal;
u64 m_next_traverse_history_step_cancelation_check_request_id { 0 };
bool m_loading_session_history_entry_from_ui_process { false };
Optional<URL::URL> m_session_history_entry_url_loading_from_ui_process;
PendingWebContentSessionHistorySeed m_pending_web_content_session_history_seed;
Optional<URL::URL> m_webdriver_pending_navigation_url;
bool m_webdriver_pending_navigation_completes_with_session_history_update { false };

View file

@ -1023,6 +1023,45 @@ def expect_ui_session_history(
)
def wait_for_ui_session_history(
webdriver_port,
session_id,
label,
expected_entry_urls,
expected_used_steps,
expected_current_used_step_index,
expected_back_enabled,
expected_forward_enabled,
log,
expected_web_content_known_used_steps,
expected_web_content_current_step,
):
def matches_expected_history(snapshot):
ui = snapshot["ui"]
web_content = snapshot["webContent"]
return (
history_entry_urls(ui) == expected_entry_urls
and history_used_steps(ui) == expected_used_steps
and ui["currentUsedStepIndex"] == expected_current_used_step_index
and ui["backButtonEnabled"] is expected_back_enabled
and ui["forwardButtonEnabled"] is expected_forward_enabled
and ui["webContentHistoryMatchesUI"]
and history_step_values(ui["webContentKnownUsedSteps"]) == expected_web_content_known_used_steps
and ui["webContentCurrentStep"] == expected_web_content_current_step
and history_current_step(ui["webContentKnownUsedSteps"]) == expected_web_content_current_step
and not ui["waitingToSeedWebContent"]
and not ui["waitingForWebContentSeedAck"]
and not ui["ignoringWebContentUpdatesUntilSeed"]
and not ui["reseedAfterCurrentHistoryLoad"]
and ui["pendingWebContentHistoryStepAfterFallbackLoad"] is None
and ui["pendingSessionHistoryNavigation"] is None
and ui["pendingSessionHistoryTraversal"] is None
and comparable_history(ui) == comparable_history(web_content)
)
return wait_for_session_history(webdriver_port, session_id, label, matches_expected_history, log)
def expect_beforeunload_cancels_webdriver_navigation(
webdriver_port,
session_id,
@ -2352,6 +2391,19 @@ return [Math.round(rect.left + rect.width / 2), Math.round(rect.top + rect.heigh
)
page_server.release_blocked_process_swap_back.set()
wait_for_event(page_server.process_swap_back_document_ran, "process-swap UI back document")
wait_for_ui_session_history(
webdriver_port,
session_id,
"after blocked process-swap UI back converges",
[url_process_swap_back_blocked, url_b],
[0, 1],
0,
False,
True,
log,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=0,
)
expect_url(
webdriver_port,
session_id,
@ -3179,6 +3231,19 @@ return [location.href, window.scriptBeforeUnloadCount, navigator.userActivation.
page_server.a_document_ran.clear()
perform_browser_history_shortcut(webdriver_port, session_id, "left", log)
wait_for_event(page_server.a_document_ran, "A document after browser shortcut back")
wait_for_ui_session_history(
webdriver_port,
session_id,
"after cross-site browser shortcut back to /a converges",
[url_a, url_b],
[0, 1],
0,
False,
True,
log,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=0,
)
expect_url(webdriver_port, session_id, "after cross-site browser shortcut back to /a", url_a, log)
expect_ui_session_history(
webdriver_port,
@ -3198,6 +3263,19 @@ return [location.href, window.scriptBeforeUnloadCount, navigator.userActivation.
page_server.b_document_ran.clear()
perform_browser_history_shortcut(webdriver_port, session_id, "right", log)
wait_for_event(page_server.b_document_ran, "B document after browser shortcut forward")
wait_for_ui_session_history(
webdriver_port,
session_id,
"after cross-site browser shortcut forward to /b converges",
[url_a, url_b],
[0, 1],
1,
True,
False,
log,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=1,
)
expect_url(webdriver_port, session_id, "after cross-site browser shortcut forward to /b", url_b, log)
expect_ui_session_history(
webdriver_port,
@ -3235,6 +3313,19 @@ return [location.href, window.scriptBeforeUnloadCount, navigator.userActivation.
page_server.b_document_ran.clear()
perform_browser_history_shortcut(webdriver_port, session_id, "left", log)
wait_for_event(page_server.b_document_ran, "B document after browser shortcut back")
wait_for_ui_session_history(
webdriver_port,
session_id,
"after browser shortcut back to /b converges",
[url_a, url_b, url_c],
[0, 1, 2],
1,
True,
True,
log,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=1,
)
expect_url(webdriver_port, session_id, "after browser shortcut back to /b", url_b, log)
expect_ui_session_history(
webdriver_port,
@ -3254,6 +3345,19 @@ return [location.href, window.scriptBeforeUnloadCount, navigator.userActivation.
page_server.c_document_ran.clear()
perform_browser_history_shortcut(webdriver_port, session_id, "right", log)
wait_for_event(page_server.c_document_ran, "C document after browser shortcut forward")
wait_for_ui_session_history(
webdriver_port,
session_id,
"after browser shortcut forward to /c converges",
[url_a, url_b, url_c],
[0, 1, 2],
2,
True,
False,
log,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=2,
)
expect_url(webdriver_port, session_id, "after browser shortcut forward to /c", url_c, log)
expect_ui_session_history(
webdriver_port,