https://gcc.gnu.org/bugzilla/show_bug.cgi?id=126813

            Bug ID: 126813
           Summary: TSAN segfault 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.

Reply via email to