3

I analyzed some source code written in C and found the following code snippet:

#include <stdio.h>

struct base_args_t {
int a0;
};

struct int_args_t {
struct base_args_t base;
int a1;
};

struct uint_args_t {
struct base_args_t base;
unsigned int a1;
};

void print_int(struct int_args_t *a)
{
    // print int
    printf("%i\n", a->a1);
    return;
}

void print_uint(struct uint_args_t *a)
{
    // print unsigned int
    printf("%u\n", a->a1);
    return;
}

int main()
{
    struct uint_args_t uint_args = {.a1 = 7};
    typedef void (*f_print_type)(struct int_args_t *);
    void (*print)(struct int_args_t *a) = (f_print_type)print_uint;

    print((void *)&uint_args);

    return 0;
}

I am wondering if it is permissible to cast a function to a pointer to a function of various kinds, as is done in the example:

void (*print)(struct int_args_t *a) = (f_print_type)print_uint;

P.S. Moreover, I've tested this example with enabled CFI sanitizer and it says:

runtime error: control flow integrity check for type 'void (struct int_args_t *)' failed during indirect function call

but it’s hard to say whether it is 100% correct.

lol lol
  • 319
  • 3
  • 18

3 Answers3

4

A call on a function pointer has to made using the same type as the function type.

In main() the statement print(...); calls print_uint using void (*print)(struct int_args_t *a) but print_uint is of type void print_uint(struct uint_args_t *a). The call is undefined behavior.

Is it legal to cast a function to a pointer to a function of various kinds per C standard?

if it is permissible to cast a function to a pointer to a function of various kinds [...]

The conversion or cast is always safe, when the other type is a function pointer. Any function pointer may be always converted to any another functions pointer type. You have to call the function with the same function pointer type as it is (more exact, the function has to be called with a compatible function pointer type).

