Skip to content

Commit f3e691b

Browse files
authored
Merge pull request #2075 from zlaski-semmle/zlaski/cpp434
[CPP-434] Detect signed overflow checks
2 parents b812a03 + 4ea8569 commit f3e691b

File tree

12 files changed

+301
-2
lines changed

12 files changed

+301
-2
lines changed

change-notes/1.23/analysis-cpp.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ The following changes in version 1.23 affect C/C++ analysis in all applications.
99
| **Query** | **Tags** | **Purpose** |
1010
|-----------------------------|-----------|--------------------------------------------------------------------|
1111
| Hard-coded Japanese era start date (`cpp/japanese-era/exact-era-date`) | reliability, japanese-era | This query is a combination of two old queries that were identical in purpose but separate as an implementation detail. This new query replaces Hard-coded Japanese era start date in call (`cpp/japanese-era/constructor-or-method-with-exact-era-date`) and Hard-coded Japanese era start date in struct (`cpp/japanese-era/struct-with-exact-era-date`). |
12+
| Signed overflow check (`cpp/signed-overflow-check`) | correctness, reliability | Finds overflow checks that rely on signed integer addition to overflow, which has undefined behavior. Example: `a + b < a`. |
1213

1314
## Changes to existing queries
1415

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
bool foo(int n1, unsigned short delta) {
2+
return n1 + delta < n1; // BAD
3+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
bool bar(unsigned short n1, unsigned short delta) {
2+
// NB: Comparison is always false
3+
return n1 + delta < n1; // GOOD (but misleading)
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#include <limits.h>
2+
bool foo(int n1, unsigned short delta) {
3+
return n1 > INT_MAX - delta; // GOOD
4+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
bool bar(unsigned short n1, unsigned short delta) {
2+
return (unsigned short)(n1 + delta) < n1; // GOOD
3+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>
7+
When checking for integer overflow, you may often write tests like
8+
<code>a + b &lt; a</code>. This works fine if <code>a</code> or
9+
<code>b</code> are unsigned integers, since any overflow in the addition
10+
will cause the value to simply "wrap around." However, using
11+
<i>signed</i> integers is problematic because signed overflow has undefined
12+
behavior according to the C and C++ standards. If the addition overflows
13+
and has an undefined result, the comparison will likewise be undefined;
14+
it may produce an unintended result, or may be deleted entirely by an
15+
optimizing compiler.
16+
</p>
17+
</overview>
18+
<recommendation>
19+
<p>
20+
Solutions to this problem can be thought of as falling into one of two
21+
categories: (1) rewrite the signed expression so that overflow cannot occur
22+
but the signedness remains, or (2) rewrite (or cast) the signed expression
23+
into unsigned form.
24+
</p>
25+
26+
<p>
27+
Below we list examples of expressions where signed overflow may
28+
occur, along with proposed solutions. The list should not be
29+
considered exhaustive.
30+
</p>
31+
32+
<p>
33+
Given <code>unsigned short i, delta</code> and <code>i + delta &lt; i</code>,
34+
it is possible to rewrite it as <code>(unsigned short)(i + delta)&nbsp;&lt;&nbsp;i</code>.
35+
Note that <code>i + delta</code>does not actually overflow, due to <code>int</code> promotion
36+
</p>
37+
38+
<p>
39+
Given <code>unsigned short i, delta</code> and <code>i + delta &lt; i</code>,
40+
it is also possible to rewrite it as <code>USHORT_MAX - delta</code>. It must be true
41+
that <code>delta &gt; 0</code> and the <code>limits.h</code> or <code>climits</code>
42+
header has been included.
43+
</p>
44+
45+
<p>
46+
Given <code>int i, delta</code> and <code>i + delta &lt; i</code>,
47+
it is possible to rewrite it as <code>INT_MAX - delta</code>. It must be true
48+
that <code>delta &gt; 0</code> and the <code>limits.h</code> or <code>climits</code>
49+
header has been included.
50+
</p>
51+
52+
<p>
53+
Given <code>int i, delta</code> and <code>i + delta &lt; i</code>,
54+
it is also possible to rewrite it as <code>(unsigned)i + delta &lt; i</code>.
55+
Note that program semantics are affected by this change.
56+
</p>
57+
58+
<p>
59+
Given <code>int i, delta</code> and <code>i + delta &lt; i</code>,
60+
it is also possible to rewrite it as <code>unsigned int i, delta</code> and
61+
<code>i + delta &lt; i</code>. Note that program semantics are
62+
affected by this change.
63+
</p>
64+
</recommendation>
65+
66+
<example>
67+
<p>
68+
In the following example, even though <code>delta</code> has been declared
69+
<code>unsigned short</code>, C/C++ type promotion rules require that its
70+
type is promoted to the larger type used in the addition and comparison,
71+
namely a <code>signed int</code>. Addition is performed on
72+
signed integers, and may have undefined behavior if an overflow occurs.
73+
As a result, the entire (comparison) expression may also have an undefined
74+
result.
75+
</p>
76+
<sample src="SignedOverflowCheck-bad1.cpp" />
77+
<p>
78+
The following example builds upon the previous one. Instead of
79+
performing an addition (which could overflow), we have re-framed the
80+
solution so that a subtraction is used instead. Since <code>delta</code>
81+
is promoted to a <code>signed int</code> and <code>INT_MAX</code> denotes
82+
the largest possible positive value for an <code>signed int</code>,
83+
the expression <code>INT_MAX - delta</code> can never be less than zero
84+
or more than <code>INT_MAX</code>. Hence, any overflow and underflow
85+
are avoided.
86+
</p>
87+
<sample src="SignedOverflowCheck-good1.cpp" />
88+
<p>
89+
In the following example, even though both <code>n</code> and <code>delta</code>
90+
have been declared <code>unsigned short</code>, both are promoted to
91+
<code>signed int</code> prior to addition. Because we started out with the
92+
narrower <code>short</code> type, the addition is guaranteed not to overflow
93+
and is therefore defined. But the fact that <code>n1 + delta</code> never
94+
overflows means that the condition <code>n1 + delta &lt; n1</code> will never
95+
hold true, which likely is not what the programmer intended. (see also the
96+
<code>cpp/bad-addition-overflow-check</code> query).
97+
</p>
98+
<sample src="SignedOverflowCheck-bad2.cpp" />
99+
<p>
100+
The next example provides a solution to the previous one. Even though
101+
<code>i + delta</code> does not overflow, casting it to an
102+
<code>unsigned short</code> truncates the addition modulo 2^16,
103+
so that <code>unsigned short</code> "wrap around" may now be observed.
104+
Furthermore, since the left-hand side is now of type <code>unsigned short</code>,
105+
the right-hand side does not need to be promoted to a <code>signed int</code>.
106+
</p>
107+
108+
<sample src="SignedOverflowCheck-good2.cpp" />
109+
</example>
110+
<references>
111+
<li><a href="http://c-faq.com/expr/preservingrules.html">comp.lang.c FAQ list · Question 3.19 (Preserving rules)</a></li>
112+
<li><a href="https://wiki.sei.cmu.edu/confluence/display/c/INT31-C.+Ensure+that+integer+conversions+do+not+result+in+lost+or+misinterpreted+data">INT31-C. Ensure that integer conversions do not result in lost or misinterpreted data</a></li>
113+
<li>W. Dietz, P. Li, J. Regehr, V. Adve. <a href="https://www.cs.utah.edu/~regehr/papers/overflow12.pdf">Understanding Integer Overflow in C/C++</a></li>
114+
</references>
115+
</qhelp>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* @name Undefined result of signed test for overflow
3+
* @description Testing for overflow by adding a value to a variable
4+
* to see if it "wraps around" works only for
5+
* unsigned integer values.
6+
* @kind problem
7+
* @problem.severity warning
8+
* @precision high
9+
* @id cpp/signed-overflow-check
10+
* @tags reliability
11+
* security
12+
*/
13+
14+
import cpp
15+
private import semmle.code.cpp.valuenumbering.GlobalValueNumbering
16+
private import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis
17+
18+
from RelationalOperation ro, AddExpr add, Expr expr1, Expr expr2
19+
where
20+
ro.getAnOperand() = add and
21+
add.getAnOperand() = expr1 and
22+
ro.getAnOperand() = expr2 and
23+
globalValueNumber(expr1) = globalValueNumber(expr2) and
24+
add.getUnspecifiedType().(IntegralType).isSigned() and
25+
not exists(MacroInvocation mi | mi.getAnAffectedElement() = add) and
26+
exprMightOverflowPositively(add) and
27+
exists(Compilation c | c.getAFileCompiled() = ro.getFile() |
28+
not c.getAnArgument() = "-fwrapv" and
29+
not c.getAnArgument() = "-fno-strict-overflow"
30+
)
31+
select ro, "Testing for signed overflow may produce undefined results."
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
| SignedOverflowCheck.cpp:35:9:35:23 | ... < ... | Bad overflow check. |
2+
| SignedOverflowCheck.cpp:113:12:113:66 | ... < ... | Bad overflow check. |
13
| test.cpp:3:11:3:19 | ... < ... | Bad overflow check. |
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Signed-comparison tests
2+
3+
/* 1. Signed-signed comparison. The semantics are undefined. */
4+
bool cannotHoldAnother8(int n1) {
5+
// clang 8.0.0 -O2: deleted (silently)
6+
// gcc 9.2 -O2: deleted (silently)
7+
// msvc 19.22 /O2: not deleted
8+
return n1 + 8 < n1; // BAD
9+
}
10+
11+
/* 2. Signed comparison with a narrower unsigned type. The narrower
12+
type gets promoted to the (signed) larger type, and so the
13+
semantics are undefined. */
14+
bool cannotHoldAnotherUShort(int n1, unsigned short delta) {
15+
// clang 8.0.0 -O2: deleted (silently)
16+
// gcc 9.2 -O2: deleted (silently)
17+
// msvc 19.22 /O2: not deleted
18+
return n1 + delta < n1; // BAD
19+
}
20+
21+
/* 3. Signed comparison with a non-narrower unsigned type. The
22+
signed type gets promoted to (a possibly wider) unsigned type,
23+
and the resulting comparison is unsigned. */
24+
bool cannotHoldAnotherUInt(int n1, unsigned int delta) {
25+
// clang 8.0.0 -O2: not deleted
26+
// gcc 9.2 -O2: not deleted
27+
// msvc 19.22 /O2: not deleted
28+
return n1 + delta < n1; // GOOD
29+
}
30+
31+
bool shortShort1(unsigned short n1, unsigned short delta) {
32+
33+
// BAD [BadAdditionOverflowCheck.ql]
34+
// GOOD [SigneOverflowCheck.ql]: Test always fails, but will never overflow.
35+
return n1 + delta < n1;
36+
}
37+
38+
bool shortShort2(unsigned short n1, unsigned short delta) {
39+
// clang 8.0.0 -O2: not deleted
40+
// gcc 9.2 -O2: not deleted
41+
// msvc 19.22 /O2: not deleted
42+
return (unsigned short)(n1 + delta) < n1; // GOOD
43+
}
44+
45+
/* Distinguish `varname` from `ptr->varname` and `obj.varname` */
46+
struct N {
47+
int n1;
48+
} n, *np;
49+
50+
bool shortStruct1(unsigned short n1, unsigned short delta) {
51+
return np->n1 + delta < n1; // GOOD
52+
}
53+
54+
bool shortStruct1a(unsigned short n1, unsigned short delta) {
55+
return n1 + delta < n.n1; // GOOD
56+
}
57+
58+
bool shortStruct2(unsigned short n1, unsigned short delta) {
59+
return (unsigned short)(n1 + delta) < n.n1; // GOOD
60+
}
61+
62+
struct se {
63+
int xPos;
64+
short yPos;
65+
short xSize;
66+
short ySize;
67+
};
68+
69+
extern se *getSo(void);
70+
71+
bool func1(se *so) {
72+
se *o = getSo();
73+
if (so->xPos + so->xSize < so->xPos // BAD
74+
|| so->xPos > o->xPos + o->xSize) { // GOOD
75+
// clang 8.0.0 -O2: not deleted
76+
// gcc 9.2 -O2: not deleted
77+
// msvc 19.22 /O2: not deleted
78+
return false;
79+
}
80+
return true;
81+
}
82+
83+
bool checkOverflow3(unsigned int a, unsigned short b) {
84+
return (a + b < a); // GOOD
85+
}
86+
87+
struct C {
88+
unsigned int length;
89+
};
90+
91+
int checkOverflow4(unsigned int ioff, C c) {
92+
// not deleted by gcc or clang
93+
if ((int)(ioff + c.length) < (int)ioff) return 0; // GOOD
94+
return 1;
95+
}
96+
97+
int overflow12(int n) {
98+
// not deleted by gcc or clang
99+
return (n + 32 <= (unsigned)n? -1: 1); // BAD: n + 32 can overflow
100+
}
101+
102+
bool multipleCasts(char x) {
103+
104+
// BAD [UNDETECTED - BadAdditionOverflowCheck.ql]
105+
// GOOD [SigneOverflowCheck.ql]: Test always fails, but will never overflow.
106+
return (int)(unsigned short)x + 2 < (int)(unsigned short)x; // GOOD: cannot overflow
107+
}
108+
109+
bool multipleCasts2(char x) {
110+
111+
// BAD [BadAdditionOverflowCheck.ql]
112+
// GOOD [SigneOverflowCheck.ql]: Test always fails, but will never overflow.
113+
return (int)(unsigned short)(x + '1') < (int)(unsigned short)x;
114+
}
115+
116+
int does_it_overflow(int n1, unsigned short delta) {
117+
return n1 + (unsigned)delta < n1; // GOOD: everything converted to unsigned
118+
}
119+
120+
int overflow12b(int n) {
121+
// not deleted by gcc or clang
122+
return ((unsigned)(n + 32) <= (unsigned)n? -1: 1); // BAD: n + 32 may overflow
123+
}
124+
125+
#define MACRO(E1, E2) (E1) <= (E2)? -1: 1
126+
127+
int overflow12_macro(int n) {
128+
return MACRO((unsigned)(n + 32), (unsigned)n); // GOOD: inside a macro expansion
129+
}
130+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
| SignedOverflowCheck.cpp:8:12:8:22 | ... < ... | Testing for signed overflow may produce undefined results. |
2+
| SignedOverflowCheck.cpp:18:12:18:26 | ... < ... | Testing for signed overflow may produce undefined results. |
3+
| SignedOverflowCheck.cpp:73:6:73:36 | ... < ... | Testing for signed overflow may produce undefined results. |
4+
| SignedOverflowCheck.cpp:99:10:99:30 | ... <= ... | Testing for signed overflow may produce undefined results. |
5+
| SignedOverflowCheck.cpp:122:10:122:42 | ... <= ... | Testing for signed overflow may produce undefined results. |

0 commit comments

Comments
 (0)