Since you asked on a computer science site, I'll give you a computer science answer. This might not be the most directly helpful from a programmer's point of view, though understanding this will definitely make you a better programmer.
The first argument to the printf
function is a char *
, i.e. a pointer to a byte¹. However, not all pointers to bytes are valid arguments to printf
. The type system of C is not precise enough to express the type of the argument.
It's well-known that the first argument to printf
must be a string. A string, in C, is a zone of memory (i.e. a region of bytes with contiguous addresses) which is not empty and where the last byte has the value 0. The string is designated through a pointer to its first byte. A string is isomorphic to an array of nonzero bytes.
A null pointer, or a pointer value that does not point to a valid object, are not valid first arguments to printf
. C does not offer any guarantees when a program is invalid. If you pass an invalid argument to printf
(or any other function), **the behavior is undefined--. That means the behavior of the program is at the whim of the implementation. It may cause your program to crash. Or to print nonsense. Or to make demons fly out of your nose². Or to execute some code which was specially chosen by the attacker who specified the first argument — in a security context, undefined behavior is typically whatever the attacker has decided it to be (that's what exploit writing is all about).
Not all strings are valid first arguments to printf
. The most obvious restriction is that the string must be a format string. In a format string, the character³ %
has a special meaning. For example, "%B"
and "%0s"
are not valid format strings in standard C. A string which isn't a well-formed format string isn't a valid first argument to printf
.
It gets worse. Not all valid format strings are valid first arguments to printf
. For example, the call printf("%s")
is not well-formed: the format specifier %s
consumes an argument, but there is none; the behavior of this call is undefined. In practice, in many implementations, printf
will grab the value at the top of the stack and assume that it's a valid string; this may crash, print out confidential information, print out data that whatever is reading the output of the program won't understand, or any combination thereof.
The type of printf
is really a dependent type: the number and type of subsequent arguments depends on the value of the first argument. This is very far beyond the capabilities of C's type system⁴. Like just about everywhere in C, the onus is on the programmer to write a correct program.
Conceptually, a call like printf(buffer)
is wrong, because a buffer is an arbitrary string, whereas the argument to printf
must be a printf format. All printf formats are strings (the type of formats is a subtype of the type of strings), but not all strings are printf formats. The call printf(buffer)
is only valid if the string buffer
happens to be a printf format with zero arguments. This means no format specified other than %%
, in standard C; in other words, the valid values of buffer
in printf(buffer)
are the strings where every %
is followed by another %
.
If you have a string lying around, how do you know whether it's a valid printf format? If the string is a literal written syntactically within the printf
argument list, then it is very easy for the programmer or a code reviewer to check whether the format is correct. Any good compiler can check the format string and report a compilation error if it is incorrect, too. If the string comes from a variable, verifying that it is correct is a lot more difficult.
The correctness of the printf
call becomes more and more difficult to ascertain as the value of the format string is specified further and further from the point of use. This is where trust comes into play. In order for the program to behave as desired, the format string must be valid for the call. When you claim that your program behaves as desired, you rely on the format string being valid — in other words, you trust the format string. This is only valid if you trust the entity where the format string originates to provide a valid format string. If the format string comes from a source that is external to your program, that source is often untrusted. Even when it is trusted, tracing that source is a heavy burden, which makes relying on such an external source bad engineering practice.
As a parting piece, why is printf("%s", buffer)
valid? The %s
format specifier expects a string, so this call is valid if and only if buffer
is a valid string. This is as safe as it gets in C.
¹ It's in fact a little more complicated, because char
isn't exactly a byte, but for our purposes here it's close enough.
² Sadly, many computers lack the hardware device that would make this very educational behavior possible.
³ By “character”, I mean the byte whose interpretation in the execution character set is the %
(percent) character.
⁴ There are languages where printf
is provided and has a type that describes its behavior. The obvious way to do it is in dynamically typed languages like Lisp, Perl, etc., where the behavior of printf
if the arguments are not well-formed is to signal an error. OCaml typechecks printf
statically; it requires that the format string be known at the compile time, as the compiler parses it and deduces the type of the remaining arguments.