[lld] [ELF] Support multiple PT_GNU_RELRO when SECTIONS is used without PHDRS (PR #203675)

Fangrui Song via llvm-commits llvm-commits at lists.llvm.org
Fri Jun 12 23:00:24 PDT 2026


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

When a SECTIONS command interleaves relro and non-relro sections, the relro
region is split into discontiguous runs. lld emits an error since
https://reviews.llvm.org/D40359

    error: section: <name> is not contiguous with other relro sections

This is overly strict: while glibc only honors the first PT_GNU_RELRO,
other loaders (e.g. Bionic and FreeBSD rtld-elf) protect every
PT_GNU_RELRO segment.

Emit one PT_GNU_RELRO segment for each contiguous run of relro sections.
Track the boundary section so that `createPhdrs` starts a fresh PT_LOAD
at each relro->non-relro transition, as before.


>From d649efe60d03157b51d7fd4dfee969d6ee0e9e2a Mon Sep 17 00:00:00 2001
From: Fangrui Song <i at maskray.me>
Date: Fri, 12 Jun 2026 19:07:37 -0700
Subject: [PATCH] [ELF] Support multiple PT_GNU_RELRO when SECTIONS is used
 without PHDRS

When a SECTIONS command interleaves relro and non-relro sections, the relro
region is split into discontiguous runs. lld emits an error since
https://reviews.llvm.org/D40359

    error: section: <name> is not contiguous with other relro sections

This is overly strict: while glibc only honors the first PT_GNU_RELRO,
other loaders (e.g. Bionic and FreeBSD rtld-elf) protect every
PT_GNU_RELRO segment.

Emit one PT_GNU_RELRO segment for each contiguous run of relro sections.
Track the boundary section so that `createPhdrs` starts a fresh PT_LOAD
at each relro->non-relro transition, as before.
---
 lld/ELF/Writer.cpp                            | 41 +++++-----
 lld/docs/ReleaseNotes.rst                     |  4 +
 lld/test/ELF/keep-data-section-prefix.s       | 14 +++-
 .../ELF/linkerscript/data-segment-relro.test  | 12 ++-
 .../ELF/relro-non-contiguous-script-data.s    | 60 ++++++++------
 lld/test/ELF/relro-non-contiguous.s           | 79 +++++++++++++------
 6 files changed, 137 insertions(+), 73 deletions(-)

diff --git a/lld/ELF/Writer.cpp b/lld/ELF/Writer.cpp
index 792566a376d5d..4fde241ac472c 100644
--- a/lld/ELF/Writer.cpp
+++ b/lld/ELF/Writer.cpp
@@ -26,6 +26,7 @@
 #include "lld/Common/Filesystem.h"
 #include "lld/Common/Strings.h"
 #include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallPtrSet.h"
 #include "llvm/ADT/StringMap.h"
 #include "llvm/Support/BLAKE3.h"
 #include "llvm/Support/Parallel.h"
@@ -2277,29 +2278,27 @@ SmallVector<std::unique_ptr<PhdrEntry>, 0> Writer<ELFT>::createPhdrs() {
     load->add(ctx.out.programHeaders.get());
   }
 
-  // PT_GNU_RELRO includes all sections that should be marked as
-  // read-only by dynamic linker after processing relocations.
-  // Current dynamic loaders only support one PT_GNU_RELRO PHDR, give
-  // an error message if more than one PT_GNU_RELRO PHDR is required.
-  auto relRo = std::make_unique<PhdrEntry>(ctx, PT_GNU_RELRO, PF_R);
-  bool inRelroPhdr = false;
-  OutputSection *relroEnd = nullptr;
+  // PT_GNU_RELRO includes all sections that should be marked as read-only by
+  // dynamic linker after processing relocations. Create one PT_GNU_RELRO for
+  // each run of contiguous relro sections. No diagnostics even if some loaders
+  // only honor one PT_GNU_RELRO.
+  SmallVector<std::unique_ptr<PhdrEntry>, 0> relRos;
+  SmallPtrSet<OutputSection *, 1> relroEnds;
+  PhdrEntry *relRo = nullptr;
   for (OutputSection *sec : ctx.outputSections) {
     if (!needsPtLoad(sec))
       continue;
     if (isRelroSection(ctx, sec)) {
-      inRelroPhdr = true;
-      if (!relroEnd)
-        relRo->add(sec);
-      else
-        ErrAlways(ctx) << "section: " << sec->name
-                       << " is not contiguous with other relro" << " sections";
-    } else if (inRelroPhdr) {
-      inRelroPhdr = false;
-      relroEnd = sec;
+      if (!relRo) {
+        relRos.push_back(std::make_unique<PhdrEntry>(ctx, PT_GNU_RELRO, PF_R));
+        relRo = relRos.back().get();
+      }
+      relRo->add(sec);
+    } else if (relRo) {
+      relRo = nullptr;
+      relroEnds.insert(sec);
     }
   }
-  relRo->p_align = 1;
 
   for (OutputSection *sec : ctx.outputSections) {
     if (!needsPtLoad(sec))
@@ -2337,7 +2336,7 @@ SmallVector<std::unique_ptr<PhdrEntry>, 0> Writer<ELFT>::createPhdrs() {
 
     bool sameLMARegion =
         load && !sec->lmaExpr && sec->lmaRegion == load->firstSec->lmaRegion;
-    if (load && sec != relroEnd &&
+    if (load && !relroEnds.contains(sec) &&
         sec->memRegion == load->firstSec->memRegion &&
         (sameLMARegion || load->lastSec == ctx.out.programHeaders.get()) &&
         (ctx.script->hasSectionsCommand || sec->type == SHT_NOBITS ||
@@ -2364,8 +2363,10 @@ SmallVector<std::unique_ptr<PhdrEntry>, 0> Writer<ELFT>::createPhdrs() {
     if (OutputSection *sec = ctx.in.dynamic->getParent())
       addHdr(PT_DYNAMIC, sec->getPhdrFlags())->add(sec);
 
-  if (relRo->firstSec)
-    ret.push_back(std::move(relRo));
+  for (std::unique_ptr<PhdrEntry> &phdr : relRos) {
+    phdr->p_align = 1;
+    ret.push_back(std::move(phdr));
+  }
 
   // PT_GNU_EH_FRAME is a special section pointing on .eh_frame_hdr.
   if (ctx.in.ehFrameHdr && ctx.in.ehFrameHdr->isNeeded())
diff --git a/lld/docs/ReleaseNotes.rst b/lld/docs/ReleaseNotes.rst
index 729d6af51d31d..8995a71341a0a 100644
--- a/lld/docs/ReleaseNotes.rst
+++ b/lld/docs/ReleaseNotes.rst
@@ -40,6 +40,10 @@ ELF Improvements
   beats positional last-match-wins; default: positional). In ELF, the glob
   matches input section names (e.g. ``.text.unlikely.code1``).
 
+* When a ``SECTIONS`` command interleaves relro and non-relro sections, lld now
+  emits one ``PT_GNU_RELRO`` segment per contiguous run of relro sections
+  instead of reporting a ``not contiguous with other relro sections`` error.
+
 Breaking changes
 ----------------
 
diff --git a/lld/test/ELF/keep-data-section-prefix.s b/lld/test/ELF/keep-data-section-prefix.s
index 4b08f53620ea4..3419cf284becc 100644
--- a/lld/test/ELF/keep-data-section-prefix.s
+++ b/lld/test/ELF/keep-data-section-prefix.s
@@ -16,8 +16,18 @@
 # RUN: llvm-readelf -l out3 | FileCheck --check-prefixes=SEG,PRE %s
 # RUN: llvm-readelf -S out3 | FileCheck %s --check-prefix=CHECK-PRE
 
-# RUN: not ld.lld -T x.lds a.o 2>&1 | FileCheck %s
-# CHECK: error: section: .relro_padding is not contiguous with other relro sections
+## Without -z keep-data-section-prefix, .data.rel.ro.hot and .data.rel.ro.unlikely
+## are not relro, splitting the relro region into two PT_GNU_RELRO segments.
+# RUN: ld.lld -T x.lds a.o -o out4
+# RUN: llvm-readelf -l out4 | FileCheck %s --check-prefix=SEG2
+
+#            Type      Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
+# SEG2:      GNU_RELRO 0x000248 0x0000000000202248 0x0000000000202248 0x000260 0x000260 R   0x1
+# SEG2-NEXT: GNU_RELRO 0x0014a8 0x00000000002044a8 0x00000000002044a8 0x000000 0x000b58 R   0x1
+
+# SEG2:      Section to Segment mapping:
+# SEG2:      06     .data.rel.ro {{$}}
+# SEG2-NEXT: 07     .relro_padding {{$}}
 
 ## The first RW PT_LOAD segment has FileSiz 0x126f (0x1000 + 0x200 + 0x60 + 0xf),
 ## and its p_offset p_vaddr p_paddr p_filesz should match PT_GNU_RELRO.
diff --git a/lld/test/ELF/linkerscript/data-segment-relro.test b/lld/test/ELF/linkerscript/data-segment-relro.test
index 79bc9156e803f..332386e485dee 100644
--- a/lld/test/ELF/linkerscript/data-segment-relro.test
+++ b/lld/test/ELF/linkerscript/data-segment-relro.test
@@ -44,10 +44,18 @@
 # CHECK2:      Section to Segment mapping:
 # CHECK2:        06     .dynamic __libc_atexit .got .relro_padding {{$}}
 
+## Without DATA_SEGMENT_RELRO_END, __libc_atexit is not relro, splitting the
+## relro region into two PT_GNU_RELRO segments (.dynamic and .got).
 # RUN: sed '/DATA_SEGMENT_RELRO_END/d' %t/1.t > %t/2.t
-# RUN: not ld.lld %t/a.o %t/b.so -T %t/2.t -o /dev/null 2>&1 | FileCheck %s --check-prefix=ERR
+# RUN: ld.lld %t/a.o %t/b.so -T %t/2.t -o %t/a3
+# RUN: llvm-readelf -l %t/a3 | FileCheck %s --check-prefix=CHECK3
 
-# ERR: error: section: .got is not contiguous with other relro sections
+# CHECK3:      GNU_RELRO      0x001000 0x0000000000001000 0x0000000000001000 0x000100 0x000100 R   0x1
+# CHECK3-NEXT: GNU_RELRO      0x001108 0x0000000000001108 0x0000000000001108 0x000008 0x000008 R   0x1
+
+# CHECK3:      Section to Segment mapping:
+# CHECK3:        07     .dynamic {{$}}
+# CHECK3-NEXT:   08     .got {{$}}
 
 #--- a.s
 .global _start
diff --git a/lld/test/ELF/relro-non-contiguous-script-data.s b/lld/test/ELF/relro-non-contiguous-script-data.s
index 530fc7c84eb91..fc30712a71341 100644
--- a/lld/test/ELF/relro-non-contiguous-script-data.s
+++ b/lld/test/ELF/relro-non-contiguous-script-data.s
@@ -1,27 +1,41 @@
-// REQUIRES: x86
-
-// RUN: llvm-mc -filetype=obj -triple=x86_64 /dev/null -o %t2.o
-// RUN: ld.lld -shared -soname=t2 %t2.o -o %t2.so
-// RUN: echo "SECTIONS { \
-// RUN: .dynamic : { *(.dynamic) } \
-// RUN: .non_ro : { . += 1; } \
-// RUN: .jcr : { *(.jcr) } \
-// RUN: } " > %t.script
-// RUN: llvm-mc -filetype=obj -triple=x86_64-pc-linux %s -o %t.o
-// RUN: not ld.lld %t.o %t2.so -o /dev/null --script=%t.script 2>&1 | FileCheck %s
-
-// RUN: echo "SECTIONS { \
-// RUN: .dynamic : { *(.dynamic) } \
-// RUN: .non_ro : { BYTE(1); } \
-// RUN: .jcr : { *(.jcr) } \
-// RUN: } " > %t2.script
-// RUN: not ld.lld %t.o %t2.so -o /dev/null --script=%t2.script 2>&1 | FileCheck %s
-
-// CHECK: error: section: .jcr is not contiguous with other relro sections
+# REQUIRES: x86
+## Script data commands (`. += 1` or BYTE) make an output section need a
+## PT_LOAD, splitting the relro region. Emit one PT_GNU_RELRO per run.
 
+# RUN: rm -rf %t && split-file %s %t && cd %t
+# RUN: llvm-mc -filetype=obj -triple=x86_64 /dev/null -o b.o
+# RUN: ld.lld -shared -soname=b b.o -o b.so
+# RUN: llvm-mc -filetype=obj -triple=x86_64 a.s -o a.o
+
+# RUN: ld.lld a.o b.so -T 1.lds -o out1
+# RUN: llvm-readelf -l out1 | FileCheck %s
+
+# RUN: ld.lld a.o b.so -T 2.lds -o out2
+# RUN: llvm-readelf -l out2 | FileCheck %s
+
+# CHECK:      Type      Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
+# CHECK:      GNU_RELRO 0x001048 0x0000000000000048 0x0000000000000048 0x000090 0x000090 R   0x1
+# CHECK-NEXT: GNU_RELRO 0x0010dc 0x00000000000000dc 0x00000000000000dc 0x000008 0x000008 R   0x1
+
+#--- 1.lds
+SECTIONS {
+  .dynamic : { *(.dynamic) }
+  .non_ro : { . += 1; }
+  .jcr : { *(.jcr) }
+}
+
+#--- 2.lds
+SECTIONS {
+  .dynamic : { *(.dynamic) }
+  .non_ro : { BYTE(1); }
+  .jcr : { *(.jcr) }
+}
+
+#--- a.s
 .global _start
 _start:
 
-        // non-empty relro section
-        .section .jcr, "aw", @progbits
-        .quad 0
+## non-empty relro section
+.section .jcr, "aw", @progbits
+.p2align 2
+.quad 0
diff --git a/lld/test/ELF/relro-non-contiguous.s b/lld/test/ELF/relro-non-contiguous.s
index 3eeeff7c52191..095bca8ea8c2d 100644
--- a/lld/test/ELF/relro-non-contiguous.s
+++ b/lld/test/ELF/relro-non-contiguous.s
@@ -1,28 +1,55 @@
-// REQUIRES: x86
-// RUN: llvm-mc -filetype=obj -triple=x86_64-unknown-linux %p/Inputs/shared.s -o %t.o
-// RUN: llvm-mc -filetype=obj -triple=x86_64-unknown-linux %p/Inputs/copy-in-shared.s -o %t2.o
-// RUN: ld.lld -shared %t.o %t2.o -o %t.so
-
-// Separate RelRo sections .dynamic .bss.rel.ro with non RelRo .got.plt.
-// This causes the RelRo sections to be non-contiguous.
-// RUN: echo "SECTIONS { \
-// RUN: .dynamic : { *(.dynamic) } \
-// RUN: .got.plt : { *(.got.plt) } \
-// RUN: .bss.rel.ro : { *(.bss.rel.o) } \
-// RUN: } " > %t.script
-// RUN: llvm-mc -filetype=obj -triple=x86_64-unknown-linux %s -o %t3.o
-
-// Expect error for non-contiguous relro
-// RUN: not ld.lld %t3.o %t.so -z relro -o /dev/null --script=%t.script 2>&1 | FileCheck %s
-// No error when we do not request relro.
-// RUN: ld.lld %t3.o %t.so -z norelro -o %t --script=%t.script
-
-// CHECK: error: section: .bss.rel.ro is not contiguous with other relro sections
-        .section .text, "ax", @progbits
-        .global _start
-        .global bar
-        .global foo
+# REQUIRES: x86
+## A SECTIONS command may interleave relro and non-relro sections. Emit one
+## PT_GNU_RELRO segment for each contiguous run of relro sections.
+
+# RUN: rm -rf %t && split-file %s %t && cd %t
+# RUN: llvm-mc -filetype=obj -triple=x86_64 a.s -o a.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64 b.s -o b.o
+# RUN: ld.lld -shared -soname=b b.o -o b.so
+
+## .dynamic and the copy relocation in .bss.rel.ro are relro, while the
+## intervening .got.plt is not (no -z now).
+# RUN: ld.lld a.o b.so -z relro -T a.lds -o out
+# RUN: llvm-readelf -l out | FileCheck %s
+
+## -z norelro => No PT_GNU_RELRO.
+# RUN: ld.lld a.o b.so -z norelro -T a.lds -o out2
+# RUN: llvm-readelf -l out2 | FileCheck %s --check-prefix=NORELRO
+
+#             Type      Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
+# CHECK:      GNU_RELRO 0x001100 0x0000000000000100 0x0000000000000100 0x000100 0x000100 R   0x1
+# CHECK-NEXT: GNU_RELRO 0x001220 0x0000000000001000 0x0000000000001000 0x000000 0x000004 R   0x1
+
+# CHECK:      Section to Segment mapping:
+# CHECK:      05     .dynamic {{$}}
+# CHECK-NEXT: 06     .bss.rel.ro {{$}}
+
+# NORELRO-NOT: GNU_RELRO
+
+#--- a.lds
+SECTIONS {
+  .dynamic : { *(.dynamic) }
+  .got.plt : { *(.got.plt) }
+  . = ALIGN(CONSTANT(MAXPAGESIZE));
+  .bss.rel.ro : { *(.bss.rel.ro) }
+}
+
+#--- a.s
+.global _start
 _start:
-        .quad bar
-        .quad foo
+  .quad bar
+  .quad foo
+
+#--- b.s
+.global bar
+.type bar, %function
+bar:
+  nop
 
+## foo is read-only in the DSO so the copy relocation lands in .bss.rel.ro.
+.p2align 2
+.type foo, @object
+.global foo
+foo:
+.size foo, 4
+.space 4



More information about the llvm-commits mailing list