Skip to content

Commit f95a67d

Browse files
scottconstabletopperc
authored andcommitted
[X86] Add RET-hardening Support to mitigate Load Value Injection (LVI)
Adding a pass that replaces every ret instruction with the sequence: pop <scratch-reg> lfence jmp *<scratch-reg> where <scratch-reg> is some available scratch register, according to the calling convention of the function being mitigated. Differential Revision: https://reviews.llvm.org/D75935
1 parent ba87430 commit f95a67d

File tree

7 files changed

+219
-0
lines changed

7 files changed

+219
-0
lines changed

llvm/lib/Target/X86/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ set(sources
5252
X86InstrInfo.cpp
5353
X86EvexToVex.cpp
5454
X86LegalizerInfo.cpp
55+
X86LoadValueInjectionRetHardening.cpp
5556
X86MCInstLower.cpp
5657
X86MachineFunctionInfo.cpp
5758
X86MacroFusion.cpp

llvm/lib/Target/X86/X86.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ InstructionSelector *createX86InstructionSelector(const X86TargetMachine &TM,
142142
X86Subtarget &,
143143
X86RegisterBankInfo &);
144144

145+
FunctionPass *createX86LoadValueInjectionRetHardeningPass();
145146
FunctionPass *createX86SpeculativeLoadHardeningPass();
146147

147148
void initializeEvexToVexInstPassPass(PassRegistry &);
@@ -158,6 +159,7 @@ void initializeX86DomainReassignmentPass(PassRegistry &);
158159
void initializeX86ExecutionDomainFixPass(PassRegistry &);
159160
void initializeX86ExpandPseudoPass(PassRegistry &);
160161
void initializeX86FlagsCopyLoweringPassPass(PassRegistry &);
162+
void initializeX86LoadValueInjectionRetHardeningPassPass(PassRegistry &);
161163
void initializeX86OptimizeLEAPassPass(PassRegistry &);
162164
void initializeX86PartialReductionPass(PassRegistry &);
163165
void initializeX86SpeculativeLoadHardeningPassPass(PassRegistry &);
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
//===-- X86LoadValueInjectionRetHardening.cpp - LVI RET hardening for x86 --==//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
///
9+
/// Description: Replaces every `ret` instruction with the sequence:
10+
/// ```
11+
/// pop <scratch-reg>
12+
/// lfence
13+
/// jmp *<scratch-reg>
14+
/// ```
15+
/// where `<scratch-reg>` is some available scratch register, according to the
16+
/// calling convention of the function being mitigated.
17+
///
18+
//===----------------------------------------------------------------------===//
19+
20+
#include "X86.h"
21+
#include "X86InstrBuilder.h"
22+
#include "X86Subtarget.h"
23+
#include "llvm/ADT/Statistic.h"
24+
#include "llvm/CodeGen/MachineBasicBlock.h"
25+
#include "llvm/CodeGen/MachineFunction.h"
26+
#include "llvm/CodeGen/MachineFunctionPass.h"
27+
#include "llvm/CodeGen/MachineInstrBuilder.h"
28+
#include "llvm/IR/Function.h"
29+
#include "llvm/Support/Debug.h"
30+
#include <bitset>
31+
32+
using namespace llvm;
33+
34+
#define PASS_KEY "x86-lvi-ret"
35+
#define DEBUG_TYPE PASS_KEY
36+
37+
STATISTIC(NumFences, "Number of LFENCEs inserted for LVI mitigation");
38+
STATISTIC(NumFunctionsConsidered, "Number of functions analyzed");
39+
STATISTIC(NumFunctionsMitigated, "Number of functions for which mitigations "
40+
"were deployed");
41+
42+
namespace {
43+
44+
class X86LoadValueInjectionRetHardeningPass : public MachineFunctionPass {
45+
public:
46+
X86LoadValueInjectionRetHardeningPass() : MachineFunctionPass(ID) {}
47+
StringRef getPassName() const override {
48+
return "X86 Load Value Injection (LVI) Ret-Hardening";
49+
}
50+
bool runOnMachineFunction(MachineFunction &MF) override;
51+
52+
static char ID;
53+
};
54+
55+
} // end anonymous namespace
56+
57+
char X86LoadValueInjectionRetHardeningPass::ID = 0;
58+
59+
bool X86LoadValueInjectionRetHardeningPass::runOnMachineFunction(
60+
MachineFunction &MF) {
61+
LLVM_DEBUG(dbgs() << "***** " << getPassName() << " : " << MF.getName()
62+
<< " *****\n");
63+
const X86Subtarget *Subtarget = &MF.getSubtarget<X86Subtarget>();
64+
if (!Subtarget->useLVIControlFlowIntegrity() || !Subtarget->is64Bit())
65+
return false; // FIXME: support 32-bit
66+
67+
// Don't skip functions with the "optnone" attr but participate in opt-bisect.
68+
const Function &F = MF.getFunction();
69+
if (!F.hasOptNone() && skipFunction(F))
70+
return false;
71+
72+
++NumFunctionsConsidered;
73+
const X86RegisterInfo *TRI = Subtarget->getRegisterInfo();
74+
const X86InstrInfo *TII = Subtarget->getInstrInfo();
75+
unsigned ClobberReg = X86::NoRegister;
76+
std::bitset<X86::NUM_TARGET_REGS> UnclobberableGR64s;
77+
UnclobberableGR64s.set(X86::RSP); // can't clobber stack pointer
78+
UnclobberableGR64s.set(X86::RIP); // can't clobber instruction pointer
79+
UnclobberableGR64s.set(X86::RAX); // used for function return
80+
UnclobberableGR64s.set(X86::RDX); // used for function return
81+
82+
// We can clobber any register allowed by the function's calling convention.
83+
for (const MCPhysReg *PR = TRI->getCalleeSavedRegs(&MF); auto Reg = *PR; ++PR)
84+
UnclobberableGR64s.set(Reg);
85+
for (auto &Reg : X86::GR64RegClass) {
86+
if (!UnclobberableGR64s.test(Reg)) {
87+
ClobberReg = Reg;
88+
break;
89+
}
90+
}
91+
92+
if (ClobberReg != X86::NoRegister) {
93+
LLVM_DEBUG(dbgs() << "Selected register "
94+
<< Subtarget->getRegisterInfo()->getRegAsmName(ClobberReg)
95+
<< " to clobber\n");
96+
} else {
97+
LLVM_DEBUG(dbgs() << "Could not find a register to clobber\n");
98+
}
99+
100+
bool Modified = false;
101+
for (auto &MBB : MF) {
102+
MachineInstr &MI = MBB.back();
103+
if (MI.getOpcode() != X86::RETQ)
104+
continue;
105+
106+
if (ClobberReg != X86::NoRegister) {
107+
MBB.erase_instr(&MI);
108+
BuildMI(MBB, MBB.end(), DebugLoc(), TII->get(X86::POP64r))
109+
.addReg(ClobberReg, RegState::Define)
110+
.setMIFlag(MachineInstr::FrameDestroy);
111+
BuildMI(MBB, MBB.end(), DebugLoc(), TII->get(X86::LFENCE));
112+
BuildMI(MBB, MBB.end(), DebugLoc(), TII->get(X86::JMP64r))
113+
.addReg(ClobberReg);
114+
} else {
115+
// In case there is no available scratch register, we can still read from
116+
// RSP to assert that RSP points to a valid page. The write to RSP is
117+
// also helpful because it verifies that the stack's write permissions
118+
// are intact.
119+
MachineInstr *Fence = BuildMI(MBB, MI, DebugLoc(), TII->get(X86::LFENCE));
120+
addRegOffset(BuildMI(MBB, Fence, DebugLoc(), TII->get(X86::SHL64mi)),
121+
X86::RSP, false, 0)
122+
.addImm(0)
123+
->addRegisterDead(X86::EFLAGS, TRI);
124+
}
125+
126+
++NumFences;
127+
Modified = true;
128+
}
129+
130+
if (Modified)
131+
++NumFunctionsMitigated;
132+
return Modified;
133+
}
134+
135+
INITIALIZE_PASS(X86LoadValueInjectionRetHardeningPass, PASS_KEY,
136+
"X86 LVI ret hardener", false, false)
137+
138+
FunctionPass *llvm::createX86LoadValueInjectionRetHardeningPass() {
139+
return new X86LoadValueInjectionRetHardeningPass();
140+
}

llvm/lib/Target/X86/X86TargetMachine.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ extern "C" LLVM_EXTERNAL_VISIBILITY void LLVMInitializeX86Target() {
8383
initializeX86SpeculativeLoadHardeningPassPass(PR);
8484
initializeX86FlagsCopyLoweringPassPass(PR);
8585
initializeX86CondBrFoldingPassPass(PR);
86+
initializeX86LoadValueInjectionRetHardeningPassPass(PR);
8687
initializeX86OptimizeLEAPassPass(PR);
8788
initializeX86PartialReductionPass(PR);
8889
}
@@ -540,6 +541,7 @@ void X86PassConfig::addPreEmitPass2() {
540541
// Identify valid longjmp targets for Windows Control Flow Guard.
541542
if (TT.isOSWindows())
542543
addPass(createCFGuardLongjmpPass());
544+
addPass(createX86LoadValueInjectionRetHardeningPass());
543545
}
544546

545547
std::unique_ptr<CSEConfigBase> X86PassConfig::getCSEConfig() const {

llvm/test/CodeGen/X86/O0-pipeline.ll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
; CHECK-NEXT: Live DEBUG_VALUE analysis
7575
; CHECK-NEXT: X86 Indirect Thunks
7676
; CHECK-NEXT: Check CFA info and insert CFI instructions if needed
77+
; CHECK-NEXT: X86 Load Value Injection (LVI) Ret-Hardening
7778
; CHECK-NEXT: Lazy Machine Block Frequency Analysis
7879
; CHECK-NEXT: Machine Optimization Remark Emitter
7980
; CHECK-NEXT: X86 Assembly Printer

llvm/test/CodeGen/X86/O3-pipeline.ll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@
184184
; CHECK-NEXT: Live DEBUG_VALUE analysis
185185
; CHECK-NEXT: X86 Indirect Thunks
186186
; CHECK-NEXT: Check CFA info and insert CFI instructions if needed
187+
; CHECK-NEXT: X86 Load Value Injection (LVI) Ret-Hardening
187188
; CHECK-NEXT: Lazy Machine Block Frequency Analysis
188189
; CHECK-NEXT: Machine Optimization Remark Emitter
189190
; CHECK-NEXT: X86 Assembly Printer
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
; RUN: llc -verify-machineinstrs -mtriple=x86_64-unknown < %s | FileCheck %s
2+
3+
define dso_local void @one_instruction() #0 {
4+
; CHECK-LABEL: one_instruction:
5+
entry:
6+
ret void
7+
; CHECK-NOT: retq
8+
; CHECK: popq %[[x:[^ ]*]]
9+
; CHECK-NEXT: lfence
10+
; CHECK-NEXT: jmpq *%[[x]]
11+
}
12+
13+
; Function Attrs: noinline nounwind optnone uwtable
14+
define dso_local i32 @ordinary_function(i32 %x, i32 %y) #0 {
15+
; CHECK-LABEL: ordinary_function:
16+
entry:
17+
%x.addr = alloca i32, align 4
18+
%y.addr = alloca i32, align 4
19+
store i32 %x, i32* %x.addr, align 4
20+
store i32 %y, i32* %y.addr, align 4
21+
%0 = load i32, i32* %x.addr, align 4
22+
%1 = load i32, i32* %y.addr, align 4
23+
%add = add nsw i32 %0, %1
24+
ret i32 %add
25+
; CHECK-NOT: retq
26+
; CHECK: popq %[[x:[^ ]*]]
27+
; CHECK-NEXT: lfence
28+
; CHECK-NEXT: jmpq *%[[x]]
29+
}
30+
31+
; Function Attrs: noinline nounwind optnone uwtable
32+
define dso_local i32 @no_caller_saved_registers_function(i32 %x, i32 %y) #1 {
33+
; CHECK-LABEL: no_caller_saved_registers_function:
34+
entry:
35+
%x.addr = alloca i32, align 4
36+
%y.addr = alloca i32, align 4
37+
store i32 %x, i32* %x.addr, align 4
38+
store i32 %y, i32* %y.addr, align 4
39+
%0 = load i32, i32* %x.addr, align 4
40+
%1 = load i32, i32* %y.addr, align 4
41+
%add = add nsw i32 %0, %1
42+
ret i32 %add
43+
; CHECK-NOT: retq
44+
; CHECK: shlq $0, (%{{[^ ]*}})
45+
; CHECK-NEXT: lfence
46+
; CHECK-NEXT: retq
47+
}
48+
49+
; Function Attrs: noinline nounwind optnone uwtable
50+
define dso_local preserve_mostcc void @preserve_most() #0 {
51+
; CHECK-LABEL: preserve_most:
52+
entry:
53+
ret void
54+
; CHECK-NOT: retq
55+
; CHECK: popq %r11
56+
; CHECK-NEXT: lfence
57+
; CHECK-NEXT: jmpq *%r11
58+
}
59+
60+
; Function Attrs: noinline nounwind optnone uwtable
61+
define dso_local preserve_allcc void @preserve_all() #0 {
62+
; CHECK-LABEL: preserve_all:
63+
entry:
64+
ret void
65+
; CHECK-NOT: retq
66+
; CHECK: popq %r11
67+
; CHECK-NEXT: lfence
68+
; CHECK-NEXT: jmpq *%r11
69+
}
70+
71+
attributes #0 = { "target-features"="+lvi-cfi" }
72+
attributes #1 = { "no_caller_saved_registers" "target-features"="+lvi-cfi" }

0 commit comments

Comments
 (0)