Skip to content

Commit 2dff88d

Browse files
committed
[-Wunsafe-buffer-usage] Warn string_view constructions that not guarantee null-termination
One could use `string_view` as the safe view over strings in read-only scenarios as it is hardened, preserving bounds info and guaranteeing null-termination with the new warning. If one finds the warning being too restrictive, turn it off with `-Wno-unsafe-buffer-usage-in-string-view`. The rest of the warnings will not assume null-termination guarantee of `string_view` as well then.
1 parent 0ccb5a8 commit 2dff88d

File tree

8 files changed

+216
-19
lines changed

8 files changed

+216
-19
lines changed

clang/include/clang/Analysis/Analyses/UnsafeBufferUsage.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,16 @@ class UnsafeBufferUsageHandler {
147147
virtual bool isSafeBufferOptOut(const SourceLocation &Loc) const = 0;
148148

149149
/// \return true iff unsafe uses in containers should NOT be reported at
150-
/// `Loc`; false otherwise.
150+
/// `Loc`; false otherwise. Controlled by `-Wunsafe-buffer-usage-in-container`
151151
virtual bool
152152
ignoreUnsafeBufferInContainer(const SourceLocation &Loc) const = 0;
153153

154+
/// \return true iff unsafe uses in string_views should NOT be reported at
155+
/// `Loc`; false otherwise. Controlled by
156+
/// `-Wunsafe-buffer-usage-in-string-view`
157+
virtual bool
158+
ignoreUnsafeBufferInStringView(const SourceLocation &Loc) const = 0;
159+
154160
virtual std::string
155161
getUnsafeBufferUsageAttributeTextAt(SourceLocation Loc,
156162
StringRef WSSuffix = "") const = 0;

clang/include/clang/Analysis/Analyses/UnsafeBufferUsageGadgets.def

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818
#define WARNING_GADGET(name) GADGET(name)
1919
#endif
2020

21+
/// Make `WARNING_LIBC_GADGET` self a subset of WARNING_GADGET so that it can be
22+
/// tuned specially according to whether we warn `StringViewUnsafeConstructor`.
23+
#ifndef WARNING_LIBC_GADGET
24+
#define WARNING_LIBC_GADGET(name) WARNING_GADGET(name)
25+
#endif
26+
2127
/// A `WARNING_GADGET` subset, where the code pattern of each gadget
2228
/// corresponds uses of a (possibly hardened) contatiner (e.g., `std::span`).
2329
#ifndef WARNING_CONTAINER_GADGET
@@ -38,8 +44,9 @@ WARNING_GADGET(PointerArithmetic)
3844
WARNING_GADGET(UnsafeBufferUsageAttr)
3945
WARNING_GADGET(UnsafeBufferUsageCtorAttr)
4046
WARNING_GADGET(DataInvocation)
41-
WARNING_GADGET(UnsafeLibcFunctionCall)
47+
WARNING_LIBC_GADGET(UnsafeLibcFunctionCall)
4248
WARNING_CONTAINER_GADGET(SpanTwoParamConstructor) // Uses of `std::span(arg0, arg1)`
49+
WARNING_CONTAINER_GADGET(StringViewUnsafeConstructor) // `std::string_view` constructed from std::string guarantees null-termination
4350
FIXABLE_GADGET(ULCArraySubscript) // `DRE[any]` in an Unspecified Lvalue Context
4451
FIXABLE_GADGET(DerefSimplePtrArithFixable)
4552
FIXABLE_GADGET(PointerDereference)
@@ -53,5 +60,7 @@ FIXABLE_GADGET(PointerInit)
5360

5461
#undef FIXABLE_GADGET
5562
#undef WARNING_GADGET
63+
#undef WARNING_LIBC_GADGET
5664
#undef WARNING_CONTAINER_GADGET
65+
#undef WARNING_STRINGVIEW_GADGET
5766
#undef GADGET

clang/include/clang/Basic/DiagnosticGroups.td

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1551,7 +1551,8 @@ def HLSLAvailability : DiagGroup<"hlsl-availability">;
15511551
def ReadOnlyPlacementChecks : DiagGroup<"read-only-types">;
15521552

15531553
// Warnings and fixes to support the "safe buffers" programming model.
1554-
def UnsafeBufferUsageInContainer : DiagGroup<"unsafe-buffer-usage-in-container">;
1554+
def UnsafeBufferUsageInStringView : DiagGroup<"unsafe-buffer-usage-in-string-view">;
1555+
def UnsafeBufferUsageInContainer : DiagGroup<"unsafe-buffer-usage-in-container", [UnsafeBufferUsageInStringView]>;
15551556
def UnsafeBufferUsage : DiagGroup<"unsafe-buffer-usage", [UnsafeBufferUsageInContainer]>;
15561557

15571558
// Warnings and notes related to the function effects system underlying

clang/include/clang/Basic/DiagnosticSemaKinds.td

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12388,6 +12388,9 @@ def note_safe_buffer_usage_suggestions_disabled : Note<
1238812388
def warn_unsafe_buffer_usage_in_container : Warning<
1238912389
"the two-parameter std::span construction is unsafe as it can introduce mismatch between buffer size and the bound information">,
1239012390
InGroup<UnsafeBufferUsageInContainer>, DefaultIgnore;
12391+
def warn_unsafe_buffer_usage_in_string_view : Warning<
12392+
"construct string_view from raw pointers does not guarantee null-termination, construct from std::string instead">,
12393+
InGroup<UnsafeBufferUsageInStringView>, DefaultIgnore;
1239112394
#ifndef NDEBUG
1239212395
// Not a user-facing diagnostic. Useful for debugging false negatives in
1239312396
// -fsafe-buffer-usage-suggestions (i.e. lack of -Wunsafe-buffer-usage fixits).

clang/lib/Analysis/UnsafeBufferUsage.cpp

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -247,11 +247,16 @@ AST_MATCHER_P(Stmt, notInSafeBufferOptOut, const UnsafeBufferUsageHandler *,
247247
return !Handler->isSafeBufferOptOut(Node.getBeginLoc());
248248
}
249249

250-
AST_MATCHER_P(Stmt, ignoreUnsafeBufferInContainer,
250+
AST_MATCHER_P(Stmt, ignoreSpanTwoParamConstructor,
251251
const UnsafeBufferUsageHandler *, Handler) {
252252
return Handler->ignoreUnsafeBufferInContainer(Node.getBeginLoc());
253253
}
254254

255+
AST_MATCHER_P(Stmt, ignoreStringViewUnsafeConstructor,
256+
const UnsafeBufferUsageHandler *, Handler) {
257+
return Handler->ignoreUnsafeBufferInStringView(Node.getBeginLoc());
258+
}
259+
255260
AST_MATCHER_P(CastExpr, castSubExpr, internal::Matcher<Expr>, innerMatcher) {
256261
return innerMatcher.matches(*Node.getSubExpr(), Finder, Builder);
257262
}
@@ -448,7 +453,8 @@ AST_MATCHER(ArraySubscriptExpr, isSafeArraySubscript) {
448453
return false;
449454
}
450455

451-
AST_MATCHER(CallExpr, isUnsafeLibcFunctionCall) {
456+
AST_MATCHER_P(CallExpr, isUnsafeLibcFunctionCall, internal::Matcher<Expr>,
457+
hasUnsafeStringView) {
452458
static const std::set<StringRef> PredefinedNames{
453459
// numeric conversion:
454460
"atof",
@@ -525,7 +531,9 @@ AST_MATCHER(CallExpr, isUnsafeLibcFunctionCall) {
525531
// checks for `printf`s:
526532
struct FuncNameMatch {
527533
const CallExpr *const Call;
528-
FuncNameMatch(const CallExpr *Call) : Call(Call) {}
534+
const bool hasUnsafeStringView;
535+
FuncNameMatch(const CallExpr *Call, bool hasUnsafeStringView)
536+
: Call(Call), hasUnsafeStringView(hasUnsafeStringView) {}
529537

530538
// For a name `S` in `PredefinedNames` or a member of the printf/scanf
531539
// family, define matching function names with `S` by the grammar below:
@@ -605,27 +613,35 @@ AST_MATCHER(CallExpr, isUnsafeLibcFunctionCall) {
605613

606614
// A pointer type expression is known to be null-terminated, if it has the
607615
// form: E.c_str(), for any expression E of `std::string` type.
608-
static bool isNullTermPointer(const Expr *Ptr) {
616+
static bool isNullTermPointer(const Expr *Ptr,
617+
bool UnsafeStringView = true) {
609618
if (isa<StringLiteral>(Ptr->IgnoreParenImpCasts()))
610619
return true;
611620
if (auto *MCE = dyn_cast<CXXMemberCallExpr>(Ptr->IgnoreParenImpCasts())) {
612621
const CXXMethodDecl *MD = MCE->getMethodDecl();
613622
const CXXRecordDecl *RD = MCE->getRecordDecl()->getCanonicalDecl();
614623

615-
if (MD && RD && RD->isInStdNamespace())
624+
if (MD && RD && RD->isInStdNamespace()) {
616625
if (MD->getName() == "c_str" && RD->getName() == "basic_string")
617626
return true;
627+
if (!UnsafeStringView && MD->getName() == "data" &&
628+
RD->getName() == "basic_string_view")
629+
return true;
630+
}
618631
}
619632
return false;
620633
}
621634

622635
// Check safe patterns for printfs w.r.t their prefixes:
623636
bool isUnsafePrintf(StringRef Prefix /* empty, 'k', 'f', 's', or 'sn' */) {
624-
auto AnyUnsafeStrPtr = [](const Expr *Arg) -> bool {
625-
return Arg->getType()->isPointerType() && !isNullTermPointer(Arg);
637+
const bool hasUnsafeStringView = this->hasUnsafeStringView;
638+
auto AnyUnsafeStrPtr = [&hasUnsafeStringView](const Expr *Arg) -> bool {
639+
return Arg->getType()->isPointerType() &&
640+
!isNullTermPointer(Arg, hasUnsafeStringView);
626641
};
627642

628-
if (Prefix.empty() || Prefix == "k") // printf: all pointer args should be null-terminated
643+
if (Prefix.empty() ||
644+
Prefix == "k") // printf: all pointer args should be null-terminated
629645
return llvm::any_of(Call->arguments(), AnyUnsafeStrPtr);
630646

631647
if (Prefix == "f" && Call->getNumArgs() > 1) {
@@ -677,7 +693,7 @@ AST_MATCHER(CallExpr, isUnsafeLibcFunctionCall) {
677693
return Name == "strlen" && Call->getNumArgs() == 1 &&
678694
isa<StringLiteral>(Call->getArg(0)->IgnoreParenImpCasts());
679695
}
680-
} FuncNameMatch{&Node};
696+
} FuncNameMatch{&Node, hasUnsafeStringView.matches(Node, Finder, Builder)};
681697

682698
const FunctionDecl *FD = Node.getDirectCallee();
683699
const IdentifierInfo *II;
@@ -1037,6 +1053,47 @@ class SpanTwoParamConstructorGadget : public WarningGadget {
10371053
}
10381054
};
10391055

1056+
class StringViewUnsafeConstructorGadget : public WarningGadget {
1057+
static constexpr const char *const Tag = "StringViewUnsafeConstructor";
1058+
const CXXConstructExpr *Ctor;
1059+
1060+
public:
1061+
StringViewUnsafeConstructorGadget(const MatchFinder::MatchResult &Result)
1062+
: WarningGadget(Kind::StringViewUnsafeConstructor),
1063+
Ctor(Result.Nodes.getNodeAs<CXXConstructExpr>(Tag)) {}
1064+
1065+
static bool classof(const Gadget *G) {
1066+
return G->getKind() == Kind::StringViewUnsafeConstructor;
1067+
}
1068+
1069+
// Matches any `basic_string_view` constructor call that is not a copy
1070+
// constructor or on a string literal:
1071+
static Matcher matcher() {
1072+
auto isStringViewDecl = hasDeclaration(cxxConstructorDecl(
1073+
hasDeclContext(isInStdNamespace()), hasName("basic_string_view")));
1074+
1075+
return stmt(
1076+
cxxConstructExpr(
1077+
isStringViewDecl,
1078+
unless(
1079+
hasArgument(0, expr(ignoringParenImpCasts(stringLiteral())))),
1080+
unless(hasDeclaration(cxxConstructorDecl(isCopyConstructor()))))
1081+
.bind(Tag));
1082+
}
1083+
1084+
const Stmt *getBaseStmt() const override { return Ctor; }
1085+
1086+
DeclUseList getClaimedVarUseSites() const override {
1087+
// If the constructor call is of the form `std::basic_string_view{ptrVar,
1088+
// ...}`, `ptrVar` is considered an unsafe variable.
1089+
if (auto *DRE = dyn_cast<DeclRefExpr>(Ctor->getArg(0))) {
1090+
if (DRE->getType()->isPointerType() && isa<VarDecl>(DRE->getDecl()))
1091+
return {DRE};
1092+
}
1093+
return {};
1094+
}
1095+
};
1096+
10401097
/// A pointer initialization expression of the form:
10411098
/// \code
10421099
/// int *p = q;
@@ -1293,8 +1350,10 @@ class UnsafeLibcFunctionCallGadget : public WarningGadget {
12931350
: WarningGadget(Kind::UnsafeLibcFunctionCall),
12941351
Call(Result.Nodes.getNodeAs<CallExpr>(Tag)) {}
12951352

1296-
static Matcher matcher() {
1297-
return stmt(callExpr(isUnsafeLibcFunctionCall()).bind(Tag));
1353+
static Matcher
1354+
matcher(ast_matchers::internal::Matcher<Expr> HasUnsafeStringView) {
1355+
return stmt(
1356+
callExpr(isUnsafeLibcFunctionCall(HasUnsafeStringView)).bind(Tag));
12981357
}
12991358

13001359
const Stmt *getBaseStmt() const override { return Call; }
@@ -1724,10 +1783,13 @@ findGadgets(const Decl *D, const UnsafeBufferUsageHandler &Handler,
17241783
#define WARNING_GADGET(x) \
17251784
allOf(x ## Gadget::matcher().bind(#x), \
17261785
notInSafeBufferOptOut(&Handler)),
1786+
#define WARNING_LIBC_GADGET(x) \
1787+
allOf(x ## Gadget::matcher(ignoreStringViewUnsafeConstructor(&Handler)).bind(#x), \
1788+
notInSafeBufferOptOut(&Handler)),
17271789
#define WARNING_CONTAINER_GADGET(x) \
17281790
allOf(x ## Gadget::matcher().bind(#x), \
17291791
notInSafeBufferOptOut(&Handler), \
1730-
unless(ignoreUnsafeBufferInContainer(&Handler))),
1792+
unless(ignore ## x(&Handler))),
17311793
#include "clang/Analysis/Analyses/UnsafeBufferUsageGadgets.def"
17321794
// Avoid a hanging comma.
17331795
unless(stmt())

clang/lib/Sema/AnalysisBasedWarnings.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2256,6 +2256,17 @@ class UnsafeBufferUsageReporter : public UnsafeBufferUsageHandler {
22562256
Range = UO->getSubExpr()->getSourceRange();
22572257
MsgParam = 1;
22582258
}
2259+
} else if (const auto *CtorExpr = dyn_cast<CXXConstructExpr>(Operation)) {
2260+
if (CtorExpr->getConstructor()->getCanonicalDecl()->getNameAsString() ==
2261+
"span")
2262+
S.Diag(CtorExpr->getLocation(),
2263+
diag::warn_unsafe_buffer_usage_in_container)
2264+
<< CtorExpr->getSourceRange();
2265+
else // it is warning about "string_view":
2266+
S.Diag(CtorExpr->getLocation(),
2267+
diag::warn_unsafe_buffer_usage_in_string_view)
2268+
<< CtorExpr->getSourceRange();
2269+
return;
22592270
} else {
22602271
if (isa<CallExpr>(Operation) || isa<CXXConstructExpr>(Operation)) {
22612272
// note_unsafe_buffer_operation doesn't have this mode yet.
@@ -2370,6 +2381,12 @@ class UnsafeBufferUsageReporter : public UnsafeBufferUsageHandler {
23702381
return S.Diags.isIgnored(diag::warn_unsafe_buffer_usage_in_container, Loc);
23712382
}
23722383

2384+
bool
2385+
ignoreUnsafeBufferInStringView(const SourceLocation &Loc) const override {
2386+
return S.Diags.isIgnored(diag::warn_unsafe_buffer_usage_in_string_view,
2387+
Loc);
2388+
}
2389+
23732390
// Returns the text representation of clang::unsafe_buffer_usage attribute.
23742391
// `WSSuffix` holds customized "white-space"s, e.g., newline or whilespace
23752392
// characters.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// RUN: %clang_cc1 -std=c++20 -Wno-all -Wunsafe-buffer-usage -verify %s
2+
// RUN: %clang_cc1 -std=c++20 -Wno-all -Wunsafe-buffer-usage -verify %s -Wno-unsafe-buffer-usage-in-string-view -D_IGNORE_STRING_VIEW
3+
4+
namespace std {
5+
struct iterator{};
6+
7+
template<typename T>
8+
struct basic_string_view {
9+
T* p;
10+
T *data();
11+
unsigned size_bytes();
12+
iterator begin();
13+
iterator end();
14+
15+
constexpr basic_string_view( const T* s ){};
16+
constexpr basic_string_view( const T* s, unsigned count );
17+
constexpr basic_string_view( const basic_string_view& other ) noexcept = default;
18+
template< class It, class End >
19+
constexpr basic_string_view( It first, End last ) {};
20+
21+
};
22+
23+
typedef basic_string_view<char> string_view;
24+
typedef basic_string_view<wchar_t> wstring_view;
25+
26+
template<typename T>
27+
struct basic_string {
28+
T *c_str();
29+
operator std::basic_string_view<T>() {
30+
}
31+
};
32+
33+
typedef basic_string<char> string;
34+
typedef basic_string<wchar_t> wstring;
35+
36+
template <class T> struct span {
37+
constexpr span(T *, unsigned){}
38+
};
39+
}
40+
41+
void foo(std::string_view);
42+
void f(char * p, std::string S, std::string_view V) {
43+
std::string_view SVs[] {S, "S"};
44+
foo(S);
45+
foo("S");
46+
foo({S});
47+
foo({"S"});
48+
foo(V);
49+
#ifndef _IGNORE_STRING_VIEW
50+
foo(p); // expected-warning{{construct string_view from raw pointers does not guarantee null-termination, construct from std::string instead}}
51+
#else
52+
foo(p); // no warn
53+
#endif
54+
#ifndef _IGNORE_STRING_VIEW
55+
foo({p, 10}); // expected-warning{{construct string_view from raw pointers does not guarantee null-termination, construct from std::string instead}}
56+
#else
57+
foo({p, 10}); // no warn
58+
#endif
59+
60+
std::string_view SV{S};
61+
std::string_view SV2{"S"};
62+
std::string_view SV3 = S;
63+
std::string_view SV4 = "S";
64+
std::string_view SV5 = std::string_view(S);
65+
#ifndef _IGNORE_STRING_VIEW
66+
std::string_view SV10 = p; // expected-warning{{construct string_view from raw pointers does not guarantee null-termination, construct from std::string instead}}
67+
#else
68+
std::string_view SV10 = p; // no warn
69+
#endif
70+
#ifndef _IGNORE_STRING_VIEW
71+
std::string_view SV11{p}; // expected-warning{{construct string_view from raw pointers does not guarantee null-termination, construct from std::string instead}}
72+
#else
73+
std::string_view SV11{p}; // no warn
74+
#endif
75+
#ifndef _IGNORE_STRING_VIEW
76+
std::string_view SV12{V.begin(), V.end()}; // expected-warning{{construct string_view from raw pointers does not guarantee null-termination, construct from std::string instead}}
77+
#else
78+
std::string_view SV12{V.begin(), V.end()}; // no warn
79+
#endif
80+
}
81+
82+
void g(int * p) {
83+
// This warning is not affected:
84+
std::span<int> S{p, 10}; // expected-warning{{the two-parameter std::span construction is unsafe as it can introduce mismatch between buffer size and the bound information}}
85+
}

clang/test/SemaCXX/warn-unsafe-buffer-usage-libc-functions.cpp

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
// RUN: %clang_cc1 -std=c++20 -Wno-all -Wunsafe-buffer-usage \
2-
// RUN: -verify %s
1+
// RUN: %clang_cc1 -std=c++20 -Wno-all -Wunsafe-buffer-usage -verify %s
2+
// RUN: %clang_cc1 -std=c++20 -Wno-all -Wunsafe-buffer-usage -verify %s -Wno-unsafe-buffer-usage-in-string-view -D_IGNORE_STRING_VIEW
33

44
typedef struct {} FILE;
55
void memcpy();
@@ -42,6 +42,15 @@ namespace std {
4242

4343
typedef basic_string<char> string;
4444
typedef basic_string<wchar_t> wstring;
45+
46+
template<typename T>
47+
struct basic_string_view {
48+
T* p;
49+
T *data();
50+
unsigned size_bytes();
51+
};
52+
53+
typedef basic_string_view<char> string_view;
4554
}
4655

4756
void f(char * p, char * q, std::span<char> s) {
@@ -69,8 +78,13 @@ void f(char * p, char * q, std::span<char> s) {
6978
strlen("hello");// no warn
7079
}
7180

72-
void v(std::string s1, std::wstring s2) {
73-
snprintf(s1.data(), s1.size_bytes(), "%s%d", s1.c_str(), 0); // no warn
81+
void v(std::string s, std::string_view sv) {
82+
snprintf(s.data(), s.size_bytes(), "%s%d", s.c_str(), 0); // no warn
83+
#ifndef _IGNORE_STRING_VIEW
84+
snprintf(sv.data(), sv.size_bytes(), "%s%d", sv.data(), 0); // no warn
85+
#else
86+
snprintf(sv.data(), sv.size_bytes(), "%s%d", sv.data(), 0); // expected-warning{{function introduces unsafe buffer manipulation}} expected-note{{pass -fsafe-buffer-usage-suggestions to receive code hardening suggestions}}
87+
#endif
7488
}
7589

7690

0 commit comments

Comments
 (0)