Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4100 +/- ##
==========================================
+ Coverage 81.14% 81.15% +0.02%
==========================================
Files 446 446
Lines 18922 18922
==========================================
+ Hits 15353 15355 +2
+ Misses 3569 3567 -2 🚀 New features to boost your workflow:
|
71ca0f2 to
bc349e7
Compare
Fixes open-telemetry#3849. etw::TracerProvider::GetTracer() was newing up a fresh Tracer per call and returning the only shared_ptr to it. The catch is that etw::Span stores its parent as a raw reference (Tracer &owner_), so the moment a caller drops or reassigns that shared_ptr, every Span that came from that Tracer is left with a dangling owner_. Span::End() then crashes inside owner_.EndSpan(). The snippet from the issue makes it obvious: auto provider = std::make_unique<etw::TracerProvider>(); auto tracer = provider->GetTracer("Foo"); auto spanFoo = tracer->StartSpan("Span-Foo"); tracer = provider->GetTracer("Bar"); // Tracer "Foo" goes away auto spanBar = tracer->StartSpan("Span-Bar"); spanFoo->End(); // crash spanBar->End(); Under MSVC Debug + cdb this lands as an AV in Span::End dereferencing 0xFEEEFEEE -- the debug heap's freed-memory fill -- which is about as unambiguous as UAF gets. The fix is to let TracerProvider keep the Tracers it hands out, keyed by name and guarded by a mutex, and have GetTracer() return the cached entry on repeat calls. That makes the Tracer outlive any Span derived from it, which is the lifetime contract Span's raw owner_ already assumes. It is also the same pattern sdk::trace::TracerProvider has always used, which is why the issue reporter noted the SDK provider does not crash under the same usage. There was an earlier attempt in PR open-telemetry#4070 that switched Span to take a shared_ptr<Tracer> via shared_from_this(). That was turned down because StartSpan() is the ETW exporter's hot path and nobody wants an extra atomic refcount bump on every span just to keep the parent alive. Doing the lifetime work once in GetTracer() keeps owner_ as a plain reference and costs nothing per span. While putting this together I tripped over a second bug that the cache exposes -- worth describing here because the fix lives in the same file. etw_tracer.h and etw_provider.h between them have three independent function-local statics that get tangled at process shutdown: 1. etw::TracerProvider itself (now owning the tracer cache). 2. Tracer::etwProvider() returns a `static ETWProvider instance`. 3. ETWProvider::providers() returns a `static std::map<...> providers` that lives inside that singleton's method, not inside the singleton object, and is lazily created on the first open()/is_registered() call. Before this change, all three were initialized in that order on the very first GetTracer() call: the TracerProvider was already constructed, then the Tracer ctor called etwProvider().open(...), which constructed open-telemetry#2 and open-telemetry#3. Reverse-order destruction therefore took out open-telemetry#3 first, while the TracerProvider (and its cached Tracers, which hold references into open-telemetry#3) was still alive. ~Tracer then ran etwProvider().close(provHandle) against a dangling reference and we got an AV at exit. With the old "fresh Tracer per call, caller drops it immediately" pattern this was hidden because the Tracer (and its provHandle reference) was almost always gone long before shutdown. The cache extends Tracer lifetime to match TracerProvider lifetime, which is exactly what tripped this over locally and on the Windows CI legs. The fix is small: both TracerProvider constructors now do (void)Tracer::etwProvider().is_registered(std::string{}); which forces both ETWProvider singletons to be constructed before the TracerProvider's body finishes. Reverse destruction then guarantees they outlive the cached Tracers. is_registered() is a public, side- effect-free read on ETWProvider that internally touches the providers map, which is exactly the static-init we need. It requires Tracer to befriend TracerProvider so the constructor can call the private etwProvider() accessor. A few things worth calling out: - No API or ABI change. Only private members are added to TracerProvider, and <map>/<memory>/<mutex> are already included by etw_tracer.h. The friend declaration is purely internal. - The cache deliberately does not evict. Span holds its parent by raw reference, so dropping a Tracer that still has live Spans would put the original bug right back. In practice the ETW exporter is built around "one Tracer per provider name, reused for the lifetime of the process", and Windows itself caps registered ETW providers per process well below anything you would worry about for memory. - Keying on name only is consistent with the existing etw::Tracer constructor, which already declares args and schema_url UNREFERENCED_PARAMETER. - etw::LoggerProvider was checked for the same shape and does not currently cache Loggers, so the destruction-order fix is not needed there today. If a similar cache is ever added to LoggerProvider it will want the same one-line force-init in its constructor. - Updated ETWTracer.GlobalSingletonTracer: it previously asserted that two GetTracer() calls with the same provider name produce different trace ids, which only held because of the bug being fixed here. GetTracer(name) is now idempotent per name, so the assertion is flipped to EXPECT_EQ and the stale sample-event comment updated to match. All other ETW tracer tests continue to pass. Repro before the change: AV in Span::End with the freed-fill operand, plus exit-time AV in ~Tracer on ctest runs that exercise a single test in isolation. Same repros after: both run cleanly to completion. Verified locally with the full ctest suite on a Windows abiv2 maintainer-mode build (1104/1104 passing) and on an abiv1 Debug build (43/43 ETW tests passing). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bc349e7 to
a994110
Compare
|
This is related to ETW, please take a look. Thanks. |
| // outstanding Spans (use-after-free in Span::End()). | ||
| std::lock_guard<std::mutex> lock(tracers_lock_); | ||
| auto &cached = tracers_[std::string{name.data(), name.size()}]; | ||
| if (!cached) |
There was a problem hiding this comment.
This cache-hit condition only checks whether an entry exists, not whether the cached Tracer is still usable. In ABI v1, CloseWithMicroseconds() marks it closed, and ETWProvider::close() erases its Handle node when the refcount reaches zero. The cached Tracer then retains a dangling provHandle reference.
auto t1 = tp.GetTracer("Foo");
t1->CloseWithMicroseconds(0);
auto t2 = tp.GetTracer("Foo"); // same closed Tracer
t2->StartSpan("s")->End(); // writes through dangling provHandlePlease treat a closed cache hit as a miss: retain the old Tracer so existing Spans keep their owner, cache a fresh Tracer, and make writes from the retired/closed Tracer return before touching provHandle (or otherwise keep the Handle node stable). Please add this exact close/reacquire/end sequence as a regression test.
| // moment the caller drops the returned shared_ptr, orphaning any | ||
| // outstanding Spans (use-after-free in Span::End()). | ||
| std::lock_guard<std::mutex> lock(tracers_lock_); | ||
| auto &cached = tracers_[std::string{name.data(), name.size()}]; |
There was a problem hiding this comment.
The scope is per TracerProvider, not process-wide, but this cache still locks in incorrect root-span correlation. traceId_ is generated once in the ETW Tracer constructor, and StartSpan() reuses it whenever there is no valid parent. Returning the same cached Tracer therefore gives two independent root spans the same TraceId:
auto tracer = tp.GetTracer("Foo");
auto a = tracer->StartSpan("a");
auto b = tracer->StartSpan("b");
EXPECT_NE(a->GetContext().trace_id(), b->GetContext().trace_id());The OpenTelemetry specification requires a new TraceId for every root span. This was already the ETW bug reported in #3846, caching removes the previous GetTracer() workaround, while the changed EXPECT_EQ assertion pins the wrong behavior.
Please keep the lifetime cache, but generate a new TraceId inside StartSpan() whenever the parent is invalid, and inherit the parent's TraceId otherwise. Please restore the distinct-root assertion and add a child-span inheritance test.
|
@lukeina2z Are you still interested in contributing this change? Please see feedback from Tom above. |
Fixes #3849.
etw::TracerProvider::GetTracer() was newing up a fresh Tracer per call and returning the only shared_ptr to it. The catch is that etw::Span stores its parent as a raw reference (Tracer &owner_), so the moment a caller drops or reassigns that shared_ptr, every Span that came from that Tracer is left with a dangling owner_. Span::End() then crashes inside owner_.EndSpan().
The snippet from the issue makes it obvious:
Under MSVC Debug + cdb this lands as an AV in Span::End dereferencing 0xFEEEFEEE -- the debug heap's freed-memory fill -- which is about as unambiguous as UAF gets.
The fix is to let TracerProvider keep the Tracers it hands out, keyed by name and guarded by a mutex, and have GetTracer() return the cached entry on repeat calls. That makes the Tracer outlive any Span derived from it, which is the lifetime contract Span's raw owner_ already assumes. It is also the same pattern sdk::trace::TracerProvider has always used, which is why the issue reporter noted the SDK provider does not crash under the same usage.
There was an earlier attempt in PR #4070 that switched Span to take a shared_ptr via shared_from_this(). That was turned down because StartSpan() is the ETW exporter's hot path and nobody wants an extra atomic refcount bump on every span just to keep the parent alive. Doing the lifetime work once in GetTracer() keeps owner_ as a plain reference and costs nothing per span.
A few things worth calling out:
Repro before the change: AV in Span::End with the freed-fill operand.
Same repro after: runs cleanly to completion.
Fixes # (issue)
Changes
Please provide a brief description of the changes here.
For significant contributions please make sure you have completed the following items:
CHANGELOG.mdupdated for non-trivial changes