4

Given the following code, I have some questions related to type punning. I do not see any way that this isn't violating strict aliasing rules, but I cannot point to the specific violation. My best guess is that passing the union members into the function violates strict aliasing.

The following code is on Compiler Explorer.

#include <stdint.h>

union my_type
{
    uint8_t m8[8];
    uint16_t m16[4];
    uint32_t m32[2];
    uint64_t m64;
};

int func(uint16_t *x, uint32_t *y)
{
    return *y += *x;
}

int main(int argc, char *argv[])
{
    union my_type mine = {.m64 = 1234567890};
    return func(mine.m16, mine.m32);
}

My observations:

  • Assuming the arguments to func do not alias each other, func does not violate strict aliasing.
  • In C, it is permissible to use a union for type punning.
  • Passing m16 and m32 into func must violate something.

My questions:

  • Is type punning with arrays like this valid?
  • What exactly am I violating by passing the pointers into func?
  • What other gotchas am I missing in this example?
BZKN
  • 1,499
  • 2
  • 10
  • 25
Graznarak
  • 3,626
  • 4
  • 28
  • 47
  • I think it looks fine apart from implementation defined behavior depending on endianness, and possible arithmetic overflow if `int` is 16 bits wide, and interpretation of the return value from `main`. (I would note that `restrict`-qualified pointers are not in use.) – Ian Abbott Mar 15 '22 at 18:26
  • I'm not the "rules" guy, but in the example writing to `*x` will modify `*y` although `*y` is not `volatile`. I don't know if this has anything to do with aliasing rules, but it violates the rule that variables shall be `volatile` if they can change "outside" the program flow. – Martin Rosenau Mar 15 '22 at 18:27
  • @MartinRosenau you probably mean it the other way around. Writing to `*y` will change `*x` as well. Where do you see a change outside the program flow? `main` must be aware that `func` can change the values. – Gerhardh Mar 15 '22 at 18:31
  • 1
    What you have looks OK, though I'd change the type of func() to uint32_t if you're going to use it as you do. Also, the line "return *y += *x;" is coyote ugly, and I wouldn't pass a code review if I saw that...just my $.02. – mzimmers Mar 15 '22 at 18:35
  • Ignore the part I wrote earlier about 16 bits (I got the types of `x` and `y` mentally swapped). I should have mentioned arithmetic overflow when the result of the addition is greater than `INT_MAX`. – Ian Abbott Mar 15 '22 at 18:39
  • Q: In C, it is permissible to use a union for [type punning](https://en.wikipedia.org/wiki/Type_punning). A: yes. Q: Passing m16 and m32 into func must violate something. A: No. Per Chux: `I do not see this as an array or union issue, just one of passing pointers to overlapping data that the function func() is not obliged to account for.` – paulsm4 Mar 15 '22 at 18:51
  • @mzimmers I would also fail that in a code review. This is just example code, not something that I would ever normally write. – Graznarak Mar 15 '22 at 19:13
  • @IanAbbott with `*y += *x` there is no signed integer overflow here, regardless of the width of `int`. `INT_MAX` is not a concern here. – chux - Reinstate Monica Mar 15 '22 at 19:15
  • See https://stackoverflow.com/questions/68963247/why-are-the-results-of-this-code-different-with-and-without-fsanitize-undefine/68967014#68967014 for a related question. – Nate Eldredge Mar 15 '22 at 21:54
  • @Gerhardh Using some very strange optimization settings or using some strange CPU architecture, a compiler would be allowed to perform `*x += *y;` in the following order: Read `*x`, set `*x` to a fixed value, read `*y`, add the values, write `*x`. Reading `*y` would produce the wrong result then. – Martin Rosenau Mar 16 '22 at 10:27
  • @chux-ReinstateMonica Yes, sorry. I meant when the final value of `*y` (resulting from the addition and assignment to `*y`) is returned as an `int`. – Ian Abbott Mar 16 '22 at 10:35

3 Answers3

7

The rule violated is C 2018 6.5.16.1 3:

If the value being stored in an object is read from another object that overlaps in any way the storage of the first object, then the overlap shall be exact and the two objects shall have qualified or unqualified versions of a compatible type; otherwise, the behavior is undefined.

Specifically, in *y += *x, the value being stored in the object pointed to by y, mine.m16, is read from another object, mine.m32, that overlaps the storage of mine.m16, but the overlap is not exact and neither do the objects have compatible types, regardless of qualifiers.

Note that this rule is for simple assignment, as in E1 = E2, whereas the code has a component assignment, E1 += E2. However, the compound assignment E1 += E2 is defined in 6.5.16.2 3 to be equivalent to E1 = E1 + E2 except that the lvalue E1 is evaluated only once.

Is type punning with arrays like this valid?

Yes, the C standard allows aliasing via union members; reading a member other than the last one stored will reinterpret the bytes in the new type. However, this does not absolve a program of conforming to other rules if its behavior is to be defined by the C standard, notably the rule quoted above.

What exactly am I violating by passing the pointers into func?

No rule is violated by passing the pointers. The assignment using the pointers violates a rule, as answered above.

What other gotchas am I missing in this example?

If we change func:

int func(uint16_t *x, uint32_t *y)
{
    *y += 1;
    *x += 1;
    return *y;
}

then the rule in 6.5.16.1 3 does not apply, as there is no assignment involving overlapping objects. And the aliasing rules in 6.5 7 are not violated, as *y is an object defined as the type used to access it, uint16_t, and *x is an object defined as the type used to access it, uint32_t. Yet, if this function is translated in isolation (without the union definition visible), the compiler is permitted to assume *x and *y do not overlap, so it may cache the value of *y produced by *y += 1; and return that cached value, in ignorance of the fact that *x += 1; changes *y. This is a defect in the C standard.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • The 6.5.16.1 3 rule is confusing because it is specifically in the semantics section for simple assignment. It would be better to list it in the semantics for 6.5.16 if it is common to both simple and compound assignment. – Ian Abbott Mar 16 '22 at 10:48
  • @IanAbbott: I have no control over where the rule is located. – Eric Postpischil Mar 16 '22 at 11:00
  • I realize that. :-) Also, it is not clear that the value being stored is read from an object. – Ian Abbott Mar 16 '22 at 11:11
