Clear stale NavigationParams from HistoryController.

This prevents newly created iframes during a back/forward from
targeting the wrong NavigationEntry.

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

Review-Url: https://codereview.chromium.org/2144823002
Cr-Commit-Position: refs/heads/master@{#404870}
diff --git a/content/browser/frame_host/navigation_controller_impl_browsertest.cc b/content/browser/frame_host/navigation_controller_impl_browsertest.cc
index bc4ba82c..f776f54 100644
--- a/content/browser/frame_host/navigation_controller_impl_browsertest.cc
+++ b/content/browser/frame_host/navigation_controller_impl_browsertest.cc
@@ -4685,6 +4685,94 @@
   EXPECT_EQ(0U, root->child_count());
 }
 
+// Ensure that we do not corrupt a NavigationEntry's PageState if two forward
+// navigations compete in different frames, and the main frame entry contains an
+// iframe of its own.  See https://crbug.com/623319.
+IN_PROC_BROWSER_TEST_F(NavigationControllerBrowserTest,
+                       PageStateWithIframeAfterForwardInCompetingFrames) {
+  // Navigate to a page with an iframe.
+  GURL url_a(embedded_test_server()->GetURL(
+      "/navigation_controller/page_with_data_iframe.html"));
+  GURL data_url("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(data_url, root->child_at(0)->current_url());
+
+  // Navigate the iframe to a first real page.
+  GURL frame_url_a1 = embedded_test_server()->GetURL(
+      "/navigation_controller/simple_page_1.html");
+  NavigateFrameToURL(root->child_at(0), frame_url_a1);
+
+  // Navigate the iframe to a second real page.
+  GURL frame_url_a2 = embedded_test_server()->GetURL(
+      "/navigation_controller/simple_page_2.html");
+  NavigateFrameToURL(root->child_at(0), frame_url_a2);
+  EXPECT_EQ(3, controller.GetEntryCount());
+  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
+  EXPECT_EQ(url_a, root->current_url());
+  EXPECT_EQ(frame_url_a2, 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 with an iframe.  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/page_with_data_iframe.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());
+  EXPECT_EQ(data_url, root->child_at(0)->current_url());
+
+  // Go back to the original page.
+  controller.GoBack();
+  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
+
+  // Navigate forward twice using script.  This will race, but in either outcome
+  // we want to ensure that the subframes target entry index 1 and not 2.  In
+  // https://crbug.com/623319, the subframes targeted the wrong entry, leading
+  // to a URL spoof and renderer kill.
+  EXPECT_TRUE(ExecuteScript(shell()->web_contents(),
+                            "history.forward(); history.forward();"));
+  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
+  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
+  EXPECT_TRUE(root->current_frame_host()->IsRenderFrameLive());
+  EXPECT_EQ(url_b, root->current_url());
+  EXPECT_EQ(data_url, root->child_at(0)->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()));
+
+  // 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(data_url, root->child_at(0)->current_url());
+}
+
 // 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 5f0f28b..14ced35 100644
--- a/content/renderer/history_controller.cc
+++ b/content/renderer/history_controller.cc
@@ -201,7 +201,8 @@
         // 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.
+        // HistoryEntry corresponding to the commit, and clear any stale
+        // NavigationParams which might point to the wrong entry.
         //
         // This will lack any subframe history items that were in the original
         // provisional entry, but we don't know what those were after discarding
@@ -214,8 +215,10 @@
         // 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())
+        if (frame->IsMainFrame()) {
           current_entry_.reset(new HistoryEntry(item));
+          navigation_params_.reset();
+        }
 
         return;
       }
@@ -251,6 +254,13 @@
 
       if (HistoryEntry::HistoryNode* node =
               current_entry_->GetHistoryNodeForFrame(frame)) {
+        // Clear the children and any NavigationParams if this commit isn't for
+        // the same item.  Otherwise we might have stale data from a race.
+        if (node->item().itemSequenceNumber() != item.itemSequenceNumber()) {
+          node->RemoveChildren();
+          navigation_params_.reset();
+        }
+
         node->set_item(item);
       }
       break;
diff --git a/content/renderer/history_controller.h b/content/renderer/history_controller.h
index ff20596..ac52241 100644
--- a/content/renderer/history_controller.h
+++ b/content/renderer/history_controller.h
@@ -153,11 +153,15 @@
   // A HistoryEntry representing the page that is being loaded, or an empty
   // scoped_ptr if no page is being loaded.
   std::unique_ptr<HistoryEntry> provisional_entry_;
-  // The NavigationParams corresponding to the last load that was initiated by
-  // |GoToEntry|. This is kept around so that it can be passed into existing
-  // frames modified during a history navigation in GoToEntry(), and can be
+
+  // The NavigationParams corresponding to the last back/forward load that was
+  // initiated by |GoToEntry|. This is kept around so that it can be passed into
+  // existing frames affected by a history navigation in GoToEntry(), and can be
   // passed into frames created after the commit that resulted from the
   // navigation in GetItemForNewChildFrame().
+  //
+  // This is reset in UpdateForCommit if we see a commit from a different
+  // navigation, to avoid using stale parameters.
   std::unique_ptr<NavigationParams> navigation_params_;
 
   DISALLOW_COPY_AND_ASSIGN(HistoryController);