[BOLT] Gadget scanner: detect authentication oracles (#135663)

Implement the detection of authentication instructions whose results can
be inspected by an attacker to know whether authentication succeeded.

As the properties of output registers of authentication instructions are
inspected, add a second set of analysis-related classes to iterate over
the instructions in reverse order.
This commit is contained in:
Anatoly Trosinenko 2025-06-19 15:15:26 +03:00 committed by GitHub
parent b73720cf6c
commit 2b4d757290
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 1534 additions and 39 deletions

View File

@ -199,8 +199,7 @@ namespace PAuthGadgetScanner {
// to distinguish intermediate and final results at the type level.
//
// Here is an overview of issue life-cycle:
// * an analysis (SrcSafetyAnalysis at now, DstSafetyAnalysis will be added
// later to support the detection of authentication oracles) computes register
// * an analysis (SrcSafetyAnalysis or DstSafetyAnalysis) computes register
// state for each instruction in the function.
// * for each instruction, it is checked whether it is a gadget of some kind,
// taking the computed state into account. If a gadget is found, its kind
@ -273,6 +272,11 @@ public:
virtual ~ExtraInfo() {}
};
/// The set of instructions writing to the affected register in an unsafe
/// manner.
///
/// This is a hint to be printed alongside the report. It should be further
/// analyzed by the user.
class ClobberingInfo : public ExtraInfo {
SmallVector<MCInstReference> ClobberingInstrs;
@ -282,6 +286,20 @@ public:
void print(raw_ostream &OS, const MCInstReference Location) const override;
};
/// The set of instructions leaking the authenticated pointer before the
/// result of authentication was checked.
///
/// This is a hint to be printed alongside the report. It should be further
/// analyzed by the user.
class LeakageInfo : public ExtraInfo {
SmallVector<MCInstReference> LeakingInstrs;
public:
LeakageInfo(ArrayRef<MCInstReference> Instrs) : LeakingInstrs(Instrs) {}
void print(raw_ostream &OS, const MCInstReference Location) const override;
};
/// A brief version of a report that can be further augmented with the details.
///
/// A half-baked report produced on the first run of the analysis. An extra,
@ -322,6 +340,9 @@ class FunctionAnalysisContext {
void findUnsafeUses(SmallVector<PartialReport<MCPhysReg>> &Reports);
void augmentUnsafeUseReports(ArrayRef<PartialReport<MCPhysReg>> Reports);
void findUnsafeDefs(SmallVector<PartialReport<MCPhysReg>> &Reports);
void augmentUnsafeDefReports(ArrayRef<PartialReport<MCPhysReg>> Reports);
/// Process the reports which do not have to be augmented, and remove them
/// from Reports.
void handleSimpleReports(SmallVector<PartialReport<MCPhysReg>> &Reports);

View File

@ -152,6 +152,8 @@ public:
// in the gadgets to be reported. This information is used in the second run
// to also track which instructions last wrote to those registers.
typedef SmallPtrSet<const MCInst *, 4> SetOfRelatedInsts;
/// A state representing which registers are safe to use by an instruction
/// at a given program point.
///
@ -195,7 +197,7 @@ struct SrcState {
/// pac-ret analysis, the expectation is that almost all return instructions
/// only use register `X30`, and therefore, this vector will probably have
/// length 1 in the second run.
std::vector<SmallPtrSet<const MCInst *, 4>> LastInstWritingReg;
std::vector<SetOfRelatedInsts> LastInstWritingReg;
/// Construct an empty state.
SrcState() {}
@ -230,12 +232,11 @@ struct SrcState {
bool operator!=(const SrcState &RHS) const { return !((*this) == RHS); }
};
static void
printLastInsts(raw_ostream &OS,
ArrayRef<SmallPtrSet<const MCInst *, 4>> LastInstWritingReg) {
static void printInstsShort(raw_ostream &OS,
ArrayRef<SetOfRelatedInsts> Insts) {
OS << "Insts: ";
for (unsigned I = 0; I < LastInstWritingReg.size(); ++I) {
auto &Set = LastInstWritingReg[I];
for (unsigned I = 0; I < Insts.size(); ++I) {
auto &Set = Insts[I];
OS << "[" << I << "](";
for (const MCInst *MCInstP : Set)
OS << MCInstP << " ";
@ -243,14 +244,14 @@ printLastInsts(raw_ostream &OS,
}
}
raw_ostream &operator<<(raw_ostream &OS, const SrcState &S) {
static raw_ostream &operator<<(raw_ostream &OS, const SrcState &S) {
OS << "src-state<";
if (S.empty()) {
OS << "empty";
} else {
OS << "SafeToDerefRegs: " << S.SafeToDerefRegs << ", ";
OS << "TrustedRegs: " << S.TrustedRegs << ", ";
printLastInsts(OS, S.LastInstWritingReg);
printInstsShort(OS, S.LastInstWritingReg);
}
OS << ">";
return OS;
@ -279,7 +280,7 @@ void SrcStatePrinter::print(raw_ostream &OS, const SrcState &S) const {
OS << ", TrustedRegs: ";
RegStatePrinter.print(OS, S.TrustedRegs);
OS << ", ";
printLastInsts(OS, S.LastInstWritingReg);
printInstsShort(OS, S.LastInstWritingReg);
}
OS << ">";
}
@ -323,13 +324,12 @@ protected:
DenseMap<const MCInst *, std::pair<MCPhysReg, const MCInst *>>
CheckerSequenceInfo;
SmallPtrSet<const MCInst *, 4> &lastWritingInsts(SrcState &S,
MCPhysReg Reg) const {
SetOfRelatedInsts &lastWritingInsts(SrcState &S, MCPhysReg Reg) const {
unsigned Index = RegsToTrackInstsFor.getIndex(Reg);
return S.LastInstWritingReg[Index];
}
const SmallPtrSet<const MCInst *, 4> &lastWritingInsts(const SrcState &S,
MCPhysReg Reg) const {
const SetOfRelatedInsts &lastWritingInsts(const SrcState &S,
MCPhysReg Reg) const {
unsigned Index = RegsToTrackInstsFor.getIndex(Reg);
return S.LastInstWritingReg[Index];
}
@ -433,8 +433,7 @@ protected:
SrcStatePrinter P(BC);
LLVM_DEBUG({
dbgs() << " SrcSafetyAnalysis::ComputeNext(";
BC.InstPrinter->printInst(&const_cast<MCInst &>(Point), 0, "", *BC.STI,
dbgs());
BC.InstPrinter->printInst(&Point, 0, "", *BC.STI, dbgs());
dbgs() << ", ";
P.print(dbgs(), Cur);
dbgs() << ")\n";
@ -612,6 +611,42 @@ protected:
StringRef getAnnotationName() const { return "DataflowSrcSafetyAnalysis"; }
};
/// A helper base class for implementing a simplified counterpart of a dataflow
/// analysis for functions without CFG information.
template <typename StateTy> class CFGUnawareAnalysis {
BinaryContext &BC;
BinaryFunction &BF;
MCPlusBuilder::AllocatorIdTy AllocId;
unsigned StateAnnotationIndex;
void cleanStateAnnotations() {
for (auto &I : BF.instrs())
BC.MIB->removeAnnotation(I.second, StateAnnotationIndex);
}
protected:
CFGUnawareAnalysis(BinaryFunction &BF, MCPlusBuilder::AllocatorIdTy AllocId,
StringRef AnnotationName)
: BC(BF.getBinaryContext()), BF(BF), AllocId(AllocId) {
StateAnnotationIndex = BC.MIB->getOrCreateAnnotationIndex(AnnotationName);
}
void setState(MCInst &Inst, const StateTy &S) {
// Check if we need to remove an old annotation (this is the case if
// this is the second, detailed run of the analysis).
if (BC.MIB->hasAnnotation(Inst, StateAnnotationIndex))
BC.MIB->removeAnnotation(Inst, StateAnnotationIndex);
// Attach the state.
BC.MIB->addAnnotation(Inst, StateAnnotationIndex, S, AllocId);
}
const StateTy &getState(const MCInst &Inst) const {
return BC.MIB->getAnnotationAs<StateTy>(Inst, StateAnnotationIndex);
}
virtual ~CFGUnawareAnalysis() { cleanStateAnnotations(); }
};
// A simplified implementation of DataflowSrcSafetyAnalysis for functions
// lacking CFG information.
//
@ -646,15 +681,10 @@ protected:
// of instructions without labels in between. These sequences can be processed
// the same way basic blocks are processed by data-flow analysis, assuming
// pessimistically that all registers are unsafe at the start of each sequence.
class CFGUnawareSrcSafetyAnalysis : public SrcSafetyAnalysis {
class CFGUnawareSrcSafetyAnalysis : public SrcSafetyAnalysis,
public CFGUnawareAnalysis<SrcState> {
using SrcSafetyAnalysis::BC;
BinaryFunction &BF;
MCPlusBuilder::AllocatorIdTy AllocId;
unsigned StateAnnotationIndex;
void cleanStateAnnotations() {
for (auto &I : BF.instrs())
BC.MIB->removeAnnotation(I.second, StateAnnotationIndex);
}
/// Creates a state with all registers marked unsafe (not to be confused
/// with empty state).
@ -666,9 +696,8 @@ public:
CFGUnawareSrcSafetyAnalysis(BinaryFunction &BF,
MCPlusBuilder::AllocatorIdTy AllocId,
ArrayRef<MCPhysReg> RegsToTrackInstsFor)
: SrcSafetyAnalysis(BF, RegsToTrackInstsFor), BF(BF), AllocId(AllocId) {
StateAnnotationIndex =
BC.MIB->getOrCreateAnnotationIndex("CFGUnawareSrcSafetyAnalysis");
: SrcSafetyAnalysis(BF, RegsToTrackInstsFor),
CFGUnawareAnalysis(BF, AllocId, "CFGUnawareSrcSafetyAnalysis"), BF(BF) {
}
void run() override {
@ -687,12 +716,8 @@ public:
S = createUnsafeState();
}
// Check if we need to remove an old annotation (this is the case if
// this is the second, detailed, run of the analysis).
if (BC.MIB->hasAnnotation(Inst, StateAnnotationIndex))
BC.MIB->removeAnnotation(Inst, StateAnnotationIndex);
// Attach the state *before* this instruction executes.
BC.MIB->addAnnotation(Inst, StateAnnotationIndex, S, AllocId);
setState(Inst, S);
// Compute the state after this instruction executes.
S = computeNext(Inst, S);
@ -700,10 +725,8 @@ public:
}
const SrcState &getStateBefore(const MCInst &Inst) const override {
return BC.MIB->getAnnotationAs<SrcState>(Inst, StateAnnotationIndex);
return getState(Inst);
}
~CFGUnawareSrcSafetyAnalysis() { cleanStateAnnotations(); }
};
std::shared_ptr<SrcSafetyAnalysis>
@ -717,6 +740,478 @@ SrcSafetyAnalysis::create(BinaryFunction &BF,
RegsToTrackInstsFor);
}
/// A state representing which registers are safe to be used as the destination
/// operand of an authentication instruction.
///
/// Similar to SrcState, it is the responsibility of the analysis to take
/// register aliasing into account.
///
/// Depending on the implementation (such as whether FEAT_FPAC is implemented
/// by an AArch64 CPU or not), it may be possible that an authentication
/// instruction returns an invalid pointer on failure instead of terminating
/// the program immediately (assuming the program will crash as soon as that
/// pointer is dereferenced). Since few bits are usually allocated for the PAC
/// field (such as less than 16 bits on a typical AArch64 system), an attacker
/// can try every possible signature and guess the correct one if there is a
/// gadget that tells whether the particular pointer has a correct signature
/// (a so called "authentication oracle"). For that reason, it should be
/// impossible for an attacker to test if a pointer is correctly signed -
/// either the program should be terminated on authentication failure or
/// the result of authentication should not be accessible to an attacker.
///
/// Considering the instructions in forward order as they are executed, a
/// restricted set of operations can be allowed on any register containing a
/// value derived from the result of an authentication instruction until that
/// value is checked not to contain the result of a failed authentication.
/// In DstSafetyAnalysis, these rules are adapted, so that the safety property
/// for a register is computed by iterating the instructions in backward order.
/// Then the resulting properties are used at authentication instruction sites
/// to check output registers and report the particular instruction if it writes
/// to an unsafe register.
///
/// Another approach would be to simulate the above rules as-is, iterating over
/// the instructions in forward direction. To make it possible to report the
/// particular instructions as oracles, this would probably require tracking
/// references to these instructions for each register currently containing
/// sensitive data.
///
/// In DstSafetyAnalysis, the source register Xn of an instruction Inst is safe
/// if at least one of the following is true:
/// * Inst checks if Xn contains the result of a successful authentication and
/// terminates the program on failure. Note that Inst can either naturally
/// dereference Xn (load, branch, return, etc. instructions) or be the first
/// instruction of an explicit checking sequence.
/// * Inst performs safe address arithmetic AND both source and result
/// registers, as well as any temporary registers, must be safe after
/// execution of Inst (temporaries are not used on AArch64 and thus not
/// currently supported/allowed).
/// See MCPlusBuilder::analyzeAddressArithmeticsForPtrAuth for the details.
/// * Inst fully overwrites Xn with a constant.
struct DstState {
/// The set of registers whose values cannot be inspected by an attacker in
/// a way usable as an authentication oracle. The results of authentication
/// instructions should only be written to such registers.
BitVector CannotEscapeUnchecked;
/// A vector of sets, only used on the second analysis run.
/// Each element in this vector represents one of the tracked registers.
/// For each such register we track the set of first instructions that leak
/// the authenticated pointer before it was checked. This is intended to
/// provide clues on which instruction made the particular register unsafe.
///
/// Please note that the mapping from MCPhysReg values to indexes in this
/// vector is provided by RegsToTrackInstsFor field of DstSafetyAnalysis.
std::vector<SetOfRelatedInsts> FirstInstLeakingReg;
/// Constructs an empty state.
DstState() {}
DstState(unsigned NumRegs, unsigned NumRegsToTrack)
: CannotEscapeUnchecked(NumRegs), FirstInstLeakingReg(NumRegsToTrack) {}
DstState &merge(const DstState &StateIn) {
if (StateIn.empty())
return *this;
if (empty())
return (*this = StateIn);
CannotEscapeUnchecked &= StateIn.CannotEscapeUnchecked;
for (unsigned I = 0; I < FirstInstLeakingReg.size(); ++I)
for (const MCInst *J : StateIn.FirstInstLeakingReg[I])
FirstInstLeakingReg[I].insert(J);
return *this;
}
/// Returns true if this object does not store state of any registers -
/// neither safe, nor unsafe ones.
bool empty() const { return CannotEscapeUnchecked.empty(); }
bool operator==(const DstState &RHS) const {
return CannotEscapeUnchecked == RHS.CannotEscapeUnchecked &&
FirstInstLeakingReg == RHS.FirstInstLeakingReg;
}
bool operator!=(const DstState &RHS) const { return !((*this) == RHS); }
};
static raw_ostream &operator<<(raw_ostream &OS, const DstState &S) {
OS << "dst-state<";
if (S.empty()) {
OS << "empty";
} else {
OS << "CannotEscapeUnchecked: " << S.CannotEscapeUnchecked << ", ";
printInstsShort(OS, S.FirstInstLeakingReg);
}
OS << ">";
return OS;
}
class DstStatePrinter {
public:
void print(raw_ostream &OS, const DstState &S) const;
explicit DstStatePrinter(const BinaryContext &BC) : BC(BC) {}
private:
const BinaryContext &BC;
};
void DstStatePrinter::print(raw_ostream &OS, const DstState &S) const {
RegStatePrinter RegStatePrinter(BC);
OS << "dst-state<";
if (S.empty()) {
assert(S.CannotEscapeUnchecked.empty());
assert(S.FirstInstLeakingReg.empty());
OS << "empty";
} else {
OS << "CannotEscapeUnchecked: ";
RegStatePrinter.print(OS, S.CannotEscapeUnchecked);
OS << ", ";
printInstsShort(OS, S.FirstInstLeakingReg);
}
OS << ">";
}
/// Computes which registers are safe to be written to by auth instructions.
///
/// This is the base class for two implementations: a dataflow-based analysis
/// which is intended to be used for most functions and a simplified CFG-unaware
/// version for functions without reconstructed CFG.
class DstSafetyAnalysis {
public:
DstSafetyAnalysis(BinaryFunction &BF, ArrayRef<MCPhysReg> RegsToTrackInstsFor)
: BC(BF.getBinaryContext()), NumRegs(BC.MRI->getNumRegs()),
RegsToTrackInstsFor(RegsToTrackInstsFor) {}
virtual ~DstSafetyAnalysis() {}
static std::shared_ptr<DstSafetyAnalysis>
create(BinaryFunction &BF, MCPlusBuilder::AllocatorIdTy AllocId,
ArrayRef<MCPhysReg> RegsToTrackInstsFor);
virtual void run() = 0;
virtual const DstState &getStateAfter(const MCInst &Inst) const = 0;
protected:
BinaryContext &BC;
const unsigned NumRegs;
const TrackedRegisters RegsToTrackInstsFor;
/// Stores information about the detected instruction sequences emitted to
/// check an authenticated pointer. Specifically, if such sequence is detected
/// in a basic block, it maps the first instruction of that sequence to the
/// register being checked.
///
/// As the detection of such sequences requires iterating over the adjacent
/// instructions, it should be done before calling computeNext(), which
/// operates on separate instructions.
DenseMap<const MCInst *, MCPhysReg> RegCheckedAt;
SetOfRelatedInsts &firstLeakingInsts(DstState &S, MCPhysReg Reg) const {
unsigned Index = RegsToTrackInstsFor.getIndex(Reg);
return S.FirstInstLeakingReg[Index];
}
const SetOfRelatedInsts &firstLeakingInsts(const DstState &S,
MCPhysReg Reg) const {
unsigned Index = RegsToTrackInstsFor.getIndex(Reg);
return S.FirstInstLeakingReg[Index];
}
/// Creates a state with all registers marked unsafe (not to be confused
/// with empty state).
DstState createUnsafeState() {
return DstState(NumRegs, RegsToTrackInstsFor.getNumTrackedRegisters());
}
/// Returns the set of registers that can be leaked by this instruction.
/// A register is considered leaked if it has any intersection with any
/// register read by Inst. This is similar to how the set of clobbered
/// registers is computed, but taking input operands instead of outputs.
BitVector getLeakedRegs(const MCInst &Inst) const {
BitVector Leaked(NumRegs);
// Assume a call can read all registers.
if (BC.MIB->isCall(Inst)) {
Leaked.set();
return Leaked;
}
// Compute the set of registers overlapping with any register used by
// this instruction.
const MCInstrDesc &Desc = BC.MII->get(Inst.getOpcode());
for (MCPhysReg Reg : Desc.implicit_uses())
Leaked |= BC.MIB->getAliases(Reg, /*OnlySmaller=*/false);
for (const MCOperand &Op : BC.MIB->useOperands(Inst)) {
if (Op.isReg())
Leaked |= BC.MIB->getAliases(Op.getReg(), /*OnlySmaller=*/false);
}
return Leaked;
}
SmallVector<MCPhysReg> getRegsMadeProtected(const MCInst &Inst,
const BitVector &LeakedRegs,
const DstState &Cur) const {
SmallVector<MCPhysReg> Regs;
// A pointer can be checked, or
if (auto CheckedReg =
BC.MIB->getAuthCheckedReg(Inst, /*MayOverwrite=*/true))
Regs.push_back(*CheckedReg);
if (RegCheckedAt.contains(&Inst))
Regs.push_back(RegCheckedAt.at(&Inst));
// ... it can be used as a branch target, or
if (BC.MIB->isIndirectBranch(Inst) || BC.MIB->isIndirectCall(Inst)) {
bool IsAuthenticated;
MCPhysReg BranchDestReg =
BC.MIB->getRegUsedAsIndirectBranchDest(Inst, IsAuthenticated);
assert(BranchDestReg != BC.MIB->getNoRegister());
if (!IsAuthenticated)
Regs.push_back(BranchDestReg);
}
// ... it can be used as a return target, or
if (BC.MIB->isReturn(Inst)) {
bool IsAuthenticated = false;
std::optional<MCPhysReg> RetReg =
BC.MIB->getRegUsedAsRetDest(Inst, IsAuthenticated);
if (RetReg && !IsAuthenticated)
Regs.push_back(*RetReg);
}
// ... an address can be updated in a safe manner, or
if (auto DstAndSrc = BC.MIB->analyzeAddressArithmeticsForPtrAuth(Inst)) {
MCPhysReg DstReg, SrcReg;
std::tie(DstReg, SrcReg) = *DstAndSrc;
// Note that *all* registers containing the derived values must be safe,
// both source and destination ones. No temporaries are supported at now.
if (Cur.CannotEscapeUnchecked[SrcReg] &&
Cur.CannotEscapeUnchecked[DstReg])
Regs.push_back(SrcReg);
}
// ... the register can be overwritten in whole with a constant: for that
// purpose, look for the instructions with no register inputs (neither
// explicit nor implicit ones) and no side effects (to rule out reading
// not modelled locations).
const MCInstrDesc &Desc = BC.MII->get(Inst.getOpcode());
bool HasExplicitSrcRegs = llvm::any_of(BC.MIB->useOperands(Inst),
[](auto Op) { return Op.isReg(); });
if (!Desc.hasUnmodeledSideEffects() && !HasExplicitSrcRegs &&
Desc.implicit_uses().empty()) {
for (const MCOperand &Def : BC.MIB->defOperands(Inst))
Regs.push_back(Def.getReg());
}
return Regs;
}
DstState computeNext(const MCInst &Point, const DstState &Cur) {
DstStatePrinter P(BC);
LLVM_DEBUG({
dbgs() << " DstSafetyAnalysis::ComputeNext(";
BC.InstPrinter->printInst(&Point, 0, "", *BC.STI, dbgs());
dbgs() << ", ";
P.print(dbgs(), Cur);
dbgs() << ")\n";
});
// If this instruction is reachable by the analysis, a non-empty state will
// be propagated to it sooner or later. Until then, skip computeNext().
if (Cur.empty()) {
LLVM_DEBUG(
{ dbgs() << "Skipping computeNext(Point, Cur) as Cur is empty.\n"; });
return DstState();
}
// First, compute various properties of the instruction, taking the state
// after its execution into account, if necessary.
BitVector LeakedRegs = getLeakedRegs(Point);
SmallVector<MCPhysReg> NewProtectedRegs =
getRegsMadeProtected(Point, LeakedRegs, Cur);
// Then, compute the state before this instruction is executed.
DstState Next = Cur;
Next.CannotEscapeUnchecked.reset(LeakedRegs);
for (MCPhysReg Reg : RegsToTrackInstsFor.getRegisters()) {
if (LeakedRegs[Reg])
firstLeakingInsts(Next, Reg) = {&Point};
}
BitVector NewProtectedSubregs(NumRegs);
for (MCPhysReg Reg : NewProtectedRegs)
NewProtectedSubregs |= BC.MIB->getAliases(Reg, /*OnlySmaller=*/true);
Next.CannotEscapeUnchecked |= NewProtectedSubregs;
for (MCPhysReg Reg : RegsToTrackInstsFor.getRegisters()) {
if (NewProtectedSubregs[Reg])
firstLeakingInsts(Next, Reg).clear();
}
LLVM_DEBUG({
dbgs() << " .. result: (";
P.print(dbgs(), Next);
dbgs() << ")\n";
});
return Next;
}
public:
std::vector<MCInstReference> getLeakingInsts(const MCInst &Inst,
BinaryFunction &BF,
MCPhysReg LeakedReg) const {
const DstState &S = getStateAfter(Inst);
std::vector<MCInstReference> Result;
for (const MCInst *Inst : firstLeakingInsts(S, LeakedReg)) {
MCInstReference Ref = MCInstReference::get(Inst, BF);
assert(Ref && "Expected Inst to be found");
Result.push_back(Ref);
}
return Result;
}
};
class DataflowDstSafetyAnalysis
: public DstSafetyAnalysis,
public DataflowAnalysis<DataflowDstSafetyAnalysis, DstState,
/*Backward=*/true, DstStatePrinter> {
using DFParent = DataflowAnalysis<DataflowDstSafetyAnalysis, DstState, true,
DstStatePrinter>;
friend DFParent;
using DstSafetyAnalysis::BC;
using DstSafetyAnalysis::computeNext;
public:
DataflowDstSafetyAnalysis(BinaryFunction &BF,
MCPlusBuilder::AllocatorIdTy AllocId,
ArrayRef<MCPhysReg> RegsToTrackInstsFor)
: DstSafetyAnalysis(BF, RegsToTrackInstsFor), DFParent(BF, AllocId) {}
const DstState &getStateAfter(const MCInst &Inst) const override {
// The dataflow analysis base class iterates backwards over the
// instructions, thus "after" vs. "before" difference.
return DFParent::getStateBefore(Inst).get();
}
void run() override {
for (BinaryBasicBlock &BB : Func) {
if (auto CheckerInfo = BC.MIB->getAuthCheckedReg(BB)) {
LLVM_DEBUG({
dbgs() << "Found pointer checking sequence in " << BB.getName()
<< ":\n";
traceReg(BC, "Checked register", CheckerInfo->first);
traceInst(BC, "First instruction", *CheckerInfo->second);
});
RegCheckedAt[CheckerInfo->second] = CheckerInfo->first;
}
}
DFParent::run();
}
protected:
void preflight() {}
DstState getStartingStateAtBB(const BinaryBasicBlock &BB) {
// In general, the initial state should be empty, not everything-is-unsafe,
// to give a chance for some meaningful state to be propagated to BB from
// an indirectly reachable "exit basic block" ending with a return or tail
// call instruction.
//
// A basic block without any successors, on the other hand, can be
// pessimistically initialized to everything-is-unsafe: this will naturally
// handle both return and tail call instructions and is harmless for
// internal indirect branch instructions (such as computed gotos).
if (BB.succ_empty())
return createUnsafeState();
return DstState();
}
DstState getStartingStateAtPoint(const MCInst &Point) { return DstState(); }
void doConfluence(DstState &StateOut, const DstState &StateIn) {
DstStatePrinter P(BC);
LLVM_DEBUG({
dbgs() << " DataflowDstSafetyAnalysis::Confluence(\n";
dbgs() << " State 1: ";
P.print(dbgs(), StateOut);
dbgs() << "\n";
dbgs() << " State 2: ";
P.print(dbgs(), StateIn);
dbgs() << ")\n";
});
StateOut.merge(StateIn);
LLVM_DEBUG({
dbgs() << " merged state: ";
P.print(dbgs(), StateOut);
dbgs() << "\n";
});
}
StringRef getAnnotationName() const { return "DataflowDstSafetyAnalysis"; }
};
class CFGUnawareDstSafetyAnalysis : public DstSafetyAnalysis,
public CFGUnawareAnalysis<DstState> {
using DstSafetyAnalysis::BC;
BinaryFunction &BF;
public:
CFGUnawareDstSafetyAnalysis(BinaryFunction &BF,
MCPlusBuilder::AllocatorIdTy AllocId,
ArrayRef<MCPhysReg> RegsToTrackInstsFor)
: DstSafetyAnalysis(BF, RegsToTrackInstsFor),
CFGUnawareAnalysis(BF, AllocId, "CFGUnawareDstSafetyAnalysis"), BF(BF) {
}
void run() override {
DstState S = createUnsafeState();
for (auto &I : llvm::reverse(BF.instrs())) {
MCInst &Inst = I.second;
// If Inst can change the control flow, we cannot be sure that the next
// instruction (to be executed in analyzed program) is the one processed
// on the previous iteration, thus pessimistically reset S before
// starting to analyze Inst.
if (BC.MIB->isCall(Inst) || BC.MIB->isBranch(Inst) ||
BC.MIB->isReturn(Inst)) {
LLVM_DEBUG({ traceInst(BC, "Control flow instruction", Inst); });
S = createUnsafeState();
}
// Attach the state *after* this instruction executes.
setState(Inst, S);
// Compute the next state.
S = computeNext(Inst, S);
}
}
const DstState &getStateAfter(const MCInst &Inst) const override {
return getState(Inst);
}
};
std::shared_ptr<DstSafetyAnalysis>
DstSafetyAnalysis::create(BinaryFunction &BF,
MCPlusBuilder::AllocatorIdTy AllocId,
ArrayRef<MCPhysReg> RegsToTrackInstsFor) {
if (BF.hasCFG())
return std::make_shared<DataflowDstSafetyAnalysis>(BF, AllocId,
RegsToTrackInstsFor);
return std::make_shared<CFGUnawareDstSafetyAnalysis>(BF, AllocId,
RegsToTrackInstsFor);
}
// This function could return PartialReport<T>, but currently T is always
// MCPhysReg, even though it is an implementation detail.
static PartialReport<MCPhysReg> make_generic_report(MCInstReference Location,
@ -808,6 +1303,37 @@ shouldReportSigningOracle(const BinaryContext &BC, const MCInstReference &Inst,
return make_gadget_report(SigningOracleKind, Inst, *SignedReg);
}
static std::optional<PartialReport<MCPhysReg>>
shouldReportAuthOracle(const BinaryContext &BC, const MCInstReference &Inst,
const DstState &S) {
static const GadgetKind AuthOracleKind("authentication oracle found");
bool IsChecked = false;
std::optional<MCPhysReg> AuthReg =
BC.MIB->getWrittenAuthenticatedReg(Inst, IsChecked);
if (!AuthReg || IsChecked)
return std::nullopt;
LLVM_DEBUG({
traceInst(BC, "Found auth inst", Inst);
traceReg(BC, "Authenticated reg", *AuthReg);
});
if (S.empty()) {
LLVM_DEBUG({ dbgs() << " DstState is empty!\n"; });
return make_generic_report(
Inst, "Warning: no state computed for an authentication instruction "
"(possibly unreachable)");
}
LLVM_DEBUG(
{ traceRegMask(BC, "safe output registers", S.CannotEscapeUnchecked); });
if (S.CannotEscapeUnchecked[*AuthReg])
return std::nullopt;
return make_gadget_report(AuthOracleKind, Inst, *AuthReg);
}
template <typename T> static void iterateOverInstrs(BinaryFunction &BF, T Fn) {
if (BF.hasCFG()) {
for (BinaryBasicBlock &BB : BF)
@ -889,6 +1415,52 @@ void FunctionAnalysisContext::augmentUnsafeUseReports(
}
}
void FunctionAnalysisContext::findUnsafeDefs(
SmallVector<PartialReport<MCPhysReg>> &Reports) {
if (PacRetGadgetsOnly)
return;
auto Analysis = DstSafetyAnalysis::create(BF, AllocatorId, {});
LLVM_DEBUG({ dbgs() << "Running dst register safety analysis...\n"; });
Analysis->run();
LLVM_DEBUG({
dbgs() << "After dst register safety analysis:\n";
BF.dump();
});
iterateOverInstrs(BF, [&](MCInstReference Inst) {
const DstState &S = Analysis->getStateAfter(Inst);
if (auto Report = shouldReportAuthOracle(BC, Inst, S))
Reports.push_back(*Report);
});
}
void FunctionAnalysisContext::augmentUnsafeDefReports(
ArrayRef<PartialReport<MCPhysReg>> Reports) {
SmallVector<MCPhysReg> RegsToTrack = collectRegsToTrack(Reports);
// Re-compute the analysis with register tracking.
auto Analysis = DstSafetyAnalysis::create(BF, AllocatorId, RegsToTrack);
LLVM_DEBUG(
{ dbgs() << "\nRunning detailed dst register safety analysis...\n"; });
Analysis->run();
LLVM_DEBUG({
dbgs() << "After detailed dst register safety analysis:\n";
BF.dump();
});
// Augment gadget reports.
for (auto &Report : Reports) {
MCInstReference Location = Report.Issue->Location;
LLVM_DEBUG({ traceInst(BC, "Attaching leakage info to", Location); });
assert(Report.RequestedDetails &&
"Should be removed by handleSimpleReports");
auto DetailedInfo = std::make_shared<LeakageInfo>(
Analysis->getLeakingInsts(Location, BF, *Report.RequestedDetails));
Result.Diagnostics.emplace_back(Report.Issue, DetailedInfo);
}
}
void FunctionAnalysisContext::handleSimpleReports(
SmallVector<PartialReport<MCPhysReg>> &Reports) {
// Before re-running the detailed analysis, process the reports which do not
@ -912,6 +1484,12 @@ void FunctionAnalysisContext::run() {
handleSimpleReports(UnsafeUses);
if (!UnsafeUses.empty())
augmentUnsafeUseReports(UnsafeUses);
SmallVector<PartialReport<MCPhysReg>> UnsafeDefs;
findUnsafeDefs(UnsafeDefs);
handleSimpleReports(UnsafeDefs);
if (!UnsafeDefs.empty())
augmentUnsafeDefReports(UnsafeDefs);
}
void Analysis::runOnFunction(BinaryFunction &BF,
@ -1015,6 +1593,12 @@ void ClobberingInfo::print(raw_ostream &OS,
printRelatedInstrs(OS, Location, ClobberingInstrs);
}
void LeakageInfo::print(raw_ostream &OS, const MCInstReference Location) const {
OS << " The " << LeakingInstrs.size()
<< " instructions that leak the affected registers are:\n";
printRelatedInstrs(OS, Location, LeakingInstrs);
}
void GenericDiagnostic::generateReport(raw_ostream &OS,
const BinaryContext &BC) const {
printBasicInfo(OS, BC, Text);

View File

@ -0,0 +1,812 @@
// RUN: %clang %cflags -march=armv8.3-a %s -o %t.exe
// RUN: llvm-bolt-binary-analysis --scanners=pacret %t.exe 2>&1 | FileCheck -check-prefix=PACRET %s
// RUN: llvm-bolt-binary-analysis --scanners=pauth %t.exe 2>&1 | FileCheck %s
// The detection of compiler-generated explicit pointer checks is tested in
// gs-pauth-address-checks.s, for that reason only test here "dummy-load" and
// "high-bits-notbi" checkers, as the shortest examples of checkers that are
// detected per-instruction and per-BB.
// PACRET-NOT: authentication oracle found in function
.text
.type sym,@function
sym:
ret
.size sym, .-sym
.globl callee
.type callee,@function
callee:
ret
.size callee, .-callee
.globl good_ret
.type good_ret,@function
good_ret:
// CHECK-NOT: good_ret
autia x0, x1
ret x0
.size good_ret, .-good_ret
.globl good_call
.type good_call,@function
good_call:
// CHECK-NOT: good_call
paciasp
stp x29, x30, [sp, #-16]!
mov x29, sp
autia x0, x1
blr x0
ldp x29, x30, [sp], #16
autiasp
ret
.size good_call, .-good_call
.globl good_branch
.type good_branch,@function
good_branch:
// CHECK-NOT: good_branch
autia x0, x1
br x0
.size good_branch, .-good_branch
.globl good_load_other_reg
.type good_load_other_reg,@function
good_load_other_reg:
// CHECK-NOT: good_load_other_reg
autia x0, x1
ldr x2, [x0]
ret
.size good_load_other_reg, .-good_load_other_reg
.globl good_load_same_reg
.type good_load_same_reg,@function
good_load_same_reg:
// CHECK-NOT: good_load_same_reg
autia x0, x1
ldr x0, [x0]
ret
.size good_load_same_reg, .-good_load_same_reg
.globl good_explicit_check
.type good_explicit_check,@function
good_explicit_check:
// CHECK-NOT: good_explicit_check
autia x0, x1
eor x16, x0, x0, lsl #1
tbz x16, #62, 1f
brk 0x1234
1:
ret
.size good_explicit_check, .-good_explicit_check
.globl bad_unchecked
.type bad_unchecked,@function
bad_unchecked:
// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unchecked, basic block {{[^,]+}}, at address
// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1
// CHECK-NEXT: The 0 instructions that leak the affected registers are:
autia x0, x1
ret
.size bad_unchecked, .-bad_unchecked
.globl bad_leaked_to_subroutine
.type bad_leaked_to_subroutine,@function
bad_leaked_to_subroutine:
// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_leaked_to_subroutine, basic block {{[^,]+}}, at address
// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1
// CHECK-NEXT: The 1 instructions that leak the affected registers are:
// CHECK-NEXT: 1. {{[0-9a-f]+}}: bl callee
// CHECK-NEXT: This happens in the following basic block:
// CHECK-NEXT: {{[0-9a-f]+}}: paciasp
// CHECK-NEXT: {{[0-9a-f]+}}: stp x29, x30, [sp, #-0x10]!
// CHECK-NEXT: {{[0-9a-f]+}}: mov x29, sp
// CHECK-NEXT: {{[0-9a-f]+}}: autia x0, x1
// CHECK-NEXT: {{[0-9a-f]+}}: bl callee
// CHECK-NEXT: {{[0-9a-f]+}}: ldr x2, [x0]
// CHECK-NEXT: {{[0-9a-f]+}}: ldp x29, x30, [sp], #0x10
// CHECK-NEXT: {{[0-9a-f]+}}: autiasp
// CHECK-NEXT: {{[0-9a-f]+}}: ret
paciasp
stp x29, x30, [sp, #-16]!
mov x29, sp
autia x0, x1
bl callee
ldr x2, [x0]
ldp x29, x30, [sp], #16
autiasp
ret
.size bad_leaked_to_subroutine, .-bad_leaked_to_subroutine
.globl bad_unknown_usage_read
.type bad_unknown_usage_read,@function
bad_unknown_usage_read:
// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unknown_usage_read, basic block {{[^,]+}}, at address
// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1
// CHECK-NEXT: The 1 instructions that leak the affected registers are:
// CHECK-NEXT: 1. {{[0-9a-f]+}}: mul x3, x0, x1
// CHECK-NEXT: This happens in the following basic block:
// CHECK-NEXT: {{[0-9a-f]+}}: autia x0, x1
// CHECK-NEXT: {{[0-9a-f]+}}: mul x3, x0, x1
// CHECK-NEXT: {{[0-9a-f]+}}: ldr x2, [x0]
// CHECK-NEXT: {{[0-9a-f]+}}: ret
autia x0, x1
// Registers are not accessible to an attacker under Pointer
// Authentication threat model, until spilled to memory.
// Thus, reporting the below MUL instruction is a false positive, since
// the next LDR instruction prevents any possible spilling of x3 unless
// the authentication succeeded. Though, rejecting anything except for
// a closed list of instruction types is the intended behavior of the
// analysis, so this false positive is by design.
mul x3, x0, x1
ldr x2, [x0]
ret
.size bad_unknown_usage_read, .-bad_unknown_usage_read
.globl bad_store_to_memory_and_wait
.type bad_store_to_memory_and_wait,@function
bad_store_to_memory_and_wait:
// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_store_to_memory_and_wait, basic block {{[^,]+}}, at address
// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1
// CHECK-NEXT: The 1 instructions that leak the affected registers are:
// CHECK-NEXT: 1. {{[0-9a-f]+}}: str x0, [x3]
autia x0, x1
cbz x3, 2f
str x0, [x3]
1:
// The thread performs a time-consuming computation while the result of
// authentication is accessible in memory.
nop
2:
ldr x2, [x0]
ret
.size bad_store_to_memory_and_wait, .-bad_store_to_memory_and_wait
// FIXME: Known false negative: if no return instruction is reachable from a
// program point (this probably implies an infinite loop), such
// instruction cannot be detected as an authentication oracle.
.globl bad_store_to_memory_and_hang
.type bad_store_to_memory_and_hang,@function
bad_store_to_memory_and_hang:
// CHECK-NOT: bad_store_to_memory_and_hang
autia x0, x1
cbz x3, 2f
str x0, [x3]
1:
// The thread loops indefinitely while the result of authentication
// is accessible in memory.
b 1b
2:
ldr x2, [x0]
ret
.size bad_store_to_memory_and_hang, .-bad_store_to_memory_and_hang
.globl bad_unknown_usage_subreg_read
.type bad_unknown_usage_subreg_read,@function
bad_unknown_usage_subreg_read:
// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unknown_usage_subreg_read, basic block {{[^,]+}}, at address
// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1
// CHECK-NEXT: The 1 instructions that leak the affected registers are:
// CHECK-NEXT: 1. {{[0-9a-f]+}}: mul w3, w0, w1
// CHECK-NEXT: This happens in the following basic block:
// CHECK-NEXT: {{[0-9a-f]+}}: autia x0, x1
// CHECK-NEXT: {{[0-9a-f]+}}: mul w3, w0, w1
// CHECK-NEXT: {{[0-9a-f]+}}: ldr x2, [x0]
// CHECK-NEXT: {{[0-9a-f]+}}: ret
autia x0, x1
mul w3, w0, w1
ldr x2, [x0]
ret
.size bad_unknown_usage_subreg_read, .-bad_unknown_usage_subreg_read
.globl bad_unknown_usage_update
.type bad_unknown_usage_update,@function
bad_unknown_usage_update:
// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unknown_usage_update, basic block {{[^,]+}}, at address
// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1
// CHECK-NEXT: The 1 instructions that leak the affected registers are:
// CHECK-NEXT: 1. {{[0-9a-f]+}}: movk x0, #0x2a, lsl #16
// CHECK-NEXT: This happens in the following basic block:
// CHECK-NEXT: {{[0-9a-f]+}}: autia x0, x1
// CHECK-NEXT: {{[0-9a-f]+}}: movk x0, #0x2a, lsl #16
// CHECK-NEXT: {{[0-9a-f]+}}: ldr x2, [x0]
// CHECK-NEXT: {{[0-9a-f]+}}: ret
autia x0, x1
movk x0, #42, lsl #16 // does not overwrite x0 completely
ldr x2, [x0]
ret
.size bad_unknown_usage_update, .-bad_unknown_usage_update
.globl good_overwrite_with_constant
.type good_overwrite_with_constant,@function
good_overwrite_with_constant:
// CHECK-NOT: good_overwrite_with_constant
autia x0, x1
mov x0, #42
ret
.size good_overwrite_with_constant, .-good_overwrite_with_constant
// Overwriting sensitive data by instructions with unmodelled side-effects is
// explicitly rejected, even though this particular MRS is safe.
.globl bad_overwrite_with_side_effects
.type bad_overwrite_with_side_effects,@function
bad_overwrite_with_side_effects:
// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_overwrite_with_side_effects, basic block {{[^,]+}}, at address
// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1
// CHECK-NEXT: The 0 instructions that leak the affected registers are:
autia x0, x1
mrs x0, CTR_EL0
ret
.size bad_overwrite_with_side_effects, .-bad_overwrite_with_side_effects
// Here the new value written by MUL to x0 is completely unrelated to the result
// of authentication, so this is a false positive.
// FIXME: Can/should we generalize overwriting by constant to handle such cases?
.globl good_unknown_overwrite
.type good_unknown_overwrite,@function
good_unknown_overwrite:
// CHECK-LABEL: GS-PAUTH: authentication oracle found in function good_unknown_overwrite, basic block {{[^,]+}}, at address
// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1
// CHECK-NEXT: The 0 instructions that leak the affected registers are:
autia x0, x1
mul x0, x1, x2
ret
.size good_unknown_overwrite, .-good_unknown_overwrite
// This is a false positive: when a general-purpose register is written to as
// a 32-bit register, its top 32 bits are zeroed, but according to LLVM
// representation, the instruction only overwrites the Wn register.
.globl good_wreg_overwrite
.type good_wreg_overwrite,@function
good_wreg_overwrite:
// CHECK-LABEL: GS-PAUTH: authentication oracle found in function good_wreg_overwrite, basic block {{[^,]+}}, at address
// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1
autia x0, x1
mov w0, #42
ret
.size good_wreg_overwrite, .-good_wreg_overwrite
.globl good_address_arith
.type good_address_arith,@function
good_address_arith:
// CHECK-NOT: good_address_arith
autia x0, x1
add x1, x0, #8
sub x2, x1, #16
mov x3, x2
ldr x4, [x3]
mov x0, #0
mov x1, #0
mov x2, #0
ret
.size good_address_arith, .-good_address_arith
.globl good_ret_multi_bb
.type good_ret_multi_bb,@function
good_ret_multi_bb:
// CHECK-NOT: good_ret_multi_bb
autia x0, x1
cbz x1, 1f
nop
1:
ret x0
.size good_ret_multi_bb, .-good_ret_multi_bb
.globl good_call_multi_bb
.type good_call_multi_bb,@function
good_call_multi_bb:
// CHECK-NOT: good_call_multi_bb
paciasp
stp x29, x30, [sp, #-16]!
mov x29, sp
autia x0, x1
cbz x1, 1f
nop
1:
blr x0
cbz x1, 2f
nop
2:
ldp x29, x30, [sp], #16
autiasp
ret
.size good_call_multi_bb, .-good_call_multi_bb
.globl good_branch_multi_bb
.type good_branch_multi_bb,@function
good_branch_multi_bb:
// CHECK-NOT: good_branch_multi_bb
autia x0, x1
cbz x1, 1f
nop
1:
br x0
.size good_branch_multi_bb, .-good_branch_multi_bb
.globl good_load_other_reg_multi_bb
.type good_load_other_reg_multi_bb,@function
good_load_other_reg_multi_bb:
// CHECK-NOT: good_load_other_reg_multi_bb
autia x0, x1
cbz x1, 1f
nop
1:
ldr x2, [x0]
cbz x1, 2f
nop
2:
ret
.size good_load_other_reg_multi_bb, .-good_load_other_reg_multi_bb
.globl good_load_same_reg_multi_bb
.type good_load_same_reg_multi_bb,@function
good_load_same_reg_multi_bb:
// CHECK-NOT: good_load_same_reg_multi_bb
autia x0, x1
cbz x1, 1f
nop
1:
ldr x0, [x0]
cbz x1, 2f
nop
2:
ret
.size good_load_same_reg_multi_bb, .-good_load_same_reg_multi_bb
.globl good_explicit_check_multi_bb
.type good_explicit_check_multi_bb,@function
good_explicit_check_multi_bb:
// CHECK-NOT: good_explicit_check_multi_bb
autia x0, x1
cbz x1, 1f
nop
1:
eor x16, x0, x0, lsl #1
tbz x16, #62, 2f
brk 0x1234
2:
cbz x1, 3f
nop
3:
ret
.size good_explicit_check_multi_bb, .-good_explicit_check_multi_bb
.globl bad_unchecked_multi_bb
.type bad_unchecked_multi_bb,@function
bad_unchecked_multi_bb:
// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unchecked_multi_bb, basic block {{[^,]+}}, at address
// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1
// CHECK-NEXT: The 0 instructions that leak the affected registers are:
autia x0, x1
cbz x1, 1f
ldr x2, [x0]
1:
ret
.size bad_unchecked_multi_bb, .-bad_unchecked_multi_bb
.globl bad_leaked_to_subroutine_multi_bb
.type bad_leaked_to_subroutine_multi_bb,@function
bad_leaked_to_subroutine_multi_bb:
// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_leaked_to_subroutine_multi_bb, basic block {{[^,]+}}, at address
// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1
// CHECK-NEXT: The 1 instructions that leak the affected registers are:
// CHECK-NEXT: 1. {{[0-9a-f]+}}: bl callee
paciasp
stp x29, x30, [sp, #-16]!
mov x29, sp
autia x0, x1
cbz x1, 1f
ldr x2, [x0]
1:
bl callee
ldr x2, [x0]
ldp x29, x30, [sp], #16
autiasp
ret
.size bad_leaked_to_subroutine_multi_bb, .-bad_leaked_to_subroutine_multi_bb
.globl bad_unknown_usage_read_multi_bb
.type bad_unknown_usage_read_multi_bb,@function
bad_unknown_usage_read_multi_bb:
// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unknown_usage_read_multi_bb, basic block {{[^,]+}}, at address
// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1
// CHECK-NEXT: The 1 instructions that leak the affected registers are:
// CHECK-NEXT: 1. {{[0-9a-f]+}}: mul x3, x0, x1
autia x0, x1
cbz x3, 1f
mul x3, x0, x1
1:
ldr x2, [x0]
ret
.size bad_unknown_usage_read_multi_bb, .-bad_unknown_usage_read_multi_bb
.globl bad_unknown_usage_subreg_read_multi_bb
.type bad_unknown_usage_subreg_read_multi_bb,@function
bad_unknown_usage_subreg_read_multi_bb:
// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unknown_usage_subreg_read_multi_bb, basic block {{[^,]+}}, at address
// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1
// CHECK-NEXT: The 1 instructions that leak the affected registers are:
// CHECK-NEXT: 1. {{[0-9a-f]+}}: mul w3, w0, w1
autia x0, x1
cbz x3, 1f
mul w3, w0, w1
1:
ldr x2, [x0]
ret
.size bad_unknown_usage_subreg_read_multi_bb, .-bad_unknown_usage_subreg_read_multi_bb
.globl bad_unknown_usage_update_multi_bb
.type bad_unknown_usage_update_multi_bb,@function
bad_unknown_usage_update_multi_bb:
// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unknown_usage_update_multi_bb, basic block {{[^,]+}}, at address
// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1
// CHECK-NEXT: The 1 instructions that leak the affected registers are:
// CHECK-NEXT: 1. {{[0-9a-f]+}}: movk x0, #0x2a, lsl #16
autia x0, x1
cbz x3, 1f
movk x0, #42, lsl #16 // does not overwrite x0 completely
1:
ldr x2, [x0]
ret
.size bad_unknown_usage_update_multi_bb, .-bad_unknown_usage_update_multi_bb
.globl good_overwrite_with_constant_multi_bb
.type good_overwrite_with_constant_multi_bb,@function
good_overwrite_with_constant_multi_bb:
// CHECK-NOT: good_overwrite_with_constant_multi_bb
autia x0, x1
cbz x3, 1f
1:
mov x0, #42
ret
.size good_overwrite_with_constant_multi_bb, .-good_overwrite_with_constant_multi_bb
.globl good_address_arith_multi_bb
.type good_address_arith_multi_bb,@function
good_address_arith_multi_bb:
// CHECK-NOT: good_address_arith_multi_bb
autia x0, x1
cbz x3, 1f
add x1, x0, #8
sub x2, x1, #16
mov x0, x2
mov x1, #0
mov x2, #0
1:
ldr x3, [x0]
ret
.size good_address_arith_multi_bb, .-good_address_arith_multi_bb
// FIXME: Most *_nocfg test cases contain paciasp+autiasp instructions even if
// LR is not spilled - this is a workaround for RET instructions being
// reported as non-protected, because LR state is reset at every label.
.globl good_ret_nocfg
.type good_ret_nocfg,@function
good_ret_nocfg:
// CHECK-NOT: good_ret_nocfg
adr x2, 1f
br x2
1:
autia x0, x1
ret x0
.size good_ret_nocfg, .-good_ret_nocfg
.globl good_call_nocfg
.type good_call_nocfg,@function
good_call_nocfg:
// CHECK-NOT: good_call_nocfg
paciasp
stp x29, x30, [sp, #-16]!
mov x29, sp
adr x2, 1f
br x2
1:
autia x0, x1
blr x0
ldp x29, x30, [sp], #16
autiasp
ret
.size good_call_nocfg, .-good_call_nocfg
.globl good_branch_nocfg
.type good_branch_nocfg,@function
good_branch_nocfg:
// CHECK-NOT: good_branch_nocfg
adr x2, 1f
br x2
1:
autia x0, x1
br x0
.size good_branch_nocfg, .-good_branch_nocfg
.globl good_load_other_reg_nocfg
.type good_load_other_reg_nocfg,@function
good_load_other_reg_nocfg:
// CHECK-NOT: good_load_other_reg_nocfg
paciasp
adr x2, 1f
br x2
1:
autia x0, x1
ldr x2, [x0]
autiasp
ret
.size good_load_other_reg_nocfg, .-good_load_other_reg_nocfg
.globl good_load_same_reg_nocfg
.type good_load_same_reg_nocfg,@function
good_load_same_reg_nocfg:
// CHECK-NOT: good_load_same_reg_nocfg
paciasp
adr x2, 1f
br x2
1:
autia x0, x1
ldr x0, [x0]
autiasp
ret
.size good_load_same_reg_nocfg, .-good_load_same_reg_nocfg
// FIXME: Multi-instruction checker sequences are not supported without CFG.
.globl bad_unchecked_nocfg
.type bad_unchecked_nocfg,@function
bad_unchecked_nocfg:
// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unchecked_nocfg, at address
// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1
// CHECK-NEXT: The 0 instructions that leak the affected registers are:
paciasp
adr x2, 1f
br x2
1:
autia x0, x1
autiasp
ret
.size bad_unchecked_nocfg, .-bad_unchecked_nocfg
.globl bad_leaked_to_subroutine_nocfg
.type bad_leaked_to_subroutine_nocfg,@function
bad_leaked_to_subroutine_nocfg:
// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_leaked_to_subroutine_nocfg, at address
// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1
// CHECK-NEXT: The 1 instructions that leak the affected registers are:
// CHECK-NEXT: 1. {{[0-9a-f]+}}: bl callee # Offset: 24
paciasp
stp x29, x30, [sp, #-16]!
mov x29, sp
adr x2, 1f
br x2
1:
autia x0, x1
bl callee
ldr x2, [x0]
ldp x29, x30, [sp], #16
autiasp
ret
.size bad_leaked_to_subroutine_nocfg, .-bad_leaked_to_subroutine_nocfg
.globl bad_unknown_usage_read_nocfg
.type bad_unknown_usage_read_nocfg,@function
bad_unknown_usage_read_nocfg:
// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unknown_usage_read_nocfg, at address
// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1
// CHECK-NEXT: The 1 instructions that leak the affected registers are:
// CHECK-NEXT: 1. {{[0-9a-f]+}}: mul x3, x0, x1
paciasp
adr x2, 1f
br x2
1:
autia x0, x1
mul x3, x0, x1
ldr x2, [x0]
autiasp
ret
.size bad_unknown_usage_read_nocfg, .-bad_unknown_usage_read_nocfg
.globl bad_unknown_usage_subreg_read_nocfg
.type bad_unknown_usage_subreg_read_nocfg,@function
bad_unknown_usage_subreg_read_nocfg:
// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unknown_usage_subreg_read_nocfg, at address
// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1
// CHECK-NEXT: The 1 instructions that leak the affected registers are:
// CHECK-NEXT: 1. {{[0-9a-f]+}}: mul w3, w0, w1
paciasp
adr x2, 1f
br x2
1:
autia x0, x1
mul w3, w0, w1
ldr x2, [x0]
autiasp
ret
.size bad_unknown_usage_subreg_read_nocfg, .-bad_unknown_usage_subreg_read_nocfg
.globl bad_unknown_usage_update_nocfg
.type bad_unknown_usage_update_nocfg,@function
bad_unknown_usage_update_nocfg:
// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_unknown_usage_update_nocfg, at address
// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autia x0, x1
// CHECK-NEXT: The 1 instructions that leak the affected registers are:
// CHECK-NEXT: 1. {{[0-9a-f]+}}: movk x0, #0x2a, lsl #16
paciasp
adr x2, 1f
br x2
1:
autia x0, x1
movk x0, #42, lsl #16 // does not overwrite x0 completely
ldr x2, [x0]
autiasp
ret
.size bad_unknown_usage_update_nocfg, .-bad_unknown_usage_update_nocfg
.globl good_overwrite_with_constant_nocfg
.type good_overwrite_with_constant_nocfg,@function
good_overwrite_with_constant_nocfg:
// CHECK-NOT: good_overwrite_with_constant_nocfg
paciasp
adr x2, 1f
br x2
1:
autia x0, x1
mov x0, #42
autiasp
ret
.size good_overwrite_with_constant_nocfg, .-good_overwrite_with_constant_nocfg
.globl good_address_arith_nocfg
.type good_address_arith_nocfg,@function
good_address_arith_nocfg:
// CHECK-NOT: good_address_arith_nocfg
paciasp
adr x2, 1f
br x2
1:
autia x0, x1
add x1, x0, #8
sub x2, x1, #16
mov x3, x2
ldr x4, [x3]
mov x0, #0
mov x1, #0
mov x2, #0
autiasp
ret
.size good_address_arith_nocfg, .-good_address_arith_nocfg
.globl good_explicit_check_unrelated_reg
.type good_explicit_check_unrelated_reg,@function
good_explicit_check_unrelated_reg:
// CHECK-LABEL: GS-PAUTH: authentication oracle found in function good_explicit_check_unrelated_reg, basic block {{[^,]+}}, at address
// FIXME: The below instruction is not an authentication oracle
autia x2, x3 // One of possible execution paths after this instruction
// ends at BRK below, thus BRK used as a trap instruction
// should formally "check everything" not to introduce
// false-positive here.
autia x0, x1
eor x16, x0, x0, lsl #1
tbz x16, #62, 1f
brk 0x1234
1:
ldr x4, [x2] // Right before this instruction X2 is checked - this
// should be propagated to the basic block ending with
// TBZ instruction above.
ret
.size good_explicit_check_unrelated_reg, .-good_explicit_check_unrelated_reg
// The last BB (in layout order) is processed first by the data-flow analysis.
// Its initial state is usually filled in a special way (because it ends with
// `ret` instruction), and then affects the state propagated to the other BBs
// Thus, the case of the last instruction in a function being a jump somewhere
// in the middle is special.
.globl good_no_ret_from_last_bb
.type good_no_ret_from_last_bb,@function
good_no_ret_from_last_bb:
// CHECK-NOT: good_no_ret_from_last_bb
paciasp
autiasp // authenticates LR
b 2f
1:
ret
2:
b 1b // LR is dereferenced by `ret`, which is executed next
.size good_no_ret_from_last_bb, .-good_no_ret_from_last_bb
.globl bad_no_ret_from_last_bb
.type bad_no_ret_from_last_bb,@function
bad_no_ret_from_last_bb:
// CHECK-LABEL: GS-PAUTH: authentication oracle found in function bad_no_ret_from_last_bb, basic block {{[^,]+}}, at address
// CHECK-NEXT: The instruction is {{[0-9a-f]+}}: autiasp
// CHECK-NEXT: The 0 instructions that leak the affected registers are:
paciasp
autiasp // authenticates LR
b 2f
1:
ret x0
2:
b 1b // X0 (but not LR) is dereferenced by `ret x0`
.size bad_no_ret_from_last_bb, .-bad_no_ret_from_last_bb
// Test that combined auth+something instructions are not reported as
// authentication oracles.
.globl inst_retaa
.type inst_retaa,@function
inst_retaa:
// CHECK-NOT: inst_retaa
paciasp
retaa
.size inst_retaa, .-inst_retaa
.globl inst_blraa
.type inst_blraa,@function
inst_blraa:
// CHECK-NOT: inst_blraa
paciasp
stp x29, x30, [sp, #-16]!
mov x29, sp
blraa x0, x1
ldp x29, x30, [sp], #16
retaa
.size inst_blraa, .-inst_blraa
.globl inst_braa
.type inst_braa,@function
inst_braa:
// CHECK-NOT: inst_braa
braa x0, x1
.size inst_braa, .-inst_braa
.globl inst_ldraa_no_wb
.type inst_ldraa_no_wb,@function
inst_ldraa_no_wb:
// CHECK-NOT: inst_ldraa_no_wb
ldraa x1, [x0]
ret
.size inst_ldraa_no_wb, .-inst_ldraa_no_wb
.globl inst_ldraa_wb
.type inst_ldraa_wb,@function
inst_ldraa_wb:
// CHECK-NOT: inst_ldraa_wb
ldraa x1, [x0]!
ret
.size inst_ldraa_wb, .-inst_ldraa_wb
.globl main
.type main,@function
main:
mov x0, 0
ret
.size main, .-main

View File

@ -113,7 +113,7 @@ simple:
// CHECK-EMPTY:
// PAUTH-NEXT: Found sign inst: 00000000: paciasp # DataflowSrcSafetyAnalysis: src-state<SafeToDerefRegs: BitVector, TrustedRegs: BitVector, Insts: >
// PAUTH-NEXT: Signed reg: LR
// PAUTH-NEXT: TrustedRegs: LR W30 W30_HI
// PAUTH-NEXT: TrustedRegs: LR W30 W30_HI{{[ \t]*$}}
// PAUTH-NEXT: Found call inst: 00000000: blr x0 # DataflowSrcSafetyAnalysis: src-state<SafeToDerefRegs: BitVector, TrustedRegs: BitVector, Insts: >
// PAUTH-NEXT: Call destination reg: X0
// PAUTH-NEXT: SafeToDerefRegs: W0 X0 W0_HI{{[ \t]*$}}
@ -220,10 +220,10 @@ nocfg:
// CHECK-EMPTY:
// PAUTH-NEXT: Found call inst: 00000000: br x0 # UNKNOWN CONTROL FLOW # Offset: 4 # CFGUnawareSrcSafetyAnalysis: src-state<SafeToDerefRegs: BitVector, TrustedRegs: BitVector, Insts: >
// PAUTH-NEXT: Call destination reg: X0
// PAUTH-NEXT: SafeToDerefRegs: LR W0 W30 X0 W0_HI W30_HI
// PAUTH-NEXT: SafeToDerefRegs: LR W0 W30 X0 W0_HI W30_HI{{[ \t]*$}}
// CHECK-NEXT: Found RET inst: 00000000: ret # Offset: 8 # CFGUnawareSrcSafetyAnalysis: src-state<SafeToDerefRegs: BitVector, TrustedRegs: BitVector, Insts: >
// CHECK-NEXT: RetReg: LR
// CHECK-NEXT: SafeToDerefRegs:
// CHECK-NEXT: SafeToDerefRegs:{{[ \t]*$}}
// CHECK-EMPTY:
// CHECK-NEXT: Running detailed src register safety analysis...
// CHECK-NEXT: SrcSafetyAnalysis::ComputeNext( adr x0, __ENTRY_nocfg@0x[[ENTRY_ADDR]], src-state<SafeToDerefRegs: LR W30 W30_HI , TrustedRegs: LR W30 W30_HI , Insts: [0]()>)
@ -251,6 +251,84 @@ nocfg:
// CHECK-EMPTY:
// CHECK-NEXT: Attaching clobbering info to: 00000000: ret # Offset: 8 # CFGUnawareSrcSafetyAnalysis: src-state<SafeToDerefRegs: BitVector, TrustedRegs: BitVector, Insts: [0]()>
.globl auth_oracle
.type auth_oracle,@function
auth_oracle:
autia x0, x1
ret
.size auth_oracle, .-auth_oracle
// CHECK-LABEL:Analyzing function auth_oracle, AllocatorId = 1
// CHECK-NEXT: Binary Function "auth_oracle" {
// CHECK-NEXT: Number : 4
// CHECK-NEXT: State : CFG constructed
// ...
// CHECK: BB Layout : [[BB0:[0-9a-zA-Z.]+]]
// CHECK-NEXT: }
// CHECK-NEXT: [[BB0]] (2 instructions, align : 1)
// CHECK-NEXT: Entry Point
// CHECK-NEXT: 00000000: autia x0, x1
// CHECK-NEXT: 00000004: ret
// CHECK-EMPTY:
// CHECK-NEXT: DWARF CFI Instructions:
// CHECK-NEXT: <empty>
// CHECK-NEXT: End of Function "auth_oracle"
// CHECK-EMPTY:
// CHECK-NEXT: Running src register safety analysis...
// ...
// CHECK: After src register safety analysis:
// CHECK-NEXT: Binary Function "auth_oracle" {
// ...
// CHECK: End of Function "auth_oracle"
// ...
// PAUTH: Running dst register safety analysis...
// PAUTH-NEXT: DstSafetyAnalysis::ComputeNext( ret x30, dst-state<CannotEscapeUnchecked: , Insts: >)
// PAUTH-NEXT: .. result: (dst-state<CannotEscapeUnchecked: LR W30 W30_HI , Insts: >)
// PAUTH-NEXT: DstSafetyAnalysis::ComputeNext( autia x0, x1, dst-state<CannotEscapeUnchecked: LR W30 W30_HI , Insts: >)
// PAUTH-NEXT: .. result: (dst-state<CannotEscapeUnchecked: LR W30 W30_HI , Insts: >)
// PAUTH-NEXT: After dst register safety analysis:
// PAUTH-NEXT: Binary Function "auth_oracle" {
// PAUTH-NEXT: Number : 4
// PAUTH-NEXT: State : CFG constructed
// ...
// PAUTH: BB Layout : [[BB0]]
// PAUTH-NEXT: }
// PAUTH-NEXT: [[BB0]] (2 instructions, align : 1)
// PAUTH-NEXT: Entry Point
// PAUTH-NEXT: 00000000: autia x0, x1 # DataflowDstSafetyAnalysis: dst-state<CannotEscapeUnchecked: BitVector, Insts: >
// PAUTH-NEXT: 00000004: ret # DataflowDstSafetyAnalysis: dst-state<CannotEscapeUnchecked: BitVector, Insts: >
// PAUTH-EMPTY:
// PAUTH-NEXT: DWARF CFI Instructions:
// PAUTH-NEXT: <empty>
// PAUTH-NEXT: End of Function "auth_oracle"
// PAUTH-EMPTY:
// PAUTH-NEXT: Found auth inst: 00000000: autia x0, x1 # DataflowDstSafetyAnalysis: dst-state<CannotEscapeUnchecked: BitVector, Insts: >
// PAUTH-NEXT: Authenticated reg: X0
// PAUTH-NEXT: safe output registers: LR W30 W30_HI{{[ \t]*$}}
// PAUTH-EMPTY:
// PAUTH-NEXT: Running detailed dst register safety analysis...
// PAUTH-NEXT: DstSafetyAnalysis::ComputeNext( ret x30, dst-state<CannotEscapeUnchecked: , Insts: [0]()>)
// PAUTH-NEXT: .. result: (dst-state<CannotEscapeUnchecked: LR W30 W30_HI , Insts: [0]()>)
// PAUTH-NEXT: DstSafetyAnalysis::ComputeNext( autia x0, x1, dst-state<CannotEscapeUnchecked: LR W30 W30_HI , Insts: [0]()>)
// PAUTH-NEXT: .. result: (dst-state<CannotEscapeUnchecked: LR W30 W30_HI , Insts: [0](0x{{[0-9a-f]+}} )>)
// PAUTH-NEXT: After detailed dst register safety analysis:
// PAUTH-NEXT: Binary Function "auth_oracle" {
// PAUTH-NEXT: Number : 4
// PAUTH-NEXT: State : CFG constructed
// ...
// PAUTH: BB Layout : [[BB0]]
// PAUTH-NEXT: }
// PAUTH-NEXT: [[BB0]] (2 instructions, align : 1)
// PAUTH-NEXT: Entry Point
// PAUTH-NEXT: 00000000: autia x0, x1 # DataflowDstSafetyAnalysis: dst-state<CannotEscapeUnchecked: BitVector, Insts: [0](0x{{[0-9a-f]+}} )>
// PAUTH-NEXT: 00000004: ret # DataflowDstSafetyAnalysis: dst-state<CannotEscapeUnchecked: BitVector, Insts: [0]()>
// PAUTH-EMPTY:
// PAUTH-NEXT: DWARF CFI Instructions:
// PAUTH-NEXT: <empty>
// PAUTH-NEXT: End of Function "auth_oracle"
// PAUTH-EMPTY:
// PAUTH-NEXT: Attaching leakage info to: 00000000: autia x0, x1 # DataflowDstSafetyAnalysis: dst-state<CannotEscapeUnchecked: BitVector, Insts: [0](0x{{[0-9a-f]+}} )>
// CHECK-LABEL:Analyzing function main, AllocatorId = 1
.globl main
.type main,@function