Summary
ItemSerializerHelper.SetSocketBytes encodes each socket's seed-sphere level + element + option number into a single byte:
var optionIndex = SocketOptionIndexOffsets[elementType] + elementOption;
return (byte)((sphereLevel * MaximumSocketOptions) + optionIndex);
(src/GameServer/RemoteView/ItemSerializerHelper.cs:141-146, MaximumSocketOptions = 50, SocketOptionIndexOffsets = { 0, 10, 16, 21, 29, 36 } for Fire/Water/Ice/Wind/Lightning/Earth respectively, line 62)
For sphereLevel = 5 (the maximum socket bonus tier) on any element other than Fire, sphereLevel * 50 + optionIndex starts at 250 + 10 = 260 for Water and only grows for the later elements, this exceeds byte.MaxValue (255) and silently wraps around via the implicit (byte) cast, producing a completely unrelated index. Fire is the only element that stays in range at level 5 (250 + optionIndex(0..5) ≤ 255).
Example: level 5, Water, option number 4 → optionIndex = 10 + 4 = 14 → 250 + 14 = 264 → cast to byte → 8 (wrapped), instead of a value the client can make sense of.
Impact, reachable through normal gameplay, not just admin tools
This isn't limited to GM/SQL-crafted items. Tracing the standard crafting chain:
- "Seed Creation" (Seed Master NPC) randomly rolls a Seed's
Level up to ItemDefinition.MaximumItemLevel, which is 5 for every element alike (Fire, Water, Ice, Wind, Lightning, Earth), CreateSeed() computes it from the same per-element ItemOptionDefinition that's also used for the corresponding Seed Sphere (src/Persistence/Initialization/VersionSeasonSix/Items/SocketSystem.cs).
- "Seed Sphere Creation" copies that level onto the resulting Seed Sphere (
SeedSphereCrafting.cs: result.Level = seed.Level).
- "Mount Seed Sphere" copies it again onto the equipped item's socket (
MountSeedSphereCrafting.cs: sphereOption.Level = seedSphere.Level).
So a player who legitimately rolls a Level-5 Water/Ice/Wind/Lightning/Earth seed through normal RNG and mounts it ends up with an item whose socket byte silently wraps around on every serialization. When that item is sent to a client (equipped, viewed in a shop/trade window, shown to another player, etc.), the corrupted byte is used by the client to index into a fixed-size table. We reproduced this with a real client (Season 6, English protocol build): it writes out of bounds and crashes immediately with a stack-corruption error (Run-Time Check Failure #2 - Stack around the variable 'iSeedSum' was corrupted) as soon as the item is loaded/rendered.
Suggested fix
The offset table was reverse-engineered from the original client/protocol (per the comment on line 59, "found these index offsets by trial and error"), so the safe range is a client-side constraint OpenMU should defend against server-side rather than assume is never reached:
- Cap
ItemDefinition.MaximumItemLevel for non-Fire Seeds/Seed Spheres at 4 instead of 5 (the safe range holds for every element through level 4: 4*50 + maxOptionIndexForThatElement ≤ 255), or
- Rework the encoding so level 5 fits in a byte for every element, if that's what the real client protocol expects.
Either way, an explicit bounds check (or at least a guarding comment) would help, today even the AdminPanel's item option editor lets you freely set Level=5 on a non-Fire socket option with no warning, so the unsafe state is easy to reintroduce even after a data fix.
Environment
- Confirmed against
origin/master (commit b29338b78).
- Reproduced with a real MuMain (Season 6, English protocol) client, crash occurs immediately on entering the map with the affected item equipped.
Summary
ItemSerializerHelper.SetSocketBytesencodes each socket's seed-sphere level + element + option number into a singlebyte:(
src/GameServer/RemoteView/ItemSerializerHelper.cs:141-146,MaximumSocketOptions = 50,SocketOptionIndexOffsets = { 0, 10, 16, 21, 29, 36 }for Fire/Water/Ice/Wind/Lightning/Earth respectively, line 62)For
sphereLevel = 5(the maximum socket bonus tier) on any element other than Fire,sphereLevel * 50 + optionIndexstarts at250 + 10 = 260for Water and only grows for the later elements, this exceedsbyte.MaxValue(255) and silently wraps around via the implicit(byte)cast, producing a completely unrelated index. Fire is the only element that stays in range at level 5 (250 + optionIndex(0..5) ≤ 255).Example: level 5, Water, option number 4 →
optionIndex = 10 + 4 = 14→250 + 14 = 264→ cast tobyte→ 8 (wrapped), instead of a value the client can make sense of.Impact, reachable through normal gameplay, not just admin tools
This isn't limited to GM/SQL-crafted items. Tracing the standard crafting chain:
Levelup toItemDefinition.MaximumItemLevel, which is5for every element alike (Fire, Water, Ice, Wind, Lightning, Earth),CreateSeed()computes it from the same per-elementItemOptionDefinitionthat's also used for the corresponding Seed Sphere (src/Persistence/Initialization/VersionSeasonSix/Items/SocketSystem.cs).SeedSphereCrafting.cs:result.Level = seed.Level).MountSeedSphereCrafting.cs:sphereOption.Level = seedSphere.Level).So a player who legitimately rolls a Level-5 Water/Ice/Wind/Lightning/Earth seed through normal RNG and mounts it ends up with an item whose socket byte silently wraps around on every serialization. When that item is sent to a client (equipped, viewed in a shop/trade window, shown to another player, etc.), the corrupted byte is used by the client to index into a fixed-size table. We reproduced this with a real client (Season 6, English protocol build): it writes out of bounds and crashes immediately with a stack-corruption error (
Run-Time Check Failure #2 - Stack around the variable 'iSeedSum' was corrupted) as soon as the item is loaded/rendered.Suggested fix
The offset table was reverse-engineered from the original client/protocol (per the comment on line 59, "found these index offsets by trial and error"), so the safe range is a client-side constraint OpenMU should defend against server-side rather than assume is never reached:
ItemDefinition.MaximumItemLevelfor non-Fire Seeds/Seed Spheres at4instead of5(the safe range holds for every element through level 4:4*50 + maxOptionIndexForThatElement ≤ 255), orEither way, an explicit bounds check (or at least a guarding comment) would help, today even the AdminPanel's item option editor lets you freely set
Level=5on a non-Fire socket option with no warning, so the unsafe state is easy to reintroduce even after a data fix.Environment
origin/master(commitb29338b78).