[lld] [lld][MachO] Remove `__thread_ptrs` section so TLV can relocate against dynamic-lookup symbols (PR #221364)
Peter Rong via llvm-commits
llvm-commits at lists.llvm.org
Fri Sep 11 13:56:26 PDT 2026
https://github.com/DataCorrupted updated https://github.com/llvm/llvm-project/pull/221364
>From 554fc86d71c4900ef20f06309297676cdb4d48ad Mon Sep 17 00:00:00 2001
From: Peter Rong <PeterRong at meta.com>
Date: Fri, 4 Sep 2026 16:04:28 -0700
Subject: [PATCH 1/4] [lld][MachO] Match ld-prime non-lazy TLV pointers
ld-prime emits one __got entry for an imported TLV descriptor, regardless of whether it is reached through GOT, TLV, stub, or unwind references. Routing known TLVs to __thread_ptrs left direct GOT consumers able to collide with the shared gotIndex and also produced a different section layout.
Use __got as the sole non-lazy pointer section, including for TLV descriptors. Preserve valid descriptor references through GOT, branch, and unsigned relocations, while rejecting direct code relocations to imported TLVs, which have no link-time address.
Add arm64 and x86_64 coverage for pure and mixed references, chained-fixup stubs, compact-unwind personalities, unsigned descriptor pointers, and local versus imported direct references.
---
lld/MachO/InputSection.cpp | 9 +-
lld/MachO/InputSection.h | 1 -
lld/MachO/MapFile.cpp | 5 +-
lld/MachO/Relocations.cpp | 25 +-
lld/MachO/Symbols.cpp | 1 -
lld/MachO/Symbols.h | 18 +-
lld/MachO/SyntheticSections.cpp | 35 +--
lld/MachO/SyntheticSections.h | 21 +-
lld/MachO/Writer.cpp | 13 +-
lld/test/MachO/arm64-reloc-tlv-load.s | 2 +-
lld/test/MachO/flat-namespace-interposable.s | 5 +-
lld/test/MachO/got-to-tlv-reference.s | 128 +++++++++
lld/test/MachO/indirect-symtab.s | 9 +-
.../invalid/bad-got-to-dylib-tlv-reference.s | 23 --
.../MachO/invalid/bad-got-to-tlv-reference.s | 14 -
lld/test/MachO/map-file.s | 12 +-
lld/test/MachO/tapi-link.s | 2 +-
lld/test/MachO/tlv-dylib.s | 17 +-
lld/test/MachO/tlv-dynamic-lookup-x86.s | 52 ++++
lld/test/MachO/tlv-dynamic-lookup.s | 265 ++++++++++++++++++
lld/test/MachO/tlv-non-lazy-pointer.s | 170 +++++++++++
lld/test/MachO/weak-binding.s | 10 +-
lld/test/MachO/weak-reference.s | 8 +-
23 files changed, 703 insertions(+), 142 deletions(-)
create mode 100644 lld/test/MachO/got-to-tlv-reference.s
delete mode 100644 lld/test/MachO/invalid/bad-got-to-dylib-tlv-reference.s
delete mode 100644 lld/test/MachO/invalid/bad-got-to-tlv-reference.s
create mode 100644 lld/test/MachO/tlv-dynamic-lookup-x86.s
create mode 100644 lld/test/MachO/tlv-dynamic-lookup.s
create mode 100644 lld/test/MachO/tlv-non-lazy-pointer.s
diff --git a/lld/MachO/InputSection.cpp b/lld/MachO/InputSection.cpp
index d977830161a8e..beeeb8e7d5c64 100644
--- a/lld/MachO/InputSection.cpp
+++ b/lld/MachO/InputSection.cpp
@@ -100,10 +100,11 @@ uint64_t macho::resolveSymbolOffsetVA(const Symbol *sym, uint8_t type,
// function's layout, which an interposed replacement wouldn't preserve.
// 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();
- } else if (relocAttrs.hasAttr(RelocAttrBits::TLV)) {
- symVA = sym->resolveTlvVA();
+ } else if (relocAttrs.hasAttr(RelocAttrBits::GOT) ||
+ relocAttrs.hasAttr(RelocAttrBits::TLV)) {
+ // Both kinds read the symbol's single non-lazy pointer slot; for a
+ // thread-local that slot holds the address of its TLV descriptor.
+ symVA = sym->resolveNonLazyPtrVA();
} else {
symVA = sym->getVA();
}
diff --git a/lld/MachO/InputSection.h b/lld/MachO/InputSection.h
index 8fcd16a1de35f..49ea57a24129c 100644
--- a/lld/MachO/InputSection.h
+++ b/lld/MachO/InputSection.h
@@ -375,7 +375,6 @@ constexpr const char swift[] = "__swift";
constexpr const char symbolTable[] = "__symbol_table";
constexpr const char textCoalNt[] = "__textcoal_nt";
constexpr const char text[] = "__text";
-constexpr const char threadPtrs[] = "__thread_ptrs";
constexpr const char threadVars[] = "__thread_vars";
constexpr const char unwindInfo[] = "__unwind_info";
constexpr const char weakBinding[] = "__weak_binding";
diff --git a/lld/MachO/MapFile.cpp b/lld/MachO/MapFile.cpp
index 29ebcdcf9a832..f595852ddf81f 100644
--- a/lld/MachO/MapFile.cpp
+++ b/lld/MachO/MapFile.cpp
@@ -141,8 +141,7 @@ static void printStubsEntries(
sym->getName().str().data());
}
-static void printNonLazyPointerSection(raw_fd_ostream &os,
- NonLazyPointerSectionBase *osec) {
+static void printNonLazyPointerSection(raw_fd_ostream &os, GotSection *osec) {
// ld64 considers stubs to belong to particular files, but considers GOT
// 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
@@ -265,8 +264,6 @@ void macho::writeMapFile() {
osec->getSize());
} else if (osec == in.got) {
printNonLazyPointerSection(os, in.got);
- } else if (osec == in.tlvPointers) {
- printNonLazyPointerSection(os, in.tlvPointers);
} else if (osec == in.objcMethList) {
printIsecArrSyms(in.objcMethList->getInputs());
}
diff --git a/lld/MachO/Relocations.cpp b/lld/MachO/Relocations.cpp
index 0945e7a96dee2..f06c5540829ec 100644
--- a/lld/MachO/Relocations.cpp
+++ b/lld/MachO/Relocations.cpp
@@ -68,7 +68,30 @@ bool macho::validateSymbolRelocation(const Symbol *sym,
.str();
};
- if (relocAttrs.hasAttr(RelocAttrBits::TLV) != sym->isTlv())
+ // A GOT relocation against a thread-local is valid: the slot holds the
+ // address of the TLV descriptor, which is what such a reference asks for.
+ // Branch and unsigned relocations can likewise refer to the descriptor via
+ // a stub or pointer. ld-prime accepts all three kinds.
+ //
+ // A direct relocation against an imported TLV is not valid because an
+ // imported descriptor has no link-time address. Conversely, a TLV
+ // relocation against a symbol known not to be thread-local would interpret
+ // the referent's first word as a resolver function. Keep rejecting both.
+ //
+ // A dynamic-lookup symbol has no defining dylib, and Mach-O cannot express
+ // thread-locality on an undefined reference, so its kind is unknowable here.
+ // dyld resolves it at load time; rejecting it would refuse a valid link.
+ const auto *dysym = dyn_cast<DylibSymbol>(sym);
+ const bool tlvKindIsKnown = !(dysym && dysym->isDynamicLookup());
+ const bool isTlvReloc = relocAttrs.hasAttr(RelocAttrBits::TLV);
+ const bool permitsTlvDescriptor = isTlvReloc ||
+ relocAttrs.hasAttr(RelocAttrBits::GOT) ||
+ relocAttrs.hasAttr(RelocAttrBits::BRANCH) ||
+ relocAttrs.hasAttr(RelocAttrBits::UNSIGNED);
+ const bool isImportedTlv = isa<DylibSymbol>(sym) && sym->isTlv();
+
+ if (tlvKindIsKnown && ((isTlvReloc && !sym->isTlv()) ||
+ (isImportedTlv && !permitsTlvDescriptor)))
error(message(Twine("requires that symbol ") + sym->getName() + " " +
(sym->isTlv() ? "not " : "") + "be thread-local"));
diff --git a/lld/MachO/Symbols.cpp b/lld/MachO/Symbols.cpp
index 27419caf9de1e..c743697e4041d 100644
--- a/lld/MachO/Symbols.cpp
+++ b/lld/MachO/Symbols.cpp
@@ -50,7 +50,6 @@ uint64_t Symbol::getLazyPtrVA() const {
return in.lazyPointers->getVA(stubsIndex);
}
uint64_t Symbol::getGotVA() const { return in.got->getVA(gotIndex); }
-uint64_t Symbol::getTlvVA() const { return in.tlvPointers->getVA(gotIndex); }
Defined::Defined(StringRef name, InputFile *file, InputSection *isec,
uint64_t value, uint64_t size, bool isWeakDef, bool isExternal,
diff --git a/lld/MachO/Symbols.h b/lld/MachO/Symbols.h
index 9fc7d6b079058..6b5917e70cd81 100644
--- a/lld/MachO/Symbols.h
+++ b/lld/MachO/Symbols.h
@@ -65,7 +65,9 @@ class Symbol {
virtual bool isTlv() const { return false; }
- // Whether this symbol is in the GOT or TLVPointer sections.
+ // Whether this symbol has a non-lazy pointer slot. A symbol gets at most
+ // one, always in __got (including thread-local symbols).
+ // See Writer::addNonLazyPointerEntry.
bool isInGot() const { return gotIndex != UINT32_MAX; }
// Whether this symbol is in the StubsSection.
@@ -74,17 +76,19 @@ class Symbol {
uint64_t getStubVA() const;
uint64_t getLazyPtrVA() const;
uint64_t getGotVA() 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 resolveTlvVA() const { return isInGot() ? getTlvVA() : getVA(); }
+ // The address of this symbol's non-lazy pointer slot, or the symbol's own
+ // address if it has none. A thread-local's slot holds the address of its
+ // TLV descriptor, which is also what a GOT reference to it wants, so GOT
+ // and TLV relocations both resolve through the same __got entry.
+ uint64_t resolveNonLazyPtrVA() const {
+ return isInGot() ? getGotVA() : 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.
+ // The index of this symbol's non-lazy pointer slot in __got.
uint32_t gotIndex = UINT32_MAX;
uint32_t lazyBindOffset = UINT32_MAX;
uint32_t stubsHelperIndex = UINT32_MAX;
diff --git a/lld/MachO/SyntheticSections.cpp b/lld/MachO/SyntheticSections.cpp
index c4277d54edd38..b8a51ee8b47bf 100644
--- a/lld/MachO/SyntheticSections.cpp
+++ b/lld/MachO/SyntheticSections.cpp
@@ -300,10 +300,10 @@ void RebaseSection::writeTo(uint8_t *buf) const {
memcpy(buf, contents.data(), contents.size());
}
-NonLazyPointerSectionBase::NonLazyPointerSectionBase(const char *segname,
- const char *name)
- : SyntheticSection(segname, name) {
+GotSection::GotSection()
+ : SyntheticSection(segment_names::data, section_names::got) {
align = target->wordSize;
+ flags = S_NON_LAZY_SYMBOL_POINTERS;
}
void macho::addNonLazyBindingEntries(const Symbol *sym,
@@ -336,8 +336,9 @@ void macho::addNonLazyBindingEntries(const Symbol *sym,
}
}
-void NonLazyPointerSectionBase::addEntry(Symbol *sym) {
+void GotSection::addEntry(Symbol *sym) {
if (entries.insert(sym)) {
+ // Every symbol has at most one non-lazy pointer slot.
assert(!sym->isInGot());
sym->gotIndex = entries.size() - 1;
@@ -382,7 +383,7 @@ void macho::writeChainedFixup(uint8_t *buf, const Symbol *sym, int64_t addend) {
writeChainedRebase(buf, sym->getVA() + addend);
}
-void NonLazyPointerSectionBase::writeTo(uint8_t *buf) const {
+void GotSection::writeTo(uint8_t *buf) const {
if (config->emitChainedFixups) {
for (const auto &[i, entry] : llvm::enumerate(entries))
writeChainedFixup(&buf[i * target->wordSize], entry, 0);
@@ -393,17 +394,6 @@ void NonLazyPointerSectionBase::writeTo(uint8_t *buf) const {
}
}
-GotSection::GotSection()
- : NonLazyPointerSectionBase(segment_names::data, section_names::got) {
- flags = S_NON_LAZY_SYMBOL_POINTERS;
-}
-
-TlvPointerSection::TlvPointerSection()
- : NonLazyPointerSectionBase(segment_names::data,
- section_names::threadPtrs) {
- flags = S_THREAD_LOCAL_VARIABLE_POINTERS;
-}
-
BindingSection::BindingSection()
: LinkEditSection(segment_names::linkEdit, section_names::binding) {}
@@ -1497,25 +1487,20 @@ 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.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.stubs->isNeeded();
}
void IndirectSymtabSection::finalizeContents() {
uint32_t off = 0;
in.got->reserved1 = off;
off += in.got->getEntries().size();
- in.tlvPointers->reserved1 = off;
- off += in.tlvPointers->getEntries().size();
in.stubs->reserved1 = off;
if (in.lazyPointers) {
off += in.stubs->getEntries().size();
@@ -1535,10 +1520,6 @@ void IndirectSymtabSection::writeTo(uint8_t *buf) const {
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;
- }
for (const Symbol *sym : in.stubs->getEntries()) {
write32le(buf + off * sizeof(uint32_t), indirectValue(sym));
++off;
diff --git a/lld/MachO/SyntheticSections.h b/lld/MachO/SyntheticSections.h
index 9acbd73d277e8..3b8bed5c21ddb 100644
--- a/lld/MachO/SyntheticSections.h
+++ b/lld/MachO/SyntheticSections.h
@@ -106,13 +106,11 @@ class PageZeroSection final : public SyntheticSection {
void writeTo(uint8_t *buf) const override {}
};
-// This is the base class for the GOT and TLVPointer sections, which are nearly
-// functionally identical -- they will both be populated by dyld with addresses
-// to non-lazily-loaded dylib symbols. The main difference is that the
-// TLVPointerSection stores references to thread-local variables.
-class NonLazyPointerSectionBase : public SyntheticSection {
+// The __DATA_CONST,__got section, populated by dyld with addresses to
+// non-lazily-loaded dylib symbols, including TLV descriptors.
+class GotSection final : public SyntheticSection {
public:
- NonLazyPointerSectionBase(const char *segname, const char *name);
+ GotSection();
const llvm::SetVector<const Symbol *> &getEntries() const { return entries; }
bool isNeeded() const override { return !entries.empty(); }
uint64_t getSize() const override {
@@ -128,16 +126,6 @@ class NonLazyPointerSectionBase : public SyntheticSection {
llvm::SetVector<const Symbol *> entries;
};
-class GotSection final : public NonLazyPointerSectionBase {
-public:
- GotSection();
-};
-
-class TlvPointerSection final : public NonLazyPointerSectionBase {
-public:
- TlvPointerSection();
-};
-
struct Location {
const InputSection *isec;
uint64_t offset;
@@ -845,7 +833,6 @@ struct InStruct {
LazyBindingSection *lazyBinding = nullptr;
ExportSection *exports = nullptr;
GotSection *got = nullptr;
- TlvPointerSection *tlvPointers = nullptr;
LazyPointerSection *lazyPointers = nullptr;
StubsSection *stubs = nullptr;
StubHelperSection *stubHelper = nullptr;
diff --git a/lld/MachO/Writer.cpp b/lld/MachO/Writer.cpp
index 49415ccb70af8..4af7ed150f694 100644
--- a/lld/MachO/Writer.cpp
+++ b/lld/MachO/Writer.cpp
@@ -662,6 +662,14 @@ void Writer::treatSpecialUndefineds() {
}
}
+// Give a symbol its single non-lazy pointer slot. For a thread-local,
+// __thread_ptrs and __got would hold the same value: the address of its TLV
+// descriptor. ld-prime canonicalizes both GOT and TLV references to one
+// __got entry, which also makes the choice independent of which reference is
+// scanned first. This is especially important for dynamic-lookup symbols,
+// whose thread-locality is unknowable until dyld binds the slot.
+static void addNonLazyPointerEntry(Symbol *sym) { in.got->addEntry(sym); }
+
static void prepareSymbolRelocation(Symbol *sym, const InputSection *isec,
const Relocation &r) {
if (!sym->isLive()) {
@@ -682,10 +690,10 @@ static void prepareSymbolRelocation(Symbol *sym, const InputSection *isec,
in.stubs->addEntry(sym);
} else if (relocAttrs.hasAttr(RelocAttrBits::GOT)) {
if (relocAttrs.hasAttr(RelocAttrBits::POINTER) || needsBinding(sym))
- in.got->addEntry(sym);
+ addNonLazyPointerEntry(sym);
} else if (relocAttrs.hasAttr(RelocAttrBits::TLV)) {
if (needsBinding(sym))
- in.tlvPointers->addEntry(sym);
+ addNonLazyPointerEntry(sym);
} else if (relocAttrs.hasAttr(RelocAttrBits::UNSIGNED)) {
// References from thread-local variable sections are treated as offsets
// relative to the start of the referent section, and therefore have no
@@ -1440,7 +1448,6 @@ void macho::createSyntheticSections() {
}
in.exports = make<ExportSection>();
in.got = make<GotSection>();
- in.tlvPointers = make<TlvPointerSection>();
in.stubs = make<StubsSection>();
in.objcStubs = make<ObjCStubsSection>();
in.unwindInfo = makeUnwindInfoSection();
diff --git a/lld/test/MachO/arm64-reloc-tlv-load.s b/lld/test/MachO/arm64-reloc-tlv-load.s
index c525f2db18816..d4cccff25742f 100644
--- a/lld/test/MachO/arm64-reloc-tlv-load.s
+++ b/lld/test/MachO/arm64-reloc-tlv-load.s
@@ -31,7 +31,7 @@
# DYLIB-EMPTY:
# DYLIB-NEXT: Sections:
# DYLIB-NEXT: Idx Name Size VMA Type
-# DYLIB: [[#]] __thread_ptrs 00000010 {{0*}}[[#TLV]] DATA
+# DYLIB: [[#]] __got 00000010 {{0*}}[[#TLV]] DATA
#--- main.s
.globl _main, _foo, _bar
diff --git a/lld/test/MachO/flat-namespace-interposable.s b/lld/test/MachO/flat-namespace-interposable.s
index 950de32726d12..0eabc49a5fc54 100644
--- a/lld/test/MachO/flat-namespace-interposable.s
+++ b/lld/test/MachO/flat-namespace-interposable.s
@@ -67,13 +67,12 @@
# DYLIB-DAG: __DATA __data 0x[[#%.8X, EXTERN_REF]] pointer
# DYLIB-DAG: __DATA __data 0x[[#%.8X, LOCAL_REF]] pointer
# DYLIB-DAG: __DATA __data 0x[[#%.8X, WEAK_REF]] pointer
-# DYLIB-DAG: __DATA __thread_ptrs 0x[[#%.8X, TLV_REF]] pointer
# DYLIB-EMPTY:
# DYLIB-NEXT: Bind table:
# DYLIB-NEXT: segment section address type addend dylib symbol
# DYLIB-DAG: __DATA_CONST __got {{.*}} pointer 0 flat-namespace dyld_stub_binder
# DYLIB-DAG: __DATA __data 0x[[#%.8X, EXTERN_REF]] pointer 0 flat-namespace _extern
-# DYLIB-DAG: __DATA __thread_ptrs 0x[[#%.8X, TLV_REF]] pointer 0 flat-namespace _tlv
+# DYLIB-DAG: __DATA_CONST __got 0x[[#%.8X, TLV_REF]] pointer 0 flat-namespace _tlv
# DYLIB-EMPTY:
# DYLIB-NEXT: Lazy bind table:
# DYLIB-NEXT: segment section address dylib symbol
@@ -92,7 +91,7 @@
# CHAINED-DYLIB-DAG: __DATA __data 0x[[#%x, EXTERN_REF]] {{.*}} bind 0x0 flat-namespace _extern
# CHAINED-DYLIB-DAG: __DATA __data 0x[[#%x, WEAK_REF]] {{.*}} bind 0x0 weak _weak_extern
# CHAINED-DYLIB-DAG: __DATA __data 0x[[#%x, LOCAL_REF]] {{.*}} rebase {{.*}}
-# CHAINED-DYLIB-DAG: __DATA __thread_ptrs 0x[[#%x, TLV_REF]] {{.*}} bind 0x0 flat-namespace _tlv
+# CHAINED-DYLIB-DAG: __DATA_CONST __got {{.*}} {{.*}} bind 0x0 flat-namespace _tlv
# CHAINED-DYLIB-EMPTY:
#--- foo.s
diff --git a/lld/test/MachO/got-to-tlv-reference.s b/lld/test/MachO/got-to-tlv-reference.s
new file mode 100644
index 0000000000000..72186ada4b939
--- /dev/null
+++ b/lld/test/MachO/got-to-tlv-reference.s
@@ -0,0 +1,128 @@
+# REQUIRES: x86
+# RUN: rm -rf %t; split-file %s %t
+
+## A GOT relocation against a thread-local asks for the address of that
+## symbol's TLV descriptor, which is what its non-lazy pointer slot holds.
+## lld used to reject this valid reference; ld-prime accepts it.
+
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/deftlv.s -o %t/deftlv.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/libtlv.s -o %t/libtlv.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/got.s -o %t/got.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/both.s -o %t/both.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/tlv.s -o %t/tlv.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/branch.s -o %t/branch.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/direct.s -o %t/direct.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/localtlv.s -o %t/localtlv.o
+
+## A definition in the same image needs no slot: the load relaxes to a direct
+## address computation.
+# RUN: %lld -dylib -o %t/deftlv.dylib %t/deftlv.o
+# RUN: llvm-objdump --macho --section-headers -d --no-show-raw-insn %t/deftlv.dylib | \
+# RUN: FileCheck %s --check-prefix=DEF
+# DEF-NOT: __got
+# DEF-NOT: __thread_ptrs
+# DEF-LABEL: _main:
+# DEF-NEXT: leaq _foo(%rip), %rax
+
+# RUN: %lld -dylib -install_name @executable_path/libtlv.dylib -lSystem \
+# RUN: -o %t/libtlv.dylib %t/libtlv.o
+
+## Imported from a dylib, the reference binds through __got, as with ld-prime.
+# RUN: %lld -dylib -lSystem -L%t -ltlv -o %t/got.dylib %t/got.o
+# RUN: llvm-objdump --macho --bind %t/got.dylib | FileCheck %s --check-prefix=GOT
+# GOT: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 libtlv _foo
+
+## Reaching one thread-local both ways must produce a single slot. A second
+## one would show up as a second binding.
+# RUN: %lld -dylib -lSystem -L%t -ltlv -o %t/both.dylib %t/both.o
+# RUN: llvm-objdump --macho --section-headers --bind %t/both.dylib | \
+# RUN: FileCheck %s --check-prefix=BOTH
+# BOTH-NOT: __thread_ptrs
+# BOTH: __got
+# BOTH-LABEL: Bind table:
+# BOTH: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 libtlv _foo
+# BOTH-NOT: _foo
+
+## A TLV-only reference also uses __got, matching ld-prime.
+# RUN: %lld -dylib -lSystem -L%t -ltlv -o %t/tlv.dylib %t/tlv.o
+# RUN: llvm-objdump --macho --section-headers --bind %t/tlv.dylib | \
+# RUN: FileCheck %s --check-prefix=TLV
+# TLV-NOT: __thread_ptrs
+# TLV: __got
+# TLV-LABEL: Bind table:
+# TLV: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 libtlv _foo
+
+## A chained-fixup stub and a TLV relocation share the same GOT entry.
+# RUN: %lld -dylib -fixup_chains -lSystem -L%t -ltlv \
+# RUN: -o %t/branch.dylib %t/branch.o
+# RUN: llvm-objdump --macho --section-headers --chained-fixups \
+# RUN: %t/branch.dylib | FileCheck %s --check-prefix=BRANCH
+# BRANCH-NOT: __thread_ptrs
+# BRANCH: __got
+# BRANCH: _foo
+# BRANCH-NOT: _foo
+
+## A direct reference can address a same-image descriptor, but an imported
+## descriptor has no fixed address. ld-prime accepts the first and rejects the
+## second.
+# RUN: %lld -dylib -o %t/direct-local.dylib %t/direct.o %t/localtlv.o
+# RUN: not %lld -dylib -L%t -ltlv -o /dev/null %t/direct.o 2>&1 | \
+# RUN: FileCheck %s --check-prefix=DIRECT
+# DIRECT: SIGNED relocation requires that symbol _foo not be thread-local
+
+#--- deftlv.s
+.text
+.globl _main
+_main:
+ movq _foo at GOTPCREL(%rip), %rax
+ ret
+.section __DATA,__thread_vars,thread_local_variables
+_foo:
+
+#--- libtlv.s
+.section __DATA,__thread_vars,thread_local_variables
+.globl _foo
+_foo:
+
+#--- got.s
+.text
+.globl _main
+_main:
+ movq _foo at GOTPCREL(%rip), %rax
+ ret
+
+#--- both.s
+.text
+.globl _main
+_main:
+ movq _foo at GOTPCREL(%rip), %rax
+ movq _foo at TLVP(%rip), %rcx
+ ret
+
+#--- tlv.s
+.text
+.globl _main
+_main:
+ movq _foo at TLVP(%rip), %rax
+ ret
+
+#--- branch.s
+.text
+.globl _main
+_main:
+ callq _foo
+ movq _foo at TLVP(%rip), %rax
+ ret
+
+#--- direct.s
+.text
+.globl _main
+_main:
+ leaq _foo(%rip), %rax
+ ret
+
+#--- localtlv.s
+.section __DATA,__thread_vars,thread_local_variables
+.globl _foo
+_foo:
+ .space 24
diff --git a/lld/test/MachO/indirect-symtab.s b/lld/test/MachO/indirect-symtab.s
index 52849ccb87455..20dc971c134f4 100644
--- a/lld/test/MachO/indirect-symtab.s
+++ b/lld/test/MachO/indirect-symtab.s
@@ -24,8 +24,10 @@
# CHECK-NEXT: address index name
# CHECK-NEXT: _bar_fn
# CHECK-NEXT: _foo_fn
-# CHECK-NEXT: Indirect symbols for (__DATA_CONST,__got) 4 entries
+# CHECK-NEXT: Indirect symbols for (__DATA_CONST,__got) 6 entries
# CHECK-NEXT: address index name
+# CHECK-NEXT: _bar_tlv
+# CHECK-NEXT: _foo_tlv
# CHECK-NEXT: LOCAL
# CHECK-NEXT: _bar
# CHECK-NEXT: _foo
@@ -34,11 +36,6 @@
# CHECK-NEXT: address index name
# CHECK-NEXT: _bar_fn
# CHECK-NEXT: _foo_fn
-# CHECK-NEXT: Indirect symbols for (__DATA,__thread_ptrs) 2 entries
-# CHECK-NEXT: address index name
-# CHECK-NEXT: _bar_tlv
-# CHECK-NEXT: _foo_tlv
-
# DYSYMTAB: nindirectsyms 10
#--- libfoo.s
diff --git a/lld/test/MachO/invalid/bad-got-to-dylib-tlv-reference.s b/lld/test/MachO/invalid/bad-got-to-dylib-tlv-reference.s
deleted file mode 100644
index 463a044465172..0000000000000
--- a/lld/test/MachO/invalid/bad-got-to-dylib-tlv-reference.s
+++ /dev/null
@@ -1,23 +0,0 @@
-# REQUIRES: x86
-# RUN: rm -rf %t; split-file %s %t
-
-# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/libtlv.s -o %t/libtlv.o
-# RUN: %lld -dylib -install_name @executable_path/libtlv.dylib \
-# RUN: -lSystem -o %t/libtlv.dylib %t/libtlv.o
-
-# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/test.s -o %t/test.o
-# RUN: not %lld -lSystem -L%t -ltlv -o /dev/null %t/test.o 2>&1 | FileCheck %s -DFILE=%t/test.o
-
-# CHECK: error: [[FILE]]:(symbol _main+0x3): GOT_LOAD relocation requires that symbol _foo not be thread-local
-
-#--- libtlv.s
-.section __DATA,__thread_vars,thread_local_variables
-.globl _foo
-_foo:
-
-#--- test.s
-.text
-.globl _main
-_main:
- movq _foo at GOTPCREL(%rip), %rax
- ret
diff --git a/lld/test/MachO/invalid/bad-got-to-tlv-reference.s b/lld/test/MachO/invalid/bad-got-to-tlv-reference.s
deleted file mode 100644
index 8934bc892a474..0000000000000
--- a/lld/test/MachO/invalid/bad-got-to-tlv-reference.s
+++ /dev/null
@@ -1,14 +0,0 @@
-# REQUIRES: x86
-# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %s -o %t.o
-# RUN: not %lld -o /dev/null %t.o 2>&1 | FileCheck %s -DFILE=%t.o
-
-# CHECK: error: [[FILE]]:(symbol _main+0x3): GOT_LOAD relocation requires that symbol _foo not be thread-local
-
-.text
-.globl _main
-_main:
- movq _foo at GOTPCREL(%rip), %rax
- ret
-
-.section __DATA,__thread_vars,thread_local_variables
-_foo:
diff --git a/lld/test/MachO/map-file.s b/lld/test/MachO/map-file.s
index 1c1ba5a4a2d56..25bdcf8cea824 100644
--- a/lld/test/MachO/map-file.s
+++ b/lld/test/MachO/map-file.s
@@ -25,11 +25,10 @@
# CHECK-NEXT: 3 __cstring 0000002b [[#%x,CSTR:]] DATA
# CHECK-NEXT: 4 __unwind_info 0000103c [[#%x,UNWIND:]] DATA
# CHECK-NEXT: 5 __eh_frame 00000038 [[#%x,EH_FRAME:]] DATA
-# CHECK-NEXT: 6 __got 00000010 [[#%x,GOT:]] DATA
+# CHECK-NEXT: 6 __got 00000018 [[#%x,GOT:]] DATA
# CHECK-NEXT: 7 __la_symbol_ptr 00000010 [[#%x,LAZY:]] DATA
# CHECK-NEXT: 8 __data 00000008 [[#%x,DATA:]] DATA
-# CHECK-NEXT: 9 __thread_ptrs 00000008 [[#%x,TLVP:]] DATA
-# CHECK-NEXT: 10 __common 00000001 [[#%x,BSS:]] BSS
+# CHECK-NEXT: 9 __common 00000001 [[#%x,BSS:]] BSS
# CHECK: SYMBOL TABLE:
# CHECK-DAG: [[#%x,DYLD:]] l O __DATA,__data __dyld_private
@@ -61,7 +60,6 @@
# CHECK-NEXT: 0x[[#%X,GOT]] 0x{{[0-9A-F]+}} __DATA_CONST __got
# CHECK-NEXT: 0x[[#%X,LAZY]] 0x{{[0-9A-F]+}} __DATA __la_symbol_ptr
# CHECK-NEXT: 0x[[#%X,DATA]] 0x{{[0-9A-F]+}} __DATA __data
-# CHECK-NEXT: 0x[[#%X,TLVP]] 0x{{[0-9A-F]+}} __DATA __thread_ptrs
# CHECK-NEXT: 0x[[#%X,BSS]] 0x{{[0-9A-F]+}} __DATA __common
# CHECK-NEXT: # Symbols:
@@ -80,12 +78,12 @@
## Note: ld64 prints "CIE" and "FDE for: <function>" instead of "EH_Frame".
# CHECK-NEXT: 0x[[#%X,EH_FRAME]] 0x00000018 [ 2] EH_Frame
# CHECK-NEXT: 0x[[#%X,EH_FRAME+0x18]] 0x00000020 [ 2] EH_Frame
-# CHECK-NEXT: 0x[[#%X,GOT]] 0x00000008 [ 0] non-lazy-pointer-to-local: _baz2
-# CHECK-NEXT: 0x[[#%X,GOT+8]] 0x00000008 [ 0] non-lazy-pointer-to-local: dyld_stub_binder
+# CHECK-NEXT: 0x[[#%X,GOT]] 0x00000008 [ 0] non-lazy-pointer-to-local: _baz_tlv
+# CHECK-NEXT: 0x[[#%X,GOT+8]] 0x00000008 [ 0] non-lazy-pointer-to-local: _baz2
+# CHECK-NEXT: 0x[[#%X,GOT+0x10]] 0x00000008 [ 0] non-lazy-pointer-to-local: dyld_stub_binder
# CHECK-NEXT: 0x[[#%X,LAZY]] 0x00000008 [ 5] _baz
# CHECK-NEXT: 0x[[#%X,LAZY+8]] 0x00000008 [ 2] _bar
# CHECK-NEXT: 0x[[#%X,DYLD]] 0x00000000 [ 0] __dyld_private
-# CHECK-NEXT: 0x[[#%X,TLVP]] 0x00000008 [ 0] non-lazy-pointer-to-local: _baz_tlv
# CHECK-NEXT: 0x[[#%X,BSS]] 0x00000001 [ 2] _number
# CHECK-EMPTY:
diff --git a/lld/test/MachO/tapi-link.s b/lld/test/MachO/tapi-link.s
index afe856f20cca4..09b4539a5386e 100644
--- a/lld/test/MachO/tapi-link.s
+++ b/lld/test/MachO/tapi-link.s
@@ -33,7 +33,7 @@
# CHECK-DAG: __DATA __data {{.*}} pointer 0 libc++abi ___gxx_personality_v0
# CHECK-DAG: __DATA __data {{.*}} pointer 0 libNested3 _deeply_nested
# CHECK-DAG: __DATA __data {{.*}} pointer 0 libTlvWeak _weak
-# CHECK-DAG: __DATA __thread_ptrs {{.*}} pointer 0 libTlvWeak _tlv
+# CHECK-DAG: __DATA_CONST __got {{.*}} pointer 0 libTlvWeak _tlv
# CHECK: Weak bind table:
# CHECK-DAG: __DATA __data {{.*}} pointer 0 _weak
diff --git a/lld/test/MachO/tlv-dylib.s b/lld/test/MachO/tlv-dylib.s
index 21d051fffd93c..50cdd66fc88a6 100644
--- a/lld/test/MachO/tlv-dylib.s
+++ b/lld/test/MachO/tlv-dylib.s
@@ -29,8 +29,8 @@
# CHECK-NEXT: movq [[#]](%rip), %rax ## 0x[[#%x, BAZ:]]
# CHECK-LABEL: Bind table:
-# CHECK-DAG: __DATA __thread_ptrs 0x{{0*}}[[#%x, FOO]] pointer 0 libtlv _foo
-# CHECK-DAG: __DATA __thread_ptrs 0x{{0*}}[[#%x, BAR]] pointer 0 libtlv _bar
+# CHECK-DAG: __DATA_CONST __got 0x{{0*}}[[#%x, FOO]] pointer 0 libtlv _foo
+# CHECK-DAG: __DATA_CONST __got 0x{{0*}}[[#%x, BAR]] pointer 0 libtlv _bar
# CHECK-DAG: __DATA_CONST __got 0x{{0*}}[[#%x, BAZ]] pointer 0 libtlv _baz
## Check `type` on the various TLV sections, and check that
@@ -39,7 +39,7 @@
# FLAGS: sectname __got
# FLAGS-NEXT: segname __DATA_CONST
# FLAGS-NEXT: addr
-# FLAGS-NEXT: size 0x0000000000000008
+# FLAGS-NEXT: size 0x0000000000000018
# FLAGS-NEXT: offset
# FLAGS-NEXT: align 2^3 (8)
# FLAGS-NEXT: reloff 0
@@ -54,15 +54,6 @@
# FLAGS-NEXT: reloff 0
# FLAGS-NEXT: nreloc 0
# FLAGS-NEXT: type S_THREAD_LOCAL_VARIABLES
-# FLAGS: sectname __thread_ptrs
-# FLAGS-NEXT: segname __DATA
-# FLAGS-NEXT: addr
-# FLAGS-NEXT: size 0x0000000000000010
-# FLAGS-NEXT: offset
-# FLAGS-NEXT: align 2^3 (8)
-# FLAGS-NEXT: reloff 0
-# FLAGS-NEXT: nreloc 0
-# FLAGS-NEXT: type S_THREAD_LOCAL_VARIABLE_POINTERS
# FLAGS: sectname __thread_data
# FLAGS-NEXT: segname __DATA
# FLAGS-NEXT: addr
@@ -119,7 +110,7 @@ _main:
ret
## Add some TLVs to test too, so that we can test the ordering
-## of __thread_ptrs, __thread_data, and __thread_bss.
+## of __thread_data and __thread_bss.
## Also add a .bss and a .comm for good measure too. Since they
## are both zerofill, they end up after __thread_bss.
.comm _com, 0x4000
diff --git a/lld/test/MachO/tlv-dynamic-lookup-x86.s b/lld/test/MachO/tlv-dynamic-lookup-x86.s
new file mode 100644
index 0000000000000..c39622c4fd169
--- /dev/null
+++ b/lld/test/MachO/tlv-dynamic-lookup-x86.s
@@ -0,0 +1,52 @@
+# REQUIRES: x86
+# RUN: rm -rf %t; split-file %s %t
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/movq.s -o %t/movq.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/leaq.s -o %t/leaq.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/notlv.s -o %t/notlv.o
+
+# RUN: %lld -dylib -undefined dynamic_lookup -o %t/movq.dylib %t/movq.o
+# RUN: llvm-objdump --macho --section-headers --bind -d --no-show-raw-insn %t/movq.dylib | \
+# RUN: FileCheck %s --check-prefix=MOVQ
+# MOVQ-LABEL: _f:
+# MOVQ-NEXT: movq
+# MOVQ-NOT: __thread_ptrs
+# MOVQ: __got
+# MOVQ-LABEL: Bind table:
+# MOVQ: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+
+## FIXME: leaq computes the address of the slot itself, but the ABI requires the
+## descriptor the slot holds, so this is wrong code. It is accepted because
+## X86_64::relaxGotLoad's MOVQ check only runs on the relax path, and a slot
+## exists here. Pre-existing and not specific to dynamic lookup: the same leaq
+## against a dylib that really exports _tlv as thread-local miscompiles
+## identically on stock lld. Pinned as-is so the separate fix has a baseline.
+# RUN: %lld -dylib -undefined dynamic_lookup -o %t/leaq.dylib %t/leaq.o
+# RUN: llvm-objdump --macho -d --no-show-raw-insn %t/leaq.dylib | \
+# RUN: FileCheck %s --check-prefix=LEAQ
+# LEAQ-LABEL: _f:
+# LEAQ-NEXT: leaq
+
+# RUN: %lld -dylib -install_name @rpath/libnotlv.dylib -o %t/libnotlv.dylib %t/notlv.o
+# RUN: not %lld -dylib -o /dev/null %t/movq.o %t/libnotlv.dylib 2>&1 | \
+# RUN: FileCheck %s --check-prefix=DEFINED
+# DEFINED: error: {{.*}}TLV relocation requires that symbol _tlv be thread-local
+
+#--- movq.s
+.globl _f
+_f:
+ movq _tlv at TLVP(%rip), %rax
+ retq
+.subsections_via_symbols
+
+#--- leaq.s
+.globl _f
+_f:
+ leaq _tlv at TLVP(%rip), %rax
+ retq
+.subsections_via_symbols
+
+#--- notlv.s
+.globl _tlv
+.data
+_tlv:
+ .quad 0
diff --git a/lld/test/MachO/tlv-dynamic-lookup.s b/lld/test/MachO/tlv-dynamic-lookup.s
new file mode 100644
index 0000000000000..2b2d8e36fbf75
--- /dev/null
+++ b/lld/test/MachO/tlv-dynamic-lookup.s
@@ -0,0 +1,265 @@
+# REQUIRES: aarch64
+# RUN: rm -rf %t; split-file %s %t
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/consumer.s -o %t/consumer.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/twotlv.s -o %t/twotlv.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/mixed.s -o %t/mixed.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/branch.s -o %t/branch.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/cfi.s -o %t/cfi.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/binder.s -o %t/binder.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/initoff.s -o %t/initoff.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/ep.s -o %t/ep.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/unsmix.s -o %t/unsmix.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/deftlv.s -o %t/deftlv.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/notlv.s -o %t/notlv.o
+
+## A dynamic-lookup symbol's thread-locality is unknown, so its slot goes in
+## __got. ld64 lowers this case identically.
+# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -o %t/dylookup.dylib %t/consumer.o
+# RUN: llvm-objdump --macho --section-headers --bind %t/dylookup.dylib | FileCheck %s
+# CHECK-NOT: __thread_ptrs
+# CHECK: __got
+# CHECK-LABEL: Bind table:
+# CHECK: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+
+## -U is per-symbol: _tlv is exempt, _other is left undefined.
+# RUN: not %lld -arch arm64 -dylib -U _tlv -o /dev/null %t/twotlv.o 2>&1 | \
+# RUN: FileCheck %s --check-prefix=USCOPE
+# USCOPE-NOT: _tlv
+# USCOPE: error: undefined symbol: _other
+# USCOPE-NOT: _tlv
+
+# RUN: %lld -arch arm64 -dylib -U _tlv -U _other -o %t/u.dylib %t/twotlv.o
+# RUN: llvm-objdump --macho --bind %t/u.dylib | FileCheck %s --check-prefix=UBOTH
+# UBOTH-DAG: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# UBOTH-DAG: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _other
+
+# RUN: %no-fatal-warnings-lld -arch arm64 -dylib -flat_namespace -undefined suppress \
+# RUN: -o %t/flat.dylib %t/consumer.o
+# RUN: llvm-objdump --macho --bind %t/flat.dylib | FileCheck %s --check-prefix=FLAT
+# FLAT: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+
+## Chained fixups must produce a real slot, not just a flat import.
+# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -fixup_chains \
+# RUN: -o %t/chained.dylib %t/consumer.o
+# RUN: llvm-objdump --macho --section-headers --chained-fixups %t/chained.dylib | \
+# RUN: FileCheck %s --check-prefix=CHAINED
+# CHAINED-NOT: __thread_ptrs
+# CHAINED: __got
+# CHAINED: lib_ordinal = -2 (flat-namespace)
+# CHAINED: _tlv
+
+# RUN: %lld -arch arm64 -dylib -install_name @rpath/libnotlv.dylib -o %t/libnotlv.dylib %t/notlv.o
+# RUN: %lld -arch arm64 -dylib -install_name @rpath/libtlv.dylib -undefined dynamic_lookup \
+# RUN: -o %t/libtlv.dylib %t/deftlv.o
+
+# RUN: not %lld -arch arm64 -dylib -o /dev/null %t/consumer.o %t/libnotlv.dylib 2>&1 | \
+# RUN: FileCheck %s --check-prefix=ERR
+# ERR: error: {{.*}}TLVP_LOAD_PAGE21 relocation requires that symbol _tlv be thread-local
+
+# RUN: %lld -arch arm64 -dylib -o %t/good.dylib %t/consumer.o %t/libtlv.dylib
+# RUN: llvm-objdump --macho --bind %t/good.dylib | FileCheck %s --check-prefix=GOOD
+# GOOD: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 libtlv _tlv
+
+## A dynamic-lookup symbol has no thread-locality to publish, so it must not be
+## re-exported -- a downstream link would read the missing flag as a definite
+## "not thread-local". _readTlv is the positive control: the trie is not simply
+## empty for every input.
+# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -exported_symbol _tlv \
+# RUN: -exported_symbol _readTlv -o %t/reexport.dylib %t/consumer.o
+# RUN: llvm-objdump --macho --exports-trie %t/reexport.dylib | \
+# RUN: FileCheck %s --check-prefix=REEXPORT
+# REEXPORT: _readTlv
+# REEXPORT-NOT: _tlv{{$}}
+
+## The remaining cases each reach NonLazyPointerSectionBase::addEntry by a route
+## that does not pass through prepareSymbolRelocation. None may abort the
+## linker: the symbol already has a __got slot by the time the TLV reference
+## asks for one, so addEntry must coalesce rather than allocate a second.
+
+## StubsSection::addEntry -> in.got->addEntry under chained fixups, which is the
+## default at iOS 16 / macOS 13.
+## Chained fixups records binds in LC_DYLD_CHAINED_FIXUPS, so --bind is empty
+## here; the stub and the TLV reference share the one __got slot.
+# RUN: %no-arg-lld -arch arm64 -platform_version ios 16.0 16.0 -dylib \
+# RUN: -undefined dynamic_lookup -o %t/branch-chained.dylib %t/branch.o
+# RUN: llvm-objdump --macho --section-headers --chained-fixups %t/branch-chained.dylib | \
+# RUN: FileCheck %s --check-prefix=BRANCHC
+# BRANCHC-NOT: __thread_ptrs
+# BRANCHC: __got
+# BRANCHC: lib_ordinal = -2 (flat-namespace)
+
+## Same source without chained fixups must not reach a different verdict.
+# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -no_fixup_chains \
+# RUN: -o %t/branch-lazy.dylib %t/branch.o
+# RUN: llvm-objdump --macho --bind %t/branch-lazy.dylib | FileCheck %s --check-prefix=BRANCH
+# BRANCH: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+
+## UnwindInfoSection::prepare, after the relocation scan.
+# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -o %t/cfi.dylib %t/cfi.o
+# RUN: llvm-objdump --macho --bind %t/cfi.dylib | FileCheck %s --check-prefix=CFI
+# CFI: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _pers
+
+## StubHelperSection::setUp, which runs after Writer::run's errorCount() bail
+## and so can never be protected by an error() emitted earlier.
+# RUN: %no-lsystem-lld -arch arm64 -dylib -undefined dynamic_lookup -no_fixup_chains \
+# RUN: -o %t/binder.dylib %t/binder.o
+# RUN: llvm-objdump --macho --bind %t/binder.dylib | FileCheck %s --check-prefix=BINDER
+# BINDER: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace dyld_stub_binder
+
+## InitOffsetsSection::setUp.
+# RUN: %no-arg-lld -arch arm64 -platform_version macos 13.0 13.0 \
+# RUN: -syslibroot %S/Inputs/MacOSX.sdk -lSystem -dylib -undefined dynamic_lookup \
+# RUN: -init_offsets -o %t/initoff.dylib %t/initoff.o
+# RUN: llvm-objdump --macho --section-headers --chained-fixups %t/initoff.dylib | \
+# RUN: FileCheck %s --check-prefix=INITOFF
+# INITOFF-NOT: __thread_ptrs
+# INITOFF: __got
+# INITOFF: lib_ordinal = -2 (flat-namespace)
+# INITOFF: _ctor
+
+## The entry-point stub is synthesized before the relocation scan; the only
+## reference in the source is the TLV one, so there is nothing for a user to fix.
+# RUN: %no-arg-lld -arch arm64 -platform_version macos 13.0 13.0 \
+# RUN: -syslibroot %S/Inputs/MacOSX.sdk -lSystem -e _ep -undefined dynamic_lookup \
+# RUN: -o %t/ep.out %t/ep.o
+# RUN: llvm-objdump --macho --section-headers --chained-fixups %t/ep.out | \
+# RUN: FileCheck %s --check-prefix=EP
+# EP-NOT: __thread_ptrs
+# EP: __got
+# EP: lib_ordinal = -2 (flat-namespace)
+# EP: _ep
+
+## JUDGMENT CALL. A data pointer and a TLV reference to one dynamic-lookup
+## symbol cannot both describe whatever dyld finds, but neither can lld tell
+## which is wrong, and the UNSIGNED path allocates no slot for a conflict check
+## to inspect. Accepting both binds matches how every other unverifiable
+## dynamic-lookup mismatch is already treated.
+# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -o %t/unsmix.dylib %t/unsmix.o
+# RUN: llvm-objdump --macho --bind %t/unsmix.dylib | FileCheck %s --check-prefix=UNS
+# UNS-DAG: __DATA __data 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# UNS-DAG: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+
+## Referencing one symbol both ways yields a single slot, as ld64 emits here.
+# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -o %t/mixed.dylib %t/mixed.o
+# RUN: llvm-objdump --macho --section-headers --bind %t/mixed.dylib | \
+# RUN: FileCheck %s --check-prefix=MIXED
+# MIXED-NOT: __thread_ptrs
+# MIXED: __got
+# MIXED-LABEL: Bind table:
+# MIXED: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# MIXED-NOT: _tlv
+
+#--- consumer.s
+.globl _readTlv
+.p2align 2
+_readTlv:
+ adrp x8, _tlv at TLVPPAGE
+ ldr x8, [x8, _tlv at TLVPPAGEOFF]
+ ret
+.subsections_via_symbols
+
+#--- twotlv.s
+.globl _two
+.p2align 2
+_two:
+ adrp x8, _tlv at TLVPPAGE
+ ldr x8, [x8, _tlv at TLVPPAGEOFF]
+ adrp x9, _other at TLVPPAGE
+ ldr x9, [x9, _other at TLVPPAGEOFF]
+ ret
+.subsections_via_symbols
+
+#--- mixed.s
+.globl _viaTlv, _viaGot
+.p2align 2
+_viaTlv:
+ adrp x8, _tlv at TLVPPAGE
+ ldr x8, [x8, _tlv at TLVPPAGEOFF]
+ ret
+_viaGot:
+ adrp x8, _tlv at GOTPAGE
+ ldr x8, [x8, _tlv at GOTPAGEOFF]
+ ret
+.subsections_via_symbols
+
+#--- branch.s
+.globl _g
+.p2align 2
+_g:
+ bl _tlv
+ adrp x8, _tlv at TLVPPAGE
+ ldr x8, [x8, _tlv at TLVPPAGEOFF]
+ ret
+.subsections_via_symbols
+
+#--- cfi.s
+.globl _f
+.p2align 2
+_f:
+ .cfi_startproc
+ .cfi_personality 155, _pers
+ adrp x8, _pers at TLVPPAGE
+ ldr x8, [x8, _pers at TLVPPAGEOFF]
+ ret
+ .cfi_endproc
+.subsections_via_symbols
+
+#--- binder.s
+.globl _f
+.p2align 2
+_f:
+ adrp x8, dyld_stub_binder at TLVPPAGE
+ ldr x8, [x8, dyld_stub_binder at TLVPPAGEOFF]
+ bl _lazy
+ ret
+.subsections_via_symbols
+
+#--- initoff.s
+.globl _f
+.p2align 2
+_f:
+ adrp x8, _ctor at TLVPPAGE
+ ldr x8, [x8, _ctor at TLVPPAGEOFF]
+ ret
+.section __DATA,__mod_init_func,mod_init_funcs
+.quad _ctor
+.subsections_via_symbols
+
+#--- ep.s
+.globl _main
+.p2align 2
+_main:
+ adrp x8, _ep at TLVPPAGE
+ ldr x8, [x8, _ep at TLVPPAGEOFF]
+ ret
+.subsections_via_symbols
+
+#--- unsmix.s
+.globl _a
+.p2align 2
+_a:
+ adrp x8, _tlv at TLVPPAGE
+ ldr x8, [x8, _tlv at TLVPPAGEOFF]
+ ret
+.data
+.globl _p
+_p:
+ .quad _tlv
+.subsections_via_symbols
+
+#--- deftlv.s
+.globl _tlv
+.section __DATA,__thread_data,thread_local_regular
+_tlv$tlv$init:
+ .quad 0
+.section __DATA,__thread_vars,thread_local_variables
+_tlv:
+ .quad __tlv_bootstrap
+ .quad 0
+ .quad _tlv$tlv$init
+
+#--- notlv.s
+.globl _tlv
+.data
+_tlv:
+ .quad 0
diff --git a/lld/test/MachO/tlv-non-lazy-pointer.s b/lld/test/MachO/tlv-non-lazy-pointer.s
new file mode 100644
index 0000000000000..7996a70d19ca9
--- /dev/null
+++ b/lld/test/MachO/tlv-non-lazy-pointer.s
@@ -0,0 +1,170 @@
+# REQUIRES: aarch64
+# RUN: rm -rf %t; split-file %s %t
+
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/libtlv.s -o %t/libtlv.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/tlv.s -o %t/tlv.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/got.s -o %t/got.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/mixed.s -o %t/mixed.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/branch.s -o %t/branch.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/cfi.s -o %t/cfi.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/direct.s -o %t/direct.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/unsigned.s -o %t/unsigned.o
+
+# RUN: %lld -arch arm64 -dylib -lSystem -install_name @rpath/libtlv.dylib \
+# RUN: -o %t/libtlv.dylib %t/libtlv.o
+
+## ld-prime puts an imported TLV's descriptor pointer in __got, even when the
+## input only has TLV relocations.
+# RUN: %lld -arch arm64 -dylib -o %t/tlv.dylib %t/tlv.o %t/libtlv.dylib
+# RUN: llvm-objdump --macho --section-headers --bind %t/tlv.dylib | \
+# RUN: FileCheck %s --check-prefix=TLV
+# TLV-NOT: __thread_ptrs
+# TLV: __got
+# TLV-LABEL: Bind table:
+# TLV: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 libtlv _foo
+
+## An ordinary GOT reference asks for the same descriptor address.
+# RUN: %lld -arch arm64 -dylib -o %t/got.dylib %t/got.o %t/libtlv.dylib
+# RUN: llvm-objdump --macho --section-headers --bind %t/got.dylib | \
+# RUN: FileCheck %s --check-prefix=GOT
+# GOT-NOT: __thread_ptrs
+# GOT: __got
+# GOT-LABEL: Bind table:
+# GOT: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 libtlv _foo
+
+## GOT and TLV references must coalesce to one slot, independent of input
+## relocation kind.
+# RUN: %lld -arch arm64 -dylib -o %t/mixed.dylib %t/mixed.o %t/libtlv.dylib
+# RUN: llvm-objdump --macho --section-headers --bind %t/mixed.dylib | \
+# RUN: FileCheck %s --check-prefix=MIXED
+# MIXED-NOT: __thread_ptrs
+# MIXED: __got
+# MIXED-LABEL: Bind table:
+# MIXED: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 libtlv _foo
+# MIXED-NOT: _foo
+
+## A stub needs a GOT entry under chained fixups. It and the TLV relocation
+## must share that entry rather than asserting while allocating two slots.
+# RUN: %lld -arch arm64 -dylib -fixup_chains -o %t/branch.dylib \
+# RUN: %t/branch.o %t/libtlv.dylib
+# RUN: llvm-objdump --macho --section-headers --chained-fixups \
+# RUN: %t/branch.dylib | FileCheck %s --check-prefix=BRANCH
+# BRANCH-NOT: __thread_ptrs
+# BRANCH: __got
+# BRANCH: _foo
+# BRANCH-NOT: _foo
+
+## Compact unwind personalities also request a GOT entry after the ordinary
+## relocation scan. That late request must find the same slot.
+# RUN: %lld -arch arm64 -dylib -o %t/cfi.dylib %t/cfi.o %t/libtlv.dylib
+# RUN: llvm-objdump --macho --section-headers --bind %t/cfi.dylib | \
+# RUN: FileCheck %s --check-prefix=CFI
+# CFI-NOT: __thread_ptrs
+# CFI: __got
+# CFI-LABEL: Bind table:
+# CFI: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 libtlv _foo
+# CFI-NOT: _foo
+
+## An unsigned relocation is a request for the descriptor's address. It binds
+## independently of the TLV reference's non-lazy pointer, as ld-prime does.
+# RUN: %lld -arch arm64 -dylib -o %t/unsigned.dylib %t/unsigned.o \
+# RUN: %t/libtlv.dylib
+# RUN: llvm-objdump --macho --bind %t/unsigned.dylib | \
+# RUN: FileCheck %s --check-prefix=UNSIGNED
+# UNSIGNED-DAG: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 libtlv _foo
+# UNSIGNED-DAG: __DATA __data 0x{{[0-9a-f]+}} pointer 0 libtlv _foo
+
+## A direct address relocation is valid for a descriptor defined in the same
+## image, but an imported descriptor has no fixed link-time address. ld-prime
+## accepts the former and rejects the latter.
+# RUN: %lld -arch arm64 -dylib -lSystem -o %t/direct-local.dylib \
+# RUN: %t/direct.o %t/libtlv.o
+# RUN: not %lld -arch arm64 -dylib -o /dev/null %t/direct.o \
+# RUN: %t/libtlv.dylib 2>&1 | FileCheck %s --check-prefix=DIRECT
+# DIRECT: PAGE21 relocation requires that symbol _foo not be thread-local
+
+#--- libtlv.s
+.section __DATA,__thread_data,thread_local_regular
+_foo$tlv$init:
+ .quad 0
+.section __DATA,__thread_vars,thread_local_variables
+.globl _foo
+_foo:
+ .quad __tlv_bootstrap
+ .quad 0
+ .quad _foo$tlv$init
+
+#--- tlv.s
+.globl _viaTlv
+.p2align 2
+_viaTlv:
+ adrp x8, _foo at TLVPPAGE
+ ldr x8, [x8, _foo at TLVPPAGEOFF]
+ ret
+.subsections_via_symbols
+
+#--- got.s
+.globl _viaGot
+.p2align 2
+_viaGot:
+ adrp x8, _foo at GOTPAGE
+ ldr x8, [x8, _foo at GOTPAGEOFF]
+ ret
+.subsections_via_symbols
+
+#--- mixed.s
+.globl _viaTlv, _viaGot
+.p2align 2
+_viaTlv:
+ adrp x8, _foo at TLVPPAGE
+ ldr x8, [x8, _foo at TLVPPAGEOFF]
+ ret
+_viaGot:
+ adrp x8, _foo at GOTPAGE
+ ldr x8, [x8, _foo at GOTPAGEOFF]
+ ret
+.subsections_via_symbols
+
+#--- branch.s
+.globl _viaBranchAndTlv
+.p2align 2
+_viaBranchAndTlv:
+ bl _foo
+ adrp x8, _foo at TLVPPAGE
+ ldr x8, [x8, _foo at TLVPPAGEOFF]
+ ret
+.subsections_via_symbols
+
+#--- cfi.s
+.globl _viaCfiAndTlv
+.p2align 2
+_viaCfiAndTlv:
+ .cfi_startproc
+ .cfi_personality 155, _foo
+ adrp x8, _foo at TLVPPAGE
+ ldr x8, [x8, _foo at TLVPPAGEOFF]
+ ret
+ .cfi_endproc
+.subsections_via_symbols
+
+#--- direct.s
+.globl _direct
+.p2align 2
+_direct:
+ adrp x8, _foo at PAGE
+ add x8, x8, _foo at PAGEOFF
+ ret
+.subsections_via_symbols
+
+#--- unsigned.s
+.globl _viaTlv
+.p2align 2
+_viaTlv:
+ adrp x8, _foo at TLVPPAGE
+ ldr x8, [x8, _foo at TLVPPAGEOFF]
+ ret
+.data
+.globl _descriptor
+_descriptor:
+ .quad _foo
+.subsections_via_symbols
diff --git a/lld/test/MachO/weak-binding.s b/lld/test/MachO/weak-binding.s
index 682aa8dcd2537..2a218812c02ba 100644
--- a/lld/test/MachO/weak-binding.s
+++ b/lld/test/MachO/weak-binding.s
@@ -46,7 +46,7 @@
# CHECK-DAG: __DATA __data 0x[[#%x,WEAK_DY:]] pointer 0 libfoo _weak_dysym
# CHECK-DAG: __DATA __thread_vars 0x{{[0-9a-f]*}} pointer 0 libSystem __tlv_bootstrap
# CHECK-DAG: __DATA __thread_vars 0x{{[0-9a-f]*}} pointer 0 libSystem __tlv_bootstrap
-# CHECK-DAG: __DATA __thread_ptrs 0x[[#WEAK_DY_TLV_ADDR]] pointer 0 libfoo _weak_dysym_tlv
+# CHECK-DAG: __DATA_CONST __got 0x[[#WEAK_DY_TLV_ADDR]] pointer 0 libfoo _weak_dysym_tlv
## Check that we don't have any other bindings
# CHECK-EMPTY:
@@ -59,8 +59,8 @@
# CHECK-DAG: __DATA_CONST __got 0x[[#WEAK_DY_GOT_ADDR]] pointer 0 _weak_dysym_for_gotpcrel
# CHECK-DAG: __DATA_CONST __got 0x[[#WEAK_EXT_GOT_ADDR]] pointer 0 _weak_external_for_gotpcrel
# CHECK-DAG: __DATA __data 0x[[#WEAK_DY]] pointer 0 _weak_dysym
-# CHECK-DAG: __DATA __thread_ptrs 0x[[#WEAK_TLV_ADDR]] pointer 0 _weak_tlv
-# CHECK-DAG: __DATA __thread_ptrs 0x[[#WEAK_DY_TLV_ADDR]] pointer 0 _weak_dysym_tlv
+# CHECK-DAG: __DATA_CONST __got 0x[[#WEAK_TLV_ADDR]] pointer 0 _weak_tlv
+# CHECK-DAG: __DATA_CONST __got 0x[[#WEAK_DY_TLV_ADDR]] pointer 0 _weak_dysym_tlv
# CHECK-DAG: __DATA __data 0x{{[0-9a-f]*}} pointer 2 _weak_external
# CHECK-DAG: __DATA __la_symbol_ptr 0x[[#WEAK_DY_FN]] pointer 0 _weak_dysym_fn
# CHECK-DAG: __DATA __la_symbol_ptr 0x[[#WEAK_EXT_FN]] pointer 0 _weak_external_fn
@@ -84,8 +84,8 @@
# CHAINED-DAG: __DATA __data 0x{{[0-9a-f]*}} {{.*}} rebase 0x[[#%X,WEAK_INT]]
# CHAINED-DAG: __DATA __thread_vars 0x{{[0-9a-f]*}} {{.*}} bind 0x0 libSystem __tlv_bootstrap
# CHAINED-DAG: __DATA __thread_vars 0x{{[0-9a-f]*}} {{.*}} bind 0x0 libSystem __tlv_bootstrap
-# CHAINED-DAG: __DATA __thread_ptrs 0x[[#WEAK_DY_TLV_ADDR]] {{.*}} bind 0x0 weak _weak_dysym_tlv
-# CHAINED-DAG: __DATA __thread_ptrs 0x[[#WEAK_TLV_ADDR]] {{.*}} bind 0x0 weak _weak_tlv
+# CHAINED-DAG: __DATA_CONST __got 0x[[#WEAK_DY_TLV_ADDR]] {{.*}} bind 0x0 weak _weak_dysym_tlv
+# CHAINED-DAG: __DATA_CONST __got 0x[[#WEAK_TLV_ADDR]] {{.*}} bind 0x0 weak _weak_tlv
# CHAINED-EMPTY:
#--- libfoo.s
diff --git a/lld/test/MachO/weak-reference.s b/lld/test/MachO/weak-reference.s
index ff72cc3d505fb..21d5001e4a6b9 100644
--- a/lld/test/MachO/weak-reference.s
+++ b/lld/test/MachO/weak-reference.s
@@ -29,7 +29,7 @@
# BIND-NEXT: segment section address type addend dylib symbol
# BIND-DAG: __DATA __data 0x{{[0-9a-f]+}} pointer 0 libfoo _foo (weak_import)
# BIND-DAG: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 libfoo _foo (weak_import)
-# BIND-DAG: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 libfoo _foo_tlv (weak_import)
+# BIND-DAG: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 libfoo _foo_tlv (weak_import)
# BIND-DAG: __DATA __data 0x{{[0-9a-f]+}} pointer 0 libfoo _weak_foo (weak_import)
# BIND-DAG: __DATA __la_symbol_ptr 0x{{[0-9a-f]+}} pointer 0 libfoo _weak_foo_fn (weak_import)
# BIND: Lazy bind table:
@@ -40,7 +40,7 @@
# CHAINED-NEXT: segment section address pointer type addend dylib symbol/vm address
# CHAINED-DAG: __DATA_CONST __got {{.*}} bind 0x0 libfoo _foo (weak import)
# CHAINED-DAG: __DATA __data {{.*}} bind 0x0 libfoo _foo (weak import)
-# CHAINED-DAG: __DATA __thread_ptrs {{.*}} bind 0x0 libfoo _foo_tlv (weak import)
+# CHAINED-DAG: __DATA_CONST __got {{.*}} bind 0x0 libfoo _foo_tlv (weak import)
# CHAINED-DAG: __DATA_CONST __got {{.*}} bind 0x0 libfoo _foo_fn (weak import)
# CHAINED-DAG: __DATA __data {{.*}} bind 0x0 weak _weak_foo (weak import)
# CHAINED-DAG: __DATA_CONST __got {{.*}} bind 0x0 weak _weak_foo_fn (weak import)
@@ -97,7 +97,7 @@
# STRONG-BIND-DAG: __DATA __data 0x{{[0-9a-f]+}} pointer 0 libfoo _foo{{$}}
# STRONG-BIND-DAG: __DATA __data 0x{{[0-9a-f]+}} pointer 0 libfoo _foo{{$}}
# STRONG-BIND-DAG: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 libfoo _foo{{$}}
-# STRONG-BIND-DAG: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 libfoo _foo_tlv{{$}}
+# STRONG-BIND-DAG: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 libfoo _foo_tlv{{$}}
# STRONG-BIND-DAG: __DATA __data 0x{{[0-9a-f]+}} pointer 0 libfoo _weak_foo{{$}}
# STRONG-BIND-DAG: __DATA __data 0x{{[0-9a-f]+}} pointer 0 libfoo _weak_foo{{$}}
# STRONG-BIND-DAG: __DATA __la_symbol_ptr 0x{{[0-9a-f]+}} pointer 0 libfoo _weak_foo_fn{{$}}
@@ -111,7 +111,7 @@
# STRONG-CHAINED-DAG: __DATA __data {{.*}} bind 0x0 libfoo _foo{{$}}
# STRONG-CHAINED-DAG: __DATA __data {{.*}} bind 0x0 weak _weak_foo{{$}}
# STRONG-CHAINED-DAG: __DATA __data {{.*}} bind 0x0 weak _weak_foo{{$}}
-# STRONG-CHAINED-DAG: __DATA __thread_ptrs {{.*}} bind 0x0 libfoo _foo_tlv{{$}}
+# STRONG-CHAINED-DAG: __DATA_CONST __got {{.*}} bind 0x0 libfoo _foo_tlv{{$}}
# STRONG-YAML-LABEL: WeakBindOpcodes:
# STRONG-YAML: - Opcode: BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM
>From 09bbff9323d7a475e8ecdadb34184860acac70f4 Mon Sep 17 00:00:00 2001
From: Peter Rong <PeterRong at meta.com>
Date: Fri, 11 Sep 2026 09:37:03 -0700
Subject: [PATCH 2/4] [lld][MachO] Give the imported-TLV rejection its own
diagnostic
validateSymbolRelocation now rejects two unrelated things, but reports both
with the message written for the first: "requires that symbol _foo not be
thread-local". For a direct relocation against an imported thread-local that
is actively misleading -- the symbol is supposed to be thread-local, and
making it otherwise is not the fix. The problem is that an imported TLV
descriptor has no address at link time.
Split the condition so each failure explains itself:
TLVP_LOAD_PAGE21 relocation requires that symbol _other be thread-local
PAGE21 relocation cannot reference imported thread-local symbol _foo;
its TLV descriptor has no address at link time
Also reuse the dysym already computed above instead of a second
isa<DylibSymbol> test.
---
lld/MachO/Relocations.cpp | 17 +++++++++++------
lld/test/MachO/got-to-tlv-reference.s | 2 +-
lld/test/MachO/tlv-non-lazy-pointer.s | 2 +-
3 files changed, 13 insertions(+), 8 deletions(-)
diff --git a/lld/MachO/Relocations.cpp b/lld/MachO/Relocations.cpp
index f06c5540829ec..b926d74bbded1 100644
--- a/lld/MachO/Relocations.cpp
+++ b/lld/MachO/Relocations.cpp
@@ -88,12 +88,17 @@ bool macho::validateSymbolRelocation(const Symbol *sym,
relocAttrs.hasAttr(RelocAttrBits::GOT) ||
relocAttrs.hasAttr(RelocAttrBits::BRANCH) ||
relocAttrs.hasAttr(RelocAttrBits::UNSIGNED);
- const bool isImportedTlv = isa<DylibSymbol>(sym) && sym->isTlv();
-
- if (tlvKindIsKnown && ((isTlvReloc && !sym->isTlv()) ||
- (isImportedTlv && !permitsTlvDescriptor)))
- error(message(Twine("requires that symbol ") + sym->getName() + " " +
- (sym->isTlv() ? "not " : "") + "be thread-local"));
+ const bool isImportedTlv = dysym && sym->isTlv();
+
+ if (tlvKindIsKnown) {
+ if (isTlvReloc && !sym->isTlv())
+ error(message(Twine("requires that symbol ") + sym->getName() +
+ " be thread-local"));
+ else if (isImportedTlv && !permitsTlvDescriptor)
+ error(message(Twine("cannot reference imported thread-local symbol ") +
+ sym->getName() +
+ "; its TLV descriptor has no address at link time"));
+ }
return valid;
}
diff --git a/lld/test/MachO/got-to-tlv-reference.s b/lld/test/MachO/got-to-tlv-reference.s
index 72186ada4b939..62184d84af5b4 100644
--- a/lld/test/MachO/got-to-tlv-reference.s
+++ b/lld/test/MachO/got-to-tlv-reference.s
@@ -68,7 +68,7 @@
# RUN: %lld -dylib -o %t/direct-local.dylib %t/direct.o %t/localtlv.o
# RUN: not %lld -dylib -L%t -ltlv -o /dev/null %t/direct.o 2>&1 | \
# RUN: FileCheck %s --check-prefix=DIRECT
-# DIRECT: SIGNED relocation requires that symbol _foo not be thread-local
+# DIRECT: SIGNED relocation cannot reference imported thread-local symbol _foo; its TLV descriptor has no address at link time
#--- deftlv.s
.text
diff --git a/lld/test/MachO/tlv-non-lazy-pointer.s b/lld/test/MachO/tlv-non-lazy-pointer.s
index 7996a70d19ca9..d6c3ad5e80931 100644
--- a/lld/test/MachO/tlv-non-lazy-pointer.s
+++ b/lld/test/MachO/tlv-non-lazy-pointer.s
@@ -81,7 +81,7 @@
# RUN: %t/direct.o %t/libtlv.o
# RUN: not %lld -arch arm64 -dylib -o /dev/null %t/direct.o \
# RUN: %t/libtlv.dylib 2>&1 | FileCheck %s --check-prefix=DIRECT
-# DIRECT: PAGE21 relocation requires that symbol _foo not be thread-local
+# DIRECT: PAGE21 relocation cannot reference imported thread-local symbol _foo; its TLV descriptor has no address at link time
#--- libtlv.s
.section __DATA,__thread_data,thread_local_regular
>From 8d4756c56a37b09ac525ec899f94fcf971f25d0f Mon Sep 17 00:00:00 2001
From: Peter Rong <PeterRong at meta.com>
Date: Fri, 11 Sep 2026 13:48:44 -0700
Subject: [PATCH 3/4] [NFC][lld][MachO] Correct comments left stale by the
__got consolidation
Several comments describe the two-section world that no longer exists, or
state things the code contradicts:
- GotSection was documented as "the __DATA_CONST,__got section", but the
constructor two lines below passes segment_names::data; the move to
__DATA_CONST happens later in initializeSectionRenameMap, and only when
-no_data_const is absent.
- addNonLazyPointerEntry argued against putting the slot in __thread_ptrs,
a section lld no longer emits, and was the only remaining mention of the
name. Keep the part that says something the code does not: one entry
serves both reference kinds, so the outcome does not depend on which
reference the relocation scan reaches first, which matters most for
dynamic-lookup symbols.
- resolveNonLazyPtrVA carried four lines of allocation rationale on a
one-line accessor; point at addNonLazyPointerEntry instead. isInGot
pointed at "Writer::addNonLazyPointerEntry", which is a file-static, not
a member of Writer.
- tlv-dynamic-lookup.s named NonLazyPointerSectionBase::addEntry, a class
this change deletes, and labelled a case "InitOffsetsSection::setUp"
without noting that it reaches a GOT entry only via in.stubs->addEntry.
- Reworded the "JUDGMENT CALL" note on the unsigned+TLV case to state what
is checked: two independent binds, both naming the descriptor, which is
also what ld-prime emits.
---
lld/MachO/Symbols.h | 8 ++------
lld/MachO/SyntheticSections.h | 4 ++--
lld/MachO/Writer.cpp | 10 ++++------
lld/test/MachO/tlv-dynamic-lookup.s | 20 ++++++++++----------
4 files changed, 18 insertions(+), 24 deletions(-)
diff --git a/lld/MachO/Symbols.h b/lld/MachO/Symbols.h
index 6b5917e70cd81..6d31b67e1d803 100644
--- a/lld/MachO/Symbols.h
+++ b/lld/MachO/Symbols.h
@@ -65,9 +65,7 @@ class Symbol {
virtual bool isTlv() const { return false; }
- // Whether this symbol has a non-lazy pointer slot. A symbol gets at most
- // one, always in __got (including thread-local symbols).
- // See Writer::addNonLazyPointerEntry.
+ // Whether this symbol has a non-lazy pointer slot.
bool isInGot() const { return gotIndex != UINT32_MAX; }
// Whether this symbol is in the StubsSection.
@@ -81,9 +79,7 @@ class Symbol {
return isInStubs() ? getStubVA() : getVA();
}
// The address of this symbol's non-lazy pointer slot, or the symbol's own
- // address if it has none. A thread-local's slot holds the address of its
- // TLV descriptor, which is also what a GOT reference to it wants, so GOT
- // and TLV relocations both resolve through the same __got entry.
+ // address if it has none.
uint64_t resolveNonLazyPtrVA() const {
return isInGot() ? getGotVA() : getVA();
}
diff --git a/lld/MachO/SyntheticSections.h b/lld/MachO/SyntheticSections.h
index 3b8bed5c21ddb..d863758b56c50 100644
--- a/lld/MachO/SyntheticSections.h
+++ b/lld/MachO/SyntheticSections.h
@@ -106,8 +106,8 @@ class PageZeroSection final : public SyntheticSection {
void writeTo(uint8_t *buf) const override {}
};
-// The __DATA_CONST,__got section, populated by dyld with addresses to
-// non-lazily-loaded dylib symbols, including TLV descriptors.
+// The __got section, populated by dyld with addresses to non-lazily-loaded
+// dylib symbols, including TLV descriptors.
class GotSection final : public SyntheticSection {
public:
GotSection();
diff --git a/lld/MachO/Writer.cpp b/lld/MachO/Writer.cpp
index 4af7ed150f694..c9254b7125d7f 100644
--- a/lld/MachO/Writer.cpp
+++ b/lld/MachO/Writer.cpp
@@ -662,12 +662,10 @@ void Writer::treatSpecialUndefineds() {
}
}
-// Give a symbol its single non-lazy pointer slot. For a thread-local,
-// __thread_ptrs and __got would hold the same value: the address of its TLV
-// descriptor. ld-prime canonicalizes both GOT and TLV references to one
-// __got entry, which also makes the choice independent of which reference is
-// scanned first. This is especially important for dynamic-lookup symbols,
-// whose thread-locality is unknowable until dyld binds the slot.
+// Give a symbol its single non-lazy pointer slot. A GOT reference and a TLV
+// reference to a thread-local both want the same value -- the address of its
+// TLV descriptor -- so one __got entry serves both, and the result no longer
+// depends on which reference the relocation scan reaches first.
static void addNonLazyPointerEntry(Symbol *sym) { in.got->addEntry(sym); }
static void prepareSymbolRelocation(Symbol *sym, const InputSection *isec,
diff --git a/lld/test/MachO/tlv-dynamic-lookup.s b/lld/test/MachO/tlv-dynamic-lookup.s
index 2b2d8e36fbf75..c5c983addd1ee 100644
--- a/lld/test/MachO/tlv-dynamic-lookup.s
+++ b/lld/test/MachO/tlv-dynamic-lookup.s
@@ -71,10 +71,10 @@
# REEXPORT: _readTlv
# REEXPORT-NOT: _tlv{{$}}
-## The remaining cases each reach NonLazyPointerSectionBase::addEntry by a route
-## that does not pass through prepareSymbolRelocation. None may abort the
-## linker: the symbol already has a __got slot by the time the TLV reference
-## asks for one, so addEntry must coalesce rather than allocate a second.
+## The remaining cases each reach GotSection::addEntry by a route that does not
+## pass through prepareSymbolRelocation. None may abort the linker: the symbol
+## already has a __got slot by the time the TLV reference asks for one, so
+## addEntry must coalesce rather than allocate a second.
## StubsSection::addEntry -> in.got->addEntry under chained fixups, which is the
## default at iOS 16 / macOS 13.
@@ -106,7 +106,8 @@
# RUN: llvm-objdump --macho --bind %t/binder.dylib | FileCheck %s --check-prefix=BINDER
# BINDER: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace dyld_stub_binder
-## InitOffsetsSection::setUp.
+## InitOffsetsSection::setUp -> in.stubs->addEntry -> in.got->addEntry, i.e. a
+## GOT entry reached two levels deep from a section that scans its own relocs.
# RUN: %no-arg-lld -arch arm64 -platform_version macos 13.0 13.0 \
# RUN: -syslibroot %S/Inputs/MacOSX.sdk -lSystem -dylib -undefined dynamic_lookup \
# RUN: -init_offsets -o %t/initoff.dylib %t/initoff.o
@@ -129,11 +130,10 @@
# EP: lib_ordinal = -2 (flat-namespace)
# EP: _ep
-## JUDGMENT CALL. A data pointer and a TLV reference to one dynamic-lookup
-## symbol cannot both describe whatever dyld finds, but neither can lld tell
-## which is wrong, and the UNSIGNED path allocates no slot for a conflict check
-## to inspect. Accepting both binds matches how every other unverifiable
-## dynamic-lookup mismatch is already treated.
+## An unsigned relocation binds the pointer in place rather than allocating a
+## slot, so a data pointer and a TLV reference to one symbol produce two
+## independent binds. Both name the descriptor, so neither is wrong, and
+## ld-prime emits the same pair.
# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -o %t/unsmix.dylib %t/unsmix.o
# RUN: llvm-objdump --macho --bind %t/unsmix.dylib | FileCheck %s --check-prefix=UNS
# UNS-DAG: __DATA __data 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
>From 900aea073f79a5feb73760a529778c0734fa5270 Mon Sep 17 00:00:00 2001
From: Peter Rong <PeterRong at meta.com>
Date: Fri, 11 Sep 2026 13:56:07 -0700
Subject: [PATCH 4/4] format
---
lld/MachO/Writer.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lld/MachO/Writer.cpp b/lld/MachO/Writer.cpp
index c9254b7125d7f..b97203d656ebd 100644
--- a/lld/MachO/Writer.cpp
+++ b/lld/MachO/Writer.cpp
@@ -665,7 +665,7 @@ void Writer::treatSpecialUndefineds() {
// Give a symbol its single non-lazy pointer slot. A GOT reference and a TLV
// reference to a thread-local both want the same value -- the address of its
// TLV descriptor -- so one __got entry serves both, and the result no longer
-// depends on which reference the relocation scan reaches first.
+// depends on which reference the relocation scan reaches first.
static void addNonLazyPointerEntry(Symbol *sym) { in.got->addEntry(sym); }
static void prepareSymbolRelocation(Symbol *sym, const InputSection *isec,
More information about the llvm-commits
mailing list