Skip to content

Commit fcbffc1

Browse files
committed
Add check: invalidPointerOverlapTest for unsafe pointer overflow comparison
Add a new inconclusive warning that detects the pointer overlap idiom p1 >= p2 && p1 < p2 + n (or its mirror / cast variants) where p1 and p2 are distinct pointers and n is an unsigned offset, e.g. the memcpy_s overlap check '(dest >= src && dest < src + count) || ...'. The 'p1 < p2 + n' clause assumes 'p2 + n' does not overflow past the end of the object. If n is large enough that 'p2 + n' wraps (UB), some mainstream compilers assume the wrap cannot happen and fold the comparison, silently dropping the check. The remedy in user code is to cast the pointers to uintptr_t and compare in integer space, with an explicit overflow guard. To avoid false positives, the warning only fires when the comparison is paired (via &&) with another comparison of the same two pointers, which identifies the overlap idiom; plain bounds checks like 'p < buf + len' and room checks like 'cur + need <= limit' are not flagged. The check is gated behind --enable=inconclusive. The same-pointer case 'p < p + n' is left to the existing invalidTestForOverflow check. This pattern was found in a real memcpy_s overlap check via fuzzing with UndefinedBehaviorSanitizer; the check is added so similar issues can be caught statically in the future. Signed-off-by: Tomasz Leman <tomasz.m.leman@intel.com>
1 parent 7a7c28c commit fcbffc1

3 files changed

Lines changed: 178 additions & 0 deletions

File tree

lib/checkcondition.cpp

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1688,6 +1688,63 @@ void CheckCondition::alwaysTrueFalseError(const Token* tok, const Token* conditi
16881688
(alwaysTrue ? CWE571 : CWE570), Certainty::normal);
16891689
}
16901690

1691+
// Strip enclosing casts so that `(char *)p` and `p` compare equal.
1692+
static const Token* skipPointerCast(const Token* tok)
1693+
{
1694+
while (tok && tok->isCast() && tok->astOperand1())
1695+
tok = tok->astOperand1();
1696+
return tok;
1697+
}
1698+
1699+
// Does `cmp` compare the same two pointers as p1/p2 (cast-stripped, either order)?
1700+
static bool comparesSamePointers(const Token* cmp, const Token* p1, const Token* p2, const Settings& settings)
1701+
{
1702+
if (!cmp || !cmp->isComparisonOp() || !cmp->isBinaryOp())
1703+
return false;
1704+
const Token* a = skipPointerCast(cmp->astOperand1());
1705+
const Token* b = skipPointerCast(cmp->astOperand2());
1706+
if (!a || !b)
1707+
return false;
1708+
return (isSameExpression(true, a, p1, settings, true, false) &&
1709+
isSameExpression(true, b, p2, settings, true, false)) ||
1710+
(isSameExpression(true, a, p2, settings, true, false) &&
1711+
isSameExpression(true, b, p1, settings, true, false));
1712+
}
1713+
1714+
// Search a condition subtree for a comparison of the same two pointers p1/p2.
1715+
static bool hasPairedPointerCompare(const Token* tok, const Token* p1, const Token* p2, const Settings& settings)
1716+
{
1717+
if (!tok)
1718+
return false;
1719+
if (comparesSamePointers(tok, p1, p2, settings))
1720+
return true;
1721+
return hasPairedPointerCompare(tok->astOperand1(), p1, p2, settings) ||
1722+
hasPairedPointerCompare(tok->astOperand2(), p1, p2, settings);
1723+
}
1724+
1725+
// Recognize the pointer overlap idiom: p1 >= p2 && p1 < p2 + n (or mirror).
1726+
// `cmpTok` is the `p1 < p2 + n` comparison, `p1` the bare pointer operand and
1727+
// `p2` the pointer inside the `+`. We require a sibling comparison (under &&) of
1728+
// the *same two distinct pointers*; a plain bounds check `p < buf + len` has no
1729+
// such sibling and is therefore not flagged.
1730+
static bool isOverlapIdiom(const Token* cmpTok, const Token* p1, const Token* p2, const Settings& settings)
1731+
{
1732+
const Token* p1base = skipPointerCast(p1);
1733+
const Token* p2base = skipPointerCast(p2);
1734+
if (!p1base || !p2base)
1735+
return false;
1736+
// distinct pointers only (the p<p+n case is handled by invalidTestForOverflow)
1737+
if (isSameExpression(true, p1base, p2base, settings, true, false))
1738+
return false;
1739+
const Token* child = cmpTok;
1740+
for (const Token* parent = cmpTok->astParent(); parent && parent->str() == "&&"; child = parent, parent = parent->astParent()) {
1741+
const Token* other = (parent->astOperand1() == child) ? parent->astOperand2() : parent->astOperand1();
1742+
if (hasPairedPointerCompare(other, p1base, p2base, settings))
1743+
return true;
1744+
}
1745+
return false;
1746+
}
1747+
16911748
void CheckCondition::checkInvalidTestForOverflow()
16921749
{
16931750
// Interesting blogs:
@@ -1772,10 +1829,50 @@ void CheckCondition::checkInvalidTestForOverflow()
17721829
continue;
17731830
}
17741831
}
1832+
1833+
// Pointer overlap idiom: p1 >= p2 && p1 < p2 + n (or the mirror)
1834+
// Example (memcpy_s): (dest >= src && dest < src + count) || ...
1835+
// The `p1 < p2 + n` clause assumes `p2 + n` does not overflow past
1836+
// the end of the object. If `n` is large enough that `p2 + n` wraps
1837+
// (UB), compilers may fold the comparison and drop the check.
1838+
// We only warn when the comparison is paired (via &&) with another
1839+
// comparison of the *same two pointers*, which identifies the overlap
1840+
// idiom and avoids flagging plain bounds checks like `p < buf + len`.
1841+
if (isPointer && lhs->str() == "+" && mSettings->certainty.isEnabled(Certainty::inconclusive)) {
1842+
const Token *sibling = lhs->astSibling();
1843+
if (sibling && sibling->valueType() && sibling->valueType()->pointer > 0) {
1844+
const Token *ptrOp = nullptr;
1845+
const Token *idxOp = nullptr;
1846+
for (const Token *op : exprTokens) {
1847+
if (!op || !op->valueType())
1848+
continue;
1849+
if (op->valueType()->pointer > 0 && ptrOp == nullptr)
1850+
ptrOp = op;
1851+
else if (op->valueType()->pointer == 0 &&
1852+
op->valueType()->isIntegral() &&
1853+
op->valueType()->sign == ValueType::Sign::UNSIGNED &&
1854+
idxOp == nullptr)
1855+
idxOp = op;
1856+
}
1857+
if (ptrOp && idxOp && isOverlapIdiom(tok, sibling, ptrOp, *mSettings))
1858+
invalidPointerOverlapTestError(tok);
1859+
}
1860+
}
17751861
}
17761862
}
17771863
}
17781864

