Skip to content

Commit b35a6cb

Browse files
committed
Conservatively set no_preserve_tags for MemberExprs
If we have an expression such as __builtin_memcpy(buf, &s.not_a_cap, len); and we can see the declaration for s and the entire VarDecl for s does not contain tags and len does not extend beyond the end of s, then we can set no_preserve_tags.
1 parent 4b779dd commit b35a6cb

File tree

4 files changed

+136
-68
lines changed

4 files changed

+136
-68
lines changed

clang/lib/CodeGen/CGClass.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -982,7 +982,7 @@ namespace {
982982

983983
// We can pass EffectiveTypeKnown=true since this a C++ field copy.
984984
auto PreserveTags = CGF.getTypes().copyShouldPreserveTagsForPointee(
985-
RecordTy, /*EffectiveTypeKnown=*/true, MemcpySize);
985+
RecordTy, /*EffectiveTypeKnown=*/true, MemcpySize, FirstByteOffset);
986986
emitMemcpyIR(
987987
Dest.isBitField() ? Dest.getBitFieldAddress() : Dest.getAddress(CGF),
988988
Src.isBitField() ? Src.getBitFieldAddress() : Src.getAddress(CGF),

clang/lib/CodeGen/CodeGenTypes.cpp

Lines changed: 102 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -959,11 +959,13 @@ static bool isLessThanCapSize(const ASTContext &Context,
959959
}
960960

961961
static bool copiesAtMostTypeSize(const QualType Ty, const ASTContext &Context,
962-
Optional<CharUnits> Size) {
962+
Optional<CharUnits> Size,
963+
uint64_t BitOffset = 0) {
963964
if (!Size)
964965
return false;
965-
auto TypeSize = Context.getTypeSizeInCharsIfKnown(Ty);
966-
return TypeSize && Size <= TypeSize;
966+
auto TypeSize = Context.getTypeSize(Ty);
967+
assert(BitOffset <= TypeSize);
968+
return (uint64_t)Context.toBits(*Size) <= TypeSize - BitOffset;
967969
}
968970

969971
llvm::PreserveCheriTags
@@ -1004,58 +1006,127 @@ CodeGenTypes::copyShouldPreserveTags(const Expr *DestPtr, const Expr *SrcPtr,
10041006
return DstPreserve;
10051007
}
10061008

1007-
static const VarDecl *findUnderlyingVarDecl(const Expr *E) {
1008-
// Note: this is pretty similar to E->getReferencedDeclOfCallee(); and should
1009-
// possibly be moved to Expr.cpp
1009+
struct UnderlyingVarDeclInfo {
1010+
const ValueDecl *MemberDecl = nullptr;
1011+
QualType CopiedType;
1012+
uint64_t CopiedTypeOffset = 0;
1013+
bool ConservativeApproxmation = false;
1014+
bool CanAffectOtherTypes = false;
1015+
};
1016+
1017+
static const ValueDecl *
1018+
findUnderlyingVarDeclForCopy(const ASTContext &Context, const Expr *E,
1019+
UnderlyingVarDeclInfo &Info,
1020+
Optional<CharUnits> Size) {
10101021
const Expr *UnderlyingExpr = E->IgnoreParenImpCasts();
10111022
if (auto *UO = dyn_cast<UnaryOperator>(UnderlyingExpr)) {
1023+
// NB: We only look through AddrOf here to ensure that we don't classify
1024+
// expressions such as `*(&ptr)` as having a known underlying type (it could
1025+
// still be anything as that expression is equivalent to just `ptr`).
10121026
if (UO->getOpcode() == UO_AddrOf) {
1013-
return findUnderlyingVarDecl(UO->getSubExpr());
1027+
return findUnderlyingVarDeclForCopy(Context, UO->getSubExpr(), Info,
1028+
Size);
10141029
}
1015-
} else if (auto DRE = dyn_cast<DeclRefExpr>(UnderlyingExpr)) {
1016-
return dyn_cast<const VarDecl>(DRE->getDecl());
1030+
} else if (auto *DRE = dyn_cast<DeclRefExpr>(UnderlyingExpr)) {
1031+
return DRE->getDecl();
1032+
} else if (auto *ME = dyn_cast<MemberExpr>(UnderlyingExpr)) {
1033+
Info.MemberDecl = ME->getMemberDecl();
1034+
UnderlyingVarDeclInfo IgnoredInfo; // Don't update `Info` when recursing.
1035+
// If the copy size is up to sizeof(member), we can precisely determine the
1036+
// type and do not have to be conservative.
1037+
if (copiesAtMostTypeSize(Info.MemberDecl->getType(), Context, Size)) {
1038+
Info.CopiedType = Info.MemberDecl->getType();
1039+
return findUnderlyingVarDeclForCopy(Context, ME->getBase(), IgnoredInfo,
1040+
Size);
1041+
}
1042+
// For address-of member expressions where the size is not known to be just
1043+
// that member (or a subset thereof), we must conservatively assume that
1044+
// the copy extends across all fields of the struct (and possibly
1045+
// following instances of that struct for arrays and/or containing structs).
1046+
Info.CanAffectOtherTypes = true;
1047+
Info.ConservativeApproxmation = true;
1048+
// There is one optimization we can make though: If copy size - field offset
1049+
// is up to the size of the containing struct/class we can use that struct
1050+
// as a precise approximation.
1051+
// XXX: we could only look at the following fields, but that would make
1052+
// the analysis rather complicated and is unlikely to have large benefits.
1053+
if (auto *FD = dyn_cast<FieldDecl>(Info.MemberDecl)) {
1054+
auto ParentType = QualType(FD->getParent()->getTypeForDecl(), 0);
1055+
auto FieldOffset = Context.getFieldOffset(FD);
1056+
if (copiesAtMostTypeSize(ParentType, Context, Size, FieldOffset)) {
1057+
Info.CopiedType = ParentType;
1058+
Info.CopiedTypeOffset = FieldOffset;
1059+
Info.CanAffectOtherTypes = false;
1060+
}
1061+
}
1062+
return findUnderlyingVarDeclForCopy(Context, ME->getBase(), IgnoredInfo,
1063+
Size);
10171064
}
1018-
// TODO: We could improve analysis for MemberExpr, but only if the copy size
1019-
// is <= the size of the member, since memcpy() accross multiple fields is
1020-
// a something that exists (despite not being compatible with sub-object
1021-
// bounds). For now we just look at the declaration of the entire struct
1022-
return nullptr;
1065+
Info.CanAffectOtherTypes = true;
1066+
return {};
10231067
}
10241068

10251069
llvm::PreserveCheriTags
10261070
CodeGenTypes::copyShouldPreserveTags(const Expr *E, Optional<CharUnits> Size) {
10271071
assert(E->getType()->isAnyPointerType());
1072+
assert(!isLessThanCapSize(Context, Size) &&
1073+
"Should not call this function for small copies!");
10281074
// Ignore the implicit cast to void* for the memcpy call.
10291075
// Note: IgnoreParenImpCasts() might strip function/array-to-pointer decay
10301076
// so we can't always call getPointeeType().
1031-
QualType Ty = E->IgnoreParenImpCasts()->getType();
1032-
if (Ty->isAnyPointerType())
1033-
Ty = Ty->getPointeeType();
1077+
QualType ExprTy = E->IgnoreParenImpCasts()->getType();
1078+
QualType PointeeTy =
1079+
ExprTy->isAnyPointerType() ? ExprTy->getPointeeType() : ExprTy;
1080+
QualType CheckTy = PointeeTy;
10341081
bool EffectiveTypeKnown = false;
1035-
const VarDecl *UnderlyingVar = findUnderlyingVarDecl(E);
1036-
if (UnderlyingVar) {
1037-
QualType VarTy = UnderlyingVar->getType();
1038-
assert(!VarTy->isIncompleteType() && "Unexpected incomplete type");
1039-
if (VarTy->isReferenceType()) {
1082+
UnderlyingVarDeclInfo UnderlyingInfo;
1083+
const ValueDecl *UnderlyingDecl =
1084+
findUnderlyingVarDeclForCopy(Context, E, UnderlyingInfo, Size);
1085+
if (UnderlyingDecl) {
1086+
auto UnderlyingTy = UnderlyingDecl->getType();
1087+
assert(!UnderlyingTy->isIncompleteType() && "Unexpected incomplete type");
1088+
if (UnderlyingTy->isReferenceType()) {
10401089
// If the variable declaration is a C++ reference we can assume that the
10411090
// effective type of the object matches the type of the reference since
10421091
// forming the reference would have been invalid otherwise.
1043-
Ty = VarTy->getPointeeType();
1092+
CheckTy = UnderlyingTy->getPointeeType();
10441093
EffectiveTypeKnown = true;
1045-
} else if (!VarTy->isAnyPointerType()) {
1046-
// If we found a non-pointer declaration that we are copying to/from, use
1094+
} else if (!UnderlyingTy->isAnyPointerType()) {
1095+
// If we found a non-pointer declaration (e.g. address-of a variable), use
10471096
// the type of the declaration for the analysis since that defines the
10481097
// effective type. For pointers we can't assume anything since they could
10491098
// be "allocated objects" without a declared type.
1050-
Ty = VarTy;
1051-
EffectiveTypeKnown = true;
1099+
CheckTy = UnderlyingTy;
1100+
EffectiveTypeKnown = !UnderlyingInfo.CanAffectOtherTypes;
10521101
}
1102+
// If we were able to precisely/conservatively determine the copied type
1103+
// use that instead of the VarDecl type for the tag-preservation checks.
1104+
if (!UnderlyingInfo.CopiedType.isNull())
1105+
CheckTy = UnderlyingInfo.CopiedType;
1106+
}
1107+
llvm::PreserveCheriTags Result = copyShouldPreserveTagsForPointee(
1108+
CheckTy, EffectiveTypeKnown, Size, UnderlyingInfo.CopiedTypeOffset);
1109+
if (UnderlyingInfo.ConservativeApproxmation &&
1110+
Result == llvm::PreserveCheriTags::Required) {
1111+
// If we had a MemberExpr and were looking at the entire containing struct
1112+
// rather than just the single field, we only add the must_preserve_tags
1113+
// attribute if the pointee type is actually a capability type. This avoids
1114+
// lots of unnecessary warnings and still lets backend/middle-end decide
1115+
// how to handle the copy.
1116+
// TODO: this can be removed once we no longer add must_preserve_cheri_tags.
1117+
assert(UnderlyingInfo.MemberDecl != nullptr);
1118+
if (copyShouldPreserveTagsForPointee(UnderlyingInfo.MemberDecl->getType(),
1119+
/*EffectiveTypeKnown=*/true, Size,
1120+
0) !=
1121+
llvm::PreserveCheriTags::Required)
1122+
Result = llvm::PreserveCheriTags::Unknown;
10531123
}
1054-
return copyShouldPreserveTagsForPointee(Ty, EffectiveTypeKnown, Size);
1124+
return Result;
10551125
}
10561126

10571127
llvm::PreserveCheriTags CodeGenTypes::copyShouldPreserveTagsForPointee(
1058-
QualType Pointee, bool EffectiveTypeKnown, Optional<CharUnits> Size) {
1128+
QualType Pointee, bool EffectiveTypeKnown, Optional<CharUnits> Size,
1129+
uint64_t CopyOffsetInBits) {
10591130
// Don't add the no_preserve_tags/must_preserve_tags attribute for non-CHERI
10601131
// targets to avoid changing tests and to avoid compile-time impact.
10611132
if (!Context.getTargetInfo().SupportsCapabilities())
@@ -1102,7 +1173,7 @@ llvm::PreserveCheriTags CodeGenTypes::copyShouldPreserveTagsForPointee(
11021173
// another structure and the copy could affect adjacent capability data.s
11031174
// If the copy size is <= sizeof(T), we can still mark copies as
11041175
// non-tag-preserving since it cannot affect subclass/adjacent data.
1105-
if (!copiesAtMostTypeSize(Pointee, Context, Size))
1176+
if (!copiesAtMostTypeSize(Pointee, Context, Size, CopyOffsetInBits))
11061177
return llvm::PreserveCheriTags::Unknown;
11071178
// structures without fields could be used as an opaque type -> assume it
11081179
// might contain capabilities
@@ -1120,7 +1191,7 @@ llvm::PreserveCheriTags CodeGenTypes::copyShouldPreserveTagsForPointee(
11201191
// having to call the library function.
11211192
return llvm::PreserveCheriTags::Unnecessary;
11221193
} else if (Context.cannotContainCapabilities(Pointee) &&
1123-
copiesAtMostTypeSize(Pointee, Context, Size)) {
1194+
copiesAtMostTypeSize(Pointee, Context, Size, CopyOffsetInBits)) {
11241195
// If the type cannot contain capabilities and we are copying at most
11251196
// sizeof(type), then we can use a non-tag-preserving copy.
11261197
return llvm::PreserveCheriTags::Unnecessary;

clang/lib/CodeGen/CodeGenTypes.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -324,12 +324,15 @@ class CodeGenTypes {
324324
/// pointee type rather than the type of the buffer pointer.
325325
llvm::PreserveCheriTags
326326
copyShouldPreserveTagsForPointee(QualType CopyTy, bool EffectiveTypeKnown,
327-
Optional<CharUnits> Size);
327+
Optional<CharUnits> Size,
328+
uint64_t CopyOffsetInBits);
328329
llvm::PreserveCheriTags
329330
copyShouldPreserveTagsForPointee(QualType CopyTy, bool EffectiveTypeKnown,
330-
const llvm::Value *Size) {
331+
const llvm::Value *Size,
332+
uint64_t CopyOffsetInBits = 0) {
331333
return copyShouldPreserveTagsForPointee(CopyTy, EffectiveTypeKnown,
332-
copySizeInCharUnits(Size));
334+
copySizeInCharUnits(Size),
335+
CopyOffsetInBits);
333336
}
334337

335338
bool isRecordLayoutComplete(const Type *Ty) const;

clang/test/CodeGenCXX/cheri/no-tag-copy-member-expr.cpp

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -43,26 +43,25 @@ void test_member_expr_byval(void *buf, struct TestWithCap t, struct TestNoCap t2
4343
// CHECK: call void @llvm.memcpy.p200i8.p200i8.i64(
4444
// CHECK-SAME: , i64 32, i1 false) [[MUST_PRESERVE_CHARPTR:#[0-9]+]]{{$}}
4545
// No attribute for the following four cases:
46-
__builtin_memcpy(buf, &t.not_a_cap, 32);
46+
__builtin_memcpy(buf, &t.not_a_cap, 64);
4747
// CHECK: call void @llvm.memcpy.p200i8.p200i8.i64(
48-
// CHECK-SAME: , i64 32, i1 false){{$}}
49-
__builtin_memcpy(buf, t.array, 32);
48+
// CHECK-SAME: , i64 64, i1 false){{$}}
49+
__builtin_memcpy(buf, t.array, 48);
5050
// CHECK: call void @llvm.memcpy.p200i8.p200i8.i64(
51-
// CHECK-SAME: , i64 32, i1 false){{$}}
51+
// CHECK-SAME: , i64 48, i1 false){{$}}
5252
__builtin_memcpy(buf, &t.n, 32);
5353
// CHECK: call void @llvm.memcpy.p200i8.p200i8.i64(
5454
// CHECK-SAME: , i64 32, i1 false){{$}}
55-
__builtin_memcpy(buf, (&t)->array, 32);
55+
__builtin_memcpy(buf, (&t)->array, 48);
5656
// CHECK: call void @llvm.memcpy.p200i8.p200i8.i64(
57-
// CHECK-SAME: , i64 32, i1 false){{$}}
57+
// CHECK-SAME: , i64 48, i1 false){{$}}
5858
__builtin_memcpy(buf, &(&t)->not_a_cap, 32);
5959
// CHECK: call void @llvm.memcpy.p200i8.p200i8.i64(
6060
// CHECK-SAME: , i64 32, i1 false){{$}}
6161

6262
// However, for the struct without capabilities all of these should be safe:
6363
// However, we don't know here that there is no subclass that could have
64-
// capabilities following the current object.
65-
// TODO: If we know the size of the copy <= sizeof(T), we should set the attribute.
64+
// capabilities following the current object, so we have to look at the copy size.
6665
__builtin_memcpy(buf, &t2, sizeof(t2));
6766
// CHECK: call void @llvm.memcpy.p200i8.p200i8.i64(
6867
// CHECK-SAME: , i64 80, i1 false) [[NO_PRESERVE_TAGS:#[0-9]+]]{{$}}
@@ -72,24 +71,24 @@ void test_member_expr_byval(void *buf, struct TestWithCap t, struct TestNoCap t2
7271
// CHECK-SAME: , i64 96, i1 false){{$}}
7372
__builtin_memcpy(buf, &t2.not_a_cap, 40);
7473
// CHECK: call void @llvm.memcpy.p200i8.p200i8.i64(
75-
// CHECK-SAME: , i64 40, i1 false){{$}}
76-
// TODO-CHECK-SAME: , i64 40, i1 false) [[NO_PRESERVE_TAGS]]{{$}}
74+
// CHECK-SAME: , i64 40, i1 false) [[NO_PRESERVE_TAGS]]{{$}}
75+
__builtin_memcpy(buf, t2.array, 32);
76+
// CHECK: call void @llvm.memcpy.p200i8.p200i8.i64(
77+
// CHECK-SAME: , i64 32, i1 false) [[NO_PRESERVE_TAGS]]{{$}}
78+
/// Check that we also set the attribute if we are copying beyond
79+
/// the end of the array-decay expressions.
7780
__builtin_memcpy(buf, t2.array, 40);
7881
// CHECK: call void @llvm.memcpy.p200i8.p200i8.i64(
79-
// CHECK-SAME: , i64 40, i1 false){{$}}
80-
// TODO-CHECK-SAME: , i64 40, i1 false) [[NO_PRESERVE_TAGS]]{{$}}
82+
// CHECK-SAME: , i64 40, i1 false) [[NO_PRESERVE_TAGS]]{{$}}
8183
__builtin_memcpy(buf, &t2.n, 40);
8284
// CHECK: call void @llvm.memcpy.p200i8.p200i8.i64(
83-
// CHECK-SAME: , i64 40, i1 false){{$}}
84-
// TODO-CHECK-SAME: , i64 40, i1 false) [[NO_PRESERVE_TAGS]]{{$}}
85+
// CHECK-SAME: , i64 40, i1 false) [[NO_PRESERVE_TAGS]]{{$}}
8586
__builtin_memcpy(buf, (&t2)->array, 40);
8687
// CHECK: call void @llvm.memcpy.p200i8.p200i8.i64(
87-
// CHECK-SAME: , i64 40, i1 false){{$}}
88-
// TODO-CHECK-SAME: , i64 40, i1 false) [[NO_PRESERVE_TAGS]]{{$}}
88+
// CHECK-SAME: , i64 40, i1 false) [[NO_PRESERVE_TAGS]]{{$}}
8989
__builtin_memcpy(buf, &(&t2)->not_a_cap, 40);
9090
// CHECK: call void @llvm.memcpy.p200i8.p200i8.i64(
91-
// CHECK-SAME: , i64 40, i1 false){{$}}
92-
// TODO-CHECK-SAME: , i64 40, i1 false) [[NO_PRESERVE_TAGS]]{{$}}
91+
// CHECK-SAME: , i64 40, i1 false) [[NO_PRESERVE_TAGS]]{{$}}
9392

9493
// Direct assignment should always the no_preserve_tags attribute (the C2x
9594
// 6.5 memcpy+"Allocated objects have no declared type." case does not apply):
@@ -177,15 +176,15 @@ void test_member_expr_ref(void *buf, struct TestWithCap &t, struct TestNoCap &t2
177176
__builtin_memcpy(buf, &t.not_a_cap, 32);
178177
// CHECK-CXX: call void @llvm.memcpy.p200i8.p200i8.i64(
179178
// CHECK-CXX-SAME: , i64 32, i1 false){{$}}
180-
__builtin_memcpy(buf, t.array, 32);
179+
__builtin_memcpy(buf, t.array, 40);
181180
// CHECK-CXX: call void @llvm.memcpy.p200i8.p200i8.i64(
182-
// CHECK-CXX-SAME: , i64 32, i1 false){{$}}
181+
// CHECK-CXX-SAME: , i64 40, i1 false){{$}}
183182
__builtin_memcpy(buf, &t.n, 32);
184183
// CHECK-CXX: call void @llvm.memcpy.p200i8.p200i8.i64(
185184
// CHECK-CXX-SAME: , i64 32, i1 false){{$}}
186-
__builtin_memcpy(buf, (&t)->array, 32);
185+
__builtin_memcpy(buf, (&t)->array, 40);
187186
// CHECK-CXX: call void @llvm.memcpy.p200i8.p200i8.i64(
188-
// CHECK-CXX-SAME: , i64 32, i1 false){{$}}
187+
// CHECK-CXX-SAME: , i64 40, i1 false){{$}}
189188
__builtin_memcpy(buf, &(&t)->not_a_cap, 32);
190189
// CHECK-CXX: call void @llvm.memcpy.p200i8.p200i8.i64(
191190
// CHECK-CXX-SAME: , i64 32, i1 false){{$}}
@@ -196,24 +195,19 @@ void test_member_expr_ref(void *buf, struct TestWithCap &t, struct TestNoCap &t2
196195
// CHECK-CXX-SAME: , i64 40, i1 false) [[NO_PRESERVE_TAGS]]{{$}}
197196
__builtin_memcpy(buf, &t2.not_a_cap, 40);
198197
// CHECK-CXX: call void @llvm.memcpy.p200i8.p200i8.i64(
199-
// CHECK-CXX-SAME: , i64 40, i1 false){{$}}
200-
// TODO-CHECK-CXX-SAME: , i64 40, i1 false) [[NO_PRESERVE_TAGS]]{{$}}
198+
// CHECK-CXX-SAME: , i64 40, i1 false) [[NO_PRESERVE_TAGS]]{{$}}
201199
__builtin_memcpy(buf, t2.array, 40);
202200
// CHECK-CXX: call void @llvm.memcpy.p200i8.p200i8.i64(
203-
// CHECK-CXX-SAME: , i64 40, i1 false){{$}}
204-
// TODO-CHECK-CXX-SAME: , i64 40, i1 false) [[NO_PRESERVE_TAGS]]{{$}}
201+
// CHECK-CXX-SAME: , i64 40, i1 false) [[NO_PRESERVE_TAGS]]{{$}}
205202
__builtin_memcpy(buf, &t2.n, 40);
206203
// CHECK-CXX: call void @llvm.memcpy.p200i8.p200i8.i64(
207-
// CHECK-CXX-SAME: , i64 40, i1 false){{$}}
208-
// TODO-CHECK-CXX-SAME: , i64 40, i1 false) [[NO_PRESERVE_TAGS]]{{$}}
204+
// CHECK-CXX-SAME: , i64 40, i1 false) [[NO_PRESERVE_TAGS]]{{$}}
209205
__builtin_memcpy(buf, (&t2)->array, 40);
210206
// CHECK-CXX: call void @llvm.memcpy.p200i8.p200i8.i64(
211-
// CHECK-CXX-SAME: , i64 40, i1 false){{$}}
212-
// TODO-CHECK-CXX-SAME: , i64 40, i1 false) [[NO_PRESERVE_TAGS]]{{$}}
207+
// CHECK-CXX-SAME: , i64 40, i1 false) [[NO_PRESERVE_TAGS]]{{$}}
213208
__builtin_memcpy(buf, &(&t2)->not_a_cap, 40);
214209
// CHECK-CXX: call void @llvm.memcpy.p200i8.p200i8.i64(
215-
// CHECK-CXX-SAME: , i64 40, i1 false){{$}}
216-
// TODO-CHECK-CXX-SAME: , i64 40, i1 false) [[NO_PRESERVE_TAGS]]{{$}}
210+
// CHECK-CXX-SAME: , i64 40, i1 false) [[NO_PRESERVE_TAGS]]{{$}}
217211

218212
// Direct assignment should always the no_preserve_tags attribute (the C2x
219213
// 6.5 memcpy+"Allocated objects have no declared type." case does not apply):
@@ -225,4 +219,4 @@ void test_member_expr_ref(void *buf, struct TestWithCap &t, struct TestNoCap &t2
225219

226220
// CHECK: attributes [[MUST_PRESERVE_STRUCT_WITHCAP]] = { must_preserve_cheri_tags "frontend-memtransfer-type"="'struct TestWithCap'" }
227221
// CHECK: attributes [[MUST_PRESERVE_CHARPTR]] = { must_preserve_cheri_tags "frontend-memtransfer-type"="'char * __capability'" }
228-
// CHECK-C: attributes [[NO_PRESERVE_TAGS]] = { no_preserve_cheri_tags }
222+
// CHECK: attributes [[NO_PRESERVE_TAGS]] = { no_preserve_cheri_tags }

0 commit comments

Comments
 (0)