132 lines
5.0 KiB
JavaScript
132 lines
5.0 KiB
JavaScript
"use strict";
|
|
|
|
// Bug 2011921 - Crash in BrowsingContextWebProgress::ContextReplaced
|
|
//
|
|
// When MaybeCheckUnloadingIsCanceled takes the async path for a BFCache
|
|
// navigation (because the current page has a beforeunload handler and the
|
|
// Navigation API is enabled), and another navigation replaces the browsing
|
|
// context before the callback fires, the callback calls FinishRestore →
|
|
// ReplacedBy on the already-replaced context whose mWebProgress is null.
|
|
//
|
|
// The race: two rapid back navigations both enter LoadURIs. The first takes
|
|
// the async MaybeCheckUnloadingIsCanceled path. The second may complete
|
|
// its BFCache restore (calling ReplacedBy and moving mWebProgress) before
|
|
// the first callback fires. When the first callback fires, it reaches
|
|
// FinishRestore at the unguarded direct call in MaybeLoadBFCache (which
|
|
// lacks the IsReplaced() check present in the PermitUnload callback), and
|
|
// dereferences the null mWebProgress.
|
|
|
|
const TEST_PATH = getRootDirectory(gTestPath).replace(
|
|
"chrome://mochitests/content",
|
|
"https://example.com"
|
|
);
|
|
|
|
add_task(async function test_rapid_back_with_beforeunload_bfcache() {
|
|
await SpecialPowers.pushPrefEnv({
|
|
set: [
|
|
["fission.bfcacheInParent", true],
|
|
// Navigation API must be enabled for MaybeCheckUnloadingIsCanceled.
|
|
["dom.navigation.webidl.enabled", true],
|
|
// Keep the default (true) for this pref so that beforeunload dialogs
|
|
// are suppressed in automated tests (no user interaction), while the
|
|
// beforeunload handler is still registered and NeedsBeforeUnload()
|
|
// returns true.
|
|
["dom.require_user_interaction_for_beforeunload", true],
|
|
],
|
|
});
|
|
|
|
// Run multiple iterations to increase the probability of hitting the race
|
|
// window. The race depends on IPC message ordering between the beforeunload
|
|
// check round-trip and the second navigation's HistoryGo arrival.
|
|
const ITERATIONS = 10;
|
|
|
|
for (let i = 0; i < ITERATIONS; i++) {
|
|
info(`--- Iteration ${i + 1}/${ITERATIONS} ---`);
|
|
|
|
let tab = await BrowserTestUtils.openNewForegroundTab(
|
|
gBrowser,
|
|
TEST_PATH + "dummy_page.html?page1"
|
|
);
|
|
let browser = tab.linkedBrowser;
|
|
|
|
// Navigate to page2 (page1 goes to BFCache).
|
|
BrowserTestUtils.startLoadingURIString(
|
|
browser,
|
|
TEST_PATH + "dummy_page.html?page2"
|
|
);
|
|
await BrowserTestUtils.browserLoaded(browser);
|
|
|
|
// Navigate to page3 (page2 goes to BFCache).
|
|
BrowserTestUtils.startLoadingURIString(
|
|
browser,
|
|
TEST_PATH + "dummy_page.html?page3"
|
|
);
|
|
await BrowserTestUtils.browserLoaded(browser);
|
|
|
|
// Register a beforeunload handler on the current page. This makes
|
|
// NeedsBeforeUnload() return true on the WindowGlobalParent, which causes
|
|
// MaybeCheckUnloadingIsCanceled to take the async path when navigating
|
|
// away. With dom.require_user_interaction_for_beforeunload = true and no
|
|
// user interaction, no dialog is shown but the async IPC round-trip still
|
|
// occurs.
|
|
await SpecialPowers.spawn(browser, [], () => {
|
|
content.addEventListener("beforeunload", e => {
|
|
e.preventDefault();
|
|
});
|
|
});
|
|
|
|
// Strategy: trigger two back navigations from the content process,
|
|
// separated by one event loop turn so they get different history epochs
|
|
// (the ChildSHistory epoch increments via a dispatched runnable between
|
|
// event turns). Both navigations proceed to the parent's HistoryGo →
|
|
// LoadURIs. The first enters the async MaybeCheckUnloadingIsCanceled
|
|
// path. The second arrives while the first is pending.
|
|
SpecialPowers.spawn(browser, [], () => {
|
|
content.history.back();
|
|
content.setTimeout(() => {
|
|
content.history.back();
|
|
}, 0);
|
|
});
|
|
|
|
// Wait for async callbacks and IPC round-trips to settle.
|
|
// The crash, if triggered, happens during this window.
|
|
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
// Try a different strategy too: trigger goBack from the parent process
|
|
// while a content-initiated back navigation may still be in flight.
|
|
// First, set up fresh history again.
|
|
BrowserTestUtils.startLoadingURIString(
|
|
browser,
|
|
TEST_PATH + "dummy_page.html?page4"
|
|
);
|
|
await BrowserTestUtils.browserLoaded(browser);
|
|
|
|
BrowserTestUtils.startLoadingURIString(
|
|
browser,
|
|
TEST_PATH + "dummy_page.html?page5"
|
|
);
|
|
await BrowserTestUtils.browserLoaded(browser);
|
|
|
|
await SpecialPowers.spawn(browser, [], () => {
|
|
content.addEventListener("beforeunload", e => {
|
|
e.preventDefault();
|
|
});
|
|
});
|
|
|
|
// Trigger back from content and parent nearly simultaneously.
|
|
// These use different IPC paths, potentially avoiding epoch dedup.
|
|
SpecialPowers.spawn(browser, [], () => {
|
|
content.history.back();
|
|
});
|
|
browser.goBack();
|
|
|
|
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
BrowserTestUtils.removeTab(tab);
|
|
}
|
|
|
|
ok(true, "Completed all iterations without crashing (bug 2011921)");
|
|
});
|