Skip to content

[Hexagon] Add probe-stack=inline-asm support for stack clash protection - #190568

Merged
androm3da merged 4 commits into
llvm:mainfrom
androm3da:hexagon-stack-probing
Jun 26, 2026
Merged

[Hexagon] Add probe-stack=inline-asm support for stack clash protection#190568
androm3da merged 4 commits into
llvm:mainfrom
androm3da:hexagon-stack-probing

Conversation

@androm3da

Copy link
Copy Markdown
Member

Large stack allocations can skip over guard pages, causing stack clash vulnerabilities. The probe-stack=inline-asm function attribute tells LLVM to emit inline probing code that touches each page during stack allocation, ensuring guard pages are hit.

framelimit provides hardware bounds checking, but only on the allocframe instruction itself. SP decrements via A2_addi -- used for frames >= 16k bytes and all no-FP prologues bypass it. Software probing closes that gap.

When the attribute is present and the frame size exceeds the probe size, the prologue now emits a PS_probed_stackalloc pseudo that inlineStackProbe() expands into a compare-and-branch loop:

r29 = add(r29, #-ProbeSize)
memw(r29+#0) = #0
p0 = cmp.gtu(r29, r28)
if (p0) jump LoopMBB
r29 = r28

Both the frame-pointer and no-frame-pointer prologue paths are handled. The stack-probe-size attribute is respected for custom probe sizes.

@llvmbot

llvmbot commented Apr 6, 2026

Copy link
Copy Markdown
Member

@llvm/pr-subscribers-backend-hexagon

Author: Brian Cain (androm3da)

Changes

Large stack allocations can skip over guard pages, causing stack clash vulnerabilities. The probe-stack=inline-asm function attribute tells LLVM to emit inline probing code that touches each page during stack allocation, ensuring guard pages are hit.

framelimit provides hardware bounds checking, but only on the allocframe instruction itself. SP decrements via A2_addi -- used for frames >= 16k bytes and all no-FP prologues bypass it. Software probing closes that gap.

When the attribute is present and the frame size exceeds the probe size, the prologue now emits a PS_probed_stackalloc pseudo that inlineStackProbe() expands into a compare-and-branch loop:

r29 = add(r29, #-ProbeSize)
memw(r29+#0) = #0
p0 = cmp.gtu(r29, r28)
if (p0) jump LoopMBB
r29 = r28

Both the frame-pointer and no-frame-pointer prologue paths are handled. The stack-probe-size attribute is respected for custom probe sizes.


Full diff: https://github.com/llvm/llvm-project/pull/190568.diff

6 Files Affected:

  • (modified) llvm/lib/Target/Hexagon/HexagonFrameLowering.cpp (+144-4)
  • (modified) llvm/lib/Target/Hexagon/HexagonFrameLowering.h (+3)
  • (modified) llvm/lib/Target/Hexagon/HexagonISelLowering.cpp (+18)
  • (modified) llvm/lib/Target/Hexagon/HexagonISelLowering.h (+3)
  • (modified) llvm/lib/Target/Hexagon/HexagonPseudo.td (+6)
  • (added) llvm/test/CodeGen/Hexagon/stack-probing.ll (+166)
diff --git a/llvm/lib/Target/Hexagon/HexagonFrameLowering.cpp b/llvm/lib/Target/Hexagon/HexagonFrameLowering.cpp
index 0f73efc243986..2bd40d0eb6f28 100644
--- a/llvm/lib/Target/Hexagon/HexagonFrameLowering.cpp
+++ b/llvm/lib/Target/Hexagon/HexagonFrameLowering.cpp
@@ -9,6 +9,7 @@
 
 #include "HexagonFrameLowering.h"
 #include "HexagonBlockRanges.h"
+#include "HexagonISelLowering.h"
 #include "HexagonInstrInfo.h"
 #include "HexagonMachineFunctionInfo.h"
 #include "HexagonRegisterInfo.h"
@@ -746,9 +747,29 @@ void HexagonFrameLowering::insertPrologueInBlock(MachineBasicBlock &MBB,
              .addExternalSymbol("__runtime_stack_check");
   } else if (NumBytes > 0) {
     assert(alignTo(NumBytes, 8) == NumBytes);
-    BuildMI(MBB, InsertPt, dl, HII.get(Hexagon::A2_addi), SP)
-      .addReg(SP)
-      .addImm(-int(NumBytes));
+    auto *TLI = HST.getTargetLowering();
+    bool NeedsProbing = TLI->hasInlineStackProbe(MF);
+    unsigned ProbeSize = 0;
+    if (NeedsProbing) {
+      Align StackAlign = getStackAlign();
+      ProbeSize = TLI->getStackProbeSize(MF, StackAlign);
+    }
+    if (NeedsProbing && NumBytes > ProbeSize) {
+      // Compute target SP in R28 (caller-saved scratch).
+      BuildMI(MBB, InsertPt, dl, HII.get(Hexagon::A2_addi), Hexagon::R28)
+          .addReg(SP)
+          .addImm(-int(NumBytes))
+          .setMIFlag(MachineInstr::FrameSetup);
+      // Emit pseudo to be expanded by inlineStackProbe().
+      BuildMI(MBB, InsertPt, dl, HII.get(Hexagon::PS_probed_stackalloc))
+          .addReg(Hexagon::R28)
+          .setMIFlag(MachineInstr::FrameSetup);
+    } else {
+      BuildMI(MBB, InsertPt, dl, HII.get(Hexagon::A2_addi), SP)
+          .addReg(SP)
+          .addImm(-int(NumBytes))
+          .setMIFlag(MachineInstr::FrameSetup);
+    }
   }
 }
 
@@ -892,7 +913,34 @@ void HexagonFrameLowering::insertAllocframe(MachineBasicBlock &MBB,
   DebugLoc dl = MBB.findDebugLoc(InsertPt);
   Register SP = HRI.getStackRegister();
 
-  if (NumBytes >= ALLOCFRAME_MAX) {
+  auto *TLI = HST.getTargetLowering();
+  bool NeedsProbing = TLI->hasInlineStackProbe(MF) && NumBytes > 0;
+  unsigned ProbeSize = 0;
+  if (NeedsProbing) {
+    Align StackAlign = getStackAlign();
+    ProbeSize = TLI->getStackProbeSize(MF, StackAlign);
+  }
+
+  if (NeedsProbing && NumBytes > ProbeSize) {
+    // Emit allocframe(#0) to save FP/LR only.
+    BuildMI(MBB, InsertPt, dl, HII.get(Hexagon::S2_allocframe))
+        .addDef(SP)
+        .addReg(SP)
+        .addImm(0)
+        .addMemOperand(MMO)
+        .setMIFlag(MachineInstr::FrameSetup);
+
+    // Compute target SP in R28 (caller-saved scratch).
+    BuildMI(MBB, InsertPt, dl, HII.get(Hexagon::A2_addi), Hexagon::R28)
+        .addReg(SP)
+        .addImm(-int(NumBytes))
+        .setMIFlag(MachineInstr::FrameSetup);
+
+    // Emit pseudo to be expanded by inlineStackProbe().
+    BuildMI(MBB, InsertPt, dl, HII.get(Hexagon::PS_probed_stackalloc))
+        .addReg(Hexagon::R28)
+        .setMIFlag(MachineInstr::FrameSetup);
+  } else if (NumBytes >= ALLOCFRAME_MAX) {
     // Emit allocframe(#0).
     BuildMI(MBB, InsertPt, dl, HII.get(Hexagon::S2_allocframe))
         .addDef(SP)
@@ -917,6 +965,98 @@ void HexagonFrameLowering::insertAllocframe(MachineBasicBlock &MBB,
   }
 }
 
+void HexagonFrameLowering::inlineStackProbe(
+    MachineFunction &MF, MachineBasicBlock &PrologueMBB) const {
+  // Collect PS_probed_stackalloc pseudos to expand. Collecting first avoids
+  // issues with modifying the block while iterating.
+  SmallVector<MachineInstr *, 2> ToReplace;
+  for (MachineInstr &MI : PrologueMBB)
+    if (MI.getOpcode() == Hexagon::PS_probed_stackalloc)
+      ToReplace.push_back(&MI);
+
+  auto &HST = MF.getSubtarget<HexagonSubtarget>();
+  auto &HII = *HST.getInstrInfo();
+  auto *TLI = HST.getTargetLowering();
+  Align StackAlign = getStackAlign();
+  unsigned ProbeSize = TLI->getStackProbeSize(MF, StackAlign);
+  MachineInstr::MIFlag Flags = MachineInstr::FrameSetup;
+
+  for (MachineInstr *MI : ToReplace) {
+    MachineBasicBlock::iterator MBBI = MI->getIterator();
+    DebugLoc DL = PrologueMBB.findDebugLoc(MBBI);
+    Register TargetReg = MI->getOperand(0).getReg();
+
+    // Split the block: everything after the pseudo goes into ExitMBB.
+    MachineBasicBlock *MBB = MI->getParent();
+    MachineFunction::iterator InsertPt = std::next(MBB->getIterator());
+    MachineBasicBlock *LoopMBB =
+        MF.CreateMachineBasicBlock(MBB->getBasicBlock());
+    MF.insert(InsertPt, LoopMBB);
+    MachineBasicBlock *ExitMBB =
+        MF.CreateMachineBasicBlock(MBB->getBasicBlock());
+    MF.insert(InsertPt, ExitMBB);
+
+    // Move everything after the pseudo into ExitMBB.
+    ExitMBB->splice(ExitMBB->end(), MBB, std::next(MBBI), MBB->end());
+    ExitMBB->transferSuccessorsAndUpdatePHIs(MBB);
+
+    // LoopMBB: probe each page by decrementing SP and storing zero.
+    // When NumBytes is not an exact multiple of ProbeSize the loop
+    // will overshoot by up to ProbeSize-1 bytes; the final r29 = r28
+    // in ExitMBB corrects SP to the true target.
+    //
+    // The store is placed before the compare+branch so that the
+    // packetizer can bundle them into a single VLIW packet.  All
+    // non-predicated instructions in a packet commit unconditionally,
+    // so the probe store executes on every iteration including the
+    // last (when the branch falls through).
+    //
+    //   r29 = add(r29, #-ProbeSize)
+    //   memw(r29+#0) = #0
+    //   p0 = cmp.gtu(r29, r28)
+    //   if (p0) jump LoopMBB
+    BuildMI(*LoopMBB, LoopMBB->end(), DL, HII.get(Hexagon::A2_addi),
+            Hexagon::R29)
+        .addReg(Hexagon::R29)
+        .addImm(-int(ProbeSize))
+        .setMIFlags(Flags);
+
+    BuildMI(*LoopMBB, LoopMBB->end(), DL, HII.get(Hexagon::S4_storeiri_io))
+        .addReg(Hexagon::R29)
+        .addImm(0)
+        .addImm(0)
+        .setMIFlags(Flags);
+
+    BuildMI(*LoopMBB, LoopMBB->end(), DL, HII.get(Hexagon::C2_cmpgtu),
+            Hexagon::P0)
+        .addReg(Hexagon::R29)
+        .addReg(TargetReg)
+        .setMIFlags(Flags);
+
+    BuildMI(*LoopMBB, LoopMBB->end(), DL, HII.get(Hexagon::J2_jumpt))
+        .addReg(Hexagon::P0)
+        .addMBB(LoopMBB)
+        .setMIFlags(Flags);
+
+    // ExitMBB: set final SP.
+    BuildMI(*ExitMBB, ExitMBB->begin(), DL, HII.get(Hexagon::A2_tfr),
+            Hexagon::R29)
+        .addReg(TargetReg)
+        .setMIFlags(Flags);
+
+    // Set up CFG edges.
+    MBB->addSuccessor(LoopMBB);
+    LoopMBB->addSuccessor(LoopMBB);
+    LoopMBB->addSuccessor(ExitMBB);
+
+    // Remove the pseudo.
+    MI->eraseFromParent();
+
+    // Recompute live-ins for the new blocks.
+    fullyRecomputeLiveIns({ExitMBB, LoopMBB});
+  }
+}
+
 void HexagonFrameLowering::updateEntryPaths(MachineFunction &MF,
       MachineBasicBlock &SaveB) const {
   SetVector<unsigned> Worklist;
diff --git a/llvm/lib/Target/Hexagon/HexagonFrameLowering.h b/llvm/lib/Target/Hexagon/HexagonFrameLowering.h
index 926aadb01f50e..285e6a5896461 100644
--- a/llvm/lib/Target/Hexagon/HexagonFrameLowering.h
+++ b/llvm/lib/Target/Hexagon/HexagonFrameLowering.h
@@ -113,6 +113,9 @@ class HexagonFrameLowering : public TargetFrameLowering {
 
   void insertCFIInstructions(MachineFunction &MF) const;
 
+  void inlineStackProbe(MachineFunction &MF,
+                        MachineBasicBlock &PrologueMBB) const override;
+
 protected:
   bool hasFPImpl(const MachineFunction &MF) const override;
 
diff --git a/llvm/lib/Target/Hexagon/HexagonISelLowering.cpp b/llvm/lib/Target/Hexagon/HexagonISelLowering.cpp
index 6b2a963e2e777..a81fd20d715f2 100644
--- a/llvm/lib/Target/Hexagon/HexagonISelLowering.cpp
+++ b/llvm/lib/Target/Hexagon/HexagonISelLowering.cpp
@@ -4004,3 +4004,21 @@ bool HexagonTargetLowering::isUsedByReturnOnly(SDNode *N,
   Chain = Copy->getOperand(0);
   return true;
 }
+
+bool HexagonTargetLowering::hasInlineStackProbe(
+    const MachineFunction &MF) const {
+  if (MF.getFunction().hasFnAttribute("probe-stack"))
+    return MF.getFunction().getFnAttribute("probe-stack").getValueAsString() ==
+           "inline-asm";
+  return false;
+}
+
+unsigned HexagonTargetLowering::getStackProbeSize(const MachineFunction &MF,
+                                                  Align StackAlign) const {
+  const Function &Fn = MF.getFunction();
+  unsigned StackProbeSize =
+      Fn.getFnAttributeAsParsedInteger("stack-probe-size", 4096);
+  // Round down to the stack alignment.
+  StackProbeSize = alignDown(StackProbeSize, StackAlign.value());
+  return StackProbeSize ? StackProbeSize : StackAlign.value();
+}
diff --git a/llvm/lib/Target/Hexagon/HexagonISelLowering.h b/llvm/lib/Target/Hexagon/HexagonISelLowering.h
index 55be8fd043ad5..60cf38a6abcc7 100644
--- a/llvm/lib/Target/Hexagon/HexagonISelLowering.h
+++ b/llvm/lib/Target/Hexagon/HexagonISelLowering.h
@@ -287,6 +287,9 @@ class HexagonTargetLowering : public TargetLowering {
     return AtomicExpansionKind::LLSC;
   }
 
+  bool hasInlineStackProbe(const MachineFunction &MF) const override;
+  unsigned getStackProbeSize(const MachineFunction &MF, Align StackAlign) const;
+
 private:
   void initializeHVXLowering();
   unsigned getPreferredHvxVectorAction(MVT VecTy) const;
diff --git a/llvm/lib/Target/Hexagon/HexagonPseudo.td b/llvm/lib/Target/Hexagon/HexagonPseudo.td
index 4e5e8c3d26f00..fc74d8e2d7882 100644
--- a/llvm/lib/Target/Hexagon/HexagonPseudo.td
+++ b/llvm/lib/Target/Hexagon/HexagonPseudo.td
@@ -335,6 +335,12 @@ let Defs = [R29], hasSideEffects = 1 in
 def PS_alloca: Pseudo <(outs IntRegs:$Rd),
                        (ins IntRegs:$Rs, u32_0Imm:$A), "", []>;
 
+// Probed stack allocation pseudo. Expanded in inlineStackProbe() into a
+// compare-and-branch loop that touches each page.
+let Defs = [R29, P0], Uses = [R29], hasSideEffects = 1, mayStore = 1,
+    isCodeGenOnly = 1 in
+def PS_probed_stackalloc : Pseudo<(outs), (ins IntRegs:$target), "", []>;
+
 // Load predicate.
 let isExtendable = 1, opExtendable = 2, isExtentSigned = 1, opExtentBits = 13,
     isCodeGenOnly = 1, isPseudo = 1, hasSideEffects = 0 in
diff --git a/llvm/test/CodeGen/Hexagon/stack-probing.ll b/llvm/test/CodeGen/Hexagon/stack-probing.ll
new file mode 100644
index 0000000000000..a59556a782a53
--- /dev/null
+++ b/llvm/test/CodeGen/Hexagon/stack-probing.ll
@@ -0,0 +1,166 @@
+; RUN: llc -mtriple=hexagon < %s | FileCheck %s
+
+;; Small frame (< probe size): no probing loop, normal allocframe.
+; CHECK-LABEL: small_frame:
+; CHECK: allocframe(r29,#128):raw
+; CHECK-NOT: cmp.gtu
+; CHECK: dealloc_return
+define void @small_frame() #0 {
+entry:
+  %a = alloca [128 x i8], align 1
+  call void @use(ptr %a)
+  ret void
+}
+
+;; Large frame (> probe size): probing loop emitted.
+; CHECK-LABEL: large_frame:
+; CHECK: allocframe(r29,#0):raw
+; CHECK: r28 = add(r29,#-8192)
+; CHECK: .LBB{{[0-9]+}}_{{[0-9]+}}:
+; CHECK: r29 = add(r29,#-4096)
+; CHECK: p0 = cmp.gtu(r29,r28)
+; CHECK: if (p0.new) jump:t .LBB
+; CHECK: memw(r29+#0) = #0
+; CHECK: r29 = r28
+define void @large_frame() #0 {
+entry:
+  %a = alloca [8192 x i8], align 1
+  call void @use(ptr %a)
+  ret void
+}
+
+;; Exact multiple of probe size: probing loop still emitted.
+; CHECK-LABEL: exact_multiple:
+; CHECK: allocframe(r29,#0):raw
+; CHECK: r28 = add(r29,#-12288)
+; CHECK: .LBB{{[0-9]+}}_{{[0-9]+}}:
+; CHECK: r29 = add(r29,#-4096)
+; CHECK: p0 = cmp.gtu(r29,r28)
+; CHECK: if (p0.new) jump:t .LBB
+; CHECK: memw(r29+#0) = #0
+; CHECK: r29 = r28
+define void @exact_multiple() #0 {
+entry:
+  %a = alloca [12288 x i8], align 1
+  call void @use(ptr %a)
+  ret void
+}
+
+;; No frame pointer path with no call: probing works without allocframe.
+; CHECK-LABEL: no_fp_large:
+; CHECK-NOT: allocframe
+; CHECK: r28 = add(r29,#-8192)
+; CHECK: .LBB{{[0-9]+}}_{{[0-9]+}}:
+; CHECK: r29 = add(r29,#-4096)
+; CHECK: p0 = cmp.gtu(r29,r28)
+; CHECK: if (p0.new) jump:t .LBB
+; CHECK: memw(r29+#0) = #0
+; CHECK: r29 = r28
+define void @no_fp_large() #1 {
+entry:
+  %a = alloca [8192 x i8], align 1
+  store volatile i8 0, ptr %a
+  ret void
+}
+
+;; Custom probe size of 512 bytes.
+; CHECK-LABEL: custom_probe_size:
+; CHECK: allocframe(r29,#0):raw
+; CHECK: r28 = add(r29,#-8192)
+; CHECK: .LBB{{[0-9]+}}_{{[0-9]+}}:
+; CHECK: r29 = add(r29,#-512)
+; CHECK: p0 = cmp.gtu(r29,r28)
+; CHECK: if (p0.new) jump:t .LBB
+; CHECK: memw(r29+#0) = #0
+; CHECK: r29 = r28
+define void @custom_probe_size() #2 {
+entry:
+  %a = alloca [8192 x i8], align 1
+  call void @use(ptr %a)
+  ret void
+}
+
+;; No probe attribute: normal codegen, no probing.
+; CHECK-LABEL: no_probe:
+; CHECK: allocframe
+; CHECK-NOT: cmp.gtu
+; CHECK: dealloc_return
+define void @no_probe() {
+entry:
+  %a = alloca [8192 x i8], align 1
+  call void @use(ptr %a)
+  ret void
+}
+
+;; Frame >= ALLOCFRAME_MAX (16384): allocframe(#0) + probed alloc.
+; CHECK-LABEL: very_large_frame:
+; CHECK: allocframe(r29,#0):raw
+; CHECK: r28 = add(r29,#-20480)
+; CHECK: .LBB{{[0-9]+}}_{{[0-9]+}}:
+; CHECK: r29 = add(r29,#-4096)
+; CHECK: p0 = cmp.gtu(r29,r28)
+; CHECK: if (p0.new) jump:t .LBB
+; CHECK: memw(r29+#0) = #0
+; CHECK: r29 = r28
+define void @very_large_frame() #0 {
+entry:
+  %a = alloca [20480 x i8], align 1
+  call void @use(ptr %a)
+  ret void
+}
+
+;; Frame == probe size exactly: no probing loop, normal allocframe.
+; CHECK-LABEL: exact_probe_size:
+; CHECK: allocframe(r29,#4096):raw
+; CHECK-NOT: cmp.gtu
+; CHECK: dealloc_return
+define void @exact_probe_size() #0 {
+entry:
+  %a = alloca [4096 x i8], align 1
+  call void @use(ptr %a)
+  ret void
+}
+
+;; Large frame requiring constant-extended immediate (> 32767).
+; CHECK-LABEL: const_extd_frame:
+; CHECK: allocframe(r29,#0):raw
+; CHECK: r28 = add(r29,##-65536)
+; CHECK: .LBB{{[0-9]+}}_{{[0-9]+}}:
+; CHECK: r29 = add(r29,#-4096)
+; CHECK: p0 = cmp.gtu(r29,r28)
+; CHECK: if (p0.new) jump:t .LBB
+; CHECK: memw(r29+#0) = #0
+; CHECK: r29 = r28
+define void @const_extd_frame() #0 {
+entry:
+  %a = alloca [65536 x i8], align 1
+  call void @use(ptr %a)
+  ret void
+}
+
+;; Callee-saved register spills coexist with probing.
+; CHECK-LABEL: callee_saved_regs:
+; CHECK: allocframe(r29,#0):raw
+; CHECK: r28 = add(r29,#-8216)
+; CHECK: .LBB{{[0-9]+}}_{{[0-9]+}}:
+; CHECK: r29 = add(r29,#-4096)
+; CHECK: p0 = cmp.gtu(r29,r28)
+; CHECK: if (p0.new) jump:t .LBB
+; CHECK: memw(r29+#0) = #0
+; CHECK: r29 = r28
+; CHECK: memd(r29+##{{[0-9]+}}) = r{{[0-9]+}}:{{[0-9]+}}
+; CHECK: dealloc_return
+define void @callee_saved_regs(ptr %p) #0 {
+entry:
+  %a = alloca [8192 x i8], align 1
+  call void @use(ptr %a)
+  call void asm sideeffect "", "~{r16},~{r17},~{r18},~{r19}"()
+  call void @use(ptr %p)
+  ret void
+}
+
+declare void @use(ptr)
+
+attributes #0 = { "probe-stack"="inline-asm" }
+attributes #1 = { nounwind "frame-pointer"="none" "probe-stack"="inline-asm" }
+attributes #2 = { "probe-stack"="inline-asm" "stack-probe-size"="512" }

@androm3da

Copy link
Copy Markdown
Member Author

cc @quic-mliebel @quic-mathbern

Comment thread llvm/test/CodeGen/Hexagon/stack-probing.ll Outdated
@quic-mliebel

Copy link
Copy Markdown

Fixup commit needs to be squash. LGTM otherwise.

@androm3da

Copy link
Copy Markdown
Member Author

Fixup commit needs to be squash. LGTM otherwise.

Like it or hate it -- for this project, all commits from a PR are always squashed.

@androm3da

Copy link
Copy Markdown
Member Author

ping @aankit-ca

@aankit-ca

aankit-ca commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

@androm3da Will review it by today. Sorry for the delay

@androm3da

Copy link
Copy Markdown
Member Author

@aankit-ca ping

@androm3da
androm3da requested a review from iajbar June 1, 2026 18:56
@bohara-qti

Copy link
Copy Markdown

Does the probe catch things like:

void f(int n) {
char buf[n];
use(buf);
}
...
{
r2 = add(r0,#7)
allocframe(r29,#8)
}
{
r2 = and(r2,#-8)
}
{
r0 = sub(r29,r2)
r29 = sub(r29,r2)
call use
}

@iajbar iajbar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@androm3da

Copy link
Copy Markdown
Member Author

Does the probe catch things like:

void f(int n) { char buf[n]; use(buf); } 
... 
{ r2 = add(r0,#7) 
allocframe(r29,#8) } 
{ r2 = and(r2,#-8) } 
{ r0 = sub(r29,r2)
 r29 = sub(r29,r2)
 call use }

It doesn't - good catch, thanks.

Will fix it.

Large stack allocations can skip over guard pages, causing stack clash
vulnerabilities. The probe-stack=inline-asm function attribute tells
LLVM to emit inline probing code that touches each page during stack
allocation, ensuring guard pages are hit.

`framelimit` provides hardware bounds checking, but only on the allocframe
instruction itself. SP decrements via A2_addi -- used for frames >= 16k bytes
and all no-FP prologues bypass it.  Software probing closes that gap.

When the attribute is present and the frame size exceeds the probe size,
the prologue now emits a PS_probed_stackalloc pseudo that inlineStackProbe()
expands into a compare-and-branch loop:

  r29 = add(r29, #-ProbeSize)
  memw(r29+#0) = #0
  p0 = cmp.gtu(r29, r28)
  if (p0) jump LoopMBB
  r29 = r28

Both the frame-pointer and no-frame-pointer prologue paths are handled.
The stack-probe-size attribute is respected for custom probe sizes.
…rotection

fixup! [Hexagon] Add probe-stack=inline-asm support for stack clash protection

Check VLIW packet boundaries in probe loop to verify the store is
bundled with the compare+branch.
…rotection

Extend expandAlloca() to emit a probe loop for variable-length array
(dynamic alloca) allocations when probe-stack=inline-asm is active.

The dynamic allocation size is a runtime value so the probe loop is
always emitted.  The sequence is:

  Rd  = sub(r29, Rs)       ; compute target SP
  [Rd = and(Rd, #-A)]      ; align if necessary
  LoopMBB:
    r29 = add(r29, #-ProbeSize)
    memw(r29+#0) = #0
    p0 = cmp.gtu(r29, Rd)
    if (p0.new) jump:t LoopMBB
  ExitMBB:
    r29 = Rd                ; snap SP to exact target
    [Rd = add(Rd, #CF)]     ; call-frame adjustment

Rd holds the aligned target SP throughout the loop so the
final r29 = Rd correctly snaps SP even when Rs is not a multiple of
ProbeSize.
@androm3da
androm3da force-pushed the hexagon-stack-probing branch from 787c39b to cfbea16 Compare June 24, 2026 14:25
@androm3da

Copy link
Copy Markdown
Member Author

Does the probe catch things like:

void f(int n) { char buf[n]; use(buf); } 
... 
{ r2 = add(r0,#7) 
allocframe(r29,#8) } 
{ r2 = and(r2,#-8) } 
{ r0 = sub(r29,r2)
 r29 = sub(r29,r2)
 call use }

It doesn't - good catch, thanks.

Will fix it.

Good suggestion, @bohara-qti -- now cfbea16 adds support for VLAs

@bohara-qti

Copy link
Copy Markdown

LGTM.

@androm3da
androm3da enabled auto-merge (squash) June 26, 2026 19:11
@androm3da
androm3da merged commit 153e71e into llvm:main Jun 26, 2026
10 of 11 checks passed
LouisLu060211 pushed a commit to LouisLu060211/llvm-project that referenced this pull request Jun 30, 2026
…on (llvm#190568)

Large stack allocations can skip over guard pages, causing stack clash
vulnerabilities. The probe-stack=inline-asm function attribute tells
LLVM to emit inline probing code that touches each page during stack
allocation, ensuring guard pages are hit.

`framelimit` provides hardware bounds checking, but only on the
allocframe instruction itself. SP decrements via A2_addi -- used for
frames >= 16k bytes and all no-FP prologues bypass it. Software probing
closes that gap.

When the attribute is present and the frame size exceeds the probe size,
the prologue now emits a PS_probed_stackalloc pseudo that
inlineStackProbe() expands into a compare-and-branch loop:

  r29 = add(r29, #-ProbeSize)
  memw(r29+#0) = #0
  p0 = cmp.gtu(r29, r28)
  if (p0) jump LoopMBB
  r29 = r28

Both the frame-pointer and no-frame-pointer prologue paths are handled.
The stack-probe-size attribute is respected for custom probe sizes.
maarcosrmz pushed a commit to maarcosrmz/llvm-project that referenced this pull request Jul 1, 2026
…on (llvm#190568)

Large stack allocations can skip over guard pages, causing stack clash
vulnerabilities. The probe-stack=inline-asm function attribute tells
LLVM to emit inline probing code that touches each page during stack
allocation, ensuring guard pages are hit.

`framelimit` provides hardware bounds checking, but only on the
allocframe instruction itself. SP decrements via A2_addi -- used for
frames >= 16k bytes and all no-FP prologues bypass it. Software probing
closes that gap.

When the attribute is present and the frame size exceeds the probe size,
the prologue now emits a PS_probed_stackalloc pseudo that
inlineStackProbe() expands into a compare-and-branch loop:

  r29 = add(r29, #-ProbeSize)
  memw(r29+#0) = #0
  p0 = cmp.gtu(r29, r28)
  if (p0) jump LoopMBB
  r29 = r28

Both the frame-pointer and no-frame-pointer prologue paths are handled.
The stack-probe-size attribute is respected for custom probe sizes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants