-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
[X86] Extend kCFI with a 3-bit arity indicator #121070
Conversation
@llvm/pr-subscribers-clang-driver @llvm/pr-subscribers-backend-x86 Author: Scott Constable (scottconstable) ChangesKernel Control Flow Integrity (kCFI) is a feature that hardens indirect calls by comparing a 32-bit hash of the function pointer's type against a hash of the target function's type. If the hashes do not match, the kernel may panic (or log the hash check failure, depending on the kernel's configuration). These hashes are computed at compile time by applying the xxHash64 algorithm to each mangled canonical function (or function pointer) type, then truncating the result to 32 bits. This hash is written into each indirect-callable function header by encoding it as the 32-bit immediate operand to a
This PR extends x86-based kCFI with a 3-bit arity indicator encoded in the
For example, if
This PR will benefit other CFI approaches that build on kCFI, such as FineIBT. For example, this proposed enhancement to FineIBT must be able to infer (at kernel init time) which registers are live at an indirect call target: https://lkml.org/lkml/2024/9/27/982. If the arity bits are available in the kCFI function header, then this information is trivial to infer. Note that there is another existing PR proposal that includes the 3-bit arity within the existing 32-bit immediate field, which introduces different security properties: #117121. Full diff: https://github.com/llvm/llvm-project/pull/121070.diff 4 Files Affected:
diff --git a/clang/include/clang/Basic/Features.def b/clang/include/clang/Basic/Features.def
index c82b6d9b5f6c10..dca8f4dc0fbf76 100644
--- a/clang/include/clang/Basic/Features.def
+++ b/clang/include/clang/Basic/Features.def
@@ -254,6 +254,7 @@ FEATURE(is_trivially_constructible, LangOpts.CPlusPlus)
FEATURE(is_trivially_copyable, LangOpts.CPlusPlus)
FEATURE(is_union, LangOpts.CPlusPlus)
FEATURE(kcfi, LangOpts.Sanitize.has(SanitizerKind::KCFI))
+FEATURE(kcfi_x86_arity, LangOpts.Sanitize.has(SanitizerKind::KCFI))
FEATURE(modules, LangOpts.Modules)
FEATURE(safe_stack, LangOpts.Sanitize.has(SanitizerKind::SafeStack))
FEATURE(shadow_call_stack,
diff --git a/llvm/lib/Target/X86/X86AsmPrinter.cpp b/llvm/lib/Target/X86/X86AsmPrinter.cpp
index f01e47b41cf5e4..cb21d9bb5f2879 100644
--- a/llvm/lib/Target/X86/X86AsmPrinter.cpp
+++ b/llvm/lib/Target/X86/X86AsmPrinter.cpp
@@ -181,8 +181,26 @@ void X86AsmPrinter::emitKCFITypeId(const MachineFunction &MF) {
// Embed the type hash in the X86::MOV32ri instruction to avoid special
// casing object file parsers.
EmitKCFITypePadding(MF);
+
+ Register MovReg = X86::EAX;
+ const auto &Triple = MF.getTarget().getTargetTriple();
+ if (Triple.isArch64Bit() && Triple.isOSLinux()) {
+ // Determine the function's arity (i.e., the number of arguments) at the ABI
+ // level by counting the number of parameters that are passed
+ // as registers, such as pointers and 64-bit (or smaller) integers. The
+ // Linux x86-64 ABI allows up to 6 parameters to be passed in GPRs.
+ // Additional parameters or parameters larger than 64 bits may be passed on
+ // the stack, in which case the arity is denoted as 7.
+ const unsigned ArityToRegMap[8] = {X86::EAX, X86::ECX, X86::EDX, X86::EBX,
+ X86::ESP, X86::EBP, X86::ESI, X86::EDI};
+ int Arity = MF.getInfo<X86MachineFunctionInfo>()->getArgumentStackSize() > 0
+ ? 7
+ : MF.getRegInfo().liveins().size();
+ MovReg = ArityToRegMap[Arity];
+ }
+
EmitAndCountInstruction(MCInstBuilder(X86::MOV32ri)
- .addReg(X86::EAX)
+ .addReg(MovReg)
.addImm(MaskKCFIType(Type->getZExtValue())));
if (MAI->hasDotTypeDotSizeDirective()) {
diff --git a/llvm/test/CodeGen/X86/kcfi-patchable-function-prefix.ll b/llvm/test/CodeGen/X86/kcfi-patchable-function-prefix.ll
index 1b7bd7835e890c..deababc7fd5379 100644
--- a/llvm/test/CodeGen/X86/kcfi-patchable-function-prefix.ll
+++ b/llvm/test/CodeGen/X86/kcfi-patchable-function-prefix.ll
@@ -3,7 +3,7 @@
; CHECK: .p2align 4
; CHECK-LABEL: __cfi_f1:
; CHECK-COUNT-11: nop
-; CHECK-NEXT: movl $12345678, %eax
+; CHECK-NEXT: movl $12345678, %ecx
; CHECK-LABEL: .Lcfi_func_end0:
; CHECK-NEXT: .size __cfi_f1, .Lcfi_func_end0-__cfi_f1
; CHECK-LABEL: f1:
@@ -26,7 +26,7 @@ define void @f2(ptr noundef %x) {
; CHECK: .p2align 4
; CHECK-LABEL: __cfi_f3:
; CHECK-NOT: nop
-; CHECK-NEXT: movl $12345678, %eax
+; CHECK-NEXT: movl $12345678, %ecx
; CHECK-COUNT-11: nop
; CHECK-LABEL: f3:
define void @f3(ptr noundef %x) #0 !kcfi_type !1 {
diff --git a/llvm/test/CodeGen/X86/kcfi.ll b/llvm/test/CodeGen/X86/kcfi.ll
index 059efcc71b0eb8..91d706796b8e99 100644
--- a/llvm/test/CodeGen/X86/kcfi.ll
+++ b/llvm/test/CodeGen/X86/kcfi.ll
@@ -16,7 +16,7 @@
; ASM-NEXT: nop
; ASM-NEXT: nop
; ASM-NEXT: nop
-; ASM-NEXT: movl $12345678, %eax
+; ASM-NEXT: movl $12345678, %ecx
; ASM-LABEL: .Lcfi_func_end0:
; ASM-NEXT: .size __cfi_f1, .Lcfi_func_end0-__cfi_f1
define void @f1(ptr noundef %x) !kcfi_type !1 {
@@ -90,7 +90,7 @@ define void @f4(ptr noundef %x) #0 {
;; Ensure we emit Value + 1 for unwanted values (e.g. endbr64 == 4196274163).
; ASM-LABEL: __cfi_f5:
-; ASM: movl $4196274164, %eax # imm = 0xFA1E0FF4
+; ASM: movl $4196274164, %ecx # imm = 0xFA1E0FF4
define void @f5(ptr noundef %x) !kcfi_type !2 {
; ASM-LABEL: f5:
; ASM: movl $98693132, %r10d # imm = 0x5E1F00C
@@ -100,7 +100,7 @@ define void @f5(ptr noundef %x) !kcfi_type !2 {
;; Ensure we emit Value + 1 for unwanted values (e.g. -endbr64 == 98693133).
; ASM-LABEL: __cfi_f6:
-; ASM: movl $98693134, %eax # imm = 0x5E1F00E
+; ASM: movl $98693134, %ecx # imm = 0x5E1F00E
define void @f6(ptr noundef %x) !kcfi_type !3 {
; ASM-LABEL: f6:
; ASM: movl $4196274162, %r10d # imm = 0xFA1E0FF2
@@ -138,6 +138,60 @@ define void @f8() {
ret void
}
+%struct.S9 = type { [10 x i64] }
+
+;; Ensure that functions with large (e.g., greater than 8 bytes) arguments passed on the stack are assigned arity=7
+; ASM-LABEL: __cfi_f9:
+; ASM: movl $199571451, %edi # imm = 0xBE537FB
+define dso_local void @f9(ptr noundef byval(%struct.S9) align 8 %s) !kcfi_type !4 {
+entry:
+ ret void
+}
+
+;; Ensure that functions with fewer than 7 register arguments and no stack arguments are assigned arity<7
+; ASM-LABEL: __cfi_f10:
+; ASM: movl $1046421190, %esi # imm = 0x3E5F1EC6
+define dso_local void @f10(i32 noundef %v1, i32 noundef %v2, i32 noundef %v3, i32 noundef %v4, i32 noundef %v5, i32 noundef %v6) #0 !kcfi_type !5 {
+entry:
+ %v1.addr = alloca i32, align 4
+ %v2.addr = alloca i32, align 4
+ %v3.addr = alloca i32, align 4
+ %v4.addr = alloca i32, align 4
+ %v5.addr = alloca i32, align 4
+ %v6.addr = alloca i32, align 4
+ store i32 %v1, ptr %v1.addr, align 4
+ store i32 %v2, ptr %v2.addr, align 4
+ store i32 %v3, ptr %v3.addr, align 4
+ store i32 %v4, ptr %v4.addr, align 4
+ store i32 %v5, ptr %v5.addr, align 4
+ store i32 %v6, ptr %v6.addr, align 4
+ ret void
+}
+
+;; Ensure that functions with greater than 7 register arguments and no stack arguments are assigned arity=7
+; ASM-LABEL: __cfi_f11:
+; ASM: movl $1342488295, %edi # imm = 0x5004BEE7
+define dso_local void @f11(i32 noundef %v1, i32 noundef %v2, i32 noundef %v3, i32 noundef %v4, i32 noundef %v5, i32 noundef %v6, i32 noundef %v7, i32 noundef %v8) #0 !kcfi_type !6 {
+entry:
+ %v1.addr = alloca i32, align 4
+ %v2.addr = alloca i32, align 4
+ %v3.addr = alloca i32, align 4
+ %v4.addr = alloca i32, align 4
+ %v5.addr = alloca i32, align 4
+ %v6.addr = alloca i32, align 4
+ %v7.addr = alloca i32, align 4
+ %v8.addr = alloca i32, align 4
+ store i32 %v1, ptr %v1.addr, align 4
+ store i32 %v2, ptr %v2.addr, align 4
+ store i32 %v3, ptr %v3.addr, align 4
+ store i32 %v4, ptr %v4.addr, align 4
+ store i32 %v5, ptr %v5.addr, align 4
+ store i32 %v6, ptr %v6.addr, align 4
+ store i32 %v7, ptr %v7.addr, align 4
+ store i32 %v8, ptr %v8.addr, align 4
+ ret void
+}
+
attributes #0 = { "target-features"="+retpoline-indirect-branches,+retpoline-indirect-calls" }
!llvm.module.flags = !{!0}
@@ -145,3 +199,6 @@ attributes #0 = { "target-features"="+retpoline-indirect-branches,+retpoline-ind
!1 = !{i32 12345678}
!2 = !{i32 4196274163}
!3 = !{i32 98693133}
+!4 = !{i32 199571451}
+!5 = !{i32 1046421190}
+!6 = !{i32 1342488295}
|
81ff927
to
70f1743
Compare
|
||
Register MovReg = X86::EAX; | ||
const auto &Triple = MF.getTarget().getTargetTriple(); | ||
if (Triple.isArch64Bit() && Triple.isOSLinux()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this check necessary? I notice that all of the x86-based kCFI tests use the x86_64-unknown-linux-gnu
triple. If 64-bit Linux is the only supported x86 subtarget then I think this check is redundant. But I'm not 100% certain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use assert to guarantee it won't be broken in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@phoebewang Good suggestion! I updated the PR to use an assert.
@@ -254,6 +254,7 @@ FEATURE(is_trivially_constructible, LangOpts.CPlusPlus) | |||
FEATURE(is_trivially_copyable, LangOpts.CPlusPlus) | |||
FEATURE(is_union, LangOpts.CPlusPlus) | |||
FEATURE(kcfi, LangOpts.Sanitize.has(SanitizerKind::KCFI)) | |||
FEATURE(kcfi_x86_arity, LangOpts.Sanitize.has(SanitizerKind::KCFI)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Peter Zijlstra, one of the Linux FineIBT maintainers, suggested to me that __has_feature()
might be the best interface for Kconfig to discover that clang supports this new kCFI arity extension. If this feature were to be added to rustc, then I think rustc would need a similar interface to expose the feature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For Kconfig, most features are discovered via testing a flag (e.g. -fsanitize=kcfi
), but if the kernel needs __has_feature()
anyway here, then that can also be used, yeah. For rustc
, we can do similar tests too (and since we only support one kind of Rust compiler so far, versions checks can be a fallback option too).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still think it would be best to support both hash types via flags to avoid limiting which compiler versions work together.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Encoding arity information in the register argument solves the hash compatibility issue, so I definitely prefer this approach over #117121. As you noted, the FineIBT patching code still needs to know whether both compilers emit arity information, and while using __has_feature
in C should be sufficient, how would the rustc feature test look like? A quick search didn't find any Kconfig examples for this. I assume we would need a Kconfig variable to pass this information to C code, but perhaps there are better options?
// Additional parameters or parameters larger than 64 bits may be passed on | ||
// the stack, in which case the arity is denoted as 7. | ||
const unsigned ArityToRegMap[8] = {X86::EAX, X86::ECX, X86::EDX, X86::EBX, | ||
X86::ESP, X86::EBP, X86::ESI, X86::EDI}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Astonished ESP can be used to pass the hash.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @phoebewang, based on my understanding of kCFI, the MOVri
instruction emitted by the compiler in each function header will never be executed. The purpose of this instruction is simply to insert the function's kCFI type ID into the header so that it can be verified at the call site before making the call. For example, this is how a kCFI-enforced indirect call looks when the kernel is configured with kCFI:
0000000000000010 <foo>: # foo loads the target address into rax
...
25: mov $0x70e5d6ba,%r10d # 0x70e5d6ba is the kCFI type ID that must be matched at the target
2b: add -0x4(%rax),%r10d # -0x4(%rax) refers to the immediate operand in the `MOVri` instruction, which holds the kCFI type ID
2f: je 33 <foo+0x23> # If the two type IDs match, then jump to the call; otherwise #UD
31: ud2
33: call *%rax # Note that this will jump directly to the target function, and will not execute the `MOVri`
Does this address your concern?
And you cannot use ESI when it's used to pass arguments. Suggested registers:
|
@phoebewang I think that my #121070 (comment) also applies to this concern. |
70f1743
to
73b6f63
Compare
If hashing is changed, consider replacing xxhash64 with xxh3+_64bits |
@MaskRay This PR does not change the hashing scheme at all. |
This:) Thanks for the explanation. After my pending lld/MachO change, kcfi will be the only user of the legacy |
Going back to @maurer 's comment about compatibility, I do not believe that this PR would break any compatibility with other compilers, or with other compiler versions. If a kernel is built with some of its functions having the 3-bit arity extension and the rest of its functions not having the 3-bit arity extension (e.g., because the user's clang compiler has the extension, and their rust compiler does not), then the kernel will behave the same as it would if all functions or no functions have the 3-bit arity extension. If the kernel is hot-patched with the register hardening scheme in https://lkml.org/lkml/2024/9/27/982, then only the functions that have the 3-bit arity extension will benefit from the register hardening scheme. |
I had a quick look (sorry for not getting to this earlier, was traveling around Vietnam for a few weeks). Looks good to me. Especially the trick of encoding the arity into the MOV32ri register seems very neat from a compatibility standpoint. So if I understand this correctly, the only compatibility conflict this introduces is with the kernel's handling of rewriting the kCFI function prologue, which can be checked with My only concern for the current PR was that the change might be incompatible with the handling on the kernel side (i.e., that the kernel's rewrite code assumed EAX rather than any other value in the reg field), which could fail older kernel builds with a newer toolchain. But after a quick look it seems like the decoding doesn't create conflict (see https://github.com/torvalds/linux/blob/c4b9570cfb63501638db720f3bee9f6dfd044b82/arch/x86/kernel/alternative.c#L1112). And AFAIK there's no other users of kCFI besides the Linux kernel (but please correct me if I'm wrong). I do see some initial work on supporting kCFI in FreeBSD https://reviews.freebsd.org/D46193, but this also seems compatible to me too. So in short, all seems ok to me. |
Overall this looks fine to me, but I was hoping to get an answer to my question about how the Kconfig detection is actually going to be implemented: #121070 (comment) |
There are firmware projects that use KCFI, but AFAIK none of them run on x86. I'm also fairly certain that only Linux performs runtime patching based on the code sequence we emit, so I'm not particularly concerned about breaking other KCFI users with this change. |
73b6f63
to
4fb9709
Compare
@sirmc I shared this concern with Peter Zijlstra (who authored the Linux patches that will use this PR) and here is his response: "This is correct; decode_preamble_hash() currently assumes the 0xb8 opcode. This is of course trivial to fix. Also, I think we'll need a compiler flag to enable this option (see next point), and since old kernel on new compiler will not set this flag, no problem."
@samitolvanen I also shared this concern with Peter, and he said: "Yeah, so Kconfig will have to detect the compiler option is available for both C and Rust, and only select to use it when all (selected) languages support it. So if Rust is not enabled, we don't care if it supports the feature, and we can use it if clang supports it. But if Rust is enabled, both compilers need the feature present." I just updated the PR with a new clang flag |
If we can control it with a flag, then that sounds good to me. |
4fb9709
to
54296a3
Compare
…et's arity (i.e., the number of live-in registers) in the function's __cfi header.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for adding the command line flag. I agree that this should be sufficient to address compatibility concerns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM.
@scottconstable You don't need explicit approvals from all reviewers. Let's wait for 24 hours and land it if no objections. |
Kernel Control Flow Integrity (kCFI) is a feature that hardens indirect calls by comparing a 32-bit hash of the function pointer's type against a hash of the target function's type. If the hashes do not match, the kernel may panic (or log the hash check failure, depending on the kernel's configuration). These hashes are computed at compile time by applying the xxHash64 algorithm to each mangled canonical function (or function pointer) type, then truncating the result to 32 bits. This hash is written into each indirect-callable function header by encoding it as the 32-bit immediate operand to a `MOVri` instruction, e.g.: ``` __cfi_foo: nop nop nop nop nop nop nop nop nop nop nop movl $199571451, %eax # hash of foo's type = 0xBE537FB foo: ... ``` This PR extends x86-based kCFI with a 3-bit arity indicator encoded in the `MOVri` instruction's register (reg) field as follows: | Arity Indicator | Description | Encoding in reg field | | --------------- | --------------- | --------------- | | 0 | 0 parameters | EAX | | 1 | 1 parameter in RDI | ECX | | 2 | 2 parameters in RDI and RSI | EDX | | 3 | 3 parameters in RDI, RSI, and RDX | EBX | | 4 | 4 parameters in RDI, RSI, RDX, and RCX | ESP | | 5 | 5 parameters in RDI, RSI, RDX, RCX, and R8 | EBP | | 6 | 6 parameters in RDI, RSI, RDX, RCX, R8, and R9 | ESI | | 7 | At least one parameter may be passed on the stack | EDI | For example, if `foo` takes 3 register arguments and no stack arguments then the `MOVri` instruction in its kCFI header would instead be written as: ``` movl $199571451, %ebx # hash of foo's type = 0xBE537FB ``` This PR will benefit other CFI approaches that build on kCFI, such as FineIBT. For example, this proposed enhancement to FineIBT must be able to infer (at kernel init time) which registers are live at an indirect call target: https://lkml.org/lkml/2024/9/27/982. If the arity bits are available in the kCFI function header, then this information is trivial to infer. Note that there is another existing PR proposal that includes the 3-bit arity within the existing 32-bit immediate field, which introduces different security properties: llvm#117121.
Kernel Control Flow Integrity (kCFI) is a feature that hardens indirect calls by comparing a 32-bit hash of the function pointer's type against a hash of the target function's type. If the hashes do not match, the kernel may panic (or log the hash check failure, depending on the kernel's configuration). These hashes are computed at compile time by applying the xxHash64 algorithm to each mangled canonical function (or function pointer) type, then truncating the result to 32 bits. This hash is written into each indirect-callable function header by encoding it as the 32-bit immediate operand to a
MOVri
instruction, e.g.:This PR extends x86-based kCFI with a 3-bit arity indicator encoded in the
MOVri
instruction's register (reg) field as follows:For example, if
foo
takes 3 register arguments and no stack arguments then theMOVri
instruction in its kCFI header would instead be written as:This PR will benefit other CFI approaches that build on kCFI, such as FineIBT. For example, this proposed enhancement to FineIBT must be able to infer (at kernel init time) which registers are live at an indirect call target: https://lkml.org/lkml/2024/9/27/982. If the arity bits are available in the kCFI function header, then this information is trivial to infer.
Note that there is another existing PR proposal that includes the 3-bit arity within the existing 32-bit immediate field, which introduces different security properties: #117121.