open-telemetry / open-telemetry/opentelemetry-cpp

[BUG] An asynchronous curl request whose Setup fails leaks its easy handle and header list

Open
#4,397 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

needs-triage
Dominant language
C++
Stars
1.4k
Forks
632
Avg merge
1d 13h
Merged PRs (30d)
75

Description

Describe your environment main at f6e48180, Ubuntu 24.04, gcc 14, libcurl 8.14.1, CMake, -fsanitize=address with leak detection on.

An asynchronous request whose Setup() fails leaks its easy handle and its header list. The easiest way in is a method the client does not implement:

  else
  {
    OTEL_INTERNAL_LOG_ERROR("Unexpected HTTP method");          // http_operation_curl.cc:1347
    return CURLE_UNSUPPORTED_PROTOCOL;
  }

Everything except Get and Post lands there, so Put, Options, Head, Patch and Delete all fail Setup(). SendAsync reports it and returns:

  CURLcode code     = Setup();
  last_curl_result_ = code;
  if (code != CURLE_OK)
  {
    const char *message = GetCurlErrorMessage(code);
    DispatchEvent(...ConnectFailed, message);                   // :1449
    return code;
  }

By then async_data_ exists, because SendAsync builds it before calling Setup(), and async_data_->session is still null because that is published later. When the operation is destroyed, Cleanup() takes the asynchronous branch on the strength of async_data_ being present, finds no session to hand the resource to, and returns:

    // Set value to promise to continue Finish()
    if (true == async_data_->is_promise_running.exchange(false, ...))
    {
      async_data_->result_promise.set_value(last_curl_result_);
    }

    return;                                                     // :577
  }

  // Sync mode
  if (curl_resource_.easy_handle != nullptr)
  {
    curl_easy_cleanup(curl_resource_.easy_handle);              // :583
    ...
  }

  if (curl_resource_.headers_chunk != nullptr)
  {
    curl_slist_free_all(curl_resource_.headers_chunk);          // :589

The only code that frees the handle and the list is below that return, in the synchronous branch. HttpCurlEasyResource has a move constructor and a move assignment but no destructor, so nothing else picks them up either.

Steps to reproduce One request, using the existing fixture:

TEST_F(BasicCurlHttpTests, SetupFailureLeaksTheHandle)
{
  auto session_manager = std::make_shared<http_client::curl::HttpCurlClientFactory>()->Create();
  auto session = session_manager->CreateSession("http://127.0.0.1:19000");
  auto request = session->CreateRequest();
  request->SetUri("get/");
  request->SetMethod(http_client::Method::Head);

  auto handler = std::make_shared<TerminalCountingHandler>();
  session->SendRequest(handler);
  session->FinishSession();
  session_manager->FinishAllSessions();
}
Direct leak of 5456 byte(s) in 1 object(s) allocated from:
    #0 calloc
    #1 (/lib/x86_64-linux-gnu/libcurl.so.4+0x8e605)

Indirect leak of 128 byte(s) in 5 object(s) allocated from:
    #0 strdup
    #1 (/lib/x86_64-linux-gnu/libcurl.so.4+0x7afec)

SUMMARY: AddressSanitizer: 5584 byte(s) leaked in 6 allocation(s).

The 5456 bytes are what curl_easy_init allocates and the five strdups are the header list. The current suite is clean under the same build, because nothing in it uses a method other than Get or Post.

What is the expected behavior? A request that never reaches the multi handle releases the resources it allocated.

What is the actual behavior? One easy handle and one header list leak per request. For an exporter that keeps retrying a misconfigured endpoint, that accumulates.

Additional context The fix is confined to the paths that return before the handle is scheduled: free the list and the handle there, since nothing else owns them yet. It must stay confined to those paths, because once the handle is in the multi handle libcurl requires it to be removed before it can be cleaned up, which is #4391.

Two things worth separating from this. The first is that Put, Head and the rest failing at all is its own question, since Method offers them and the curl client answers CURLE_UNSUPPORTED_PROTOCOL. The second is that the event reported here is ConnectFailed when nothing was connected, which is part of what #4360 covers.

Found while looking at the startup paths for #4390.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in http_operation_curl.cc by tracing SendAsync, Setup(), and Cleanup(), focusing on the asynchronous path when Setup returns before the handle reaches the multi handle. Run the existing BasicCurlHttpTests fixture with the SetupFailureLeaksTheHandle case under AddressSanitizer. Done means the failed request releases its easy handle and header list without changing cleanup for scheduled handles.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
networking
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.