Skip to content

Commit 826e9d7

Browse files
committed
[BOLT] Gadget scanner: detect signing oracles
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 49981c9 commit 826e9d7

File tree

6 files changed

+2092
-75
lines changed

6 files changed

+2092
-75
lines changed

bolt/include/bolt/Core/MCPlusBuilder.h

Lines changed: 40 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();
@@ -615,6 +621,40 @@ class MCPlusBuilder {
615621
return std::make_pair(getNoRegister(), getNoRegister());
616622
}
617623

624+
/// Analyzes if a pointer is checked to be valid by the end of BB.
625+
///
626+
/// It is possible for pointer authentication instructions not to terminate
627+
/// the program abnormally on authentication failure and return some *invalid
628+
/// pointer* instead (like it is done on AArch64 when FEAT_FPAC is not
629+
/// implemented). This might be enough to crash on invalid memory access
630+
/// when the pointer is later used as the destination of load/store or branch
631+
/// instruction. On the other hand, when the pointer is not used right away,
632+
/// it may be important for the compiler to check the address explicitly not
633+
/// to introduce signing or authentication oracle.
634+
///
635+
/// If this function returns a (Reg, Inst) pair, then it is known that in any
636+
/// successor of BB either
637+
/// * Reg is trusted, provided it was safe-to-dereference before Inst, or
638+
/// * the program is terminated abnormally without introducing any signing
639+
/// or authentication oracles
640+
virtual std::optional<std::pair<MCPhysReg, MCInst *>>
641+
getAuthCheckedReg(BinaryBasicBlock &BB) const {
642+
llvm_unreachable("not implemented");
643+
return std::nullopt;
644+
}
645+
646+
/// Returns the register that is checked to be authenticated successfully.
647+
///
648+
/// If the returned register was safe-to-dereference before execution of Inst,
649+
/// it becomes trusted afterward (if MayOverwrite is false) or at least does
650+
/// not escape in a way usable as an authentication oracle (if MayOverwrite
651+
/// is true).
652+
virtual MCPhysReg getAuthCheckedReg(const MCInst &Inst,
653+
bool MayOverwrite) const {
654+
llvm_unreachable("not implemented");
655+
return getNoRegister();
656+
}
657+
618658
virtual bool isTerminator(const MCInst &Inst) const;
619659

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

bolt/lib/Passes/PAuthGadgetScanner.cpp

