C++: Apply my own review comments from #2218

This commit is contained in:
Jonas Jensen
2019-11-12 15:31:27 +01:00
parent 18cc539c8d
commit bd08c64933
6 changed files with 66 additions and 52 deletions

View File

@@ -1,3 +1,3 @@
bool not_in_range(T *ptr, size_t a) {
return ptr + a < ptr; // BAD
bool not_in_range(T *ptr, T *ptr_end, size_t a) {
return ptr + a >= ptr_end || ptr + a < ptr; // BAD
}

View File

@@ -1,3 +1,3 @@
bool in_range(T *ptr, T *ptr_end, size_t a) {
return a < ptr_end - ptr; // GOOD
bool not_in_range(T *ptr, T *ptr_end, size_t a) {
return a >= ptr_end - ptr; // GOOD
}

View File

@@ -4,16 +4,18 @@
<qhelp>
<overview>
<p>
When checking for out-of-range pointer values, we might write tests like
<code>p + a &lt; p</code> and check if the value "wraps around".
Such a test is wrong in that it relies on the overflow of <code>p + a</code>,
which has undefined behavior. In fact, many optimizing compilers will remove
<code>p + a &lt; p</code> altogether and replace it with the value <code>0</code>
(<code>false</code>). Conversely, should <code>p + a</code> <i>not</i> overflow,
the programmer may erroneously assume that the memory location <code>p + a</code>
is accessible to the program, even though it may be inaccessible (protected by
the operating system) or nonexistent; writing to such memory, if allowed, can
lead to memory corruption.
The expression <code>ptr + a &lt; ptr</code> is equivalent to <code>a &lt;
0</code>, and an optimizing compiler is likely to make that replacement,
thereby removing a range check that might have been necessary for security.
If `a` is known to be non-negative, the compiler can even replace <code>ptr +
a &lt; ptr</code> with <code>false</code>.
</p>
<p>
The reason is that pointer arithmetic overflow in C/C++ is undefined
behavior. The optimizing compiler can assume that the program has no
undefined behavior, which means that adding a positive number to `ptr` cannot
produce a pointer less than `ptr`.
</p>
</overview>
<recommendation>
@@ -21,30 +23,42 @@ lead to memory corruption.
To check whether an index <code>a</code> is less than the length of an array,
simply compare these two numbers as unsigned integers: <code>a &lt; ARRAY_LENGTH</code>.
If the length of the array is defined as the difference between two pointers
<code>p</code> and <code>p_end</code>, write <code>a &lt; p_end - p</code>.
<code>ptr</code> and <code>p_end</code>, write <code>a &lt; p_end - ptr</code>.
If a is <code>signed</code>, cast it to <code>unsigned</code>
in order to guard against negative <code>a</code>. For example, write
<code>(size_t)a &lt; p_end - p</code>.</p>
<code>(size_t)a &lt; p_end - ptr</code>.
</p>
</recommendation>
<example>
<p>
In the first example, an index <code>a</code>is being added to a pointer <code>p</code>
to an array with elements of type <code>T</code>. Since we are not provided a
separate pointer pointing to the end of the array, we fall back
on a check for address "wrap-around" to see if <code>p + a</code> points at
valid memory. This scheme does not work, unfortunately, since the
value of <code>p + a</code> is undefined if it points at invalid memory (for
example, outside our array). Even if <code>p + a</code> were to point to
some accessible memory location, it would almost certainly lie
<i>outside</i> the bounds of the array.
An invalid check for pointer overflow is most often seen as part of checking
whether a number <code>a</code> is too large by checking first if adding the
number to <code>ptr</code> goes past the end of an allocation and then
checking if adding it to <code>ptr</code> creates a pointer so large that it
overflows and wraps around.
</p>
<sample src="PointerWrapAround-bad.cpp" />
<p>
The next example shows how to properly check for an out-of-range pointer.
In order to do so, we need to obtain the value <code>ptr_end</code>
representing the end of the array (or the address immediately past
the end). We can then express the condition <code>p + a &lt; p_end</code> as a
difference of two pointers, even if <code>p + a</code> happens to be undefined.
In both of these checks, the operations are performed in the wrong order.
First, an expression that may lead to undefined behavior is evaluated
(<code>ptr + a</code>), and then the result is checked for being in range.
But once undefined behavior has happened in the pointer addition, it cannot
be recovered from: it's too late to perform the range check after a possible
pointer overflow.
</p>
<p>
While it's not the subject of this query, the expression <code>ptr + a &lt;
ptr_end</code> is also an invalid range check. It's undefined behavor in
C/C++ to create a pointer that points more than one past the end of an
allocation.
</p>
<p>
The next example shows how to portably check whether a number is outside the
range of an allocation between <code>ptr</code> and <code>ptr_end</code>.
</p>
<sample src="PointerWrapAround-good.cpp" />
</example>

View File

@@ -1,13 +1,12 @@
/**
* @name Reliance on pointer wrap-around
* @description Testing for out-of-range pointers by adding a value to a pointer
* @description Adding a value to a pointer
* to see if it "wraps around" is dangerous because it relies
* on undefined behavior and may lead to attempted use of
* nonexistent or inaccessible memory locations.
* on undefined behavior and may lead to memory corruption.
* @kind problem
* @problem.severity error
* @precision high
* @id cpp/pointer-overflow-check
* @id cpp/pointer-wrap-around
* @tags reliability
* security
*/
@@ -15,22 +14,18 @@
import cpp
private import semmle.code.cpp.valuenumbering.GlobalValueNumbering
class AssertMacro extends Macro {
AssertMacro() {
getName() = "assert" or
getName() = "_ASSERT" or
getName() = "_ASSERTE" or
getName() = "_ASSERT_EXPR"
}
}
from RelationalOperation ro, PointerAddExpr add, Expr expr1, Expr expr2
where
ro.getAnOperand() = add and
add.getAnOperand() = expr1 and
ro.getAnOperand() = expr2 and
globalValueNumber(expr1) = globalValueNumber(expr2) and
// Exclude macros except for assert macros.
// TODO: port that location-based macro check we have in another query. Then
// we don't need to special-case on names.
not exists(MacroInvocation mi |
mi.getAnAffectedElement() = add and not mi.getMacro() instanceof AssertMacro
mi.getAnAffectedElement() = add and
not mi.getMacroName().toLowerCase().matches("%assert%")
)
select ro, "Pointer out-of-range check relying on pointer overflow is undefined."
// TODO: Add a check for -fno-strict-overflow and -fwrapv-pointer
select ro, "Range check relying on pointer overflow."

View File

@@ -1,2 +1,2 @@
| test.cpp:6:12:6:33 | ... < ... | Pointer out-of-range check relying on pointer overflow is undefined. |
| test.cpp:32:12:32:24 | ... < ... | Pointer out-of-range check relying on pointer overflow is undefined. |
| test.cpp:6:12:6:33 | ... < ... | Range check relying on pointer overflow. |
| test.cpp:33:9:33:21 | ... < ... | Range check relying on pointer overflow. |

View File

@@ -21,17 +21,22 @@ struct Q {
void foo(int untrusted_int) {
Q q;
if (q.begin() + untrusted_int > q.end() || // GOOD
if (q.begin() + untrusted_int > q.end() || // GOOD (for the purpose of this test)
q.begin() + untrusted_int < q.begin()) // BAD [NOT DETECTED]
throw q;
}
typedef unsigned long long size_t;
typedef unsigned long size_t;
bool not_in_range(Q *ptr, size_t a) {
return ptr + a < ptr; // BAD
bool not_in_range_bad(Q *ptr, Q *ptr_end, size_t a) {
return ptr + a >= ptr_end || // GOOD (for the purpose of this test)
ptr + a < ptr; // BAD
}
bool not_in_range_good(Q *ptr, Q *ptr_end, size_t a) {
return a >= ptr_end - ptr; // GOOD
}
bool in_range(Q *ptr, Q *ptr_end, size_t a) {
return a < ptr_end - ptr; // GOOD
}
}