37

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

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:

A.L
  • 10,259
  • 10
  • 67
  • 98
nh2
  • 24,526
  • 11
  • 79
  • 128
  • 2
    Would you consider using an additional checker tool? There are products which work compiler-like on code. Some work on single code files (lint, QAC, MISRA), some work on whole linkable projects (Klocwork). They are highly configurable and I am sure that you can configure them as nosy and annoying, sorry, as thorough and in-depth, as you like. – Yunnosch Aug 05 '23 at 08:29
  • 4
    I don't accept the premiss. Is this really a *common* source of bugs? – user207421 Aug 05 '23 at 08:50
  • In C++ for this specific case and switching from `size_t n = 5000000000;` to `const size_t n = 5000000000;` you get a warning, thus the compiler can analyze and emit a warning with values that are known at compile time, but you don't get any warning for a well-defined behavior (as in your second snippet). – David Ranieri Aug 05 '23 at 09:28
  • 11
    @machine_1: You could say that about anything warnings exist for. The responsibility for writing correct code always lies on the programmer. But warnings can help with doing that. – user2357112 Aug 05 '23 at 17:36
  • 15
    @user207421 Yes, it is a common source of bugs. For example, I just [hit and fixed](https://github.com/mkazhdan/PoissonRecon/pull/269) it in the most-used algorithm for 3D surface reconstruction. I also found and fixed a similar 32-bit overflow bug in the [`glibc` `realloc()` function](https://sourceware.org/bugzilla/show_bug.cgi?id=24027), which is one of the most used functions in one of the most used libraries in the world. – nh2 Aug 05 '23 at 19:57
  • 6
    For clang, the `-Weverything` flag is useful for questions like this. It enables every possible warning, which although it is much too noisy for normal use, will at least show you if the compiler is *capable* of warning about the code; and if so, which specific `-Wxxx` option enables it. In this case, [`-Weverything` doesn't produce any relevant warnings](https://godbolt.org/z/rxofoovoc), so we can conclude that it isn't possible to get clang to warn about it. – Nate Eldredge Aug 06 '23 at 19:09
  • 2
    @user207421: You never noticed it, because loops rarely run for over 2 billion iterations, especially in unit tests, but it was there. – user2357112 Aug 07 '23 at 05:48
  • @n.m.couldbeanAI I have [submitted a bug report to GCC](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=110933) now, titled `Add warning flags to check against integer overflow`. – nh2 Aug 07 '23 at 11:01
  • 3
    @user207421 of course you don't notice it. A lot of people don't notice it either. Hence the need for a warning. – Cubic Aug 07 '23 at 12:30
  • "For example, on Linux, unsigned int is 32-bit, while size_t is 64-bit" This is NOT entirely correct. size_t is always the same size as a pointer, and the size of a pointer is 64bit on LP64 (64bit Unix) and LLP64 (64bit Windows), but is 32bit on ILP32 (32 Bit Unix and 32bit Windows), which makes it the same size as (unsigned) int on 32bit platforms. Such bugs usually can be found in code that was originally written for 32bit platforms and overlooked when ported to 64bit, where the sizeof(unsigned int) == sizeof(size_t) assumption was no longer correct. – Kaiserludi Aug 07 '23 at 18:42

6 Answers6

24

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.

Scott McPeak
  • 8,803
  • 2
  • 40
  • 79
  • This looks like a good approach. Great explanation. Some questions: (1) _"passes -w to `clang-tidy`"_ Did you also mean `clang-query` here? I cannot find documentation about the full set of flags supported for clang-query, in particular where its `--` separator semantics is explained, could you link it? (2) The main noise the current query produces on first try is false positives of integer litarals, e.g. it finds `if (__n == 1)` in GCC's `include/c++/12.2.0/bits/basic_string.h`, since `1` is upcast to a wider type (bug-free). Do you have a trick to exclude literals? – nh2 Aug 07 '23 at 10:19
  • (3) What resources do you recommend for learning to iterate on this code? Is the [AST Matcher Reference](https://clang.llvm.org/docs/LibASTMatchersReference.html) the best one? (4) Is it possible to have the match display which of the types are involved? – nh2 Aug 07 '23 at 10:21
  • @nh2 Good questions; I've edited the answer accordingly. – Scott McPeak Aug 07 '23 at 21:11
9

This does not directly answer the question (provide a warning), but would you consider an alternative which avoids the problem entirely?

    size_t n = // ...

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

It now doesn't matter what type n is, since i will always be the same type as n, you should never have trouble with infinite loops resulting from i being a smaller type or having a smaller range than n.

brhans
  • 402
  • 3
  • 10
  • 8
    `typeof` is compiler specific, but `decltype` should work everywhere. Also, `for(auto i = n - n; ...)` should work. – Simon Richter Aug 07 '23 at 01:40
  • 2
    @SimonRichter Please note that [`typeof`](https://www.iso-9899.info/n3047.html#6.7.2.5) will apparently be included in C23. In C++, yes, there's `decltype`. – Bob__ Aug 07 '23 at 11:27
8

PVS Studio can issue such warning (and many more), here is almost identical example from their docs:

https://pvs-studio.com/en/docs/warnings/v104/

It is a paid tool, but they give free license to Open Source projects.

I did not find such a warning in Clang-tidy, a free linter tool from LLVM project, but it would be very simple to add a check for comparison of integers of different sizes (a later reply by Scott McPeak with excellent clang-query did most of the work - the remaining part is just plugging this query to clang-tidy). It would be very noisy check though. One can restrict the noise by limiting the check to conditions of loops, that can be done with Clang-tidy too, but a bit more work with AST matchers.

Michael Entin
  • 7,189
  • 3
  • 21
  • 26
5

Recent versions of gcc seem to support -Warith-conversion for this purpose:

-Warith-conversion

Do warn about implicit conversions from arithmetic operations even when conversion of the operands to the same type cannot change their values. This affects warnings from -Wconversion, -Wfloat-conversion, and -Wsign-conversion.

void f (char c, int i)
{
    c = c + i; // warns with -Wconversion
    c = c + 1; // only warns with -Warith-conversion
}

Yet it does not work for your example, probably because i < n is not an arithmetic expression. There does not seem to be a variant of this warning for generic binary expressions.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
chqrlie
  • 131,814
  • 10
  • 121
  • 189
2

For C++ you may be able to do even better than a compiler warning, assuming n is a compile time constant. This also works for non-gcc compilers. This logic is not available for C code though.

The idea is basically encoding the value information in the variable type instead of the variable value.

template<std::integral T, auto N>
constexpr bool operator<(T value, std::integral_constant<decltype(N), N>)
{
    static_assert(std::is_signed_v<T> == std::is_signed_v<decltype(N)>, "the types have different signs");
    static_assert((std::numeric_limits<T>::max)() >= N, "the maximum of type T is smaller than N");
    return value < N;
}

// todo: overload with swapped operator parameter types

int main()
{
    constexpr std::integral_constant<size_t, 500'000'000> n; // go with 5'000'000'000, and you'll get a compiler error 

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

    }
}

If the value is not a compile time constant, you could still create a wrapper template type for the integer and overload the < operator for comparisons with integral values, adding the static_asserts into the body of this operator.

template<std::integral T>
class IntWrapper
{
    T m_value;
public:
    constexpr IntWrapper(T value)
        : m_value(value)
    {}

    template<std::integral U>
    friend constexpr bool operator<(U o1, IntWrapper o2)
    {
        static_assert(std::is_signed_v<U> == std::is_signed_v<T>, "types have different signedness");
        static_assert((std::numeric_limits<U>::max)() >= (std::numeric_limits<T>::max)(),
            "the comparison may never yield false because of the maxima of the types involved");

        return o1 < o2.m_value;
    }
};

void f(IntWrapper<uint64_t> n)
{
    for (uint32_t i = 0; i < n; ++i) {
    }
}

Note that the necessity of changing the type for one of the operands of the comparison operator can be both a benefit and a drawback: it requires you to modify the code, but it also allows you to apply check on a per-variable basis...

fabian
  • 80,457
  • 12
  • 86
  • 114
  • This adds complexity and overhead to the code. I think the OP was asking for gcc flag. – machine_1 Aug 05 '23 at 10:57
  • @machine_1 what overhead are you talking about? Any optimizer worthy of it's name will optimize the logic to the same instructions for both the code in the question and mine. As for the added complexity: That's a tradeoff between making this work for any standard compliant compiler vs possibly having to adjust the compiler flags for every single compiler and possibly not even having diagnostics available for some compilers. It may not be exaclty what the OP is looking for, but it may be of interest for people looking for a solution for a similar issue in the future... – fabian Aug 05 '23 at 11:08
  • @machine_1 I guess to avoid such problem is more a programmer-issue rather than the compiler's job. Maybe we can use a configure file to check the environment or something else written by ourselves, rather than depending these things on the compiler. – LiuYuan Aug 05 '23 at 11:12
  • 1
    Are you sure that it's legal to overload operators for builtin and `std` types? I'm pretty sure at least one argument must be of user defined type. – chrysante Aug 05 '23 at 14:32
  • @chrysante I skimmed though the section of the standard that seems relevant. However I didn't find this kind of restriction (I may have overlooked it though). Assuming the operator is implemented in a namespace other than `std` and sub-namespaces. It should be ok to add this implementation. – fabian Aug 06 '23 at 10:16
2

Follow a coding standard which stops it at source

Most coding standards for embedded software prohibit the use of "int", for exactly the reasons you say. The C standard requires the existence of equivalent data types of fixed lengths (8, 16 and 32 bits have been universally available for years; 64-bit is newer but still supported basically everywhere). It is good practise to use them everywhere, but in safety-related software it is normally mandatory. Many tools exist for popular coding standards such as MISRA-C which will catch these issues for you.

Your example apparently seems obscure to many commenters above, because it needs 2^32 iterations to overflow. What many modern coders forget is that int can also be 16-bit, and this made overflows much easier in the past. The Ariane-5 disaster was caused by a 64-bit value overflowing when truncated to 16-bit. The Therac-25 disaster was also partly caused by an integer overflow which cleared the error status.

Examples do exist in 32-bit too, though. Windows 95 and 98 famously crashed after 49.7 days caused by overflow of a 32-bit millisecond timer. And in 15 years time we have the Y2038 problem to look forward to. Most modern systems are Y2038-ready, but it's likely we'll get there and have some surprises!

All of these form part of the institutional history of software engineering as an engineering discipline, in the same way as the Tay Bridge and Tacoma Narrows Bridge form part of the institutional history of civil engineering. I'm somewhat shocked to see someone above saying they'd never heard of this in 40 years of C coding. It is certainly possible to be just a coder and be unaware of this, just as it is possible to be just a builder and be unaware of civil engineering principles. An engineer must be aware of the deeper principles behind their designs though. This issue, as trivial as it may seem, is one thing which clearly differentiates expertise levels between software engineers and mere coders. Given Ariane-5 and Therac-25, I make no apology for being judgemental about this.

Graham
  • 1,655
  • 9
  • 19
  • 3
    I'm surprised to hear that "_8, 16 and 32 bits have been mandatory for years_" - when did that happen? Certainly, for C11, we have (in section 7.20.1.1.3) "_**These types are optional**. However, if an implementation provides integer types with widths of 8, 16, 32, or 64 bits, no padding bits, and (for the signed types) that have a two’s complement representation, it shall define the corresponding typedef names._" (my emphasis) – Toby Speight Aug 07 '23 at 14:11
  • @TobySpeight In practise, all recent platforms use power-of-2 register lengths - and as the following paragraph says, it's mandatory for compilers to provide typedefs where the compiler implementation has power-of-2 ints (which it fairly naturally will when the underlying processor has power-of-2 registers). The last processors in widespread use to *not* use powers-of-2 registers were Microchip PICs, which are long obsolete. – Graham Aug 07 '23 at 14:54
  • 1
    I think there are popular DSPs whose smallest register size is 16 bits - they obviously can't provide (`u`)`int8_t`. That said, I don't do DSP, so I'm relying on second-hand information. Anyway, perhaps "common" would be a more accurate word than "mandatory". – Toby Speight Aug 07 '23 at 15:50
  • @TobySpeight: Ran into one of those DSPs first-hand, can confirm your comment is correct (although it's more about memory addressing granularity than register size). – Ben Voigt Aug 07 '23 at 17:39
  • 1
    @TobySpeight I can confirm they exist too - and with 32-bit registers too. The int sizes they do support have fixed-size versions. In fact the compilers give you 8-bit support too - it just bitmasks/shifts as needed. I'd be happy to say "universally available" instead of "mandatory". :) – Graham Aug 07 '23 at 18:38
  • 2
    @Graham: My recollection is that compilers for some of the more advanced TI DSPs could be configured to either use 8-bit `char` types that are inefficient to process and may be subject to race conditions when accessing adjacent `char` objects in main-line and interrupt code, or use 16-bit `char` types which are better in almost every way in situations where the larger size would be tolerable. – supercat Aug 07 '23 at 19:46