10

Take this arguably questionable code.

struct X {
     int arr[1];
     float something_else;
};
    
struct X get_x(int first)
{
     struct X ret = { .arr = { first } };
     return ret;
}

int main(int argc, char **argv) {
    int *p = get_x(argc+50).arr;
    
    return *p;
}

get_x returns a struct X. I'm only interested in its member arr. Why would I make a local variable for the entire struct if I only want arr...

But.. is that code correct?

In the shown example, does the C standard know to keep the return value of get_x on the stack until the end of the calling stack frame because I'm peeking inside it with a pointer?

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
aganm
  • 1,245
  • 1
  • 11
  • 30
  • This is not related to language-lawyer, but is there a compiler which currently uses this fact to produce some code with a detectable UB? I tried with GCC 13 and Clang 16, both with sanitizers, and I couldn't trigger it. Just curious if in practice compilers extend the lifetime as is the case in C++. – anol Jul 26 '23 at 09:54

4 Answers4

16

What you're doing is not allowed by the standard.

The struct returned from the function has temporary lifetime which ends outside of the expression it is used in. So right after p is initialized, it points to an object whose lifetime has ended and its value becomes indeterminate. Then attempting to dereference p (which is now indeterminate) in the following statement triggers undefined behavior.

This is documented in section 6.2.4p8 of the C standard:

A non-lvalue expression with structure or union type, where the structure or union contains a member with array type (including, recursively, members of all contained structures and unions) refers to an object with automatic storage duration and temporary lifetime. Its lifetime begins when the expression is evaluated and its initial value is the value of the expression.
Its lifetime ends when the evaluation of the containing full expression or full declarator ends. Any attempt to modify an object with temporary lifetime results in undefined behavior.

Where the lifetime of an object and what happens to a pointer to an object when its lifetime ends is specified in section 6.2.4p2:

The lifetime of an object is the portion of program execution during which storage is guaranteed to be reserved for it. An object exists, has a constant address, and retains its last-stored value throughout its lifetime. If an object is referred to outside of its lifetime, the behavior is undefined. The value of a pointer becomes indeterminate when the object it points to (or just past) reaches the end of its lifetime

If you were to assign the return value of the function to an instance of struct X, then you can safely access the arr member of that instance.

dbush
  • 205,898
  • 23
  • 218
  • 273
  • 2
    It seems to me the struct is returned by value, which makes an entire copy of it into a temporary variable, making the OP's code valid and allowed by the standard. That would make the other answers right, and this statement ("What you're doing is not allowed by the standard.") wrong. – Gabriel Staples Mar 26 '21 at 03:40
  • 4
    @GabrielStaples The *lifetime* of that temporary ends after the expression in which it appears. So the following statement which uses `*p` is dereferencing a pointer to an object which no longer exists. – dbush Mar 26 '21 at 03:42
  • dbush, I see now. I agree with you. I have added an answer to provide further light and clarity: https://stackoverflow.com/a/66813744/4561887. – Gabriel Staples Mar 26 '21 at 08:59
  • 1
    Re “Then attempting to dereference p in the following statement accesses that object”: Actually, the pointer `p` becomes invalid when the lifetime ends, so we do not know where it does or does not point, so we do not know that it “accesses that object.” The behavior is undefined because C 2018 6.5.3.2 4 says applying `*` to an invalid value has undefined behavior, not because it accesses an object whose lifetime has ended. – Eric Postpischil Mar 26 '21 at 09:43
  • 1
    @EricPostpischil Good distinction there. Updated to reflect. – dbush Mar 26 '21 at 12:22
2

In the shown example, does the C standard know to keep the return value of get_x on the stack until the end of the calling stack frame because I'm peeking inside it with a pointer?

No, it cannot ever do this, even if it "knew" to do so. Things are popped off the stack when a function returns, and the contents of anything "above" that point become undefined.

Even so,

But.. is that code correct?

That part is! This is because you are not creating a pointer to the struct that was in the callee's stack frame. You are creating a pointer to a copy, which was implicitly created when you returned a struct by value.

Conceptually, the code will copy this struct into space reserved in the caller's stack frame (because you're specifically calling a function that returns a struct, in the general case the value can't be returned in a register). In practice, an optimizing compiler might return it in a register (if your machine's registers can fit a struct containing an int and a float), construct it directly in place in the caller's stack frame (the right location can easily be found as an offset from the base of the callee's stack frame), shuffle memory around (a destructive overlapping-move operation is acceptable exactly because of the "memory contents are now undefined" thing), etc.

