[TUHS] printf (was: python)

Alejandro Colomar alx.manpages at gmail.com
Sat Aug 5 02:57:50 AEST 2023

Hello Dan,

On 2023-08-04 18:06, Dan Cross wrote:
> On Thu, Aug 3, 2023 at 7:55 PM Alejandro Colomar <alx.manpages at gmail.com> wrote:
>> On 2023-08-03 23:29, Dan Cross wrote:
>>> On Thu, Aug 3, 2023 at 2:05 PM Alejandro Colomar <alx.manpages at gmail.com> wrote:
>>>> -  It is type-safe, with the right tools.
>>> No it's not, and it really can't be. True, there are linters that can
>>> try to match up types _if_ the format string is a constant and all the
>>> arguments are known at e.g. compile time, but C permits one to
>>> construct the format string at run time (or just select between a
>>> bunch of variants); the language gives you no tools to enforce type
>>> safety in a meaningful way once you do that.
>> Isn't a variable format string a security vulnerability?  Where do you
>> need it?
> It _can_ be a security vulnerability, but it doesn't necessarily
> _need_ to be. If one is careful in how one constructs it, such things
> can be very safe indeed.
> As to where one needs it, there are examples like `vsyslog()`,

I guessed you'd mention v*() formatting functions, as that's the only
case where a variable format string is indeed necessary (or kind of).

I'll simplify your example to vwarnx(3), from the BSDs, which does less
job, but has a similar API regarding our discussion.

I'm not sure if you meant vsyslog() uses or its implementation, but
I'll cover both (but for vwarnx(3)).


This function (and all v*() functions) will be used to implement a
wrapper variadic function, like for example warnx(3).  It's there, in
the variadic function, where the string /must be/ a literal, and where
the arguments are checked.  There's never a good reason to use a
non-literal there (AFAIK), and there are compiler warnings and linters
to enforce that.  Since those args have been previously checked, you
should just pass the va_list pristine to other formatting functions.
Then, as long as libc doesn't have bugs, you're fine.

In the implementation of a v*() function:

Do /not/ touch the va_list.  Just pass it to the next function.  Of
course, in the end, libc will have to iterate over it and do the job,
but that's not the typical programmer's problem.  Here's the libbsd
implementation of vwarnx(3), which does exactly that: no messing with
the va_list.

$ grepc vwarnx
void vwarnx(const char *format, va_list ap)
	__printflike(1, 0);

vwarnx(const char *format, va_list ap)
	fprintf(stderr, "%s: ", getprogname());
	if (format)
		vfprintf(stderr, format, ap);
	fprintf(stderr, "\n");

Just put a [[gnu::format(printf)]] in the outermost wrapper, which
should be using a string literal, and you'll be fine.

> but
> that's almost besides the point, which is that given that you _can_ do
> things like that, the language can't really save you by type-checking
> the arguments to printf; and once varargs are in the mix? Forget about
> it.

Not really.  You can do that _only_ if you really want.  If you want to
not be able, you can "drop privileges" by adding a few flags to your
compiler, such as -Werror=format-security -Werror=format-nonliteral,
and add a bunch of linters to your build system for more redundancy,
and voila, your project is now safe.


>         - Dan C.

More information about the TUHS mailing list