In both C (n1570 7.21.6.1/10) and C++ (by inclusion of the C standard library) it is undefined behavior to provide an argument to printf whose type does not match its conversion specification. A simple example:
printf("%d", 1.9)
The format string specifies an int, while the argument is a floating point type.
This question is inspired by the question of a user who encountered legacy code with an abundance of conversion mismatches which apparently did no harm, cf. undefined behaviour in theory and in practice.
Declaring a mere format mismatch UB seems drastic at first. It is clear that the output can be wrong, depending on things like the exact mismatch, argument types, endianness, possibly stack layout and other issues. This extends, as one commentator there pointed out, also to subsequent (or even previous?) arguments. But that is far from general UB. Personally, I never encountered anything else but the expected wrong output.
To venture a guess, I would exclude alignment issues. What I can imagine is that providing a format string which makes printf expect large data together with small actual arguments possibly lets printf
read beyond the stack, but I lack deeper insight in the var args mechanism and specific printf implementation details to verify that.
I had a quick look at the printf sources, but they are pretty opaque to the casual reader.
Therefore my question: What are the specific dangers of mis-matching conversion specifiers and arguments in printf
which make it UB?
printf
only works as described by the standard if you use it correctly. If you use it incorrectly, the behaviour is undefined. Why should the standard define what happens when you use it wrong?Concretely, on some architectures floating point arguments are passed in different registers to integer arguments, so inside
printf
when it tries to find anint
matching the format specifier it will find garbage in the corresponding register. Since those details are outside the scope of the standard there is no way to deal with that kind of misbehaviour except to say it's undefined.For an example of how badly it could go wrong, using a format specifier of
"%p"
but passing a floating point type could mean thatprintf
tries to read a pointer from a register or stack location which hasn't been set to a valid value and could contain a trap representation, which would cause the program to abort.Any
printf
format/argument mismatch will cause erroneous output, so you cannot rely on anything once you do that. It is hard to tell which will have dire consequences beyond garbage output because it depends completely no the specifics of the platform you are compiling for and the actual details of theprintf
implementation.Passing invalid arguments to a
printf
instance that has a%s
format can cause invalid pointers to be dereferenced. But invalid arguments for simpler types such asint
ordouble
can cause alignment errors with similar consequences.I'll start by asking you to be aware of the fact that
long
is 64-bit for 64-bit versions of OS X, Linux, the BSD clones, and various Unix flavors if you aren't already aware. 64-bit Windows, however, keptlong
as 32-bit.What does this have to do with
printf()
and UB with respect to its conversion specifications?Internally,
printf()
will use theva_arg()
macro. If you use%ld
on 64-bit Linux and only pass anint
, the other 32 bits will be retrieved from adjacent memory. If you use%d
and pass along
on 64-bit Linux, the other 32 bits will still be on the argument stack. In other words, the conversion specification indicates the type (int
,long
, whatever) tova_arg()
, and the size of the corresponding type determines the number of bytes by whichva_arg()
adjusts its argument pointer. Whereas it will just work on Windows sincesizeof(int)==sizeof(long)
, porting it to another 64-bit platform can cause trouble, especially when you have aint *nptr;
and try to use%ld
with*nptr
. If you don't have access to the adjacent memory, you'll likely get a segfault. So the possible concrete cases are:long
andint
are the same, so it just worksI'm not sure if alignment is an issue on some platforms, but if it is, it would depend upon the implementation of passing function parameters. Some "intelligent" compiler-specific
printf()
with a short argument list might bypassva_arg()
altogether and represent the passed data as a string of bytes rather than working with a stack. If that happened,printf("%x %lx\n", LONG_MAX, INT_MIN);
has three possibilities:long
andint
are the same, so it just worksffffffff ffffffff80000000
is printedAs for why the C standard says that it causes undefined behavior, it doesn't specify exactly how
va_arg()
works, how function parameters are passed and represented in memory, or the explicit sizes ofint
,long
, or other primitive data types because it doesn't unnecessarily constrain implementations. As a result, whatever happens is something the C standard cannot predict. Just looking at the examples above should be an indication of that fact, and I can't imagine what else other implementations exist that might behave differently altogether.Some compilers may implement variable-format arguments in a way that allows the types of arguments to be validated; since having a program trap on incorrect usage may be better than possibly having it output seemingly-valid-but-wrong information, some platforms may choose to do that.
Because the behavior of traps is outside the realm of the C Standard, any action which might plausibly trap is classified as invoking Undefined Behavior.
Note that the possibility of implementations trapping based on incorrect formatting means that behavior is considered undefined even in cases where the expected type and the actual passed type have the same representation, except that signed and unsigned numbers of the same rank are interchangeable if the values they hold are within the range which is common to both [i.e. if a "long" holds 23, it may be output with "%lX" but not with "%X" even if "int" and "long" are the same size].
Note also that the C89 committee introduced a rule by fiat, which remains to this day, which states that even if "int" and "long" have the same format, the code:
invokes Undefined Behavior since it causes information which was written as type "long" to be read as type "int" (behavior would also be Undefined if it was type "unsigned int"). Since a "%X" format specifier would cause data to be read as type "unsigned int", passing the data as type "long" would almost certainly cause the data to be stored somewhere as "long" but subsequently read as type "unsigned int", such behavior would almost likely violate the aforementioned rule.
Just to take your example: suppose that your architecture's procedure call standard says that floating-point arguments are passed in floating-point registers. But
printf
thinks you are passing an integer, because of the%d
format specifier. So it expects an argument on the call stack, which isn't there. Now anything can happen.