Skip to content

Commit 24d83f3

Browse files
committed
[BOLT] Gadget scanner: detect untrusted LR before tail call
Implement the detection of tail calls performed with untrusted link register, which violates the assumption made on entry to every function. Unlike other pauth gadgets, this one involves some amount of guessing which branch instructions should be checked as tail calls.
1 parent 49f2679 commit 24d83f3

File tree

4 files changed

+730
-40
lines changed

4 files changed

+730
-40
lines changed

bolt/lib/Passes/PAuthGadgetScanner.cpp

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -659,8 +659,9 @@ class DataflowSrcSafetyAnalysis
659659
//
660660
// Then, a function can be split into a number of disjoint contiguous sequences
661661
// of instructions without labels in between. These sequences can be processed
662-
// the same way basic blocks are processed by data-flow analysis, assuming
663-
// pessimistically that all registers are unsafe at the start of each sequence.
662+
// the same way basic blocks are processed by data-flow analysis, with the same
663+
// pessimistic estimation of the initial state at the start of each sequence
664+
// (except the first instruction of the function).
664665
class CFGUnawareSrcSafetyAnalysis : public SrcSafetyAnalysis {
665666
BinaryFunction &BF;
666667
MCPlusBuilder::AllocatorIdTy AllocId;
@@ -671,6 +672,30 @@ class CFGUnawareSrcSafetyAnalysis : public SrcSafetyAnalysis {
671672
BC.MIB->removeAnnotation(I.second, StateAnnotationIndex);
672673
}
673674

675+
/// Compute a reasonably pessimistic estimation of the register state when
676+
/// the previous instruction is not known for sure. Take the set of registers
677+
/// which are trusted at function entry and remove all registers that can be
678+
/// clobbered inside this function.
679+
SrcState computePessimisticState(BinaryFunction &BF) {
680+
BitVector ClobberedRegs(NumRegs);
681+
for (auto &I : BF.instrs()) {
682+
MCInst &Inst = I.second;
683+
BC.MIB->getClobberedRegs(Inst, ClobberedRegs);
684+
685+
// If this is a call instruction, no register is safe anymore, unless
686+
// it is a tail call. Ignore tail calls for the purpose of estimating the
687+
// worst-case scenario, assuming no instructions are executed in the
688+
// caller after this point anyway.
689+
if (BC.MIB->isCall(Inst) && !BC.MIB->isTailCall(Inst))
690+
ClobberedRegs.set();
691+
}
692+
693+
SrcState S = createEntryState();
694+
S.SafeToDerefRegs.reset(ClobberedRegs);
695+
S.TrustedRegs.reset(ClobberedRegs);
696+
return S;
697+
}
698+
674699
public:
675700
CFGUnawareSrcSafetyAnalysis(BinaryFunction &BF,
676701
MCPlusBuilder::AllocatorIdTy AllocId,
@@ -681,6 +706,7 @@ class CFGUnawareSrcSafetyAnalysis : public SrcSafetyAnalysis {
681706
}
682707

683708
void run() override {
709+
const SrcState DefaultState = computePessimisticState(BF);
684710
SrcState S = createEntryState();
685711
for (auto &I : BF.instrs()) {
686712
MCInst &Inst = I.second;
@@ -695,7 +721,7 @@ class CFGUnawareSrcSafetyAnalysis : public SrcSafetyAnalysis {
695721
LLVM_DEBUG({
696722
traceInst(BC, "Due to label, resetting the state before", Inst);
697723
});
698-
S = createUnsafeState();
724+
S = DefaultState;
699725
}
700726

701727
// Check if we need to remove an old annotation (this is the case if
@@ -1240,6 +1266,83 @@ shouldReportReturnGadget(const BinaryContext &BC, const MCInstReference &Inst,
12401266
return make_gadget_report(RetKind, Inst, *RetReg);
12411267
}
12421268

1269+
/// While BOLT already marks some of the branch instructions as tail calls,
1270+
/// this function tries to improve the coverage by including less obvious cases
1271+
/// when it is possible to do without introducing too many false positives.
1272+
static bool shouldAnalyzeTailCallInst(const BinaryContext &BC,
1273+
const BinaryFunction &BF,
1274+
const MCInstReference &Inst) {
1275+
// Some BC.MIB->isXYZ(Inst) methods simply delegate to MCInstrDesc::isXYZ()
1276+
// (such as isBranch at the time of writing this comment), some don't (such
1277+
// as isCall). For that reason, call MCInstrDesc's methods explicitly when
1278+
// it is important.
1279+
const MCInstrDesc &Desc =
1280+
BC.MII->get(static_cast<const MCInst &>(Inst).getOpcode());
1281+
// Tail call should be a branch (but not necessarily an indirect one).
1282+
if (!Desc.isBranch())
1283+
return false;
1284+
1285+
// Always analyze the branches already marked as tail calls by BOLT.
1286+
if (BC.MIB->isTailCall(Inst))
1287+
return true;
1288+
1289+
// Try to also check the branches marked as "UNKNOWN CONTROL FLOW" - the
1290+
// below is a simplified condition from BinaryContext::printInstruction.
1291+
bool IsUnknownControlFlow =
1292+
BC.MIB->isIndirectBranch(Inst) && !BC.MIB->getJumpTable(Inst);
1293+
1294+
if (BF.hasCFG() && IsUnknownControlFlow)
1295+
return true;
1296+
1297+
return false;
1298+
}
1299+
1300+
static std::optional<PartialReport<MCPhysReg>>
1301+
shouldReportUnsafeTailCall(const BinaryContext &BC, const BinaryFunction &BF,
1302+
const MCInstReference &Inst, const SrcState &S) {
1303+
static const GadgetKind UntrustedLRKind(
1304+
"untrusted link register found before tail call");
1305+
1306+
if (!shouldAnalyzeTailCallInst(BC, BF, Inst))
1307+
return std::nullopt;
1308+
1309+
// Not only the set of registers returned by getTrustedLiveInRegs() can be
1310+
// seen as a reasonable target-independent _approximation_ of "the LR", these
1311+
// are *exactly* those registers used by SrcSafetyAnalysis to initialize the
1312+
// set of trusted registers on function entry.
1313+
// Thus, this function basically checks that the precondition expected to be
1314+
// imposed by a function call instruction (which is hardcoded into the target-
1315+
// specific getTrustedLiveInRegs() function) is also respected on tail calls.
1316+
SmallVector<MCPhysReg> RegsToCheck = BC.MIB->getTrustedLiveInRegs();
1317+
LLVM_DEBUG({
1318+
traceInst(BC, "Found tail call inst", Inst);
1319+
traceRegMask(BC, "Trusted regs", S.TrustedRegs);
1320+
});
1321+
1322+
// In musl on AArch64, the _start function sets LR to zero and calls the next
1323+
// stage initialization function at the end, something along these lines:
1324+
//
1325+
// _start:
1326+
// mov x30, #0
1327+
// ; ... other initialization ...
1328+
// b _start_c ; performs "exit" system call at some point
1329+
//
1330+
// As this would produce a false positive for every executable linked with
1331+
// such libc, ignore tail calls performed by ELF entry function.
1332+
if (BC.StartFunctionAddress &&
1333+
*BC.StartFunctionAddress == Inst.getFunction()->getAddress()) {
1334+
LLVM_DEBUG({ dbgs() << " Skipping tail call in ELF entry function.\n"; });
1335+
return std::nullopt;
1336+
}
1337+
1338+
// Returns at most one report per instruction - this is probably OK...
1339+
for (auto Reg : RegsToCheck)
1340+
if (!S.TrustedRegs[Reg])
1341+
return make_gadget_report(UntrustedLRKind, Inst, Reg);
1342+
1343+
return std::nullopt;
1344+
}
1345+
12431346
static std::optional<PartialReport<MCPhysReg>>
12441347
shouldReportCallGadget(const BinaryContext &BC, const MCInstReference &Inst,
12451348
const SrcState &S) {
@@ -1407,6 +1510,9 @@ void FunctionAnalysisContext::findUnsafeUses(
14071510
if (PacRetGadgetsOnly)
14081511
return;
14091512

1513+
if (auto Report = shouldReportUnsafeTailCall(BC, BF, Inst, S))
1514+
Reports.push_back(*Report);
1515+
14101516
if (auto Report = shouldReportCallGadget(BC, Inst, S))
14111517
Reports.push_back(*Report);
14121518
if (auto Report = shouldReportSigningOracle(BC, Inst, S))

bolt/test/binary-analysis/AArch64/gs-pacret-autiasp.s

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -229,20 +229,33 @@ f_unreachable_instruction:
229229
ret
230230
.size f_unreachable_instruction, .-f_unreachable_instruction
231231

232-
// Expected false positive: without CFG, the state is reset to all-unsafe
233-
// after an unconditional branch.
234-
235-
.globl state_is_reset_after_indirect_branch_nocfg
236-
.type state_is_reset_after_indirect_branch_nocfg,@function
237-
state_is_reset_after_indirect_branch_nocfg:
238-
// CHECK-LABEL: GS-PAUTH: non-protected ret found in function state_is_reset_after_indirect_branch_nocfg, at address
239-
// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: ret
232+
// Without CFG, the state is reset at labels, assuming every register that can
233+
// be clobbered in the function was actually clobbered.
234+
235+
.globl lr_untouched_nocfg
236+
.type lr_untouched_nocfg,@function
237+
lr_untouched_nocfg:
238+
// CHECK-NOT: lr_untouched_nocfg
239+
adr x2, 1f
240+
br x2
241+
1:
242+
ret
243+
.size lr_untouched_nocfg, .-lr_untouched_nocfg
244+
245+
.globl lr_clobbered_nocfg
246+
.type lr_clobbered_nocfg,@function
247+
lr_clobbered_nocfg:
248+
// CHECK-LABEL: GS-PAUTH: non-protected ret found in function lr_clobbered_nocfg, at address
249+
// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: ret
240250
// CHECK-NEXT: The 0 instructions that write to the affected registers after any authentication are:
241251
adr x2, 1f
242252
br x2
243253
1:
254+
b 2f
255+
bl g // never executed, but affects the expected worst-case scenario
256+
2:
244257
ret
245-
.size state_is_reset_after_indirect_branch_nocfg, .-state_is_reset_after_indirect_branch_nocfg
258+
.size lr_clobbered_nocfg, .-lr_clobbered_nocfg
246259

247260
/// Now do a basic sanity check on every different Authentication instruction:
248261

bolt/test/binary-analysis/AArch64/gs-pauth-debug-output.s

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,8 @@ nocfg:
199199
// CHECK-NEXT: SrcSafetyAnalysis::ComputeNext( br x0, src-state<SafeToDerefRegs: LR W0 W30 X0 W0_HI W30_HI , TrustedRegs: LR W0 W30 X0 W0_HI W30_HI , Insts: >)
200200
// CHECK-NEXT: .. result: (src-state<SafeToDerefRegs: LR W0 W30 X0 W0_HI W30_HI , TrustedRegs: LR W0 W30 X0 W0_HI W30_HI , Insts: >)
201201
// CHECK-NEXT: Due to label, resetting the state before: 00000000: ret # Offset: 8
202-
// CHECK-NEXT: SrcSafetyAnalysis::ComputeNext( ret x30, src-state<SafeToDerefRegs: , TrustedRegs: , Insts: >)
203-
// CHECK-NEXT: .. result: (src-state<SafeToDerefRegs: , TrustedRegs: , Insts: >)
202+
// CHECK-NEXT: SrcSafetyAnalysis::ComputeNext( ret x30, src-state<SafeToDerefRegs: LR W30 W30_HI , TrustedRegs: LR W30 W30_HI , Insts: >)
203+
// CHECK-NEXT: .. result: (src-state<SafeToDerefRegs: LR W30 W30_HI , TrustedRegs: LR W30 W30_HI , Insts: >)
204204
// CHECK-NEXT: After src register safety analysis:
205205
// CHECK-NEXT: Binary Function "nocfg" {
206206
// CHECK-NEXT: Number : 3
@@ -224,32 +224,6 @@ nocfg:
224224
// CHECK-NEXT: Found RET inst: 00000000: ret # Offset: 8 # CFGUnawareSrcSafetyAnalysis: src-state<SafeToDerefRegs: BitVector, TrustedRegs: BitVector, Insts: >
225225
// CHECK-NEXT: RetReg: LR
226226
// CHECK-NEXT: SafeToDerefRegs:
227-
// CHECK-EMPTY:
228-
// CHECK-NEXT: Running detailed src register safety analysis...
229-
// CHECK-NEXT: SrcSafetyAnalysis::ComputeNext( adr x0, __ENTRY_nocfg@0x[[ENTRY_ADDR]], src-state<SafeToDerefRegs: LR W30 W30_HI , TrustedRegs: LR W30 W30_HI , Insts: [0]()>)
230-
// CHECK-NEXT: .. result: (src-state<SafeToDerefRegs: LR W0 W30 X0 W0_HI W30_HI , TrustedRegs: LR W0 W30 X0 W0_HI W30_HI , Insts: [0]()>)
231-
// CHECK-NEXT: SrcSafetyAnalysis::ComputeNext( br x0, src-state<SafeToDerefRegs: LR W0 W30 X0 W0_HI W30_HI , TrustedRegs: LR W0 W30 X0 W0_HI W30_HI , Insts: [0]()>)
232-
// CHECK-NEXT: .. result: (src-state<SafeToDerefRegs: LR W0 W30 X0 W0_HI W30_HI , TrustedRegs: LR W0 W30 X0 W0_HI W30_HI , Insts: [0]()>)
233-
// CHECK-NEXT: Due to label, resetting the state before: 00000000: ret # Offset: 8
234-
// CHECK-NEXT: SrcSafetyAnalysis::ComputeNext( ret x30, src-state<SafeToDerefRegs: , TrustedRegs: , Insts: [0]()>)
235-
// CHECK-NEXT: .. result: (src-state<SafeToDerefRegs: , TrustedRegs: , Insts: [0]()>)
236-
// CHECK-NEXT: After detailed src register safety analysis:
237-
// CHECK-NEXT: Binary Function "nocfg" {
238-
// CHECK-NEXT: Number : 3
239-
// ...
240-
// CHECK: Secondary Entry Points : __ENTRY_nocfg@0x[[ENTRY_ADDR]]
241-
// CHECK-NEXT: }
242-
// CHECK-NEXT: .{{[A-Za-z0-9]+}}:
243-
// CHECK-NEXT: 00000000: adr x0, __ENTRY_nocfg@0x[[ENTRY_ADDR]] # CFGUnawareSrcSafetyAnalysis: src-state<SafeToDerefRegs: BitVector, TrustedRegs: BitVector, Insts: [0]()>
244-
// CHECK-NEXT: 00000004: br x0 # UNKNOWN CONTROL FLOW # Offset: 4 # CFGUnawareSrcSafetyAnalysis: src-state<SafeToDerefRegs: BitVector, TrustedRegs: BitVector, Insts: [0]()>
245-
// CHECK-NEXT: __ENTRY_nocfg@0x[[ENTRY_ADDR]] (Entry Point):
246-
// CHECK-NEXT: .{{[A-Za-z0-9]+}}:
247-
// CHECK-NEXT: 00000008: ret # Offset: 8 # CFGUnawareSrcSafetyAnalysis: src-state<SafeToDerefRegs: BitVector, TrustedRegs: BitVector, Insts: [0]()>
248-
// CHECK-NEXT: DWARF CFI Instructions:
249-
// CHECK-NEXT: <empty>
250-
// CHECK-NEXT: End of Function "nocfg"
251-
// CHECK-EMPTY:
252-
// CHECK-NEXT: Attaching clobbering info to: 00000000: ret # Offset: 8 # CFGUnawareSrcSafetyAnalysis: src-state<SafeToDerefRegs: BitVector, TrustedRegs: BitVector, Insts: [0]()>
253227

254228
.globl auth_oracle
255229
.type auth_oracle,@function

0 commit comments

Comments
 (0)