Skip to content

Commit d5684b8

Browse files
authored
Warn when unique objects might be duplicated in shared libraries (#117622)
When a hidden object is built into multiple shared libraries, each instance of the library will get its own copy. If the object was supposed to be globally unique (e.g. a global variable or static data member), this can cause very subtle bugs. An object might be incorrectly duplicated if it: - Is defined in a header (so it might appear in multiple TUs), and - Has external linkage (otherwise it's supposed to be duplicated), and - Has hidden visibility (or else the dynamic linker will handle it) The duplication is only a problem semantically if one of the following is true: 1. The object is mutable (the copies won't be in sync), or 2. Its initialization has side effects (it may now run more than once), or 3. The value of its address is used (different copies have different addresses). To detect this, we add a new -Wunique-object-duplication warning. It warns on cases (1) and (2) above. To be conservative, we only warn in case (2) if we are certain the initializer has side effects, and we don't warn on `new` because the only side effect is some extra memory usage. We don't currently warn on case (3) because doing so is prone to false positives: there are many reasons for taking the address which aren't inherently problematic (e.g. passing to a function that expects a pointer). We only run into problems if the code inspects the value of the address. The check is currently disabled for windows, which uses its own analogue of visibility (declimport/declexport). The check is also disabled inside templates, since it can give false positives if a template is never instantiated. ### Resolving the warning The warning can be fixed in several ways: - If the object in question doesn't need to be mutable, it should be made const. Note that the variable must be completely immutable, e.g. we'll warn on `const int* p` because the pointer itself is mutable. To silence the warning, it should instead be `const int* const p`. - If the object must be mutable, it (or the enclosing function, in the case of static local variables) should be made visible using `__attribute((visibility("default")))` - If the object is supposed to be duplicated, it should be be given internal linkage. ### Testing I've tested the warning by running it on clang itself, as well as on chromium. Compiling clang resulted in [10 warnings across 6 files](https://github.com/user-attachments/files/17908069/clang-warnings.txt), while Chromium resulted in [160 warnings across 85 files](https://github.com/user-attachments/files/17908072/chromium-warnings.txt), mostly in third-party code. Almost all warnings were due to mutable variables. I evaluated the warnings by manual inspection. I believe all the resulting warnings are true positives, i.e. they represent potentially-problematic code where duplication might cause a problem. For the clang warnings, I also validated them by either adding `const` or visibility annotations as appropriate. ### Limitations I am aware of four main limitations with the current warning: 1. We do not warn when the address of a duplicated object is taken, since doing so is prone to false positives. I'm hopeful that we can create a refined version in the future, however. 2. We only warn for side-effectful initialization if we are certain side effects exist. Warning on potential side effects produced a huge number of false positives; I don't expect there's much that can be done about this in modern C++ code bases, since proving a lack of side effects is difficult. 3. Windows uses a different system (declexport/import) instead of visibility. From manual testing, it seems to behave analogously to the visibility system for the purposes of this warning, but to keep things simple the warning is disabled on windows for now. 4. We don't warn on code inside templates. This is unfortuate, since it masks many real issues, e.g. a templated variable which is implicitly instantiated the same way in multiple TUs should be globally unique, but may accidentally be duplicated. Unfortunately, we found some potential false positives during testing that caused us to disable the warning for now.
1 parent 82c6b8f commit d5684b8

File tree

7 files changed

+323
-0
lines changed

7 files changed

+323
-0
lines changed

clang/docs/ReleaseNotes.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ Attribute Changes in Clang
113113
Improvements to Clang's diagnostics
114114
-----------------------------------
115115

116+
- The ``-Wunique-object-duplication`` warning has been added to warn about objects
117+
which are supposed to only exist once per program, but may get duplicated when
118+
built into a shared library.
119+
116120
Improvements to Clang's time-trace
117121
----------------------------------
118122

clang/include/clang/Basic/DiagnosticGroups.td

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,36 @@ def SuspiciousMemaccess : DiagGroup<"suspicious-memaccess",
694694
NonTrivialMemaccess, MemsetTransposedArgs, SuspiciousBzero]>;
695695
def StaticInInline : DiagGroup<"static-in-inline">;
696696
def StaticLocalInInline : DiagGroup<"static-local-in-inline">;
697+
def UniqueObjectDuplication : DiagGroup<"unique-object-duplication"> {
698+
code Documentation = [{
699+
Warns when objects which are supposed to be globally unique might get duplicated
700+
when built into a shared library.
701+
702+
If an object with hidden visibility is built into a shared library, each instance
703+
of the library will get its own copy. This can cause very subtle bugs if there was
704+
only supposed to be one copy of the object in question: singletons aren't single,
705+
changes to one object won't affect the others, the object's initializer will run
706+
once per copy, etc.
707+
708+
Specifically, this warning fires when it detects an object which:
709+
1. Appears in a header file (so it might get compiled into multiple libaries), and
710+
2. Has external linkage (otherwise it's supposed to be duplicated), and
711+
3. Has hidden visibility.
712+
713+
As well as one of the following:
714+
1. The object is mutable, or
715+
2. The object's initializer definitely has side effects.
716+
717+
The warning is best resolved by making the object ``const`` (if possible), or by explicitly
718+
giving the object non-hidden visibility, e.g. using ``__attribute((visibility("default")))``.
719+
Note that all levels of a pointer variable must be constant; ``const int*`` will
720+
trigger the warning because the pointer itself is mutable.
721+
722+
This warning is currently disabled on Windows since it uses import/export rules
723+
instead of visibility.
724+
}];
725+
}
726+
697727
def GNUStaticFloatInit : DiagGroup<"gnu-static-float-init">;
698728
def StaticFloatInit : DiagGroup<"static-float-init", [GNUStaticFloatInit]>;
699729
// Allow differentiation between GNU statement expressions in a macro versus

clang/include/clang/Basic/DiagnosticSemaKinds.td

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6167,6 +6167,15 @@ def warn_static_local_in_extern_inline : Warning<
61676167
def note_convert_inline_to_static : Note<
61686168
"use 'static' to give inline function %0 internal linkage">;
61696169

6170+
def warn_possible_object_duplication_mutable : Warning<
6171+
"%0 may be duplicated when built into a shared library: "
6172+
"it is mutable, has hidden visibility, and external linkage">,
6173+
InGroup<UniqueObjectDuplication>, DefaultIgnore;
6174+
def warn_possible_object_duplication_init : Warning<
6175+
"initializeation of %0 may run twice when built into a shared library: "
6176+
"it has hidden visibility and external linkage">,
6177+
InGroup<UniqueObjectDuplication>, DefaultIgnore;
6178+
61706179
def ext_redefinition_of_typedef : ExtWarn<
61716180
"redefinition of typedef %0 is a C11 feature">,
61726181
InGroup<DiagGroup<"typedef-redefinition"> >;

clang/include/clang/Sema/Sema.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3664,6 +3664,12 @@ class Sema final : public SemaBase {
36643664
NonTrivialCUnionContext UseContext,
36653665
unsigned NonTrivialKind);
36663666

3667+
/// Certain globally-unique variables might be accidentally duplicated if
3668+
/// built into multiple shared libraries with hidden visibility. This can
3669+
/// cause problems if the variable is mutable, its initialization is
3670+
/// effectful, or its address is taken.
3671+
bool GloballyUniqueObjectMightBeAccidentallyDuplicated(const VarDecl *Dcl);
3672+
36673673
/// AddInitializerToDecl - Adds the initializer Init to the
36683674
/// declaration dcl. If DirectInit is true, this is C++ direct
36693675
/// initialization rather than copy initialization.

clang/lib/Sema/SemaDecl.cpp

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13380,6 +13380,62 @@ void Sema::checkNonTrivialCUnion(QualType QT, SourceLocation Loc,
1338013380
.visit(QT, nullptr, false);
1338113381
}
1338213382

13383+
bool Sema::GloballyUniqueObjectMightBeAccidentallyDuplicated(
13384+
const VarDecl *Dcl) {
13385+
if (!getLangOpts().CPlusPlus)
13386+
return false;
13387+
13388+
// We only need to warn if the definition is in a header file, so wait to
13389+
// diagnose until we've seen the definition.
13390+
if (!Dcl->isThisDeclarationADefinition())
13391+
return false;
13392+
13393+
// If an object is defined in a source file, its definition can't get
13394+
// duplicated since it will never appear in more than one TU.
13395+
if (Dcl->getASTContext().getSourceManager().isInMainFile(Dcl->getLocation()))
13396+
return false;
13397+
13398+
// If the variable we're looking at is a static local, then we actually care
13399+
// about the properties of the function containing it.
13400+
const ValueDecl *Target = Dcl;
13401+
// VarDecls and FunctionDecls have different functions for checking
13402+
// inline-ness, so we have to do it manually.
13403+
bool TargetIsInline = Dcl->isInline();
13404+
13405+
// Update the Target and TargetIsInline property if necessary
13406+
if (Dcl->isStaticLocal()) {
13407+
const DeclContext *Ctx = Dcl->getDeclContext();
13408+
if (!Ctx)
13409+
return false;
13410+
13411+
const FunctionDecl *FunDcl =
13412+
dyn_cast_if_present<FunctionDecl>(Ctx->getNonClosureAncestor());
13413+
if (!FunDcl)
13414+
return false;
13415+
13416+
Target = FunDcl;
13417+
// IsInlined() checks for the C++ inline property
13418+
TargetIsInline = FunDcl->isInlined();
13419+
}
13420+
13421+
// Non-inline variables can only legally appear in one TU
13422+
// FIXME: This also applies to templated variables, but that can rarely lead
13423+
// to false positives so templates are disabled for now.
13424+
if (!TargetIsInline)
13425+
return false;
13426+
13427+
// If the object isn't hidden, the dynamic linker will prevent duplication.
13428+
clang::LinkageInfo Lnk = Target->getLinkageAndVisibility();
13429+
if (Lnk.getVisibility() != HiddenVisibility)
13430+
return false;
13431+
13432+
// If the obj doesn't have external linkage, it's supposed to be duplicated.
13433+
if (!isExternalFormalLinkage(Lnk.getLinkage()))
13434+
return false;
13435+
13436+
return true;
13437+
}
13438+
1338313439
void Sema::AddInitializerToDecl(Decl *RealDecl, Expr *Init, bool DirectInit) {
1338413440
// If there is no declaration, there was an error parsing it. Just ignore
1338513441
// the initializer.
@@ -14786,6 +14842,51 @@ void Sema::FinalizeDeclaration(Decl *ThisDecl) {
1478614842
if (DC->getRedeclContext()->isFileContext() && VD->isExternallyVisible())
1478714843
AddPushedVisibilityAttribute(VD);
1478814844

14845+
// If this object has external linkage and hidden visibility, it might be
14846+
// duplicated when built into a shared library, which causes problems if it's
14847+
// mutable (since the copies won't be in sync) or its initialization has side
14848+
// effects (since it will run once per copy instead of once globally)
14849+
// FIXME: Windows uses dllexport/dllimport instead of visibility, and we don't
14850+
// handle that yet. Disable the warning on Windows for now.
14851+
// FIXME: Checking templates can cause false positives if the template in
14852+
// question is never instantiated (e.g. only specialized templates are used).
14853+
if (!Context.getTargetInfo().shouldDLLImportComdatSymbols() &&
14854+
!VD->isTemplated() &&
14855+
GloballyUniqueObjectMightBeAccidentallyDuplicated(VD)) {
14856+
// Check mutability. For pointers, ensure that both the pointer and the
14857+
// pointee are (recursively) const.
14858+
QualType Type = VD->getType().getNonReferenceType();
14859+
if (!Type.isConstant(VD->getASTContext())) {
14860+
Diag(VD->getLocation(), diag::warn_possible_object_duplication_mutable)
14861+
<< VD;
14862+
} else {
14863+
while (Type->isPointerType()) {
14864+
Type = Type->getPointeeType();
14865+
if (Type->isFunctionType())
14866+
break;
14867+
if (!Type.isConstant(VD->getASTContext())) {
14868+
Diag(VD->getLocation(),
14869+
diag::warn_possible_object_duplication_mutable)
14870+
<< VD;
14871+
break;
14872+
}
14873+
}
14874+
}
14875+
14876+
// To keep false positives low, only warn if we're certain that the
14877+
// initializer has side effects. Don't warn on operator new, since a mutable
14878+
// pointer will trigger the previous warning, and an immutable pointer
14879+
// getting duplicated just results in a little extra memory usage.
14880+
const Expr *Init = VD->getAnyInitializer();
14881+
if (Init &&
14882+
Init->HasSideEffects(VD->getASTContext(),
14883+
/*IncludePossibleEffects=*/false) &&
14884+
!isa<CXXNewExpr>(Init->IgnoreParenImpCasts())) {
14885+
Diag(Init->getExprLoc(), diag::warn_possible_object_duplication_init)
14886+
<< VD;
14887+
}
14888+
}
14889+
1478914890
// FIXME: Warn on unused var template partial specializations.
1479014891
if (VD->isFileVarDecl() && !isa<VarTemplatePartialSpecializationDecl>(VD))
1479114892
MarkUnusedFileScopedDecl(VD);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// RUN: %clang_cc1 -fsyntax-only -verify=hidden -Wunique-object-duplication -fvisibility=hidden -Wno-unused-value %s
2+
// RUN: %clang_cc1 -fsyntax-only -verify -Wunique-object-duplication -Wno-unused-value %s
3+
// The check is currently disabled on windows. The test should fail because we're not getting the expected warnings.
4+
// XFAIL: target={{.*}}-windows{{.*}}
5+
6+
#include "unique_object_duplication.h"
7+
8+
// Everything in these namespaces here is defined in the cpp file,
9+
// so won't get duplicated
10+
11+
namespace GlobalTest {
12+
float Test::allowedStaticMember1 = 2.3;
13+
}
14+
15+
bool disallowed4 = true;
16+
constexpr inline bool disallowed5 = true;
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/**
2+
* This file contains tests for the -Wunique_object_duplication warning.
3+
* See the warning's documentation for more information.
4+
*/
5+
6+
#define HIDDEN __attribute__((visibility("hidden")))
7+
#define DEFAULT __attribute__((visibility("default")))
8+
9+
// Helper functions
10+
constexpr int init_constexpr(int x) { return x; };
11+
extern double init_dynamic(int);
12+
13+
/******************************************************************************
14+
* Case one: Static local variables in an externally-visible function
15+
******************************************************************************/
16+
namespace StaticLocalTest {
17+
18+
inline void has_static_locals_external() {
19+
// Mutable
20+
static int disallowedStatic1 = 0; // hidden-warning {{'disallowedStatic1' may be duplicated when built into a shared library: it is mutable, has hidden visibility, and external linkage}}
21+
// Initialization might run more than once
22+
static const double disallowedStatic2 = disallowedStatic1++; // hidden-warning {{initializeation of 'disallowedStatic2' may run twice when built into a shared library: it has hidden visibility and external linkage}}
23+
24+
// OK, because immutable and compile-time-initialized
25+
static constexpr int allowedStatic1 = 0;
26+
static const float allowedStatic2 = 1;
27+
static constexpr int allowedStatic3 = init_constexpr(2);
28+
static const int allowedStatic4 = init_constexpr(3);
29+
}
30+
31+
// Don't warn for non-inline functions, since they can't (legally) appear
32+
// in more than one TU in the first place.
33+
void has_static_locals_non_inline() {
34+
// Mutable
35+
static int allowedStatic1 = 0;
36+
// Initialization might run more than once
37+
static const double allowedStatic2 = allowedStatic1++;
38+
}
39+
40+
// Everything in this function is OK because the function is TU-local
41+
static void has_static_locals_internal() {
42+
static int allowedStatic1 = 0;
43+
static double allowedStatic2 = init_dynamic(2);
44+
static char allowedStatic3 = []() { return allowedStatic1++; }();
45+
static constexpr int allowedStatic4 = 0;
46+
}
47+
48+
namespace {
49+
50+
// Everything in this function is OK because the function is also TU-local
51+
void has_static_locals_anon() {
52+
static int allowedStatic1 = 0;
53+
static double allowedStatic2 = init_dynamic(2);
54+
static char allowedStatic3 = []() { return allowedStatic1++; }();
55+
static constexpr int allowedStatic4 = init_constexpr(3);
56+
}
57+
58+
} // Anonymous namespace
59+
60+
HIDDEN inline void static_local_always_hidden() {
61+
static int disallowedStatic1 = 3; // hidden-warning {{'disallowedStatic1' may be duplicated when built into a shared library: it is mutable, has hidden visibility, and external linkage}}
62+
// expected-warning@-1 {{'disallowedStatic1' may be duplicated when built into a shared library: it is mutable, has hidden visibility, and external linkage}}
63+
{
64+
static int disallowedStatic2 = 3; // hidden-warning {{'disallowedStatic2' may be duplicated when built into a shared library: it is mutable, has hidden visibility, and external linkage}}
65+
// expected-warning@-1 {{'disallowedStatic2' may be duplicated when built into a shared library: it is mutable, has hidden visibility, and external linkage}}
66+
}
67+
68+
auto lmb = []() {
69+
static int disallowedStatic3 = 3; // hidden-warning {{'disallowedStatic3' may be duplicated when built into a shared library: it is mutable, has hidden visibility, and external linkage}}
70+
// expected-warning@-1 {{'disallowedStatic3' may be duplicated when built into a shared library: it is mutable, has hidden visibility, and external linkage}}
71+
};
72+
}
73+
74+
DEFAULT void static_local_never_hidden() {
75+
static int allowedStatic1 = 3;
76+
77+
{
78+
static int allowedStatic2 = 3;
79+
}
80+
81+
auto lmb = []() {
82+
static int allowedStatic3 = 3;
83+
};
84+
}
85+
86+
// Don't warn on this because it's not in a function
87+
const int setByLambda = ([]() { static int x = 3; return x++; })();
88+
89+
inline void has_extern_local() {
90+
extern int allowedAddressExtern; // Not a definition
91+
}
92+
93+
inline void has_regular_local() {
94+
int allowedAddressLocal = 0;
95+
}
96+
97+
inline void has_thread_local() {
98+
// thread_local variables are static by default
99+
thread_local int disallowedThreadLocal = 0; // hidden-warning {{'disallowedThreadLocal' may be duplicated when built into a shared library: it is mutable, has hidden visibility, and external linkage}}
100+
}
101+
102+
} // namespace StaticLocalTest
103+
104+
/******************************************************************************
105+
* Case two: Globals with external linkage
106+
******************************************************************************/
107+
namespace GlobalTest {
108+
// Mutable
109+
inline float disallowedGlobal1 = 3.14; // hidden-warning {{'disallowedGlobal1' may be duplicated when built into a shared library: it is mutable, has hidden visibility, and external linkage}}
110+
111+
// Initialization might run more than once
112+
inline const double disallowedGlobal5 = disallowedGlobal1++; // hidden-warning {{initializeation of 'disallowedGlobal5' may run twice when built into a shared library: it has hidden visibility and external linkage}}
113+
114+
// OK because internal linkage, so duplication is intended
115+
static float allowedGlobal1 = 3.14;
116+
const double allowedGlobal2 = init_dynamic(2);
117+
static const char allowedGlobal3 = []() { return disallowedGlobal1++; }();
118+
static inline double allowedGlobal4 = init_dynamic(2);
119+
120+
// OK, because immutable and compile-time-initialized
121+
constexpr int allowedGlobal5 = 0;
122+
const float allowedGlobal6 = 1;
123+
constexpr int allowedGlobal7 = init_constexpr(2);
124+
const int allowedGlobal8 = init_constexpr(3);
125+
126+
// We don't warn on this because non-inline variables can't (legally) appear
127+
// in more than one TU.
128+
float allowedGlobal9 = 3.14;
129+
130+
// Pointers need to be double-const-qualified
131+
inline float& nonConstReference = disallowedGlobal1; // hidden-warning {{'nonConstReference' may be duplicated when built into a shared library: it is mutable, has hidden visibility, and external linkage}}
132+
const inline int& constReference = allowedGlobal5;
133+
134+
inline int* nonConstPointerToNonConst = nullptr; // hidden-warning {{'nonConstPointerToNonConst' may be duplicated when built into a shared library: it is mutable, has hidden visibility, and external linkage}}
135+
inline int const* nonConstPointerToConst = nullptr; // hidden-warning {{'nonConstPointerToConst' may be duplicated when built into a shared library: it is mutable, has hidden visibility, and external linkage}}
136+
inline int* const constPointerToNonConst = nullptr; // hidden-warning {{'constPointerToNonConst' may be duplicated when built into a shared library: it is mutable, has hidden visibility, and external linkage}}
137+
inline int const* const constPointerToConst = nullptr;
138+
// Don't warn on new because it tends to generate false positives
139+
inline int const* const constPointerToConstNew = new int(7);
140+
141+
inline int const * const * const * const nestedConstPointer = nullptr;
142+
inline int const * const ** const * const nestedNonConstPointer = nullptr; // hidden-warning {{'nestedNonConstPointer' may be duplicated when built into a shared library: it is mutable, has hidden visibility, and external linkage}}
143+
144+
struct Test {
145+
static inline float disallowedStaticMember1; // hidden-warning {{'disallowedStaticMember1' may be duplicated when built into a shared library: it is mutable, has hidden visibility, and external linkage}}
146+
// Defined below, in the header file
147+
static float disallowedStaticMember2;
148+
// Defined in the cpp file, so won't get duplicated
149+
static float allowedStaticMember1;
150+
151+
// Tests here are sparse because the AddrTest case below will define plenty
152+
// more, which aren't problematic to define (because they're immutable), but
153+
// may still cause problems if their address is taken.
154+
};
155+
156+
inline float Test::disallowedStaticMember2 = 2.3; // hidden-warning {{'disallowedStaticMember2' may be duplicated when built into a shared library: it is mutable, has hidden visibility, and external linkage}}
157+
} // namespace GlobalTest

0 commit comments

Comments
 (0)