0

I'd like to write a asnprintf function -- which is a wrapper around snprintf, but it mallocs the string according to its output size. Unfortunately when I compile I get a warning (promoted to error on my system) format string is not a string literal [-Werror,-Wformat-nonliteral].

I looked up the warning and apparently there are security concerns with passing a non-literal to printf functions, but in my case, I need to take in a format pointer, and pass that on.

Is there a good way around this that does not expose the same security vulnerability?

My function as is is as follows:

int
asnprintf(char **strp, int max_len, const char *fmt, ...)
{
 int len;
 va_list ap,ap2;
 va_start(ap, fmt);
 va_copy(ap2, ap);
 len = vsnprintf(NULL, 0, fmt, ap);
 if ( len > max_len)
 len = max_len;
 *strp = malloc(len+1);
 if (*strp == NULL)
 return -1;
 len = vsnprintf(*strp, len+1, fmt, ap2);
 va_end(ap2);
 va_end(ap);
 return len;
}
asked Nov 16, 2022 at 18:56
16
  • You can implement this with fmemopen and/or open_memstream and do a single fprintf instead of reinventing it. Commented Nov 16, 2022 at 19:00
  • 1
    @CraigEstey either way that wouldn't resolve OP's issue, wince fprintf would still need a fmt that is not a string literal. OP's implementation is more or less the de-facto standard asnprintf implementation, nothing wrong with it. Commented Nov 16, 2022 at 19:04
  • 1
    This is a great example of why coding standards must have some provision for waivers. Commented Nov 16, 2022 at 19:05
  • 1
    Interesting that clang puts up a warning for this, considering that you can't check the arguments to the v*printf functions at compile time. The docs for gcc on this option specifically says that these functions are excluded regarding this warning. Commented Nov 16, 2022 at 19:21
  • 1
    @MarcoBonelli I've seen the double sprintf impl. I've even used it in an answer here. But, I must not be understanding the impact/purpose of -Wformat-nonliteral as doing v*printf calls are okay here [AFAICT]. But, with the v* versions, you can't check the args in ap anyway. Maybe asnprintf needs (e.g.) __attribute__((__format__(__printf__,3,4))) I've just compiled it semi-cleanly, but I had to change len = vsnprintf(strp, len + 1, fmt, ap2); into len = vsnprintf(*strp, len + 1, fmt, ap2); Commented Nov 16, 2022 at 19:22

2 Answers 2

3

If you are targeting only GCC and Clang as compilers, you can get around this pretty easily by temporarily disabling the warning for that specific function:

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
#pragma GCC diagnostic ignored "-Wformat-security"
// Function definition here...
#pragma GCC diagnostic pop

Clang should recognize #pragma GCC too. You may also need to ignore -Wformat-security as I did above depending on your compiler flags.

Godbolt link to working example with Clang 11.


I'm wondering if it's possible for example to require that my function take only string literals

As Craig Estey suggests above, you could make use of the format function attribute to make the compiler perform this check for you:

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
#pragma GCC diagnostic ignored "-Wformat-security"
int __attribute__((format(printf, 3, 4))) asnprintf(char **strp, int max_len, const char *fmt, ...) {
 // ... implementation ...
}
#pragma GCC diagnostic pop
char global_fmt[100];
int main(void) {
 char *res;
 asnprintf(&res, 100, "asd"); // will compile
 asnprintf(&res, 100, global_fmt); // will NOT compile
 return 0;
}

You could also do this, with a bit of trickery, using a macro and some compiler built-ins:

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
#pragma GCC diagnostic ignored "-Wformat-security"
int internal_asnprintf(char **strp, int max_len, const char *fmt, ...) {
 return printf(fmt);
}
#pragma GCC diagnostic pop
#define asnprintf(strp, maxlen, fmt, ...) ({ \
 _Static_assert(__builtin_constant_p(fmt), "format string is not a constant"); \
 internal_asnprintf(strp, maxlen, fmt, __VA_ARGS__); \
})
char global_fmt[100];
int main(void) {
 char *res;
 asnprintf(&res, 100, "asd"); // will compile
 asnprintf(&res, 100, global_fmt); // will NOT compile
 return 0;
}

Note that the above code makes use of statement expressions (({...})) which are a non-standard extension and may or may not be available depending on your compiler flags.

answered Nov 16, 2022 at 19:07
Sign up to request clarification or add additional context in comments.

Comments

1

From my top comments ...

Just add __attribute__((__format__(__printf__,3,4))) to your asnprintf declaration and/or definition.

This will cue the compiler to not complain.

And, the further benefit is that it will check the variadic arguments passed to asnprintf against the format string.

So:

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
// put this in a .h file!?
int __attribute__((__format__(__printf__,3,4)))
asnprintf(char **strp, int max_len, const char *fmt, ...);
int
asnprintf(char **strp, int max_len, const char *fmt, ...)
{
 int len;
 va_list ap, ap2;
 va_start(ap, fmt);
 va_copy(ap2, ap);
 len = vsnprintf(NULL, 0, fmt, ap);
 if (len > max_len)
 len = max_len;
 *strp = malloc(len + 1);
 if (*strp == NULL)
 return -1;
 len = vsnprintf(*strp, len + 1, fmt, ap2);
 va_end(ap2);
 va_end(ap);
 return len;
}
answered Nov 16, 2022 at 19:42

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.