Two socket/handle leaks in the native SDK, found while chasing a player report on our Unity build. Same report from the Unity SDK side: GameAnalytics/GA-SDK-UNITY#59
One of our Steam players ended up with 31,911 open TCP connections owned by our game after about 6 hours of play — 99.8% of all connections on his machine, all loopback pairs inside the game's own PID (127.0.0.1:65110 <-> 127.0.0.1:65111 and so on). Once Windows ran out of socket resources, every other app on the box started failing with WSAENOBUFS (10055): browsers stopped loading, the machine looked like it had lost its internet connection. Quitting the game freed everything instantly. He has hit this three times, always after long uptime. On a fresh start he measured ~15 new connections per minute, growing linearly, and only 1 of the ~32k connections went anywhere outside the machine.
Versions: com.gameanalytics.sdk 8.1.0 (8.0.1 behaves the same), native SDK reports itself as cpp 5.4.0, Unity 6000.5.8f1, IL2CPP, Standalone.
1. Leaked curl handle on the error path — this is the Windows one
GAHttpCurl.cpp:57-65:
CURLcode res = curl_easy_perform(curl);
if (res != CURLE_OK)
{
logging::GALogger::d("CURL request failed: %s", curl_easy_strerror(res));
return {}; // curl handle is never cleaned up
}
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response.code);
curl_easy_cleanup(curl);
Every failed request leaks the easy handle. curl_easy_perform builds an internal multi handle whose wakeup mechanism on Windows is a socketpair emulated as a pair of TCP connections over 127.0.0.1 — so each leaked handle leaves behind two loopback sockets that stay ESTABLISHED forever, while the actual connection to the server is closed by the remote side. That matches the report exactly: loopback pairs owned by the game, effectively no external connections, and roughly one pair per 8-second event-queue flush (~15 sockets/minute).
Reproduced by running the shipped library with no network reachable, so every request fails: fds grow by exactly 2 per CURL request failed, and they are the internal wakeup descriptors (anon_inode:[eventfd] on Linux, where curl uses eventfd instead of a socketpair):
before init: fds=4
t= 5s fds=10
t= 10s fds=12
t= 20s fds=14
t= 25s fds=16
Same function also never calls curl_slist_free_all on the header list built in createRequest, so the headers leak on every request, successful ones included.
2. Leaked socket per network interface on every event — Linux/Unix
GALinux.cpp:258-280 — the inner sock shadows the outer one, so the close() at the bottom always sees -1 and never runs:
int sock = -1;
if (!current->ifa_addr || current->ifa_addr->sa_family != AF_PACKET)
{
...
int sock = socket(AF_INET, SOCK_STREAM, 0); // shadows the outer sock
...
}
if(sock != -1) // always -1
close(sock);
getConnectionType() is called from GAState::getEventAnnotations(), i.e. once per event, and the loop runs per interface — so this leaks one socket per network interface per event.
Repro: dlopen the libGameAnalytics.so that ships in the package, send one design event every 5 seconds, count /proc/self/fd:
before init: fds=4 sockets=0
t= 5s fds=54 sockets=48
t= 10s fds=78 sockets=72
t= 30s fds=176 sockets=168
t= 85s fds=438 sockets=432
Linear, nothing released. Confirmed these are the leaked sockets and not something else with an LD_PRELOAD shim that keeps a backtrace per fd on socket(), drops it on close(), and dumps whatever is still open at exit — every live fd has the same stack:
gameanalytics::GAPlatformLinux::getConnectionType()
gameanalytics::device::GADevice::getConnectionType()
gameanalytics::state::GAState::getEventAnnotations(nlohmann::json&)
gameanalytics::events::GAEvents::addEventToStore(nlohmann::json&)
Notes
7.9.1 is unaffected: Standalone didn't use the native C++ SDK then (it shipped the managed Runtime/GameAnalytics.dll + sqlite3). Both leaks reach Unity users with 8.0.0, when the desktop path moved onto the native SDK.
Fixes are small: clean up the handle (and free the header list) on every path out of sendRequest, and drop the int on the inner sock in GALinux.cpp.
Two socket/handle leaks in the native SDK, found while chasing a player report on our Unity build. Same report from the Unity SDK side: GameAnalytics/GA-SDK-UNITY#59
One of our Steam players ended up with 31,911 open TCP connections owned by our game after about 6 hours of play — 99.8% of all connections on his machine, all loopback pairs inside the game's own PID (
127.0.0.1:65110 <-> 127.0.0.1:65111and so on). Once Windows ran out of socket resources, every other app on the box started failing withWSAENOBUFS(10055): browsers stopped loading, the machine looked like it had lost its internet connection. Quitting the game freed everything instantly. He has hit this three times, always after long uptime. On a fresh start he measured ~15 new connections per minute, growing linearly, and only 1 of the ~32k connections went anywhere outside the machine.Versions:
com.gameanalytics.sdk8.1.0 (8.0.1 behaves the same), native SDK reports itself ascpp 5.4.0, Unity 6000.5.8f1, IL2CPP, Standalone.1. Leaked curl handle on the error path — this is the Windows one
GAHttpCurl.cpp:57-65:Every failed request leaks the easy handle.
curl_easy_performbuilds an internal multi handle whose wakeup mechanism on Windows is a socketpair emulated as a pair of TCP connections over 127.0.0.1 — so each leaked handle leaves behind two loopback sockets that stay ESTABLISHED forever, while the actual connection to the server is closed by the remote side. That matches the report exactly: loopback pairs owned by the game, effectively no external connections, and roughly one pair per 8-second event-queue flush (~15 sockets/minute).Reproduced by running the shipped library with no network reachable, so every request fails: fds grow by exactly 2 per
CURL request failed, and they are the internal wakeup descriptors (anon_inode:[eventfd]on Linux, where curl uses eventfd instead of a socketpair):Same function also never calls
curl_slist_free_allon the header list built increateRequest, so the headers leak on every request, successful ones included.2. Leaked socket per network interface on every event — Linux/Unix
GALinux.cpp:258-280— the innersockshadows the outer one, so theclose()at the bottom always sees-1and never runs:getConnectionType()is called fromGAState::getEventAnnotations(), i.e. once per event, and the loop runs per interface — so this leaks one socket per network interface per event.Repro: dlopen the
libGameAnalytics.sothat ships in the package, send one design event every 5 seconds, count/proc/self/fd:Linear, nothing released. Confirmed these are the leaked sockets and not something else with an
LD_PRELOADshim that keeps abacktraceper fd onsocket(), drops it onclose(), and dumps whatever is still open at exit — every live fd has the same stack:Notes
7.9.1 is unaffected: Standalone didn't use the native C++ SDK then (it shipped the managed
Runtime/GameAnalytics.dll+ sqlite3). Both leaks reach Unity users with 8.0.0, when the desktop path moved onto the native SDK.Fixes are small: clean up the handle (and free the header list) on every path out of
sendRequest, and drop theinton the innersockinGALinux.cpp.