Skip to content

Reapply "[analyzer] Fix false positive in strchr/strrchr with constant args" - #212124

Open
steakhal wants to merge 2 commits into
llvm:mainfrom
steakhal:bb/reland-strchr-const-args
Open

Reapply "[analyzer] Fix false positive in strchr/strrchr with constant args"#212124
steakhal wants to merge 2 commits into
llvm:mainfrom
steakhal:bb/reland-strchr-const-args

Conversation

@steakhal

Copy link
Copy Markdown
Contributor

Relands #211857

steakhal added 2 commits July 26, 2026 15:25
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.
@llvmorg-github-actions llvmorg-github-actions Bot added clang Clang issues not falling into any other category clang:static analyzer labels Jul 26, 2026
@llvmorg-github-actions

llvmorg-github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown

@llvm/pr-subscribers-clang

@llvm/pr-subscribers-clang-static-analyzer-1

Author: Balázs Benics (steakhal)

Changes

Relands #211857


Patch is 30.17 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/212124.diff

2 Files Affected:

  • (modified) clang/lib/StaticAnalyzer/Checkers/CStringChecker.cpp (+214-26)
  • (modified) clang/test/Analysis/string-search-modeling.c (+414-1)
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..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
@@ -176,3 +185,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, ...
[truncated]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:static analyzer clang Clang issues not falling into any other category

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant