[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 21:22:08 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/4] [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/4] 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 35d2dbb23fece43bf1962f6d16df2dd765ad8431 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/4] [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 inverts two long-standing tests rather than dropping
them, 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.

Both tests keep their inputs and now assert the new behaviour, moving out
of invalid/ to got-to-tlv-reference.s and got-to-dylib-tlv-reference.s since
neither input is invalid any more. (git scores them at 26-28% similarity, so
-M20% is needed to see the moves as renames.)

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                     | 11 ++--
 lld/MachO/Symbols.cpp                         | 10 +---
 lld/MachO/Symbols.h                           | 22 ++++---
 lld/MachO/SyntheticSections.cpp               | 16 +++--
 lld/MachO/SyntheticSections.h                 | 26 ++++----
 lld/MachO/Writer.cpp                          |  5 +-
 lld/test/MachO/got-to-dylib-tlv-reference.s   | 48 +++++++++++++++
 lld/test/MachO/got-to-tlv-reference.s         | 24 ++++++++
 .../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 ++++++++++---------
 lld/test/MachO/tlv-synthetic-got-entry.s      | 44 ++++++++++++++
 15 files changed, 210 insertions(+), 127 deletions(-)
 create mode 100644 lld/test/MachO/got-to-dylib-tlv-reference.s
 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-synthetic-got-entry.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..419ccf36e81d6 100644
--- a/lld/MachO/Relocations.cpp
+++ b/lld/MachO/Relocations.cpp
@@ -68,14 +68,15 @@ 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 a TLV relocation against a symbol that is not thread-local should be
+  // banned; as long as the sym is dynamic lookup, `isTlv` is just a default
+  // false and we can still 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..025cc6668840d 100644
--- a/lld/MachO/Symbols.h
+++ b/lld/MachO/Symbols.h
@@ -65,14 +65,10 @@ 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.
   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 +80,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..8e5e78fc3f810 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);
   }
 }
 
@@ -399,6 +400,13 @@ GotSection::GotSection()
   flags = S_NON_LAZY_SYMBOL_POINTERS;
 }
 
+void GotSection::addEntry(Symbol *sym) {
+  if (sym->isTlv())
+    in.tlvPointers->addEntry(sym);
+  else
+    NonLazyPointerSectionBase::addEntry(sym);
+}
+
 TlvPointerSection::TlvPointerSection()
     : NonLazyPointerSectionBase(segment_names::data,
                                 section_names::threadPtrs) {
diff --git a/lld/MachO/SyntheticSections.h b/lld/MachO/SyntheticSections.h
index 5c9006d3104b6..956e77b5c4f41 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,19 @@ 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; }
+
+  // Redirects a thread-local to __thread_ptrs instead of allocating a slot
+  // here. Both sections would hold the same value -- the address of the
+  // symbol's TLV descriptor -- and Symbol::gotIndex can only describe one
+  // slot, so a symbol must never be in both. Several synthetic sections add
+  // GOT entries without passing through the relocation scan, so the choice
+  // has to live here, where every caller reaches it.
+  void addEntry(Symbol *sym);
 };
 
 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..d8d1b5c8bb005 100644
