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

            Bug ID: 128043
           Summary: Guard variable of an inline static data member takes
                    #pragma GCC visibility in effect at end of TU, not the
                    variable's visibility
           Product: gcc
           Version: 16.2.0
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: c++
          Assignee: unassigned at gcc dot gnu.org
          Reporter: dante at cadence dot com
  Target Milestone: ---

Disclaimer: generated with help of Claude AI

When a translation unit ends inside "#pragma GCC visibility push(hidden)", the
guard variable of a vague-linkage variable with dynamic initialization is
emitted HIDDEN, even though the variable itself is DEFAULT. The variable was
declared before the push and the pragma does not apply to it. Linked into
several shared objects, the program has one variable (STB_GNU_UNIQUE, DEFAULT)
but one guard per shared object. Each such shared object runs the initializer
again and registers another destructor. In our product, the shared
std::unordered_map is constructed twice and destroyed twice, and the process
aborts at exit with "double free or corruption".

Testcase (pr.C):

    struct N { N(); };
    struct S { static inline N m; };
    #pragma GCC visibility push(hidden)
    N& use() { return S::m; }

    $ g++ -std=c++17 -O2 -fPIC -c pr.C && readelf -sW pr.o | grep -E
'_ZN1S1mE|_ZGVN1S1mE'
         7: 0000000000000000     1 OBJECT  UNIQUE DEFAULT   12 _ZN1S1mE
         8: 0000000000000000     8 OBJECT  UNIQUE HIDDEN    11 _ZGVN1S1mE

Expected: _ZGVN1S1mE UNIQUE DEFAULT, matching _ZN1S1mE. That is what gcc emits
without the trailing push, or with an explicit visibility attribute on S or on
S::m, and what clang emits in all cases.


Testcase 2:
a.C:
    struct N { N(); };
    struct S { static inline N m; };
    N& a() { return S::m; }
b.C:
    struct N { N(); };
    struct S { static inline N m; };
    __attribute__((visibility("default"))) N& use();
    #pragma GCC visibility push(hidden)
    N& use() { return S::m; }
n.C:
    #include <cstdio>
    struct N { N(); };
    N::N() { std::printf("N::N() this=%p\n", (void*)this); }
main.C:
    struct N;
    N& a();
    N& use();
    int main() { a(); use(); }

run.sh:
    #!/bin/sh
    # usage: run.sh <g++>
    CXX=${1:-g++}
    set -e
    $CXX -std=c++17 -O2 -fPIC -c pr.C
    readelf -sW pr.o | grep -E ' _ZN1S1mE| _ZGVN1S1mE'
    $CXX -std=c++17 -O2 -fPIC -shared n.C -o libn.so
    $CXX -std=c++17 -O2 -fPIC -shared a.C -o liba.so -L. -ln
    $CXX -std=c++17 -O2 -fPIC -shared b.C -o libb.so -L. -ln
    $CXX -std=c++17 main.C -o main -L. -la -lb -ln -Wl,-rpath,'$ORIGIN'
    ./main

The same happens for a namespace-scope inline variable (inline N v;) and for a
static local of a function template instantiated in the TU. It does not happen
for a static local of a non-template inline function, whose guard is created
while that function is compiled.

Runtime effect: the attached run.sh builds liba.so (uses S::m, no pragma),
libb.so (b.C: the same, with the push left open) and main, which loads both:

    gcc 16.2:     N::N() this=0x7f1181bdb028
                  N::N() this=0x7f1181bdb028     <- same object constructed
twice
    clang 22.1.8: N::N() this=0x7fc857ac5041

Analysis (gcc/cp/decl2.cc, trunk):

  get_guard() builds the guard and calls copy_linkage (guard, decl), which
copies DECL_VISIBILITY and DECL_VISIBILITY_SPECIFIED from the variable ("The
guard should have the same linkage as what it guards."). It then calls
pushdecl_top_level_and_finish, and cp_finish_decl calls determine_visibility on
the guard. The guard is a namespace-scope, non-template decl, so if
DECL_VISIBILITY_SPECIFIED is false, this branch runs:

      else if (! DECL_VISIBILITY_SPECIFIED (decl))
        ...
          DECL_VISIBILITY (decl) = default_visibility;

  It replaces the copied visibility with the pragma state at the point the
guard is created. For inline variables and template instantiations, that point
is the  static-initialization emission at end of TU (c_parse_final_cleanups ->
one_static_initialization_or_destruction -> get_guard). A variable that got
DEFAULT implicitly (no pragma or attribute in effect at its declaration) has
DECL_VISIBILITY_SPECIFIED false, so its guard picks up whatever pragma is still
open at EOF.

Suggested fix: the guard's visibility should always be the guarded variable's.
For example, keep it from being recomputed in determine_visibility for a guard,
or re-apply copy_linkage after pushdecl_top_level_and_finish.

Workaround: close every "#pragma GCC visibility push" before the end of the TU,
or give the class or variable an explicit visibility attribute.

Reply via email to