Skip to content

Commit cded51e

Browse files
authored
Fix #14150 (Add unionzeroinit check) (#7870)
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 c2bd13e commit cded51e

File tree

4 files changed

+246
-1
lines changed

4 files changed

+246
-1
lines changed

lib/checkother.cpp

Lines changed: 136 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,139 @@ 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+
explicit Union(const Scope &scope)
4379+
: scope(&scope)
4380+
, name(scope.className) {}
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);
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("CheckOther::checkUnionZeroInit"); // 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.emplace(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+
if (largestMember == nullptr) {
4475+
throw InternalError(tok, "Largest union member not found",
4476+
InternalError::INTERNAL);
4477+
}
4478+
assert(largestMember != nullptr);
4479+
unionZeroInitError(tok, *largestMember);
4480+
}
4481+
}
4482+
}
4483+
4484+
void CheckOther::unionZeroInitError(const Token *tok,
4485+
const UnionMember& largestMember)
4486+
{
4487+
reportError(tok, Severity::portability, "UnionZeroInit",
4488+
(tok != nullptr ? "$symbol:" + tok->str() + "\n" : "") +
4489+
"Zero initializing union '$symbol' does not guarantee " +
4490+
"its complete storage to be zero initialized as its largest member " +
4491+
"is not declared as the first member. Consider making " +
4492+
largestMember.name + " the first member or favor memset().");
4493+
}
4494+
43614495
//-----------------------------------------------------------------------------
43624496
// Overlapping write (undefined behavior)
43634497
//-----------------------------------------------------------------------------
@@ -4576,6 +4710,7 @@ void CheckOther::runChecks(const Tokenizer &tokenizer, ErrorLogger *errorLogger)
45764710
checkOther.checkAccessOfMovedVariable();
45774711
checkOther.checkModuloOfOne();
45784712
checkOther.checkOverlappingWrite();
4713+
checkOther.checkUnionZeroInit();
45794714
}
45804715

45814716
void CheckOther::getErrorMessages(ErrorLogger *errorLogger, const Settings *settings) const
@@ -4658,4 +4793,5 @@ void CheckOther::getErrorMessages(ErrorLogger *errorLogger, const Settings *sett
46584793
const std::vector<const Token *> nullvec;
46594794
c.funcArgOrderDifferent("function", nullptr, nullptr, nullvec, nullvec);
46604795
c.checkModuloOfOneError(nullptr);
4796+
c.unionZeroInitError(nullptr, UnionMember());
46614797
}

lib/checkother.h

Lines changed: 5 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

@@ -291,6 +295,7 @@ class CPPCHECKLIB CheckOther : public Check {
291295
// portability
292296
"- Passing NULL pointer to function with variable number of arguments leads to UB.\n"
293297
"- Casting non-zero integer literal in decimal or octal format to pointer.\n"
298+
"- Incorrect zero initialization of unions can lead to access of uninitialized memory.\n"
294299

295300
// style
296301
"- C-style pointer cast in C++ code\n"

releasenotes.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
Release Notes for Cppcheck 2.19
22

33
New checks:
4-
-
4+
- Detect zero initialization of unions in which its largest member is not
5+
declared as the first one. Depending on the compiler, there's no guarantee
6+
that the complete union will be zero initialized in such scenarios leading to
7+
potential access of uninitialized memory.
58

69
Improved checking:
710
-

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__)
@@ -13266,6 +13283,90 @@ class TestOther : public TestFixture {
1326613283
"[test.cpp:2:13] -> [test.cpp:3:11]: (style) The comparison 'i > +1' is always false. [knownConditionTrueFalse]\n",
1326713284
errout_str());
1326813285
}
13286+
13287+
void unionZeroInitBasic() {
13288+
check(
13289+
"union bad_union_0 {\n"
13290+
" char c;\n"
13291+
" long long i64;\n"
13292+
" void *p;\n"
13293+
"};\n"
13294+
"\n"
13295+
"typedef union {\n"
13296+
" char c;\n"
13297+
" int i;\n"
13298+
"} bad_union_1;\n"
13299+
"\n"
13300+
"extern void e(union bad_union_0 *);\n"
13301+
"\n"
13302+
"void\n"
13303+
"foo(void)\n"
13304+
"{\n"
13305+
" union { int i; char c; } good0 = {0};\n"
13306+
" union { int i; char c; } good1 = {};\n"
13307+
"\n"
13308+
" union { char c; int i; } bad0 = {0};\n"
13309+
" union bad_union_0 bad1 = {0};\n"
13310+
" e(&bad1);\n"
13311+
" bad_union_1 bad2 = {0};\n"
13312+
"}");
13313+
const std::string exp = unionZeroInitMessage(20, 28, "bad0", "i") +
13314+
unionZeroInitMessage(21, 21, "bad1", "i64") +
13315+
unionZeroInitMessage(23, 15, "bad2", "i");
13316+
ASSERT_EQUALS(exp, errout_str());
13317+
}
13318+
13319+
void unionZeroInitArrayMember() {
13320+
check(
13321+
"void foo(void) {\n"
13322+
" union { int c; char s8[2]; } u = {0};\n"
13323+
"}");
13324+
ASSERT_EQUALS("", errout_str());
13325+
}
13326+
13327+
void unionZeroInitStructMember() {
13328+
check(
13329+
"void foo(void) {\n"
13330+
" union {\n"
13331+
" int c;\n"
13332+
" struct {\n"
13333+
" char x;\n"
13334+
" struct {\n"
13335+
" char y;\n"
13336+
" } s1;\n"
13337+
" } s0;\n"
13338+
" } u = {0};\n"
13339+
"}");
13340+
ASSERT_EQUALS("", errout_str());
13341+
}
13342+
13343+
void unionZeroInitUnknownType() {
13344+
check(
13345+
"union u {\n"
13346+
" Unknown x;\n"
13347+
"}");
13348+
ASSERT_EQUALS("", errout_str());
13349+
}
13350+
13351+
void unionZeroInitBitfields() {
13352+
check(
13353+
"typedef union Evex {\n"
13354+
" int u32;\n"
13355+
" struct {\n"
13356+
" char mmm:3,\n"
13357+
" b4:1,\n"
13358+
" r4:1,\n"
13359+
" b3:1,\n"
13360+
" x3:1,\n"
13361+
" r3:1;\n"
13362+
" } extended;\n"
13363+
"} Evex;\n"
13364+
"\n"
13365+
"void foo(void) {\n"
13366+
" Evex evex = {0};\n"
13367+
"}");
13368+
ASSERT_EQUALS("", errout_str());
13369+
}
1326913370
};
1327013371

1327113372
REGISTER_TEST(TestOther)

0 commit comments

Comments
 (0)