KamilCuk
  • 120,984
  • 8
  • 59
  • 111
  • So, you mean that the code from the question is of undefined behavior even if the memory layout of "struct int_args_t" is identical to "struct uint_args_t". Am I right? – lol lol Jul 03 '20 at 14:53
  • Yes, it's undefined behavior. – KamilCuk Jul 03 '20 at 14:54
  • Re “A call on a function pointer has to made using the same type as the function type”: The rules are laxer than that. They allow for some slippage between prototypes and non-prototypes, default promotions, and stuff. And they are complicated to explain for general situations. – Eric Postpischil Jul 03 '20 at 14:56
  • @KamilCuk If it is really UB, could you please prove this by quotation of C standard? Aren't "int_args_t" and "uint_args_t" of compatible types? – lol lol Jul 03 '20 at 15:17
  • No, two distinct structures are not compatible types. The other question I linked has the quotation for functions pointer calls. – KamilCuk Jul 03 '20 at 15:25
  • 1
    @lollol: For two structures, `struct int_args_t` and `struct uint_args_t`, to be compatible, they must both be declared with the same tag (or no tag), must have corresponding members in the same order with the same names compatible types and alignment specifiers, with some allowances for incomplete declarations and such, per C 2018 6.2.7 1. Compatible does not mean “I think they are laid out the same in memory.” Compatible means they obey particular rules in the C standard about being effectively the same type, where the effects involve semantics and other issues. – Eric Postpischil Jul 03 '20 at 15:25
  • @lollol: C 2018 6.5.2.2 6: “… If the function is defined with a type that includes a prototype, and either … or the types of the arguments after promotion are not compatible with the types of the parameters, the behavior is undefined…” `print_uint` is defined with a prototype, its parameter type is `struct uint_args_t *`, and you call it with a `struct int_args_t *`, which is incompatible, and therefore the behavior is undefined. Except 6.2.5 28 says pointers to structures have the same representations as each other, and footnote 49 tells us this means they are meant to be interchangeable. – Eric Postpischil Jul 03 '20 at 15:36
  • (A note for the above: In `print((void *)&uint_args);`, the `void *` argument `(void *)&uint_args` is automatically converted to the parameter type, so the actual argument passed as type `struct int_args_t *` even though originates as a `struct uint_args_t *`.) – Eric Postpischil Jul 03 '20 at 15:42
  • Given 6.2.5 28 and footnote 49, the fact that pointers to structures are supposed to be interchangeable may mean that passing a parameter of type `struct int_args_t *` does not violate the rule that an argument type must be compatible with the pointer type. Clearly passing a `struct uint_args_t *` would not violate the rule (it is the same type as the parameter and so is compatible), and we are supposed to be able to change one for the other, so passing a `struct int_args_t *` should give the same behavior. – Eric Postpischil Jul 03 '20 at 15:42
  • @Eric Postpischil It looks very complicated. – lol lol Jul 03 '20 at 15:43
  • @EricPostpischil Do you mean that if I remove "(void *)" cast from "print((void *)&uint_args);" the code becomes of defined behavior? – lol lol Jul 03 '20 at 15:45
  • @lollol: Without the cast, the compiler will warn that the types are incompatible. Aside from that, if you compile in spite of that, and the compiler continues with the normal conversion of the argument to the parameter type, the resulting behavior requirements will be the same with or without the cast. Whether it is undefined or not depends on how you interpret 6.2.5 28 and footnote 49. If you interpret the interchangeability intent broadly (it allows passing any type of structure pointer for any other), the behavior is defined. If you interpret it narrowly, the behavior is undefined. – Eric Postpischil Jul 03 '20 at 15:51
  • @EricPostpischil [Ref](https://stackoverflow.com/questions/62717335/is-it-legal-to-cast-a-function-to-a-pointer-to-a-function-of-various-kinds-per-c/62717785#comment110911136_62717533) For that they must also be "declared in separate translation units", which is not the case here: "*Moreover, two structure, union, or enumerated types **declared in separate translation units** are compatible if their tags and members satisfy the following requirements:*" - 6.7.2/1 – RobertS supports Monica Cellio Jul 03 '20 at 16:50
  • 1
    @RobertSsupportsMonicaCellio: Yes, which means it is even stricter within a translation unit: This rule providing compatibility for declarations in different units does not apply, so the only rule for structures is that “their types must be the same.” – Eric Postpischil Jul 03 '20 at 16:56
2

Is it legal to cast a pointer to a function to a pointer to a function of various kinds per C standard?

Yes, You can assign a function pointer to any other kind of function pointer:

"A pointer to a function of one type may be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer. If a converted pointer is used to call a function whose type is not compatible with the pointed-to type, the behavior is undefined."

Source: C11, 6.3.2.3/8

So the assignment:

void (*print)(struct int_args_t *a) = (f_print_type)print_uint;

is correct and legal.


What invokes undefined behavior is using the pointer print() to refer to call print_uint:

print((void *)&uint_args);

because:

"If a converted pointer is used to call a function whose type is not compatible with the pointed-to type, the behavior is undefined."

print_uint of type

"function with struct uint_args_t parameter returning void"

is not compatible to type

"function with struct int_args_t parameter returning void", which print is declared to point to.

The type of the parameter and the called pointer is different.

The structures themselves are not identical nor compatible.


Regarding compatibility:

For two function types to be compatible, both shall specify compatible return types127.

Moreover, the parameter type lists, if both are present, shall agree in the number of parameters and in use of the ellipsis terminator; corresponding parameters shall have compatible types. If one type has a parameter type list and the other type is specified by a function declarator that is not part of a function definition and that contains an empty identifier list, the parameter list shall not have an ellipsis terminator and the type of each parameter shall be compatible with the type that results from the application of the default argument promotions. If one type has a parameter type list and the other type is specified by a function definition that contains a (possibly empty) identifier list, both shall agree in the number of parameters, and the type of each prototype parameter shall be compatible with the type that results from the application of the default argument promotions to the type of the corresponding identifier. (In the determination of type compatibility and of a composite type, each parameter declared with function or array type is taken as having the adjusted type and each parameter declared with qualified type is taken as having the unqualified version of its declared type.)

  1. If both function types are ‘‘old style’’, parameter types are not compared.

Source: C18, §6.7.6.3/15


Two types have compatible type if their types are the same. Additional rules for determining whether two types are compatible are described in 6.7.2 for type specifiers, in 6.7.3 for type qualifiers, and in 6.7.6 for declarators.56)

56)Two types need not be identical to be compatible.

Source: C18, §6.2.7/1


EXAMPLE 2 After the declarations

typedef structs1 { int x; } t1, *tp1;
typedef structs2 { int x; } t2, *tp2;

type t1and the type pointed to by tp1 are compatible. Type t1 is also compatible with type structs1, but not compatible with the types structs2, t2, the type pointed to by tp2, or int.

C18, §6.7.8/5

Two structures of different tags are never compatible, even if they would have the same set of members and alignment, which isn't the case here too since the type of the member a is different between the two structure types.

0

