Compile-time consistency checks for types in C

July 6, 2015

Walkthrough of a compile-time consistency check for types in C language 

Compile-time

Compile-time consistency checks

Say that in existing source code, you happened upon the construct below:

#define CHECKED_TYPE(original_type, p) ((conversion_type*) (1 ? p : (original_type*) 0))

 

What might the purpose of this strange construct be?

 At run-time, the inner conditional expression always evaluates to p, and thus the entire expression evaluates to the conversion to conversion_type* of p. Thus, with regard to what it evaluates to, the macro might as well have been written (conversion_type*) p.

 At compile-time, however, a C11 compiler will apply the rules prescribed by the standard for the conditional expression:

6.5.15 Conditional operator

Syntax

1

conditional-expression: …
logical-OR-expression ? expression : conditional-expression

3 One of the following shall hold for the second and third operands:
— both operands have arithmetic type;
— both operands have the same structure or union type;
— both operands have void type;
— both operands are pointers to qualified or unqualified versions of compatible types;
— one operand is a pointer and the other is a null pointer constant; or
— one operand is a pointer to an object type and the other is a pointer to a qualified or unqualified version of void.

Because the macro hard-codes an expression with type original_type*, cases 1, 2, 3 will never apply. In practice neither the type pointed by p nor original_type is void, so the sixth case does not apply either. That leaves only cases 4 and 5: the pointer p must have compatible types up to qualifiers, or p must be a null pointer constant.

 Clause 6.5.15:3 above falls below a Constraints section, which means that a compliant compiler has to emit a diagnostic if a C program doesn’t respect the stated conditions. Thus the macro CHECKED_TYPE(original_type, p) expands to something that causes at least a compiler warning if p is not of a type compatible up to qualifiers with original_type*, or a null pointer constant (e.g. NULL, 0, (void*)0).

 In short, the macro CHECKED_TYPE(original_type, p) checks that the pointer p has type original_type* before converting it to the hard-coded type conversion_type*. If the check fails, compilation will likely stop (depending on the compiler, but a compliant compiler has to at least emit a warning). Clang uses an enabled-by-default warning:

$ cat t.c
#define CHECKED_TYPE(original_type, p) \
((void*) (1 ? p : (original_type*) 0))

int main(void) {
int x = 1;
void *p = CHECKED_TYPE(int, &x);
char y = 2;
void *q = CHECKED_TYPE(int, &y);
float z = 3;
void *r = CHECKED_TYPE(int, &z);
}

$ clang t.c
t.c:8:13: warning: pointer type mismatch ('char *' and 'int *') [-Wpointer-type-mismatch]
void *q = CHECKED_TYPE(int, &y);
^~~~~~~~~~~~~~~~~~~~~
t.c:2:15: note: expanded from macro 'CHECKED_TYPE'
((void*) (1 ? p : (original_type*) 0))
^ ~~~~~~~~~~~~~~~~~~
t.c:10:13: warning: pointer type mismatch ('float *' and 'int *') [-Wpointer-type-mismatch]
void *r = CHECKED_TYPE(int, &z);
^~~~~~~~~~~~~~~~~~~~~
t.c:2:15: note: expanded from macro 'CHECKED_TYPE'
((void*) (1 ? p : (original_type*) 0))
^ ~~~~~~~~~~~~~~~~~~
2 warnings generated.

 

In real software, this idiom is found in OpenSSL’s source code.

Newsletter

Contact us

Ensure your software is immune from vulnerabilities and does not crash whatever the input.

Contact Us