Skip to content

Commit bea52ed

Browse files
committed
[CFI] Fix Direct Call Issues in CFI Dispatch Table
This commit addresses two issues related to the use of Control Flow Integrity (CFI) dispatch table entries as direct calls. Issue with Inlining: When a dispatch table entry contains only a single function pointer, it can be replaced with a direct call to the jump table. However, if this function is inlined, the unreachable instruction that follows can disrupt the control flow of the containing function. This is illustrated in the following code snippet: ```llvm ; Function Attrs: naked nocf_check define private void @.cfi.jumptable() #6 align 8 { entry: call void asm sideeffect "jmp ${0:c}@plt\0Aint3\0Aint3\0Aint3\0A", "s"(ptr @_Z7throw_ei) unreachable } ``` Issue with Unwinding: When unwinding, a direct call of the jump table can cause all exception handling code of the containing function to be dropped. This occurs even if the indirectly dispatched function throws an exception which then results in unhandled exception crashes.
1 parent 86e99e1 commit bea52ed

File tree

8 files changed

+446
-24
lines changed

8 files changed

+446
-24
lines changed

llvm/lib/Transforms/IPO/LowerTypeTests.cpp

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1467,9 +1467,19 @@ void LowerTypeTestsModule::createJumpTable(
14671467
SmallVector<Value *, 16> AsmArgs;
14681468
AsmArgs.reserve(Functions.size() * 2);
14691469

1470-
for (GlobalTypeMember *GTM : Functions)
1470+
// Check if all entries have the NoUnwind attribute.
1471+
// If all entries have it, we can safely mark the
1472+
// cfi.jumptable as NoUnwind, otherwise, direct calls
1473+
// to the jump table will not handle exceptions properly
1474+
bool areAllEntriesNounwind = true;
1475+
for (GlobalTypeMember *GTM : Functions) {
1476+
if (!llvm::cast<llvm::Function>(GTM->getGlobal())
1477+
->hasFnAttribute(llvm::Attribute::NoUnwind)) {
1478+
areAllEntriesNounwind = false;
1479+
}
14711480
createJumpTableEntry(AsmOS, ConstraintOS, JumpTableArch, AsmArgs,
14721481
cast<Function>(GTM->getGlobal()));
1482+
}
14731483

14741484
// Align the whole table by entry size.
14751485
F->setAlignment(Align(getJumpTableEntrySize()));
@@ -1512,8 +1522,13 @@ void LowerTypeTestsModule::createJumpTable(
15121522
// -fcf-protection=.
15131523
if (JumpTableArch == Triple::x86 || JumpTableArch == Triple::x86_64)
15141524
F->addFnAttr(Attribute::NoCfCheck);
1515-
// Make sure we don't emit .eh_frame for this function.
1516-
F->addFnAttr(Attribute::NoUnwind);
1525+
1526+
// Make sure we don't emit .eh_frame for this function if it isn't needed.
1527+
if (areAllEntriesNounwind)
1528+
F->addFnAttr(Attribute::NoUnwind);
1529+
1530+
// Make sure we do not inline any calls to the cfi.jumptable.
1531+
F->addFnAttr(Attribute::NoInline);
15171532

15181533
BasicBlock *BB = BasicBlock::Create(M.getContext(), "entry", F);
15191534
IRBuilder<> IRB(BB);

llvm/test/Transforms/LowerTypeTests/aarch64-jumptable.ll

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --include-generated-funcs --version 2
12
; RUN: opt -S -passes=lowertypetests -mtriple=aarch64-unknown-linux-gnu %s | FileCheck --check-prefixes=AARCH64 %s
23

34
; Test for the jump table generation with branch protection on AArch64
@@ -6,7 +7,6 @@ target datalayout = "e-p:64:64"
67

78
@0 = private unnamed_addr constant [2 x ptr] [ptr @f, ptr @g], align 16
89

9-
; AARCH64: @f = alias void (), ptr @[[JT:.*]]
1010

1111
define void @f() !type !0 {
1212
ret void
@@ -29,11 +29,30 @@ define i1 @foo(ptr %p) {
2929

3030
!1 = !{i32 4, !"branch-target-enforcement", i32 1}
3131

32-
; AARCH64: define private void @[[JT]]() #[[ATTR:.*]] align 8 {
3332

34-
; AARCH64: bti c
35-
; AARCH64-SAME: b $0
36-
; AARCH64-SAME: bti c
37-
; AARCH64-SAME: b $1
38-
39-
; AARCH64: attributes #[[ATTR]] = { naked nounwind "branch-target-enforcement"="false" "sign-return-address"="none"
33+
; AARCH64-LABEL: define hidden void @f.cfi() !type !1 {
34+
; AARCH64-NEXT: ret void
35+
;
36+
;
37+
; AARCH64-LABEL: define internal void @g.cfi() !type !1 {
38+
; AARCH64-NEXT: ret void
39+
;
40+
;
41+
; AARCH64-LABEL: define i1 @foo
42+
; AARCH64-SAME: (ptr [[P:%.*]]) {
43+
; AARCH64-NEXT: [[TMP1:%.*]] = ptrtoint ptr [[P]] to i64
44+
; AARCH64-NEXT: [[TMP2:%.*]] = sub i64 [[TMP1]], ptrtoint (ptr @.cfi.jumptable to i64)
45+
; AARCH64-NEXT: [[TMP3:%.*]] = lshr i64 [[TMP2]], 3
46+
; AARCH64-NEXT: [[TMP4:%.*]] = shl i64 [[TMP2]], 61
47+
; AARCH64-NEXT: [[TMP5:%.*]] = or i64 [[TMP3]], [[TMP4]]
48+
; AARCH64-NEXT: [[TMP6:%.*]] = icmp ule i64 [[TMP5]], 1
49+
; AARCH64-NEXT: ret i1 [[TMP6]]
50+
;
51+
;
52+
; AARCH64: Function Attrs: naked noinline
53+
; AARCH64-LABEL: define private void @.cfi.jumptable
54+
; AARCH64-SAME: () #[[ATTR1:[0-9]+]] align 8 {
55+
; AARCH64-NEXT: entry:
56+
; AARCH64-NEXT: call void asm sideeffect "bti c\0Ab $0\0Abti c\0Ab $1\0A", "s,s"(ptr @f.cfi, ptr @g.cfi)
57+
; AARCH64-NEXT: unreachable
58+
;
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --include-generated-funcs --version 2
2+
; RUN: opt < %s -passes='lowertypetests,default<O3>' -S | FileCheck %s
3+
4+
; This IR is based of the following C++
5+
; which was compiled with:
6+
; clang -cc1 -fexceptions -fcxx-exceptions \
7+
; -std=c++11 -internal-isystem llvm-project/build/lib/clang/17/include \
8+
; -nostdsysteminc -triple x86_64-unknown-linux -fsanitize=cfi-icall \
9+
; -fsanitize-cfi-cross-dso -fsanitize-trap=cfi-icall -Oz -S -emit-llvm
10+
; int (*catch_ptr)(int);
11+
; int nothrow_e (int num) noexcept {
12+
; if (num) return 1;
13+
; return 0;
14+
; }
15+
; int call_catch(int num) {
16+
; catch_ptr = &nothrow_e;
17+
; return catch_ptr(num);
18+
; }
19+
20+
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
21+
target triple = "x86_64-unknown-linux"
22+
23+
@catch_ptr = local_unnamed_addr global ptr null, align 8
24+
@llvm.used = appending global [1 x ptr] [ptr @__cfi_check_fail], section "llvm.metadata"
25+
26+
; Function Attrs: minsize mustprogress nofree norecurse nosync nounwind optsize willreturn memory(none)
27+
define dso_local noundef i32 @_Z9nothrow_ei(i32 noundef %num) #0 !type !4 !type !5 !type !6 {
28+
entry:
29+
%tobool.not = icmp ne i32 %num, 0
30+
%. = zext i1 %tobool.not to i32
31+
ret i32 %.
32+
}
33+
34+
; Function Attrs: minsize mustprogress nounwind optsize
35+
define dso_local noundef i32 @_Z10call_catchi(i32 noundef %num) local_unnamed_addr #1 !type !4 !type !5 !type !6 {
36+
entry:
37+
store ptr @_Z9nothrow_ei, ptr @catch_ptr, align 8, !tbaa !7
38+
%0 = tail call i1 @llvm.type.test(ptr nonnull @_Z9nothrow_ei, metadata !"_ZTSFiiE"), !nosanitize !11
39+
br i1 %0, label %cfi.cont, label %cfi.slowpath, !prof !12, !nosanitize !11
40+
41+
cfi.slowpath: ; preds = %entry
42+
tail call void @__cfi_slowpath(i64 5174074510188755522, ptr nonnull @_Z9nothrow_ei) #5, !nosanitize !11
43+
br label %cfi.cont, !nosanitize !11
44+
45+
cfi.cont: ; preds = %cfi.slowpath, %entry
46+
%tobool.not.i = icmp ne i32 %num, 0
47+
%..i = zext i1 %tobool.not.i to i32
48+
ret i32 %..i
49+
}
50+
51+
; Function Attrs: mustprogress nocallback nofree nosync nounwind speculatable willreturn memory(none)
52+
declare i1 @llvm.type.test(ptr, metadata) #2
53+
54+
declare void @__cfi_slowpath(i64, ptr) local_unnamed_addr
55+
56+
; Function Attrs: minsize optsize
57+
define weak_odr hidden void @__cfi_check_fail(ptr noundef %0, ptr noundef %1) #3 {
58+
entry:
59+
%.not = icmp eq ptr %0, null, !nosanitize !11
60+
br i1 %.not, label %trap, label %cont, !nosanitize !11
61+
62+
trap: ; preds = %cont, %entry
63+
tail call void @llvm.ubsantrap(i8 2) #6, !nosanitize !11
64+
unreachable, !nosanitize !11
65+
66+
cont: ; preds = %entry
67+
%2 = load i8, ptr %0, align 4, !nosanitize !11
68+
%switch = icmp ult i8 %2, 5
69+
br i1 %switch, label %trap, label %cont6
70+
71+
cont6: ; preds = %cont
72+
ret void, !nosanitize !11
73+
}
74+
75+
; Function Attrs: cold noreturn nounwind
76+
declare void @llvm.ubsantrap(i8 immarg) #4
77+
78+
define weak void @__cfi_check(i64 %0, ptr %1, ptr %2) local_unnamed_addr {
79+
entry:
80+
tail call void @llvm.trap()
81+
unreachable
82+
}
83+
84+
; Function Attrs: cold noreturn nounwind
85+
declare void @llvm.trap() #4
86+
87+
attributes #0 = { minsize mustprogress nofree norecurse nosync nounwind optsize willreturn memory(none) "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-features"="+cx8,+mmx,+sse,+sse2,+x87" }
88+
attributes #1 = { minsize mustprogress nounwind optsize "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-features"="+cx8,+mmx,+sse,+sse2,+x87" }
89+
attributes #2 = { mustprogress nocallback nofree nosync nounwind speculatable willreturn memory(none) }
90+
attributes #3 = { minsize optsize "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-features"="+cx8,+mmx,+sse,+sse2,+x87" }
91+
attributes #4 = { cold noreturn nounwind }
92+
attributes #5 = { nounwind }
93+
attributes #6 = { noreturn nounwind }
94+
95+
!llvm.module.flags = !{!0, !1, !2}
96+
!llvm.ident = !{!3}
97+
98+
!0 = !{i32 1, !"wchar_size", i32 4}
99+
!1 = !{i32 4, !"Cross-DSO CFI", i32 1}
100+
!2 = !{i32 4, !"CFI Canonical Jump Tables", i32 0}
101+
!3 = !{!"clang version 17.0.2"}
102+
!4 = !{i64 0, !"_ZTSFiiE"}
103+
!5 = !{i64 0, !"_ZTSFiiE.generalized"}
104+
!6 = !{i64 0, i64 5174074510188755522}
105+
!7 = !{!8, !8, i64 0}
106+
!8 = !{!"any pointer", !9, i64 0}
107+
!9 = !{!"omnipotent char", !10, i64 0}
108+
!10 = !{!"Simple C++ TBAA"}
109+
!11 = !{}
110+
!12 = !{!"branch_weights", i32 1048575, i32 1}
111+
; CHECK: Function Attrs: minsize mustprogress nofree norecurse nosync nounwind optsize willreturn memory(none)
112+
; CHECK-LABEL: define dso_local noundef i32 @_Z9nothrow_ei
113+
; CHECK-SAME: (i32 noundef [[NUM:%.*]]) #[[ATTR0:[0-9]+]] !type !4 !type !5 !type !6 {
114+
; CHECK-NEXT: entry:
115+
; CHECK-NEXT: [[TOBOOL_NOT:%.*]] = icmp ne i32 [[NUM]], 0
116+
; CHECK-NEXT: [[DOT:%.*]] = zext i1 [[TOBOOL_NOT]] to i32
117+
; CHECK-NEXT: ret i32 [[DOT]]
118+
;
119+
;
120+
; CHECK: Function Attrs: minsize mustprogress nofree norecurse nosync nounwind optsize willreturn memory(write, argmem: none, inaccessiblemem: none)
121+
; CHECK-LABEL: define dso_local noundef i32 @_Z10call_catchi
122+
; CHECK-SAME: (i32 noundef [[NUM:%.*]]) local_unnamed_addr #[[ATTR1:[0-9]+]] !type !4 !type !5 !type !6 {
123+
; CHECK-NEXT: entry:
124+
; CHECK-NEXT: store ptr @_Z9nothrow_ei.cfi_jt, ptr @catch_ptr, align 8, !tbaa [[TBAA7:![0-9]+]]
125+
; CHECK-NEXT: [[TOBOOL_NOT_I:%.*]] = icmp ne i32 [[NUM]], 0
126+
; CHECK-NEXT: [[DOT_I:%.*]] = zext i1 [[TOBOOL_NOT_I]] to i32
127+
; CHECK-NEXT: ret i32 [[DOT_I]]
128+
;
129+
;
130+
; CHECK: Function Attrs: minsize optsize
131+
; CHECK-LABEL: define weak_odr hidden void @__cfi_check_fail
132+
; CHECK-SAME: (ptr noundef [[TMP0:%.*]], ptr noundef [[TMP1:%.*]]) #[[ATTR2:[0-9]+]] {
133+
; CHECK-NEXT: entry:
134+
; CHECK-NEXT: [[DOTNOT:%.*]] = icmp eq ptr [[TMP0]], null, !nosanitize !11
135+
; CHECK-NEXT: br i1 [[DOTNOT]], label [[TRAP:%.*]], label [[CONT:%.*]], !nosanitize !11
136+
; CHECK: trap:
137+
; CHECK-NEXT: tail call void @llvm.ubsantrap(i8 2) #[[ATTR5:[0-9]+]], !nosanitize !11
138+
; CHECK-NEXT: unreachable, !nosanitize !11
139+
; CHECK: cont:
140+
; CHECK-NEXT: [[TMP2:%.*]] = load i8, ptr [[TMP0]], align 4, !nosanitize !11
141+
; CHECK-NEXT: [[SWITCH:%.*]] = icmp ult i8 [[TMP2]], 5
142+
; CHECK-NEXT: br i1 [[SWITCH]], label [[TRAP]], label [[CONT6:%.*]]
143+
; CHECK: cont6:
144+
; CHECK-NEXT: ret void, !nosanitize !11
145+
;
146+
;
147+
; CHECK-LABEL: define weak void @__cfi_check
148+
; CHECK-SAME: (i64 [[TMP0:%.*]], ptr [[TMP1:%.*]], ptr [[TMP2:%.*]]) local_unnamed_addr {
149+
; CHECK-NEXT: entry:
150+
; CHECK-NEXT: tail call void @llvm.trap()
151+
; CHECK-NEXT: unreachable
152+
;
153+
;
154+
; CHECK: Function Attrs: naked nocf_check noinline nounwind
155+
; CHECK-LABEL: define internal void @_Z9nothrow_ei.cfi_jt
156+
; CHECK-SAME: () #[[ATTR4:[0-9]+]] align 8 {
157+
; CHECK-NEXT: entry:
158+
; CHECK-NEXT: tail call void asm sideeffect "jmp ${0:c}@plt\0Aint3\0Aint3\0Aint3\0A", "s"(ptr nonnull @_Z9nothrow_ei) #[[ATTR6:[0-9]+]]
159+
; CHECK-NEXT: unreachable
160+
;

0 commit comments

Comments
 (0)