Skip to content

Commit 1e578f1

Browse files
committed
[-Wunsafe-buffer-usage][BoundsSafety] support __null_terminated in the interop mode
For a __null_terminated pointer 'p', only a __null_terminated pointer, a string literal or a call to `std::string::c_str` can be assigned to (/passed to/used to initialize) 'p'. Otherwise, in the interop mode, an -Wunsafe-buffer-usage warning will be reported. Similar to how __counted_by is handled, incompatibility in nested __null_terminated pointers (e.g., input/output pointer type arguments) is reported as Bounds Safety type checking errors. (rdar://138798346)
1 parent 991600e commit 1e578f1

File tree

7 files changed

+314
-3
lines changed

7 files changed

+314
-3
lines changed

clang/include/clang/Basic/DiagnosticSemaKinds.td

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13603,6 +13603,19 @@ def note_unsafe_count_attributed_pointer_argument : Note<
1360313603
def warn_unsafe_single_pointer_argument : Warning<
1360413604
"unsafe assignment to function parameter of __single pointer type">,
1360513605
InGroup<UnsafeBufferUsage>, DefaultIgnore;
13606+
def warn_unsafe_assign_to_null_terminated_pointer : Warning<
13607+
"unsafe %select{assignment to a pointer of|"
13608+
"assignment to a parameter of|"
13609+
"return of|"
13610+
"conversion to|"
13611+
"initialization of a pointer of|"
13612+
"assignment to a pointer of|"
13613+
"casting to}2 '__null_terminated' type; "
13614+
"only '__null_terminated' pointers, string literals, and 'std::string::c_str' calls are compatible with '__null_terminated' pointers">,
13615+
InGroup<UnsafeBufferUsage>, DefaultIgnore;
13616+
def warn_unsafe_named_cast_to_null_terminated_pointer : Warning<
13617+
"C++ named cast to '__null_terminated' type is unsafe">,
13618+
InGroup<UnsafeBufferUsage>, DefaultIgnore;
1360613619
#ifndef NDEBUG
1360713620
// Not a user-facing diagnostic. Useful for debugging false negatives in
1360813621
// -fsafe-buffer-usage-suggestions (i.e. lack of -Wunsafe-buffer-usage fixits).

clang/lib/Sema/SemaCast.cpp

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ namespace {
205205
/// int *__bidi_indexable p1;
206206
/// int *__indexable p2 = (int *__indexable)p1;
207207
/// \endcode
208-
if (Self.getLangOpts().BoundsSafety) {
208+
if (Self.getLangOpts().hasBoundsSafetyAttributes()) {
209209
unsigned DiagKind = 0;
210210
bool isInvalid = false;
211211
// The type error may be nested, so any pointer can result in VT errors
@@ -238,6 +238,17 @@ namespace {
238238
const auto *DstPointerType = DestType->getAs<ValueTerminatedType>();
239239
IsDstNullTerm =
240240
DstPointerType->getTerminatorValue(Self.Context).isZero();
241+
if (Self.LangOpts.isBoundsSafetyAttributeOnlyMode() &&
242+
Self.LangOpts.CPlusPlus &&
243+
!Self.Diags.isIgnored(
244+
diag::warn_unsafe_assign_to_null_terminated_pointer,
245+
SrcExpr.get()->getExprLoc())) {
246+
// In C++ Safe Buffers/Bounds Safety interop mode, the compiler
247+
// reports a warning under -Wunsafe-buffer-usage:
248+
DiagKind = diag::warn_unsafe_assign_to_null_terminated_pointer;
249+
isInvalid = true;
250+
break;
251+
}
241252
if (Self.getLangOpts().BoundsSafetyRelaxedSystemHeaders) {
242253
DiagKind = diag::
243254
warn_bounds_safety_incompatible_non_terminated_by_to_terminated_by;
@@ -576,6 +587,17 @@ Sema::BuildCXXNamedCast(SourceLocation OpLoc, tok::TokenKind Kind,
576587

577588
Op.checkQualifiedDestType();
578589

590+
// In C++ Safe Buffers/Bounds Safety interop mode, forbidding for now all
591+
// named cast to __null_terminated pointer type.
592+
if (DestType->isPointerType() && LangOpts.hasBoundsSafetyAttributes() &&
593+
!Diags.isIgnored(diag::warn_unsafe_named_cast_to_null_terminated_pointer,
594+
E->getBeginLoc())) {
595+
if (DestType->getAs<ValueTerminatedType>()) {
596+
Diag(E->getBeginLoc(),
597+
diag::warn_unsafe_named_cast_to_null_terminated_pointer);
598+
return ExprError();
599+
}
600+
}
579601
switch (Kind) {
580602
default: llvm_unreachable("Unknown C++ cast!");
581603

clang/lib/Sema/SemaExpr.cpp

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12186,13 +12186,39 @@ Sema::CheckValueTerminatedAssignmentConstraints(QualType LHSType,
1218612186
Context.hasSameUnqualifiedType(
1218712187
Context.getCorrespondingSignedType(LPointee),
1218812188
Context.getCorrespondingSignedType(RPointee)));
12189-
if (CompatiblePointees &&
12189+
if (CompatiblePointees && RHSExpr->isPRValue() &&
1219012190
RHSExpr->tryEvaluateTerminatorElement(Evald, Context) &&
1219112191
Evald.Val.isInt() &&
1219212192
llvm::APSInt::isSameValue(LVTT->getTerminatorValue(Context),
1219312193
Evald.Val.getInt())) {
1219412194
return Sema::Compatible;
1219512195
}
12196+
// If in C++ Safe Buffers/Bounds Safety interoperation mode, the RHS can
12197+
// be 'std::string::c_str':
12198+
bool SafeBuffersEnabled = !Diags.isIgnored(
12199+
diag::warn_unsafe_assign_to_null_terminated_pointer,
12200+
RHSExpr->getBeginLoc());
12201+
12202+
if (LangOpts.CPlusPlus && SafeBuffersEnabled) {
12203+
if (const auto *MCE =
12204+
dyn_cast<CXXMemberCallExpr>(RHSExpr->IgnoreParenImpCasts())) {
12205+
IdentifierInfo *MethodDeclII = nullptr, *ObjTypeII = nullptr;
12206+
12207+
if (MCE->getMethodDecl())
12208+
MethodDeclII = MCE->getMethodDecl()->getIdentifier();
12209+
if (const auto *RecordDecl =
12210+
MCE->getObjectType()->getAsRecordDecl()) {
12211+
// If not in std namespace, let `ObjTypeII` be null so that the
12212+
// match fails:
12213+
if (RecordDecl->isInStdNamespace())
12214+
ObjTypeII = RecordDecl->getIdentifier();
12215+
}
12216+
if (MethodDeclII && ObjTypeII &&
12217+
MethodDeclII->getName() == "c_str" &&
12218+
ObjTypeII->getName() == "basic_string")
12219+
return Sema::Compatible;
12220+
}
12221+
}
1219612222
}
1219712223
return Nested
1219812224
? Sema::
@@ -20783,6 +20809,16 @@ bool Sema::DiagnoseAssignmentResult(AssignConvertType ConvTy,
2078320809
const auto *DstPointerType = DstType->getAs<ValueTerminatedType>();
2078420810
IsDstNullTerm =
2078520811
DstPointerType->getTerminatorValue(getASTContext()).isZero();
20812+
if (getLangOpts().isBoundsSafetyAttributeOnlyMode() &&
20813+
getLangOpts().CPlusPlus &&
20814+
!Diags.isIgnored(diag::warn_unsafe_assign_to_null_terminated_pointer,
20815+
Loc)) {
20816+
// In C++ Safe Buffers/Bounds Safety interop mode, the compiler reports a
20817+
// warning under -Wunsafe-buffer-usage:
20818+
DiagKind = diag::warn_unsafe_assign_to_null_terminated_pointer;
20819+
isInvalid = false;
20820+
break;
20821+
}
2078620822
if (getLangOpts().BoundsSafetyRelaxedSystemHeaders) {
2078720823
DiagKind =
2078820824
diag::warn_bounds_safety_incompatible_non_terminated_by_to_terminated_by;

clang/lib/Sema/SemaExprCXX.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4225,6 +4225,17 @@ Sema::PerformImplicitConversion(Expr *From, QualType ToType,
42254225
const ImplicitConversionSequence &ICS,
42264226
AssignmentAction Action,
42274227
CheckedConversionKind CCK) {
4228+
if (ToType->isPointerType() && LangOpts.hasBoundsSafetyAttributes() &&
4229+
!Diags.isIgnored(diag::warn_unsafe_assign_to_null_terminated_pointer,
4230+
From->getBeginLoc())) {
4231+
// In C++ Safe Buffers/Bounds Safety interop mode, warn about implicit
4232+
// conversions from non-`__null_terminated` to `__null_terminated`:
4233+
AssignConvertType ConvTy =
4234+
CheckValueTerminatedAssignmentConstraints(ToType, From);
4235+
if (DiagnoseAssignmentResult(ConvTy, From->getExprLoc(), ToType,
4236+
From->getType(), From, Action))
4237+
return ExprError();
4238+
}
42284239
// C++ [over.match.oper]p7: [...] operands of class type are converted [...]
42294240
if (CCK == CheckedConversionKind::ForBuiltinOverloadedOp &&
42304241
!From->getType()->isRecordType())

clang/lib/Sema/SemaInit.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9046,7 +9046,7 @@ ExprResult InitializationSequence::Perform(Sema &S,
90469046
Entity.getKind() == InitializedEntity::EK_Result);
90479047

90489048
/* TO_UPSTREAM(BoundsSafety) ON*/
9049-
if (S.LangOpts.BoundsSafety) {
9049+
if (S.LangOpts.hasBoundsSafetyAttributes()) {
90509050
auto *Init = CurInit.get();
90519051
const auto K = Entity.getKind();
90529052
if (Init && Entity.getParent() == nullptr &&

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
// RUN: -verify %s -x objective-c++
55
// RUN: %clang_cc1 -std=c++20 -Wno-all -Wunsafe-buffer-usage-in-libc-call \
66
// RUN: -verify %s
7+
// RUN: %clang_cc1 -std=c++20 -Wno-all -Wunsafe-buffer-usage \
8+
// RUN: -verify -fexperimental-bounds-safety-attributes %s -DINTEROP_MODE
79

10+
#ifndef INTEROP_MODE
811
typedef struct {} FILE;
912
void memcpy();
1013
void __asan_memcpy();
@@ -155,3 +158,47 @@ void ff(char * p, char * q, std::span<char> s, std::span<char> s2) {
155158
wcscpy_s();
156159
#pragma clang diagnostic pop
157160
}
161+
162+
#else
163+
164+
#include <ptrcheck.h>
165+
typedef unsigned size_t;
166+
167+
// expected-note@+2{{consider using a safe container and passing '.data()' to the parameter 'dst' and '.size()' to its dependent parameter 'size' or 'std::span' and passing '.first(...).data()' to the parameter 'dst'}}
168+
// expected-note@+1{{consider using a safe container and passing '.data()' to the parameter 'src' and '.size()' to its dependent parameter 'size' or 'std::span' and passing '.first(...).data()' to the parameter 'src'}}
169+
void memcpy(void * __sized_by(size) dst, const void * __sized_by(size) src, unsigned size);
170+
unsigned strlen( const char* __null_terminated str );
171+
// expected-note@+1{{consider using a safe container and passing '.data()' to the parameter 'buffer' and '.size()' to its dependent parameter 'buf_size' or 'std::span' and passing '.first(...).data()' to the parameter 'buffer'}}
172+
int snprintf( char* __counted_by(buf_size) buffer, unsigned buf_size, const char* format, ... );
173+
int snwprintf( char* __counted_by(buf_size) buffer, unsigned buf_size, const char* format, ... );
174+
int vsnprintf( char* __counted_by(buf_size) buffer, unsigned buf_size, const char* format, ... );
175+
int sprintf( char* __counted_by(10) buffer, const char* format, ... );
176+
177+
void test(char * p, char * q, const char * str,
178+
const char * __null_terminated safe_str,
179+
char * __counted_by(n) safe_p,
180+
size_t n,
181+
char * __counted_by(10) safe_ten) {
182+
memcpy(p, q, 10); // expected-warning2{{unsafe assignment to function parameter of count-attributed type}}
183+
snprintf(p, 10, "%s", "hlo"); // expected-warning{{unsafe assignment to function parameter of count-attributed type}}
184+
185+
// We still warn about unsafe string pointer arguments to printfs:
186+
187+
snprintf(safe_p, n, "%s", str); // expected-warning{{function 'snprintf' is unsafe}} expected-note{{string argument is not guaranteed to be null-terminated}}
188+
snwprintf(safe_p, n, "%s", str); // expected-warning{{function 'snwprintf' is unsafe}} expected-note{{string argument is not guaranteed to be null-terminated}}
189+
190+
memcpy(safe_p, safe_p, n); // no warn
191+
strlen(str); // expected-warning{{unsafe assignment to a parameter of '__null_terminated' type; only '__null_terminated' pointers, string literals, and 'std::string::c_str' calls are compatible with '__null_terminated' pointers}}
192+
snprintf(safe_p, n, "%s", "hlo"); // no warn
193+
snprintf(safe_p, n, "%s", safe_str); // no warn
194+
snwprintf(safe_p, n, "%s", safe_str); // no warn
195+
196+
// v-printf functions and sprintf are still warned about because
197+
// they cannot be fully safe:
198+
199+
vsnprintf(safe_p, n, "%s", safe_str); // expected-warning{{function 'vsnprintf' is unsafe}} expected-note{{'va_list' is unsafe}}
200+
sprintf(safe_ten, "%s", safe_str); // expected-warning{{function 'sprintf' is unsafe}} expected-note{{change to 'snprintf' for explicit bounds checking}}
201+
202+
}
203+
204+
#endif //#ifdef INTEROP_MODE

0 commit comments

Comments
 (0)