On Tue, 15 Apr 2025, Jakub Jelinek wrote:

> Hi!
> 
> Calls with musttail attribute don't really work with -fsanitize=thread in
> GCC.  The problem is that TSan instrumentation adds
>   __tsan_func_entry (__builtin_return_address (0));
> calls at the start of each instrumented function and
>   __tsan_func_exit ();
> call at the end of those and the latter stands in a way of normal tail calls
> as well as musttail tail calls.
> 
> Looking at what LLVM does, for normal calls -fsanitize=thread also prevents
> tail calls like in GCC (well, the __tsan_func_exit () call itself can be
> tail called in GCC (and from what I see not in clang)).
> But for [[clang::musttail]] calls it arranges to move the
> __tsan_func_exit () before the musttail call instead of after it.
> 
> The following patch handles it similarly.  If we for -fsanitize=thread
> instrumented function detect __builtin_tsan_func_exit () call, we process
> it normally (so that the call can be tail called in function returning void)
> but set a flag that the builtin has been seen (only for cfun->has_musttail
> in the diag_musttail phase).  And then let tree_optimize_tail_calls_1
> call find_tail_calls again in a new mode where the __tsan_func_exit ()
> call is ignored and so we are able to find calls before it, but only
> accept that if the call before it is actually a musttail.  For C++ it needs
> to verify that EH cleanup if any also has the __tsan_func_exit () call
> and if all goes well, the musttail call is registered for tailcalling with
> a flag that it has __tsan_func_exit () after it and when optimizing that
> we emit __tsan_func_exit (); call before the musttail tail call (or musttail
> tail recursion).
> 
> Bootstrapped/regtested on x86_64-linux and i686-linux, ok for trunk?

OK.

Richard.

