diff --git a/chrome/renderer/autofill/password_generation_agent_browsertest.cc b/chrome/renderer/autofill/password_generation_agent_browsertest.cc index 1e660a126c707..9ba78d86f4f4e 100644 --- a/chrome/renderer/autofill/password_generation_agent_browsertest.cc +++ b/chrome/renderer/autofill/password_generation_agent_browsertest.cc @@ -37,12 +37,14 @@ #include "services/service_manager/public/cpp/interface_provider.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" +#include "third_party/blink/public/common/renderer_preferences/renderer_preferences.h" #include "third_party/blink/public/platform/web_string.h" #include "third_party/blink/public/web/web_document.h" #include "third_party/blink/public/web/web_element.h" #include "third_party/blink/public/web/web_frame_widget.h" #include "third_party/blink/public/web/web_input_element.h" #include "third_party/blink/public/web/web_local_frame.h" +#include "third_party/blink/public/web/web_view.h" #include "ui/events/keycodes/keyboard_codes.h" using autofill::mojom::FocusedFieldType; @@ -1582,5 +1584,107 @@ TEST_F(PasswordGenerationAgentTest, AutomaticSuggestionOnHasBeenPasswordField) { ExpectAutomaticGenerationAvailable(kPasswordElementId, kAvailable); } +// ===================================================================== +// Regression test: UAF in GeneratedPasswordAccepted() when JS re-enters +// MaybeCreateCurrentGenerationItem() during the password_elements_ loop. +// +// On Android Chrome (default flags) a JS .focus() during transient user +// activation synchronously routes through AutofillAgent::FocusedElementChanged +// to ShowPasswordGenerationSuggestions(). On desktop this path is disabled +// (focus_requires_scroll = true). This fixture flips +// uses_platform_autofill so that the AutofillAgent under test is created +// with focus_requires_scroll = false, enabling the same synchronous path. +// ===================================================================== +class PasswordGenerationAgentReentrantUafTest + : public PasswordGenerationAgentTest { + public: + void RegisterMainFrameRemoteInterfaces() override { + blink::RendererPreferences prefs = + GetMainRenderFrame()->GetWebView()->GetRendererPreferences(); + prefs.uses_platform_autofill = true; + GetMainRenderFrame()->GetWebView()->SetRendererPreferences(prefs); + PasswordGenerationAgentTest::RegisterMainFrameRemoteInterfaces(); + } +}; + +constexpr char kTwoGenerationFormsHTML[] = + "
" + " " + " " + "
" + "
" + " " + " " + "
" + "
x
" + ""; + +TEST_F(PasswordGenerationAgentReentrantUafTest, + GeneratedPasswordAcceptedReentrantFree) { + // Allow any driver/client mojo calls during the re-entrant chain; the + // crash happens before they're flushed but TearDown may still see them. + EXPECT_CALL(fake_pw_client_, AutomaticGenerationAvailable(_)) + .Times(AnyNumber()); + EXPECT_CALL(fake_pw_client_, PasswordNoLongerGenerated(_)) + .Times(AnyNumber()); + EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(_, _)) + .Times(AnyNumber()); +#if !BUILDFLAG(IS_ANDROID) + EXPECT_CALL(fake_pw_client_, GenerationElementLostFocus()) + .Times(AnyNumber()); + EXPECT_CALL(fake_pw_client_, FrameWasScrolled()).Times(AnyNumber()); + EXPECT_CALL(fake_pw_client_, ShowPasswordEditingPopup).Times(AnyNumber()); + EXPECT_CALL(fake_pw_client_, PasswordGenerationRejectedByTyping()) + .Times(AnyNumber()); +#endif + + LoadHTMLWithUserGesture(kTwoGenerationFormsHTML); + + // Mark both password fields as generation-eligible (simulates two + // FoundFormEligibleForGeneration messages from the browser). + SetFoundFormEligibleForGeneration(password_generation_, + GetMainFrame()->GetDocument(), + /*new_password_id=*/"pwA", + /*confirm_password_id=*/nullptr); + SetFoundFormEligibleForGeneration(password_generation_, + GetMainFrame()->GetDocument(), + /*new_password_id=*/"pwB", + /*confirm_password_id=*/nullptr); + + // Click pwA: grants transient user activation and (via the + // FocusedElementChanged -> ShowSuggestions path) creates + // current_generation_item_ for pwA. + ASSERT_TRUE(SimulateElementClick("pwA")); + + // Install an `input` handler on pwA. SetAutofillValue() inside the + // GeneratedPasswordAccepted() loop dispatches `input` synchronously, so + // this runs while the loop holds a reference into + // current_generation_item_->password_elements_. + // 1. pwA.value = '' -> generation_element_.Value().length() == 0 + // 2. txt.focus() -> moves focus off pwA so step 3 is a real change + // 3. pwA.focus() -> ShowPasswordGenerationSuggestions(pwA) + // -> PasswordNoLongerGenerated() + // -> password_is_generated_ = false (disarm) + // 4. pwB.focus() -> MaybeCreateCurrentGenerationItem(pwB) + // -> guard at :859-862 falls through + // -> current_generation_item_ reassigned + // -> old password_elements_ buffer freed + ExecuteJavaScriptForTests( + "var ran = false;" + "document.getElementById('pwA').addEventListener('input', function() {" + " if (ran) return; ran = true;" + " document.getElementById('pwA').value = '';" + " document.getElementById('txt').focus();" + " document.getElementById('pwA').focus();" + " document.getElementById('pwB').focus();" + "});"); + + // Enter the vulnerable loop. SetAutofillValue(pwA) at :408 dispatches the + // input event above; on return `password_element` and `this` inside + // WebFormControlElement::SetAutofillValue point into freed heap. + // Under ASAN this reports heap-use-after-free. + password_generation_->GeneratedPasswordAccepted(u"random_password"); +} + } // namespace } // namespace autofill