Commit 71ca0f2
[EXPORTER ETW] Fix Span::End() use-after-free when caller drops Tracer
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:
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 #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.
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 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 have
it -- Logger holds a back-reference to the Provider (long-lived),
not to itself -- so no equivalent change is needed there.
Repro before the change: AV in Span::End with the freed-fill operand.
Same repro after: runs cleanly to completion.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>1 parent 72953ea commit 71ca0f2
1 file changed
Lines changed: 38 additions & 3 deletions
Lines changed: 38 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1152 | 1152 | | |
1153 | 1153 | | |
1154 | 1154 | | |
| 1155 | + | |
| 1156 | + | |
| 1157 | + | |
| 1158 | + | |
| 1159 | + | |
| 1160 | + | |
| 1161 | + | |
| 1162 | + | |
| 1163 | + | |
| 1164 | + | |
| 1165 | + | |
| 1166 | + | |
| 1167 | + | |
| 1168 | + | |
| 1169 | + | |
| 1170 | + | |
| 1171 | + | |
| 1172 | + | |
| 1173 | + | |
| 1174 | + | |
| 1175 | + | |
| 1176 | + | |
| 1177 | + | |
| 1178 | + | |
| 1179 | + | |
1155 | 1180 | | |
1156 | 1181 | | |
1157 | 1182 | | |
| |||
1177 | 1202 | | |
1178 | 1203 | | |
1179 | 1204 | | |
1180 | | - | |
1181 | | - | |
1182 | | - | |
| 1205 | + | |
| 1206 | + | |
| 1207 | + | |
| 1208 | + | |
| 1209 | + | |
| 1210 | + | |
| 1211 | + | |
| 1212 | + | |
| 1213 | + | |
| 1214 | + | |
| 1215 | + | |
| 1216 | + | |
| 1217 | + | |
1183 | 1218 | | |
1184 | 1219 | | |
1185 | 1220 | | |
| |||
0 commit comments