[clang] [compiler-rt] [llvm] [Hexagon] Add ShadowCallStack support (PR #200508)
Brian Cain via llvm-commits
llvm-commits at lists.llvm.org
Thu Jun 25 18:47:31 PDT 2026
https://github.com/androm3da updated https://github.com/llvm/llvm-project/pull/200508
>From 65c9001cb6beebe8385d056dc356f2c0756fdf46 Mon Sep 17 00:00:00 2001
From: Brian Cain <brian.cain at oss.qualcomm.com>
Date: Thu, 28 May 2026 20:16:39 -0700
Subject: [PATCH 1/2] [Hexagon] Add ShadowCallStack support
Implement the software ShadowCallStack for Hexagon.
On Hexagon, r19 is used as the shadow stack pointer (reserved via
-ffixed-r19). On function entry the LR (r31) is saved to the shadow
stack and the pointer is advanced; on exit the LR is restored from the
shadow stack before returning.
Prologue sequence:
r19 = add(r19, #4)
memw(r19+#-4) = r31
Epilogue sequence (between deallocframe/jumpr r31):
r31 = memw(r19+#-4)
r19 = add(r19, #-4)
SCS is only emitted when hasFP() is true: the function uses allocframe.
Leaf functions that never touch the stack do not need protection.
When SCS is active the combined L4_return (deallocframe+jump) is not
used; instead L2_deallocframe and PS_jmpret are kept separate so the
shadow-stack restore can be inserted between them.
---
clang/lib/Driver/SanitizerArgs.cpp | 8 +
clang/lib/Driver/ToolChains/Linux.cpp | 2 +
.../fsanitize-shadow-call-stack-hexagon.c | 12 ++
.../cmake/Modules/AllSupportedArchDefs.cmake | 2 +-
.../test/shadowcallstack/libc_support.h | 26 +++
compiler-rt/test/shadowcallstack/lit.cfg.py | 8 +-
.../test/shadowcallstack/minimal_runtime.h | 2 +
.../Target/Hexagon/HexagonFrameLowering.cpp | 154 ++++++++++++++++--
.../test/CodeGen/Hexagon/shadow-call-stack.ll | 133 +++++++++++++++
9 files changed, 332 insertions(+), 15 deletions(-)
create mode 100644 clang/test/Driver/fsanitize-shadow-call-stack-hexagon.c
create mode 100644 llvm/test/CodeGen/Hexagon/shadow-call-stack.ll
diff --git a/clang/lib/Driver/SanitizerArgs.cpp b/clang/lib/Driver/SanitizerArgs.cpp
index 74ebd0bf375d3..77ad06cfcbd0d 100644
--- a/clang/lib/Driver/SanitizerArgs.cpp
+++ b/clang/lib/Driver/SanitizerArgs.cpp
@@ -784,6 +784,14 @@ SanitizerArgs::SanitizerArgs(const ToolChain &TC,
<< "-ffixed-x18";
}
+ if ((Kinds & SanitizerKind::ShadowCallStack) &&
+ TC.getTriple().getArch() == llvm::Triple::hexagon &&
+ !Args.hasArg(options::OPT_ffixed_r19) && DiagnoseErrors) {
+ D.Diag(diag::err_drv_argument_only_allowed_with)
+ << lastArgumentForMask(D, Args, Kinds & SanitizerKind::ShadowCallStack)
+ << "-ffixed-r19";
+ }
+
// Report error if there are non-trapping sanitizers that require
// c++abi-specific parts of UBSan runtime, and they are not provided by the
// toolchain. We don't have a good way to check the latter, so we just
diff --git a/clang/lib/Driver/ToolChains/Linux.cpp b/clang/lib/Driver/ToolChains/Linux.cpp
index 512788d235fec..1111d547fbe15 100644
--- a/clang/lib/Driver/ToolChains/Linux.cpp
+++ b/clang/lib/Driver/ToolChains/Linux.cpp
@@ -1006,6 +1006,8 @@ Linux::getSupportedSanitizers(StringRef BoundArch,
if (IsX86_64 || IsAArch64 || IsRISCV64) {
Res |= SanitizerKind::HWAddress;
}
+ if (IsHexagon)
+ Res |= SanitizerKind::ShadowCallStack;
if (IsX86_64 || IsAArch64) {
Res |= SanitizerKind::KernelHWAddress;
}
diff --git a/clang/test/Driver/fsanitize-shadow-call-stack-hexagon.c b/clang/test/Driver/fsanitize-shadow-call-stack-hexagon.c
new file mode 100644
index 0000000000000..89d186315d9c1
--- /dev/null
+++ b/clang/test/Driver/fsanitize-shadow-call-stack-hexagon.c
@@ -0,0 +1,12 @@
+// Test that -fsanitize=shadow-call-stack on Hexagon requires -ffixed-r19.
+
+// RUN: not %clang --target=hexagon-unknown-linux-musl \
+// RUN: -fsanitize=shadow-call-stack %s -### 2>&1 \
+// RUN: | FileCheck %s --check-prefix=HEXAGON-SCS-NO-R19
+
+// RUN: %clang --target=hexagon-unknown-linux-musl \
+// RUN: -fsanitize=shadow-call-stack -ffixed-r19 %s -### 2>&1 \
+// RUN: | FileCheck %s --check-prefix=HEXAGON-SCS-WITH-R19
+
+// HEXAGON-SCS-NO-R19: error: invalid argument '-fsanitize=shadow-call-stack' only allowed with '-ffixed-r19'
+// HEXAGON-SCS-WITH-R19: "-fsanitize=shadow-call-stack"
diff --git a/compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake b/compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake
index a535cf9e3a8da..9c03892dffe5a 100644
--- a/compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake
+++ b/compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake
@@ -125,7 +125,7 @@ set(ALL_XRAY_SUPPORTED_ARCH ${X86_64} ${ARM32} ${ARM64} ${MIPS32} ${MIPS64}
powerpc64le ${HEXAGON} ${LOONGARCH64} ${RISCV32} ${RISCV64} ${S390X})
endif()
set(ALL_XRAY_DSO_SUPPORTED_ARCH ${X86_64} ${ARM64})
-set(ALL_SHADOWCALLSTACK_SUPPORTED_ARCH ${ARM64})
+set(ALL_SHADOWCALLSTACK_SUPPORTED_ARCH ${ARM64} ${HEXAGON})
if (UNIX)
if (OS_NAME MATCHES "Linux")
diff --git a/compiler-rt/test/shadowcallstack/libc_support.h b/compiler-rt/test/shadowcallstack/libc_support.h
index 9c8e0566380bc..00a3a38a8780b 100644
--- a/compiler-rt/test/shadowcallstack/libc_support.h
+++ b/compiler-rt/test/shadowcallstack/libc_support.h
@@ -32,6 +32,32 @@ __attribute__((noinline)) void scs_fputs_stdout(const char *p) {
: "x0", "x1", "x2", "x8");
}
+#elif defined(__hexagon__)
+# ifndef __linux__
+# error Unsupported platform
+# endif
+
+size_t scs_strlen(const char *p) {
+ size_t retval = 0;
+ while (*p++)
+ retval++;
+ return retval;
+}
+
+__attribute__((noinline)) void scs_fputs_stdout(const char *p) {
+ size_t len = scs_strlen(p);
+ __asm__ __volatile__("{\n\t"
+ " r0 = #1\n\t" // fd = stdout
+ " r1 = %[buf]\n\t"
+ " r2 = %[count]\n\t"
+ " r6 = #64\n\t" // __NR_write
+ "}\n\t"
+ "trap0(#1)\n\t"
+ :
+ : [buf] "r"(p), [count] "r"(len)
+ : "r0", "r1", "r2", "r6", "memory");
+}
+
#else
#error Unsupported platform
#endif
diff --git a/compiler-rt/test/shadowcallstack/lit.cfg.py b/compiler-rt/test/shadowcallstack/lit.cfg.py
index 5b95deb1b0986..81151b0b7cbbc 100644
--- a/compiler-rt/test/shadowcallstack/lit.cfg.py
+++ b/compiler-rt/test/shadowcallstack/lit.cfg.py
@@ -25,6 +25,8 @@
scs_arch_cflags = config.target_cflags
if config.target_arch == "aarch64":
scs_arch_cflags += " -ffixed-x18 "
+elif config.target_arch == "hexagon":
+ scs_arch_cflags += " -ffixed-r19 "
config.substitutions.append(
(
"%clang_scs ",
@@ -32,5 +34,9 @@
)
)
-if config.target_os not in ["Linux"] or config.target_arch not in ["aarch64", "riscv64"]:
+if config.target_os not in ["Linux"] or config.target_arch not in [
+ "aarch64",
+ "hexagon",
+ "riscv64",
+]:
config.unsupported = True
diff --git a/compiler-rt/test/shadowcallstack/minimal_runtime.h b/compiler-rt/test/shadowcallstack/minimal_runtime.h
index fab4fdf8006f7..09a6bcf1efa57 100644
--- a/compiler-rt/test/shadowcallstack/minimal_runtime.h
+++ b/compiler-rt/test/shadowcallstack/minimal_runtime.h
@@ -19,6 +19,8 @@ static void __shadowcallstack_init() {
#if defined(__aarch64__)
__asm__ __volatile__("mov x18, %0" ::"r"(stack));
+#elif defined(__hexagon__)
+ __asm__ __volatile__("r19 = %0" ::"r"(stack));
#else
#error Unsupported platform
#endif
diff --git a/llvm/lib/Target/Hexagon/HexagonFrameLowering.cpp b/llvm/lib/Target/Hexagon/HexagonFrameLowering.cpp
index 0f73efc243986..efa4a5f726f00 100644
--- a/llvm/lib/Target/Hexagon/HexagonFrameLowering.cpp
+++ b/llvm/lib/Target/Hexagon/HexagonFrameLowering.cpp
@@ -21,6 +21,8 @@
#include "llvm/ADT/SetVector.h"
#include "llvm/ADT/SmallSet.h"
#include "llvm/ADT/SmallVector.h"
+#include "llvm/BinaryFormat/Dwarf.h"
+#include "llvm/CodeGen/CFIInstBuilder.h"
#include "llvm/CodeGen/LivePhysRegs.h"
#include "llvm/CodeGen/MachineBasicBlock.h"
#include "llvm/CodeGen/MachineDominators.h"
@@ -148,6 +150,89 @@
using namespace llvm;
+static void emitSCSPrologue(MachineFunction &MF, MachineBasicBlock &MBB,
+ MachineBasicBlock::iterator MI,
+ const DebugLoc &DL) {
+ if (!MF.getFunction().hasFnAttribute(Attribute::ShadowCallStack))
+ return;
+
+ const auto &HST = MF.getSubtarget<HexagonSubtarget>();
+ // Hexagon saves LR (R31) via allocframe. If there is no frame, LR is
+ // not on the regular stack and does not need shadow-stack protection.
+ if (!HST.getFrameLowering()->hasFP(MF))
+ return;
+
+ Register SCSPReg = Hexagon::R19;
+ if (!MF.getSubtarget().isRegisterReservedByUser(SCSPReg))
+ report_fatal_error("Must reserve r19 to use shadow call stack on Hexagon");
+
+ const auto &HII = *HST.getInstrInfo();
+
+ // r19 = add(r19, #4)
+ BuildMI(MBB, MI, DL, HII.get(Hexagon::A2_addi), SCSPReg)
+ .addReg(SCSPReg)
+ .addImm(4)
+ .setMIFlag(MachineInstr::FrameSetup);
+ // memw(r19 + #-4) = r31
+ BuildMI(MBB, MI, DL, HII.get(Hexagon::S2_storeri_io))
+ .addReg(SCSPReg)
+ .addImm(-4)
+ .addReg(Hexagon::R31)
+ .setMIFlag(MachineInstr::FrameSetup);
+
+ MBB.addLiveIn(SCSPReg);
+
+ if (!MF.needsFrameMoves())
+ return;
+
+ // CFI: DW_CFA_val_expression for the SCS register, DW_OP_bregN -4
+ // Tells the unwinder that the SCS register at entry = current value - 4.
+ const auto &TRI = *MF.getSubtarget().getRegisterInfo();
+ unsigned DwarfSCSReg = TRI.getDwarfRegNum(SCSPReg, /*IsEH=*/true);
+ // DW_OP_breg0..DW_OP_breg31 (0x70..0x8f) are 32 opcodes indexed by
+ // register number, so the register number must fit in [0, 31].
+ assert(DwarfSCSReg < 32 && "SCS register should be < 32");
+ const char CFIInst[] = {
+ (char)dwarf::DW_CFA_val_expression,
+ (char)DwarfSCSReg,
+ 2, // expression length
+ (char)(unsigned)(dwarf::DW_OP_breg0 + DwarfSCSReg),
+ (char)(-4 & 0x7f), // SLEB128 -4
+ };
+ CFIInstBuilder(MBB, MI, MachineInstr::FrameSetup)
+ .buildEscape(StringRef(CFIInst, sizeof(CFIInst)));
+}
+
+static void emitSCSEpilogue(MachineFunction &MF, MachineBasicBlock &MBB,
+ MachineBasicBlock::iterator MI,
+ const DebugLoc &DL) {
+ if (!MF.getFunction().hasFnAttribute(Attribute::ShadowCallStack))
+ return;
+
+ // hasFP() is true at both call sites: the non-vararg path in
+ // insertEpilogueInBlock returns early when !hasFP(), and the vararg+musl
+ // path is inside the hasFP() branch. Check defensively.
+ if (!MF.getSubtarget<HexagonSubtarget>().getFrameLowering()->hasFP(MF))
+ report_fatal_error("SCS epilogue requires a frame");
+
+ Register SCSPReg = Hexagon::R19;
+ const auto &HII = *MF.getSubtarget<HexagonSubtarget>().getInstrInfo();
+
+ // r31 = memw(r19 + #-4)
+ BuildMI(MBB, MI, DL, HII.get(Hexagon::L2_loadri_io), Hexagon::R31)
+ .addReg(SCSPReg)
+ .addImm(-4)
+ .setMIFlag(MachineInstr::FrameDestroy);
+ // r19 = add(r19, #-4)
+ BuildMI(MBB, MI, DL, HII.get(Hexagon::A2_addi), SCSPReg)
+ .addReg(SCSPReg)
+ .addImm(-4)
+ .setMIFlag(MachineInstr::FrameDestroy);
+
+ if (MF.needsFrameMoves())
+ CFIInstBuilder(MBB, MI, MachineInstr::FrameDestroy).buildRestore(SCSPReg);
+}
+
static cl::opt<bool> DisableDeallocRet("disable-hexagon-dealloc-ret",
cl::Hidden, cl::desc("Disable Dealloc Return for Hexagon target"));
@@ -511,6 +596,19 @@ void HexagonFrameLowering::emitPrologue(MachineFunction &MF,
bool PrologueStubs = false;
insertCSRSpillsInBlock(*PrologB, CSI, HRI, PrologueStubs);
insertPrologueInBlock(*PrologB, PrologueStubs);
+ // Insert the SCS prologue after all FrameSetup instructions so that it
+ // follows allocframe and any CSR spills in the instruction stream. The
+ // packetizer may still fuse the SCS store with the first call in the
+ // function, but because Hexagon packets use old-value reads the original
+ // R31 is always what is stored.
+ {
+ MachineBasicBlock::iterator AfterProlog = PrologB->begin();
+ while (AfterProlog != PrologB->end() &&
+ AfterProlog->getFlag(MachineInstr::FrameSetup))
+ ++AfterProlog;
+ DebugLoc PrologDL = PrologB->findDebugLoc(AfterProlog);
+ emitSCSPrologue(MF, *PrologB, AfterProlog, PrologDL);
+ }
updateEntryPaths(MF, *PrologB);
if (EpilogB) {
@@ -786,6 +884,8 @@ void HexagonFrameLowering::insertEpilogueInBlock(MachineBasicBlock &MBB) const {
// Handle EH_RETURN.
if (RetOpc == Hexagon::EH_RETURN_JMPR) {
+ // EH paths overwrite R31 with a handler address; the shadow stack is
+ // not read on this path, so no SCS epilogue is needed.
BuildMI(MBB, InsertPt, dl, HII.get(Hexagon::L2_deallocframe))
.addDef(Hexagon::D15)
.addReg(Hexagon::R30);
@@ -797,6 +897,10 @@ void HexagonFrameLowering::insertEpilogueInBlock(MachineBasicBlock &MBB) const {
// Check for RESTORE_DEALLOC_RET* tail call. Don't emit an extra dealloc-
// frame instruction if we encounter it.
+ // These spill-stub tail calls include r19 in their save range, but SCS
+ // requires -ffixed-r19, which prevents the allocator from selecting stubs
+ // that cover r19. The two features are therefore mutually exclusive and no
+ // SCS epilogue is needed here.
if (RetOpc == Hexagon::RESTORE_DEALLOC_RET_JMP_V4 ||
RetOpc == Hexagon::RESTORE_DEALLOC_RET_JMP_V4_PIC ||
RetOpc == Hexagon::RESTORE_DEALLOC_RET_JMP_V4_EXT ||
@@ -816,29 +920,45 @@ void HexagonFrameLowering::insertEpilogueInBlock(MachineBasicBlock &MBB) const {
// It is possible that the restoring code is a call to a library function.
// All of the restore* functions include "deallocframe", so we need to make
// sure that we don't add an extra one.
+ bool NeedsSCS = MF.getFunction().hasFnAttribute(Attribute::ShadowCallStack);
bool NeedsDeallocframe = true;
+ unsigned PrevOpc = 0;
if (!MBB.empty() && InsertPt != MBB.begin()) {
MachineBasicBlock::iterator PrevIt = std::prev(InsertPt);
- unsigned COpc = PrevIt->getOpcode();
- if (COpc == Hexagon::RESTORE_DEALLOC_BEFORE_TAILCALL_V4 ||
- COpc == Hexagon::RESTORE_DEALLOC_BEFORE_TAILCALL_V4_PIC ||
- COpc == Hexagon::RESTORE_DEALLOC_BEFORE_TAILCALL_V4_EXT ||
- COpc == Hexagon::RESTORE_DEALLOC_BEFORE_TAILCALL_V4_EXT_PIC ||
- COpc == Hexagon::PS_call_nr || COpc == Hexagon::PS_callr_nr)
+ PrevOpc = PrevIt->getOpcode();
+ if (PrevOpc == Hexagon::RESTORE_DEALLOC_BEFORE_TAILCALL_V4 ||
+ PrevOpc == Hexagon::RESTORE_DEALLOC_BEFORE_TAILCALL_V4_PIC ||
+ PrevOpc == Hexagon::RESTORE_DEALLOC_BEFORE_TAILCALL_V4_EXT ||
+ PrevOpc == Hexagon::RESTORE_DEALLOC_BEFORE_TAILCALL_V4_EXT_PIC ||
+ PrevOpc == Hexagon::PS_call_nr || PrevOpc == Hexagon::PS_callr_nr)
NeedsDeallocframe = false;
}
if (!MF.getSubtarget<HexagonSubtarget>().isEnvironmentMusl() ||
!MF.getFunction().isVarArg()) {
- if (!NeedsDeallocframe)
+ if (!NeedsDeallocframe) {
+ // RESTORE_DEALLOC_BEFORE_TAILCALL stubs include r19 in their save range,
+ // but SCS requires -ffixed-r19 which prevents the allocator from
+ // selecting stubs that cover r19, so SCS and stubs are mutually
+ // exclusive. PS_call_nr/PS_callr_nr are noreturn calls so the shadow
+ // stack entry is never read - no SCS epilogue is needed on either path.
+ if (NeedsSCS && PrevOpc != Hexagon::PS_call_nr &&
+ PrevOpc != Hexagon::PS_callr_nr)
+ report_fatal_error("SCS with RESTORE_DEALLOC stub: "
+ "-ffixed-r19 should have prevented this");
return;
+ }
// If the returning instruction is PS_jmpret, replace it with
// dealloc_return, otherwise just add deallocframe. The function
// could be returning via a tail call.
- if (RetOpc != Hexagon::PS_jmpret || DisableDeallocRet) {
+ if (RetOpc != Hexagon::PS_jmpret || DisableDeallocRet || NeedsSCS) {
BuildMI(MBB, InsertPt, dl, HII.get(Hexagon::L2_deallocframe))
.addDef(Hexagon::D15)
.addReg(Hexagon::R30);
+ // When shadow call stack is active, overwrite R31 restored by
+ // deallocframe with the shadow-stack copy, then retract the pointer.
+ if (NeedsSCS)
+ emitSCSEpilogue(MF, MBB, InsertPt, dl);
return;
}
unsigned NewOpc = Hexagon::L4_return;
@@ -858,11 +978,14 @@ void HexagonFrameLowering::insertEpilogueInBlock(MachineBasicBlock &MBB) const {
MachineBasicBlock::iterator Term = MBB.getFirstTerminator();
MachineBasicBlock::iterator I = (Term == MBB.begin()) ? MBB.end()
: std::prev(Term);
- if (I == MBB.end() ||
- (I->getOpcode() != Hexagon::RESTORE_DEALLOC_BEFORE_TAILCALL_V4_EXT &&
- I->getOpcode() != Hexagon::RESTORE_DEALLOC_BEFORE_TAILCALL_V4_EXT_PIC &&
- I->getOpcode() != Hexagon::RESTORE_DEALLOC_BEFORE_TAILCALL_V4 &&
- I->getOpcode() != Hexagon::RESTORE_DEALLOC_BEFORE_TAILCALL_V4_PIC))
+ bool HasRestoreStub =
+ I != MBB.end() &&
+ (I->getOpcode() == Hexagon::RESTORE_DEALLOC_BEFORE_TAILCALL_V4_EXT ||
+ I->getOpcode() ==
+ Hexagon::RESTORE_DEALLOC_BEFORE_TAILCALL_V4_EXT_PIC ||
+ I->getOpcode() == Hexagon::RESTORE_DEALLOC_BEFORE_TAILCALL_V4 ||
+ I->getOpcode() == Hexagon::RESTORE_DEALLOC_BEFORE_TAILCALL_V4_PIC);
+ if (!HasRestoreStub)
BuildMI(MBB, InsertPt, dl, HII.get(Hexagon::L2_deallocframe))
.addDef(Hexagon::D15)
.addReg(Hexagon::R30);
@@ -870,6 +993,11 @@ void HexagonFrameLowering::insertEpilogueInBlock(MachineBasicBlock &MBB) const {
BuildMI(MBB, InsertPt, dl, HII.get(Hexagon::A2_addi), SP)
.addReg(SP)
.addImm(RegisterSavedAreaSizePlusPadding);
+ // RESTORE_DEALLOC stubs are mutually exclusive with SCS (-ffixed-r19
+ // prevents stubs that cover r19), so only emit SCS epilogue when we
+ // emitted our own deallocframe above.
+ if (NeedsSCS && !HasRestoreStub)
+ emitSCSEpilogue(MF, MBB, InsertPt, dl);
}
}
diff --git a/llvm/test/CodeGen/Hexagon/shadow-call-stack.ll b/llvm/test/CodeGen/Hexagon/shadow-call-stack.ll
new file mode 100644
index 0000000000000..83f02d8afcdbf
--- /dev/null
+++ b/llvm/test/CodeGen/Hexagon/shadow-call-stack.ll
@@ -0,0 +1,133 @@
+; RUN: llc -mtriple=hexagon -mattr=+reserved-r19 < %s | FileCheck %s
+;; Test that the backend fatally errors without reserved-r19 (backstop for
+;; the driver diagnostic in SanitizerArgs.cpp).
+; RUN: not --crash llc -mtriple=hexagon < %s 2>&1 | FileCheck %s --check-prefix=ERR
+; RUN: llc -mtriple=hexagon -mattr=+reserved-r19 < %s | FileCheck %s --check-prefix=CFI
+; RUN: llc -mtriple=hexagon-unknown-linux-musl -mattr=+reserved-r19 < %s | FileCheck %s --check-prefix=MUSL
+
+;; Leaf function - no LR spill, SCS should not emit any r19 instructions.
+; CHECK-LABEL: leaf:
+; CHECK-NOT: r19
+; CHECK: jumpr r31
+
+;; Non-leaf function - SCS emits prologue (addi + store) and epilogue (load + addi).
+;; The SCS store is fused into the same packet as the first call; because
+;; Hexagon packets use old-value reads the original R31 is saved regardless.
+;; The epilogue load and addi are also in the same packet; the load uses the
+;; old (pre-decrement) r19 value per Hexagon packet semantics, and the -4
+;; offset correctly addresses the saved slot.
+; CHECK-LABEL: nonleaf:
+; CHECK: r19 = add(r19,#4)
+; CHECK: call bar
+; CHECK: memw(r19+#-4) = r31
+; CHECK-DAG: r19 = add(r19,#-4)
+; CHECK-DAG: r31 = memw(r19+#-4)
+; CHECK: jumpr r31
+
+;; Multi-call function - only one SCS prologue/epilogue pair, not one per call.
+; CHECK-LABEL: twocalls:
+; CHECK: r19 = add(r19,#4)
+; CHECK: call bar
+; CHECK: memw(r19+#-4) = r31
+; CHECK: call bar
+; CHECK-DAG: r19 = add(r19,#-4)
+; CHECK-DAG: r31 = memw(r19+#-4)
+; CHECK: jumpr r31
+
+;; Conditional call (shrink-wrapping): the early-return path is a leaf and
+;; has no SCS prologue/epilogue. The call path gets the SCS pair.
+; CHECK-LABEL: condcall:
+; CHECK: if (!p0.new) jumpr:nt r31
+; CHECK: r19 = add(r19,#4)
+; CHECK: call bar
+; CHECK: memw(r19+#-4) = r31
+; CHECK-DAG: r19 = add(r19,#-4)
+; CHECK-DAG: r31 = memw(r19+#-4)
+; CHECK: jumpr r31
+
+;; Tail call - SCS prologue and epilogue are both emitted; the epilogue
+;; instructions and the tail jump are fused into the same packet.
+; CHECK-LABEL: tailcall:
+; CHECK: r19 = add(r19,#4)
+; CHECK: memw(r19+#-4) = r31
+; CHECK-DAG: r19 = add(r19,#-4)
+; CHECK-DAG: r31 = memw(r19+#-4)
+; CHECK-DAG: jump bar
+
+;; Noreturn call - SCS prologue is emitted but no SCS epilogue since the
+;; function never returns.
+; CHECK-LABEL: noret:
+; CHECK: r19 = add(r19,#4)
+; CHECK: memw(r19+#-4) = r31
+; CHECK-NOT: r31 = memw
+; CHECK-NOT: r19 = add(r19,#-4)
+; CHECK-LABEL: nonleaf_cfi:
+
+;; Without r19 reserved, SCS should report an error.
+; ERR: Must reserve r19 to use shadow call stack on Hexagon
+
+;; Non-leaf with uwtable - exercises CFI escape (DW_CFA_val_expression for r19)
+;; and cfi_restore on epilogue.
+; CFI-LABEL: nonleaf_cfi:
+; CFI: r19 = add(r19,#4)
+; CFI: memw(r19+#-4) = r31
+; CFI: .cfi_escape 0x16, 0x13, 0x02, 0x83, 0x7c
+; CFI-DAG: r31 = memw(r19+#-4)
+; CFI-DAG: r19 = add(r19,#-4)
+; CFI: .cfi_restore r19
+; CFI: jumpr r31
+
+;; Musl vararg - exercises the vararg epilogue path with SCS.
+; MUSL-LABEL: vararg_musl:
+; MUSL: r19 = add(r19,#4)
+; MUSL: memw(r19+#-4) = r31
+; MUSL-DAG: r19 = add(r19,#-4)
+; MUSL-DAG: r31 = memw(r19+#-4)
+; MUSL: jumpr r31
+
+declare void @bar()
+
+define void @leaf() shadowcallstack nounwind {
+ ret void
+}
+
+define void @nonleaf() shadowcallstack nounwind {
+ call void @bar()
+ ret void
+}
+
+define void @twocalls() shadowcallstack nounwind {
+ call void @bar()
+ call void @bar()
+ ret void
+}
+
+define void @condcall(i1 %cond) shadowcallstack nounwind {
+ br i1 %cond, label %call, label %ret
+call:
+ call void @bar()
+ br label %ret
+ret:
+ ret void
+}
+
+define void @tailcall() shadowcallstack nounwind {
+ call void @bar()
+ tail call void @bar()
+ ret void
+}
+
+define void @noret() shadowcallstack nounwind {
+ call void @bar() noreturn
+ unreachable
+}
+
+define void @nonleaf_cfi() shadowcallstack uwtable {
+ call void @bar()
+ ret void
+}
+
+define void @vararg_musl(i32 %a, ...) shadowcallstack nounwind {
+ call void @bar()
+ ret void
+}
>From a73b1c8d16552809936a9d5e0fc3744d570141c5 Mon Sep 17 00:00:00 2001
From: Brian Cain <brian.cain at oss.qualcomm.com>
Date: Tue, 23 Jun 2026 17:53:02 -0700
Subject: [PATCH 2/2] fixup! [Hexagon] Add ShadowCallStack support
---
llvm/test/CodeGen/Hexagon/shadow-call-stack.ll | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/llvm/test/CodeGen/Hexagon/shadow-call-stack.ll b/llvm/test/CodeGen/Hexagon/shadow-call-stack.ll
index 83f02d8afcdbf..ae6b254f5ed8e 100644
--- a/llvm/test/CodeGen/Hexagon/shadow-call-stack.ll
+++ b/llvm/test/CodeGen/Hexagon/shadow-call-stack.ll
@@ -20,8 +20,10 @@
; CHECK: r19 = add(r19,#4)
; CHECK: call bar
; CHECK: memw(r19+#-4) = r31
+; CHECK: {
; CHECK-DAG: r19 = add(r19,#-4)
; CHECK-DAG: r31 = memw(r19+#-4)
+; CHECK: }
; CHECK: jumpr r31
;; Multi-call function - only one SCS prologue/epilogue pair, not one per call.
@@ -30,8 +32,10 @@
; CHECK: call bar
; CHECK: memw(r19+#-4) = r31
; CHECK: call bar
+; CHECK: {
; CHECK-DAG: r19 = add(r19,#-4)
; CHECK-DAG: r31 = memw(r19+#-4)
+; CHECK: }
; CHECK: jumpr r31
;; Conditional call (shrink-wrapping): the early-return path is a leaf and
@@ -41,8 +45,10 @@
; CHECK: r19 = add(r19,#4)
; CHECK: call bar
; CHECK: memw(r19+#-4) = r31
+; CHECK: {
; CHECK-DAG: r19 = add(r19,#-4)
; CHECK-DAG: r31 = memw(r19+#-4)
+; CHECK: }
; CHECK: jumpr r31
;; Tail call - SCS prologue and epilogue are both emitted; the epilogue
@@ -50,9 +56,11 @@
; CHECK-LABEL: tailcall:
; CHECK: r19 = add(r19,#4)
; CHECK: memw(r19+#-4) = r31
+; CHECK: {
; CHECK-DAG: r19 = add(r19,#-4)
; CHECK-DAG: r31 = memw(r19+#-4)
; CHECK-DAG: jump bar
+; CHECK: }
;; Noreturn call - SCS prologue is emitted but no SCS epilogue since the
;; function never returns.
@@ -72,8 +80,10 @@
; CFI: r19 = add(r19,#4)
; CFI: memw(r19+#-4) = r31
; CFI: .cfi_escape 0x16, 0x13, 0x02, 0x83, 0x7c
+; CFI: {
; CFI-DAG: r31 = memw(r19+#-4)
; CFI-DAG: r19 = add(r19,#-4)
+; CFI: }
; CFI: .cfi_restore r19
; CFI: jumpr r31
@@ -81,8 +91,10 @@
; MUSL-LABEL: vararg_musl:
; MUSL: r19 = add(r19,#4)
; MUSL: memw(r19+#-4) = r31
+; MUSL: {
; MUSL-DAG: r19 = add(r19,#-4)
; MUSL-DAG: r31 = memw(r19+#-4)
+; MUSL: }
; MUSL: jumpr r31
declare void @bar()
More information about the llvm-commits
mailing list