https://github.com/earnol updated https://github.com/llvm/llvm-project/pull/210154
>From 474892b4ab48116650bf305b46acc4c2bbb58946 Mon Sep 17 00:00:00 2001 From: Vladislav Aranov <[email protected]> Date: Thu, 16 Jul 2026 21:46:43 +0200 Subject: [PATCH] [analyzer] Fix false positive in strchr/strrchr with constant args When both the source string and the search target are compile-time constants, determine the outcome precisely and only emit the feasible branch (found or not-found). This eliminates false positives from core.NullPointerArithm when e.g. strrchr is used in the common FILE_BASENAME macro pattern. Handles strchr, strrchr, memchr, strstr, and strpbrk. Fixes: https://github.com/llvm/llvm-project/issues/209905 --- .../Checkers/CStringChecker.cpp | 234 ++++++++++++-- clang/test/Analysis/string-search-modeling.c | 302 ++++++++++++++++++ 2 files changed, 511 insertions(+), 25 deletions(-) diff --git a/clang/lib/StaticAnalyzer/Checkers/CStringChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/CStringChecker.cpp index 745297dd1f057..8913c99a7859e 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, @@ -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->getString(); + 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,154 @@ 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); +} + +void CStringChecker::evalStrchr(CheckerContext &C, + const CallEvent &Call) const { + CurrentFunctionDescription = "strchr()"; + auto Search = [](CheckerContext &C, const CallEvent &Call, + StringRef Haystack) -> std::optional<size_t> { + auto Ch = getSearchChar(C, Call); + if (!Ch) + return std::nullopt; + StringRef CStr = getCStr(Haystack); + return (*Ch == '\0') ? CStr.size() : CStr.find(*Ch); + }; + evalStrchrCommon(C, Call, /*CanReturnNull=*/true, + computeStringOffset(C, Call, Search)); +} + +void CStringChecker::evalStrrchr(CheckerContext &C, + const CallEvent &Call) const { + CurrentFunctionDescription = "strrchr()"; + auto Search = [](CheckerContext &C, const CallEvent &Call, + StringRef Haystack) -> std::optional<size_t> { + auto Ch = getSearchChar(C, Call); + if (!Ch) + return std::nullopt; + StringRef CStr = getCStr(Haystack); + return (*Ch == '\0') ? CStr.size() : CStr.rfind(*Ch); + }; + evalStrchrCommon(C, Call, /*CanReturnNull=*/true, + computeStringOffset(C, Call, Search)); +} + +void CStringChecker::evalStrchrnul(CheckerContext &C, + const CallEvent &Call) const { + CurrentFunctionDescription = "strchrnul()"; + auto Search = [](CheckerContext &C, const CallEvent &Call, + StringRef Haystack) -> std::optional<size_t> { + auto Ch = getSearchChar(C, Call); + if (!Ch) + return std::nullopt; + StringRef CStr = getCStr(Haystack); + return (*Ch == '\0') ? CStr.size() : CStr.find(*Ch); + }; + evalStrchrCommon(C, Call, /*CanReturnNull=*/false, + computeStringOffset(C, Call, Search)); +} + +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); + return Needle->empty() ? size_t{0} : CStr.find(*Needle); + }; + 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(*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 +2821,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 +2849,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..1ae91cadb1760 100644 --- a/clang/test/Analysis/string-search-modeling.c +++ b/clang/test/Analysis/string-search-modeling.c @@ -176,3 +176,305 @@ 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}} +} _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
