[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