Skip to content

Commit 38255d1

Browse files
committed
[-Wunsafe-buffer-usage] Implement fixits from const size array to std::array
Array subscript on a const size array is not bounds-checked. The idiomatic replacement is std::array which is bounds-safe in hardened mode of libc++. This commit extends the fixit-producing machine to consider std::array as a transformation target type and teaches it to handle the array subscript on const size arrays with a trivial (empty) fixit. The transformation is straightforward for most cases: T array[N]; => std::array<T, N> array; but for array of const pointers it needs to be: T* const array[N]; => const std::array<T*, N> array; This case and other special cases are to be implemented in follow-up patches.
1 parent 463a990 commit 38255d1

File tree

7 files changed

+230
-17
lines changed

7 files changed

+230
-17
lines changed

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ class UnsafeBufferUsageHandler {
9595
#endif
9696

9797
public:
98+
enum class TargetType { Span, Array };
99+
98100
UnsafeBufferUsageHandler() = default;
99101
virtual ~UnsafeBufferUsageHandler() = default;
100102

@@ -112,9 +114,11 @@ class UnsafeBufferUsageHandler {
112114
///
113115
/// `D` is the declaration of the callable under analysis that owns `Variable`
114116
/// and all of its group mates.
115-
virtual void handleUnsafeVariableGroup(const VarDecl *Variable,
116-
const VariableGroupsManager &VarGrpMgr,
117-
FixItList &&Fixes, const Decl *D) = 0;
117+
virtual void
118+
handleUnsafeVariableGroup(const VarDecl *Variable,
119+
const VariableGroupsManager &VarGrpMgr,
120+
FixItList &&Fixes, const Decl *D,
121+
const FixitStrategy &VarTargetTypes) = 0;
118122

119123
#ifndef NDEBUG
120124
public:

clang/lib/Analysis/UnsafeBufferUsage.cpp

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1495,6 +1495,7 @@ ULCArraySubscriptGadget::getFixits(const FixitStrategy &S) const {
14951495
return FixItList{};
14961496
}
14971497
case FixitStrategy::Kind::Array:
1498+
return FixItList{};
14981499
case FixitStrategy::Kind::Wontfix:
14991500
case FixitStrategy::Kind::Iterator:
15001501
case FixitStrategy::Kind::Vector:
@@ -2462,6 +2463,71 @@ static FixItList fixVariableWithSpan(const VarDecl *VD,
24622463
return fixLocalVarDeclWithSpan(VD, Ctx, getUserFillPlaceHolder(), Handler);
24632464
}
24642465

2466+
static FixItList fixVarDeclWithArray(const VarDecl *D, const ASTContext &Ctx,
2467+
UnsafeBufferUsageHandler &Handler) {
2468+
FixItList FixIts{};
2469+
2470+
if (auto CAT = dyn_cast<clang::ConstantArrayType>(D->getType())) {
2471+
const QualType &ArrayEltT = CAT->getElementType();
2472+
assert(!ArrayEltT.isNull() && "Trying to fix a non-array type variable!");
2473+
// Producing fix-it for variable declaration---make `D` to be of std::array
2474+
// type:
2475+
SmallString<32> Replacement;
2476+
raw_svector_ostream OS(Replacement);
2477+
2478+
// For most types the transformation is simple:
2479+
// T foo[10]; => std::array<T, 10> foo;
2480+
// Cv-specifiers are straigtforward:
2481+
// const T foo[10]; => std::array<const T, 10> foo;
2482+
// Pointers are straightforward:
2483+
// T * foo[10]; => std::array<T *, 10> foo;
2484+
//
2485+
// However, for const pointers the transformation is different:
2486+
// T * const foo[10]; => const std::array<T *, 10> foo;
2487+
if (ArrayEltT->isPointerType() && ArrayEltT.isConstQualified()) {
2488+
DEBUG_NOTE_DECL_FAIL(D, " : const size array of const pointers");
2489+
// FIXME: implement the support
2490+
// FIXME: bail if the const pointer is a typedef
2491+
return {};
2492+
}
2493+
2494+
std::optional<StringRef> IdentText =
2495+
getVarDeclIdentifierText(D, Ctx.getSourceManager(), Ctx.getLangOpts());
2496+
2497+
if (!IdentText) {
2498+
DEBUG_NOTE_DECL_FAIL(D, " : failed to locate the identifier");
2499+
return {};
2500+
}
2501+
2502+
OS << "std::array<" << ArrayEltT.getAsString() << ", "
2503+
<< getAPIntText(CAT->getSize()) << "> " << IdentText->str();
2504+
2505+
FixIts.push_back(FixItHint::CreateReplacement(
2506+
SourceRange{D->getBeginLoc(), D->getTypeSpecEndLoc()}, OS.str()));
2507+
}
2508+
2509+
return FixIts;
2510+
}
2511+
2512+
static FixItList fixVariableWithArray(const VarDecl *VD,
2513+
const DeclUseTracker &Tracker,
2514+
const ASTContext &Ctx,
2515+
UnsafeBufferUsageHandler &Handler) {
2516+
const DeclStmt *DS = Tracker.lookupDecl(VD);
2517+
assert(DS && "Fixing non-local variables not implemented yet!");
2518+
if (!DS->isSingleDecl()) {
2519+
// FIXME: to support handling multiple `VarDecl`s in a single `DeclStmt`
2520+
return {};
2521+
}
2522+
// Currently DS is an unused variable but we'll need it when
2523+
// non-single decls are implemented, where the pointee type name
2524+
// and the '*' are spread around the place.
2525+
(void)DS;
2526+
2527+
// FIXME: handle cases where DS has multiple declarations
2528+
return fixVarDeclWithArray(VD, Ctx, Handler);
2529+
}
2530+
24652531
// TODO: we should be consistent to use `std::nullopt` to represent no-fix due
24662532
// to any unexpected problem.
24672533
static FixItList
@@ -2507,7 +2573,13 @@ fixVariable(const VarDecl *VD, FixitStrategy::Kind K,
25072573
DEBUG_NOTE_DECL_FAIL(VD, " : not a pointer");
25082574
return {};
25092575
}
2510-
case FixitStrategy::Kind::Array:
2576+
case FixitStrategy::Kind::Array: {
2577+
if (VD->isLocalVarDecl() && isa<clang::ConstantArrayType>(VD->getType()))
2578+
return fixVariableWithArray(VD, Tracker, Ctx, Handler);
2579+
2580+
DEBUG_NOTE_DECL_FAIL(VD, " : not a local const-size array");
2581+
return {};
2582+
}
25112583
case FixitStrategy::Kind::Iterator:
25122584
case FixitStrategy::Kind::Vector:
25132585
llvm_unreachable("FixitStrategy not implemented yet!");
@@ -2696,7 +2768,10 @@ static FixitStrategy
26962768
getNaiveStrategy(llvm::iterator_range<VarDeclIterTy> UnsafeVars) {
26972769
FixitStrategy S;
26982770
for (const VarDecl *VD : UnsafeVars) {
2699-
S.set(VD, FixitStrategy::Kind::Span);
2771+
if (isa<ConstantArrayType>(VD->getType()))
2772+
S.set(VD, FixitStrategy::Kind::Array);
2773+
else
2774+
S.set(VD, FixitStrategy::Kind::Span);
27002775
}
27012776
return S;
27022777
}
@@ -3018,9 +3093,9 @@ void clang::checkUnsafeBufferUsage(const Decl *D,
30183093
auto FixItsIt = FixItsForVariableGroup.find(VD);
30193094
Handler.handleUnsafeVariableGroup(VD, VarGrpMgr,
30203095
FixItsIt != FixItsForVariableGroup.end()
3021-
? std::move(FixItsIt->second)
3022-
: FixItList{},
3023-
D);
3096+
? std::move(FixItsIt->second)
3097+
: FixItList{},
3098+
D, NaiveStrategy);
30243099
for (const auto &G : WarningGadgets) {
30253100
Handler.handleUnsafeOperation(G->getBaseStmt(), /*IsRelatedToDecl=*/true,
30263101
D->getASTContext());

clang/lib/Sema/AnalysisBasedWarnings.cpp

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2297,7 +2297,8 @@ class UnsafeBufferUsageReporter : public UnsafeBufferUsageHandler {
22972297

22982298
void handleUnsafeVariableGroup(const VarDecl *Variable,
22992299
const VariableGroupsManager &VarGrpMgr,
2300-
FixItList &&Fixes, const Decl *D) override {
2300+
FixItList &&Fixes, const Decl *D,
2301+
const FixitStrategy &VarTargetTypes) override {
23012302
assert(!SuggestSuggestions &&
23022303
"Unsafe buffer usage fixits displayed without suggestions!");
23032304
S.Diag(Variable->getLocation(), diag::warn_unsafe_buffer_variable)
@@ -2312,7 +2313,18 @@ class UnsafeBufferUsageReporter : public UnsafeBufferUsageHandler {
23122313
// NOT explain how the variables are grouped as the reason is non-trivial
23132314
// and irrelavant to users' experience:
23142315
const auto VarGroupForVD = VarGrpMgr.getGroupOfVar(Variable, &BriefMsg);
2315-
unsigned FixItStrategy = 0; // For now we only have 'std::span' strategy
2316+
unsigned FixItStrategy = 0;
2317+
switch (VarTargetTypes.lookup(Variable)) {
2318+
case clang::FixitStrategy::Kind::Span:
2319+
FixItStrategy = 0;
2320+
break;
2321+
case clang::FixitStrategy::Kind::Array:
2322+
FixItStrategy = 1;
2323+
break;
2324+
default:
2325+
assert(false && "We support only std::span and std::array");
2326+
};
2327+
23162328
const auto &FD =
23172329
S.Diag(Variable->getLocation(),
23182330
BriefMsg ? diag::note_unsafe_buffer_variable_fixit_together
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// RUN: %clang_cc1 -std=c++20 -Wno-all -Wunsafe-buffer-usage \
2+
// RUN: -fsafe-buffer-usage-suggestions \
3+
// RUN: -verify %s
4+
5+
// CHECK-NOT: [-Wunsafe-buffer-usage]
6+
7+
8+
void foo(unsigned idx) {
9+
int buffer[10]; // expected-warning{{'buffer' is an unsafe buffer that does not perform bounds}}
10+
// expected-note@-1{{change type of 'buffer' to 'std::array' to preserve bounds information}}
11+
buffer[idx] = 0; // expected-note{{used in buffer access here}}
12+
}
13+
14+
int global_buffer[10]; // expected-warning{{'global_buffer' is an unsafe buffer that does not perform bounds}}
15+
void foo2(unsigned idx) {
16+
global_buffer[idx] = 0; // expected-note{{used in buffer access here}}
17+
}
18+
19+
struct Foo {
20+
int member_buffer[10];
21+
};
22+
void foo2(Foo& f, unsigned idx) {
23+
f.member_buffer[idx] = 0; // expected-warning{{unsafe buffer access}}
24+
}

clang/test/SemaCXX/warn-unsafe-buffer-usage-debug.cpp

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,11 @@ void foo() {
3232
// debug-note{{safe buffers debug: gadget 'ULCArraySubscript' refused to produce a fix}}
3333
}
3434

35-
void failed_decl() {
36-
int a[10]; // expected-warning{{'a' is an unsafe buffer that does not perform bounds checks}} \
37-
// debug-note{{safe buffers debug: failed to produce fixit for declaration 'a' : not a pointer}}
38-
39-
for (int i = 0; i < 10; i++) {
40-
a[i] = i; // expected-note{{used in buffer access here}}
41-
}
35+
void failed_decl(const int* out, unsigned idx) {
36+
const int* const a[10] = {nullptr}; // expected-warning{{'a' is an unsafe buffer that does not perform bounds checks}} \
37+
// debug-note{{safe buffers debug: failed to produce fixit for declaration 'a' : const size array of const pointers}}
38+
39+
out = a[idx]; // expected-note{{used in buffer access here}}
4240
}
4341

4442
void failed_multiple_decl() {
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// RUN: %clang_cc1 -std=c++20 -Wunsafe-buffer-usage \
2+
// RUN: -fsafe-buffer-usage-suggestions \
3+
// RUN: -fdiagnostics-parseable-fixits %s 2>&1 | FileCheck %s
4+
typedef int * Int_ptr_t;
5+
typedef int Int_t;
6+
7+
void local_array(unsigned idx) {
8+
int buffer[10];
9+
// CHECK: fix-it:"{{.*}}":{[[@LINE-1]]:3-[[@LINE-1]]:17}:"std::array<int, 10> buffer"
10+
buffer[idx] = 0;
11+
}
12+
13+
void unsupported_multi_decl1(unsigned idx) {
14+
int a, buffer[10];
15+
// CHECK-NOT: fix-it:"{{.*}}":{[[@LINE-1]]
16+
buffer[idx] = 0;
17+
}
18+
19+
void unsupported_multi_decl2(unsigned idx) {
20+
int buffer[10], b;
21+
// CHECK-NOT: fix-it:"{{.*}}":{[[@LINE-1]]
22+
buffer[idx] = 0;
23+
}
24+
25+
void local_array_ptr_to_const(unsigned idx, const int*& a) {
26+
const int * buffer[10] = {a};
27+
// CHECK: fix-it:"{{.*}}":{[[@LINE-1]]:3-[[@LINE-1]]:25}:"std::array<const int *, 10> buffer"
28+
a = buffer[idx];
29+
}
30+
31+
void local_array_const_ptr(unsigned idx, int*& a) {
32+
int * const buffer[10] = {a};
33+
// FIXME: implement support
34+
// CHECK-NOT: fix-it:"{{.*}}":{[[@LINE-1]]:.*-[[@LINE-1]]:.*}
35+
a = buffer[idx];
36+
37+
}
38+
39+
void local_array_const_ptr_to_const(unsigned idx, const int*& a) {
40+
const int * const buffer[10] = {a};
41+
// FIXME: implement support
42+
// CHECK-NOT: fix-it:"{{.*}}":{[[@LINE-1]]:.*-[[@LINE-1]]:.*}
43+
a = buffer[idx];
44+
45+
}
46+
47+
template<typename T>
48+
void local_array_in_template(unsigned idx) {
49+
T buffer[10];
50+
// CHECK-NOT: fix-it:"{{.*}}":{[[@LINE-1]]:.*-[[@LINE-1]]:.*}
51+
buffer[idx] = 0;
52+
}
53+
// Instantiate the template function to force its analysis.
54+
template void local_array_in_template<int>(unsigned); // FIXME: expected note {{in instantiation of}}
55+
56+
void macro_as_identifier(unsigned idx) {
57+
#define MY_BUFFER buffer
58+
// Although fix-its include macros, the macros do not overlap with
59+
// the bounds of the source range of these fix-its. So these fix-its
60+
// are valid.
61+
62+
int MY_BUFFER[10];
63+
// CHECK: fix-it:"{{.*}}":{[[@LINE-1]]:3-[[@LINE-1]]:20}:"std::array<int, 10> MY_BUFFER"
64+
MY_BUFFER[idx] = 0;
65+
66+
#undef MY_BUFFER
67+
}
68+
69+
void unsupported_fixit_overlapping_macro(unsigned idx) {
70+
#define MY_INT int
71+
MY_INT buffer[10];
72+
// CHECK-NOT: fix-it:"{{.*}}":{[[@LINE-1]]
73+
buffer[idx] = 0;
74+
#undef MY_INT
75+
}
76+
77+
void subscript_negative() {
78+
int buffer[10];
79+
// CHECK: fix-it:"{{.*}}":{[[@LINE-1]]:3-[[@LINE-1]]:17}:"std::array<int, 10> buffer"
80+
81+
// For constant-size arrays any negative index will lead to buffer underflow.
82+
// std::array::operator[] has unsigned parameter so the value will be casted to unsigned.
83+
// This will very likely be buffer overflow but hardened std::array catch these at runtime.
84+
buffer[-5] = 0;
85+
}
86+
87+
void subscript_signed(int signed_idx) {
88+
int buffer[10];
89+
// CHECK: fix-it:"{{.*}}":{[[@LINE-1]]:3-[[@LINE-1]]:17}:"std::array<int, 10> buffer"
90+
91+
// For constant-size arrays any negative index will lead to buffer underflow.
92+
// std::array::operator[] has unsigned parameter so the value will be casted to unsigned.
93+
// This will very likely be buffer overflow but hardened std::array catch these at runtime.
94+
buffer[signed_idx] = 0;
95+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ void testArraySubscripts(int *p, int **pp) {
6161
);
6262

6363
int a[10]; // expected-warning{{'a' is an unsafe buffer that does not perform bounds checks}}
64+
// expected-note@-1{{change type of 'a' to 'std::array' to preserve bounds information}}
6465
int b[10][10]; // expected-warning{{'b' is an unsafe buffer that does not perform bounds checks}}
6566

6667
foo(a[1], 1[a], // expected-note2{{used in buffer access here}}
@@ -174,6 +175,7 @@ auto file_scope_lambda = [](int *ptr) {
174175
void testLambdaCapture() {
175176
int a[10]; // expected-warning{{'a' is an unsafe buffer that does not perform bounds checks}}
176177
int b[10]; // expected-warning{{'b' is an unsafe buffer that does not perform bounds checks}}
178+
// expected-note@-1{{change type of 'b' to 'std::array' to preserve bounds information}}
177179
int c[10];
178180

179181
auto Lam1 = [a]() {
@@ -191,7 +193,9 @@ void testLambdaCapture() {
191193

192194
void testLambdaImplicitCapture() {
193195
int a[10]; // expected-warning{{'a' is an unsafe buffer that does not perform bounds checks}}
196+
// expected-note@-1{{change type of 'a' to 'std::array' to preserve bounds information}}
194197
int b[10]; // expected-warning{{'b' is an unsafe buffer that does not perform bounds checks}}
198+
// expected-note@-1{{change type of 'b' to 'std::array' to preserve bounds information}}
195199

196200
auto Lam1 = [=]() {
197201
return a[1]; // expected-note{{used in buffer access here}}
@@ -344,6 +348,7 @@ template<typename T> void fArr(T t[]) {
344348
// expected-warning@-1{{'t' is an unsafe pointer used for buffer access}}
345349
foo(t[1]); // expected-note{{used in buffer access here}}
346350
T ar[8]; // expected-warning{{'ar' is an unsafe buffer that does not perform bounds checks}}
351+
// expected-note@-1{{change type of 'ar' to 'std::array' to preserve bounds information}}
347352
foo(ar[5]); // expected-note{{used in buffer access here}}
348353
}
349354

0 commit comments

Comments
 (0)