Skip to content

Commit f3458d3

Browse files
atrosinenkoJaddyen
authored andcommitted
[BOLT] Gadget scanner: detect signing oracles (llvm#134146)
Implement the detection of signing oracles. In this patch, a signing oracle is defined as a sign instruction that accepts a "non-protected" pointer, but for a slightly different definition of "non-protected" compared to control flow instructions. A second BitVector named TrustedRegs is added to the register state computed by the data-flow analysis. The difference between a "safe-to-dereference" and a "trusted" register states is that to make an unsafe register trusted by authentication, one has to make sure that the authentication succeeded. For example, on AArch64 without FEAT_PAuth2 and FEAT_EPAC, an authentication instruction produces an invalid pointer on failure, so that subsequent memory access triggers an error, but re-signing such pointer would "fix" the signature. Note that while a separate "trusted" register state may be redundant depending on the specific semantics of auth and sign operations, it is still important to check signing operations: while code like this resign: autda x0, x1 pacda x0, x2 ret is probably safe provided `autda` generates an error on authentication failure, this function sign_anything: pacda x0, x1 ret is inherently unsafe.
1 parent b7d50ea commit f3458d3

File tree

8 files changed

+2227
-124
lines changed

8 files changed

+2227
-124
lines changed

bolt/include/bolt/Core/MCPlusBuilder.h

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class MCSymbol;
4949
class raw_ostream;
5050

5151
namespace bolt {
52+
class BinaryBasicBlock;
5253
class BinaryFunction;
5354

5455
/// Different types of indirect branches encountered during disassembly.
@@ -572,6 +573,11 @@ class MCPlusBuilder {
572573
return false;
573574
}
574575

576+
virtual MCPhysReg getSignedReg(const MCInst &Inst) const {
577+
llvm_unreachable("not implemented");
578+
return getNoRegister();
579+
}
580+
575581
virtual ErrorOr<MCPhysReg> getRegUsedAsRetDest(const MCInst &Inst) const {
576582
llvm_unreachable("not implemented");
577583
return getNoRegister();
@@ -622,6 +628,54 @@ class MCPlusBuilder {
622628
return std::make_pair(getNoRegister(), getNoRegister());
623629
}
624630

631+
/// Analyzes if a pointer is checked to be authenticated successfully
632+
/// by the end of the basic block.
633+
///
634+
/// It is possible for pointer authentication instructions not to terminate
635+
/// the program abnormally on authentication failure and return some invalid
636+
/// pointer instead (like it is done on AArch64 when FEAT_FPAC is not
637+
/// implemented). This might be enough to crash on invalid memory access when
638+
/// the pointer is later used as the destination of a load, store, or branch
639+
/// instruction. On the other hand, when the pointer is not used right away,
640+
/// it may be important for the compiler to check the address explicitly not
641+
/// to introduce a signing or authentication oracle.
642+
///
643+
/// This function is intended to detect a complex, multi-instruction pointer-
644+
/// checking sequence spanning a contiguous range of instructions at the end
645+
/// of the basic block (as these sequences are expected to end with a
646+
/// conditional branch - this is how they are implemented on AArch64 by LLVM).
647+
/// If a (Reg, FirstInst) pair is returned and before execution of FirstInst
648+
/// Reg was last written to by an authentication instruction, then it is known
649+
/// that in any successor of BB either
650+
/// * the authentication instruction that last wrote to Reg succeeded, or
651+
/// * the program is terminated abnormally without introducing any signing
652+
/// or authentication oracles
653+
///
654+
/// Note that this function is not expected to repeat the results returned
655+
/// by getAuthCheckedReg(Inst, MayOverwrite) function below.
656+
virtual std::optional<std::pair<MCPhysReg, MCInst *>>
657+
getAuthCheckedReg(BinaryBasicBlock &BB) const {
658+
llvm_unreachable("not implemented");
659+
return std::nullopt;
660+
}
661+
662+
/// Returns the register that is checked to be authenticated successfully.
663+
///
664+
/// If the returned register was last written to by an authentication
665+
/// instruction and that authentication failed, then the program is known
666+
/// to be terminated abnormally as a result of execution of Inst.
667+
///
668+
/// Additionally, if MayOverwrite is false, it is known that the authenticated
669+
/// pointer is not clobbered by Inst itself.
670+
///
671+
/// Use this function for simple, single-instruction patterns instead of
672+
/// its getAuthCheckedReg(BB) counterpart.
673+
virtual MCPhysReg getAuthCheckedReg(const MCInst &Inst,
674+
bool MayOverwrite) const {
675+
llvm_unreachable("not implemented");
676+
return getNoRegister();
677+
}
678+
625679
virtual bool isTerminator(const MCInst &Inst) const;
626680

627681
virtual bool isNoop(const MCInst &Inst) const {

bolt/lib/Passes/PAuthGadgetScanner.cpp

Lines changed: 150 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,24 @@ class TrackedRegisters {
173173
/// X30 is safe-to-dereference - the state computed for sub- and
174174
/// super-registers is not inspected.
175175
struct SrcState {
176-
/// A BitVector containing the registers that are either safe at function
177-
/// entry and were not clobbered yet, or those not clobbered since being
178-
/// authenticated.
176+
/// A BitVector containing the registers that are either authenticated
177+
/// (assuming failed authentication is permitted to produce an invalid
178+
/// address, provided it generates an error on memory access) or whose
179+
/// value is known not to be attacker-controlled under Pointer Authentication
180+
/// threat model. The registers in this set are either
181+
/// * not clobbered since being authenticated, or
182+
/// * trusted at function entry and were not clobbered yet, or
183+
/// * contain a safely materialized address.
179184
BitVector SafeToDerefRegs;
185+
/// A BitVector containing the registers that are either authenticated
186+
/// *successfully* or whose value is known not to be attacker-controlled
187+
/// under Pointer Authentication threat model.
188+
/// The registers in this set are either
189+
/// * authenticated and then checked to be authenticated successfully
190+
/// (and not clobbered since then), or
191+
/// * trusted at function entry and were not clobbered yet, or
192+
/// * contain a safely materialized address.
193+
BitVector TrustedRegs;
180194
/// A vector of sets, only used in the second data flow run.
181195
/// Each element in the vector represents one of the registers for which we
182196
/// track the set of last instructions that wrote to this register. For
@@ -189,7 +203,8 @@ struct SrcState {
189203
SrcState() {}
190204

191205
SrcState(unsigned NumRegs, unsigned NumRegsToTrack)
192-
: SafeToDerefRegs(NumRegs), LastInstWritingReg(NumRegsToTrack) {}
206+
: SafeToDerefRegs(NumRegs), TrustedRegs(NumRegs),
207+
LastInstWritingReg(NumRegsToTrack) {}
193208

194209
SrcState &merge(const SrcState &StateIn) {
195210
if (StateIn.empty())
@@ -198,6 +213,7 @@ struct SrcState {
198213
return (*this = StateIn);
199214

200215
SafeToDerefRegs &= StateIn.SafeToDerefRegs;
216+
TrustedRegs &= StateIn.TrustedRegs;
201217
for (unsigned I = 0; I < LastInstWritingReg.size(); ++I)
202218
for (const MCInst *J : StateIn.LastInstWritingReg[I])
203219
LastInstWritingReg[I].insert(J);
@@ -210,6 +226,7 @@ struct SrcState {
210226

211227
bool operator==(const SrcState &RHS) const {
212228
return SafeToDerefRegs == RHS.SafeToDerefRegs &&
229+
TrustedRegs == RHS.TrustedRegs &&
213230
LastInstWritingReg == RHS.LastInstWritingReg;
214231
}
215232
bool operator!=(const SrcState &RHS) const { return !((*this) == RHS); }
@@ -234,6 +251,7 @@ raw_ostream &operator<<(raw_ostream &OS, const SrcState &S) {
234251
OS << "empty";
235252
} else {
236253
OS << "SafeToDerefRegs: " << S.SafeToDerefRegs << ", ";
254+
OS << "TrustedRegs: " << S.TrustedRegs << ", ";
237255
printLastInsts(OS, S.LastInstWritingReg);
238256
}
239257
OS << ">";
@@ -254,18 +272,22 @@ void SrcStatePrinter::print(raw_ostream &OS, const SrcState &S) const {
254272
OS << "src-state<";
255273
if (S.empty()) {
256274
assert(S.SafeToDerefRegs.empty());
275+
assert(S.TrustedRegs.empty());
257276
assert(S.LastInstWritingReg.empty());
258277
OS << "empty";
259278
} else {
260279
OS << "SafeToDerefRegs: ";
261280
RegStatePrinter.print(OS, S.SafeToDerefRegs);
281+
OS << ", TrustedRegs: ";
282+
RegStatePrinter.print(OS, S.TrustedRegs);
262283
OS << ", ";
263284
printLastInsts(OS, S.LastInstWritingReg);
264285
}
265286
OS << ">";
266287
}
267288

268-
/// Computes which registers are safe to be used by control flow instructions.
289+
/// Computes which registers are safe to be used by control flow and signing
290+
/// instructions.
269291
///
270292
/// This is the base class for two implementations: a dataflow-based analysis
271293
/// which is intended to be used for most functions and a simplified CFG-unaware
@@ -293,6 +315,17 @@ class SrcSafetyAnalysis {
293315
/// RegToTrackInstsFor is the set of registers for which the dataflow analysis
294316
/// must compute which the last set of instructions writing to it are.
295317
const TrackedRegisters RegsToTrackInstsFor;
318+
/// Stores information about the detected instruction sequences emitted to
319+
/// check an authenticated pointer. Specifically, if such sequence is detected
320+
/// in a basic block, it maps the last instruction of that basic block to
321+
/// (CheckedRegister, FirstInstOfTheSequence) pair, see the description of
322+
/// MCPlusBuilder::getAuthCheckedReg(BB) method.
323+
///
324+
/// As the detection of such sequences requires iterating over the adjacent
325+
/// instructions, it should be done before calling computeNext(), which
326+
/// operates on separate instructions.
327+
DenseMap<const MCInst *, std::pair<MCPhysReg, const MCInst *>>
328+
CheckerSequenceInfo;
296329

297330
SmallPtrSet<const MCInst *, 4> &lastWritingInsts(SrcState &S,
298331
MCPhysReg Reg) const {
@@ -308,7 +341,8 @@ class SrcSafetyAnalysis {
308341
SrcState createEntryState() {
309342
SrcState S(NumRegs, RegsToTrackInstsFor.getNumTrackedRegisters());
310343
for (MCPhysReg Reg : BC.MIB->getTrustedLiveInRegs())
311-
S.SafeToDerefRegs |= BC.MIB->getAliases(Reg, /*OnlySmaller=*/true);
344+
S.TrustedRegs |= BC.MIB->getAliases(Reg, /*OnlySmaller=*/true);
345+
S.SafeToDerefRegs = S.TrustedRegs;
312346
return S;
313347
}
314348

@@ -355,6 +389,47 @@ class SrcSafetyAnalysis {
355389
return Regs;
356390
}
357391

392+
// Returns all registers made trusted by this instruction.
393+
SmallVector<MCPhysReg> getRegsMadeTrusted(const MCInst &Point,
394+
const SrcState &Cur) const {
395+
SmallVector<MCPhysReg> Regs;
396+
const MCPhysReg NoReg = BC.MIB->getNoRegister();
397+
398+
// An authenticated pointer can be checked, or
399+
MCPhysReg CheckedReg =
400+
BC.MIB->getAuthCheckedReg(Point, /*MayOverwrite=*/false);
401+
if (CheckedReg != NoReg && Cur.SafeToDerefRegs[CheckedReg])
402+
Regs.push_back(CheckedReg);
403+
404+
if (CheckerSequenceInfo.contains(&Point)) {
405+
MCPhysReg CheckedReg;
406+
const MCInst *FirstCheckerInst;
407+
std::tie(CheckedReg, FirstCheckerInst) = CheckerSequenceInfo.at(&Point);
408+
409+
// FirstCheckerInst should belong to the same basic block (see the
410+
// assertion in DataflowSrcSafetyAnalysis::run()), meaning it was
411+
// deterministically processed a few steps before this instruction.
412+
const SrcState &StateBeforeChecker =
413+
getStateBefore(*FirstCheckerInst).get();
414+
if (StateBeforeChecker.SafeToDerefRegs[CheckedReg])
415+
Regs.push_back(CheckedReg);
416+
}
417+
418+
// ... a safe address can be materialized, or
419+
MCPhysReg NewAddrReg = BC.MIB->getMaterializedAddressRegForPtrAuth(Point);
420+
if (NewAddrReg != NoReg)
421+
Regs.push_back(NewAddrReg);
422+
423+
// ... an address can be updated in a safe manner, producing the result
424+
// which is as trusted as the input address.
425+
if (auto DstAndSrc = BC.MIB->analyzeAddressArithmeticsForPtrAuth(Point)) {
426+
if (Cur.TrustedRegs[DstAndSrc->second])
427+
Regs.push_back(DstAndSrc->first);
428+
}
429+
430+
return Regs;
431+
}
432+
358433
SrcState computeNext(const MCInst &Point, const SrcState &Cur) {
359434
SrcStatePrinter P(BC);
360435
LLVM_DEBUG({
@@ -381,11 +456,34 @@ class SrcSafetyAnalysis {
381456
BitVector Clobbered = getClobberedRegs(Point);
382457
SmallVector<MCPhysReg> NewSafeToDerefRegs =
383458
getRegsMadeSafeToDeref(Point, Cur);
459+
SmallVector<MCPhysReg> NewTrustedRegs = getRegsMadeTrusted(Point, Cur);
460+
461+
// Ideally, being trusted is a strictly stronger property than being
462+
// safe-to-dereference. To simplify the computation of Next state, enforce
463+
// this for NewSafeToDerefRegs and NewTrustedRegs. Additionally, this
464+
// fixes the properly for "cumulative" register states in tricky cases
465+
// like the following:
466+
//
467+
// ; LR is safe to dereference here
468+
// mov x16, x30 ; start of the sequence, LR is s-t-d right before
469+
// xpaclri ; clobbers LR, LR is not safe anymore
470+
// cmp x30, x16
471+
// b.eq 1f ; end of the sequence: LR is marked as trusted
472+
// brk 0x1234
473+
// 1:
474+
// ; at this point LR would be marked as trusted,
475+
// ; but not safe-to-dereference
476+
//
477+
for (auto TrustedReg : NewTrustedRegs) {
478+
if (!is_contained(NewSafeToDerefRegs, TrustedReg))
479+
NewSafeToDerefRegs.push_back(TrustedReg);
480+
}
384481

385482
// Then, compute the state after this instruction is executed.
386483
SrcState Next = Cur;
387484

388485
Next.SafeToDerefRegs.reset(Clobbered);
486+
Next.TrustedRegs.reset(Clobbered);
389487
// Keep track of this instruction if it writes to any of the registers we
390488
// need to track that for:
391489
for (MCPhysReg Reg : RegsToTrackInstsFor.getRegisters())
@@ -406,6 +504,10 @@ class SrcSafetyAnalysis {
406504
lastWritingInsts(Next, Reg).clear();
407505
}
408506

507+
// Process new trusted registers.
508+
for (MCPhysReg TrustedReg : NewTrustedRegs)
509+
Next.TrustedRegs |= BC.MIB->getAliases(TrustedReg, /*OnlySmaller=*/true);
510+
409511
LLVM_DEBUG({
410512
dbgs() << " .. result: (";
411513
P.print(dbgs(), Next);
@@ -462,7 +564,26 @@ class DataflowSrcSafetyAnalysis
462564
return DFParent::getStateBefore(Inst);
463565
}
464566

465-
void run() override { DFParent::run(); }
567+
void run() override {
568+
for (BinaryBasicBlock &BB : Func) {
569+
if (auto CheckerInfo = BC.MIB->getAuthCheckedReg(BB)) {
570+
MCPhysReg CheckedReg = CheckerInfo->first;
571+
MCInst &FirstInst = *CheckerInfo->second;
572+
MCInst &LastInst = *BB.getLastNonPseudoInstr();
573+
LLVM_DEBUG({
574+
dbgs() << "Found pointer checking sequence in " << BB.getName()
575+
<< ":\n";
576+
traceReg(BC, "Checked register", CheckedReg);
577+
traceInst(BC, "First instruction", FirstInst);
578+
traceInst(BC, "Last instruction", LastInst);
579+
});
580+
assert(llvm::any_of(BB, [&](MCInst &I) { return &I == &FirstInst; }) &&
581+
"Data-flow analysis expects the checker not to cross BBs");
582+
CheckerSequenceInfo[&LastInst] = *CheckerInfo;
583+
}
584+
}
585+
DFParent::run();
586+
}
466587

467588
protected:
468589
void preflight() {}
@@ -658,6 +779,26 @@ shouldReportCallGadget(const BinaryContext &BC, const MCInstReference &Inst,
658779
return std::make_shared<GadgetReport>(CallKind, Inst, DestReg);
659780
}
660781

782+
static std::shared_ptr<Report>
783+
shouldReportSigningOracle(const BinaryContext &BC, const MCInstReference &Inst,
784+
const SrcState &S) {
785+
static const GadgetKind SigningOracleKind("signing oracle found");
786+
787+
MCPhysReg SignedReg = BC.MIB->getSignedReg(Inst);
788+
if (SignedReg == BC.MIB->getNoRegister())
789+
return nullptr;
790+
791+
LLVM_DEBUG({
792+
traceInst(BC, "Found sign inst", Inst);
793+
traceReg(BC, "Signed reg", SignedReg);
794+
traceRegMask(BC, "TrustedRegs", S.TrustedRegs);
795+
});
796+
if (S.TrustedRegs[SignedReg])
797+
return nullptr;
798+
799+
return std::make_shared<GadgetReport>(SigningOracleKind, Inst, SignedReg);
800+
}
801+
661802
template <typename T> static void iterateOverInstrs(BinaryFunction &BF, T Fn) {
662803
if (BF.hasCFG()) {
663804
for (BinaryBasicBlock &BB : BF)
@@ -702,6 +843,8 @@ Analysis::findGadgets(BinaryFunction &BF,
702843

703844
if (auto Report = shouldReportCallGadget(BC, Inst, S))
704845
Result.Diagnostics.push_back(Report);
846+
if (auto Report = shouldReportSigningOracle(BC, Inst, S))
847+
Result.Diagnostics.push_back(Report);
705848
});
706849
return Result;
707850
}

0 commit comments

Comments
 (0)