[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