Static Analysis in GCC 10


I work at Red Hat on the GCC, GNU Compiler Collection . For the next major release of GCC, GCC 10 , I implemented a new -fanalyzer option : a static analysis pass to detect various problems at compile time rather than run time.

I think it’s better to identify problems as soon as possible as you write the code, using the compiler as part of the compile-edit-debug cycle, rather than using static analysis as an additional “on the side” (possibly proprietary) tool. Therefore, it seems appropriate to have a static analyzer built into the compiler that sees the code exactly the same as the compiler sees - because this is the compiler.

This question, of course, is a huge problem that needs to be addressed. For this release, I focused on the types of problems seen in the C code, and, in particular, on double-free errors , but with the goal of creating a framework that we can expand in future releases (when we can add more checks and support for languages ​​other than C).

I hope that the analyzer provides a decent amount of additional verification, while not being too overhead. I tried to make -fanalyzer “just” double the compilation time as a reasonable compromise between additional checks. I have not succeeded so far, as you will see below, but I am working on it.

Now the code is in the main GCC branch for GCC 10 and can be tested in the Compiler Explorer, aka godbolt.org . It works well for small and medium-sized examples, but there are bugs that mean that it is not ready for industrial use. I am working hard on corrections in the hope that by the time GCC 10 is released (most likely in April) this feature will be effectively applicable to C code.

Diagnostic Ways


Here is the simplest example of a double-free error:

#include <stdlib.h>

void test(void *ptr)
{
  free(ptr);
  free(ptr);
}

GCC 10 with -fanalyzer reports this as follows:
$ gcc -c -fanalyzer double-free-1.c
double-free-1.c: In functiontest’:
double-free-1.c:6:3: warning: double-‘free’ of ‘ptr’ [CWE-415] [-Wanalyzer-double-free]
    6 |   free(ptr);
      |   ^~~~~~~~~test’: events 1-2
    |
    |    5 |   free(ptr);
    |      |   ^~~~~~~~~
    |      |   |
    |      |   (1) first ‘free’ here
    |    6 |   free(ptr);
    |      |   ~~~~~~~~~
    |      |   |
    |      |   (2) second ‘free’ here; first ‘free’ was at (1)
    |

This log shows that the GCC has learned some new tricks; firstly, the ability to diagnose having Common Weakness Enumeration (CWE) identifiers . In this example, double-free diagnostics are tagged with CWE-415 . We hope that this tag will make the conclusion more understandable, increase accuracy and give you something simple to enter in search engines. So far, only diagnostics from -fanalyzer are marked with CWE vulnerability identifiers.

If you use GCC 10 with a suitable terminal (for example, a fresh gnome-terminal), then the CWE identifier is a hyperlink leading to a description of the problem. Speaking of hyperlinks, for many releases, when the GCC issues a warning, it prints an option that governs this warning. Starting with GCC 10, this option text is now a click-through hyperlink (again, assuming a fairly developed terminal ), which should lead you to the documentation for this option (for any warning, and not just those related to the analyzer).

GCC diagnostics can now have an associated event chain that describes the path through the code that triggers the problem. Given the lack of control flow in the above example, it has only two events, but you can see how the second event relates to the first in its description.

