This post shows a potential bug in printing unsigned long long, which cost David, Leo and me a few days for debugging. One product got segv on cPSB lab – a platform based on MontaVista Linux and PowerPC CPU. Back trace showed that the segv fired in C lib function ‘snprintf’. As PowerPC’s assembly is not easy to read, we tried to reproduce this issue on x86 based system and succeeded. Below is the C source of the issued code and the x86 assembly code. Enjoy:)
/* Issued C source */
/home/daveti/Ctest: cat longlong.c
#include <stdio.h>
int main( void)
{
char tmpbuf[ 50];
unsigned long long count = 0;
strcpy( tmpbuf, “daveti”);
printf( “%dt%sn”, count, tmpbuf);
return 0;
}
/* Build the source */
/home/daveti/Ctest: gcc -o longlong -g2 -static longlong.c
/* Run the binary */
/home/daveti/Ctest: ./longlong
0 (null)
/* Get the assembly code */
/home/daveti/Ctest: objdump -S -C -d -g -T -t -x -f longlong > longlong.objdump
objdump: longlong: not a dynamic object
objdump: longlong: no recognized debugging information
/* Check the x86 assembly code */
080481d0 <main>:
#include <stdio.h>
int main( void)
{
80481d0: 55 push %ebp // save base pointer
80481d1: 89 e5 mov %esp,%ebp // update base pointer with currentl stack pointer
80481d3: 83 ec 58 sub $0x58,%esp // ‘grow’ 88 bytes in the stack
80481d6: 83 e4 f0 and $0xfffffff0,%esp // 4-byte alignment for stack pointer: ‘grow’ stack again – make sure stack pointe
r would NOT be like 0xXXXXXXX(odd number)
80481d9: b8 00 00 00 00 mov $0x0,%eax
80481de: 29 c4 sub %eax,%esp
char tmpbuf[ 50];
unsigned long long count = 0;
80481e0: c7 45 b0 00 00 00 00 movl $0x0,0xffffffb0(%ebp) // init low-4 bytes of count with zero
80481e7: c7 45 b4 00 00 00 00 movl $0x0,0xffffffb4(%ebp) // init high-4 bytes of count with zero
strcpy( tmpbuf, “daveti”);
80481ee: 83 ec 08 sub $0x8,%esp // ‘grow’ 8 bytes in the stack
80481f1: 68 e8 f7 08 08 push $0x808f7e8 // push address of “daveti”
80481f6: 8d 45 b8 lea 0xffffffb8(%ebp),%eax
80481f9: 50 push %eax // push address of tmpbuf
80481fa: e8 65 57 00 00 call 804d964 <strcpy>
80481ff: 83 c4 10 add $0x10,%esp // ‘degrow’ 16 bytes in the stack after calling strcpy
printf( “%dt%sn”, count, tmpbuf);
8048202: 8d 45 b8 lea 0xffffffb8(%ebp),%eax
8048205: 50 push %eax // push address of tmpbuf
8048206: ff 75 b4 pushl 0xffffffb4(%ebp) // push high-4 bytes of count
8048209: ff 75 b0 pushl 0xffffffb0(%ebp) // push low-4 bytes of count
804820c: 68 ef f7 08 08 push $0x808f7ef // push address of “%dt%sn”
8048211: e8 b2 06 00 00 call 80488c8 <_IO_printf> // BUG: then the first 3 parameters of printf would be “%dt%sn”, low-4 bytes of
count, high-4 bytes of count because “%d” only accepts 320-bit value – tmpbuf would never be called.
8048216: 83 c4 10 add $0x10,%esp
return 0;
8048219: b8 00 00 00 00 mov $0x0,%eax // return value 0
}
804821e: c9 leave
804821f: c3 ret
/* Fixed example with ‘count’=0xffffffff */
/home/daveti/Ctest: cat longlong2.c
#include <stdio.h>
int main( void)
{
char tmpbuf[ 50];
unsigned long long count = 0xffffffff;
strcpy( tmpbuf, “daveti”);
printf( “%llut%sn”, count, tmpbuf);
return 0;
}
/* Run the binary */
/home/daveti/Ctest: ./longlong2
4294967295 daveti
/* Check the x86 assembly code */
080481d0 <main>:
#include <stdio.h>
int main( void)
{
80481d0: 55 push %ebp
80481d1: 89 e5 mov %esp,%ebp
80481d3: 83 ec 58 sub $0x58,%esp
80481d6: 83 e4 f0 and $0xfffffff0,%esp
80481d9: b8 00 00 00 00 mov $0x0,%eax
80481de: 29 c4 sub %eax,%esp
char tmpbuf[ 50];
unsigned long long count = 0xffffffff;
80481e0: c7 45 b0 ff ff ff ff movl $0xffffffff,0xffffffb0(%ebp) // init low-4 byte of count with 0xffffffff
80481e7: c7 45 b4 00 00 00 00 movl $0x0,0xffffffb4(%ebp) // init high-4 byte of count with zero
strcpy( tmpbuf, “daveti”);
80481ee: 83 ec 08 sub $0x8,%esp
80481f1: 68 e8 f7 08 08 push $0x808f7e8
80481f6: 8d 45 b8 lea 0xffffffb8(%ebp),%eax
80481f9: 50 push %eax
80481fa: e8 65 57 00 00 call 804d964 <strcpy>
80481ff: 83 c4 10 add $0x10,%esp
printf( “%llut%sn”, count, tmpbuf);
8048202: 8d 45 b8 lea 0xffffffb8(%ebp),%eax
8048205: 50 push %eax
8048206: ff 75 b4 pushl 0xffffffb4(%ebp)
8048209: ff 75 b0 pushl 0xffffffb0(%ebp)
804820c: 68 ef f7 08 08 push $0x808f7ef
8048211: e8 b2 06 00 00 call 80488c8 <_IO_printf> // Then the 3 parameters of printf would be “%llut%sn”, count, tmpbuf be
cause “%llu” would instruct printf to get the 64-bit long value – tmpbuf would be finally passed then.
8048216: 83 c4 10 add $0x10,%esp
return 0;
8048219: b8 00 00 00 00 mov $0x0,%eax
}
804821e: c9 leave
804821f: c3 ret
Another format string to print unsigned long long may be “%I64u” besides “%llu”. However, based on David and my’s trial, it seems that either “%I64u” working or “%llu” or both depends on the compiler and CPU arch (32/64 bit, x86/PowerPC/Sparc). If our inspection is true, then we should avoid using unsigned long long in C programming – do we really need that so big number. On the other hand, let us get back to the original SegV happened on PowerPC. Though PowerPC uses registers to pass parameters of function calls instead of stack, the same case like x86 above could be imagined: when count is 0xffffffff and “%d%s” is format string, x86 (little endian) would take low-4 byte of count – 0xffffffff as the input for “%d” and high-4 byte of count – 0x00000000 as the input of “%s”, which is the same output as above. PowerPC (big endian), on the other hand, does it in the opposite way, which is passing the high-4 byte of count – 0x00000000 as the input for “%d” and low-4 bytes of count – 0xffffffff as the input of “%s”. Then as long as the count is not zero, the code is trying to access the memory at 0xcount, which may cause the SegV here.
After the discussion for the bug above, apparently this issue belongs to static code analysis scope. More over, Coverity does have a parse warning checker called “PRINTF_ARGS_MISMATCH” to catch this kind of issue. However, this checker focuses on the mismatch too much – each “%d” with unsigned long parameter would be reported, which is indeed not a secure coding but not so fatal as the bug above. When developer is frustrated by the tons of reports from this checker, it is acceptable that even fatal bug would be ignored. Then for Coverity, I would suggest to detail each checker to do one thing and only one thing or rank the severity level under this checker.
Moving forward, I am thinking if there is a kind of meta language or model to describe different kind of defect to make static code analysis much more flex and accurate. More over, users should feel easy to write the model for their certain cases…
Finally cleared: “I64u” should belong to Windows and “llu” (should) works in Linux.
Input from Coverity Support: as long as this Xprintf call is a standard lib call, PW.PRINTF_ARGS_MISMATCH should report this bug. For private implementation, Coverity could do nothing.