7

In these two examples, does accessing members of the struct by offsetting pointers from other members result in Undefined / Unspecified / Implementation Defined Behavior?

struct {
  int a;
  int b;
} foo1 = {0, 0};

(&foo1.a)[1] = 1;
printf("%d", foo1.b);


struct {
  int arr[1];
  int b;
} foo2 = {{0}, 0};

foo2.arr[1] = 1;
printf("%d", foo2.b);

Paragraph 14 of C11 § 6.7.2.1 seems to indicate that this should be implementation-defined:

Each non-bit-field member of a structure or union object is aligned in an implementation-defined manner appropriate to its type.

and later goes on to say:

There may be unnamed padding within a structure object, but not at its beginning.

However, code like the following appears to be fairly common:

union {
  int arr[2];
  struct {
    int a;
    int b;
  };
} foo3 = {{0, 0}};

foo3.arr[1] = 1;
printf("%d", foo3.b);

(&foo3.a)[1] = 2; // appears to be illegal despite foo3.arr == &foo3.a
printf("%d", foo3.b);

The standard appears to guarantee that foo3.arr is the same as &foo3.a, and it doesn't make sense that referring to it one way is legal and the other not, but equally it doesn't make sense that adding the outer union with the array should suddenly make (&foo3.a)[1] legal.

My reasoning for thinking the first examples must also therefore be legal:

  1. foo3.arr is guaranteed to be the same as &foo.a
  2. foo3.arr + 1 and &foo3.b point to the same memory location
  3. &foo3.a + 1 and &foo3.b must therefore point to the same memory location (from 1 and 2)
  4. struct layouts are required to be consistent, so &foo1.a and &foo1.b should be laid out exactly the same as &foo3.a and &foo3.b
  5. &foo1.a + 1 and &foo1.b must therefore point to the same memory location (from 3 and 4)

I've come across some outside sources that suggest that both the foo3.arr[1] and (&foo3.a)[1] examples are illegal, however I haven't been able to find a concrete statement in the standard that would make it so. Even if they were both illegal though, it's also possible to construct the same scenario with flexible array pointers which, as far as I can tell, does have standard-defined behavior.

union {
  struct {
    int x;
    int arr[];
  };
  struct {
    int y;
    int a;
    int b;
  };
} foo4;

The original application is considering whether or not a buffer overflow from one struct field into another is strictly speaking defined by the standard:

struct {
  char buffer[8];
  char overflow[8];
} buf;
strcpy(buf.buffer, "Hello world!");
println(buf.overflow);

I would expect this to output "rld!" on nearly any real-world compiler, but is this behavior guaranteed by the standard, or is it an undefined or implementation-defined behavior?

AJMansfield
  • 4,039
  • 3
  • 29
  • 50
  • @M.M The reason for the second part is the idea that the supposed validity of the union code seems to imply that the first sample should be valid as well. I guess it might make sense to split off another question asking just about the validity of the union code though. – AJMansfield Aug 08 '18 at 03:31
  • @AJMansfield There's no such implication in the standard; unions have special rules – M.M Aug 08 '18 at 03:35
  • 1
    `foo.arr[1] = 1;` is UB, No spec that the next member is `foo.arr[1]`. – chux - Reinstate Monica Aug 08 '18 at 04:39
  • @chux that objection can be fixed with an `assert`; or by noting that it's legal to write to padding bytes – M.M Aug 08 '18 at 05:34
  • @M.M Legal to write to padding. Hmmm. I doubt writing any bit pattern to padding is OK. Some times that is where check bits are hidden. Perhaps a good question. – chux - Reinstate Monica Aug 08 '18 at 05:56

2 Answers2

10

Introduction: The standard is inadequate in this area, and there is decades of history of argument on this topic and strict aliasing with no convincing resolution or proposal to fix.

This answer reflects my view rather than any imposition of the Standard.


Firstly: it's generally agreed that the code in your first code sample is undefined behaviour due to accessing outside the bounds of an array via direct pointer arithmetic.

The rule is C11 6.5.6/8 . It says that indexing from a pointer must remain within "the array object" (or one past the end). It doesn't say which array object but it is generally agreed that in the case int *p = &foo.a; then "the array object" is foo.a, and not any larger object of which foo.a is a subobject.

Relevant links: one, two.


Secondly: it's generally agreed that both of your union examples are correct. The standard explicitly says that any member of a union may be read; and whatever the contents of the relevant memory location are are interpreted as the type of the union member being read.


You suggest that the union being correct implies that the first code should be correct too, but it does not. The issue is not with specifying the memory location read; the issue is with how we arrived at the expression specifying that memory location.

