Skip to content

Conversation

mptre
Copy link
Contributor

@mptre mptre commented Sep 22, 2025

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 addon 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

@danmar
Copy link
Owner

danmar commented Sep 23, 2025

nice work. I would like that we add a ticket in trac and point to that in the PR title.

I am spontanously thinking that this could possibly be a internal core checker instead (portability). Are there some situations when you think users will consider these warnings to be false positives, assuming they want to fix portability issues?

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 addon 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
@mptre mptre changed the title addons: add unionzeroinit addons: add unionzeroinit (#14150) Sep 23, 2025
@mptre
Copy link
Contributor Author

mptre commented Sep 23, 2025

nice work. I would like that we add a ticket in trac and point to that in the PR title.

Done

I am spontanously thinking that this could possibly be a internal core checker instead (portability).

Agreed, this more of a proof-of-concept that turned out well IMO.

Are there some situations when you think users will consider these warnings to be false positives, assuming they want to fix portability issues?

Say you have union { uint8_t u8; uint64_t u64 } u = {0} and only operate on u8 would be a false positive.

I changed the severity from warning to portability.


nested_variables = variable.nameToken.valueType.typeScope.varlist

# Circumvent what seems to be a bug in which only the last bitfield has
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danmar this smells like a bug. Reproducable using one of the added test cases.

cppcheck --quiet --dump addons/test/unionzeroinit/bitfields.c

Note that only the r3 field has valueType-bits="1" whereas the remaining bitfields has the same attribute set to zero.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that only the r3 field has valueType-bits="1" whereas the remaining bitfields has the same attribute set to zero.

if that is true then yes it is a bug!

would you like to write a ticket about it or should I?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need, fixed in the following commit (as part of this PR).

7344d5f

@danmar danmar changed the title addons: add unionzeroinit (#14150) Fix #14150 (addons: add unionzeroinit) Sep 28, 2025
@danmar
Copy link
Owner

danmar commented Sep 28, 2025

Say you have union { uint8_t u8; uint64_t u64 } u = {0} and only operate on u8 would be a false positive.

yes true but then it would be pretty redundant to use a union I guess.. I have a good feeling about this checker and would like to move this into core cppcheck instead.. could you look into that? that would be used by a lot more cppcheck users. :-)

@mptre
Copy link
Contributor Author

mptre commented Sep 28, 2025 via email

@mptre mptre changed the title Fix #14150 (addons: add unionzeroinit) Fix #14150 (Add unionzeroinit check) Sep 29, 2025
const Union &u = it->second;
if (!u.isLargestMemberFirst()) {
const UnionMember *largestMember = u.largestMember();
assert(largestMember != nullptr);

Check warning

Code scanning / CppCheck

The assert macro shall not be used with a constant-expression Warning

The assert macro shall not be used with a constant-expression
Copy link
Owner

@danmar danmar Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assert macro shall not be used with a constant-expression

This is a false positive. We will fix it in Cppcheck Premium. The expression is not a constant-expression.

@mptre mptre force-pushed the unionzeroinit branch 2 times, most recently from 4ff2ed0 to 51d7983 Compare September 29, 2025 18:33
@mptre
Copy link
Contributor Author

mptre commented Sep 29, 2025

@danmar migrated to cppcheck core. Let me know what you think.

!Token::simpleMatch(tok->tokAt(2), "default :")) {
Token *tok1 = typeTok->next();
if (Token::Match(tok1, "%name% : %num% [;=]"))
if (Token::Match(tok1, "%name% : %num% [;=,]"))
Copy link
Owner

@danmar danmar Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good.. but I am not sure about doing this in the same PR.

separate ticket+pr+testcase (in testtokenizer.cpp) would be ideal..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #7865.

{"CheckType::checkLongCast","style"},
{"CheckType::checkSignConversion","warning"},
{"CheckType::checkTooBigBitwiseShift","platform"},
{"CheckUninitVar::check",""},
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this does not seem correct but anyway it is auto generated so will be fixed later..

@@ -0,0 +1,209 @@
/*
Copy link
Owner

@danmar danmar Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My feeling is that we could add this in some existing checkers class.. checkother maybe. it is just 1 checker in this class and I doubt we will add more related checkers here later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't find that argument compelling and rather keep the logic separated.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have more than 100 checkers and we have always grouped checkers into classes we did not write each checker in its own class.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would find it more difficult to navigate our sourcecode, buildfiles, etc if we had 100's more of checkxyz.cpp , checkxyz.h and testxyz.cpp files..

Comment on lines +119 to +120
return Token::simpleMatch(tok, "= { 0 } ;") ||
Token::simpleMatch(tok, "= { } ;");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return Token::simpleMatch(tok, "= { 0 } ;") ||
Token::simpleMatch(tok, "= { } ;");
return Token::Match(tok, "= { 0| };");

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does not work as intended...

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops I forgot a space

Suggested change
return Token::simpleMatch(tok, "= { 0 } ;") ||
Token::simpleMatch(tok, "= { } ;");
return Token::Match(tok, "= { 0| } ;");

const Union &u = it->second;
if (!u.isLargestMemberFirst()) {
const UnionMember *largestMember = u.largestMember();
assert(largestMember != nullptr);
Copy link
Owner

@danmar danmar Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assert macro shall not be used with a constant-expression

This is a false positive. We will fix it in Cppcheck Premium. The expression is not a constant-expression.

Copy link

sonarqubecloud bot commented Oct 1, 2025

@danmar danmar merged commit 4823852 into danmar:main Oct 3, 2025
53 checks passed
danmar added a commit that referenced this pull request Oct 3, 2025
@danmar
Copy link
Owner

danmar commented Oct 3, 2025

oh shit I merged by mistake :-( I revert it with #7868

danmar added a commit that referenced this pull request Oct 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants