diff --git a/bolt/docs/BinaryAnalysis.md b/bolt/docs/BinaryAnalysis.md index f91b77d046de8..9f0f018980517 100644 --- a/bolt/docs/BinaryAnalysis.md +++ b/bolt/docs/BinaryAnalysis.md @@ -9,9 +9,182 @@ analyses implemented in the BOLT libraries. ## Which binary analyses are implemented? -At the moment, no binary analyses are implemented. +* [Security scanners](#security-scanners) + * [pac-ret analysis](#pac-ret-analysis) -The goal is to make it easy using a plug-in framework to add your own analyses. +### Security scanners + +For the past 25 years, a large numbers of exploits have been built and used in +the wild to undermine computer security. The majority of these exploits abuse +memory vulnerabilities in programs, see evidence from +[Microsoft](https://youtu.be/PjbGojjnBZQ?si=oCHCa0SHgaSNr6Gr&t=836), +[Chromium](https://www.chromium.org/Home/chromium-security/memory-safety/) and +[Android](https://security.googleblog.com/2021/01/data-driven-security-hardening-in.html). + +It is not surprising therefore, that a large number of mitigations have been +added to instruction sets and toolchains to make it harder to build an exploit +using a memory vulnerability. Examples are: stack canaries, stack clash, +pac-ret, shadow stacks, arm64e, and many more. + +These mitigations guarantee a so-called "security property" on the binaries they +produce. For example, for stack canaries, the security property is roughly that +a canary is located on the stack between the set of saved registers and the set +of local variables. For pac-ret, it is roughly that either the return address is +never stored/retrieved to/from memory; or, there are no writes to the register +containing the return address between an instruction authenticating it and a +return instruction using it. + +From time to time, however, a bug gets found in the implementation of such +mitigations in toolchains. Also, code that is written in assembler by hand +requires the developer to ensure these security properties by hand. + +In short, it is sometimes found that a few places in the binary code are not +protected as well as expected given the requested mitigations. Attackers could +make use of those places (sometimes called gadgets) to circumvent the protection +that the mitigation should give. + +One of the reasons that such gadgets, or holes in the mitigation implementation, +exist is that typically the amount of testing and verification for these +security properties is limited to checking results on specific examples. + +In comparison, for testing functional correctness, or for testing performance, +toolchain and software in general typically get tested with large test suites +and benchmarks. In contrast, this typically does not get done for testing the +security properties of binary code. + +Unlike functional correctness where compilation errors result in test failures, +and performance where speed and size differences are measurable, broken security +properties cannot be easily observed using existing testing and benchmarking +tools. + +The security scanners implemented in `llvm-bolt-binary-analysis` aim to enable +the testing of security hardening in arbitrary programs and not just specific +examples. + + +#### pac-ret analysis + +`pac-ret` protection is a security hardening scheme implemented in compilers +such as GCC and Clang, using the command line option +`-mbranch-protection=pac-ret`. This option is enabled by default on most widely +used Linux distributions. + +The hardening scheme mitigates +[Return-Oriented Programming (ROP)](https://llsoftsec.github.io/llsoftsecbook/#return-oriented-programming) +attacks by making sure that return addresses are only ever stored to memory with +a cryptographic hash, called a +["Pointer Authentication Code" (PAC)](https://llsoftsec.github.io/llsoftsecbook/#pointer-authentication), +in the upper bits of the pointer. This makes it substantially harder for +attackers to divert control flow by overwriting a return address with a +different value. + +The hardening scheme relies on compilers producing appropriate code sequences when +processing return addresses, especially when these are stored to and retrieved +from memory. + +The `pac-ret` binary analysis can be invoked using the command line option +`--scanners=pac-ret`. It makes `llvm-bolt-binary-analysis` scan through the +provided binary, checking each function for the following security property: + +> For each procedure and exception return instruction, the destination register +> must have one of the following properties: +> +> 1. be immutable within the function, or +> 2. the last write to the register must be by an authenticating instruction. This +> includes combined authentication and return instructions such as `RETAA`. + +##### Example 1 + +For example, a typical non-pac-ret-protected function looks as follows: + +``` + stp x29, x30, [sp, #-0x10]! + mov x29, sp + bl g@PLT + add x0, x0, #0x3 + ldp x29, x30, [sp], #0x10 + ret +``` + +The return instruction `ret` implicitly uses register `x30` as the address to +return to. Register `x30` was last written by instruction `ldp`, which is not an +authenticating instruction. `llvm-bolt-binary-analysis --scanners=pac-ret` will +report this as follows: + +``` +GS-PACRET: non-protected ret found in function f1, basic block .LBB00, at address 10310 + The return instruction is 00010310: ret # pacret-gadget: pac-ret-gadget, Overwriting:[MCInstBBRef ]> + The 1 instructions that write to the return register after any authentication are: + 1. 0001030c: ldp x29, x30, [sp], #0x10 + This happens in the following basic block: + 000102fc: stp x29, x30, [sp, #-0x10]! + 00010300: mov x29, sp + 00010304: bl g@PLT + 00010308: add x0, x0, #0x3 + 0001030c: ldp x29, x30, [sp], #0x10 + 00010310: ret # pacret-gadget: pac-ret-gadget, Overwriting:[MCInstBBRef ]> +``` + +The exact format of how `llvm-bolt-binary-analysis` reports this is expected to +evolve over time. + +##### Example 2: multiple "last-overwriting" instructions + +A simple example that shows how there can be a set of "last overwriting" +instructions of a register follows: + +``` + paciasp + stp x29, x30, [sp, #-0x10]! + ldp x29, x30, [sp], #0x10 + cbnz x0, 1f + autiasp +1: + ret +``` + +This will produce the following diagnostic: + +``` +GS-PACRET: non-protected ret found in function f_crossbb1, basic block .Ltmp0, at address 102dc + The return instruction is 000102dc: ret # pacret-gadget: pac-ret-gadget, Overwriting:[MCInstBBRef MCInstBBRef ]> + The 2 instructions that write to the return register after any authentication are: + 1. 000102d0: ldp x29, x30, [sp], #0x10 + 2. 000102d8: autiasp +``` + +(Yes, this diagnostic could be improved because the second "overwriting" +instruction, `autiasp`, is an authenticating instruction...) + +##### Known false positives or negatives + +The following are current known cases of false positives: + +1. Not handling "no-return" functions. See issue + [#115154](https://github.com/llvm/llvm-project/issues/115154) for details and + pointers to open PRs to fix this. +2. Not recognizing that a move of a properly authenticated value between registers, + results in the destination register having a properly authenticated value. + For example, the scanner currently produces a false negative for the following + code sequence: + ``` + autiasp + mov x16, x30 + ret x16 + ``` + +The following are current known cases of false negatives: + +1. Not handling functions for which the CFG cannot be reconstructed by BOLT. The + plan is to implement support for this, picking up the implementation from the + [prototype branch]( + https://github.com/llvm/llvm-project/compare/main...kbeyls:llvm-project:bolt-gadget-scanner-prototype). + +BOLT cannot currently handle functions with `cfi_negate_ra_state` correctly, +i.e. any binaries built with `-mbranch-protection=pac-ret`. The scanner is meant +to be used on specifically such binaries, so this is a major limitation! Work is +going on in PR [#120064](https://github.com/llvm/llvm-project/pull/120064) to +fix this. ## How to add your own binary analysis diff --git a/bolt/include/bolt/Core/MCPlusBuilder.h b/bolt/include/bolt/Core/MCPlusBuilder.h index 3634fed9757ce..fd62f2e22d3e1 100644 --- a/bolt/include/bolt/Core/MCPlusBuilder.h +++ b/bolt/include/bolt/Core/MCPlusBuilder.h @@ -27,6 +27,7 @@ #include "llvm/MC/MCInstrAnalysis.h" #include "llvm/MC/MCInstrDesc.h" #include "llvm/MC/MCInstrInfo.h" +#include "llvm/MC/MCRegister.h" #include "llvm/Support/Allocator.h" #include "llvm/Support/Casting.h" #include "llvm/Support/ErrorHandling.h" @@ -550,6 +551,22 @@ class MCPlusBuilder { return Analysis->isReturn(Inst); } + virtual ErrorOr getAuthenticatedReg(const MCInst &Inst) const { + llvm_unreachable("not implemented"); + return getNoRegister(); + } + + virtual bool isAuthenticationOfReg(const MCInst &Inst, + MCPhysReg AuthenticatedReg) const { + llvm_unreachable("not implemented"); + return false; + } + + virtual ErrorOr getRegUsedAsRetDest(const MCInst &Inst) const { + llvm_unreachable("not implemented"); + return getNoRegister(); + } + virtual bool isTerminator(const MCInst &Inst) const; virtual bool isNoop(const MCInst &Inst) const { diff --git a/bolt/include/bolt/Passes/NonPacProtectedRetAnalysis.h b/bolt/include/bolt/Passes/NonPacProtectedRetAnalysis.h new file mode 100644 index 0000000000000..a701a202bf71f --- /dev/null +++ b/bolt/include/bolt/Passes/NonPacProtectedRetAnalysis.h @@ -0,0 +1,259 @@ +//===- bolt/Passes/NonPacProtectedRetAnalysis.h -----------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef BOLT_PASSES_NONPACPROTECTEDRETANALYSIS_H +#define BOLT_PASSES_NONPACPROTECTEDRETANALYSIS_H + +#include "bolt/Core/BinaryContext.h" +#include "bolt/Core/BinaryFunction.h" +#include "bolt/Passes/BinaryPasses.h" +#include "llvm/ADT/SmallSet.h" +#include "llvm/Support/raw_ostream.h" +#include "llvm/Testing/Annotations/Annotations.h" +#include + +namespace llvm { +namespace bolt { + +/// @brief MCInstReference represents a reference to an MCInst as stored either +/// in a BinaryFunction (i.e. before a CFG is created), or in a BinaryBasicBlock +/// (after a CFG is created). It aims to store the necessary information to be +/// able to find the specific MCInst in either the BinaryFunction or +/// BinaryBasicBlock data structures later, so that e.g. the InputAddress of +/// the corresponding instruction can be computed. + +struct MCInstInBBReference { + BinaryBasicBlock *BB; + int64_t BBIndex; + MCInstInBBReference(BinaryBasicBlock *BB, int64_t BBIndex) + : BB(BB), BBIndex(BBIndex) {} + MCInstInBBReference() : BB(nullptr), BBIndex(0) {} + static MCInstInBBReference get(const MCInst *Inst, BinaryFunction &BF) { + for (BinaryBasicBlock &BB : BF) + for (size_t I = 0; I < BB.size(); ++I) + if (Inst == &BB.getInstructionAtIndex(I)) + return MCInstInBBReference(&BB, I); + return {}; + } + bool operator==(const MCInstInBBReference &RHS) const { + return BB == RHS.BB && BBIndex == RHS.BBIndex; + } + bool operator<(const MCInstInBBReference &RHS) const { + if (BB != RHS.BB) + return BB < RHS.BB; + return BBIndex < RHS.BBIndex; + } + operator MCInst &() const { + assert(BB != nullptr); + return BB->getInstructionAtIndex(BBIndex); + } + uint64_t getAddress() const { + // 4 bytes per instruction on AArch64. + // FIXME: the assumption of 4 byte per instruction needs to be fixed before + // this method gets used on any non-AArch64 binaries (but should be fine for + // pac-ret analysis, as that is an AArch64-specific feature). + return BB->getFunction()->getAddress() + BB->getOffset() + BBIndex * 4; + } +}; + +raw_ostream &operator<<(raw_ostream &OS, const MCInstInBBReference &); + +struct MCInstInBFReference { + BinaryFunction *BF; + uint64_t Offset; + MCInstInBFReference(BinaryFunction *BF, uint64_t Offset) + : BF(BF), Offset(Offset) {} + MCInstInBFReference() : BF(nullptr), Offset(0) {} + bool operator==(const MCInstInBFReference &RHS) const { + return BF == RHS.BF && Offset == RHS.Offset; + } + bool operator<(const MCInstInBFReference &RHS) const { + if (BF != RHS.BF) + return BF < RHS.BF; + return Offset < RHS.Offset; + } + operator MCInst &() const { + assert(BF != nullptr); + return *BF->getInstructionAtOffset(Offset); + } + + uint64_t getOffset() const { return Offset; } + + uint64_t getAddress() const { return BF->getAddress() + getOffset(); } +}; + +raw_ostream &operator<<(raw_ostream &OS, const MCInstInBFReference &); + +struct MCInstReference { + enum Kind { FunctionParent, BasicBlockParent }; + Kind ParentKind; + union U { + MCInstInBBReference BBRef; + MCInstInBFReference BFRef; + U(MCInstInBBReference BBRef) : BBRef(BBRef) {} + U(MCInstInBFReference BFRef) : BFRef(BFRef) {} + } U; + MCInstReference(MCInstInBBReference BBRef) + : ParentKind(BasicBlockParent), U(BBRef) {} + MCInstReference(MCInstInBFReference BFRef) + : ParentKind(FunctionParent), U(BFRef) {} + MCInstReference(BinaryBasicBlock *BB, int64_t BBIndex) + : MCInstReference(MCInstInBBReference(BB, BBIndex)) {} + MCInstReference(BinaryFunction *BF, uint32_t Offset) + : MCInstReference(MCInstInBFReference(BF, Offset)) {} + + bool operator<(const MCInstReference &RHS) const { + if (ParentKind != RHS.ParentKind) + return ParentKind < RHS.ParentKind; + switch (ParentKind) { + case BasicBlockParent: + return U.BBRef < RHS.U.BBRef; + case FunctionParent: + return U.BFRef < RHS.U.BFRef; + } + llvm_unreachable(""); + } + + bool operator==(const MCInstReference &RHS) const { + if (ParentKind != RHS.ParentKind) + return false; + switch (ParentKind) { + case BasicBlockParent: + return U.BBRef == RHS.U.BBRef; + case FunctionParent: + return U.BFRef == RHS.U.BFRef; + } + llvm_unreachable(""); + } + + operator MCInst &() const { + switch (ParentKind) { + case BasicBlockParent: + return U.BBRef; + case FunctionParent: + return U.BFRef; + } + llvm_unreachable(""); + } + + uint64_t getAddress() const { + switch (ParentKind) { + case BasicBlockParent: + return U.BBRef.getAddress(); + case FunctionParent: + return U.BFRef.getAddress(); + } + llvm_unreachable(""); + } + + BinaryFunction *getFunction() const { + switch (ParentKind) { + case FunctionParent: + return U.BFRef.BF; + case BasicBlockParent: + return U.BBRef.BB->getFunction(); + } + llvm_unreachable(""); + } + + BinaryBasicBlock *getBasicBlock() const { + switch (ParentKind) { + case FunctionParent: + return nullptr; + case BasicBlockParent: + return U.BBRef.BB; + } + llvm_unreachable(""); + } +}; + +raw_ostream &operator<<(raw_ostream &OS, const MCInstReference &); + +struct GeneralDiagnostic { + std::string Text; + GeneralDiagnostic(const std::string &Text) : Text(Text) {} + bool operator==(const GeneralDiagnostic &RHS) const { + return Text == RHS.Text; + } +}; + +raw_ostream &operator<<(raw_ostream &OS, const GeneralDiagnostic &Diag); + +namespace NonPacProtectedRetAnalysis { +struct Annotation { + MCInstReference RetInst; + Annotation(MCInstReference RetInst) : RetInst(RetInst) {} + virtual bool operator==(const Annotation &RHS) const { + return RetInst == RHS.RetInst; + } + Annotation &operator=(const Annotation &Other) { + if (this == &Other) + return *this; + RetInst = Other.RetInst; + return *this; + } + virtual ~Annotation() {} + virtual void generateReport(raw_ostream &OS, + const BinaryContext &BC) const = 0; +}; + +struct Gadget : public Annotation { + std::vector OverwritingRetRegInst; + virtual bool operator==(const Gadget &RHS) const { + return Annotation::operator==(RHS) && + OverwritingRetRegInst == RHS.OverwritingRetRegInst; + } + Gadget(MCInstReference RetInst, + const std::vector &OverwritingRetRegInst) + : Annotation(RetInst), OverwritingRetRegInst(OverwritingRetRegInst) {} + virtual void generateReport(raw_ostream &OS, + const BinaryContext &BC) const override; +}; + +struct GenDiag : public Annotation { + GeneralDiagnostic Diag; + virtual bool operator==(const GenDiag &RHS) const { + return Annotation::operator==(RHS) && Diag == RHS.Diag; + } + GenDiag(MCInstReference RetInst, const std::string &Text) + : Annotation(RetInst), Diag(Text) {} + virtual void generateReport(raw_ostream &OS, + const BinaryContext &BC) const override; +}; + +class PacRetAnalysis; + +struct FunctionAnalysisResult { + SmallSet RegistersAffected; + std::vector> Diagnostics; +}; + +class Analysis : public BinaryFunctionPass { + void runOnFunction(BinaryFunction &Function, + MCPlusBuilder::AllocatorIdTy AllocatorId); + FunctionAnalysisResult + computeDfState(PacRetAnalysis &PRA, BinaryFunction &BF, + MCPlusBuilder::AllocatorIdTy AllocatorId); + + std::map AnalysisResults; + std::mutex AnalysisResultsMutex; + +public: + explicit Analysis() : BinaryFunctionPass(false) {} + + const char *getName() const override { return "non-pac-protected-rets"; } + + /// Pass entry point + Error runOnFunctions(BinaryContext &BC) override; +}; + +} // namespace NonPacProtectedRetAnalysis +} // namespace bolt +} // namespace llvm + +#endif diff --git a/bolt/include/bolt/Utils/CommandLineOpts.h b/bolt/include/bolt/Utils/CommandLineOpts.h index 111eb650c3746..fefd7969ef6f4 100644 --- a/bolt/include/bolt/Utils/CommandLineOpts.h +++ b/bolt/include/bolt/Utils/CommandLineOpts.h @@ -80,6 +80,10 @@ extern llvm::cl::opt Verbosity; /// Return true if we should process all functions in the binary. bool processAllFunctions(); +enum GadgetScannerKind { GS_PACRET, GS_ALL }; + +extern llvm::cl::list GadgetScannersToRun; + } // namespace opts namespace llvm { diff --git a/bolt/lib/Passes/CMakeLists.txt b/bolt/lib/Passes/CMakeLists.txt index 1c1273b3d2420..b3fbb0ba0108f 100644 --- a/bolt/lib/Passes/CMakeLists.txt +++ b/bolt/lib/Passes/CMakeLists.txt @@ -23,6 +23,7 @@ add_llvm_library(LLVMBOLTPasses LoopInversionPass.cpp LivenessAnalysis.cpp MCF.cpp + NonPacProtectedRetAnalysis.cpp PatchEntries.cpp PettisAndHansen.cpp PLTCall.cpp diff --git a/bolt/lib/Passes/NonPacProtectedRetAnalysis.cpp b/bolt/lib/Passes/NonPacProtectedRetAnalysis.cpp new file mode 100644 index 0000000000000..dc7cb275f5664 --- /dev/null +++ b/bolt/lib/Passes/NonPacProtectedRetAnalysis.cpp @@ -0,0 +1,526 @@ +//===- bolt/Passes/NonPacProtectedRetAnalysis.cpp -------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// This file implements a pass that looks for any AArch64 return instructions +// that may not be protected by PAuth authentication instructions when needed. +// +//===----------------------------------------------------------------------===// + +#include "bolt/Passes/NonPacProtectedRetAnalysis.h" +#include "bolt/Core/ParallelUtilities.h" +#include "bolt/Passes/DataflowAnalysis.h" +#include "llvm/ADT/SmallSet.h" +#include "llvm/MC/MCInst.h" +#include "llvm/Support/Format.h" +#include + +#define DEBUG_TYPE "bolt-nonpacprotectedret" + +namespace llvm { +namespace bolt { + +raw_ostream &operator<<(raw_ostream &OS, const MCInstInBBReference &Ref) { + OS << "MCInstBBRef<"; + if (Ref.BB == nullptr) + OS << "BB:(null)"; + else + OS << "BB:" << Ref.BB->getName() << ":" << Ref.BBIndex; + OS << ">"; + return OS; +} + +raw_ostream &operator<<(raw_ostream &OS, const MCInstInBFReference &Ref) { + OS << "MCInstBFRef<"; + if (Ref.BF == nullptr) + OS << "BF:(null)"; + else + OS << "BF:" << Ref.BF->getPrintName() << ":" << Ref.getOffset(); + OS << ">"; + return OS; +} + +raw_ostream &operator<<(raw_ostream &OS, const MCInstReference &Ref) { + switch (Ref.ParentKind) { + case MCInstReference::BasicBlockParent: + OS << Ref.U.BBRef; + return OS; + case MCInstReference::FunctionParent: + OS << Ref.U.BFRef; + return OS; + } + llvm_unreachable(""); +} + +namespace NonPacProtectedRetAnalysis { + +// The security property that is checked is: +// When a register is used as the address to jump to in a return instruction, +// that register must either: +// (a) never be changed within this function, i.e. have the same value as when +// the function started, or +// (b) the last write to the register must be by an authentication instruction. + +// This property is checked by using dataflow analysis to keep track of which +// registers have been written (def-ed), since last authenticated. Those are +// exactly the registers containing values that should not be trusted (as they +// could have changed since the last time they were authenticated). For pac-ret, +// any return instruction using such a register is a gadget to be reported. For +// PAuthABI, probably at least any indirect control flow using such a register +// should be reported. + +// Furthermore, when producing a diagnostic for a found non-pac-ret protected +// return, the analysis also lists the last instructions that wrote to the +// register used in the return instruction. +// The total set of registers used in return instructions in a given function is +// small. It almost always is just `X30`. +// In order to reduce the memory consumption of storing this additional state +// during the dataflow analysis, this is computed by running the dataflow +// analysis twice: +// 1. In the first run, the dataflow analysis only keeps track of the security +// property: i.e. which registers have been overwritten since the last +// time they've been authenticated. +// 2. If the first run finds any return instructions using a register last +// written by a non-authenticating instruction, the dataflow analysis will +// be run a second time. The first run will return which registers are used +// in the gadgets to be reported. This information is used in the second run +// to also track which instructions last wrote to those registers. + +struct State { + /// A BitVector containing the registers that have been clobbered, and + /// not authenticated. + BitVector NonAutClobRegs; + /// A vector of sets, only used in the second data flow run. + /// Each element in the vector represents one of the registers for which we + /// track the set of last instructions that wrote to this register. For + /// 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; + State() {} + State(unsigned NumRegs, unsigned NumRegsToTrack) + : NonAutClobRegs(NumRegs), LastInstWritingReg(NumRegsToTrack) {} + State &operator|=(const State &StateIn) { + NonAutClobRegs |= StateIn.NonAutClobRegs; + for (unsigned I = 0; I < LastInstWritingReg.size(); ++I) + for (const MCInst *J : StateIn.LastInstWritingReg[I]) + LastInstWritingReg[I].insert(J); + return *this; + } + bool operator==(const State &RHS) const { + return NonAutClobRegs == RHS.NonAutClobRegs && + LastInstWritingReg == RHS.LastInstWritingReg; + } + bool operator!=(const State &RHS) const { return !((*this) == RHS); } +}; + +static void printLastInsts( + raw_ostream &OS, + const std::vector> &LastInstWritingReg) { + OS << "Insts: "; + for (unsigned I = 0; I < LastInstWritingReg.size(); ++I) { + auto &Set = LastInstWritingReg[I]; + OS << "[" << I << "]("; + for (const MCInst *MCInstP : Set) + OS << MCInstP << " "; + OS << ")"; + } +} + +raw_ostream &operator<<(raw_ostream &OS, const State &S) { + OS << "pacret-state<"; + OS << "NonAutClobRegs: " << S.NonAutClobRegs << ", "; + printLastInsts(OS, S.LastInstWritingReg); + OS << ">"; + return OS; +} + +class PacStatePrinter { +public: + void print(raw_ostream &OS, const State &State) const; + explicit PacStatePrinter(const BinaryContext &BC) : BC(BC) {} + +private: + const BinaryContext &BC; +}; + +void PacStatePrinter::print(raw_ostream &OS, const State &S) const { + RegStatePrinter RegStatePrinter(BC); + OS << "pacret-state<"; + OS << "NonAutClobRegs: "; + RegStatePrinter.print(OS, S.NonAutClobRegs); + OS << ", "; + printLastInsts(OS, S.LastInstWritingReg); + OS << ">"; +} + +class PacRetAnalysis + : public DataflowAnalysis { + using Parent = + DataflowAnalysis; + friend Parent; + +public: + PacRetAnalysis(BinaryFunction &BF, MCPlusBuilder::AllocatorIdTy AllocId, + const std::vector &RegsToTrackInstsFor) + : Parent(BF, AllocId), NumRegs(BF.getBinaryContext().MRI->getNumRegs()), + RegsToTrackInstsFor(RegsToTrackInstsFor), + TrackingLastInsts(!RegsToTrackInstsFor.empty()), + Reg2StateIdx(RegsToTrackInstsFor.empty() + ? 0 + : *llvm::max_element(RegsToTrackInstsFor) + 1, + -1) { + for (unsigned I = 0; I < RegsToTrackInstsFor.size(); ++I) + Reg2StateIdx[RegsToTrackInstsFor[I]] = I; + } + virtual ~PacRetAnalysis() {} + +protected: + const unsigned NumRegs; + /// RegToTrackInstsFor is the set of registers for which the dataflow analysis + /// must compute which the last set of instructions writing to it are. + const std::vector RegsToTrackInstsFor; + const bool TrackingLastInsts; + /// Reg2StateIdx maps Register to the index in the vector used in State to + /// track which instructions last wrote to this register. + std::vector Reg2StateIdx; + + SmallPtrSet &lastWritingInsts(State &S, + MCPhysReg Reg) const { + assert(Reg < Reg2StateIdx.size()); + assert(isTrackingReg(Reg)); + return S.LastInstWritingReg[Reg2StateIdx[Reg]]; + } + const SmallPtrSet &lastWritingInsts(const State &S, + MCPhysReg Reg) const { + assert(Reg < Reg2StateIdx.size()); + assert(isTrackingReg(Reg)); + return S.LastInstWritingReg[Reg2StateIdx[Reg]]; + } + + bool isTrackingReg(MCPhysReg Reg) const { + return llvm::is_contained(RegsToTrackInstsFor, Reg); + } + + void preflight() {} + + State getStartingStateAtBB(const BinaryBasicBlock &BB) { + return State(NumRegs, RegsToTrackInstsFor.size()); + } + + State getStartingStateAtPoint(const MCInst &Point) { + return State(NumRegs, RegsToTrackInstsFor.size()); + } + + void doConfluence(State &StateOut, const State &StateIn) { + PacStatePrinter P(BC); + LLVM_DEBUG({ + dbgs() << " PacRetAnalysis::Confluence(\n"; + dbgs() << " State 1: "; + P.print(dbgs(), StateOut); + dbgs() << "\n"; + dbgs() << " State 2: "; + P.print(dbgs(), StateIn); + dbgs() << ")\n"; + }); + + StateOut |= StateIn; + + LLVM_DEBUG({ + dbgs() << " merged state: "; + P.print(dbgs(), StateOut); + dbgs() << "\n"; + }); + } + + State computeNext(const MCInst &Point, const State &Cur) { + PacStatePrinter P(BC); + LLVM_DEBUG({ + dbgs() << " PacRetAnalysis::ComputeNext("; + BC.InstPrinter->printInst(&const_cast(Point), 0, "", *BC.STI, + dbgs()); + dbgs() << ", "; + P.print(dbgs(), Cur); + dbgs() << ")\n"; + }); + + State Next = Cur; + BitVector Written = BitVector(NumRegs, false); + // Assume a call can clobber all registers, including callee-saved + // registers. There's a good chance that callee-saved registers will be + // saved on the stack at some point during execution of the callee. + // Therefore they should also be considered as potentially modified by an + // attacker/written to. + // Also, not all functions may respect the AAPCS ABI rules about + // caller/callee-saved registers. + if (BC.MIB->isCall(Point)) + Written.set(); + else + // FIXME: `getWrittenRegs` only sets the register directly written in the + // instruction, and the smaller aliasing registers. It does not set the + // larger aliasing registers. To also set the larger aliasing registers, + // we'd have to call `getClobberedRegs`. + // It is unclear if there is any test case which shows a different + // behaviour between using `getWrittenRegs` vs `getClobberedRegs`. We'd + // first would like to see such a test case before making a decision + // on whether using `getClobberedRegs` below would be better. + // Also see the discussion on this at + // https://github.com/llvm/llvm-project/pull/122304#discussion_r1939511909 + BC.MIB->getWrittenRegs(Point, Written); + Next.NonAutClobRegs |= Written; + // Keep track of this instruction if it writes to any of the registers we + // need to track that for: + for (MCPhysReg Reg : RegsToTrackInstsFor) + if (Written[Reg]) + lastWritingInsts(Next, Reg) = {&Point}; + + ErrorOr AutReg = BC.MIB->getAuthenticatedReg(Point); + if (AutReg && *AutReg != BC.MIB->getNoRegister()) { + // FIXME: should we use `OnlySmaller=false` below? See similar + // FIXME about `getWrittenRegs` above and further discussion about this + // at + // https://github.com/llvm/llvm-project/pull/122304#discussion_r1939515516 + Next.NonAutClobRegs.reset( + BC.MIB->getAliases(*AutReg, /*OnlySmaller=*/true)); + if (TrackingLastInsts && isTrackingReg(*AutReg)) + lastWritingInsts(Next, *AutReg).clear(); + } + + LLVM_DEBUG({ + dbgs() << " .. result: ("; + P.print(dbgs(), Next); + dbgs() << ")\n"; + }); + + return Next; + } + + StringRef getAnnotationName() const { return StringRef("PacRetAnalysis"); } + +public: + std::vector + getLastClobberingInsts(const MCInst Ret, BinaryFunction &BF, + const BitVector &UsedDirtyRegs) const { + if (!TrackingLastInsts) + return {}; + auto MaybeState = getStateAt(Ret); + if (!MaybeState) + llvm_unreachable("Expected State to be present"); + const State &S = *MaybeState; + // Due to aliasing registers, multiple registers may have been tracked. + std::set LastWritingInsts; + for (MCPhysReg TrackedReg : UsedDirtyRegs.set_bits()) { + for (const MCInst *Inst : lastWritingInsts(S, TrackedReg)) + LastWritingInsts.insert(Inst); + } + std::vector Result; + for (const MCInst *Inst : LastWritingInsts) { + MCInstInBBReference Ref = MCInstInBBReference::get(Inst, BF); + assert(Ref.BB != nullptr && "Expected Inst to be found"); + Result.push_back(MCInstReference(Ref)); + } + return Result; + } +}; + +FunctionAnalysisResult +Analysis::computeDfState(PacRetAnalysis &PRA, BinaryFunction &BF, + MCPlusBuilder::AllocatorIdTy AllocatorId) { + PRA.run(); + LLVM_DEBUG({ + dbgs() << " After PacRetAnalysis:\n"; + BF.dump(); + }); + + FunctionAnalysisResult Result; + // Now scan the CFG for non-authenticating return instructions that use an + // overwritten, non-authenticated register as return address. + BinaryContext &BC = BF.getBinaryContext(); + for (BinaryBasicBlock &BB : BF) { + for (int64_t I = BB.size() - 1; I >= 0; --I) { + MCInst &Inst = BB.getInstructionAtIndex(I); + if (BC.MIB->isReturn(Inst)) { + ErrorOr MaybeRetReg = BC.MIB->getRegUsedAsRetDest(Inst); + if (MaybeRetReg.getError()) { + Result.Diagnostics.push_back(std::make_shared( + MCInstInBBReference(&BB, I), + "Warning: pac-ret analysis could not analyze this return " + "instruction")); + continue; + } + MCPhysReg RetReg = *MaybeRetReg; + LLVM_DEBUG({ + dbgs() << " Found RET inst: "; + BC.printInstruction(dbgs(), Inst); + dbgs() << " RetReg: " << BC.MRI->getName(RetReg) + << "; authenticatesReg: " + << BC.MIB->isAuthenticationOfReg(Inst, RetReg) << "\n"; + }); + if (BC.MIB->isAuthenticationOfReg(Inst, RetReg)) + break; + BitVector UsedDirtyRegs = PRA.getStateAt(Inst)->NonAutClobRegs; + LLVM_DEBUG({ + dbgs() << " NonAutClobRegs at Ret: "; + RegStatePrinter RSP(BC); + RSP.print(dbgs(), UsedDirtyRegs); + dbgs() << "\n"; + }); + UsedDirtyRegs &= BC.MIB->getAliases(RetReg, /*OnlySmaller=*/true); + LLVM_DEBUG({ + dbgs() << " Intersection with RetReg: "; + RegStatePrinter RSP(BC); + RSP.print(dbgs(), UsedDirtyRegs); + dbgs() << "\n"; + }); + if (UsedDirtyRegs.any()) { + // This return instruction needs to be reported + Result.Diagnostics.push_back(std::make_shared( + MCInstInBBReference(&BB, I), + PRA.getLastClobberingInsts(Inst, BF, UsedDirtyRegs))); + for (MCPhysReg RetRegWithGadget : UsedDirtyRegs.set_bits()) + Result.RegistersAffected.insert(RetRegWithGadget); + } + } + } + } + return Result; +} + +void Analysis::runOnFunction(BinaryFunction &BF, + MCPlusBuilder::AllocatorIdTy AllocatorId) { + LLVM_DEBUG({ + dbgs() << "Analyzing in function " << BF.getPrintName() << ", AllocatorId " + << AllocatorId << "\n"; + BF.dump(); + }); + + if (BF.hasCFG()) { + PacRetAnalysis PRA(BF, AllocatorId, {}); + FunctionAnalysisResult FAR = computeDfState(PRA, BF, AllocatorId); + if (!FAR.RegistersAffected.empty()) { + // Redo the analysis, but now also track which instructions last wrote + // to any of the registers in RetRegsWithGadgets, so that better + // diagnostics can be produced. + std::vector RegsToTrack; + for (MCPhysReg R : FAR.RegistersAffected) + RegsToTrack.push_back(R); + PacRetAnalysis PRWIA(BF, AllocatorId, RegsToTrack); + FAR = computeDfState(PRWIA, BF, AllocatorId); + } + + // `runOnFunction` is typically getting called from multiple threads in + // parallel. Therefore, use a lock to avoid data races when storing the + // result of the analysis in the `AnalysisResults` map. + { + std::lock_guard Lock(AnalysisResultsMutex); + AnalysisResults[&BF] = FAR; + } + } +} + +static void printBB(const BinaryContext &BC, const BinaryBasicBlock *BB, + size_t StartIndex = 0, size_t EndIndex = -1) { + if (EndIndex == (size_t)-1) + EndIndex = BB->size() - 1; + const BinaryFunction *BF = BB->getFunction(); + for (unsigned I = StartIndex; I <= EndIndex; ++I) { + // FIXME: this assumes all instructions are 4 bytes in size. This is true + // for AArch64, but it might be good to extract this function so it can be + // used elsewhere and for other targets too. + uint64_t Address = BB->getOffset() + BF->getAddress() + 4 * I; + const MCInst &Inst = BB->getInstructionAtIndex(I); + if (BC.MIB->isCFI(Inst)) + continue; + BC.printInstruction(outs(), Inst, Address, BF); + } +} + +static void reportFoundGadgetInSingleBBSingleOverwInst( + raw_ostream &OS, const BinaryContext &BC, const MCInstReference OverwInst, + const MCInstReference RetInst) { + BinaryBasicBlock *BB = RetInst.getBasicBlock(); + assert(OverwInst.ParentKind == MCInstReference::BasicBlockParent); + assert(RetInst.ParentKind == MCInstReference::BasicBlockParent); + MCInstInBBReference OverwInstBB = OverwInst.U.BBRef; + if (BB == OverwInstBB.BB) { + // overwriting inst and ret instruction are in the same basic block. + assert(OverwInstBB.BBIndex < RetInst.U.BBRef.BBIndex); + OS << " This happens in the following basic block:\n"; + printBB(BC, BB); + } +} + +void Gadget::generateReport(raw_ostream &OS, const BinaryContext &BC) const { + GenDiag(RetInst, "non-protected ret found").generateReport(OS, BC); + + BinaryFunction *BF = RetInst.getFunction(); + OS << " The " << OverwritingRetRegInst.size() + << " instructions that write to the return register after any " + "authentication are:\n"; + // Sort by address to ensure output is deterministic. + std::vector ORRI = OverwritingRetRegInst; + llvm::sort(ORRI, [](const MCInstReference &A, const MCInstReference &B) { + return A.getAddress() < B.getAddress(); + }); + for (unsigned I = 0; I < ORRI.size(); ++I) { + MCInstReference InstRef = ORRI[I]; + OS << " " << (I + 1) << ". "; + BC.printInstruction(OS, InstRef, InstRef.getAddress(), BF); + }; + LLVM_DEBUG({ + dbgs() << " .. OverWritingRetRegInst:\n"; + for (MCInstReference Ref : OverwritingRetRegInst) { + dbgs() << " " << Ref << "\n"; + } + }); + if (OverwritingRetRegInst.size() == 1) { + const MCInstReference OverwInst = OverwritingRetRegInst[0]; + assert(OverwInst.ParentKind == MCInstReference::BasicBlockParent); + reportFoundGadgetInSingleBBSingleOverwInst(OS, BC, OverwInst, RetInst); + } +} + +void GenDiag::generateReport(raw_ostream &OS, const BinaryContext &BC) const { + BinaryFunction *BF = RetInst.getFunction(); + BinaryBasicBlock *BB = RetInst.getBasicBlock(); + + OS << "\nGS-PACRET: " << Diag.Text; + OS << " in function " << BF->getPrintName(); + if (BB) + OS << ", basic block " << BB->getName(); + OS << ", at address " << llvm::format("%x", RetInst.getAddress()) << "\n"; + OS << " The return instruction is "; + BC.printInstruction(OS, RetInst, RetInst.getAddress(), BF); +} + +Error Analysis::runOnFunctions(BinaryContext &BC) { + ParallelUtilities::WorkFuncWithAllocTy WorkFun = + [&](BinaryFunction &BF, MCPlusBuilder::AllocatorIdTy AllocatorId) { + runOnFunction(BF, AllocatorId); + }; + + ParallelUtilities::PredicateTy SkipFunc = [&](const BinaryFunction &BF) { + return false; + }; + + ParallelUtilities::runOnEachFunctionWithUniqueAllocId( + BC, ParallelUtilities::SchedulingPolicy::SP_INST_LINEAR, WorkFun, + SkipFunc, "NonPacProtectedRetAnalysis"); + + for (BinaryFunction *BF : BC.getAllBinaryFunctions()) + if (AnalysisResults.count(BF) > 0) { + for (const std::shared_ptr &A : + AnalysisResults[BF].Diagnostics) + A->generateReport(outs(), BC); + } + return Error::success(); +} + +} // namespace NonPacProtectedRetAnalysis +} // namespace bolt +} // namespace llvm diff --git a/bolt/lib/Rewrite/RewriteInstance.cpp b/bolt/lib/Rewrite/RewriteInstance.cpp index 4329235d47049..42a5978c7ed1d 100644 --- a/bolt/lib/Rewrite/RewriteInstance.cpp +++ b/bolt/lib/Rewrite/RewriteInstance.cpp @@ -20,6 +20,7 @@ #include "bolt/Passes/BinaryPasses.h" #include "bolt/Passes/CacheMetrics.h" #include "bolt/Passes/IdenticalCodeFolding.h" +#include "bolt/Passes/NonPacProtectedRetAnalysis.h" #include "bolt/Passes/ReorderFunctions.h" #include "bolt/Profile/BoltAddressTranslation.h" #include "bolt/Profile/DataAggregator.h" @@ -245,6 +246,13 @@ static cl::opt WriteBoltInfoSection( "bolt-info", cl::desc("write bolt info section in the output binary"), cl::init(true), cl::Hidden, cl::cat(BoltOutputCategory)); +cl::list + GadgetScannersToRun("scanners", cl::desc("which gadget scanners to run"), + cl::values(clEnumValN(GS_PACRET, "pacret", "pac-ret"), + clEnumValN(GS_ALL, "all", "all")), + cl::ZeroOrMore, cl::CommaSeparated, + cl::cat(BinaryAnalysisCategory)); + } // namespace opts // FIXME: implement a better way to mark sections for replacement. @@ -3490,7 +3498,24 @@ void RewriteInstance::runOptimizationPasses() { BC->logBOLTErrorsAndQuitOnFatal(BinaryFunctionPassManager::runAllPasses(*BC)); } -void RewriteInstance::runBinaryAnalyses() {} +void RewriteInstance::runBinaryAnalyses() { + NamedRegionTimer T("runBinaryAnalyses", "run binary analysis passes", + TimerGroupName, TimerGroupDesc, opts::TimeRewrite); + BinaryFunctionPassManager Manager(*BC); + // FIXME: add a pass that warns about which functions do not have CFG, + // and therefore, analysis is most likely to be less accurate. + using GSK = opts::GadgetScannerKind; + // if no command line option was given, act as if "all" was specified. + if (opts::GadgetScannersToRun.empty()) + opts::GadgetScannersToRun.addValue(GSK::GS_ALL); + for (GSK ScannerToRun : opts::GadgetScannersToRun) { + if (ScannerToRun == GSK::GS_PACRET || ScannerToRun == GSK::GS_ALL) + Manager.registerPass( + std::make_unique()); + } + + BC->logBOLTErrorsAndQuitOnFatal(Manager.runPasses()); +} void RewriteInstance::preregisterSections() { // Preregister sections before emission to set their order in the output. diff --git a/bolt/lib/Target/AArch64/AArch64MCPlusBuilder.cpp b/bolt/lib/Target/AArch64/AArch64MCPlusBuilder.cpp index 679c9774c767f..167ad2e34c591 100644 --- a/bolt/lib/Target/AArch64/AArch64MCPlusBuilder.cpp +++ b/bolt/lib/Target/AArch64/AArch64MCPlusBuilder.cpp @@ -23,6 +23,7 @@ #include "llvm/MC/MCFixupKindInfo.h" #include "llvm/MC/MCInstBuilder.h" #include "llvm/MC/MCInstrInfo.h" +#include "llvm/MC/MCRegister.h" #include "llvm/MC/MCRegisterInfo.h" #include "llvm/Support/DataExtractor.h" #include "llvm/Support/Debug.h" @@ -148,6 +149,88 @@ class AArch64MCPlusBuilder : public MCPlusBuilder { return false; } + ErrorOr getAuthenticatedReg(const MCInst &Inst) const override { + switch (Inst.getOpcode()) { + case AArch64::AUTIAZ: + case AArch64::AUTIBZ: + case AArch64::AUTIASP: + case AArch64::AUTIBSP: + case AArch64::AUTIASPPCi: + case AArch64::AUTIBSPPCi: + case AArch64::AUTIASPPCr: + case AArch64::AUTIBSPPCr: + case AArch64::RETAA: + case AArch64::RETAB: + case AArch64::RETAASPPCi: + case AArch64::RETABSPPCi: + case AArch64::RETAASPPCr: + case AArch64::RETABSPPCr: + return AArch64::LR; + + case AArch64::AUTIA1716: + case AArch64::AUTIB1716: + case AArch64::AUTIA171615: + case AArch64::AUTIB171615: + return AArch64::X17; + + case AArch64::ERETAA: + case AArch64::ERETAB: + // The ERETA{A,B} instructions use either register ELR_EL1, ELR_EL2 or + // ELR_EL3, depending on the current Exception Level at run-time. + // + // Furthermore, these registers are not modelled by LLVM as a regular + // MCPhysReg.... So there is no way to indicate that through the current + // API. + // + // Therefore, return an error to indicate that LLVM/BOLT cannot model + // this. + return make_error_code(std::errc::result_out_of_range); + + case AArch64::AUTIA: + case AArch64::AUTIB: + case AArch64::AUTDA: + case AArch64::AUTDB: + case AArch64::AUTIZA: + case AArch64::AUTIZB: + case AArch64::AUTDZA: + case AArch64::AUTDZB: + return Inst.getOperand(0).getReg(); + + // FIXME: BL?RA(A|B)Z? and LDRA(A|B) should probably be handled here too. + + default: + return getNoRegister(); + } + } + + bool isAuthenticationOfReg(const MCInst &Inst, MCPhysReg Reg) const override { + if (Reg == getNoRegister()) + return false; + ErrorOr AuthenticatedReg = getAuthenticatedReg(Inst); + return AuthenticatedReg.getError() ? false : *AuthenticatedReg == Reg; + } + + ErrorOr getRegUsedAsRetDest(const MCInst &Inst) const override { + assert(isReturn(Inst)); + switch (Inst.getOpcode()) { + case AArch64::RET: + return Inst.getOperand(0).getReg(); + case AArch64::RETAA: + case AArch64::RETAB: + case AArch64::RETAASPPCi: + case AArch64::RETABSPPCi: + case AArch64::RETAASPPCr: + case AArch64::RETABSPPCr: + return AArch64::LR; + case AArch64::ERET: + case AArch64::ERETAA: + case AArch64::ERETAB: + return make_error_code(std::errc::result_out_of_range); + default: + llvm_unreachable("Unhandled return instruction"); + } + } + bool isADRP(const MCInst &Inst) const override { return Inst.getOpcode() == AArch64::ADRP; } diff --git a/bolt/test/binary-analysis/AArch64/cmdline-args.test b/bolt/test/binary-analysis/AArch64/cmdline-args.test index e414818644a3b..1204d5b1289af 100644 --- a/bolt/test/binary-analysis/AArch64/cmdline-args.test +++ b/bolt/test/binary-analysis/AArch64/cmdline-args.test @@ -13,7 +13,7 @@ NONEXISTINGFILEARG: llvm-bolt-binary-analysis: 'non-existing-file': No suc RUN: not llvm-bolt-binary-analysis %p/Inputs/dummy.txt 2>&1 | FileCheck -check-prefix=NOELFFILEARG %s NOELFFILEARG: llvm-bolt-binary-analysis: '{{.*}}/Inputs/dummy.txt': The file was not recognized as a valid object file. -RUN: %clang %cflags %p/../../Inputs/asm_foo.s %p/../../Inputs/asm_main.c -o %t.exe +RUN: %clang %cflags -Wl,--emit-relocs %p/../../Inputs/asm_foo.s %p/../../Inputs/asm_main.c -o %t.exe RUN: llvm-bolt-binary-analysis %t.exe 2>&1 | FileCheck -check-prefix=VALIDELFFILEARG --allow-empty %s # Check that there are no BOLT-WARNING or BOLT-ERROR output lines VALIDELFFILEARG: BOLT-INFO: @@ -30,4 +30,10 @@ HELP-NEXT: USAGE: llvm-bolt-binary-analysis [options] HELP-EMPTY: HELP-NEXT: OPTIONS: HELP-EMPTY: +HELP-NEXT: BinaryAnalysis options: +HELP-EMPTY: +HELP-NEXT: --scanners= - which gadget scanners to run +HELP-NEXT: =pacret - pac-ret +HELP-NEXT: =all - all +HELP-EMPTY: HELP-NEXT: Generic Options: diff --git a/bolt/test/binary-analysis/AArch64/gs-pacret-autiasp.s b/bolt/test/binary-analysis/AArch64/gs-pacret-autiasp.s new file mode 100644 index 0000000000000..cc2fe96d00496 --- /dev/null +++ b/bolt/test/binary-analysis/AArch64/gs-pacret-autiasp.s @@ -0,0 +1,952 @@ +// RUN: %clang %cflags -march=armv9.5-a+pauth-lr -mbranch-protection=pac-ret %s %p/../../Inputs/asm_main.c -o %t.exe +// RUN: llvm-bolt-binary-analysis --scanners=pacret %t.exe 2>&1 | FileCheck %s + + .text + + .globl f1 + .type f1,@function +f1: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + // autiasp +// CHECK-LABEL: GS-PACRET: non-protected ret found in function f1, basic block .LBB{{[0-9]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: ret +// CHECK-NEXT: The 1 instructions that write to the return register after any authentication are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// 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]+}}: bl g@PLT +// CHECK-NEXT: {{[0-9a-f]+}}: add x0, x0, #0x3 +// CHECK-NEXT: {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// CHECK-NEXT: {{[0-9a-f]+}}: ret + ret + .size f1, .-f1 + + + .globl f_intermediate_overwrite1 + .type f_intermediate_overwrite1,@function +f_intermediate_overwrite1: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + autiasp + ldp x29, x30, [sp], #16 +// CHECK-LABEL: GS-PACRET: non-protected ret found in function f_intermediate_overwrite1, basic block .LBB +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: ret +// CHECK-NEXT: The 1 instructions that write to the return register after any authentication are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// 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]+}}: bl g@PLT +// CHECK-NEXT: {{[0-9a-f]+}}: add x0, x0, #0x3 +// CHECK-NEXT: {{[0-9a-f]+}}: autiasp +// CHECK-NEXT: {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// CHECK-NEXT: {{[0-9a-f]+}}: ret + ret + .size f_intermediate_overwrite1, .-f_intermediate_overwrite1 + + .globl f_intermediate_overwrite2 + .type f_intermediate_overwrite2,@function +f_intermediate_overwrite2: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autiasp + mov x30, x0 +// CHECK-LABEL: GS-PACRET: non-protected ret found in function f_intermediate_overwrite2, basic block .LBB{{[0-9]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: ret +// CHECK-NEXT: The 1 instructions that write to the return register after any authentication are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: mov x30, x0 +// 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]+}}: bl g@PLT +// CHECK-NEXT: {{[0-9a-f]+}}: add x0, x0, #0x3 +// CHECK-NEXT: {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// CHECK-NEXT: {{[0-9a-f]+}}: autiasp +// CHECK-NEXT: {{[0-9a-f]+}}: mov x30, x0 +// CHECK-NEXT: {{[0-9a-f]+}}: ret + ret + .size f_intermediate_overwrite2, .-f_intermediate_overwrite2 + + .globl f_intermediate_read + .type f_intermediate_read,@function +f_intermediate_read: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autiasp + mov x0, x30 +// CHECK-NOT: function f_intermediate_read + ret + .size f_intermediate_read, .-f_intermediate_read + + .globl f_intermediate_overwrite3 + .type f_intermediate_overwrite3,@function +f_intermediate_overwrite3: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autiasp + mov w30, w0 +// CHECK-LABEL: GS-PACRET: non-protected ret found in function f_intermediate_overwrite3, basic block .LBB{{[0-9]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: ret +// CHECK-NEXT: The 1 instructions that write to the return register after any authentication are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: mov w30, w0 +// 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]+}}: bl g@PLT +// CHECK-NEXT: {{[0-9a-f]+}}: add x0, x0, #0x3 +// CHECK-NEXT: {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// CHECK-NEXT: {{[0-9a-f]+}}: autiasp +// CHECK-NEXT: {{[0-9a-f]+}}: mov w30, w0 +// CHECK-NEXT: {{[0-9a-f]+}}: ret + ret + .size f_intermediate_overwrite3, .-f_intermediate_overwrite3 + + .globl f_nonx30_ret + .type f_nonx30_ret,@function +f_nonx30_ret: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + mov x16, x30 + autiasp +// CHECK-LABEL: GS-PACRET: non-protected ret found in function f_nonx30_ret, basic block .LBB{{[0-9]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: ret x16 +// CHECK-NEXT: The 1 instructions that write to the return register after any authentication are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: mov x16, x30 +// 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]+}}: bl g@PLT +// CHECK-NEXT: {{[0-9a-f]+}}: add x0, x0, #0x3 +// CHECK-NEXT: {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// CHECK-NEXT: {{[0-9a-f]+}}: mov x16, x30 +// CHECK-NEXT: {{[0-9a-f]+}}: autiasp +// CHECK-NEXT: {{[0-9a-f]+}}: ret x16 + ret x16 + .size f_nonx30_ret, .-f_nonx30_ret + + .globl f_nonx30_ret_ok + .type f_nonx30_ret_ok,@function +f_nonx30_ret_ok: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + // FIXME: Should the scanner understand that an authenticated register (below x30, + // after the autiasp instruction), is OK to be moved to another register + // and then that register being used to return? + // This respects that pac-ret hardening intent, but the scanner currently + // will produce a false positive for this. + // Is it worthwhile to make the scanner more complex for this case? + // So far, scanning many millions of instructions across a linux distro, + // I haven't encountered such an example. + // The ".if 0" block below tests this case and currently fails. +.if 0 + autiasp + mov x16, x30 +.else + mov x16, x30 + autia x16, sp +.endif +// CHECK-NOT: function f_nonx30_ret_ok + ret x16 + .size f_nonx30_ret_ok, .-f_nonx30_ret_ok + + .globl f_detect_clobbered_x30_passed_to_other + .type f_detect_clobbered_x30_passed_to_other,@function +f_detect_clobbered_x30_passed_to_other: + str x30, [sp] + ldr x30, [sp] +// FIXME: Ideally, the pac-ret scanner would report on the following instruction, which +// performs a tail call, that x30 might be attacker-controlled. +// CHECK-NOT: function f_detect_clobbered_x30_passed_to_other + b f_tail_called + .size f_detect_clobbered_x30_passed_to_other, .-f_detect_clobbered_x30_passed_to_other + + .globl f_tail_called + .type f_tail_called,@function +f_tail_called: + ret + .size f_tail_called, .-f_tail_called + + .globl f_nonx30_ret_non_auted + .type f_nonx30_ret_non_auted,@function +f_nonx30_ret_non_auted: +// FIXME: x1 is not authenticated, so should this be reported? +// Note that we assume it's fine for x30 to not be authenticated before +// returning to, as assuming that x30 is not attacker controlled at function +// entry is part (implicitly) of the pac-ret hardening scheme. +// It's probably an open question whether for other hardening schemes, such as +// PAuthABI, which registers should be considered "clean" or not at function entry. +// In other words, which registers have to be authenticated before being used as +// a pointer and which ones not? +// For a more detailed discussion, see +// https://github.com/llvm/llvm-project/pull/122304#discussion_r1923662744 +// CHECK-NOT: f_nonx30_ret_non_auted + ret x1 + .size f_nonx30_ret_non_auted, .-f_nonx30_ret_non_auted + + + .globl f_callclobbered_x30 + .type f_callclobbered_x30,@function +f_callclobbered_x30: + bl g +// CHECK-LABEL: GS-PACRET: non-protected ret found in function f_callclobbered_x30, basic block .LBB{{[0-9]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: ret +// CHECK-NEXT: The 1 instructions that write to the return register after any authentication are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: bl g@PLT +// CHECK-NEXT: This happens in the following basic block: +// CHECK-NEXT: {{[0-9a-f]+}}: bl g@PLT +// CHECK-NEXT: {{[0-9a-f]+}}: ret + ret + .size f_callclobbered_x30, .-f_callclobbered_x30 + + .globl f_callclobbered_calleesaved + .type f_callclobbered_calleesaved,@function +f_callclobbered_calleesaved: + bl g +// CHECK-LABEL: GS-PACRET: non-protected ret found in function f_callclobbered_calleesaved, basic block .LBB{{[0-9]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: ret x19 +// CHECK-NEXT: The 1 instructions that write to the return register after any authentication are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: bl g@PLT +// CHECK-NEXT: This happens in the following basic block: +// CHECK-NEXT: {{[0-9a-f]+}}: bl g@PLT +// CHECK-NEXT: {{[0-9a-f]+}}: ret x19 + // x19, according to the Arm ABI (AAPCS) is a callee-saved register. + // Therefore, if function g respects the AAPCS, it should not write + // anything to x19. However, we can't know whether function g actually + // does respect the AAPCS rules, so the scanner should assume x19 can + // get overwritten, and report a gadget if the code does not properly + // deal with that. + // Furthermore, there's a good chance that callee-saved registers have + // been saved on the stack at some point during execution of the callee, + // and so should be considered as potentially modified by an + // attacker/written to. + ret x19 + .size f_callclobbered_calleesaved, .-f_callclobbered_calleesaved + + +/// Now do a basic sanity check on every different Authentication instruction: + + .globl f_autiasp + .type f_autiasp,@function +f_autiasp: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autiasp +// CHECK-NOT: function f_autiasp + ret + .size f_autiasp, .-f_autiasp + + .globl f_autibsp + .type f_autibsp,@function +f_autibsp: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autibsp +// CHECK-NOT: function f_autibsp + ret + .size f_autibsp, .-f_autibsp + + .globl f_autiaz + .type f_autiaz,@function +f_autiaz: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autiaz +// CHECK-NOT: function f_autiaz + ret + .size f_autiaz, .-f_autiaz + + .globl f_autibz + .type f_autibz,@function +f_autibz: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autibz +// CHECK-NOT: function f_autibz + ret + .size f_autibz, .-f_autibz + + .globl f_autia1716 + .type f_autia1716,@function +f_autia1716: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autia1716 +// CHECK-LABEL: GS-PACRET: non-protected ret found in function f_autia1716, basic block .LBB{{[0-9]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: ret +// CHECK-NEXT: The 1 instructions that write to the return register after any authentication are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// 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]+}}: bl g@PLT +// CHECK-NEXT: {{[0-9a-f]+}}: add x0, x0, #0x3 +// CHECK-NEXT: {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// CHECK-NEXT: {{[0-9a-f]+}}: autia1716 +// CHECK-NEXT: {{[0-9a-f]+}}: ret + ret + .size f_autia1716, .-f_autia1716 + + .globl f_autib1716 + .type f_autib1716,@function +f_autib1716: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autib1716 +// CHECK-LABEL: GS-PACRET: non-protected ret found in function f_autib1716, basic block .LBB{{[0-9]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: ret +// CHECK-NEXT: The 1 instructions that write to the return register after any authentication are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// 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]+}}: bl g@PLT +// CHECK-NEXT: {{[0-9a-f]+}}: add x0, x0, #0x3 +// CHECK-NEXT: {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// CHECK-NEXT: {{[0-9a-f]+}}: autib1716 +// CHECK-NEXT: {{[0-9a-f]+}}: ret + ret + .size f_autib1716, .-f_autib1716 + + .globl f_autiax12 + .type f_autiax12,@function +f_autiax12: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autia x12, sp +// CHECK-LABEL: GS-PACRET: non-protected ret found in function f_autiax12, basic block .LBB{{[0-9]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: ret +// CHECK-NEXT: The 1 instructions that write to the return register after any authentication are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// 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]+}}: bl g@PLT +// CHECK-NEXT: {{[0-9a-f]+}}: add x0, x0, #0x3 +// CHECK-NEXT: {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// CHECK-NEXT: {{[0-9a-f]+}}: autia x12, sp +// CHECK-NEXT: {{[0-9a-f]+}}: ret + ret + .size f_autiax12, .-f_autiax12 + + .globl f_autibx12 + .type f_autibx12,@function +f_autibx12: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autib x12, sp +// CHECK-LABEL: GS-PACRET: non-protected ret found in function f_autibx12, basic block .LBB{{[0-9]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: ret +// CHECK-NEXT: The 1 instructions that write to the return register after any authentication are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// 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]+}}: bl g@PLT +// CHECK-NEXT: {{[0-9a-f]+}}: add x0, x0, #0x3 +// CHECK-NEXT: {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// CHECK-NEXT: {{[0-9a-f]+}}: autib x12, sp +// CHECK-NEXT: {{[0-9a-f]+}}: ret + ret + .size f_autibx12, .-f_autibx12 + + .globl f_autiax30 + .type f_autiax30,@function +f_autiax30: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autia x30, sp +// CHECK-NOT: function f_autiax30 + ret + .size f_autiax30, .-f_autiax30 + + .globl f_autibx30 + .type f_autibx30,@function +f_autibx30: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autib x30, sp +// CHECK-NOT: function f_autibx30 + ret + .size f_autibx30, .-f_autibx30 + + + .globl f_autdax12 + .type f_autdax12,@function +f_autdax12: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autda x12, sp +// CHECK-LABEL: GS-PACRET: non-protected ret found in function f_autdax12, basic block .LBB{{[0-9]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: ret +// CHECK-NEXT: The 1 instructions that write to the return register after any authentication are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// 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]+}}: bl g@PLT +// CHECK-NEXT: {{[0-9a-f]+}}: add x0, x0, #0x3 +// CHECK-NEXT: {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// CHECK-NEXT: {{[0-9a-f]+}}: autda x12, sp +// CHECK-NEXT: {{[0-9a-f]+}}: ret + ret + .size f_autdax12, .-f_autdax12 + + .globl f_autdbx12 + .type f_autdbx12,@function +f_autdbx12: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autdb x12, sp +// CHECK-LABEL: GS-PACRET: non-protected ret found in function f_autdbx12, basic block .LBB{{[0-9]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: ret +// CHECK-NEXT: The 1 instructions that write to the return register after any authentication are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// 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]+}}: bl g@PLT +// CHECK-NEXT: {{[0-9a-f]+}}: add x0, x0, #0x3 +// CHECK-NEXT: {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// CHECK-NEXT: {{[0-9a-f]+}}: autdb x12, sp +// CHECK-NEXT: {{[0-9a-f]+}}: ret + ret + .size f_autdbx12, .-f_autdbx12 + + .globl f_autdax30 + .type f_autdax30,@function +f_autdax30: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autda x30, sp +// CHECK-NOT: function f_autdax30 + ret + .size f_autdax30, .-f_autdax30 + + .globl f_autdbx30 + .type f_autdbx30,@function +f_autdbx30: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autdb x30, sp +// CHECK-NOT: function f_autdbx30 + ret + .size f_autdbx30, .-f_autdbx30 + + + .globl f_autizax12 + .type f_autizax12,@function +f_autizax12: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autiza x12 +// CHECK-LABEL: GS-PACRET: non-protected ret found in function f_autizax12, basic block .LBB{{[0-9]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: ret +// CHECK-NEXT: The 1 instructions that write to the return register after any authentication are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// 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]+}}: bl g@PLT +// CHECK-NEXT: {{[0-9a-f]+}}: add x0, x0, #0x3 +// CHECK-NEXT: {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// CHECK-NEXT: {{[0-9a-f]+}}: autiza x12 +// CHECK-NEXT: {{[0-9a-f]+}}: ret + ret + .size f_autizax12, .-f_autizax12 + + .globl f_autizbx12 + .type f_autizbx12,@function +f_autizbx12: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autizb x12 +// CHECK-LABEL: GS-PACRET: non-protected ret found in function f_autizbx12, basic block .LBB{{[0-9]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: ret +// CHECK-NEXT: The 1 instructions that write to the return register after any authentication are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// 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]+}}: bl g@PLT +// CHECK-NEXT: {{[0-9a-f]+}}: add x0, x0, #0x3 +// CHECK-NEXT: {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// CHECK-NEXT: {{[0-9a-f]+}}: autizb x12 +// CHECK-NEXT: {{[0-9a-f]+}}: ret + ret + .size f_autizbx12, .-f_autizbx12 + + .globl f_autizax30 + .type f_autizax30,@function +f_autizax30: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autiza x30 +// CHECK-NOT: function f_autizax30 + ret + .size f_autizax30, .-f_autizax30 + + .globl f_autizbx30 + .type f_autizbx30,@function +f_autizbx30: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autizb x30 +// CHECK-NOT: function f_autizbx30 + ret + .size f_autizbx30, .-f_autizbx30 + + + .globl f_autdzax12 + .type f_autdzax12,@function +f_autdzax12: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autdza x12 +// CHECK-LABEL: GS-PACRET: non-protected ret found in function f_autdzax12, basic block .LBB{{[0-9]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: ret +// CHECK-NEXT: The 1 instructions that write to the return register after any authentication are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// 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]+}}: bl g@PLT +// CHECK-NEXT: {{[0-9a-f]+}}: add x0, x0, #0x3 +// CHECK-NEXT: {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// CHECK-NEXT: {{[0-9a-f]+}}: autdza x12 +// CHECK-NEXT: {{[0-9a-f]+}}: ret + ret + .size f_autdzax12, .-f_autdzax12 + + .globl f_autdzbx12 + .type f_autdzbx12,@function +f_autdzbx12: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autdzb x12 +// CHECK-LABEL: GS-PACRET: non-protected ret found in function f_autdzbx12, basic block .LBB{{[0-9]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: ret +// CHECK-NEXT: The 1 instructions that write to the return register after any authentication are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// 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]+}}: bl g@PLT +// CHECK-NEXT: {{[0-9a-f]+}}: add x0, x0, #0x3 +// CHECK-NEXT: {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// CHECK-NEXT: {{[0-9a-f]+}}: autdzb x12 +// CHECK-NEXT: {{[0-9a-f]+}}: ret + ret + .size f_autdzbx12, .-f_autdzbx12 + + .globl f_autdzax30 + .type f_autdzax30,@function +f_autdzax30: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autdza x30 +// CHECK-NOT: function f_autdzax30 + ret + .size f_autdzax30, .-f_autdzax30 + + .globl f_autdzbx30 + .type f_autdzbx30,@function +f_autdzbx30: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autdzb x30 +// CHECK-NOT: function f_autdzbx30 + ret + .size f_autdzbx30, .-f_autdzbx30 + + .globl f_retaa + .type f_retaa,@function +f_retaa: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 +// CHECK-NOT: function f_retaa + retaa + .size f_retaa, .-f_retaa + + .globl f_retab + .type f_retab,@function +f_retab: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 +// CHECK-NOT: function f_retab + retab + .size f_retab, .-f_retab + + .globl f_eretaa + .type f_eretaa,@function +f_eretaa: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 +// CHECK-LABEL: GS-PACRET: Warning: pac-ret analysis could not analyze this return instruction in function f_eretaa, basic block .LBB{{[0-9]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: eretaa + eretaa + .size f_eretaa, .-f_eretaa + + .globl f_eretab + .type f_eretab,@function +f_eretab: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 +// CHECK-LABEL: GS-PACRET: Warning: pac-ret analysis could not analyze this return instruction in function f_eretab, basic block .LBB{{[0-9]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: eretab + eretab + .size f_eretab, .-f_eretab + + .globl f_eret + .type f_eret,@function +f_eret: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 +// CHECK-LABEL: GS-PACRET: Warning: pac-ret analysis could not analyze this return instruction in function f_eret, basic block .LBB{{[0-9]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: eret + eret + .size f_eret, .-f_eret + + .globl f_movx30reg + .type f_movx30reg,@function +f_movx30reg: +// CHECK-LABEL: GS-PACRET: non-protected ret found in function f_movx30reg, basic block .LBB{{[0-9]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: ret +// CHECK-NEXT: The 1 instructions that write to the return register after any authentication are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: mov x30, x22 +// CHECK-NEXT: This happens in the following basic block: +// CHECK-NEXT: {{[0-9a-f]+}}: mov x30, x22 +// CHECK-NEXT: {{[0-9a-f]+}}: ret + mov x30, x22 + ret + .size f_movx30reg, .-f_movx30reg + + .globl f_autiasppci + .type f_autiasppci,@function +f_autiasppci: +0: + pacnbiasppc + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autiasppc 0b +// CHECK-NOT: function f_autiasppci + ret + .size f_autiasppci, .-f_autiasppci + + .globl f_autibsppci + .type f_autibsppci,@function +f_autibsppci: +0: + pacnbibsppc + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autibsppc 0b +// CHECK-NOT: function f_autibsppci + ret + .size f_autibsppci, .-f_autibsppci + + .globl f_autiasppcr + .type f_autiasppcr,@function + +f_autiasppcr: +0: + pacnbiasppc + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + adr x28, 0b + autiasppcr x28 +// CHECK-NOT: function f_autiasppcr + ret + .size f_autiasppcr, .-f_autiasppcr + +f_autibsppcr: +0: + pacnbibsppc + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + adr x28, 0b + autibsppcr x28 +// CHECK-NOT: function f_autibsppcr + ret + .size f_autibsppcr, .-f_autibsppcr + + .globl f_retaasppci + .type f_retaasppci,@function +f_retaasppci: +0: + pacnbiasppc + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 +// CHECK-NOT: function f_retaasppci + retaasppc 0b + .size f_retaasppci, .-f_retaasppci + + .globl f_retabsppci + .type f_retabsppci,@function +f_retabsppci: +0: + pacnbibsppc + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 +// CHECK-NOT: function f_retabsppci + retabsppc 0b + .size f_retabsppci, .-f_retabsppci + + .globl f_retaasppcr + .type f_retaasppcr,@function + +f_retaasppcr: +0: + pacnbiasppc + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + adr x28, 0b +// CHECK-NOT: function f_retaasppcr + retaasppcr x28 + .size f_retaasppcr, .-f_retaasppcr + +f_retabsppcr: +0: + pacnbibsppc + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + adr x28, 0b +// CHECK-NOT: function f_retabsppcr + retabsppcr x28 + .size f_retabsppcr, .-f_retabsppcr + + .globl f_autia171615 + .type f_autia171615,@function +f_autia171615: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autia171615 +// CHECK-LABEL: GS-PACRET: non-protected ret found in function f_autia171615, basic block .LBB{{[0-9]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: ret +// CHECK-NEXT: The 1 instructions that write to the return register after any authentication are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// 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]+}}: bl g@PLT +// CHECK-NEXT: {{[0-9a-f]+}}: add x0, x0, #0x3 +// CHECK-NEXT: {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// CHECK-NEXT: {{[0-9a-f]+}}: autia171615 +// CHECK-NEXT: {{[0-9a-f]+}}: ret + ret + .size f_autia171615, .-f_autia171615 + + .globl f_autib171615 + .type f_autib171615,@function +f_autib171615: + paciasp + stp x29, x30, [sp, #-16]! + mov x29, sp + bl g + add x0, x0, #3 + ldp x29, x30, [sp], #16 + autib171615 +// CHECK-LABEL: GS-PACRET: non-protected ret found in function f_autib171615, basic block .LBB{{[0-9]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: ret +// CHECK-NEXT: The 1 instructions that write to the return register after any authentication are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// 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]+}}: bl g@PLT +// CHECK-NEXT: {{[0-9a-f]+}}: add x0, x0, #0x3 +// CHECK-NEXT: {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// CHECK-NEXT: {{[0-9a-f]+}}: autib171615 +// CHECK-NEXT: {{[0-9a-f]+}}: ret + ret + .size f_autib171615, .-f_autib171615 + diff --git a/bolt/test/binary-analysis/AArch64/gs-pacret-multi-bb.s b/bolt/test/binary-analysis/AArch64/gs-pacret-multi-bb.s new file mode 100644 index 0000000000000..e9401e4823927 --- /dev/null +++ b/bolt/test/binary-analysis/AArch64/gs-pacret-multi-bb.s @@ -0,0 +1,75 @@ +// RUN: %clang %cflags -march=armv8.3-a -mbranch-protection=pac-ret %s %p/../../Inputs/asm_main.c -o %t.exe +// RUN: llvm-bolt-binary-analysis --scanners=pacret %t.exe 2>&1 | FileCheck %s + + +// Verify that we can also detect gadgets across basic blocks + + .globl f_crossbb1 + .type f_crossbb1,@function +f_crossbb1: + paciasp + stp x29, x30, [sp, #-16]! + ldp x29, x30, [sp], #16 + cbnz x0, 1f + autiasp +1: + ret + .size f_crossbb1, .-f_crossbb1 +// CHECK-LABEL: GS-PACRET: non-protected ret found in function f_crossbb1, basic block .L{{[^,]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: ret +// CHECK-NEXT: The 2 instructions that write to the return register after any authentication are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 +// CHECK-NEXT: 2. {{[0-9a-f]+}}: autiasp + +// A test that checks that the dataflow state tracking across when merging BBs +// seems to work: + .globl f_mergebb1 + .type f_mergebb1,@function +f_mergebb1: + paciasp +2: + stp x29, x30, [sp, #-16]! + ldp x29, x30, [sp], #16 + sub x0, x0, #1 + cbnz x0, 1f + autiasp + b 2b +1: + ret + .size f_mergebb1, .-f_mergebb1 +// CHECK-LABEL: GS-PACRET: non-protected ret found in function f_mergebb1, basic block .L{{[^,]+}}, at address +// CHECK-NEXT: The return instruction is {{[0-9a-f]+}}: ret +// CHECK-NEXT: The 1 instructions that write to the return register after any authentication are: +// CHECK-NEXT: 1. {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10 + + .globl f_shrinkwrapping + .type f_shrinkwrapping,@function +f_shrinkwrapping: + cbz x0, 1f + paciasp + stp x29, x30, [sp, #-16]! + ldp x29, x30, [sp], #16 + autiasp +1: + ret + .size f_shrinkwrapping, .-f_shrinkwrapping +// CHECK-NOT: f_shrinkwrapping + + .globl f_multi_auth_insts + .type f_multi_auth_insts,@function +f_multi_auth_insts: + paciasp + stp x29, x30, [sp, #-16]! + ldp x29, x30, [sp], #16 + cbnz x0, 1f + autibsp + b 2f +1: + autiasp +2: + ret + .size f_multi_auth_insts, .-f_multi_auth_insts +// CHECK-NOT: f_multi_auth_insts + +// TODO: also verify that false negatives exist in across-BB gadgets in functions +// for which bolt cannot reconstruct the call graph. diff --git a/bolt/test/binary-analysis/AArch64/lit.local.cfg b/bolt/test/binary-analysis/AArch64/lit.local.cfg index 6f247dd52e82f..6ac7d3cc0fec7 100644 --- a/bolt/test/binary-analysis/AArch64/lit.local.cfg +++ b/bolt/test/binary-analysis/AArch64/lit.local.cfg @@ -1,7 +1,7 @@ if "AArch64" not in config.root.targets: config.unsupported = True -flags = "--target=aarch64-linux-gnu -nostartfiles -nostdlib -ffreestanding -Wl,--emit-relocs" +flags = "--target=aarch64-linux-gnu -nostartfiles -nostdlib -ffreestanding" config.substitutions.insert(0, ("%cflags", f"%cflags {flags}")) config.substitutions.insert(0, ("%cxxflags", f"%cxxflags {flags}")) diff --git a/llvm/lib/Target/AArch64/AArch64InstrInfo.td b/llvm/lib/Target/AArch64/AArch64InstrInfo.td index ec891ea4bac85..aa4aa56cdc45b 100644 --- a/llvm/lib/Target/AArch64/AArch64InstrInfo.td +++ b/llvm/lib/Target/AArch64/AArch64InstrInfo.td @@ -2022,6 +2022,8 @@ let Predicates = [HasPAuthLR] in { // opcode2, opcode, asm def AUTIASPPCr : SignAuthOneReg<0b00001, 0b100100, "autiasppcr">; def AUTIBSPPCr : SignAuthOneReg<0b00001, 0b100101, "autibsppcr">; + } + let Defs = [X17], Uses = [X15, X16, X17] in { // opcode2, opcode, asm def PACIA171615 : SignAuthFixedRegs<0b00001, 0b100010, "pacia171615">; def PACIB171615 : SignAuthFixedRegs<0b00001, 0b100011, "pacib171615">;