Hello Dan,
On 2023-08-04 18:06, Dan Cross wrote:
On Thu, Aug 3, 2023 at 7:55 PM Alejandro Colomar
<alx.manpages(a)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(a)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)).
Uses:
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
./include/bsd/err.h:63:
void vwarnx(const char *format, va_list ap)
__printflike(1, 0);
./src/err.c:97:
void
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.
Alex
- Dan C.