Skip to content

Commit

Permalink
[Clang] Re-land Overflow Pattern Exclusions (llvm#104889)
Browse files Browse the repository at this point in the history
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 <justinstitt@google.com>
Co-authored-by: Bill Wendling <morbo@google.com>
  • Loading branch information
JustinStitt and bwendling authored Aug 20, 2024
1 parent 2599d69 commit 295fe0b
Show file tree
Hide file tree
Showing 16 changed files with 517 additions and 2 deletions.
30 changes: 30 additions & 0 deletions clang/docs/ReleaseNotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,36 @@ Moved checkers
Sanitizers
----------

- Added the ``-fsanitize-undefined-ignore-overflow-pattern`` flag which can be
used to disable specific overflow-dependent code patterns. The supported
patterns are: ``add-overflow-test``, ``negated-unsigned-const``, and
``post-decr-while``. The sanitizer instrumentation can be toggled off for all
available patterns by specifying ``all``. Conversely, you can disable all
exclusions with ``none``.

.. code-block:: c++

/// specified with ``-fsanitize-undefined-ignore-overflow-pattern=add-overflow-test``
int common_overflow_check_pattern(unsigned base, unsigned offset) {
if (base + offset < base) { /* ... */ } // The pattern of `a + b < a`, and other re-orderings, won't be instrumented
}
/// specified with ``-fsanitize-undefined-ignore-overflow-pattern=negated-unsigned-const``
void negation_overflow() {
unsigned long foo = -1UL; // No longer causes a negation overflow warning
unsigned long bar = -2UL; // and so on...
}

/// specified with ``-fsanitize-undefined-ignore-overflow-pattern=post-decr-while``
void while_post_decrement() {
unsigned char count = 16;
while (count--) { /* ... */} // No longer causes unsigned-integer-overflow sanitizer to trip
}
Many existing projects have a large amount of these code patterns present.
This new flag should allow those projects to enable integer sanitizers with
less noise.

Python Binding Changes
----------------------
- Fixed an issue that led to crashes when calling ``Type.get_exception_specification_kind``.
Expand Down
42 changes: 42 additions & 0 deletions clang/docs/UndefinedBehaviorSanitizer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,48 @@ To silence reports from unsigned integer overflow, you can set
``-fsanitize-recover=unsigned-integer-overflow``, is particularly useful for
providing fuzzing signal without blowing up logs.

Disabling instrumentation for common overflow patterns
------------------------------------------------------

There are certain overflow-dependent or overflow-prone code patterns which
produce a lot of noise for integer overflow/truncation sanitizers. Negated
unsigned constants, post-decrements in a while loop condition and simple
overflow checks are accepted and pervasive code patterns. However, the signal
received from sanitizers instrumenting these code patterns may be too noisy for
some projects. To disable instrumentation for these common patterns one should
use ``-fsanitize-undefined-ignore-overflow-pattern=``.

Currently, this option supports three overflow-dependent code idioms:

``negated-unsigned-const``

.. code-block:: c++

/// -fsanitize-undefined-ignore-overflow-pattern=negated-unsigned-const
unsigned long foo = -1UL; // No longer causes a negation overflow warning
unsigned long bar = -2UL; // and so on...

``post-decr-while``

.. code-block:: c++

/// -fsanitize-undefined-ignore-overflow-pattern=post-decr-while
unsigned char count = 16;
while (count--) { /* ... */ } // No longer causes unsigned-integer-overflow sanitizer to trip
``add-overflow-test``

.. code-block:: c++

/// -fsanitize-undefined-ignore-overflow-pattern=add-overflow-test
if (base + offset < base) { /* ... */ } // The pattern of `a + b < a`, and other re-orderings,
// won't be instrumented (same for signed types)
You can enable all exclusions with
``-fsanitize-undefined-ignore-overflow-pattern=all`` or disable all exclusions
with ``-fsanitize-undefined-ignore-overflow-pattern=none``. Specifying ``none``
has precedence over other values.

Issue Suppression
=================

Expand Down
10 changes: 10 additions & 0 deletions clang/include/clang/AST/Expr.h
Original file line number Diff line number Diff line change
Expand Up @@ -3888,6 +3888,7 @@ class BinaryOperator : public Expr {
/// Construct an empty binary operator.
explicit BinaryOperator(EmptyShell Empty) : Expr(BinaryOperatorClass, Empty) {
BinaryOperatorBits.Opc = BO_Comma;
BinaryOperatorBits.ExcludedOverflowPattern = false;
}

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

/// Set and get the bit that informs arithmetic overflow sanitizers whether
/// or not they should exclude certain BinaryOperators from instrumentation
void setExcludedOverflowPattern(bool B) {
BinaryOperatorBits.ExcludedOverflowPattern = B;
}
bool hasExcludedOverflowPattern() const {
return BinaryOperatorBits.ExcludedOverflowPattern;
}

/// Get FPFeatures from trailing storage
FPOptionsOverride getStoredFPFeatures() const {
assert(hasStoredFPFeatures());
Expand Down
5 changes: 5 additions & 0 deletions clang/include/clang/AST/Stmt.h
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,11 @@ class alignas(void *) Stmt {
LLVM_PREFERRED_TYPE(bool)
unsigned HasFPFeatures : 1;

/// Whether or not this BinaryOperator should be excluded from integer
/// overflow sanitization.
LLVM_PREFERRED_TYPE(bool)
unsigned ExcludedOverflowPattern : 1;

SourceLocation OpLoc;
};

Expand Down
28 changes: 28 additions & 0 deletions clang/include/clang/Basic/LangOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,21 @@ class LangOptionsBase {
PerThread,
};

/// Exclude certain code patterns from being instrumented by arithmetic
/// overflow sanitizers
enum OverflowPatternExclusionKind {
/// Don't exclude any overflow patterns from sanitizers
None = 1 << 0,
/// Exclude all overflow patterns (below)
All = 1 << 1,
/// if (a + b < a)
AddOverflowTest = 1 << 2,
/// -1UL
NegUnsignedConst = 1 << 3,
/// while (count--)
PostDecrInWhile = 1 << 4,
};

enum class DefaultVisiblityExportMapping {
None,
/// map only explicit default visibilities to exported
Expand Down Expand Up @@ -555,6 +570,11 @@ class LangOptions : public LangOptionsBase {
/// The default stream kind used for HIP kernel launching.
GPUDefaultStreamKind GPUDefaultStream;

/// Which overflow patterns should be excluded from sanitizer instrumentation
unsigned OverflowPatternExclusionMask = 0;

std::vector<std::string> OverflowPatternExclusionValues;

/// The seed used by the randomize structure layout feature.
std::string RandstructSeed;

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

bool isOverflowPatternExcluded(OverflowPatternExclusionKind Kind) const {
if (OverflowPatternExclusionMask & OverflowPatternExclusionKind::None)
return false;
if (OverflowPatternExclusionMask & OverflowPatternExclusionKind::All)
return true;
return OverflowPatternExclusionMask & Kind;
}

/// Reset all of the options that are not considered when building a
/// module.
void resetNonModularOptions();
Expand Down
5 changes: 5 additions & 0 deletions clang/include/clang/Driver/Options.td
Original file line number Diff line number Diff line change
Expand Up @@ -2565,6 +2565,11 @@ defm sanitize_stats : BoolOption<"f", "sanitize-stats",
"Disable">,
BothFlags<[], [ClangOption], " sanitizer statistics gathering.">>,
Group<f_clang_Group>;
def fsanitize_undefined_ignore_overflow_pattern_EQ : CommaJoined<["-"], "fsanitize-undefined-ignore-overflow-pattern=">,
HelpText<"Specify the overflow patterns to exclude from artihmetic sanitizer instrumentation">,
Visibility<[ClangOption, CC1Option]>,
Values<"none,all,add-overflow-test,negated-unsigned-const,post-decr-while">,
MarshallingInfoStringVector<LangOpts<"OverflowPatternExclusionValues">>;
def fsanitize_thread_memory_access : Flag<["-"], "fsanitize-thread-memory-access">,
Group<f_clang_Group>,
HelpText<"Enable memory access instrumentation in ThreadSanitizer (default)">;
Expand Down
1 change: 1 addition & 0 deletions clang/include/clang/Driver/SanitizerArgs.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class SanitizerArgs {
std::vector<std::string> BinaryMetadataIgnorelistFiles;
int CoverageFeatures = 0;
int BinaryMetadataFeatures = 0;
int OverflowPatternExclusions = 0;
int MsanTrackOrigins = 0;
bool MsanUseAfterDtor = true;
bool MsanParamRetval = true;
Expand Down
55 changes: 55 additions & 0 deletions clang/lib/AST/Expr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4759,6 +4759,53 @@ ParenListExpr *ParenListExpr::CreateEmpty(const ASTContext &Ctx,
return new (Mem) ParenListExpr(EmptyShell(), NumExprs);
}

/// Certain overflow-dependent code patterns can have their integer overflow
/// sanitization disabled. Check for the common pattern `if (a + b < a)` and
/// return the resulting BinaryOperator responsible for the addition so we can
/// elide overflow checks during codegen.
static std::optional<BinaryOperator *>
getOverflowPatternBinOp(const BinaryOperator *E) {
Expr *Addition, *ComparedTo;
if (E->getOpcode() == BO_LT) {
Addition = E->getLHS();
ComparedTo = E->getRHS();
} else if (E->getOpcode() == BO_GT) {
Addition = E->getRHS();
ComparedTo = E->getLHS();
} else {
return {};
}

const Expr *AddLHS = nullptr, *AddRHS = nullptr;
BinaryOperator *BO = dyn_cast<BinaryOperator>(Addition);

if (BO && BO->getOpcode() == clang::BO_Add) {
// now store addends for lookup on other side of '>'
AddLHS = BO->getLHS();
AddRHS = BO->getRHS();
}

if (!AddLHS || !AddRHS)
return {};

const Decl *LHSDecl, *RHSDecl, *OtherDecl;

LHSDecl = AddLHS->IgnoreParenImpCasts()->getReferencedDeclOfCallee();
RHSDecl = AddRHS->IgnoreParenImpCasts()->getReferencedDeclOfCallee();
OtherDecl = ComparedTo->IgnoreParenImpCasts()->getReferencedDeclOfCallee();

if (!OtherDecl)
return {};

if (!LHSDecl && !RHSDecl)
return {};

if ((LHSDecl && LHSDecl == OtherDecl && LHSDecl != RHSDecl) ||
(RHSDecl && RHSDecl == OtherDecl && RHSDecl != LHSDecl))
return BO;
return {};
}

BinaryOperator::BinaryOperator(const ASTContext &Ctx, Expr *lhs, Expr *rhs,
Opcode opc, QualType ResTy, ExprValueKind VK,
ExprObjectKind OK, SourceLocation opLoc,
Expand All @@ -4768,8 +4815,15 @@ BinaryOperator::BinaryOperator(const ASTContext &Ctx, Expr *lhs, Expr *rhs,
assert(!isCompoundAssignmentOp() &&
"Use CompoundAssignOperator for compound assignments");
BinaryOperatorBits.OpLoc = opLoc;
BinaryOperatorBits.ExcludedOverflowPattern = false;
SubExprs[LHS] = lhs;
SubExprs[RHS] = rhs;
if (Ctx.getLangOpts().isOverflowPatternExcluded(
LangOptions::OverflowPatternExclusionKind::AddOverflowTest)) {
std::optional<BinaryOperator *> Result = getOverflowPatternBinOp(this);
if (Result.has_value())
Result.value()->BinaryOperatorBits.ExcludedOverflowPattern = true;
}
BinaryOperatorBits.HasFPFeatures = FPFeatures.requiresTrailingStorage();
if (hasStoredFPFeatures())
setStoredFPFeatures(FPFeatures);
Expand All @@ -4782,6 +4836,7 @@ BinaryOperator::BinaryOperator(const ASTContext &Ctx, Expr *lhs, Expr *rhs,
FPOptionsOverride FPFeatures, bool dead2)
: Expr(CompoundAssignOperatorClass, ResTy, VK, OK) {
BinaryOperatorBits.Opc = opc;
BinaryOperatorBits.ExcludedOverflowPattern = false;
assert(isCompoundAssignmentOp() &&
"Use CompoundAssignOperator for compound assignments");
BinaryOperatorBits.OpLoc = opLoc;
Expand Down
41 changes: 39 additions & 2 deletions clang/lib/CodeGen/CGExprScalar.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "clang/AST/Attr.h"
#include "clang/AST/DeclObjC.h"
#include "clang/AST/Expr.h"
#include "clang/AST/ParentMapContext.h"
#include "clang/AST/RecordLayout.h"
#include "clang/AST/StmtVisitor.h"
#include "clang/Basic/CodeGenOptions.h"
Expand Down Expand Up @@ -195,13 +196,24 @@ static bool CanElideOverflowCheck(const ASTContext &Ctx, const BinOpInfo &Op) {
if (!Op.mayHaveIntegerOverflow())
return true;

const UnaryOperator *UO = dyn_cast<UnaryOperator>(Op.E);

if (UO && UO->getOpcode() == UO_Minus &&
Ctx.getLangOpts().isOverflowPatternExcluded(
LangOptions::OverflowPatternExclusionKind::NegUnsignedConst) &&
UO->isIntegerConstantExpr(Ctx))
return true;

// If a unary op has a widened operand, the op cannot overflow.
if (const auto *UO = dyn_cast<UnaryOperator>(Op.E))
if (UO)
return !UO->canOverflow();

// We usually don't need overflow checks for binops with widened operands.
// Multiplication with promoted unsigned operands is a special case.
const auto *BO = cast<BinaryOperator>(Op.E);
if (BO->hasExcludedOverflowPattern())
return true;

auto OptionalLHSTy = getUnwidenedIntegerType(Ctx, BO->getLHS());
if (!OptionalLHSTy)
return false;
Expand Down Expand Up @@ -2766,6 +2778,26 @@ llvm::Value *ScalarExprEmitter::EmitIncDecConsiderOverflowBehavior(
llvm_unreachable("Unknown SignedOverflowBehaviorTy");
}

/// For the purposes of overflow pattern exclusion, does this match the
/// "while(i--)" pattern?
static bool matchesPostDecrInWhile(const UnaryOperator *UO, bool isInc,
bool isPre, ASTContext &Ctx) {
if (isInc || isPre)
return false;

// -fsanitize-undefined-ignore-overflow-pattern=post-decr-while
if (!Ctx.getLangOpts().isOverflowPatternExcluded(
LangOptions::OverflowPatternExclusionKind::PostDecrInWhile))
return false;

// all Parents (usually just one) must be a WhileStmt
for (const auto &Parent : Ctx.getParentMapContext().getParents(*UO))
if (!Parent.get<WhileStmt>())
return false;

return true;
}

namespace {
/// Handles check and update for lastprivate conditional variables.
class OMPLastprivateConditionalUpdateRAII {
Expand Down Expand Up @@ -2877,6 +2909,10 @@ ScalarExprEmitter::EmitScalarPrePostIncDec(const UnaryOperator *E, LValue LV,
} else if (type->isIntegerType()) {
QualType promotedType;
bool canPerformLossyDemotionCheck = false;

bool excludeOverflowPattern =
matchesPostDecrInWhile(E, isInc, isPre, CGF.getContext());

if (CGF.getContext().isPromotableIntegerType(type)) {
promotedType = CGF.getContext().getPromotedIntegerType(type);
assert(promotedType != type && "Shouldn't promote to the same type.");
Expand Down Expand Up @@ -2936,7 +2972,8 @@ ScalarExprEmitter::EmitScalarPrePostIncDec(const UnaryOperator *E, LValue LV,
} else if (E->canOverflow() && type->isSignedIntegerOrEnumerationType()) {
value = EmitIncDecConsiderOverflowBehavior(E, value, isInc);
} else if (E->canOverflow() && type->isUnsignedIntegerType() &&
CGF.SanOpts.has(SanitizerKind::UnsignedIntegerOverflow)) {
CGF.SanOpts.has(SanitizerKind::UnsignedIntegerOverflow) &&
!excludeOverflowPattern) {
value = EmitOverflowCheckedBinOp(createBinOpInfoFromIncDec(
E, value, isInc, E->getFPFeaturesInEffect(CGF.getLangOpts())));
} else {
Expand Down
Loading

0 comments on commit 295fe0b

Please sign in to comment.