Skip to content

[AArch64][BTI] Mark EH landing pads as jump targets #149680

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

shashforge
Copy link
Contributor

@shashforge shashforge commented Jul 19, 2025

Clang wasn’t putting a BTI “jump” hint at the start of C++ catch/cleanup blocks.
With GCC 14’s runtime, those pads are entered via an indirect br, and BTI-enforced kernels kill the program with SIGILL.

Fix
Treat every isEHPad() block as a possible indirect-jump target in AArch64BranchTargets.cpp, so the existing pass adds bti j.


- if (AddrTaken || JumpTableTargets.count(&MBB))
+ if (AddrTaken || JumpTableTargets.count(&MBB) || MBB.isEHPad())
     CouldJump = true;

Impact

  • One 4-byte bti j in cold EH blocks, only when BTI is requested.
  • No change for builds without -mbranch-protection=bti.

Test

New lit test bti-ehpad.ll checks that the landing pad starts with bti.

No other code paths touched; full check-all passes.

Fixes #149267

@shashforge shashforge force-pushed the aarch64-bti-ehpad branch 2 times, most recently from 8ea51a3 to baf4849 Compare July 20, 2025 22:29
Copy link

github-actions bot commented Jul 20, 2025

✅ With the latest revision this PR passed the undef deprecator.

@shashforge shashforge changed the title [AArch64] Emit BTI j on EH landing pads () [AArch64][BTI] Mark EH landing pads as jump targets Jul 20, 2025
Landing pads reached by the unwinder are entered via 'br', so they must
start with BTI j when -mbranch-protection requests BTI.  Add isEHPad()
to the jump-target test.

Size/perf: +4 B per pad only when BTI is enabled.

Test: new CodeGen/AArch64/bti-ehpad.ll.
Signed-off-by: Shashi Shankar <[email protected]>
@shashforge shashforge marked this pull request as ready for review July 20, 2025 22:48
@llvmbot
Copy link
Member

llvmbot commented Jul 20, 2025

@llvm/pr-subscribers-backend-aarch64

Author: Shashi Shankar (shashforge)

Changes

Clang wasn’t putting a BTI “jump” hint at the start of C++ catch/cleanup blocks.
With GCC 14’s runtime, those pads are entered via an indirect br, and BTI-enforced kernels kill the program with SIGILL.

Fix
Treat every isEHPad() block as a possible indirect-jump target in AArch64BranchTargets.cpp, so the existing pass adds bti j.


- if (AddrTaken || JumpTableTargets.count(&amp;MBB))
+ if (AddrTaken || JumpTableTargets.count(&amp;MBB) || MBB.isEHPad())
     CouldJump = true;

Impact

  • One 4-byte bti j in cold EH blocks, only when BTI is requested.
  • No change for builds without -mbranch-protection=bti.

Test

New lit test bti-ehpad.ll checks that the landing pad starts with bti.

No other code paths touched; full check-all passes.

Fixes #149267


Full diff: https://github.com/llvm/llvm-project/pull/149680.diff

2 Files Affected:

  • (modified) llvm/lib/Target/AArch64/AArch64BranchTargets.cpp (+1-1)
  • (added) llvm/test/CodeGen/AArch64/bti-ehpad.ll (+26)
diff --git a/llvm/lib/Target/AArch64/AArch64BranchTargets.cpp b/llvm/lib/Target/AArch64/AArch64BranchTargets.cpp
index 3436dc9ef4521..1999195051aa5 100644
--- a/llvm/lib/Target/AArch64/AArch64BranchTargets.cpp
+++ b/llvm/lib/Target/AArch64/AArch64BranchTargets.cpp
@@ -100,7 +100,7 @@ bool AArch64BranchTargets::runOnMachineFunction(MachineFunction &MF) {
     // If the block itself is address-taken, it could be indirectly branched
     // to, but not called.
     if (MBB.isMachineBlockAddressTaken() || MBB.isIRBlockAddressTaken() ||
-        JumpTableTargets.count(&MBB))
+        JumpTableTargets.count(&MBB) || MBB.isEHPad())
       CouldJump = true;
 
     if (CouldCall || CouldJump) {
diff --git a/llvm/test/CodeGen/AArch64/bti-ehpad.ll b/llvm/test/CodeGen/AArch64/bti-ehpad.ll
new file mode 100644
index 0000000000000..70e43ff5c5d5d
--- /dev/null
+++ b/llvm/test/CodeGen/AArch64/bti-ehpad.ll
@@ -0,0 +1,26 @@
+; llvm/test/CodeGen/AArch64/bti-ehpad.ll
+; REQUIRES: aarch64-registered-target
+; RUN: llc -mtriple=aarch64-none-linux-gnu %s -o - | FileCheck %s
+
+declare i32 @__gxx_personality_v0(...)
+
+define void @test() #0 personality ptr @__gxx_personality_v0 {
+entry:
+  invoke void @may_throw()
+          to label %ret unwind label %lpad
+lpad:                               ; catch.dispatch
+  landingpad { ptr, i32 }
+          cleanup
+  ret void
+ret:
+  ret void
+}
+
+declare void @may_throw()
+
+attributes #0 = { "branch-target-enforcement"="true" }
+
+; Function needs both the architectural feature *and* the enforcement request.
+attributes #0 = { "branch-target-enforcement"="true" "target-features"="+bti" }
+
+; CHECK:      bti

