Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I get a warning when comparing unsigned integers of different sizes in C and C++?

A common source of bugs in C or C++ is a code like this:

size_t n = // ...

for (unsigned int i = 0; i < n; i++) // ...

which can infinite-loop when the unsigned int overflows.

For example, on Linux, unsigned int is 32-bit, while size_t is 64-bit, so if n = 5000000000, we get an infinite loop.

How can I get a warning about this with GCC or Clang?

GCC's -Wall -Wextra doesn't do it:

#include <stdint.h>

void f(uint64_t n)
{
    for (uint32_t i = 0; i < n; ++i) {
    }
}
gcc-13 -std=c17 \
       -Wall -Wextra -Wpedantic \
       -Warray-bounds -Wconversion \
       -fanalyzer \
       -c -o 76840686.o 76840686.c

(no output)


  • I am looking for a solution that does not require n to be a compile-time constant.
  • Ideally the solution would work on existing C/C++ projects without having to rewrite them entirely.
  • Suggesting other tools than compiler warnings would also be useful, but compiler warnings themselves would be better

Edit: Upstream compiler feature requests

Answers have indicated no such warnings exist in current compilers. I have started to file upstream feature requests:

  • GCC: Add warning flags to check against integer overflow
like image 702
nh2 Avatar asked Aug 30 '25 15:08

nh2


1 Answers

There does not appear to be a warning option built in to gcc or clang that does what is requested. However, we can use clang-query instead.

Below is a clang-query command that will report comparison of 32-bit and 64-bit integers, on the assumption that int is 32 bits and long is 64 bits. (More about that below.)

#!/bin/sh

PATH=$HOME/opt/clang+llvm-14.0.0-x86_64-linux-gnu-ubuntu-18.04/bin:$PATH

# In this query, the comments are ignored because clang-query (not the
# shell) recognizes and discards them.
query='m
  binaryOperator(                            # Find a binary operator expression
    anyOf(                                   #  such that any of:
      hasOperatorName("<"),                  #   is operator <, or
      hasOperatorName("<="),                 #   is operator <=, or
      hasOperatorName(">"),                  #   is operator >, or
      hasOperatorName(">="),                 #   is operator >=, or
      hasOperatorName("=="),                 #   is operator ==, or
      hasOperatorName("!=")                  #   is operator !=;
    ),

    hasEitherOperand(                        #  and where either operand
      implicitCastExpr(                      #   is an implicit cast
        has(                                 #    from
          expr(                              #     an expression
            hasType(                         #      whose type
              hasCanonicalType(              #       after resolving typedefs
                anyOf(                       #        is either
                  asString("int"),           #         int or
                  asString("unsigned int")   #         unsigned int,
                )
              )
            ),
            unless(                          #      unless that expression
              integerLiteral()               #       is an integer literal,
            )
          )
        ),
        hasImplicitDestinationType(          #    and to a type
          hasCanonicalType(                  #     that after typedefs
            anyOf(                           #      is either
              asString("long"),              #       long or
              asString("unsigned long")      #       unsigned long.
            )
          )
        )
      ).bind("operand")
    )
  )
'

# Run the query on test.c.
clang-query \
  -c="set bind-root false" \
  -c="$query" \
  test.c -- -w

# EOF

When run on the following test.c it reports all of the indicated cases:

// test.c
// Demonstrate reporting comparisons of different-size operands.

#include <stddef.h>          // size_t
#include <stdint.h>          // int32_t, etc.

void test(int32_t i32, int64_t i64, uint32_t u32, uint64_t u64)
{
  i32 < i32;                 // Not reported: same sizes.
  i32 < i64;                 // reported
  i64 < i64;

  u32 < u32;
  u32 < u64;                 // reported
  u64 < u64;

  i32 < u64;                 // reported
  u32 < i64;                 // reported

  i32 <= i64;                // reported

  i64 > i32;                 // reported
  i64 >= i32;                // reported

  i32 == i64;                // reported
  u64 != u32;                // reported

  i32 + i64;                 // Not reported: not a comparison operator.

  ((int64_t)i32) < i64;      // Not reported: explicit cast.

  u64 < 3;                   // Not reported: comparison with integer literal.

  // Example #1 in question.
  size_t n = 0;
  for (unsigned int i = 0; i < n; i++) {}        // reported
}

// Example #2 in question.
void f(uint64_t n)
{
  for (uint32_t i = 0; i < n; ++i) {             // reported
  }
}

// EOF

Some details about the clang-query command:

  • The command passes -w to clang-query to suppress other warnings. That's just because I wrote the test in a way that provokes warnings about unused values, and is not necessary with normal code.

  • It passes set bind-root false so the only reported site is the operand of interest rather than also reporting the entire expression.

  • Unfortunately it is not possible to have the query also print the names of the types involved. Attempting to do so with a binding causes clang-query to complain, "Matcher does not support binding."

The unsatisfying aspect of the query is it explicitly lists the source and destination types. Unfortunately, clang-query does not have a matcher to, say, report any 32-bit type, so they have to be listed individually. You might want to add [unsigned] long long on the destination side. You might also need to remove [unsigned] long if running this code with compiler options that target an IL32 platform like Windows.

Relatedly, note that clang-query accepts compiler options after the --, or alternatively in a compile_commands.json file. Unfortunately there isn't dedicated documentation of the clang-query command line, and even its --help does not mention the -- command line option. The best I can link is the documentation for libtooling, as clang-query uses that library internally for command line processing.

Finally, I'll note that I haven't done any "tuning" of this query on real code. It is likely to produce a lot of noise, and will need further tweaking. For a tutorial on how to work with clang-query, I recommend the blog post Exploring Clang Tooling Part 2: Examining the Clang AST with clang-query by Stephen Kelly. There is also the AST Matcher Reference, but the documentation there is quite terse.

like image 98
Scott McPeak Avatar answered Sep 14 '25 09:09

Scott McPeak