[BOLT][binary-analysis] Add initial pac-ret gadget scanner (#122304)

This adds an initial pac-ret gadget scanner to the
llvm-bolt-binary-analysis-tool.

The scanner is taken from the prototype that was published last year at
https://github.com/llvm/llvm-project/compare/main...kbeyls:llvm-project:bolt-gadget-scanner-prototype,
and has been discussed in RFC

https://discourse.llvm.org/t/rfc-bolt-based-binary-analysis-tool-to-verify-correctness-of-security-hardening/78148
and in the EuroLLVM 2024 keynote "Does LLVM implement security
hardenings correctly? A BOLT-based static analyzer to the rescue?"
[Video](https://youtu.be/Sn_Fxa0tdpY)
[Slides](https://llvm.org/devmtg/2024-04/slides/Keynote/Beyls_EuroLLVM2024_security_hardening_keynote.pdf)

In the spirit of incremental development, this PR aims to add a minimal
implementation that is "fully working" on its own, but has major
limitations, as described in the bolt/docs/BinaryAnalysis.md
documentation in this proposed commit. These and other limitations will
be fixed in follow-on PRs, mostly based on code already existing in the
prototype branch. I hope incrementally upstreaming will make it easier
to review the code.

Note that I believe that this could also form the basis of a scanner to
analyze correct implementation of PAuthABI.
This commit is contained in:
Kristof Beyls 2025-02-24 08:26:28 +01:00 committed by GitHub
parent 86cb0bd3a2
commit 850b492976
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 2126 additions and 5 deletions

View File

@ -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<Ret:MCInstBBRef<BB:.LBB00:6>, Overwriting:[MCInstBBRef<BB:.LBB00:5> ]>
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<Ret:MCInstBBRef<BB:.LBB00:6>, Overwriting:[MCInstBBRef<BB:.LBB00:5> ]>
```
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<Ret:MCInstBBRef<BB:.Ltmp0:0>, Overwriting:[MCInstBBRef<BB:.LFT0:0> MCInstBBRef<BB:.LBB00:2> ]>
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

View File

@ -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 @@ public:
return Analysis->isReturn(Inst);
}
virtual ErrorOr<MCPhysReg> 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<MCPhysReg> 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 {

View File

@ -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 <memory>
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<MCInstReference> OverwritingRetRegInst;
virtual bool operator==(const Gadget &RHS) const {
return Annotation::operator==(RHS) &&
OverwritingRetRegInst == RHS.OverwritingRetRegInst;
}
Gadget(MCInstReference RetInst,
const std::vector<MCInstReference> &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<MCPhysReg, 1> RegistersAffected;
std::vector<std::shared_ptr<Annotation>> Diagnostics;
};
class Analysis : public BinaryFunctionPass {
void runOnFunction(BinaryFunction &Function,
MCPlusBuilder::AllocatorIdTy AllocatorId);
FunctionAnalysisResult
computeDfState(PacRetAnalysis &PRA, BinaryFunction &BF,
MCPlusBuilder::AllocatorIdTy AllocatorId);
std::map<const BinaryFunction *, FunctionAnalysisResult> 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

View File

@ -80,6 +80,10 @@ extern llvm::cl::opt<unsigned> Verbosity;
/// Return true if we should process all functions in the binary.
bool processAllFunctions();
enum GadgetScannerKind { GS_PACRET, GS_ALL };
extern llvm::cl::list<GadgetScannerKind> GadgetScannersToRun;
} // namespace opts
namespace llvm {

View File

@ -23,6 +23,7 @@ add_llvm_library(LLVMBOLTPasses
LoopInversionPass.cpp
LivenessAnalysis.cpp
MCF.cpp
NonPacProtectedRetAnalysis.cpp
PatchEntries.cpp
PettisAndHansen.cpp
PLTCall.cpp

View File

@ -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 <memory>
#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<SmallPtrSet<const MCInst *, 4>> 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<SmallPtrSet<const MCInst *, 4>> &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<PacRetAnalysis, State, /*Backward=*/false,
PacStatePrinter> {
using Parent =
DataflowAnalysis<PacRetAnalysis, State, false, PacStatePrinter>;
friend Parent;
public:
PacRetAnalysis(BinaryFunction &BF, MCPlusBuilder::AllocatorIdTy AllocId,
const std::vector<MCPhysReg> &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<MCPhysReg> 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<uint16_t> Reg2StateIdx;
SmallPtrSet<const MCInst *, 4> &lastWritingInsts(State &S,
MCPhysReg Reg) const {
assert(Reg < Reg2StateIdx.size());
assert(isTrackingReg(Reg));
return S.LastInstWritingReg[Reg2StateIdx[Reg]];
}
const SmallPtrSet<const MCInst *, 4> &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<MCInst &>(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<MCPhysReg> 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<MCInstReference>
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<const MCInst *> LastWritingInsts;
for (MCPhysReg TrackedReg : UsedDirtyRegs.set_bits()) {
for (const MCInst *Inst : lastWritingInsts(S, TrackedReg))
LastWritingInsts.insert(Inst);
}
std::vector<MCInstReference> 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<MCPhysReg> MaybeRetReg = BC.MIB->getRegUsedAsRetDest(Inst);
if (MaybeRetReg.getError()) {
Result.Diagnostics.push_back(std::make_shared<GenDiag>(
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<Gadget>(
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<MCPhysReg> 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<std::mutex> 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<MCInstReference> 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<Annotation> &A :
AnalysisResults[BF].Diagnostics)
A->generateReport(outs(), BC);
}
return Error::success();
}
} // namespace NonPacProtectedRetAnalysis
} // namespace bolt
} // namespace llvm

View File

@ -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<bool> WriteBoltInfoSection(
"bolt-info", cl::desc("write bolt info section in the output binary"),
cl::init(true), cl::Hidden, cl::cat(BoltOutputCategory));
cl::list<GadgetScannerKind>
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<NonPacProtectedRetAnalysis::Analysis>());
}
BC->logBOLTErrorsAndQuitOnFatal(Manager.runPasses());
}
void RewriteInstance::preregisterSections() {
// Preregister sections before emission to set their order in the output.

View File

@ -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"
@ -183,6 +184,88 @@ public:
return false;
}
ErrorOr<MCPhysReg> 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<MCPhysReg> AuthenticatedReg = getAuthenticatedReg(Inst);
return AuthenticatedReg.getError() ? false : *AuthenticatedReg == Reg;
}
ErrorOr<MCPhysReg> 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;
}

View File

@ -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] <executable>
HELP-EMPTY:
HELP-NEXT: OPTIONS:
HELP-EMPTY:
HELP-NEXT: BinaryAnalysis options:
HELP-EMPTY:
HELP-NEXT: --scanners=<value> - which gadget scanners to run
HELP-NEXT: =pacret - pac-ret
HELP-NEXT: =all - all
HELP-EMPTY:
HELP-NEXT: Generic Options:

View File

@ -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

View File

@ -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.

View File

@ -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}"))