> 2025-04-15  Jakub Jelinek  <ja...@redhat.com>
> 
>       PR sanitizer/119801
>       * sanitizer.def (BUILT_IN_TSAN_FUNC_EXIT): Use BT_FN_VOID rather
>       than BT_FN_VOID_PTR.
>       * tree-tailcall.cc: Include attribs.h and asan.h.
>       (struct tailcall): Add has_tsan_func_exit member.
>       (empty_eh_cleanup): Add eh_has_tsan_func_exit argument, set what
>       it points to to 1 if there is exactly one __tsan_func_exit call
>       and ignore that call otherwise.  Adjust recursive call.
>       (find_tail_calls): Add RETRY_TSAN_FUNC_EXIT argument, pass it
>       to recursive calls.  When seeing __tsan_func_exit call with
>       RETRY_TSAN_FUNC_EXIT 0, set it to -1.  If RETRY_TSAN_FUNC_EXIT
>       is 1, initially ignore __tsan_func_exit calls.  Adjust
>       empty_eh_cleanup caller.  When looking through stmts after the call,
>       ignore exactly one __tsan_func_exit call but remember it in
>       t->has_tsan_func_exit.  Diagnose if EH cleanups didn't have
>       __tsan_func_exit and normal path did or vice versa.
>       (optimize_tail_call): Emit __tsan_func_exit before the tail call
>       or tail recursion.
>       (tree_optimize_tail_calls_1): Adjust find_tail_calls callers.  If
>       find_tail_calls changes retry_tsan_func_exit to -1, set it to 1
>       and call it again with otherwise the same arguments.
> 
>       * c-c++-common/tsan/pr119801.c: New test.
> 
> --- gcc/sanitizer.def.jj      2025-04-14 19:30:31.804837079 +0200
> +++ gcc/sanitizer.def 2025-04-15 09:48:23.752349037 +0200
> @@ -247,7 +247,7 @@ DEF_SANITIZER_BUILTIN(BUILT_IN_TSAN_INIT
>  DEF_SANITIZER_BUILTIN(BUILT_IN_TSAN_FUNC_ENTRY, "__tsan_func_entry",
>                     BT_FN_VOID_PTR, ATTR_NOTHROW_LEAF_LIST)
>  DEF_SANITIZER_BUILTIN(BUILT_IN_TSAN_FUNC_EXIT, "__tsan_func_exit",
> -                   BT_FN_VOID_PTR, ATTR_NOTHROW_LEAF_LIST)
> +                   BT_FN_VOID, ATTR_NOTHROW_LEAF_LIST)
>  DEF_SANITIZER_BUILTIN(BUILT_IN_TSAN_VPTR_UPDATE, "__tsan_vptr_update",
>                     BT_FN_VOID_PTR_PTR, ATTR_NOTHROW_LEAF_LIST)
>  DEF_SANITIZER_BUILTIN(BUILT_IN_TSAN_READ1, "__tsan_read1",
> --- gcc/tree-tailcall.cc.jj   2025-04-14 19:30:31.976834786 +0200
> +++ gcc/tree-tailcall.cc      2025-04-15 10:12:48.879501238 +0200
> @@ -51,6 +51,8 @@ along with GCC; see the file COPYING3.
>  #include "symbol-summary.h"
>  #include "ipa-cp.h"
>  #include "ipa-prop.h"
> +#include "attribs.h"
> +#include "asan.h"
>  
>  /* The file implements the tail recursion elimination.  It is also used to
>     analyze the tail calls in general, passing the results to the rtl level
> @@ -122,6 +124,9 @@ struct tailcall
>    /* True if it is a call to the current function.  */
>    bool tail_recursion;
>  
> +  /* True if there is __tsan_func_exit call after the call.  */
> +  bool has_tsan_func_exit;
> +
>    /* The return value of the caller is mult * f + add, where f is the return
>       value of the call.  */
>    tree mult, add;
> @@ -504,7 +509,7 @@ maybe_error_musttail (gcall *call, const
>     Search at most CNT basic blocks (so that we don't need to do trivial
>     loop discovery).  */
>  static bool
> -empty_eh_cleanup (basic_block bb, int cnt)
> +empty_eh_cleanup (basic_block bb, int *eh_has_tsan_func_exit, int cnt)
>  {
>    if (EDGE_COUNT (bb->succs) > 1)
>      return false;
> @@ -515,6 +520,14 @@ empty_eh_cleanup (basic_block bb, int cn
>        gimple *g = gsi_stmt (gsi);
>        if (is_gimple_debug (g) || gimple_clobber_p (g))
>       continue;
> +      if (eh_has_tsan_func_exit
> +       && !*eh_has_tsan_func_exit
> +       && sanitize_flags_p (SANITIZE_THREAD)
> +       && gimple_call_builtin_p (g, BUILT_IN_TSAN_FUNC_EXIT))
> +     {
> +       *eh_has_tsan_func_exit = 1;
> +       continue;
> +     }
>        if (is_gimple_resx (g) && stmt_can_throw_external (cfun, g))
>       return true;
>        return false;
> @@ -523,7 +536,7 @@ empty_eh_cleanup (basic_block bb, int cn
>      return false;
>    if (cnt == 1)
>      return false;
> -  return empty_eh_cleanup (single_succ (bb), cnt - 1);
> +  return empty_eh_cleanup (single_succ (bb), eh_has_tsan_func_exit, cnt - 1);
>  }
>  
>  /* Argument for compute_live_vars/live_vars_at_stmt and what 
> compute_live_vars
> @@ -531,14 +544,22 @@ empty_eh_cleanup (basic_block bb, int cn
>  static live_vars_map *live_vars;
>  static vec<bitmap_head> live_vars_vec;
>  
> -/* Finds tailcalls falling into basic block BB. The list of found tailcalls 
> is
> +/* Finds tailcalls falling into basic block BB.  The list of found tailcalls 
> is
>     added to the start of RET.  When ONLY_MUSTTAIL is set only handle 
> musttail.
>     Update OPT_TAILCALLS as output parameter.  If DIAG_MUSTTAIL, diagnose
> -   failures for musttail calls.  */
> +   failures for musttail calls.  RETRY_TSAN_FUNC_EXIT is initially 0 and
> +   in that case the last call is attempted to be tail called, including
> +   __tsan_func_exit with -fsanitize=thread.  It is set to -1 if we
> +   detect __tsan_func_exit call and in that case tree_optimize_tail_calls_1
> +   will retry with it set to 1 (regardless of whether turning the
> +   __tsan_func_exit was successfully detected as tail call or not) and that
> +   will allow turning musttail calls before that call into tail calls as well
> +   by adding __tsan_func_exit call before the call.  */
>  
>  static void
>  find_tail_calls (basic_block bb, struct tailcall **ret, bool only_musttail,
> -              bool &opt_tailcalls, bool diag_musttail)
> +              bool &opt_tailcalls, bool diag_musttail,
> +              int &retry_tsan_func_exit)
>  {
>    tree ass_var = NULL_TREE, ret_var, func, param;
>    gimple *stmt;
> @@ -552,6 +573,8 @@ find_tail_calls (basic_block bb, struct
>    size_t idx;
>    tree var;
>    bool only_tailr = false;
> +  bool has_tsan_func_exit = false;
> +  int eh_has_tsan_func_exit = -1;
>  
>    if (!single_succ_p (bb)
>        && (EDGE_COUNT (bb->succs) || !cfun->has_musttail || !diag_musttail))
> @@ -585,6 +608,17 @@ find_tail_calls (basic_block bb, struct
>         || is_gimple_debug (stmt))
>       continue;
>  
> +      if (cfun->has_musttail
> +       && sanitize_flags_p (SANITIZE_THREAD)
> +       && gimple_call_builtin_p (stmt, BUILT_IN_TSAN_FUNC_EXIT)
> +       && diag_musttail)
> +     {
> +       if (retry_tsan_func_exit == 0)
> +         retry_tsan_func_exit = -1;
> +       else if (retry_tsan_func_exit == 1)
> +         continue;
> +     }
> +
>        if (!last_stmt)
>       last_stmt = stmt;
>        /* Check for a call.  */
> @@ -635,7 +669,7 @@ find_tail_calls (basic_block bb, struct
>        /* Recurse to the predecessors.  */
>        FOR_EACH_EDGE (e, ei, bb->preds)
>       find_tail_calls (e->src, ret, only_musttail, opt_tailcalls,
> -                      diag_musttail);
> +                      diag_musttail, retry_tsan_func_exit);
>  
>        return;
>      }
> @@ -715,8 +749,12 @@ find_tail_calls (basic_block bb, struct
>       return;
>        }
>  
> +    if (diag_musttail && gimple_call_must_tail_p (call))
> +      eh_has_tsan_func_exit = 0;
>      if (!gimple_call_must_tail_p (call)
> -     || !empty_eh_cleanup (e->dest, 20)
> +     || !empty_eh_cleanup (e->dest,
> +                           eh_has_tsan_func_exit
> +                           ? NULL : &eh_has_tsan_func_exit, 20)
>       || EDGE_COUNT (bb->succs) > 2)
>        {
>       maybe_error_musttail (call, _("call may throw exception caught "
> @@ -947,6 +985,17 @@ find_tail_calls (basic_block bb, struct
>         || is_gimple_debug (stmt))
>       continue;
>  
> +      if (cfun->has_musttail
> +       && sanitize_flags_p (SANITIZE_THREAD)
> +       && retry_tsan_func_exit == 1
> +       && gimple_call_builtin_p (stmt, BUILT_IN_TSAN_FUNC_EXIT)
> +       && !has_tsan_func_exit
> +       && gimple_call_must_tail_p (call))
> +     {
> +       has_tsan_func_exit = true;
> +       continue;
> +     }
> +
>        if (gimple_code (stmt) != GIMPLE_ASSIGN)
>       {
>         maybe_error_musttail (call, _("unhandled code after call"),
> @@ -1110,6 +1159,19 @@ find_tail_calls (basic_block bb, struct
>        return;
>      }
>  
> +  if (eh_has_tsan_func_exit != -1
> +      && eh_has_tsan_func_exit != has_tsan_func_exit)
> +    {
> +      if (eh_has_tsan_func_exit)
> +     maybe_error_musttail (call, _("call may throw exception caught "
> +                                   "locally or perform cleanups"),
> +                           diag_musttail);
> +      else
> +     maybe_error_musttail (call, _("exception cleanups omit "
> +                                   "__tsan_func_exit call"), diag_musttail);
> +      return;
> +    }
> +
>    /* Move queued defs.  */
>    if (tail_recursion)
>      {
> @@ -1138,6 +1200,7 @@ find_tail_calls (basic_block bb, struct
>    nw->call_gsi = gsi;
>  
>    nw->tail_recursion = tail_recursion;
> +  nw->has_tsan_func_exit = has_tsan_func_exit;
>  
>    nw->mult = m;
>    nw->add = a;
> @@ -1472,6 +1535,14 @@ static bool
>  optimize_tail_call (struct tailcall *t, bool opt_tailcalls,
>                   class loop *&new_loop)
>  {
> +  if (t->has_tsan_func_exit && (t->tail_recursion || opt_tailcalls))
> +    {
> +      tree builtin_decl = builtin_decl_implicit (BUILT_IN_TSAN_FUNC_EXIT);
> +      gimple *g = gimple_build_call (builtin_decl, 0);
> +      gimple_set_location (g, cfun->function_end_locus);
> +      gsi_insert_before (&t->call_gsi, g, GSI_SAME_STMT);
> +    }
> +
>    if (t->tail_recursion)
>      {
>        eliminate_tail_call (t, new_loop);
> @@ -1490,6 +1561,7 @@ optimize_tail_call (struct tailcall *t,
>         print_gimple_stmt (dump_file, stmt, 0, dump_flags);
>         fprintf (dump_file, " in bb %i\n", (gsi_bb (t->call_gsi))->index);
>       }
> +      return t->has_tsan_func_exit;
>      }
>  
>    return false;
> @@ -1539,12 +1611,23 @@ tree_optimize_tail_calls_1 (bool opt_tai
>        /* Only traverse the normal exits, i.e. those that end with return
>        statement.  */
>        if (safe_is_a <greturn *> (*gsi_last_bb (e->src)))
> -     find_tail_calls (e->src, &tailcalls, only_musttail, opt_tailcalls,
> -                      diag_musttail);
> +     {
> +       int retry_tsan_func_exit = 0;
> +       find_tail_calls (e->src, &tailcalls, only_musttail, opt_tailcalls,
> +                        diag_musttail, retry_tsan_func_exit);
> +       if (retry_tsan_func_exit == -1)
> +         {
> +           retry_tsan_func_exit = 1;
> +           find_tail_calls (e->src, &tailcalls, only_musttail,
> +                            opt_tailcalls, diag_musttail,
> +                            retry_tsan_func_exit);
> +         }
> +     }
>      }
>    if (cfun->has_musttail && diag_musttail)
>      {
>        basic_block bb;
> +      int retry_tsan_func_exit = 0;
>        FOR_EACH_BB_FN (bb, cfun)
>       if (EDGE_COUNT (bb->succs) == 0
>           || (single_succ_p (bb)
> @@ -1554,7 +1637,7 @@ tree_optimize_tail_calls_1 (bool opt_tai
>               && gimple_call_must_tail_p (as_a <gcall *> (c))
>               && gimple_call_noreturn_p (as_a <gcall *> (c)))
>             find_tail_calls (bb, &tailcalls, only_musttail, opt_tailcalls,
> -                            diag_musttail);
> +                            diag_musttail, retry_tsan_func_exit);
>      }
>  
>    if (live_vars)
> --- gcc/testsuite/c-c++-common/tsan/pr119801.c.jj     2025-04-15 
> 09:48:23.759348942 +0200
> +++ gcc/testsuite/c-c++-common/tsan/pr119801.c        2025-04-15 
> 09:48:23.759348942 +0200
> @@ -0,0 +1,24 @@
> +/* PR sanitizer/119801 */
> +/* { dg-do compile } */
> +/* { dg-options "-fsanitize=thread" } */
> +
> +[[gnu::noipa]] int
> +bar (int *p)
> +{
> +  return ++*p;
> +}
> +
> +int
> +foo (int *p)
> +{
> +  ++*p;
> +  [[gnu::musttail]] return bar (p);
> +}
> +
> +[[gnu::noinline]] int
> +baz (int x)
> +{
> +  if (x < 10)
> +    return x;
> +  [[gnu::musttail]] return baz (x - 2);
> +}
> 
>       Jakub
> 
> 

-- 
Richard Biener <rguent...@suse.de>
SUSE Software Solutions Germany GmbH,
Frankenstrasse 146, 90461 Nuernberg, Germany;
GF: Ivo Totev, Andrew McDonald, Werner Knoblich; (HRB 36809, AG Nuernberg)

Reply via email to