Reland "Handle race of SwapIn() and browser destroying the speculative MainFrame"

This is a reland of e7d59816288ef3246defc4b86cb4e03e28d75956

TestExpectations added for the portals test that is hitting crbug.com/838348

[email protected], [email protected]

Original change's description:
> Handle race of SwapIn() and browser destroying the speculative MainFrame
>
> When WebContentsImpl destroys, it deletes the speculative main frame,
> but the renderer may have taken ownership of it already, and the notice
> of such action is in flight to the browser still. This leads to DCHECKs
> failing in the renderer. So inform the FrameMsg_Delete IPC what the
> intention of the browser is, there are 3 modes:
>
> - Deleting a non-main frame. This is the common case. These frames are
> all owned by the browser so it's all good, no races.
> - Deleting a speculative main frame at shutdown. This is the race we
> address here.
> - Deleting a speculative main frame because it's no longer needed. This
> race is not handled by this CL but we CHECK() it explicitly now instead
> of letting the renderer continue with a missing RenderFrame that it
> expects to be present until it crashes somewhere random later.
>
> [email protected], [email protected]
>
> Bug: 957858, 838348
> Change-Id: I2110bdaf8b116df48037f69db6cb992fa3796e29
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1594834
> Commit-Queue: danakj <[email protected]>
> Reviewed-by: Daniel Cheng <[email protected]>
> Reviewed-by: Avi Drissman <[email protected]>
> Cr-Commit-Position: refs/heads/master@{#660025}

Bug: 957858, 838348
Change-Id: Iba424cc7be7db053c7ca617677b3cacf972fa067
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1614132
Commit-Queue: danakj <[email protected]>
Reviewed-by: Lucas Gadani <[email protected]>
Reviewed-by: Avi Drissman <[email protected]>
Cr-Commit-Position: refs/heads/master@{#660128}
diff --git a/content/browser/frame_host/render_frame_host_impl.cc b/content/browser/frame_host/render_frame_host_impl.cc
index 3cc4e533..47e5d1da8 100644
--- a/content/browser/frame_host/render_frame_host_impl.cc
+++ b/content/browser/frame_host/render_frame_host_impl.cc
@@ -1758,12 +1758,12 @@
   return true;
 }
 
-void RenderFrameHostImpl::DeleteRenderFrame() {
+void RenderFrameHostImpl::DeleteRenderFrame(FrameDeleteIntention intent) {
   if (!is_active())
     return;
 
   if (render_frame_created_) {
-    Send(new FrameMsg_Delete(routing_id_));
+    Send(new FrameMsg_Delete(routing_id_, intent));
 
     if (!frame_tree_node_->IsMainFrame() && IsCurrent()) {
       // If this subframe has an unload handler (and isn't speculative), ensure
@@ -2036,7 +2036,8 @@
       // observers are notified of its deletion.
       std::unique_ptr<FrameTreeNode> node_to_delete(std::move(*iter));
       children_.erase(iter);
-      node_to_delete->current_frame_host()->DeleteRenderFrame();
+      node_to_delete->current_frame_host()->DeleteRenderFrame(
+          FrameDeleteIntention::kNotMainFrame);
       // Speculative RenderFrameHosts are deleted by the FrameTreeNode's
       // RenderFrameHostManager's destructor. RenderFrameProxyHosts send
       // FrameMsg_Delete automatically in the destructor.
@@ -2059,7 +2060,8 @@
   // this RenderFrameHostImpl to detach the current frame's children, rather
   // than messaging each child's current frame host...
   for (auto& child : children)
-    child->current_frame_host()->DeleteRenderFrame();
+    child->current_frame_host()->DeleteRenderFrame(
+        FrameDeleteIntention::kNotMainFrame);
 }
 
 void RenderFrameHostImpl::SetLastCommittedUrl(const GURL& url) {
@@ -2389,7 +2391,7 @@
     return;
 
   // Start pending deletion on this frame and its children.
-  DeleteRenderFrame();
+  DeleteRenderFrame(FrameDeleteIntention::kNotMainFrame);
   StartPendingDeletionOnSubtree();
   // Some children with no unload handler may be eligible for immediate
   // deletion. Cut the dead branches now. This is a performance optimization.
@@ -4525,7 +4527,7 @@
           local_ancestor = rfh;
       }
 
-      local_ancestor->DeleteRenderFrame();
+      local_ancestor->DeleteRenderFrame(FrameDeleteIntention::kNotMainFrame);
       if (local_ancestor != child) {
         child->unload_state_ =
             child->GetSuddenTerminationDisablerState(blink::kUnloadHandler)
diff --git a/content/browser/frame_host/render_frame_host_impl.h b/content/browser/frame_host/render_frame_host_impl.h
index d5ad7ff..2c3a4ae 100644
--- a/content/browser/frame_host/render_frame_host_impl.h
+++ b/content/browser/frame_host/render_frame_host_impl.h
@@ -41,6 +41,7 @@
 #include "content/common/content_export.h"
 #include "content/common/content_security_policy/csp_context.h"
 #include "content/common/frame.mojom.h"
+#include "content/common/frame_delete_intention.h"
 #include "content/common/frame_message_enums.h"
 #include "content/common/frame_replication_state.h"
 #include "content/common/image_downloader/image_downloader.mojom.h"
@@ -332,7 +333,7 @@
 
   // Deletes the RenderFrame in the renderer process.
   // Postcondition: |is_active()| will return false.
-  void DeleteRenderFrame();
+  void DeleteRenderFrame(FrameDeleteIntention intent);
 
   // Tracks whether the RenderFrame for this RenderFrameHost has been created in
   // the renderer process.  This is currently only used for subframes.
diff --git a/content/browser/frame_host/render_frame_host_manager.cc b/content/browser/frame_host/render_frame_host_manager.cc
index de85226a..184de12 100644
--- a/content/browser/frame_host/render_frame_host_manager.cc
+++ b/content/browser/frame_host/render_frame_host_manager.cc
@@ -823,7 +823,10 @@
 std::unique_ptr<RenderFrameHostImpl>
 RenderFrameHostManager::UnsetSpeculativeRenderFrameHost() {
   speculative_render_frame_host_->GetProcess()->RemovePendingView();
-  speculative_render_frame_host_->DeleteRenderFrame();
+  speculative_render_frame_host_->DeleteRenderFrame(
+      frame_tree_node_->parent()
+          ? FrameDeleteIntention::kNotMainFrame
+          : FrameDeleteIntention::kSpeculativeMainFrameForNavigationCancelled);
   return std::move(speculative_render_frame_host_);
 }
 
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
index aa8c31f..0acd4b2c 100644
--- a/content/browser/web_contents/web_contents_impl.cc
+++ b/content/browser/web_contents/web_contents_impl.cc
@@ -689,7 +689,8 @@
   // Do not update state as the WebContents is being destroyed.
   frame_tree_.root()->ResetNavigationRequest(true, true);
   if (root->speculative_frame_host()) {
-    root->speculative_frame_host()->DeleteRenderFrame();
+    root->speculative_frame_host()->DeleteRenderFrame(
+        FrameDeleteIntention::kSpeculativeMainFrameForShutdown);
     root->speculative_frame_host()->SetRenderFrameCreated(false);
     root->speculative_frame_host()->ResetNavigationRequests();
   }
diff --git a/content/common/BUILD.gn b/content/common/BUILD.gn
index 0b464d4..4029ff0 100644
--- a/content/common/BUILD.gn
+++ b/content/common/BUILD.gn
@@ -112,6 +112,7 @@
     "font_list_fontconfig.cc",
     "font_list_mac.mm",
     "font_list_win.cc",
+    "frame_delete_intention.h",
     "frame_message_enums.h",
     "frame_message_structs.cc",
     "frame_message_structs.h",
diff --git a/content/common/frame_delete_intention.h b/content/common/frame_delete_intention.h
new file mode 100644
index 0000000..f30103c3
--- /dev/null
+++ b/content/common/frame_delete_intention.h
@@ -0,0 +1,27 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CONTENT_COMMON_FRAME_DELETE_INTENTION_H_
+#define CONTENT_COMMON_FRAME_DELETE_INTENTION_H_
+
+namespace content {
+
+enum class FrameDeleteIntention {
+  // The frame being deleted isn't a (speculative) main frame.
+  kNotMainFrame,
+  // The frame being deleted is a speculative main frame, and it is being
+  // deleted as part of the shutdown for that WebContents. The entire RenderView
+  // etc will be destroyed by a separate IPC sent later.
+  kSpeculativeMainFrameForShutdown,
+  // The frame being deleted is a speculative main frame, and it is being
+  // deleted because the speculative navigation was cancelled. This is not part
+  // of shutdown.
+  kSpeculativeMainFrameForNavigationCancelled,
+
+  kMaxValue = kSpeculativeMainFrameForNavigationCancelled
+};
+
+}  // namespace content
+
+#endif  // CONTENT_COMMON_FRAME_DELETE_INTENTION_H_
diff --git a/content/common/frame_messages.h b/content/common/frame_messages.h
index 9308081..78b5de6 100644
--- a/content/common/frame_messages.h
+++ b/content/common/frame_messages.h
@@ -26,6 +26,7 @@
 #include "content/common/content_param_traits.h"
 #include "content/common/content_security_policy/csp_context.h"
 #include "content/common/content_security_policy_header.h"
+#include "content/common/frame_delete_intention.h"
 #include "content/common/frame_message_enums.h"
 #include "content/common/frame_message_structs.h"
 #include "content/common/frame_owner_properties.h"
@@ -101,6 +102,8 @@
 #define IPC_MESSAGE_EXPORT CONTENT_EXPORT
 
 #define IPC_MESSAGE_START FrameMsgStart
+IPC_ENUM_TRAITS_MAX_VALUE(content::FrameDeleteIntention,
+                          content::FrameDeleteIntention::kMaxValue)
 IPC_ENUM_TRAITS_MAX_VALUE(blink::FrameOwnerElementType,
                           blink::FrameOwnerElementType::kMaxValue)
 IPC_ENUM_TRAITS_MAX_VALUE(
@@ -813,7 +816,7 @@
 IPC_MESSAGE_ROUTED1(FrameMsg_VisualStateRequest, uint64_t /* id */)
 
 // Instructs the renderer to delete the RenderFrame.
-IPC_MESSAGE_ROUTED0(FrameMsg_Delete)
+IPC_MESSAGE_ROUTED1(FrameMsg_Delete, content::FrameDeleteIntention)
 
 // Instructs the renderer to invoke the frame's beforeunload event handler.
 // Expects the result to be returned via FrameHostMsg_BeforeUnload_ACK.
diff --git a/content/renderer/render_frame_impl.cc b/content/renderer/render_frame_impl.cc
index b8106b9..be8e442 100644
--- a/content/renderer/render_frame_impl.cc
+++ b/content/renderer/render_frame_impl.cc
@@ -2418,7 +2418,47 @@
   SwapIn();
 }
 
-void RenderFrameImpl::OnDeleteFrame() {
+void RenderFrameImpl::OnDeleteFrame(FrameDeleteIntention intent) {
+  // The main frame (when not provisional) is owned by the renderer's frame tree
+  // via WebViewImpl. When a provisional main frame is swapped in, the ownership
+  // moves from the browser to the renderer, but this happens in the renderer
+  // process and is then the browser is informed.
+  // If the provisional main frame is swapped in while the browser is destroying
+  // it, the browser may request to delete |this|, thinking it has ownership
+  // of it, but the renderer has already taken ownership via SwapIn().
+  switch (intent) {
+    case FrameDeleteIntention::kNotMainFrame:
+      // The frame was not a main frame, so the browser should always have
+      // ownership of it and we can just proceed with deleting it on
+      // request.
+      DCHECK(!is_main_frame_);
+      break;
+    case FrameDeleteIntention::kSpeculativeMainFrameForShutdown:
+      // In this case the renderer has taken ownership of the provisional main
+      // frame but the browser did not know yet and is shutting down. We can
+      // ignore this request as the frame will be destroyed when the RenderView
+      // is. This handles the shutdown case of https://crbug.com/957858.
+      DCHECK(is_main_frame_);
+      if (in_frame_tree_)
+        return;
+      break;
+    case FrameDeleteIntention::kSpeculativeMainFrameForNavigationCancelled:
+      // In this case the browser was navigating and cancelled the speculative
+      // navigation. The renderer *should* undo the SwapIn() but the old state
+      // has already been destroyed. Both ignoring the message or handling it
+      // would leave the renderer in an inconsistent state now. If we ignore it
+      // then the browser thinks the RenderView has a remote main frame, but it
+      // is incorrect. If we handle it, then we are deleting a local main frame
+      // out from under the RenderView and we will have bad pointers in the
+      // renderer. So all we can do is crash. We should instead prevent this
+      // scenario by blocking the browser from dropping the speculative main
+      // frame when a commit (and ownership transfer) is imminent.
+      // TODO(dcheng): This is the case of https://crbug.com/838348.
+      DCHECK(is_main_frame_);
+      CHECK(!in_frame_tree_);
+      break;
+  }
+
   // This will result in a call to RenderFrameImpl::FrameDetached, which
   // deletes the object. Do not access |this| after detach.
   frame_->Detach();
diff --git a/content/renderer/render_frame_impl.h b/content/renderer/render_frame_impl.h
index e63b99d1..097fe84 100644
--- a/content/renderer/render_frame_impl.h
+++ b/content/renderer/render_frame_impl.h
@@ -34,6 +34,7 @@
 #include "content/common/buildflags.h"
 #include "content/common/download/mhtml_file_writer.mojom.h"
 #include "content/common/frame.mojom.h"
+#include "content/common/frame_delete_intention.h"
 #include "content/common/frame_message_enums.h"
 #include "content/common/host_zoom.mojom.h"
 #include "content/common/media/renderer_audio_input_stream_factory.mojom.h"
@@ -1108,7 +1109,7 @@
   void OnSwapOut(int proxy_routing_id,
                  bool is_loading,
                  const FrameReplicationState& replicated_frame_state);
-  void OnDeleteFrame();
+  void OnDeleteFrame(FrameDeleteIntention intent);
   void OnStop();
   void OnCollapse(bool collapse);
   void OnShowContextMenu(const gfx::Point& location);
diff --git a/content/renderer/render_frame_impl_browsertest.cc b/content/renderer/render_frame_impl_browsertest.cc
index 644316c..6a5c000 100644
--- a/content/renderer/render_frame_impl_browsertest.cc
+++ b/content/renderer/render_frame_impl_browsertest.cc
@@ -497,7 +497,7 @@
       1, "foo", true /* match_case */, true /* forward */,
       false /* find_next */, true /* force */, false /* wrap_within_frame */);
 
-  FrameMsg_Delete delete_message(0);
+  FrameMsg_Delete delete_message(0, FrameDeleteIntention::kNotMainFrame);
   frame()->OnMessageReceived(delete_message);
 }