... But only that part, as pointed out by @dbush. To create a copy properly (i.e., with a long enough lifetime to use this way), the return value from the function would need to be an lvalue. Conceptually, the compiler is allowed to pop that copy off the stack once it's done retrieving the .arr member. In practice, the stack pointer wouldn't get adjusted, but an optimizing compiler would consider that part of the stack free to use for other local variables.

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
  • Re “No, it cannot ever do this, even if it "knew" to do so. Things are popped off the stack when a function returns, and the contents of anything "above" that point become undefined.”: The first sentence is incorrect, and the purportedly supporting reasoning in the following sentences is invalid. Values of types too large to fit in one or a few processor registers are commonly returned by having the caller provide space for them and passing the address if the space to the called routine. So the calling function does have an instance of the type and could keep it to the end of its execution… – Eric Postpischil Mar 26 '21 at 16:46
  • … if we chose to make that part of the language semantics. – Eric Postpischil Mar 26 '21 at 16:47
1

Does the C standard keep the return struct of a function on the stack if I keep a pointer to a value inside it?

No, not if the struct is a NON l-value, meaning you have not stored it into a variable after it was returned from the function.

In the shown example, does the C standard know to keep the return value of get_x on the stack until the end of the calling stack frame because I'm peeking inside it with a pointer?

No. Read the C standard reference in @dbush's answer.

The problem isn't the get_x() function--that's all fine. Rather, in the erroneous code in the original question and in Example 1 below, the problem is simply the fact that the returned-by-value struct X (returned by get_x()) is NOT an l-value (assigned to a variable), so it is ephemeral, meaning its storage duration ends once the int *p = get_x(argc+50).arr; line is evaluated. Therefore, the *p in return *p is undefined behavior since it accesses memory for a struct X which was never stored into an l-value and therefore no longer exists. Examples 2 and 3 below, however, solve this problem and exhibit no undefined behavior, and are valid.

Example 1 (from the question; is undefined behavior):

Therefore, this is NOT legal:

int *p = get_x(argc+50).arr;
return *p;

See these warnings output by the clang 11.0.1 LLVM C++ compiler: https://godbolt.org/z/PajThdsxz :

<source>:15:14: warning: temporary whose address is used as 
value of local variable 'p' will be destroyed at the end of 
the full-expression [-Wdangling]
    int *p = get_x(argc+50).arr;
             ^~~~~~~~~~~~~~
1 warning generated.
ASM generation compiler returned: 0
<source>:15:14: warning: temporary whose address is used as 
value of local variable 'p' will be destroyed at the end of 
the full-expression [-Wdangling]
    int *p = get_x(argc+50).arr;
             ^~~~~~~~~~~~~~
1 warning generated.
Execution build compiler returned: 0
Program returned: 51

When using the clang 11.0.1 C compiler, however, no such warnings exist: https://godbolt.org/z/Y3zdszMvG. I don't know why.

Example 2 (ok):

But this is fine:

int p = get_x(argc+50).arr[0];
return p;

Example 3 (ok):

...and this is fine too:

struct X x = get_x(argc+50);
int *p = x.arr;
return *p;

Interestingly enough though,the compiled assembly generated by all 3 versions above is exactly identical (only when compiled in C++), indicating that while the first may be undefined, it works just as well as the other two for this particular compiler when compiled in C++. Here is the C++ assembly output:

get_x(int):                              # @get_x(int)
        mov     eax, edi
        ret
main:                                   # @main
        push    rax
        add     edi, 50
        call    get_x(int)
        pop     rcx
        ret

However, the C-compiler-generated assembly is different for all 3 cases, and significantly longer than the C++-compiler-generated assembly. See the last godbolt link just above to see for yourself.

It looks like the clang C++ compiler is significantly smarter than the clang C compiler.

Gabriel Staples
  • 36,492
  • 15
  • 194
  • 265
0

OP goals differ from code.

I'm only interested in its member arr. Why would I make a local variable for the entire struct if I only want arr... (?)

Member .arr is an array. So int *p = get_x(argc+50).arr; does not copy the array .arr, but copies the address of the .arr[0] into p. Copying the address does not fulfill "only interested in its member arr". If you want the data in .arr, copy the data.

To make a copy of only array .arr and not the entire struct X, use memcpy().

int my_copy[sizeof (struct X){0}.arr / sizeof *(struct X){0}.arr];
memcpy(my_copy, get_x(argc+50).arr, sizeof my_copy);

return my_copy[0];
Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256