Skip to content

[LFI][AArch64] Add rewrites for memory accesses#195167

Merged
zyedidia merged 11 commits into
llvm:mainfrom
lfi-project:llvm-pr/lfi/aarch64-rewriter-3
Jun 10, 2026
Merged

[LFI][AArch64] Add rewrites for memory accesses#195167
zyedidia merged 11 commits into
llvm:mainfrom
lfi-project:llvm-pr/lfi/aarch64-rewriter-3

Conversation

@zyedidia

Copy link
Copy Markdown
Member

This PR adds LFI rewrites for loads and stores, and guards for stack pointer modifications. In this implementation, I've taken the approach of using functions with switch tables to track all the addressing mode information we need: base/offset indices and whether the access uses pre/post-indexing. I've tried to use macros to shrink the size of these switch statements and avoid duplication. In a previous implementation I used TSFlags bits set from TableGen to record this information, but the approach in this PR is more contained to just the LFI rewriter.

This also introduces the +no-lfi-stores and +no-lfi-loads features to control the granularity of memory sandboxing.

With these changes the rewriter now supports making fully software-sandboxed programs. I've tested the changes on SPEC 2017 and the LLVM test suite. There are some minor changes needed in libunwind to avoid emitting SVE/MTE instructions that cause verification failure, which I'll address in a future patch.

After this patch I'll add the guard elimination optimization, which will improve performance. After that the initial version of AArch64 LFI is complete. I also plan to make a patch to Clang to control the LFI subtarget features from a frontend option, and that can also set a macro that communicates the feature level (currently we just have __LFI__).

@llvmorg-github-actions

Copy link
Copy Markdown

@llvm/pr-subscribers-backend-aarch64

Author: Zachary Yedidia (zyedidia)

Changes

This PR adds LFI rewrites for loads and stores, and guards for stack pointer modifications. In this implementation, I've taken the approach of using functions with switch tables to track all the addressing mode information we need: base/offset indices and whether the access uses pre/post-indexing. I've tried to use macros to shrink the size of these switch statements and avoid duplication. In a previous implementation I used TSFlags bits set from TableGen to record this information, but the approach in this PR is more contained to just the LFI rewriter.

This also introduces the +no-lfi-stores and +no-lfi-loads features to control the granularity of memory sandboxing.

With these changes the rewriter now supports making fully software-sandboxed programs. I've tested the changes on SPEC 2017 and the LLVM test suite. There are some minor changes needed in libunwind to avoid emitting SVE/MTE instructions that cause verification failure, which I'll address in a future patch.

After this patch I'll add the guard elimination optimization, which will improve performance. After that the initial version of AArch64 LFI is complete. I also plan to make a patch to Clang to control the LFI subtarget features from a frontend option, and that can also set a macro that communicates the feature level (currently we just have __LFI__).


Patch is 101.66 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/195167.diff

18 Files Affected:

  • (modified) llvm/docs/LFI.rst (-6)
  • (modified) llvm/lib/Target/AArch64/AArch64Features.td (+5)
  • (modified) llvm/lib/Target/AArch64/AArch64InstrInfo.cpp (+29-3)
  • (modified) llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp (+962-1)
  • (modified) llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.h (+31)
  • (added) llvm/test/MC/AArch64/LFI/exclusive.s (+140)
  • (added) llvm/test/MC/AArch64/LFI/fp.s (+204)
  • (added) llvm/test/MC/AArch64/LFI/jumps-only.s (+41)
  • (added) llvm/test/MC/AArch64/LFI/literal.s (+37)
  • (added) llvm/test/MC/AArch64/LFI/lse.s (+166)
  • (added) llvm/test/MC/AArch64/LFI/mem.s (+507)
  • (added) llvm/test/MC/AArch64/LFI/no-lfi-loads.s (+33)
  • (added) llvm/test/MC/AArch64/LFI/no-lfi-stores.s (+35)
  • (added) llvm/test/MC/AArch64/LFI/prefetch.s (+81)
  • (added) llvm/test/MC/AArch64/LFI/rcpc.s (+19)
  • (added) llvm/test/MC/AArch64/LFI/simd.s (+650)
  • (added) llvm/test/MC/AArch64/LFI/stack.s (+41)
  • (modified) llvm/test/MC/AArch64/LFI/sys.s (+24)
diff --git a/llvm/docs/LFI.rst b/llvm/docs/LFI.rst
index a173cd57e0ee0..fb28195fa23d4 100644
--- a/llvm/docs/LFI.rst
+++ b/llvm/docs/LFI.rst
@@ -115,8 +115,6 @@ Example:
 Compiler Options
 ++++++++++++++++
 
-**Note**: these options are not yet implemented.
-
 The LFI target has several configuration options, specified via ``-mattr=``:
 
 * ``+no-lfi-loads``: Disable sandboxing for load instructions (stores-only mode).
@@ -197,8 +195,6 @@ require any rewrite.
 Memory accesses
 ~~~~~~~~~~~~~~~
 
-**Note**: not yet implemented.
-
 Memory accesses are rewritten to use the ``[x27, wM, uxtw]`` addressing mode if
 it is available, which is automatically safe. Otherwise, rewrites fall back to
 using ``x28`` along with an instruction to safely load it with the target
