https://gcc.gnu.org/bugzilla/show_bug.cgi?id=117662

            Bug ID: 117662
           Summary: Weird -fanalyzer behavior with code split across
                    multiple files
           Product: gcc
           Version: 15.0
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: analyzer
          Assignee: dmalcolm at gcc dot gnu.org
          Reporter: frantisek at sumsal dot cz
  Target Milestone: ---

Hey,

As Fedora started recently utilizing gcc's static analyzer through OpenScanHub,
I started going through results for the systemd package, and would like to
eventually get rid of as much findings as possible.

There's one false positive that appears in multiple cases, and I managed to
somewhat minimize it to the following dummy code:

$ cat log.c
#include <stdio.h>

#include "log.h"

#define foo_log_oom(msg) foo_log(ENOMEM, msg)

int foo_log(int error, const char *msg) {
    fprintf(stderr, "%s\n", msg);
    return -error;
}

$ cat log.h
#pragma once

#include <errno.h>

#define foo_log_oom(msg) foo_log(ENOMEM, msg)

int foo_log(int error, const char *msg);

$ cat main.c
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "log.h"

int foo_new(size_t size, char **ret) {
    char *p = NULL;

    assert(ret);

    p = malloc(size);
    if (!p)
        return foo_log_oom("foo_new() failed");

    *ret = p;

    return 0;
}

int main(void) {
    char *p = NULL;
    int r;

    r = foo_new(1024, &p);
    if (r < 0)
        return 1;

    strcpy(p, "foo");
    printf("%s\n", p);

    free(p);

    return 0;
}

This is a very minimal example that resembles the bare minimum of the logging
infra that systemd usesm, but it reproduces the issue. In its current form it
generates a warning:

$ /opt/gcc-latest/bin/gcc -o main main.c log.c -fanalyzer 
main.c: In function ‘main’:
main.c:30:5: warning: use of NULL where non-null expected [CWE-476]
[-Wanalyzer-null-argument]
   30 |     strcpy(p, "foo");
      |     ^~~~~~~~~~~~~~~~
  ‘main’: events 1-3
    │
    │   22 | int main(void) {
    │      |     ^~~~
    │      |     |
    │      |     (1) entry to ‘main’
    │   23 |     char *p = NULL;
    │      |           ~
    │      |           |
    │      |           (2) ‘p’ is NULL
    │......
    │   26 |     r = foo_new(1024, &p);
    │      |         ~~~~~~~~~~~~~~~~~
    │      |         |
    │      |         (3) calling ‘foo_new’ from ‘main’
    │
    └──> ‘foo_new’: events 4-5
           │
           │    8 | int foo_new(size_t size, char **ret) {
           │      |     ^~~~~~~
           │      |     |
           │      |     (4) entry to ‘foo_new’
           │    9 |     char *p = NULL;
           │      |           ~
           │      |           |
           │      |           (5) ‘p’ is NULL
           │
         ‘foo_new’: event 6
           │
           │   11 |     assert(ret);
           │      |     ^~~~~~
           │      |     |
           │      |     (6) following ‘true’ branch (when ‘ret’ is non-NULL)...
─>─┐
           │      |                                                            
   │
           │
         ‘foo_new’: events 7-8
           │
           │      |                                                            
   │
           │     
|┌───────────────────────────────────────────────────────────────┘
           │   13 |│    p = malloc(size);
           │      |│        ^~~~~~~~~~~~
           │      |│        |
           │      |└───────>(7) ...to here
           │   14 |     if (!p)
           │      |        ~ 
           │      |        |
           │      |        (8) following ‘true’ branch (when ‘p’ is NULL)...
─>─┐
           │      |                                                            
│
           │
         ‘foo_new’: event 9
           │
           │log.h:5:26:
           │      |                                                            
│
           │     
|┌────────────────────────────────────────────────────────────┘
           │    5 |│#define foo_log_oom(msg) foo_log(ENOMEM, msg)
           │      |│                         ^~~~~~~~~~~~~~~~~~~~
           │      |│                         |
           │      |└────────────────────────>(9) ...to here
main.c:15:16: note: in expansion of macro ‘foo_log_oom’
           │   15 |         return foo_log_oom("foo_new() failed");
           │      |                ^~~~~~~~~~~
           │
    <──────┘
    │
  ‘main’: events 10-16
    │
    │   26 |     r = foo_new(1024, &p);
    │      |         ^~~~~~~~~~~~~~~~~
    │      |         |
    │      |         (10) returning to ‘main’ from ‘foo_new’
    │   27 |     if (r < 0)
    │      |        ~ 
    │      |        |
    │      |        (11) following ‘false’ branch (when ‘r >= 0’)... ─>─┐
    │      |                                                            │
    │......
    │      |                                                            │
    │      |┌───────────────────────────────────────────────────────────┘
    │   30 |│    strcpy(p, "foo");
    │      |│    ~~~~~~~~~~~~~~~~
    │      |│    |
    │      |└───>(12) ...to here
    │      |     (13) ‘p’ is NULL
    │      |     (16) ⚠  argument 1 (‘p’) NULL where non-null expected
    │   31 |     printf("%s\n", p);
    │      |     ~~~~~~~~~~~~~~~~~
    │      |     |
    │      |     (14) ‘p’ is NULL
    │   32 | 
    │   33 |     free(p);
    │      |     ~~~~~~~
    │      |     |
    │      |     (15) ‘p’ is NULL
    │
<built-in>: note: argument 1 of ‘__builtin_memcpy’ must be non-null

But this is incorrect, since the foo_log_oom() function returns -ENOMEM, so the
if (r < 0) condition can't follow the false branch.

Interestingly enough, if I shove all the code into a single file, the warning
disappears:

$ cat main.c log.[ch] | /opt/gcc-latest/bin/gcc -o main -fanalyzer -x c -
<stdin>:47:9: warning: ‘#pragma once’ in main file
[-Wpragma-once-outside-header]

(this is a quick and dirty example, hence the #pragma warning, but the outcome
is the same even if the code is _properly_ integrated into the main source
file, i.e. there's no -Wanalyzer-null-argument warning).

Is this expected?

Reply via email to