We give a more complete example. Do you see the problem in the following code? (Hint: this time it's not a double release):

#include <setjmp.h>
#include <stdlib.h>

static jmp_buf env;

static void inner(void)
{
  longjmp(env, 1);
}

static void middle(void)
{
  void *ptr = malloc(1024);
  inner();
  free(ptr);
}

void outer(void)
{
  int i;

  i = setjmp(env);
  if (i == 0)
    middle();
}

Here's what GCC- fanalyzer reports , which shows the inter- procedure control flow using ASCII output:

$ gcc -c -fanalyzer longjmp-demo.c
longjmp-demo.c: In function ‘inner’:
longjmp-demo.c:8:3: warning: leak of ‘ptr’ [CWE-401] [-Wanalyzer-malloc-leak]
    8 |   longjmp(env, 1);
      |   ^~~~~~~~~~~~~~~
  ‘outer’: event 1
    |
    |   18 | void outer(void)
    |      |      ^~~~~
    |      |      |
    |      |      (1) entry to ‘outer’
    |
  ‘outer’: event 2
    |
    |   22 |   i = setjmp(env);
    |      |       ^~~~~~
    |      |       |
    |      |       (2) ‘setjmp’ called here
    |
  ‘outer’: events 3-5
    |
    |   23 |   if (i == 0)
    |      |      ^
    |      |      |
    |      |      (3) following ‘true’ branch (when ‘i == 0’)...
    |   24 |     middle();
    |      |     ~~~~~~~~
    |      |     |
    |      |     (4) ...to here
    |      |     (5) calling ‘middle’ from ‘outer’
    |
    +--> ‘middle’: events 6-8
           |
           |   11 | static void middle(void)
           |      |             ^~~~~~
           |      |             |
           |      |             (6) entry to ‘middle’
           |   12 | {
           |   13 |   void *ptr = malloc(1024);
           |      |               ~~~~~~~~~~~~
           |      |               |
           |      |               (7) allocated here
           |   14 |   inner();
           |      |   ~~~~~~~
           |      |   |
           |      |   (8) calling ‘inner’ from ‘middle’
           |
           +--> ‘inner’: events 9-11
                  |
                  |    6 | static void inner(void)
                  |      |             ^~~~~
                  |      |             |
                  |      |             (9) entry to ‘inner’
                  |    7 | {
                  |    8 |   longjmp(env, 1);
                  |      |   ~~~~~~~~~~~~~~~
                  |      |   |
                  |      |   (10) ‘ptr’ leaks here; was allocated at (7)
                  |      |   (11) rewinding from ‘longjmp’ in ‘inner’...
                  |
    <-------------+
    |
  ‘outer’: event 12
    |
    |   22 |   i = setjmp(env);
    |      |       ^~~~~~
    |      |       |
    |      |       (12) ...to ‘setjmp’ in ‘outer’ (saved at (2))
    |

The above is pretty verbose, although perhaps this should be the case in order to convey what is happening, given the use of setjmp and longjmp . I hope the description is clear enough: a memory leak occurs when a longjmp call expands the stack back to outer past the cleanup point in middle , without causing cleanup.

If you do not like the ASCII output shown above, you can view events as a separate “note” diagnosis using -fdiagnostics-path-format = separate-events :

$ gcc -c -fanalyzer -fdiagnostics-path-format=separate-events longjmp-demo.c
longjmp-demo.c: In function ‘inner’:
longjmp-demo.c:8:3: warning: leak of ‘ptr’ [CWE-401] [-Wanalyzer-malloc-leak]
    8 |   longjmp(env, 1);
      |   ^~~~~~~~~~~~~~~
longjmp-demo.c:18:6: note: (1) entry to ‘outer’
   18 | void outer(void)
      |      ^~~~~
In file included from longjmp-demo.c:1:
longjmp-demo.c:22:7: note: (2) ‘setjmp’ called here
   22 |   i = setjmp(env);
      |       ^~~~~~
longjmp-demo.c:23:6: note: (3) following ‘true’ branch (when ‘i == 0’)...
   23 |   if (i == 0)
      |      ^
longjmp-demo.c:24:5: note: (4) ...to here
   24 |     middle();
      |     ^~~~~~~~
longjmp-demo.c:24:5: note: (5) calling ‘middle’ from ‘outer’
longjmp-demo.c:11:13: note: (6) entry to ‘middle’
   11 | static void middle(void)
      |             ^~~~~~
longjmp-demo.c:13:15: note: (7) allocated here
   13 |   void *ptr = malloc(1024);
      |               ^~~~~~~~~~~~
longjmp-demo.c:14:3: note: (8) calling ‘inner’ from ‘middle’
   14 |   inner();
      |   ^~~~~~~
longjmp-demo.c:6:13: note: (9) entry to ‘inner’
    6 | static void inner(void)
      |             ^~~~~
longjmp-demo.c:8:3: note: (10) ‘ptr’ leaks here; was allocated at (7)
    8 |   longjmp(env, 1);
      |   ^~~~~~~~~~~~~~~
longjmp-demo.c:8:3: note: (11) rewinding from ‘longjmp’ in ‘inner’...
In file included from longjmp-demo.c:1:
longjmp-demo.c:22:7: note: (12) ...to ‘setjmp’ in ‘outer’ (saved at (2))
   22 |   i = setjmp(env);
      |       ^~~~~~

or even turn them off with -fdiagnostics-path-format = none . There is also a JSON output format.

All new diagnostics have the name of the type -Wanalyzer-SOMETHING : We have already seen -Wanalyzer-double-free and -Wanalyzer-malloc-leak above. All these diagnostics are enabled when -fanalyzer is enabled , but they can be selectively disabled using the -Wno-analyzer-SOMETHING options (for example, using pragmas).

What new warnings will be?


Along with double-free detection, malloc and fopen leak checks are carried out :

#include <stdio.h>
#include <stdlib.h>

void test(const char *filename)
{
  FILE *f = fopen(filename, "r");
  void *p = malloc(1024);
  /* do stuff */
}

$ gcc -c -fanalyzer leak.c
leak.c: In functiontest’:
leak.c:9:1: warning: leak of ‘p’ [CWE-401] [-Wanalyzer-malloc-leak]
    9 | }
      | ^test’: events 1-2
    |
    |    7 |   void *p = malloc(1024);
    |      |             ^~~~~~~~~~~~
    |      |             |
    |      |             (1) allocated here
    |    8 |   /* do stuff */
    |    9 | }
    |      | ~
    |      | |
    |      | (2) ‘p’ leaks here; was allocated at (1)
    |
leak.c:9:1: warning: leak of FILE ‘f’ [CWE-775] [-Wanalyzer-file-leak]
    9 | }
      | ^test’: events 1-2
    |
    |    6 |   FILE *f = fopen(filename, "r");
    |      |             ^~~~~~~~~~~~~~~~~~~~
    |      |             |
    |      |             (1) opened here
    |......
    |    9 | }
    |      | ~
    |      | |
    |      | (2) ‘f’ leaks here; was opened at (1)
    |

