[llvm] [X86] Match ENDBR immediates in little-endian order (PR #208754)
via llvm-commits
llvm-commits at lists.llvm.org
Tue Aug 25 07:39:33 PDT 2026
https://github.com/BIGSMATER updated https://github.com/llvm/llvm-project/pull/208754
>From 430e72bda3a7a1c036c4e76499013cb8cc404e13 Mon Sep 17 00:00:00 2001
From: BIGSMATER <1025076763 at qq.com>
Date: Fri, 10 Jul 2026 14:09:22 +0800
Subject: [PATCH] [X86] Match ENDBR immediates in little-endian order
The CET immediate scrubber checks constants that would place ENDBR32 or ENDBR64 byte sequences into executable code. Match those sequences in the order emitted for little-endian immediate operands instead of the display-order integer spelling.
Treat unsupported immediate widths as non-matches and assert the ISD::Constant type invariant. Update the X86 CET regression test to cover ENDBR32 and optional-prefix encodings.
---
llvm/lib/Target/X86/X86ISelDAGToDAG.cpp | 65 ++++++++++++-------
.../test/CodeGen/X86/cet_endbr_imm_enhance.ll | 59 ++++++++---------
2 files changed, 68 insertions(+), 56 deletions(-)
diff --git a/llvm/lib/Target/X86/X86ISelDAGToDAG.cpp b/llvm/lib/Target/X86/X86ISelDAGToDAG.cpp
index c3fc43c2bc6db..5fd070bb3e743 100644
--- a/llvm/lib/Target/X86/X86ISelDAGToDAG.cpp
+++ b/llvm/lib/Target/X86/X86ISelDAGToDAG.cpp
@@ -933,22 +933,31 @@ static bool isCalleeLoad(SDValue Callee, SDValue &Chain, bool HasCallSeq) {
}
}
-static bool isEndbrImm64(uint64_t Imm) {
-// There may be some other prefix bytes between 0xF3 and 0x0F1EFA.
-// i.g: 0xF3660F1EFA, 0xF3670F1EFA
- if ((Imm & 0x00FFFFFF) != 0x0F1EFA)
+static bool isEndbrImm(uint64_t Imm, unsigned BitWidth) {
+ if (BitWidth > 64 || BitWidth % 8 != 0)
return false;
- uint8_t OptionalPrefixBytes [] = {0x26, 0x2e, 0x36, 0x3e, 0x64,
- 0x65, 0x66, 0x67, 0xf0, 0xf2};
- int i = 24; // 24bit 0x0F1EFA has matched
- while (i < 64) {
- uint8_t Byte = (Imm >> i) & 0xFF;
- if (Byte == 0xF3)
+ const unsigned NumBytes = BitWidth / 8;
+ if (NumBytes < 4)
+ return false;
+
+ const uint8_t OptionalPrefixBytes[] = {0x26, 0x2e, 0x36, 0x3e, 0x64,
+ 0x65, 0x66, 0x67, 0xf0, 0xf2};
+ uint8_t Bytes[8];
+ for (unsigned I = 0; I != NumBytes; ++I)
+ Bytes[I] = (Imm >> (I * 8)) & 0xFF;
+
+ for (unsigned I = 0; I + 3 < NumBytes; ++I) {
+ if (Bytes[I] != 0xf3)
+ continue;
+
+ unsigned J = I + 1;
+ while (J < NumBytes && llvm::is_contained(OptionalPrefixBytes, Bytes[J]))
+ ++J;
+
+ if (J + 2 < NumBytes && Bytes[J] == 0x0f && Bytes[J + 1] == 0x1e &&
+ (Bytes[J + 2] == 0xfa || Bytes[J + 2] == 0xfb))
return true;
- if (!llvm::is_contained(OptionalPrefixBytes, Byte))
- return false;
- i += 8;
}
return false;
@@ -969,28 +978,34 @@ void X86DAGToDAGISel::PreprocessISelDAG() {
// ENDBR32 and ENDBR64 have specific opcodes:
// ENDBR32: F3 0F 1E FB
// ENDBR64: F3 0F 1E FA
- // And we want that attackers won’t find unintended ENDBR32/64
- // opcode matches in the binary
- // Here’s an example:
+ // We want to prevent attackers from finding unintended ENDBR32/64 opcode
+ // matches in executable code. Here's an example:
// If the compiler had to generate asm for the following code:
- // a = 0xF30F1EFA
+ // a = 0xFA1E0FF3
// it could, for example, generate:
- // mov 0xF30F1EFA, dword ptr[a]
- // In such a case, the binary would include a gadget that starts
- // with a fake ENDBR64 opcode. Therefore, we split such generation
- // into multiple operations, let it not shows in the binary
+ // mov 0xFA1E0FF3, dword ptr[a]
+ // In such a case, the binary would include a gadget that starts with a
+ // fake ENDBR64 opcode. Split such constants into multiple operations so
+ // the byte sequence does not appear in executable code.
if (N->getOpcode() == ISD::Constant) {
MVT VT = N->getSimpleValueType(0);
- int64_t Imm = cast<ConstantSDNode>(N)->getSExtValue();
- int32_t EndbrImm = Subtarget->is64Bit() ? 0xF30F1EFA : 0xF30F1EFB;
- if (Imm == EndbrImm || isEndbrImm64(Imm)) {
+ assert(VT.isScalarInteger() &&
+ "ISD::Constant must have a scalar integer type");
+ if (!VT.isScalarInteger() || VT.getSizeInBits() > 64)
+ continue;
+
+ uint64_t Imm = cast<ConstantSDNode>(N)->getZExtValue();
+ if (isEndbrImm(Imm, VT.getSizeInBits())) {
// Check that the cf-protection-branch is enabled.
Metadata *CFProtectionBranch =
MF->getFunction().getParent()->getModuleFlag(
"cf-protection-branch");
if (CFProtectionBranch || IndirectBranchTracking) {
SDLoc dl(N);
- SDValue Complement = CurDAG->getConstant(~Imm, dl, VT, false, true);
+ uint64_t ComplementImm =
+ (~Imm) & maskTrailingOnes<uint64_t>(VT.getSizeInBits());
+ SDValue Complement =
+ CurDAG->getConstant(ComplementImm, dl, VT, false, true);
Complement = CurDAG->getNOT(dl, Complement, VT);
--I;
CurDAG->ReplaceAllUsesOfValueWith(SDValue(N, 0), Complement);
diff --git a/llvm/test/CodeGen/X86/cet_endbr_imm_enhance.ll b/llvm/test/CodeGen/X86/cet_endbr_imm_enhance.ll
index 98d315ad14e68..6435f66cf7549 100644
--- a/llvm/test/CodeGen/X86/cet_endbr_imm_enhance.ll
+++ b/llvm/test/CodeGen/X86/cet_endbr_imm_enhance.ll
@@ -6,20 +6,22 @@
; ENDBR32 and ENDBR64 have specific opcodes:
; ENDBR32: F3 0F 1E FB
; ENDBR64: F3 0F 1E FA
-; And we want that attackers won’t find unintended ENDBR32/64
-; opcode matches in the binary
-; Here’s an example:
+; We want to prevent attackers from finding unintended ENDBR32/64 opcode
+; matches in executable code. Here's an example:
; If the compiler had to generate asm for the following code:
-; a = 0xF30F1EFA
+; a = 0xFA1E0FF3
; it could, for example, generate:
-; mov 0xF30F1EFA, dword ptr[a]
-; In such a case, the binary would include a gadget that starts
-; with a fake ENDBR64 opcode. Therefore, we split such generation
-; into multiple operations, let it not shows in the binary.
+; mov 0xFA1E0FF3, dword ptr[a]
+; In such a case, the binary would include a gadget that starts with a fake
+; ENDBR64 opcode. Split such constants into multiple operations so the byte
+; sequence does not appear in executable code.
-; 0xF30F1EFA == -217112838 ~0xF30F1EFA == 217112837 (0xCF0E105)
-; 0x000123F32E0F1EFA == 321002333478650
-; ~0x000123F32E0F1EFA == -321002333478651 (0XFFFEDC0CD1F0E105)
+; 0xFA1E0FF3 == -98693133 ~0xFA1E0FF3 == 98693132 (0x5E1F00C)
+; 0xFB1E0FF3 == -81915917 ~0xFB1E0FF3 == 81915916 (0x4E1F00C)
+; 0x000123FA1E0FF300 == 321032129868544
+; ~0x000123FA1E0FF300 == -321032129868545 (0xFFFEDC05E1F00CFF)
+; 0x0000FA1E0F6766F3 == 275007014397683
+; ~0x0000FA1E0F6766F3 == -275007014397684 (0xFFFF05E1F098990C)
; test for MOV64ri
define dso_local i64 @foo(ptr %azx) #0 {
@@ -27,7 +29,7 @@ define dso_local i64 @foo(ptr %azx) #0 {
; CHECK: # %bb.0: # %entry
; CHECK-NEXT: endbr64
; CHECK-NEXT: movq %rdi, -{{[0-9]+}}(%rsp)
-; CHECK-NEXT: movabsq $-321002333478651, %rax # imm = 0xFFFEDC0CD1F0E105
+; CHECK-NEXT: movabsq $-321032129868545, %rax # imm = 0xFFFEDC05E1F00CFF
; CHECK-NEXT: notq %rax
; CHECK-NEXT: andq %rax, (%rdi)
; CHECK-NEXT: movq -{{[0-9]+}}(%rsp), %rax
@@ -38,7 +40,7 @@ entry:
store ptr %azx, ptr %azx.addr, align 8
%0 = load ptr, ptr %azx.addr, align 8
%1 = load i64, ptr %0, align 8
- %and = and i64 %1, 321002333478650
+ %and = and i64 %1, 321032129868544
%2 = load ptr, ptr %azx.addr, align 8
store i64 %and, ptr %2, align 8
%3 = load ptr, ptr %azx.addr, align 8
@@ -46,42 +48,36 @@ entry:
ret i64 %4
}
- at bzx = dso_local local_unnamed_addr global i32 -217112837, align 4
-
; test for AND32ri
-define dso_local i32 @foo2() local_unnamed_addr #0 {
+define dso_local i32 @foo2(i32 %x) local_unnamed_addr #0 {
; CHECK-LABEL: foo2:
; CHECK: # %bb.0: # %entry
; CHECK-NEXT: endbr64
-; CHECK-NEXT: movl bzx(%rip), %ecx
-; CHECK-NEXT: addl %ecx, %ecx
-; CHECK-NEXT: movl $217112837, %eax # imm = 0xCF0E105
+; CHECK-NEXT: movl $98693132, %eax # imm = 0x5E1F00C
; CHECK-NEXT: notl %eax
-; CHECK-NEXT: andl %ecx, %eax
+; CHECK-NEXT: andl %edi, %eax
; CHECK-NEXT: retq
entry:
- %0 = load i32, ptr @bzx, align 4
- %mul = shl nsw i32 %0, 1
- %and = and i32 %mul, -217112838
+ %and = and i32 %x, -98693133
ret i32 %and
}
- at czx = dso_local global i32 -217112837, align 4
+ at czx = dso_local global i32 0, align 4
-; test for AND32mi
+; test for ENDBR32 in AND32mi
define dso_local nonnull ptr @foo3() local_unnamed_addr #0 {
; CHECK-LABEL: foo3:
; CHECK: # %bb.0: # %entry
; CHECK-NEXT: endbr64
-; CHECK-NEXT: movl $217112837, %eax # imm = 0xCF0E105
+; CHECK-NEXT: movl $81915916, %eax # imm = 0x4E1F00C
; CHECK-NEXT: notl %eax
; CHECK-NEXT: andl %eax, czx(%rip)
; CHECK-NEXT: movl $czx, %eax
; CHECK-NEXT: retq
entry:
%0 = load i32, ptr @czx, align 4
- %and = and i32 %0, -217112838
+ %and = and i32 %0, -81915917
store i32 %and, ptr @czx, align 4
ret ptr @czx
}
@@ -91,28 +87,29 @@ define dso_local i32 @foo4() #0 {
; CHECK-LABEL: foo4:
; CHECK: # %bb.0: # %entry
; CHECK-NEXT: endbr64
-; CHECK-NEXT: movl $217112837, %eax # imm = 0xCF0E105
+; CHECK-NEXT: movl $98693132, %eax # imm = 0x5E1F00C
; CHECK-NEXT: notl %eax
; CHECK-NEXT: movl %eax, -{{[0-9]+}}(%rsp)
; CHECK-NEXT: retq
entry:
%dzx = alloca i32, align 4
- store i32 -217112838, ptr %dzx, align 4
+ store i32 -98693133, ptr %dzx, align 4
%0 = load i32, ptr %dzx, align 4
ret i32 %0
}
+; test for optional prefixes in a 64-bit immediate
define dso_local i64 @foo5() #0 {
; CHECK-LABEL: foo5:
; CHECK: # %bb.0: # %entry
; CHECK-NEXT: endbr64
-; CHECK-NEXT: movabsq $-4077854459, %rax # imm = 0xFFFFFFFF0CF0E105
+; CHECK-NEXT: movabsq $-275007014397684, %rax # imm = 0xFFFF05E1F098990C
; CHECK-NEXT: notq %rax
; CHECK-NEXT: movq %rax, -{{[0-9]+}}(%rsp)
; CHECK-NEXT: retq
entry:
%ezx = alloca i64, align 8
- store i64 4077854458, ptr %ezx, align 8
+ store i64 275007014397683, ptr %ezx, align 8
%0 = load i64, ptr %ezx, align 8
ret i64 %0
}
More information about the llvm-commits
mailing list