@@ -279,8 +275,6 @@ address.
 Stack pointer modification
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-**Note**: not yet implemented.
-
 When the stack pointer is modified, we write the modified value to a temporary,
 before moving it back into ``sp`` with a safe ``add``.
 
diff --git a/llvm/lib/Target/AArch64/AArch64Features.td b/llvm/lib/Target/AArch64/AArch64Features.td
index 8cd2278a05747..ab77106eaa1bc 100644
--- a/llvm/lib/Target/AArch64/AArch64Features.td
+++ b/llvm/lib/Target/AArch64/AArch64Features.td
@@ -1075,6 +1075,11 @@ def FeatureHardenSlsNoComdat : SubtargetFeature<"harden-sls-nocomdat",
   "Generate thunk code for SLS mitigation in the normal text section">;
 
 
+def FeatureNoLFILoads : SubtargetFeature<"no-lfi-loads", "NoLFILoads", "true",
+  "Disable LFI sandboxing for load instructions">;
+def FeatureNoLFIStores : SubtargetFeature<"no-lfi-stores", "NoLFIStores", "true",
+  "Disable LFI sandboxing for store instructions">;
+
 // Only intended to be used by disassemblers.
 def FeatureAll
     : SubtargetFeature<"all", "IsAll", "true", "Enable all instructions">;
diff --git a/llvm/lib/Target/AArch64/AArch64InstrInfo.cpp b/llvm/lib/Target/AArch64/AArch64InstrInfo.cpp
index 844adc3eb54c4..decb3572c6638 100644
--- a/llvm/lib/Target/AArch64/AArch64InstrInfo.cpp
+++ b/llvm/lib/Target/AArch64/AArch64InstrInfo.cpp
@@ -16,6 +16,7 @@
 #include "AArch64PointerAuth.h"
 #include "AArch64Subtarget.h"
 #include "MCTargetDesc/AArch64AddressingModes.h"
+#include "MCTargetDesc/AArch64MCLFIRewriter.h"
 #include "MCTargetDesc/AArch64MCTargetDesc.h"
 #include "Utils/AArch64BaseInfo.h"
 #include "llvm/ADT/ArrayRef.h"
@@ -129,14 +130,39 @@ static std::optional<unsigned> getLFIInstSizeInBytes(const MachineInstr &MI) {
     if (MI.getOperand(0).getReg() != AArch64::LR)
       return 8;
     return 4;
+  case AArch64::SYSxt:
+    // VA-based DC/IC ops (op1=3, Cn=7, op2=1) expand to 2 instructions.
+    if (MI.getOperand(0).getImm() == 3 && MI.getOperand(1).getImm() == 7 &&
+        MI.getOperand(3).getImm() == 1)
+      return 8;
+    return std::nullopt;
   default:
     break;
   }
 
-  // Instructions that explicitly modify LR expand to 2 instructions.
+  // Instructions that explicitly modify LR get an extra LR mask.
+  bool ModifiesLR = false;
   for (const MachineOperand &MO : MI.explicit_operands())
-    if (MO.isReg() && MO.isDef() && MO.getReg() == AArch64::LR)
-      return 8;
+    if (MO.isReg() && MO.isDef() && MO.getReg() == AArch64::LR) {
+      ModifiesLR = true;
+      break;
+    }
+
+  // Memory accesses expand to a base-register guard plus the rewritten access
+  // (8 bytes), with an extra base-register update for pre/post-index forms (12
+  // bytes total). If the access also defines LR, an LR mask is appended (+4
+  // bytes). Depending on additional optimizations that the rewriter performs,
+  // this may be an overestimate.
+  if (MI.mayLoadOrStore()) {
+    unsigned Size = isLFIPrePostMemAccess(MI.getOpcode()) ? 12 : 8;
+    if (ModifiesLR)
+      Size += 4;
+    return Size;
+  }
+
+  // Non-memory instructions that modify LR expand to 2 instructions.
+  if (ModifiesLR)
+    return 8;
 
   // Default case: instructions that don't cause expansion.
   // - TP accesses in LFI are a single load/store, so no expansion.
diff --git a/llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp b/llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp
index 6066ddb7e59a9..6a81a59341c0a 100644
--- a/llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp
+++ b/llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp
@@ -17,6 +17,8 @@
 #include "Utils/AArch64BaseInfo.h"
 
 #include "llvm/MC/MCInst.h"
+#include "llvm/MC/MCInstrDesc.h"
+#include "llvm/MC/MCInstrInfo.h"
 #include "llvm/MC/MCStreamer.h"
 #include "llvm/MC/MCSubtargetInfo.h"
 
@@ -64,6 +66,97 @@ static bool isPrivilegedTPAccess(const MCInst &Inst) {
   return false;
 }
 
