Skip to content

[BUG] HttpOperation holds references to constructor arguments that its own default arguments do not outlive #4433

Description

@thc1006

Describe your environment main at 3fb1d31, Ubuntu, gcc 14, libcurl 8.14.1, CMake, -fsanitize=address.

HttpOperation keeps a reference to four of its constructor arguments rather than a copy, in ext/include/opentelemetry/ext/http/client/curl/http_operation_curl.h:

  const opentelemetry::ext::http::client::HttpSslOptions &ssl_options_;   // :336
  const Headers &request_headers_;                                        // :338
  const opentelemetry::ext::http::client::Body &request_body_;            // :339
  const opentelemetry::ext::http::client::Compression &compression_;      // :344

Three of those four parameters have a default argument:

                const opentelemetry::ext::http::client::Headers &request_headers =
                    opentelemetry::ext::http::client::Headers(),
                const opentelemetry::ext::http::client::Body &request_body =
                    opentelemetry::ext::http::client::Body(),
                const opentelemetry::ext::http::client::Compression &compression =
                    opentelemetry::ext::http::client::Compression::kNone,

A default argument is a temporary created in the caller and destroyed at the end of the full-expression that contains the call, so a member reference bound to one dangles as soon as the constructor returns. The same happens for any caller that passes a prvalue for one of those parameters rather than a named object.

The header is installed, so this is reachable from outside the repository with nothing unusual: four arguments and the defaults do it.

Steps to reproduce

TEST_F(BasicCurlHttpTests, DefaultArgumentsDangle)
{
  http_client::HttpSslOptions no_ssl;
  curl::HttpOperation operation(http_client::Method::Get, "http://127.0.0.1:19000/get/", no_ssl,
                                nullptr);
  operation.Send();
}

Built with -fsanitize=address on main at 3fb1d31:

ERROR: AddressSanitizer: stack-use-after-scope on address 0x7f9071000830
SUMMARY: AddressSanitizer: stack-use-after-scope
  ext/src/http/client/curl/http_operation_curl.cc:1182 in HttpOperation::Setup()
    #0 HttpOperation::Setup()  http_operation_curl.cc:1182
    #1 HttpOperation::Send()   http_operation_curl.cc:1402

:1182 is the read of compression_. The constructor itself is fine because it uses the parameters, which are alive for the length of the call. The dangling read is later, when Setup() reads the member.

What is the expected behavior? Constructing an HttpOperation and using it is defined, however the caller spells the arguments.

What is the actual behavior? The defaults, and any prvalue argument, leave the object holding references to storage that is already gone.

Additional context Nothing in this repository trips it. Session::SendRequest passes members of the Request it owns, and HttpClientSync::Get passes named locals, so all four references outlive the operation in both. The tests pass named locals too. So this is about what the installed header allows rather than about a failure anyone is seeing today.

Two ways out that I can see, and the choice is a design one rather than mine to make. Holding the four by value costs a copy of the headers and the body per request, which is the straightforward fix and matches what retry_policy_ already does. Deleting the three default arguments costs nothing at runtime and closes the shape that invites it, but leaves a prvalue argument dangling just the same. Happy to send either.

Found while writing a test that constructs an HttpOperation directly, on the way through the curl client work in #4391 and #4390.

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageIndicates an issue or PR lacks a `triage/foo` label and requires one.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions