Jump to content

[C++] Exchange dupe glitch exploit?


Recommended Posts

  • Active Member

Hey everyone 👋

While working on a server (3k+ online) that uses my serverfiles, some players found a strange issue in the exchange system — a “ghost dupe” situation that made it look like items were duplicated after a trade.

In some cases, an item given from Player A to Player B would remain visible in both inventories.
Player A actually kept the item, but Player B could also see it in his inventory.

In the database (item table), the item existed only once — owned by Player A.
After teleport or relog, the item disappeared from B’s inventory, so it wasn’t a real duplication — but it definitely looked like one to players.

After reviewing the code, I discovered the cause:
the original CExchange::Done() handled skip_save, ownership, and database flush in the wrong order.
This caused a temporary desynchronization and double visibility between memory and database states.

I’m not sure if any of you have noticed or run into this issue before, but here’s my fix for it.
The problem may also be triggered or influenced by other things in my server (i don't know).

But, if you have this problem, after applying this fix, all trades are now synchronized correctly — no ghost dupes.

 

 bool CExchange::Done()
 {
-	int		empty_pos, i;
-	LPITEM	item;
-
-	LPCHARACTER	victim = GetCompany()->GetOwner();
-
-	for (i = 0; i < EXCHANGE_ITEM_MAX_NUM; ++i)
+	int empty_pos;
+	LPITEM item;
+	LPCHARACTER victim = GetCompany()->GetOwner();
+
+	std::vector<LPITEM> itemsToFlush;
+
+	for (int i = 0; i < EXCHANGE_ITEM_MAX_NUM; ++i)
 	{
 		if (!(item = m_apItems[i]))
 			continue;
@@
 		if (item->GetWindow() == INVENTORY)
 		{
 			m_pOwner->SyncQuickslot(QUICKSLOT_TYPE_ITEM, item->GetCell(), 255);
 		}

 		item->RemoveFromCharacter();
 		if (item->IsDragonSoul())
 			item->AddToCharacter(victim, TItemPos(DRAGON_SOUL_INVENTORY, empty_pos));
 #ifdef ENABLE_SPECIAL_STORAGE
 		else if(item->IsUpgradeItem())
 			item->AddToCharacter(victim, TItemPos(UPGRADE_INVENTORY, empty_pos));
 		else if(item->IsBook())
 			item->AddToCharacter(victim, TItemPos(BOOK_INVENTORY, empty_pos));
 		else if(item->IsStone())
 			item->AddToCharacter(victim, TItemPos(STONE_INVENTORY, empty_pos));
 		else if(item->IsChest())
 			item->AddToCharacter(victim, TItemPos(CHEST_INVENTORY, empty_pos));
 #endif
 		else
 			item->AddToCharacter(victim, TItemPos(INVENTORY, empty_pos));
-
-		ITEM_MANAGER::instance().FlushDelayedSave(item);
-
-		item->SetExchanging(false);
+		
+		
+		item->SetSkipSave(false);
+		item->SetOwnership(victim);
+		item->SetExchanging(false);
+
+		ITEM_MANAGER::instance().SaveSingleItem(item);
+		itemsToFlush.push_back(item);
@@
 		{
 			char exchange_buf[51];
@@
 		m_apItems[i] = NULL;
 	}
@@
 	if (m_lGold)
 	{
 		GetOwner()->PointChange(POINT_GOLD, -m_lGold, true);
 		victim->PointChange(POINT_GOLD, m_lGold, true);
@@
 	}

-	m_pGrid->Clear();
-	return true;

+	for (LPITEM it : itemsToFlush)
+		ITEM_MANAGER::instance().FlushDelayedSave(it);
+
+	m_pGrid->Clear();
+	return true;
 }

 

  • Metin2 Dev 1
  • muscle 1
Link to comment
https://metin2.dev/topic/34079-c-exchange-dupe-glitch-exploit/
Share on other sites

  • Premium

It’s not really a fix — your modified code still performs the same operation, just with an extra loop triggering the same function again. That doesn’t solve the problem (and in fact, it didn’t).

The root cause occurs when, during or immediately after a trade, one or both trading players switch to another core and request a DB load.
After the load, the item reverts to its previous owner and gets saved there again.

So yes, at that moment, there are technically two instances of the same item. This state persists until the other character also performs a load request — at which point the item is removed from their inventory.

In short, there’s no definitive fix for this. However, if you add an exchange-time check to functions that can move a player to another core (like channel switch or warp), this issue won’t happen during normal operations.
But if the client crashes or is force-closed during a trade, the problem can still reoccur.

  • Active Member
3 hours ago, DenizCALISKAN said:

It’s not really a fix — your modified code still performs the same operation, just with an extra loop triggering the same function again. That doesn’t solve the problem (and in fact, it didn’t).

The root cause occurs when, during or immediately after a trade, one or both trading players switch to another core and request a DB load.
After the load, the item reverts to its previous owner and gets saved there again.

So yes, at that moment, there are technically two instances of the same item. This state persists until the other character also performs a load request — at which point the item is removed from their inventory.

In short, there’s no definitive fix for this. However, if you add an exchange-time check to functions that can move a player to another core (like channel switch or warp), this issue won’t happen during normal operations.
But if the client crashes or is force-closed during a trade, the problem can still reoccur.

        char.h

 

      bool IsExchanging() const { return m_pkExchange != nullptr; }

char.cpp in ::WarpSet

    if (IsExchanging())
    {
        return false;
    }

Hi,

You’re right that the issue comes from DB-cache synchronization — especially when a player changes channel or warps during an exchange.

However, my modification isn’t just a redundant loop. It forces an immediate synchronous save, which removes the actual cause of the duplication: the item still being owned by the previous player in DB memory.

And yes — if the server crashes exactly during the save operation, that’s a DB issue, not an Exchange logic problem.

  • 2 months later...
On 11/1/2025 at 2:42 PM, aXseee said:

It’s not really a fix — your modified code still performs the same operation, just with an extra loop triggering the same function again. That doesn’t solve the problem (and in fact, it didn’t).

The root cause occurs when, during or immediately after a trade, one or both trading players switch to another core and request a DB load.
After the load, the item reverts to its previous owner and gets saved there again.

So yes, at that moment, there are technically two instances of the same item. This state persists until the other character also performs a load request — at which point the item is removed from their inventory.

In short, there’s no definitive fix for this. However, if you add an exchange-time check to functions that can move a player to another core (like channel switch or warp), this issue won’t happen during normal operations.
But if the client crashes or is force-closed during a trade, the problem can still reoccur.

 

On 11/1/2025 at 6:21 PM, Kidro said:

        char.h

 

      bool IsExchanging() const { return m_pkExchange != nullptr; }

char.cpp in ::WarpSet

    if (IsExchanging())
    {
        return false;
    }

Hi,

You’re right that the issue comes from DB-cache synchronization — especially when a player changes channel or warps during an exchange.

However, my modification isn’t just a redundant loop. It forces an immediate synchronous save, which removes the actual cause of the duplication: the item still being owned by the previous player in DB memory.

And yes — if the server crashes exactly during the save operation, that’s a DB issue, not an Exchange logic problem.

 

The problem stems from the DB's packet reading system. There are issues that cause packets to not be read completely during heavy operations, and some packets to be lost or corrupted. If the lost or corrupted packet is an item recorded after a trade, then this problem occurs. Reducing the number of packets sent to the DB does not provide a complete solution; the problem persists, albeit to a lesser extent. The definitive solution to the problem is to rewrite the DB's packet reading system from scratch. The system must be able to read packets completely and accurately. This is a somewhat detailed and delicate process with a high risk of error. Only experienced individuals should perform it.

  • 1 month later...
  • Premium
On 1/17/2026 at 4:59 AM, Agares said:

 

 

The problem stems from the DB's packet reading system. There are issues that cause packets to not be read completely during heavy operations, and some packets to be lost or corrupted. If the lost or corrupted packet is an item recorded after a trade, then this problem occurs. Reducing the number of packets sent to the DB does not provide a complete solution; the problem persists, albeit to a lesser extent. The definitive solution to the problem is to rewrite the DB's packet reading system from scratch. The system must be able to read packets completely and accurately. This is a somewhat detailed and delicate process with a high risk of error. Only experienced individuals should perform it.

The issue is caused by m_iCacheFlushCountLimit in ClientManager. It is set to 200 by default within the class, and if you haven't increased this value in conf.txt (CACHE_FLUSH_LIMIT_PER_SECOND), items that hit the limit cannot be saved instantly. If a request for new data happens to coincide with this moment, the items end up getting rolled back. It may have been configured for the old system (such as MySQL 5.5). Setting it 750+ does not cause any issues.

Don't use any images from : imgur, turkmmop, freakgamers, inforge, hizliresim... Or your content will be deleted without notice...
Use : https://metin2.download/media/add/

Please use https://metin2.download/ when uploading files smaller than 100MB, otherwise the approval will take longer due to manual upload.

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now
×
×
  • Create New...

Important Information

Terms of Use / Privacy Policy / Guidelines / We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.