Skip to content

ci: add static analysis and more compiler warnings - #690

Merged
jnovy merged 9 commits into
containers:mainfrom
kolyshkin:ci-static-analysis
Aug 26, 2026
Merged

ci: add static analysis and more compiler warnings#690
jnovy merged 9 commits into
containers:mainfrom
kolyshkin:ci-static-analysis

Conversation

@kolyshkin

Copy link
Copy Markdown
Collaborator

Until now the only check on the C code itself was clang-format. This adds
two static analyzers and a larger set of compiler warnings, plus the small
fixes needed to make the tree clean under them.

Static analyzers

Two new make targets, so they can be run locally exactly the way CI runs
them, and two new blocking jobs in validate.yml:

  • make analyze runs clang --analyze over every source file;
  • make cppcheck runs cppcheck with the warning, performance and
    portability checks.

Three clang checkers are disabled and cppcheck's style checks are not
enabled; the Makefile says why. The short version is that the clang static
analyzer does not model __attribute__((cleanup)), which this code base
uses throughout, so every _cleanup_free_ / _cleanup_close_ /
_cleanup_fclose_ variable looks either leaked or assigned but never read.
The CFG does carry a CleanupFunction element, but the engine never
evaluates it, and ownership_takes does not help, since the attribute
would refer to the address of the variable rather than to the pointer.

gcc -fanalyzer is deliberately not part of this. It does model the
cleanup attribute, but it does not know that execv(2) does not return,
which costs six false fd-leak reports around the fork/exec path.

Warning flags

Everything added here is already clean on the tree, so it only guards
against new code. Flags that only some compilers have go through a
cc-option helper that probes the compiler first, because clang rejects
an unknown -W option outright. -Wformat-signedness and
-Wjump-misses-init exist in gcc and in clang 19 and later, but not in
clang 18, which is what ubuntu-latest still ships.

Three flags were considered and left out, each for a stated reason:
-Wcast-align (fires on the CMSG_DATA() casts, where the kernel
guarantees the alignment), -Wcast-qual (three of five hits cannot be
fixed: volatile hash table keys written by a signal handler, and the
non-const iov_base of writev(2)), and -Wswitch-enum (its only hit
already has a default case).

Code changes

All of them are fixes for the above, and none is meant to change
behaviour:

  • socket_parent_dir() used the only plain strdup() left in the tree;
  • disconnect_std_streams() took two parameters that shadowed the globals
    of the same name, which both call sites passed in anyway;
  • an outer ret in main() shadowed a genuinely local one further down;
  • the inotify event buffer in oom_cb_cgroup_v2() was a variable-length
    array, because a const size_t is not an integer constant expression;
  • arithmetic on the void * iov_base is a GNU extension;
  • remote_sock_shutdown() switches over an int, so it wants a default;
  • st_uid is unsigned and was printed with %d;
  • a good deal of missing const on strings that are only ever read.

Testing

make, make analyze, make cppcheck and the meson build were all run
against gcc 13 with clang 18 and gcc 16 with clang 22. The test suite was
not run locally, so CI is its first pass.

kolyshkin and others added 9 commits August 25, 2026 01:00
This was the only plain strdup() left in the tree; everything else uses
g_strdup(), which aborts on allocation failure rather than returning
NULL. The other return path of this very function already hands back
memory from g_build_filename(), so this makes both paths consistent.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
So far the only check on the C code itself was clang-format. Add two
static analyzers, each behind a make target so they can be run locally
the same way CI runs them:

 - "make analyze" runs clang --analyze over every source file;
 - "make cppcheck" runs cppcheck with warning, performance and
   portability checks.

Both are clean on the current tree and are wired into validate.yml as
blocking jobs.

Three clang checkers are disabled, and cppcheck's "style" checks are not
enabled. The reasons are spelled out in the Makefile: none of the three
models __attribute__((cleanup)), which this code base uses throughout, so
they only produce false positives here.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
...and fix found issues.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
... and fix found warnings.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
All of these are already clean on the current tree, so they cost nothing
today and only guard against new code:

  -Wmissing-prototypes -Wmissing-declarations -Wformat=2 -Wundef
  -Winit-self -Wnull-dereference -Wfloat-equal -Wdouble-promotion
  -Walloca -Wduplicated-cond -Wduplicated-branches -Wtrampolines

The last three exist in gcc but not in clang, which rejects an unknown
-W option outright, so they are added through a cc-option helper that
probes the compiler first. The analyze target runs clang over the same
CFLAGS, and so also needs -Wno-unknown-warning-option.

-Wcast-align is deliberately not in the list: gcc does not warn about it
on x86, but clang does, and it fires on the CMSG_DATA() casts in
src/cmsg.c, where the kernel guarantees the alignment.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both parameters were named after, and shadowed, the globals of the same
name, and both call sites passed in exactly those globals. Read the
globals directly instead, so that a future reference to dev_null_r or
dev_null_w inside this function cannot silently mean something else.

No functional change.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One shadowed variable was left. The outer ret in main() was used only on
the line right after it was assigned, while the inner one, in the
waitpid() loop further down, is genuinely local to that block. Drop the
outer variable rather than renaming either of them.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each of these had exactly one or two hits, all fixed here:

 - -Wvla: the inotify event buffer in oom_cb_cgroup_v2() was sized by a
   const size_t, which is not an integer constant expression in C, so it
   was a variable-length array. Size it by the expression itself and use
   sizeof() at the read() call.
 - -Wpointer-arith: arithmetic on the void * iov_base is a GNU extension;
   go through char * instead.
 - -Wjump-misses-init: the errorf() macro jumps to a label past the
   declaration of ret. The label does not use ret, so this was harmless,
   but declaring it up front with the other locals is both clearer and
   consistent with the rest of the function.
 - -Wswitch-default: remote_sock_shutdown() switches over an int, not an
   enum, so an explicit default belongs there.
 - -Wformat-signedness: st_uid is unsigned, and was printed with %d.

-Wformat-signedness and -Wjump-misses-init exist in gcc and in clang 19
and later, but not in clang 18, which is what ubuntu-latest still ships,
so they go through cc-option as well.

-Wswitch-enum is deliberately left out: its only hit is stdpipe_name(),
which already has a default case, and -Wall's -Wswitch already covers
the case that matters.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The flag gives string literals the type they morally have, const char[],
so that assigning one to a plain char * is diagnosed. All eleven hits
were places that only ever read the string, so the fix throughout is to
add the missing const:

 - opt_socket_path, whose default value is a literal;
 - the label member of struct local_sock_s;
 - the filename and error_var_name parameters of setup_fifo();
 - the socket_relative_name parameter of bind_unix_socket();
 - the local command in configure_runtime_args();
 - driver, path and k8s_log_path in the log driver parsing, which also
   lets a cast away from const disappear.

opt_socket_path stays a valid G_OPTION_ARG_STRING target: glib takes the
address as a gpointer, and const char ** converts to void * without
discarding anything, since the pointed-at type is itself unqualified.

-Wcast-qual is not added. Three of its five hits cannot be fixed: two
hash table keys are volatile because a signal handler writes them, and
writev(2) takes a non-const iov_base, so the const has to come off.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jnovy

jnovy commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

LGTM

@jnovy
jnovy merged commit 53edd15 into containers:main Aug 26, 2026
33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants