Skip to content

Commit 3dd1d88

Browse files
authored
[Clang] Implement labelled type filtering for overflow/truncation sanitizers w/ SSCLs (#107332)
[Related RFC](https://discourse.llvm.org/t/rfc-support-globpattern-add-operator-to-invert-matches/80683/5?u=justinstitt) ### Summary Implement type-based filtering via [Sanitizer Special Case Lists](https://clang.llvm.org/docs/SanitizerSpecialCaseList.html) for the arithmetic overflow and truncation sanitizers. Currently, using the `type:` prefix with these sanitizers does nothing. I've hooked up the SSCL parsing with Clang codegen so that we don't emit the overflow/truncation checks if the arithmetic contains an ignored type. ### Usefulness You can craft ignorelists that ignore specific types that are expected to overflow or wrap-around. For example, to ignore `my_type` from `unsigned-integer-overflow` instrumentation: ```bash $ cat ignorelist.txt [unsigned-integer-overflow] type:my_type=no_sanitize $ cat foo.c typedef unsigned long my_type; void foo() { my_type a = ULONG_MAX; ++a; } $ clang foo.c -fsanitize=unsigned-integer-overflow -fsanitize-ignorelist=ignorelist.txt ; ./a.out // --> no sanitizer error ``` If a type is functionally intended to overflow, like [refcount_t](https://kernsec.org/wiki/index.php/Kernel_Protections/refcount_t) and its associated APIs in the Linux kernel, then this type filtering would prove useful for reducing sanitizer noise. Currently, the Linux kernel dealt with this by [littering](https://elixir.bootlin.com/linux/v6.10.8/source/include/linux/refcount.h#L139 ) `__attribute__((no_sanitize("signed-integer-overflow")))` annotations on all the `refcount_t` APIs. I think this serves as an example of how a codebase could be made cleaner. We could make custom types that are filtered out in an ignorelist, allowing for types to be more expressive -- without the need for annotations. This accomplishes a similar goal to #86618. Yet another use case for this type filtering is whitelisting. We could ignore _all_ types, save a few. ```bash $ cat ignorelist.txt [implicit-signed-integer-truncation] type:*=no_sanitize # ignore literally all types type:short=sanitize # except `short` $ cat bar.c // compile with -fsanitize=implicit-signed-integer-truncation void bar(int toobig) { char a = toobig; // not instrumented short b = toobig; // instrumented } ``` ### Other ways to accomplish the goal of sanitizer allowlisting/whitelisting * ignore list SSCL type support (this PR that you're reading) * [my sanitize-allowlist branch](main...JustinStitt:llvm-project:sanitize-allowlist) - this just implements a sibling flag `-fsanitize-allowlist=`, removing some of the double negative logic present with `skip`/`ignore` when trying to whitelist something. * [Glob Negation](https://discourse.llvm.org/t/rfc-support-globpattern-add-operator-to-invert-matches/80683) - Implement a negation operator to the GlobPattern class so the ignorelist query can use them to simulate allowlisting Please let me know which of the three options we like best. They are not necessarily mutually exclusive. Here's [another related PR](#86618) which implements a `wraps` attribute. This can accomplish a similar goal to this PR but requires in-source changes to codebases and also covers a wider variety of integer definedness problems. ### CCs @kees @vitalybuka @bwendling --------- Signed-off-by: Justin Stitt <[email protected]>
1 parent f8535fd commit 3dd1d88

File tree

7 files changed

+266
-7
lines changed

7 files changed

+266
-7
lines changed

clang/docs/ReleaseNotes.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,14 @@ Sanitizers
871871
This new flag should allow those projects to enable integer sanitizers with
872872
less noise.
873873

874+
- Arithmetic overflow sanitizers ``-fsanitize=signed-integer-overflow`` and
875+
``-fsanitize=unsigned-integer-overflow`` as well as the implicit integer
876+
truncation sanitizers ``-fsanitize=implicit-signed-integer-truncation`` and
877+
``-fsanitize=implicit-unsigned-integer-truncation`` now properly support the
878+
"type" prefix within `Sanitizer Special Case Lists (SSCL)
879+
<https://clang.llvm.org/docs/SanitizerSpecialCaseList.html>`_. See that link
880+
for examples.
881+
874882
Python Binding Changes
875883
----------------------
876884
- Fixed an issue that led to crashes when calling ``Type.get_exception_specification_kind``.

clang/docs/SanitizerSpecialCaseList.rst

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ file at compile-time.
1515
Goal and usage
1616
==============
1717

18-
Users of sanitizer tools, such as :doc:`AddressSanitizer`, :doc:`ThreadSanitizer`
19-
or :doc:`MemorySanitizer` may want to disable or alter some checks for
20-
certain source-level entities to:
18+
Users of sanitizer tools, such as :doc:`AddressSanitizer`,
19+
:doc:`ThreadSanitizer`, :doc:`MemorySanitizer` or :doc:`UndefinedBehaviorSanitizer`
20+
may want to disable or alter some checks for certain source-level entities to:
2121

2222
* speedup hot function, which is known to be correct;
2323
* ignore a function that does some low-level magic (e.g. walks through the
@@ -48,6 +48,61 @@ Example
4848
$ clang -fsanitize=address -fsanitize-ignorelist=ignorelist.txt foo.c ; ./a.out
4949
# No error report here.
5050
51+
Usage with UndefinedBehaviorSanitizer
52+
=====================================
53+
54+
The arithmetic overflow sanitizers ``unsigned-integer-overflow`` and
55+
``signed-integer-overflow`` as well as the implicit integer truncation
56+
sanitizers ``implicit-signed-integer-truncation`` and
57+
``implicit-unsigned-integer-truncation`` support the ability to adjust
58+
instrumentation based on type.
59+
60+
By default, supported sanitizers will have their instrumentation disabled for
61+
types specified within an ignorelist.
62+
63+
.. code-block:: bash
64+
65+
$ cat foo.c
66+
void foo() {
67+
int a = 2147483647; // INT_MAX
68+
++a; // Normally, an overflow with -fsanitize=signed-integer-overflow
69+
}
70+
$ cat ignorelist.txt
71+
[signed-integer-overflow]
72+
type:int
73+
$ clang -fsanitize=signed-integer-overflow -fsanitize-ignorelist=ignorelist.txt foo.c ; ./a.out
74+
# no signed-integer-overflow error
75+
76+
For example, supplying the above ``ignorelist.txt`` to
77+
``-fsanitize-ignorelist=ignorelist.txt`` disables overflow sanitizer
78+
instrumentation for arithmetic operations containing values of type ``int``.
79+
80+
The ``=sanitize`` category is also supported. Any types assigned to the
81+
``sanitize`` category will have their sanitizer instrumentation remain. If the
82+
same type appears within or across ignorelists with different categories the
83+
``sanitize`` category takes precedence -- regardless of order.
84+
85+
With this, one may disable instrumentation for some or all types and
86+
specifically allow instrumentation for one or many types -- including types
87+
created via ``typedef``. This is a way to achieve a sort of "allowlist" for
88+
supported sanitizers.
89+
90+
.. code-block:: bash
91+
92+
$ cat ignorelist.txt
93+
[implicit-signed-integer-truncation]
94+
type:*
95+
type:T=sanitize
96+
97+
$ cat foo.c
98+
typedef char T;
99+
typedef char U;
100+
void foo(int toobig) {
101+
T a = toobig; // instrumented
102+
U b = toobig; // not instrumented
103+
char c = toobig; // also not instrumented
104+
}
105+
51106
Format
52107
======
53108

clang/include/clang/AST/ASTContext.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,9 @@ class ASTContext : public RefCountedBase<ASTContext> {
838838

839839
const NoSanitizeList &getNoSanitizeList() const { return *NoSanitizeL; }
840840

841+
bool isTypeIgnoredBySanitizer(const SanitizerMask &Mask,
842+
const QualType &Ty) const;
843+
841844
const XRayFunctionFilter &getXRayFilter() const {
842845
return *XRayFilter;
843846
}

clang/lib/AST/ASTContext.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,15 @@ ASTContext::getCanonicalTemplateTemplateParmDecl(
831831
return CanonTTP;
832832
}
833833

834+
/// Check if a type can have its sanitizer instrumentation elided based on its
835+
/// presence within an ignorelist.
836+
bool ASTContext::isTypeIgnoredBySanitizer(const SanitizerMask &Mask,
837+
const QualType &Ty) const {
838+
std::string TyName = Ty.getUnqualifiedType().getAsString(getPrintingPolicy());
839+
return NoSanitizeL->containsType(Mask, TyName) &&
840+
!NoSanitizeL->containsType(Mask, TyName, "sanitize");
841+
}
842+
834843
TargetCXXABI::Kind ASTContext::getCXXABIKind() const {
835844
auto Kind = getTargetInfo().getCXXABI().getKind();
836845
return getLangOpts().CXXABI.value_or(Kind);

clang/lib/CodeGen/CGExprScalar.cpp

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,18 @@ static bool CanElideOverflowCheck(const ASTContext &Ctx, const BinOpInfo &Op) {
197197
if (!Op.mayHaveIntegerOverflow())
198198
return true;
199199

200+
if (Op.Ty->isSignedIntegerType() &&
201+
Ctx.isTypeIgnoredBySanitizer(SanitizerKind::SignedIntegerOverflow,
202+
Op.Ty)) {
203+
return true;
204+
}
205+
206+
if (Op.Ty->isUnsignedIntegerType() &&
207+
Ctx.isTypeIgnoredBySanitizer(SanitizerKind::UnsignedIntegerOverflow,
208+
Op.Ty)) {
209+
return true;
210+
}
211+
200212
const UnaryOperator *UO = dyn_cast<UnaryOperator>(Op.E);
201213

202214
if (UO && UO->getOpcode() == UO_Minus &&
@@ -1125,6 +1137,10 @@ void ScalarExprEmitter::EmitIntegerTruncationCheck(Value *Src, QualType SrcType,
11251137
if (!CGF.SanOpts.has(Check.second.second))
11261138
return;
11271139

1140+
// Does some SSCL ignore this type?
1141+
if (CGF.getContext().isTypeIgnoredBySanitizer(Check.second.second, DstType))
1142+
return;
1143+
11281144
llvm::Constant *StaticArgs[] = {
11291145
CGF.EmitCheckSourceLocation(Loc), CGF.EmitCheckTypeDescriptor(SrcType),
11301146
CGF.EmitCheckTypeDescriptor(DstType),
@@ -1235,6 +1251,15 @@ void ScalarExprEmitter::EmitIntegerSignChangeCheck(Value *Src, QualType SrcType,
12351251
// Because here sign change check is interchangeable with truncation check.
12361252
return;
12371253
}
1254+
// Does an SSCL have an entry for the DstType under its respective sanitizer
1255+
// section?
1256+
if (DstSigned && CGF.getContext().isTypeIgnoredBySanitizer(
1257+
SanitizerKind::ImplicitSignedIntegerTruncation, DstType))
1258+
return;
1259+
if (!DstSigned &&
1260+
CGF.getContext().isTypeIgnoredBySanitizer(
1261+
SanitizerKind::ImplicitUnsignedIntegerTruncation, DstType))
1262+
return;
12381263
// That's it. We can't rule out any more cases with the data we have.
12391264

12401265
CodeGenFunction::SanitizerScope SanScope(&CGF);
@@ -2784,10 +2809,11 @@ llvm::Value *ScalarExprEmitter::EmitIncDecConsiderOverflowBehavior(
27842809
return Builder.CreateNSWAdd(InVal, Amount, Name);
27852810
[[fallthrough]];
27862811
case LangOptions::SOB_Trapping:
2787-
if (!E->canOverflow())
2812+
BinOpInfo Info = createBinOpInfoFromIncDec(
2813+
E, InVal, IsInc, E->getFPFeaturesInEffect(CGF.getLangOpts()));
2814+
if (!E->canOverflow() || CanElideOverflowCheck(CGF.getContext(), Info))
27882815
return Builder.CreateNSWAdd(InVal, Amount, Name);
2789-
return EmitOverflowCheckedBinOp(createBinOpInfoFromIncDec(
2790-
E, InVal, IsInc, E->getFPFeaturesInEffect(CGF.getLangOpts())));
2816+
return EmitOverflowCheckedBinOp(Info);
27912817
}
27922818
llvm_unreachable("Unknown SignedOverflowBehaviorTy");
27932819
}
@@ -2990,7 +3016,9 @@ ScalarExprEmitter::EmitScalarPrePostIncDec(const UnaryOperator *E, LValue LV,
29903016
value = EmitIncDecConsiderOverflowBehavior(E, value, isInc);
29913017
} else if (E->canOverflow() && type->isUnsignedIntegerType() &&
29923018
CGF.SanOpts.has(SanitizerKind::UnsignedIntegerOverflow) &&
2993-
!excludeOverflowPattern) {
3019+
!excludeOverflowPattern &&
3020+
!CGF.getContext().isTypeIgnoredBySanitizer(
3021+
SanitizerKind::UnsignedIntegerOverflow, E->getType())) {
29943022
value = EmitOverflowCheckedBinOp(createBinOpInfoFromIncDec(
29953023
E, value, isInc, E->getFPFeaturesInEffect(CGF.getLangOpts())));
29963024
} else {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// RUN: rm -rf %t
2+
// RUN: split-file %s %t
3+
4+
// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=signed-integer-overflow,unsigned-integer-overflow -fsanitize-ignorelist=%t/order-0.ignorelist -emit-llvm %t/test.c -o - | FileCheck %s
5+
// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=signed-integer-overflow,unsigned-integer-overflow -fsanitize-ignorelist=%t/order-1.ignorelist -emit-llvm %t/test.c -o - | FileCheck %s
6+
// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=signed-integer-overflow,unsigned-integer-overflow -fsanitize-ignorelist=%t/order-2.ignorelist -emit-llvm %t/test.c -o - | FileCheck %s
7+
// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=signed-integer-overflow,unsigned-integer-overflow -fsanitize-ignorelist=%t/order-3.ignorelist -emit-llvm %t/test.c -o - | FileCheck %s
8+
// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=signed-integer-overflow,unsigned-integer-overflow -fsanitize-ignorelist=%t/order-4.ignorelist -emit-llvm %t/test.c -o - | FileCheck %s
9+
// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=signed-integer-overflow,unsigned-integer-overflow -fsanitize-ignorelist=%t/order-5.ignorelist -emit-llvm %t/test.c -o - | FileCheck %s
10+
// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=signed-integer-overflow,unsigned-integer-overflow -fsanitize-ignorelist=%t/order-6.ignorelist -emit-llvm %t/test.c -o - | FileCheck %s
11+
// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=signed-integer-overflow,unsigned-integer-overflow -fsanitize-ignorelist=%t/order-7.ignorelist -emit-llvm %t/test.c -o - | FileCheck %s
12+
13+
// The same type can appear multiple times within an ignorelist. This is a test
14+
// to make sure "=sanitize" has priority regardless of the order in which
15+
// duplicate type entries appear. This is a precautionary measure; we would
16+
// much rather eagerly sanitize than silently forgo sanitization.
17+
18+
//--- order-0.ignorelist
19+
type:int
20+
type:int=sanitize
21+
22+
//--- order-1.ignorelist
23+
type:int=sanitize
24+
type:int
25+
26+
//--- order-2.ignorelist
27+
type:in*
28+
type:int=sanitize
29+
30+
//--- order-3.ignorelist
31+
type:in*=sanitize
32+
type:int
33+
34+
//--- order-4.ignorelist
35+
type:int
36+
type:in*=sanitize
37+
38+
//--- order-5.ignorelist
39+
type:int=sanitize
40+
type:in*
41+
42+
//--- order-6.ignorelist
43+
type:int=sanitize
44+
type:in*
45+
46+
//--- order-7.ignorelist
47+
type:int
48+
type:int=sanitize
49+
50+
51+
52+
53+
//--- test.c
54+
// CHECK-LABEL: @test
55+
void test(int A) {
56+
// CHECK: @llvm.sadd.with.overflow.i32
57+
++A;
58+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// RUN: rm -rf %t
2+
// RUN: split-file %s %t
3+
4+
// Verify ubsan doesn't emit checks for ignorelisted types
5+
// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=signed-integer-overflow,unsigned-integer-overflow -fsanitize-ignorelist=%t/int.ignorelist -emit-llvm %t/test.cpp -o - | FileCheck %s --check-prefix=INT
6+
// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=signed-integer-overflow,unsigned-integer-overflow -fsanitize-ignorelist=%t/nosection.ignorelist -emit-llvm %t/test.cpp -o - | FileCheck %s --check-prefix=INT
7+
// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=signed-integer-overflow,unsigned-integer-overflow -fsanitize-ignorelist=%t/nosan-same-as-no-category.ignorelist -emit-llvm %t/test.cpp -o - | FileCheck %s --check-prefix=INT
8+
// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=signed-integer-overflow,unsigned-integer-overflow -fsanitize-ignorelist=%t/myty.ignorelist -emit-llvm %t/test.cpp -o - | FileCheck %s --check-prefix=MYTY
9+
// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=implicit-signed-integer-truncation,implicit-unsigned-integer-truncation -fsanitize-ignorelist=%t/trunc.ignorelist -emit-llvm %t/test.cpp -o - | FileCheck %s --check-prefix=TRUNC
10+
// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=implicit-signed-integer-truncation,implicit-unsigned-integer-truncation -fsanitize-ignorelist=%t/docs.ignorelist -emit-llvm %t/test.cpp -o - | FileCheck %s --check-prefix=TRUNC2
11+
12+
//--- int.ignorelist
13+
[{unsigned-integer-overflow,signed-integer-overflow}]
14+
type:int
15+
16+
//--- nosection.ignorelist
17+
type:int
18+
19+
//--- nosan-same-as-no-category.ignorelist
20+
type:int
21+
22+
//--- myty.ignorelist
23+
[{unsigned-integer-overflow,signed-integer-overflow}]
24+
type:*
25+
type:myty=sanitize
26+
27+
//--- trunc.ignorelist
28+
[{implicit-signed-integer-truncation,implicit-unsigned-integer-truncation}]
29+
type:char
30+
type:unsigned char
31+
32+
//--- docs.ignorelist
33+
[implicit-signed-integer-truncation]
34+
type:*
35+
type:T=sanitize
36+
37+
//--- test.cpp
38+
// INT-LABEL: ignore_int
39+
void ignore_int(int A, int B, unsigned C, unsigned D, long E) {
40+
// INT: llvm.uadd.with.overflow.i32
41+
(void)(C+D);
42+
// INT-NOT: llvm.sadd.with.overflow.i32
43+
(void)(A+B);
44+
// INT: llvm.sadd.with.overflow.i64
45+
(void)(++E);
46+
}
47+
48+
49+
typedef unsigned long myty;
50+
typedef myty derivative;
51+
// INT-LABEL: ignore_all_except_myty
52+
// MYTY-LABEL: ignore_all_except_myty
53+
void ignore_all_except_myty(myty A, myty B, int C, unsigned D, derivative E) {
54+
// MYTY-NOT: llvm.sadd.with.overflow.i32
55+
(void)(++C);
56+
57+
// MYTY-NOT: llvm.uadd.with.overflow.i32
58+
(void)(D+D);
59+
60+
// MYTY-NOT: llvm.umul.with.overflow.i64
61+
(void)(E*2);
62+
63+
// MYTY: llvm.uadd.with.overflow.i64
64+
(void)(A+B);
65+
}
66+
67+
// INT-LABEL: truncation
68+
// MYTY-LABEL: truncation
69+
// TRUNC-LABEL: truncation
70+
void truncation(char A, int B, unsigned char C, short D) {
71+
// TRUNC-NOT: %handler.implicit_conversion
72+
A = B;
73+
// TRUNC-NOT: %handler.implicit_conversion
74+
A = C;
75+
// TRUNC-NOT: %handler.implicit_conversion
76+
C = B;
77+
78+
// TRUNC: %handler.implicit_conversion
79+
D = B;
80+
81+
(void)A;
82+
(void)D;
83+
}
84+
85+
86+
// Matches the example from clang/docs/SanitizerSpecialCaseList.rst
87+
typedef char T;
88+
typedef char U;
89+
// TRUNC2-LABEL: docs_example
90+
void docs_example(int toobig) {
91+
// TRUNC2: %handler.implicit_conversion
92+
T a = toobig;
93+
// TRUNC2-NOT: %handler.implicit_conversion
94+
U b = toobig;
95+
// TRUNC2-NOT: %handler.implicit_conversion
96+
char c = toobig;
97+
}
98+

0 commit comments

Comments
 (0)