6

I have a repeated print that prints a bunch of non null-terminated char*'s like so:

    int len1, len2, len3, len4;
    char *str1, *str2, *str3, *str4;
    get_vals(message, val1, &str1, &len1);
    get_vals(message, val2, &str2, &len2);
    get_vals(message, val3, &str3, &len3);
    get_vals(message, val4, &str4, &len4);
    printf("%.*s %.*s %.*s %.*s", len1, str1, len2, str2, len3, str3, len4, str4);

where get_vals sets the char * pointer to the value in memory and sets the len to the length of the text. It is possible for the passed in char * to be set to NULL, and if so, the length field will be set to 0. It seems this print has happened many times and not segfaulted, I assume due to the fact that the length specifier is 0 and so there is probably no dereferencing. However, is this always safe? Perhaps it is OS or libc version dependent? Is it worth doing a safety check like so:

    printf("%.*s %.*s %.*s %.*s",
           len1, str1 ? str1 : "",
           len2, str2 ? str2 : "",
           len3, str3 ? str3 : "",
           len4, str4 ? str4 : "");
Machavity
  • 30,841
  • 27
  • 92
  • 100
someone serious
  • 197
  • 1
  • 1
  • 8
  • 4
    Standard does not mention this case so you have to assume that it is UB. Your implementation does not derefence it but it is not guaranteed. – 0___________ Dec 27 '21 at 19:41
  • 1
    Though I don't know the answer to your question I would recommend creating a macro like so: `#define SAFE_STR(S) (S ? S : "")` – Lev M. Dec 27 '21 at 20:18
  • 1
    MSVC outputs "warning C4477: 'printf' : format string '%.*s' requires an argument of type 'char *', but variadic argument 2 has type 'void *'". – Weather Vane Dec 27 '21 at 20:25
  • 2
    It's perfectly fine, when implementing `printf`, to just call `strlen` on the argument to `%s`, independently of precision. It shouldn't be an invalid pointer. – KamilCuk Dec 28 '21 at 02:48
  • @KamilCuk: That should be an answer. – Eric Postpischil Jan 04 '22 at 00:45
  • The title doesn't match the question ; `NULL` is a macro which may expand to various things, while the sample code in the question shows the argument as a variable of type `char *` . (I see from the edit history that someone jumped in and edited the title which invalidates existing answers, and a mod has rolled it back --- so am leaving this comment here as a warning to readers!) – M.M Feb 08 '22 at 03:50

2 Answers2

6

When you write printf("%.*s", len1, str1), where len1 is zero and str1 is a null pointer, you are using a s specifier and setting the precision to 0. I looked through the relevant parts of section 7.21.6 of N1570. When documenting the s specifier, it says:

the argument shall be a pointer to the initial element of an array of character type. Characters from the array are written up to (but not including) the terminating null character. If the precision is specified, no more than that many bytes are written.

So, technically, just looking at the first part of that quote, you do need to provide a pointer to an array instead of providing a null pointer. So your code is not following that part of the standard.

However, you set your precision to 0, so the second part of the quote tells us that the printf function is not actually going to write any characters from that array to the output. This implies to me that it won't try to read any characters either: reading past the end of the array is unsafe so printf implementations should not do that. So your code will probably work in practice and it's hard to imagine a case where it would fail. The biggest problem I can think of in practice is that static analyzers or validators might complain about your code.

David Grayson
  • 84,103
  • 24
  • 152
  • 189
  • 1
    A case where it might fail is when there are other arguments to the function. This [answer](https://stackoverflow.com/a/1241314/4142924) states "*[Pointers] are not guaranteed to be the same size.*" If so, they might not be aligned/positioned as expected, which is why we cast a pointer to `(void*)` when passing to `%p`. – Weather Vane Dec 27 '21 at 20:32
  • You're misrepresenting the width part of the `printf` specification. It's the *minimum* width, not the guaranteed width. The pointer will definitely be dereferenced, leading to undefined behavior. – Mark Ransom Jan 04 '22 at 00:26
  • 2
    It's the *precision*, not the *width*, because a period was used before the number. I also quoted the sentence in the standard that says how the precision works, and the standard makes it clear it's a maximum width. – David Grayson Jan 04 '22 at 00:32
  • 1
    OK, I'll grant you that one - I was confused because I've *never* seen a precision specified on the `s` format. But that still doesn't guarantee that the pointer isn't dereferenced, and dereferencing a null pointer is undefined behavior no matter where or how it's done. – Mark Ransom Jan 04 '22 at 01:29
  • "it's hard to imagine a case where it would fail." - I could imagine an implementation testing the argument corresponding to `%s` against `NULL` and aborting or taking some other unexpected action if so – M.M Feb 08 '22 at 03:55
3

Is it guaranteed by the C standard to be safe to do printf("%.*s", 0, NULL)?

No, not quite.

NULL is a implementation-defined null pointer constant. Its type may be void *, int, long, long long, unsigned, and a few other integer types. It is likely to be the same size and ... parameter compatible with char * or void*, but is not specified as such. Passing NULL to match a "%s" incurs a risk of undefined behavior.

Better to ask:

Is it guaranteed by the C standard to be safe to do printf("%.*s", 0, (char *) NULL)?

In this case, the answer by @David Grayson well applies: Technical undefined behavior, but often OK. [Although I do not abide by "This implies to me that it won't try to read any characters" as library implementations do not need to follow the rules of user code - they cheat.]


For high portability, do not use printf("%.*s", 0, NULL) for these 2 reasons.

Alternative:

printf("%.*s", 0, "");
chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
  • 1
    Note that in spite of the title, OP’s argument has type `char *`; they are passing some variable such as `str1`, not literally `NULL`. The title was just an attempt at abbreviating the question. – Eric Postpischil Jan 04 '22 at 00:39
  • @EricPostpischil The tile was not certainly "just an attempt", but a key aspect of using `printf()` that can fail code. Rather than unliterally changing that key aspect of the question and nullifying this upvoted answer after 7 days, such a change should have been promptly posed to the OP. Recommend a roll-back. – chux - Reinstate Monica Jan 04 '22 at 00:57
  • 1
    The code in the question makes clear the situation the OP is facing, and it has nothing to do with `NULL`. – Eric Postpischil Jan 04 '22 at 01:46