[llvm-branch-commits] [lld] [lld-macho] Call through authenticated stubs on arm64e (PR #234003)

via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Wed Oct 21 10:46:59 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-lld-macho

Author: Oskar Wirga (oskarwirga)

<details>
<summary>Changes</summary>

Call dylib functions from arm64e code through `__auth_stubs` rather than `__stubs`, as ld64 does. Each stub is four instructions instead of three:

```
  adrp x17, slot@<!-- -->page
  add  x17, x17, slot@<!-- -->pageoff
  ldr  x16, [x17]
  braa x16, x17
```

where the slot is in `__auth_got` and holds a pointer that dyld signs with the IA key, blended with the slot's address. The raw target is only ever held in x16 and x17, so a stub never branches through an unauthenticated pointer loaded from writable memory. On arm64e, name the stubs section `__auth_stubs`, make stubs 16 bytes, and put stub targets in `__auth_got`. Fast ObjC stubs load `_objc_msgSend` from `__auth_got` and `braa` to it.

`__auth_stubs` gets placed directly after `__text`. `TextOutputSection::needsThunks()` estimates the reach of branches to the stubs as the size of `__text` plus the size of the stubs. If it were to be sorted as an unknown section, `__auth_stubs` would land after all of `__TEXT`'s read-only data, the estimate would miss it, and thunking would silently switch off, so a large arm64e app would fail to link with out-of-range BRANCH26 relocations to its stubs.

Assisted-by: Claude Code


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


6 Files Affected:

- (modified) lld/MachO/Arch/ARM64.cpp (+52) 
- (modified) lld/MachO/InputSection.h (+1) 
- (modified) lld/MachO/OutputSegment.cpp (+2) 
- (modified) lld/MachO/SyntheticSections.cpp (+21-8) 
- (added) lld/test/MachO/arm64e-auth-stubs-section-order.s (+35) 
- (modified) lld/test/MachO/arm64e-stubs.s (+56-14) 


``````````diff
diff --git a/lld/MachO/Arch/ARM64.cpp b/lld/MachO/Arch/ARM64.cpp
index 76b549981c4d0..0ba5422c1788b 100644
--- a/lld/MachO/Arch/ARM64.cpp
+++ b/lld/MachO/Arch/ARM64.cpp
@@ -44,6 +44,10 @@ struct ARM64 : ARM64Common {
 
 struct ARM64e final : ARM64 {
   ARM64e();
+  void writeStub(uint8_t *buf, const Symbol &, uint64_t) const override;
+  void writeObjCMsgSendStub(uint8_t *buf, Symbol *sym, uint64_t stubsAddr,
+                            uint64_t &stubOffset, uint64_t selrefVA,
+                            Symbol *objcMsgSend) const override;
 };
 } // namespace
 
@@ -243,8 +247,56 @@ TargetInfo *macho::createARM64TargetInfo() {
   return &t;
 }
 
+// arm64e stubs authenticate the target they load from __auth_got.
+static constexpr uint32_t authStubCode[] = {
+    0x90000011, // 00: adrp  x17, __auth_got at page
+    0x91000231, // 04: add   x17, x17, __auth_got at pageoff
+    0xf9400230, // 08: ldr   x16, [x17]
+    0xd71f0a11, // 0c: braa  x16, x17
+};
+
+void ARM64e::writeStub(uint8_t *buf8, const Symbol &sym,
+                       uint64_t pointerVA) const {
+  auto *buf32 = reinterpret_cast<uint32_t *>(buf8);
+  SymbolDiagnostic d = {&sym, "stub"};
+  uint64_t pcPageBits = pageBits(in.stubs->addr + sym.stubsIndex * stubSize);
+  encodePage21(&buf32[0], d, authStubCode[0], pageBits(pointerVA) - pcPageBits);
+  encodePageOff12(&buf32[1], d, authStubCode[1], pointerVA);
+  buf32[2] = authStubCode[2];
+  buf32[3] = authStubCode[3];
+}
+
+// The fast stubs reach _objc_msgSend through __auth_got in the same way.
+static constexpr uint32_t authObjcStubsFastCode[] = {
+    0x90000001, // adrp  x1, __objc_selrefs at page
+    0xf9400021, // ldr   x1, [x1, @selector("foo")@pageoff]
+    0x90000011, // adrp  x17, __auth_got at page
+    0x91000231, // add   x17, x17, _objc_msgSend at pageoff
+    0xf9400230, // ldr   x16, [x17]
+    0xd71f0a11, // braa  x16, x17
+    0xd4200020, // brk   #0x1
+    0xd4200020, // brk   #0x1
+};
+
+void ARM64e::writeObjCMsgSendStub(uint8_t *buf, Symbol *sym, uint64_t stubsAddr,
+                                  uint64_t &stubOffset, uint64_t selrefVA,
+                                  Symbol *objcMsgSend) const {
+  // Small stubs branch to _objc_msgSend or its stub, as on arm64.
+  if (config->objcStubsMode == ObjCStubsMode::small) {
+    ARM64::writeObjCMsgSendStub(buf, sym, stubsAddr, stubOffset, selrefVA,
+                                objcMsgSend);
+    return;
+  }
+  ::writeObjCMsgSendFastStub<LP64>(buf, authObjcStubsFastCode, sym, stubsAddr,
+                                   stubOffset, selrefVA,
+                                   in.authGot->getVA(objcMsgSend),
+                                   /*msgSendIndex=*/0);
+  stubOffset += objcStubsFastSize;
+}
+
 ARM64e::ARM64e() {
   cpuSubtype = CPU_SUBTYPE_ARM64E;
+  stubSize = sizeof(authStubCode);
   relocAttrs = relocAttrsArray;
 }
 
diff --git a/lld/MachO/InputSection.h b/lld/MachO/InputSection.h
index bdbf53aa588a6..abb130b4c363e 100644
--- a/lld/MachO/InputSection.h
+++ b/lld/MachO/InputSection.h
@@ -372,6 +372,7 @@ constexpr const char staticInit[] = "__StaticInit";
 constexpr const char stringTable[] = "__string_table";
 constexpr const char stubHelper[] = "__stub_helper";
 constexpr const char stubs[] = "__stubs";
+constexpr const char authStubs[] = "__auth_stubs";
 constexpr const char swift[] = "__swift";
 constexpr const char symbolTable[] = "__symbol_table";
 constexpr const char textCoalNt[] = "__textcoal_nt";
diff --git a/lld/MachO/OutputSegment.cpp b/lld/MachO/OutputSegment.cpp
index 6d8eadb703f5a..c0eb59dd3b0d8 100644
--- a/lld/MachO/OutputSegment.cpp
+++ b/lld/MachO/OutputSegment.cpp
@@ -110,6 +110,8 @@ static int sectionOrder(OutputSection *osec) {
     int knownPriority =
         StringSwitch<int>(osec->name)
             .Case(section_names::stubs, -4)
+            // needsThunks() assumes that the stubs follow __text.
+            .Case(section_names::authStubs, -4)
             .Case(section_names::stubHelper, -3)
             .Case(section_names::objcStubs, -2)
             .Case(section_names::initOffsets, -1)
diff --git a/lld/MachO/SyntheticSections.cpp b/lld/MachO/SyntheticSections.cpp
index c774cb60ed795..03f985b00b0fe 100644
--- a/lld/MachO/SyntheticSections.cpp
+++ b/lld/MachO/SyntheticSections.cpp
@@ -833,7 +833,9 @@ void WeakBindingSection::writeTo(uint8_t *buf) const {
 }
 
 StubsSection::StubsSection()
-    : SyntheticSection(segment_names::text, section_names::stubs) {
+    : SyntheticSection(segment_names::text, config->arch() == AK_arm64e
+                                                ? section_names::authStubs
+                                                : section_names::stubs) {
   flags = S_SYMBOL_STUBS | S_ATTR_SOME_INSTRUCTIONS | S_ATTR_PURE_INSTRUCTIONS;
   // The stubs section comprises machine instructions, which are aligned to
   // 4 bytes on the archs we care about.
@@ -848,8 +850,13 @@ uint64_t StubsSection::getSize() const {
 void StubsSection::writeTo(uint8_t *buf) const {
   size_t off = 0;
   for (const Symbol *sym : entries) {
-    uint64_t pointerVA =
-        config->emitChainedFixups ? sym->getGotVA() : sym->getLazyPtrVA();
+    uint64_t pointerVA;
+    if (!config->emitChainedFixups)
+      pointerVA = sym->getLazyPtrVA();
+    else if (config->arch() == AK_arm64e)
+      pointerVA = in.authGot->getVA(sym);
+    else
+      pointerVA = sym->getGotVA();
     target->writeStub(buf + off, *sym, pointerVA);
     off += target->stubSize;
   }
@@ -889,10 +896,12 @@ void StubsSection::addEntry(Symbol *sym) {
   if (inserted) {
     sym->stubsIndex = entries.size() - 1;
 
-    if (config->emitChainedFixups)
-      in.got->addEntry(sym);
-    else
+    if (!config->emitChainedFixups)
       addBindingsForStub(sym);
+    else if (config->arch() == AK_arm64e)
+      in.authGot->addEntry(sym);
+    else
+      in.got->addEntry(sym);
   }
 }
 
@@ -1059,8 +1068,12 @@ void ObjCStubsSection::setUp() {
                          "lazy binding (normally in libobjc.dylib)");
   objcMsgSend->used = true;
   if (config->objcStubsMode == ObjCStubsMode::fast) {
-    in.got->addEntry(objcMsgSend);
-    assert(objcMsgSend->isInGot());
+    if (config->arch() == AK_arm64e) {
+      in.authGot->addEntry(objcMsgSend);
+    } else {
+      in.got->addEntry(objcMsgSend);
+      assert(objcMsgSend->isInGot());
+    }
   } else {
     assert(config->objcStubsMode == ObjCStubsMode::small);
     // In line with ld64's behavior, when objc_msgSend is a direct symbol,
diff --git a/lld/test/MachO/arm64e-auth-stubs-section-order.s b/lld/test/MachO/arm64e-auth-stubs-section-order.s
new file mode 100644
index 0000000000000..b78cd404395f6
--- /dev/null
+++ b/lld/test/MachO/arm64e-auth-stubs-section-order.s
@@ -0,0 +1,35 @@
+# REQUIRES: aarch64
+## arm64e names the stubs section __auth_stubs rather than __stubs. Like
+## __stubs, it must come directly after __text: needsThunks() assumes that when
+## it estimates the reach of branches to the stubs.
+
+# RUN: llvm-mc -filetype=obj -triple=arm64e-apple-darwin %s -o %t.o
+# RUN: %lld -arch arm64e -lSystem -U _external_func -o %t.out %t.o
+# RUN: llvm-objdump --section-headers %t.out | FileCheck %s
+
+## __auth_stubs must come directly after __text, before any read-only data.
+# CHECK:      __text
+# CHECK-NEXT: __auth_stubs
+
+## Same placement for plain arm64, where the section is called __stubs.
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %s -o %t64.o
+# RUN: %lld -arch arm64 -lSystem -U _external_func -o %t64.out %t64.o
+# RUN: llvm-objdump --section-headers %t64.out | FileCheck --check-prefix=ARM64 %s
+
+# ARM64:      __text
+# ARM64-NEXT: __stubs
+
+.globl _main
+.p2align 2
+_main:
+  stp x29, x30, [sp, #-16]!
+  bl _external_func
+  ldp x29, x30, [sp], #16
+  ret
+
+.section __TEXT,__const
+.p2align 4
+_rodata:
+  .space 64
+
+.subsections_via_symbols
diff --git a/lld/test/MachO/arm64e-stubs.s b/lld/test/MachO/arm64e-stubs.s
index 4ed21ab9e0d6f..1965ffe3a2e0e 100644
--- a/lld/test/MachO/arm64e-stubs.s
+++ b/lld/test/MachO/arm64e-stubs.s
@@ -1,6 +1,8 @@
 # REQUIRES: aarch64
-## On arm64e, calls to dylib functions go through __stubs, which load the
-## target from an unsigned __got slot, as on arm64.
+## On arm64e, calls to dylib functions go through __auth_stubs, which load the
+## target from a signed __auth_got slot and authenticate it against the slot's
+## address with braa, as ld64 does. Fast ObjC stubs reach _objc_msgSend the
+## same way; small ones branch to its stub.
 
 # RUN: rm -rf %t; split-file %s %t
 # RUN: llvm-mc -filetype=obj -triple=arm64e-apple-macos -o %t/foo.o %t/foo.s
@@ -8,35 +10,75 @@
 # RUN: %lld -arch arm64e -platform_version macos 13.0 13.0 -dylib \
 # RUN:   -install_name @executable_path/libfoo.dylib %t/foo.o -o %t/libfoo.dylib
 # RUN: %lld -arch arm64e -platform_version macos 13.0 13.0 %t/test.o \
-# RUN:   %t/libfoo.dylib -o %t/test
+# RUN:   %t/libfoo.dylib -objc_stubs_fast -o %t/test
 # RUN: llvm-objdump --macho --section-headers %t/test > %t/out
 # RUN: llvm-objdump --no-print-imm-hex --macho -d --no-show-raw-insn \
-# RUN:   --section=__TEXT,__stubs %t/test >> %t/out
+# RUN:   --section=__TEXT,__auth_stubs --section=__TEXT,__objc_stubs \
+# RUN:   %t/test >> %t/out
 # RUN: llvm-objdump --macho --dyld-info %t/test >> %t/out
-# RUN: FileCheck %s < %t/out --implicit-check-not=__auth
+# RUN: FileCheck %s < %t/out --implicit-check-not=__stubs \
+# RUN:   --implicit-check-not=__got
 
-# CHECK:      __stubs
-# CHECK-NEXT: __got {{[0-9a-f]+}} {{0*}}[[#%x,GOT:]] DATA
+# CHECK:      __text
+# CHECK-NEXT: __auth_stubs {{0*}}00000010
+# CHECK:      __auth_got {{[0-9a-f]+}} {{0*}}[[#%x,GOT:]] DATA
 
 # CHECK-LABEL: _main:
 # CHECK-NEXT:  bl {{.*}} ; symbol stub for: _foo
-# CHECK-LABEL: Contents of (__TEXT,__stubs) section
-# CHECK-NEXT:  adrp x16, {{.*}} ; 0x[[#GOT]]
-# CHECK-NEXT:  ldr x16, [x16] ; literal pool symbol address: _foo
-# CHECK-NEXT:  br x16
+# CHECK-LABEL: Contents of (__TEXT,__auth_stubs) section
+# CHECK-NEXT:  adrp x17, {{.*}} ; 0x[[#GOT]]
+# CHECK-NEXT:  add x17, x17, #[[#%u,FOO:]] ; literal pool symbol address: _foo
+# CHECK-NEXT:  ldr x16, [x17]
+# CHECK-NEXT:  braa x16, x17
+# CHECK-LABEL: Contents of (__TEXT,__objc_stubs) section
+# CHECK-NEXT:  _objc_msgSend$foo:
+# CHECK-NEXT:  adrp x1
+# CHECK-NEXT:  ldr x1, [x1
+# CHECK-NEXT:  adrp x17, {{.*}} ; 0x[[#GOT]]
+# CHECK-NEXT:  add x17, x17, #[[#%u,MSGSEND:]]
+# CHECK-NEXT:  ldr x16, [x17]
+# CHECK-NEXT:  braa x16, x17
+# CHECK-NEXT:  brk #0x1
+# CHECK-NEXT:  brk #0x1
 
-## The __got slot is an unauthenticated bind of _foo.
+## Both __auth_got slots are authenticated binds, of _objc_msgSend and _foo,
+## with the IA key, address diversity and no extra discriminator.
 # CHECK-LABEL: dyld information:
-# CHECK:       __DATA_CONST __got 0x[[#%X,GOT]] 0x4000000000000000 bind 0x0 libfoo _foo
+# CHECK:       __DATA_CONST __auth_got 0x[[#%X,GOT]] 0xC009000000000000 bind 0x0000 true IA 0x0 libfoo _objc_msgSend
+# CHECK-NEXT:  __DATA_CONST __auth_got 0x[[#%X,GOT+8]] 0xC001000000000001 bind 0x0000 true IA 0x0 libfoo _foo
+
+## Small ObjC stubs keep branching to _objc_msgSend's stub.
+# RUN: %lld -arch arm64e -platform_version macos 13.0 13.0 %t/test.o \
+# RUN:   %t/libfoo.dylib -objc_stubs_small -o %t/small
+# RUN: llvm-objdump --no-print-imm-hex --macho -d --no-show-raw-insn \
+# RUN:   --section=__TEXT,__objc_stubs %t/small | FileCheck %s --check-prefix=SMALL
+
+# SMALL-LABEL: Contents of (__TEXT,__objc_stubs) section
+# SMALL-NEXT:  _objc_msgSend$foo:
+# SMALL-NEXT:  adrp x1
+# SMALL-NEXT:  ldr x1, [x1
+# SMALL-NEXT:  b {{.*}} ; symbol stub for: _objc_msgSend
 
 #--- foo.s
-.globl _foo
+.globl _foo, _objc_msgSend
 _foo:
   ret
+_objc_msgSend:
+  ret
 
 #--- test.s
+.section __TEXT,__objc_methname,cstring_literals
+lselref1:
+  .asciz "foo"
+
+.section __DATA,__objc_selrefs,literal_pointers,no_dead_strip
+.p2align 3
+.quad lselref1
+
+.text
 .globl _main
 .p2align 2
 _main:
   bl _foo
+  bl _objc_msgSend$foo
   ret

``````````

</details>


https://github.com/llvm/llvm-project/pull/234003


More information about the llvm-branch-commits mailing list