gcc options: -Wformat

Requires :

  • compiler: gcc 2.8 later
  • glibc 2.2 later

printf 와 같이 format을 사용하는 glibc 함수들을 사용할 때, 잘못된 format을 사용할 경우 compile warning이 발생하는 것을 종종 보았을 것입니다.

-> If format is used incorrectly, a compile warning occurs.

$ cat ./format_warning.c
#include <stdio.h>

int main(void) {
	printf("%d, %d\n", (int)1, (unsigned long)2);

	return 0;
}
$ gcc -o format_warning format_warning.c
format_warning.c: In function ‘main’:
format_warning.c:4:15: warning: format ‘%d’ expects argument \
  of type ‘int’, but argument 3 has type ‘long unsigned int’ [-Wformat=]
  printf("%d, %d\n", (int)1, (unsigned long)2);
              ~^             ~~~~~~~~~~~~~~~~

compiler라 unsigned long을 왜 %d로 출력하려고 하냐고 친절하게 warning으로 알려줍니다.
argument type 뿐만 아니라 argument 갯수가 잘못된 상황도 warning으로 보여줍니다.

-> Even if the number of arguments is incorrectly set.

$ cat ./format_warning2.c
#include <stdio.h>

int main(void) {
	printf("%d, %d\n", 1);

	return 0;
}
$ gcc -o format_warning2 format_warning2.c
format_warning2.c: In function ‘main’:
format_warning2.c:4:15: warning: format ‘%d’ expects a matching ‘int’ argument [-Wformat=]
    printf("%d, %d\n", 1);

-Wformat에 대해서 우선 gcc 문서를 살펴 보겠습니다.


gcc-7.5.0/Warning-Options
-Wformat
-Wformat=n
Check calls to printf and scanf, etc.,
to make sure that the arguments supplied have types appropriate to the format string specified, and that the conversions specified in the format string make sense.
This includes standard functions, and others specified by format attributes (see Function Attributes),
in the printf, scanf, strftime and strfmon (an X/Open extension, not in the C standard) families (or other target-specific families).
Which functions are checked without format attributes having been specified depends on the standard version selected, and such checks of functions without the attribute specified are disabled by -ffreestanding or -fno-builtin.

The formats are checked against the format features supported by GNU libc version 2.2.
These include all ISO C90 and C99 features, as well as features from the Single Unix Specification and some BSD and GNU extensions.
Other library implementations may not support all these features; GCC does not support warning about features that go beyond a particular library’s limitations.
However, if -Wpedantic is used with -Wformat, warnings are given about format features not in the selected standard version (but not for strfmon formats, since those are not in any version of the C standard).

ref. https://gcc.gnu.org/onlinedocs/gcc-7.5.0/gcc/Warning-Options.html#Warning-Options

glibc 2.2 이후의 몇몇 standard function에 대해서 지원한다고 나와 있습니다.
그럼 glibc의 어떤 함수들이 -Wformat을 지원할까요?
gcc 코드를 살펴보면 지원되는 함수 종류들을 볼 수 있습니다.

-> git checkout releases/gcc-3.0

