Skip to content

[BOLT] Gadget scanner: detect address materialization and arithmetic #132540

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

Merged
merged 7 commits into from
Apr 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions bolt/include/bolt/Core/MCPlusBuilder.h
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,41 @@ class MCPlusBuilder {
return getNoRegister();
}

/// Returns the register containing an address safely materialized by `Inst`
/// under the Pointer Authentication threat model.
///
/// Returns the register `Inst` writes to if:
/// 1. the register is a materialized address, and
/// 2. the register has been materialized safely, i.e. cannot be attacker-
/// controlled, under the Pointer Authentication threat model.
///
/// If the instruction does not write to any register satisfying the above
/// two conditions, NoRegister is returned.
///
/// The Pointer Authentication threat model assumes an attacker is able to
/// modify any writable memory, but not executable code (due to W^X).
virtual MCPhysReg
getMaterializedAddressRegForPtrAuth(const MCInst &Inst) const {
llvm_unreachable("not implemented");
return getNoRegister();
}

/// Analyzes if this instruction can safely perform address arithmetics
/// under Pointer Authentication threat model.
///
/// If an (OutReg, InReg) pair is returned, then after Inst is executed,
/// OutReg is as trusted as InReg is.
///
/// The arithmetic instruction is considered safe if OutReg is not attacker-
/// controlled, provided InReg and executable code are not. Please note that
/// registers other than InReg as well as the contents of memory which is
/// writable by the process should be considered attacker-controlled.
virtual std::optional<std::pair<MCPhysReg, MCPhysReg>>
analyzeAddressArithmeticsForPtrAuth(const MCInst &Inst) const {
llvm_unreachable("not implemented");
return std::make_pair(getNoRegister(), getNoRegister());
}

virtual bool isTerminator(const MCInst &Inst) const;

virtual bool isNoop(const MCInst &Inst) const {
Expand Down
87 changes: 64 additions & 23 deletions bolt/lib/Passes/PAuthGadgetScanner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,49 @@ class PacRetAnalysis
});
}

BitVector getClobberedRegs(const MCInst &Point) const {
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.
// 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))
Clobbered.set();
else
BC.MIB->getClobberedRegs(Point, Clobbered);
return Clobbered;
}

// Returns all registers that can be treated as if they are written by an
// authentication instruction.
SmallVector<MCPhysReg> getRegsMadeSafeToDeref(const MCInst &Point,
const State &Cur) const {
SmallVector<MCPhysReg> Regs;
const MCPhysReg NoReg = BC.MIB->getNoRegister();

// A signed pointer can be authenticated, or
ErrorOr<MCPhysReg> AutReg = BC.MIB->getAuthenticatedReg(Point);
if (AutReg && *AutReg != NoReg)
Regs.push_back(*AutReg);

// ... a safe address can be materialized, or
MCPhysReg NewAddrReg = BC.MIB->getMaterializedAddressRegForPtrAuth(Point);
if (NewAddrReg != NoReg)
Regs.push_back(NewAddrReg);

// ... an address can be updated in a safe manner, producing the result
// which is as trusted as the input address.
if (auto DstAndSrc = BC.MIB->analyzeAddressArithmeticsForPtrAuth(Point)) {
if (Cur.SafeToDerefRegs[DstAndSrc->second])
Regs.push_back(DstAndSrc->first);
}

return Regs;
}

State computeNext(const MCInst &Point, const State &Cur) {
PacStatePrinter P(BC);
LLVM_DEBUG({
Expand All @@ -355,37 +398,35 @@ class PacRetAnalysis
return State();
}

// First, compute various properties of the instruction, taking the state
// before its execution into account, if necessary.

BitVector Clobbered = getClobberedRegs(Point);
SmallVector<MCPhysReg> NewSafeToDerefRegs =
getRegsMadeSafeToDeref(Point, Cur);

// Then, compute the state after this instruction is executed.
State Next = Cur;
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.
// 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))
Clobbered.set();
else
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 (Clobbered[Reg])
lastWritingInsts(Next, Reg) = {&Point};

ErrorOr<MCPhysReg> AutReg = BC.MIB->getAuthenticatedReg(Point);
if (AutReg && *AutReg != BC.MIB->getNoRegister()) {
// 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();
}
// After accounting for clobbered registers in general, override the state
// according to authentication and other *special cases* of clobbering.

// The sub-registers are also safe-to-dereference now, but not their
// super-registers (as they retain untrusted register units).
BitVector NewSafeSubregs(NumRegs);
for (MCPhysReg SafeReg : NewSafeToDerefRegs)
NewSafeSubregs |= BC.MIB->getAliases(SafeReg, /*OnlySmaller=*/true);
for (MCPhysReg Reg : NewSafeSubregs.set_bits()) {
Next.SafeToDerefRegs.set(Reg);
if (RegsToTrackInstsFor.isTracked(Reg))
lastWritingInsts(Next, Reg).clear();
}

LLVM_DEBUG({
Expand Down
37 changes: 37 additions & 0 deletions bolt/lib/Target/AArch64/AArch64MCPlusBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,43 @@ class AArch64MCPlusBuilder : public MCPlusBuilder {
}
}

MCPhysReg
getMaterializedAddressRegForPtrAuth(const MCInst &Inst) const override {
switch (Inst.getOpcode()) {
case AArch64::ADR:
case AArch64::ADRP:
// These instructions produce an address value based on the information
// encoded into the instruction itself (which should reside in a read-only
// code memory) and the value of PC register (that is, the location of
// this instruction), so the produced value is not attacker-controlled.
return Inst.getOperand(0).getReg();
default:
return getNoRegister();
}
}

std::optional<std::pair<MCPhysReg, MCPhysReg>>
analyzeAddressArithmeticsForPtrAuth(const MCInst &Inst) const override {
switch (Inst.getOpcode()) {
default:
return std::nullopt;
case AArch64::ADDXri:
case AArch64::SUBXri:
// The immediate addend is encoded into the instruction itself, so it is
// not attacker-controlled under Pointer Authentication threat model.
return std::make_pair(Inst.getOperand(0).getReg(),
Inst.getOperand(1).getReg());
case AArch64::ORRXrs:
// "mov Xd, Xm" is equivalent to "orr Xd, XZR, Xm, lsl #0"
if (Inst.getOperand(1).getReg() != AArch64::XZR ||
Inst.getOperand(3).getImm() != 0)
return std::nullopt;

return std::make_pair(Inst.getOperand(0).getReg(),
Inst.getOperand(2).getReg());
}
}

bool isADRP(const MCInst &Inst) const override {
return Inst.getOpcode() == AArch64::ADRP;
}
Expand Down
15 changes: 0 additions & 15 deletions bolt/test/binary-analysis/AArch64/gs-pacret-autiasp.s
Original file line number Diff line number Diff line change
Expand Up @@ -141,24 +141,9 @@ f_nonx30_ret_ok:
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
Expand Down
Loading