Ok, this is UB, calling a function pointer through a different type than its definition is a pretty clear example of UB. The problem here is that there's a confusion between "void * and char * are implicitly convertible in C" and "void * and char * are the same". The latter is true for many platforms (especially older ones) but not all (I think there were platforms where functionally they had `typedef char void`). There's a side note of conflating "this has defined behavior on my platform that is stable and works for me" and "it's not UB if it's stable and works on a platform", just like integer overflow is UB despite being entirely defined behavior on every platform under the sun.

Anyway, if folk are curious there are many platforms where not only can the representation of `void()(void)` and `void()(char)` be different - even if pointing to the same function - but the representation of even just the data pointers void* and char* may not be the same, again while pointing to the same memory.

For example, on platforms with pointer authentication function pointers are generally (I would say "always" but in principle it can be avoided) signed, and in some configuration the type of the function is incorporated into the function. Calling the function pointer requires authenticating the pointer, and authenticating the pointer requires that the call site agrees on the type of the pointer because otherwise the signature fails.

Absent actual pointer auth hardware you could imagine someone implementing this as some kind of monstrosity like this (very hypothetical, unpleasant, and footgun heavy) horror:

#define SIGN_FPTR(fptr) (typeof(fptr))(((uintptr_t)fptr)|(MAGIC_HASH(stringify(typeof(fptr)) << some_number_of_bits))
#define AUTH_FPTR(fptr) (typeof(fptr))(((uintptr_t)fptr)^(MAGIC_HASH(stringify(typeof(fptr)) << some_number_of_bits))

and you can immediately see that if you had code that used these but disagreed on the type of the function you'd have a bad time. With compiler+hardware pointer auth this is just handled transparently.

In principle a pointer auth environment could apply this type discrimination logic to data pointers as well, but I'm unaware of any that do so implicitly. But if a platform did do so, then the incorrect type of the parameter would mean you would fail inside the function, if you were able to call it (say if you weren't using a function pointer, but had mistyped the prototype).

Similarly, in other environments, the pointer may be directly aware the type being referenced, and I believe that CHERI supports this, in which case even if you could call the function pointer when attempting to read the pointer I believe it would fail.

Having got here, you might be saying "but hang on C says void* and char* are the same", and we go all the way back to my first sentence where I said "are implicitly convertible" :D

On plenty of systems casting from one pointer type to another is an entirely source level feature and once lowered is completely invisible. But in the environments we're discussing

(Type1*)pointerToType2

Under pointer auth it requires re-signing the pointer (you have to auth the original value to verify it, and then sign the result according to the new schema), and under CHERI I believe there are instructions for controlling how a pointer is tagged.

But the important thing is the C does not say they are the same thing, just that the conversion is automatic, just like numbers and bools, or numbers and bools in JS, or numbers and strings in JS, or objects and strings in JS, or nothing and strings in JS, or .... :D