Update HistoryController::current_entry_ on all main frame back/forwards.

This fixes a case where it was left stale on a cross-origin commit
because the provisional_entry_ had been cleared by a different commit.

BUG=623319
TEST=See bug comment 14 for repro steps.
CQ_INCLUDE_TRYBOTS=tryserver.chromium.linux:linux_site_isolation

Review-Url: https://codereview.chromium.org/2134493002
Cr-Commit-Position: refs/heads/master@{#404537}
diff --git a/content/browser/frame_host/navigation_controller_impl_browsertest.cc b/content/browser/frame_host/navigation_controller_impl_browsertest.cc
index 9f573af..1ad247d 100644
--- a/content/browser/frame_host/navigation_controller_impl_browsertest.cc
+++ b/content/browser/frame_host/navigation_controller_impl_browsertest.cc
@@ -4600,6 +4600,91 @@
             GURL(exploded_state.top.children.at(0).url_string.string()));
 }
 
+// Ensure that we do not corrupt a NavigationEntry's PageState if two forward
+// navigations compete in different frames.  See https://crbug.com/623319.
+IN_PROC_BROWSER_TEST_F(NavigationControllerBrowserTest,
+                       PageStateAfterForwardInCompetingFrames) {
+  // Navigate to a page with an iframe.
+  GURL url_a(embedded_test_server()->GetURL(
+      "/navigation_controller/page_with_data_iframe.html"));
+  GURL frame_url_a1("data:text/html,Subframe");
+  EXPECT_TRUE(NavigateToURL(shell(), url_a));
+
+  NavigationController& controller = shell()->web_contents()->GetController();
+  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
+                            ->GetFrameTree()
+                            ->root();
+  EXPECT_EQ(url_a, root->current_url());
+  EXPECT_EQ(frame_url_a1, root->child_at(0)->current_url());
+
+  // Navigate the iframe to a second page.
+  GURL frame_url_a2 = embedded_test_server()->GetURL(
+      "/navigation_controller/simple_page_1.html");
+  NavigateFrameToURL(root->child_at(0), frame_url_a2);
+
+  // Navigate the iframe to about:blank.
+  GURL blank_url(url::kAboutBlankURL);
+  NavigateFrameToURL(root->child_at(0), blank_url);
+  EXPECT_EQ(3, controller.GetEntryCount());
+  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
+  EXPECT_EQ(url_a, root->current_url());
+  EXPECT_EQ(blank_url, root->child_at(0)->current_url());
+
+  // Go back to the middle entry.
+  controller.GoBack();
+  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
+  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
+
+  // Replace the entry with a cross-site top-level page.  By doing a
+  // replacement, the main frame pages before and after have the same item
+  // sequence number, and thus going between them only requires loads in the
+  // subframe.
+  GURL url_b(embedded_test_server()->GetURL(
+      "b.com", "/navigation_controller/simple_page_2.html"));
+  std::string replace_script = "location.replace('" + url_b.spec() + "')";
+  TestNavigationObserver replace_observer(shell()->web_contents());
+  EXPECT_TRUE(ExecuteScript(shell()->web_contents(), replace_script));
+  replace_observer.Wait();
+  EXPECT_EQ(3, controller.GetEntryCount());
+  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
+  EXPECT_EQ(url_b, root->current_url());
+
+  // Go back to the original page.
+  controller.GoBack();
+  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
+
+  // Navigate forward twice using script.  In https://crbug.com/623319, this
+  // caused a mismatch between the NavigationEntry's URL and PageState.
+  EXPECT_TRUE(ExecuteScript(shell()->web_contents(),
+                            "history.forward(); history.forward();"));
+  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
+  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
+  EXPECT_EQ(url_b, root->current_url());
+  NavigationEntry* entry = controller.GetLastCommittedEntry();
+  EXPECT_EQ(url_b, entry->GetURL());
+  ExplodedPageState exploded_state;
+  EXPECT_TRUE(
+      DecodePageState(entry->GetPageState().ToEncodedData(), &exploded_state));
+  EXPECT_EQ(url_b, GURL(exploded_state.top.url_string.string()));
+  // TODO(creis): Clear subframe FNEs after location.replace in
+  // --isolate-extensions mode.  See https://crbug.com/596707.
+  if (!SiteIsolationPolicy::UseSubframeNavigationEntries())
+    EXPECT_EQ(0U, exploded_state.top.children.size());
+
+  // Go back and then forward to see if the PageState loads correctly.
+  controller.GoBack();
+  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
+  controller.GoForward();
+  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
+
+  // We should be on url_b, and the renderer process shouldn't be killed.
+  ASSERT_TRUE(root->current_frame_host()->IsRenderFrameLive());
+  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
+  EXPECT_EQ(url_b, shell()->web_contents()->GetVisibleURL());
+  EXPECT_EQ(url_b, root->current_url());
+  EXPECT_EQ(0U, root->child_count());
+}
+
 // Ensure that forward navigations in cloned tabs can commit if they redirect to
 // a different site than before.  This causes the navigation's item sequence
 // number to change, meaning that we can't use it for determining whether the
diff --git a/content/renderer/history_controller.cc b/content/renderer/history_controller.cc
index 8f1bc34..5f0f28b 100644
--- a/content/renderer/history_controller.cc
+++ b/content/renderer/history_controller.cc
@@ -196,8 +196,29 @@
                                         bool navigation_within_page) {
   switch (commit_type) {
     case blink::WebBackForwardCommit:
-      if (!provisional_entry_)
+      if (!provisional_entry_) {
+        // The provisional entry may have been discarded due to a navigation in
+        // a different frame.  For main frames, it is not safe to leave the
+        // current_entry_ in place, which may have a cross-site page and will be
+        // included in the PageState for this commit.  Replace it with a new
+        // HistoryEntry corresponding to the commit.
+        //
+        // This will lack any subframe history items that were in the original
+        // provisional entry, but we don't know what those were after discarding
+        // it.  We'll load the default URL in those subframes instead.
+        //
+        // TODO(creis): It's also possible to get here for subframe commits.
+        // We'll leave a stale current_entry_ in that case, but that only causes
+        // an earlier URL to load in the subframe when leaving and coming back,
+        // and only in rare cases.  It does not risk a URL spoof, unlike the
+        // main frame case.  Since this bug is not present in the new
+        // FrameNavigationEntry-based navigation path (https://crbug.com/236848)
+        // we'll wait for that to fix the subframe case.
+        if (frame->IsMainFrame())
+          current_entry_.reset(new HistoryEntry(item));
+
         return;
+      }
 
       // If the current entry is null, this must be a main frame commit.
       DCHECK(current_entry_ || frame->IsMainFrame());