Skip to content

sendRequest leaks the curl handle on the error path; GALinux getConnectionType leaks a socket per interface #52

Description

@Neype

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.

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