diff --git a/bolt/include/bolt/Passes/PAuthGadgetScanner.h b/bolt/include/bolt/Passes/PAuthGadgetScanner.h index c6b9cc2eb4b9c..721fd664a3253 100644 --- a/bolt/include/bolt/Passes/PAuthGadgetScanner.h +++ b/bolt/include/bolt/Passes/PAuthGadgetScanner.h @@ -199,8 +199,7 @@ namespace PAuthGadgetScanner { // to distinguish intermediate and final results at the type level. // // Here is an overview of issue life-cycle: -// * an analysis (SrcSafetyAnalysis at now, DstSafetyAnalysis will be added -// later to support the detection of authentication oracles) computes register +// * an analysis (SrcSafetyAnalysis or DstSafetyAnalysis) computes register // state for each instruction in the function. // * for each instruction, it is checked whether it is a gadget of some kind, // taking the computed state into account. If a gadget is found, its kind @@ -273,6 +272,11 @@ class ExtraInfo { virtual ~ExtraInfo() {} }; +/// The set of instructions writing to the affected register in an unsafe +/// manner. +/// +/// This is a hint to be printed alongside the report. It should be further +/// analyzed by the user. class ClobberingInfo : public ExtraInfo { SmallVector ClobberingInstrs; @@ -282,6 +286,20 @@ class ClobberingInfo : public ExtraInfo { void print(raw_ostream &OS, const MCInstReference Location) const override; }; +/// The set of instructions leaking the authenticated pointer before the +/// result of authentication was checked. +/// +/// This is a hint to be printed alongside the report. It should be further +/// analyzed by the user. +class LeakageInfo : public ExtraInfo { + SmallVector LeakingInstrs; + +public: + LeakageInfo(ArrayRef Instrs) : LeakingInstrs(Instrs) {} + + void print(raw_ostream &OS, const MCInstReference Location) const override; +}; + /// A brief version of a report that can be further augmented with the details. /// /// A half-baked report produced on the first run of the analysis. An extra, @@ -322,6 +340,9 @@ class FunctionAnalysisContext { void findUnsafeUses(SmallVector> &Reports); void augmentUnsafeUseReports(ArrayRef> Reports); + void findUnsafeDefs(SmallVector> &Reports); + void augmentUnsafeDefReports(ArrayRef> Reports); + /// Process the reports which do not have to be augmented, and remove them /// from Reports. void handleSimpleReports(SmallVector> &Reports); diff --git a/bolt/lib/Passes/PAuthGadgetScanner.cpp b/bolt/lib/Passes/PAuthGadgetScanner.cpp index 971ea5fdef420..7682d7fe2c542 100644 --- a/bolt/lib/Passes/PAuthGadgetScanner.cpp +++ b/bolt/lib/Passes/PAuthGadgetScanner.cpp @@ -152,6 +152,8 @@ class TrackedRegisters { // in the gadgets to be reported. This information is used in the second run // to also track which instructions last wrote to those registers. +typedef SmallPtrSet SetOfRelatedInsts; + /// A state representing which registers are safe to use by an instruction /// at a given program point. /// @@ -195,7 +197,7 @@ struct SrcState { /// pac-ret analysis, the expectation is that almost all return instructions /// only use register `X30`, and therefore, this vector will probably have /// length 1 in the second run. - std::vector> LastInstWritingReg; + std::vector LastInstWritingReg; /// Construct an empty state. SrcState() {} @@ -230,12 +232,11 @@ struct SrcState { bool operator!=(const SrcState &RHS) const { return !((*this) == RHS); } }; -static void -printLastInsts(raw_ostream &OS, - ArrayRef> LastInstWritingReg) { +static void printInstsShort(raw_ostream &OS, + ArrayRef Insts) { OS << "Insts: "; - for (unsigned I = 0; I < LastInstWritingReg.size(); ++I) { - auto &Set = LastInstWritingReg[I]; + for (unsigned I = 0; I < Insts.size(); ++I) { + auto &Set = Insts[I]; OS << "[" << I << "]("; for (const MCInst *MCInstP : Set) OS << MCInstP << " "; @@ -243,14 +244,14 @@ printLastInsts(raw_ostream &OS, } } -raw_ostream &operator<<(raw_ostream &OS, const SrcState &S) { +static raw_ostream &operator<<(raw_ostream &OS, const SrcState &S) { OS << "src-state<"; if (S.empty()) { OS << "empty"; } else { OS << "SafeToDerefRegs: " << S.SafeToDerefRegs << ", "; OS << "TrustedRegs: " << S.TrustedRegs << ", "; - printLastInsts(OS, S.LastInstWritingReg); + printInstsShort(OS, S.LastInstWritingReg); } OS << ">"; return OS; @@ -279,7 +280,7 @@ void SrcStatePrinter::print(raw_ostream &OS, const SrcState &S) const { OS << ", TrustedRegs: "; RegStatePrinter.print(OS, S.TrustedRegs); OS << ", "; - printLastInsts(OS, S.LastInstWritingReg); + printInstsShort(OS, S.LastInstWritingReg); } OS << ">"; } @@ -323,13 +324,12 @@ class SrcSafetyAnalysis { DenseMap> CheckerSequenceInfo; - SmallPtrSet &lastWritingInsts(SrcState &S, - MCPhysReg Reg) const { + SetOfRelatedInsts &lastWritingInsts(SrcState &S, MCPhysReg Reg) const { unsigned Index = RegsToTrackInstsFor.getIndex(Reg); return S.LastInstWritingReg[Index]; } - const SmallPtrSet &lastWritingInsts(const SrcState &S, - MCPhysReg Reg) const { + const SetOfRelatedInsts &lastWritingInsts(const SrcState &S, + MCPhysReg Reg) const { unsigned Index = RegsToTrackInstsFor.getIndex(Reg); return S.LastInstWritingReg[Index]; } @@ -433,8 +433,7 @@ class SrcSafetyAnalysis { SrcStatePrinter P(BC); LLVM_DEBUG({ dbgs() << " SrcSafetyAnalysis::ComputeNext("; - BC.InstPrinter->printInst(&const_cast(Point), 0, "", *BC.STI, - dbgs()); + BC.InstPrinter->printInst(&Point, 0, "", *BC.STI, dbgs()); dbgs() << ", "; P.print(dbgs(), Cur); dbgs() << ")\n"; @@ -612,6 +611,42 @@ class DataflowSrcSafetyAnalysis StringRef getAnnotationName() const { return "DataflowSrcSafetyAnalysis"; } }; +/// A helper base class for implementing a simplified counterpart of a dataflow +/// analysis for functions without CFG information. +template class CFGUnawareAnalysis { + BinaryContext &BC; + BinaryFunction &BF; + MCPlusBuilder::AllocatorIdTy AllocId; + unsigned StateAnnotationIndex; + + void cleanStateAnnotations() { + for (auto &I : BF.instrs()) + BC.MIB->removeAnnotation(I.second, StateAnnotationIndex); + } + +protected: + CFGUnawareAnalysis(BinaryFunction &BF, MCPlusBuilder::AllocatorIdTy AllocId, + StringRef AnnotationName) + : BC(BF.getBinaryContext()), BF(BF), AllocId(AllocId) { + StateAnnotationIndex = BC.MIB->getOrCreateAnnotationIndex(AnnotationName); + } + + void setState(MCInst &Inst, const StateTy &S) { + // Check if we need to remove an old annotation (this is the case if + // this is the second, detailed run of the analysis). + if (BC.MIB->hasAnnotation(Inst, StateAnnotationIndex)) + BC.MIB->removeAnnotation(Inst, StateAnnotationIndex); + // Attach the state. + BC.MIB->addAnnotation(Inst, StateAnnotationIndex, S, AllocId); + } + + const StateTy &getState(const MCInst &Inst) const { + return BC.MIB->getAnnotationAs(Inst, StateAnnotationIndex); + } + + virtual ~CFGUnawareAnalysis() { cleanStateAnnotations(); } +}; + // A simplified implementation of DataflowSrcSafetyAnalysis for functions // lacking CFG information. // @@ -646,15 +681,10 @@ class DataflowSrcSafetyAnalysis // of instructions without labels in between. These sequences can be processed // the same way basic blocks are processed by data-flow analysis, assuming // pessimistically that all registers are unsafe at the start of each sequence. -class CFGUnawareSrcSafetyAnalysis : public SrcSafetyAnalysis { +class CFGUnawareSrcSafetyAnalysis : public SrcSafetyAnalysis, + public CFGUnawareAnalysis { + using SrcSafetyAnalysis::BC; BinaryFunction &BF; - MCPlusBuilder::AllocatorIdTy AllocId; - unsigned StateAnnotationIndex; - - void cleanStateAnnotations() { - for (auto &I : BF.instrs()) - BC.MIB->removeAnnotation(I.second, StateAnnotationIndex); - } /// Creates a state with all registers marked unsafe (not to be confused /// with empty state). @@ -666,9 +696,8 @@ class CFGUnawareSrcSafetyAnalysis : public SrcSafetyAnalysis { CFGUnawareSrcSafetyAnalysis(BinaryFunction &BF, MCPlusBuilder::AllocatorIdTy AllocId, ArrayRef RegsToTrackInstsFor) - : SrcSafetyAnalysis(BF, RegsToTrackInstsFor), BF(BF), AllocId(AllocId) { - StateAnnotationIndex = - BC.MIB->getOrCreateAnnotationIndex("CFGUnawareSrcSafetyAnalysis"); + : SrcSafetyAnalysis(BF, RegsToTrackInstsFor), + CFGUnawareAnalysis(BF, AllocId, "CFGUnawareSrcSafetyAnalysis"), BF(BF) { } void run() override { @@ -687,12 +716,8 @@ class CFGUnawareSrcSafetyAnalysis : public SrcSafetyAnalysis { S = createUnsafeState(); } - // Check if we need to remove an old annotation (this is the case if - // this is the second, detailed, run of the analysis). - if (BC.MIB->hasAnnotation(Inst, StateAnnotationIndex)) - BC.MIB->removeAnnotation(Inst, StateAnnotationIndex); // Attach the state *before* this instruction executes. - BC.MIB->addAnnotation(Inst, StateAnnotationIndex, S, AllocId); + setState(Inst, S); // Compute the state after this instruction executes. S = computeNext(Inst, S); @@ -700,10 +725,8 @@ class CFGUnawareSrcSafetyAnalysis : public SrcSafetyAnalysis { } const SrcState &getStateBefore(const MCInst &Inst) const override { - return BC.MIB->getAnnotationAs(Inst, StateAnnotationIndex); + return getState(Inst); } - - ~CFGUnawareSrcSafetyAnalysis() { cleanStateAnnotations(); } }; std::shared_ptr @@ -717,6 +740,478 @@ SrcSafetyAnalysis::create(BinaryFunction &BF, RegsToTrackInstsFor); } +/// A state representing which registers are safe to be used as the destination +/// operand of an authentication instruction. +/// +/// Similar to SrcState, it is the responsibility of the analysis to take +/// register aliasing into account. +/// +/// Depending on the implementation (such as whether FEAT_FPAC is implemented +/// by an AArch64 CPU or not), it may be possible that an authentication +/// instruction returns an invalid pointer on failure instead of terminating +/// the program immediately (assuming the program will crash as soon as that +/// pointer is dereferenced). Since few bits are usually allocated for the PAC +/// field (such as less than 16 bits on a typical AArch64 system), an attacker +/// can try every possible signature and guess the correct one if there is a +/// gadget that tells whether the particular pointer has a correct signature +/// (a so called "authentication oracle"). For that reason, it should be +/// impossible for an attacker to test if a pointer is correctly signed - +/// either the program should be terminated on authentication failure or +/// the result of authentication should not be accessible to an attacker. +/// +/// Considering the instructions in forward order as they are executed, a +/// restricted set of operations can be allowed on any register containing a +/// value derived from the result of an authentication instruction until that +/// value is checked not to contain the result of a failed authentication. +/// In DstSafetyAnalysis, these rules are adapted, so that the safety property +/// for a register is computed by iterating the instructions in backward order. +/// Then the resulting properties are used at authentication instruction sites +/// to check output registers and report the particular instruction if it writes +/// to an unsafe register. +/// +/// Another approach would be to simulate the above rules as-is, iterating over +/// the instructions in forward direction. To make it possible to report the +/// particular instructions as oracles, this would probably require tracking +/// references to these instructions for each register currently containing +/// sensitive data. +/// +/// In DstSafetyAnalysis, the source register Xn of an instruction Inst is safe +/// if at least one of the following is true: +/// * Inst checks if Xn contains the result of a successful authentication and +/// terminates the program on failure. Note that Inst can either naturally +/// dereference Xn (load, branch, return, etc. instructions) or be the first +/// instruction of an explicit checking sequence. +/// * Inst performs safe address arithmetic AND both source and result +/// registers, as well as any temporary registers, must be safe after +/// execution of Inst (temporaries are not used on AArch64 and thus not +/// currently supported/allowed). +/// See MCPlusBuilder::analyzeAddressArithmeticsForPtrAuth for the details. +/// * Inst fully overwrites Xn with a constant. +struct DstState { + /// The set of registers whose values cannot be inspected by an attacker in + /// a way usable as an authentication oracle. The results of authentication + /// instructions should only be written to such registers. + BitVector CannotEscapeUnchecked; + + /// A vector of sets, only used on the second analysis run. + /// Each element in this vector represents one of the tracked registers. + /// For each such register we track the set of first instructions that leak + /// the authenticated pointer before it was checked. This is intended to + /// provide clues on which instruction made the particular register unsafe. + /// + /// Please note that the mapping from MCPhysReg values to indexes in this + /// vector is provided by RegsToTrackInstsFor field of DstSafetyAnalysis. + std::vector FirstInstLeakingReg; + + /// Constructs an empty state. + DstState() {} + + DstState(unsigned NumRegs, unsigned NumRegsToTrack) + : CannotEscapeUnchecked(NumRegs), FirstInstLeakingReg(NumRegsToTrack) {} + + DstState &merge(const DstState &StateIn) { + if (StateIn.empty()) + return *this; + if (empty()) + return (*this = StateIn); + + CannotEscapeUnchecked &= StateIn.CannotEscapeUnchecked; + for (unsigned I = 0; I < FirstInstLeakingReg.size(); ++I) + for (const MCInst *J : StateIn.FirstInstLeakingReg[I]) + FirstInstLeakingReg[I].insert(J); + return *this; + } + + /// Returns true if this object does not store state of any registers - + /// neither safe, nor unsafe ones. + bool empty() const { return CannotEscapeUnchecked.empty(); } + + bool operator==(const DstState &RHS) const { + return CannotEscapeUnchecked == RHS.CannotEscapeUnchecked && + FirstInstLeakingReg == RHS.FirstInstLeakingReg; + } + bool operator!=(const DstState &RHS) const { return !((*this) == RHS); } +}; + +static raw_ostream &operator<<(raw_ostream &OS, const DstState &S) { + OS << "dst-state<"; + if (S.empty()) { + OS << "empty"; + } else { + OS << "CannotEscapeUnchecked: " << S.CannotEscapeUnchecked << ", "; + printInstsShort(OS, S.FirstInstLeakingReg); + } + OS << ">"; + return OS; +} + +class DstStatePrinter { +public: + void print(raw_ostream &OS, const DstState &S) const; + explicit DstStatePrinter(const BinaryContext &BC) : BC(BC) {} + +private: + const BinaryContext &BC; +}; + +void DstStatePrinter::print(raw_ostream &OS, const DstState &S) const { + RegStatePrinter RegStatePrinter(BC); + OS << "dst-state<"; + if (S.empty()) { + assert(S.CannotEscapeUnchecked.empty()); + assert(S.FirstInstLeakingReg.empty()); + OS << "empty"; + } else { + OS << "CannotEscapeUnchecked: "; + RegStatePrinter.print(OS, S.CannotEscapeUnchecked); + OS << ", "; + printInstsShort(OS, S.FirstInstLeakingReg); + } + OS << ">"; +} + +/// Computes which registers are safe to be written to by auth instructions. +/// +/// This is the base class for two implementations: a dataflow-based analysis +/// which is intended to be used for most functions and a simplified CFG-unaware +/// version for functions without reconstructed CFG. +class DstSafetyAnalysis { +public: + DstSafetyAnalysis(BinaryFunction &BF, ArrayRef RegsToTrackInstsFor) + : BC(BF.getBinaryContext()), NumRegs(BC.MRI->getNumRegs()), + RegsToTrackInstsFor(RegsToTrackInstsFor) {} + + virtual ~DstSafetyAnalysis() {} + + static std::shared_ptr + create(BinaryFunction &BF, MCPlusBuilder::AllocatorIdTy AllocId, + ArrayRef RegsToTrackInstsFor); + + virtual void run() = 0; + virtual const DstState &getStateAfter(const MCInst &Inst) const = 0; + +protected: + BinaryContext &BC; + const unsigned NumRegs; + + const TrackedRegisters RegsToTrackInstsFor; + + /// Stores information about the detected instruction sequences emitted to + /// check an authenticated pointer. Specifically, if such sequence is detected + /// in a basic block, it maps the first instruction of that sequence to the + /// register being checked. + /// + /// As the detection of such sequences requires iterating over the adjacent + /// instructions, it should be done before calling computeNext(), which + /// operates on separate instructions. + DenseMap RegCheckedAt; + + SetOfRelatedInsts &firstLeakingInsts(DstState &S, MCPhysReg Reg) const { + unsigned Index = RegsToTrackInstsFor.getIndex(Reg); + return S.FirstInstLeakingReg[Index]; + } + const SetOfRelatedInsts &firstLeakingInsts(const DstState &S, + MCPhysReg Reg) const { + unsigned Index = RegsToTrackInstsFor.getIndex(Reg); + return S.FirstInstLeakingReg[Index]; + } + + /// Creates a state with all registers marked unsafe (not to be confused + /// with empty state). + DstState createUnsafeState() { + return DstState(NumRegs, RegsToTrackInstsFor.getNumTrackedRegisters()); + } + + /// Returns the set of registers that can be leaked by this instruction. + /// A register is considered leaked if it has any intersection with any + /// register read by Inst. This is similar to how the set of clobbered + /// registers is computed, but taking input operands instead of outputs. + BitVector getLeakedRegs(const MCInst &Inst) const { + BitVector Leaked(NumRegs); + + // Assume a call can read all registers. + if (BC.MIB->isCall(Inst)) { + Leaked.set(); + return Leaked; + } + + // Compute the set of registers overlapping with any register used by + // this instruction. + + const MCInstrDesc &Desc = BC.MII->get(Inst.getOpcode()); + + for (MCPhysReg Reg : Desc.implicit_uses()) + Leaked |= BC.MIB->getAliases(Reg, /*OnlySmaller=*/false); + + for (const MCOperand &Op : BC.MIB->useOperands(Inst)) { + if (Op.isReg()) + Leaked |= BC.MIB->getAliases(Op.getReg(), /*OnlySmaller=*/false); + } + + return Leaked; + } + + SmallVector getRegsMadeProtected(const MCInst &Inst, + const BitVector &LeakedRegs, + const DstState &Cur) const { + SmallVector Regs; + + // A pointer can be checked, or + if (auto CheckedReg = + BC.MIB->getAuthCheckedReg(Inst, /*MayOverwrite=*/true)) + Regs.push_back(*CheckedReg); + if (RegCheckedAt.contains(&Inst)) + Regs.push_back(RegCheckedAt.at(&Inst)); + + // ... it can be used as a branch target, or + if (BC.MIB->isIndirectBranch(Inst) || BC.MIB->isIndirectCall(Inst)) { + bool IsAuthenticated; + MCPhysReg BranchDestReg = + BC.MIB->getRegUsedAsIndirectBranchDest(Inst, IsAuthenticated); + assert(BranchDestReg != BC.MIB->getNoRegister()); + if (!IsAuthenticated) + Regs.push_back(BranchDestReg); + } + + // ... it can be used as a return target, or + if (BC.MIB->isReturn(Inst)) { + bool IsAuthenticated = false; + std::optional RetReg = + BC.MIB->getRegUsedAsRetDest(Inst, IsAuthenticated); + if (RetReg && !IsAuthenticated) + Regs.push_back(*RetReg); + } + + // ... an address can be updated in a safe manner, or + if (auto DstAndSrc = BC.MIB->analyzeAddressArithmeticsForPtrAuth(Inst)) { + MCPhysReg DstReg, SrcReg; + std::tie(DstReg, SrcReg) = *DstAndSrc; + // Note that *all* registers containing the derived values must be safe, + // both source and destination ones. No temporaries are supported at now. + if (Cur.CannotEscapeUnchecked[SrcReg] && + Cur.CannotEscapeUnchecked[DstReg]) + Regs.push_back(SrcReg); + } + + // ... the register can be overwritten in whole with a constant: for that + // purpose, look for the instructions with no register inputs (neither + // explicit nor implicit ones) and no side effects (to rule out reading + // not modelled locations). + const MCInstrDesc &Desc = BC.MII->get(Inst.getOpcode()); + bool HasExplicitSrcRegs = llvm::any_of(BC.MIB->useOperands(Inst), + [](auto Op) { return Op.isReg(); }); + if (!Desc.hasUnmodeledSideEffects() && !HasExplicitSrcRegs && + Desc.implicit_uses().empty()) { + for (const MCOperand &Def : BC.MIB->defOperands(Inst)) + Regs.push_back(Def.getReg()); + } + + return Regs; + } + + DstState computeNext(const MCInst &Point, const DstState &Cur) { + DstStatePrinter P(BC); + LLVM_DEBUG({ + dbgs() << " DstSafetyAnalysis::ComputeNext("; + BC.InstPrinter->printInst(&Point, 0, "", *BC.STI, dbgs()); + dbgs() << ", "; + P.print(dbgs(), Cur); + dbgs() << ")\n"; + }); + + // If this instruction is reachable by the analysis, a non-empty state will + // be propagated to it sooner or later. Until then, skip computeNext(). + if (Cur.empty()) { + LLVM_DEBUG( + { dbgs() << "Skipping computeNext(Point, Cur) as Cur is empty.\n"; }); + return DstState(); + } + + // First, compute various properties of the instruction, taking the state + // after its execution into account, if necessary. + + BitVector LeakedRegs = getLeakedRegs(Point); + SmallVector NewProtectedRegs = + getRegsMadeProtected(Point, LeakedRegs, Cur); + + // Then, compute the state before this instruction is executed. + DstState Next = Cur; + + Next.CannotEscapeUnchecked.reset(LeakedRegs); + for (MCPhysReg Reg : RegsToTrackInstsFor.getRegisters()) { + if (LeakedRegs[Reg]) + firstLeakingInsts(Next, Reg) = {&Point}; + } + + BitVector NewProtectedSubregs(NumRegs); + for (MCPhysReg Reg : NewProtectedRegs) + NewProtectedSubregs |= BC.MIB->getAliases(Reg, /*OnlySmaller=*/true); + Next.CannotEscapeUnchecked |= NewProtectedSubregs; + for (MCPhysReg Reg : RegsToTrackInstsFor.getRegisters()) { + if (NewProtectedSubregs[Reg]) + firstLeakingInsts(Next, Reg).clear(); + } + + LLVM_DEBUG({ + dbgs() << " .. result: ("; + P.print(dbgs(), Next); + dbgs() << ")\n"; + }); + + return Next; + } + +public: + std::vector getLeakingInsts(const MCInst &Inst, + BinaryFunction &BF, + MCPhysReg LeakedReg) const { + const DstState &S = getStateAfter(Inst); + + std::vector Result; + for (const MCInst *Inst : firstLeakingInsts(S, LeakedReg)) { + MCInstReference Ref = MCInstReference::get(Inst, BF); + assert(Ref && "Expected Inst to be found"); + Result.push_back(Ref); + } + return Result; + } +}; + +class DataflowDstSafetyAnalysis + : public DstSafetyAnalysis, + public DataflowAnalysis { + using DFParent = DataflowAnalysis; + friend DFParent; + + using DstSafetyAnalysis::BC; + using DstSafetyAnalysis::computeNext; + +public: + DataflowDstSafetyAnalysis(BinaryFunction &BF, + MCPlusBuilder::AllocatorIdTy AllocId, + ArrayRef RegsToTrackInstsFor) + : DstSafetyAnalysis(BF, RegsToTrackInstsFor), DFParent(BF, AllocId) {} + + const DstState &getStateAfter(const MCInst &Inst) const override { + // The dataflow analysis base class iterates backwards over the + // instructions, thus "after" vs. "before" difference. + return DFParent::getStateBefore(Inst).get(); + } + + void run() override { + for (BinaryBasicBlock &BB : Func) { + if (auto CheckerInfo = BC.MIB->getAuthCheckedReg(BB)) { + LLVM_DEBUG({ + dbgs() << "Found pointer checking sequence in " << BB.getName() + << ":\n"; + traceReg(BC, "Checked register", CheckerInfo->first); + traceInst(BC, "First instruction", *CheckerInfo->second); + }); + RegCheckedAt[CheckerInfo->second] = CheckerInfo->first; + } + } + DFParent::run(); + } + +protected: + void preflight() {} + + DstState getStartingStateAtBB(const BinaryBasicBlock &BB) { + // In general, the initial state should be empty, not everything-is-unsafe, + // to give a chance for some meaningful state to be propagated to BB from + // an indirectly reachable "exit basic block" ending with a return or tail + // call instruction. + // + // A basic block without any successors, on the other hand, can be + // pessimistically initialized to everything-is-unsafe: this will naturally + // handle both return and tail call instructions and is harmless for + // internal indirect branch instructions (such as computed gotos). + if (BB.succ_empty()) + return createUnsafeState(); + + return DstState(); + } + + DstState getStartingStateAtPoint(const MCInst &Point) { return DstState(); } + + void doConfluence(DstState &StateOut, const DstState &StateIn) { + DstStatePrinter P(BC); + LLVM_DEBUG({ + dbgs() << " DataflowDstSafetyAnalysis::Confluence(\n"; + dbgs() << " State 1: "; + P.print(dbgs(), StateOut); + dbgs() << "\n"; + dbgs() << " State 2: "; + P.print(dbgs(), StateIn); + dbgs() << ")\n"; + }); + + StateOut.merge(StateIn); + + LLVM_DEBUG({ + dbgs() << " merged state: "; + P.print(dbgs(), StateOut); + dbgs() << "\n"; + }); + } + + StringRef getAnnotationName() const { return "DataflowDstSafetyAnalysis"; } +}; + +class CFGUnawareDstSafetyAnalysis : public DstSafetyAnalysis, + public CFGUnawareAnalysis { + using DstSafetyAnalysis::BC; + BinaryFunction &BF; + +public: + CFGUnawareDstSafetyAnalysis(BinaryFunction &BF, + MCPlusBuilder::AllocatorIdTy AllocId, + ArrayRef RegsToTrackInstsFor) + : DstSafetyAnalysis(BF, RegsToTrackInstsFor), + CFGUnawareAnalysis(BF, AllocId, "CFGUnawareDstSafetyAnalysis"), BF(BF) { + } + + void run() override { + DstState S = createUnsafeState(); + for (auto &I : llvm::reverse(BF.instrs())) { + MCInst &Inst = I.second; + + // If Inst can change the control flow, we cannot be sure that the next + // instruction (to be executed in analyzed program) is the one processed + // on the previous iteration, thus pessimistically reset S before + // starting to analyze Inst. + if (BC.MIB->isCall(Inst) || BC.MIB->isBranch(Inst) || + BC.MIB->isReturn(Inst)) { + LLVM_DEBUG({ traceInst(BC, "Control flow instruction", Inst); }); + S = createUnsafeState(); + } + + // Attach the state *after* this instruction executes. + setState(Inst, S); + + // Compute the next state. + S = computeNext(Inst, S); + } + } + + const DstState &getStateAfter(const MCInst &Inst) const override { + return getState(Inst); + } +}; + +std::shared_ptr +DstSafetyAnalysis::create(BinaryFunction &BF, + MCPlusBuilder::AllocatorIdTy AllocId, + ArrayRef RegsToTrackInstsFor) { + if (BF.hasCFG()) + return std::make_shared(BF, AllocId, + RegsToTrackInstsFor); + return std::make_shared(BF, AllocId, + RegsToTrackInstsFor); +} + // This function could return PartialReport, but currently T is always // MCPhysReg, even though it is an implementation detail. static PartialReport make_generic_report(MCInstReference Location, @@ -808,6 +1303,37 @@ shouldReportSigningOracle(const BinaryContext &BC, const MCInstReference &Inst, return make_gadget_report(SigningOracleKind, Inst, *SignedReg); } +static std::optional> +shouldReportAuthOracle(const BinaryContext &BC, const MCInstReference &Inst, + const DstState &S) { + static const GadgetKind AuthOracleKind("authentication oracle found"); + + bool IsChecked = false; + std::optional AuthReg = + BC.MIB->getWrittenAuthenticatedReg(Inst, IsChecked); + if (!AuthReg || IsChecked) + return std::nullopt; + + LLVM_DEBUG({ + traceInst(BC, "Found auth inst", Inst); + traceReg(BC, "Authenticated reg", *AuthReg); + }); + + if (S.empty()) { + LLVM_DEBUG({ dbgs() << " DstState is empty!\n"; }); + return make_generic_report( + Inst, "Warning: no state computed for an authentication instruction " + "(possibly unreachable)"); + } + + LLVM_DEBUG( + { traceRegMask(BC, "safe output registers", S.CannotEscapeUnchecked); }); + if (S.CannotEscapeUnchecked[*AuthReg]) + return std::nullopt; + + return make_gadget_report(AuthOracleKind, Inst, *AuthReg); +} + template static void iterateOverInstrs(BinaryFunction &BF, T Fn) { if (BF.hasCFG()) { for (BinaryBasicBlock &BB : BF) @@ -889,6 +1415,52 @@ void FunctionAnalysisContext::augmentUnsafeUseReports( } } +void FunctionAnalysisContext::findUnsafeDefs( + SmallVector> &Reports) { + if (PacRetGadgetsOnly) + return; + + auto Analysis = DstSafetyAnalysis::create(BF, AllocatorId, {}); + LLVM_DEBUG({ dbgs() << "Running dst register safety analysis...\n"; }); + Analysis->run(); + LLVM_DEBUG({ + dbgs() << "After dst register safety analysis:\n"; + BF.dump(); + }); + + iterateOverInstrs(BF, [&](MCInstReference Inst) { + const DstState &S = Analysis->getStateAfter(Inst); + + if (auto Report = shouldReportAuthOracle(BC, Inst, S)) + Reports.push_back(*Report); + }); +} + +void FunctionAnalysisContext::augmentUnsafeDefReports( + ArrayRef> Reports) { + SmallVector RegsToTrack = collectRegsToTrack(Reports); + // Re-compute the analysis with register tracking. + auto Analysis = DstSafetyAnalysis::create(BF, AllocatorId, RegsToTrack); + LLVM_DEBUG( + { dbgs() << "\nRunning detailed dst register safety analysis...\n"; }); + Analysis->run(); + LLVM_DEBUG({ + dbgs() << "After detailed dst register safety analysis:\n"; + BF.dump(); + }); + + // Augment gadget reports. + for (auto &Report : Reports) { + MCInstReference Location = Report.Issue->Location; + LLVM_DEBUG({ traceInst(BC, "Attaching leakage info to", Location); }); + assert(Report.RequestedDetails && + "Should be removed by handleSimpleReports"); + auto DetailedInfo = std::make_shared( + Analysis->getLeakingInsts(Location, BF, *Report.RequestedDetails)); + Result.Diagnostics.emplace_back(Report.Issue, DetailedInfo); + } +} + void FunctionAnalysisContext::handleSimpleReports( SmallVector> &Reports) { // Before re-running the detailed analysis, process the reports which do not @@ -912,6 +1484,12 @@ void FunctionAnalysisContext::run() { handleSimpleReports(UnsafeUses); if (!UnsafeUses.empty()) augmentUnsafeUseReports(UnsafeUses); + + SmallVector> UnsafeDefs; + findUnsafeDefs(UnsafeDefs); + handleSimpleReports(UnsafeDefs); + if (!UnsafeDefs.empty()) + augmentUnsafeDefReports(UnsafeDefs); } void Analysis::runOnFunction(BinaryFunction &BF, @@ -1015,6 +1593,12 @@ void ClobberingInfo::print(raw_ostream &OS, printRelatedInstrs(OS, Location, ClobberingInstrs); } +void LeakageInfo::print(raw_ostream &OS, const MCInstReference Location) const { + OS << " The " << LeakingInstrs.size() + << " instructions that leak the affected registers are:\n"; + printRelatedInstrs(OS, Location, LeakingInstrs); +} + void GenericDiagnostic::generateReport(raw_ostream &OS, const BinaryContext &BC) const { printBasicInfo(OS, BC, Text); diff --git a/bolt/test/binary-analysis/AArch64/gs-pauth-authentication-oracles.s b/bolt/test/binary-analysis/AArch64/gs-pauth-authentication-oracles.s new file mode 100644 index 0000000000000..717bf40df3d02 --- /dev/null +++ b/bolt/test/binary-analysis/AArch64/gs-pauth-authentication-oracles.s @@ -0,0 +1,812 @@ +// RUN: %clang %cflags -march=armv8.3-a %s -o %t.exe +// RUN: llvm-bolt-binary-analysis --scanners=pacret %t.exe 2>&1 | FileCheck -check-prefix=PACRET %s +// RUN: llvm-bolt-binary-analysis --scanners=pauth %t.exe 2>&1 | FileCheck %s + +// The detection of compiler-generated explicit pointer checks is tested in +// gs-pauth-address-checks.s, for that reason only test here "dummy-load" and +// "high-bits-notbi" checkers, as the shortest examples of checkers that are +// detected per-instruction and per-BB. + +// PACRET-NOT: authentication oracle found in function + + .text + + .type sym,@function +sym: + ret + .size sym, .-sym + + .globl callee + .type callee,@function +callee: + ret + .size callee, .-callee + + .globl good_ret + .type good_ret,@function +good_ret: +// CHECK-NOT: good_ret + autia x0, x1 + ret x0 + .size good_ret, .-good_ret + + .globl good_call + .type good_call,@function +good_call: +// CHECK-NOT: good_call + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + + autia x0, x1 + blr x0 + + ldp x29, x30, [sp], #16 + autiasp + ret + .size good_call, .-good_call + + .globl good_branch + .type good_branch,@function +good_branch: +// CHECK-NOT: good_branch + autia x0, x1 + br x0 + .size good_branch, .-good_branch + + .globl good_load_other_reg + .type good_load_other_reg,@function +good_load_other_reg: +// CHECK-NOT: good_load_other_reg + autia x0, x1 + ldr x2, [x0] + ret + .size good_load_other_reg, .-good_load_other_reg + + .globl good_load_same_reg + .type good_load_same_reg,@function +good_load_same_reg: +// CHECK-NOT: good_load_same_reg + autia x0, x1 + ldr x0, [x0] + ret + .size good_load_same_reg, .-good_load_same_reg + + .globl good_explicit_check + .type good_explicit_check,@function +good_explicit_check: +// CHECK-NOT: good_explicit_check + autia x0, x1 + eor x16, x0, x0, lsl #1 + tbz x16, #62, 1f + brk 0x1234 +1: + ret + .size good_explicit_check, .-good_explicit_check + + .globl bad_unchecked + .type bad_unchecked,@function +bad_unchecked: +// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unchecked, basic block {{[^,]+}}, at address +// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1 +// CHECK-NEXT: The 0 instructions that leak the affected registers are: + autia x0, x1 + ret + .size bad_unchecked, .-bad_unchecked + + .globl bad_leaked_to_subroutine + .type bad_leaked_to_subroutine,@function +bad_leaked_to_subroutine: +// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_leaked_to_subroutine, basic block {{[^,]+}}, at address +// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1 +// CHECK-NEXT: The 1 instructions that leak the affected registers are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: bl callee +// CHECK-NEXT: This happens in the following basic block: +// CHECK-NEXT: {{[0-9a-f]+}}: paciasp +// CHECK-NEXT: {{[0-9a-f]+}}: stp x29, x30, [sp, #-0x10]! +// CHECK-NEXT: {{[0-9a-f]+}}: mov x29, sp +// CHECK-NEXT: {{[0-9a-f]+}}: autia x0, x1 +// CHECK-NEXT: {{[0-9a-f]+}}: bl callee +// CHECK-NEXT: {{[0-9a-f]+}}: ldr x2, [x0] +// CHECK-NEXT: {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// CHECK-NEXT: {{[0-9a-f]+}}: autiasp +// CHECK-NEXT: {{[0-9a-f]+}}: ret + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + + autia x0, x1 + bl callee + ldr x2, [x0] + + ldp x29, x30, [sp], #16 + autiasp + ret + .size bad_leaked_to_subroutine, .-bad_leaked_to_subroutine + + .globl bad_unknown_usage_read + .type bad_unknown_usage_read,@function +bad_unknown_usage_read: +// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unknown_usage_read, basic block {{[^,]+}}, at address +// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1 +// CHECK-NEXT: The 1 instructions that leak the affected registers are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: mul x3, x0, x1 +// CHECK-NEXT: This happens in the following basic block: +// CHECK-NEXT: {{[0-9a-f]+}}: autia x0, x1 +// CHECK-NEXT: {{[0-9a-f]+}}: mul x3, x0, x1 +// CHECK-NEXT: {{[0-9a-f]+}}: ldr x2, [x0] +// CHECK-NEXT: {{[0-9a-f]+}}: ret + autia x0, x1 + // Registers are not accessible to an attacker under Pointer + // Authentication threat model, until spilled to memory. + // Thus, reporting the below MUL instruction is a false positive, since + // the next LDR instruction prevents any possible spilling of x3 unless + // the authentication succeeded. Though, rejecting anything except for + // a closed list of instruction types is the intended behavior of the + // analysis, so this false positive is by design. + mul x3, x0, x1 + ldr x2, [x0] + ret + .size bad_unknown_usage_read, .-bad_unknown_usage_read + + .globl bad_store_to_memory_and_wait + .type bad_store_to_memory_and_wait,@function +bad_store_to_memory_and_wait: +// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_store_to_memory_and_wait, basic block {{[^,]+}}, at address +// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1 +// CHECK-NEXT: The 1 instructions that leak the affected registers are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: str x0, [x3] + autia x0, x1 + cbz x3, 2f + str x0, [x3] +1: + // The thread performs a time-consuming computation while the result of + // authentication is accessible in memory. + nop +2: + ldr x2, [x0] + ret + .size bad_store_to_memory_and_wait, .-bad_store_to_memory_and_wait + +// FIXME: Known false negative: if no return instruction is reachable from a +// program point (this probably implies an infinite loop), such +// instruction cannot be detected as an authentication oracle. + .globl bad_store_to_memory_and_hang + .type bad_store_to_memory_and_hang,@function +bad_store_to_memory_and_hang: +// CHECK-NOT: bad_store_to_memory_and_hang + autia x0, x1 + cbz x3, 2f + str x0, [x3] +1: + // The thread loops indefinitely while the result of authentication + // is accessible in memory. + b 1b +2: + ldr x2, [x0] + ret + .size bad_store_to_memory_and_hang, .-bad_store_to_memory_and_hang + + .globl bad_unknown_usage_subreg_read + .type bad_unknown_usage_subreg_read,@function +bad_unknown_usage_subreg_read: +// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unknown_usage_subreg_read, basic block {{[^,]+}}, at address +// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1 +// CHECK-NEXT: The 1 instructions that leak the affected registers are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: mul w3, w0, w1 +// CHECK-NEXT: This happens in the following basic block: +// CHECK-NEXT: {{[0-9a-f]+}}: autia x0, x1 +// CHECK-NEXT: {{[0-9a-f]+}}: mul w3, w0, w1 +// CHECK-NEXT: {{[0-9a-f]+}}: ldr x2, [x0] +// CHECK-NEXT: {{[0-9a-f]+}}: ret + autia x0, x1 + mul w3, w0, w1 + ldr x2, [x0] + ret + .size bad_unknown_usage_subreg_read, .-bad_unknown_usage_subreg_read + + .globl bad_unknown_usage_update + .type bad_unknown_usage_update,@function +bad_unknown_usage_update: +// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unknown_usage_update, basic block {{[^,]+}}, at address +// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1 +// CHECK-NEXT: The 1 instructions that leak the affected registers are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: movk x0, #0x2a, lsl #16 +// CHECK-NEXT: This happens in the following basic block: +// CHECK-NEXT: {{[0-9a-f]+}}: autia x0, x1 +// CHECK-NEXT: {{[0-9a-f]+}}: movk x0, #0x2a, lsl #16 +// CHECK-NEXT: {{[0-9a-f]+}}: ldr x2, [x0] +// CHECK-NEXT: {{[0-9a-f]+}}: ret + autia x0, x1 + movk x0, #42, lsl #16 // does not overwrite x0 completely + ldr x2, [x0] + ret + .size bad_unknown_usage_update, .-bad_unknown_usage_update + + .globl good_overwrite_with_constant + .type good_overwrite_with_constant,@function +good_overwrite_with_constant: +// CHECK-NOT: good_overwrite_with_constant + autia x0, x1 + mov x0, #42 + ret + .size good_overwrite_with_constant, .-good_overwrite_with_constant + +// Overwriting sensitive data by instructions with unmodelled side-effects is +// explicitly rejected, even though this particular MRS is safe. + .globl bad_overwrite_with_side_effects + .type bad_overwrite_with_side_effects,@function +bad_overwrite_with_side_effects: +// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_overwrite_with_side_effects, basic block {{[^,]+}}, at address +// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1 +// CHECK-NEXT: The 0 instructions that leak the affected registers are: + autia x0, x1 + mrs x0, CTR_EL0 + ret + .size bad_overwrite_with_side_effects, .-bad_overwrite_with_side_effects + +// Here the new value written by MUL to x0 is completely unrelated to the result +// of authentication, so this is a false positive. +// FIXME: Can/should we generalize overwriting by constant to handle such cases? + .globl good_unknown_overwrite + .type good_unknown_overwrite,@function +good_unknown_overwrite: +// CHECK-LABEL: GS-PAUTH: authentication oracle found in function good_unknown_overwrite, basic block {{[^,]+}}, at address +// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1 +// CHECK-NEXT: The 0 instructions that leak the affected registers are: + autia x0, x1 + mul x0, x1, x2 + ret + .size good_unknown_overwrite, .-good_unknown_overwrite + +// This is a false positive: when a general-purpose register is written to as +// a 32-bit register, its top 32 bits are zeroed, but according to LLVM +// representation, the instruction only overwrites the Wn register. + .globl good_wreg_overwrite + .type good_wreg_overwrite,@function +good_wreg_overwrite: +// CHECK-LABEL: GS-PAUTH: authentication oracle found in function good_wreg_overwrite, basic block {{[^,]+}}, at address +// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1 + autia x0, x1 + mov w0, #42 + ret + .size good_wreg_overwrite, .-good_wreg_overwrite + + .globl good_address_arith + .type good_address_arith,@function +good_address_arith: +// CHECK-NOT: good_address_arith + autia x0, x1 + + add x1, x0, #8 + sub x2, x1, #16 + mov x3, x2 + + ldr x4, [x3] + mov x0, #0 + mov x1, #0 + mov x2, #0 + + ret + .size good_address_arith, .-good_address_arith + + .globl good_ret_multi_bb + .type good_ret_multi_bb,@function +good_ret_multi_bb: +// CHECK-NOT: good_ret_multi_bb + autia x0, x1 + cbz x1, 1f + nop +1: + ret x0 + .size good_ret_multi_bb, .-good_ret_multi_bb + + .globl good_call_multi_bb + .type good_call_multi_bb,@function +good_call_multi_bb: +// CHECK-NOT: good_call_multi_bb + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + + autia x0, x1 + cbz x1, 1f + nop +1: + blr x0 + cbz x1, 2f + nop +2: + ldp x29, x30, [sp], #16 + autiasp + ret + .size good_call_multi_bb, .-good_call_multi_bb + + .globl good_branch_multi_bb + .type good_branch_multi_bb,@function +good_branch_multi_bb: +// CHECK-NOT: good_branch_multi_bb + autia x0, x1 + cbz x1, 1f + nop +1: + br x0 + .size good_branch_multi_bb, .-good_branch_multi_bb + + .globl good_load_other_reg_multi_bb + .type good_load_other_reg_multi_bb,@function +good_load_other_reg_multi_bb: +// CHECK-NOT: good_load_other_reg_multi_bb + autia x0, x1 + cbz x1, 1f + nop +1: + ldr x2, [x0] + cbz x1, 2f + nop +2: + ret + .size good_load_other_reg_multi_bb, .-good_load_other_reg_multi_bb + + .globl good_load_same_reg_multi_bb + .type good_load_same_reg_multi_bb,@function +good_load_same_reg_multi_bb: +// CHECK-NOT: good_load_same_reg_multi_bb + autia x0, x1 + cbz x1, 1f + nop +1: + ldr x0, [x0] + cbz x1, 2f + nop +2: + ret + .size good_load_same_reg_multi_bb, .-good_load_same_reg_multi_bb + + .globl good_explicit_check_multi_bb + .type good_explicit_check_multi_bb,@function +good_explicit_check_multi_bb: +// CHECK-NOT: good_explicit_check_multi_bb + autia x0, x1 + cbz x1, 1f + nop +1: + eor x16, x0, x0, lsl #1 + tbz x16, #62, 2f + brk 0x1234 +2: + cbz x1, 3f + nop +3: + ret + .size good_explicit_check_multi_bb, .-good_explicit_check_multi_bb + + .globl bad_unchecked_multi_bb + .type bad_unchecked_multi_bb,@function +bad_unchecked_multi_bb: +// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unchecked_multi_bb, basic block {{[^,]+}}, at address +// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1 +// CHECK-NEXT: The 0 instructions that leak the affected registers are: + autia x0, x1 + cbz x1, 1f + ldr x2, [x0] +1: + ret + .size bad_unchecked_multi_bb, .-bad_unchecked_multi_bb + + .globl bad_leaked_to_subroutine_multi_bb + .type bad_leaked_to_subroutine_multi_bb,@function +bad_leaked_to_subroutine_multi_bb: +// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_leaked_to_subroutine_multi_bb, basic block {{[^,]+}}, at address +// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1 +// CHECK-NEXT: The 1 instructions that leak the affected registers are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: bl callee + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + + autia x0, x1 + cbz x1, 1f + ldr x2, [x0] +1: + bl callee + ldr x2, [x0] + + ldp x29, x30, [sp], #16 + autiasp + ret + .size bad_leaked_to_subroutine_multi_bb, .-bad_leaked_to_subroutine_multi_bb + + .globl bad_unknown_usage_read_multi_bb + .type bad_unknown_usage_read_multi_bb,@function +bad_unknown_usage_read_multi_bb: +// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unknown_usage_read_multi_bb, basic block {{[^,]+}}, at address +// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1 +// CHECK-NEXT: The 1 instructions that leak the affected registers are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: mul x3, x0, x1 + autia x0, x1 + cbz x3, 1f + mul x3, x0, x1 +1: + ldr x2, [x0] + ret + .size bad_unknown_usage_read_multi_bb, .-bad_unknown_usage_read_multi_bb + + .globl bad_unknown_usage_subreg_read_multi_bb + .type bad_unknown_usage_subreg_read_multi_bb,@function +bad_unknown_usage_subreg_read_multi_bb: +// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unknown_usage_subreg_read_multi_bb, basic block {{[^,]+}}, at address +// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1 +// CHECK-NEXT: The 1 instructions that leak the affected registers are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: mul w3, w0, w1 + autia x0, x1 + cbz x3, 1f + mul w3, w0, w1 +1: + ldr x2, [x0] + ret + .size bad_unknown_usage_subreg_read_multi_bb, .-bad_unknown_usage_subreg_read_multi_bb + + .globl bad_unknown_usage_update_multi_bb + .type bad_unknown_usage_update_multi_bb,@function +bad_unknown_usage_update_multi_bb: +// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unknown_usage_update_multi_bb, basic block {{[^,]+}}, at address +// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1 +// CHECK-NEXT: The 1 instructions that leak the affected registers are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: movk x0, #0x2a, lsl #16 + autia x0, x1 + cbz x3, 1f + movk x0, #42, lsl #16 // does not overwrite x0 completely +1: + ldr x2, [x0] + ret + .size bad_unknown_usage_update_multi_bb, .-bad_unknown_usage_update_multi_bb + + .globl good_overwrite_with_constant_multi_bb + .type good_overwrite_with_constant_multi_bb,@function +good_overwrite_with_constant_multi_bb: +// CHECK-NOT: good_overwrite_with_constant_multi_bb + autia x0, x1 + cbz x3, 1f +1: + mov x0, #42 + ret + .size good_overwrite_with_constant_multi_bb, .-good_overwrite_with_constant_multi_bb + + .globl good_address_arith_multi_bb + .type good_address_arith_multi_bb,@function +good_address_arith_multi_bb: +// CHECK-NOT: good_address_arith_multi_bb + autia x0, x1 + cbz x3, 1f + + add x1, x0, #8 + sub x2, x1, #16 + mov x0, x2 + + mov x1, #0 + mov x2, #0 +1: + ldr x3, [x0] + ret + .size good_address_arith_multi_bb, .-good_address_arith_multi_bb + +// FIXME: Most *_nocfg test cases contain paciasp+autiasp instructions even if +// LR is not spilled - this is a workaround for RET instructions being +// reported as non-protected, because LR state is reset at every label. + + .globl good_ret_nocfg + .type good_ret_nocfg,@function +good_ret_nocfg: +// CHECK-NOT: good_ret_nocfg + adr x2, 1f + br x2 +1: + autia x0, x1 + + ret x0 + .size good_ret_nocfg, .-good_ret_nocfg + + .globl good_call_nocfg + .type good_call_nocfg,@function +good_call_nocfg: +// CHECK-NOT: good_call_nocfg + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + + adr x2, 1f + br x2 +1: + autia x0, x1 + blr x0 + + ldp x29, x30, [sp], #16 + autiasp + ret + .size good_call_nocfg, .-good_call_nocfg + + .globl good_branch_nocfg + .type good_branch_nocfg,@function +good_branch_nocfg: +// CHECK-NOT: good_branch_nocfg + adr x2, 1f + br x2 +1: + autia x0, x1 + br x0 + .size good_branch_nocfg, .-good_branch_nocfg + + .globl good_load_other_reg_nocfg + .type good_load_other_reg_nocfg,@function +good_load_other_reg_nocfg: +// CHECK-NOT: good_load_other_reg_nocfg + paciasp + adr x2, 1f + br x2 +1: + autia x0, x1 + ldr x2, [x0] + + autiasp + ret + .size good_load_other_reg_nocfg, .-good_load_other_reg_nocfg + + .globl good_load_same_reg_nocfg + .type good_load_same_reg_nocfg,@function +good_load_same_reg_nocfg: +// CHECK-NOT: good_load_same_reg_nocfg + paciasp + adr x2, 1f + br x2 +1: + autia x0, x1 + ldr x0, [x0] + + autiasp + ret + .size good_load_same_reg_nocfg, .-good_load_same_reg_nocfg + +// FIXME: Multi-instruction checker sequences are not supported without CFG. + + .globl bad_unchecked_nocfg + .type bad_unchecked_nocfg,@function +bad_unchecked_nocfg: +// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unchecked_nocfg, at address +// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1 +// CHECK-NEXT: The 0 instructions that leak the affected registers are: + paciasp + adr x2, 1f + br x2 +1: + autia x0, x1 + + autiasp + ret + .size bad_unchecked_nocfg, .-bad_unchecked_nocfg + + .globl bad_leaked_to_subroutine_nocfg + .type bad_leaked_to_subroutine_nocfg,@function +bad_leaked_to_subroutine_nocfg: +// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_leaked_to_subroutine_nocfg, at address +// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1 +// CHECK-NEXT: The 1 instructions that leak the affected registers are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: bl callee # Offset: 24 + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + + adr x2, 1f + br x2 +1: + autia x0, x1 + bl callee + ldr x2, [x0] + + ldp x29, x30, [sp], #16 + autiasp + ret + .size bad_leaked_to_subroutine_nocfg, .-bad_leaked_to_subroutine_nocfg + + .globl bad_unknown_usage_read_nocfg + .type bad_unknown_usage_read_nocfg,@function +bad_unknown_usage_read_nocfg: +// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unknown_usage_read_nocfg, at address +// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1 +// CHECK-NEXT: The 1 instructions that leak the affected registers are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: mul x3, x0, x1 + paciasp + adr x2, 1f + br x2 +1: + autia x0, x1 + mul x3, x0, x1 + ldr x2, [x0] + + autiasp + ret + .size bad_unknown_usage_read_nocfg, .-bad_unknown_usage_read_nocfg + + .globl bad_unknown_usage_subreg_read_nocfg + .type bad_unknown_usage_subreg_read_nocfg,@function +bad_unknown_usage_subreg_read_nocfg: +// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unknown_usage_subreg_read_nocfg, at address +// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1 +// CHECK-NEXT: The 1 instructions that leak the affected registers are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: mul w3, w0, w1 + paciasp + adr x2, 1f + br x2 +1: + autia x0, x1 + mul w3, w0, w1 + ldr x2, [x0] + + autiasp + ret + .size bad_unknown_usage_subreg_read_nocfg, .-bad_unknown_usage_subreg_read_nocfg + + .globl bad_unknown_usage_update_nocfg + .type bad_unknown_usage_update_nocfg,@function +bad_unknown_usage_update_nocfg: +// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unknown_usage_update_nocfg, at address +// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1 +// CHECK-NEXT: The 1 instructions that leak the affected registers are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: movk x0, #0x2a, lsl #16 + paciasp + adr x2, 1f + br x2 +1: + autia x0, x1 + movk x0, #42, lsl #16 // does not overwrite x0 completely + ldr x2, [x0] + + autiasp + ret + .size bad_unknown_usage_update_nocfg, .-bad_unknown_usage_update_nocfg + + .globl good_overwrite_with_constant_nocfg + .type good_overwrite_with_constant_nocfg,@function +good_overwrite_with_constant_nocfg: +// CHECK-NOT: good_overwrite_with_constant_nocfg + paciasp + adr x2, 1f + br x2 +1: + autia x0, x1 + mov x0, #42 + + autiasp + ret + .size good_overwrite_with_constant_nocfg, .-good_overwrite_with_constant_nocfg + + .globl good_address_arith_nocfg + .type good_address_arith_nocfg,@function +good_address_arith_nocfg: +// CHECK-NOT: good_address_arith_nocfg + paciasp + adr x2, 1f + br x2 +1: + autia x0, x1 + add x1, x0, #8 + sub x2, x1, #16 + mov x3, x2 + + ldr x4, [x3] + mov x0, #0 + mov x1, #0 + mov x2, #0 + + autiasp + ret + .size good_address_arith_nocfg, .-good_address_arith_nocfg + + .globl good_explicit_check_unrelated_reg + .type good_explicit_check_unrelated_reg,@function +good_explicit_check_unrelated_reg: +// CHECK-LABEL: GS-PAUTH: authentication oracle found in function good_explicit_check_unrelated_reg, basic block {{[^,]+}}, at address + // FIXME: The below instruction is not an authentication oracle + autia x2, x3 // One of possible execution paths after this instruction + // ends at BRK below, thus BRK used as a trap instruction + // should formally "check everything" not to introduce + // false-positive here. + autia x0, x1 + eor x16, x0, x0, lsl #1 + tbz x16, #62, 1f + brk 0x1234 +1: + ldr x4, [x2] // Right before this instruction X2 is checked - this + // should be propagated to the basic block ending with + // TBZ instruction above. + ret + .size good_explicit_check_unrelated_reg, .-good_explicit_check_unrelated_reg + +// The last BB (in layout order) is processed first by the data-flow analysis. +// Its initial state is usually filled in a special way (because it ends with +// `ret` instruction), and then affects the state propagated to the other BBs +// Thus, the case of the last instruction in a function being a jump somewhere +// in the middle is special. + + .globl good_no_ret_from_last_bb + .type good_no_ret_from_last_bb,@function +good_no_ret_from_last_bb: +// CHECK-NOT: good_no_ret_from_last_bb + paciasp + autiasp // authenticates LR + b 2f +1: + ret +2: + b 1b // LR is dereferenced by `ret`, which is executed next + .size good_no_ret_from_last_bb, .-good_no_ret_from_last_bb + + .globl bad_no_ret_from_last_bb + .type bad_no_ret_from_last_bb,@function +bad_no_ret_from_last_bb: +// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_no_ret_from_last_bb, basic block {{[^,]+}}, at address +// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autiasp +// CHECK-NEXT: The 0 instructions that leak the affected registers are: + paciasp + autiasp // authenticates LR + b 2f +1: + ret x0 +2: + b 1b // X0 (but not LR) is dereferenced by `ret x0` + .size bad_no_ret_from_last_bb, .-bad_no_ret_from_last_bb + +// Test that combined auth+something instructions are not reported as +// authentication oracles. + + .globl inst_retaa + .type inst_retaa,@function +inst_retaa: +// CHECK-NOT: inst_retaa + paciasp + retaa + .size inst_retaa, .-inst_retaa + + .globl inst_blraa + .type inst_blraa,@function +inst_blraa: +// CHECK-NOT: inst_blraa + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + + blraa x0, x1 + + ldp x29, x30, [sp], #16 + retaa + .size inst_blraa, .-inst_blraa + + .globl inst_braa + .type inst_braa,@function +inst_braa: +// CHECK-NOT: inst_braa + braa x0, x1 + .size inst_braa, .-inst_braa + + .globl inst_ldraa_no_wb + .type inst_ldraa_no_wb,@function +inst_ldraa_no_wb: +// CHECK-NOT: inst_ldraa_no_wb + ldraa x1, [x0] + ret + .size inst_ldraa_no_wb, .-inst_ldraa_no_wb + + .globl inst_ldraa_wb + .type inst_ldraa_wb,@function +inst_ldraa_wb: +// CHECK-NOT: inst_ldraa_wb + ldraa x1, [x0]! + ret + .size inst_ldraa_wb, .-inst_ldraa_wb + + .globl main + .type main,@function +main: + mov x0, 0 + ret + .size main, .-main diff --git a/bolt/test/binary-analysis/AArch64/gs-pauth-debug-output.s b/bolt/test/binary-analysis/AArch64/gs-pauth-debug-output.s index 82494d834a15c..686557eb1e529 100644 --- a/bolt/test/binary-analysis/AArch64/gs-pauth-debug-output.s +++ b/bolt/test/binary-analysis/AArch64/gs-pauth-debug-output.s @@ -113,7 +113,7 @@ simple: // CHECK-EMPTY: // PAUTH-NEXT: Found sign inst: 00000000: paciasp # DataflowSrcSafetyAnalysis: src-state // PAUTH-NEXT: Signed reg: LR -// PAUTH-NEXT: TrustedRegs: LR W30 W30_HI +// PAUTH-NEXT: TrustedRegs: LR W30 W30_HI{{[ \t]*$}} // PAUTH-NEXT: Found call inst: 00000000: blr x0 # DataflowSrcSafetyAnalysis: src-state // PAUTH-NEXT: Call destination reg: X0 // PAUTH-NEXT: SafeToDerefRegs: W0 X0 W0_HI{{[ \t]*$}} @@ -220,10 +220,10 @@ nocfg: // CHECK-EMPTY: // PAUTH-NEXT: Found call inst: 00000000: br x0 # UNKNOWN CONTROL FLOW # Offset: 4 # CFGUnawareSrcSafetyAnalysis: src-state // PAUTH-NEXT: Call destination reg: X0 -// PAUTH-NEXT: SafeToDerefRegs: LR W0 W30 X0 W0_HI W30_HI +// PAUTH-NEXT: SafeToDerefRegs: LR W0 W30 X0 W0_HI W30_HI{{[ \t]*$}} // CHECK-NEXT: Found RET inst: 00000000: ret # Offset: 8 # CFGUnawareSrcSafetyAnalysis: src-state // CHECK-NEXT: RetReg: LR -// CHECK-NEXT: SafeToDerefRegs: +// CHECK-NEXT: SafeToDerefRegs:{{[ \t]*$}} // CHECK-EMPTY: // CHECK-NEXT: Running detailed src register safety analysis... // CHECK-NEXT: SrcSafetyAnalysis::ComputeNext( adr x0, __ENTRY_nocfg@0x[[ENTRY_ADDR]], src-state) @@ -251,6 +251,84 @@ nocfg: // CHECK-EMPTY: // CHECK-NEXT: Attaching clobbering info to: 00000000: ret # Offset: 8 # CFGUnawareSrcSafetyAnalysis: src-state + .globl auth_oracle + .type auth_oracle,@function +auth_oracle: + autia x0, x1 + ret + .size auth_oracle, .-auth_oracle + +// CHECK-LABEL:Analyzing function auth_oracle, AllocatorId = 1 +// CHECK-NEXT: Binary Function "auth_oracle" { +// CHECK-NEXT: Number : 4 +// CHECK-NEXT: State : CFG constructed +// ... +// CHECK: BB Layout : [[BB0:[0-9a-zA-Z.]+]] +// CHECK-NEXT: } +// CHECK-NEXT: [[BB0]] (2 instructions, align : 1) +// CHECK-NEXT: Entry Point +// CHECK-NEXT: 00000000: autia x0, x1 +// CHECK-NEXT: 00000004: ret +// CHECK-EMPTY: +// CHECK-NEXT: DWARF CFI Instructions: +// CHECK-NEXT: +// CHECK-NEXT: End of Function "auth_oracle" +// CHECK-EMPTY: +// CHECK-NEXT: Running src register safety analysis... +// ... +// CHECK: After src register safety analysis: +// CHECK-NEXT: Binary Function "auth_oracle" { +// ... +// CHECK: End of Function "auth_oracle" +// ... +// PAUTH: Running dst register safety analysis... +// PAUTH-NEXT: DstSafetyAnalysis::ComputeNext( ret x30, dst-state) +// PAUTH-NEXT: .. result: (dst-state) +// PAUTH-NEXT: DstSafetyAnalysis::ComputeNext( autia x0, x1, dst-state) +// PAUTH-NEXT: .. result: (dst-state) +// PAUTH-NEXT: After dst register safety analysis: +// PAUTH-NEXT: Binary Function "auth_oracle" { +// PAUTH-NEXT: Number : 4 +// PAUTH-NEXT: State : CFG constructed +// ... +// PAUTH: BB Layout : [[BB0]] +// PAUTH-NEXT: } +// PAUTH-NEXT: [[BB0]] (2 instructions, align : 1) +// PAUTH-NEXT: Entry Point +// PAUTH-NEXT: 00000000: autia x0, x1 # DataflowDstSafetyAnalysis: dst-state +// PAUTH-NEXT: 00000004: ret # DataflowDstSafetyAnalysis: dst-state +// PAUTH-EMPTY: +// PAUTH-NEXT: DWARF CFI Instructions: +// PAUTH-NEXT: +// PAUTH-NEXT: End of Function "auth_oracle" +// PAUTH-EMPTY: +// PAUTH-NEXT: Found auth inst: 00000000: autia x0, x1 # DataflowDstSafetyAnalysis: dst-state +// PAUTH-NEXT: Authenticated reg: X0 +// PAUTH-NEXT: safe output registers: LR W30 W30_HI{{[ \t]*$}} +// PAUTH-EMPTY: +// PAUTH-NEXT: Running detailed dst register safety analysis... +// PAUTH-NEXT: DstSafetyAnalysis::ComputeNext( ret x30, dst-state) +// PAUTH-NEXT: .. result: (dst-state) +// PAUTH-NEXT: DstSafetyAnalysis::ComputeNext( autia x0, x1, dst-state) +// PAUTH-NEXT: .. result: (dst-state) +// PAUTH-NEXT: After detailed dst register safety analysis: +// PAUTH-NEXT: Binary Function "auth_oracle" { +// PAUTH-NEXT: Number : 4 +// PAUTH-NEXT: State : CFG constructed +// ... +// PAUTH: BB Layout : [[BB0]] +// PAUTH-NEXT: } +// PAUTH-NEXT: [[BB0]] (2 instructions, align : 1) +// PAUTH-NEXT: Entry Point +// PAUTH-NEXT: 00000000: autia x0, x1 # DataflowDstSafetyAnalysis: dst-state +// PAUTH-NEXT: 00000004: ret # DataflowDstSafetyAnalysis: dst-state +// PAUTH-EMPTY: +// PAUTH-NEXT: DWARF CFI Instructions: +// PAUTH-NEXT: +// PAUTH-NEXT: End of Function "auth_oracle" +// PAUTH-EMPTY: +// PAUTH-NEXT: Attaching leakage info to: 00000000: autia x0, x1 # DataflowDstSafetyAnalysis: dst-state + // CHECK-LABEL:Analyzing function main, AllocatorId = 1 .globl main .type main,@function