Skip to content

Commit d1d6337

Browse files
committed
Fix #14150 (Add unionzeroinit check)
This has bitten me before and is definitely a foot gun. GCC 15[1] changed their semantics related to zero initialization of unions; from initializing the complete union (sizeof union) to only zero initializing the first member. If the same first member is not the largest one, the state of the remaining storage is considered undefined and in practice most likely stack garbage. The unionzeroinit checker can detect such scenarios and emit a warning. It does not cover the designated initializers as I would interpret those as being intentional. Example output from one of my projects: ``` x86-decoder.c:294:7: warning: Zero initializing union Evex does not guarantee its complete storage to be zero initialized as its largest member is not declared as the first member. Consider making u32 the first member or favor memset(). [unionzeroinit-unionzeroinit] Evex evex = {0}; ^ ``` [1] https://trofi.github.io/posts/328-c-union-init-and-gcc-15.html
1 parent e3c8bdc commit d1d6337

3 files changed

Lines changed: 237 additions & 0 deletions

File tree

lib/checkother.cpp

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
#include <map>
4444
#include <set>
4545
#include <sstream>
46+
#include <unordered_map>
4647
#include <utility>
4748

4849
//---------------------------------------------------------------------------
@@ -4358,6 +4359,135 @@ void CheckOther::checkModuloOfOneError(const Token *tok)
43584359
reportError(tok, Severity::style, "moduloofone", "Modulo of one is always equal to zero");
43594360
}
43604361

4362+
static const std::string noname;
4363+
4364+
struct UnionMember {
4365+
UnionMember()
4366+
: name(noname)
4367+
, size(0) {}
4368+
4369+
UnionMember(const std::string &name_, size_t size_)
4370+
: name(name_)
4371+
, size(size_) {}
4372+
4373+
const std::string &name;
4374+
size_t size;
4375+
};
4376+
4377+
struct Union {
4378+
Union(const Scope *scope_, const std::string &name_)
4379+
: scope(scope_)
4380+
, name(name_) {}
4381+
4382+
const Scope *scope;
4383+
const std::string &name;
4384+
std::vector<UnionMember> members;
4385+
4386+
const UnionMember *largestMember() const {
4387+
const UnionMember *largest = nullptr;
4388+
for (const UnionMember &m : members) {
4389+
if (m.size == 0)
4390+
return nullptr;
4391+
if (largest == nullptr || m.size > largest->size)
4392+
largest = &m;
4393+
}
4394+
return largest;
4395+
}
4396+
4397+
bool isLargestMemberFirst() const {
4398+
const UnionMember *largest = largestMember();
4399+
return largest == nullptr || largest == &members[0];
4400+
}
4401+
};
4402+
4403+
static UnionMember parseUnionMember(const Variable &var,
4404+
const Settings &settings)
4405+
{
4406+
const Token *nameToken = var.nameToken();
4407+
if (nameToken == nullptr)
4408+
return UnionMember();
4409+
4410+
const ValueType *vt = nameToken->valueType();
4411+
size_t size = 0;
4412+
if (var.isArray()) {
4413+
size = var.dimension(0);
4414+
} else if (vt != nullptr) {
4415+
size = ValueFlow::getSizeOf(*vt, settings,
4416+
ValueFlow::Accuracy::ExactOrZero);
4417+
}
4418+
return UnionMember(nameToken->str(), size);
4419+
}
4420+
4421+
static std::vector<Union> parseUnions(const SymbolDatabase &symbolDatabase,
4422+
const Settings &settings)
4423+
{
4424+
std::vector<Union> unions;
4425+
4426+
for (const Scope &scope : symbolDatabase.scopeList) {
4427+
if (scope.type != ScopeType::eUnion)
4428+
continue;
4429+
4430+
Union u(&scope, scope.className);
4431+
for (const Variable &var : scope.varlist) {
4432+
u.members.push_back(parseUnionMember(var, settings));
4433+
}
4434+
unions.push_back(u);
4435+
}
4436+
4437+
return unions;
4438+
}
4439+
4440+
static bool isZeroInitializer(const Token *tok)
4441+
{
4442+
return Token::Match(tok, "= { 0| } ;");
4443+
}
4444+
4445+
4446+
void CheckOther::checkUnionZeroInit()
4447+
{
4448+
if (!mSettings->severity.isEnabled(Severity::portability))
4449+
return;
4450+
4451+
logChecker("CheckUnionZeroInit::check"); // portability
4452+
4453+
const SymbolDatabase *symbolDatabase = mTokenizer->getSymbolDatabase();
4454+
4455+
std::unordered_map<const Scope *, Union> unionsByScopeId;
4456+
const std::vector<Union> unions = parseUnions(*symbolDatabase, *mSettings);
4457+
for (const Union &u : unions) {
4458+
unionsByScopeId.insert({u.scope, u});
4459+
}
4460+
4461+
for (const Token *tok = mTokenizer->tokens(); tok; tok = tok->next()) {
4462+
if (!tok->isName() || !isZeroInitializer(tok->next()))
4463+
continue;
4464+
4465+
const ValueType *vt = tok->valueType();
4466+
if (vt == nullptr || vt->typeScope == nullptr)
4467+
continue;
4468+
auto it = unionsByScopeId.find(vt->typeScope);
4469+
if (it == unionsByScopeId.end())
4470+
continue;
4471+
const Union &u = it->second;
4472+
if (!u.isLargestMemberFirst()) {
4473+
const UnionMember *largestMember = u.largestMember();
4474+
assert(largestMember != nullptr);
4475+
unionZeroInitError(tok, *largestMember);
4476+
}
4477+
}
4478+
}
4479+
4480+
void CheckOther::unionZeroInitError(const Token *tok,
4481+
const UnionMember& largestMember)
4482+
{
4483+
reportError(tok, Severity::portability, "UnionZeroInit",
4484+
"$symbol:" + (tok != nullptr ? tok->str() : "") + "\n"
4485+
"Zero initializing union '$symbol' does not guarantee " +
4486+
"its complete storage to be zero initialized as its largest member " +
4487+
"is not declared as the first member. Consider making " +
4488+
largestMember.name + " the first member or favor memset().");
4489+
}
4490+
43614491
//-----------------------------------------------------------------------------
43624492
// Overlapping write (undefined behavior)
43634493
//-----------------------------------------------------------------------------
@@ -4576,6 +4706,7 @@ void CheckOther::runChecks(const Tokenizer &tokenizer, ErrorLogger *errorLogger)
45764706
checkOther.checkAccessOfMovedVariable();
45774707
checkOther.checkModuloOfOne();
45784708
checkOther.checkOverlappingWrite();
4709+
checkOther.checkUnionZeroInit();
45794710
}
45804711