--- a/lld/MachO/Writer.cpp
+++ b/lld/MachO/Writer.cpp
@@ -684,8 +684,11 @@ static void prepareSymbolRelocation(Symbol *sym, const InputSection *isec,
     if (relocAttrs.hasAttr(RelocAttrBits::POINTER) || needsBinding(sym))
       in.got->addEntry(sym);
   } else if (relocAttrs.hasAttr(RelocAttrBits::TLV)) {
+    // GotSection::addEntry redirects a thread-local to __thread_ptrs. A
+    // dynamic-lookup symbol, whose thread-locality cannot be known here,
+    // keeps its slot in __got -- which is how ld64 lowers it too.
     if (needsBinding(sym))
-      in.tlvPointers->addEntry(sym);
+      in.got->addEntry(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-dylib-tlv-reference.s b/lld/test/MachO/got-to-dylib-tlv-reference.s
new file mode 100644
index 0000000000000..94ad56f07031a
--- /dev/null
+++ b/lld/test/MachO/got-to-dylib-tlv-reference.s
@@ -0,0 +1,48 @@
+# 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
+
+## Imported from a dylib, a GOT reference to a thread-local binds through the
+## __thread_ptrs slot -- the section lld uses for a symbol it knows to be
+## thread-local. lld used to reject this outright; ld64 accepts it, and puts
+## the slot in __got instead. Both hold the descriptor's address.
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/test.s -o %t/test.o
+# RUN: %lld -dylib -lSystem -L%t -ltlv -o %t/test.dylib %t/test.o
+# RUN: llvm-objdump --macho --bind %t/test.dylib | FileCheck %s
+# CHECK: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 libtlv _foo
+
+## Reaching the same thread-local through both a GOT and a TLV relocation must
+## produce one slot, not two bound to the same address. A second would appear
+## as a __got section alongside __thread_ptrs.
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/both.s -o %t/both.o
+# 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
+
+#--- 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
+
+#--- 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/got-to-tlv-reference.s b/lld/test/MachO/got-to-tlv-reference.s
new file mode 100644
index 0000000000000..00d273bdc0b84
--- /dev/null
+++ b/lld/test/MachO/got-to-tlv-reference.s
@@ -0,0 +1,24 @@
+# REQUIRES: x86
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %s -o %t.o
+# RUN: %lld -dylib -o %t.dylib %t.o
+# RUN: llvm-objdump --macho --section-headers -d --no-show-raw-insn %t.dylib | FileCheck %s
+
+## 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 outright; ld64 accepts it. Defined in the same image the
+## reference needs no slot at all -- the load relaxes to a direct address
+## computation, which is also what ld64 emits.
+
+# CHECK-NOT:   __got
+# CHECK-NOT:   __thread_ptrs
+# CHECK-LABEL: _main:
+# CHECK-NEXT:  leaq _foo(%rip), %rax
+
+.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/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
diff --git a/lld/test/MachO/tlv-synthetic-got-entry.s b/lld/test/MachO/tlv-synthetic-got-entry.s
new file mode 100644
index 0000000000000..a2db2f4fbdda0
--- /dev/null
+++ b/lld/test/MachO/tlv-synthetic-got-entry.s
@@ -0,0 +1,44 @@
+# REQUIRES: aarch64
+# RUN: rm -rf %t; split-file %s %t
+
+## UnwindInfoSection adds a GOT entry for a CFI personality directly, without
+## passing through the relocation scan. If that personality is a thread-local
+## already given a __thread_ptrs slot by a TLV relocation, the symbol would end
+## up in both sections -- tripping assert(!sym->isInGot()), or silently
+## overwriting Symbol::gotIndex in a release build. It must yield one slot.
+
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/libtlv.s -o %t/libtlv.o
+# RUN: %lld -arch arm64 -dylib -install_name @executable_path/libtlv.dylib \
+# RUN:   -lSystem -o %t/libtlv.dylib %t/libtlv.o
+
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/cfi.s -o %t/cfi.o
+# RUN: %lld -arch arm64 -dylib -lSystem -L%t -ltlv -o %t/cfi.dylib %t/cfi.o
+# RUN: llvm-objdump --macho --section-headers --bind %t/cfi.dylib | FileCheck %s
+# CHECK-NOT:   __got
+# CHECK:       __thread_ptrs
+# CHECK-LABEL: Bind table:
+# CHECK:       __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 libtlv _pers
+# CHECK-NOT:   _pers
+
+#--- libtlv.s
+.globl _pers
+.section __DATA,__thread_data,thread_local_regular
+_pers$tlv$init:
+  .quad 0
+.section __DATA,__thread_vars,thread_local_variables
+_pers:
+  .quad __tlv_bootstrap
+  .quad 0
+  .quad _pers$tlv$init
+
+#--- 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

>From 4061bde2a78d079b9f9878c29d727090271cb012 Mon Sep 17 00:00:00 2001
From: Peter Rong <PeterRong at meta.com>
Date: Thu, 10 Sep 2026 17:09:22 -0700
Subject: [PATCH 4/4] [NFC][lld][MachO] Rename gotIndex/isInGot to reflect both
 sections

A symbol's non-lazy pointer slot lives in __thread_ptrs when the symbol is
a known thread-local and in __got otherwise, so the `got` in these two
names no longer matches what they mean. It read as an outright bug in
Symbol::getTlvVA, which indexed __thread_ptrs with something called
`gotIndex`.

Rename to match the vocabulary already used for these sections --
NonLazyPointerSectionBase, addNonLazyBindingEntries, resolveNonLazyPtrVA:

  Symbol::gotIndex -> Symbol::nonLazyPtrIndex
  Symbol::isInGot  -> Symbol::hasNonLazyPtr

NonLazyPointerSectionBase::getVA's parameter becomes `index`, since the
base class serves both sections. The comments that existed to explain away
the old names are dropped or reduced to stating the invariant. A few lines
are reflowed by clang-format because the new name is longer.
---
 lld/MachO/Arch/ARM64.cpp        |  2 +-
 lld/MachO/Arch/ARM64Common.h    |  2 +-
 lld/MachO/Arch/X86_64.cpp       |  6 +++---
 lld/MachO/InputSection.cpp      |  2 +-
 lld/MachO/MapFile.cpp           |  2 +-
 lld/MachO/Symbols.cpp           |  6 ++++--
 lld/MachO/Symbols.h             | 13 ++++++-------
 lld/MachO/SyntheticSections.cpp |  9 +++++----
 lld/MachO/SyntheticSections.h   | 12 ++++++------
 lld/MachO/UnwindInfoSection.cpp |  2 +-
 10 files changed, 29 insertions(+), 27 deletions(-)

diff --git a/lld/MachO/Arch/ARM64.cpp b/lld/MachO/Arch/ARM64.cpp
index 7acf48cbbf123..2caa2a56f8ec2 100644
--- a/lld/MachO/Arch/ARM64.cpp
+++ b/lld/MachO/Arch/ARM64.cpp
@@ -131,7 +131,7 @@ void ARM64::writeObjCMsgSendStub(uint8_t *buf, Symbol *sym, uint64_t stubsAddr,
   if (config->objcStubsMode == ObjCStubsMode::fast) {
     objcStubSize = target->objcStubsFastSize;
     objcMsgSendAddr = in.got->addr;
-    objcMsgSendIndex = objcMsgSend->gotIndex;
+    objcMsgSendIndex = objcMsgSend->nonLazyPtrIndex;
     ::writeObjCMsgSendFastStub<LP64>(buf, objcStubsFastCode, sym, stubsAddr,
                                      stubOffset, selrefVA, objcMsgSendAddr,
                                      objcMsgSendIndex);
diff --git a/lld/MachO/Arch/ARM64Common.h b/lld/MachO/Arch/ARM64Common.h
index f20cd4b170249..cb2a1f9784910 100644
--- a/lld/MachO/Arch/ARM64Common.h
+++ b/lld/MachO/Arch/ARM64Common.h
@@ -134,7 +134,7 @@ inline void writeStubHelperHeader(uint8_t *buf8,
   encodePageOff12(&buf32[1], d, stubHelperHeaderCode[1], loaderVA);
   buf32[2] = stubHelperHeaderCode[2];
   uint64_t binderVA =
-      in.got->addr + in.stubHelper->stubBinder->gotIndex * LP::wordSize;
+      in.got->addr + in.stubHelper->stubBinder->nonLazyPtrIndex * LP::wordSize;
   encodePage21(&buf32[3], d, stubHelperHeaderCode[3],
                pageBits(binderVA) - pcPageBits(3));
   encodePageOff12(&buf32[4], d, stubHelperHeaderCode[4], binderVA);
diff --git a/lld/MachO/Arch/X86_64.cpp b/lld/MachO/Arch/X86_64.cpp
index c0bdffa22f254..6e9472a5838ec 100644
--- a/lld/MachO/Arch/X86_64.cpp
+++ b/lld/MachO/Arch/X86_64.cpp
@@ -174,8 +174,8 @@ void X86_64::writeStubHelperHeader(uint8_t *buf) const {
   writeRipRelative(d, buf, in.stubHelper->addr, 7,
                    in.imageLoaderCache->getVA());
   writeRipRelative(d, buf, in.stubHelper->addr, 0xf,
-                   in.got->addr +
-                       in.stubHelper->stubBinder->gotIndex * LP64::wordSize);
+                   in.got->addr + in.stubHelper->stubBinder->nonLazyPtrIndex *
+                                      LP64::wordSize);
 }
 
 static constexpr uint8_t stubHelperEntry[] = {
@@ -200,7 +200,7 @@ void X86_64::writeObjCMsgSendStub(uint8_t *buf, Symbol *sym, uint64_t stubsAddr,
                                   uint64_t &stubOffset, uint64_t selrefVA,
                                   Symbol *objcMsgSend) const {
   uint64_t objcMsgSendAddr = in.got->addr;
-  uint64_t objcMsgSendIndex = objcMsgSend->gotIndex;
+  uint64_t objcMsgSendIndex = objcMsgSend->nonLazyPtrIndex;
 
   memcpy(buf, objcStubsFastCode, sizeof(objcStubsFastCode));
   SymbolDiagnostic d = {sym, sym->getName()};
diff --git a/lld/MachO/InputSection.cpp b/lld/MachO/InputSection.cpp
index beeeb8e7d5c64..74c0b2a2bff17 100644
--- a/lld/MachO/InputSection.cpp
+++ b/lld/MachO/InputSection.cpp
@@ -250,7 +250,7 @@ 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())
+          !referentSym->hasNonLazyPtr())
         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..47e0ec06fa9ef 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 + sym->nonLazyPtrIndex * target->wordSize,
                  target->wordSize, sym->getName().str().data());
 }
 
diff --git a/lld/MachO/Symbols.cpp b/lld/MachO/Symbols.cpp
index 27419caf9de1e..18ab4bc3c79cb 100644
--- a/lld/MachO/Symbols.cpp
+++ b/lld/MachO/Symbols.cpp
@@ -49,8 +49,10 @@ uint64_t Symbol::getStubVA() const { return in.stubs->getVA(stubsIndex); }
 uint64_t Symbol::getLazyPtrVA() const {
   return in.lazyPointers->getVA(stubsIndex);
 }
-uint64_t Symbol::getGotVA() const { return in.got->getVA(gotIndex); }
-uint64_t Symbol::getTlvVA() const { return in.tlvPointers->getVA(gotIndex); }
+uint64_t Symbol::getGotVA() const { return in.got->getVA(nonLazyPtrIndex); }
+uint64_t Symbol::getTlvVA() const {
+  return in.tlvPointers->getVA(nonLazyPtrIndex);
+}
 
 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 025cc6668840d..f904310756bd9 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: in __thread_ptrs if it is a known thread-local, in __got otherwise.
-  bool isInGot() const { return gotIndex != UINT32_MAX; }
+  bool hasNonLazyPtr() const { return nonLazyPtrIndex != UINT32_MAX; }
 
   // Whether this symbol is in the StubsSection.
   bool isInStubs() const { return stubsIndex != UINT32_MAX; }
@@ -85,14 +83,15 @@ class Symbol {
   // 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())
+    if (!hasNonLazyPtr())
       return getVA();
     return isTlv() ? getTlvVA() : getGotVA();
   }
 
-  // The index of this symbol's non-lazy pointer slot within whichever of
-  // __got / __thread_ptrs holds it.
-  uint32_t gotIndex = UINT32_MAX;
+  // A symbol has at most one non-lazy pointer slot: in __thread_ptrs if it is
+  // a known thread-local, in __got otherwise. This indexes whichever of the
+  // two holds it.
+  uint32_t nonLazyPtrIndex = UINT32_MAX;
   uint32_t lazyBindOffset = UINT32_MAX;
   uint32_t stubsHelperIndex = UINT32_MAX;
   uint32_t stubsIndex = UINT32_MAX;
diff --git a/lld/MachO/SyntheticSections.cpp b/lld/MachO/SyntheticSections.cpp
index 8e5e78fc3f810..61fe58a24bc76 100644
--- a/lld/MachO/SyntheticSections.cpp
+++ b/lld/MachO/SyntheticSections.cpp
@@ -340,10 +340,11 @@ void NonLazyPointerSectionBase::addEntry(Symbol *sym) {
   if (entries.insert(sym)) {
     // 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;
+    assert(!sym->hasNonLazyPtr());
+    sym->nonLazyPtrIndex = entries.size() - 1;
 
-    addNonLazyBindingEntries(sym, isec, sym->gotIndex * target->wordSize);
+    addNonLazyBindingEntries(sym, isec,
+                             sym->nonLazyPtrIndex * target->wordSize);
   }
 }
 
@@ -940,7 +941,7 @@ void ObjCStubsSection::setUp() {
   objcMsgSend->used = true;
   if (config->objcStubsMode == ObjCStubsMode::fast) {
     in.got->addEntry(objcMsgSend);
-    assert(objcMsgSend->isInGot());
+    assert(objcMsgSend->hasNonLazyPtr());
   } else {
     assert(config->objcStubsMode == ObjCStubsMode::small);
     // In line with ld64's behavior, when objc_msgSend is a direct symbol,
diff --git a/lld/MachO/SyntheticSections.h b/lld/MachO/SyntheticSections.h
index 956e77b5c4f41..9631de5451180 100644
--- a/lld/MachO/SyntheticSections.h
+++ b/lld/MachO/SyntheticSections.h
@@ -120,8 +120,8 @@ 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;
   }
 
 private:
@@ -134,10 +134,10 @@ class GotSection final : public NonLazyPointerSectionBase {
 
   // Redirects a thread-local to __thread_ptrs instead of allocating a slot
   // here. Both sections would hold the same value -- the address of the
-  // symbol's TLV descriptor -- and Symbol::gotIndex can only describe one
-  // slot, so a symbol must never be in both. Several synthetic sections add
-  // GOT entries without passing through the relocation scan, so the choice
-  // has to live here, where every caller reaches it.
+  // symbol's TLV descriptor -- and a symbol has only one slot, so it must
+  // never be in both. Several synthetic sections add GOT entries without
+  // passing through the relocation scan, so the choice has to live here,
+  // where every caller reaches it.
   void addEntry(Symbol *sym);
 };
 
diff --git a/lld/MachO/UnwindInfoSection.cpp b/lld/MachO/UnwindInfoSection.cpp
index 9775a723a92fd..ff3c7b2a0eb9a 100644
--- a/lld/MachO/UnwindInfoSection.cpp
+++ b/lld/MachO/UnwindInfoSection.cpp
@@ -305,7 +305,7 @@ void UnwindInfoSectionImpl::prepareRelocations(ConcatInputSection *isec) {
         Defined *const *gotEntry =
             llvm::find_if(referentIsec->symbols, [&](Defined const *d) {
               return d->value == static_cast<uint64_t>(r.addend) &&
-                     d->isInGot();
+                     d->hasNonLazyPtr();
             });
         if (gotEntry != referentIsec->symbols.end()) {
           s = *gotEntry;



More information about the llvm-commits mailing list