[llvm] [RISC-V] Extend support for -fsanitize=shadow-call-stack (PR #230853)
Malhar Badve via llvm-commits
llvm-commits at lists.llvm.org
Sat Oct 10 13:30:16 PDT 2026
https://github.com/mips-mbadve created https://github.com/llvm/llvm-project/pull/230853
LLVM already has priliminary support for -fsanitize=shadow-call-stack in RISC-V,
which is to store a return address onto the shadow stack during the prologue
of a function and restore it during the epilogue of the function.
This commit extends that support.
1) Along with storing and restoring the return address on gp,
now the code also checks for shadow stack buffer overflow as well
as checks the integrity of the return address on shadow stack
2) If the user specifies `-Os` command line option, instead of emitting
the prologue/epilogue modifications inline, delegate them to
library calls `__shadow_stack_save` and `__shadow_stack_restore`
This functionality was tested using Qemu for functional testing.
I have opened this draft PR for opening a discussion and RFC on the methodology
for this implementation.
This PR requires a number of additional changes including doc changes,
inclusion of the `__shadow_stack_save/restore` library calls to compiler-rt
and testcases, hence opening as draft for now.
>From 22a10565c078dcbd241b39e21cc1647b4a2cf125 Mon Sep 17 00:00:00 2001
From: Malhar Badve <malharbadve2046 at gmail.com>
Date: Mon, 5 Oct 2026 17:49:38 +0530
Subject: [PATCH] RISC-V: Extend support for -fsanitize=shadow-call-stack
LLVM already has priliminary support for -fsanitize=shadow-call-stack
in RISC-V, which is to store a return address onto the shadow stack
during the prologue of a function and restore it during the epilogue
of the function.
This commit extends that support.
1) Along with storing and restoring the return address on gp,
now the code also checks for shadow stack buffer overflow as well
as checks the integrity of the return address on shadow stack
2) If the user specifies `-Os` command line option, instead of emitting
the prologue/epilogue modifications inline, delegate them to
library calls `__shadow_stack_save` and `__shadow_stack_restore`
This functionality was tested using qemu for functional testing.
---
llvm/lib/Target/RISCV/RISCVFrameLowering.cpp | 191 ++++++++++++++++++-
1 file changed, 189 insertions(+), 2 deletions(-)
diff --git a/llvm/lib/Target/RISCV/RISCVFrameLowering.cpp b/llvm/lib/Target/RISCV/RISCVFrameLowering.cpp
index 441ba01ff8ad3..4293155c40422 100644
--- a/llvm/lib/Target/RISCV/RISCVFrameLowering.cpp
+++ b/llvm/lib/Target/RISCV/RISCVFrameLowering.cpp
@@ -103,8 +103,7 @@ static bool needsDwarfCFI(const MachineFunction &MF) {
return MF.needsFrameMoves();
}
-// For now we use x3, a.k.a gp, as pointer to shadow call stack.
-// User should not use x3 in their asm.
+// Handle the shadow call stack prologue expand
static void emitSCSPrologue(MachineFunction &MF, MachineBasicBlock &MBB,
MachineBasicBlock::iterator MI,
const DebugLoc &DL) {
@@ -123,6 +122,7 @@ static void emitSCSPrologue(MachineFunction &MF, MachineBasicBlock &MBB,
CSI, [&](CalleeSavedInfo &CSR) { return CSR.getReg() == RAReg; }))
return;
+ // If we have support for hardware shadow stack (zicfiss) then use that
const RISCVInstrInfo *TII = STI.getInstrInfo();
if (SSK == RISCVMachineFunctionInfo::ShadowStackKind::Hardware) {
BuildMI(MBB, MI, DL, TII->get(RISCV::SSPUSH))
@@ -134,6 +134,9 @@ static void emitSCSPrologue(MachineFunction &MF, MachineBasicBlock &MBB,
assert(SSK == RISCVMachineFunctionInfo::ShadowStackKind::Software &&
"Unexpected Shadow Stack Kind");
+ // Otherwise defer to the software shadow call stack
+ // The top of shadow stack is stored in the gp (x3) register
+ // User should not use x3 in their asm
Register SCSPReg = RISCVABI::getSCSPReg();
bool IsRV64 = STI.is64Bit();
@@ -151,6 +154,78 @@ static void emitSCSPrologue(MachineFunction &MF, MachineBasicBlock &MBB,
.addImm(-SlotSize)
.setMIFlag(MachineInstr::FrameSetup);
+ if (/* inline = */ !MF.getFunction().hasOptSize()) {
+ // emit the shadow stack modifications inline in the prologue
+
+ // we'll be splitting the original basic block as follows
+ // [InitialMBB] (new block)
+ // ld t1, __shadow_stack_top
+ // addi gp, gp, [4|8]
+ // bgeu t1, gp, label (buffer overflow)---> [AbortMBB] (new block) call abort
+ // |
+ // | (no buffer overflow)
+ // v
+ // [MBB] (original block)
+ // s[w|d] ra, -[4|8](gp)
+
+ Register T1 = RISCV::X6; // t1
+ MachineBasicBlock *InitialMBB = MF.CreateMachineBasicBlock(MBB.getBasicBlock());
+ MachineBasicBlock *AbortMBB = MF.CreateMachineBasicBlock(MBB.getBasicBlock());
+
+ MF.insert(MBB.getIterator(), InitialMBB);
+ MF.insert(MBB.getIterator(), AbortMBB);
+
+ InitialMBB->addSuccessor(AbortMBB, BranchProbability(0, 1));
+ InitialMBB->addSuccessor(&MBB, BranchProbability(1, 1));
+
+ for (auto &Pred : MBB.predecessors())
+ Pred->replaceSuccessor(&MBB, InitialMBB);
+
+ // l[d|w] t1, __shadow_stack_top
+ BuildMI(*InitialMBB, InitialMBB->end(), DL, TII->get(RISCV::LUI), T1)
+ .addExternalSymbol("__shadow_stack_top", RISCVII::MO_HI);
+ BuildMI(*InitialMBB, InitialMBB->end(), DL, TII->get(RISCV::ADDI), T1)
+ .addReg(T1)
+ .addExternalSymbol("__shadow_stack_top", RISCVII::MO_LO);
+
+ // addi gp, gp, [4|8]
+ BuildMI(*InitialMBB, InitialMBB->end(), DL, TII->get(RISCV::ADDI))
+ .addReg(SCSPReg, RegState::Define)
+ .addReg(SCSPReg)
+ .addImm(SlotSize)
+ .setMIFlag(MachineInstr::FrameSetup);
+
+ // bgeu t1, gp, label (if gp <= __shadow_stack_top, continue, otherwise abort)
+ BuildMI(*InitialMBB, InitialMBB->end(), DL, TII->get(RISCV::BGEU))
+ .addReg(T1)
+ .addReg(SCSPReg)
+ .addMBB(&MBB);
+
+ BuildMI(*AbortMBB, AbortMBB->end(), DL, TII->get(RISCV::PseudoCALL))
+ .addExternalSymbol("abort", RISCVII::MO_CALL);
+
+ // s[w|d] ra, -[4|8](gp)
+ BuildMI(MBB, MI, DL, TII->get(IsRV64 ? RISCV::SD : RISCV::SW))
+ .addReg(RAReg)
+ .addReg(SCSPReg)
+ .addImm(-SlotSize)
+ .setMIFlag(MachineInstr::FrameSetup);
+
+ } else {
+ // call a library function __shadow_stack_save
+ // for the shadow stack modifications in the prologue
+
+ Register T0 = RISCV::X5;
+
+ // jalr t0, t1, 0
+ // mark t0, ra as used and t1 as clobbered
+ BuildMI(MBB, MI, DL, TII->get(RISCV::PseudoCALLReg), T0)
+ .addExternalSymbol("__shadow_stack_save", RISCVII::MO_CALL)
+ .setMIFlag(MachineInstr::FrameSetup);
+
+ MBB.addLiveIn(RAReg);
+ }
+
if (!needsDwarfCFI(MF))
return;
@@ -380,6 +455,118 @@ static void emitSiFiveCLICPreemptibleRestores(MachineFunction &MF,
RISCV::NoSubRegister, MachineInstr::FrameDestroy);
if (needsDwarfCFI(MF))
CFIBuilder.buildRestore(RISCV::X5);
+
+ if (!MF.getFunction().hasOptSize()) {
+ // emit the shadow stack modifications inline in the epilogue
+
+ // we'll be splitting the original basic block as follows
+ // the return from MBB is shifted to OkMBB
+ //
+ // [MBB] (original block)
+ // l[d|w] t1, -SlotSize(gp)
+ // xor t0, ra, t1
+ // beq t0, x0, [label] (integrity check fail)---> [AbortMBB] (new block) call abort
+ // |
+ // | (integrity check pass)
+ // v
+ // [OkMBB] (new block)
+ // addi gp, gp, -SlotSize
+ // mv ra, t1
+ // ret
+
+ Register T0 = RISCV::X5;
+ Register T1 = RISCV::X6;
+
+ MachineBasicBlock *OkMBB = MF.CreateMachineBasicBlock(MBB.getBasicBlock());
+ MachineBasicBlock *AbortMBB = MF.CreateMachineBasicBlock(MBB.getBasicBlock());
+
+ MachineFunction::iterator MBBIt = std::next(MBB.getIterator());
+ MF.insert(MBBIt, AbortMBB);
+ MF.insert(MBBIt, OkMBB);
+
+ MBB.addSuccessor(OkMBB, BranchProbability(1, 1));
+ MBB.addSuccessor(AbortMBB, BranchProbability(0, 1));
+
+ // l[d|w] t1, -SlotSize(gp)
+ BuildMI(MBB, MI, DL, TII->get(IsRV64 ? RISCV::LD : RISCV::LW), T1)
+ .addReg(SCSPReg)
+ .addImm(-SlotSize)
+ .setMIFlag(MachineInstr::FrameDestroy);
+
+ // xor t0, ra, t1
+ BuildMI(MBB, MI, DL, TII->get(RISCV::XOR), T0)
+ .addReg(RAReg)
+ .addReg(T1)
+ .setMIFlag(MachineInstr::FrameDestroy);
+
+ // beq t0, x0, OkMBB
+ BuildMI(MBB, MI, DL, TII->get(RISCV::BEQ))
+ .addReg(T0)
+ .addReg(RISCV::X0)
+ .addMBB(OkMBB);
+
+ OkMBB->splice(OkMBB->end(), &MBB, MI, MBB.end());
+ OkMBB->transferSuccessorsAndUpdatePHIs(&MBB);
+
+ BuildMI(*AbortMBB, AbortMBB->end(), DL, TII->get(RISCV::PseudoCALL))
+ .addExternalSymbol("abort", RISCVII::MO_CALL);
+
+ // Now emit the remaining instructions at the start of OkMBB
+ MachineBasicBlock::iterator OkMI = OkMBB->begin();
+
+ // addi gp, gp, -SlotSize
+ BuildMI(*OkMBB, OkMI, DL, TII->get(RISCV::ADDI))
+ .addReg(SCSPReg, RegState::Define)
+ .addReg(SCSPReg)
+ .addImm(-SlotSize)
+ .setMIFlag(MachineInstr::FrameDestroy);
+
+ // mv ra, t1
+ BuildMI(*OkMBB, OkMI, DL, TII->get(RISCV::ADDI), RAReg)
+ .addReg(T1)
+ .addImm(0)
+ .setMIFlag(MachineInstr::FrameDestroy);
+
+ // Restore the SCS pointer
+ unsigned CFIIndex = MF.addFrameInst(MCCFIInstruction::createRestore(
+ nullptr,
+ STI.getRegisterInfo()->getDwarfRegNum(SCSPReg, /*IsEH*/ true)));
+
+ BuildMI(*OkMBB, OkMI, DL, TII->get(TargetOpcode::CFI_INSTRUCTION))
+ .addCFIIndex(CFIIndex)
+ .setMIFlags(MachineInstr::FrameDestroy);
+
+ } else {
+ // tail call a library function __shadow_stack_restore
+ // for the shadow stack modifications in the epilogue
+
+ Register T0 = RISCV::X5;
+
+ // ra already holds the regular-stack return address
+ // __shadow_stack_return expects the return address to be in t0
+ // mv t0, ra
+ BuildMI(MBB, MI, DL, TII->get(RISCV::ADDI), T0)
+ .addReg(RAReg)
+ .addImm(0)
+ .setMIFlag(MachineInstr::FrameDestroy);
+
+ BuildMI(MBB, MI, DL, TII->get(RISCV::PseudoTAIL))
+ .addExternalSymbol("__shadow_stack_restore", RISCVII::MO_CALL)
+ .addReg(T0, RegState::Implicit)
+ .setMIFlag(MachineInstr::FrameDestroy);
+
+ // Restore the SCS pointer
+ unsigned CFIIndex = MF.addFrameInst(MCCFIInstruction::createRestore(
+ nullptr,
+ STI.getRegisterInfo()->getDwarfRegNum(SCSPReg, /*IsEH*/ true)));
+
+ BuildMI(MBB, MI, DL, TII->get(TargetOpcode::CFI_INSTRUCTION))
+ .addCFIIndex(CFIIndex)
+ .setMIFlags(MachineInstr::FrameDestroy);
+
+ // Remove the original PseudoRET since we're tail-calling instead
+ MI->eraseFromParent();
+ }
}
// Get the ID of the libcall used for spilling and restoring callee saved
More information about the llvm-commits
mailing list