C Puzzle: Double Trouble

I ran into an interesting C program that both of my C oracles (KCC and Frama-C) consider to be well-defined, but that are inconsistently compiled by the current development versions of GCC and Clang on x86-64.

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

void fn2 (int p1)
{
  printf ("%d\n", p1);
}

union U0 {
  short f0;
  int f1;
};

union U0 b;
int *c = &b.f1;

int main ()
{
  short *d = &b.f0;
  *d = 0;
  *c = 1;
  fn2 (b.f0);
  return 0;
}

The results:

[regehr@gamow 1]$ current-gcc -O1 small.c ; ./a.out
1
[regehr@gamow 1]$ current-gcc -O2 small.c ; ./a.out
0
[regehr@gamow 1]$ clang -O1 small.c ; ./a.out
1
[regehr@gamow 1]$ clang -O2 small.c ; ./a.out
0

GCC is revision 183992 and Clang is 150180.

The question is: Is this program well-defined (in which case I get to report two compiler bugs) or is it undefined (in which case I get to report two bugs in C analysis tools)?

UPDATE:

Just to confuse things a bit more, I’ll add this program. GCC prints 1 at all optimization levels but Clang changes its answer to 0 at higher levels.

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

union U0 {
  short f0;
  int f1;
} b;

union U0 *c = &b;

int main ()
{
  b.f0 = 0;
  c->f1 = 1;
  printf ("%d\n", b.f0);
  return 0;
}

I decided to report this as an LLVM bug just to make sure that the folks there have thought this kind of example over. I suspect it’ll get closed as a WONTFIX, which is OK — real compiler design involves a lot of hard tradeoffs.