3

Passing m16 and m32 into func must violate something.

func(uint16_t *x, uint32_t *y) is free to assume *x and *y do not overlap as x, y are different enough pointer types. Since the referenced data does overlap in OP's code, we have a problem.

The special issues about unions and aliasing do not apply here in the body of func() as the union-ness of the calling code is lost.

Alternate "safe" code could have been:

// Use volatile to prevent folding these 2 lines of code.
// The key is that even with optimized code, 
// the sum must be done before *y assignment.
volatile uint32_t sum = *y + *x;
*y = sum;

return (int) (*y);

What exactly am I violating by passing the pointers into func?

Passing pointers to overlapping data that the function func() is not obliged to account for.


Is type punning with arrays like this valid?

I do not see this as an array or union issue, just one of passing pointers to overlapping data that the function func() is not obliged to account for.

What other gotchas am I missing in this example?

Minor: int may be 16-bit, potentially causing implementation defined behavior in the conversion of uint32_t to int.


Consider the difference between

uint32_t fun1(uint32_t *a, uint32_t *b);
uint32_t fun2(uint32_t * restrict a, uint32_t * restrict b);

fun1() would have to consider an overlap potential. fun2() would not.

chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
  • Why "union-ness" is lost? The union type is visible in `func` – tstanisl Mar 15 '22 at 19:43
  • “Passing pointers to overlapping data that the function `func()` is not obliged to account for” does not answer the question “What exactly am I violating by passing the pointers into func?” because it is not exactly a rule in the C standard. – Eric Postpischil Mar 15 '22 at 19:48
  • 1
    @tstanisl Yes viable, but not used. `int func(uint16_t *x, uint32_t *y) { return *y += *x; }` is compile-able without knowledge of the `union my_type`. The union-ness is lost as `union my_type` is not needed, used or referred to. Hence its functionality is independent of `union my_type`. – chux - Reinstate Monica Mar 15 '22 at 20:06
  • @tstanisl, the visible union clause (6.5.2.3/6 in N2176) applies for the common initial sequence of structs which are in an union. I don't remember something similar for all members (if I'm mistaken, I welcome a reference) – AProgrammer Mar 15 '22 at 20:16
  • I don't get why `volatile` is used there. The value of the variable is not being changed in any unusual way. – Ian Abbott Mar 16 '22 at 11:45
  • @IanAbbott Without `volatile` (or some sequence point control), `uint32_t sum = *y + *x; *y = sum;` could be optimized as if `*y = *y + *x;`, which is UB when `x == y`. – chux - Reinstate Monica Mar 16 '22 at 12:51
  • But there is already a sequence point between `uint32_t sum = *y + *x;` and `*y = sum;`. – Ian Abbott Mar 16 '22 at 13:28
