By using flexible array members (FAMs) within structure types, are we exposing our programs to the possibility of undefined behavior?
Is it possible for a program to use FAMs and still be a strictly conforming program?
Is the offset of the flexible array member required to be at the end of the struct?
The questions apply to both C99 (TC3)
and C11 (TC1)
.
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
int main(void) {
struct s {
size_t len;
char pad;
int array[];
};
struct s *s = malloc(sizeof *s + sizeof *s->array);
printf("sizeof *s: %zu\n", sizeof *s);
printf("offsetof(struct s, array): %zu\n", offsetof(struct s, array));
s->array[0] = 0;
s->len = 1;
printf("%d\n", s->array[0]);
free(s);
return 0;
}
Output:
sizeof *s: 16
offsetof(struct s, array): 12
0
If one allows for a strictly conforming program to make use of Implementation-Defined behavior in cases where it would "work" with all legitimate behaviors (notwithstanding that almost any kind of useful output would depend upon Implementation-Defined details such as the execution character set), use of flexible array members within a strictly-conforming program should be possible provided that the program does not care whether the offset of the flexible array member coincides with the length of the structure.
Arrays are not regarded as having any padding internally, so any padding that gets added because of the FAM would precede it. If there is enough space either within or beyond a structure to accommodate members in an FAM, those members are part of the FAM. For example, given:
the size of "foo" may be padded out beyond the start of
z
because of alignment, but any such padding will be usable as part ofz
. Writingy
might disturb padding that precedesz
, but should not disturb any part ofz
itself.This is a long answer dealing extensively with a tricky topic.
TL;DR
I disagree with the analysis by Dror K.
The key problem is a misunderstanding of what the §6.2.1 ¶6 in the C99 and C11 standards means, and inappropriately applying it to a simple integer assignment such as:
This assignment is not allowed to change any padding bytes in the structure pointed at by
fam_ptr
. Consequently, the analysis based on the presumption that this can change padding bytes in the structure is erroneous.Background
In principle, I'm not dreadfully concerned about the C99 standard and its corrigenda; they're not the current standard. However, the evolution of the specification of flexible array members is informative.
The C99 standard — ISO/IEC 9899:1999 — had 3 technical corrigenda:
It was TC3, for example, that stated that
gets()
was obsolescent and deprecated, leading to it being removed from the C11 standard.The C11 standard — ISO/IEC 9899:2011 — has one technical corrigendum, but that simply sets the value of two macros accidentally left in the form
201ymmL
— the values required for__STDC_VERSION__
and__STDC_LIB_EXT1__
were corrected to the value201112L
. (You can see the TC1 — formally "ISO/IEC 9899:2011/Cor.1:2012(en) Information technology — Programming languages — C TECHNICAL CORRIGENDUM 1" — at https://www.iso.org/obp/ui/#iso:std:iso-iec:9899:ed-3:v1:cor:1:v1:en. I've not worked out how you get a download of it, but it is so simple that it really doesn't matter very much.C99 standard on flexible array members
ISO/IEC 9899:1999 (before TC2) §6.7.2.1 ¶16:
(This footnote is removed in the rewrite.) The original C99 standard included one example:
Some of this example material was removed in C11. The change was not noted (and did not need to be noted) in TC2 because the examples are not normative. But the rewritten material in C11 is informative when studied.
N983 paper identifying a problem with flexible array members
N983 from the WG14 Pre-Santa Cruz-2002 mailing is, I believe, the initial statement of a defect report. It states that some C compilers (citing three) manage to put a FAM before the padding at the end of a structure. The final defect report was DR 282.
As I understand it, this report led to the change in TC2, though I have not traced all the steps in the process. It appears that the DR is no longer available separately.
TC2 used the wording found in the C11 standard in the normative material.
C11 standard on flexible array members
So, what does the C11 standard have to say about flexible array members?
This firmly positions the FAM at the end of the structure — 'the last member' is by definition at the end of the structure, and this is confirmed by:
This paragraph contains the change in ¶20 of ISO/IEC 9899:1999/Cor.2:2004(E) — the TC2 for C99;
The data at the end of the main part of a structure containing a flexible array member is regular trailing padding that can occur with any structure type. Such padding can't be accessed legitimately, but can be passed to library functions etc via pointers to the structure without incurring undefined behaviour.
The C11 standard contains three examples, but the first and third are related to anonymous structures and unions rather than the mechanics of flexible array members. Remember, examples are not 'normative', but they are illustrative.
Note that this changed between C99 and C11.
Another part of the standard describes this copying behaviour:
Illustrating the problematic FAM structure
In the C chat room, I wrote some information of which this is a paraphrase:
Consider:
Assuming double requires 8-byte alignment (or 4-byte; it doesn't matter too much, but I'll stick with 8), then
struct non_fam1a { double d; char c; };
would have 7 padding bytes afterc
and a size of 16. Further,struct non_fam1b { double d; char c; char nonfam[4]; };
would have 3 bytes padding after thenonfam
array, and a size of 16.The suggestion is that the start of
fam
instruct fam1
can be at offset 9, even thoughsizeof(struct fam1)
is 16. So that the bytes afterc
are not padding (necessarily).So, for a small enough FAM, the size of the struct plus FAM might still be less than size of
struct fam
.The prototypical allocation is:
when the FAM is of type
char
(as instruct fam1
). That's a (gross) over-estimate when the offset of fam is less thansizeof(struct fam1)
.Dror K. pointed out:
Addressing the question
The question asks:
I believe that if you code correctly, the answers are "No", "Yes", "No and Yes, depending …".
Question 1
I am assuming that the intent of question 1 is "must your program inevitably be exposed to undefined behaviour if you use any FAM anywhere?" To state what I think is obvious: there are lots of ways of exposing a program to undefined behaviour (and some of those ways involve structures with flexible array members).
I do not think that simply using a FAM means that the program automatically has (invokes, is exposed to) undefined behaviour.
Question 2
Section §4 Conformance defines:
I don't think there are any features of standard C which, if used in the way that the standard intends, makes the program not strictly conforming. If there are any such, they are related to locale-dependent behaviour. The behaviour of FAM code is not inherently locale-dependent.
I do not think that the use of a FAM inherently means that the program is not strictly conforming.
Question 3
I think question 3 is ambiguous between:
The answer to 3A is "No" (witness the C11 example at ¶25, quoted above).
The answer to 3B is "Yes" (witness §6.7.2.1 ¶15, quoted above).
Dissenting from Dror's answer
I need to quote the C standard and Dror's answer. I'll use
[DK]
to indicate the start of a quote from Dror's answer, and unmarked quotations are from the C standard.As of 2017-07-01 18:00 -08:00, the short answer by Dror K said:
I'm not convinced that simply using a FAM means that the program automatically has undefined behaviour.
I'm not convinced that the use of a FAM automatically renders a program not strictly conforming.
This is the answer to my interpretation 3A, and I agree with this.
The long answer contains interpretation of the short answers above.
I agree with this analysis.
I agree that the new specification removed the requirement that the FAM be stored at an offset greater than or equal to the size of the structure.
I don't agree that there is a problem with the padding bytes.
The standard explicitly says that structure assignment for a structure containing a FAM effectively ignores the FAM (§6.7.2.1 ¶18). It must copy the non-FAM members. It is explicitly stated that padding bytes need not be copied at all (§6.2.6.1 ¶6 and footnote 51). And the Example 2 explicitly states (non-normatively §6.7.2.1 ¶25) that if the FAM overlaps the space defined by the structure, the data from the part of the FAM that overlaps with the end of the structure might or might not be copied.
I don't see this as a problem. Any expectation that you can copy a structure containing a FAM using structure assignment and have the FAM array copied is is inherently flawed — the copy leaves the FAM data logically uncopied. Any program that depends on the FAM data within the scope of the structure is broken; that is a property of the (flawed) program, not the standard.
Ideally, of course, the code would set the named member
pad
to a determinate value, but that doesn't cause actually cause a problem since it is never accessed.I emphatically disagree that the value of
s->array[0]
in theprintf()
is indeterminate; its value is123
.The prior standard quote is (it is the same §6.2.6.1 ¶6 in both C99 and C11, though the footnote number is 42 in C99 and 51 in C11):
Note that
s->len
is not an assignment to an object of structure or union type; it is an assignment to an object of typesize_t
. I think this may be the main source of confusion here.If the code included:
then the value printed is indeed indeterminate. However, that's because copying a structure with a FAM is not guaranteed to copy the FAM. More nearly correct code would be (assuming you add
#include <string.h>
, of course):Now the value printed is determinate (it is
123
). Note that the condition onif (sizeof *s > offsetof(struct s, array))
is immaterial to my analysis.Since the rest of the long answer (mainly the section identified by the heading 'undefined behavior') is based on a false inference about the possibility of the padding bytes of a structure changing when assigning to an integer member of a structure, the rest of the discussion does not need further analysis.
This is based on a false premise; the conclusion is false.
The Short Answer
Yes. Common conventions of using FAMs expose our programs to the possibility of undefined behavior. Having said that, I'm unaware of any existing conforming implementation that would misbehave.
Possible, but unlikely. Even if we don't actually reach undefined behavior, we are still likely to fail strict conformance.
No. The offset of the FAM is not required to be at the end of the struct, it may overlay any trailing padding bytes.
The answers apply to both
C99 (TC3)
andC11 (TC1)
.The Long Answer
FAMs were first introduced in C99 (TC0) (Dec 1999), and their original specification required the offset of the FAM to be at the end of the struct. The original specification was well-defined and as such couldn't lead to undefined behavior or be an issue with regards to strict conformance.
C99 (TC0) §6.7.2.1 p16
(Dec 1999)The problem was that common C99 implementations, such as GCC, didn't follow the requirement of the standard, and allowed the FAM to overlay any trailing padding bytes. Their approach was considered to be more efficient, and since for them to follow the requirement of the standard- would result with breaking backwards compatibility, the committee chose to change the specification, and as of C99 TC2 (Nov 2004) the standard no longer required the offset of the FAM to be at the end of the struct.
C99 (TC2) §6.7.2.1 p16
(Nov 2004)The new specification removed the statement that required the offset of the FAM to be at the end of the struct, and it introduced a very unfortunate consequence, because the standard gives the implementation the liberty not to keep the values of any padding bytes within structures or unions in a consistent state. More specifically:
C99 (TC3) §6.2.6.1 p6
This means that if any of our FAM elements correspond to (or overlay) any trailing padding bytes, upon storing to a member of the struct- they (may) take unspecified values. We don't even need to ponder whether this applies to a value stored to the FAM itself, even the strict interpretation that this only applies to members other than the FAM, is damaging enough.
Once we store to a member of the struct, the padding bytes take unspecified bytes, and therefore any assumption made about the values of the FAM elements that correspond to any trailing padding bytes, is now false. Which means that any assumption leads to us failing strict conformance.
Undefined behavior
Although the values of the padding bytes are "unspecified values", the same can't be said about the type being affected by them, because an object representation which is based on unspecified values can generate a trap representation. So the only standard term which describes these two possibilities would be "indeterminate value". If the type of the FAM happens to have trap representations, then accessing it is not just a concern of an unspecified value, but undefined behavior.
But wait, there's more. If we agree that the only standard term to describe such value is as being an "indeterminate value", then even if the type of the FAM happens not to have trap representations, we've reached undefined behavior, since the official interpretation of the C standards committee is that passing indeterminate values to standard library functions is undefined behavior.