Skip to content

Server crash: GetBinding() returns nullptr by design but 52 call sites dereference it unchecked #537

Description

@secoonday

Summary

Eluna::GetBinding<T>() is written to return nullptr, but the returned pointer is dereferenced without a null check in 52 call sites (AzerothCore build). This crashed our production server.

Crash

AzerothCore 3.3.5a, rev 007b4464f5d2 (2026-07-22), ~50 concurrent players. Backtrace from a core dump:

#0  std::_Hashtable<EventKey<Hooks::ServerEvents>, ...>::empty (this=0x18)
#1  std::unordered_map<...>::empty (this=0x18)
#2  BindingMap<EventKey<Hooks::ServerEvents>>::HasBindingsFor (this=0x0, key=...)
        at src/server/game/LuaEngine/BindingMap.h:165
#3  Eluna::OnPacketSendAny (this=0x715637fc0280, player=0x7155c67f4c00, packet=..., result=@0x7156255fb297: true)
        at src/server/game/LuaEngine/hooks/PacketHooks.cpp:40
#4  Eluna::OnPacketSend (this=0x715637fc0280, session=<optimized out>, packet=...)
        at src/server/game/LuaEngine/hooks/PacketHooks.cpp:34
#5  WorldSession::SendPacket (this=0x7155a135c100, packet=0x7156255fb2e8)
        at src/server/game/Server/WorldSession.cpp:351
#6  Group::UpdatePlayerOutOfRange (this=<optimized out>, player=0x71554076d7c0)
        at src/server/game/Groups/Group.cpp:1888
#7  Player::SendUpdateToOutOfRangeGroupMembers (this=0x71554076d7c0)
        at src/server/game/Entities/Player/Player.cpp:11946
#8  Player::Update (this=0x71554076d7c0, p_time=1)
        at src/server/game/Entities/Player/PlayerUpdates.cpp:391
#9  Map::Update (this=0x715635914300, t_diff=100, s_diff=1)
        at src/server/game/Maps/Map.cpp:503

BindingMap::HasBindingsFor is called with this=0x0.

Worth noting: no Lua script on our server registers any packet event. The hook that crashed the server does nothing for us — it fires on every outgoing packet only to check a binding map that happened to be null.

Root cause

LuaEngine.h:392GetBinding() has two explicit return nullptr paths:

template<typename T>
BindingMap<T>* GetBinding(std::underlying_type_t<Hooks::RegisterTypes> type)
{
    if (type >= Hooks::REGTYPE_COUNT)
        return nullptr;

    auto& binding = bindingMaps[type];
    if (!binding)
        return nullptr;

    return dynamic_cast<BindingMap<T>*>(binding.get());
}

dynamic_cast can also yield nullptr on type mismatch.

Every START_HOOK* macro then does:

auto binding = GetBinding<...>(REGTYPE_...);
auto key = ...;
if (!binding->HasBindingsFor(key))   // <-- unchecked dereference
    return;

Scope

Call sites that dereference the result without a null check, in the code actually compiled for AzerothCore:

Location Sites
hooks/ (13 files) 27
methods/AzerothCore/GlobalMethods.h 16
LuaEngine.cpp 9
Total 52

The same GlobalMethods.h pattern is duplicated per core, so methods/{Mangos,CMangos,VMangos,TrinityCore} add 16 sites each — roughly 116 sites across all supported cores.

Suggested fix

Guard at the call sites (what we did locally, 52 changes, running in production):

if (!binding || !binding->HasBindingsFor(key))
    return;

Alternatively — and probably better — have the START_HOOK* macros do the null check once, so new hooks cannot reintroduce the bug. A third option is returning a reference to a shared empty BindingMap instead of nullptr, which removes the failure mode entirely.

Reproduction

We could not reproduce on demand: it happened once in ~17 hours of uptime with ~50 players. The core dump backtrace above is the evidence.

Happy to test a patch against a live server.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions