Subject: Re: [PATCH v4 2/2] ci: compile "linux-gcc-default" job with -Og

From: Junio C Hamano <[email-masked]>
Date: Mon, 10 Jun 2024 09:06:51 -0700

Patrick Steinhardt <[email-masked]> writes:

> While we could adapt all jobs to compile with `-Og` now, that would
> potentially mask other warnings that only get diagnosed with `-O2`.

I would say it is not just "potentially".  It is likely that higher
optimization levels will diagnose breakage more correctly, while
lower optimization levels may trigger warnings more often.  I
suspect a large part of those that lower optimization levels alone
find may be due to false positives.

So, ... I had a strong knee-jerk reaction against "While we could
adapt all" with "why would anybody sane would even want to do such a
thing in the first place?"  If it were phrased like "as we have
plenty of jobs compiling with -O2, we can afford to switch a few of
them to different optimization levels, and we may find bugs that the
exiting level did not notice, as the compilers' understanding of the
data and control flow is affected by these -O levels", I would have
understood it, and I think that is what happens in this patch.

I just tried compiling pack-mtimes.c (untouced between maint and
seen) with -Og using gcc-11, gcc-12, and gcc-13, using the exact
copy of the command line with V=1 output I see at the GitHub CI
[*1*] (after all, this was a good use case, even though it was
dropped from this round ;-)

The earlier versions of the compiler mark the use of mtimes_size
that is guarded in "if (data)" as maybe-uninitialized:

        pack-mtimes.c: In function ‘load_pack_mtimes_file’:
        pack-mtimes.c:89:25: error: ‘mtimes_size’ may be used uninitialized [-Werror=maybe-uninitialized]
           89 |                         munmap(data, mtimes_size);
              |                         ^~~~~~~~~~~~~~~~~~~~~~~~~
        pack-mtimes.c:32:16: note: ‘mtimes_size’ was declared here
           32 |         size_t mtimes_size, expected_size;
              |                ^~~~~~~~~~~
        cc1: all warnings being treated as errors
        make: *** [Makefile:2750: pack-mtimes.o] Error 1

But as I already reported elsewhere, this is a false positive.  The
place "data" can possibly become non-NULL happens only after the
control passes the place mtimes_size get assigned a meaningful
value.  If the control reached here and data were non-NULL, the
control flow guarantees that mtimes_size has been assigned
xsize_t(st.st_size) and cannot be uninitialized.

We can squelch the warning by initializing the variable to a
meaningless value, like 0, but I consider that such a change makes
the resulting code a lot worse than the original, and that is not
because I do not want an extra and meaningless assignment emitted
[*2*] in the resulting binary whose performance impact cannot be
measured by anybody.

It is bad because it defeats efforts by compilers that understand
the data flow a bit better to diagnose a true "used uninitialized"
bugs we may introduce in the future.  Adding such a workaround goes
against the longer-term health of the codebase.

