Skip to content

Commit 295fe0b

Browse files
[Clang] Re-land Overflow Pattern Exclusions (#104889)
Introduce "-fsanitize-undefined-ignore-overflow-pattern=" which can be used to disable sanitizer instrumentation for common overflow-dependent code patterns. For a wide selection of projects, proper overflow sanitization could help catch bugs and solve security vulnerabilities. Unfortunately, in some cases the integer overflow sanitizers are too noisy for their users and are often left disabled. Providing users with a method to disable sanitizer instrumentation of common patterns could mean more projects actually utilize the sanitizers in the first place. One such project that has opted to not use integer overflow (or truncation) sanitizers is the Linux Kernel. There has been some discussion[1] recently concerning mitigation strategies for unexpected arithmetic overflow. This discussion is still ongoing and a succinct article[2] accurately sums up the discussion. In summary, many Kernel developers do not want to introduce more arithmetic wrappers when most developers understand the code patterns as they are. Patterns like: if (base + offset < base) { ... } or while (i--) { ... } or #define SOME -1UL are extremely common in a code base like the Linux Kernel. It is perhaps too much to ask of kernel developers to use arithmetic wrappers in these cases. For example: while (wrapping_post_dec(i)) { ... } which wraps some builtin would not fly. This would incur too many changes to existing code; the code churn would be too much, at least too much to justify turning on overflow sanitizers. Currently, this commit tackles three pervasive idioms: 1. "if (a + b < a)" or some logically-equivalent re-ordering like "if (a > b + a)" 2. "while (i--)" (for unsigned) a post-decrement always overflows here 3. "-1UL, -2UL, etc" negation of unsigned constants will always overflow The patterns that are excluded can be chosen from the following list: - add-overflow-test - post-decr-while - negated-unsigned-const These can be enabled with a comma-separated list: -fsanitize-undefined-ignore-overflow-pattern=add-overflow-test,negated-unsigned-const "all" or "none" may also be used to specify that all patterns should be excluded or that none should be. [1] https://lore.kernel.org/all/202404291502.612E0A10@keescook/ [2] https://lwn.net/Articles/979747/ CCs: @efriedma-quic @kees @jyknight @fmayer @vitalybuka Signed-off-by: Justin Stitt <[email protected]> Co-authored-by: Bill Wendling <[email protected]>
1 parent 2599d69 commit 295fe0b

File tree

16 files changed

+517
-2
lines changed

16 files changed

+517
-2
lines changed

clang/docs/ReleaseNotes.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,36 @@ Moved checkers
438438
Sanitizers
439439
----------
440440

441+
- Added the ``-fsanitize-undefined-ignore-overflow-pattern`` flag which can be
442+
used to disable specific overflow-dependent code patterns. The supported
443+
patterns are: ``add-overflow-test``, ``negated-unsigned-const``, and
444+
``post-decr-while``. The sanitizer instrumentation can be toggled off for all
445+
available patterns by specifying ``all``. Conversely, you can disable all
446+
exclusions with ``none``.
447+
448+
.. code-block:: c++
449+
450+
/// specified with ``-fsanitize-undefined-ignore-overflow-pattern=add-overflow-test``
451+
int common_overflow_check_pattern(unsigned base, unsigned offset) {
452+
if (base + offset < base) { /* ... */ } // The pattern of `a + b < a`, and other re-orderings, won't be instrumented
453+
}
454+
455+
/// specified with ``-fsanitize-undefined-ignore-overflow-pattern=negated-unsigned-const``
456+
void negation_overflow() {
457+
unsigned long foo = -1UL; // No longer causes a negation overflow warning
458+
unsigned long bar = -2UL; // and so on...
459+
}
460+
461+
/// specified with ``-fsanitize-undefined-ignore-overflow-pattern=post-decr-while``
462+
void while_post_decrement() {
463+
unsigned char count = 16;
464+
while (count--) { /* ... */} // No longer causes unsigned-integer-overflow sanitizer to trip
465+
}
466+
467+
Many existing projects have a large amount of these code patterns present.
468+
This new flag should allow those projects to enable integer sanitizers with
469+
less noise.
470+
441471
Python Binding Changes
442472
----------------------
443473
- Fixed an issue that led to crashes when calling ``Type.get_exception_specification_kind``.

clang/docs/UndefinedBehaviorSanitizer.rst

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,48 @@ To silence reports from unsigned integer overflow, you can set
293293
``-fsanitize-recover=unsigned-integer-overflow``, is particularly useful for
294294
providing fuzzing signal without blowing up logs.
295295

296+
Disabling instrumentation for common overflow patterns
297+
------------------------------------------------------
298+
299+
There are certain overflow-dependent or overflow-prone code patterns which
300+
produce a lot of noise for integer overflow/truncation sanitizers. Negated
301+
unsigned constants, post-decrements in a while loop condition and simple
302+
overflow checks are accepted and pervasive code patterns. However, the signal
303+
received from sanitizers instrumenting these code patterns may be too noisy for
304+
some projects. To disable instrumentation for these common patterns one should
305+
use ``-fsanitize-undefined-ignore-overflow-pattern=``.
306+
307+
Currently, this option supports three overflow-dependent code idioms:
308+
309+
``negated-unsigned-const``
310+
311+
.. code-block:: c++
312+
313+
/// -fsanitize-undefined-ignore-overflow-pattern=negated-unsigned-const
314+
unsigned long foo = -1UL; // No longer causes a negation overflow warning
315+
unsigned long bar = -2UL; // and so on...
316+
317+
``post-decr-while``
318+
319+
.. code-block:: c++
320+
321+
/// -fsanitize-undefined-ignore-overflow-pattern=post-decr-while
322+
unsigned char count = 16;
323+
while (count--) { /* ... */ } // No longer causes unsigned-integer-overflow sanitizer to trip
324+
325+
``add-overflow-test``
326+
327+
.. code-block:: c++
328+
329+
/// -fsanitize-undefined-ignore-overflow-pattern=add-overflow-test
330+
if (base + offset < base) { /* ... */ } // The pattern of `a + b < a`, and other re-orderings,
331+
// won't be instrumented (same for signed types)
332+
333+
You can enable all exclusions with
334+
``-fsanitize-undefined-ignore-overflow-pattern=all`` or disable all exclusions
335+
with ``-fsanitize-undefined-ignore-overflow-pattern=none``. Specifying ``none``
336+
has precedence over other values.
337+
296338
Issue Suppression
297339
=================
298340

clang/include/clang/AST/Expr.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3888,6 +3888,7 @@ class BinaryOperator : public Expr {
38883888
/// Construct an empty binary operator.
38893889
explicit BinaryOperator(EmptyShell Empty) : Expr(BinaryOperatorClass, Empty) {
38903890
BinaryOperatorBits.Opc = BO_Comma;
3891+
BinaryOperatorBits.ExcludedOverflowPattern = false;
38913892
}
38923893

38933894
public:
@@ -4043,6 +4044,15 @@ class BinaryOperator : public Expr {
40434044
void setHasStoredFPFeatures(bool B) { BinaryOperatorBits.HasFPFeatures = B; }
40444045
bool hasStoredFPFeatures() const { return BinaryOperatorBits.HasFPFeatures; }
40454046

4047+
/// Set and get the bit that informs arithmetic overflow sanitizers whether
4048+
/// or not they should exclude certain BinaryOperators from instrumentation
4049+
void setExcludedOverflowPattern(bool B) {
4050+
BinaryOperatorBits.ExcludedOverflowPattern = B;
4051+
}
4052+
bool hasExcludedOverflowPattern() const {
4053+
return BinaryOperatorBits.ExcludedOverflowPattern;
4054+
}
4055+
40464056
/// Get FPFeatures from trailing storage
40474057
FPOptionsOverride getStoredFPFeatures() const {
40484058
assert(hasStoredFPFeatures());

clang/include/clang/AST/Stmt.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,11 @@ class alignas(void *) Stmt {
650650
LLVM_PREFERRED_TYPE(bool)
651651
unsigned HasFPFeatures : 1;
652652

653+
/// Whether or not this BinaryOperator should be excluded from integer
654+
/// overflow sanitization.
655+
LLVM_PREFERRED_TYPE(bool)
656+
unsigned ExcludedOverflowPattern : 1;
657+
653658
SourceLocation OpLoc;
654659
};
655660

clang/include/clang/Basic/LangOptions.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,21 @@ class LangOptionsBase {
367367
PerThread,
368368
};
369369

370+
/// Exclude certain code patterns from being instrumented by arithmetic
371+
/// overflow sanitizers
372+
enum OverflowPatternExclusionKind {
373+
/// Don't exclude any overflow patterns from sanitizers
374+
None = 1 << 0,
375+
/// Exclude all overflow patterns (below)
376+
All = 1 << 1,
377+
/// if (a + b < a)
378+
AddOverflowTest = 1 << 2,
379+
/// -1UL
380+
NegUnsignedConst = 1 << 3,
381+
/// while (count--)
382+
PostDecrInWhile = 1 << 4,
383+
};
384+
370385
enum class DefaultVisiblityExportMapping {
371386
None,
372387
/// map only explicit default visibilities to exported
@@ -555,6 +570,11 @@ class LangOptions : public LangOptionsBase {
555570
/// The default stream kind used for HIP kernel launching.
556571
GPUDefaultStreamKind GPUDefaultStream;
557572

573+
/// Which overflow patterns should be excluded from sanitizer instrumentation
574+
unsigned OverflowPatternExclusionMask = 0;
575+
576+
std::vector<std::string> OverflowPatternExclusionValues;
577+
558578
/// The seed used by the randomize structure layout feature.
559579
std::string RandstructSeed;
560580

@@ -630,6 +650,14 @@ class LangOptions : public LangOptionsBase {
630650
return MSCompatibilityVersion >= MajorVersion * 100000U;
631651
}
632652

653+
bool isOverflowPatternExcluded(OverflowPatternExclusionKind Kind) const {
654+
if (OverflowPatternExclusionMask & OverflowPatternExclusionKind::None)
655+
return false;
656+
if (OverflowPatternExclusionMask & OverflowPatternExclusionKind::All)
657+
return true;
658+
return OverflowPatternExclusionMask & Kind;
659+
}
660+
633661
/// Reset all of the options that are not considered when building a
634662
/// module.
635663
void resetNonModularOptions();

clang/include/clang/Driver/Options.td

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2565,6 +2565,11 @@ defm sanitize_stats : BoolOption<"f", "sanitize-stats",
25652565
"Disable">,
25662566
BothFlags<[], [ClangOption], " sanitizer statistics gathering.">>,
25672567
Group<f_clang_Group>;
2568+
def fsanitize_undefined_ignore_overflow_pattern_EQ : CommaJoined<["-"], "fsanitize-undefined-ignore-overflow-pattern=">,
2569+
HelpText<"Specify the overflow patterns to exclude from artihmetic sanitizer instrumentation">,
2570+
Visibility<[ClangOption, CC1Option]>,
2571+
Values<"none,all,add-overflow-test,negated-unsigned-const,post-decr-while">,
2572+
MarshallingInfoStringVector<LangOpts<"OverflowPatternExclusionValues">>;
25682573
def fsanitize_thread_memory_access : Flag<["-"], "fsanitize-thread-memory-access">,
25692574
Group<f_clang_Group>,
25702575
HelpText<"Enable memory access instrumentation in ThreadSanitizer (default)">;

clang/include/clang/Driver/SanitizerArgs.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class SanitizerArgs {
3333
std::vector<std::string> BinaryMetadataIgnorelistFiles;
3434
int CoverageFeatures = 0;
3535
int BinaryMetadataFeatures = 0;
36+
int OverflowPatternExclusions = 0;
3637
int MsanTrackOrigins = 0;
3738
bool MsanUseAfterDtor = true;
3839
bool MsanParamRetval = true;

clang/lib/AST/Expr.cpp

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4759,6 +4759,53 @@ ParenListExpr *ParenListExpr::CreateEmpty(const ASTContext &Ctx,
47594759
return new (Mem) ParenListExpr(EmptyShell(), NumExprs);
47604760
}
47614761

4762+
/// Certain overflow-dependent code patterns can have their integer overflow
4763+
/// sanitization disabled. Check for the common pattern `if (a + b < a)` and
4764+
/// return the resulting BinaryOperator responsible for the addition so we can
4765+
/// elide overflow checks during codegen.
4766+
static std::optional<BinaryOperator *>
4767+
getOverflowPatternBinOp(const BinaryOperator *E) {
4768+
Expr *Addition, *ComparedTo;
4769+
if (E->getOpcode() == BO_LT) {
4770+
Addition = E->getLHS();
4771+
ComparedTo = E->getRHS();
4772+
} else if (E->getOpcode() == BO_GT) {
4773+
Addition = E->getRHS();
4774+
ComparedTo = E->getLHS();
4775+
} else {
4776+
return {};
4777+
}
4778+
4779+
const Expr *AddLHS = nullptr, *AddRHS = nullptr;
4780+
BinaryOperator *BO = dyn_cast<BinaryOperator>(Addition);
4781+
4782+
if (BO && BO->getOpcode() == clang::BO_Add) {
4783+
// now store addends for lookup on other side of '>'
4784+
AddLHS = BO->getLHS();
4785+
AddRHS = BO->getRHS();
4786+
}
4787+
4788+
if (!AddLHS || !AddRHS)
4789+
return {};
4790+
4791+
const Decl *LHSDecl, *RHSDecl, *OtherDecl;
4792+
4793+
LHSDecl = AddLHS->IgnoreParenImpCasts()->getReferencedDeclOfCallee();
4794+
RHSDecl = AddRHS->IgnoreParenImpCasts()->getReferencedDeclOfCallee();
4795+
OtherDecl = ComparedTo->IgnoreParenImpCasts()->getReferencedDeclOfCallee();
4796+
4797+
if (!OtherDecl)
4798+
return {};
4799+
4800+
if (!LHSDecl && !RHSDecl)
4801+
return {};
4802+
4803+
if ((LHSDecl && LHSDecl == OtherDecl && LHSDecl != RHSDecl) ||
4804+
(RHSDecl && RHSDecl == OtherDecl && RHSDecl != LHSDecl))
4805+
return BO;
4806+
return {};
4807+
}
4808+
47624809
BinaryOperator::BinaryOperator(const ASTContext &Ctx, Expr *lhs, Expr *rhs,
47634810
Opcode opc, QualType ResTy, ExprValueKind VK,
47644811
ExprObjectKind OK, SourceLocation opLoc,
@@ -4768,8 +4815,15 @@ BinaryOperator::BinaryOperator(const ASTContext &Ctx, Expr *lhs, Expr *rhs,
47684815
assert(!isCompoundAssignmentOp() &&
47694816
"Use CompoundAssignOperator for compound assignments");
47704817
BinaryOperatorBits.OpLoc = opLoc;
4818+
BinaryOperatorBits.ExcludedOverflowPattern = false;
47714819
SubExprs[LHS] = lhs;
47724820
SubExprs[RHS] = rhs;
4821+
if (Ctx.getLangOpts().isOverflowPatternExcluded(
4822+
LangOptions::OverflowPatternExclusionKind::AddOverflowTest)) {
4823+
std::optional<BinaryOperator *> Result = getOverflowPatternBinOp(this);
4824+
if (Result.has_value())
4825+
Result.value()->BinaryOperatorBits.ExcludedOverflowPattern = true;
4826+
}
47734827
BinaryOperatorBits.HasFPFeatures = FPFeatures.requiresTrailingStorage();
47744828
if (hasStoredFPFeatures())
47754829
setStoredFPFeatures(FPFeatures);
@@ -4782,6 +4836,7 @@ BinaryOperator::BinaryOperator(const ASTContext &Ctx, Expr *lhs, Expr *rhs,
47824836
FPOptionsOverride FPFeatures, bool dead2)
47834837
: Expr(CompoundAssignOperatorClass, ResTy, VK, OK) {
47844838
BinaryOperatorBits.Opc = opc;
4839+
BinaryOperatorBits.ExcludedOverflowPattern = false;
47854840
assert(isCompoundAssignmentOp() &&
47864841
"Use CompoundAssignOperator for compound assignments");
47874842
BinaryOperatorBits.OpLoc = opLoc;

clang/lib/CodeGen/CGExprScalar.cpp

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "clang/AST/Attr.h"
2525
#include "clang/AST/DeclObjC.h"
2626
#include "clang/AST/Expr.h"
27+
#include "clang/AST/ParentMapContext.h"
2728
#include "clang/AST/RecordLayout.h"
2829
#include "clang/AST/StmtVisitor.h"
2930
#include "clang/Basic/CodeGenOptions.h"
@@ -195,13 +196,24 @@ static bool CanElideOverflowCheck(const ASTContext &Ctx, const BinOpInfo &Op) {
195196
if (!Op.mayHaveIntegerOverflow())
196197
return true;
197198

199+
const UnaryOperator *UO = dyn_cast<UnaryOperator>(Op.E);
200+
201+
if (UO && UO->getOpcode() == UO_Minus &&
202+
Ctx.getLangOpts().isOverflowPatternExcluded(
203+
LangOptions::OverflowPatternExclusionKind::NegUnsignedConst) &&
204+
UO->isIntegerConstantExpr(Ctx))
205+
return true;
206+
198207
// If a unary op has a widened operand, the op cannot overflow.
199-
if (const auto *UO = dyn_cast<UnaryOperator>(Op.E))
208+
if (UO)
200209
return !UO->canOverflow();
201210

202211
// We usually don't need overflow checks for binops with widened operands.
203212
// Multiplication with promoted unsigned operands is a special case.
204213
const auto *BO = cast<BinaryOperator>(Op.E);
214+
if (BO->hasExcludedOverflowPattern())
215+
return true;
216+
205217
auto OptionalLHSTy = getUnwidenedIntegerType(Ctx, BO->getLHS());
206218
if (!OptionalLHSTy)
207219
return false;
@@ -2766,6 +2778,26 @@ llvm::Value *ScalarExprEmitter::EmitIncDecConsiderOverflowBehavior(
27662778
llvm_unreachable("Unknown SignedOverflowBehaviorTy");
27672779
}
27682780

2781+
/// For the purposes of overflow pattern exclusion, does this match the
2782+
/// "while(i--)" pattern?
2783+
static bool matchesPostDecrInWhile(const UnaryOperator *UO, bool isInc,
2784+
bool isPre, ASTContext &Ctx) {
2785+
if (isInc || isPre)
2786+
return false;
2787+
2788+
// -fsanitize-undefined-ignore-overflow-pattern=post-decr-while
2789+
if (!Ctx.getLangOpts().isOverflowPatternExcluded(
2790+
LangOptions::OverflowPatternExclusionKind::PostDecrInWhile))
2791+
return false;
2792+
2793+
// all Parents (usually just one) must be a WhileStmt
2794+
for (const auto &Parent : Ctx.getParentMapContext().getParents(*UO))
2795+
if (!Parent.get<WhileStmt>())
2796+
return false;
2797+
2798+
return true;
2799+
}
2800+
27692801
namespace {
27702802
/// Handles check and update for lastprivate conditional variables.
27712803
class OMPLastprivateConditionalUpdateRAII {
@@ -2877,6 +2909,10 @@ ScalarExprEmitter::EmitScalarPrePostIncDec(const UnaryOperator *E, LValue LV,
28772909
} else if (type->isIntegerType()) {
28782910
QualType promotedType;
28792911
bool canPerformLossyDemotionCheck = false;
2912+
2913+
bool excludeOverflowPattern =
2914+
matchesPostDecrInWhile(E, isInc, isPre, CGF.getContext());
2915+
28802916
if (CGF.getContext().isPromotableIntegerType(type)) {
28812917
promotedType = CGF.getContext().getPromotedIntegerType(type);
28822918
assert(promotedType != type && "Shouldn't promote to the same type.");
@@ -2936,7 +2972,8 @@ ScalarExprEmitter::EmitScalarPrePostIncDec(const UnaryOperator *E, LValue LV,
29362972
} else if (E->canOverflow() && type->isSignedIntegerOrEnumerationType()) {
29372973
value = EmitIncDecConsiderOverflowBehavior(E, value, isInc);
29382974
} else if (E->canOverflow() && type->isUnsignedIntegerType() &&
2939-
CGF.SanOpts.has(SanitizerKind::UnsignedIntegerOverflow)) {
2975+
CGF.SanOpts.has(SanitizerKind::UnsignedIntegerOverflow) &&
2976+
!excludeOverflowPattern) {
29402977
value = EmitOverflowCheckedBinOp(createBinOpInfoFromIncDec(
29412978
E, value, isInc, E->getFPFeaturesInEffect(CGF.getLangOpts())));
29422979
} else {

0 commit comments

Comments
 (0)