| From cdfdb06c9155080ec97d6e4f4dd90b6e7cefb0ca Mon Sep 17 00:00:00 2001 | |
| From: Michael Pratt <michael@prattmic.com> | |
| Date: Fri, 12 Dec 2025 16:31:44 +1100 | |
| Subject: [PATCH] [TSan] Zero-initialize Trace.local_head | |
| Trace.local_head is currently uninitialized when Trace is created. It is | |
| first initialized when the first event is added to the trace, via the | |
| first call to TraceSwitchPartImpl. | |
| However, ThreadContext::OnFinished uses local_head, assuming that it is | |
| initialized. If it has not been initialized, we have undefined behavior, | |
| likely crashing if the contents are garbage. The allocator (Alloc) | |
| reuses previously allocations, so the contents of the uninitialized | |
| memory are arbitrary. | |
| In a C/C++ TSAN binary it is likely very difficult for a thread to start | |
| and exit without a single event inbetween. For Go programs, code running | |
| in the Go runtime itself is not TSan-instrumented, so goroutines that | |
| exclusively run runtime code (such as GC workers) can quite reasonably | |
| have no TSan events. | |
| The addition of such a goroutine to the Go test.c is sufficient to | |
| trigger this case, though for reliable failure (segfault) I've found it | |
| necessary to poison the ThreadContext allocation like so: | |
| (Example patch redacted because patch tries to apply this as a real | |
| patch. See full commit at | |
| https://github.com/llvm/llvm-project/commit/cdfdb06c9155080ec97d6e4f4dd90b6e7cefb0ca). | |
| The fix is trivial: local_head should be zero-initialized. | |
| --- | |
| compiler-rt/lib/tsan/go/test.c | 4 ++++ | |
| compiler-rt/lib/tsan/rtl/tsan_trace.h | 2 +- | |
| 2 files changed, 5 insertions(+), 1 deletion(-) | |
| diff --git a/compiler-rt/lib/tsan/go/test.c b/compiler-rt/lib/tsan/go/test.c | |
| index d328ab1b331d7..fcd396227a4ab 100644 | |
| --- a/compiler-rt/lib/tsan/go/test.c | |
| +++ b/compiler-rt/lib/tsan/go/test.c | |
| @@ -91,6 +91,10 @@ int main(void) { | |
| __tsan_go_start(thr0, &thr1, (char*)&barfoo + 1); | |
| void *thr2 = 0; | |
| __tsan_go_start(thr0, &thr2, (char*)&barfoo + 1); | |
| + // Goroutine that exits without a single event. | |
| + void *thr3 = 0; | |
| + __tsan_go_start(thr0, &thr3, (char*)&barfoo + 1); | |
| + __tsan_go_end(thr3); | |
| __tsan_func_exit(thr0); | |
| __tsan_func_enter(thr1, (char*)&foobar + 1); | |
| __tsan_func_enter(thr1, (char*)&foobar + 1); | |
| diff --git a/compiler-rt/lib/tsan/rtl/tsan_trace.h b/compiler-rt/lib/tsan/rtl/tsan_trace.h | |
| index 01bb7b34f43a2..1e791ff765fec 100644 | |
| --- a/compiler-rt/lib/tsan/rtl/tsan_trace.h | |
| +++ b/compiler-rt/lib/tsan/rtl/tsan_trace.h | |
| @@ -190,7 +190,7 @@ struct Trace { | |
| Mutex mtx; | |
| IList<TraceHeader, &TraceHeader::trace_parts, TracePart> parts; | |
| // First node non-queued into ctx->trace_part_recycle. | |
| - TracePart* local_head; | |
| + TracePart* local_head = nullptr; | |
| // Final position in the last part for finished threads. | |
| Event* final_pos = nullptr; | |
| // Number of trace parts allocated on behalf of this trace specifically. |