Skip to content

[CrossDSO CFI] Make sure __cfi_check has BTI attribute. #131224

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

efriedma-quic
Copy link
Collaborator

When we're doing CrossDSO CFI, clang tries to generate a stub for __cfi_check so it has the correct attributes. However, this doesn't work in some scenarios: if you're doing ThinLTO, and some code is forced into the "fulllto" module, __cfi_check is generated in that module, and there is no stub.

In this scenario, there's an inconsistency between the "branch-target-enforcement" module flag, and the function attribute: the function attribute is missing. So the "bti c" isn't generated, and it faults at runtime.

This fix works around the issue by forcing the "branch-target-enforcement" attribute onto the function.

@llvmbot
Copy link
Member

llvmbot commented Mar 13, 2025

@llvm/pr-subscribers-llvm-transforms

Author: Eli Friedman (efriedma-quic)

Changes

When we're doing CrossDSO CFI, clang tries to generate a stub for __cfi_check so it has the correct attributes. However, this doesn't work in some scenarios: if you're doing ThinLTO, and some code is forced into the "fulllto" module, __cfi_check is generated in that module, and there is no stub.

In this scenario, there's an inconsistency between the "branch-target-enforcement" module flag, and the function attribute: the function attribute is missing. So the "bti c" isn't generated, and it faults at runtime.

This fix works around the issue by forcing the "branch-target-enforcement" attribute onto the function.


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

2 Files Affected:

  • (modified) llvm/lib/Transforms/IPO/CrossDSOCFI.cpp (+9)
  • (added) llvm/test/Transforms/CrossDSOCFI/aarch64-bti.ll (+53)