Even though we know that &foo.a + 1 and &foo.b are the same memory address, it's valid to access an int through the second and not valid to access an int through the first.

It's generally agreed that you can access the int by computing its address in other ways that don't break the 6.5.6/8 rule, e.g.:

((int *)((char *)&foo + offsetof(foo, b))[0]

or

((int *)((uintptr_t)&foo.a + sizeof(int)))[0]

Relevant links: one, two


It's not generally agreed on whether ((int *)&foo)[1] is valid. Some say it's basically the same as your first code, since the standard says "a pointer to an object, suitably converted, points to the element's first object". Others say it's basically the same as my (char *) example above because it follows from the specification of pointer casting. A few even claim it's a strict aliasing violation because it aliases a struct as an array.

Maybe relevant is N2090 - Pointer provenance proposal. This does not directly address the issue, and doesn't propose a repeal of 6.5.6/8.

M.M
  • 138,810
  • 21
  • 208
  • 365
  • 1
    Good answer, I'd just like to add that the C committee has recently created a "memory model working group" to discuss this kind of questions and to come up with a more conclusive model for C2x. – Jens Gustedt Aug 08 '18 at 06:52
  • @JensGustedt Cool, looking forward to seeing what they do come up with :) – M.M Aug 08 '18 at 07:05
  • The reasoning for thinking the first examples are correct is that since `foo.arr` is guaranteed to be the same as `&foo.a`, and `foo.arr + 1` points to the same location as `&foo.b`, then replacing `foo.arr` with `&foo.a`, as in `&foo.a + 1`, should also be guaranteed to point to the same location as `&foo.b`; and that since taking the union of the inner struct with the array shouldn't change the memory layout of the struct, this expression should also be legal without it. I'll edit my question to make the chain of reasoning more explicit. – AJMansfield Aug 08 '18 at 16:47
  • 3
    Until C99, it was pretty much universally recognized that compilers intended for various purposes would need to support type usage patterns that went beyond those the Standard mandated for all compilers, and support for such patterns was recognized as a Quality of Implementation issue. Limitations that would be appropriate in a compiler intended or configured solely for high-end number crunching would make a compiler useless for processing low-level memory management code. Absent a willingness to recognize different kinds of implementations, any attempt to settle on a single set of rules... – supercat Aug 08 '18 at 21:14
  • 3
    ...for everything would be pretty much guaranteed to break a lot of code while needlessly impairing many optimizations. The fact that one part of the Standard *allows* a compiler to treat in arbitrary fashion an action whose behavior would otherwise be defined elsewhere does not imply any judgment that quality compilers intended for any particular purpose *should* do so. – supercat Aug 08 '18 at 21:20
2

According to C11 draft N1570 6.5p7, an attempt to access the stored value of a struct or union object using anything other than an lvalue of character type, the struct or union type, or a containing struct or union type, invokes UB even if behavior would otherwise be fully described by other parts of the Standard. This section contains no provision that would allow an lvalue of a non-character member type (or any non-character numeric type, for that matter) to be used to access the stored value of a struct or union.

According to the published Rationale document, however, the authors of the Standard recognized that different implementations offered different behavioral guarantees in cases where the Standard imposed no requirements, and regarded such "popular extensions" as a good and useful thing. They judged that questions of when and how such extensions should be supported would be better answered by the marketplace than by the Committee. While it may seem weird that the Standard would allow an obtuse compiler to ignore the possibility that someStruct.array[i] might affect the stored value of someStruct, the authors of the Standard recognized that any compiler whose authors aren't deliberately obtuse will support such a construct whether the Standard mandates or not, and that any attempt to mandate any kind of useful behavior from obtusely-designed compilers would be futile.

Thus, a compiler's level of support for essentially anything having to do with structures or unions is a quality-of-implementation issue. Compiler writers who are focused on being compatible with a wide range of programs will support a wide range of constructs. Those which are focused on maximizing the performance of code that needs only those constructs without which the language would be totally useless, will support a much narrower set. The Standard, however, is devoid of guidance on such issues.

PS--Compilers that are configured to be compatible with MSVC-style volatile semantics will interpret that qualifier as a indicating that an access to the pointer may have side-effects that interact with objects whose address has been taken and that aren't guarded by restrict, whether or not there is any other reason to expect such a possibility. Use of such a qualifier when accessing storage in "unusual" ways may make it more obvious to human readers that the code is doing something "weird" at the same time as it will thus ensure compatibility with any compiler that uses such semantics, even if such compiler would not otherwise recognize that access pattern. Unfortunately, some compiler writers refuse to support such semantics at anything other than optimization level 0 except with programs that demand it using non-standard syntax.

supercat
  • 77,689
  • 9
  • 166
  • 211