Skip to content

Commit 7a5af4f

Browse files
authored
[BOLT] Gadget scanner: detect untrusted LR before tail call (#137224)
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, detection of this one involves some amount of guessing which branch instructions should be checked as tail calls.
1 parent 2e196a0 commit 7a5af4f

File tree

2 files changed

+684
-0
lines changed

2 files changed

+684
-0
lines changed

bolt/lib/Passes/PAuthGadgetScanner.cpp

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,6 +1319,90 @@ shouldReportReturnGadget(const BinaryContext &BC, const MCInstReference &Inst,
13191319
return make_gadget_report(RetKind, Inst, *RetReg);
13201320
}
13211321

1322+
/// While BOLT already marks some of the branch instructions as tail calls,
1323+
/// this function tries to detect less obvious cases, assuming false positives
1324+
/// are acceptable as long as there are not too many of them.
1325+
///
1326+
/// It is possible that not all the instructions classified as tail calls by
1327+
/// this function are safe to be considered as such for the purpose of code
1328+
/// transformations performed by BOLT. The intention of this function is to
1329+
/// spot some of actually missed tail calls (and likely a number of unrelated
1330+
/// indirect branch instructions) as long as this doesn't increase the amount
1331+
/// of false positive reports unacceptably.
1332+
static bool shouldAnalyzeTailCallInst(const BinaryContext &BC,
1333+
const BinaryFunction &BF,
1334+
const MCInstReference &Inst) {
1335+
// Some BC.MIB->isXYZ(Inst) methods simply delegate to MCInstrDesc::isXYZ()
1336+
// (such as isBranch at the time of writing this comment), some don't (such
1337+
// as isCall). For that reason, call MCInstrDesc's methods explicitly when
1338+
// it is important.
1339+
const MCInstrDesc &Desc =
1340+
BC.MII->get(static_cast<const MCInst &>(Inst).getOpcode());
1341+
// Tail call should be a branch (but not necessarily an indirect one).
1342+
if (!Desc.isBranch())
1343+
return false;
1344+
1345+
// Always analyze the branches already marked as tail calls by BOLT.
1346+
if (BC.MIB->isTailCall(Inst))
1347+
return true;
1348+
1349+
// Try to also check the branches marked as "UNKNOWN CONTROL FLOW" - the
1350+
// below is a simplified condition from BinaryContext::printInstruction.
1351+
bool IsUnknownControlFlow =
1352+
BC.MIB->isIndirectBranch(Inst) && !BC.MIB->getJumpTable(Inst);
1353+
1354+
if (BF.hasCFG() && IsUnknownControlFlow)
1355+
return true;
1356+
1357+
return false;
1358+
}
1359+
1360+
static std::optional<PartialReport<MCPhysReg>>
1361+
shouldReportUnsafeTailCall(const BinaryContext &BC, const BinaryFunction &BF,
1362+
const MCInstReference &Inst, const SrcState &S) {
1363+
static const GadgetKind UntrustedLRKind(
1364+
"untrusted link register found before tail call");
1365+
1366+
if (!shouldAnalyzeTailCallInst(BC, BF, Inst))
1367+
return std::nullopt;
1368+
1369+
// Not only the set of registers returned by getTrustedLiveInRegs() can be
1370+
// seen as a reasonable target-independent _approximation_ of "the LR", these
1371+
// are *exactly* those registers used by SrcSafetyAnalysis to initialize the
1372+
// set of trusted registers on function entry.
1373+
// Thus, this function basically checks that the precondition expected to be
1374+
// imposed by a function call instruction (which is hardcoded into the target-
1375+
// specific getTrustedLiveInRegs() function) is also respected on tail calls.
1376+
SmallVector<MCPhysReg> RegsToCheck = BC.MIB->getTrustedLiveInRegs();
1377+
LLVM_DEBUG({
1378+
traceInst(BC, "Found tail call inst", Inst);
1379+
traceRegMask(BC, "Trusted regs", S.TrustedRegs);
1380+
});
1381+
1382+
// In musl on AArch64, the _start function sets LR to zero and calls the next
1383+
// stage initialization function at the end, something along these lines:
1384+
//
1385+
// _start:
1386+
// mov x30, #0
1387+
// ; ... other initialization ...
1388+
// b _start_c ; performs "exit" system call at some point
1389+
//
1390+
// As this would produce a false positive for every executable linked with
1391+
// such libc, ignore tail calls performed by ELF entry function.
1392+
if (BC.StartFunctionAddress &&
1393+
*BC.StartFunctionAddress == Inst.getFunction()->getAddress()) {
1394+
LLVM_DEBUG({ dbgs() << " Skipping tail call in ELF entry function.\n"; });
1395+
return std::nullopt;
1396+
}
1397+
1398+
// Returns at most one report per instruction - this is probably OK...
1399+
for (auto Reg : RegsToCheck)
1400+
if (!S.TrustedRegs[Reg])
1401+
return make_gadget_report(UntrustedLRKind, Inst, Reg);
1402+
1403+
return std::nullopt;
1404+
}
1405+
13221406
static std::optional<PartialReport<MCPhysReg>>
13231407
shouldReportCallGadget(const BinaryContext &BC, const MCInstReference &Inst,
13241408
const SrcState &S) {
@@ -1478,6 +1562,9 @@ void FunctionAnalysisContext::findUnsafeUses(
14781562
if (PacRetGadgetsOnly)
14791563
return;
14801564

1565+
if (auto Report = shouldReportUnsafeTailCall(BC, BF, Inst, S))
1566+
Reports.push_back(*Report);
1567+
14811568
if (auto Report = shouldReportCallGadget(BC, Inst, S))
14821569
Reports.push_back(*Report);
14831570
if (auto Report = shouldReportSigningOracle(BC, Inst, S))

0 commit comments

Comments
 (0)