5

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("%d\n", 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:

  1. Is there something wrong with my code? Is taking the address of multiple members of the union invalid in C?
  2. 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.
Boann
  • 48,794
  • 16
  • 117
  • 146
Suo
  • 89
  • 5
  • 2
    Why are you writing a prototype declaration for `printf` yourself, instead of simply writing `#include `? That seems a bit dangerous to me, because `printf` may be declared a bit differently in `stdio.h`, using some compiler-specific extensions. – Andreas Wenzel Aug 28 '21 at 10:11
  • @AndreasWenzel: The C standard supports declaring a standard library function yourself, so there is nothing dangerous about it, and a C implementation may not require it to be declared “a bit differently” with any compiler-specific extensions. C 2018 7.1.3 2 says “Provided that a library function can be declared without reference to any type defined in a header, it is also permissible to declare the function and use it without including its associated header.” – Eric Postpischil Aug 28 '21 at 10:45
  • 3
    Ugh, why use unrelated uninformative letters for everything? If you make the union `union { long l; short s; int i; } u;` and the pointers `int *ui = *u.i; long *ul = *u.l;` (or even `int *u = &u.i; long *l = &u.l;`) and the other variables `int counter, zero;`, the example code would be much easier to read. – Eric Postpischil Aug 28 '21 at 10:54
  • @EricPostpischil: Thanks for pointing out that declaring standard library functions yourself is permissible, as I was not aware of that. But even if the standard says that it is permissible, it still could have a negative performance impact, if the declaration in the header uses a compiler-specific extension that you do not use in your declaration. Therefore, I still think that it is generally better to include the header, instead of declaring the function yourself. Note that you were referring to section 7.1.3 in your previous comment, but probably meant 7.1.4. – Andreas Wenzel Aug 28 '21 at 11:37
  • I'm sure it has something to do with sequence points, and that the compiler does not see that `h` and `e` point to the same object. Single-stepping in the debugger revealed that `*h = g;` is executed a second time after checking `f` against 0 (being 1). But I'm no language lawyer, so I cannot write a definite answer. – the busybee Aug 28 '21 at 12:01
  • 1
    You might have written code with undefined behavior. – the busybee Aug 28 '21 at 12:02
  • @AndreasWenzel: Yes, 7.1.4. – Eric Postpischil Aug 28 '21 at 12:15
  • @thebusybee: There are sequence points after each full expression, so there are sequence points after `*h = g;` and `*e = 6;`, before the use of `d.b`. I do not see any undefined behavior. The only questionable action is using `d.b` even though `b` was not the last member written, but this is defined in C by C 2018 6.5.2.3 3 (as clarified by note 99). – Eric Postpischil Aug 28 '21 at 12:18
  • This program looks fully defined to me except as to the implementation-defined aspects of the representations of `int` and `short`. Whatever the representations are, using `d.b` ought to reinterpret the bytes of the `int` that was stored in the union as a `short`. I believe it is a compiler bug. – Eric Postpischil Aug 28 '21 at 12:19
  • @NateEldredge: The aliasing rule is clear here and is not violated. The first item in the list in C 2018 6.5 7 is “a type compatible with the effective type of the object.” `*h` has type `long` and accesses `d.a`, which has effective type `long`, and `long` is compatible with `long`. `*e` has type `int` and accesses `d.c`, which has effective type `int`, and `int` is compatible with `int`. `d.b` has type `short` and accesses `d.b`, which has effective type `short`, and `short` is compatible with `short`. – Eric Postpischil Aug 28 '21 at 14:37
  • We can see in [the assembly](https://godbolt.org/z/f39nGGfnj) that the compiler has hoisted `*h = g;` out of the loop and put it afterwards. I guess the idea is that we only should need to assign `*h` once, with whatever the final value of `g` turns out to be. But we have to do the loop before loading `g`, because it believes that `e` could potentially point to `g` (though in fact it does not). Adding `-fno-strict-aliasing` makes it go away, so it seems that clang thinks this code is legal under the strict aliasing rule. – Nate Eldredge Aug 28 '21 at 14:40

2 Answers2

4

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 and d.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 that e doesn't point to g, so the value of g needs to be reloaded after the loop. So the final value of d.b is derived from *h = g which effectively does d.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.

#include <stdio.h>

short int zero = 0;

void a(int *pi, long *pl) {
    for (int x = 0; x < 1000; x++) {
        *pl = x;
        *pi = zero;
    }
}

int main(void) {
    union { int i; long l; } u;
    a(&u.i, &u.l);
    printf("%d\n", u.i);
}

Try on godbolt

As read literally, this code would appear to print 0 on any implementation: the last assignment in a() was to u.i, so u.i should be the active member, and the printf should output the value 0 which was assigned to it. However, with clang -O2, the stores are reordered and the program outputs 999.


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.

Nate Eldredge
  • 48,811
  • 6
  • 54
  • 82
0

I will rewrite the code for ease of reading:

int printf(const char *, ...);

union
{
    long  l;
    short s;
    int   i;
} u;

long *ul = &u.l;
int  *ui = &u.i;

int counter, zero;

int main(void)
{
    for (; counter <= 0; counter++)
    {
        *ul = zero;
        *ui = 6;
    }
    printf("%d\n", u.s);
}

The only questionable code here is the use of u.s in the printf, when u.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 of u.s is that of the named member, and note 99 clarifies this means that, if s is not the member last used to store a value, the appropriate bytes are reinterpreted as a short. This is well established.

The other code is ordinary: *ul = zero; stores a value in a union member. There is no aliasing violating because ul points to a long and is used to access a long. *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.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • 2
    I doubt that it is a compiler bug. It cannot be expected that the compiler remembers the fact that the pointers `ul` and `ui` point inside a `union` and that the pointers may therefore alias. In my opinion, the compiler can only be expected to be aware that a `union` is being accessed when accessing the `union` directly or through a pointer to the whole `union`, but not through a pointer to an individual member. However, as far as I can tell, the ISO C standard does not clearly address this situation, including the latest C23 draft. – Andreas Wenzel Aug 28 '21 at 15:31
  • 2
    Yes, I am with @AndreasWenzel here. Consider a function like `int func(short *ps, int *pi) { *pi = 0; *ps = 7; return *pi; }`. I thought the whole point of the strict aliasing rule is that the compiler can optimize out the reload of `*pi` and return 0 unconditionally, and indeed gcc, clang, icc all do so.. But Eric's argument seems to say that `func(&u.s, &u.i)` must return 7 (assuming an implementation with typical little-endian representations). If that is so then it is a loophole that swallows the strict aliasing rule. – Nate Eldredge Aug 28 '21 at 15:34
  • 1
    I can't find fault with Eric's logic, but I don't think the conclusion is what was intended by the standard authors, so maybe this is a defect? – Nate Eldredge Aug 28 '21 at 15:36
  • 1
    It looks like this has been previously discussed at https://stackoverflow.com/questions/22897037/is-it-undefined-behaviour-to-call-a-function-with-pointers-to-different-elements, without a really satisfactory resolution. – Nate Eldredge Aug 28 '21 at 17:48