1865+
void CheckCondition::invalidPointerOverlapTestError(const Token *tok)
1866+
{
1867+
const std::string expr = (tok ? tok->expressionString() : std::string("p1 < p2 + n"));
1868+
const std::string errmsg =
1869+
"Invalid test for overflow '" + expr + "'; pointer overflow is undefined behavior. "
1870+
"The offset added to the pointer may be large enough to overflow, and some mainstream "
1871+
"compilers assume such overflow cannot happen and fold the comparison. Cast the pointers "
1872+
"to uintptr_t and compare in integer space instead.";
1873+
reportError(tok, Severity::warning, "invalidPointerOverlapTest", errmsg, uncheckedErrorConditionCWE, Certainty::inconclusive);
1874+
}
1875+
17791876
void CheckCondition::invalidTestForOverflow(const Token* tok, const ValueType *valueType, const std::string &replace)
17801877
{
17811878
const std::string expr = (tok ? tok->expressionString() : std::string("x + c < x"));
@@ -2136,6 +2233,7 @@ void CheckCondition::getErrorMessages(ErrorLogger *errorLogger, const Settings *
21362233
c.clarifyConditionError(nullptr, true, false);
21372234
c.alwaysTrueFalseError(nullptr, nullptr, nullptr);
21382235
c.invalidTestForOverflow(nullptr, nullptr, "false");
2236+
c.invalidPointerOverlapTestError(nullptr);
21392237
c.pointerAdditionResultNotNullError(nullptr, nullptr);
21402238
c.duplicateConditionalAssignError(nullptr, nullptr);
21412239
c.assignmentInCondition(nullptr);

lib/checkcondition.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ class CPPCHECKLIB CheckCondition : public Check {
149149
void alwaysTrueFalseError(const Token* tok, const Token* condition, const ValueFlow::Value* value);
150150

151151
void invalidTestForOverflow(const Token* tok, const ValueType *valueType, const std::string &replace);
152+
void invalidPointerOverlapTestError(const Token *tok);
152153
void pointerAdditionResultNotNullError(const Token *tok, const Token *calc);
153154

154155
void duplicateConditionalAssignError(const Token *condTok, const Token* assignTok, bool isRedundant = false);

test/testcondition.cpp

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6154,8 +6154,87 @@ class TestCondition : public TestFixture {
61546154
check("int f(int x, int y) { return x - y > x; }");
61556155
ASSERT_EQUALS(MSG("x-y>x", "y<0"), errout_str());
61566156

6157+
// x - y >= x
61576158
check("int f(int x, int y) { return x - y >= x; }");
61586159
ASSERT_EQUALS(MSG("x-y>=x", "y<=0"), errout_str());
6160+
6161+
// Pointer overlap idiom: p1 >= p2 && p1 < p2 + n (distinct pointers,
6162+
// unsigned offset). Only flagged when paired (via &&) with a comparison
6163+
// of the same two pointers -- inconclusive.
6164+
const Settings sInc = settingsBuilder(settings0).certainty(Certainty::inconclusive).build();
6165+
6166+
#undef MSG
6167+
#define MSG(LOC, EXPR) "[test.cpp:2:" LOC "]: (warning, inconclusive) Invalid test for overflow '" EXPR "'; pointer overflow is undefined behavior. The offset added to the pointer may be large enough to overflow, and some mainstream compilers assume such overflow cannot happen and fold the comparison. Cast the pointers to uintptr_t and compare in integer space instead. [invalidPointerOverlapTest]\n"
6168+
6169+
// the memcpy_s overlap check
6170+
check("int f(const char *dest, const char *src, size_t count) {\n"
6171+
" return dest >= src && dest < src + count;\n"
6172+
"}", sInc);
6173+
ASSERT_EQUALS(MSG("32", "dest<src+count"), errout_str());
6174+
6175+
// mirror order of the two clauses
6176+
check("int f(const char *dest, const char *src, size_t count) {\n"
6177+
" return dest < src + count && dest >= src;\n"
6178+
"}", sInc);
6179+
ASSERT_EQUALS(MSG("17", "dest<src+count"), errout_str());
6180+
6181+
// offset on the left side of the comparison
6182+
check("int f(const char *dest, const char *src, size_t count) {\n"
6183+
" return dest >= src && src + count > dest;\n"
6184+
"}", sInc);
6185+
ASSERT_EQUALS(MSG("39", "src+count>dest"), errout_str());
6186+
6187+
// casts on the offending clause (as in real memcpy_s)
6188+
check("int f(void *dest, const void *src, size_t count) {\n"
6189+
" return dest >= src && (char*)dest < (char*)src + count;\n"
6190+
"}", sInc);
6191+
ASSERT_EQUALS(MSG("39", "(char*)dest<(char*)src+count"), errout_str());
6192+
6193+
// the paired comparison may use any comparison operator
6194+
check("int f(const char *dest, const char *src, size_t count) {\n"
6195+
" return dest != src && dest < src + count;\n"
6196+
"}", sInc);
6197+
ASSERT_EQUALS(MSG("32", "dest<src+count"), errout_str());
6198+
6199+
// --- negatives ---
6200+
6201+
// plain bounds check, no paired pointer comparison -> NOT flagged
6202+
check("int f(const char *p, const char *buf, size_t len) {\n"
6203+
" return p < buf + len;\n"
6204+
"}", sInc);
6205+
ASSERT_EQUALS("", errout_str());
6206+
6207+
// room check -> NOT flagged
6208+
check("int f(const char *cur, const char *limit, size_t need) {\n"
6209+
" return cur + need <= limit;\n"
6210+
"}", sInc);
6211+
ASSERT_EQUALS("", errout_str());
6212+
6213+
// && partner compares unrelated operands -> NOT flagged
6214+
check("int f(const char *p, const char *buf, size_t len, int flag) {\n"
6215+
" return flag > 0 && p < buf + len;\n"
6216+
"}", sInc);
6217+
ASSERT_EQUALS("", errout_str());
6218+
6219+
// signed offset -> NOT flagged (overflow check is meaningful)
6220+
check("int f(const char *dest, const char *src, int count) {\n"
6221+
" return dest >= src && dest < src + count;\n"
6222+
"}", sInc);
6223+
ASSERT_EQUALS("", errout_str());
6224+
6225+
// not inconclusive -> NOT reported
6226+
check("int f(const char *dest, const char *src, size_t count) {\n"
6227+
" return dest >= src && dest < src + count;\n"
6228+
"}");
6229+
ASSERT_EQUALS("", errout_str());
6230+
6231+
// same pointer var on both sides -> existing check applies, not the new one
6232+
check("int f(const char *p, size_t n) {\n"
6233+
" return p < p + n;\n"
6234+
"}", sInc);
6235+
ASSERT_EQUALS(
6236+
"[test.cpp:2:14]: (warning) Invalid test for overflow 'p<p+n'; pointer overflow is undefined behavior. Some mainstream compilers remove such overflow tests when optimising the code and assume it's always true. [invalidTestForOverflow]\n",
6237+
errout_str());
61596238
}
61606239

61616240
void checkConditionIsAlwaysTrueOrFalseInsideIfWhile() {

0 commit comments

Comments
 (0)