phosphor-objmgr: remove pending associations by service

If Service A has an association on Object X with Object Y (which comes
from Service C), and Service B also has an association definition for
Object X, there is a scenario where the X-Y association will not be
created, even though both objects are present in the D-Bus tree.

This change matches with the service while removing the pending
association for an object. Only the pending association introduced by
the service which removes the object/interface will be erased.

The change is accepted in Upstream.

Tested: Unit Tested. Also tested in a machine and checked all the
associations were created correctly following the above steps.

Fusion-Link:
platform5: https://fusion2.corp.google.com/741e9e05-9d22-33b3-ac0f-17610fe1ddfd
platform15: https://fusion2.corp.google.com/1c47d1b1-4b57-3df4-a172-acab8f30eb99
platform18: https://fusion2.corp.google.com/755b43f1-f9e8-32a5-880c-2d57bb0ad784
platform17: https://fusion2.corp.google.com/63ef3f9b-ad59-3528-8c54-4a986f05c649
platform11+ext: https://fusion2.corp.google.com/2fba6019-1d63-35b3-bb09-535621a27da4
platform11: https://fusion2.corp.google.com/86d39ef0-61aa-38bf-a34c-781793592b2b

Platforms-Affected: All
Google-Bug-Id: 488925115
Change-Id: I79bc7cfcf782a6aeee68370a3463ca84c124995e
Signed-off-by: Munawar Hussain <munawarhussain@google.com>
(cherry picked from commit 74ace72c37ecdbcfc11fd216754f08bef490fa9a)
diff --git a/recipes-phosphor/dbus/phosphor-objmgr/0001-remove-pending-associations-by-owner.patch b/recipes-phosphor/dbus/phosphor-objmgr/0001-remove-pending-associations-by-owner.patch
new file mode 100644
index 0000000..8364b3b
--- /dev/null
+++ b/recipes-phosphor/dbus/phosphor-objmgr/0001-remove-pending-associations-by-owner.patch
@@ -0,0 +1,146 @@
+From fc757250682036146d706aeae1e18d4de4bd4813 Mon Sep 17 00:00:00 2001
+From: Munawar Hussain <munawarhussain@google.com>
+Date: Mon, 9 Mar 2026 09:25:10 +0000
+Subject: [PATCH] remove pending associations by owner
+
+Changes:
+Match with the service while removing the pending association for an
+object. Only the pending association introduced by the service which
+removes the object/interface will be erased.
+
+If Service A has an association on Object X with Object Y (which comes
+from Service C), and Service B also has an association definition for
+Object X, there is a scenario where the X-Y association will not be
+created, even though both objects are present in the D-Bus tree.
+
+Steps to Reproduce:
+1. Service A starts and creates Object X. It creates a pending
+association Y-X.
+2. Service B starts, registers properties for Object X, and creates
+other associations.
+3. Service B drops Object X and its associated definitions. As a
+result, all pending associations for Object X are wiped out (including
+X-Y which is from A).
+4. Service B creates Object X again along with other associations
+related to Service B (but not the ones from Service A).
+5. Service C finally starts and creates Object Y, but no pending
+association is processed to complete the X-Y link.
+
+Tested:
+Unit tested. Also testing on a machine with the above steps and all
+the associations were still created.
+
+Patch Tracking Bug: b/491287006
+Upstream info / review: https://gerrit.openbmc.org/c/openbmc/phosphor-objmgr/+/88131
+Upstream-Status: Accepted
+Justification: Bugfix
+  - b/488925115
+
+Google-Bug-Id: 488925115
+Signed-off-by: Munawar Hussain <munawarhussain@google.com>
+---
+ src/associations.cpp      |  6 ++++--
+ src/associations.hpp      |  2 ++
+ src/test/associations.cpp | 29 +++++++++++++++++++++++++++--
+ 3 files changed, 33 insertions(+), 4 deletions(-)
+
+diff --git a/src/associations.cpp b/src/associations.cpp
+index db911f9..1eceddd 100644
+--- a/src/associations.cpp
++++ b/src/associations.cpp
+@@ -129,7 +129,7 @@ void removeAssociation(boost::asio::io_context& io,
+ 
+     // If we were still waiting on the other side of this association to
+     // show up, cancel that wait.
+-    removeFromPendingAssociations(sourcePath, assocMaps);
++    removeFromPendingAssociations(sourcePath, owner, assocMaps);
+ }
+ 
+ void removeAssociationEndpoints(
+@@ -345,6 +345,7 @@ void addPendingAssociation(const std::string& objectPath,
+ }
+ 
+ void removeFromPendingAssociations(const std::string& endpointPath,
++                                   const std::string& owner,
+                                    AssociationMaps& assocMaps)
+ {
+     auto assoc = assocMaps.pending.begin();
+@@ -354,7 +355,8 @@ void removeFromPendingAssociations(const std::string& endpointPath,
+         while (endpoint != assoc->second.end())
+         {
+             auto& e = std::get<assocPos>(*endpoint);
+-            if (std::get<reversePathPos>(e) == endpointPath)
++            if (std::get<reversePathPos>(e) == endpointPath &&
++                std::get<ownerPos>(*endpoint) == owner)
+             {
+                 endpoint = assoc->second.erase(endpoint);
+                 continue;
+diff --git a/src/associations.hpp b/src/associations.hpp
+index 1d7b778..d9bdbb1 100644
+--- a/src/associations.hpp
++++ b/src/associations.hpp
+@@ -126,9 +126,11 @@ void addPendingAssociation(const std::string& objectPath,
+  * If the last endpoint is removed, removes the whole entry
+  *
+  * @param[in] endpointPath  - the endpoint path to remove
++ * @param[in] owner         - The service name that owns the association
+  * @param[in,out] assocMaps - The association maps
+  */
+ void removeFromPendingAssociations(const std::string& endpointPath,
++                                   const std::string& owner,
+                                    AssociationMaps& assocMaps);
+ 
+ /** @brief Adds a single association D-Bus object (<path>/<type>)
+diff --git a/src/test/associations.cpp b/src/test/associations.cpp
+index fe0a826..cfd7de9 100644
+--- a/src/test/associations.cpp
++++ b/src/test/associations.cpp
+@@ -433,7 +433,8 @@ TEST_F(TestAssociations, testRemoveFromPendingAssociations)
+ 
+     EXPECT_EQ(assocMaps.pending.size(), 1);
+ 
+-    removeFromPendingAssociations("some/other/endpoint", assocMaps);
++    removeFromPendingAssociations("some/other/endpoint", defaultDbusSvc,
++                                  assocMaps);
+ 
+     // Still 1 pending entry, but down to 1 endpoint
+     EXPECT_EQ(assocMaps.pending.size(), 1);
+@@ -444,10 +445,34 @@ TEST_F(TestAssociations, testRemoveFromPendingAssociations)
+     EXPECT_EQ(endpoints.size(), 1);
+ 
+     // Now nothing pending
+-    removeFromPendingAssociations(defaultEndpoint, assocMaps);
++    removeFromPendingAssociations(defaultEndpoint, defaultDbusSvc, assocMaps);
+     EXPECT_EQ(assocMaps.pending.size(), 0);
+ }
+ 
++// Test removing pending associations by multiple owners
++TEST_F(TestAssociations, testRemoveFromPendingAssociationsMultipleOwners)
++{
++    AssociationMaps assocMaps;
++
++    addPendingAssociation(defaultSourcePath, "inventory", defaultEndpoint,
++                          "error", "serviceA", assocMaps);
++
++    addPendingAssociation(defaultSourcePath, "inventory", defaultEndpoint,
++                          "error", "serviceB", assocMaps);
++
++    EXPECT_EQ(assocMaps.pending.size(), 1);
++
++    removeFromPendingAssociations(defaultEndpoint, "serviceB", assocMaps);
++
++    EXPECT_EQ(assocMaps.pending.size(), 1);
++
++    auto assoc = assocMaps.pending.find(defaultSourcePath);
++    EXPECT_NE(assoc, assocMaps.pending.end());
++    auto& endpoints = assoc->second;
++    EXPECT_EQ(endpoints.size(), 1);
++    EXPECT_EQ(std::get<ownerPos>(endpoints[0]), "serviceA");
++}
++
+ // Test moving a pending association to a real one
+ TEST_F(TestAssociations, checkIfPending)
+ {
+-- 
+2.53.0.473.g4a7958ca14-goog
+
diff --git a/recipes-phosphor/dbus/phosphor-objmgr_%.bbappend b/recipes-phosphor/dbus/phosphor-objmgr_%.bbappend
index 47840e3..a7daf8d 100644
--- a/recipes-phosphor/dbus/phosphor-objmgr_%.bbappend
+++ b/recipes-phosphor/dbus/phosphor-objmgr_%.bbappend
@@ -2,4 +2,5 @@
 
 SRC_URI:append:gbmc = "\
     file://0001-associations-fix-leak-in-assocMaps.ifaces.patch \
+    file://0001-remove-pending-associations-by-owner.patch \
 "