In this specific case, it's completely safe, because:

  1. int_args_t and uint_args_t are identical, memory layout-wise. And specifically, int and uint are identical (there's no such thing as signed/unsigned registers or memory locations).

  2. Even if 1 wasn't true, the two function definitions have an identical signature -- they receive a pointer and return void.

  3. The function bodies are also identical on an assembly level, since you use the same field at the same offset from the pointer you receive, and the field you're using has the same memory layout (as discussed in 1).

If you strip it all down to the basic assembly code generated by the compiler, your code is perfectly safe. What the sanitizer is telling you is that the actual C types you defined aren't as interchangeable, but that doesn't matter in the end.

ryyker
  • 22,849
  • 3
  • 43
  • 87
Blindy
  • 65,249
  • 10
  • 91
  • 131
  • About #3, the string argument passed to `printf` is obviously different between the two functions, but I meant the assembly instructions themselves are identical, not their arguments. – Blindy Jul 03 '20 at 14:40
  • 1
    The question is tagged language-lawyer, meaning an answer should be based on the formal specification of the language. Reasoning about what the assembly language would be is inherently wrong for a language-lawyer question. (I see the tags were edited, but it looks like language-lawyer was on the question when this answer was posted.) – Eric Postpischil Jul 03 '20 at 14:58
  • The formal specification of the language allows what he did, as evidenced by the fact that it both compiles and runs, and my explanation describes why it runs. I don't see a problem with my answer, though of course you're free to downvote if you wish. In fact, it's more correct than Kamil's answer which claims you have to call a function with the same pointer type it expects (which is just plain wrong, the entirety of COM cries at you). – Blindy Jul 03 '20 at 15:03
  • 2
    Using the formal specification of the language means reasoning from what the C standard says. Reasoning from “it both compiles and runs” is wrong. Reasoning from Microsoft’s Component Object Model is wrong. Neither the fact that a program compiles and runs with a compiler you tried nor the statements of Microsoft’s COM are proof that the C standard says the program must behave the same way. The question of a language-lawyer tag is **what does the language standard say**, not what does a compiler do or what does Microsoft say. – Eric Postpischil Jul 03 '20 at 15:21
  • *The formal specification of the language allows what he did, as evidenced by the fact that it both compiles and runs* "Compil[ing] and run[ning]" are ***NOT*** evidence of compliance with C language specifications. *(which is just plain wrong, the entirety of COM cries at you)* Oooof. An implementation is free to leverage specifics of that implementation. That does not mean your code that uses that implementation to compile and run is not violating something in the language specification if it does the same. – Andrew Henle Jul 03 '20 at 15:21
  • Maybe I didn't make myself clear, it compiles and runs as written in OP *in any C compiler implementation*, precisely because of what I described in my answer. There are no specificities involved at all. Though with all this language lawyer talk, I'm still waiting for a quote from the specification that proves me wrong, and I'm more and more disappointed with every new comment. – Blindy Jul 03 '20 at 15:30
  • 1. is irrelevant; it only asserts the structures are identical in memory layout. This is insufficient to prove they are compatible, as the rules for structure compatibility in C 2018 6.2.7 1 are not based on memory compatibility. In fact, even if the **tag names** of two structures differ, they are not compatible, even if their definitions are otherwise completely identical. Then, because the structures are not compatible, 6.5.2.2 6 says the behavior of calling a function defined to take a parameter of type `struct uint_args_t *` with a parameter of type `struct int_args_t *` is not defined. – Eric Postpischil Jul 03 '20 at 15:56
  • 2. and 3. are similarly irrelevant, as they do not derive from rules in the C standard. Because of 6.5.2.2 6, a C implementation is licensed by the standard to treat mismatched parameter/argument types has not having defined behavior, and it may, during compilation and optimization, replace or eliminate this code as desired, regardless of how one might wish it were implemented in assembly language. – Eric Postpischil Jul 03 '20 at 15:58
  • What would it eliminate? The function is referenced by an address-of operator, stored in a variable and called, it can't be eliminated, and the `struct` definition is just that, a `struct`, a description of memory layout. What's there to be eliminated? – Blindy Jul 03 '20 at 16:03
  • Whenever C code has the form `if (X) foo; else bar;` and the compiler can deduce that `foo` contains undefined behavior, the compiler is permitted by the C standard to replace `if (X) foo; else bar;` by `bar`. Similarly, if the body of `main` contains undefined behavior, the compiler is permitted by the C standard to eliminate it completely or to replace it as desired. The compiler may replace it with `return 0;` or `return EXIT_FAILURE;` or `abort();` or `{}` or may refuse to compile it. In other words, what there is to eliminate is everything. The compiler may remove the code completely. – Eric Postpischil Jul 03 '20 at 16:58
  • 1
    Once again, the question is not whether any such compilers exist. A language-lawyer tag asks to use the formal specification of the language. The **only** correct way to answer the question is to base it on what the C standard says. What you think compilers will or will not do is irrelevant. That is simply not the question asked. The question is “What does the C standard say about this?” Period. – Eric Postpischil Jul 03 '20 at 18:32