[lld] [ELF] --discard-{locals, all}: mark local symbols referenced by retained non-SHF_ALLOC sections (PR #209042)

Fangrui Song via llvm-commits llvm-commits at lists.llvm.org
Sun Jul 12 11:23:44 PDT 2026


https://github.com/MaskRay created https://github.com/llvm/llvm-project/pull/209042

With --gc-sections, mark() does not scan relocations of retained
non-SHF_ALLOC sections, so local symbols referenced only by such
sections (e.g. .L symbols in .debug_str_offsets referenced by
.debug_info) do not get the USED flag.

-r/--emit-relocs with --discard-{locals,all} would discard the symbols
and rewrite relocations to reference the null symbol, corrupting DWARF
in the output. This is common on RISC-V, where
RISCVELFObjectWriter::needsRelocateWithSymbol returns true, but is also
possible on other targets with --reloc-section-sym=none.

Call markUsedLocalSymbols from the retention loop (introduced by #209035)
to set the flag.

Fix #160789

>From a3bffde9127e39c75c3423ad82c9b0213767b16d Mon Sep 17 00:00:00 2001
From: Fangrui Song <i at maskray.me>
Date: Sun, 12 Jul 2026 10:01:25 -0700
Subject: [PATCH] [ELF] --discard-{locals,all}: mark local symbols referenced
 by retained non-SHF_ALLOC sections

With --gc-sections, mark() does not scan relocations of retained
non-SHF_ALLOC sections, so local symbols referenced only by such
sections (e.g. .L symbols in .debug_str_offsets referenced by
.debug_info) do not get the USED flag.

-r/--emit-relocs with --discard-{locals,all} would discard the symbols
and rewrite relocations to reference the null symbol, corrupting DWARF
in the output. This is common on RISC-V, where
RISCVELFObjectWriter::needsRelocateWithSymbol returns true, but is also
possible on other targets with --reloc-section-sym=none.

Call markUsedLocalSymbols from the retention loop (introduced by #209035)
to set the flag.

Fix #160789
---
 lld/ELF/MarkLive.cpp                      |  5 +++++
 lld/test/ELF/emit-relocs-discard-locals.s | 27 ++++++++++++++++++++---
 2 files changed, 29 insertions(+), 3 deletions(-)

diff --git a/lld/ELF/MarkLive.cpp b/lld/ELF/MarkLive.cpp
index 0f0a49f0d1333..d1e05b5eb9296 100644
--- a/lld/ELF/MarkLive.cpp
+++ b/lld/ELF/MarkLive.cpp
@@ -385,6 +385,7 @@ void MarkLive<ELFT, TrackWhyLive>::run() {
   // referenced by .eh_frame sections, so we scan them for that here.
   for (EhInputSection *eh : ctx.ehInputSections)
     scanEhFrameSection(*eh);
+  bool markUsed = ctx.arg.copyRelocs && ctx.arg.discard != DiscardPolicy::None;
   for (InputSectionBase *sec : ctx.inputSections) {
     if (sec->flags & SHF_GNU_RETAIN) {
       enqueue(sec, /*offset=*/0, /*sym=*/nullptr, {std::nullopt, "retained"});
@@ -420,6 +421,10 @@ void MarkLive<ELFT, TrackWhyLive>::run() {
         sec->markLive();
         for (InputSection *isec : sec->dependentSections)
           isec->markLive();
+        // If -r or --emit-relocs, ensure referenced local symbols are
+        // preserved by --discard-{locals,all} (see shouldKeepInSymtab).
+        if (markUsed)
+          markUsedLocalSymbols<ELFT>(*sec);
       }
     }
 
diff --git a/lld/test/ELF/emit-relocs-discard-locals.s b/lld/test/ELF/emit-relocs-discard-locals.s
index c9841a6fb5186..0fa9678f51cae 100644
--- a/lld/test/ELF/emit-relocs-discard-locals.s
+++ b/lld/test/ELF/emit-relocs-discard-locals.s
@@ -4,8 +4,10 @@
 
 ## There are two separate code paths that mark used local symbols when
 ## --gc-sections is specified and when is not. The test checks both.
+## --reloc-section-sym=none keeps relocations referencing local symbols
+## instead of section symbols.
 
-# RUN: llvm-mc -filetype=obj -triple=x86_64 -save-temp-labels %s -o %t.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64 -save-temp-labels --reloc-section-sym=none %s -o %t.o
 
 # RUN: ld.lld --emit-relocs --discard-locals %t.o -o %tlocal
 # RUN: llvm-readelf -s %tlocal | FileCheck --check-prefixes=DISCARD-LOCALS,DISCARD-LOCALS-NOGC %s
@@ -14,7 +16,7 @@
 ## --gc-sections can discard symbols relative to GCed sections (including STT_SECTION).
 # RUN: ld.lld --emit-relocs --discard-locals --gc-sections %t.o -o %tlocal.gc
 # RUN: llvm-readelf -s %tlocal.gc | FileCheck --check-prefix=DISCARD-LOCALS %s
-# RUN: llvm-readobj -r %tlocal | FileCheck --check-prefix=REL %s
+# RUN: llvm-readobj -r %tlocal.gc | FileCheck --check-prefix=REL %s
 
 # RUN: ld.lld --emit-relocs --discard-all %t.o -o %tall
 # RUN: llvm-readelf -s %tall | FileCheck --check-prefixes=DISCARD-ALL,DISCARD-ALL-NOGC %s
@@ -24,15 +26,20 @@
 # RUN: llvm-readelf -s %tall.gc | FileCheck --check-prefix=DISCARD-ALL %s
 # RUN: llvm-readobj -r %tall.gc | FileCheck --check-prefix=REL %s
 
-## --discard-locals removes unused local symbols which start with ".L"
+## --discard-locals removes unused local symbols which start with ".L".
+## .Llive is referenced by a relocation from a retained non-SHF_ALLOC section
+## and must be kept (#160789).
 # DISCARD-LOCALS:    0: {{0+}} 0 NOTYPE  LOCAL  DEFAULT UND
 # DISCARD-LOCALS-NEXT:           NOTYPE  LOCAL  DEFAULT {{.*}} .Lused
 # DISCARD-LOCALS-NEXT:           NOTYPE  LOCAL  DEFAULT {{.*}} used
 # DISCARD-LOCALS-NEXT:           NOTYPE  LOCAL  DEFAULT {{.*}} unused
 # DISCARD-LOCALS-NOGC-NEXT:      NOTYPE  LOCAL  DEFAULT {{.*}} unused_gc
+# DISCARD-LOCALS-NEXT:           NOTYPE  LOCAL  DEFAULT {{.*}} .Llive
 # DISCARD-LOCALS-NEXT:           SECTION LOCAL  DEFAULT {{.*}} .text
 # DISCARD-LOCALS-NEXT:           SECTION LOCAL  DEFAULT {{.*}} text
 # DISCARD-LOCALS-NOGC-NEXT:      SECTION LOCAL  DEFAULT {{.*}} gc
+# DISCARD-LOCALS-NEXT:           SECTION LOCAL  DEFAULT {{.*}} .debug_str_offsets
+# DISCARD-LOCALS-NEXT:           SECTION LOCAL  DEFAULT {{.*}} .debug_info
 # DISCARD-LOCALS-NEXT:           SECTION LOCAL  DEFAULT {{.*}} .comment
 # DISCARD-LOCALS-NEXT:           NOTYPE  GLOBAL DEFAULT {{.*}} _start
 
@@ -40,9 +47,12 @@
 # DISCARD-ALL:       0: {{0+}} 0 NOTYPE  LOCAL  DEFAULT UND
 # DISCARD-ALL-NEXT:              NOTYPE  LOCAL  DEFAULT {{.*}} .Lused
 # DISCARD-ALL-NEXT:              NOTYPE  LOCAL  DEFAULT {{.*}} used
+# DISCARD-ALL-NEXT:              NOTYPE  LOCAL  DEFAULT {{.*}} .Llive
 # DISCARD-ALL-NEXT:              SECTION LOCAL  DEFAULT {{.*}} .text
 # DISCARD-ALL-NEXT:              SECTION LOCAL  DEFAULT {{.*}} text
 # DISCARD-ALL-NOGC-NEXT:         SECTION LOCAL  DEFAULT {{.*}} gc
+# DISCARD-ALL-NEXT:              SECTION LOCAL  DEFAULT {{.*}} .debug_str_offsets
+# DISCARD-ALL-NEXT:              SECTION LOCAL  DEFAULT {{.*}} .debug_info
 # DISCARD-ALL-NEXT:              SECTION LOCAL  DEFAULT {{.*}} .comment
 # DISCARD-ALL-NEXT:              NOTYPE  GLOBAL DEFAULT {{.*}} _start
 
@@ -51,6 +61,9 @@
 # REL-NEXT:   R_X86_64_PLT32 .Lused 0xFFFFFFFFFFFFFFFC
 # REL-NEXT:   R_X86_64_PLT32 used 0xFFFFFFFFFFFFFFFC
 # REL-NEXT: }
+# REL:      .rela.debug_info {
+# REL-NEXT:   R_X86_64_32 .Llive 0x3
+# REL-NEXT: }
 
 .globl _start
 _start:
@@ -68,3 +81,11 @@ used:
 .Lunused_gc:
 unused_gc:
   ret
+
+## .Llive, relative to a retained non-SHF_ALLOC section, is live.
+.section .debug_str_offsets,"", at progbits
+.Llive:
+.byte 0
+
+.section .debug_info,"", at progbits
+.long .Llive+3



More information about the llvm-commits mailing list