Monitoring memory usage after it is freed:

#include <stdlib.h>

struct link { struct link *next; };

int free_a_list_badly(struct link *n)
{
  while (n) {
    free(n);
    n = n->next;
  }
}

$ gcc -c -fanalyzer use-after-free.c
use-after-free.c: In function ‘free_a_list_badly’:
use-after-free.c:9:7: warning: use after ‘free’ of ‘n’ [CWE-416] [-Wanalyzer-use-after-free]
    9 |     n = n->next;
      |     ~~^~~~~~~~~
  ‘free_a_list_badly’: events 1-4
    |
    |    7 |   while (n) {
    |      |         ^
    |      |         |
    |      |         (1) following ‘true’ branch (when ‘n’ is non-NULL)...
    |    8 |     free(n);
    |      |     ~~~~~~~
    |      |     |
    |      |     (2) ...to here
    |      |     (3) freed here
    |    9 |     n = n->next;
    |      |     ~~~~~~~~~~~
    |      |       |
    |      |       (4) use after ‘free’ of ‘n’; freed at (3)
    |

Non-heap pointer release control:

#include <stdlib.h>

void test(int n)
{
  int buf[10];
  int *ptr;

  if (n < 10)
    ptr = buf;
  else
    ptr = (int *)malloc(sizeof (int) * n);

  /* do stuff.  */

  /* oops; this free should be conditionalized.  */
  free(ptr);
}

$ gcc -c -fanalyzer heap-vs-stack.c
heap-vs-stack.c: In functiontest’:
heap-vs-stack.c:16:3: warning: ‘free’ of ‘ptr’ which points to memory not on the heap [CWE-590] [-Wanalyzer-free-of-non-heap]
   16 |   free(ptr);
      |   ^~~~~~~~~test’: events 1-4
    |
    |    8 |   if (n < 10)
    |      |      ^
    |      |      |
    |      |      (1) following ‘true’ branch (when ‘n <= 9’)...
    |    9 |     ptr = buf;
    |      |     ~~~~~~~~~
    |      |         |
    |      |         (2) ...to here
    |      |         (3) pointer is from here
    |......
    |   16 |   free(ptr);
    |      |   ~~~~~~~~~
    |      |   |
    |      |   (4) call to ‘free’ here
    |

Monitoring the use of a function that is known to be unsafe to use inside the signal handler :

#include <stdio.h>
#include <signal.h>

extern void body_of_program(void);

void custom_logger(const char *msg)
{
  fprintf(stderr, "LOG: %s", msg);
}

static void handler(int signum)
{
  custom_logger("got signal");
}

int main(int argc, const char *argv)
{
  custom_logger("started");

  signal(SIGINT, handler);

  body_of_program();

  custom_logger("stopped");

  return 0;
}

$ gcc -c -fanalyzer signal.c
signal.c: In function ‘custom_logger’:
signal.c:8:3: warning: call to ‘fprintf’ from within signal handler [CWE-479] [-Wanalyzer-unsafe-call-within-signal-handler]
    8 |   fprintf(stderr, "LOG: %s", msg);
      |   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  ‘main’: events 1-2
    |
    |   16 | int main(int argc, const char *argv)
    |      |     ^~~~
    |      |     |
    |      |     (1) entry to ‘main’
    |......
    |   20 |   signal(SIGINT, handler);
    |      |   ~~~~~~~~~~~~~~~~~~~~~~~
    |      |   |
    |      |   (2) registering ‘handler’ as signal handler
    |
  event 3
    |
    |cc1:
    | (3): later on, when the signal is delivered to the process
    |
    +--> ‘handler’: events 4-5
           |
           |   11 | static void handler(int signum)
           |      |             ^~~~~~~
           |      |             |
           |      |             (4) entry to ‘handler’
           |   12 | {
           |   13 |   custom_logger("got signal");
           |      |   ~~~~~~~~~~~~~~~~~~~~~~~~~~~
           |      |   |
           |      |   (5) calling ‘custom_logger’ from ‘handler’
           |
           +--> ‘custom_logger’: events 6-7
                  |
                  |    6 | void custom_logger(const char *msg)
                  |      |      ^~~~~~~~~~~~~
                  |      |      |
                  |      |      (6) entry to ‘custom_logger’
                  |    7 | {
                  |    8 |   fprintf(stderr, "LOG: %s", msg);
                  |      |   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                  |      |   |
                  |      |   (7) call to ‘fprintf’ from within signal handler
                  |

Along with other warnings .

What remains to be done?


In its current form, verification works well for small and medium-sized examples, but there are two problem areas that I encounter when scaling to real C code.

Firstly, there are errors in my state control code. Inside the checker there are classes for the abstract description of the state of the program. The checker examines the program by constructing a directed graph of pairs (point, state) with the logic of simplifying the state and merging states at the connection points of the control flow.

Theoretically, if the state becomes too complex, the examiner should go to the least defined state, but with this approach errors occur that lead to an explosion in the number of states at a given point, which then leads to the inspector working slowly, eventually reaching the safety limit , and does not examine the program completely. To fix this, I rewrote the guts of the state control code. Hope to rewrite master next week.

Further, even if we fully examine the program, paths through the code generated by the -fanalyzer analyzerare sometimes absurdly verbose. The worst I've seen is a 110-event path for using uninitialized data reported when compiling GCC itself. I think this is a false positive, and it’s obvious that it’s not reasonable to expect users to go through something like this.

The analyzer tries to find the shortest possible path through the graph (point, state), generates a chain of events from it, and then tries to simplify this chain. In fact, he applies a series of peephole optimizations to the event chain to get a minimal chain that demonstrates the problem.

I recently implemented a way to filter non-essential edges of a control flow from a path that should help, and am working on a similar patch to eliminate redundant inter-procedure edges.

As a specific example, I tried the analyzer on a real error (albeit fifteen years ago) - CVE-2005-1689 , the double-free vulnerability in krb5 1.4.1. It correctly identifies the error without false positives, but at the moment stderr has 170 lines of output. Instead of showing the output in a line here, you can see it at this link .

Originally it was 1187 lines. I fixed various errors and implemented more simplifications to bring it to 170 lines. Part of the problem is that free is executed using the krb5_xfree macro, and the path print code shows how each macro expands every time an event occurs inside the macro. Perhaps the output should show the macro extension only once per diagnostic. Also, the first few events in each diagnostics are interprocedural logic, which is actually irrelevant for the user (I'm working on fixes for this). With these changes, the conclusion should be much shorter.

Perhaps the best interface could produce a separate HTML file, one per warning, and give a “note” indicating the location of additional information?

I want to give the end user enough information about the warning, but without overloading it. Are there any better ways to introduce this? Let me know in the comments.

How to try


GCC 10 will appear in Fedora 32, due out in a couple of months.

For simple code examples, you can play around with the new GCC online at godbolt.org (select gcc "trunk" and add -fanalyzer to the compiler options).

Good luck

Further added by the translator from the forums.

Dlang, Walter Bright's comments on Hacker News


This is consistent with the advancement of [my] idea to make D safe @ safe by default , and implement the @ live ownership / borrowing system .

Either we jump on this bus, or it will move us.

Double-free's can be tracked by performing data flow analysis (DFA) in the function. This is exactly how D does this in its nascent implementation of the tenure system. This can be done without DFA, having received only 90% of the correct results and having a lot of false positives.

In the past, I used a lot of static checkers, and the percentage of false positives was high enough to refuse to use them. This is why D uses DFA to give 100% positive signals with 0% false positives (Note here that all leaks detected are 100% leaks, and not that 100% of all possible leaks are caught). I knew that this would be possible because the compilers used DFA in the optimization pass.

For tracking to work, you can’t just track events for a function called “free”. In the end, the usual thing is to write your own memory allocators, and the compiler will not know what it is. Therefore, there must be some mechanism to tell the compiler when the parameter parameter type function is “consumed” by the called function and when it is simply “lent” to it (hence the nomenclature of the Owner / Borrower system).

One of the difficulties that can be overcome with the help of D is that there are several complex semantic constructions that need to be broken down into their component operations with pointers. I noticed that Rust simplified this problem by simplifying the language :-).

But once this is done, it works, and works satisfactorily well.

Please note that none of this is critical of what GCC 10 does, because the article lacks details to make reasonable conclusions. But I consider this as part of the general trend that people are tired of memory security errors in programming languages, and it is very nice to see progress on all fronts.

Timon Hera's comment for understanding D from the discussion forum on Dlang.org


@ live is not a ownership / borrowing system, although it does rely on concepts related to ownership and borrowing.

The system of ownership / borrowing imposes the semantics of ownership in the code @ safe , @ live - no. This is only a linter for @ system and @ trusted code without security guarantees.

All Articles