12 Replies to “C Puzzle: Double Trouble”

  1. Here’s what I got from the standard. It looked like it was all well-defined up until the last point.

    If the member used to access the contents of a union object is not the same as the member last used to
    store a value in the object, the appropriate part of the object representation of the value is reinterpreted
    as an object representation in the new type as described in 6.2.6 (a process sometimes called “type
    punning”). This might be a trap representation.

    All pointers to members of the same union object compare equal.

    The size of a union is sufficient to contain the largest of its members. The value of at
    most one of the members can be stored in a union object at any time. A pointer to a
    union object, suitably converted, points to each of its members (or if a member is a bit-
    field, then to the unit in which it resides), and vice versa.

    The following are unspecified:

    The value of a union member other than the last one stored into (6.2.6.1).

  2. (Let’s try that again…)

    Here’s what I got from the standard. It looked like it was all well-defined up until the last point.

    If the member used to access the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called “type punning”). This might be a trap representation.

    All pointers to members of the same union object compare equal.

    The size of a union is sufficient to contain the largest of its members. The value of at most one of the members can be stored in a union object at any time. A pointer to a union object, suitably converted, points to each of its members (or if a member is a bit- field, then to the unit in which it resides), and vice versa.

    The following are unspecified:

    The value of a union member other than the last one stored into (6.2.6.1).

  3. If the program is undefined, then what about replacing *c = 1; with *(&b.f1) = 1; and if that’s still undefined, why should it be different from b.f1 = 1; ?

    Note that Frama-C’s value analysis is documented as not doing anything (yet) about strict aliasing (p. 39 of the manual).

  4. My impression is that the program is undefined, and would still be undefined with changes suggested by Pascal. As far as I understand the “strict aliasing” interpretation, it is never ok to read from b.f0 when the last store was to b.f1. So, “b.f1 = 1; fn2(b.f0);” is equally undefined.

    I would be very grateful to have this understanding corrected, though, since I quite frankly hate this interpretation and would love to find out that either (a) this is not what GCC is doing or (b) GCC should not be doing this.

  5. Is it *unspecified* without being *undefined*?

    “When a value is stored in an object of structure or union type, including in a member object, the bytes of the object representation that correspond to any padding bytes take unspecified values.42) The values of padding bytes shall not affect whether the value of such an object is a trap representation.”

    “When a value is stored in a member of an object of union type, the bytes of the object representation that do not correspond to that member but do correspond to other members take unspecified values, but the value of the union object shall not thereby become a trap representation.”

    But I don’t know what “the value of the union object” means outside of the values of its members.

  6. As a note, we should all be reading from C99TC3 (or C11). C99 was amended in TC3 following this defect report:

    http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm

    In addition, GCC has always documented that it explicitly allows type-punning through unions. John’s program with “*c = 1;” replaced with “b.f1 = 1;” is an example that GCC developers might have used to clarify what GCC allows.

    GCC generating programs that compute differently depending on the optimization level indicates either an oversight or that in GCC developers’ minds, John’s original program does not use the union properly for type-punning.

  7. The program is undefined due to violating strict aliasing rules. Section 6.5 of the spec:

    An object shall have its stored value accessed only by an lvalue expression that has one of
    the following types:
    — a type compatible with the effective type of the object,
    — a qualified version of a type compatible with the effective type of the object,
    — a type that is the signed or unsigned type corresponding to the effective type of the
    object,
    — a type that is the signed or unsigned type corresponding to a qualified version of the
    effective type of the object,
    — an aggregate or union type that includes one of the aforementioned types among its
    members (including, recursively, a member of a subaggregate or contained union), or
    — a character type.
    […]
    6.5.2.3 Structure and union members
    […]
    A postfix expression followed by the . operator and an identifier designates a member of
    a structure or union object. The value is that of the named member,82)
    […]
    82) If the member used to access the contents of a union object is not the same as the member last used to
    store a value in the object, the appropriate part of the object representation of the value is reinterpreted
    as an object representation in the new type as described in 6.2.6 (a process sometimes called “type
    punning”). This might be a trap representation.

  8. In your example, the code happens to read from a member of a union which is not the most recently written one. However, it is possible to write similar code which does not have this problem, yet which still exhibits problematic behavior. See the examples in this Defect Report:

    http://open-std.org/JTC1/SC22/WG14/www/docs/dr_236.htm

    This report is categorized as Closed, however the explanation in the Committee Response is based on reasoning which isn’t obviously supported by the standard.

    In general, there does not appear to be any way to have sound type-based aliasing rules in a language that also has non-discriminated unions, untyped (e.g. malloc) memory, and pointers. However, type-based alias rules are considered an important feature, and the committee has chosen to keep it anyway. Consequently, the standard is inconsistent. In practice, each compiler is left to choose for itself the extent to which it is willing to trade safety for optimization.

  9. From the GCC manual: “Even with -fstrict-aliasing, type-punning is allowed, provided the memory is accessed through the union type. … access by taking the address, casting the resulting pointer and dereferencing the result has undefined behavior, even if the cast uses a union type.”

    Also, -fstrict-aliasing is enabled for -O2 but not -O1. I imagine that if you used -fstrict-aliasing without a -O flag, you’d have the same problem.

  10. This is offtopic to this post but there is an elegant way to solve your “Iterating Over The Full Range” problem with the inclusive for loop idiom:

    for (bool go = true; go; (go = (f != l)) && (++f, true)) {
    use(f);
    }

    // or

    do {
    use(f);
    } while ((f != l) && (++f, true));

    // or my favorite which only works in C++ because stupid C99 doesn’t allow variables to be defined in the if expression

    #define inclusive_for(init, cond, inc) \
    if (bool ar3_d0n3_ = false) {} \
    else for (init; !ar3_d0n3_; (ar3_d0n3_ = !(bool)(cond)) || ((inc), false))

    inclusive_for (int i = INT_MIN, i != INT_MAX, ++i) {
    use(f);
    }

  11. The aliasing rules are clearly intended to prohibit this sort of thing, but as far as I can tell, they do not. The union members are distinct objects and they are accessed through pointers of the appropriate type. This situation is obviously allowed by the aliasing rules. Equally obviously, though, the pointers to these objects compare equal, so accessing one is accessing a differently-typed object at the same time, something the aliasing rules are supposed to prevent.

    It’s possible to reason this is prevented by the aliasing rules, but this causes inconsistency with the rest of the standard, which explicitly allows type-punning through unions (which should likewise be disallowed under this interpretation, through the same reasoning). The standard would need an explicit change to cover both situations in the intended manner.

    I think neither the compilers nor the verifiers are buggy — it’s the standard that needs a fix. I disagree with Dan that it’s “impossible” to have sound rules (at the very least, every compiler implementation at a particular optimization level, however obtuse, represents one consistent implementation of a set of rules, however complicated), but the fix in this case would be complicated and inelegant — pointers to union members would need special treatment, likely causing a lot of ugly adjustments to sections that have nothing to do with unions.

Comments are closed.