Chrome · Sync › Sync
CVE-2026-113004
Memory Corruption in Sync (Sync)
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/sync/engine/loopback_server/loopback_server.cc |
modified | |
GetLoopbackServerEntityTypecomponents/sync/engine/loopback_server/loopback_server_entity.cc |
modified | |
PersistentBookmarkEntitycomponents/sync/engine/loopback_server/loopback_server_entity.h |
modified | |
LoopbackServerEntitycomponents/sync/engine/loopback_server/loopback_server_entity.h |
modified | |
TESTcomponents/sync/engine/loopback_server/persistent_bookmark_entity_unittest.cc |
modified |
Files Changed
components/sync/engine/loopback_server/persistent_bookmark_entity.cccomponents/sync/engine/loopback_server/loopback_server_unittest.cccomponents/sync/engine/loopback_server/persistent_bookmark_entity.hcomponents/sync/engine/loopback_server/loopback_server_entity.cccomponents/sync/engine/loopback_server/loopback_server_entity.hcomponents/sync/engine/loopback_server/persistent_bookmark_entity_unittest.cccomponents/sync/engine/loopback_server/loopback_server.cc
Patch
From 820b0d2ed3d4873da6e10dd470252f2aea92276b Mon Sep 17 00:00:00 2001 From: Mikel Astiz <[email protected]> Date: Mon, 21 Sep 2026 02:17:29 -0700 Subject: [PATCH] [sync] Fix unsafe downcasting in LoopbackServer Previously, various codepaths including `PersistentBookmarkEntity::CreateUpdatedVersion()` assumed that any non-deleted entity being handled was an instance of `PersistentBookmarkEntity`, and performed an unchecked `static_cast`. However, in advanced scenarios like corrupt data on disk, this condition may not hold true. To fix this, this patch replaces all static_cast usages with newly-introduced virtual functions in LoopbackServerEntity that allow safe downcasting (return null otherwise). Change-Id: I2b37122213124728bff619285fe9041ce46e34bd Fixed: 513394775 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8425824 Commit-Queue: Mikel Astiz <[email protected]> Reviewed-by: Rushan Suleymanov <[email protected]> Cr-Commit-Position: refs/heads/main@{#1701610} --- diff --git a/components/sync/engine/loopback_server/loopback_server.cc b/components/sync/engine/loopback_server/loopback_server.cc index a15dd88aa..a70931e 100644 --- a/components/sync/engine/loopback_server/loopback_server.cc +++ b/components/sync/engine/loopback_server/loopback_server.cc @@ -912,13 +912,14 @@ const sync_pb::EntitySpecifics& updated_specifics) { EntityMap::const_iterator iter = entities_.find(id); if (iter == entities_.end() || - iter->second->GetDataType() != syncer::BOOKMARKS || GetDataTypeFromSpecifics(updated_specifics) != syncer::BOOKMARKS) { return false; } - PersistentBookmarkEntity* entity = - static_cast<PersistentBookmarkEntity*>(iter->second.get()); + PersistentBookmarkEntity* entity = iter->second->AsBookmarkEntity(); + if (!entity) { + return false; + } entity->SetParentId(parent_id); entity->SetSpecifics(updated_specifics); @@ -962,9 +963,10 @@ for (int i = 0; i < proto.entities_size(); ++i) { std::unique_ptr<LoopbackServerEntity> entity = LoopbackServerEntity::CreateEntityFromProto(proto.entities(i)); - // Silently drop entities that cannot be successfully deserialized. - if (entity) { - entities_[proto.entities(i).entity().id_string()] = std::move(entity); + // Silently drop entities that cannot be successfully deserialized or have + // a mismatched ID. + if (entity && entity->GetId() == proto.entities(i).entity().id_string()) { + entities_[entity->GetId()] = std::move(entity); } } diff --git a/components/sync/engine/loopback_server/loopback_server_entity.cc b/components/sync/engine/loopback_server/loopback_server_entity.cc index e5416ad..219836a7 100644 --- a/components/sync/engine/loopback_server/loopback_server_entity.cc +++ b/components/sync/engine/loopback_server/loopback_server_entity.cc @@ -116,6 +116,14 @@ return false; } +PersistentBookmarkEntity* LoopbackServerEntity::AsBookmarkEntity() { + return nullptr; +} + +const PersistentBookmarkEntity* LoopbackServerEntity::AsBookmarkEntity() const { + return nullptr; +} + sync_pb::LoopbackServerEntity_Type LoopbackServerEntity::GetLoopbackServerEntityType() const { NOTREACHED(); diff --git a/components/sync/engine/loopback_server/loopback_server_entity.h b/components/sync/engine/loopback_server/loopback_server_entity.h index c6327bb..c4d0efc 100644 --- a/components/sync/engine/loopback_server/loopback_server_entity.h +++ b/components/sync/engine/loopback_server/loopback_server_entity.h @@ -22,6 +22,8 @@ namespace syncer { +class PersistentBookmarkEntity; + // The representation of a Sync entity for the loopback server. class LoopbackServerEntity { public: @@ -67,6 +69,8 @@ virtual bool IsDeleted() const; virtual bool IsFolder() const; virtual bool IsPermanent() const; + virtual PersistentBookmarkEntity* AsBookmarkEntity(); + virtual const PersistentBookmarkEntity* AsBookmarkEntity() const; virtual void SerializeAsLoopbackServerEntity( sync_pb::LoopbackServerEntity* entity) const; diff --git a/components/sync/engine/loopback_server/persistent_bookmark_entity.cc b/components/sync/engine/loopback_server/persistent_bookmark_entity.cc index 22507a8..0e16f3b 100644 --- a/components/sync/engine/loopback_server/persistent_bookmark_entity.cc +++ b/components/sync/engine/loopback_server/persistent_bookmark_entity.cc @@ -31,7 +31,7 @@ PersistentBookmarkEntity::~PersistentBookmarkEntity() = default; // static -std::unique_ptr<LoopbackServerEntity> PersistentBookmarkEntity::CreateNew( +std::unique_ptr<PersistentBookmarkEntity> PersistentBookmarkEntity::CreateNew( const sync_pb::SyncEntity& client_entity, const string& parent_id, const string& originator_cache_guid, @@ -60,7 +60,7 @@ } // static -std::unique_ptr<LoopbackServerEntity> +std::unique_ptr<PersistentBookmarkEntity> PersistentBookmarkEntity::CreateUpdatedVersion( const sync_pb::SyncEntity& client_entity, const LoopbackServerEntity& current_server_entity, @@ -80,24 +80,24 @@ std::string originator_client_item_id; std::string client_tag_hash; - if (current_server_entity.IsDeleted()) { - // Handle undeletions. + if (const PersistentBookmarkEntity* current_bookmark_entity = + current_server_entity.AsBookmarkEntity()) { + // Regular case (non-undeletion). + originator_cache_guid = current_bookmark_entity->originator_cache_guid_; + originator_client_item_id = + current_bookmark_entity->originator_client_item_id_; + // Note that the client tag provided by the client in `client_entity` is + // ignored during non-creations updates, since it's meant to be immutable. + client_tag_hash = current_bookmark_entity->client_tag_hash_; + } else { + // Handle undeletions (or unexpected non-bookmark server entities) by + // replacing the entity using the committing client's metadata. originator_cache_guid = updating_client_cache_guid; originator_client_item_id = LoopbackServerEntity::GetInnerIdFromId(client_entity.id_string()); // An undeletion is similar to a creation, so let's honor the client tag // provided by the client (if any). client_tag_hash = client_entity.client_tag_hash(); - } else { - // Regular case (non-undeletion). - const PersistentBookmarkEntity& current_bookmark_entity = - static_cast<const PersistentBookmarkEntity&>(current_server_entity); - originator_cache_guid = current_bookmark_entity.originator_cache_guid_; - originator_client_item_id = - current_bookmark_entity.originator_client_item_id_; - // Note that the client tag provided by the client in `client_entity` is - // ignored during non-creations updates, since it's meant to be immutable. - client_tag_hash = current_bookmark_entity.client_tag_hash_; } // Using a version of 0 is okay here as it'll be updated before this entity is @@ -111,7 +111,7 @@ } // static -std::unique_ptr<LoopbackServerEntity> +std::unique_ptr<PersistentBookmarkEntity> PersistentBookmarkEntity::CreateFromEntity( const sync_pb::SyncEntity& client_entity) { if (!IsBookmark(client_entity)) { @@ -214,4 +214,13 @@ return is_folder_; } +PersistentBookmarkEntity* PersistentBookmarkEntity::AsBookmarkEntity() { + return this; +} + +const PersistentBookmarkEntity* PersistentBookmarkEntity::AsBookmarkEntity() + const { + return this; +} + } // namespace syncer diff --git a/components/sync/engine/loopback_server/persistent_bookmark_entity.h b/components/sync/engine/loopback_server/persistent_bookmark_entity.h index 8a24dac..11b429d 100644 --- a/components/sync/engine/loopback_server/persistent_bookmark_entity.h +++ b/components/sync/engine/loopback_server/persistent_bookmark_entity.h @@ -30,7 +30,7 @@ // Factory function for PersistentBookmarkEntity. This factory should be used // only for the first time that a specific bookmark is seen by the server. - static std::unique_ptr<LoopbackServerEntity> CreateNew( + static std::unique_ptr<PersistentBookmarkEntity> CreateNew( const sync_pb::SyncEntity& client_entity, const std::string& parent_id, const std::string& originator_cache_guid, @@ -40,15 +40,15 @@ // for this ID, `current_server_entity`, is passed here because the client // does not always send the complete entity over the wire. This requires // copying of some of the existing entity when creating a new entity. - static std::unique_ptr<LoopbackServerEntity> CreateUpdatedVersion( + static std::unique_ptr<PersistentBookmarkEntity> CreateUpdatedVersion( const sync_pb::SyncEntity& client_entity, const LoopbackServerEntity& current_server_entity, const std::string& parent_id, - const std::string& originator_cache_guid); + const std::string& updating_client_cache_guid); // Factory function for PersistentBookmarkEntity used when de-serializing the // information stored in the persistent storage. - static std::unique_ptr<LoopbackServerEntity> CreateFromEntity( + static std::unique_ptr<PersistentBookmarkEntity> CreateFromEntity( const sync_pb::SyncEntity& client_entity); PersistentBookmarkEntity(const std::string& id, @@ -72,6 +72,8 @@ std::string GetParentId() const override; void SerializeAsProto(sync_pb::SyncEntity* proto) const override; bool IsFolder() const override; + PersistentBookmarkEntity* AsBookmarkEntity() override; + const PersistentBookmarkEntity* AsBookmarkEntity() const override; sync_pb::LoopbackServerEntity_Type GetLoopbackServerEntityType() const override; diff --git a/components/sync/engine/loopback_server/loopback_server_unittest.cc b/components/sync/engine/loopback_server/loopback_server_unittest.cc index 2ef338b..c6ca628 100644 --- a/components/sync/engine/loopback_server/loopback_server_unittest.cc +++ b/components/sync/engine/loopback_server/loopback_server_unittest.cc @@ -362,4 +362,22 @@ EXPECT_FALSE(bookmarks[id].has_client_tag_hash()); } +TEST_F(LoopbackServerTest, + ShouldReplaceNonBookmarkServerEntityOnBookmarkCommit) { + // Ensure the permanent bookmark folders (e.g. `32904_bookmark_bar`, which is + // a PersistentPermanentEntity) are created on the server. + GetUpdatesForType(EntitySpecifics::kBookmarkFieldNumber); + + // Committing a bookmark update when the existing server entity is not a + // PersistentBookmarkEntity should safely replace the previous entity without + // downcasting it to PersistentBookmarkEntity. + SyncEntity entity; + entity.set_id_string("32904_bookmark_bar"); + entity.set_parent_id_string("32904_google_chrome_bookmarks"); + entity.set_version(1); + entity.mutable_specifics()->mutable_bookmark()->set_url(kUrl1); + + EXPECT_EQ("32904_bookmark_bar", CommitVerifySuccess(entity)); +} + } // namespace syncer diff --git a/components/sync/engine/loopback_server/persistent_bookmark_entity_unittest.cc b/components/sync/engine/loopback_server/persistent_bookmark_entity_unittest.cc index 232b4d3..0ab6b61 100644 --- a/components/sync/engine/loopback_server/persistent_bookmark_entity_unittest.cc +++ b/components/sync/engine/loopback_server/persistent_bookmark_entity_unittest.cc @@ -4,7 +4,10 @@ #include "components/sync/engine/loopback_server/persistent_bookmark_entity.h" +#include <memory> + #include "base/uuid.h" +#include "components/sync/engine/loopback_server/persistent_unique_client_entity.h" #include "components/sync/protocol/sync_entity.pb.h" #include "testing/gtest/include/gtest/gtest.h" @@ -30,12 +33,15 @@ TEST(PersistentBookmarkEntityTest, CreateUpdatedVersion) { sync_pb::SyncEntity client_entity; + client_entity.set_id_string( + "32904_" + base::Uuid::GenerateRandomV4().AsLowercaseString()); client_entity.mutable_specifics()->mutable_bookmark(); - auto server_entity = + std::unique_ptr<PersistentBookmarkEntity> server_entity = PersistentBookmarkEntity::CreateFromEntity(client_entity); ASSERT_TRUE(server_entity); + EXPECT_EQ(server_entity->AsBookmarkEntity(), server_entity.get()); - // Fails with since there's no version + // Fails since there's no version. ASSERT_FALSE(PersistentBookmarkEntity::CreateUpdatedVersion( client_entity, *server_entity, "parent_id", "updating_guid")); @@ -44,7 +50,27 @@ ASSERT_TRUE(PersistentBookmarkEntity::CreateUpdatedVersion( client_entity, *server_entity, "parent_id", "updating_guid")); - // But fails when not actually a bookmark. + // Succeeds when the existing server entity is not a bookmark entity (e.g. a + // tombstone or unexpected non-bookmark entity), replacing it using the + // committing client's metadata without downcasting. + sync_pb::EntitySpecifics unique_specifics; + unique_specifics.mutable_preference(); + std::unique_ptr<LoopbackServerEntity> non_bookmark_server_entity = + PersistentUniqueClientEntity::CreateFromSpecificsForTesting( + "non_unique_name", "client_tag", unique_specifics, + /*creation_time=*/0, /*last_modified_time=*/0); + ASSERT_TRUE(non_bookmark_server_entity); + EXPECT_EQ(non_bookmark_server_entity->AsBookmarkEntity(), nullptr); + std::unique_ptr<PersistentBookmarkEntity> replaced_entity = + PersistentBookmarkEntity::CreateUpdatedVersion( + client_entity, *non_bookmark_server_entity, "parent_id", + "updating_guid"); + ASSERT_TRUE(replaced_entity); + sync_pb::SyncEntity replaced_proto; + replaced_entity->SerializeAsProto(&replaced_proto); + EXPECT_EQ(replaced_proto.originator_cache_guid(), "updating_guid"); + + // Fails when client_entity is not actually a bookmark. client_entity.clear_specifics(); client_entity.mutable_specifics()->mutable_preference(); ASSERT_FALSE(PersistentBookmarkEntity::CreateUpdatedVersion(
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/components/sync/engine/loopback_server/loopback_server_unittest.cc b/components/sync/engine/loopback_server/loopback_server_unittest.cc
index 2ef338b..c6ca628 100644
--- a/components/sync/engine/loopback_server/loopback_server_unittest.cc
+++ b/components/sync/engine/loopback_server/loopback_server_unittest.cc
@@ -362,4 +362,22 @@
EXPECT_FALSE(bookmarks[id].has_client_tag_hash());
}
+TEST_F(LoopbackServerTest,
+ ShouldReplaceNonBookmarkServerEntityOnBookmarkCommit) {
+ // Ensure the permanent bookmark folders (e.g. `32904_bookmark_bar`, which is
+ // a PersistentPermanentEntity) are created on the server.
+ GetUpdatesForType(EntitySpecifics::kBookmarkFieldNumber);
+
+ // Committing a bookmark update when the existing server entity is not a
+ // PersistentBookmarkEntity should safely replace the previous entity without
+ // downcasting it to PersistentBookmarkEntity.
+ SyncEntity entity;
+ entity.set_id_string("32904_bookmark_bar");
+ entity.set_parent_id_string("32904_google_chrome_bookmarks");
+ entity.set_version(1);
+ entity.mutable_specifics()->mutable_bookmark()->set_url(kUrl1);
+
+ EXPECT_EQ("32904_bookmark_bar", CommitVerifySuccess(entity));
+}
+
} // namespace syncer
diff --git a/components/sync/engine/loopback_server/persistent_bookmark_entity_unittest.cc b/components/sync/engine/loopback_server/persistent_bookmark_entity_unittest.cc
index 232b4d3..0ab6b61 100644
--- a/components/sync/engine/loopback_server/persistent_bookmark_entity_unittest.cc
+++ b/components/sync/engine/loopback_server/persistent_bookmark_entity_unittest.cc
@@ -4,7 +4,10 @@
#include "components/sync/engine/loopback_server/persistent_bookmark_entity.h"
+#include <memory>
+
#include "base/uuid.h"
+#include "components/sync/engine/loopback_server/persistent_unique_client_entity.h"
#include "components/sync/protocol/sync_entity.pb.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -30,12 +33,15 @@
TEST(PersistentBookmarkEntityTest, CreateUpdatedVersion) {
sync_pb::SyncEntity client_entity;
+ client_entity.set_id_string(
+ "32904_" + base::Uuid::GenerateRandomV4().AsLowercaseString());
client_entity.mutable_specifics()->mutable_bookmark();
- auto server_entity =
+ std::unique_ptr<PersistentBookmarkEntity> server_entity =
PersistentBookmarkEntity::CreateFromEntity(client_entity);
ASSERT_TRUE(server_entity);
+ EXPECT_EQ(server_entity->AsBookmarkEntity(), server_entity.get());
- // Fails with since there's no version
+ // Fails since there's no version.
ASSERT_FALSE(PersistentBookmarkEntity::CreateUpdatedVersion(
client_entity, *server_entity, "parent_id", "updating_guid"));
@@ -44,7 +50,27 @@
ASSERT_TRUE(PersistentBookmarkEntity::CreateUpdatedVersion(
client_entity, *server_entity, "parent_id", "updating_guid"));
- // But fails when not actually a bookmark.
+ // Succeeds when the existing server entity is not a bookmark entity (e.g. a
+ // tombstone or unexpected non-bookmark entity), replacing it using the
+ // committing client's metadata without downcasting.
+ sync_pb::EntitySpecifics unique_specifics;
+ unique_specifics.mutable_preference();
+ std::unique_ptr<LoopbackServerEntity> non_bookmark_server_entity =
+ PersistentUniqueClientEntity::CreateFromSpecificsForTesting(
+ "non_unique_name", "client_tag", unique_specifics,
+ /*creation_time=*/0, /*last_modified_time=*/0);
+ ASSERT_TRUE(non_bookmark_server_entity);
+ EXPECT_EQ(non_bookmark_server_entity->AsBookmarkEntity(), nullptr);
+ std::unique_ptr<PersistentBookmarkEntity> replaced_entity =
+ PersistentBookmarkEntity::CreateUpdatedVersion(
+ client_entity, *non_bookmark_server_entity, "parent_id",
+ "updating_guid");
+ ASSERT_TRUE(replaced_entity);
+ sync_pb::SyncEntity replaced_proto;
+ replaced_entity->SerializeAsProto(&replaced_proto);
+ EXPECT_EQ(replaced_proto.originator_cache_guid(), "updating_guid");
+
+ // Fails when client_entity is not actually a bookmark.
client_entity.clear_specifics();
client_entity.mutable_specifics()->mutable_preference();
ASSERT_FALSE(PersistentBookmarkEntity::CreateUpdatedVersion(
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page