https://gcc.gnu.org/bugzilla/show_bug.cgi?id=126812
Bug ID: 126812
Summary: TSAN segfault when used on `omp declare simd`
Product: gcc
Version: 16.0
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: middle-end
Assignee: unassigned at gcc dot gnu.org
Reporter: matmal01 at gcc dot gnu.org
Target Milestone: ---
Note: I hit this problem while running the libgomp testsuite under TSAN. I
asked AI to debug and produce the following bug report (with my oversight).
The problem can be seen in the libgomp fortran tests, but we can find a
reproducer on C as well.
## Summary
GCC can emit an unbalanced pair of ThreadSanitizer function hooks for a
function marked `omp declare simd`. The generated SIMD clone calls
`__tsan_func_entry` once at clone entry, but calls `__tsan_func_exit` inside
the loop that executes the scalar function body for each active SIMD lane.
There is no matching exit call at the clone's actual return.
After a multi-lane invocation, TSAN's shadow-stack pointer is below the start
of its shadow stack. The next synchronization operation that asks TSAN to
capture the current stack can then crash inside TSAN's stack depot.
The problem reproduces with both C and Fortran. Both examples below are
self-contained and use `-fopenmp-simd`; neither executable links libgomp.
## Environment
```text
Target: aarch64-unknown-linux-gnu
Thread model: posix
gcc version 17.0.0 20260514 (experimental) (GCC)
Linux 6.8.0-1031-nvidia-64k aarch64
```
## C reproducer
Save as `repro.c`:
```c
#include <pthread.h>
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
#pragma omp declare simd inbranch linear(p:1)
__attribute__((noinline)) int
f(const int *p)
{
return *p + 1;
}
int
main(void)
{
int input[30];
int sum = 0;
for (int i = 0; i < 30; ++i)
input[i] = i;
#pragma omp simd reduction(+:sum)
for (int i = 0; i < 30; ++i)
sum += f(&input[i]);
if (sum != 465)
return 1;
/* Cause TSAN to capture the stack corrupted by the SIMD clone. */
pthread_mutex_lock(&lock);
pthread_mutex_unlock(&lock);
return 0;
}
```
Compile and run:
```sh
gcc repro.c -O1 -g -fsanitize=thread -fopenmp-simd -pthread -o repro-c
./repro-c
```
The failure is deterministic in five consecutive runs. A typical result is:
```text
ThreadSanitizer:DEADLYSIGNAL
==717872==ERROR: ThreadSanitizer: SEGV on unknown address 0xfffff60b0000 (pc
0xfffff6f05264 bp 0xfffffffff7e0 sp 0xfffffffff640 T717872)
==717872==The signal is caused by a READ memory access.
ThreadSanitizer:DEADLYSIGNAL
ThreadSanitizer: nested bug in the same thread, aborting.
```
The process exits with status 66.
## Fortran reproducer
Save as `repro.f90`:
```fortran
program fibonacci
implicit none
integer, parameter :: n = 30
integer :: a(0:n-1), b(0:n-1), a_ref(0:n-1)
integer :: i
integer, external :: fib
!$omp simd
do i = 0, n-1
b(i) = i
end do
!$omp simd
do i = 0, n-1
a(i) = fib(b(i))
end do
call fib_ref(a_ref, n)
do i = 0, n-1
if (a(i) /= a_ref(i)) stop 1
end do
end program
recursive function fib(n) result(r)
!$omp declare simd(fib) inbranch
integer :: n, r
if (n <= 1) then
r = n
else
r = fib(n-1) + fib(n-2)
end if
end function
subroutine fib_ref(a_ref, n)
integer :: n, a_ref(0:n-1), i
a_ref(0) = 0
a_ref(1) = 1
do i = 2, n-1
a_ref(i) = a_ref(i-1) + a_ref(i-2)
end do
end subroutine
```
Compile and run:
```sh
gfortran repro.f90 -O1 -g -fsanitize=thread -fopenmp-simd \
-fno-backtrace -o repro-fortran
./repro-fortran
```
This also fails deterministically with a TSAN nested-bug diagnostic and status
66. For example:
```text
ThreadSanitizer:DEADLYSIGNAL
==741140==ERROR: ThreadSanitizer: SEGV on unknown address 0xfffff537fff8 (pc
0xfffff6ee0c24 bp 0xfffffffff580 sp 0xfffffffff580 T741140)
==741140==The signal is caused by a WRITE memory access.
ThreadSanitizer:DEADLYSIGNAL
ThreadSanitizer: nested bug in the same thread, aborting.
```
Without `-fno-backtrace`, this reproducer hangs after the initial fault.
## Generated code
The C reproducer's generated SIMD clone `_ZGVnM4l4_f` has the following
control-flow shape (addresses omitted):
```text
_ZGVnM4l4_f:
...
bl __tsan_func_entry
...
.Llane_loop:
...
bl __tsan_read4
ldr w21, [x20]
add w21, w21, 1
bl __tsan_func_exit
...
b .Llane_loop
...
ret
```
There is one call to `__tsan_func_entry` before the lane loop. The call to
`__tsan_func_exit` is executed once per active lane, and there is no exit call
immediately before `ret`. Every active lane after the first therefore
over-pops TSAN's shadow stack.
The same malformed layout occurs in the Fortran clone.
## Debugger evidence
Stopping at the initial SIGSEGV shows the failure in:
```text
__sanitizer::StackDepotNode::hash
at libsanitizer/sanitizer_common/sanitizer_stackdepot.cpp:39
for (uptr i = 0; i < args.size; i++)
H.add(args.trace[i]);
```
The relevant TSAN thread state was:
```text
shadow_stack = 0xfffff5380000
shadow_stack_pos = 0xfffff537ff98
shadow_stack_end = 0xfffff5400000
shadow_stack_pos - shadow_stack = -13 entries
```
The resulting `StackTrace` passed to the stack depot was:
```text
trace = 0xfffff5380000
size = 4294967283
tag = 0
```
`4294967283` is `UINT32_MAX - 12`, consistent with the shadow-stack pointer
being 13 entries below its base. StackDepot then reads beyond valid memory
while hashing this wrapped stack trace.
The observed call chain was:
```text
StackDepotNode::hash
StackDepotPut
__tsan::CurrentStackId
__tsan::SyncVar::Init
__tsan::MetaMap::GetSync
__tsan::MutexPreLock
TSAN pthread lock interceptor
```
## Likely source of the imbalance
During gimplification, a TSAN-enabled function is wrapped in a
`GIMPLE_TRY_FINALLY` whose cleanup contains `IFN_TSAN_FUNC_EXIT`
(`gcc/gimplify.cc`, in `gimplify_function_tree`).
The language-independent OpenMP SIMD-clone pass subsequently transforms the
original scalar body into a per-lane loop. It redirects the original exit
through the loop increment block and creates a new return after the loop
(`gcc/omp-simd-clone.cc`, in `simd_clone_adjust`). The internal TSAN exit
marker remains part of the copied scalar body, so it becomes loop-local.
Later, the TSAN instrumentation pass replaces that marker with
`__tsan_func_exit` and inserts one `__tsan_func_entry` on the clone's entry
edge (`gcc/tsan.cc`, in `instrument_memory_accesses` and
`instrument_func_entry`).
The C function's pointer load is important because it causes TSAN to
instrument the clone. A clone containing only arithmetic on by-value scalar
arguments has no instrumented access, so GCC removes the unused internal exit
marker and emits neither function hook; that simpler case does not expose the
imbalance.
## Expected result
Both programs should exit successfully. Generated SIMD clones must execute
exactly one `__tsan_func_exit` for each `__tsan_func_entry`, regardless of the
number of active SIMD lanes.