Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unexpected snprintf behaviour

Tags:

c++

I noticied a (to my opinion) very strange behaviour of snprintf with c++ on several platforms. Consider the following code (minimum working example which causes the observed behaviour):

#include <stdio.h>

char test1[512];
char test2[512];
char test3[1024];
char test4[1024];

int main()
{
    snprintf(test1, sizeof(test1), "test1");
    snprintf(test2, sizeof(test2), "test2");
    snprintf(test3, sizeof(test3), "%s %s", test1, test2);
    return 0;
}

When running though valgrind with --tool=exp-sgcheck, the following error is reported (for the 3rd snprintf statement):

==30302== Invalid read of size 1
==30302==    at 0x568E4EB: vfprintf (in /lib64/libc-2.19.so)
==30302==    by 0x56B7608: vsnprintf (in /lib64/libc-2.19.so)
==30302==    by 0x5695209: snprintf (in /lib64/libc-2.19.so)
==30302==    by 0x4006AD: main (1.cc:12)
==30302==  Address 0x601460 expected vs actual:
==30302==  Expected: global array "test1" of size 1,024 in object with soname "NONE"
==30302==  Actual:   global array "test2" of size 512 in object with soname "NONE"
==30302==  Actual:   is 0 after Expected

So passing test1 as argument to the first %s leads to a read after the end of the test1 array.

This behaviour caused several page faults in a windows driver (yes I know it's static data...). Luckily the code is portable, and when ported to linux valgrind reported that error.

But to my knowledge, snprintf should terminate test1 with \0 at the 6th byte (which it does, checked that). So why causes the 3rd snprintf statement a read after the end of the test1 array? Changing the 3rd snprintf statement to

snprintf(test3, sizeof(test3), "%.512s %s", test1, test2);

solves the problem on both platforms. Compiling the code as C code (not C++) results in no error.

UPDATE: On linux (and maybe windows as well) the error only occurs, if the code is compiled with debug information included and optimizations disabled (-g -O0 for gcc).

like image 916
Johannes Avatar asked Jan 20 '15 13:01

Johannes


1 Answers

Since global objects (like the arrays in your example) are 0-initialized the last snprintf should never read beyond the string's end, independent of whether the previous sprintfs copied the terminating 0 char or not. The only explanation is that the previous snprintfs copied much more than the submitted "test1" to the target test1, overwriting all the 0s with non-0s (no 0s would btw be unlikely with random memory).

That is very unlikely -- such an obvious bug would have been found earlier. With respect to the error in the driver I would suspect that the memory is being overwritten by a completely unrelated "process" (in a general sense, maybe another driver). For a desktop application I have no explanation why it would fail. Trying your example on Codingground with gcc 4.8.3 was running just fine and printed the expected strings when I added a printf() at the end.

Btw, it's not surprising that the original code runs fine with optimizations enabled: Since there is no observable effect the compiler may emit just a NOP.

like image 85
Peter - Reinstate Monica Avatar answered Nov 10 '22 03:11

Peter - Reinstate Monica