45814712
void CheckOther::getErrorMessages(ErrorLogger *errorLogger, const Settings *settings) const
@@ -4658,4 +4789,5 @@ void CheckOther::getErrorMessages(ErrorLogger *errorLogger, const Settings *sett
46584789
const std::vector<const Token *> nullvec;
46594790
c.funcArgOrderDifferent("function", nullptr, nullptr, nullvec, nullvec);
46604791
c.checkModuloOfOneError(nullptr);
4792+
c.unionZeroInitError(nullptr, UnionMember());
46614793
}

lib/checkother.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class Function;
4040
class Variable;
4141
class ErrorLogger;
4242
class Tokenizer;
43+
struct UnionMember;
4344

4445
/// @addtogroup Checks
4546
/// @{
@@ -194,6 +195,8 @@ class CPPCHECKLIB CheckOther : public Check {
194195

195196
void checkModuloOfOne();
196197

198+
void checkUnionZeroInit();
199+
197200
void checkOverlappingWrite();
198201
void overlappingWriteUnion(const Token *tok);
199202
void overlappingWriteFunction(const Token *tok, const std::string& funcname);
@@ -257,6 +260,7 @@ class CPPCHECKLIB CheckOther : public Check {
257260
void knownPointerToBoolError(const Token* tok, const ValueFlow::Value* value);
258261
void comparePointersError(const Token *tok, const ValueFlow::Value *v1, const ValueFlow::Value *v2);
259262
void checkModuloOfOneError(const Token *tok);
263+
void unionZeroInitError(const Token *tok, const UnionMember& largestMember);
260264

261265
void getErrorMessages(ErrorLogger *errorLogger, const Settings *settings) const override;
262266

test/testother.cpp

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@
2727
#include <cstddef>
2828
#include <string>
2929

30+
static std::string unionZeroInitMessage(int lno, int cno, const std::string &varName, const std::string &largestMemberName)
31+
{
32+
std::stringstream ss;
33+
ss << "[test.cpp:" << lno << ":" << cno << "]: (portability) ";
34+
ss << "Zero initializing union '" << varName << "' ";
35+
ss << "does not guarantee its complete storage to be zero initialized as its largest member is not declared as the first member. ";
36+
ss << "Consider making " << largestMemberName << " the first member or favor memset(). [UnionZeroInit]";
37+
ss << std::endl;
38+
return ss.str();
39+
}
40+
3041
class TestOther : public TestFixture {
3142
public:
3243
TestOther() : TestFixture("TestOther") {}
@@ -307,6 +318,12 @@ class TestOther : public TestFixture {
307318

308319
TEST_CASE(knownConditionFloating);
309320
TEST_CASE(knownConditionPrefixed);
321+
322+
TEST_CASE(unionZeroInitBasic);
323+
TEST_CASE(unionZeroInitArrayMember);
324+
TEST_CASE(unionZeroInitStructMember);
325+
TEST_CASE(unionZeroInitUnknownType);
326+
TEST_CASE(unionZeroInitBitfields);
310327
}
311328

312329
#define check(...) check_(__FILE__, __LINE__, __VA_ARGS__)
@@ -13242,6 +13259,90 @@ class TestOther : public TestFixture {
1324213259
"[test.cpp:2:13] -> [test.cpp:3:11]: (style) The comparison 'i > +1' is always false. [knownConditionTrueFalse]\n",
1324313260
errout_str());
1324413261
}
13262+
13263+
void unionZeroInitBasic() {
13264+
check(
13265+
"union bad_union_0 {\n"
13266+
" char c;\n"
13267+
" long long i64;\n"
13268+
" void *p;\n"
13269+
"};\n"
13270+
"\n"
13271+
"typedef union {\n"
13272+
" char c;\n"
13273+
" int i;\n"
13274+
"} bad_union_1;\n"
13275+
"\n"
13276+
"extern void e(union bad_union_0 *);\n"
13277+
"\n"
13278+
"void\n"
13279+
"foo(void)\n"
13280+
"{\n"
13281+
" union { int i; char c; } good0 = {0};\n"
13282+
" union { int i; char c; } good1 = {};\n"
13283+
"\n"
13284+
" union { char c; int i; } bad0 = {0};\n"
13285+
" union bad_union_0 bad1 = {0};\n"
13286+
" e(&bad1);\n"
13287+
" bad_union_1 bad2 = {0};\n"
13288+
"}");
13289+
const std::string exp = unionZeroInitMessage(20, 28, "bad0", "i") +
13290+
unionZeroInitMessage(21, 21, "bad1", "i64") +
13291+
unionZeroInitMessage(23, 15, "bad2", "i");
13292+
ASSERT_EQUALS(exp, errout_str());
13293+
}
13294+
13295+
void unionZeroInitArrayMember() {
13296+
check(
13297+
"void foo(void) {\n"
13298+
" union { int c; char s8[2]; } u = {0};\n"
13299+
"}");
13300+
ASSERT_EQUALS("", errout_str());
13301+
}
13302+
13303+
void unionZeroInitStructMember() {
13304+
check(
13305+
"void foo(void) {\n"
13306+
" union {\n"
13307+
" int c;\n"
13308+
" struct {\n"
13309+
" char x;\n"
13310+
" struct {\n"
13311+
" char y;\n"
13312+
" } s1;\n"
13313+
" } s0;\n"
13314+
" } u = {0};\n"
13315+
"}");
13316+
ASSERT_EQUALS("", errout_str());
13317+
}
13318+
13319+
void unionZeroInitUnknownType() {
13320+
check(
13321+
"union u {\n"
13322+
" Unknown x;\n"
13323+
"}");
13324+
ASSERT_EQUALS("", errout_str());
13325+
}
13326+
13327+
void unionZeroInitBitfields() {
13328+
check(
13329+
"typedef union Evex {\n"
13330+
" int u32;\n"
13331+
" struct {\n"
13332+
" char mmm:3,\n"
13333+
" b4:1,\n"
13334+
" r4:1,\n"
13335+
" b3:1,\n"
13336+
" x3:1,\n"
13337+
" r3:1;\n"
13338+
" } extended;\n"
13339+
"} Evex;\n"
13340+
"\n"
13341+
"void foo(void) {\n"
13342+
" Evex evex = {0};\n"
13343+
"}");
13344+
ASSERT_EQUALS("", errout_str());
13345+
}
1324513346
};
1324613347

1324713348
REGISTER_TEST(TestOther)

0 commit comments

Comments
 (0)