Skip to content

Commit 8d9b4ba

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 26bff2d commit 8d9b4ba

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
@@ -1687,6 +1687,63 @@ void CheckConditionImpl::alwaysTrueFalseError(const Token* tok, const Token* con
16871687
(alwaysTrue ? CWE571 : CWE570), Certainty::normal);
16881688
}
16891689

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

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

lib/checkcondition.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ class CPPCHECKLIB CheckConditionImpl : public CheckImpl {
174174
void alwaysTrueFalseError(const Token* tok, const Token* condition, const ValueFlow::Value* value);
175175

176176
void invalidTestForOverflow(const Token* tok, const ValueType *valueType, const std::string &replace);
177+
void invalidPointerOverlapTestError(const Token *tok);
177178
void pointerAdditionResultNotNullError(const Token *tok, const Token *calc);
178179

179180
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
@@ -6155,8 +6155,87 @@ class TestCondition : public TestFixture {
61556155
check("int f(int x, int y) { return x - y > x; }");
61566156
ASSERT_EQUALS(MSG("x-y>x", "y<0"), errout_str());
61576157

6158+
// x - y >= x
61586159
check("int f(int x, int y) { return x - y >= x; }");
61596160
ASSERT_EQUALS(MSG("x-y>=x", "y<=0"), errout_str());
6161+
6162+
// Pointer overlap idiom: p1 >= p2 && p1 < p2 + n (distinct pointers,
6163+
// unsigned offset). Only flagged when paired (via &&) with a comparison
6164+
// of the same two pointers -- inconclusive.
6165+
const Settings sInc = settingsBuilder(settings0).certainty(Certainty::inconclusive).build();
6166+
6167+
#undef MSG
6168+
#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"
6169+
6170+
// the memcpy_s overlap check
6171+
check("int f(const char *dest, const char *src, size_t count) {\n"
6172+
" return dest >= src && dest < src + count;\n"
6173+
"}", sInc);
6174+
ASSERT_EQUALS(MSG("32", "dest<src+count"), errout_str());
6175+
6176+
// mirror order of the two clauses
6177+
check("int f(const char *dest, const char *src, size_t count) {\n"
6178+
" return dest < src + count && dest >= src;\n"
6179+
"}", sInc);
6180+
ASSERT_EQUALS(MSG("17", "dest<src+count"), errout_str());
6181+
6182+
// offset on the left side of the comparison
6183+
check("int f(const char *dest, const char *src, size_t count) {\n"
6184+
" return dest >= src && src + count > dest;\n"
6185+
"}", sInc);
6186+
ASSERT_EQUALS(MSG("39", "src+count>dest"), errout_str());
6187+
6188+
// casts on the offending clause (as in real memcpy_s)
6189+
check("int f(void *dest, const void *src, size_t count) {\n"
6190+
" return dest >= src && (char*)dest < (char*)src + count;\n"
6191+
"}", sInc);
6192+
ASSERT_EQUALS(MSG("39", "(char*)dest<(char*)src+count"), errout_str());
6193+
6194+
// the paired comparison may use any comparison operator
6195+
check("int f(const char *dest, const char *src, size_t count) {\n"
6196+
" return dest != src && dest < src + count;\n"
6197+
"}", sInc);
6198+
ASSERT_EQUALS(MSG("32", "dest<src+count"), errout_str());
6199+
6200+
// --- negatives ---
6201+
6202+
// plain bounds check, no paired pointer comparison -> NOT flagged
6203+
check("int f(const char *p, const char *buf, size_t len) {\n"
6204+
" return p < buf + len;\n"
6205+
"}", sInc);
6206+
ASSERT_EQUALS("", errout_str());
6207+
6208+
// room check -> NOT flagged
6209+
check("int f(const char *cur, const char *limit, size_t need) {\n"
6210+
" return cur + need <= limit;\n"
6211+
"}", sInc);
6212+
ASSERT_EQUALS("", errout_str());
6213+
6214+
// && partner compares unrelated operands -> NOT flagged
6215+
check("int f(const char *p, const char *buf, size_t len, int flag) {\n"
6216+
" return flag > 0 && p < buf + len;\n"
6217+
"}", sInc);
6218+
ASSERT_EQUALS("", errout_str());
6219+
6220+
// signed offset -> NOT flagged (overflow check is meaningful)
6221+
check("int f(const char *dest, const char *src, int count) {\n"
6222+
" return dest >= src && dest < src + count;\n"
6223+
"}", sInc);
6224+
ASSERT_EQUALS("", errout_str());
6225+
6226+
// not inconclusive -> NOT reported
6227+
check("int f(const char *dest, const char *src, size_t count) {\n"
6228+
" return dest >= src && dest < src + count;\n"
6229+
"}");
6230+
ASSERT_EQUALS("", errout_str());
6231+
6232+
// same pointer var on both sides -> existing check applies, not the new one
6233+
check("int f(const char *p, size_t n) {\n"
6234+
" return p < p + n;\n"
6235+
"}", sInc);
6236+
ASSERT_EQUALS(
6237+
"[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",
6238+
errout_str());
61606239
}
61616240

61626241
void checkConditionIsAlwaysTrueOrFalseInsideIfWhile() {

0 commit comments

Comments
 (0)