[clang] Reapply "[analyzer] Fix false positive in strchr/strrchr with constant args" (PR #212124)
Balázs Benics via cfe-commits
cfe-commits at lists.llvm.org
Sun Jul 26 07:41:05 PDT 2026
https://github.com/steakhal created https://github.com/llvm/llvm-project/pull/212124
Relands #211857
>From f0f375211bafb61efa3b57d053d0ba9c05d77e48 Mon Sep 17 00:00:00 2001
From: Balazs Benics <benicsbalazs at gmail.com>
Date: Sun, 26 Jul 2026 15:25:16 +0100
Subject: [PATCH 1/2] Reapply "[analyzer] Fix false positive in strchr/strrchr
with constant args" (#211857)
This reverts commit ca78391cea0b15a1cbb141dd8830a51fb7bee47a.
---
.../Checkers/CStringChecker.cpp | 240 +++++++++--
clang/test/Analysis/string-search-modeling.c | 404 ++++++++++++++++++
2 files changed, 618 insertions(+), 26 deletions(-)
diff --git a/clang/lib/StaticAnalyzer/Checkers/CStringChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/CStringChecker.cpp
index 745297dd1f057..b47fca0e40af4 100644
--- a/clang/lib/StaticAnalyzer/Checkers/CStringChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/CStringChecker.cpp
@@ -164,23 +164,14 @@ class CStringChecker
{{CDM::CLibrary, {"strsep"}, 2}, &CStringChecker::evalStrsep},
{{CDM::CLibrary, {"strxfrm"}, 3}, &CStringChecker::evalStrxfrm},
{{CDM::CLibraryMaybeHardened, {"strchr"}, 2},
- llvm::bind_back(&CStringChecker::evalStrchrCommon, "strchr()",
- /*CanReturnNull=*/true)},
+ &CStringChecker::evalStrchr},
{{CDM::CLibraryMaybeHardened, {"strrchr"}, 2},
- llvm::bind_back(&CStringChecker::evalStrchrCommon, "strrchr()",
- /*CanReturnNull=*/true)},
+ &CStringChecker::evalStrrchr},
{{CDM::CLibraryMaybeHardened, {"memchr"}, 3},
- llvm::bind_back(&CStringChecker::evalStrchrCommon, "memchr()",
- /*CanReturnNull=*/true)},
- {{CDM::CLibrary, {"strstr"}, 2},
- llvm::bind_back(&CStringChecker::evalStrchrCommon, "strstr()",
- /*CanReturnNull=*/true)},
- {{CDM::CLibrary, {"strpbrk"}, 2},
- llvm::bind_back(&CStringChecker::evalStrchrCommon, "strpbrk()",
- /*CanReturnNull=*/true)},
- {{CDM::CLibrary, {"strchrnul"}, 2},
- llvm::bind_back(&CStringChecker::evalStrchrCommon, "strchrnul()",
- /*CanReturnNull=*/false)},
+ &CStringChecker::evalMemchr},
+ {{CDM::CLibrary, {"strstr"}, 2}, &CStringChecker::evalStrstr},
+ {{CDM::CLibrary, {"strpbrk"}, 2}, &CStringChecker::evalStrpbrk},
+ {{CDM::CLibrary, {"strchrnul"}, 2}, &CStringChecker::evalStrchrnul},
{{CDM::CLibrary, {"bcopy"}, 3}, &CStringChecker::evalBcopy},
{{CDM::CLibrary, {"bcmp"}, 3},
std::bind(&CStringChecker::evalMemcmp, _1, _2, _3, CK_Regular)},
@@ -244,8 +235,18 @@ class CStringChecker
void evalStrsep(CheckerContext &C, const CallEvent &Call) const;
+ void evalStrchr(CheckerContext &C, const CallEvent &Call) const;
+ void evalStrrchr(CheckerContext &C, const CallEvent &Call) const;
+ void evalMemchr(CheckerContext &C, const CallEvent &Call) const;
+ void evalStrstr(CheckerContext &C, const CallEvent &Call) const;
+ void evalStrpbrk(CheckerContext &C, const CallEvent &Call) const;
+ void evalStrchrnul(CheckerContext &C, const CallEvent &Call) const;
+
+ /// Shared transition logic for strchr-family functions.
+ /// ConstOffset: nullopt = unknown, npos = not found, other = exact offset.
void evalStrchrCommon(CheckerContext &C, const CallEvent &Call,
- StringRef FnName, bool CanReturnNull) const;
+ bool CanReturnNull,
+ std::optional<size_t> ConstOffset) const;
void evalStdCopy(CheckerContext &C, const CallEvent &Call) const;
void evalStdCopyBackward(CheckerContext &C, const CallEvent &Call) const;
@@ -272,6 +273,8 @@ class CStringChecker
const MemRegion *MR,
bool hypothetical);
static const StringLiteral *getStringLiteralFromRegion(const MemRegion *MR);
+ // Like getStringLiteralFromRegion, but also handles ElementRegion offsets.
+ static std::optional<StringRef> getStringRefAtRegion(const MemRegion *R);
SVal getCStringLength(CheckerContext &C,
ProgramStateRef &state,
@@ -1047,7 +1050,7 @@ CStringChecker::getStringLiteralFromRegion(const MemRegion *MR) {
return cast<StringRegion>(MR)->getStringLiteral();
case MemRegion::NonParamVarRegionKind:
if (const VarDecl *Decl = cast<NonParamVarRegion>(MR)->getDecl();
- Decl->getType().isConstQualified() && Decl->hasGlobalStorage())
+ Decl->getType().isConstQualified())
return dyn_cast_or_null<StringLiteral>(Decl->getInit());
return nullptr;
default:
@@ -1055,6 +1058,29 @@ CStringChecker::getStringLiteralFromRegion(const MemRegion *MR) {
}
}
+std::optional<StringRef>
+CStringChecker::getStringRefAtRegion(const MemRegion *R) {
+ if (!R)
+ return std::nullopt;
+ size_t Offset = 0;
+ const MemRegion *Base = R->StripCasts();
+ if (const auto *ER = dyn_cast<ElementRegion>(Base)) {
+ if (auto Idx = ER->getIndex().getAs<nonloc::ConcreteInt>()) {
+ Offset = Idx->getValue().get()->getZExtValue();
+ Base = ER->getSuperRegion()->StripCasts();
+ } else {
+ return std::nullopt;
+ }
+ }
+ const StringLiteral *Lit = getStringLiteralFromRegion(Base);
+ if (!Lit)
+ return std::nullopt;
+ StringRef S = Lit->getBytes();
+ if (Offset > S.size())
+ return std::nullopt;
+ return S.substr(Offset);
+}
+
SVal CStringChecker::getCStringLength(CheckerContext &C, ProgramStateRef &state,
const Expr *Ex, SVal Buf,
bool hypothetical) const {
@@ -2639,14 +2665,158 @@ void CStringChecker::evalStrsep(CheckerContext &C,
C.addTransition(State);
}
+/// Compute the constant search offset for strchr/strrchr/strchrnul.
+/// Try to resolve the source (first) argument to its string literal content.
+static std::optional<StringRef> getHaystack(CheckerContext &C,
+ const CallEvent &Call) {
+ ProgramStateRef State = C.getState();
+ const StackFrame *SF = C.getStackFrame();
+ SVal SrcVal = State->getSVal(Call.getArgExpr(0), SF);
+ return CStringChecker::getStringRefAtRegion(SrcVal.getAsRegion());
+}
+
+/// Get the null-terminated C string view of the haystack.
+static StringRef getCStr(StringRef Haystack) {
+ size_t NulPos = Haystack.find('\0');
+ return (NulPos != StringRef::npos) ? Haystack.substr(0, NulPos) : Haystack;
+}
+
+/// Try to extract the constant character from the second argument.
+static std::optional<char> getSearchChar(CheckerContext &C,
+ const CallEvent &Call) {
+ SValBuilder &SVB = C.getSValBuilder();
+ SVal Arg1Val = C.getState()->getSVal(Call.getArgExpr(1), C.getStackFrame());
+ const llvm::APSInt *CharInt = SVB.getKnownValue(C.getState(), Arg1Val);
+ if (!CharInt)
+ return std::nullopt;
+ return static_cast<char>(CharInt->getExtValue());
+}
+
+/// Resolve the haystack and delegate to a function-specific search lambda.
+using SearchFn = std::function<std::optional<size_t>(
+ CheckerContext &, const CallEvent &, StringRef)>;
+
+static std::optional<size_t>
+computeStringOffset(CheckerContext &C, const CallEvent &Call, SearchFn Search) {
+ auto Haystack = getHaystack(C, Call);
+ if (!Haystack)
+ return std::nullopt;
+ return Search(C, Call, *Haystack);
+}
+
+/// Search for a character in the null-terminated C string view.
+// SearchFn for strchr/strrchr/strchrnul.
+static std::optional<size_t> searchChar(CheckerContext &C,
+ const CallEvent &Call,
+ StringRef Haystack, bool Reverse,
+ bool NulOnMiss) {
+ auto Ch = getSearchChar(C, Call);
+ if (!Ch)
+ return std::nullopt;
+ StringRef CStr = getCStr(Haystack);
+ if (*Ch == '\0')
+ return CStr.size();
+ size_t Pos = Reverse ? CStr.rfind(*Ch) : CStr.find(*Ch);
+ if (Pos == StringRef::npos && NulOnMiss)
+ return CStr.size();
+ return Pos;
+}
+
+void CStringChecker::evalStrchr(CheckerContext &C,
+ const CallEvent &Call) const {
+ CurrentFunctionDescription = "strchr()";
+ evalStrchrCommon(
+ C, Call, /*CanReturnNull=*/true,
+ computeStringOffset(
+ C, Call,
+ llvm::bind_back(searchChar, /*Reverse=*/false, /*NulOnMiss=*/false)));
+}
+
+void CStringChecker::evalStrrchr(CheckerContext &C,
+ const CallEvent &Call) const {
+ CurrentFunctionDescription = "strrchr()";
+ evalStrchrCommon(
+ C, Call, /*CanReturnNull=*/true,
+ computeStringOffset(
+ C, Call,
+ llvm::bind_back(searchChar, /*Reverse=*/true, /*NulOnMiss=*/false)));
+}
+
+void CStringChecker::evalStrchrnul(CheckerContext &C,
+ const CallEvent &Call) const {
+ CurrentFunctionDescription = "strchrnul()";
+ evalStrchrCommon(
+ C, Call, /*CanReturnNull=*/false,
+ computeStringOffset(
+ C, Call,
+ llvm::bind_back(searchChar, /*Reverse=*/false, /*NulOnMiss=*/true)));
+}
+
+void CStringChecker::evalMemchr(CheckerContext &C,
+ const CallEvent &Call) const {
+ CurrentFunctionDescription = "memchr()";
+ auto Search = [](CheckerContext &C, const CallEvent &Call,
+ StringRef Haystack) -> std::optional<size_t> {
+ auto Ch = getSearchChar(C, Call);
+ if (!Ch || Call.getNumArgs() < 3)
+ return std::nullopt;
+ SValBuilder &SVB = C.getSValBuilder();
+ const llvm::APSInt *Len = SVB.getKnownValue(
+ C.getState(),
+ C.getState()->getSVal(Call.getArgExpr(2), C.getStackFrame()));
+ if (!Len)
+ return std::nullopt;
+ uint64_t N = Len->getZExtValue();
+ // Include the implicit null terminator in the searchable region.
+ SmallString<64> Buf(Haystack);
+ Buf.push_back('\0');
+ StringRef Region = StringRef(Buf.data(), Buf.size());
+ if (N > Region.size())
+ return std::nullopt;
+ return Region.substr(0, N).find(*Ch);
+ };
+ evalStrchrCommon(C, Call, /*CanReturnNull=*/true,
+ computeStringOffset(C, Call, Search));
+}
+
+void CStringChecker::evalStrstr(CheckerContext &C,
+ const CallEvent &Call) const {
+ CurrentFunctionDescription = "strstr()";
+ auto Search = [](CheckerContext &C, const CallEvent &Call,
+ StringRef Haystack) -> std::optional<size_t> {
+ SVal Arg1Val = C.getState()->getSVal(Call.getArgExpr(1), C.getStackFrame());
+ auto Needle = CStringChecker::getStringRefAtRegion(Arg1Val.getAsRegion());
+ if (!Needle)
+ return std::nullopt;
+ StringRef CStr = getCStr(Haystack);
+ StringRef CNeedle = getCStr(*Needle);
+ return CNeedle.empty() ? size_t{0} : CStr.find(CNeedle);
+ };
+ evalStrchrCommon(C, Call, /*CanReturnNull=*/true,
+ computeStringOffset(C, Call, Search));
+}
+
+void CStringChecker::evalStrpbrk(CheckerContext &C,
+ const CallEvent &Call) const {
+ CurrentFunctionDescription = "strpbrk()";
+ auto Search = [](CheckerContext &C, const CallEvent &Call,
+ StringRef Haystack) -> std::optional<size_t> {
+ SVal Arg1Val = C.getState()->getSVal(Call.getArgExpr(1), C.getStackFrame());
+ auto Accept = CStringChecker::getStringRefAtRegion(Arg1Val.getAsRegion());
+ if (!Accept)
+ return std::nullopt;
+ return getCStr(Haystack).find_first_of(getCStr(*Accept));
+ };
+ evalStrchrCommon(C, Call, /*CanReturnNull=*/true,
+ computeStringOffset(C, Call, Search));
+}
+
void CStringChecker::evalStrchrCommon(CheckerContext &C, const CallEvent &Call,
- StringRef FnName,
- bool CanReturnNull) const {
- CurrentFunctionDescription = FnName;
+ bool CanReturnNull,
+ std::optional<size_t> ConstOffset) const {
const Expr *CE = Call.getOriginExpr();
assert(CE);
- // These functions always return a pointer.
if (!CE->getType()->isPointerType())
return;
@@ -2655,21 +2825,26 @@ void CStringChecker::evalStrchrCommon(CheckerContext &C, const CallEvent &Call,
SValBuilder &SVB = C.getSValBuilder();
ASTContext &Ctx = C.getASTContext();
- // The first argument must be non-null for all functions in this family.
SourceArgExpr Src = {{Call.getArgExpr(0), 0}};
SVal SrcVal = State->getSVal(Src.Expression, SF);
State = checkNonNull(C, State, Src, SrcVal);
if (!State)
return;
- // NULL (no-match) branch.
- if (CanReturnNull) {
+ bool MustMatch = ConstOffset && *ConstOffset != StringRef::npos;
+ bool MustNotMatch = ConstOffset && *ConstOffset == StringRef::npos;
+
+ // NULL (no-match) branch — skip when the match is guaranteed.
+ if (CanReturnNull && !MustMatch) {
ProgramStateRef NullState =
State->BindExpr(CE, SF, SVB.makeNullWithType(CE->getType()));
C.addTransition(NullState);
}
- // Found branch: a pointer within the source; needs a Loc for the arithmetic.
+ // Found branch — skip when the match is impossible.
+ if (MustNotMatch)
+ return;
+
std::optional<Loc> SrcLoc = SrcVal.getAs<Loc>();
if (!SrcLoc) {
SVal Result = SVB.conjureSymbolVal(Call, C.blockCount());
@@ -2678,13 +2853,26 @@ void CStringChecker::evalStrchrCommon(CheckerContext &C, const CallEvent &Call,
return;
}
- // The result is: Src + SymOffset
+ // If we know the exact offset, use a concrete value.
+ if (MustMatch) {
+ NonLoc ConcreteOffset =
+ SVB.makeIntVal(*ConstOffset, Ctx.getSizeType()).castAs<NonLoc>();
+ SVal Result = SVB.evalBinOpLN(State, BO_Add, *SrcLoc, ConcreteOffset,
+ Src.Expression->getType());
+ State = State->BindExpr(CE, SF, Result);
+ C.addTransition(State);
+ return;
+ }
+
+ // Unknown match: use a symbolic offset constrained to be in bounds.
auto RemainingExtentBytes =
getDynamicExtentWithOffset(State, *SrcLoc).castAs<DefinedOrUnknownSVal>();
NonLoc SymOffset =
SVB.conjureSymbolVal(Call, Ctx.getSizeType(), C.blockCount())
.castAs<NonLoc>();
State = State->assumeInBound(SymOffset, RemainingExtentBytes, true);
+ if (!State)
+ return;
SVal Result = SVB.evalBinOpLN(State, BO_Add, *SrcLoc, SymOffset,
Src.Expression->getType());
diff --git a/clang/test/Analysis/string-search-modeling.c b/clang/test/Analysis/string-search-modeling.c
index a50ec439731a3..ac29425ea10a4 100644
--- a/clang/test/Analysis/string-search-modeling.c
+++ b/clang/test/Analysis/string-search-modeling.c
@@ -176,3 +176,407 @@ void no_invalidation_of_globals(const char *p) {
clang_analyzer_eval(local_unmodified == 10); // expected-warning {{TRUE}}
clang_analyzer_eval(global_unmodified == 20); // expected-warning {{TRUE}}
}
+
+//===----------------------------------------------------------------------===//
+// When both arguments are compile-time constants, only the correct branch is
+// taken: found when the target exists, null when it does not.
+// See: https://github.com/llvm/llvm-project/issues/209905
+//===----------------------------------------------------------------------===//
+
+// --- strchr / strrchr: target character IS in the literal ---
+const char *test_strrchr_const_no_fp(void) {
+ // This is the original reproducer from #209905.
+ return strrchr("/foo/bar.c", '/') ? strrchr("/foo/bar.c", '/') + 1 : "/foo/bar.c"; // no-warning
+}
+
+void test_strchr_const_found(void) {
+ clang_analyzer_eval(strchr("/foo/bar.c", '/') == 0); // expected-warning {{FALSE}}
+}
+
+void test_strrchr_const_found(void) {
+ clang_analyzer_eval(strrchr("/foo/bar.c", '/') == 0); // expected-warning {{FALSE}}
+}
+
+// --- strchr / strrchr: target character is NOT in the literal ---
+void test_strchr_const_not_found(void) {
+ clang_analyzer_eval(strchr("hello", 'z') == 0); // expected-warning {{TRUE}}
+}
+
+void test_strrchr_const_not_found(void) {
+ clang_analyzer_eval(strrchr("hello", 'z') == 0); // expected-warning {{TRUE}}
+}
+
+// --- memchr: character within bounds ---
+void test_memchr_const_found(void) {
+ clang_analyzer_eval(memchr("abcdef", 'c', 6) == 0); // expected-warning {{FALSE}}
+}
+
+// --- memchr: character beyond the specified length ---
+void test_memchr_const_not_in_range(void) {
+ clang_analyzer_eval(memchr("abcdef", 'f', 3) == 0); // expected-warning {{TRUE}}
+}
+
+// --- strstr: needle IS a substring ---
+void test_strstr_const_found(void) {
+ clang_analyzer_eval(strstr("hello world", "world") == 0); // expected-warning {{FALSE}}
+}
+
+// --- strstr: needle is NOT a substring ---
+void test_strstr_const_not_found(void) {
+ clang_analyzer_eval(strstr("hello world", "xyz") == 0); // expected-warning {{TRUE}}
+}
+
+// --- strpbrk: accept set has a match ---
+void test_strpbrk_const_found(void) {
+ clang_analyzer_eval(strpbrk("hello", "aeiou") == 0); // expected-warning {{FALSE}}
+}
+
+// --- strpbrk: no character from accept set in source ---
+void test_strpbrk_const_not_found(void) {
+ clang_analyzer_eval(strpbrk("hello", "xyz") == 0); // expected-warning {{TRUE}}
+}
+
+// --- Non-constant source: both branches must still exist ---
+void test_strchr_non_const_source(const char *p) {
+ clang_analyzer_eval(strchr(p, '/') == 0); // expected-warning {{TRUE}} expected-warning {{FALSE}}
+}
+
+// --- Various constant source forms: const, static const, #define, __FILE__ ---
+static const char static_const_path[] = "/usr/local/bin/tool";
+
+void test_strchr_static_const(void) {
+ clang_analyzer_eval(strchr(static_const_path, '/') == 0); // expected-warning {{FALSE}}
+}
+
+const char global_const_path[] = "/etc/config";
+
+void test_strrchr_global_const(void) {
+ clang_analyzer_eval(strrchr(global_const_path, '/') == 0); // expected-warning {{FALSE}}
+}
+
+#define FIXED_PATH "/home/user/project/file.c"
+
+void test_strchr_define(void) {
+ clang_analyzer_eval(strchr(FIXED_PATH, '/') == 0); // expected-warning {{FALSE}}
+}
+
+#define MY_FILE_BASENAME (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)
+
+void test_file_basename_macro(void) {
+ const char *base = MY_FILE_BASENAME; // no-warning
+ (void)base;
+}
+
+#define PREFIX "module"
+#define SUFFIX "_handler"
+// Adjacent string literal concatenation (the realistic preprocessor pattern):
+#define MODULE_PATH "/opt/" PREFIX "/" SUFFIX ".so"
+
+void test_strchr_concatenated_define(void) {
+ clang_analyzer_eval(strchr(MODULE_PATH, '/') == 0); // expected-warning {{FALSE}}
+}
+
+// --- Character argument via #define ---
+#define SEPARATOR '/'
+
+void test_strchr_define_char(void) {
+ clang_analyzer_eval(strchr("/foo/bar", SEPARATOR) == 0); // expected-warning {{FALSE}}
+}
+
+#define SEARCH_CHAR 'x'
+
+void test_strchr_define_char_not_found(void) {
+ clang_analyzer_eval(strchr("/foo/bar", SEARCH_CHAR) == 0); // expected-warning {{TRUE}}
+}
+
+// --- Edge cases: null character '\0' ---
+void test_strchr_null_char_always_found(void) {
+ // strchr(s, '\0') always finds the terminator.
+ clang_analyzer_eval(strchr("hello", '\0') == 0); // expected-warning {{FALSE}}
+}
+
+void test_strrchr_null_char_always_found(void) {
+ clang_analyzer_eval(strrchr("hello", '\0') == 0); // expected-warning {{FALSE}}
+}
+
+void test_memchr_null_char_within_bounds(void) {
+ // "abc" has terminator at index 3; searching 4 bytes includes it.
+ clang_analyzer_eval(memchr("abc", '\0', 4) == 0); // expected-warning {{FALSE}}
+}
+
+void test_memchr_null_char_out_of_bounds(void) {
+ // "abc" has terminator at index 3; searching only 3 bytes misses it.
+ clang_analyzer_eval(memchr("abc", '\0', 3) == 0); // expected-warning {{TRUE}}
+}
+
+// --- Edge cases: empty strings ---
+void test_strchr_empty_haystack(void) {
+ // Empty string only contains '\0'; '/' is not there.
+ clang_analyzer_eval(strchr("", '/') == 0); // expected-warning {{TRUE}}
+}
+
+void test_strchr_empty_haystack_null_char(void) {
+ // strchr("", '\0') finds the terminator.
+ clang_analyzer_eval(strchr("", '\0') == 0); // expected-warning {{FALSE}}
+}
+
+void test_strstr_empty_needle(void) {
+ // strstr(s, "") always returns s.
+ clang_analyzer_eval(strstr("hello", "") == 0); // expected-warning {{FALSE}}
+}
+
+void test_strpbrk_empty_accept(void) {
+ // strpbrk(s, "") never matches.
+ clang_analyzer_eval(strpbrk("hello", "") == 0); // expected-warning {{TRUE}}
+}
+
+//===----------------------------------------------------------------------===//
+// Verify exact pointer offsets when both arguments are compile-time constants.
+// The enhanced modeling returns Src + concrete_offset rather than a symbolic
+// offset, enabling precise pointer arithmetic downstream.
+//===----------------------------------------------------------------------===//
+
+// --- strchr: returns pointer to first occurrence ---
+void test_strchr_exact_offset(void) {
+ const char *s = "/foo/bar.c";
+ // '/' first appears at index 0.
+ clang_analyzer_eval(strchr(s, '/') == s); // expected-warning {{TRUE}}
+}
+
+// --- strrchr: returns pointer to last occurrence ---
+void test_strrchr_exact_offset(void) {
+ const char *s = "/foo/bar.c";
+ // '/' last appears at index 4.
+ clang_analyzer_eval(strrchr(s, '/') == s + 4); // expected-warning {{TRUE}}
+}
+
+// --- strrchr + 1: the FILE_BASENAME pattern ---
+void test_strrchr_plus_one(void) {
+ const char *s = "/foo/bar.c";
+ const char *base = strrchr(s, '/') + 1;
+ // Should point to 'b' at index 5.
+ clang_analyzer_eval(base == s + 5); // expected-warning {{TRUE}}
+}
+
+// --- strchr with null terminator: points to end of string ---
+void test_strchr_null_terminator_offset(void) {
+ const char *s = "hello";
+ // strchr(s, '\0') returns pointer to the null terminator at index 5.
+ clang_analyzer_eval(strchr(s, '\0') == s + 5); // expected-warning {{TRUE}}
+}
+
+// --- strstr: returns pointer to first substring match ---
+void test_strstr_exact_offset(void) {
+ const char *s = "hello world";
+ // "world" starts at index 6.
+ clang_analyzer_eval(strstr(s, "world") == s + 6); // expected-warning {{TRUE}}
+}
+
+// --- strstr with empty needle: returns the source pointer ---
+void test_strstr_empty_needle_offset(void) {
+ const char *s = "hello";
+ clang_analyzer_eval(strstr(s, "") == s); // expected-warning {{TRUE}}
+}
+
+// --- strpbrk: returns pointer to first matching character ---
+void test_strpbrk_exact_offset(void) {
+ const char *s = "hello";
+ // First vowel 'e' is at index 1.
+ clang_analyzer_eval(strpbrk(s, "aeiou") == s + 1); // expected-warning {{TRUE}}
+}
+
+// --- memchr: returns pointer to character within bounds ---
+void test_memchr_exact_offset(void) {
+ const char *s = "abcdef";
+ // 'c' is at index 2.
+ clang_analyzer_eval(memchr(s, 'c', 6) == s + 2); // expected-warning {{TRUE}}
+}
+
+// --- memchr with embedded null characters ---
+void test_memchr_embedded_null(void) {
+ // String literal "ab\0cd" has a null at index 2, then 'c' at 3, 'd' at 4,
+ // and the implicit terminator at index 5.
+ const char *s = "ab\0cd";
+ // memchr searching 5 bytes finds the first '\0' at index 2.
+ clang_analyzer_eval(memchr(s, '\0', 5) == s + 2); // expected-warning {{TRUE}}
+}
+
+void test_memchr_second_segment_after_null(void) {
+ const char *s = "ab\0cd";
+ // 'c' is at index 3; searching 5 bytes should find it.
+ clang_analyzer_eval(memchr(s, 'c', 5) == s + 3); // expected-warning {{TRUE}}
+}
+
+void test_memchr_char_before_null_boundary(void) {
+ const char *s = "ab\0cd";
+ // 'b' is at index 1; searching only 2 bytes still finds it.
+ clang_analyzer_eval(memchr(s, 'b', 2) == s + 1); // expected-warning {{TRUE}}
+}
+
+void test_memchr_char_hidden_by_short_len(void) {
+ const char *s = "ab\0cd";
+ // 'c' is at index 3 but searching only 3 bytes (indices 0-2) misses it.
+ clang_analyzer_eval(memchr(s, 'c', 3) == 0); // expected-warning {{TRUE}}
+}
+
+// --- strstr/strpbrk with embedded null characters ---
+void test_strstr_hidden_by_null(void) {
+ const char *s = "ab\0cd";
+ // C strstr stops at the first null; "cd" is unreachable.
+ clang_analyzer_eval(strstr(s, "cd") == 0); // expected-warning {{TRUE}}
+}
+
+void test_strpbrk_hidden_by_null(void) {
+ const char *s = "ab\0cd";
+ // C strpbrk stops at the first null; 'c' is unreachable.
+ clang_analyzer_eval(strpbrk(s, "cd") == 0); // expected-warning {{TRUE}}
+}
+
+void test_strstr_before_null(void) {
+ const char *s = "1ab\0cd";
+ // "ab" is before the null, so strstr finds it at offset 1.
+ clang_analyzer_eval(strstr(s, "ab") == s + 1); // expected-warning {{TRUE}}
+}
+
+void test_strpbrk_before_null(void) {
+ const char *s = "ab\0cd";
+ // 'a' is before the null, so strpbrk finds it.
+ clang_analyzer_eval(strpbrk(s, "a") == s); // expected-warning {{TRUE}}
+}
+
+// --- memchr with pointer past embedded null ---
+void test_memchr_pointer_past_null(void) {
+ const char *s = "1ab\0cdf\0qwrt";
+ // Starting from s+3 ("\0cdf\0qwrt"), search for 'd' in 4 bytes.
+ clang_analyzer_eval(memchr(s + 3, 'd', 4) == s + 5); // expected-warning {{TRUE}}
+}
+
+// --- Second argument with pointer offset ---
+void test_strstr_needle_with_offset(void) {
+ const char *needles = "xxworld";
+ // needles + 2 is "world"; strstr finds it at index 6.
+ clang_analyzer_eval(strstr("hello world", needles + 2) == 0); // expected-warning {{FALSE}}
+}
+
+void test_strpbrk_accept_with_offset(void) {
+ const char *chars = "xxaeiou";
+ // chars + 2 is "aeiou"; first vowel 'e' in "hello" is at index 1.
+ clang_analyzer_eval(strpbrk("hello", chars + 2) == 0); // expected-warning {{FALSE}}
+}
+
+// --- Both arguments with pointer offsets ---
+void test_strstr_both_offsets(void) {
+ const char *s = "XXhello world";
+ const char *needles = "xxworld";
+ // s+2 is "hello world", needles+2 is "world"; found at offset 6 from s+2.
+ clang_analyzer_eval(strstr(s + 2, needles + 2) == s + 8); // expected-warning {{TRUE}}
+}
+
+void test_strpbrk_both_offsets(void) {
+ const char *s = "XXhello";
+ const char *chars = "xxaeiou";
+ // s+2 is "hello", chars+2 is "aeiou"; first vowel 'e' at offset 1 from s+2.
+ clang_analyzer_eval(strpbrk(s + 2, chars + 2) == s + 3); // expected-warning {{TRUE}}
+}
+
+// --- strchrnul: always returns non-null (pointer to found char or terminator)
+void test_strchrnul_not_found_points_to_terminator(void) {
+ const char *s = "hello";
+ // strchrnul(s, 'z') should return s + 5 (the null terminator).
+ clang_analyzer_eval(strchrnul(s, 'z') == s + 5); // expected-warning {{TRUE}}
+}
+
+void test_strchrnul_found_negative(void) {
+ const char *s = "xxuhello";
+ const char *ss = s + 3;
+ // 'u' is at index 1 in "xuhello", so strchrnul(ss-2, 'u') == s + 1 + 1 == s + 2.
+ clang_analyzer_eval(strchrnul(ss - 2, 'u') == s + 2); // expected-warning {{TRUE}}
+}
+
+void test_strchrnul_found_with_offset(void) {
+ const char *s = "xxxhello";
+ const char *ss = s + 1;
+ // ss is "xxhello", 'l' is at index 4 in "xxhello", so ss + 4 == s + 5.
+ clang_analyzer_eval(strchrnul(ss, 'l') == s + 5); // expected-warning {{TRUE}}
+}
+
+// --- Embedded null in the needle/accept set argument ---
+void test_strstr_needle_embedded_null(void) {
+ // Needle "cd\0e" is truncated to "cd" by C semantics; "cd" is in "abcd".
+ clang_analyzer_eval(strstr("abcd", "cd\0e") == 0); // expected-warning {{FALSE}}
+}
+
+void test_strpbrk_accept_embedded_null(void) {
+ // Accept "a\0b" is truncated to "a"; 'a' is not in "xb", so result is null.
+ clang_analyzer_eval(strpbrk("xb", "a\0b") == 0); // expected-warning {{TRUE}}
+}
+
+// --- Wide string cast to char*: resolved via raw bytes ---
+const __CHAR16_TYPE__ wide_str_global[] = u"abc";
+void test_strchr_wide_string_global(void) {
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+ // LE: bytes are 'a',0,'b',0,... — CStr is "a", strchr finds 'a' at offset 0.
+ clang_analyzer_eval(strchr((const char *)wide_str_global, 'a') == (const char *)wide_str_global); // expected-warning {{TRUE}}
+#else
+ // BE: bytes are 0,'a',0,'b',... — first byte is null, CStr is empty.
+ clang_analyzer_eval(strchr((const char *)wide_str_global, 'a') == 0); // expected-warning {{TRUE}}
+#endif
+}
+
+void test_strchr_wide_string_local(void) {
+ const __CHAR16_TYPE__ w[] = u"abc";
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+ // LE: same as global — finds 'a' at offset 0.
+ clang_analyzer_eval(strchr((const char *)w, 'a') == (const char *)w); // expected-warning {{TRUE}}
+#else
+ // BE: first byte is null, CStr is empty.
+ clang_analyzer_eval(strchr((const char *)w, 'a') == 0); // expected-warning {{TRUE}}
+#endif
+}
+
+// --- Wide string with 4-byte characters (UTF-32) ---
+const __CHAR32_TYPE__ wide32_global[] = U"abc";
+void test_strchr_wide32_string(void) {
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+ // LE: U"abc" bytes are 'a',0,0,0,'b',0,0,0,... CStr is "a".
+ clang_analyzer_eval(strchr((const char *)wide32_global, 'a') == (const char *)wide32_global); // expected-warning {{TRUE}}
+#else
+ // BE: bytes are 0,0,0,'a',... — first byte is null, CStr is empty.
+ clang_analyzer_eval(strchr((const char *)wide32_global, 'a') == 0); // expected-warning {{TRUE}}
+#endif
+}
+
+// --- Wide string as needle argument ---
+const __CHAR16_TYPE__ wide_needle[] = u"lo";
+void test_strstr_wide_needle(void) {
+ const char *s = "hello";
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+ // LE: u"lo" bytes are 'l',0,'o',0,0,0 — getCStr gives "l".
+ // strstr("hello", "l") finds 'l' at offset 2.
+ clang_analyzer_eval(strstr(s, (const char *)wide_needle) == s + 2); // expected-warning {{TRUE}}
+#else
+ // BE: bytes are 0,'l',0,'o',... — CStr is empty, strstr returns haystack.
+ clang_analyzer_eval(strstr(s, (const char *)wide_needle) == s); // expected-warning {{TRUE}}
+#endif
+}
+
+// --- Struct cast to char* ---
+struct FourChars {
+ char a, b, c, d;
+};
+void test_strchr_struct_cast(void) {
+ // TODO: resolve const struct initializers (no padding between char members).
+ struct FourChars s = {'h', 'e', 'l', 'l'};
+ clang_analyzer_eval(strchr((const char *)&s, 'e') == 0); // expected-warning {{TRUE}} expected-warning {{FALSE}}
+}
+
+// --- Union with wide and narrow access ---
+union CharUnion {
+ __CHAR16_TYPE__ w[4];
+ char c[8];
+};
+const union CharUnion cu = { .w = u"abc" };
+void test_strchr_union_narrow_access(void) {
+ // TODO: resolve union members with known initializers.
+ clang_analyzer_eval(strchr(cu.c, 'a') == 0); // expected-warning {{TRUE}} expected-warning {{FALSE}}
+}
>From 63a8321d296596c062f3c07692fd34abef3f55de Mon Sep 17 00:00:00 2001
From: Balazs Benics <benicsbalazs at gmail.com>
Date: Sun, 26 Jul 2026 15:32:18 +0100
Subject: [PATCH 2/2] [analyzer][NFC] Pin string-search-modeling.c to -std=c17
The test uses u"" and U"" string literals, which require C11 or later.
The RUN line didn't specify a standard, so it picked up the target
default. That is gnu17 on most targets, but PS4 defaults to gnu99
(see getDefaultLanguageStandard in clang/lib/Basic/LangStandards.cpp),
so the test failed on the SIE buildbot with "use of undeclared
identifier 'u'".
Reproduce with:
clang -cc1 -triple x86_64-scei-ps4 -analyze -verify ...
Pin the RUN line to -std=c17, and add a second RUN line that explicitly
uses the PS4 triple, so that losing the -std= again is caught on every
bot instead of only on the SIE ones.
---
clang/test/Analysis/string-search-modeling.c | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/clang/test/Analysis/string-search-modeling.c b/clang/test/Analysis/string-search-modeling.c
index ac29425ea10a4..931f1bcbd526d 100644
--- a/clang/test/Analysis/string-search-modeling.c
+++ b/clang/test/Analysis/string-search-modeling.c
@@ -1,4 +1,13 @@
-// RUN: %clang_analyze_cc1 -verify %s \
+// The u"" and U"" string literals below need C11 or later. Pin the standard
+// because targets such as PS4 default to gnu99.
+// RUN: %clang_analyze_cc1 -std=c17 -verify %s \
+// RUN: -analyzer-checker=core,unix \
+// RUN: -analyzer-checker=debug.ExprInspection \
+// RUN: -analyzer-config eagerly-assume=false
+//
+// Also check a target whose default C standard is gnu99, so that dropping the
+// -std= above is caught everywhere instead of only on the SIE buildbots.
+// RUN: %clang_analyze_cc1 -triple x86_64-scei-ps4 -std=c17 -verify %s \
// RUN: -analyzer-checker=core,unix \
// RUN: -analyzer-checker=debug.ExprInspection \
// RUN: -analyzer-config eagerly-assume=false
More information about the cfe-commits
mailing list