[llvm] [LFI][AArch64] Add PAC support (PR #207915)

Zachary Yedidia via llvm-commits llvm-commits at lists.llvm.org
Tue Jul 14 00:03:59 PDT 2026


https://github.com/zyedidia updated https://github.com/llvm/llvm-project/pull/207915

>From 320e12558b10a520f8e53441ec13102f4c48673e Mon Sep 17 00:00:00 2001
From: Zachary Yedidia <zyedidia at gmail.com>
Date: Tue, 7 Jul 2026 03:38:12 -0400
Subject: [PATCH 1/2] [LFI][AArch64] Add PAC support

---
 llvm/docs/LFI.rst                             |  92 +++++++++-
 llvm/include/llvm/MC/MCLFIRewriter.h          |   9 +-
 llvm/lib/MC/MCStreamer.cpp                    |   5 +-
 llvm/lib/Target/AArch64/AArch64InstrInfo.cpp  |  23 +++
 .../MCTargetDesc/AArch64MCLFIRewriter.cpp     | 172 +++++++++++++++++-
 .../MCTargetDesc/AArch64MCLFIRewriter.h       |  20 +-
 llvm/test/MC/AArch64/LFI/guard-elim.s         |   4 +-
 llvm/test/MC/AArch64/LFI/mem-lr.s             |  26 +++
 llvm/test/MC/AArch64/LFI/pac-errors.s         |  12 ++
 llvm/test/MC/AArch64/LFI/pac.s                |  53 ++++++
 llvm/test/MC/AArch64/LFI/return.s             |  43 +++++
 11 files changed, 448 insertions(+), 11 deletions(-)
 create mode 100644 llvm/test/MC/AArch64/LFI/pac-errors.s
 create mode 100644 llvm/test/MC/AArch64/LFI/pac.s

diff --git a/llvm/docs/LFI.rst b/llvm/docs/LFI.rst
index 74cfe02c76f21..b1603c2d4d136 100644
--- a/llvm/docs/LFI.rst
+++ b/llvm/docs/LFI.rst
@@ -296,8 +296,13 @@ before moving it back into ``sp`` with a safe ``add``.
 Link register modification
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-When the link register is modified, we write the modified value to a
-temporary, before loading it back into ``x30`` with a safe ``add``.
+When the link register is modified, it is guarded back into the sandbox with a
+safe ``add x30, x27, w30, uxtw``. This guard is deferred until the next
+control-flow instruction rather than emitted immediately after the
+modification. Deferral keeps a signed return address intact so that a following
+authentication instruction (such as ``autiasp``) can run before the guard,
+which would otherwise destroy the pointer authentication signature. See
+`Pointer Authentication Code (PAC) support`_.
 
 +---------------------------+-------------------------------+
 |         Original          |           Rewritten           |
@@ -317,6 +322,89 @@ temporary, before loading it back into ``x30`` with a safe ``add``.
 |                           |                               |
 +---------------------------+-------------------------------+
 
+Pointer Authentication Code (PAC) support
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+LFI is compatible with Arm Pointer Authentication Code (PAC) instructions,
+which are used to sign and authenticate ``x30`` to protect against control-flow
+hijacking.
+
+To gain the security benefit of PAC under LFI, the hardware must implement
+``FEAT_FPAC``, so that authentication failures fault immediately. Without
+``FEAT_FPAC``, a failed authentication produces a poisoned pointer, which LFI
+still keeps confined to the sandbox by masking it, but the mask overwrites the
+poison caused by the authentication failure.
+
++-------------------+------------------------------+
+|     Original      |          Rewritten           |
++-------------------+------------------------------+
+| .. code-block::   | .. code-block::              |
+|                   |                              |
+|    paciasp        |    paciasp                   |
+|                   |                              |
++-------------------+------------------------------+
+| .. code-block::   | .. code-block::              |
+|                   |                              |
+|    autiasp        |    autiasp                   |
+|    ret            |    add x30, x27, w30, uxtw   |
+|                   |    ret                       |
+|                   |                              |
++-------------------+------------------------------+
+
+Authenticated returns (``retaa``/``retab``) combine authentication with return,
+and must be expanded during rewriting.
+
++-----------------+-------------------------------+
+|    Original     |           Rewritten           |
++-----------------+-------------------------------+
+| .. code-block:: | .. code-block::               |
+|                 |                               |
+|    retaa        |    autiasp                    |
+|                 |    add x30, x27, w30, uxtw    |
+|                 |    ret                        |
+|                 |                               |
++-----------------+-------------------------------+
+| .. code-block:: | .. code-block::               |
+|                 |                               |
+|    retab        |    autibsp                    |
+|                 |    add x30, x27, w30, uxtw    |
+|                 |    ret                        |
+|                 |                               |
++-----------------+-------------------------------+
+
+Authenticated branches (``braa``/``brab``/``braaz``/``brabz``) and calls
+(``blraa``/``blrab``/``blraaz``/``blrabz``) combine authentication with an
+indirect branch or call. They are expanded by first authenticating the target
+register in place, then performing a normal sandboxed branch or call.
+
++-------------------+-------------------------------+
+|     Original      |           Rewritten           |
++-------------------+-------------------------------+
+| .. code-block::   | .. code-block::               |
+|                   |                               |
+|    braa xN, xM    |    autia xN, xM               |
+|                   |    add x28, x27, wN, uxtw     |
+|                   |    br x28                     |
+|                   |                               |
++-------------------+-------------------------------+
+| .. code-block::   | .. code-block::               |
+|                   |                               |
+|    braaz xN       |    autiza xN                  |
+|                   |    add x28, x27, wN, uxtw     |
+|                   |    br x28                     |
+|                   |                               |
++-------------------+-------------------------------+
+| .. code-block::   | .. code-block::               |
+|                   |                               |
+|    blraa xN, xM   |    autia xN, xM               |
+|                   |    add x28, x27, wN, uxtw     |
+|                   |    blr x28                    |
+|                   |                               |
++-------------------+-------------------------------+
+
+Authenticated exception returns (``eret``/``eretaa``/``eretab``) are privileged
+and are not supported: the rewriter reports an error for them.
+
 System instructions
 ~~~~~~~~~~~~~~~~~~~
 
diff --git a/llvm/include/llvm/MC/MCLFIRewriter.h b/llvm/include/llvm/MC/MCLFIRewriter.h
index 33d51f29ce99e..82a57ef54ce6c 100644
--- a/llvm/include/llvm/MC/MCLFIRewriter.h
+++ b/llvm/include/llvm/MC/MCLFIRewriter.h
@@ -64,8 +64,13 @@ class MCLFIRewriter {
                            const MCSubtargetInfo &STI) = 0;
 
   // Called when a label is emitted. Used for optimizations that require
-  // information about jump targets, such as guard elimination.
-  virtual void onLabel(const MCSymbol *Symbol) {}
+  // information about jump targets, such as guard elimination, and to flush
+  // any rewriter state that must not cross a potential branch target.
+  virtual void onLabel(const MCSymbol *Symbol, MCStreamer &Out) {}
+
+  // Called when the stream is finalized. Used to flush any pending rewriter
+  // state before the stream ends.
+  virtual void finish(MCStreamer &Out) {}
 };
 
 } // namespace llvm
