-
Notifications
You must be signed in to change notification settings - Fork 13.6k
[BOLT] Gadget scanner: reformulate the state for data-flow analysis #131898
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -126,18 +126,16 @@ class TrackedRegisters { | |
|
||
// 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 | ||
// that register must be safe-to-dereference. It must either | ||
// (a) be safe-to-dereference at function entry and 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. | ||
// registers have been written (def-ed), since last authenticated. For pac-ret, | ||
// any return instruction using a register which is not safe-to-dereference 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 | ||
|
@@ -156,29 +154,62 @@ class TrackedRegisters { | |
// in the gadgets to be reported. This information is used in the second run | ||
// to also track which instructions last wrote to those registers. | ||
|
||
/// A state representing which registers are safe to use by an instruction | ||
/// at a given program point. | ||
/// | ||
/// To simplify reasoning, let's stick with the following approach: | ||
/// * when state is updated by the data-flow analysis, the sub-, super- and | ||
/// overlapping registers are marked as needed | ||
/// * when the particular instruction is checked if it represents a gadget, | ||
/// the specific bit of BitVector should be usable to answer this. | ||
/// | ||
/// For example, on AArch64: | ||
/// * An AUTIZA X0 instruction marks both X0 and W0 (as well as W0_HI) as | ||
/// safe-to-dereference. It does not change the state of X0_X1, for example, | ||
/// as super-registers partially retain their old, unsafe values. | ||
/// * LDR X1, [X0] marks as unsafe both X1 itself and anything it overlaps | ||
/// with: W1, W1_HI, X0_X1 and so on. | ||
/// * RET (which is implicitly RET X30) is a protected return if and only if | ||
/// X30 is safe-to-dereference - the state computed for sub- and | ||
/// super-registers is not inspected. | ||
struct State { | ||
/// A BitVector containing the registers that have been clobbered, and | ||
/// not authenticated. | ||
BitVector NonAutClobRegs; | ||
/// A BitVector containing the registers that are either safe at function | ||
/// entry and were not clobbered yet, or those not clobbered since being | ||
/// authenticated. | ||
BitVector SafeToDerefRegs; | ||
/// 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; | ||
|
||
/// Construct an empty state. | ||
State() {} | ||
|
||
State(unsigned NumRegs, unsigned NumRegsToTrack) | ||
: NonAutClobRegs(NumRegs), LastInstWritingReg(NumRegsToTrack) {} | ||
State &operator|=(const State &StateIn) { | ||
NonAutClobRegs |= StateIn.NonAutClobRegs; | ||
: SafeToDerefRegs(NumRegs), LastInstWritingReg(NumRegsToTrack) {} | ||
|
||
State &merge(const State &StateIn) { | ||
if (StateIn.empty()) | ||
return *this; | ||
if (empty()) | ||
return (*this = StateIn); | ||
|
||
SafeToDerefRegs &= StateIn.SafeToDerefRegs; | ||
for (unsigned I = 0; I < LastInstWritingReg.size(); ++I) | ||
for (const MCInst *J : StateIn.LastInstWritingReg[I]) | ||
LastInstWritingReg[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 SafeToDerefRegs.empty(); } | ||
|
||
bool operator==(const State &RHS) const { | ||
return NonAutClobRegs == RHS.NonAutClobRegs && | ||
return SafeToDerefRegs == RHS.SafeToDerefRegs && | ||
LastInstWritingReg == RHS.LastInstWritingReg; | ||
} | ||
bool operator!=(const State &RHS) const { return !((*this) == RHS); } | ||
|
@@ -199,8 +230,12 @@ static void printLastInsts( | |
|
||
raw_ostream &operator<<(raw_ostream &OS, const State &S) { | ||
OS << "pacret-state<"; | ||
OS << "NonAutClobRegs: " << S.NonAutClobRegs << ", "; | ||
printLastInsts(OS, S.LastInstWritingReg); | ||
if (S.empty()) { | ||
OS << "empty"; | ||
} else { | ||
OS << "SafeToDerefRegs: " << S.SafeToDerefRegs << ", "; | ||
printLastInsts(OS, S.LastInstWritingReg); | ||
} | ||
OS << ">"; | ||
return OS; | ||
} | ||
|
@@ -217,10 +252,16 @@ class PacStatePrinter { | |
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); | ||
if (S.empty()) { | ||
assert(S.SafeToDerefRegs.empty()); | ||
assert(S.LastInstWritingReg.empty()); | ||
OS << "empty"; | ||
} else { | ||
OS << "SafeToDerefRegs: "; | ||
RegStatePrinter.print(OS, S.SafeToDerefRegs); | ||
OS << ", "; | ||
printLastInsts(OS, S.LastInstWritingReg); | ||
} | ||
OS << ">"; | ||
} | ||
|
||
|
@@ -257,14 +298,22 @@ class PacRetAnalysis | |
|
||
void preflight() {} | ||
|
||
State getStartingStateAtBB(const BinaryBasicBlock &BB) { | ||
return State(NumRegs, RegsToTrackInstsFor.getNumTrackedRegisters()); | ||
State createEntryState() { | ||
State S(NumRegs, RegsToTrackInstsFor.getNumTrackedRegisters()); | ||
for (MCPhysReg Reg : BC.MIB->getTrustedLiveInRegs()) | ||
S.SafeToDerefRegs |= BC.MIB->getAliases(Reg, /*OnlySmaller=*/true); | ||
return S; | ||
} | ||
|
||
State getStartingStateAtPoint(const MCInst &Point) { | ||
return State(NumRegs, RegsToTrackInstsFor.getNumTrackedRegisters()); | ||
State getStartingStateAtBB(const BinaryBasicBlock &BB) { | ||
if (BB.isEntryPoint()) | ||
return createEntryState(); | ||
|
||
return State(); | ||
} | ||
|
||
State getStartingStateAtPoint(const MCInst &Point) { return State(); } | ||
|
||
void doConfluence(State &StateOut, const State &StateIn) { | ||
PacStatePrinter P(BC); | ||
LLVM_DEBUG({ | ||
|
@@ -277,7 +326,7 @@ class PacRetAnalysis | |
dbgs() << ")\n"; | ||
}); | ||
|
||
StateOut |= StateIn; | ||
StateOut.merge(StateIn); | ||
|
||
LLVM_DEBUG({ | ||
dbgs() << " merged state: "; | ||
|
@@ -297,8 +346,17 @@ class PacRetAnalysis | |
dbgs() << ")\n"; | ||
}); | ||
|
||
// If this instruction is reachable, a non-empty state will be propagated | ||
// to it from the entry basic block sooner or later. Until then, it is both | ||
// more efficient and easier to reason about to skip computeNext(). | ||
if (Cur.empty()) { | ||
LLVM_DEBUG( | ||
{ dbgs() << "Skipping computeNext(Point, Cur) as Cur is empty.\n"; }); | ||
return State(); | ||
} | ||
|
||
State Next = Cur; | ||
BitVector Written = BitVector(NumRegs, false); | ||
BitVector Clobbered(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. | ||
|
@@ -307,36 +365,27 @@ class PacRetAnalysis | |
// Also, not all functions may respect the AAPCS ABI rules about | ||
// caller/callee-saved registers. | ||
if (BC.MIB->isCall(Point)) | ||
Written.set(); | ||
Clobbered.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; | ||
BC.MIB->getClobberedRegs(Point, Clobbered); | ||
Next.SafeToDerefRegs.reset(Clobbered); | ||
// Keep track of this instruction if it writes to any of the registers we | ||
// need to track that for: | ||
for (MCPhysReg Reg : RegsToTrackInstsFor.getRegisters()) | ||
if (Written[Reg]) | ||
if (Clobbered[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 (RegsToTrackInstsFor.isTracked(*AutReg)) | ||
lastWritingInsts(Next, *AutReg).clear(); | ||
// The sub-registers of *AutReg are also trusted now, but not its | ||
// super-registers (as they retain untrusted register units). | ||
BitVector AuthenticatedSubregs = | ||
BC.MIB->getAliases(*AutReg, /*OnlySmaller=*/true); | ||
for (MCPhysReg Reg : AuthenticatedSubregs.set_bits()) { | ||
Next.SafeToDerefRegs.set(Reg); | ||
if (RegsToTrackInstsFor.isTracked(Reg)) | ||
lastWritingInsts(Next, Reg).clear(); | ||
} | ||
} | ||
|
||
LLVM_DEBUG({ | ||
|
@@ -397,14 +446,11 @@ shouldReportReturnGadget(const BinaryContext &BC, const MCInstReference &Inst, | |
}); | ||
if (BC.MIB->isAuthenticationOfReg(Inst, RetReg)) | ||
return nullptr; | ||
BitVector UsedDirtyRegs = S.NonAutClobRegs; | ||
LLVM_DEBUG({ traceRegMask(BC, "NonAutClobRegs at Ret", UsedDirtyRegs); }); | ||
UsedDirtyRegs &= BC.MIB->getAliases(RetReg, /*OnlySmaller=*/true); | ||
LLVM_DEBUG({ traceRegMask(BC, "Intersection with RetReg", UsedDirtyRegs); }); | ||
if (!UsedDirtyRegs.any()) | ||
LLVM_DEBUG({ traceRegMask(BC, "SafeToDerefRegs", S.SafeToDerefRegs); }); | ||
if (S.SafeToDerefRegs[RetReg]) | ||
return nullptr; | ||
|
||
return std::make_shared<GadgetReport>(RetKind, Inst, UsedDirtyRegs); | ||
return std::make_shared<GadgetReport>(RetKind, Inst, RetReg); | ||
} | ||
|
||
FunctionAnalysisResult | ||
|
@@ -425,6 +471,14 @@ Analysis::findGadgets(BinaryFunction &BF, | |
MCInstReference Inst(&BB, I); | ||
const State &S = *PRA.getStateAt(Inst); | ||
|
||
// If non-empty state was never propagated from the entry basic block | ||
// to Inst, assume it to be unreachable and report a warning. | ||
if (S.empty()) { | ||
Result.Diagnostics.push_back(std::make_shared<GenericReport>( | ||
Inst, "Warning: unreachable instruction found")); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I seem to remember that if you do this analysis on a large amount of code (for example all libraries in a linux distro), it will find unreachable code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My initial intention was not to skip instructions silently (in case it is skipped due to some error in PacRetAnalysis). On one hand, this should be caught by the tests, on the other hand, this warning helps implementing the tests. Anyway, this should be trivial to remove this warning later. |
||
continue; | ||
} | ||
|
||
if (auto Report = shouldReportReturnGadget(BC, Inst, S)) | ||
Result.Diagnostics.push_back(Report); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if
MCPlusBuilder
is the right place for this to live...The reason why I'm not sure is that to me it seems that
MCPlusBuilder
is mostly about querying the property of instructions, maybe at most relative to an assumed ABI.It seems to me that
getTrustedLiveInRegs
might be encoding an assumed, implicit, threat model too?Apologies for not explaining this very well. I'm just trying to make sure this function goes into the most appropriate place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like the set of registers returned by
getTrustedLiveInRegs
on AArch64 can be derived from the fact that LR is set by branch-with-link instructions. To some extent, this does look more like a property of the ABI, but as far as I can see target-specific hooks are placed either tolib/Target/XYZ/XYZMCPlusBuilder.cpp
or tolib/Target/XYZ/XYZMCSymbolizer.(h|cpp)
, so there doesn't seem to be many places where such target-specific hook can be defined.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes, you're right, there aren't many places in Bolt that look target-specific... OK, let's just keep it in MCPlusBuilder.