For example, I could add another use of mtimes_size that is not
guarded by "if (data)" right next to it, i.e.

        @@ -87,6 +87,7 @@ static int load_pack_mtimes_file(char *mtimes_file,
                if (ret) {
                        if (data)
                                munmap(data, mtimes_size);
        +		printf("debug %d\n", (int)mtimes_size);
                } else {
                        *len_p = mtimes_size;
                        *data_p = data;

gcc-13 does notice that we are now truly using the variable
uninitialized and flags it for us (gcc-12 also does notice but the
important difference is that gcc-13 refrained from warning at the
safe use of the variable that is guarded by "if (data)").

Once we initialize the variable to meaninless 0 as a workaround,
however, the compiler cannot help us with the "used uninitialized"
warning, and there is no "the program uses the variable what was
initialized with meaningless value before it get set a meaningful
value" warning, unfortunately X-<.  And that, not an extra
assignment, is why such a workaround  goes against the longer-term
health of the codebase.

By the way, it is not just pack-mtimes.c [*3*]; there are quite a few
obvious false positives, e.g.

builtin/branch.c: In function ‘print_current_branch_name’:
builtin/branch.c:509:17: error: ‘shortname’ may be used uninitialized [-Werror=maybe-uninitialized]
  509 |                 puts(shortname);
      |                 ^~~~~~~~~~~~~~~

where after skip_prefix() computed shortname like so:

	else if (skip_prefix(refname, "refs/heads/", &shortname))
		puts(shortname);

and the compiler at that low optimization level does not even notice
that the static inline function skip_prefix() never returns true
without updating its third parameter to a valid pointer X-<.

And adding a workaround for each and every uses of variable that are
set by skip_prefix() like so?  I am not sure if that is a good move

If you saw some diagnosis that "-Og" made but not with higher
optimization levels (with better understanding of the data and
control flow on the compiler's part), I am somewhat curious.

With gcc-13 everything seems to pass locally for me, so I personally
do not mind this patch, but from what I've seen "gcc-12 -Wall -Og"
so far, especially given that GitHub CI (and GitLab CI as well?
otherwise you wouldn't be sending this change, I would imagine)
seems to use a version of compiler whose "-Og -Wall" notices and
flags these "bugs", I am not sure if we want to enable this option
for us.  At least, -Werror and more stupid compilers is not a mix I
would want to live with and I am sure I would not want to add
workaround for such a mix to make our code worse (to end users and
builders who want to use older comiplers with different optimization
levels, we can tell them not to enable -Werror in their build).

Also, with "-Os", we seem to find different set of "bugs" that "-Og"
did not see.  For example, "gcc-13 -Os -Wall" flags that ppid may be
uninitialized in compat/linux/procinfo.c:push_ancestry_name(), but
that is because it does not read stat_parent_pid() [*4*] and realize
that the only time ppid is left unset is when the function returns
-1:

        static void push_ancestry_name(struct strvec *names, pid_t pid)
        {
                int ppid;

                if (stat_parent_pid(pid, &name, &ppid) < 0)
                        goto cleanup;
                ...
                if (ppid)
                        push_ancestry_name(names, ppid);
        cleanup:
                strbuf_release(&name);
		...

It is the same pattern as how "gcc-12 -Og" mishandles skip_prefix().
Another one "-Os" found is in builtin/fast-import.c:file_change_m()
where the compiler does not notice that parse_mode() never returns
non-NULL without setting "mode".  It is exactly the same pattern.

[Footnotes]

 *1* https://github.com/git/git/actions/runs/9432273715/job/25981831882#step:4:132

 *2* We used to do the (IIRC GCC only) trick to initialize the
     variable to itself, i.e.

        @@ -29,7 +29,7 @@ static int load_pack_mtimes_file(char *mtimes_file,
                int fd, ret = 0;
                struct stat st;
                uint32_t *data = NULL;
        -	size_t mtimes_size, expected_size;
        +	size_t mtimes_size = mtimes_size, expected_size;
                struct mtimes_header header;

                fd = git_open(mtimes_file);

     to work around stupid compilers that flag a variable whose use
     is safe as potentially-used-uninitialized but these days we got
     rid of the trick as some other compiler did not like it (for
     legitimate reasons).  Using the trick is not any better than
     initializing the variable to a meaningless value, like 0, as
     both will prevent smarter compilers from noticing real bugs
     (after all, that is the point of the workaround--not to flag
     use of variable the compiler thinks uninitialized as a bug).

 *3* I was planning to send "these are horrible workarounds to work
     around the recent -Og build failures that has a huge negative
     consequences in the longer term" patch on top of your series.

     But I do not think it is a good idea to add such a huge number
     of workarounds for various optimization levels.  If we could
     add -Og with -Werror disabled, those who may be interested in
     sifting through false positives to find real errors may be able
     to do so without breaking build for others.

 *4* As "-Os" is for smaller code, I would imagine that it didn't
     try to auto inline the function.  Come to think of it, "-Og" is
     to forbid optimizations that interfere with debugging, and code
     inlining may be considered as one, which would certainly
     explain why it has problem with skip_prefix().