diff --git a/llvm/lib/MC/MCStreamer.cpp b/llvm/lib/MC/MCStreamer.cpp
index 1cd94715b2592..e8e88ce7b83c9 100644
--- a/llvm/lib/MC/MCStreamer.cpp
+++ b/llvm/lib/MC/MCStreamer.cpp
@@ -413,7 +413,7 @@ void MCStreamer::emitLabel(MCSymbol *Symbol, SMLoc Loc) {
   Symbol->setFragment(&getCurrentSectionOnly()->getDummyFragment());
 
   if (LFIRewriter)
-    LFIRewriter->onLabel(Symbol);
+    LFIRewriter->onLabel(Symbol, *this);
 
   MCTargetStreamer *TS = getTargetStreamer();
   if (TS)
@@ -1283,6 +1283,9 @@ void MCStreamer::finish(SMLoc EndLoc) {
     return;
   }
 
+  if (LFIRewriter)
+    LFIRewriter->finish(*this);
+
   MCTargetStreamer *TS = getTargetStreamer();
   if (TS)
     TS->finish();
diff --git a/llvm/lib/Target/AArch64/AArch64InstrInfo.cpp b/llvm/lib/Target/AArch64/AArch64InstrInfo.cpp
index b2ec50ee3835c..d05bc4d3b3d28 100644
--- a/llvm/lib/Target/AArch64/AArch64InstrInfo.cpp
+++ b/llvm/lib/Target/AArch64/AArch64InstrInfo.cpp
@@ -130,6 +130,29 @@ static std::optional<unsigned> getLFIInstSizeInBytes(const MachineInstr &MI) {
     if (MI.getOperand(0).getReg() != AArch64::LR)
       return 8;
     return 4;
+  case AArch64::RETAA:
+  case AArch64::RETAB:
+    // Authenticated returns expand to 3 instructions (authenticate + guard +
+    // ret).
+    return 12;
+  case AArch64::BRAA:
+  case AArch64::BRAAZ:
+  case AArch64::BRAB:
+  case AArch64::BRABZ:
+  case AArch64::BLRAA:
+  case AArch64::BLRAAZ:
+  case AArch64::BLRAB:
+  case AArch64::BLRABZ:
+    // Authenticated branches/calls expand to 3 instructions (authenticate +
+    // guard + branch).
+    return 12;
+  case AArch64::AUTIASP:
+  case AArch64::AUTIBSP:
+  case AArch64::AUTIAZ:
+  case AArch64::AUTIBZ:
+  case AArch64::XPACLRI:
+    // Authenticating LR expands to the instruction plus a deferred LR guard.
+    return 8;
   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 &&
diff --git a/llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp b/llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp
index 29f2ace01ade3..3899fba5f3a87 100644
--- a/llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp
+++ b/llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp
@@ -156,6 +156,52 @@ static bool mayPrefetch(const MCInst &Inst) {
   }
 }
 
+static bool isAuthenticatedBranch(unsigned Opcode) {
+  switch (Opcode) {
+  case AArch64::BRAA:
+  case AArch64::BRAAZ:
+  case AArch64::BRAB:
+  case AArch64::BRABZ:
+    return true;
+  default:
+    return false;
+  }
+}
+
+static bool isAuthenticatedCall(unsigned Opcode) {
+  switch (Opcode) {
+  case AArch64::BLRAA:
+  case AArch64::BLRAAZ:
+  case AArch64::BLRAB:
+  case AArch64::BLRABZ:
+    return true;
+  default:
+    return false;
+  }
+}
+
+static bool isAuthenticatedReturn(unsigned Opcode) {
+  return Opcode == AArch64::RETAA || Opcode == AArch64::RETAB;
+}
+
+static bool isExceptionReturn(unsigned Opcode) {
+  return Opcode == AArch64::ERET || Opcode == AArch64::ERETAA ||
+         Opcode == AArch64::ERETAB;
+}
+
+static bool authenticatesLR(const MCInst &Inst) {
+  switch (Inst.getOpcode()) {
+  case AArch64::AUTIASP:
+  case AArch64::AUTIBSP:
+  case AArch64::AUTIAZ:
+  case AArch64::AUTIBZ:
+  case AArch64::XPACLRI:
+    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) {
@@ -251,11 +297,26 @@ MCRegister AArch64MCLFIRewriter::mayModifyReserved(const MCInst &Inst) const {
   return {};
 }
 
-void AArch64MCLFIRewriter::onLabel(const MCSymbol *) {
+void AArch64MCLFIRewriter::onLabel(const MCSymbol *, MCStreamer &Out) {
+  // Flush a deferred LR guard before the label, since the label is a potential
+  // branch target and code reached through it may use LR for control flow.
+  if (DeferredLRGuard && LastSTI) {
+    emitAddMask(AArch64::LR, AArch64::LR, Out, *LastSTI);
+    DeferredLRGuard = false;
+  }
+
   // Invalidate guard state since the label is a potential branch target.
   ActiveGuardReg = std::nullopt;
 }
 
+void AArch64MCLFIRewriter::finish(MCStreamer &Out) {
+  // Flush a deferred LR guard at the end of the stream.
+  if (DeferredLRGuard && LastSTI) {
+    emitAddMask(AArch64::LR, AArch64::LR, Out, *LastSTI);
+    DeferredLRGuard = false;
+  }
+}
+
 void AArch64MCLFIRewriter::emitInst(const MCInst &Inst, MCStreamer &Out,
                                     const MCSubtargetInfo &STI) {
   // Invalidate the active guard if this instruction modifies the guarded
@@ -438,7 +499,7 @@ void AArch64MCLFIRewriter::rewriteReturn(const MCInst &Inst, MCStreamer &Out,
 // modify x30
 // ->
 // modify x30
-// add x30, x27, w30, uxtw
+// add x30, x27, w30, uxtw (deferred)
 void AArch64MCLFIRewriter::rewriteLRModification(const MCInst &Inst,
                                                  MCStreamer &Out,
                                                  const MCSubtargetInfo &STI) {
@@ -447,7 +508,77 @@ void AArch64MCLFIRewriter::rewriteLRModification(const MCInst &Inst,
     rewriteLoadStore(Inst, Out, STI);
   else
     emitInst(Inst, Out, STI);
+
+  // Defer the LR guard until the next control-flow instruction or label. This
+  // keeps a signed return address intact so that an authentication instruction
+  // can run before the mask destroys the PAC bits.
+  DeferredLRGuard = true;
+}
+
+// retaa / retab
+// ->
+// autiasp / autibsp
+// add x30, x27, w30, uxtw
+// ret
+void AArch64MCLFIRewriter::rewriteAuthenticatedReturn(
+    const MCInst &Inst, MCStreamer &Out, const MCSubtargetInfo &STI) {
+  MCInst Auth;
+  Auth.setOpcode(Inst.getOpcode() == AArch64::RETAA ? AArch64::AUTIASP
+                                                    : AArch64::AUTIBSP);
+  emitInst(Auth, Out, STI);
+
   emitAddMask(AArch64::LR, AArch64::LR, Out, STI);
+  emitBranch(AArch64::RET, AArch64::LR, Out, STI);
+}
+
+// {braa,brab,braaz,brabz} xN[, xM]  (blra* for calls)
+// ->
+// {autia,autib,autiza,autizb} xN[, xM]
+// add x28, x27, wN, uxtw
+// {br,blr} x28
+void AArch64MCLFIRewriter::rewriteAuthenticatedBranchOrCall(
+    const MCInst &Inst, unsigned BranchOpcode, MCStreamer &Out,
+    const MCSubtargetInfo &STI) {
+  MCRegister TargetReg = Inst.getOperand(0).getReg();
+
+  // Authenticate the target in place. The zero-modifier variants
+  // (braaz/brabz/blraaz/blrabz) have no modifier operand.
+  MCInst Auth;
+  switch (Inst.getOpcode()) {
+  case AArch64::BRAA:
+  case AArch64::BLRAA:
+    Auth.setOpcode(AArch64::AUTIA);
+    Auth.addOperand(MCOperand::createReg(TargetReg)); // dst
+    Auth.addOperand(MCOperand::createReg(TargetReg)); // src (tied to dst)
+    Auth.addOperand(Inst.getOperand(1));              // modifier
+    break;
+  case AArch64::BRAB:
+  case AArch64::BLRAB:
+    Auth.setOpcode(AArch64::AUTIB);
+    Auth.addOperand(MCOperand::createReg(TargetReg)); // dst
+    Auth.addOperand(MCOperand::createReg(TargetReg)); // src (tied to dst)
+    Auth.addOperand(Inst.getOperand(1));              // modifier
+    break;
+  case AArch64::BRAAZ:
+  case AArch64::BLRAAZ:
+    Auth.setOpcode(AArch64::AUTIZA);
+    Auth.addOperand(MCOperand::createReg(TargetReg)); // dst
+    Auth.addOperand(MCOperand::createReg(TargetReg)); // src (tied to dst)
+    break;
+  case AArch64::BRABZ:
+  case AArch64::BLRABZ:
+    Auth.setOpcode(AArch64::AUTIZB);
+    Auth.addOperand(MCOperand::createReg(TargetReg)); // dst
+    Auth.addOperand(MCOperand::createReg(TargetReg)); // src (tied to dst)
+    break;
+  default:
+    llvm_unreachable("unexpected authenticated branch/call opcode");
+  }
+  emitInst(Auth, Out, STI);
+
+  // Guard the authenticated target and branch/call through x28.
+  emitAddMask(LFIAddrReg, TargetReg, Out, STI);
+  emitBranch(BranchOpcode, LFIAddrReg, Out, STI);
 }
 
 // svc #0
@@ -806,6 +937,33 @@ void AArch64MCLFIRewriter::doRewriteInst(const MCInst &Inst, MCStreamer &Out,
   if (isVASysOp(Inst))
     return rewriteVASysOp(Inst, Out, STI);
 
+  if (isExceptionReturn(Inst.getOpcode())) {
+    error(Inst, "exception returns are not supported by LFI");
+    return;
+  }
+
+  // PAC authenticated returns expand to authenticate + guarded RET. The
+  // expansion emits its own LR guard, so discard any deferred guard: masking
+  // before the authentication would corrupt the signed return address.
+  if (isAuthenticatedReturn(Inst.getOpcode())) {
+    DeferredLRGuard = false;
+    return rewriteAuthenticatedReturn(Inst, Out, STI);
+  }
+
+  // Flush a deferred LR guard before any control-flow instruction, so that a
+  // modified LR is sandboxed before it can be used to transfer control.
+  if (DeferredLRGuard && (isReturn(Inst) || isIndirectBranch(Inst) ||
+                          isCall(Inst) || isBranch(Inst))) {
+    emitAddMask(AArch64::LR, AArch64::LR, Out, STI);
+    DeferredLRGuard = false;
+  }
+
+  // PAC authenticated branches/calls expand to authenticate + guarded branch.
+  if (isAuthenticatedBranch(Inst.getOpcode()))
+    return rewriteAuthenticatedBranchOrCall(Inst, AArch64::BR, Out, STI);
+  if (isAuthenticatedCall(Inst.getOpcode()))
+    return rewriteAuthenticatedBranchOrCall(Inst, AArch64::BLR, Out, STI);
+
   // Control flow.
   switch (Inst.getOpcode()) {
   case AArch64::RET:
@@ -819,8 +977,10 @@ void AArch64MCLFIRewriter::doRewriteInst(const MCInst &Inst, MCStreamer &Out,
   if (mayModifySP(Inst))
     return rewriteSPModification(Inst, Out, STI);
 
-  // Link register modification.
-  if (explicitlyModifiesRegister(Inst, AArch64::LR))
+  // Link register modification. This covers explicit writes to x30 as well as
+  // PAC instructions that authenticate LR in place (autiasp, ...), which
+  // define LR implicitly.
+  if (explicitlyModifiesRegister(Inst, AArch64::LR) || authenticatesLR(Inst))
     return rewriteLRModification(Inst, Out, STI);
 
   // Memory access.
@@ -859,6 +1019,10 @@ bool AArch64MCLFIRewriter::rewriteInst(const MCInst &Inst, MCStreamer &Out,
     return false;
   Guard = true;
 
+  // Record the subtarget so a deferred LR guard can be emitted from
+  // onLabel/finish, which are not given an MCSubtargetInfo.
+  LastSTI = &STI;
+
   doRewriteInst(Inst, Out, STI);
 
   Guard = false;
diff --git a/llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.h b/llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.h
index b21c381393cfd..d7a70b0e163a5 100644
--- a/llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.h
+++ b/llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.h
@@ -52,12 +52,23 @@ class AArch64MCLFIRewriter : public MCLFIRewriter {
   bool rewriteInst(const MCInst &Inst, MCStreamer &Out,
                    const MCSubtargetInfo &STI) override;
 
-  void onLabel(const MCSymbol *Symbol) override;
+  void onLabel(const MCSymbol *Symbol, MCStreamer &Out) override;
+  void finish(MCStreamer &Out) override;
 
 private:
   /// Recursion guard to prevent infinite loops when emitting instructions.
   bool Guard = false;
 
+  /// When set, an instruction has modified the link register but its guard
+  /// (`add x30, x27, w30, uxtw`) has not been emitted yet. The guard is
+  /// deferred until the next control-flow instruction so that pointer
+  /// authentication can run on the signed value before the mask overwrites the
+  /// upper bits of the pointer.
+  bool DeferredLRGuard = false;
+
+  /// Most recently seen MCSubtargetInfo.
+  const MCSubtargetInfo *LastSTI = nullptr;
+
   /// Deferred `.tlsdesccall` symbol. The directive attaches a
   /// R_AARCH64_TLSDESC_CALL relocation to the following BLR. Since LFI inserts
   /// a guard before that BLR, the marker is deferred and re-emitted between
@@ -120,6 +131,13 @@ class AArch64MCLFIRewriter : public MCLFIRewriter {
   void rewriteLRModification(const MCInst &Inst, MCStreamer &Out,
                              const MCSubtargetInfo &STI);
 
+  // PAC instructions.
+  void rewriteAuthenticatedReturn(const MCInst &Inst, MCStreamer &Out,
+                                  const MCSubtargetInfo &STI);
+  void rewriteAuthenticatedBranchOrCall(const MCInst &Inst,
+                                        unsigned BranchOpcode, MCStreamer &Out,
+                                        const MCSubtargetInfo &STI);
+
   // System instructions.
   void rewriteSyscall(const MCInst &Inst, MCStreamer &Out,
                       const MCSubtargetInfo &STI);
diff --git a/llvm/test/MC/AArch64/LFI/guard-elim.s b/llvm/test/MC/AArch64/LFI/guard-elim.s
index 8a684027f4180..f4c5c5869fba2 100644
--- a/llvm/test/MC/AArch64/LFI/guard-elim.s
+++ b/llvm/test/MC/AArch64/LFI/guard-elim.s
@@ -153,12 +153,14 @@ lr_mask_between:
 ldr x0, [x12, #8]
 ldr x30, [x12, #16]
 ldr x1, [x12, #24]
+ret
 // CHECK-LABEL: lr_mask_between:
 // CHECK-NEXT: add x28, x27, w12, uxtw
 // CHECK-NEXT: ldr x0, [x28, #8]
 // CHECK-NEXT: ldr x30, [x28, #16]
-// CHECK-NEXT: add x30, x27, w30, uxtw
 // CHECK-NEXT: ldr x1, [x28, #24]
+// CHECK-NEXT: add x30, x27, w30, uxtw
+// CHECK-NEXT: ret
 
 // A .lfi_rewrite_disable region invalidates the guard, since the instructions
 // inside it bypass the rewriter and may modify the base register or x28.
diff --git a/llvm/test/MC/AArch64/LFI/mem-lr.s b/llvm/test/MC/AArch64/LFI/mem-lr.s
index e9b53131042f6..3ae87090b9f73 100644
--- a/llvm/test/MC/AArch64/LFI/mem-lr.s
+++ b/llvm/test/MC/AArch64/LFI/mem-lr.s
@@ -4,68 +4,94 @@
 // in addition to masking LR after the access.
 
 ldr x30, [x0, #0x100]
+ret
 // CHECK:      add x28, x27, w0, uxtw
 // CHECK-NEXT: ldr x30, [x28, #256]
 // CHECK-NEXT: add x30, x27, w30, uxtw
+// CHECK-NEXT: ret
 
 ldr x30, [x0]
+ret
 // CHECK:      ldr x30, [x27, w0, uxtw]
 // CHECK-NEXT: add x30, x27, w30, uxtw
+// CHECK-NEXT: ret
 
 ldr x30, [x0, #8]!
+ret
 // CHECK:      add x0, x0, #8
 // CHECK-NEXT: ldr x30, [x27, w0, uxtw]
 // CHECK-NEXT: add x30, x27, w30, uxtw
+// CHECK-NEXT: ret
 
 ldr x30, [x0, #-8]!
+ret
 // CHECK:      sub x0, x0, #8
 // CHECK-NEXT: ldr x30, [x27, w0, uxtw]
 // CHECK-NEXT: add x30, x27, w30, uxtw
+// CHECK-NEXT: ret
 
 ldr x30, [x0], #8
+ret
 // CHECK:      ldr x30, [x27, w0, uxtw]
 // CHECK-NEXT: add x0, x0, #8
 // CHECK-NEXT: add x30, x27, w30, uxtw
+// CHECK-NEXT: ret
 
 ldr x30, [x0, x1]
+ret
 // CHECK:      add x26, x0, x1
 // CHECK-NEXT: ldr x30, [x27, w26, uxtw]
 // CHECK-NEXT: add x30, x27, w30, uxtw
+// CHECK-NEXT: ret
 
 ldr x30, [x0, x1, lsl #3]
+ret
 // CHECK:      add x26, x0, x1, lsl #3
 // CHECK-NEXT: ldr x30, [x27, w26, uxtw]
 // CHECK-NEXT: add x30, x27, w30, uxtw
+// CHECK-NEXT: ret
 
 ldur x30, [x0, #4]
+ret
 // CHECK:      add x28, x27, w0, uxtw
 // CHECK-NEXT: ldur x30, [x28, #4]
 // CHECK-NEXT: add x30, x27, w30, uxtw
+// CHECK-NEXT: ret
 
 ldp x29, x30, [x0]
+ret
 // CHECK:      add x28, x27, w0, uxtw
 // CHECK-NEXT: ldp x29, x30, [x28]
 // CHECK-NEXT: add x30, x27, w30, uxtw
+// CHECK-NEXT: ret
 
 ldp x29, x30, [x0, #16]
+ret
 // CHECK:      add x28, x27, w0, uxtw
 // CHECK-NEXT: ldp x29, x30, [x28, #16]
 // CHECK-NEXT: add x30, x27, w30, uxtw
+// CHECK-NEXT: ret
 
 ldp x29, x30, [x0, #16]!
+ret
 // CHECK:      add x28, x27, w0, uxtw
 // CHECK-NEXT: ldp x29, x30, [x28, #16]
 // CHECK-NEXT: add x0, x0, #16
 // CHECK-NEXT: add x30, x27, w30, uxtw
+// CHECK-NEXT: ret
 
 ldp x29, x30, [x0], #16
+ret
 // CHECK:      add x28, x27, w0, uxtw
 // CHECK-NEXT: ldp x29, x30, [x28]
 // CHECK-NEXT: add x0, x0, #16
 // CHECK-NEXT: add x30, x27, w30, uxtw
+// CHECK-NEXT: ret
 
 // SP-based LR loads are safe without base sandboxing.
 
 ldr x30, [sp, #16]
+ret
 // CHECK:      ldr x30, [sp, #16]
 // CHECK-NEXT: add x30, x27, w30, uxtw
+// CHECK-NEXT: ret
diff --git a/llvm/test/MC/AArch64/LFI/pac-errors.s b/llvm/test/MC/AArch64/LFI/pac-errors.s
new file mode 100644
index 0000000000000..9259c034ec794
--- /dev/null
+++ b/llvm/test/MC/AArch64/LFI/pac-errors.s
@@ -0,0 +1,12 @@
+// RUN: not llvm-mc -triple aarch64_lfi %s 2>&1 | FileCheck %s
+
+.arch_extension pauth
+
+eret
+// CHECK: error: exception returns are not supported by LFI
+
+eretaa
+// CHECK: error: exception returns are not supported by LFI
+
+eretab
+// CHECK: error: exception returns are not supported by LFI
diff --git a/llvm/test/MC/AArch64/LFI/pac.s b/llvm/test/MC/AArch64/LFI/pac.s
new file mode 100644
index 0000000000000..861b09ed62ef9
--- /dev/null
+++ b/llvm/test/MC/AArch64/LFI/pac.s
@@ -0,0 +1,53 @@
+// RUN: llvm-mc -triple aarch64_lfi %s | FileCheck %s
+
+.arch_extension pauth
+
+retaa
+// CHECK:      autiasp
+// CHECK-NEXT: add x30, x27, w30, uxtw
+// CHECK-NEXT: ret
+
+retab
+// CHECK:      autibsp
+// CHECK-NEXT: add x30, x27, w30, uxtw
+// CHECK-NEXT: ret
+
+braa x0, x1
+// CHECK:      autia x0, x1
+// CHECK-NEXT: add x28, x27, w0, uxtw
+// CHECK-NEXT: br x28
+
+braaz x2
+// CHECK:      autiza x2
+// CHECK-NEXT: add x28, x27, w2, uxtw
+// CHECK-NEXT: br x28
+
+brab x3, x4
+// CHECK:      autib x3, x4
+// CHECK-NEXT: add x28, x27, w3, uxtw
+// CHECK-NEXT: br x28
+
+brabz x5
+// CHECK:      autizb x5
+// CHECK-NEXT: add x28, x27, w5, uxtw
+// CHECK-NEXT: br x28
+
+blraa x0, x1
+// CHECK:      autia x0, x1
+// CHECK-NEXT: add x28, x27, w0, uxtw
+// CHECK-NEXT: blr x28
+
+blraaz x2
+// CHECK:      autiza x2
+// CHECK-NEXT: add x28, x27, w2, uxtw
+// CHECK-NEXT: blr x28
+
+blrab x3, x4
+// CHECK:      autib x3, x4
+// CHECK-NEXT: add x28, x27, w3, uxtw
+// CHECK-NEXT: blr x28
+
+blrabz x5
+// CHECK:      autizb x5
+// CHECK-NEXT: add x28, x27, w5, uxtw
+// CHECK-NEXT: blr x28
diff --git a/llvm/test/MC/AArch64/LFI/return.s b/llvm/test/MC/AArch64/LFI/return.s
index 7e7431df57aa0..9becff0a55e9d 100644
--- a/llvm/test/MC/AArch64/LFI/return.s
+++ b/llvm/test/MC/AArch64/LFI/return.s
@@ -1,5 +1,7 @@
 // RUN: llvm-mc -triple aarch64_lfi %s | FileCheck %s
 
+.arch_extension pauth
+
 mov x30, x0
 ret
 // CHECK:      mov x30, x0
@@ -23,3 +25,44 @@ ret
 // CHECK:      ldp x30, x29, [sp]
 // CHECK-NEXT: add x30, x27, w30, uxtw
 // CHECK-NEXT: ret
+
+mov x30, x0
+next_func:
+nop
+// CHECK:      mov x30, x0
+// CHECK-NEXT: add x30, x27, w30, uxtw
+// CHECK:      nop
+
+autiasp
+ret
+// CHECK:      autiasp
+// CHECK-NEXT: add x30, x27, w30, uxtw
+// CHECK-NEXT: ret
+
+paciasp
+nop
+// CHECK:      paciasp
+// CHECK-NEXT: nop
+
+mov x30, x0
+bl some_func
+// CHECK:      mov x30, x0
+// CHECK-NEXT: add x30, x27, w30, uxtw
+// CHECK-NEXT: bl some_func
+
+mov x30, x0
+blr x1
+// CHECK:      mov x30, x0
+// CHECK-NEXT: add x30, x27, w30, uxtw
+// CHECK-NEXT: add x28, x27, w1, uxtw
+// CHECK-NEXT: blr x28
+
+mov x30, x0
+b some_func
+// CHECK:      mov x30, x0
+// CHECK-NEXT: add x30, x27, w30, uxtw
+// CHECK-NEXT: b some_func
+
+mov x30, x0
+// CHECK:      mov x30, x0
+// CHECK-NEXT: add x30, x27, w30, uxtw

>From 6189ff12de5005ae7a072b625129f76fea39753a Mon Sep 17 00:00:00 2001
From: Zachary Yedidia <zyedidia at gmail.com>
Date: Tue, 14 Jul 2026 03:03:43 -0400
Subject: [PATCH 2/2] Update based on review feedback

---
 llvm/docs/LFI.rst                             |  8 ++++++
 .../MCTargetDesc/AArch64MCLFIRewriter.cpp     | 25 +++++++------------
 2 files changed, 17 insertions(+), 16 deletions(-)

diff --git a/llvm/docs/LFI.rst b/llvm/docs/LFI.rst
index b1603c2d4d136..c932855ef5ba7 100644
--- a/llvm/docs/LFI.rst
+++ b/llvm/docs/LFI.rst
@@ -329,6 +329,14 @@ LFI is compatible with Arm Pointer Authentication Code (PAC) instructions,
 which are used to sign and authenticate ``x30`` to protect against control-flow
 hijacking.
 
+The typical use is ``-mbranch-protection=pac-ret``, which signs only the return
+address in ``x30`` using the hint-space ``paciasp`` and ``autiasp``
+instructions. The combined authenticate-and-branch and authenticate-and-return
+instructions covered below require Armv8.3-a and are not produced by
+``-mbranch-protection``. They can appear in hand-written assembly or from
+environments that sign all code pointers, so the rewriter still sandboxes them
+rather than passing them through unmodified.
+
 To gain the security benefit of PAC under LFI, the hardware must implement
 ``FEAT_FPAC``, so that authentication failures fault immediately. Without
 ``FEAT_FPAC``, a failed authentication produces a poisoned pointer, which LFI
diff --git a/llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp b/llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp
index 3899fba5f3a87..3c24f5ab62a61 100644
--- a/llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp
+++ b/llvm/lib/Target/AArch64/MCTargetDesc/AArch64MCLFIRewriter.cpp
@@ -189,7 +189,7 @@ static bool isExceptionReturn(unsigned Opcode) {
          Opcode == AArch64::ERETAB;
 }
 
-static bool authenticatesLR(const MCInst &Inst) {
+static bool pacWritesLR(const MCInst &Inst) {
   switch (Inst.getOpcode()) {
   case AArch64::AUTIASP:
   case AArch64::AUTIBSP:
@@ -541,39 +541,33 @@ void AArch64MCLFIRewriter::rewriteAuthenticatedBranchOrCall(
     const MCSubtargetInfo &STI) {
   MCRegister TargetReg = Inst.getOperand(0).getReg();
 
-  // Authenticate the target in place. The zero-modifier variants
-  // (braaz/brabz/blraaz/blrabz) have no modifier operand.
+  // Select the authentication opcode for the target register.
   MCInst Auth;
   switch (Inst.getOpcode()) {
   case AArch64::BRAA:
   case AArch64::BLRAA:
     Auth.setOpcode(AArch64::AUTIA);
-    Auth.addOperand(MCOperand::createReg(TargetReg)); // dst
-    Auth.addOperand(MCOperand::createReg(TargetReg)); // src (tied to dst)
-    Auth.addOperand(Inst.getOperand(1));              // modifier
     break;
   case AArch64::BRAB:
   case AArch64::BLRAB:
     Auth.setOpcode(AArch64::AUTIB);
-    Auth.addOperand(MCOperand::createReg(TargetReg)); // dst
-    Auth.addOperand(MCOperand::createReg(TargetReg)); // src (tied to dst)
-    Auth.addOperand(Inst.getOperand(1));              // modifier
     break;
   case AArch64::BRAAZ:
   case AArch64::BLRAAZ:
     Auth.setOpcode(AArch64::AUTIZA);
-    Auth.addOperand(MCOperand::createReg(TargetReg)); // dst
-    Auth.addOperand(MCOperand::createReg(TargetReg)); // src (tied to dst)
     break;
   case AArch64::BRABZ:
   case AArch64::BLRABZ:
     Auth.setOpcode(AArch64::AUTIZB);
-    Auth.addOperand(MCOperand::createReg(TargetReg)); // dst
-    Auth.addOperand(MCOperand::createReg(TargetReg)); // src (tied to dst)
     break;
   default:
     llvm_unreachable("unexpected authenticated branch/call opcode");
   }
+
+  Auth.addOperand(MCOperand::createReg(TargetReg)); // dst
+  Auth.addOperand(MCOperand::createReg(TargetReg)); // src (tied to dst)
+  if (Auth.getOpcode() == AArch64::AUTIA || Auth.getOpcode() == AArch64::AUTIB)
+    Auth.addOperand(Inst.getOperand(1)); // modifier
   emitInst(Auth, Out, STI);
 
   // Guard the authenticated target and branch/call through x28.
@@ -978,9 +972,8 @@ void AArch64MCLFIRewriter::doRewriteInst(const MCInst &Inst, MCStreamer &Out,
     return rewriteSPModification(Inst, Out, STI);
 
   // Link register modification. This covers explicit writes to x30 as well as
-  // PAC instructions that authenticate LR in place (autiasp, ...), which
-  // define LR implicitly.
-  if (explicitlyModifiesRegister(Inst, AArch64::LR) || authenticatesLR(Inst))
+  // PAC instructions that write LR in place, which define LR implicitly.
+  if (explicitlyModifiesRegister(Inst, AArch64::LR) || pacWritesLR(Inst))
     return rewriteLRModification(Inst, Out, STI);
 
   // Memory access.



More information about the llvm-commits mailing list