1 | //===--- SuspiciousMemsetUsageCheck.cpp - clang-tidy-----------------------===// |
2 | // |
3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
4 | // See https://llvm.org/LICENSE.txt for license information. |
5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
6 | // |
7 | //===----------------------------------------------------------------------===// |
8 | |
9 | #include "SuspiciousMemsetUsageCheck.h" |
10 | #include "clang/AST/ASTContext.h" |
11 | #include "clang/ASTMatchers/ASTMatchFinder.h" |
12 | #include "clang/ASTMatchers/ASTMatchers.h" |
13 | #include "clang/Lex/Lexer.h" |
14 | #include "clang/Tooling/FixIt.h" |
15 | |
16 | using namespace clang::ast_matchers; |
17 | |
18 | namespace clang::tidy::bugprone { |
19 | |
20 | void SuspiciousMemsetUsageCheck::registerMatchers(MatchFinder *Finder) { |
21 | // Match the standard memset: |
22 | // void *memset(void *buffer, int fill_char, size_t byte_count); |
23 | auto MemsetDecl = |
24 | functionDecl(hasName(Name: "::memset" ), |
25 | parameterCountIs(N: 3), |
26 | hasParameter(N: 0, InnerMatcher: hasType(InnerMatcher: pointerType(pointee(voidType())))), |
27 | hasParameter(N: 1, InnerMatcher: hasType(InnerMatcher: isInteger())), |
28 | hasParameter(N: 2, InnerMatcher: hasType(InnerMatcher: isInteger()))); |
29 | |
30 | // Look for memset(x, '0', z). Probably memset(x, 0, z) was intended. |
31 | Finder->addMatcher( |
32 | NodeMatch: callExpr( |
33 | callee(InnerMatcher: MemsetDecl), argumentCountIs(N: 3), |
34 | hasArgument(N: 1, InnerMatcher: characterLiteral(equals(Value: static_cast<unsigned>('0'))) |
35 | .bind(ID: "char-zero-fill" )), |
36 | unless(hasArgument( |
37 | N: 0, InnerMatcher: anyOf(hasType(InnerMatcher: pointsTo(InnerMatcher: isAnyCharacter())), |
38 | hasType(InnerMatcher: arrayType(hasElementType(isAnyCharacter()))))))), |
39 | Action: this); |
40 | |
41 | // Look for memset with an integer literal in its fill_char argument. |
42 | // Will check if it gets truncated. |
43 | Finder->addMatcher( |
44 | NodeMatch: callExpr(callee(InnerMatcher: MemsetDecl), argumentCountIs(N: 3), |
45 | hasArgument(N: 1, InnerMatcher: integerLiteral().bind(ID: "num-fill" ))), |
46 | Action: this); |
47 | |
48 | // Look for memset(x, y, 0) as that is most likely an argument swap. |
49 | Finder->addMatcher( |
50 | NodeMatch: callExpr(callee(InnerMatcher: MemsetDecl), argumentCountIs(N: 3), |
51 | unless(hasArgument(N: 1, InnerMatcher: anyOf(characterLiteral(equals( |
52 | Value: static_cast<unsigned>('0'))), |
53 | integerLiteral())))) |
54 | .bind(ID: "call" ), |
55 | Action: this); |
56 | } |
57 | |
58 | void SuspiciousMemsetUsageCheck::check(const MatchFinder::MatchResult &Result) { |
59 | if (const auto *CharZeroFill = |
60 | Result.Nodes.getNodeAs<CharacterLiteral>(ID: "char-zero-fill" )) { |
61 | // Case 1: fill_char of memset() is a character '0'. Probably an |
62 | // integer zero was intended. |
63 | |
64 | SourceRange CharRange = CharZeroFill->getSourceRange(); |
65 | auto Diag = |
66 | diag(Loc: CharZeroFill->getBeginLoc(), Description: "memset fill value is char '0', " |
67 | "potentially mistaken for int 0" ); |
68 | |
69 | // Only suggest a fix if no macros are involved. |
70 | if (CharRange.getBegin().isMacroID()) |
71 | return; |
72 | Diag << FixItHint::CreateReplacement( |
73 | RemoveRange: CharSourceRange::getTokenRange(R: CharRange), Code: "0" ); |
74 | } |
75 | |
76 | else if (const auto *NumFill = |
77 | Result.Nodes.getNodeAs<IntegerLiteral>(ID: "num-fill" )) { |
78 | // Case 2: fill_char of memset() is larger in size than an unsigned char |
79 | // so it gets truncated during conversion. |
80 | |
81 | const auto UCharMax = (1 << Result.Context->getCharWidth()) - 1; |
82 | Expr::EvalResult EVResult; |
83 | if (!NumFill->EvaluateAsInt(EVResult, *Result.Context)) |
84 | return; |
85 | |
86 | llvm::APSInt NumValue = EVResult.Val.getInt(); |
87 | if (NumValue >= 0 && NumValue <= UCharMax) |
88 | return; |
89 | |
90 | diag(Loc: NumFill->getBeginLoc(), Description: "memset fill value is out of unsigned " |
91 | "character range, gets truncated" ); |
92 | } |
93 | |
94 | else if (const auto *Call = Result.Nodes.getNodeAs<CallExpr>(ID: "call" )) { |
95 | // Case 3: byte_count of memset() is zero. This is most likely an |
96 | // argument swap. |
97 | |
98 | const Expr *FillChar = Call->getArg(Arg: 1); |
99 | const Expr *ByteCount = Call->getArg(Arg: 2); |
100 | |
101 | // Return if `byte_count` is not zero at compile time. |
102 | Expr::EvalResult Value2; |
103 | if (ByteCount->isValueDependent() || |
104 | !ByteCount->EvaluateAsInt(Result&: Value2, Ctx: *Result.Context) || |
105 | Value2.Val.getInt() != 0) |
106 | return; |
107 | |
108 | // Return if `fill_char` is known to be zero or negative at compile |
109 | // time. In these cases, swapping the args would be a nop, or |
110 | // introduce a definite bug. The code is likely correct. |
111 | Expr::EvalResult EVResult; |
112 | if (!FillChar->isValueDependent() && |
113 | FillChar->EvaluateAsInt(Result&: EVResult, Ctx: *Result.Context)) { |
114 | llvm::APSInt Value1 = EVResult.Val.getInt(); |
115 | if (Value1 == 0 || Value1.isNegative()) |
116 | return; |
117 | } |
118 | |
119 | // `byte_count` is known to be zero at compile time, and `fill_char` is |
120 | // either not known or known to be a positive integer. Emit a warning |
121 | // and fix-its to swap the arguments. |
122 | auto D = diag(Loc: Call->getBeginLoc(), |
123 | Description: "memset of size zero, potentially swapped arguments" ); |
124 | StringRef RHSString = tooling::fixit::getText(Node: *ByteCount, Context: *Result.Context); |
125 | StringRef LHSString = tooling::fixit::getText(Node: *FillChar, Context: *Result.Context); |
126 | if (LHSString.empty() || RHSString.empty()) |
127 | return; |
128 | |
129 | D << tooling::fixit::createReplacement(Destination: *FillChar, Source: RHSString) |
130 | << tooling::fixit::createReplacement(Destination: *ByteCount, Source: LHSString); |
131 | } |
132 | } |
133 | |
134 | } // namespace clang::tidy::bugprone |
135 | |