Skip to content

Commit 29992cf

Browse files
[Clang][CodeGen] Emit “trap reasons” on UBSan traps (#145967)
This patch adds a human readable trap category and message to UBSan traps. The category and message are encoded in a fake frame in the debug info where the function is a fake inline function where the name encodes the trap category and message. This is the same mechanism used by Clang’s `__builtin_verbose_trap()`. This change allows consumers of binaries built with trapping UBSan to more easily identify the reason for trapping. In particular LLDB already has a frame recognizer that recognizes the fake function names emitted in debug info by this patch. A patch testing this behavior in LLDB will be added in a separately. The human readable trap messages are based on the messages currently emitted by the userspace runtime for UBSan in compiler-rt. Note the wording is not identical because the userspace UBSan runtime has access to dynamic information that is not available during Clang’s codegen. Test cases for each UBSan trap kind are included. This complements the [`-fsanitize-annotate-debug-info` feature](#141997). While `-fsanitize-annotate-debug-info` attempts to annotate all UBSan-added instructions, this feature (`-fsanitize-debug-trap-reasons`) only annotates the final trap instruction using SanitizerHandler information. This work is part of a GSoc 2025 project.
1 parent fa79c23 commit 29992cf

38 files changed

+588
-102
lines changed

clang/docs/ReleaseNotes.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,18 @@ Non-comprehensive list of changes in this release
8989
-------------------------------------------------
9090
- Added ``__builtin_elementwise_minnumnum`` and ``__builtin_elementwise_maxnumnum``.
9191

92+
- Trapping UBSan (e.g. ``-fsanitize-trap=undefined``) now emits a string describing the reason for
93+
trapping into the generated debug info. This feature allows debuggers (e.g. LLDB) to display
94+
the reason for trapping if the trap is reached. The string is currently encoded in the debug
95+
info as an artificial frame that claims to be inlined at the trap location. The function used
96+
for the artificial frame is an artificial function whose name encodes the reason for trapping.
97+
The encoding used is currently the same as ``__builtin_verbose_trap`` but might change in the future.
98+
This feature is enabled by default but can be disabled by compiling with
99+
``-fno-sanitize-annotate-debug-info-traps``.
92100

93101
New Compiler Flags
94102
------------------
103+
- New option ``-fno-sanitize-annotate-debug-info-traps`` added to disable emitting trap reasons into the debug info when compiling with trapping UBSan (e.g. ``-fsanitize-trap=undefined``).
95104

96105
Deprecated Compiler Flags
97106
-------------------------

clang/include/clang/Basic/CodeGenOptions.def

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ CODEGENOPT(SanitizeBinaryMetadataAtomics, 1, 0, Benign) ///< Emit PCs for atomic
307307
CODEGENOPT(SanitizeBinaryMetadataUAR, 1, 0, Benign) ///< Emit PCs for start of functions
308308
///< that are subject for use-after-return checking.
309309
CODEGENOPT(SanitizeStats , 1, 0, Benign) ///< Collect statistics for sanitizers.
310+
CODEGENOPT(SanitizeDebugTrapReasons, 1, 1 , Benign) ///< Enable UBSan trapping messages
310311
CODEGENOPT(SimplifyLibCalls , 1, 1, Benign) ///< Set when -fbuiltin is enabled.
311312
CODEGENOPT(SoftFloat , 1, 0, Benign) ///< -soft-float.
312313
CODEGENOPT(SpeculativeLoadHardening, 1, 0, Benign) ///< Enable speculative load hardening.

clang/include/clang/Driver/Options.td

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2597,6 +2597,16 @@ def fsanitize_undefined_trap_on_error
25972597
def fno_sanitize_undefined_trap_on_error
25982598
: Flag<["-"], "fno-sanitize-undefined-trap-on-error">, Group<f_clang_Group>,
25992599
Alias<fno_sanitize_trap_EQ>, AliasArgs<["undefined"]>;
2600+
defm sanitize_debug_trap_reasons
2601+
: BoolFOption<
2602+
"sanitize-debug-trap-reasons",
2603+
CodeGenOpts<"SanitizeDebugTrapReasons">, DefaultTrue,
2604+
PosFlag<SetTrue, [], [ClangOption, CC1Option],
2605+
"Annotate trap blocks in debug info with UBSan trap reasons">,
2606+
NegFlag<SetFalse, [], [ClangOption, CC1Option],
2607+
"Do not annotate trap blocks in debug info with UBSan trap "
2608+
"reasons">>;
2609+
26002610
defm sanitize_minimal_runtime : BoolOption<"f", "sanitize-minimal-runtime",
26012611
CodeGenOpts<"SanitizeMinimalRuntime">, DefaultFalse,
26022612
PosFlag<SetTrue>,

clang/lib/CodeGen/CGDebugInfo.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6435,7 +6435,7 @@ CodeGenFunction::LexicalScope::~LexicalScope() {
64356435
static std::string SanitizerHandlerToCheckLabel(SanitizerHandler Handler) {
64366436
std::string Label;
64376437
switch (Handler) {
6438-
#define SANITIZER_CHECK(Enum, Name, Version) \
6438+
#define SANITIZER_CHECK(Enum, Name, Version, Msg) \
64396439
case Enum: \
64406440
Label = "__ubsan_check_" #Name; \
64416441
break;

clang/lib/CodeGen/CGExpr.cpp

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,16 @@ enum VariableTypeDescriptorKind : uint16_t {
8585
// Miscellaneous Helper Methods
8686
//===--------------------------------------------------------------------===//
8787

88+
static llvm::StringRef GetUBSanTrapForHandler(SanitizerHandler ID) {
89+
switch (ID) {
90+
#define SANITIZER_CHECK(Enum, Name, Version, Msg) \
91+
case SanitizerHandler::Enum: \
92+
return Msg;
93+
LIST_SANITIZER_CHECKS
94+
#undef SANITIZER_CHECK
95+
}
96+
}
97+
8898
/// CreateTempAlloca - This creates a alloca and inserts it into the entry
8999
/// block.
90100
RawAddress
@@ -3649,7 +3659,7 @@ struct SanitizerHandlerInfo {
36493659
}
36503660

36513661
const SanitizerHandlerInfo SanitizerHandlers[] = {
3652-
#define SANITIZER_CHECK(Enum, Name, Version) {#Name, Version},
3662+
#define SANITIZER_CHECK(Enum, Name, Version, Msg) {#Name, Version},
36533663
LIST_SANITIZER_CHECKS
36543664
#undef SANITIZER_CHECK
36553665
};
@@ -3954,6 +3964,8 @@ void CodeGenFunction::EmitCfiCheckFail() {
39543964
StartFunction(GlobalDecl(), CGM.getContext().VoidTy, F, FI, Args,
39553965
SourceLocation());
39563966

3967+
ApplyDebugLocation ADL = ApplyDebugLocation::CreateArtificial(*this);
3968+
39573969
// This function is not affected by NoSanitizeList. This function does
39583970
// not have a source location, but "src:*" would still apply. Revert any
39593971
// changes to SanOpts made in StartFunction.
@@ -4051,6 +4063,15 @@ void CodeGenFunction::EmitTrapCheck(llvm::Value *Checked,
40514063

40524064
llvm::BasicBlock *&TrapBB = TrapBBs[CheckHandlerID];
40534065

4066+
llvm::DILocation *TrapLocation = Builder.getCurrentDebugLocation();
4067+
llvm::StringRef TrapMessage = GetUBSanTrapForHandler(CheckHandlerID);
4068+
4069+
if (getDebugInfo() && !TrapMessage.empty() &&
4070+
CGM.getCodeGenOpts().SanitizeDebugTrapReasons && TrapLocation) {
4071+
TrapLocation = getDebugInfo()->CreateTrapFailureMessageFor(
4072+
TrapLocation, "Undefined Behavior Sanitizer", TrapMessage);
4073+
}
4074+
40544075
NoMerge = NoMerge || !CGM.getCodeGenOpts().OptimizationLevel ||
40554076
(CurCodeDecl && CurCodeDecl->hasAttr<OptimizeNoneAttr>());
40564077

@@ -4059,8 +4080,8 @@ void CodeGenFunction::EmitTrapCheck(llvm::Value *Checked,
40594080
auto Call = TrapBB->begin();
40604081
assert(isa<llvm::CallInst>(Call) && "Expected call in trap BB");
40614082

4062-
Call->applyMergedLocation(Call->getDebugLoc(),
4063-
Builder.getCurrentDebugLocation());
4083+
Call->applyMergedLocation(Call->getDebugLoc(), TrapLocation);
4084+
40644085
Builder.CreateCondBr(Checked, Cont, TrapBB,
40654086
MDHelper.createLikelyBranchWeights());
40664087
} else {
@@ -4069,6 +4090,8 @@ void CodeGenFunction::EmitTrapCheck(llvm::Value *Checked,
40694090
MDHelper.createLikelyBranchWeights());
40704091
EmitBlock(TrapBB);
40714092

4093+
ApplyDebugLocation applyTrapDI(*this, TrapLocation);
4094+
40724095
llvm::CallInst *TrapCall =
40734096
Builder.CreateCall(CGM.getIntrinsic(llvm::Intrinsic::ubsantrap),
40744097
llvm::ConstantInt::get(CGM.Int8Ty, CheckHandlerID));

clang/lib/CodeGen/SanitizerHandler.h

Lines changed: 61 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,35 +14,69 @@
1414
#define LLVM_CLANG_LIB_CODEGEN_SANITIZER_HANDLER_H
1515

1616
#define LIST_SANITIZER_CHECKS \
17-
SANITIZER_CHECK(AddOverflow, add_overflow, 0) \
18-
SANITIZER_CHECK(BuiltinUnreachable, builtin_unreachable, 0) \
19-
SANITIZER_CHECK(CFICheckFail, cfi_check_fail, 0) \
20-
SANITIZER_CHECK(DivremOverflow, divrem_overflow, 0) \
21-
SANITIZER_CHECK(DynamicTypeCacheMiss, dynamic_type_cache_miss, 0) \
22-
SANITIZER_CHECK(FloatCastOverflow, float_cast_overflow, 0) \
23-
SANITIZER_CHECK(FunctionTypeMismatch, function_type_mismatch, 0) \
24-
SANITIZER_CHECK(ImplicitConversion, implicit_conversion, 0) \
25-
SANITIZER_CHECK(InvalidBuiltin, invalid_builtin, 0) \
26-
SANITIZER_CHECK(InvalidObjCCast, invalid_objc_cast, 0) \
27-
SANITIZER_CHECK(LoadInvalidValue, load_invalid_value, 0) \
28-
SANITIZER_CHECK(MissingReturn, missing_return, 0) \
29-
SANITIZER_CHECK(MulOverflow, mul_overflow, 0) \
30-
SANITIZER_CHECK(NegateOverflow, negate_overflow, 0) \
31-
SANITIZER_CHECK(NullabilityArg, nullability_arg, 0) \
32-
SANITIZER_CHECK(NullabilityReturn, nullability_return, 1) \
33-
SANITIZER_CHECK(NonnullArg, nonnull_arg, 0) \
34-
SANITIZER_CHECK(NonnullReturn, nonnull_return, 1) \
35-
SANITIZER_CHECK(OutOfBounds, out_of_bounds, 0) \
36-
SANITIZER_CHECK(PointerOverflow, pointer_overflow, 0) \
37-
SANITIZER_CHECK(ShiftOutOfBounds, shift_out_of_bounds, 0) \
38-
SANITIZER_CHECK(SubOverflow, sub_overflow, 0) \
39-
SANITIZER_CHECK(TypeMismatch, type_mismatch, 1) \
40-
SANITIZER_CHECK(AlignmentAssumption, alignment_assumption, 0) \
41-
SANITIZER_CHECK(VLABoundNotPositive, vla_bound_not_positive, 0) \
42-
SANITIZER_CHECK(BoundsSafety, bounds_safety, 0)
17+
SANITIZER_CHECK(AddOverflow, add_overflow, 0, "Integer addition overflowed") \
18+
SANITIZER_CHECK(BuiltinUnreachable, builtin_unreachable, 0, \
19+
"_builtin_unreachable(), execution reached an unreachable " \
20+
"program point") \
21+
SANITIZER_CHECK(CFICheckFail, cfi_check_fail, 0, \
22+
"Control flow integrity check failed") \
23+
SANITIZER_CHECK(DivremOverflow, divrem_overflow, 0, \
24+
"Integer divide or remainder overflowed") \
25+
SANITIZER_CHECK(DynamicTypeCacheMiss, dynamic_type_cache_miss, 0, \
26+
"Dynamic type cache miss, member call made on an object " \
27+
"whose dynamic type differs from the expected type") \
28+
SANITIZER_CHECK(FloatCastOverflow, float_cast_overflow, 0, \
29+
"Floating-point to integer conversion overflowed") \
30+
SANITIZER_CHECK(FunctionTypeMismatch, function_type_mismatch, 0, \
31+
"Function called with mismatched signature") \
32+
SANITIZER_CHECK(ImplicitConversion, implicit_conversion, 0, \
33+
"Implicit integer conversion overflowed or lost data") \
34+
SANITIZER_CHECK(InvalidBuiltin, invalid_builtin, 0, \
35+
"Invalid use of builtin function") \
36+
SANITIZER_CHECK(InvalidObjCCast, invalid_objc_cast, 0, \
37+
"Invalid Objective-C cast") \
38+
SANITIZER_CHECK(LoadInvalidValue, load_invalid_value, 0, \
39+
"Loaded an invalid or uninitialized value for the type") \
40+
SANITIZER_CHECK(MissingReturn, missing_return, 0, \
41+
"Execution reached the end of a value-returning function " \
42+
"without returning a value") \
43+
SANITIZER_CHECK(MulOverflow, mul_overflow, 0, \
44+
"Integer multiplication overflowed") \
45+
SANITIZER_CHECK(NegateOverflow, negate_overflow, 0, \
46+
"Integer negation overflowed") \
47+
SANITIZER_CHECK( \
48+
NullabilityArg, nullability_arg, 0, \
49+
"Passing null as an argument which is annotated with _Nonnull") \
50+
SANITIZER_CHECK(NullabilityReturn, nullability_return, 1, \
51+
"Returning null from a function with a return type " \
52+
"annotated with _Nonnull") \
53+
SANITIZER_CHECK(NonnullArg, nonnull_arg, 0, \
54+
"Passing null pointer as an argument which is declared to " \
55+
"never be null") \
56+
SANITIZER_CHECK(NonnullReturn, nonnull_return, 1, \
57+
"Returning null pointer from a function which is declared " \
58+
"to never return null") \
59+
SANITIZER_CHECK(OutOfBounds, out_of_bounds, 0, "Array index out of bounds") \
60+
SANITIZER_CHECK(PointerOverflow, pointer_overflow, 0, \
61+
"Pointer arithmetic overflowed bounds") \
62+
SANITIZER_CHECK(ShiftOutOfBounds, shift_out_of_bounds, 0, \
63+
"Shift exponent is too large for the type") \
64+
SANITIZER_CHECK(SubOverflow, sub_overflow, 0, \
65+
"Integer subtraction overflowed") \
66+
SANITIZER_CHECK(TypeMismatch, type_mismatch, 1, \
67+
"Type mismatch in operation") \
68+
SANITIZER_CHECK(AlignmentAssumption, alignment_assumption, 0, \
69+
"Alignment assumption violated") \
70+
SANITIZER_CHECK( \
71+
VLABoundNotPositive, vla_bound_not_positive, 0, \
72+
"Variable length array bound evaluates to non-positive value") \
73+
SANITIZER_CHECK(BoundsSafety, bounds_safety, 0, \
74+
"") // BoundsSafety Msg is empty because it is not considered
75+
// part of UBSan; therefore, no trap reason is emitted for
76+
// this case.
4377

4478
enum SanitizerHandler {
45-
#define SANITIZER_CHECK(Enum, Name, Version) Enum,
79+
#define SANITIZER_CHECK(Enum, Name, Version, Msg) Enum,
4680
LIST_SANITIZER_CHECKS
4781
#undef SANITIZER_CHECK
4882
};

clang/lib/Driver/SanitizerArgs.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,6 +1382,12 @@ void SanitizerArgs::addArgs(const ToolChain &TC, const llvm::opt::ArgList &Args,
13821382
CmdArgs.push_back(Args.MakeArgString("-fsanitize-annotate-debug-info=" +
13831383
toString(AnnotateDebugInfo)));
13841384

1385+
if (const Arg *A =
1386+
Args.getLastArg(options::OPT_fsanitize_debug_trap_reasons,
1387+
options::OPT_fno_sanitize_debug_trap_reasons)) {
1388+
CmdArgs.push_back(Args.MakeArgString(A->getAsString(Args)));
1389+
}
1390+
13851391
addSpecialCaseListOpt(Args, CmdArgs,
13861392
"-fsanitize-ignorelist=", UserIgnorelistFiles);
13871393
addSpecialCaseListOpt(Args, CmdArgs,

clang/test/CodeGen/bounds-checking-debuginfo.c

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ void d(double*);
2525
// CHECK-TRAP-NEXT: [[TMP1:%.*]] = icmp ult i64 [[TMP0]], 10, !dbg [[DBG23]], !nosanitize [[META10]]
2626
// CHECK-TRAP-NEXT: br i1 [[TMP1]], label %[[CONT:.*]], label %[[TRAP:.*]], !dbg [[DBG23]], !prof [[PROF27:![0-9]+]], !nosanitize [[META10]]
2727
// CHECK-TRAP: [[TRAP]]:
28-
// CHECK-TRAP-NEXT: call void @llvm.ubsantrap(i8 18) #[[ATTR3:[0-9]+]], !dbg [[DBG23]], !nosanitize [[META10]]
29-
// CHECK-TRAP-NEXT: unreachable, !dbg [[DBG23]], !nosanitize [[META10]]
28+
// CHECK-TRAP-NEXT: call void @llvm.ubsantrap(i8 18) #[[ATTR3:[0-9]+]], !dbg [[DBGTRAP:![0-9]+]], !nosanitize [[META10]]
29+
// CHECK-TRAP-NEXT: unreachable, !dbg [[DBGTRAP]], !nosanitize [[META10]]
3030
// CHECK-TRAP: [[CONT]]:
3131
// CHECK-TRAP-NEXT: [[IDXPROM:%.*]] = sext i32 [[CALL]] to i64, !dbg [[DBG26:![0-9]+]]
3232
// CHECK-TRAP-NEXT: [[ARRAYIDX:%.*]] = getelementptr inbounds [10 x double], ptr [[A]], i64 0, i64 [[IDXPROM]], !dbg [[DBG26]]
3333
// CHECK-TRAP-NEXT: [[TMP2:%.*]] = load double, ptr [[ARRAYIDX]], align 8, !dbg [[DBG26]]
34-
// CHECK-TRAP-NEXT: ret double [[TMP2]], !dbg [[DBG28:![0-9]+]]
34+
// CHECK-TRAP-NEXT: ret double [[TMP2]], !dbg [[DBG30:![0-9]+]]
3535
//
3636
// CHECK-NOTRAP-LABEL: define dso_local double @f1(
3737
// CHECK-NOTRAP-SAME: i32 noundef [[B:%.*]], i32 noundef [[I:%.*]]) #[[ATTR0:[0-9]+]] !dbg [[DBG4:![0-9]+]] {
@@ -93,7 +93,9 @@ double f1(int b, int i) {
9393
// CHECK-TRAP: [[META25]] = !DISubroutineType(types: null)
9494
// CHECK-TRAP: [[DBG26]] = !DILocation(line: 66, column: 10, scope: [[DBG4]])
9595
// CHECK-TRAP: [[PROF27]] = !{!"branch_weights", i32 1048575, i32 1}
96-
// CHECK-TRAP: [[DBG28]] = !DILocation(line: 66, column: 3, scope: [[DBG4]])
96+
// CHECK-TRAP: [[DBGTRAP]] = !DILocation(line: 0, scope: [[TRAPMSG:![0-9]+]], inlinedAt: [[DBG23]])
97+
// CHECK-TRAP: [[TRAPMSG]] = distinct !DISubprogram(name: "__clang_trap_msg$Undefined Behavior Sanitizer$Array index out of bounds", scope: [[META5]], file: [[META5]], type: [[META25]], flags: DIFlagArtificial, spFlags: DISPFlagDefinition, unit: [[META0]])
98+
// CHECK-TRAP: [[DBG30]] = !DILocation(line: 66, column: 3, scope: [[DBG4]])
9799
//.
98100
// CHECK-NOTRAP: [[META0:![0-9]+]] = distinct !DICompileUnit(language: DW_LANG_C11, file: [[META1:![0-9]+]], isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, splitDebugInlining: false, nameTableKind: None)
99101
// CHECK-NOTRAP: [[META1]] = !DIFile(filename: "<stdin>", directory: {{.*}})

0 commit comments

Comments
 (0)