-
Notifications
You must be signed in to change notification settings - Fork 341
[-Wunsafe-buffer-usage][BoundsSafety] support __null_terminated in the interop mode #10060
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1e578f1
e8e5a60
453073f
eebc20c
1607947
7f1b823
623da8f
92d269a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12186,13 +12186,39 @@ Sema::CheckValueTerminatedAssignmentConstraints(QualType LHSType, | |
Context.hasSameUnqualifiedType( | ||
Context.getCorrespondingSignedType(LPointee), | ||
Context.getCorrespondingSignedType(RPointee))); | ||
if (CompatiblePointees && | ||
if (CompatiblePointees && RHSExpr->isPRValue() && | ||
RHSExpr->tryEvaluateTerminatorElement(Evald, Context) && | ||
Evald.Val.isInt() && | ||
llvm::APSInt::isSameValue(LVTT->getTerminatorValue(Context), | ||
Evald.Val.getInt())) { | ||
return Sema::Compatible; | ||
} | ||
// If in C++ Safe Buffers/Bounds Safety interoperation mode, the RHS can | ||
// be 'std::string::c_str/data': | ||
bool isNullTerm = LVTT->getTerminatorValue(Context).isZero(); | ||
rapidsna marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if (isNullTerm && isCXXSafeBuffersBoundsSafetyInteropEnabledAt( | ||
RHSExpr->getBeginLoc())) { | ||
if (const auto *MCE = | ||
dyn_cast<CXXMemberCallExpr>(RHSExpr->IgnoreParenImpCasts())) { | ||
IdentifierInfo *MethodDeclII = nullptr, *ObjTypeII = nullptr; | ||
|
||
ziqingluo-90 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (MCE->getMethodDecl()) | ||
MethodDeclII = MCE->getMethodDecl()->getIdentifier(); | ||
if (const auto *RecordDecl = | ||
MCE->getObjectType()->getAsRecordDecl(); | ||
RecordDecl && RecordDecl->isInStdNamespace()) | ||
// If not in std namespace, let `ObjTypeII` be null so that the | ||
// match fails: | ||
ObjTypeII = RecordDecl->getIdentifier(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there a reason we support There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://en.cppreference.com/w/cpp/string/basic_string/c_str There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aha, it's changed since C++11. |
||
if (MethodDeclII && ObjTypeII && | ||
(MethodDeclII->getName() == "c_str" || | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this work if LHS is terminated by a value different than 0, e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. Need to check it is 0-terminator. |
||
// std::string::data is also null-terminated since C++11: | ||
ziqingluo-90 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
MethodDeclII->getName() == "data") && | ||
ObjTypeII->getName() == "basic_string") | ||
return Sema::Compatible; | ||
} | ||
} | ||
} | ||
return Nested | ||
? Sema:: | ||
|
@@ -20783,9 +20809,11 @@ bool Sema::DiagnoseAssignmentResult(AssignConvertType ConvTy, | |
const auto *DstPointerType = DstType->getAs<ValueTerminatedType>(); | ||
IsDstNullTerm = | ||
DstPointerType->getTerminatorValue(getASTContext()).isZero(); | ||
if (getLangOpts().BoundsSafetyRelaxedSystemHeaders) { | ||
DiagKind = | ||
diag::warn_bounds_safety_incompatible_non_terminated_by_to_terminated_by; | ||
|
||
if (getLangOpts().BoundsSafetyRelaxedSystemHeaders || | ||
isCXXSafeBuffersBoundsSafetyInteropEnabledAt(Loc)) { | ||
DiagKind = diag:: | ||
warn_bounds_safety_incompatible_non_terminated_by_to_terminated_by; | ||
isInvalid = false; | ||
} else { | ||
DiagKind = | ||
|
@@ -21061,8 +21089,11 @@ bool Sema::DiagnoseAssignmentResult(AssignConvertType ConvTy, | |
diag:: | ||
warn_bounds_safety_incompatible_non_terminated_by_to_terminated_by) { | ||
FDiag << FirstType << SecondType << ActionForDiag | ||
<< SrcExpr->getSourceRange() << | ||
(IsDstNullTerm ? /*null_terminated*/ 1 : /*terminated_by*/ 0); | ||
<< SrcExpr->getSourceRange() | ||
<< (!isCXXSafeBuffersBoundsSafetyInteropEnabledAt(Loc) | ||
? (IsDstNullTerm ? /*null_terminated*/ 1 | ||
: /*terminated_by*/ 0) | ||
: 2 /* cut message irrelevant to that mode*/); | ||
} else { | ||
FDiag << FirstType << SecondType << ActionForDiag | ||
<< SrcExpr->getSourceRange(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
// RUN: %clang_cc1 -fsyntax-only -std=c++20 -Wno-all -Wunsafe-buffer-usage -fsafe-buffer-usage-suggestions -fexperimental-bounds-safety-attributes -verify %s | ||
|
||
#include <ptrcheck.h> | ||
|
||
typedef unsigned size_t; | ||
namespace std { | ||
template <typename CharT> | ||
struct basic_string { | ||
const CharT *data() const noexcept; | ||
CharT *data() noexcept; | ||
const CharT *c_str() const noexcept; | ||
size_t size() const noexcept; | ||
size_t length() const noexcept; | ||
}; | ||
|
||
typedef basic_string<char> string; | ||
|
||
template <typename CharT> | ||
struct basic_string_view { | ||
basic_string_view(basic_string<CharT> str); | ||
const CharT *data() const noexcept; | ||
size_t size() const noexcept; | ||
size_t length() const noexcept; | ||
}; | ||
|
||
typedef basic_string_view<char> string_view; | ||
} // namespace std | ||
|
||
void nt_parm(const char * __null_terminated); | ||
const char * __null_terminated get_nt(const char *, size_t); | ||
|
||
void basics(const char * cstr, size_t cstr_len, std::string cxxstr) { | ||
const char * __null_terminated p = "hello"; // safe init | ||
|
||
nt_parm(p); | ||
nt_parm(get_nt(cstr, cstr_len)); | ||
|
||
const char * __null_terminated p2 = cxxstr.c_str(); // safe init | ||
const char * __null_terminated p3; | ||
|
||
p3 = cxxstr.c_str(); // safe assignment | ||
p3 = "hello"; // safe assignment | ||
p3 = p; // safe assignment | ||
p3 = get_nt(cstr, cstr_len); // safe assignment | ||
|
||
// expected-error@+1 {{initializing 'const char * __terminated_by(0)' (aka 'const char *') with an expression of incompatible type 'const char *' is an unsafe operation}} | ||
const char * __null_terminated p4 = cstr; // warn | ||
|
||
// expected-error@+1 {{passing 'const char *' to parameter of incompatible type 'const char * __terminated_by(0)' (aka 'const char *') is an unsafe operation}} | ||
nt_parm(cstr); // warn | ||
// expected-error@+1 {{assigning to 'const char * __terminated_by(0)' (aka 'const char *') from incompatible type 'const char *' is an unsafe operation}} | ||
p4 = cstr; // warn | ||
|
||
std::string_view view{cxxstr}; | ||
|
||
// expected-error@+1 {{assigning to 'const char * __terminated_by(0)' (aka 'const char *') from incompatible type 'const char *' is an unsafe operation}} | ||
p4 = view.data(); // warn | ||
// expected-error@+1 {{passing 'const char *' to parameter of incompatible type 'const char * __terminated_by(0)' (aka 'const char *') is an unsafe operation}} | ||
nt_parm(view.data()); // warn | ||
|
||
const char * __null_terminated p5 = 0; // nullptr is ok | ||
// expected-error@+1 {{initializing 'const char * __terminated_by(0)' (aka 'const char *') with an expression of incompatible type 'const char *' is an unsafe operation}} | ||
const char * __null_terminated p6 = (const char *)1; // other integer literal is unsafe | ||
|
||
// (non-0)-terminated pointer is NOT compatible with 'std::string::c_str': | ||
// expected-error@+1 {{initializing 'const char * __terminated_by(42)' (aka 'const char *') with an expression of incompatible type 'const char *' is an unsafe operation}} | ||
const char * __terminated_by(42) p7 = cxxstr.c_str(); | ||
} | ||
|
||
void test_explicit_cast(char * p, const char * q) { | ||
// expected-error@+1 {{casting 'char *' to incompatible type 'const char * __terminated_by(0)' (aka 'const char *') is an unsafe operation}} | ||
const char * __null_terminated nt = (const char * __null_terminated) p; | ||
const char * __null_terminated nt2 = reinterpret_cast<const char * __null_terminated> (p); // FP for now | ||
|
||
// expected-error@+1 {{casting 'char *' to incompatible type 'const char * __terminated_by(0)' (aka 'const char *') is an unsafe operation}} | ||
nt = (const char * __null_terminated) p; | ||
nt2 = reinterpret_cast<const char * __null_terminated> (p); // FP for now | ||
// expected-error@+1 {{casting 'char *' to incompatible type 'const char * __terminated_by(0)' (aka 'const char *') is an unsafe operation}} | ||
nt2 = static_cast<const char * __null_terminated> (p); // FP for now | ||
|
||
// expected-error@+1 {{casting 'const char *' to incompatible type 'const char * __terminated_by(0)' (aka 'const char *') is an unsafe operation}} | ||
const char * __null_terminated nt3 = (const char * __null_terminated) q; | ||
const char * __null_terminated nt4 = reinterpret_cast<const char * __null_terminated> (q); // FP for now | ||
|
||
// expected-error@+1 {{casting 'const char *' to incompatible type 'const char * __terminated_by(0)' (aka 'const char *') is an unsafe operation}} | ||
nt3 = (const char * __null_terminated) q; | ||
nt4 = reinterpret_cast<const char * __null_terminated> (q); // FP for now | ||
// expected-error@+1 {{casting 'const char *' to incompatible type 'const char * __terminated_by(0)' (aka 'const char *') is an unsafe operation}} | ||
nt4 = static_cast<const char * __null_terminated> (q); | ||
|
||
// OK cases for C-style casts | ||
char * __null_terminated nt_p; | ||
const char * __null_terminated nt_p2; | ||
int * __null_terminated nt_p3; | ||
const int * __null_terminated nt_p4; | ||
|
||
const char * __null_terminated nt5 = (const char * __null_terminated) nt_p; | ||
const char * __null_terminated nt6 = (const char * __null_terminated) nt_p2; | ||
const char * __null_terminated nt7 = (const char * __null_terminated) nt_p3; | ||
const char * __null_terminated nt8 = (const char * __null_terminated) nt_p4; | ||
|
||
nt5 = (const char * __null_terminated) nt_p; | ||
nt6 = (const char * __null_terminated) nt_p2; | ||
nt7 = (const char * __null_terminated) nt_p3; | ||
nt8 = (const char * __null_terminated) nt_p4; | ||
|
||
|
||
char * __null_terminated nt9 = (char * __null_terminated) nt_p; | ||
char * __null_terminated nt10 = (char * __null_terminated) nt_p2; | ||
char * __null_terminated nt11 = (char * __null_terminated) nt_p3; | ||
char * __null_terminated nt12 = (char * __null_terminated) nt_p4; | ||
|
||
nt9 = (char * __null_terminated) nt_p; | ||
nt10 = (char * __null_terminated) nt_p2; | ||
nt11 = (char * __null_terminated) nt_p3; | ||
nt12 = (char * __null_terminated) nt_p4; | ||
} | ||
|
||
const char * __null_terminated test_return(const char * p, char * q, std::string &str) { | ||
if (p) | ||
return p; // expected-error {{returning 'const char *' from a function with incompatible result type 'const char * __terminated_by(0)' (aka 'const char *') is an unsafe operation}} | ||
if (q) | ||
return q; // expected-error {{returning 'char *' from a function with incompatible result type 'const char * __terminated_by(0)' (aka 'const char *') is an unsafe operation}} | ||
return str.c_str(); | ||
} | ||
|
||
void test_array(char * cstr) { | ||
const char arr[__null_terminated 3] = {'h', 'i', '\0'}; | ||
// expected-error@+1 {{array 'arr2' with '__terminated_by' attribute is initialized with an incorrect terminator (expected: 0; got 'i')}} | ||
const char arr2[__null_terminated 2] = {'h', 'i'}; | ||
const char * __null_terminated arr3[] = {"hello", "world"}; | ||
// expected-error@+1 {{initializing 'const char * __terminated_by(0)' (aka 'const char *') with an expression of incompatible type 'char *' is an unsafe operation}} | ||
const char * __null_terminated arr4[] = {"hello", "world", cstr}; | ||
|
||
// expected-error@+1 {{assigning to 'const char * __terminated_by(0)' (aka 'const char *') from incompatible type 'char *' is an unsafe operation}} | ||
arr3[0] = cstr; | ||
} | ||
|
||
struct T { | ||
int a; | ||
const char * __null_terminated p; | ||
struct TT { | ||
int a; | ||
const char * __null_terminated p; | ||
} tt; | ||
}; | ||
void test_compound(char * cstr) { | ||
std::string str; | ||
T t = {42, "hello"}; | ||
T t2 = {.a = 42}; | ||
T t3 = {.p = str.c_str()}; | ||
// expected-error@+1 {{initializing 'const char * __terminated_by(0)' (aka 'const char *') with an expression of incompatible type 'char *' is an unsafe operation}} | ||
T t4 = {42, "hello", {.p = cstr}}; | ||
|
||
// expected-error@+1 {{initializing 'const char * __terminated_by(0)' (aka 'const char *') with an expression of incompatible type 'char *' is an unsafe operation}} | ||
t4 = (struct T){42, "hello", {.p = cstr}}; | ||
} | ||
|
||
// expected-error@+3 {{initializing 'const char * __terminated_by(0)' (aka 'const char *') with an expression of incompatible type 'char *' is an unsafe operation}} | ||
class C { | ||
const char * __null_terminated p; | ||
const char * __null_terminated q = (char *) 1; // warn | ||
struct T t; | ||
public: | ||
// expected-error@+1 2{{initializing 'const char * __terminated_by(0)' (aka 'const char *') with an expression of incompatible type 'char *' is an unsafe operation}} | ||
C(char * p): p(p), t({0, p}) {}; | ||
C(const char * __null_terminated p, struct T t); | ||
}; | ||
|
||
void f(const C &c); | ||
C test_class(char * cstr) { | ||
// expected-error@+2 {{passing 'char *' to parameter of incompatible type 'const char * __terminated_by(0)' (aka 'const char *') is an unsafe operation}} | ||
// expected-error@+1 {{initializing 'const char * __terminated_by(0)' (aka 'const char *') with an expression of incompatible type 'char *' is an unsafe operation}} | ||
C c{cstr, {0, cstr}}; | ||
// expected-error@+2 {{passing 'char *' to parameter of incompatible type 'const char * __terminated_by(0)' (aka 'const char *') is an unsafe operation}} | ||
// expected-error@+1 {{initializing 'const char * __terminated_by(0)' (aka 'const char *') with an expression of incompatible type 'char *' is an unsafe operation}} | ||
C c1(cstr, {0, cstr}); | ||
// expected-error@+2 {{passing 'char *' to parameter of incompatible type 'const char * __terminated_by(0)' (aka 'const char *') is an unsafe operation}} | ||
// expected-error@+1 {{initializing 'const char * __terminated_by(0)' (aka 'const char *') with an expression of incompatible type 'char *' is an unsafe operation}} | ||
C *c2 = new C(cstr, {0, cstr}); | ||
// expected-error@+2 {{passing 'char *' to parameter of incompatible type 'const char * __terminated_by(0)' (aka 'const char *') is an unsafe operation}} | ||
// expected-error@+1 {{initializing 'const char * __terminated_by(0)' (aka 'const char *') with an expression of incompatible type 'char *' is an unsafe operation}} | ||
f(C{cstr, {0, cstr}}); | ||
|
||
C("hello", {0, "hello"}); | ||
if (1-1) | ||
return {cstr, {}}; // expected-error {{passing 'char *' to parameter of incompatible type 'const char * __terminated_by(0)' (aka 'const char *') is an unsafe operation}} | ||
return {"hello", {}}; | ||
} | ||
|
||
|
||
// Test input/output __null_terminated parameter. | ||
// expected-note@+1 {{candidate function not viable:}} | ||
void g(const char * __null_terminated *p); | ||
void test_output(const char * __null_terminated p) { | ||
const char * __null_terminated local_nt = p; | ||
const char * const_local; | ||
char * local; | ||
|
||
g(&local_nt); // safe | ||
// expected-error@+1 {{passing 'const char **' to parameter of incompatible type 'const char * __terminated_by(0)*' (aka 'const char **') that adds '__terminated_by' attribute is not allowed}} | ||
g(&const_local); | ||
// expected-error@+1 {{no matching function for call to 'g'}} | ||
g(&local); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to assert that
Self.getLangOpts().BoundsSafety || ConvertTy == Sema::Compatible || (ConvertTy == Sema::IncompatibleNonValueTerminatedToValueTerminatedPointer && InCXXBoundsSafetyMode)
? Especially sinceCheckValueTerminatedAssignmentConstraints
used to only run underBoundsSafety
, but now it runs underBoundsSafetyAttributes
even for C and in C++ without safe buffers mode. We really don't want it to accidentally start returning errors in these modes. Otherwise LGTMUh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair point. But maybe the following is enough?
Self.getLangOpts().BoundsSafety || Self.isCXXSafeBuffersBoundsSafetyInteropEnabledAt(SrcExpr.get()->getBeginLoc())
,where
isCXXSafeBuffersBoundsSafetyInteropEnabledAt
is defined byThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That works for me 👍