Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Initializer with constant expression having possible overflow in C99

Is this valid C99 code? If so, does it define an implementation-defined behavior?

int a;
unsigned long b[] = {(unsigned long)&a+1};

From my understanding of the C99 standard, from §6.6 in the ISO C99 standard, this might be valid:

  1. An integer constant expression shall have integer type and shall only have operands that are integer constants (...) Cast operators in an integer constant expression shall only convert arithmetic types to integer types, except as part of an operand to the sizeof operator.

  2. More latitude is permitted for constant expressions in initializers. Such a constant expression shall be, or evaluate to, one of the following:

    • an arithmetic constant expression,
    • (...)
    • an address constant for an object type plus or minus an integer constant expression.

However, because there is the possibility of the addition overflowing, this might not be considered a constant expression and therefore not valid C99 code.

Could someone please confirm if my reasoning is correct?

Note that both GCC and Clang accept this code without warnings, even when using -std=c99 -pedantic. However, when casting to unsigned int instead of unsigned long, that is, using the following code:

int a;
unsigned long b[] = {(unsigned int)&a+1};

Then both compilers complain that the expression is not a compile-time constant.

like image 325
anol Avatar asked Apr 15 '15 13:04

anol


3 Answers

From this clang developers thread on a similar issue: Function pointer is compile-time constant when cast to long but not int? the rationale is that the standard does not require the compiler to support this(this scenario is not included in any of bullets in 6.6p7) and although it is allowed to support this supporting truncated addresses would be burdensome:

I assume that sizeof(int) < sizeof(void(*)()) == sizeof(long) on your target. The problem is that the tool chain almost certainly can't express a truncated address as a relocation.

C only requires the implementation to support initializer values that are either (1) constant binary data, (2) the address of some object, or (3) or an offset added to the address of some object. We're allowed, but not required, to support more esoteric things like subtracting two addresses or multiplying an address by a constant or, as in your case, truncating the top bits of an address away. That kind of calculation would require support from the entire tool chain from assembler to loader, including various file formats along the way. That support generally doesn't exist.

Your case, which is casting a pointer to a integer type does not fit any of the cases under 6.6 paragraph 7:

More latitude is permitted for constant expressions in initializers. Such a constant expression shall be, or evaluate to, one of the following:

  • an arithmetic constant expression,
  • anull pointer constant,
  • an address constant, or
  • an address constant for an object type plus or minus an integer constant expression.

but as mentioned in the post compiler are allowed to support other forms of constant expression:

An implementation may accept other forms of constant expressions.

but neither clang nor gcc accept this.

like image 71
Shafik Yaghmour Avatar answered Nov 09 '22 15:11

Shafik Yaghmour


This code is not required to be accepted by a conforming implementation. You quoted the relevant passage in your question:

  1. More latitude is permitted for constant expressions in initializers. Such a constant expression shall be, or evaluate to, one of the following:
    • an arithmetic constant expression,
    • a null pointer constant,
    • an address constant, or
    • an address constant for an object type plus or minus an integer constant expression.

(unsigned long)&x is none of those things. It's not an arithmetic constant because of C11 6.6/8:

Cast operators in an arithmetic constant expression shall only convert arithmetic types to arithmetic types

(pointer types are not arithmetic types, 6.2.5/18); and it is not an address constant because all address constants are pointers (6.6/9). Finally a pointer plus or minus an ICE is another pointer, so it is not that either.


However 6.6/10 says that an implementation may accept other forms of constant expressions. I'm not sure whether this means the original code should be called ill-formed or not (ill-formed code requires a diagnostic). Clearly your compiler is accepting some other constant expressions here.


The next issue is that casting from pointer to integer is implementation-defined. It may also be undefined if there is no integer representation corresponding to the particular pointer. (6.3.2.3/6)

Finally, the + 1 on the end makes no difference. unsigned long arithmetic is well-defined on addition and subtraction, so it is OK if and only if (unsigned long)&x is OK.

like image 42
M.M Avatar answered Nov 09 '22 14:11

M.M


First of all, your initializer is not necessarily a constant expression. If a has local scope, then it is assigned an address in run-time, when it gets pushed on the stack. C11 6.6/7 says that in order for a pointer to be a constant expression, it has to be an address constant, which is defined in 6.6/9 as:

An address constant is a null pointer, a pointer to an lvalue designating an object of static storage duration, or a pointer to a function designator; it shall be created explicitly using the unary & operator or an integer constant cast to pointer type, or implicitly by the use of an expression of array or function type.

(Emphasis mine)


As for whether your code is standard C, yes it is. Pointer conversions to integers are allowed, although they may come with various forms of poorly specified behavior. Specified in 6.5/6:

Any pointer type may be converted to an integer type. Except as previously specified, the result is implementation-defined. If the result cannot be represented in the integer type, the behavior is undefined. The result need not be in the range of values of any integer type.

To safely ensure that the pointer can fit into the integer, you need to use uintptr_t. But I don't think pointer to integer conversion was the reason you posted this question.


Regarding whether an integer overflow would prevent it from being a compile time constant, I'm not sure where you got that idea from. I don't believe your reasoning is correct, for example (INT_MAX + INT_MAX) is a compile time constant and it is also guaranteed to overflow. (GCC gives you a warning.) In case it overflows, it will invoke undefined behavior.


As for why you get errors about the expression not being a compile-time constant, I don't know. I can't reproduce it on gcc 4.9.1. I tried declaring a with both static and automatic storage duration but no difference.

Sounds like you somehow accidentally compiled as C90, in which case gcc will tell you "error: initializer element is not computable at load time". Or maybe there was a compiler bug which has been fixed in my version of gcc.

like image 34
Lundin Avatar answered Nov 09 '22 15:11

Lundin