Skip to content

[BUG] Send one request per curl session - #4431

Draft
thc1006 wants to merge 1 commit into
open-telemetry:mainfrom
thc1006:bugfix/session-is-single-use-4396
Draft

[BUG] Send one request per curl session#4431
thc1006 wants to merge 1 commit into
open-telemetry:mainfrom
thc1006:bugfix/session-is-single-use-4396

Conversation

@thc1006

@thc1006 thc1006 commented Aug 14, 2026

Copy link
Copy Markdown
Member

Fixes #4396.

What happens today

Session::SendRequest() replaces the operation the session owns. The easy handle of the first request holds that operation in every pointer libcurl calls back through, and holds the session in CURLOPT_PRIVATE. The message loop reads that back and resolves it to whichever operation the session owns by then:

curl_easy_getinfo(easy_handle, CURLINFO_PRIVATE, &session);
const auto operation = (nullptr != session) ? session->GetOperation().get() : nullptr;

So a second request takes delivery of the first one's completion, and the first one's handle keeps calling back into an operation nobody owns.

From a handler that sends again out of OnResponse it is worse than a mix up. Cleanup() takes the completion callback and runs it while the operation it belongs to is still on the stack, so the unique_ptr::reset destroys the object whose Cleanup() is two frames up, and Cleanup() reads it again on the way out.

What this does

A session carries one request:

  • the first SendRequest consumes it, and a second reports CreateFailed to its own handler and sends nothing, which is what a send that cannot be made already reports on this class;
  • CreateRequest() after that leaves the request the send is reading from where it is, because libcurl does not copy the body or the header list.

Evidence

The case is the reproduction from the issue, a handler that sends again from OnResponse. Same binary, same command, either side of the change:

before after
ASecondRequestFromInsideTheResponseIsRefused exit 1, heap-use-after-free in unique_ptr<AsyncData>::_M_ptr() passes
AddressSanitizer reports 1 0
whole curl_http_test under ASan 28 of 28, no sanitizer output

The second case, ASessionSendsOneRequest, is the same rule without the re-entrancy: send, wait, send again, and check that the second is refused and the backing request was not replaced under the first.

Is anything relying on reuse

Not here. Both callers make a session, send once and let it go:

  • exporters/otlp/src/otlp_http_client.cc:1029 creates the session, :1098 is the only send;
  • exporters/elasticsearch/src/es_log_record_exporter.cc:405 creates it, :462 and :467 are the two arms of one #ifdef.

No case in curl_http_test sends twice on one session either, and the three OTLP HTTP exporter test binaries pass under ASan unchanged: 55, 16 and 22.

What I did not do

I have not put the rule in the ext/http/client/http_client.h interface, only on the curl Session that implements it. Saying it in the interface would bind every implementation, including the one in examples/custom_http_client, and that reads like your call rather than mine. Say the word and I will move it.

The alternative is to keep reuse and give each operation an identity of its own: CURLOPT_PRIVATE would carry the operation rather than the session, the pending records would own the exact operation, its request and its handler, and the message loop would resolve the operation that owns the handle rather than the one the session owns now. That is a rewrite of how this client owns things, it conflicts with every open change to the same file, and nothing in this repository asks for reuse. If you want it, it wants its own issue.

Checks

result
curl_http_test under ASan with detect_leaks=1 28 of 28, no sanitizer output
same, WITH_OTLP_RETRY_PREVIEW=ON 28 of 28
bazel test //ext/test/http:curl_http_test passes
otlp_http_exporter_test, log record, metric, all under ASan 55, 16, 22, all pass
clang-format 18.1.8 clean

For significant contributions please make sure you have completed the following items:

  • CHANGELOG.md updated for non-trivial changes
  • Unit tests have been added
  • Changes in public API reviewed

SendRequest replaces the operation the session owns. The easy handle of the
first request holds that operation in every pointer libcurl calls back through,
and holds the session in CURLOPT_PRIVATE, which the message loop reads back and
resolves to whichever operation the session owns by then. So a second request
takes delivery of the first one's completion.

From a handler that sends again out of OnResponse it is worse than a mix up.
Cleanup takes the completion callback and runs it while the operation it belongs
to is still on the stack, so the reset destroys the object whose Cleanup is two
frames up, and Cleanup reads it again on the way out. AddressSanitizer reports
heap-use-after-free in unique_ptr<AsyncData>::_M_ptr, and the case that provokes
it is the reproduction from the issue.

A session now carries one request. A second SendRequest reports CreateFailed to
its own handler and sends nothing, which is what a send that cannot be made
already reports, and CreateRequest leaves the request a send is reading from
where it is, since libcurl does not copy the body or the header list.

Every caller here already works that way: the OTLP HTTP client and the
Elasticsearch exporter each make a session, send once and let it go, and no case
in the suite sends twice on one session.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.59%. Comparing base (3fb1d31) to head (637ce6d).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4431      +/-   ##
==========================================
+ Coverage   82.58%   82.59%   +0.01%     
==========================================
  Files         511      511              
  Lines       20039    20045       +6     
==========================================
+ Hits        16548    16554       +6     
  Misses       3491     3491              
Files with missing lines Coverage Δ
...ntelemetry/ext/http/client/curl/http_client_curl.h 95.13% <100.00%> (+0.13%) ⬆️
ext/src/http/client/curl/http_client_curl.cc 90.42% <100.00%> (+0.09%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Reusing a curl Session destroys the HttpOperation that is still running its own completion

1 participant