3

From https://stackoverflow.com/a/13067917/156458

snprintf ... Writes the results to a character string buffer. (...) will be terminated with a null character, unless buf_size is zero.

So why does the following example from The Linux Programming Interface explicitly set the last character of a string (assigned by snprintf()) to be \0?

char *
inetAddressStr(const struct sockaddr *addr, socklen_t addrlen,
               char *addrStr, int addrStrLen)
{
    char host[NI_MAXHOST], service[NI_MAXSERV];
    if (getnameinfo(addr, addrlen, host, NI_MAXHOST,
                    service, NI_MAXSERV, NI_NUMERICSERV) == 0)
        snprintf(addrStr, addrStrLen, "(%s, %s)", host, service);
    else
        snprintf(addrStr, addrStrLen, "(?UNKNOWN?)");
    addrStr[addrStrLen - 1] = '\0';     /* Ensure result is null-terminated */
    return addrStr;
}
Tim
  • 1
  • 141
  • 372
  • 590
  • 1
    Check the bugs section in the man page. Quote: "Thus, the use of snprintf() with early libc4 leads to serious security problems". – Hans Passant Mar 03 '19 at 15:53
  • 1
    I recall man pages used to be vague about the functionality of snprintf, with descriptions like "The functions snprintf() and vsnprintf() do not write more than size bytes (including the terminating null byte ('\0'))". Since this doesn't actually say whether the string will be null-terminated or not when truncated, I assume the author of that code wanted to make extra sure. I believe I have done similar things in the past. – Johnny Johansson Mar 03 '19 at 16:07
  • @HansPassant and how is setting the last byte to 0 helping with libc4, which "contains an snprintf() equivalent to sprintf(), that is, **one that ignores the size argument**."? (completing the BUGS quote). –  Mar 03 '19 at 19:01

2 Answers2

3

Setting the last character of the target array to '\0' explicitly seems useless if snprintf conforms to the C Standard.

Note however that the Linux kernel uses its own version of the C library which may or may not conform to the C Standard. Most functions are older than the C99 standard where snprintf was first specified, which may explain implementation differences. The current version of the kernel's snprintf implementation does set the null terminator. The posted code seems to come from user code, not kernel code, so this is irrelevant.

Note also that some C libraries have non standard behavior. For example, Microsoft C runtime library did not support C99 extensions for almost 15 years after its publication and are still not fully conformant for various reasons. For example, snprintf has non-standard behavior for the %n conversion.

Note also that some Microsoft extensions with very similar names to the standard functions behave in surprising and confusing ways. For example _snprintf() will not set a null terminator in the target array if the output is truncated. see this documentation page for the gory details.

For the posted code, the C library is very unlikely to come from Microsoft, but the linux kernel may be used in systems with various C libraries: the GNU libc is used for most desktop distributions, but Android for example uses a different library. The author of the code fragment either:

  • does not fully understand the C Standard and wrote redundant code.
  • does not trust the implementation of the local C library and does not want to assume snprintf writes the null terminator.
  • uses defensive programming as a matter of habit and wrote redundant code, just in case.
chqrlie
  • 131,814
  • 10
  • 121
  • 189
  • the `snprintf` from the linux kernel does zero terminate its buffer; see the link from my answer. –  Mar 03 '19 at 20:12
  • @pizdelect: yes, correct. The code is either from an old kernel or was written a long time ago and not updated. – chqrlie Mar 03 '19 at 20:25
  • that is sample userland code (from a book), not kernel code -- notice the use of getnameinfo(3), which, if I'm not mistaken, postdates c99 and snprintf(). –  Mar 03 '19 at 20:28
  • @pizdelect: indeed, you are correct. Defensive programming I suppose. – chqrlie Mar 03 '19 at 20:29
1

That shouldn't be necessary with any proper snprintf implementation. However, on some systems, snprintf was a wrapper around some "similar" functions, which did funny things like not writing a NUL byte at the end or ignoring the length argument. That is not the case with the snprintf from any POSIX system, or the snprintf from the linux or freebsd kernels.

Also, that code is not checking the return value of snprintf (was it able to copy the whole string?) which for an example code, is pretty gross.


snprintf always terminates the string it copies to the buffer with a NUL byte, unless the length/size argument is 0, in which case it does not copy anything. Quoting from susv4[1]:

int snprintf(char *restrict s, size_t n, const char *restrict format, ...);

The snprintf() function shall be equivalent to sprintf(), with the addition of the n argument which states the size of the buffer referred to by s. If n is zero, nothing shall be written and s may be a null pointer. Otherwise, output bytes beyond the n-1st shall be discarded instead of being written to the array, and a null byte is written at the end of the bytes actually written into the array.

If copying takes place between objects that overlap as a result of a call to sprintf() or snprintf(), the results are undefined.

[1] the language in the C99 standard is identical