1

My observations:

  • Assuming the arguments to func do not alias each other, func does not violate strict aliasing.

Not reliably true. The so-called strict-aliasing rule is expressed in terms of the lvalue used to access a given object, relative to that object's effective type. The two arguments to func() do not need to alias each other for execution of func() to produce a strict-aliasing violation. Example:

uint32_t x = 0, y = 1;
func((uint16_t *)&x, &y);
// func will violate strict aliasing when it dereferences its first parameter

Issues revolving around function parameters aliasing each other would be the realm of restrict-qualified pointers, which you are not using.


  • In C, it is permissible to use a union for type punning.

Yes, provided that the punning is performed via the union object. This is covered by C17 6.5/7, the aforementioned strict-aliasing rule:

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:

[...]

  • an aggregate or union type that includes one of the aforementioned types among its members

Note well that this isn't about the storage being accessed actually being inside a union object, but rather about the type of lvalue used to access it relative to the actual (effective) type of the object being accessed.


  • Passing m16 and m32 into func must violate something.

It does, though the language specification could be a lot clearer about that than it is. It does, however, say:

The value of at most one of the members can be stored in a union object at any time.

(C17 6.7.2.1/16)

In your particular example, neither mine.m16 nor mine.m32 has a value stored in it at the time of the call, but under any circumstance, at most one of them could have a value. When func then tries to read the values stored in those objects the results are not defined (because they don't actually have values stored in them).

That interpretation is supported by the inclusion in the spec of paragraph 6.5.2.3/6:

One special guarantee is made in order to simplify the use of unions: if a union contains several structures that share a common initial sequence (see below), and if the union object currently contains one of these structures, it is permitted to inspect the common initial part of any of them anywhere that a declaration of the completed type of the union is visible.

No such special provision would be needed if it were generally ok to access random union members regardless of which one actually had a value stored.


My questions:

  • Is type punning with arrays like this valid?

Not like that, no. There are other, variations on array type-punning that are allowed by the spec.


  • What exactly am I violating by passing the pointers into func?

The call itself does not violate anything. It is legal to take the address of a union member, even one that does not currently have a value stored in it, and it is legal to pass the resulting pointer values to functions. But when called with those arguments, the function commits strict-aliasing violations when it attempts to dereference one or both pointers, as described above.


  • What other gotchas am I missing in this example?

Contrary to one of your other answers, the code presented does not run afoul of paragraph 6.5.16.1/3. The value being stored in *y is not read from overlapping object *x, but rather is the sum of that value with the original value of *y. That sum is computed, not read from an object, so 6.5.16.1/3 does not apply. But you may be missing that it would violate 6.5.16.1/3 if func() performed a simple assignment instead of a plussignment.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
  • “The value being stored in *y is not read from overlapping object” is an overly literal interpretation. A purpose of this rule is to allow processing objects in subunits, such as implementing multibyte integers with instructions. So a C implementation can implement `a=b` by reading one byte at a time, writing it, and repeating until done. Applying the rule only to direct assignment of a value would be useless, as a C implementation that needed this for `a=b` would also need it for `a=b+c`. Also, that interpretation means `a=+b` is defined even when `a=b` is not, since the former is “computed.” – Eric Postpischil Mar 15 '22 at 22:15
  • @EricPostpischil, I can speak only to what is actually written in the specification, which I know you can appreciate. If the spec meant to be read as you describe then it could have said so with not too many more words, but it does not. Example: "if the expression for the value being stored reads the value of an object that overlaps in any way the storage of the first object ...". For its part, the C99 Rationale document does not suggest a broader interpretation: its example is assigning the value of one union member to another member to effect a type conversion. – John Bollinger Mar 15 '22 at 22:31