+// Instructions that have mayLoad/mayStore set in TableGen but don't actually
+// perform memory accesses.
+static bool isNotMemAccess(const MCInst &Inst) {
+  switch (Inst.getOpcode()) {
+  case AArch64::DMB:
+  case AArch64::DSB:
+  case AArch64::ISB:
+  case AArch64::HINT:
+    return true;
+  default:
+    return false;
+  }
+}
+
+static bool mayPrefetch(const MCInst &Inst) {
+  switch (Inst.getOpcode()) {
+  case AArch64::PRFMl:
+  case AArch64::PRFMroW:
+  case AArch64::PRFMroX:
+  case AArch64::PRFMui:
+  case AArch64::PRFUMi:
+    return true;
+  default:
+    return false;
+  }
+}
+
+// User-mode DC/IC instructions that take a virtual address operand. Encoded as
+// SYSxt with op1=3, Cn=7, op2=1 where the Cm field selects the operation.
+static bool isVASysOp(const MCInst &Inst) {
+  if (Inst.getOpcode() != AArch64::SYSxt)
+    return false;
+  if (Inst.getOperand(0).getImm() != 3 || Inst.getOperand(1).getImm() != 7 ||
+      Inst.getOperand(3).getImm() != 1)
+    return false;
+  switch (Inst.getOperand(2).getImm()) {
+  case 4:  // DC ZVA
+  case 5:  // IC IVAU
+  case 10: // DC CVAC
+  case 11: // DC CVAU
+  case 12: // DC CVAP
+  case 13: // DC CVADP
+  case 14: // DC CIVAC
+    return true;
+  default:
+    return false;
+  }
+}
+
+static MCInst replaceRegAt(const MCInst &Inst, unsigned Idx,
+                           MCRegister NewReg) {
+  MCInst New;
+  New.setOpcode(Inst.getOpcode());
+  New.setLoc(Inst.getLoc());
+  for (unsigned I = 0, E = Inst.getNumOperands(); I != E; ++I) {
+    if (I == Idx)
+      New.addOperand(MCOperand::createReg(NewReg));
+    else
+      New.addOperand(Inst.getOperand(I));
+  }
+  return New;
+}
+
+// Memory instruction information for the basic load/store rewriting path.
+// Provides operand indices for the base register and offset, and whether
+// the instruction uses pre/post-indexed addressing.
+struct MemInstInfo {
+  int BaseRegIdx;
+  int OffsetIdx; // -1 if no offset operand.
+  bool IsPrePost;
+  bool IsLiteral;
+};
+
+// Returns memory instruction info for a given opcode, or std::nullopt if
+// the opcode is not a recognized memory instruction.
+static std::optional<MemInstInfo> getMemInstInfo(unsigned Op);
+
+static unsigned convertUiToRoW(unsigned Op);
+static unsigned convertPreToRoW(unsigned Op);
+static unsigned convertPostToRoW(unsigned Op);
+static unsigned convertRoXToRoW(unsigned Op, unsigned &Shift);
+static bool getRoWShift(unsigned Op, unsigned &Shift);
+static unsigned getPrePostScale(unsigned Op);
+static unsigned convertPrePostToBase(unsigned Op, bool &IsPre,
+                                     bool &IsNoOffset);
+static int getSIMDNaturalOffset(unsigned Op);
+
+bool AArch64MCLFIRewriter::mayModifyStack(const MCInst &Inst) const {
+  return mayModifyRegister(Inst, AArch64::SP);
+}
+
 bool AArch64MCLFIRewriter::mayModifyReserved(const MCInst &Inst) const {
   return mayModifyRegister(Inst, LFIAddrReg) ||
          mayModifyRegister(Inst, LFIBaseReg) ||
@@ -111,6 +204,75 @@ void AArch64MCLFIRewriter::emitMov(MCRegister Dest, MCRegister Src,
   emitInst(Inst, Out, STI);
 }
 
+void AArch64MCLFIRewriter::emitAddImm(MCRegister Dest, MCRegister Src,
+                                      int64_t Imm, MCStreamer &Out,
+                                      const MCSubtargetInfo &STI) {
+  assert(std::abs(Imm) <= 4095);
+  MCInst Inst;
+  if (Imm >= 0) {
+    Inst.setOpcode(AArch64::ADDXri);
+    Inst.addOperand(MCOperand::createReg(Dest));
+    Inst.addOperand(MCOperand::createReg(Src));
+    Inst.addOperand(MCOperand::createImm(Imm));
+    Inst.addOperand(MCOperand::createImm(0)); // shift
+  } else {
+    Inst.setOpcode(AArch64::SUBXri);
+    Inst.addOperand(MCOperand::createReg(Dest));
+    Inst.addOperand(MCOperand::createReg(Src));
+    Inst.addOperand(MCOperand::createImm(-Imm));
+    Inst.addOperand(MCOperand::createImm(0)); // shift
+  }
+  emitInst(Inst, Out, STI);
+}
+
+void AArch64MCLFIRewriter::emitAddReg(MCRegister Dest, MCRegister Src1,
+                                      MCRegister Src2, unsigned Shift,
+                                      MCStreamer &Out,
+                                      const MCSubtargetInfo &STI) {
+  // add Dest, Src1, Src2, lsl #Shift
+  MCInst Inst;
+  Inst.setOpcode(AArch64::ADDXrs);
+  Inst.addOperand(MCOperand::createReg(Dest));
+  Inst.addOperand(MCOperand::createReg(Src1));
+  Inst.addOperand(MCOperand::createReg(Src2));
+  Inst.addOperand(
+      MCOperand::createImm(AArch64_AM::getShifterImm(AArch64_AM::LSL, Shift)));
+  emitInst(Inst, Out, STI);
+}
+
+void AArch64MCLFIRewriter::emitAddRegExtend(MCRegister Dest, MCRegister Src1,
+                                            MCRegister Src2,
+                                            AArch64_AM::ShiftExtendType ExtType,
+                                            unsigned Shift, MCStreamer &Out,
+                                            const MCSubtargetInfo &STI) {
+  // add Dest, Src1, Src2, ExtType #Shift
+  MCInst Inst;
+  if (ExtType == AArch64_AM::SXTX || ExtType == AArch64_AM::UXTX)
+    Inst.setOpcode(AArch64::ADDXrx64);
+  else
+    Inst.setOpcode(AArch64::ADDXrx);
+  Inst.addOperand(MCOperand::createReg(Dest));
+  Inst.addOperand(MCOperand::createReg(Src1));
+  Inst.addOperand(MCOperand::createReg(Src2));
+  Inst.addOperand(
+      MCOperand::createImm(AArch64_AM::getArithExtendImm(ExtType, Shift)));
+  emitInst(Inst, Out, STI);
+}
+
+void AArch64MCLFIRewriter::emitMemRoW(unsigned Opcode, const MCOperand &DataOp,
+                                      MCRegister BaseReg, MCStreamer &Out,
+                                      const MCSubtargetInfo &STI) {
+  // Op DataOp, [LFIBaseReg, W(BaseReg), uxtw]
+  MCInst Inst;
+  Inst.setOpcode(Opcode);
+  Inst.addOperand(DataOp);
+  Inst.addOperand(MCOperand::createReg(LFIBaseReg));
+  Inst.addOperand(MCOperand::createReg(getWRegFromXReg(BaseReg)));
+  Inst.addOperand(MCOperand::createImm(0)); // S bit = 0 (UXTW).
+  Inst.addOperand(MCOperand::createImm(0)); // Shift amount = 0 (unscaled).
+  emitInst(Inst, Out, STI);
+}
+
 // {br,blr} xN
 // ->
 // add x28, x27, wN, uxtw
@@ -151,7 +313,11 @@ void AArch64MCLFIRewriter::rewriteReturn(const MCInst &Inst, MCStreamer &Out,
 void AArch64MCLFIRewriter::rewriteLRModification(const MCInst &Inst,
                                                  MCStreamer &Out,
                                                  const MCSubtargetInfo &STI) {
-  emitInst(Inst, Out, STI);
+  if (!isNotMemAccess(Inst) &&
+      (mayLoad(Inst) || mayStore(Inst) || mayPrefetch(Inst)))
+    rewriteLoadStore(Inst, Out, STI);
+  else
+    emitInst(Inst, Out, STI);
   emitAddMask(AArch64::LR, AArch64::LR, Out, STI);
 }
 
@@ -211,6 +377,254 @@ void AArch64MCLFIRewriter::rewriteTPWrite(const MCInst &Inst, MCStreamer &Out,
   emitInst(Store, Out, STI);
 }
 
+bool AArch64MCLFIRewriter::rewriteLoadStoreRoW(const MCInst &Inst,
+                                               MCStreamer &Out,
+                                               const MCSubtargetInfo &STI) {
+  unsigned Op = Inst.getOpcode();
+  unsigned MemOp;
+
+  // Case 1: Indexed load/store with zero immediate offset.
+  // ldr xN, [xM, #0] -> ldr xN, [x27, wM, uxtw]
+  if ((MemOp = convertUiToRoW(Op)) != AArch64::INSTRUCTION_LIST_END) {
+    MCRegister BaseReg = Inst.getOperand(1).getReg();
+    if (BaseReg == AArch64::SP)
+      return false;
+    const MCOperand &OffsetOp = Inst.getOperand(2);
+    if (OffsetOp.isImm() && OffsetOp.getImm() == 0) {
+      emitMemRoW(MemOp, Inst.getOperand(0), BaseReg, Out, STI);
+      return true;
+    }
+    return false;
+  }
+
+  // Case 2: Pre-index load/store.
+  // ldr xN, [xM, #imm]! -> add xM, xM, #imm; ldr xN, [x27, wM, uxtw]
+  if ((MemOp = convertPreToRoW(Op)) != AArch64::INSTRUCTION_LIST_END) {
+    MCRegister BaseReg = Inst.getOperand(2).getReg();
+    if (BaseReg == AArch64::SP)
+      return false;
+    int64_t Imm = Inst.getOperand(3).getImm();
+    emitAddImm(BaseReg, BaseReg, Imm, Out, STI);
+    emitMemRoW(MemOp, Inst.getOperand(1), BaseReg, Out, STI);
+    return true;
+  }
+
+  // Case 3: Post-index load/store.
+  // ldr xN, [xM], #imm -> ldr xN, [x27, wM, uxtw]; add xM, xM, #imm
+  if ((MemOp = convertPostToRoW(Op)) != AArch64::INSTRUCTION_LIST_END) {
+    MCRegister BaseReg = Inst.getOperand(2).getReg();
+    if (BaseReg == AArch64::SP)
+      return false;
+    int64_t Imm = Inst.getOperand(3).getImm();
+    emitMemRoW(MemOp, Inst.getOperand(1), BaseReg, Out, STI);
+    emitAddImm(BaseReg, BaseReg, Imm, Out, STI);
+    return true;
+  }
+
+  // Case 4: Register-offset-X load/store.
+  // ldr xN, [xM1, xM2] -> add x26, xM1, xM2; ldr xN, [x27, w26, uxtw]
+  unsigned Shift;
+  if ((MemOp = convertRoXToRoW(Op, Shift)) != AArch64::INSTRUCTION_LIST_END) {
+    MCRegister Reg1 = Inst.getOperand(1).getReg();
+    MCRegister Reg2 = Inst.getOperand(2).getReg();
+    int64_t Extend = Inst.getOperand(3).getImm();
+    int64_t IsShift = Inst.getOperand(4).getImm();
+
+    if (!IsShift)
+      Shift = 0;
+
+    if (Extend)
+      emitAddRegExtend(LFIScratchReg, Reg1, Reg2, AArch64_AM::SXTX, Shift, Out,
+                       STI);
+    else
+      emitAddReg(LFIScratchReg, Reg1, Reg2, Shift, Out, STI);
+    emitMemRoW(MemOp, Inst.getOperand(0), LFIScratchReg, Out, STI);
+    return true;
+  }
+
+  // Case 5: Register-offset-W load/store.
+  // ldr xN, [xM1, wM2, uxtw] -> add x26, xM1, wM2, uxtw;
+  //                             ldr xN, [x27, w26, uxtw]
+  if (getRoWShift(Op, Shift)) {
+    MCRegister Reg1 = Inst.getOperand(1).getReg();
+    MCRegister Reg2 = Inst.getOperand(2).getReg();
+    int64_t S = Inst.getOperand(3).getImm();
+    int64_t IsShift = Inst.getOperand(4).getImm();
+
+    if (!IsShift)
+      Shift = 0;
+
+    if (S)
+      emitAddRegExtend(LFIScratchReg, Reg1, Reg2, AArch64_AM::SXTW, Shift, Out,
+                       STI);
+    else
+      emitAddRegExtend(LFIScratchReg, Reg1, Reg2, AArch64_AM::UXTW, Shift, Out,
+                       STI);
+    emitMemRoW(Op, Inst.getOperand(0), LFIScratchReg, Out, STI);
+    return true;
+  }
+
+  return false;
+}
+
+void AArch64MCLFIRewriter::rewriteLoadStoreBasic(const MCInst &Inst,
+                                                 MCStreamer &Out,
+                                                 const MCSubtargetInfo &STI) {
+  unsigned Opcode = Inst.getOpcode();
+  auto Info = getMemInstInfo(Opcode);
+
+  if (!Info)
+    return error(Inst, "unknown addressing mode for memory instruction in LFI");
+
+  if (Info->IsLiteral)
+    return error(Inst, "PC-relative literal loads are not supported in LFI");
+
+  MCRegister BaseReg = Inst.getOperand(Info->BaseRegIdx).getReg();
+
+  // Stack accesses don't need address sandboxing, except when sp is modified
+  // with a register post-index operand.
+  bool BaseIsSP = BaseReg == AArch64::SP;
+  if (BaseIsSP) {
+    if (Info->OffsetIdx < 0 || !Inst.getOperand(Info->OffsetIdx).isReg())
+      return emitInst(Inst, Out, STI);
+    MCRegister OffReg = Inst.getOperand(Info->OffsetIdx).getReg();
+    if (OffReg == AArch64::XZR || OffReg == AArch64::WZR)
+      return emitInst(Inst, Out, STI);
+  }
+
+  // Guard the base register, unless it is SP.
+  MCRegister AccessBase = BaseIsSP ? AArch64::SP : LFIAddrReg;
+  if (!BaseIsSP)
+    emitAddMask(LFIAddrReg, BaseReg, Out, STI);
+
+  if (Info->IsPrePost) {
+    bool IsPre = false;
+    bool IsNoOffset = false;
+    unsigned BaseOpcode = convertPrePostToBase(Opcode, IsPre, IsNoOffset);
+
+    if (BaseOpcode != AArch64::INSTRUCTION_LIST_END) {
+      // Demote pre/post-index to base indexed form.
+      MCInst NewInst;
+      NewInst.setOpcode(BaseOpcode);
+      NewInst.setLoc(Inst.getLoc());
+
+      // Skip writeback operand (operand 0) and copy data operands up to base.
+      for (int I = 1; I < Info->BaseRegIdx; ++I)
+        NewInst.addOperand(Inst.getOperand(I));
+
+      // Add the access base register (LFIAddrReg or SP).
+      NewInst.addOperand(MCOperand::createReg(AccessBase));
+
+      // For pre-index, include the offset; for post-index, use zero.
+      if (IsPre && Info->OffsetIdx >= 0)
+        NewInst.addOperand(Inst.getOperand(Info->OffsetIdx));
+      else if (!IsNoOffset)
+        NewInst.addOperand(MCOperand::createImm(0));
+
+      emitInst(NewInst, Out, STI);
+
+      // Update the base register with the offset. If the base is SP, a
+      // register offset must be sandboxed (the result is otherwise
+      // unbounded), and ADDXrs cannot take SP, so the extended-register form
+      // via the scratch register is used.
+      if (Info->OffsetIdx >= 0) {
+        const MCOperand &OffsetOp = Inst.getOperand(Info->OffsetIdx);
+        if (OffsetOp.isImm()) {
+          int64_t Scale = getPrePostScale(Opcode);
+          int64_t Offset = OffsetOp.getImm() * Scale;
+          emitAddImm(BaseReg, BaseReg, Offset, Out, STI);
+        } else if (OffsetOp.isReg()) {
+          // SIMD post-index uses a register offset (XZR for natural offset).
+          MCRegister OffReg = OffsetOp.getReg();
+          if (OffReg == AArch64::XZR) {
+            int NaturalOffset = getSIMDNaturalOffset(Opcode);
+            if (NaturalOffset > 0)
+              emitAddImm(BaseReg, BaseReg, NaturalOffset, Out, STI);
+          } else if (OffReg != AArch64::WZR) {
+            if (BaseIsSP) {
+              emitAddRegExtend(LFIScratchReg, AArch64::SP, OffReg,
+                               AArch64_AM::UXTX, 0, Out, STI);
+              emitAddMask(AArch64::SP, LFIScratchReg, Out, STI);
+            } else {
+              emitAddReg(BaseReg, BaseReg, OffReg, 0, Out, STI);
+            }
+          }
+        }
+      }
+    } else {
+      error(Inst, "unhandled pre/post-index instruction in LFI rewriter");
+    }
+  } else {
+    // Non-pre/post instruction: replace the base register operand.
+    MCInst NewInst = replaceRegAt(Inst, Info->BaseRegIdx, LFIAddrReg);
+    emitInst(NewInst, Out, STI);
+  }
+}
+
+void AArch64MCLFIRewriter::rewriteLoadStore(const MCInst &Inst, MCStreamer &Out,
+                                            const MCSubtargetInfo &STI) {
+  bool IsStore = mayStore(Inst);
+  bool IsLoad = mayLoad(Inst) || mayPrefetch(Inst);
+
+  bool SkipLoads = STI.hasFeature(AArch64::FeatureNoLFILoads);
+  bool SkipStores = STI.hasFeature(AArch64::FeatureNoLFIStores);
+
+  if ((!IsLoad || SkipLoads) && (!IsStore || SkipStores))
+    return emitInst(Inst, Out, STI);
+
+  if (rewriteLoadStoreRoW(Inst, Out, STI))
+    return;
+
+  rewriteLoadStoreBasic(Inst, Out, STI);
+}
+
+// modify sp
+// ->
+// modify x26
+// add sp, x27, w26, uxtw
+void AArch64MCLFIRewriter::rewriteStackModification(
+    const MCInst &Inst, MCStreamer &Out, const MCSubtargetInfo &STI) {
+  // Route through rewriteLRModification or rewriteLoadStore for memory
+  // accesses. Those helpers automatically handle dangerous stack modifications
+  // that can happen via register post-index.
+  if (mayLoad(Inst) || mayStore(Inst)) {
+    if (mayModifyRegister(Inst, AArch64::LR))
+      return rewriteLRModification(Inst, Out, STI);
+    return rewriteLoadStore(Inst, Out, STI);
+  }
+
+  // No stack sandboxing if sandboxing is disabled for both loads and stores.
+  bool SkipLoads = STI.hasFeature(AArch64::FeatureNoLFILoads);
+  bool SkipStores = STI.hasFeature(AArch64::FeatureNoLFIStores);
+  if (SkipLoads && SkipStores)
+    return emitInst(Inst, Out, STI);
+
+  // Redirect SP modification destination to scratch, then sandbox.
+  MCInst ModInst = replaceRegAt(Inst, 0, LFIScratchReg);
+  emitInst(ModInst, Out, STI);
+  emitAddMask(AArch64::SP, LFIScratchReg, Out, STI);
+}
+
+// {dc,ic} <op>, xN
+// ->
+// add x28, x27, wN, uxtw
+// {dc,ic} <op>, x28
+void AArch64MCLFIRewriter::rewriteVASysOp(const MCInst &Inst, MCStreamer &Out,
+      ...
[truncated]

@smithp35 smithp35 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.

I've made a start, have left a marker where I stopped. Will try and make some progress through it over the next few days.

Comment thread llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp Outdated
Comment thread llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp
Comment thread llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp
Comment thread llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp
Comment thread llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp Outdated
Comment thread llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp
Comment thread llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp
Comment thread llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp
Comment thread llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp Outdated
Comment thread llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp

@smithp35 smithp35 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.

GitHub is giving me an internal error when I try to leave more comments. It doesn't seem to affect my colleagues so there may be some odd state in its system.

A couple more comments that I can express here (hopefully these won't end up as duplicates):
Line 400
I suggest adding writeback
// Case 2: Pre-index load/store, with writeback.

Line 432
Is it possible that XM1 could be SP? All the other code paths return false in this situation.

@zyedidia

zyedidia commented May 7, 2026

Copy link
Copy Markdown
Member Author

Thanks for the review so far!

Line 432
Is it possible that XM1 could be SP? All the other code paths return false in this situation.

In this case since a general-purpose register is being added to SP (rather than a static immediate), we need a rewrite (see line 171 in mem.s for an example test case). The other code paths can rely on sp always having an address within the sandbox for safety, and guard pages on either side of the sandbox that are larger than the possible immediates will prevent accesses outside the sandbox.

@smithp35

smithp35 commented May 8, 2026

Copy link
Copy Markdown
Contributor

Thanks for the review so far!

Line 432
Is it possible that XM1 could be SP? All the other code paths return false in this situation.

In this case since a general-purpose register is being added to SP (rather than a static immediate), we need a rewrite (see line 171 in mem.s for an example test case). The other code paths can rely on sp always having an address within the sandbox for safety, and guard pages on either side of the sandbox that are larger than the possible immediates will prevent accesses outside the sandbox.

Thanks for the explanation. For the benefit of a new reader of the code, could be worth a comment. Will take a look through the remainder of the file next week.

Comment thread llvm/lib/Target/AArch64/AArch64InstrInfo.cpp Outdated
Comment thread llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp Outdated
Comment thread llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp Outdated
Comment thread llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp Outdated
Comment thread llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp Outdated
Comment thread llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp Outdated
Comment thread llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp Outdated
Comment thread llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp Outdated
Comment thread llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp
Comment thread llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp
Comment thread llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp Outdated
@zyedidia zyedidia force-pushed the llvm-pr/lfi/aarch64-rewriter-3 branch from 22badd1 to 117d9a0 Compare May 12, 2026 23:11
X(STRD, 3) \
X(STRX, 3) \
X(LDRQ, 4) \
X(STRQ, 4)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I really despise the x macros pattern; it quickly can become difficult to follow. Is there nothing smarter that can be done in tablegen to maintain these tables inside the instruction definitions themselves?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

In the original more monolithic PR for the AArch64 rewriter (#184277) I used a TSFlags-based approach to encode some of the necessary info in TableGen. See this for the relevant TableGen updates: https://github.com/llvm/llvm-project/pull/184277/changes#diff-b7c2a6d4b6cc2fac3fd43b4877e6ccd05c395aeb38c605d2e1ff862753f9ce3f. Even with that info, some large switch tables were necessary (see the bottom of https://github.com/llvm/llvm-project/pull/184277/changes#diff-026171a3feab4865a9a843b323382856be51e14125b69301a370bdde28e068dc). In general, I am seeking guidance on whether to go with the TSFlags/TableGen approach, or the switch table approach. When refactoring for this PR, I did manage to use macros to significantly reduce the switch table source code size, so I went with that since it is more self-contained vs. touching TSFlags/TableGen, but I am still unsure of which approach is better.

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.

I had two contradictory reactions to seeing the original tablegen modifications and seeing the macros. On seeing the Tablegen my initial thought was that LFI, for better or worse, would be put in front of a lot more developers. For example let's assume that there is some new load in a future architecture extension, the developer may need to interact and understand the LFI flags when making modifications. Whereas with the representation here, LFI can take its time to catch up.

On seeing the macros I had the same reaction as Nick, and thought can we put this in Tablegen!

I don't have a strong opinion one way or the other as there are trade-offs. If there were a new TableGen backend that could auto-generate the switch tables then this would likely swing it favour of TableGen. I guess that could be a refactoring afterwards.

I'll see if I can get any reaction from my colleagues internally, you may also want to ask the specific question in Discourse.

@zyedidia zyedidia May 15, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I realized after this discussion that I could use TableGen in a self-contained way, independent of the existing TableGen instruction definitions, to avoid these macros. I've updated the implementation to do that. Let me know what you think of this approach. The latest commit shows the diff: 64ae66f.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For example let's assume that there is some new load in a future architecture extension

Yeah, I guess this is the largest risk (IMO) that isn't mitigated by using tablegen or x-macros.

I was wondering if perhaps instead of hard coding values for every single aarch64 instruction we would need to rewrite, can we make the code significantly shorter and less brittle to new arch extensions by making the code more generic? Is it possible to use MCInstrDesc::{mayLoad,mayStore} to figure out if a given instruction is a load or store, then calculate the necessary values the x-macros-switch/tablegen would, such that we're not having to maintain tables at all?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@smithp35 @kbeyls maybe some folks on the ARM side can take a look at the above two approaches, in contrast with the current PR (well, stacked on top, I guess) and provide feedback? (No rush).

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.

I can take a look, although thanks to a couple of short weeks (company and country holidays) I'm a bit behind, please do give the occasional ping. I'll try and summarise the two approaches and will post them internally to see if anyone has any strong opinions.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Worst case, we do have a verifier IIUC? Perhaps we could delegate erroring on newly added-to-the-ISA loads/stores there, and proceed to land this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes that sounds good to me, the verifier is the final source of truth on whether an instruction has been rewritten properly, so even if they pass through the rewriter unmodified (a warning will be produced by the current code), the verifier will notify the user if the generated addressing mode is unsafe.

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.

I've had a chance to look through Nick's proposed TableGen alternatives.

I agree with Nick that lfi-project#6 is the better of the two alternatives. I don't think the additional changes in AArch64InstrFormats.td are significantly more complex than lfi-project#5

I found it difficult to compare 3 implementations across the size of the diffs. I think it would likely be simpler for a non LFI expert to have a [NFC] refactoring and put that to a wider audience to see if there are any objections. The feeling I get is that an overall simpler more robust solution is preferred to keeping LFI isolated.

One small comment on the TableGen, there were some classes with parameters that didn't look like they were used. There could be some inheritance that I'm missing though.

class LFILoadStoreRO<bits<2> sz, bit V> : LFIInstruction
  let LFIInst = !cast<Instruction>(NAME);
  let IsLFIMem = 1;
  let BaseIdx = 1;
  let OffsetIdx = 2;
  let HasOffset = 1;
}

// multiple switch tables without repetition. Each macro takes a callback X and
// invokes X(NAME, VALUE) for each entry.

// Scalar memory ops that have ui, pre, post, roX, and roW variants.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you expand on what "ui, pre, post, roX, and roW variants" are (in comments)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

There is a comment that explains the meaning of Ui, RoW, and RoX earlier in the file (line 147) when these functions are forward-declared. I can also add explanation of pre/post there.

@smithp35 smithp35 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.

I've got to the end of the file. I must confess to not going over all of the cases in the switch-statements.

As mentioned in the inline comment, I don't have a particularly strong opinion vs the question of whether to use tablegen, although I suspect others might. Possibly worth widening out to Discourse.

There's also the possibility of refactoring it afterwards, which may be easier to review.

Comment thread llvm/test/MC/AArch64/LFI/lse.s Outdated
Comment thread llvm/test/MC/AArch64/LFI/no-lfi-stores.s
Comment thread llvm/test/MC/AArch64/LFI/mem.s
Comment thread llvm/test/MC/AArch64/LFI/mem.s Outdated
Comment thread llvm/test/MC/AArch64/LFI/mem.s Outdated
Comment thread llvm/test/MC/AArch64/LFI/stack.s
Comment thread llvm/test/MC/AArch64/LFI/stack.s
@zyedidia zyedidia force-pushed the llvm-pr/lfi/aarch64-rewriter-3 branch from b665ebc to 64ae66f Compare May 15, 2026 03:12

@smithp35 smithp35 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.

Thanks for adding the tablegen. I think that helps a lot, definitely easier to follow than the macros.

I don't have any more significant comments at the moment. This is looking good on my side.

Comment thread llvm/lib/Target/AArch64/AArch64.td
Comment thread llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp
Comment thread llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp

@nickdesaulniers nickdesaulniers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks like this needs to be rebased before landing. Please leave this open for another week to collect any other feedback.

@smithp35

smithp35 commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

I owe some feedback on the TableGen proposals mentioned in #195167 (comment)

I don't think that should prevent this from landing, I think it could be handled as a NFC refactoring change afterwards. Hope to be able to comment on the tablegen this week.

@zyedidia zyedidia force-pushed the llvm-pr/lfi/aarch64-rewriter-3 branch from eb6e583 to f2b56f1 Compare June 2, 2026 20:10

@smithp35 smithp35 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.

I've marked this LGTM on my side.

@zyedidia

Copy link
Copy Markdown
Member Author

SGTM, I will merge this tomorrow morning.

@zyedidia zyedidia merged commit e07b47e into llvm:main Jun 10, 2026
11 checks passed
Jianhui-Li pushed a commit to Jianhui-Li/llvm-project that referenced this pull request Jun 11, 2026
This patch adds LFI rewrites for loads and stores, and guards for stack
pointer modifications.

This also introduces the `+no-lfi-stores` and `+no-lfi-loads` features
to control the granularity of memory sandboxing.

With these changes the rewriter now supports making fully
software-sandboxed programs. There are some minor changes
needed in libunwind to avoid emitting SVE/MTE instructions that
cause verification failure, which will be addressed in a future patch.

Future work includes the guard elimination optimization, which will
improve performance, Clang flags to control the LFI subtarget
features from the frontend, and that can also set a macro that
communicates the LFI feature level.
carlobertolli pushed a commit to carlobertolli/llvm-project that referenced this pull request Jun 11, 2026
This patch adds LFI rewrites for loads and stores, and guards for stack
pointer modifications.

This also introduces the `+no-lfi-stores` and `+no-lfi-loads` features
to control the granularity of memory sandboxing.

With these changes the rewriter now supports making fully
software-sandboxed programs. There are some minor changes
needed in libunwind to avoid emitting SVE/MTE instructions that
cause verification failure, which will be addressed in a future patch.

Future work includes the guard elimination optimization, which will
improve performance, Clang flags to control the LFI subtarget
features from the frontend, and that can also set a macro that
communicates the LFI feature level.
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.

4 participants