I found that this code produces different results with "-fsanitize=undefined,address" and without it.
int printf(const char *, ...);
union {
long a;
short b;
int c;
} d;
int *e = &d.c;
int f, g;
long *h = &d.a;
int main() {
for (; f <= 0; f++) {
*h = g;
*e = 6;
}
printf("%dn", d.b);
}
The command line is:
$ clang -O0 -fsanitize=undefined,address a.c -o out0
$ clang -O1 -fsanitize=undefined,address a.c -o out1
$ clang -O1 a.c -o out11
$ ./out0
6
$ ./out1
6
$ ./out11
0
The Clang version is:
$ clang -v
clang version 13.0.0 (/data/src/llvm-dev/llvm-project/clang 3eb2158f4fea90d56aeb200a5ca06f536c1df683)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /data/bin/llvm-dev/bin
Found candidate GCC installation: /opt/rh/devtoolset-7/root/usr/lib/gcc/x86_64-redhat-linux/7
Selected GCC installation: /opt/rh/devtoolset-7/root/usr/lib/gcc/x86_64-redhat-linux/7
Candidate multilib: .;@m64
Candidate multilib: 32;@m32
Selected multilib: .;@m64
Found CUDA installation: /usr/local/cuda, version 10.2
The OS and platform are:
CentOS Linux release 7.8.2003 (Core).0, x86_64 GNU/Linux
My questions:
- Is there something wrong with my code? Is taking the address of multiple members of the union invalid in C?
- If there is something wrong with my code, how do I get LLVM (or GCC) to warn me? I have used -Wall -Wextra but LLVM and GCC show no warning.
2
Answers
I will rewrite the code for ease of reading:
The only questionable code here is the use of
u.s
in theprintf
, whenu.s
is not the last member of the union that was stored. That is defined by C 2018 6.5.2.3, which says the value ofu.s
is that of the named member, and note 99 clarifies this means that, ifs
is not the member last used to store a value, the appropriate bytes are reinterpreted as ashort
. This is well established.The other code is ordinary:
*ul = zero;
stores a value in a union member. There is no aliasing violating becauseul
points to along
and is used to access along
.*ui = 6;
stores a value in another union member and is also not an aliasing violation.The specific bytes used to represent 6 in an
int
are implementation-defined in regard to ordering and padding bits. However, whatever they are, they should be the same with or without Clang’s “sanitization” and the same in optimization levels 0 and 1. Therefore, the same result should be obtained in all compilations.This is a compiler bug.
I agree with other comments and answer that this is likely a defect in the C standard, as it makes the aliasing rule largely useless. Nonetheless, the sample code conforms to the requirements of the C standard and ought to work as described.
Is there something wrong with the code?
For practical purposes, yes.
I think this is the same underlying issue as Is it undefined behaviour to call a function with pointers to different elements of a union as arguments?
As Eric Postpischil points out, the C standard as read literally seems to permit your code, and require it to print out 6 (assuming that’s consistent with how your implementation represents integer types and how it lays out unions). However, this literal reading would render the strict aliasing rule almost entirely impotent, so in my opinion it’s not what the standard authors would have intended.
The spirit of the strict aliasing rule is that the same object may not be accessed through pointers to different types (with certain exceptions for character types, etc) and that the compiler may optimize on the assumption that this never happens. Although
d.a
andd.c
are not strictly speaking "the same object", they do have overlapping storage, and I think compiler authors interpret the rule as also not allowing overlapping objects to be accessed through pointers to different types. Under that interpretation your code would have undefined behavior.In Defect Report 236 the committee considered a similar example and stated that it has undefined behavior, because of its use of pointers that "have different types but designate the same region of storage". However, wording to clarify this does not seem to have ever made it into any subsequent version of the standard.
Anyhow, I think the practical upshot is that you cannot expect your code to work "correctly" under modern compilers that enforce their interpretations of the strict aliasing rule. Whether or not this is a clang bug is a matter of opinion, but even if you do think it is, then it’s a bug that they are probably not ever going to fix.
Why does it behave this way?
If you use the
-fno-strict-aliasing
flag, then you get back to the 6 behavior. My guess is that the sanitizers happen to inhibit some of these optimizations, which is why you don’t see the 0 behavior when using those options.What seems to have happened under the hood with
-O1
is the compiler assumed that the stores to*h
and*e
don’t interact (because of their different types) and therefore can be freely reordered. So it hoisted*h = g
outside the loop, since after all multiple stores to the same address, with no intervening load, are redundant and only the last one needs to be kept. It happened to put it after the loop, presumably because it can’t prove thate
doesn’t point tog
, so the value ofg
needs to be reloaded after the loop. So the final value ofd.b
is derived from*h = g
which effectively doesd.a = 0
.How to get a warning?
Unfortunately, compilers are not good at checking, either statically or at runtime, for violations of (their interpretation of) the strict aliasing rule. I’m not aware of any way to get a warning for such code. With clang you can use
-Weverything
to enable every warning option that it supports (many of which are useless or counterproductive), and even with that, it gives no relevant warnings about your program.Another example
In case anyone is curious, here’s another test case that doesn’t rely on any type pun, reinterpretation, or other implementation-defined behavior.
Try on godbolt
As read literally, this code would appear to print 0 on any implementation: the last assignment in
a()
was tou.i
, sou.i
should be the active member, and theprintf
should output the value 0 which was assigned to it. However, withclang -O2
, the stores are reordered and the program outputs999
.Just as a counterpoint, though, if you read the standard so as to make the above example UB, then this leads to the somewhat absurd conclusion that
u.l = 0; u.i = 5; print(u.i);
is well defined and prints 5, but that*&u.l = 0; *&u.i = 5; print(u.i);
is UB. (Recall that the "cancellation rule" of&
and*
applies to&*p
but not to*&x
.)The whole situation is rather unsatisfactory.