$ cat ./gcc/c-format.c
...
void
init_function_format_info ()
{
  if (flag_hosted)
    {
      /* Functions from ISO/IEC 9899:1990.  */
      record_function_format (get_identifier ("printf"), NULL_TREE,
			      printf_format_type, 1, 2);
      record_function_format (get_identifier ("__builtin_printf"), NULL_TREE,
			      printf_format_type, 1, 2);
      record_function_format (get_identifier ("fprintf"), NULL_TREE,
			      printf_format_type, 2, 3);
      record_function_format (get_identifier ("__builtin_fprintf"), NULL_TREE,
			      printf_format_type, 2, 3);
      record_function_format (get_identifier ("sprintf"), NULL_TREE,
			      printf_format_type, 2, 3);
      record_function_format (get_identifier ("scanf"), NULL_TREE,
			      scanf_format_type, 1, 2);
      record_function_format (get_identifier ("fscanf"), NULL_TREE,
			      scanf_format_type, 2, 3);
      record_function_format (get_identifier ("sscanf"), NULL_TREE,
			      scanf_format_type, 2, 3);
      record_function_format (get_identifier ("vprintf"), NULL_TREE,
			      printf_format_type, 1, 0);
      record_function_format (get_identifier ("vfprintf"), NULL_TREE,
			      printf_format_type, 2, 0);
      record_function_format (get_identifier ("vsprintf"), NULL_TREE,
			      printf_format_type, 2, 0);
      record_function_format (get_identifier ("strftime"), NULL_TREE,
			      strftime_format_type, 3, 0);
    }

  if (flag_hosted && flag_isoc99)
    {
      /* ISO C99 adds the snprintf and vscanf family functions.  */
      record_function_format (get_identifier ("snprintf"), NULL_TREE,
			      printf_format_type, 3, 4);
      record_function_format (get_identifier ("vsnprintf"), NULL_TREE,
			      printf_format_type, 3, 0);
      record_function_format (get_identifier ("vscanf"), NULL_TREE,
			      scanf_format_type, 1, 0);
      record_function_format (get_identifier ("vfscanf"), NULL_TREE,
			      scanf_format_type, 2, 0);
      record_function_format (get_identifier ("vsscanf"), NULL_TREE,
			      scanf_format_type, 2, 0);
    }
  ...

gcc 코드는 너무 복잡해서 자세히는 이해 못 했지만, 크게 printf, scanf, strftime로 분류하는 것으로 보입니다.

그렇다면 glibc standard 함수가 아닌 va_arg()를 이용해 정의한 자신만의 함수도 -Wformat의 검출 대상으로 추가하는 방법은 없을까요?
위 문서에서도 살짝 언급했듯 __attribute__((format))를 사용하면 됩니다.
__attribute__((format))에 대해서는 다른 post에서 자세히 설명하겠습니다.
(사실 __attribute__((format))를 소개하고 싶어서 -Wformat를 먼저 설명하고 있는 것입니다…)

-Wformat 종류의 option은 많은 종류가 있습니다. 이것들에 대해서 간략하게 확인해봅시다.
gcc-7.5.0 기준으로 알아보겠습니다.

-Wformat-contains-nul

format에 NUL string(‘\0’)이 포함되어 있는지를 검사합니다.

-> sample source code: format_contains_nul.c

#include <stdio.h>

int main(void) {
    char buf[16];
    sprintf(buf, "%s\0", "test");

    return 0;
}

-> compile

$ gcc -o format_contains_nul format_contains_nul.c -Wformat-contains-nul

-> output

format_contains_nul.c: In function ‘main’:
format_contains_nul.c:5:21: warning: embedded ‘\0in format [-Wformat-contains-nul]
    5 |     sprintf(buf, "%s\0", "test");
      |                     ^~

-Wformat-extra-args

format의 specifiers 보다 argument 개수가 많을 경우를 검사합니다.

-> sample source code: format_extra_args.c

#include <stdio.h>

int main(void) {
    printf("excess arguments: %s", __LINE__, "what");

    return 0;
}

-> compile

$ gcc -o format_extra_args format_extra_args.c -Wformat-extra-args

-> output

format_extra_args.c: In function ‘main’:
...
format_extra_args.c:4:12: warning: too many arguments for format [-Wformat-extra-args]
    4 |     printf("excess arguments: %s", __LINE__, "what");

-Wformat-overflow

sprintf나 vsprintf와 같이 format에 따라 destination buffer에 쓰는 경우, buffer size 보다 큰 값이 들어와 overflow가 발생하는지를 검사합니다.

-> sample source code: format_overflow.c

#include <stdio.h>

int main(void) {
    char buf[10];
    sprintf(buf, "overflow %s", "ho!");

    return 0;
}

-> compile

$ gcc -o format_overflow format_overflow.c -Wformat-overflow

-> output

format_overflow.c: In function ‘main’:
format_overflow.c:5:28: warning: ‘%s’ directive writing 3 bytes into a region of size 1 [-Wformat-overflow=]
    5 |     sprintf(buf, "overflow %s", "ho!");
      |                            ^~   ~~~~~
format_overflow.c:5:5: note: ‘sprintf’ output 13 bytes into a destination of size 10
    5 |     sprintf(buf, "overflow %s", "ho!");
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

만약 compile time에 예측하기 힘든 상황에서는 어떻게 될까요?

-> sample source code: format_overflow.c

#include <stdio.h>

int main(int argc, const char *argv[]) {
    char buf[10];
    sprintf(buf, "overflow %s", argv[0]);

    return 0;
}

-> compile

$ gcc -o format_overflow format_overflow.c -Wformat-overflow

-> output

...

사실 argv 값에 상관없이 format에 의해 이미 buffer overflow가 발생하는 코드지만, -Wformat-overflow가 overflow를 감지하지 못하고 있습니다.

이럴 때는 Wformat-overflow의 level을 지정하면 됩니다.

-> compile with -Wformat-overflow=2

$  gcc -o format_overflow format_overflow.c -Wformat-overflow=2

-> output

format_overflow.c: In function ‘main’:
format_overflow.c:5:30: warning: ‘sprintf’ may write a terminating nul past the end of the destination [-Wformat-overflow=]
    5 |     sprintf(buf, "overflow %s", argv[0]);
      |                              ^
format_overflow.c:5:5: note: ‘sprintf’ output 10 or more bytes (assuming 11) into a destination of size 10
    5 |     sprintf(buf, "overflow %s", argv[0]);
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

-Wformat-zero-length

format이 zero length일 경우를 검사합니다.

-> sample source code: format_zero_length.c

#include <stdio.h>

int main(void) {
    printf("","zero-length");
    return 0;
}

-> compile

$ gcc -o format_zero_length format_zero_length.c -Wformat-zero-length

-> output

format_zero_length.c: In function ‘main’:
format_zero_length.c:4:12: warning: zero-length gnu_printf format string [-Wformat-zero-length]
    4 |     printf("","zero-length");
      |            ^~

사실상 이런 코드는 작성될 리가 없겠죠? 하지만 이런 코드는 의외로 자주 발생됩니다.

    const char fmt[] = "";
    printf(fmt,"zero-length");

-Wformat-nonliteral

format이 string literal 인지 검사합니다.

-> sample source code: format_nonliteral.c

#include <stdio.h>

int main(void) {
    char fmt[] = "%s";
    printf(fmt,"test");

    return 0;
}

-> compile

$ gcc -o format_nonliteral format_nonliteral.c -Wformat-nonliteral

-> output

format_nonliteral.c: In function ‘main’:
format_nonliteral.c:5:12: warning: format not a string literal, argument types not checked [-Wformat-nonliteral]
    5 |     printf(fmt,"test");
      |            ^~~

fmt를 const char로 지정할 경우 이 warning은 사라지게 됩니다.

- char fmt[] = "%s";
+ const char fmt[] = "%s";

하지만 경우에 따라 fmt를 dynamic 하게 변경해야 하는 코드를 작성해야 하는 경우도 있습니다.

#include <stdio.h>

char* get_format(int value) {
    if (value == 1)
        return "%d";
    else
        return "%s";
}

int main(void) {
    printf(get_format(1),"test");

    return 0;
}

그래서 경우에 따라 이 option을 무시하는 코드를 추가하기도 합니다.

#pragma GCC diagnostic ignored "-Wformat-nonliteral"
...
#pragma GCC diagnostic warning "-Wformat-nonliteral"

-Wformat-security

format이 string iternal이 아니면서, argument가 없을 경우를 감지합니다.

-> sample source code: format_security.c

#include <stdio.h>

int main(void) {
    char *fmt = "%s";
    printf(fmt);

    return 0;
}

-> compile

$ gcc -o format_security format_security.c -Wformat-security

-> output

format_security.c: In function ‘main’:
format_security.c:5:5: warning: format not a string literal and no format arguments [-Wformat-security]
    5 |     printf(fmt);
      |     ^~~~~~

이것을 감지하는 이유는 무엇일까요? option 이름이 security인 이유가 있습니다.
non-iternal string일 경우 format을 %n으로 변경 가능하며, stack overflow 등을 통해서 arugment를 조작 가능하기 때문에 Format String Bug Exploration 가능한 코드가 됩니다.
즉 security hole이 있는 코드가 됩니다.
Format String Bug Exploration에 대해서는 기회가 된다면 post로 다뤄볼 계획입니다.

-Wformat-signedness

이름 그대로 format이 signed인데 argument를 unsigned로 사용했을 경우, 또는 그 반대의 경우를 감지합니다.

-> sample source code: format_signedness

#include <stdio.h>

int main(void) {
    unsigned int a = 10;
    signed int b = -10;

    printf("%d %u", a, b);

    return 0;
}

-> compile

$ gcc -o format_signedness format_signedness.c -Wformat-signedness

-> output

format_signedness.c: In function ‘main’:
format_signedness.c:7:14: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘unsigned int’ [-Wformat=]
    7 |     printf("%d %u", a, b);
      |             ~^      ~
      |              |      |
      |              int    unsigned int
      |             %d
format_signedness.c:7:17: warning: format ‘%u’ expects argument of type ‘unsigned int’, but argument 3 has type ‘int’ [-Wformat=]
    7 |     printf("%d %u", a, b);
      |                ~^      ~
      |                 |      |
      |                 |      int
      |                 unsigned int
      |                %u

-Wformat-truncation

snprintf와 같이 length를 지정하는 function에 의해 결과가 잘린다는 것을 감지하고 경고해 줍니다.

-> sample source code: format_truncation

#include <stdio.h>

int main(void) {
    char buf[10];
    snprintf(buf, sizeof(buf), "%s", "string truncation");

    return 0;
}

-> compile

$ gcc -o format_truncation format_truncation.c -Wformat-truncation

-> output

format_truncation.c: In function ‘main’:
format_truncation.c:5:33: warning: ‘%s’ directive output truncated writing 17 bytes into a region of size 10 [-Wformat-truncation=]
    5 |     snprintf(buf, sizeof(buf), "%s", "string truncation");
      |                                 ^~   ~~~~~~~~~~~~~~~~~~~
format_truncation.c:5:5: note: ‘snprintf’ output 18 bytes into a destination of size 10
    5 |     snprintf(buf, sizeof(buf), "%s", "string truncation");
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

위와 같은 문제들은 사소해 보일 수도 있지만, 상황에 따라 runtime에 stack overflow가 발생할 수도 있으며, security hole이 될 수도 있습니다.
프로젝트가 커질수록 이러한 문제들을 Compile time에 인지하고 미리 수정 가능하다는 것이 큰 이점으로 작용할 수 있습니다.
따라서, 최대한 compile option에 -Wformat 혹은 -Werror=format을 추가하는 것을 권장 드립니다.

jooojub.

gcc attribute: format, format_arg

The format attribute specifies that a function takes printf, scanf, strftime or strfmon style arguments which should be type-checked against a format string. Continue reading

gcc builtin: choose_expr

Published on September 15, 2019

gcc builtin: alloca

Published on August 04, 2019