Lines changed: 144 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,24 @@ template <typename T> static void iterateOverInstrs(BinaryFunction &BF, T Fn) {
194194
/// X30 is safe-to-dereference - the state computed for sub- and
195195
/// super-registers is not inspected.
196196
struct State {
197-
/// A BitVector containing the registers that are either safe at function
198-
/// entry and were not clobbered yet, or those not clobbered since being
199-
/// authenticated.
197+
/// A BitVector containing the registers that are either authenticated
198+
/// (assuming failed authentication is permitted to produce an invalid
199+
/// address, provided it generates an error on memory access) or whose
200+
/// value is known not to be attacker-controlled under Pointer Authentication
201+
/// threat model. The registers in this set are either
202+
/// * not clobbered since being authenticated, or
203+
/// * trusted at function entry and were not clobbered yet, or
204+
/// * contain a safely materialized address.
200205
BitVector SafeToDerefRegs;
206+
/// A BitVector containing the registers that are either authenticated
207+
/// *successfully* or whose value is known not to be attacker-controlled
208+
/// under Pointer Authentication threat model.
209+
/// The registers in this set are either
210+
/// * authenticated and then checked to be authenticated successfully
211+
/// (and not clobbered since then), or
212+
/// * trusted at function entry and were not clobbered yet, or
213+
/// * contain a safely materialized address.
214+
BitVector TrustedRegs;
201215
/// A vector of sets, only used in the second data flow run.
202216
/// Each element in the vector represents one of the registers for which we
203217
/// track the set of last instructions that wrote to this register. For
@@ -210,7 +224,8 @@ struct State {
210224
State() {}
211225

212226
State(unsigned NumRegs, unsigned NumRegsToTrack)
213-
: SafeToDerefRegs(NumRegs), LastInstWritingReg(NumRegsToTrack) {}
227+
: SafeToDerefRegs(NumRegs), TrustedRegs(NumRegs),
228+
LastInstWritingReg(NumRegsToTrack) {}
214229

215230
State &merge(const State &StateIn) {
216231
if (StateIn.empty())
@@ -219,6 +234,7 @@ struct State {
219234
return (*this = StateIn);
220235

221236
SafeToDerefRegs &= StateIn.SafeToDerefRegs;
237+
TrustedRegs &= StateIn.TrustedRegs;
222238
for (unsigned I = 0; I < LastInstWritingReg.size(); ++I)
223239
for (const MCInst *J : StateIn.LastInstWritingReg[I])
224240
LastInstWritingReg[I].insert(J);
@@ -231,6 +247,7 @@ struct State {
231247

232248
bool operator==(const State &RHS) const {
233249
return SafeToDerefRegs == RHS.SafeToDerefRegs &&
250+
TrustedRegs == RHS.TrustedRegs &&
234251
LastInstWritingReg == RHS.LastInstWritingReg;
235252
}
236253
bool operator!=(const State &RHS) const { return !((*this) == RHS); }
@@ -255,6 +272,7 @@ raw_ostream &operator<<(raw_ostream &OS, const State &S) {
255272
OS << "empty";
256273
} else {
257274
OS << "SafeToDerefRegs: " << S.SafeToDerefRegs << ", ";
275+
OS << "TrustedRegs: " << S.TrustedRegs << ", ";
258276
printLastInsts(OS, S.LastInstWritingReg);
259277
}
260278
OS << ">";
@@ -275,11 +293,14 @@ void PacStatePrinter::print(raw_ostream &OS, const State &S) const {
275293
OS << "pacret-state<";
276294
if (S.empty()) {
277295
assert(S.SafeToDerefRegs.empty());
296+
assert(S.TrustedRegs.empty());
278297
assert(S.LastInstWritingReg.empty());
279298
OS << "empty";
280299
} else {
281300
OS << "SafeToDerefRegs: ";
282301
RegStatePrinter.print(OS, S.SafeToDerefRegs);
302+
OS << ", TrustedRegs: ";
303+
RegStatePrinter.print(OS, S.TrustedRegs);
283304
OS << ", ";
284305
printLastInsts(OS, S.LastInstWritingReg);
285306
}
@@ -308,6 +329,17 @@ class PacRetAnalysis {
308329
/// RegToTrackInstsFor is the set of registers for which the dataflow analysis
309330
/// must compute which the last set of instructions writing to it are.
310331
const TrackedRegisters RegsToTrackInstsFor;
332+
/// Stores information about the detected instruction sequences emitted to
333+
/// check an authenticated pointer. Specifically, if such sequence is detected
334+
/// in a basic block, it maps the last instruction of that basic block to
335+
/// (CheckedRegister, FirstInstOfTheSequence) pair, see the description of
336+
/// MCPlusBuilder::getAuthCheckedReg(BB) method.
337+
///
338+
/// As the detection of such sequences requires iterating over the adjacent
339+
/// instructions, it should be done before calling computeNext(), which
340+
/// operates on separate instructions.
341+
DenseMap<const MCInst *, std::pair<MCPhysReg, const MCInst *>>
342+
CheckerSequenceInfo;
311343

312344
SmallPtrSet<const MCInst *, 4> &lastWritingInsts(State &S,
313345
MCPhysReg Reg) const {
@@ -322,8 +354,10 @@ class PacRetAnalysis {
322354

323355
State createEntryState() {
324356
State S(NumRegs, RegsToTrackInstsFor.getNumTrackedRegisters());
325-
for (MCPhysReg Reg : BC.MIB->getTrustedLiveInRegs())
326-
S.SafeToDerefRegs |= BC.MIB->getAliases(Reg, /*OnlySmaller=*/true);
357+
for (MCPhysReg Reg : BC.MIB->getTrustedLiveInRegs()) {
358+
S.TrustedRegs |= BC.MIB->getAliases(Reg, /*OnlySmaller=*/true);
359+
S.SafeToDerefRegs = S.TrustedRegs;
360+
}
327361
return S;
328362
}
329363

@@ -370,6 +404,45 @@ class PacRetAnalysis {
370404
return Regs;
371405
}
372406

407+
// Returns all registers made trusted by this instruction.
408+
SmallVector<MCPhysReg> getRegsMadeTrusted(const MCInst &Point,
409+
const State &Cur) const {
410+
SmallVector<MCPhysReg> Regs;
411+
const MCPhysReg NoReg = BC.MIB->getNoRegister();
412+
413+
// An authenticated pointer can be checked, or
414+
MCPhysReg CheckedReg =
415+
BC.MIB->getAuthCheckedReg(Point, /*MayOverwrite=*/false);
416+
if (CheckedReg != NoReg && Cur.SafeToDerefRegs[CheckedReg])
417+
Regs.push_back(CheckedReg);
418+
419+
if (CheckerSequenceInfo.contains(&Point)) {
420+
MCPhysReg CheckedReg;
421+
const MCInst *FirstCheckerInst;
422+
std::tie(CheckedReg, FirstCheckerInst) = CheckerSequenceInfo.at(&Point);
423+
424+
// FirstCheckerInst should belong to the same basic block, meaning
425+
// it was deterministically processed a few steps before this instruction.
426+
const State &StateBeforeChecker = getStateBefore(*FirstCheckerInst).get();
427+
if (StateBeforeChecker.SafeToDerefRegs[CheckedReg])
428+
Regs.push_back(CheckedReg);
429+
}
430+
431+
// ... a safe address can be materialized, or
432+
MCPhysReg NewAddrReg = BC.MIB->getMaterializedAddressRegForPtrAuth(Point);
433+
if (NewAddrReg != NoReg)
434+
Regs.push_back(NewAddrReg);
435+
436+
// ... an address can be updated in a safe manner, producing the result
437+
// which is as trusted as the input address.
438+
if (auto DstAndSrc = BC.MIB->analyzeAddressArithmeticsForPtrAuth(Point)) {
439+
if (Cur.TrustedRegs[DstAndSrc->second])
440+
Regs.push_back(DstAndSrc->first);
441+
}
442+
443+
return Regs;
444+
}
445+
373446
State computeNext(const MCInst &Point, const State &Cur) {
374447
PacStatePrinter P(BC);
375448
LLVM_DEBUG({
@@ -396,11 +469,34 @@ class PacRetAnalysis {
396469
BitVector Clobbered = getClobberedRegs(Point);
397470
SmallVector<MCPhysReg> NewSafeToDerefRegs =
398471
getRegsMadeSafeToDeref(Point, Cur);
472+
SmallVector<MCPhysReg> NewTrustedRegs = getRegsMadeTrusted(Point, Cur);
473+
474+
// Ideally, being trusted is a strictly stronger property than being
475+
// safe-to-dereference. To simplify the computation of Next state, enforce
476+
// this for NewSafeToDerefRegs and NewTrustedRegs. Additionally, this
477+
// fixes the properly for "cumulative" register states in tricky cases
478+
// like the following:
479+
//
480+
// ; LR is safe to dereference here
481+
// mov x16, x30 ; start of the sequence, LR is s-t-d right before
482+
// xpaclri ; clobbers LR, LR is not safe anymore
483+
// cmp x30, x16
484+
// b.eq 1f ; end of the sequence: LR is marked as trusted
485+
// brk 0x1234
486+
// 1:
487+
// ; at this point LR would be marked as trusted,
488+
// ; but not safe-to-dereference
489+
//
490+
for (auto TrustedReg : NewTrustedRegs) {
491+
if (!is_contained(NewSafeToDerefRegs, TrustedReg))
492+
NewSafeToDerefRegs.push_back(TrustedReg);
493+
}
399494

400495
// Then, compute the state after this instruction is executed.
401496
State Next = Cur;
402497

403498
Next.SafeToDerefRegs.reset(Clobbered);
499+
Next.TrustedRegs.reset(Clobbered);
404500
// Keep track of this instruction if it writes to any of the registers we
405501
// need to track that for:
406502
for (MCPhysReg Reg : RegsToTrackInstsFor.getRegisters())
@@ -421,6 +517,10 @@ class PacRetAnalysis {
421517
lastWritingInsts(Next, Reg).clear();
422518
}
423519

520+
// Process new trusted registers.
521+
for (MCPhysReg TrustedReg : NewTrustedRegs)
522+
Next.TrustedRegs |= BC.MIB->getAliases(TrustedReg, /*OnlySmaller=*/true);
523+
424524
LLVM_DEBUG({
425525
dbgs() << " .. result: (";
426526
P.print(dbgs(), Next);
@@ -476,7 +576,22 @@ class PacRetDFAnalysis
476576
return DFParent::getStateBefore(Inst);
477577
}
478578

479-
void run() override { DFParent::run(); }
579+
void run() override {
580+
for (BinaryBasicBlock &BB : Func) {
581+
if (auto CheckerInfo = BC.MIB->getAuthCheckedReg(BB)) {
582+
MCInst *LastInstOfChecker = BB.getLastNonPseudoInstr();
583+
LLVM_DEBUG({
584+
dbgs() << "Found pointer checking sequence in " << BB.getName()
585+
<< ":\n";
586+
traceReg(BC, "Checked register", CheckerInfo->first);
587+
traceInst(BC, "First instruction", *CheckerInfo->second);
588+
traceInst(BC, "Last instruction", *LastInstOfChecker);
589+
});
590+
CheckerSequenceInfo[LastInstOfChecker] = *CheckerInfo;
591+
}
592+
}
593+
DFParent::run();
594+
}
480595

481596
protected:
482597
void preflight() {}
@@ -634,6 +749,26 @@ shouldReportCallGadget(const BinaryContext &BC, const MCInstReference &Inst,
634749
return std::make_shared<GadgetReport>(CallKind, Inst, DestReg);
635750
}
636751

752+
static std::shared_ptr<Report>
753+
shouldReportSigningOracle(const BinaryContext &BC, const MCInstReference &Inst,
754+
const State &S) {
755+
static const GadgetKind SigningOracleKind("signing oracle found");
756+
757+
MCPhysReg SignedReg = BC.MIB->getSignedReg(Inst);
758+
if (SignedReg == BC.MIB->getNoRegister())
759+
return nullptr;
760+
761+
LLVM_DEBUG({
762+
traceInst(BC, "Found sign inst", Inst);
763+
traceReg(BC, "Signed reg", SignedReg);
764+
traceRegMask(BC, "TrustedRegs", S.TrustedRegs);
765+
});
766+
if (S.TrustedRegs[SignedReg])
767+
return nullptr;
768+
769+
return std::make_shared<GadgetReport>(SigningOracleKind, Inst, SignedReg);
770+
}
771+
637772
FunctionAnalysisResult
638773
Analysis::findGadgets(BinaryFunction &BF,
639774
MCPlusBuilder::AllocatorIdTy AllocatorId) {
@@ -666,6 +801,8 @@ Analysis::findGadgets(BinaryFunction &BF,
666801

667802
if (auto Report = shouldReportCallGadget(BC, Inst, S))
668803
Result.Diagnostics.push_back(Report);
804+
if (auto Report = shouldReportSigningOracle(BC, Inst, S))
805+
Result.Diagnostics.push_back(Report);
669806
});
670807
return Result;
671808
}

0 commit comments

Comments
 (0)