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 faeture requests
Answers have indicated no such warnings exist in current compilers. I have started to file upstream feature requests:
11
6 Answers
6
Reset to default
Highest score (default)
Trending (recent votes count more)
Date modified (newest first)
Date created (oldest first)
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,
)
)
)
)
),
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.
// 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
toclang-tidy
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.
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.
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.
2
-
This looks like a good approach. Great explanation. Some questions: (1) "passes -w to
clang-tidy
" Did you also meanclang-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 findsif (__n == 1)
in GCC'sinclude/c++/12.2.0/bits/basic_string.h
, since1
is upcast to a wider type (bug-free). Do you have a trick to exclude literals?– nh21 hour ago
-
(3) What resources do you recommend for learning to iterate on this code? Is the AST Matcher Reference the best one? (4) Is it possible to have the match display which of the types are involved?
– nh21 hour ago
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
.
3
-
1
This is the kind of educational frame challenge that I love SO for. Nicely done.
– Wayne Conrad17 hours ago
-
3
typeof
is compiler specific, butdecltype
should work everywhere. Also,for(auto i = n - n; ...)
should work.– Simon Richter10 hours ago
-
@SimonRichter Please note that
typeof
will apparently be included in C23. In C++, yes, there'sdecltype
.– Bob__33 mins ago
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.
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.
0
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_assert
s 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…
5
-
This adds complexity and overhead to the code. I think the OP was asking for gcc flag.
– machine_12 days ago
-
@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…
– fabian2 days ago
-
@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.
– LiuYuan2 days ago
-
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.– chrysanteyesterday
-
@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.– fabianyesterday
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 mandatory 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.
Your Answer
Sign up or log in
Post as a guest
Required, but never shown
Post as a guest
Required, but never shown
By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.
Not the answer you're looking for? Browse other questions tagged
or ask your own question.
or ask your own question.
I don't accept the premiss. Is this really a common source of bugs?
2 days ago
@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.
yesterday
@user207421 Yes, it is a common source of bugs. For example, I just hit and fixed 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, which is one of the most used functions in one of the most used libraries in the world.yesterday
Submit bug reports to gcc clang and msvc teams?
yesterday
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, so we can conclude that it isn't possible to get clang to warn about it.16 hours ago
|
Show 6 more comments