// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// POC unit test for a use-after-free in BookmarksFolderChooserMediator.
//
// Scenario: a user is moving a child bookmark via the iOS folder chooser.
// While the chooser is open, a remote sync update deletes the *parent* folder
// of that bookmark. BookmarkModel only notifies observers about the root node
// being removed, so -[BookmarksFolderChooserMediator bookmarkNodeDeleted:] is
// invoked with the parent folder, not the child. The mediator only checks for
// an exact match in `_editedNodes` and therefore never evicts the child. The
// child's memory is freed (extensive sync changes suspend undo tracking, so
// nothing keeps it alive), leaving a dangling pointer in `_editedNodes`. When
// the user confirms the move, the dangling pointer is dereferenced in
// bookmark_utils_ios::MoveBookmarksWithUndoSnackbar -> node->is_folder().
//
// Append this test to:
//   ios/chrome/browser/bookmarks/folder_chooser/test/
//       bookmarks_folder_chooser_mediator_unittest.mm
//
// Build & run (macOS host, iOS simulator, ASAN):
//   autoninja -C out/asan-ios ios_chrome_unittests
//   out/asan-ios/iossim out/asan-ios/ios_chrome_unittests.app -- \
//       --gtest_filter=BookmarksFolderChooserMediatorUAFTest.*

#import "ios/chrome/browser/bookmarks/folder_chooser/coordinator/bookmarks_folder_chooser_mediator.h"

#import "base/location.h"
#import "components/bookmarks/browser/bookmark_model.h"
#import "components/bookmarks/browser/bookmark_node.h"
#import "components/signin/public/base/consent_level.h"
#import "components/sync/test/test_sync_service.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_ios_unit_test_support.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/bookmark_utils_ios.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/signin/model/fake_system_identity.h"
#import "ios/chrome/browser/signin/model/fake_system_identity_manager.h"
#import "testing/gtest_mac.h"
#import "url/gurl.h"

using bookmarks::BookmarkNode;

namespace {

class BookmarksFolderChooserMediatorUAFTest
    : public BookmarkIOSUnitTestSupport {
 public:
  void SetUp() override {
    BookmarkIOSUnitTestSupport::SetUp();

    authentication_service_ =
        AuthenticationServiceFactory::GetForProfile(profile_.get());
    FakeSystemIdentityManager* system_identity_manager =
        FakeSystemIdentityManager::FromSystemIdentityManager(
            GetApplicationContext()->GetSystemIdentityManager());
    FakeSystemIdentity* fake_identity = [FakeSystemIdentity fakeIdentity1];
    system_identity_manager->AddIdentity(fake_identity);
    authentication_service_->SignIn(
        fake_identity, signin_metrics::AccessPoint::kBookmarkManager);
    sync_service_.SetSignedIn(signin::ConsentLevel::kSignin);
  }

  void TearDown() override {
    if (mediator_) {
      [mediator_ disconnect];
    }
    BookmarkIOSUnitTestSupport::TearDown();
  }

 protected:
  BookmarksFolderChooserMediator* mediator_ = nil;
  raw_ptr<AuthenticationService> authentication_service_;
  syncer::TestSyncService sync_service_;
};

// Demonstrates that deleting an ancestor folder while one of its descendants
// is selected in the folder chooser leaves a dangling pointer in
// `_editedNodes`, and that completing the move dereferences freed memory.
TEST_F(BookmarksFolderChooserMediatorUAFTest,
       DanglingEditedNodeAfterAncestorDeletedViaSync) {
  // Step 1: Build  mobile_node / Folder A / Bookmark X.
  const BookmarkNode* mobile = bookmark_model_->mobile_node();
  const BookmarkNode* folder_a =
      bookmark_model_->AddFolder(mobile, 0, u"Folder A");
  const BookmarkNode* bookmark_x = bookmark_model_->AddURL(
      folder_a, 0, u"Bookmark X", GURL("https://example.com/x"));

  // Destination for the move (must survive the deletion).
  const BookmarkNode* destination =
      bookmark_model_->AddFolder(mobile, 1, u"Destination");

  // Step 2: User taps Edit, selects Bookmark X, taps "Move".
  // The coordinator constructs the mediator with the selected node.
  std::set<raw_ptr<const BookmarkNode>> edited_nodes;
  edited_nodes.insert(bookmark_x);
  mediator_ = [[BookmarksFolderChooserMediator alloc]
      initWithBookmarkModel:bookmark_model_
                editedNodes:std::move(edited_nodes)
      authenticationService:authentication_service_
                syncService:&sync_service_];

  ASSERT_EQ(1u, [mediator_ editedNodes].size());

  // Step 3: A remote sync update arrives that deletes Folder A.
  // BookmarkDataTypeProcessor wraps the apply in ScopedRemoteUpdateBookmarks,
  // which calls BeginExtensiveChanges(). This causes BookmarkUndoService to
  // suspend undo tracking, so the removed subtree's unique_ptr is dropped on
  // the floor instead of being retained by an undo operation.
  bookmark_model_->BeginExtensiveChanges();
  bookmark_model_->Remove(folder_a, bookmarks::metrics::BookmarkEditSource::kOther,
                          FROM_HERE);
  bookmark_model_->EndExtensiveChanges();

  // BUG: The mediator received bookmarkNodeDeleted: for `folder_a` only.
  // Because `_editedNodes` contains `bookmark_x` (a descendant, not
  // `folder_a` itself), the contains() check fails and `bookmark_x` is left
  // in the set even though its backing memory has been freed.
  //
  // With the fix applied, `_editedNodes` would be empty here and the mediator
  // would have asked its delegate to dismiss.
  ASSERT_FALSE([mediator_ editedNodes].empty())
      << "Mediator evicted the descendant — bug appears to be fixed.";

  // Step 4: User taps the destination folder and confirms the move.
  // BookmarksFolderChooserCoordinator collects [mediator_ editedNodes] into a
  // vector of raw pointers and calls MoveBookmarksWithUndoSnackbar, which
  // immediately dereferences each node via node->is_folder().
  std::vector<const BookmarkNode*> bookmarks_to_move;
  for (const BookmarkNode* node : [mediator_ editedNodes]) {
    bookmarks_to_move.push_back(node);
  }

  // *** Use-After-Free triggers here. ***
  // Under ASAN this reports heap-use-after-free in BookmarkNode::is_folder().
  bookmark_utils_ios::MoveBookmarksWithUndoSnackbar(
      bookmarks_to_move, bookmark_model_, destination, profile_.get(),
      authentication_service_->GetWeakPtr(), &sync_service_);
}

}  // namespace
