[lld] [lld][MachO] Don't reject TLV relocations against dynamic-lookup symbols (PR #221364)
Peter Rong via llvm-commits
llvm-commits at lists.llvm.org
Thu Sep 10 15:46:58 PDT 2026
https://github.com/DataCorrupted updated https://github.com/llvm/llvm-project/pull/221364
>From 3707c64af7894db636fcd2d2e80ad34bfab44e8a 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/3] [lld][MachO] Don't reject TLV relocations against
dynamic-lookup symbols
Reading an `extern _Thread_local` variable whose definition lives in
another image fails to link under `-undefined dynamic_lookup`. This
long-standing bug will be exposed more often by C++20's `constinit`, which
lets the compiler elide the wrapper function a cross-TU `thread_local`
access normally routes through: without the wrapper, the referencing
object emits a direct TLV relocation instead of an ordinary call.
```
$ echo 'extern _Thread_local int tlsVal; int read_tls(void) { return tlsVal; }' > /tmp/a.c
$ clang -target arm64-apple-macos11 -fuse-ld=lld -dynamiclib -nostdlib \
-Wl,-undefined,dynamic_lookup /tmp/a.c -o /tmp/a.dylib
ld64.lld: error: /tmp/a-<hash>.o:(symbol read_tls+0x8): TLVP_LOAD_PAGE21
relocation requires that symbol _tlsVal be thread-local
ld64.lld: error: /tmp/a-<hash>.o:(symbol read_tls+0xc): TLVP_LOAD_PAGEOFF12
relocation requires that symbol _tlsVal be thread-local
```
`validateSymbolRelocation` requires that a relocation carrying the TLV
attribute point at a symbol that `isTlv()`, which is reasonable.
However, a dynamic-lookup symbol fails the check because it has no defining dylib, and Mach-O
has no field for thread-locality on an undefined symbol reference, so there is no way to know.
`addDynamicLookup` creates the symbol with `isTlv = false` because that is the only value available.
Therefore, we relax the check by skipping it if the symbol came from dynamic lookup.
---
lld/MachO/Relocations.cpp | 7 ++++-
lld/test/MachO/tlv-dynamic-lookup.s | 45 +++++++++++++++++++++++++++++
2 files changed, 51 insertions(+), 1 deletion(-)
create mode 100644 lld/test/MachO/tlv-dynamic-lookup.s
diff --git a/lld/MachO/Relocations.cpp b/lld/MachO/Relocations.cpp
index 0945e7a96dee2..319443b631a66 100644
--- a/lld/MachO/Relocations.cpp
+++ b/lld/MachO/Relocations.cpp
@@ -68,7 +68,12 @@ bool macho::validateSymbolRelocation(const Symbol *sym,
.str();
};
- if (relocAttrs.hasAttr(RelocAttrBits::TLV) != sym->isTlv())
+ // A dynamic-lookup symbol's thread-locality is unknown at link time but
+ // resolved by dyld at load time; rejecting it would refuse a valid link.
+ const auto *dysym = dyn_cast<DylibSymbol>(sym);
+ bool tlvKindIsKnown = !(dysym && dysym->isDynamicLookup());
+
+ if (tlvKindIsKnown && relocAttrs.hasAttr(RelocAttrBits::TLV) != sym->isTlv())
error(message(Twine("requires that symbol ") + sym->getName() + " " +
(sym->isTlv() ? "not " : "") + "be thread-local"));
diff --git a/lld/test/MachO/tlv-dynamic-lookup.s b/lld/test/MachO/tlv-dynamic-lookup.s
new file mode 100644
index 0000000000000..5b1cb5706ebfe
--- /dev/null
+++ b/lld/test/MachO/tlv-dynamic-lookup.s
@@ -0,0 +1,45 @@
+# 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/notlv.s -o %t/notlv.o
+
+# 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
+
+# RUN: %lld -arch arm64 -dylib -U _tlv -o %t/u.dylib %t/consumer.o
+# RUN: llvm-objdump --macho --section-headers --bind %t/u.dylib | FileCheck %s
+
+# RUN: %lld -arch arm64 -dylib -flat_namespace -undefined suppress -o %t/flat.dylib %t/consumer.o
+# RUN: llvm-objdump --macho --section-headers --bind %t/flat.dylib | FileCheck %s
+
+# CHECK: __thread_ptrs
+# CHECK-LABEL: Bind table:
+# CHECK: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+
+## The import gets the flat-lookup ordinal.
+# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -fixup_chains \
+# RUN: -o %t/chained.dylib %t/consumer.o
+# RUN: llvm-objdump --macho --chained-fixups %t/chained.dylib | FileCheck %s --check-prefix=CHAINED
+# CHAINED: lib_ordinal = -2 (flat-namespace)
+
+## A dylib that does define the symbol still tells us its thread-locality, so
+## the mismatch check keeps working there.
+# RUN: %lld -arch arm64 -dylib -install_name @rpath/libnotlv.dylib -o %t/libnotlv.dylib %t/notlv.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
+
+#--- consumer.s
+.globl _readTlv
+.p2align 2
+_readTlv:
+ adrp x8, _tlv at TLVPPAGE
+ ldr x8, [x8, _tlv at TLVPPAGEOFF]
+ ret
+
+#--- notlv.s
+.globl _tlv
+.data
+_tlv:
+ .quad 0
>From 88ff7755921281471fada24ef0b245c14df63c21 Mon Sep 17 00:00:00 2001
From: Peter Rong <PeterRong at meta.com>
Date: Thu, 10 Sep 2026 13:07:31 -0700
Subject: [PATCH 2/3] 2nd try
---
lld/MachO/InputSection.cpp | 9 +-
lld/MachO/MapFile.cpp | 2 +-
lld/MachO/Symbols.cpp | 10 +-
lld/MachO/Symbols.h | 13 +-
lld/MachO/SyntheticSections.cpp | 7 +-
lld/MachO/SyntheticSections.h | 18 +-
lld/test/MachO/tlv-dynamic-lookup-x86.s | 51 +++++
lld/test/MachO/tlv-dynamic-lookup.s | 241 ++++++++++++++++++++++--
8 files changed, 324 insertions(+), 27 deletions(-)
create mode 100644 lld/test/MachO/tlv-dynamic-lookup-x86.s
diff --git a/lld/MachO/InputSection.cpp b/lld/MachO/InputSection.cpp
index d977830161a8e..d2841a0f73c39 100644
--- a/lld/MachO/InputSection.cpp
+++ b/lld/MachO/InputSection.cpp
@@ -248,8 +248,13 @@ void ConcatInputSection::writeTo(uint8_t *buf) {
}
referentVA = minuendVA - fromSym->getVA();
} else if (auto *referentSym = r.referent.dyn_cast<Symbol *>()) {
- if (target->hasAttr(r.type, RelocAttrBits::LOAD) &&
- !referentSym->isInGot())
+ // Relaxing a load means "there is no slot; compute the address
+ // directly". Ask about the section this relocation actually uses:
+ // __thread_ptrs for TLV relocations, __got for everything else.
+ bool hasSlot = target->hasAttr(r.type, RelocAttrBits::TLV)
+ ? referentSym->isInTlvPointers()
+ : referentSym->isInGot();
+ if (target->hasAttr(r.type, RelocAttrBits::LOAD) && !hasSlot)
target->relaxGotLoad(loc, r.type);
// For dtrace symbols, do not handle them as normal undefined symbols
if (referentSym->getName().starts_with("___dtrace_")) {
diff --git a/lld/MachO/MapFile.cpp b/lld/MachO/MapFile.cpp
index 29ebcdcf9a832..4d5e543c24c8d 100644
--- a/lld/MachO/MapFile.cpp
+++ b/lld/MachO/MapFile.cpp
@@ -149,7 +149,7 @@ 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->getIndex(*sym) * target->wordSize,
target->wordSize, sym->getName().str().data());
}
diff --git a/lld/MachO/Symbols.cpp b/lld/MachO/Symbols.cpp
index 27419caf9de1e..b622dbcb1901a 100644
--- a/lld/MachO/Symbols.cpp
+++ b/lld/MachO/Symbols.cpp
@@ -50,7 +50,15 @@ 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); }
+bool Symbol::isInTlvPointers() const {
+ return in.tlvPointers->getIndex(*this) != UINT32_MAX;
+}
+uint64_t Symbol::getTlvVA() const {
+ return in.tlvPointers->getVA(in.tlvPointers->getIndex(*this));
+}
+uint64_t Symbol::resolveTlvVA() const {
+ return isInTlvPointers() ? getTlvVA() : getVA();
+}
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..6c89e7a77266a 100644
--- a/lld/MachO/Symbols.h
+++ b/lld/MachO/Symbols.h
@@ -65,9 +65,14 @@ class Symbol {
virtual bool isTlv() const { return false; }
- // Whether this symbol is in the GOT or TLVPointer sections.
+ // Whether this symbol has a __got slot.
bool isInGot() const { return gotIndex != UINT32_MAX; }
+ // Whether this symbol has a __thread_ptrs slot. Unlike the GOT index this
+ // is kept in a side table owned by TlvPointerSection: thread-local imports
+ // are rare, and Symbol is instantiated in the millions.
+ bool isInTlvPointers() const;
+
// Whether this symbol is in the StubsSection.
bool isInStubs() const { return stubsIndex != UINT32_MAX; }
@@ -80,11 +85,9 @@ class Symbol {
return isInStubs() ? getStubVA() : getVA();
}
uint64_t resolveGotVA() const { return isInGot() ? getGotVA() : getVA(); }
- uint64_t resolveTlvVA() const { return isInGot() ? getTlvVA() : getVA(); }
+ uint64_t resolveTlvVA() const;
- // 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 in the 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..8503e0ed6a452 100644
--- a/lld/MachO/SyntheticSections.cpp
+++ b/lld/MachO/SyntheticSections.cpp
@@ -338,10 +338,11 @@ void macho::addNonLazyBindingEntries(const Symbol *sym,
void NonLazyPointerSectionBase::addEntry(Symbol *sym) {
if (entries.insert(sym)) {
- assert(!sym->isInGot());
- sym->gotIndex = entries.size() - 1;
+ assert(getIndex(*sym) == UINT32_MAX);
+ uint32_t index = entries.size() - 1;
+ setIndex(*sym, index);
- addNonLazyBindingEntries(sym, isec, sym->gotIndex * target->wordSize);
+ addNonLazyBindingEntries(sym, isec, index * target->wordSize);
}
}
diff --git a/lld/MachO/SyntheticSections.h b/lld/MachO/SyntheticSections.h
index 9acbd73d277e8..5c9006d3104b6 100644
--- a/lld/MachO/SyntheticSections.h
+++ b/lld/MachO/SyntheticSections.h
@@ -120,9 +120,13 @@ class NonLazyPointerSectionBase : public SyntheticSection {
}
void writeTo(uint8_t *buf) const override;
void addEntry(Symbol *sym);
- uint64_t getVA(uint32_t gotIndex) const {
- return addr + gotIndex * target->wordSize;
+ uint64_t getVA(uint32_t index) const {
+ return addr + index * target->wordSize;
}
+ // This symbol's slot index within this section, or UINT32_MAX. __got and
+ // __thread_ptrs track indices separately so a symbol may appear in both.
+ virtual uint32_t getIndex(const Symbol &sym) const = 0;
+ virtual void setIndex(Symbol &sym, uint32_t index) = 0;
private:
llvm::SetVector<const Symbol *> entries;
@@ -131,11 +135,21 @@ class NonLazyPointerSectionBase : public SyntheticSection {
class GotSection final : public NonLazyPointerSectionBase {
public:
GotSection();
+ uint32_t getIndex(const Symbol &sym) const override { return sym.gotIndex; }
+ void setIndex(Symbol &sym, uint32_t index) override { sym.gotIndex = index; }
};
class TlvPointerSection final : public NonLazyPointerSectionBase {
public:
TlvPointerSection();
+ uint32_t getIndex(const Symbol &sym) const override {
+ auto it = indices.find(&sym);
+ return it == indices.end() ? UINT32_MAX : it->second;
+ }
+ void setIndex(Symbol &sym, uint32_t index) override { indices[&sym] = index; }
+
+private:
+ llvm::DenseMap<const Symbol *, uint32_t> indices;
};
struct Location {
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..80ef492218373
--- /dev/null
+++ b/lld/test/MachO/tlv-dynamic-lookup-x86.s
@@ -0,0 +1,51 @@
+# 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: __thread_ptrs
+# MOVQ-LABEL: Bind table:
+# MOVQ: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+
+## FIXME: leaq computes the address of the __thread_ptrs slot, 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
index 5b1cb5706ebfe..96fb12e1541d1 100644
--- a/lld/test/MachO/tlv-dynamic-lookup.s
+++ b/lld/test/MachO/tlv-dynamic-lookup.s
@@ -1,35 +1,149 @@
# 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
# 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: __thread_ptrs
+# CHECK-LABEL: Bind table:
+# CHECK: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
-# RUN: %lld -arch arm64 -dylib -U _tlv -o %t/u.dylib %t/consumer.o
-# RUN: llvm-objdump --macho --section-headers --bind %t/u.dylib | FileCheck %s
+## -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 -flat_namespace -undefined suppress -o %t/flat.dylib %t/consumer.o
-# RUN: llvm-objdump --macho --section-headers --bind %t/flat.dylib | FileCheck %s
+# 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 __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# UBOTH-DAG: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _other
-# CHECK: __thread_ptrs
-# CHECK-LABEL: Bind table:
-# CHECK: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# 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 __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
-## The import gets the flat-lookup ordinal.
+## Chained fixups must produce a __thread_ptrs slot, not just a flat import --
+## a GOT-only binary yields a byte-identical import table.
# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -fixup_chains \
# RUN: -o %t/chained.dylib %t/consumer.o
-# RUN: llvm-objdump --macho --chained-fixups %t/chained.dylib | FileCheck %s --check-prefix=CHAINED
-# CHAINED: lib_ordinal = -2 (flat-namespace)
+# RUN: llvm-objdump --macho --section-headers --chained-fixups %t/chained.dylib | \
+# RUN: FileCheck %s --check-prefix=CHAINED
+# CHAINED: __thread_ptrs
+# CHAINED-NOT: __got
+# CHAINED: lib_ordinal = -2 (flat-namespace)
+# CHAINED: _tlv
-## A dylib that does define the symbol still tells us its thread-locality, so
-## the mismatch check keeps working there.
# 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 __thread_ptrs 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. Where lld synthesizes the competing entry itself the user has no way
+## to act on a diagnostic, so these must link.
+
+## 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's __got slot and the TLV's __thread_ptrs slot must coexist.
+# 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-DAG: __got
+# BRANCHC-DAG: __thread_ptrs
+# 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 __thread_ptrs 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 __thread_ptrs 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 __thread_ptrs 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-DAG: __got
+# INITOFF-DAG: __thread_ptrs
+# 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-DAG: __got
+# EP-DAG: __thread_ptrs
+# 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 __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+
+## JUDGMENT CALL. Same question for a GOT reference, which does allocate a slot.
+## Treated the same way for consistency with UNS and BRANCH above.
+# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -o %t/mixed.dylib %t/mixed.o
+# RUN: llvm-objdump --macho --bind %t/mixed.dylib | FileCheck %s --check-prefix=MIXED
+# MIXED-DAG: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# MIXED-DAG: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+
#--- consumer.s
.globl _readTlv
.p2align 2
@@ -37,6 +151,107 @@ _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
>From 1c25f79626afaa15dac7f5187f022b82b3f675f5 Mon Sep 17 00:00:00 2001
From: Peter Rong <PeterRong at meta.com>
Date: Thu, 10 Sep 2026 13:50:36 -0700
Subject: [PATCH 3/3] [lld][MachO] Give a TLV symbol one non-lazy pointer slot,
as ld64 does
A thread-local symbol's __thread_ptrs slot and its __got slot hold the
very same value -- the address of its TLV descriptor -- so a symbol
referenced both ways needs only one of them, and either reference kind
can read it. Route both through a single slot:
- a known thread-local gets its slot in __thread_ptrs,
- anything else, including a dynamic-lookup symbol whose
thread-locality is unknowable at link time, gets one in __got.
This fixes two bugs at once.
Since 3707c64af789 relaxed validateSymbolRelocation for dynamic-lookup
symbols, a TLV and a GOT relocation against one such symbol could both
allocate a slot. Symbol::gotIndex served both sections, so the second
addEntry tripped `assert(!sym->isInGot())`, or in a release build
silently overwrote the first index. Sending both to __got makes them
coalesce. ld64 lowers this input identically -- one __got slot, both
references reading it -- so the output is now instruction-for-
instruction what the reference implementation emits.
Second, only one direction of the TLV/non-TLV relocation mismatch is
really an error. A GOT relocation against a thread-local is meaningful:
it asks for the descriptor's address, which is what the slot holds.
Rejecting it also left the reciprocal half of the original bug unfixed.
A TLV relocation against a symbol known not to be thread-local stays an
error, because the TLV sequence would call the referent's first word as
if it were a resolver function.
That second change deletes two long-standing tests, so the claim that
their diagnostic was overreach is measured rather than argued. Against
Apple's ld-1230.1 on macOS 26.6.2, a GOT relocation targeting a
thread-local links with no diagnostic in every configuration the deleted
tests covered:
- defined in the same image (bad-got-to-tlv-reference.s): exit 0, no
slot at all, the load relaxed to a direct address computation.
- exported by a dylib (bad-got-to-dylib-tlv-reference.s): exit 0 on
both x86_64 and arm64, one __got slot bound to libtlv/_foo.
lld now links both too. For the dylib case it puts the slot in __thread_ptrs rather than __got,
since unlike ld64 it does track that the symbol is thread-local; the two sections hold the same
descriptor address, and a two-thread harness confirms a GOT reference served out of a __thread_ptrs
slot yields a descriptor whose resolver returns the calling thread's storage.
The dynamic-lookup case was verified end to end the same way: lld emits
the same single __got slot and the same instructions as ld64, and dyld
binds it to the descriptor with per-thread reads resolving correctly.
---
lld/MachO/InputSection.cpp | 18 ++---
lld/MachO/MapFile.cpp | 2 +-
lld/MachO/Relocations.cpp | 21 ++++--
lld/MachO/Symbols.cpp | 10 +--
lld/MachO/Symbols.h | 23 +++---
lld/MachO/SyntheticSections.cpp | 9 +--
lld/MachO/SyntheticSections.h | 18 +----
lld/MachO/Writer.cpp | 20 +++++-
lld/test/MachO/got-to-tlv-reference.s | 71 +++++++++++++++++++
.../invalid/bad-got-to-dylib-tlv-reference.s | 23 ------
.../MachO/invalid/bad-got-to-tlv-reference.s | 14 ----
lld/test/MachO/tlv-dynamic-lookup-x86.s | 15 ++--
lld/test/MachO/tlv-dynamic-lookup.s | 59 ++++++++-------
13 files changed, 175 insertions(+), 128 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
diff --git a/lld/MachO/InputSection.cpp b/lld/MachO/InputSection.cpp
index d2841a0f73c39..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();
}
@@ -248,13 +249,8 @@ void ConcatInputSection::writeTo(uint8_t *buf) {
}
referentVA = minuendVA - fromSym->getVA();
} else if (auto *referentSym = r.referent.dyn_cast<Symbol *>()) {
- // Relaxing a load means "there is no slot; compute the address
- // directly". Ask about the section this relocation actually uses:
- // __thread_ptrs for TLV relocations, __got for everything else.
- bool hasSlot = target->hasAttr(r.type, RelocAttrBits::TLV)
- ? referentSym->isInTlvPointers()
- : referentSym->isInGot();
- if (target->hasAttr(r.type, RelocAttrBits::LOAD) && !hasSlot)
+ if (target->hasAttr(r.type, RelocAttrBits::LOAD) &&
+ !referentSym->isInGot())
target->relaxGotLoad(loc, r.type);
// For dtrace symbols, do not handle them as normal undefined symbols
if (referentSym->getName().starts_with("___dtrace_")) {
diff --git a/lld/MachO/MapFile.cpp b/lld/MachO/MapFile.cpp
index 4d5e543c24c8d..29ebcdcf9a832 100644
--- a/lld/MachO/MapFile.cpp
+++ b/lld/MachO/MapFile.cpp
@@ -149,7 +149,7 @@ 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 + osec->getIndex(*sym) * target->wordSize,
+ osec->addr + sym->gotIndex * target->wordSize,
target->wordSize, sym->getName().str().data());
}
diff --git a/lld/MachO/Relocations.cpp b/lld/MachO/Relocations.cpp
index 319443b631a66..09032cea21820 100644
--- a/lld/MachO/Relocations.cpp
+++ b/lld/MachO/Relocations.cpp
@@ -68,14 +68,25 @@ bool macho::validateSymbolRelocation(const Symbol *sym,
.str();
};
- // A dynamic-lookup symbol's thread-locality is unknown at link time but
- // resolved by dyld at load time; rejecting it would refuse a valid link.
+ // Only one direction of the TLV/non-TLV mismatch is an error.
+ //
+ // A GOT relocation against a thread-local is fine: the slot holds the
+ // address of the TLV descriptor, which is what such a reference asks for.
+ // ld64 accepts it too.
+ //
+ // The reverse -- a TLV relocation against a symbol that is not
+ // thread-local -- is a real bug: the TLV sequence would call the referent's
+ // first word as if it were a descriptor's resolver function. But a
+ // dynamic-lookup symbol has no defining dylib and Mach-O cannot express
+ // thread-locality on an undefined reference, so `isTlv()` is false merely
+ // because nothing better is available. dyld resolves it at load time;
+ // rejecting it here would refuse a valid link.
const auto *dysym = dyn_cast<DylibSymbol>(sym);
bool tlvKindIsKnown = !(dysym && dysym->isDynamicLookup());
- if (tlvKindIsKnown && relocAttrs.hasAttr(RelocAttrBits::TLV) != sym->isTlv())
- error(message(Twine("requires that symbol ") + sym->getName() + " " +
- (sym->isTlv() ? "not " : "") + "be thread-local"));
+ if (tlvKindIsKnown && relocAttrs.hasAttr(RelocAttrBits::TLV) && !sym->isTlv())
+ error(message(Twine("requires that symbol ") + sym->getName() +
+ " be thread-local"));
return valid;
}
diff --git a/lld/MachO/Symbols.cpp b/lld/MachO/Symbols.cpp
index b622dbcb1901a..27419caf9de1e 100644
--- a/lld/MachO/Symbols.cpp
+++ b/lld/MachO/Symbols.cpp
@@ -50,15 +50,7 @@ uint64_t Symbol::getLazyPtrVA() const {
return in.lazyPointers->getVA(stubsIndex);
}
uint64_t Symbol::getGotVA() const { return in.got->getVA(gotIndex); }
-bool Symbol::isInTlvPointers() const {
- return in.tlvPointers->getIndex(*this) != UINT32_MAX;
-}
-uint64_t Symbol::getTlvVA() const {
- return in.tlvPointers->getVA(in.tlvPointers->getIndex(*this));
-}
-uint64_t Symbol::resolveTlvVA() const {
- return isInTlvPointers() ? getTlvVA() : getVA();
-}
+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 6c89e7a77266a..93a4a453dfb2a 100644
--- a/lld/MachO/Symbols.h
+++ b/lld/MachO/Symbols.h
@@ -65,14 +65,11 @@ class Symbol {
virtual bool isTlv() const { return false; }
- // Whether this symbol has a __got slot.
+ // Whether this symbol has a non-lazy pointer slot. A symbol gets at most
+ // one: in __thread_ptrs if it is a known thread-local, in __got otherwise.
+ // See Writer::addNonLazyPointerEntry.
bool isInGot() const { return gotIndex != UINT32_MAX; }
- // Whether this symbol has a __thread_ptrs slot. Unlike the GOT index this
- // is kept in a side table owned by TlvPointerSection: thread-local imports
- // are rare, and Symbol is instantiated in the millions.
- bool isInTlvPointers() const;
-
// Whether this symbol is in the StubsSection.
bool isInStubs() const { return stubsIndex != UINT32_MAX; }
@@ -84,10 +81,18 @@ class Symbol {
assert(isa<Defined>(this) || isa<DylibSymbol>(this));
return isInStubs() ? getStubVA() : getVA();
}
- uint64_t resolveGotVA() const { return isInGot() ? getGotVA() : getVA(); }
- uint64_t resolveTlvVA() const;
+ // 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 here and share the one slot.
+ uint64_t resolveNonLazyPtrVA() const {
+ if (!isInGot())
+ return getVA();
+ return isTlv() ? getTlvVA() : getGotVA();
+ }
- // The index of this symbol in the GOT.
+ // The index of this symbol's non-lazy pointer slot within whichever of
+ // __got / __thread_ptrs holds it.
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 8503e0ed6a452..3d57bedae342e 100644
--- a/lld/MachO/SyntheticSections.cpp
+++ b/lld/MachO/SyntheticSections.cpp
@@ -338,11 +338,12 @@ void macho::addNonLazyBindingEntries(const Symbol *sym,
void NonLazyPointerSectionBase::addEntry(Symbol *sym) {
if (entries.insert(sym)) {
- assert(getIndex(*sym) == UINT32_MAX);
- uint32_t index = entries.size() - 1;
- setIndex(*sym, index);
+ // A symbol belongs to at most one non-lazy pointer section, so the index
+ // is unambiguous even though both sections share the field.
+ assert(!sym->isInGot());
+ sym->gotIndex = entries.size() - 1;
- addNonLazyBindingEntries(sym, isec, index * target->wordSize);
+ addNonLazyBindingEntries(sym, isec, sym->gotIndex * target->wordSize);
}
}
diff --git a/lld/MachO/SyntheticSections.h b/lld/MachO/SyntheticSections.h
index 5c9006d3104b6..9acbd73d277e8 100644
--- a/lld/MachO/SyntheticSections.h
+++ b/lld/MachO/SyntheticSections.h
@@ -120,13 +120,9 @@ class NonLazyPointerSectionBase : public SyntheticSection {
}
void writeTo(uint8_t *buf) const override;
void addEntry(Symbol *sym);
- uint64_t getVA(uint32_t index) const {
- return addr + index * target->wordSize;
+ uint64_t getVA(uint32_t gotIndex) const {
+ return addr + gotIndex * target->wordSize;
}
- // This symbol's slot index within this section, or UINT32_MAX. __got and
- // __thread_ptrs track indices separately so a symbol may appear in both.
- virtual uint32_t getIndex(const Symbol &sym) const = 0;
- virtual void setIndex(Symbol &sym, uint32_t index) = 0;
private:
llvm::SetVector<const Symbol *> entries;
@@ -135,21 +131,11 @@ class NonLazyPointerSectionBase : public SyntheticSection {
class GotSection final : public NonLazyPointerSectionBase {
public:
GotSection();
- uint32_t getIndex(const Symbol &sym) const override { return sym.gotIndex; }
- void setIndex(Symbol &sym, uint32_t index) override { sym.gotIndex = index; }
};
class TlvPointerSection final : public NonLazyPointerSectionBase {
public:
TlvPointerSection();
- uint32_t getIndex(const Symbol &sym) const override {
- auto it = indices.find(&sym);
- return it == indices.end() ? UINT32_MAX : it->second;
- }
- void setIndex(Symbol &sym, uint32_t index) override { indices[&sym] = index; }
-
-private:
- llvm::DenseMap<const Symbol *, uint32_t> indices;
};
struct Location {
diff --git a/lld/MachO/Writer.cpp b/lld/MachO/Writer.cpp
index 49415ccb70af8..152ef39b0933f 100644
--- a/lld/MachO/Writer.cpp
+++ b/lld/MachO/Writer.cpp
@@ -662,6 +662,22 @@ void Writer::treatSpecialUndefineds() {
}
}
+// Give a symbol its single non-lazy pointer slot. For a thread-local both a
+// __thread_ptrs slot and a __got slot would hold the very same value -- the
+// address of its TLV descriptor -- so a symbol referenced both ways needs
+// only one of them, and every reference can read it. ld64 likewise emits a
+// single slot for such a symbol.
+//
+// A dynamic-lookup symbol's thread-locality is unknowable at link time, so it
+// lands in __got even when reached through a TLV relocation; ld64 lowers that
+// case the same way. dyld binds the slot to the descriptor either way.
+static void addNonLazyPointerEntry(Symbol *sym) {
+ if (sym->isTlv())
+ in.tlvPointers->addEntry(sym);
+ else
+ in.got->addEntry(sym);
+}
+
static void prepareSymbolRelocation(Symbol *sym, const InputSection *isec,
const Relocation &r) {
if (!sym->isLive()) {
@@ -682,10 +698,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
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..25e1fd69f5631
--- /dev/null
+++ b/lld/test/MachO/got-to-tlv-reference.s
@@ -0,0 +1,71 @@
+# 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; ld64 links every case below without a diagnostic.
+
+# 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
+
+## 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 __thread_ptrs -- the
+## section lld uses for a symbol it knows to be thread-local. ld64 uses __got
+## here; both hold the descriptor's address.
+# 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 __thread_ptrs 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 __got section alongside __thread_ptrs.
+# 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: __got
+# BOTH: __thread_ptrs
+# BOTH-LABEL: Bind table:
+# BOTH: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 libtlv _foo
+# BOTH-NOT: _foo
+
+#--- 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
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/tlv-dynamic-lookup-x86.s b/lld/test/MachO/tlv-dynamic-lookup-x86.s
index 80ef492218373..c39622c4fd169 100644
--- a/lld/test/MachO/tlv-dynamic-lookup-x86.s
+++ b/lld/test/MachO/tlv-dynamic-lookup-x86.s
@@ -9,15 +9,16 @@
# RUN: FileCheck %s --check-prefix=MOVQ
# MOVQ-LABEL: _f:
# MOVQ-NEXT: movq
-# MOVQ: __thread_ptrs
+# MOVQ-NOT: __thread_ptrs
+# MOVQ: __got
# MOVQ-LABEL: Bind table:
-# MOVQ: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# MOVQ: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
-## FIXME: leaq computes the address of the __thread_ptrs slot, 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
+## 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 | \
diff --git a/lld/test/MachO/tlv-dynamic-lookup.s b/lld/test/MachO/tlv-dynamic-lookup.s
index 96fb12e1541d1..3c1a147ad7ea4 100644
--- a/lld/test/MachO/tlv-dynamic-lookup.s
+++ b/lld/test/MachO/tlv-dynamic-lookup.s
@@ -12,11 +12,14 @@
# 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: __thread_ptrs
+# CHECK-NOT: __thread_ptrs
+# CHECK: __got
# CHECK-LABEL: Bind table:
-# CHECK: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# 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 | \
@@ -27,22 +30,21 @@
# 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 __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
-# UBOTH-DAG: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _other
+# 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 __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# FLAT: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
-## Chained fixups must produce a __thread_ptrs slot, not just a flat import --
-## a GOT-only binary yields a byte-identical import table.
+## 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: __thread_ptrs
-# CHAINED-NOT: __got
+# CHAINED-NOT: __thread_ptrs
+# CHAINED: __got
# CHAINED: lib_ordinal = -2 (flat-namespace)
# CHAINED: _tlv
@@ -71,38 +73,38 @@
## The remaining cases each reach NonLazyPointerSectionBase::addEntry by a route
## that does not pass through prepareSymbolRelocation. None may abort the
-## linker. Where lld synthesizes the competing entry itself the user has no way
-## to act on a diagnostic, so these must link.
+## 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's __got slot and the TLV's __thread_ptrs slot must coexist.
+## 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-DAG: __got
-# BRANCHC-DAG: __thread_ptrs
+# 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 __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# 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 __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _pers
+# 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 __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace dyld_stub_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 \
@@ -110,8 +112,8 @@
# 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-DAG: __got
-# INITOFF-DAG: __thread_ptrs
+# INITOFF-NOT: __thread_ptrs
+# INITOFF: __got
# INITOFF: lib_ordinal = -2 (flat-namespace)
# INITOFF: _ctor
@@ -122,8 +124,8 @@
# 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-DAG: __got
-# EP-DAG: __thread_ptrs
+# EP-NOT: __thread_ptrs
+# EP: __got
# EP: lib_ordinal = -2 (flat-namespace)
# EP: _ep
@@ -135,14 +137,17 @@
# 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 __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# UNS-DAG: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
-## JUDGMENT CALL. Same question for a GOT reference, which does allocate a slot.
-## Treated the same way for consistency with UNS and BRANCH above.
+## 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 --bind %t/mixed.dylib | FileCheck %s --check-prefix=MIXED
-# MIXED-DAG: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
-# MIXED-DAG: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# 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
More information about the llvm-commits
mailing list