diff --git a/llvm/lib/Transforms/IPO/CrossDSOCFI.cpp b/llvm/lib/Transforms/IPO/CrossDSOCFI.cpp
index 2d884078940cc..813adeba75564 100644
--- a/llvm/lib/Transforms/IPO/CrossDSOCFI.cpp
+++ b/llvm/lib/Transforms/IPO/CrossDSOCFI.cpp
@@ -94,6 +94,15 @@ void CrossDSOCFI::buildCFICheck(Module &M) {
   Triple T(M.getTargetTriple());
   if (T.isARM() || T.isThumb())
     F->addFnAttr("target-features", "+thumb-mode");
+  if (T.isAArch64()) {
+    if (const auto *BTE = mdconst::extract_or_null<ConstantInt>(
+          M.getModuleFlag("branch-target-enforcement"))) {
+      if (BTE->getZExtValue() != 0) {
+        F->addFnAttr("branch-target-enforcement");
+      }
+    }
+  }
+
 
   auto args = F->arg_begin();
   Value &CallSiteTypeId = *(args++);
diff --git a/llvm/test/Transforms/CrossDSOCFI/aarch64-bti.ll b/llvm/test/Transforms/CrossDSOCFI/aarch64-bti.ll
new file mode 100644
index 0000000000000..baed432f874c8
--- /dev/null
+++ b/llvm/test/Transforms/CrossDSOCFI/aarch64-bti.ll
@@ -0,0 +1,53 @@
+; RUN: opt -S -passes=cross-dso-cfi < %s | FileCheck %s
+
+; CHECK: define void @__cfi_check({{.*}}) [[ATTR:#[0-9]+]] align 4096
+; CHECK: [[ATTR]] = { "branch-target-enforcement" }
+
+target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
+target triple = "aarch64-unknown-linux-gnu"
+
+@_ZTV1A = constant i8 0, !type !4, !type !5
+@_ZTV1B = constant i8 0, !type !4, !type !5, !type !6, !type !7
+
+define signext i8 @f11() "branch-target-enforcement" !type !0 !type !1 {
+entry:
+  ret i8 1
+}
+
+define signext i8 @f12() "branch-target-enforcement" !type !0 !type !1 {
+entry:
+  ret i8 2
+}
+
+define signext i8 @f13() "branch-target-enforcement" !type !0 !type !1 {
+entry:
+  ret i8 3
+}
+
+define i32 @f21() "branch-target-enforcement" !type !2 !type !3 {
+entry:
+  ret i32 4
+}
+
+define i32 @f22() "branch-target-enforcement" !type !2 !type !3 {
+entry:
+  ret i32 5
+}
+
+define weak_odr hidden void @__cfi_check_fail(ptr, ptr) "branch-target-enforcement" {
+entry:
+  ret void
+}
+
+!llvm.module.flags = !{!8, !9}
+
+!0 = !{i64 0, !"_ZTSFcvE"}
+!1 = !{i64 0, i64 111}
+!2 = !{i64 0, !"_ZTSFivE"}
+!3 = !{i64 0, i64 222}
+!4 = !{i64 16, !"_ZTS1A"}
+!5 = !{i64 16, i64 333}
+!6 = !{i64 16, !"_ZTS1B"}
+!7 = !{i64 16, i64 444}
+!8 = !{i32 4, !"Cross-DSO CFI", i32 1}
+!9 = !{i32 8, !"branch-target-enforcement", i32 1}

@efriedma-quic
Copy link
Collaborator Author

I'm not confident this patch is the best solution, but I'm not sure what a better solution looks like; should clang be setting the relevant function attributes somehow?

Copy link

github-actions bot commented Mar 13, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

When we're doing CrossDSO CFI, clang tries to generate a stub for
__cfi_check so it has the correct attributes.  However, this doesn't
work in some scenarios: if you're doing ThinLTO, and some code is forced
into the "fulllto" module, __cfi_check is generated in that module, and
there is no stub.

In this scenario, there's an inconsistency between the
"branch-target-enforcement" module flag, and the function: the function
is missing the attribute.  So the "bti c" isn't generated, and it faults
at runtime.

This fix works around the issue by forcing the
"branch-target-enforcement" attribute onto the function.

See also c7cacb2.
@pirama-arumuga-nainar
Copy link
Collaborator

Adding @pcc as well.

@@ -94,6 +94,14 @@ void CrossDSOCFI::buildCFICheck(Module &M) {
Triple T(M.getTargetTriple());
if (T.isARM() || T.isThumb())
F->addFnAttr("target-features", "+thumb-mode");
if (T.isAArch64()) {
Copy link
Member

Choose a reason for hiding this comment

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

M.getOrInsertFunction could be replaced with the Function::createWithDefaultAttr to create the functions with the right set of attributes. It is used in other places in the sanitisers to create functions. It will solve this and future problems.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Gave this a shot. Seems to be okay for aarch64, but I'm a little concerned this will cause issues for other targets (see #98673).

Copy link
Contributor

Choose a reason for hiding this comment

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

The way I think this should work is that the function is created by the frontend with the correct attributes and then filled in by the LTO backend. See #100833 for details. That approach is compatible with #98673 because the attributes are still coming from the frontend.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I briefly described why that doesn't work in the commit message, but I'll go into more detail:

In ThinLTO, the code is in modules which correspond to translation units in the original source code. However, depending on the exact structure of the inputs, you can end up with an additional "base" module that only contains some global variables, and no code. LTO chooses to generate __cfi_check into that module; when it does, there is no function to take the attributes from.

So there's a function named __cfi_check, created by the frontend, in every module except the one that actually matters. So I'm just trying to repair the IR the best I can once we've reached this point.

If you think something is supposed to happen earlier in the pipeline, can you describe what, exactly, that looks like?

Copy link
Contributor

Choose a reason for hiding this comment

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

My expectation would be that __cfi_check always appears on the full LTO side and has the correct attributes. If we end up generating code for that function without attributes, it sounds like a bug. Do you have a reproducer showing how __cfi_check can end up being code generated without attributes?

Copy link
Collaborator Author

@efriedma-quic efriedma-quic Mar 17, 2025

Choose a reason for hiding this comment

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

$ cat a.c
int a = 3;
void f(){}
$ cat b.c
void g(){}
$ clang a.c b.c -flto=thin -fsanitize=cfi -fsanitize-cfi-cross-dso -O2 -c -fvisibility=default
$ clang a.o b.o -flto=thin -fsanitize=cfi -fsanitize-cfi-cross-dso -O2 -fuse-ld=lld -shared -Wl,-save-temps -fvisibility=default

__cfi_check is generated in a.out.0.5.precodegen.bc, which doesn't contain a stub.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks. With the attached patch (not extensively tested) it looks like __cfi_check now goes to the correct module. Next step to fix this bug would be to resolve issues pointed out in #100833 so that the frontend gives it the correct attributes.

0001-ThinLTOBitcodeWriter-Move-__cfi_check-to-merged-modu.patch.txt

Copy link
Member

@DanielKristofKiss DanielKristofKiss left a comment

Choose a reason for hiding this comment

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

Thanks, LGTM

@teresajohnson teresajohnson requested review from vitalybuka and pcc March 17, 2025 15:00
@teresajohnson
Copy link
Contributor

Added @vitalybuka and @pcc who are more familiar with CFI. The change looks ok to me but I am not sure whether this should have been handled elsewhere.

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.

6 participants