[lld] [llvm] [lld][MachO] Add ARM64e pointer authentication linking support (PR #188378)
Oskar Wirga via llvm-commits
llvm-commits at lists.llvm.org
Thu Apr 2 19:56:13 PDT 2026
https://github.com/oskarwirga updated https://github.com/llvm/llvm-project/pull/188378
>From 0f0ec0e9fe6c66e2bd1bda27f6bd53ef4a82b354 Mon Sep 17 00:00:00 2001
From: Oskar Wirga <oskarwirga at meta.com>
Date: Mon, 2 Mar 2026 16:28:04 -0800
Subject: [PATCH 01/12] [llvm][MachO] Add arm64e chained fixup pointer
structures
Add struct definitions for the arm64e chained fixup pointer formats
used by dyld. These correspond to the DYLD_CHAINED_PTR_ARM64E and
DYLD_CHAINED_PTR_ARM64E_USERLAND24 format types already enumerated
in this header. The structs encode rebase, bind, and authenticated
bind/rebase fixup entries, and will be consumed by the arm64e chained
fixup encoding implementation in LLD.
Layouts match Apple's dyld fixup-chains.h definitions.
---
llvm/include/llvm/BinaryFormat/MachO.h | 71 ++++++++++++++++++++++++++
1 file changed, 71 insertions(+)
diff --git a/llvm/include/llvm/BinaryFormat/MachO.h b/llvm/include/llvm/BinaryFormat/MachO.h
index 4dfc1ff204555..6f0ba5535e144 100644
--- a/llvm/include/llvm/BinaryFormat/MachO.h
+++ b/llvm/include/llvm/BinaryFormat/MachO.h
@@ -1192,6 +1192,77 @@ struct dyld_chained_ptr_64_rebase {
uint64_t bind : 1; // set to 0
};
+// DYLD_CHAINED_PTR_ARM64E / DYLD_CHAINED_PTR_ARM64E_USERLAND
+struct dyld_chained_ptr_arm64e_rebase {
+ uint64_t target : 43;
+ uint64_t high8 : 8;
+ uint64_t next : 11; // 4 or 8-byte stride
+ uint64_t bind : 1; // == 0
+ uint64_t auth : 1; // == 0
+};
+
+struct dyld_chained_ptr_arm64e_bind {
+ uint64_t ordinal : 16;
+ uint64_t zero : 16;
+ uint64_t addend : 19; // +/-256K
+ uint64_t next : 11; // 4 or 8-byte stride
+ uint64_t bind : 1; // == 1
+ uint64_t auth : 1; // == 0
+};
+
+struct dyld_chained_ptr_arm64e_auth_bind {
+ uint64_t ordinal : 16;
+ uint64_t zero : 16;
+ uint64_t diversity : 16;
+ uint64_t addrDiv : 1;
+ uint64_t key : 2;
+ uint64_t next : 11; // 4 or 8-byte stride
+ uint64_t bind : 1; // == 1
+ uint64_t auth : 1; // == 1
+};
+
+struct dyld_chained_ptr_arm64e_auth_rebase {
+ uint64_t target : 32; // runtimeOffset
+ uint64_t diversity : 16;
+ uint64_t addrDiv : 1;
+ uint64_t key : 2;
+ uint64_t next : 11; // 4 or 8-byte stride
+ uint64_t bind : 1; // == 0
+ uint64_t auth : 1; // == 1
+};
+
+// DYLD_CHAINED_PTR_ARM64E_USERLAND24
+// Used for arm64e userland binaries targeting:
+// - macOS 12.0+
+// - iOS 15.0+
+// - watchOS 8.0+
+// - tvOS 15.0+
+// The key difference from DYLD_CHAINED_PTR_ARM64E is:
+// - 24-bit ordinal (vs 16-bit) allowing more imports
+// - 8-bit zero field (vs 16-bit)
+// - Non-auth rebase targets are vm offsets (vs vmaddr)
+// Rebase structs are shared with DYLD_CHAINED_PTR_ARM64E.
+struct dyld_chained_ptr_arm64e_bind24 {
+ uint64_t ordinal : 24;
+ uint64_t zero : 8;
+ uint64_t addend : 19; // +/-256K
+ uint64_t next : 11; // 8-byte stride
+ uint64_t bind : 1; // == 1
+ uint64_t auth : 1; // == 0
+};
+
+// DYLD_CHAINED_PTR_ARM64E_USERLAND24
+struct dyld_chained_ptr_arm64e_auth_bind24 {
+ uint64_t ordinal : 24;
+ uint64_t zero : 8;
+ uint64_t diversity : 16;
+ uint64_t addrDiv : 1;
+ uint64_t key : 2;
+ uint64_t next : 11; // 8-byte stride
+ uint64_t bind : 1; // == 1
+ uint64_t auth : 1; // == 1
+};
+
// Byte order swapping functions for MachO structs
inline void swapStruct(fat_header &mh) {
>From b11a326f30c10823440b5b5d527af9683b18c425 Mon Sep 17 00:00:00 2001
From: Oskar Wirga <oskarwirga at meta.com>
Date: Mon, 2 Mar 2026 16:37:03 -0800
Subject: [PATCH 02/12] [lld][MachO] Add AUTH relocation attribute and extend
Reloc for arm64e
Add data structures to support arm64e pointer authentication in the
MachO linker. The AUTH relocation attribute marks relocations that
carry ptrauth metadata (key, diversity, address discrimination).
The Relocation struct is extended with AuthInfo and authEncodingBits to
propagate this metadata through the linking pipeline.
Also adds the __auth_stubs section name constant and fixes the
namespace closing comment (Macho -> macho).
---
lld/MachO/InputSection.h | 1 +
lld/MachO/Relocations.h | 38 ++++++++++++++++++++++++++++++++++++--
2 files changed, 37 insertions(+), 2 deletions(-)
diff --git a/lld/MachO/InputSection.h b/lld/MachO/InputSection.h
index e0a90a2edc0af..ac23b9a9b4e57 100644
--- a/lld/MachO/InputSection.h
+++ b/lld/MachO/InputSection.h
@@ -363,6 +363,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/Relocations.h b/lld/MachO/Relocations.h
index f5d4c37082968..077a8ac69e9ff 100644
--- a/lld/MachO/Relocations.h
+++ b/lld/MachO/Relocations.h
@@ -16,6 +16,7 @@
#include <cstddef>
#include <cstdint>
+#include <optional>
namespace lld::macho {
LLVM_ENABLE_BITMASK_ENUMS_IN_NAMESPACE();
@@ -41,7 +42,8 @@ enum class RelocAttrBits {
LOAD = 1 << 13, // Relaxable indirect load
POINTER = 1 << 14, // Non-relaxable indirect load (pointer is taken)
UNSIGNED = 1 << 15, // *_UNSIGNED relocs
- LLVM_MARK_AS_BITMASK_ENUM(/*LargestValue*/ (1 << 16) - 1),
+ AUTH = 1 << 16, // ARM64e ptrauth relocs
+ LLVM_MARK_AS_BITMASK_ENUM(/*LargestValue*/ (1 << 17) - 1),
};
// Note: SUBTRACTOR always pairs with UNSIGNED (a delta between two symbols).
@@ -51,16 +53,39 @@ struct RelocAttrs {
bool hasAttr(RelocAttrBits b) const { return (bits & b) == b; }
};
+// Auth info for ARM64e pointer authentication relocations.
+// Used both inside the Relocation union and as a standalone parameter type.
+struct AuthInfo {
+ uint16_t diversity;
+ uint8_t key; // 0-3 (IA/IB/DA/DB)
+ uint8_t addrDiv;
+};
+
+// Packed representation of a 32-bit addend plus auth metadata.
+// Used inside Relocation's union to overlay with the 64-bit addend.
+struct AuthReloc {
+ int32_t addend;
+ AuthInfo info;
+};
+
struct Relocation {
uint8_t type = llvm::MachO::GENERIC_RELOC_INVALID;
bool pcrel = false;
uint8_t length = 0;
+ // True when this relocation carries ptrauth metadata (ARM64e AUTH relocs).
+ bool hasAuth = false;
// The offset from the start of the subsection that this relocation belongs
// to.
uint32_t offset = 0;
// Adding this offset to the address of the referent symbol or subsection
// gives the destination that this relocation refers to.
- int64_t addend = 0;
+ //
+ // For AUTH relocs the addend is 32-bit and the upper 32 bits carry
+ // diversity/key/addrDiv, so we use a union to keep sizeof(Relocation)==24.
+ union {
+ int64_t addend = 0; // non-AUTH relocs: full 64-bit addend
+ AuthReloc authData; // AUTH relocs: 32-bit addend + auth fields
+ };
llvm::PointerUnion<Symbol *, InputSection *> referent = nullptr;
Relocation() = default;
@@ -71,6 +96,15 @@ struct Relocation {
: type(type), pcrel(pcrel), length(length), offset(offset),
addend(addend), referent(referent) {}
+ // Convenience accessors for auth metadata.
+ AuthInfo getAuthInfo() const {
+ assert(hasAuth);
+ return authData.info;
+ }
+ int64_t getAddend() const {
+ return hasAuth ? static_cast<int64_t>(authData.addend) : addend;
+ }
+
InputSection *getReferentInputSection() const;
// Must point to an offset within a CStringInputSection or a
>From f32eb9df02fc62fd0ceb97861c018666d787f071 Mon Sep 17 00:00:00 2001
From: Oskar Wirga <oskarwirga at meta.com>
Date: Mon, 2 Mar 2026 16:48:58 -0800
Subject: [PATCH 03/12] [lld][MachO] Add ARM64e target with authenticated stubs
Add the ARM64e target to LLD's MachO linker, enabling linking of
arm64e binaries with pointer authentication support.
Key components:
- ARM64e.cpp: New target with 16-byte authenticated stubs using braa
(authenticated branch) with x17 as the context register, plus
authenticated ObjC stubs, thunks, and ICF safe thunks
- ARM64Common.cpp: Handle ARM64_RELOC_AUTHENTICATED_POINTER in
getEmbeddedAddend (low 32 bits only) and relocateOne
- Driver.cpp: Route arm64e CPU subtypes to the new target, add arm64e
to chained fixups support
- InputFiles.cpp: Parse ptrauth metadata (key, diversity, addrDiv)
from high bits of authenticated pointer relocations
---
lld/MachO/Arch/ARM64Common.cpp | 9 +-
lld/MachO/Arch/ARM64e.cpp | 269 +++++++++++++++++++++++++++
lld/MachO/CMakeLists.txt | 1 +
lld/MachO/Driver.cpp | 7 +-
lld/MachO/InputFiles.cpp | 22 ++-
lld/MachO/Target.h | 1 +
lld/test/MachO/arm64e-auth-data.s | 49 +++++
lld/test/MachO/arm64e-reject-mixed.s | 43 +++++
8 files changed, 397 insertions(+), 4 deletions(-)
create mode 100644 lld/MachO/Arch/ARM64e.cpp
create mode 100644 lld/test/MachO/arm64e-auth-data.s
create mode 100644 lld/test/MachO/arm64e-reject-mixed.s
diff --git a/lld/MachO/Arch/ARM64Common.cpp b/lld/MachO/Arch/ARM64Common.cpp
index 599f1e18efda6..e137cdb5679f5 100644
--- a/lld/MachO/Arch/ARM64Common.cpp
+++ b/lld/MachO/Arch/ARM64Common.cpp
@@ -19,7 +19,8 @@ using namespace lld::macho;
int64_t ARM64Common::getEmbeddedAddend(MemoryBufferRef mb, uint64_t offset,
const relocation_info rel) const {
if (rel.r_type != ARM64_RELOC_UNSIGNED &&
- rel.r_type != ARM64_RELOC_SUBTRACTOR) {
+ rel.r_type != ARM64_RELOC_SUBTRACTOR &&
+ rel.r_type != ARM64_RELOC_AUTHENTICATED_POINTER) {
// All other reloc types should use the ADDEND relocation to store their
// addends.
// TODO(gkm): extract embedded addend just so we can assert that it is 0
@@ -28,6 +29,12 @@ int64_t ARM64Common::getEmbeddedAddend(MemoryBufferRef mb, uint64_t offset,
const auto *buf = reinterpret_cast<const uint8_t *>(mb.getBufferStart());
const uint8_t *loc = buf + offset + rel.r_address;
+
+ if (rel.r_type == ARM64_RELOC_AUTHENTICATED_POINTER) {
+ // Only the low 32 bits are the addend; upper bits hold ptrauth fields.
+ return llvm::SignExtend64<32>(read32le(loc));
+ }
+
switch (rel.r_length) {
case 2:
return static_cast<int32_t>(read32le(loc));
diff --git a/lld/MachO/Arch/ARM64e.cpp b/lld/MachO/Arch/ARM64e.cpp
new file mode 100644
index 0000000000000..4c7b5b5f868d8
--- /dev/null
+++ b/lld/MachO/Arch/ARM64e.cpp
@@ -0,0 +1,269 @@
+//===- ARM64e.cpp ---------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "Arch/ARM64Common.h"
+#include "InputFiles.h"
+#include "Symbols.h"
+#include "SyntheticSections.h"
+#include "Target.h"
+
+#include "lld/Common/ErrorHandler.h"
+#include "mach-o/compact_unwind_encoding.h"
+#include "llvm/BinaryFormat/MachO.h"
+
+using namespace llvm;
+using namespace llvm::MachO;
+using namespace lld;
+using namespace lld::macho;
+
+namespace {
+
+struct ARM64e : ARM64Common {
+ ARM64e();
+ void writeStub(uint8_t *buf, const Symbol &, uint64_t) const override;
+ void writeStubHelperHeader(uint8_t *buf) const override;
+ void writeStubHelperEntry(uint8_t *buf, const Symbol &,
+ uint64_t entryAddr) const override;
+
+ void writeObjCMsgSendStub(uint8_t *buf, Symbol *sym, uint64_t stubsAddr,
+ uint64_t &stubOffset, uint64_t selrefVA,
+ Symbol *objcMsgSend) const override;
+ void populateThunk(InputSection *thunk, Symbol *funcSym) override;
+
+ void initICFSafeThunkBody(InputSection *thunk,
+ Symbol *targetSym) const override;
+ Symbol *getThunkBranchTarget(InputSection *thunk) const override;
+ uint32_t getICFSafeThunkSize() const override;
+};
+
+} // namespace
+
+// Random notes on reloc types:
+// ADDEND always pairs with BRANCH26, PAGE21, or PAGEOFF12
+// POINTER_TO_GOT: ld64 supports a 4-byte pc-relative form as well as an 8-byte
+// absolute version of this relocation. The semantics of the absolute relocation
+// are weird -- it results in the value of the GOT slot being written, instead
+// of the address. Let's not support it unless we find a real-world use case.
+static constexpr std::array<RelocAttrs, 12> relocAttrsArray{{
+#define B(x) RelocAttrBits::x
+ {"UNSIGNED",
+ B(UNSIGNED) | B(ABSOLUTE) | B(EXTERN) | B(LOCAL) | B(BYTE4) | B(BYTE8)},
+ {"SUBTRACTOR", B(SUBTRAHEND) | B(EXTERN) | B(BYTE4) | B(BYTE8)},
+ {"BRANCH26", B(PCREL) | B(EXTERN) | B(BRANCH) | B(BYTE4)},
+ {"PAGE21", B(PCREL) | B(EXTERN) | B(BYTE4)},
+ {"PAGEOFF12", B(ABSOLUTE) | B(EXTERN) | B(BYTE4)},
+ {"GOT_LOAD_PAGE21", B(PCREL) | B(EXTERN) | B(GOT) | B(BYTE4)},
+ {"GOT_LOAD_PAGEOFF12",
+ B(ABSOLUTE) | B(EXTERN) | B(GOT) | B(LOAD) | B(BYTE4)},
+ {"POINTER_TO_GOT", B(PCREL) | B(EXTERN) | B(GOT) | B(POINTER) | B(BYTE4)},
+ {"TLVP_LOAD_PAGE21", B(PCREL) | B(EXTERN) | B(TLV) | B(BYTE4)},
+ {"TLVP_LOAD_PAGEOFF12",
+ B(ABSOLUTE) | B(EXTERN) | B(TLV) | B(LOAD) | B(BYTE4)},
+ {"ADDEND", B(ADDEND)},
+ // ARM64e-specific: AUTHENTICATED_POINTER (64-bit absolute, external or
+ // local)
+ {"AUTHENTICATED_POINTER",
+ B(ABSOLUTE) | B(UNSIGNED) | B(EXTERN) | B(LOCAL) | B(BYTE8) | B(AUTH)},
+#undef B
+}};
+
+// ARM64e uses authenticated stubs with braa instruction.
+// These are 16 bytes (4 instructions) instead of the regular 12 bytes.
+// The stub computes the GOT address in x17 for use as authentication context.
+static constexpr uint32_t stubCode[] = {
+ 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 ; authenticate with IA key, context=x17
+};
+
+void ARM64e::writeStub(uint8_t *buf8, const Symbol &sym,
+ uint64_t pointerVA) const {
+ auto *buf32 = reinterpret_cast<uint32_t *>(buf8);
+ constexpr size_t stubCodeSize = 4 * sizeof(uint32_t);
+ SymbolDiagnostic d = {&sym, "stub"};
+ uint64_t stubAddr = in.stubs->addr + sym.stubsIndex * stubCodeSize;
+ uint64_t pcPageBits = pageBits(stubAddr);
+ uint64_t targetPageBits = pageBits(pointerVA);
+ int64_t pageDiff = static_cast<int64_t>(targetPageBits - pcPageBits);
+ // adrp x17, __auth_got at page
+ encodePage21(&buf32[0], d, stubCode[0], pageDiff);
+ // add x17, x17, __auth_got at pageoff
+ encodePageOff12(&buf32[1], d, stubCode[1], pointerVA);
+ // ldr x16, [x17]
+ buf32[2] = stubCode[2];
+ // braa x16, x17
+ buf32[3] = stubCode[3];
+}
+
+static constexpr uint32_t stubHelperHeaderCode[] = {
+ 0x90000011, // 00: adrp x17, _dyld_private at page
+ 0x91000231, // 04: add x17, x17, _dyld_private at pageoff
+ 0xa9bf47f0, // 08: stp x16/x17, [sp, #-16]!
+ 0x90000010, // 0c: adrp x16, dyld_stub_binder at page
+ 0xf9400210, // 10: ldr x16, [x16, dyld_stub_binder at pageoff]
+ 0xd61f0200, // 14: br x16
+};
+
+void ARM64e::writeStubHelperHeader(uint8_t *buf8) const {
+ ::writeStubHelperHeader<LP64>(buf8, stubHelperHeaderCode);
+}
+
+static constexpr uint32_t stubHelperEntryCode[] = {
+ 0x18000050, // 00: ldr w16, l0
+ 0x14000000, // 04: b stubHelperHeader
+ 0x00000000, // 08: l0: .long 0
+};
+
+void ARM64e::writeStubHelperEntry(uint8_t *buf8, const Symbol &sym,
+ uint64_t entryVA) const {
+ ::writeStubHelperEntry(buf8, stubHelperEntryCode, sym, entryVA);
+}
+
+// ARM64e uses authenticated ObjC stubs with braa instruction.
+// Uses x17 as both the address register and authentication context,
+// matching the pattern used in ARM64e auth stubs.
+static constexpr uint32_t objcStubsFastCode[] = {
+ 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, __auth_got at pageoff
+ 0xf9400230, // ldr x16, [x17]
+ 0xd71f0a11, // braa x16, x17 ; authenticate with IA key
+ 0xd4200020, // brk #0x1
+ 0xd4200020, // brk #0x1
+};
+
+static constexpr uint32_t objcStubsSmallCode[] = {
+ 0x90000001, // adrp x1, __objc_selrefs at page
+ 0xf9400021, // ldr x1, [x1, @selector("foo")@pageoff]
+ 0x14000000, // b _objc_msgSend
+};
+
+void ARM64e::writeObjCMsgSendStub(uint8_t *buf, Symbol *sym, uint64_t stubsAddr,
+ uint64_t &stubOffset, uint64_t selrefVA,
+ Symbol *objcMsgSend) const {
+ uint64_t objcMsgSendAddr;
+ uint64_t objcStubSize;
+ uint64_t objcMsgSendIndex;
+
+ if (config->objcStubsMode == ObjCStubsMode::fast) {
+ objcStubSize = target->objcStubsFastSize;
+ // ARM64e uses authgot for objc_msgSend.
+ objcMsgSendAddr = in.authgot->addr;
+ objcMsgSendIndex = objcMsgSend->gotIndex;
+ ::writeObjCMsgSendFastStub<LP64>(buf, objcStubsFastCode, sym, stubsAddr,
+ stubOffset, selrefVA, objcMsgSendAddr,
+ objcMsgSendIndex);
+ } else {
+ assert(config->objcStubsMode == ObjCStubsMode::small);
+ objcStubSize = target->objcStubsSmallSize;
+ if (auto *d = dyn_cast<Defined>(objcMsgSend)) {
+ objcMsgSendAddr = d->getVA();
+ objcMsgSendIndex = 0;
+ } else {
+ objcMsgSendAddr = in.stubs->addr;
+ objcMsgSendIndex = objcMsgSend->stubsIndex;
+ }
+ ::writeObjCMsgSendSmallStub<LP64>(buf, objcStubsSmallCode, sym, stubsAddr,
+ stubOffset, selrefVA, objcMsgSendAddr,
+ objcMsgSendIndex);
+ }
+ stubOffset += objcStubSize;
+}
+
+// A thunk is the relaxed variation of stubCode. We don't need the
+// extra indirection through a lazy pointer because the target address
+// is known at link time.
+static constexpr uint32_t thunkCode[] = {
+ 0x90000010, // 00: adrp x16, <thunk.ptr>@page
+ 0x91000210, // 04: add x16, [x16,<thunk.ptr>@pageoff]
+ 0xd61f0200, // 08: br x16
+};
+
+void ARM64e::populateThunk(InputSection *thunk, Symbol *funcSym) {
+ thunk->align = 4;
+ thunk->data = {reinterpret_cast<const uint8_t *>(thunkCode),
+ sizeof(thunkCode)};
+ thunk->relocs.emplace_back(/*type=*/ARM64_RELOC_PAGEOFF12,
+ /*pcrel=*/false, /*length=*/2,
+ /*offset=*/4, /*addend=*/0,
+ /*referent=*/funcSym);
+ thunk->relocs.emplace_back(/*type=*/ARM64_RELOC_PAGE21,
+ /*pcrel=*/true, /*length=*/2,
+ /*offset=*/0, /*addend=*/0,
+ /*referent=*/funcSym);
+}
+
+// Just a single direct branch to the target function.
+static constexpr uint32_t icfSafeThunkCode[] = {
+ 0x14000000, // 00: b target
+};
+
+void ARM64e::initICFSafeThunkBody(InputSection *thunk,
+ Symbol *targetSym) const {
+ // The base data here will not be itself modified, we'll just be adding a
+ // reloc below. So we can directly use the constexpr above as the data.
+ thunk->data = {reinterpret_cast<const uint8_t *>(icfSafeThunkCode),
+ sizeof(icfSafeThunkCode)};
+
+ thunk->relocs.emplace_back(/*type=*/ARM64_RELOC_BRANCH26,
+ /*pcrel=*/true, /*length=*/2,
+ /*offset=*/0, /*addend=*/0,
+ /*referent=*/targetSym);
+}
+
+Symbol *ARM64e::getThunkBranchTarget(InputSection *thunk) const {
+ assert(thunk->relocs.size() == 1 &&
+ "expected a single reloc on ARM64 ICF thunk");
+ auto &reloc = thunk->relocs[0];
+ assert(isa<Symbol *>(reloc.referent) &&
+ "ARM64 thunk reloc is expected to point to a Symbol");
+
+ return cast<Symbol *>(reloc.referent);
+}
+
+uint32_t ARM64e::getICFSafeThunkSize() const {
+ return sizeof(icfSafeThunkCode);
+}
+
+ARM64e::ARM64e() : ARM64Common(LP64()) {
+ cpuType = CPU_TYPE_ARM64;
+ // ARM64e-specific: Use ARM64E subtype with pointer authentication ABI version
+ // 0
+ cpuSubtype = CPU_SUBTYPE_ARM64E_WITH_PTRAUTH_VERSION(/*version*/ 0,
+ /*kernel*/ false);
+
+ stubSize = sizeof(stubCode);
+ thunkSize = sizeof(thunkCode);
+
+ objcStubsFastSize = sizeof(objcStubsFastCode);
+ objcStubsFastAlignment = 32;
+ objcStubsSmallSize = sizeof(objcStubsSmallCode);
+ objcStubsSmallAlignment = 4;
+
+ // Branch immediate is two's complement 26 bits, which is implicitly
+ // multiplied by 4 (since all functions are 4-aligned: The branch range
+ // is -4*(2**(26-1))..4*(2**(26-1) - 1).
+ backwardBranchRange = 128 * 1024 * 1024;
+ forwardBranchRange = backwardBranchRange - 4;
+
+ modeDwarfEncoding = UNWIND_ARM64_MODE_DWARF;
+ subtractorRelocType = ARM64_RELOC_SUBTRACTOR;
+ unsignedRelocType = ARM64_RELOC_UNSIGNED;
+
+ stubHelperHeaderSize = sizeof(stubHelperHeaderCode);
+ stubHelperEntrySize = sizeof(stubHelperEntryCode);
+
+ relocAttrs = {relocAttrsArray.data(), relocAttrsArray.size()};
+}
+
+TargetInfo *macho::createARM64eTargetInfo() {
+ static ARM64e t;
+ return &t;
+}
diff --git a/lld/MachO/CMakeLists.txt b/lld/MachO/CMakeLists.txt
index 72631f11511bf..e8afdc06609e9 100644
--- a/lld/MachO/CMakeLists.txt
+++ b/lld/MachO/CMakeLists.txt
@@ -6,6 +6,7 @@ include_directories(${LLVM_MAIN_SRC_DIR}/../libunwind/include)
add_lld_library(lldMachO
Arch/ARM64.cpp
+ Arch/ARM64e.cpp
Arch/ARM64Common.cpp
Arch/ARM64_32.cpp
Arch/X86_64.cpp
diff --git a/lld/MachO/Driver.cpp b/lld/MachO/Driver.cpp
index 58fbe64c2d1f9..7b8557b94e0da 100644
--- a/lld/MachO/Driver.cpp
+++ b/lld/MachO/Driver.cpp
@@ -951,6 +951,10 @@ static TargetInfo *createTargetInfo(InputArgList &args) {
case CPU_TYPE_X86_64:
return createX86_64TargetInfo();
case CPU_TYPE_ARM64:
+ if (cpuSubtype == CPU_SUBTYPE_ARM64E ||
+ cpuSubtype == CPU_SUBTYPE_ARM64E_VERSIONED_PTRAUTH_ABI_MASK ||
+ cpuSubtype == CPU_SUBTYPE_ARM64E_WITH_PTRAUTH_VERSION(0, 0))
+ return createARM64eTargetInfo();
return createARM64TargetInfo();
case CPU_TYPE_ARM64_32:
return createARM64_32TargetInfo();
@@ -1254,7 +1258,8 @@ static bool shouldEmitChainedFixups(const InputArgList &args) {
return false;
}
- if (!is_contained({AK_x86_64, AK_x86_64h, AK_arm64}, config->arch())) {
+ if (!is_contained({AK_x86_64, AK_x86_64h, AK_arm64, AK_arm64e},
+ config->arch())) {
if (requested)
error("-fixup_chains is only supported on x86_64 and arm64 targets");
diff --git a/lld/MachO/InputFiles.cpp b/lld/MachO/InputFiles.cpp
index cc7eae51175bc..5e4cbae158944 100644
--- a/lld/MachO/InputFiles.cpp
+++ b/lld/MachO/InputFiles.cpp
@@ -263,7 +263,7 @@ std::optional<MemoryBufferRef> macho::readFile(StringRef path) {
// FIXME: LD64 has a more complex fallback logic here.
// Consider implementing that as well?
if (cpuType != static_cast<uint32_t>(target->cpuType) ||
- cpuSubtype != target->cpuSubtype) {
+ cpuSubtype != (target->cpuSubtype & ~MachO::CPU_SUBTYPE_MASK)) {
archs.emplace_back(getArchName(cpuType, cpuSubtype));
continue;
}
@@ -580,9 +580,27 @@ void ObjFile::parseRelocations(ArrayRef<SectionHeader> sectionHeaders,
r.pcrel = relInfo.r_pcrel;
r.length = relInfo.r_length;
r.offset = relInfo.r_address;
+
+ // For ARM64e authenticated pointer relocations, extract the auth info
+ // (diversity, key, addrDiv) from the upper bits of the raw pointer value
+ // and store them in the union's authData member.
+ if (target->hasAttr(relInfo.r_type, RelocAttrBits::AUTH)) {
+ const uint8_t *loc = buf + sec.offset + relInfo.r_address;
+ uint64_t raw = read64le(loc);
+ // The auth bit (bit 63) should be set for authenticated pointers
+ if ((raw >> 63) & 1) {
+ r.hasAuth = true;
+ r.authData.addend = isSubtrahend ? 0 : static_cast<int32_t>(totalAddend);
+ r.authData.info.diversity = (raw >> 32) & 0xFFFF;
+ r.authData.info.addrDiv = (raw >> 48) & 0x1;
+ r.authData.info.key = (raw >> 49) & 0x3;
+ }
+ }
+
if (relInfo.r_extern) {
r.referent = symbols[relInfo.r_symbolnum];
- r.addend = isSubtrahend ? 0 : totalAddend;
+ if (!r.hasAuth)
+ r.addend = isSubtrahend ? 0 : totalAddend;
} else {
assert(!isSubtrahend);
const SectionHeader &referentSecHead =
diff --git a/lld/MachO/Target.h b/lld/MachO/Target.h
index 145c14037995a..952d3f7d1e67c 100644
--- a/lld/MachO/Target.h
+++ b/lld/MachO/Target.h
@@ -159,6 +159,7 @@ class TargetInfo {
TargetInfo *createX86_64TargetInfo();
TargetInfo *createARM64TargetInfo();
+TargetInfo *createARM64eTargetInfo();
TargetInfo *createARM64_32TargetInfo();
struct LP64 {
diff --git a/lld/test/MachO/arm64e-auth-data.s b/lld/test/MachO/arm64e-auth-data.s
new file mode 100644
index 0000000000000..7b038a1f69f8b
--- /dev/null
+++ b/lld/test/MachO/arm64e-auth-data.s
@@ -0,0 +1,49 @@
+# REQUIRES: aarch64
+
+## Test that authenticated pointer relocations correctly encode auth metadata
+## (key, diversity, address diversity) through the Relocation union into
+## chained fixup entries. This verifies the union-based auth data storage.
+
+# RUN: rm -rf %t; split-file %s %t
+# RUN: llvm-mc -filetype=obj -triple=arm64e-apple-macos -o %t/foo.o %t/foo.s
+# RUN: llvm-mc -filetype=obj -triple=arm64e-apple-macos -o %t/test.o %t/test.s
+# RUN: %no-arg-lld -arch arm64e -platform_version macos 13.0 13.0 \
+# RUN: -syslibroot %S/Inputs/MacOSX.sdk -lSystem \
+# RUN: -dylib -install_name @executable_path/libfoo.dylib %t/foo.o -o %t/libfoo.dylib
+# RUN: %no-arg-lld -arch arm64e -platform_version macos 13.0 13.0 \
+# RUN: -syslibroot %S/Inputs/MacOSX.sdk -lSystem \
+# RUN: %t/libfoo.dylib %t/test.o -o %t/test
+
+## Verify the binary is valid arm64e with chained fixups.
+# RUN: llvm-objdump --macho --private-header %t/test | FileCheck %s --check-prefix=HEADER
+# RUN: llvm-objdump --macho --chained-fixups %t/test | FileCheck %s --check-prefix=FIXUPS
+
+# HEADER: ARM64 E
+
+## Verify chained fixups use the ARM64E_USERLAND24 format and import _foo.
+# FIXUPS: pointer_format = 12 (DYLD_CHAINED_PTR_ARM64E_USERLAND24)
+# FIXUPS: _foo
+
+## Verify the data section contains non-zero content (the auth pointer
+## should have been encoded, not left as zero).
+# RUN: llvm-objdump --macho -s --section __DATA,__data %t/test | FileCheck %s --check-prefix=DATA
+# DATA-NOT: 00000000 00000000
+
+#--- foo.s
+.globl _foo
+_foo:
+ ret
+
+#--- test.s
+.text
+.globl _main
+
+.p2align 2
+_main:
+ ret
+
+.data
+.p2align 3
+## Authenticated pointer with IA key, discriminator 0x1234, address diversity.
+_auth_ptr:
+.quad _foo at AUTH(ia,0x1234,addr)
diff --git a/lld/test/MachO/arm64e-reject-mixed.s b/lld/test/MachO/arm64e-reject-mixed.s
new file mode 100644
index 0000000000000..7968e8593ad93
--- /dev/null
+++ b/lld/test/MachO/arm64e-reject-mixed.s
@@ -0,0 +1,43 @@
+# REQUIRES: aarch64
+
+## Test that mixing arm64 and arm64e object files is rejected.
+## Even though both have CPU_TYPE_ARM64, arm64e requires pointer
+## authentication and plain arm64 objects would cause PAC failures
+## at runtime.
+
+# RUN: rm -rf %t; split-file %s %t
+# RUN: llvm-mc -filetype=obj -triple=arm64e-apple-macos -o %t/test.o %t/test.s
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-macos -o %t/arm64.o %t/lib.s
+# RUN: llvm-mc -filetype=obj -triple=arm64e-apple-macos -o %t/arm64e.o %t/lib.s
+
+## Linking arm64e main with arm64 object should produce a warning about
+## the architecture mismatch. The arm64 object is rejected, leading to
+## an undefined symbol error.
+# RUN: not %no-arg-lld -arch arm64e -platform_version macos 13.0 13.0 \
+# RUN: -syslibroot %S/Inputs/MacOSX.sdk -lSystem \
+# RUN: %t/test.o %t/arm64.o -o %t/out 2>&1 | FileCheck %s --check-prefix=WARN
+
+# WARN: warning: {{.*}}arm64.o has architecture arm64 which is incompatible with target architecture arm64e (arm64e requires pointer authentication)
+# WARN: error: undefined symbol: _helper
+
+## Linking arm64e main with arm64e object should succeed silently.
+# RUN: %no-arg-lld -arch arm64e -platform_version macos 13.0 13.0 \
+# RUN: -syslibroot %S/Inputs/MacOSX.sdk -lSystem -fatal_warnings \
+# RUN: %t/test.o %t/arm64e.o -o %t/out-ok
+
+#--- test.s
+.text
+.globl _main
+
+.p2align 2
+_main:
+ bl _helper
+ ret
+
+#--- lib.s
+.text
+.globl _helper
+
+.p2align 2
+_helper:
+ ret
>From 1ccf347996e91b0b867010d882d35b42ade720cc Mon Sep 17 00:00:00 2001
From: Oskar Wirga <oskarwirga at meta.com>
Date: Mon, 2 Mar 2026 16:50:44 -0800
Subject: [PATCH 04/12] [lld][MachO] Add AuthGotSection for arm64e
Add the authenticated GOT (__auth_got) section for arm64e pointer
authentication. On arm64e, symbols accessed through authenticated
pointers need a separate GOT section whose entries are signed with
address-diversified PAC. The __auth_got is placed before __got in
section ordering so the linker processes authenticated entries first.
This commit adds the infrastructure: AuthGotSection class, authgot
pointer in InStruct, auth-aware writeChainedRebase/writeChainedFixup
signatures, and the forceOutline parameter for bindings that need
full auth encoding. Population and routing are in follow-up commits.
---
lld/MachO/OutputSegment.cpp | 1 +
lld/MachO/SyntheticSections.h | 25 +++++++++++++++++++------
2 files changed, 20 insertions(+), 6 deletions(-)
diff --git a/lld/MachO/OutputSegment.cpp b/lld/MachO/OutputSegment.cpp
index 5824b5c7772b3..6d8eadb703f5a 100644
--- a/lld/MachO/OutputSegment.cpp
+++ b/lld/MachO/OutputSegment.cpp
@@ -148,6 +148,7 @@ static int sectionOrder(OutputSection *osec) {
return std::numeric_limits<int>::max();
default:
return StringSwitch<int>(osec->name)
+ .Case(section_names::authGot, -4)
.Case(section_names::got, -3)
.Case(section_names::lazySymbolPtr, -2)
.Case(section_names::const_, -1)
diff --git a/lld/MachO/SyntheticSections.h b/lld/MachO/SyntheticSections.h
index a37dd66107ee7..f9499c4641d32 100644
--- a/lld/MachO/SyntheticSections.h
+++ b/lld/MachO/SyntheticSections.h
@@ -14,6 +14,7 @@
#include "InputSection.h"
#include "OutputSection.h"
#include "OutputSegment.h"
+#include "Relocations.h"
#include "Target.h"
#include "Writer.h"
@@ -115,7 +116,8 @@ class PageZeroSection final : public SyntheticSection {
// TLVPointerSection stores references to thread-local variables.
class NonLazyPointerSectionBase : public SyntheticSection {
public:
- NonLazyPointerSectionBase(const char *segname, const char *name);
+ NonLazyPointerSectionBase(const char *segname, const char *name,
+ bool isAuth = false);
const llvm::SetVector<const Symbol *> &getEntries() const { return entries; }
bool isNeeded() const override { return !entries.empty(); }
uint64_t getSize() const override {
@@ -129,6 +131,7 @@ class NonLazyPointerSectionBase : public SyntheticSection {
private:
llvm::SetVector<const Symbol *> entries;
+ bool isAuth;
};
class GotSection final : public NonLazyPointerSectionBase {
@@ -136,6 +139,11 @@ class GotSection final : public NonLazyPointerSectionBase {
GotSection();
};
+class AuthGotSection final : public NonLazyPointerSectionBase {
+public:
+ AuthGotSection();
+};
+
class TlvPointerSection final : public NonLazyPointerSectionBase {
public:
TlvPointerSection();
@@ -787,13 +795,14 @@ class ChainedFixupsSection final : public LinkEditSection {
locations.emplace_back(isec, offset);
}
void addBinding(const Symbol *dysym, const InputSection *isec,
- uint64_t offset, int64_t addend = 0);
+ uint64_t offset, int64_t addend = 0,
+ bool forceOutline = false);
void setHasNonWeakDefinition() { hasNonWeakDef = true; }
// Returns an (ordinal, inline addend) tuple used by dyld_chained_ptr_64_bind.
- std::pair<uint32_t, uint8_t> getBinding(const Symbol *sym,
- int64_t addend) const;
+ std::pair<uint32_t, uint8_t> getBinding(const Symbol *sym, int64_t addend,
+ bool forceOutline = false) const;
const std::vector<Location> &getLocations() const { return locations; }
@@ -829,8 +838,11 @@ class ChainedFixupsSection final : public LinkEditSection {
llvm::MachO::ChainedImportFormat importFormat;
};
-void writeChainedRebase(uint8_t *buf, uint64_t targetVA);
-void writeChainedFixup(uint8_t *buf, const Symbol *sym, int64_t addend);
+void writeChainedRebase(uint8_t *buf, uint64_t targetVA, uint64_t segmentBase,
+ const Relocation::AuthInfo *ai);
+void writeChainedFixup(uint8_t *buf, const Symbol *sym, int64_t addend,
+ int64_t segmentBase, const Relocation::AuthInfo *ai,
+ uint32_t authEncodingBits = 0);
struct InStruct {
const uint8_t *bufferStart = nullptr;
@@ -847,6 +859,7 @@ struct InStruct {
LazyBindingSection *lazyBinding = nullptr;
ExportSection *exports = nullptr;
GotSection *got = nullptr;
+ AuthGotSection *authgot = nullptr;
TlvPointerSection *tlvPointers = nullptr;
LazyPointerSection *lazyPointers = nullptr;
StubsSection *stubs = nullptr;
>From 4da561a4be2391f9c135581f71c33433cffb2d04 Mon Sep 17 00:00:00 2001
From: Oskar Wirga <oskarwirga at meta.com>
Date: Mon, 2 Mar 2026 17:02:05 -0800
Subject: [PATCH 05/12] [lld][MachO] Route arm64e symbols to authgot
Route symbols to the authenticated GOT (__auth_got) when arm64e pointer
authentication requires it. Symbols that are called (BRANCH relocations)
need stubs, and arm64e stubs use authgot for braa-based authenticated
jumps. A pre-scan of relocations identifies these symbols so that
subsequent GOT_LOAD relocations for the same symbols are also routed
to authgot.
Also routes personality pointers in unwind info to authgot on arm64e,
since exception handling needs authenticated personality function
pointers.
---
lld/MachO/Symbols.cpp | 8 +++++++-
lld/MachO/SyntheticSections.cpp | 26 ++++++++++++++++++--------
lld/MachO/UnwindInfoSection.cpp | 28 ++++++++++++++++++++++------
lld/MachO/Writer.cpp | 33 +++++++++++++++++++++++++++------
lld/MachO/Writer.h | 3 ++-
5 files changed, 76 insertions(+), 22 deletions(-)
diff --git a/lld/MachO/Symbols.cpp b/lld/MachO/Symbols.cpp
index 9faf01e09de05..894cd2f04ad94 100644
--- a/lld/MachO/Symbols.cpp
+++ b/lld/MachO/Symbols.cpp
@@ -49,7 +49,13 @@ uint64_t Symbol::getStubVA() const { return in.stubs->getVA(stubsIndex); }
uint64_t Symbol::getLazyPtrVA() const {
return in.lazyPointers->getVA(stubsIndex);
}
-uint64_t Symbol::getGotVA() const { return in.got->getVA(gotIndex); }
+uint64_t Symbol::getGotVA() const {
+ // For arm64e, symbols may be in authgot instead of regular got.
+ // Check which section contains this symbol.
+ if (in.authgot && in.authgot->getEntries().contains(this))
+ return in.authgot->getVA(gotIndex);
+ return in.got->getVA(gotIndex);
+}
uint64_t Symbol::getTlvVA() const { return in.tlvPointers->getVA(gotIndex); }
Defined::Defined(StringRef name, InputFile *file, InputSection *isec,
diff --git a/lld/MachO/SyntheticSections.cpp b/lld/MachO/SyntheticSections.cpp
index 36d15419a1091..40bd4120d887f 100644
--- a/lld/MachO/SyntheticSections.cpp
+++ b/lld/MachO/SyntheticSections.cpp
@@ -299,17 +299,18 @@ void RebaseSection::writeTo(uint8_t *buf) const {
}
NonLazyPointerSectionBase::NonLazyPointerSectionBase(const char *segname,
- const char *name)
- : SyntheticSection(segname, name) {
+ const char *name,
+ bool isAuth)
+ : SyntheticSection(segname, name), isAuth(isAuth) {
align = target->wordSize;
}
void macho::addNonLazyBindingEntries(const Symbol *sym,
const InputSection *isec, uint64_t offset,
- int64_t addend) {
+ int64_t addend, bool forceOutlineAuth) {
if (config->emitChainedFixups) {
if (needsBinding(sym))
- in.chainedFixups->addBinding(sym, isec, offset, addend);
+ in.chainedFixups->addBinding(sym, isec, offset, addend, forceOutlineAuth);
else if (isa<Defined>(sym))
in.chainedFixups->addRebase(isec, offset);
else
@@ -396,6 +397,12 @@ GotSection::GotSection()
flags = S_NON_LAZY_SYMBOL_POINTERS;
}
+AuthGotSection::AuthGotSection()
+ : NonLazyPointerSectionBase(segment_names::data, section_names::authGot,
+ /*isAuth=*/true) {
+ flags = S_NON_LAZY_SYMBOL_POINTERS;
+}
+
TlvPointerSection::TlvPointerSection()
: NonLazyPointerSectionBase(segment_names::data,
section_names::threadPtrs) {
@@ -2357,9 +2364,10 @@ bool ChainedFixupsSection::isNeeded() const {
void ChainedFixupsSection::addBinding(const Symbol *sym,
const InputSection *isec, uint64_t offset,
- int64_t addend) {
+ int64_t addend, bool forceOutline) {
locations.emplace_back(isec, offset);
- int64_t outlineAddend = (addend < 0 || addend > 0xFF) ? addend : 0;
+ int64_t outlineAddend =
+ (forceOutline || addend < 0 || addend > 0xFF) ? addend : 0;
auto [it, inserted] = bindings.insert(
{{sym, outlineAddend}, static_cast<uint32_t>(bindings.size())});
@@ -2374,8 +2382,10 @@ void ChainedFixupsSection::addBinding(const Symbol *sym,
}
std::pair<uint32_t, uint8_t>
-ChainedFixupsSection::getBinding(const Symbol *sym, int64_t addend) const {
- int64_t outlineAddend = (addend < 0 || addend > 0xFF) ? addend : 0;
+ChainedFixupsSection::getBinding(const Symbol *sym, int64_t addend,
+ bool forceOutline) const {
+ int64_t outlineAddend =
+ (forceOutline || addend < 0 || addend > 0xFF) ? addend : 0;
auto it = bindings.find({sym, outlineAddend});
assert(it != bindings.end() && "binding not found in the imports table");
if (outlineAddend == 0)
diff --git a/lld/MachO/UnwindInfoSection.cpp b/lld/MachO/UnwindInfoSection.cpp
index 9775a723a92fd..615d77ba1fca0 100644
--- a/lld/MachO/UnwindInfoSection.cpp
+++ b/lld/MachO/UnwindInfoSection.cpp
@@ -280,7 +280,11 @@ void UnwindInfoSectionImpl::prepareRelocations(ConcatInputSection *isec) {
personalityTable[{defined->isec(), defined->value}];
if (personality == nullptr) {
personality = defined;
- in.got->addEntry(defined);
+ // For arm64e, personality pointers go in the authenticated GOT
+ if (config->arch() == AK_arm64e)
+ in.authgot->addEntry(defined);
+ else
+ in.got->addEntry(defined);
} else if (personality != defined) {
r.referent = personality;
}
@@ -288,7 +292,11 @@ void UnwindInfoSectionImpl::prepareRelocations(ConcatInputSection *isec) {
}
assert(isa<DylibSymbol>(s));
- in.got->addEntry(s);
+ // For arm64e, personality pointers go in the authenticated GOT
+ if (config->arch() == AK_arm64e)
+ in.authgot->addEntry(s);
+ else
+ in.got->addEntry(s);
continue;
}
@@ -319,7 +327,11 @@ void UnwindInfoSectionImpl::prepareRelocations(ConcatInputSection *isec) {
/*isReferencedDynamically=*/false,
/*noDeadStrip=*/false);
s->used = true;
- in.got->addEntry(s);
+ // For arm64e, personality pointers go in the authenticated GOT
+ if (config->arch() == AK_arm64e)
+ in.authgot->addEntry(s);
+ else
+ in.got->addEntry(s);
}
}
r.referent = s;
@@ -637,9 +649,13 @@ void UnwindInfoSectionImpl::writeTo(uint8_t *buf) const {
for (const auto &encoding : commonEncodings)
*i32p++ = encoding.first;
- // Personalities
- for (const Symbol *personality : personalities)
- *i32p++ = personality->getGotVA() - in.header->addr;
+ // Personalities - for arm64e, use authgot instead of got
+ for (const Symbol *personality : personalities) {
+ uint64_t personalityVA = config->arch() == AK_arm64e
+ ? in.authgot->getVA(personality->gotIndex)
+ : personality->getGotVA();
+ *i32p++ = personalityVA - in.header->addr;
+ }
// FIXME: LD64 checks and warns aboutgaps or overlapse in cuEntries address
// ranges. We should do the same too
diff --git a/lld/MachO/Writer.cpp b/lld/MachO/Writer.cpp
index f9fd12a13dba3..fe0cae02a1ebb 100644
--- a/lld/MachO/Writer.cpp
+++ b/lld/MachO/Writer.cpp
@@ -24,6 +24,7 @@
#include "lld/Common/Arrays.h"
#include "lld/Common/CommonLinkerContext.h"
+#include "llvm/ADT/DenseSet.h"
#include "llvm/BinaryFormat/MachO.h"
#include "llvm/Config/llvm-config.h"
#include "llvm/Support/Parallel.h"
@@ -681,8 +682,19 @@ static void prepareSymbolRelocation(Symbol *sym, const InputSection *isec,
if (needsBinding(sym))
in.stubs->addEntry(sym);
} else if (relocAttrs.hasAttr(RelocAttrBits::GOT)) {
- if (relocAttrs.hasAttr(RelocAttrBits::POINTER) || needsBinding(sym))
- in.got->addEntry(sym);
+ if (relocAttrs.hasAttr(RelocAttrBits::POINTER) || needsBinding(sym)) {
+ // GOT_LOAD consumers apply their own signing (paciza) and need a
+ // raw pointer from regular __got. Stubs independently add to
+ // __auth_got via StubsSection::addEntry(). AUTH relocations or
+ // eh_frame personality pointers go to authgot.
+ bool needsAuthGot =
+ relocAttrs.hasAttr(RelocAttrBits::AUTH) ||
+ (config->arch() == AK_arm64e && isEhFrameSection(isec));
+ if (needsAuthGot)
+ in.authgot->addEntry(sym);
+ else
+ in.got->addEntry(sym);
+ }
} else if (relocAttrs.hasAttr(RelocAttrBits::TLV)) {
if (needsBinding(sym))
in.tlvPointers->addEntry(sym);
@@ -690,8 +702,10 @@ static void prepareSymbolRelocation(Symbol *sym, const InputSection *isec,
// References from thread-local variable sections are treated as offsets
// relative to the start of the referent section, and therefore have no
// need of rebase opcodes.
- if (!(isThreadLocalVariables(isec->getFlags()) && isa<Defined>(sym)))
- addNonLazyBindingEntries(sym, isec, r.offset, r.addend);
+ if (!(isThreadLocalVariables(isec->getFlags()) && isa<Defined>(sym))) {
+ bool forceOutline = relocAttrs.hasAttr(RelocAttrBits::AUTH);
+ addNonLazyBindingEntries(sym, isec, r.offset, r.addend, forceOutline);
+ }
}
}
@@ -1284,8 +1298,14 @@ void Writer::buildFixupChains() {
" is not a multiple of the stride). Re-link with -no_fixup_chains");
// The "next" field is in the same location for bind and rebase entries.
- reinterpret_cast<dyld_chained_ptr_64_bind *>(buf + loc[i - 1].offset)
- ->next = offset / stride;
+ if (config->arch() == AK_arm64e) {
+ reinterpret_cast<dyld_chained_ptr_arm64e_auth_bind *>(buf +
+ loc[i - 1].offset)
+ ->next = offset / 8;
+ } else {
+ reinterpret_cast<dyld_chained_ptr_64_bind *>(buf + loc[i - 1].offset)
+ ->next = offset / stride;
+ }
++i;
}
}
@@ -1395,6 +1415,7 @@ void macho::createSyntheticSections() {
}
in.exports = make<ExportSection>();
in.got = make<GotSection>();
+ in.authgot = make<AuthGotSection>();
in.tlvPointers = make<TlvPointerSection>();
in.stubs = make<StubsSection>();
in.objcStubs = make<ObjCStubsSection>();
diff --git a/lld/MachO/Writer.h b/lld/MachO/Writer.h
index 066a0fd5fd3aa..aa24df9f0912a 100644
--- a/lld/MachO/Writer.h
+++ b/lld/MachO/Writer.h
@@ -31,7 +31,8 @@ void createSyntheticSections();
// Add bindings for symbols that need weak or non-lazy bindings.
void addNonLazyBindingEntries(const Symbol *, const InputSection *,
- uint64_t offset, int64_t addend = 0);
+ uint64_t offset, int64_t addend = 0,
+ bool forceOutlineAuth = false);
extern OutputSection *firstTLVDataSection;
>From 66617f367f02e0deedb277c73773e36f0e30a85c Mon Sep 17 00:00:00 2001
From: Oskar Wirga <oskarwirga at meta.com>
Date: Mon, 2 Mar 2026 17:08:04 -0800
Subject: [PATCH 06/12] [lld][MachO] Implement arm64e chained fixup encoding
and dual GOT support
Implement the chained fixup encoding for arm64e using the
DYLD_CHAINED_PTR_ARM64E_USERLAND24 format, and add dual GOT support
for symbols that need both authenticated and raw GOT entries.
Chained fixup encoding:
- Auth rebases use dyld_chained_ptr_arm64e_auth_rebase with PAC
key/diversity/addrDiv fields
- Auth binds use dyld_chained_ptr_arm64e_auth_bind24 (24-bit ordinal)
- Non-auth arm64e fixups use corresponding non-auth structs
- Handles authEncodingBits for C++ RTTI pointers in UNSIGNED relocs
Dual GOT routing:
- Symbols with both BRANCH and GOT_LOAD relocations need different
GOT entries: BRANCH -> __auth_got (braa), GOT_LOAD -> __got (paciza)
- Adds authGotIndex alongside gotIndex in Symbol
- GOT_LOAD resolves through __got to prevent double-signing (Crash 7)
---
lld/MachO/Arch/ARM64e.cpp | 2 +-
lld/MachO/Driver.cpp | 2 +
lld/MachO/InputFiles.cpp | 12 +-
lld/MachO/InputSection.cpp | 24 ++-
lld/MachO/MapFile.cpp | 4 +-
lld/MachO/Symbols.cpp | 22 ++-
lld/MachO/Symbols.h | 9 +-
lld/MachO/SyntheticSections.cpp | 245 +++++++++++++++++++++---
lld/MachO/SyntheticSections.h | 9 +-
lld/MachO/UnwindInfoSection.cpp | 2 +-
lld/MachO/Writer.cpp | 3 +-
lld/test/MachO/arm64e-auth-got.s | 51 +++++
lld/test/MachO/arm64e-auth-reloc.s | 46 +++++
lld/test/MachO/arm64e-chained-fixups.s | 52 +++++
lld/test/MachO/arm64e-no-fixup-chains.s | 21 ++
lld/test/MachO/arm64e-stubs.s | 51 +++++
16 files changed, 510 insertions(+), 45 deletions(-)
create mode 100644 lld/test/MachO/arm64e-auth-got.s
create mode 100644 lld/test/MachO/arm64e-auth-reloc.s
create mode 100644 lld/test/MachO/arm64e-chained-fixups.s
create mode 100644 lld/test/MachO/arm64e-no-fixup-chains.s
create mode 100644 lld/test/MachO/arm64e-stubs.s
diff --git a/lld/MachO/Arch/ARM64e.cpp b/lld/MachO/Arch/ARM64e.cpp
index 4c7b5b5f868d8..65d3e572e7fed 100644
--- a/lld/MachO/Arch/ARM64e.cpp
+++ b/lld/MachO/Arch/ARM64e.cpp
@@ -156,7 +156,7 @@ void ARM64e::writeObjCMsgSendStub(uint8_t *buf, Symbol *sym, uint64_t stubsAddr,
objcStubSize = target->objcStubsFastSize;
// ARM64e uses authgot for objc_msgSend.
objcMsgSendAddr = in.authgot->addr;
- objcMsgSendIndex = objcMsgSend->gotIndex;
+ objcMsgSendIndex = objcMsgSend->authGotIndex;
::writeObjCMsgSendFastStub<LP64>(buf, objcStubsFastCode, sym, stubsAddr,
stubOffset, selrefVA, objcMsgSendAddr,
objcMsgSendIndex);
diff --git a/lld/MachO/Driver.cpp b/lld/MachO/Driver.cpp
index 7b8557b94e0da..8b85a7ae33d37 100644
--- a/lld/MachO/Driver.cpp
+++ b/lld/MachO/Driver.cpp
@@ -1980,6 +1980,8 @@ bool link(ArrayRef<const char *> argsArr, llvm::raw_ostream &stdoutOS,
config->emitDataInCodeInfo =
args.hasFlag(OPT_data_in_code_info, OPT_no_data_in_code_info, true);
config->emitChainedFixups = shouldEmitChainedFixups(args);
+ if (config->arch() == AK_arm64e && !config->emitChainedFixups)
+ error("arm64e requires chained fixups; cannot use -no_fixup_chains");
config->emitInitOffsets =
config->emitChainedFixups || args.hasArg(OPT_init_offsets);
config->emitRelativeMethodLists = shouldEmitRelativeMethodLists(args);
diff --git a/lld/MachO/InputFiles.cpp b/lld/MachO/InputFiles.cpp
index 5e4cbae158944..b9b76f1aa2259 100644
--- a/lld/MachO/InputFiles.cpp
+++ b/lld/MachO/InputFiles.cpp
@@ -201,6 +201,15 @@ static bool compatWithTargetArch(const InputFile *file, const Header *hdr) {
return false;
}
+ // Reject arm64 objects when linking for arm64e.
+ if (config->arch() == AK_arm64e && hdr->cputype == CPU_TYPE_ARM64 &&
+ (hdr->cpusubtype & ~CPU_SUBTYPE_MASK) == CPU_SUBTYPE_ARM64_ALL) {
+ warn(toString(file) +
+ " has architecture arm64 which is incompatible with "
+ "target architecture arm64e (arm64e requires pointer authentication)");
+ return false;
+ }
+
return checkCompatibility(file);
}
@@ -590,7 +599,8 @@ void ObjFile::parseRelocations(ArrayRef<SectionHeader> sectionHeaders,
// The auth bit (bit 63) should be set for authenticated pointers
if ((raw >> 63) & 1) {
r.hasAuth = true;
- r.authData.addend = isSubtrahend ? 0 : static_cast<int32_t>(totalAddend);
+ r.authData.addend =
+ isSubtrahend ? 0 : static_cast<int32_t>(totalAddend);
r.authData.info.diversity = (raw >> 32) & 0xFFFF;
r.authData.info.addrDiv = (raw >> 48) & 0x1;
r.authData.info.key = (raw >> 49) & 0x3;
diff --git a/lld/MachO/InputSection.cpp b/lld/MachO/InputSection.cpp
index 34847adc85954..04dd0ded61b52 100644
--- a/lld/MachO/InputSection.cpp
+++ b/lld/MachO/InputSection.cpp
@@ -101,7 +101,14 @@ static uint64_t resolveSymbolOffsetVA(const Symbol *sym, uint8_t type,
// There's no meaningful way to "interpose" an interior offset.
symVA = (offset != 0) ? sym->getVA() : sym->resolveBranchVA();
} else if (relocAttrs.hasAttr(RelocAttrBits::GOT)) {
- symVA = sym->resolveGotVA();
+ // GOT_LOAD (no POINTER attr) should use regular __got when available,
+ // because the compiler applies paciza on the loaded value and needs
+ // a raw (non-auth) pointer. POINTER_TO_GOT (has POINTER attr) should
+ // use __auth_got (the default from getGotVA/resolveGotVA).
+ if (!relocAttrs.hasAttr(RelocAttrBits::POINTER) && sym->isInGot())
+ symVA = in.got->getVA(sym->gotIndex);
+ else
+ symVA = sym->resolveGotVA();
} else if (relocAttrs.hasAttr(RelocAttrBits::TLV)) {
symVA = sym->resolveTlvVA();
} else {
@@ -253,7 +260,7 @@ void ConcatInputSection::writeTo(uint8_t *buf) {
target->handleDtraceReloc(referentSym, r, loc);
continue;
}
- referentVA = resolveSymbolOffsetVA(referentSym, r.type, r.addend);
+ referentVA = resolveSymbolOffsetVA(referentSym, r.type, r.getAddend());
if (isThreadLocalVariables(getFlags()) && isa<Defined>(referentSym)) {
// References from thread-local variable sections are treated as offsets
@@ -262,15 +269,22 @@ void ConcatInputSection::writeTo(uint8_t *buf) {
// contiguous).
referentVA -= firstTLVDataSection->addr;
} else if (needsFixup) {
- writeChainedFixup(loc, referentSym, r.addend);
+ writeChainedFixup(loc, referentSym, r);
continue;
}
} else if (auto *referentIsec = r.referent.dyn_cast<InputSection *>()) {
assert(!::shouldOmitFromOutput(referentIsec));
- referentVA = referentIsec->getVA(r.addend);
+ referentVA = referentIsec->getVA(r.getAddend());
if (needsFixup) {
- writeChainedRebase(loc, referentVA);
+ AuthInfo aiStorage;
+ const AuthInfo *ai = nullptr;
+ if (r.hasAuth) {
+ aiStorage = r.getAuthInfo();
+ ai = &aiStorage;
+ }
+ uint64_t segmentBase = referentIsec->parent->parent->addr;
+ writeChainedRebase(loc, referentVA, segmentBase, ai);
continue;
}
}
diff --git a/lld/MachO/MapFile.cpp b/lld/MachO/MapFile.cpp
index 29ebcdcf9a832..5b77d6cd8b604 100644
--- a/lld/MachO/MapFile.cpp
+++ b/lld/MachO/MapFile.cpp
@@ -149,7 +149,9 @@ static void printNonLazyPointerSection(raw_fd_ostream &os,
// associations.
for (const Symbol *sym : osec->getEntries())
os << format("0x%08llX\t0x%08zX\t[ 0] non-lazy-pointer-to-local: %s\n",
- osec->addr + sym->gotIndex * target->wordSize,
+ osec->addr +
+ (osec->getIsAuth() ? sym->authGotIndex : sym->gotIndex) *
+ target->wordSize,
target->wordSize, sym->getName().str().data());
}
diff --git a/lld/MachO/Symbols.cpp b/lld/MachO/Symbols.cpp
index 894cd2f04ad94..60499ffa3fbb5 100644
--- a/lld/MachO/Symbols.cpp
+++ b/lld/MachO/Symbols.cpp
@@ -15,13 +15,13 @@ using namespace llvm;
using namespace lld;
using namespace lld::macho;
-static_assert(sizeof(void *) != 8 || sizeof(Symbol) == 56,
+static_assert(sizeof(void *) != 8 || sizeof(Symbol) == 64,
"Try to minimize Symbol's size; we create many instances");
// The Microsoft ABI doesn't support using parent class tail padding for child
// members, hence the _MSC_VER check.
#if !defined(_MSC_VER)
-static_assert(sizeof(void *) != 8 || sizeof(Defined) == 88,
+static_assert(sizeof(void *) != 8 || sizeof(Defined) == 96,
"Try to minimize Defined's size; we create many instances");
#endif
@@ -50,11 +50,19 @@ uint64_t Symbol::getLazyPtrVA() const {
return in.lazyPointers->getVA(stubsIndex);
}
uint64_t Symbol::getGotVA() const {
- // For arm64e, symbols may be in authgot instead of regular got.
- // Check which section contains this symbol.
- if (in.authgot && in.authgot->getEntries().contains(this))
- return in.authgot->getVA(gotIndex);
- return in.got->getVA(gotIndex);
+ // On arm64e, a symbol may be in both __got and __auth_got.
+ // Prefer __auth_got -- this is used by POINTER_TO_GOT relocations
+ // (eh_frame personality pointers) which need the auth_got address.
+ // GOT_LOAD relocations use getNonAuthGotVA() instead.
+ if (isInAuthGot())
+ return in.authgot->getVA(authGotIndex);
+ if (isInGot())
+ return in.got->getVA(gotIndex);
+ llvm_unreachable("symbol not in any GOT section");
+}
+uint64_t Symbol::getAuthGotVA() const {
+ assert(isInAuthGot());
+ return in.authgot->getVA(authGotIndex);
}
uint64_t Symbol::getTlvVA() const { return in.tlvPointers->getVA(gotIndex); }
diff --git a/lld/MachO/Symbols.h b/lld/MachO/Symbols.h
index beb97b35bf881..619127b71e6bb 100644
--- a/lld/MachO/Symbols.h
+++ b/lld/MachO/Symbols.h
@@ -68,24 +68,31 @@ class Symbol {
// Whether this symbol is in the GOT or TLVPointer sections.
bool isInGot() const { return gotIndex != UINT32_MAX; }
+ // Whether this symbol is in the AuthGotSection (arm64e).
+ bool isInAuthGot() const { return authGotIndex != UINT32_MAX; }
+
// Whether this symbol is in the StubsSection.
bool isInStubs() const { return stubsIndex != UINT32_MAX; }
uint64_t getStubVA() const;
uint64_t getLazyPtrVA() const;
uint64_t getGotVA() const;
+ uint64_t getAuthGotVA() const;
uint64_t getTlvVA() const;
uint64_t resolveBranchVA() const {
assert(isa<Defined>(this) || isa<DylibSymbol>(this));
return isInStubs() ? getStubVA() : getVA();
}
- uint64_t resolveGotVA() const { return isInGot() ? getGotVA() : getVA(); }
+ uint64_t resolveGotVA() const {
+ return (isInGot() || isInAuthGot()) ? getGotVA() : getVA();
+ }
uint64_t resolveTlvVA() const { return isInGot() ? getTlvVA() : getVA(); }
// The index of this symbol in the GOT or the TLVPointer section, depending
// on whether it is a thread-local. A given symbol cannot be referenced by
// both these sections at once.
uint32_t gotIndex = UINT32_MAX;
+ uint32_t authGotIndex = UINT32_MAX;
uint32_t lazyBindOffset = UINT32_MAX;
uint32_t stubsHelperIndex = UINT32_MAX;
uint32_t stubsIndex = UINT32_MAX;
diff --git a/lld/MachO/SyntheticSections.cpp b/lld/MachO/SyntheticSections.cpp
index 40bd4120d887f..85cad00091e4a 100644
--- a/lld/MachO/SyntheticSections.cpp
+++ b/lld/MachO/SyntheticSections.cpp
@@ -101,6 +101,11 @@ static uint32_t cpuSubtype() {
config->platformInfo.target.MinDeployment >= VersionTuple(10, 5))
subtype |= CPU_SUBTYPE_LIB64;
+ // arm64e dylibs/bundles use ptrauth version 0.
+ if (config->arch() == AK_arm64e &&
+ (config->outputType == MH_DYLIB || config->outputType == MH_BUNDLE))
+ subtype = CPU_SUBTYPE_ARM64E_WITH_PTRAUTH_VERSION(0, false);
+
return subtype;
}
@@ -337,16 +342,98 @@ void macho::addNonLazyBindingEntries(const Symbol *sym,
void NonLazyPointerSectionBase::addEntry(Symbol *sym) {
if (entries.insert(sym)) {
- assert(!sym->isInGot());
- sym->gotIndex = entries.size() - 1;
+ // On arm64e, a symbol can be in both __got and __auth_got.
+ // Use the appropriate index field based on which section this is.
+ if (isAuth) {
+ assert(!sym->isInAuthGot());
+ sym->authGotIndex = entries.size() - 1;
+ addNonLazyBindingEntries(sym, isec, sym->authGotIndex * target->wordSize,
+ 0, isAuth);
+ } else {
+ assert(!sym->isInGot());
+ sym->gotIndex = entries.size() - 1;
+ addNonLazyBindingEntries(sym, isec, sym->gotIndex * target->wordSize, 0,
+ isAuth);
+ }
+ }
+}
+
+// Determine the chained fixup pointer format for arm64e based on platform and
+// deployment target.
+static uint16_t getArm64ePointerFormat() {
+ using namespace llvm::MachO;
- addNonLazyBindingEntries(sym, isec, sym->gotIndex * target->wordSize);
+ const auto &platformInfo = config->platformInfo;
+ const VersionTuple &minVersion = platformInfo.target.MinDeployment;
+
+ switch (config->platform()) {
+ case PLATFORM_MACOS:
+ case PLATFORM_MACCATALYST:
+ if (minVersion >= VersionTuple(12, 0))
+ return DYLD_CHAINED_PTR_ARM64E_USERLAND24;
+ break;
+ case PLATFORM_IOS:
+ case PLATFORM_IOSSIMULATOR:
+ if (minVersion >= VersionTuple(15, 0))
+ return DYLD_CHAINED_PTR_ARM64E_USERLAND24;
+ break;
+ case PLATFORM_TVOS:
+ case PLATFORM_TVOSSIMULATOR:
+ if (minVersion >= VersionTuple(15, 0))
+ return DYLD_CHAINED_PTR_ARM64E_USERLAND24;
+ break;
+ case PLATFORM_WATCHOS:
+ case PLATFORM_WATCHOSSIMULATOR:
+ if (minVersion >= VersionTuple(8, 0))
+ return DYLD_CHAINED_PTR_ARM64E_USERLAND24;
+ break;
+ default:
+ break;
}
+
+ return DYLD_CHAINED_PTR_ARM64E;
}
-void macho::writeChainedRebase(uint8_t *buf, uint64_t targetVA) {
+void macho::writeChainedRebase(uint8_t *buf, uint64_t targetVA,
+ uint64_t segmentBase, const AuthInfo *ai) {
assert(config->emitChainedFixups);
assert(target->wordSize == 8 && "Only 64-bit platforms are supported");
+ if (config->arch() == AK_arm64e) {
+ uint16_t pointerFormat = getArm64ePointerFormat();
+ bool useUserland24 = (pointerFormat == DYLD_CHAINED_PTR_ARM64E_USERLAND24);
+
+ if (!ai) {
+ auto *rebase = reinterpret_cast<dyld_chained_ptr_arm64e_rebase *>(buf);
+ uint64_t targetValue;
+ if (useUserland24) {
+ targetValue = targetVA - in.header->addr;
+ } else {
+ targetValue = targetVA;
+ }
+ rebase->target = targetValue & 0x7ff'ffff'ffff;
+ rebase->high8 = (targetVA >> 56);
+ rebase->next = 0;
+ rebase->bind = 0;
+ rebase->auth = 0;
+ return;
+ }
+ auto *rebase = reinterpret_cast<dyld_chained_ptr_arm64e_auth_rebase *>(buf);
+ uint64_t runtimeOffset = targetVA - in.header->addr;
+ if (runtimeOffset > 0xFFFF'FFFFULL)
+ error("rebase target 0x" + Twine::utohexstr(targetVA) +
+ " is more than 4 GiB away from image base 0x" +
+ Twine::utohexstr(in.header->addr) +
+ " and cannot be encoded in DYLD_CHAINED_PTR_ARM64E");
+
+ rebase->target = runtimeOffset;
+ rebase->diversity = ai->diversity;
+ rebase->addrDiv = ai->addrDiv;
+ rebase->key = ai->key;
+ rebase->next = 0;
+ rebase->bind = 0;
+ rebase->auth = 1;
+ return;
+ }
auto *rebase = reinterpret_cast<dyld_chained_ptr_64_rebase *>(buf);
rebase->target = targetVA & 0xf'ffff'ffff;
rebase->high8 = (targetVA >> 56);
@@ -362,9 +449,66 @@ void macho::writeChainedRebase(uint8_t *buf, uint64_t targetVA) {
" does not fit into chained fixup. Re-link with -no_fixup_chains");
}
-static void writeChainedBind(uint8_t *buf, const Symbol *sym, int64_t addend) {
+static void writeChainedBind(uint8_t *buf, const Symbol *sym, int64_t addend,
+ const AuthInfo *ai) {
assert(config->emitChainedFixups);
assert(target->wordSize == 8 && "Only 64-bit platforms are supported");
+ if (config->arch() == AK_arm64e) {
+ uint16_t pointerFormat = getArm64ePointerFormat();
+ bool useUserland24 = (pointerFormat == DYLD_CHAINED_PTR_ARM64E_USERLAND24);
+
+ if (!ai) {
+ if (useUserland24) {
+ auto *bind = reinterpret_cast<dyld_chained_ptr_arm64e_bind24 *>(buf);
+ auto [ordinal, inlineAddend] =
+ in.chainedFixups->getBinding(sym, addend);
+ bind->ordinal = ordinal;
+ bind->zero = 0;
+ bind->addend = inlineAddend;
+ bind->next = 0;
+ bind->bind = 1;
+ bind->auth = 0;
+ } else {
+ auto *bind = reinterpret_cast<dyld_chained_ptr_arm64e_bind *>(buf);
+ auto [ordinal, inlineAddend] =
+ in.chainedFixups->getBinding(sym, addend);
+ bind->ordinal = ordinal;
+ bind->zero = 0;
+ bind->addend = inlineAddend;
+ bind->next = 0;
+ bind->bind = 1;
+ bind->auth = 0;
+ }
+ return;
+ }
+
+ if (useUserland24) {
+ auto *bind = reinterpret_cast<dyld_chained_ptr_arm64e_auth_bind24 *>(buf);
+ auto [ordinal, _ignore] =
+ in.chainedFixups->getBinding(sym, addend, /*forceOutline=*/true);
+ bind->ordinal = ordinal;
+ bind->zero = 0;
+ bind->diversity = ai->diversity;
+ bind->addrDiv = ai->addrDiv;
+ bind->key = ai->key;
+ bind->next = 0;
+ bind->bind = 1;
+ bind->auth = 1;
+ } else {
+ auto *bind = reinterpret_cast<dyld_chained_ptr_arm64e_auth_bind *>(buf);
+ auto [ordinal, _ignore] =
+ in.chainedFixups->getBinding(sym, addend, /*forceOutline=*/true);
+ bind->ordinal = ordinal;
+ bind->zero = 0;
+ bind->diversity = ai->diversity;
+ bind->addrDiv = ai->addrDiv;
+ bind->key = ai->key;
+ bind->next = 0;
+ bind->bind = 1;
+ bind->auth = 1;
+ }
+ return;
+ }
auto *bind = reinterpret_cast<dyld_chained_ptr_64_bind *>(buf);
auto [ordinal, inlineAddend] = in.chainedFixups->getBinding(sym, addend);
bind->ordinal = ordinal;
@@ -374,17 +518,42 @@ static void writeChainedBind(uint8_t *buf, const Symbol *sym, int64_t addend) {
bind->bind = 1;
}
-void macho::writeChainedFixup(uint8_t *buf, const Symbol *sym, int64_t addend) {
+// Overload for direct use from ConcatInputSection::writeTo with full
+// Relocation.
+void macho::writeChainedFixup(uint8_t *buf, const Symbol *sym,
+ const Relocation &r) {
+ int64_t addend = r.getAddend();
+ const AuthInfo *ai = nullptr;
+ AuthInfo aiStorage;
+ if (r.hasAuth) {
+ aiStorage = r.getAuthInfo();
+ ai = &aiStorage;
+ }
+ if (needsBinding(sym))
+ writeChainedBind(buf, sym, addend, ai);
+ else
+ writeChainedRebase(buf, sym->getVA() + addend, 0, ai);
+}
+
+// Overload for NonLazyPointerSectionBase::writeTo and other callers that
+// construct auth info independently (e.g. GOT entries with default signing).
+void macho::writeChainedFixup(uint8_t *buf, const Symbol *sym, int64_t addend,
+ uint64_t segmentBase, const AuthInfo *ai) {
if (needsBinding(sym))
- writeChainedBind(buf, sym, addend);
+ writeChainedBind(buf, sym, addend, ai);
else
- writeChainedRebase(buf, sym->getVA() + addend);
+ writeChainedRebase(buf, sym->getVA() + addend, segmentBase, ai);
}
void NonLazyPointerSectionBase::writeTo(uint8_t *buf) const {
+ // Auth GOT entries use IA key, diversity=0, address-diversified.
+ static const AuthInfo defaultAuthInfo = {0, 0, true};
if (config->emitChainedFixups) {
- for (const auto &[i, entry] : llvm::enumerate(entries))
- writeChainedFixup(&buf[i * target->wordSize], entry, 0);
+ for (const auto &[i, entry] : llvm::enumerate(entries)) {
+ const AuthInfo *ai = isAuth ? &defaultAuthInfo : nullptr;
+ writeChainedFixup(&buf[i * target->wordSize], entry, 0,
+ this->parent->addr, ai);
+ }
} else {
for (const auto &[i, entry] : llvm::enumerate(entries))
if (auto *defined = dyn_cast<Defined>(entry))
@@ -709,7 +878,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.
@@ -724,8 +895,16 @@ 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) {
+ // For arm64e, stubs use authgot instead of regular got
+ if (config->arch() == AK_arm64e)
+ pointerVA = in.authgot->getVA(sym->authGotIndex);
+ else
+ pointerVA = sym->getGotVA();
+ } else {
+ pointerVA = sym->getLazyPtrVA();
+ }
target->writeStub(buf + off, *sym, pointerVA);
off += target->stubSize;
}
@@ -765,10 +944,14 @@ void StubsSection::addEntry(Symbol *sym) {
if (inserted) {
sym->stubsIndex = entries.size() - 1;
- if (config->emitChainedFixups)
- in.got->addEntry(sym);
- else
+ if (config->emitChainedFixups) {
+ if (config->arch() == AK_arm64e)
+ in.authgot->addEntry(sym);
+ else
+ in.got->addEntry(sym);
+ } else {
addBindingsForStub(sym);
+ }
}
}
@@ -931,8 +1114,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());
+ // For arm64e, use authgot since objc_msgSend requires authenticated calls.
+ if (config->arch() == AK_arm64e)
+ in.authgot->addEntry(objcMsgSend);
+ else
+ in.got->addEntry(objcMsgSend);
+ assert(objcMsgSend->isInGot() || objcMsgSend->isInAuthGot());
} else {
assert(config->objcStubsMode == ObjCStubsMode::small);
// In line with ld64's behavior, when objc_msgSend is a direct symbol,
@@ -1484,23 +1671,25 @@ IndirectSymtabSection::IndirectSymtabSection()
section_names::indirectSymbolTable) {}
uint32_t IndirectSymtabSection::getNumSymbols() const {
- uint32_t size = in.got->getEntries().size() +
- in.tlvPointers->getEntries().size() +
- in.stubs->getEntries().size();
+ uint32_t size =
+ in.got->getEntries().size() + in.authgot->getEntries().size() +
+ in.tlvPointers->getEntries().size() + in.stubs->getEntries().size();
if (!config->emitChainedFixups)
size += in.stubs->getEntries().size();
return size;
}
bool IndirectSymtabSection::isNeeded() const {
- return in.got->isNeeded() || in.tlvPointers->isNeeded() ||
- in.stubs->isNeeded();
+ return in.got->isNeeded() || in.authgot->isNeeded() ||
+ in.tlvPointers->isNeeded() || in.stubs->isNeeded();
}
void IndirectSymtabSection::finalizeContents() {
uint32_t off = 0;
in.got->reserved1 = off;
off += in.got->getEntries().size();
+ in.authgot->reserved1 = off;
+ off += in.authgot->getEntries().size();
in.tlvPointers->reserved1 = off;
off += in.tlvPointers->getEntries().size();
in.stubs->reserved1 = off;
@@ -1522,6 +1711,10 @@ void IndirectSymtabSection::writeTo(uint8_t *buf) const {
write32le(buf + off * sizeof(uint32_t), indirectValue(sym));
++off;
}
+ for (const Symbol *sym : in.authgot->getEntries()) {
+ write32le(buf + off * sizeof(uint32_t), indirectValue(sym));
+ ++off;
+ }
for (const Symbol *sym : in.tlvPointers->getEntries()) {
write32le(buf + off * sizeof(uint32_t), indirectValue(sym));
++off;
@@ -2435,7 +2628,11 @@ size_t ChainedFixupsSection::SegmentInfo::writeTo(uint8_t *buf) const {
segInfo->size = getSize();
segInfo->page_size = target->getPageSize();
// FIXME: Use DYLD_CHAINED_PTR_64_OFFSET on newer OS versions.
- segInfo->pointer_format = DYLD_CHAINED_PTR_64;
+ // Use USERLAND24 format for arm64e on newer deployment targets
+ if (config->arch() == AK_arm64e)
+ segInfo->pointer_format = getArm64ePointerFormat();
+ else
+ segInfo->pointer_format = DYLD_CHAINED_PTR_64;
segInfo->segment_offset = oseg->addr - in.header->addr;
segInfo->max_valid_pointer = 0; // not used on 64-bit
segInfo->page_count = pageStarts.back().first + 1;
diff --git a/lld/MachO/SyntheticSections.h b/lld/MachO/SyntheticSections.h
index f9499c4641d32..f61a43bfe2f68 100644
--- a/lld/MachO/SyntheticSections.h
+++ b/lld/MachO/SyntheticSections.h
@@ -132,6 +132,9 @@ class NonLazyPointerSectionBase : public SyntheticSection {
private:
llvm::SetVector<const Symbol *> entries;
bool isAuth;
+
+public:
+ bool getIsAuth() const { return isAuth; }
};
class GotSection final : public NonLazyPointerSectionBase {
@@ -839,10 +842,10 @@ class ChainedFixupsSection final : public LinkEditSection {
};
void writeChainedRebase(uint8_t *buf, uint64_t targetVA, uint64_t segmentBase,
- const Relocation::AuthInfo *ai);
+ const AuthInfo *ai);
+void writeChainedFixup(uint8_t *buf, const Symbol *sym, const Relocation &r);
void writeChainedFixup(uint8_t *buf, const Symbol *sym, int64_t addend,
- int64_t segmentBase, const Relocation::AuthInfo *ai,
- uint32_t authEncodingBits = 0);
+ uint64_t segmentBase, const AuthInfo *ai);
struct InStruct {
const uint8_t *bufferStart = nullptr;
diff --git a/lld/MachO/UnwindInfoSection.cpp b/lld/MachO/UnwindInfoSection.cpp
index 615d77ba1fca0..8e302cd42a973 100644
--- a/lld/MachO/UnwindInfoSection.cpp
+++ b/lld/MachO/UnwindInfoSection.cpp
@@ -652,7 +652,7 @@ void UnwindInfoSectionImpl::writeTo(uint8_t *buf) const {
// Personalities - for arm64e, use authgot instead of got
for (const Symbol *personality : personalities) {
uint64_t personalityVA = config->arch() == AK_arm64e
- ? in.authgot->getVA(personality->gotIndex)
+ ? in.authgot->getVA(personality->authGotIndex)
: personality->getGotVA();
*i32p++ = personalityVA - in.header->addr;
}
diff --git a/lld/MachO/Writer.cpp b/lld/MachO/Writer.cpp
index fe0cae02a1ebb..f988e0be47ec2 100644
--- a/lld/MachO/Writer.cpp
+++ b/lld/MachO/Writer.cpp
@@ -704,7 +704,8 @@ static void prepareSymbolRelocation(Symbol *sym, const InputSection *isec,
// need of rebase opcodes.
if (!(isThreadLocalVariables(isec->getFlags()) && isa<Defined>(sym))) {
bool forceOutline = relocAttrs.hasAttr(RelocAttrBits::AUTH);
- addNonLazyBindingEntries(sym, isec, r.offset, r.addend, forceOutline);
+ addNonLazyBindingEntries(sym, isec, r.offset, r.getAddend(),
+ forceOutline);
}
}
}
diff --git a/lld/test/MachO/arm64e-auth-got.s b/lld/test/MachO/arm64e-auth-got.s
new file mode 100644
index 0000000000000..b5283e043a692
--- /dev/null
+++ b/lld/test/MachO/arm64e-auth-got.s
@@ -0,0 +1,51 @@
+# REQUIRES: aarch64
+
+## Test that arm64e creates __auth_got for stub targets and __got for
+## address-of-function references. A symbol that is both called and
+## has its address taken should appear in both sections.
+
+# RUN: rm -rf %t; split-file %s %t
+# RUN: llvm-mc -filetype=obj -triple=arm64e-apple-macos -o %t/foo.o %t/foo.s
+# RUN: llvm-mc -filetype=obj -triple=arm64e-apple-macos -o %t/test.o %t/test.s
+# RUN: %no-arg-lld -arch arm64e -platform_version macos 13.0 13.0 \
+# RUN: -syslibroot %S/Inputs/MacOSX.sdk -lSystem \
+# RUN: -dylib -install_name @executable_path/libfoo.dylib %t/foo.o -o %t/libfoo.dylib
+# RUN: %no-arg-lld -arch arm64e -platform_version macos 13.0 13.0 \
+# RUN: -syslibroot %S/Inputs/MacOSX.sdk -lSystem \
+# RUN: %t/libfoo.dylib %t/test.o -o %t/test
+
+## Verify both __auth_got and __got sections exist.
+# RUN: llvm-objdump --macho --all-headers %t/test | FileCheck %s --check-prefix=SECTIONS
+
+# SECTIONS: sectname __auth_got
+# SECTIONS-NEXT: segname __DATA_CONST
+# SECTIONS: sectname __got
+# SECTIONS-NEXT: segname __DATA_CONST
+
+## Verify chained fixups contain auth binds (in __auth_got) and
+## regular binds (in __got).
+# RUN: llvm-objdump --macho --chained-fixups %t/test | FileCheck %s --check-prefix=FIXUPS
+
+# FIXUPS: chained fixups header (LC_DYLD_CHAINED_FIXUPS)
+# FIXUPS: pointer_format = 12 (DYLD_CHAINED_PTR_ARM64E_USERLAND24)
+# FIXUPS: _foo
+
+#--- foo.s
+.globl _foo
+_foo:
+ ret
+
+#--- test.s
+.text
+.globl _main
+
+.p2align 2
+_main:
+ ## Call _foo — this creates a stub, which uses __auth_got.
+ bl _foo
+
+ ## Take address of _foo — uses GOT_LOAD, which goes to __got.
+ adrp x0, _foo at GOTPAGE
+ ldr x0, [x0, _foo at GOTPAGEOFF]
+
+ ret
diff --git a/lld/test/MachO/arm64e-auth-reloc.s b/lld/test/MachO/arm64e-auth-reloc.s
new file mode 100644
index 0000000000000..d4e81d4c72c6e
--- /dev/null
+++ b/lld/test/MachO/arm64e-auth-reloc.s
@@ -0,0 +1,46 @@
+# REQUIRES: aarch64
+
+## Test ARM64_RELOC_AUTHENTICATED_POINTER handling.
+## Verify that authenticated pointer relocations (@AUTH) are processed
+## correctly and result in auth chained fixup entries.
+
+# RUN: rm -rf %t; split-file %s %t
+# RUN: llvm-mc -filetype=obj -triple=arm64e-apple-macos -o %t/foo.o %t/foo.s
+# RUN: llvm-mc -filetype=obj -triple=arm64e-apple-macos -o %t/test.o %t/test.s
+# RUN: %no-arg-lld -arch arm64e -platform_version macos 13.0 13.0 \
+# RUN: -syslibroot %S/Inputs/MacOSX.sdk -lSystem \
+# RUN: -dylib -install_name @executable_path/libfoo.dylib %t/foo.o -o %t/libfoo.dylib
+# RUN: %no-arg-lld -arch arm64e -platform_version macos 13.0 13.0 \
+# RUN: -syslibroot %S/Inputs/MacOSX.sdk -lSystem \
+# RUN: %t/libfoo.dylib %t/test.o -o %t/test
+
+## Verify the output is a valid arm64e binary (ARM64 with E subtype).
+# RUN: llvm-objdump --macho --private-header %t/test | FileCheck %s --check-prefix=HEADER
+
+# HEADER: ARM64 E
+
+## Verify chained fixups contain the _foo import.
+# RUN: llvm-objdump --macho --chained-fixups %t/test | FileCheck %s --check-prefix=FIXUPS
+
+# FIXUPS: chained fixups header (LC_DYLD_CHAINED_FIXUPS)
+# FIXUPS: pointer_format = 12 (DYLD_CHAINED_PTR_ARM64E_USERLAND24)
+# FIXUPS: _foo
+
+#--- foo.s
+.globl _foo
+_foo:
+ ret
+
+#--- test.s
+.text
+.globl _main
+
+.p2align 2
+_main:
+ ret
+
+.data
+.p2align 3
+## Authenticated data pointer: sign _foo with IA key, discriminator 42,
+## address diversity enabled.
+.quad _foo at AUTH(ia,42,addr)
diff --git a/lld/test/MachO/arm64e-chained-fixups.s b/lld/test/MachO/arm64e-chained-fixups.s
new file mode 100644
index 0000000000000..527942a9874df
--- /dev/null
+++ b/lld/test/MachO/arm64e-chained-fixups.s
@@ -0,0 +1,52 @@
+# REQUIRES: aarch64
+
+## Test arm64e chained fixup pointer format selection.
+## macOS 12.0+ should use DYLD_CHAINED_PTR_ARM64E_USERLAND24;
+## older deployment targets use DYLD_CHAINED_PTR_ARM64E when
+## chained fixups are explicitly requested.
+
+# RUN: rm -rf %t; split-file %s %t
+# RUN: llvm-mc -filetype=obj -triple=arm64e-apple-macos -o %t/foo.o %t/foo.s
+# RUN: llvm-mc -filetype=obj -triple=arm64e-apple-macos -o %t/test.o %t/test.s
+# RUN: %no-arg-lld -arch arm64e -platform_version macos 13.0 13.0 \
+# RUN: -syslibroot %S/Inputs/MacOSX.sdk -lSystem \
+# RUN: -dylib -install_name @executable_path/libfoo.dylib %t/foo.o -o %t/libfoo.dylib
+
+## Link with macOS 13.0 (>= 12.0) — should use USERLAND24.
+# RUN: %no-arg-lld -arch arm64e -platform_version macos 13.0 13.0 \
+# RUN: -syslibroot %S/Inputs/MacOSX.sdk -lSystem \
+# RUN: %t/libfoo.dylib %t/test.o -o %t/test-new
+# RUN: llvm-objdump --macho --chained-fixups %t/test-new | \
+# RUN: FileCheck %s --check-prefix=USERLAND24
+
+## Link with macOS 11.0 (< 12.0) with explicit -fixup_chains
+## — should use plain DYLD_CHAINED_PTR_ARM64E format.
+# RUN: %no-arg-lld -arch arm64e -platform_version macos 11.0 11.0 \
+# RUN: -syslibroot %S/Inputs/MacOSX.sdk -lSystem -fixup_chains \
+# RUN: %t/libfoo.dylib %t/test.o -o %t/test-old
+# RUN: llvm-objdump --macho --chained-fixups %t/test-old | \
+# RUN: FileCheck %s --check-prefix=PLAIN
+
+# USERLAND24: chained fixups header (LC_DYLD_CHAINED_FIXUPS)
+# USERLAND24: pointer_format = 12 (DYLD_CHAINED_PTR_ARM64E_USERLAND24)
+
+# PLAIN: chained fixups header (LC_DYLD_CHAINED_FIXUPS)
+# PLAIN: pointer_format = 1 (DYLD_CHAINED_PTR_ARM64E)
+
+#--- foo.s
+.globl _foo
+_foo:
+ ret
+
+#--- test.s
+.text
+.globl _main
+
+.p2align 2
+_main:
+ bl _foo
+ ret
+
+.data
+.p2align 3
+.quad _foo
diff --git a/lld/test/MachO/arm64e-no-fixup-chains.s b/lld/test/MachO/arm64e-no-fixup-chains.s
new file mode 100644
index 0000000000000..6bfb796d7d213
--- /dev/null
+++ b/lld/test/MachO/arm64e-no-fixup-chains.s
@@ -0,0 +1,21 @@
+# REQUIRES: aarch64
+
+## Test that arm64e linking fails with a clear error when chained fixups
+## are disabled via -no_fixup_chains, since dyld requires chained fixups
+## for arm64e binaries.
+
+# RUN: rm -rf %t; split-file %s %t
+# RUN: llvm-mc -filetype=obj -triple=arm64e-apple-macos -o %t/test.o %t/test.s
+# RUN: not %no-arg-lld -arch arm64e -platform_version macos 13.0 13.0 \
+# RUN: -syslibroot %S/Inputs/MacOSX.sdk -lSystem \
+# RUN: -no_fixup_chains %t/test.o -o %t/test 2>&1 | FileCheck %s
+
+# CHECK: error: arm64e requires chained fixups; cannot use -no_fixup_chains
+
+#--- test.s
+.text
+.globl _main
+
+.p2align 2
+_main:
+ ret
diff --git a/lld/test/MachO/arm64e-stubs.s b/lld/test/MachO/arm64e-stubs.s
new file mode 100644
index 0000000000000..13ebd043fba99
--- /dev/null
+++ b/lld/test/MachO/arm64e-stubs.s
@@ -0,0 +1,51 @@
+# REQUIRES: aarch64
+
+## Test arm64e authenticated stubs use braa with x17 context.
+## ARM64e stubs are 16 bytes (4 instructions), not 12 like arm64,
+## because they compute the GOT address in x17 for use as the
+## authentication context in the braa instruction.
+##
+## With chained fixups on arm64e, the stubs section is called
+## __auth_stubs and references the __auth_got section.
+
+# RUN: rm -rf %t; split-file %s %t
+# RUN: llvm-mc -filetype=obj -triple=arm64e-apple-macos -o %t/foo.o %t/foo.s
+# RUN: llvm-mc -filetype=obj -triple=arm64e-apple-macos -o %t/test.o %t/test.s
+# RUN: %no-arg-lld -arch arm64e -platform_version macos 13.0 13.0 \
+# RUN: -syslibroot %S/Inputs/MacOSX.sdk -lSystem \
+# RUN: -dylib -install_name @executable_path/libfoo.dylib %t/foo.o -o %t/libfoo.dylib
+# RUN: %no-arg-lld -arch arm64e -platform_version macos 13.0 13.0 \
+# RUN: -syslibroot %S/Inputs/MacOSX.sdk -lSystem \
+# RUN: %t/libfoo.dylib %t/test.o -o %t/test
+# RUN: llvm-objdump --no-print-imm-hex --macho -d --no-show-raw-insn \
+# RUN: --section="__TEXT,__auth_stubs" %t/test | FileCheck %s
+
+## Verify the main function calls through a stub.
+# CHECK-LABEL: _main:
+# CHECK: bl {{.*}} ; symbol stub for: _foo
+
+## Verify the stub uses the arm64e 4-instruction sequence with braa.
+# CHECK-LABEL: Contents of (__TEXT,__auth_stubs) section
+# CHECK-NEXT: {{[0-9a-f]+}}: adrp x17
+# CHECK-NEXT: add x17, x17, {{.*}} ; literal pool symbol address: _foo
+# CHECK-NEXT: ldr x16, [x17]
+# CHECK-NEXT: braa x16, x17
+
+## Verify that the __auth_got section exists in __DATA_CONST.
+# RUN: llvm-objdump --macho --all-headers %t/test | FileCheck %s --check-prefix=HEADERS
+# HEADERS: sectname __auth_got
+# HEADERS-NEXT: segname __DATA_CONST
+
+#--- foo.s
+.globl _foo
+_foo:
+ ret
+
+#--- test.s
+.text
+.globl _main
+
+.p2align 2
+_main:
+ bl _foo
+ ret
>From 7e601ed782d0721af9b76bada1878a9724dcee47 Mon Sep 17 00:00:00 2001
From: Oskar Wirga <oskar.wirga at gmail.com>
Date: Wed, 25 Mar 2026 09:59:21 -0700
Subject: [PATCH 07/12] [lld][MachO] Use bitfield struct for auth pointer
decoding
Replace bare shift/mask operations in InputFiles.cpp with a
arm64e_auth_embedded_pointer bitfield struct defined in MachO.h.
This matches the pattern used by the dyld_chained_ptr_arm64e_*
structs for chained fixup encoding.
---
lld/MachO/InputFiles.cpp | 15 +++++++--------
llvm/include/llvm/BinaryFormat/MachO.h | 12 ++++++++++++
2 files changed, 19 insertions(+), 8 deletions(-)
diff --git a/lld/MachO/InputFiles.cpp b/lld/MachO/InputFiles.cpp
index b9b76f1aa2259..53811950cbfc0 100644
--- a/lld/MachO/InputFiles.cpp
+++ b/lld/MachO/InputFiles.cpp
@@ -591,19 +591,18 @@ void ObjFile::parseRelocations(ArrayRef<SectionHeader> sectionHeaders,
r.offset = relInfo.r_address;
// For ARM64e authenticated pointer relocations, extract the auth info
- // (diversity, key, addrDiv) from the upper bits of the raw pointer value
- // and store them in the union's authData member.
+ // from the in-object bitfields and store in the union's authData member.
if (target->hasAttr(relInfo.r_type, RelocAttrBits::AUTH)) {
const uint8_t *loc = buf + sec.offset + relInfo.r_address;
- uint64_t raw = read64le(loc);
- // The auth bit (bit 63) should be set for authenticated pointers
- if ((raw >> 63) & 1) {
+ auto authPtr =
+ *reinterpret_cast<const arm64e_auth_embedded_pointer *>(loc);
+ if (authPtr.auth) {
r.hasAuth = true;
r.authData.addend =
isSubtrahend ? 0 : static_cast<int32_t>(totalAddend);
- r.authData.info.diversity = (raw >> 32) & 0xFFFF;
- r.authData.info.addrDiv = (raw >> 48) & 0x1;
- r.authData.info.key = (raw >> 49) & 0x3;
+ r.authData.info.diversity = authPtr.diversity;
+ r.authData.info.addrDiv = authPtr.addrDiv;
+ r.authData.info.key = authPtr.key;
}
}
diff --git a/llvm/include/llvm/BinaryFormat/MachO.h b/llvm/include/llvm/BinaryFormat/MachO.h
index 6f0ba5535e144..c2f6587ef5856 100644
--- a/llvm/include/llvm/BinaryFormat/MachO.h
+++ b/llvm/include/llvm/BinaryFormat/MachO.h
@@ -1192,6 +1192,18 @@ struct dyld_chained_ptr_64_rebase {
uint64_t bind : 1; // set to 0
};
+// ARM64_RELOC_AUTHENTICATED_POINTER: in-object representation of an
+// authenticated pointer. The low 32 bits are the addend; the upper bits
+// carry ptrauth metadata.
+struct arm64e_auth_embedded_pointer {
+ uint64_t addend : 32;
+ uint64_t diversity : 16;
+ uint64_t addrDiv : 1;
+ uint64_t key : 2;
+ uint64_t reserved : 12;
+ uint64_t auth : 1; // == 1 for authenticated
+};
+
// DYLD_CHAINED_PTR_ARM64E / DYLD_CHAINED_PTR_ARM64E_USERLAND
struct dyld_chained_ptr_arm64e_rebase {
uint64_t target : 43;
>From 85e2225b0f3cb2ab16482f53a1da8a8866ad2eab Mon Sep 17 00:00:00 2001
From: Oskar Wirga <oskar.wirga at gmail.com>
Date: Wed, 25 Mar 2026 10:00:28 -0700
Subject: [PATCH 08/12] [lld][MachO] Handle authenticated pointers in ICF
Compare auth metadata (diversity, key, addrDiv) explicitly in
equalsConstant() so that sections with different signing schemas
are never folded together. Use getAddend() instead of raw union
access to avoid reading the wrong union member.
---
lld/MachO/ICF.cpp | 20 ++++++++++++----
lld/test/MachO/arm64e-icf.s | 48 +++++++++++++++++++++++++++++++++++++
2 files changed, 63 insertions(+), 5 deletions(-)
create mode 100644 lld/test/MachO/arm64e-icf.s
diff --git a/lld/MachO/ICF.cpp b/lld/MachO/ICF.cpp
index b03e2c5a42e00..c663808a9b99e 100644
--- a/lld/MachO/ICF.cpp
+++ b/lld/MachO/ICF.cpp
@@ -115,6 +115,16 @@ bool ICF::equalsConstant(const ConcatInputSection *ia,
return false;
if (ra.offset != rb.offset)
return false;
+ // For AUTH relocs, both must be AUTH with identical signing metadata.
+ if (ra.hasAuth != rb.hasAuth)
+ return false;
+ if (ra.hasAuth) {
+ AuthInfo aiA = ra.getAuthInfo();
+ AuthInfo aiB = rb.getAuthInfo();
+ if (aiA.diversity != aiB.diversity || aiA.key != aiB.key ||
+ aiA.addrDiv != aiB.addrDiv)
+ return false;
+ }
if (isa<Symbol *>(ra.referent) != isa<Symbol *>(rb.referent))
return false;
@@ -130,13 +140,13 @@ bool ICF::equalsConstant(const ConcatInputSection *ia,
// ICF runs before Undefineds are treated (and potentially converted into
// DylibSymbols).
if (isa<DylibSymbol>(sa) || isa<Undefined>(sa))
- return sa == sb && ra.addend == rb.addend;
+ return sa == sb && ra.getAddend() == rb.getAddend();
assert(isa<Defined>(sa));
const auto *da = cast<Defined>(sa);
const auto *db = cast<Defined>(sb);
if (!da->isec() || !db->isec()) {
assert(da->isAbsolute() && db->isAbsolute());
- return da->value + ra.addend == db->value + rb.addend;
+ return da->value + ra.getAddend() == db->value + rb.getAddend();
}
isecA = da->isec();
valueA = da->value;
@@ -164,7 +174,7 @@ bool ICF::equalsConstant(const ConcatInputSection *ia,
assert(isecA->kind() == isecB->kind());
// We will compare ConcatInputSection contents in equalsVariable.
if (isa<ConcatInputSection>(isecA))
- return ra.addend == rb.addend;
+ return ra.getAddend() == rb.getAddend();
// Else we have two literal sections. References to them are equal iff their
// offsets in the output section are equal.
if (isa<Symbol *>(ra.referent))
@@ -172,10 +182,10 @@ bool ICF::equalsConstant(const ConcatInputSection *ia,
// don't do `getOffset(value + addend)` because value + addend may not be
// a valid offset in the literal section.
return isecA->getOffset(valueA) == isecB->getOffset(valueB) &&
- ra.addend == rb.addend;
+ ra.getAddend() == rb.getAddend();
assert(valueA == 0 && valueB == 0);
// For section relocs, we compare the content at the section offset.
- return isecA->getOffset(ra.addend) == isecB->getOffset(rb.addend);
+ return isecA->getOffset(ra.getAddend()) == isecB->getOffset(rb.getAddend());
};
if (!llvm::equal(ia->relocs, ib->relocs, f))
return false;
diff --git a/lld/test/MachO/arm64e-icf.s b/lld/test/MachO/arm64e-icf.s
new file mode 100644
index 0000000000000..1974ab779905a
--- /dev/null
+++ b/lld/test/MachO/arm64e-icf.s
@@ -0,0 +1,48 @@
+# REQUIRES: aarch64
+
+## Test that ICF works correctly on arm64e binaries containing
+## authenticated pointer relocations. Identical functions should
+## still be folded, and auth relocations in data sections should
+## not cause ICF to crash or misbehave.
+
+# RUN: rm -rf %t; split-file %s %t
+# RUN: llvm-mc -filetype=obj -triple=arm64e-apple-macos -o %t/test.o %t/test.s
+# RUN: %no-arg-lld -arch arm64e -platform_version macos 13.0 13.0 \
+# RUN: -syslibroot %S/Inputs/MacOSX.sdk -lSystem \
+# RUN: --icf=all %t/test.o -o %t/test
+# RUN: llvm-objdump --macho --syms %t/test | FileCheck %s
+
+## _func_a and _func_b have identical bodies (just ret) and should be
+## folded by ICF even in an arm64e binary with auth data present.
+# CHECK-DAG: [[#%x,FUNC:]] l F __TEXT,__text _func_a
+# CHECK-DAG: [[#%x,FUNC]] l F __TEXT,__text _func_b
+
+#--- test.s
+.subsections_via_symbols
+
+.text
+.globl _main
+.p2align 2
+_main:
+ ret
+
+.globl _target
+.p2align 2
+_target:
+ ret
+
+## Two identical functions — should be folded.
+.p2align 2
+_func_a:
+ ret
+
+.p2align 2
+_func_b:
+ ret
+
+## Auth data in a data section — ensures auth relocs don't
+## interfere with ICF processing.
+.data
+.p2align 3
+_auth_ptr:
+ .quad _target at AUTH(ia,42,addr)
>From 424ec651afcd729f6fc39e8d7942d1af0cbca69e Mon Sep 17 00:00:00 2001
From: Oskar Wirga <oskar.wirga at gmail.com>
Date: Wed, 25 Mar 2026 10:01:52 -0700
Subject: [PATCH 09/12] [lld][MachO][NFC] Improve readability in MapFile and
Writer
Extract complex expressions into local variables:
- MapFile: GOT index ternary moved to a local before the format call
- Writer: reinterpret_cast results in buildFixupChains stored in locals
---
lld/MachO/MapFile.cpp | 10 +++++-----
lld/MachO/Writer.cpp | 11 ++++++-----
2 files changed, 11 insertions(+), 10 deletions(-)
diff --git a/lld/MachO/MapFile.cpp b/lld/MachO/MapFile.cpp
index 5b77d6cd8b604..5095f05275c55 100644
--- a/lld/MachO/MapFile.cpp
+++ b/lld/MachO/MapFile.cpp
@@ -147,12 +147,12 @@ static void printNonLazyPointerSection(raw_fd_ostream &os,
// entries to be linker-synthesized. Not sure why they made that decision, but
// I think we can follow suit unless there's demand for better symbol-to-file
// associations.
- for (const Symbol *sym : osec->getEntries())
+ for (const Symbol *sym : osec->getEntries()) {
+ uint32_t idx = osec->getIsAuth() ? sym->authGotIndex : sym->gotIndex;
+ uint64_t symAddr = osec->addr + idx * target->wordSize;
os << format("0x%08llX\t0x%08zX\t[ 0] non-lazy-pointer-to-local: %s\n",
- osec->addr +
- (osec->getIsAuth() ? sym->authGotIndex : sym->gotIndex) *
- target->wordSize,
- target->wordSize, sym->getName().str().data());
+ symAddr, target->wordSize, sym->getName().str().data());
+ }
}
static uint64_t getSymSizeForMap(Defined *sym) {
diff --git a/lld/MachO/Writer.cpp b/lld/MachO/Writer.cpp
index f988e0be47ec2..4f793c31bf7f2 100644
--- a/lld/MachO/Writer.cpp
+++ b/lld/MachO/Writer.cpp
@@ -1299,13 +1299,14 @@ void Writer::buildFixupChains() {
" is not a multiple of the stride). Re-link with -no_fixup_chains");
// The "next" field is in the same location for bind and rebase entries.
+ uint8_t *prev = buf + loc[i - 1].offset;
if (config->arch() == AK_arm64e) {
- reinterpret_cast<dyld_chained_ptr_arm64e_auth_bind *>(buf +
- loc[i - 1].offset)
- ->next = offset / 8;
+ auto *entry =
+ reinterpret_cast<dyld_chained_ptr_arm64e_auth_bind *>(prev);
+ entry->next = offset / 8;
} else {
- reinterpret_cast<dyld_chained_ptr_64_bind *>(buf + loc[i - 1].offset)
- ->next = offset / stride;
+ auto *entry = reinterpret_cast<dyld_chained_ptr_64_bind *>(prev);
+ entry->next = offset / stride;
}
++i;
}
>From 167e791acadb332a61c0461000c3e1b62260174c Mon Sep 17 00:00:00 2001
From: Oskar Wirga <oskar.wirga at gmail.com>
Date: Wed, 25 Mar 2026 11:56:09 -0700
Subject: [PATCH 10/12] [lld][MachO] Use correct chained fixup stride for
arm64e
ARM64E userland formats use 8-byte stride, not 4. Replace the
hardcoded stride constant with a per-architecture value and use
it uniformly in both the stride check and the next-field encoding.
---
lld/MachO/Writer.cpp | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/lld/MachO/Writer.cpp b/lld/MachO/Writer.cpp
index 4f793c31bf7f2..72b93726f48f4 100644
--- a/lld/MachO/Writer.cpp
+++ b/lld/MachO/Writer.cpp
@@ -1272,7 +1272,8 @@ void Writer::buildFixupChains() {
TimeTraceScope timeScope("Build fixup chains");
const uint64_t pageSize = target->getPageSize();
- constexpr uint32_t stride = 4; // for DYLD_CHAINED_PTR_64
+ // All ARM64E userland formats use 8-byte stride; DYLD_CHAINED_PTR_64 uses 4.
+ const uint32_t stride = config->arch() == AK_arm64e ? 8 : 4;
for (size_t i = 0, count = loc.size(); i < count;) {
const OutputSegment *oseg = loc[i].isec->parent->parent;
@@ -1303,7 +1304,7 @@ void Writer::buildFixupChains() {
if (config->arch() == AK_arm64e) {
auto *entry =
reinterpret_cast<dyld_chained_ptr_arm64e_auth_bind *>(prev);
- entry->next = offset / 8;
+ entry->next = offset / stride;
} else {
auto *entry = reinterpret_cast<dyld_chained_ptr_64_bind *>(prev);
entry->next = offset / stride;
>From 8b791b81c8a75a2c07932d400978868ce6a0ee17 Mon Sep 17 00:00:00 2001
From: Oskar Wirga <oskar.wirga at gmail.com>
Date: Mon, 30 Mar 2026 15:32:13 -0700
Subject: [PATCH 11/12] [lld][MachO][NFC] Remove unused DenseSet include from
Writer.cpp
---
lld/MachO/Writer.cpp | 1 -
1 file changed, 1 deletion(-)
diff --git a/lld/MachO/Writer.cpp b/lld/MachO/Writer.cpp
index 72b93726f48f4..6c2591f44a471 100644
--- a/lld/MachO/Writer.cpp
+++ b/lld/MachO/Writer.cpp
@@ -24,7 +24,6 @@
#include "lld/Common/Arrays.h"
#include "lld/Common/CommonLinkerContext.h"
-#include "llvm/ADT/DenseSet.h"
#include "llvm/BinaryFormat/MachO.h"
#include "llvm/Config/llvm-config.h"
#include "llvm/Support/Parallel.h"
>From 701160f62ca99eff39b737b684ae17fd577dc44c Mon Sep 17 00:00:00 2001
From: Oskar Wirga <oskar at wirga.com>
Date: Thu, 2 Apr 2026 21:28:46 -0400
Subject: [PATCH 12/12] [lld][MachO] Address review feedback for arm64e support
- Driver.cpp: Fix cpuSubtype check to match any arm64e variant using
mask instead of listing specific values
- Driver.cpp: Move arm64e chained fixups requirement into
shouldEmitChainedFixups(); warn and force-enable instead of erroring,
matching ld64 behavior
- InputFiles.cpp: Check != CPU_SUBTYPE_ARM64E instead of
== CPU_SUBTYPE_ARM64_ALL to also reject CPU_SUBTYPE_ARM64_V8
- Relocations.h: Add static_assert for AuthReloc size
- Update arm64e-no-fixup-chains.s test for warn behavior
---
lld/MachO/Driver.cpp | 14 ++++++++------
lld/MachO/InputFiles.cpp | 4 ++--
lld/MachO/Relocations.h | 1 +
lld/test/MachO/arm64e-no-fixup-chains.s | 14 +++++++++-----
4 files changed, 20 insertions(+), 13 deletions(-)
diff --git a/lld/MachO/Driver.cpp b/lld/MachO/Driver.cpp
index 8b85a7ae33d37..f27d8cb071070 100644
--- a/lld/MachO/Driver.cpp
+++ b/lld/MachO/Driver.cpp
@@ -951,9 +951,7 @@ static TargetInfo *createTargetInfo(InputArgList &args) {
case CPU_TYPE_X86_64:
return createX86_64TargetInfo();
case CPU_TYPE_ARM64:
- if (cpuSubtype == CPU_SUBTYPE_ARM64E ||
- cpuSubtype == CPU_SUBTYPE_ARM64E_VERSIONED_PTRAUTH_ABI_MASK ||
- cpuSubtype == CPU_SUBTYPE_ARM64E_WITH_PTRAUTH_VERSION(0, 0))
+ if ((cpuSubtype & ~CPU_SUBTYPE_MASK) == CPU_SUBTYPE_ARM64E)
return createARM64eTargetInfo();
return createARM64TargetInfo();
case CPU_TYPE_ARM64_32:
@@ -1247,8 +1245,14 @@ static bool dataConstDefault(const InputArgList &args) {
static bool shouldEmitChainedFixups(const InputArgList &args) {
const Arg *arg = args.getLastArg(OPT_fixup_chains, OPT_no_fixup_chains);
- if (arg && arg->getOption().matches(OPT_no_fixup_chains))
+ if (arg && arg->getOption().matches(OPT_no_fixup_chains)) {
+ if (config->arch() == AK_arm64e) {
+ warn(
+ "-no_fixup_chains is incompatible with arm64e; using chained fixups");
+ return true;
+ }
return false;
+ }
bool requested = arg && arg->getOption().matches(OPT_fixup_chains);
if (!config->isPic) {
@@ -1980,8 +1984,6 @@ bool link(ArrayRef<const char *> argsArr, llvm::raw_ostream &stdoutOS,
config->emitDataInCodeInfo =
args.hasFlag(OPT_data_in_code_info, OPT_no_data_in_code_info, true);
config->emitChainedFixups = shouldEmitChainedFixups(args);
- if (config->arch() == AK_arm64e && !config->emitChainedFixups)
- error("arm64e requires chained fixups; cannot use -no_fixup_chains");
config->emitInitOffsets =
config->emitChainedFixups || args.hasArg(OPT_init_offsets);
config->emitRelativeMethodLists = shouldEmitRelativeMethodLists(args);
diff --git a/lld/MachO/InputFiles.cpp b/lld/MachO/InputFiles.cpp
index 53811950cbfc0..7562a7a70bb6b 100644
--- a/lld/MachO/InputFiles.cpp
+++ b/lld/MachO/InputFiles.cpp
@@ -201,9 +201,9 @@ static bool compatWithTargetArch(const InputFile *file, const Header *hdr) {
return false;
}
- // Reject arm64 objects when linking for arm64e.
+ // Reject non-arm64e objects when linking for arm64e.
if (config->arch() == AK_arm64e && hdr->cputype == CPU_TYPE_ARM64 &&
- (hdr->cpusubtype & ~CPU_SUBTYPE_MASK) == CPU_SUBTYPE_ARM64_ALL) {
+ (hdr->cpusubtype & ~CPU_SUBTYPE_MASK) != CPU_SUBTYPE_ARM64E) {
warn(toString(file) +
" has architecture arm64 which is incompatible with "
"target architecture arm64e (arm64e requires pointer authentication)");
diff --git a/lld/MachO/Relocations.h b/lld/MachO/Relocations.h
index 077a8ac69e9ff..0f2c0c589ff42 100644
--- a/lld/MachO/Relocations.h
+++ b/lld/MachO/Relocations.h
@@ -67,6 +67,7 @@ struct AuthReloc {
int32_t addend;
AuthInfo info;
};
+static_assert(sizeof(AuthReloc) == 8, "AuthReloc must match int64_t size");
struct Relocation {
uint8_t type = llvm::MachO::GENERIC_RELOC_INVALID;
diff --git a/lld/test/MachO/arm64e-no-fixup-chains.s b/lld/test/MachO/arm64e-no-fixup-chains.s
index 6bfb796d7d213..1207cccb43f9a 100644
--- a/lld/test/MachO/arm64e-no-fixup-chains.s
+++ b/lld/test/MachO/arm64e-no-fixup-chains.s
@@ -1,16 +1,20 @@
# REQUIRES: aarch64
-## Test that arm64e linking fails with a clear error when chained fixups
-## are disabled via -no_fixup_chains, since dyld requires chained fixups
-## for arm64e binaries.
+## Test that arm64e linking with -no_fixup_chains produces a warning
+## and uses chained fixups anyway, since dyld requires them for arm64e.
# RUN: rm -rf %t; split-file %s %t
# RUN: llvm-mc -filetype=obj -triple=arm64e-apple-macos -o %t/test.o %t/test.s
-# RUN: not %no-arg-lld -arch arm64e -platform_version macos 13.0 13.0 \
+# RUN: %no-arg-lld -arch arm64e -platform_version macos 13.0 13.0 \
# RUN: -syslibroot %S/Inputs/MacOSX.sdk -lSystem \
# RUN: -no_fixup_chains %t/test.o -o %t/test 2>&1 | FileCheck %s
-# CHECK: error: arm64e requires chained fixups; cannot use -no_fixup_chains
+# CHECK: warning: -no_fixup_chains is incompatible with arm64e; using chained fixups
+
+## Verify the output still has chained fixups.
+# RUN: llvm-objdump --macho --all-headers %t/test | FileCheck %s --check-prefix=HEADERS
+
+# HEADERS: LC_DYLD_CHAINED_FIXUPS
#--- test.s
.text
More information about the llvm-commits
mailing list