diff --git a/third_party/blink/renderer/core/exported/web_view_test.cc b/third_party/blink/renderer/core/exported/web_view_test.cc index a59f682536d47..a9383da43b370 100644 --- a/third_party/blink/renderer/core/exported/web_view_test.cc +++ b/third_party/blink/renderer/core/exported/web_view_test.cc @@ -159,6 +159,7 @@ #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h" #include "third_party/blink/renderer/platform/testing/url_loader_mock_factory.h" #include "third_party/blink/renderer/platform/testing/url_test_helpers.h" +#include "third_party/blink/renderer/platform/widget/widget_base.h" #include "third_party/blink/renderer/platform/wtf/casting.h" #include "third_party/blink/renderer/platform/wtf/functional.h" #include "third_party/skia/include/core/SkBitmap.h" @@ -6145,6 +6146,136 @@ TEST_F(WebViewTest, DetachPluginInLayout) { web_view_helper_.Reset(); // Remove dependency on locally scoped client. } +// Tests that WidgetBase::UpdateCompositionInfo handles destruction of the +// WidgetBase during the synchronous layout it triggers. CompositionRange() +// forces UpdateStyleAndLayout, whose PluginDisposeSuspendScope destructor can +// dispose plugins which are explicitly permitted to run script (see +// ScriptForbiddenScope::AllowUserAgentScript in +// WebPluginContainerImpl::Dispose). Such script may detach a local-root +// subframe, synchronously invoking WebFrameWidgetImpl::Close which resets +// widget_base_ while UpdateCompositionInfo is still on the stack. +TEST_F(WebViewTest, UpdateCompositionInfoWidgetDestroyedDuringLayout) { + // A plugin that runs a callback during Destroy(), simulating a plugin + // implementation that runs script on destruction. + class CallbackInDestroyPlugin : public FakeWebPlugin { + public: + explicit CallbackInDestroyPlugin(const WebPluginParams& params) + : FakeWebPlugin(params) {} + void Destroy() override { + if (destroy_callback_) { + std::move(destroy_callback_).Run(); + } + FakeWebPlugin::Destroy(); + } + void SetDestroyCallback(base::OnceClosure callback) { + destroy_callback_ = std::move(callback); + } + + private: + base::OnceClosure destroy_callback_; + }; + + class PluginCreatingWebFrameClient + : public frame_test_helpers::TestWebFrameClient { + public: + WebPlugin* CreatePlugin(const WebPluginParams& params) override { + last_plugin_ = new CallbackInDestroyPlugin(params); + return last_plugin_; + } + CallbackInDestroyPlugin* last_plugin() { return last_plugin_; } + void clear_last_plugin() { last_plugin_ = nullptr; } + + private: + CallbackInDestroyPlugin* last_plugin_ = nullptr; + }; + + // Set up: Remote main frame -> Local child frame (a local root with its own + // WidgetBase). This mirrors an OOPIF scenario where a subframe has its own + // widget that can be destroyed independently of the page. + PluginCreatingWebFrameClient frame_client; + frame_test_helpers::WebViewHelper helper; + helper.InitializeRemote(); + WebLocalFrameImpl* local_frame = + helper.CreateLocalChild(*helper.RemoteMainFrame(), "child", + WebFrameOwnerProperties(), nullptr, &frame_client); + + WebURL base_url = url_test_helpers::ToKURL("http://example.com/"); + frame_test_helpers::LoadHTMLString( + local_frame, + "" + "" + "" + "", + base_url); + local_frame->FrameWidget()->Resize(gfx::Size(300, 300)); + local_frame->GetFrame()->View()->UpdateAllLifecyclePhasesForTest(); + + Document* document = local_frame->GetFrame()->GetDocument(); + Element* input = document->getElementById(AtomicString("input")); + auto* plugin_element = + To(document->getElementById(AtomicString("plugin"))); + ASSERT_TRUE(input); + ASSERT_TRUE(plugin_element); + ASSERT_TRUE(plugin_element->OwnedPlugin()); + ASSERT_TRUE(frame_client.last_plugin()); + + // Focus the input so the text input type is non-NONE and the input method + // controller is active for this widget. + helper.GetWebView()->GetPage()->GetFocusController().SetActive(true); + helper.GetWebView()->GetPage()->GetFocusController().SetFocusedFrame( + local_frame->GetFrame()); + input->Focus(); + + // Set an IME composition so CompositionEphemeralRange() is non-null and + // CompositionRange() reaches the UpdateStyleAndLayout call. + WebInputMethodController* controller = + local_frame->FrameWidgetImpl()->GetActiveWebInputMethodController(); + ASSERT_TRUE(controller); + std::vector empty_ime_text_spans; + controller->SetComposition(WebString::FromUtf8("hello"), empty_ime_text_spans, + WebRange(), 5, 5); + + // Clean layout so the next style change triggers a real style/layout update + // inside CompositionRange(). + local_frame->GetFrame()->View()->UpdateAllLifecyclePhasesForTest(); + + // When the plugin is disposed during layout, simulate script that detaches + // the local-root frame (the same effect as a same-origin grandparent calling + // frameElement.remove() on the embedding iframe). This synchronously calls + // WebFrameWidgetImpl::Close -> widget_base_.reset(). + bool callback_ran = false; + Persistent core_frame = local_frame->GetFrame(); + frame_client.last_plugin()->SetDestroyCallback(BindOnce( + [](LocalFrame* frame, bool* ran) { + fprintf(stderr, "POC: plugin Destroy() callback running, detaching\n"); + *ran = true; + frame->Detach(FrameDetachType::kRemove); + fprintf(stderr, "POC: frame detached, WidgetBase freed\n"); + }, + WrapPersistent(core_frame.Get()), Unretained(&callback_ran))); + frame_client.clear_last_plugin(); + + // Dirty layout by hiding the plugin. The next UpdateStyleAndLayout will + // detach the plugin's layout tree, queue it for deferred disposal, and + // dispose it in ~PluginDisposeSuspendScope at the end of layout. + plugin_element->setAttribute(html_names::kStyleAttr, + AtomicString("display:none")); + + // Trigger WidgetBase::UpdateCompositionInfo with immediate_request=true. + // This calls GetCompositionRange() -> CompositionRange() -> + // UpdateStyleAndLayout(), which disposes the plugin and (via the callback) + // detaches the frame, freeing the WidgetBase. The subsequent access of + // `this->client_` in GetCompositionCharacterBounds() is then a UAF. + WidgetBase* widget_base = + local_frame->FrameWidgetImpl()->widget_base_for_testing(); + fprintf(stderr, "POC: calling UpdateCompositionInfo, plugin owned=%d\n", + !!plugin_element->OwnedPlugin()); + widget_base->UpdateCompositionInfo(/*immediate_request=*/true); + fprintf(stderr, "POC: UpdateCompositionInfo returned\n"); + + EXPECT_TRUE(callback_ran); +} + // Check that first input delay is correctly reported to the document. TEST_F(WebViewTest, FirstInputDelayReported) { WebViewImpl* web_view = web_view_helper_.Initialize();