@ozbenh
Copy link

ozbenh commented Jul 20, 2025

Thanks ! I'll will verify locally as well and report back

@ozbenh
Copy link

ozbenh commented Jul 21, 2025

So I tested a quick backport to llvm15 (default on AL2023) and it didn't work:

+--- a/llvm/lib/Target/AArch64/AArch64BranchTargets.cpp
++++ b/llvm/lib/Target/AArch64/AArch64BranchTargets.cpp
+@@ -91,7 +91,7 @@ bool AArch64BranchTargets::runOnMachineFunction(MachineFunction &MF) {
+ 
+     // If the block itself is address-taken, it could be indirectly branched
+     // to, but not called.
+-    if (MBB.hasAddressTaken() || JumpTableTargets.count(&MBB))
++    if (MBB.hasAddressTaken() || JumpTableTargets.count(&MBB) || MBB.isEHPad())
+       CouldJump = true;
+ 
+     if (CouldCall || CouldJump) {

That said I might have made a mistake, juggling too many plates today. I'll test on Fedora's llvm20 asap and check llvm15 again & will report.

@ozbenh
Copy link

ozbenh commented Jul 21, 2025

So it looks like (with clang15) the bti j is generated but when throwing, the unwinder is landing on the instruction after the newly inserted bti ... at least from a quick "next" with gdb (it's possible that gdb go that wrong, I haven't single stepped the whole way, I probably won't have time today).

@ozbenh
Copy link

ozbenh commented Jul 21, 2025

Same with llvm20 on Fedora:
Before:

.../...
  400b70:	97ffff8a 	bl	400998 <__cxa_throw@plt>
  400b74:	14000021 	b	400bf8 <_fini>
  400b78:	f9000be0 	str	x0, [sp, #16]
  400b7c:	2a0103e8 	mov	w8, w1
.../...

After:

  400b70:	97ffff8a 	bl	400998 <__cxa_throw@plt>
  400b74:	14000023 	b	400c00 <_fini>
  400b78:	d503249f 	bti	j
  400b7c:	f9000be0 	str	x0, [sp, #16]
  400b80:	2a0103e8 	mov	w8, w1

But it's still crashing. I've confirmed that libgcc branches past the bti instruction:

=> 0xfffff7c57350 <_Unwind_RaiseException+260>:	br	x6
(gdb) p/x $x6
$2 = 0x400b7c
(gdb) x/i $x6
   0x400b7c <main()+80>:	str	x0, [sp, #16]
(gdb) x/i $x6-8
   0x400b74 <main()+72>:	b	0x400c00 <_fini>
(gdb) 
   0x400b78 <main()+76>:	bti	j
(gdb) 
   0x400b7c <main()+80>:	str	x0, [sp, #16]

@ozbenh
Copy link

ozbenh commented Jul 21, 2025

Additionally with that patch, clang20 build on f42 fails with

********************
Failed Tests (1):
  Clang-Unit :: Interpreter/ExceptionTests/./ClangReplInterpreterExceptionTests/0/1

Is there another change elsewhere that also needs to be backported or is this fix simply incomplete ? Next I will try to build all of llvm from git, but that will require more time.

@efriedma-quic
Copy link
Collaborator

Probably the bti instruction is getting inserted before the EH_LABEL pseudo-instruction.

@ozbenh
Copy link

ozbenh commented Jul 22, 2025

So bear with me, I have no idea what I'm doing, never looked at LLVM code that deep, and this is probably completely wrong ... but hacking AArch64BranchTargets::addBTI() this way:

-  BuildMI(MBB, MBB.begin(), MBB.findDebugLoc(MBB.begin()),
-          TII->get(AArch64::HINT))
+  for (MBBI = MBB.begin() ; MBBI != MBB.end() && MBB.isEHPad(); MBBI++)
+         if (MBBI->getOpcode() == TargetOpcode::EH_LABEL) {
+                 MBBI++;
+                 break;
+         }
+  BuildMI(MBB, MBBI, MBB.findDebugLoc(MBBI), TII->get(AArch64::HINT))
       .addImm(HintNum);

Seems to do the trick :-)

I don't know how the HasWinCFI bit right before is affected if at all, again I have no idea what I'm doing, I mostly just grepped EH_LABEL and tried to make uniformed deductions :-)

@efriedma-quic
Copy link
Collaborator

That's basically correct. I think you only want to skip past the EH_LABEL if you're handling an EHPad, though: in other cases EH_LABEL is likely tied to a subsequent call instruction you don't want to mess with.

You shouldn't need any EH_LABEL handling with HasWinCFI; Windows uses funclets, and funclets don't start with EH_LABEL.

Maybe worth adding a testcase for Windows if you don't mind. (Don't use the same IR, though; exception handling IR is very different. You can use something like void g(); struct A {~A(); };void f() {A a; g(); } compiled with --target=aarch64-windows-msvc.)

@shashforge
Copy link
Contributor Author

shashforge commented Jul 22, 2025

So bear with me, I have no idea what I'm doing, never looked at LLVM code that deep, and this is probably completely wrong ... but hacking AArch64BranchTargets::addBTI() this way:

-  BuildMI(MBB, MBB.begin(), MBB.findDebugLoc(MBB.begin()),
-          TII->get(AArch64::HINT))
+  for (MBBI = MBB.begin() ; MBBI != MBB.end() && MBB.isEHPad(); MBBI++)
+         if (MBBI->getOpcode() == TargetOpcode::EH_LABEL) {
+                 MBBI++;
+                 break;
+         }
+  BuildMI(MBB, MBBI, MBB.findDebugLoc(MBBI), TII->get(AArch64::HINT))
       .addImm(HintNum);

Seems to do the trick :-)

I don't know how the HasWinCFI bit right before is affected if at all, again I have no idea what I'm doing, I mostly just grepped EH_LABEL and tried to make uniformed deductions :-)

@ozbenh Great catch — the unwinder’s LSDA symbol is that leading EH_LABEL, so inserting the hint before it meant the indirect branch still landed after the BTI. I’ve adopted exactly your idea: for EH pads we now walk past any initial EH_LABELs before emitting the hint.

Thanks again for digging into this!

@shashforge
Copy link
Contributor Author

shashforge commented Jul 22, 2025

That's basically correct. I think you only want to skip past the EH_LABEL if you're handling an EHPad, though: in other cases EH_LABEL is likely tied to a subsequent call instruction you don't want to mess with.

You shouldn't need any EH_LABEL handling with HasWinCFI; Windows uses funclets, and funclets don't start with EH_LABEL.

Maybe worth adding a testcase for Windows if you don't mind. (Don't use the same IR, though; exception handling IR is very different. You can use something like void g(); struct A {~A(); };void f() {A a; g(); } compiled with --target=aarch64-windows-msvc.)

@efriedma-quic > Totally agree this should only happen for EH pads.
• Skip-past-EH_LABEL is now guarded by MBB.isEHPad(), so call-site EH_LABELs remain untouched.
• HasWinCFI path unchanged (funclets don’t start with EH_LABEL).
Added the Windows lit test you suggested (bti-funclet-windows.ll); it checks precisely that sequence.

Appreciate the detailed guidance — thank you!

Copy link

⚠️ C/C++ code formatter, clang-format found issues in your code. ⚠️

You can test this locally with the following command:
git-clang-format --diff HEAD~1 HEAD --extensions cpp -- llvm/lib/Target/AArch64/AArch64BranchTargets.cpp
View the diff from clang-format here.
diff --git a/llvm/lib/Target/AArch64/AArch64BranchTargets.cpp b/llvm/lib/Target/AArch64/AArch64BranchTargets.cpp
index e98044712..24419e47f 100644
--- a/llvm/lib/Target/AArch64/AArch64BranchTargets.cpp
+++ b/llvm/lib/Target/AArch64/AArch64BranchTargets.cpp
@@ -149,12 +149,10 @@ void AArch64BranchTargets::addBTI(MachineBasicBlock &MBB, bool CouldCall,
 
   MBBI = MBB.begin();
   if (MBB.isEHPad()) {
-    while (MBBI != MBB.end() &&
-           MBBI->getOpcode() == TargetOpcode::EH_LABEL)
+    while (MBBI != MBB.end() && MBBI->getOpcode() == TargetOpcode::EH_LABEL)
       ++MBBI;
   }
 
-  BuildMI(MBB, MBBI, MBB.findDebugLoc(MBBI),
-          TII->get(AArch64::HINT))
+  BuildMI(MBB, MBBI, MBB.findDebugLoc(MBBI), TII->get(AArch64::HINT))
       .addImm(HintNum);
 }

@efriedma-quic
Copy link
Collaborator

Did you forget to add bti-funclet-windows.ll to the latest commit?

@shashforge
Copy link
Contributor Author

shashforge commented Jul 22, 2025

Did you forget to add bti-funclet-windows.ll to the latest commit?

It in progress currently it failing..I am on it . It look like BTI is disabled for Windows. I am on it

Failed reason

/home/intel/src/opesource/llvm/llvm-project/llvm/test/CodeGen/AArch64/bti-funclet-windows.ll:28:10: error: CHECK: expected string not found in input
; CHECK: bti c
         ^
<stdin>:29:20: note: scanning from here
 // -- End function
                   ^

Input file: <stdin>
Check file: /home/intel/src/opesource/llvm/llvm-project/llvm/test/CodeGen/AArch64/bti-funclet-windows.ll

-dump-input=help explains the following input dump.

Input was:
<<<<<<
          .
          .
          .
         24:  .seh_save_reg_x x30, 16 
         25:  .seh_endepilogue 
         26:  ret 
         27:  .seh_endfunclet 
         28:  .seh_endproc 
         29:  // -- End function 
check:28                        X error: no match found
>>>>>>

@ozbenh
Copy link

ozbenh commented Jul 23, 2025

That's basically correct. I think you only want to skip past the EH_LABEL if you're handling an EHPad, though: in other cases EH_LABEL is likely tied to a subsequent call instruction you don't want to mess with.

My MBB.isEHPad() in the for loop condition should take care of that no ? ie, if not an EH pad, the loop exits with MBBI pointing to MBB.begin()

@shashforge
Copy link
Contributor Author

That's basically correct. I think you only want to skip past the EH_LABEL if you're handling an EHPad, though: in other cases EH_LABEL is likely tied to a subsequent call instruction you don't want to mess with.

My MBB.isEHPad() in the for loop condition should take care of that no ? ie, if not an EH pad, the loop exits with MBBI pointing to MBB.begin()

he guard on MBB.isEHPad() ensures we only advance the iterator when we’re in an EH pad, so for a normal basic block the loop/while immediately terminates and MBBI remains at MBB.begin().

@ozbenh
Copy link

ozbenh commented Jul 23, 2025

he guard on MBB.isEHPad() ensures we only advance the iterator when we’re in an EH pad, so for a normal basic block the loop/while immediately terminates and MBBI remains at MBB.begin().

Sure, what I meant is that my original proposed implementation did that without the extra if() by simply putting the test for MBB.isEHPad() in the loop condition. So the loop wouldn't have incremented the iterator for a non-EHPad either.

But I don't care either way as long as the end result works :)

The only other difference with your version is that I would look for an EH_LABEL anywhere in the MBB while your variant only works if the EH_LABEL is at the beginning. I don't know if this matters (could there be some other pseudo-op before the EH_LABEL ?), I don't now enough about llvm here.

@ozbenh
Copy link

ozbenh commented Aug 6, 2025

@shashforge What's the next step here ?

Comment on lines +21 to +24
attributes #0 = { "branch-target-enforcement"="true" }

; Function needs both the architectural feature *and* the enforcement request.
attributes #0 = { "branch-target-enforcement"="true" "target-features"="+bti" }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't define attribute #0 twice?

Comment on lines +1 to +2
; llvm/test/CodeGen/AArch64/bti-ehpad.ll
; REQUIRES: aarch64-registered-target
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove these, the REQUIRES is handled by the folder.
Also it looks like it would be worth running update_llc_test_checks.py to generate a good set of test checks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

clang++ 20 Exceptions crash with (recent) libgcc and aarch64 BTI (Linux)
5 participants