fix(network): don't leak duped-layer equipment via EquipUpdate/OPL (#2502)
## Problem A creature equipped with two items that resolve to the **same layer** can crash the legacy EA 2D client (use-after-free). Equipment `Layer` comes from tiledata (`Layer = (Layer)ItemData.Quality`), so a **two-handed weapon and a shield both resolve to `Layer.TwoHanded`**. `Mobile.FindItemOnLayer` even documents the invariant: *"We only allow 1 item per layer. Its an implicit contract."* ## Root cause - `SendMobileIncoming` (0x78) **dedupes by layer** (the `layers` span) and sends only the first item per slot — so a static creature is fine. - But the per-item **`SendEquipUpdate` (0x2E)** and the item **OPL** sends do **not** dedupe. On any equip/property delta (`Item.ProcessDelta`) they fire per item and leak the second same-layer item on its own. - The client then holds two items on one equipment slot; when that slot is torn down (e.g. a large group of such creatures and the player runs out of range → mass remove) the legacy 2D client frees one and dereferences it → UAF. ClassicUO bounds-checks and is unaffected, but the server is emitting an invalid, self-contradictory stream either way. ## Fix Make per-item equip/OPL sends honor the same first-item-per-layer rule `SendMobileIncoming` already uses: - `Item.IsDupedEquipLayer()` — `m_Parent is Mobile m && m.FindItemOnLayer(m_Layer) != this` (reuses the existing helper; true when an earlier item already holds this items layer). - `Item.ProcessDelta`: early-return before the per-client loop for a duped equipped item (skips the EquipUpdate and OPL to everyone). - `Mobile` lift-reject re-show: skip the EquipUpdate and OPL for the dupe. No behavioral change for valid equipment (distinct layers → never duped). For the invalid duped-layer case the second item was already omitted by the 0x78 packet; this just stops it leaking back via the per-item paths.
This commit is contained in:
parent
70276dcf52
commit
73d1ee3874
2 changed files with 22 additions and 2 deletions
|
|
@ -536,6 +536,14 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
}
|
||||
}
|
||||
|
||||
// SendMobileIncoming (0x78) sends only the first item per layer (it dedupes via the
|
||||
// `layers` span). Equipment Layer comes from tiledata, so e.g. a two-handed weapon and a
|
||||
// shield both resolve to Layer.TwoHanded; a later same-layer item sent on its own via
|
||||
// EquipUpdate (0x2E) or its OPL would leave the client holding two items on one equipment
|
||||
// slot (a use-after-free on the legacy 2D client when that slot is later torn down).
|
||||
// Returns true if this equipped item is such a later duplicate and must not be sent alone.
|
||||
public bool IsDupedEquipLayer() => m_Parent is Mobile m && m.FindItemOnLayer(m_Layer) != this;
|
||||
|
||||
public List<Item> Items => LookupItems() ?? EmptyItems;
|
||||
|
||||
public int LookupContainerVersion() => (this as Container)?._version ?? LookupCompactInfo()?.Version ?? 0;
|
||||
|
|
@ -1387,6 +1395,14 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
return;
|
||||
}
|
||||
|
||||
// A second item sharing an equipment layer is omitted by SendMobileIncoming (0x78);
|
||||
// sending it on its own via EquipUpdate/OPL would leave the client with two items on
|
||||
// one slot. Skip the per-client sends entirely for the dupe.
|
||||
if (IsDupedEquipLayer())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var state in map.GetClientsInRange(worldLoc, GetMaxUpdateRange()))
|
||||
{
|
||||
var m = state.Mobile;
|
||||
|
|
|
|||
|
|
@ -5187,14 +5187,18 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
}
|
||||
else if (item.Parent is Mobile)
|
||||
{
|
||||
state.SendEquipUpdate(item);
|
||||
// Don't re-show a duped-layer item on its own (see Item.IsDupedEquipLayer).
|
||||
if (!item.IsDupedEquipLayer())
|
||||
{
|
||||
state.SendEquipUpdate(item);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
item.SendInfoTo(state);
|
||||
}
|
||||
|
||||
if (item.Parent != null)
|
||||
if (item.Parent != null && !item.IsDupedEquipLayer())
|
||||
{
|
||||
item.SendOPLPacketTo(state);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue