[v2] elf: Support multiple PT_GNU_RELRO segments

Message ID 20260820210602.2420927-1-adhemerval.zanella@linaro.org (mailing list archive)
State Superseded
Delegated to: Florian Weimer
Headers
Series [v2] elf: Support multiple PT_GNU_RELRO segments |

Commit Message

Adhemerval Zanella Netto Aug. 20, 2026, 9:05 p.m. UTC
  When binaries become extremely large, PC-relative references to a
single GOT can exceed the +/- 2GB limit. To resolve this, we'd like
to generate multiple GOTs, which would require multiple PT_GNU_RELRO
segments.

This change modifies RELRO protection by removing cached fields
(l_relro_addr and l_relro_size) and instead iterating over all program
headers to protect every PT_GNU_RELRO segment discovered. The same
applies to _dl_readonly_area, where a queried range may now span
multiple adjacent segments.

Since mprotect operates on whole pages while the static linker only
aligns the segment end (and possibly only to a link-time page size
smaller than the run-time one), the protected range is computed by the
new _dl_relro_range helper. The end is rounded down so following
writable data never loses write access, and the start is rounded down
only for a segment that starts a PT_LOAD segment, where the leading
page slack is unused (BFD ld does not page-align the start, so
rounding it up could drop the protection entirely).  A RELRO segment
in the middle of a PT_LOAD segment is preceded by live writable data,
so its start is rounded up instead.

The new tests tst-relro-multi and tst-relro-multi-static carry two
placeholder writable PT_NOTE segments, placed by a linker script and
converted to PT_GNU_RELRO by the new --note-to-relro option of
scripts/tst-elf-edit.py.

Co-authored-by: Justin Rivera <jnrivera@google.com>
---
 elf/Makefile                 | 32 ++++++++++++++++++++
 elf/dl-load.c                |  5 ----
 elf/dl-readonly-area.c       | 40 +++++++++++++++++--------
 elf/dl-reloc.c               | 35 ++++++++++++----------
 elf/dl-support.c             |  5 ----
 elf/rtld.c                   | 15 ----------
 elf/tst-relro-multi-notes.S  | 58 ++++++++++++++++++++++++++++++++++++
 elf/tst-relro-multi-static.c | 19 ++++++++++++
 elf/tst-relro-multi.c        | 43 ++++++++++++++++++++++++++
 elf/tst-relro-multi.lds      | 24 +++++++++++++++
 elf/tst-relro-symbols.py     | 45 +++++++++++++++-------------
 include/link.h               |  4 ---
 scripts/tst-elf-edit.py      | 46 ++++++++++++++++++++++++++++
 sysdeps/generic/ldsodefs.h   | 36 ++++++++++++++++++++++
 14 files changed, 330 insertions(+), 77 deletions(-)
 create mode 100644 elf/tst-relro-multi-notes.S
 create mode 100644 elf/tst-relro-multi-static.c
 create mode 100644 elf/tst-relro-multi.c
 create mode 100644 elf/tst-relro-multi.lds
  

Comments

Adhemerval Zanella Netto Sept. 8, 2026, 6:33 p.m. UTC | #1
Ping.

On 20/08/26 18:05, Adhemerval Zanella wrote:
> When binaries become extremely large, PC-relative references to a
> single GOT can exceed the +/- 2GB limit. To resolve this, we'd like
> to generate multiple GOTs, which would require multiple PT_GNU_RELRO
> segments.
> 
> This change modifies RELRO protection by removing cached fields
> (l_relro_addr and l_relro_size) and instead iterating over all program
> headers to protect every PT_GNU_RELRO segment discovered. The same
> applies to _dl_readonly_area, where a queried range may now span
> multiple adjacent segments.
> 
> Since mprotect operates on whole pages while the static linker only
> aligns the segment end (and possibly only to a link-time page size
> smaller than the run-time one), the protected range is computed by the
> new _dl_relro_range helper. The end is rounded down so following
> writable data never loses write access, and the start is rounded down
> only for a segment that starts a PT_LOAD segment, where the leading
> page slack is unused (BFD ld does not page-align the start, so
> rounding it up could drop the protection entirely).  A RELRO segment
> in the middle of a PT_LOAD segment is preceded by live writable data,
> so its start is rounded up instead.
> 
> The new tests tst-relro-multi and tst-relro-multi-static carry two
> placeholder writable PT_NOTE segments, placed by a linker script and
> converted to PT_GNU_RELRO by the new --note-to-relro option of
> scripts/tst-elf-edit.py.
> 
> Co-authored-by: Justin Rivera <jnrivera@google.com>
> ---
>  elf/Makefile                 | 32 ++++++++++++++++++++
>  elf/dl-load.c                |  5 ----
>  elf/dl-readonly-area.c       | 40 +++++++++++++++++--------
>  elf/dl-reloc.c               | 35 ++++++++++++----------
>  elf/dl-support.c             |  5 ----
>  elf/rtld.c                   | 15 ----------
>  elf/tst-relro-multi-notes.S  | 58 ++++++++++++++++++++++++++++++++++++
>  elf/tst-relro-multi-static.c | 19 ++++++++++++
>  elf/tst-relro-multi.c        | 43 ++++++++++++++++++++++++++
>  elf/tst-relro-multi.lds      | 24 +++++++++++++++
>  elf/tst-relro-symbols.py     | 45 +++++++++++++++-------------
>  include/link.h               |  4 ---
>  scripts/tst-elf-edit.py      | 46 ++++++++++++++++++++++++++++
>  sysdeps/generic/ldsodefs.h   | 36 ++++++++++++++++++++++
>  14 files changed, 330 insertions(+), 77 deletions(-)
>  create mode 100644 elf/tst-relro-multi-notes.S
>  create mode 100644 elf/tst-relro-multi-static.c
>  create mode 100644 elf/tst-relro-multi.c
>  create mode 100644 elf/tst-relro-multi.lds
> 
> diff --git a/elf/Makefile b/elf/Makefile
> index 8b063e1bbae..a6c4afc6265 100644
> --- a/elf/Makefile
> +++ b/elf/Makefile
> @@ -1530,6 +1530,38 @@ tests-special += $(objpfx)tst-tls-debug-recursive.out
>  tests-special += $(objpfx)tst-dl-debug-exclude.out
>  endif
>  
> +test-srcs += tst-relro-multi
> +tests-static += tst-relro-multi-static
> +extra-test-objs += tst-relro-multi-notes.o
> +LDFLAGS-tst-relro-multi = -Wl,-z,now -Wl,-T,$(..)elf/tst-relro-multi.lds
> +LDFLAGS-tst-relro-multi-static = -Wl,-T,$(..)elf/tst-relro-multi.lds
> +$(objpfx)tst-relro-multi: +nolink-deps += tst-relro-multi.lds
> +$(objpfx)tst-relro-multi: $(objpfx)tst-relro-multi-notes.o tst-relro-multi.lds
> +$(objpfx)tst-relro-multi-static: +nolink-deps += tst-relro-multi.lds
> +$(objpfx)tst-relro-multi-static: $(objpfx)tst-relro-multi-notes.o \
> +  tst-relro-multi.lds
> +
> +$(objpfx)tst-relro-multi-patched $(objpfx)tst-relro-multi-static-patched: \
> +  %-patched: % $(..)scripts/tst-elf-edit.py
> +	cp $< $@.tmp
> +	$(PYTHON) $(..)scripts/tst-elf-edit.py --note-to-relro 2 $@.tmp
> +	mv $@.tmp $@
> +
> +$(objpfx)tst-relro-multi-patched.out: $(objpfx)tst-relro-multi-patched \
> +  $(objpfx)ld.so
> +	$(run-program-prefix) $< > $@ 2>&1; \
> +	$(evaluate-test)
> +
> +$(objpfx)tst-relro-multi-static-patched.out: \
> +  $(objpfx)tst-relro-multi-static-patched
> +	$(test-wrapper) $< > $@ 2>&1; \
> +	$(evaluate-test)
> +
> +ifeq ($(run-built-tests),yes)
> +tests-special += $(objpfx)tst-relro-multi-patched.out
> +tests-special += $(objpfx)tst-relro-multi-static-patched.out
> +endif
> +
>  # The test requires shared _and_ PIE because the executable
>  # unit test driver must be able to link with the shared object
>  # that is going to eventually go into an installed DSO.
> diff --git a/elf/dl-load.c b/elf/dl-load.c
> index 95404adae94..e76d149f1f6 100644
> --- a/elf/dl-load.c
> +++ b/elf/dl-load.c
> @@ -1091,11 +1091,6 @@ _dl_map_object_scan_phdrs (struct dl_pt_load_iterator *it,
>  	case PT_GNU_STACK:
>  	  *stack_flagsp = pf_to_prot (ph->p_flags);
>  	  break;
> -
> -	case PT_GNU_RELRO:
> -	  l->l_relro_addr = ph->p_vaddr;
> -	  l->l_relro_size = ph->p_memsz;
> -	  break;
>  	}
>      }
>  
> diff --git a/elf/dl-readonly-area.c b/elf/dl-readonly-area.c
> index 833f4559049..0b7a5c0d319 100644
> --- a/elf/dl-readonly-area.c
> +++ b/elf/dl-readonly-area.c
> @@ -17,23 +17,37 @@
>     <https://www.gnu.org/licenses/>.  */
>  
>  #include <ldsodefs.h>
> +#include <sys/param.h>
>  
> -static bool
> +static enum dl_readonly_area_error_type
>  check_relro (const struct link_map *l, uintptr_t start, uintptr_t end)
>  {
> -  if (l->l_relro_addr != 0)
> -    {
> -      uintptr_t relro_start = ALIGN_DOWN (l->l_addr + l->l_relro_addr,
> -					  GLRO(dl_pagesize));
> -      uintptr_t relro_end = ALIGN_DOWN (l->l_addr + l->l_relro_addr
> -					+ l->l_relro_size,
> -					GLRO(dl_pagesize));
> -      /* RELRO is caved out from a RW segment, so the next range is either
> -	 RW or nonexistent.  */
> -      return relro_start <= start && end <= relro_end
> -	? dl_readonly_area_rdonly : dl_readonly_area_writable;
> +  /* The range may span multiple PT_GNU_RELRO segments whose ranges are
> +     adjacent, so accumulate the covered bytes.  */
> +  size_t size = end - start;
> +  for (const ElfW(Phdr) *ph = l->l_phdr; ph < &l->l_phdr[l->l_phnum]; ++ph)
> +    if (ph->p_type == PT_GNU_RELRO)
> +      {
> +	bool at_load_start = false;
> +	for (const ElfW(Phdr) *lph = l->l_phdr;
> +	     lph < &l->l_phdr[l->l_phnum]; ++lph)
> +	  if (lph->p_type == PT_LOAD && lph->p_vaddr == ph->p_vaddr)
> +	    {
> +	      at_load_start = true;
> +	      break;
> +	    }
>  
> -    }
> +	struct dl_relro_range relro = _dl_relro_range (l, ph, at_load_start);
> +	uintptr_t from = MAX (relro.start, start);
> +	uintptr_t to = MIN (relro.end, end);
> +	if (from < to)
> +	  size -= to - from;
> +	if (size == 0)
> +	  return dl_readonly_area_rdonly;
> +      }
> +
> +  /* RELRO is caved out from a RW segment, so any range outside of
> +     a RELRO segment is either RW or nonexistent.  */
>    return dl_readonly_area_writable;
>  }
>  
> diff --git a/elf/dl-reloc.c b/elf/dl-reloc.c
> index 191d39cbbd9..5154937897e 100644
> --- a/elf/dl-reloc.c
> +++ b/elf/dl-reloc.c
> @@ -406,23 +406,28 @@ _dl_relocate_object (struct link_map *l, struct r_scope_elem *scope[],
>  void
>  _dl_protect_relro (struct link_map *l)
>  {
> -  if (l->l_relro_size == 0)
> -    return;
> +  for (const ElfW(Phdr) *ph = l->l_phdr; ph < &l->l_phdr[l->l_phnum]; ++ph)
> +    if (ph->p_type == PT_GNU_RELRO)
> +      {
> +	bool at_load_start = false;
> +	for (const ElfW(Phdr) *lph = l->l_phdr;
> +	     lph < &l->l_phdr[l->l_phnum]; ++lph)
> +	  if (lph->p_type == PT_LOAD && lph->p_vaddr == ph->p_vaddr)
> +	    {
> +	      at_load_start = true;
> +	      break;
> +	    }
>  
> -  ElfW(Addr) start = ALIGN_DOWN((l->l_addr
> -				 + l->l_relro_addr),
> -				GLRO(dl_pagesize));
> -  ElfW(Addr) end = ALIGN_DOWN((l->l_addr
> -			       + l->l_relro_addr
> -			       + l->l_relro_size),
> -			      GLRO(dl_pagesize));
> -  if (start != end
> -      && __mprotect ((void *) start, end - start, PROT_READ) < 0)
> -    {
> -      static const char errstring[] = N_("\
> +	struct dl_relro_range range = _dl_relro_range (l, ph, at_load_start);
> +	if (range.start < range.end
> +	    && __mprotect ((void *) range.start, range.end - range.start,
> +			   PROT_READ) < 0)
> +	  {
> +	    static const char errstring[] = N_("\
>  cannot apply additional memory protection after relocation");
> -      _dl_signal_error (errno, l->l_name, NULL, errstring);
> -    }
> +	    _dl_signal_error (errno, l->l_name, NULL, errstring);
> +	  }
> +      }
>  }
>  
>  void
> diff --git a/elf/dl-support.c b/elf/dl-support.c
> index b57fd74670b..041b89da797 100644
> --- a/elf/dl-support.c
> +++ b/elf/dl-support.c
> @@ -327,11 +327,6 @@ _dl_non_dynamic_init (void)
>        case PT_GNU_STACK:
>  	_dl_stack_prot_flags = pf_to_prot (ph->p_flags);
>  	break;
> -
> -      case PT_GNU_RELRO:
> -	_dl_main_map.l_relro_addr = ph->p_vaddr;
> -	_dl_main_map.l_relro_size = ph->p_memsz;
> -	break;
>        }
>  
>    _dl_handle_execstack_tunable ();
> diff --git a/elf/rtld.c b/elf/rtld.c
> index fc053df8586..b9d0047a686 100644
> --- a/elf/rtld.c
> +++ b/elf/rtld.c
> @@ -1198,11 +1198,6 @@ rtld_setup_main_map (struct link_map *main_map)
>        case PT_GNU_STACK:
>  	GL(dl_stack_prot_flags) = pf_to_prot (ph->p_flags);
>  	break;
> -
> -      case PT_GNU_RELRO:
> -	main_map->l_relro_addr = ph->p_vaddr;
> -	main_map->l_relro_size = ph->p_memsz;
> -	break;
>        }
>  
>    _dl_executable_postprocess (main_map, phdr, phnum);
> @@ -1270,16 +1265,6 @@ rtld_setup_phdr (void)
>  				   & ~(GLRO(dl_pagesize) - 1));
>  	}
>    }
> -
> -  /* PT_GNU_RELRO is usually the last phdr.  */
> -  size_t cnt = rtld_ehdr->e_phnum;
> -  while (cnt-- > 0)
> -    if (rtld_phdr[cnt].p_type == PT_GNU_RELRO)
> -      {
> -	_dl_rtld_map.l_relro_addr = rtld_phdr[cnt].p_vaddr;
> -	_dl_rtld_map.l_relro_size = rtld_phdr[cnt].p_memsz;
> -	break;
> -      }
>  }
>  
>  /* Adjusts the contents of the stack and related globals for the user
> diff --git a/elf/tst-relro-multi-notes.S b/elf/tst-relro-multi-notes.S
> new file mode 100644
> index 00000000000..94cd05f28c5
> --- /dev/null
> +++ b/elf/tst-relro-multi-notes.S
> @@ -0,0 +1,58 @@
> +/* Data definitions for the multiple PT_GNU_RELRO test.
> +   Copyright (C) 2026 Free Software Foundation, Inc.
> +   This file is part of the GNU C Library.
> +
> +   The GNU C Library is free software; you can redistribute it and/or
> +   modify it under the terms of the GNU Lesser General Public
> +   License as published by the Free Software Foundation; either
> +   version 2.1 of the License, or (at your option) any later version.
> +
> +   The GNU C Library is distributed in the hope that it will be useful,
> +   but WITHOUT ANY WARRANTY; without even the implied warranty of
> +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
> +   Lesser General Public License for more details.
> +
> +   You should have received a copy of the GNU Lesser General Public
> +   License along with the GNU C Library; if not, see
> +   <https://www.gnu.org/licenses/>.  */
> +
> +/* Assembly is used because compilers emit SHT_PROGBITS for
> +   __attribute__ ((section)) definitions and gas might warn when its
> +   name-based heuristic retypes .note.* sections; and  note sections
> +   must not have the SHF_WRITE flag for the same reason.  */
> +
> +	.section .note.a, "a", %note
> +	.balign 4
> +	.long 2f-1f		/* n_namesz */
> +	.long 4f-3f		/* n_descsz */
> +	.long 1			/* n_type */
> +1:	.asciz "GLIBC-TST-RELRO"
> +2:	.balign 4
> +3:	.globl relro_a
> +	.type relro_a, %object
> +relro_a:
> +	.dc.a 0xAAAA
> +	.size relro_a, .-relro_a
> +4:
> +
> +	.section .gap, "aw", %progbits
> +	.balign 8
> +	.globl gap_d
> +	.type gap_d, %object
> +gap_d:
> +	.dc.a 0xDDDD
> +	.size gap_d, .-gap_d
> +
> +	.section .note.b, "a", %note
> +	.balign 4
> +	.long 2f-1f		/* n_namesz */
> +	.long 4f-3f		/* n_descsz */
> +	.long 1			/* n_type */
> +1:	.asciz "GLIBC-TST-RELRO"
> +2:	.balign 4
> +3:	.globl relro_b
> +	.type relro_b, %object
> +relro_b:
> +	.dc.a 0xBBBB
> +	.size relro_b, .-relro_b
> +4:
> diff --git a/elf/tst-relro-multi-static.c b/elf/tst-relro-multi-static.c
> new file mode 100644
> index 00000000000..8f795c84bbc
> --- /dev/null
> +++ b/elf/tst-relro-multi-static.c
> @@ -0,0 +1,19 @@
> +/* Multiple PT_GNU_RELRO test, static version.
> +   Copyright (C) 2026 Free Software Foundation, Inc.
> +   This file is part of the GNU C Library.
> +
> +   The GNU C Library is free software; you can redistribute it and/or
> +   modify it under the terms of the GNU Lesser General Public
> +   License as published by the Free Software Foundation; either
> +   version 2.1 of the License, or (at your option) any later version.
> +
> +   The GNU C Library is distributed in the hope that it will be useful,
> +   but WITHOUT ANY WARRANTY; without even the implied warranty of
> +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
> +   Lesser General Public License for more details.
> +
> +   You should have received a copy of the GNU Lesser General Public
> +   License along with the GNU C Library; if not, see
> +   <https://www.gnu.org/licenses/>.  */
> +
> +#include "tst-relro-multi.c"
> diff --git a/elf/tst-relro-multi.c b/elf/tst-relro-multi.c
> new file mode 100644
> index 00000000000..be8feb3252a
> --- /dev/null
> +++ b/elf/tst-relro-multi.c
> @@ -0,0 +1,43 @@
> +/* Multiple PT_GNU_RELRO test.
> +   Copyright (C) 2026 Free Software Foundation, Inc.
> +   This file is part of the GNU C Library.
> +
> +   The GNU C Library is free software; you can redistribute it and/or
> +   modify it under the terms of the GNU Lesser General Public
> +   License as published by the Free Software Foundation; either
> +   version 2.1 of the License, or (at your option) any later version.
> +
> +   The GNU C Library is distributed in the hope that it will be useful,
> +   but WITHOUT ANY WARRANTY; without even the implied warranty of
> +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
> +   Lesser General Public License for more details.
> +
> +   You should have received a copy of the GNU Lesser General Public
> +   License along with the GNU C Library; if not, see
> +   <https://www.gnu.org/licenses/>.  */
> +
> +#include <support/check.h>
> +#include <support/check_mem_access.h>
> +
> +/* The tst-elf-edit.py --note-to-relro convert relro_a and relro_b
> +   to page-padded PT_GNU_RELRO seguments.  */
> +extern unsigned long int relro_a;
> +extern unsigned long int gap_d;
> +extern unsigned long int relro_b;
> +
> +static int
> +do_test (void)
> +{
> +  /* Both RELRO regions must be readable but not writable.  */
> +  TEST_COMPARE (check_mem_access (&relro_a, false), true);
> +  TEST_COMPARE (check_mem_access (&relro_a, true), false);
> +  TEST_COMPARE (check_mem_access (&relro_b, false), true);
> +  TEST_COMPARE (check_mem_access (&relro_b, true), false);
> +
> +  /* The gap between the two RELRO regions must remain writable.  */
> +  TEST_COMPARE (check_mem_access (&gap_d, true), true);
> +
> +  return 0;
> +}
> +
> +#include <support/test-driver.c>
> diff --git a/elf/tst-relro-multi.lds b/elf/tst-relro-multi.lds
> new file mode 100644
> index 00000000000..eeb6af15d3c
> --- /dev/null
> +++ b/elf/tst-relro-multi.lds
> @@ -0,0 +1,24 @@
> +/* Multiple PT_GNU_RELRO test.
> +   Copyright (C) 2026 Free Software Foundation, Inc.
> +   This file is part of the GNU C Library.
> +
> +   The GNU C Library is free software; you can redistribute it and/or
> +   modify it under the terms of the GNU Lesser General Public
> +   License as published by the Free Software Foundation; either
> +   version 2.1 of the License, or (at your option) any later version.
> +
> +   The GNU C Library is distributed in the hope that it will be useful,
> +   but WITHOUT ANY WARRANTY; without even the implied warranty of
> +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
> +   Lesser General Public License for more details.
> +
> +   You should have received a copy of the GNU Lesser General Public
> +   License along with the GNU C Library; if not, see
> +   <https://www.gnu.org/licenses/>.  */
> +SECTIONS {
> +  . = ALIGN(CONSTANT(MAXPAGESIZE));
> +  .note.a : { *(.note.a) . = ALIGN(CONSTANT(MAXPAGESIZE)); }
> +  .gap    : { *(.gap) .    = ALIGN(CONSTANT(MAXPAGESIZE)); }
> +  .note.b : { *(.note.b) . = ALIGN(CONSTANT(MAXPAGESIZE)); }
> +}
> +INSERT AFTER .data;
> diff --git a/elf/tst-relro-symbols.py b/elf/tst-relro-symbols.py
> index ffbe9958fed..28d2bbf80e6 100644
> --- a/elf/tst-relro-symbols.py
> +++ b/elf/tst-relro-symbols.py
> @@ -32,25 +32,30 @@ sys.path.append(os.path.join(
>  
>  import glibcelf
>  
> -def find_relro(path: str, img: glibcelf.Image) -> (int, int):
> -    """Discover the address range of the PT_GNU_RELRO segment."""
> -    for phdr in img.phdrs():
> -        if phdr.p_type == glibcelf.Pt.PT_GNU_RELRO:
> -            # The computation is not entirely accurate because
> -            # _dl_protect_relro in elf/dl-reloc.c rounds both the
> -            # start end and downwards using the run-time page size.
> -            return phdr.p_vaddr, phdr.p_vaddr + phdr.p_memsz
> -    sys.stdout.write('{}: error: no PT_GNU_RELRO segment\n'.format(path))
> -    sys.exit(1)
> +def find_relro(path: str, img: glibcelf.Image) -> list:
> +    """Discover the address ranges of the PT_GNU_RELRO segments."""
> +    # The computation is not entirely accurate because
> +    # _dl_protect_relro in elf/dl-reloc.c rounds both the
> +    # start end and downwards using the run-time page size.
> +    regions = [(phdr.p_vaddr, phdr.p_vaddr + phdr.p_memsz)
> +               for phdr in img.phdrs()
> +               if phdr.p_type == glibcelf.Pt.PT_GNU_RELRO]
> +    if not regions:
> +        sys.stdout.write('{}: error: no PT_GNU_RELRO segment\n'.format(path))
> +        sys.exit(1)
> +    return regions
>  
> -def check_in_relro(kind, relro_begin, relro_end, name, start, size, error):
> -    """Check if a section or symbol falls within in the RELRO segment."""
> +def check_in_relro(kind, relro_regions, name, start, size, error):
> +    """Check if a section or symbol falls within in any RELRO segment."""
>      end = start + size - 1
> -    if not (relro_begin <= start < end < relro_end):
> -        error(
> -            '{} {!r} of size {} at 0x{:x} is not in RELRO range [0x{:x}, 0x{:x})'.format(
> -                kind, name.decode('UTF-8'), start, size,
> -                relro_begin, relro_end))
> +    for relro_begin, relro_end in relro_regions:
> +        if relro_begin <= start <= end < relro_end:
> +            return
> +    error(
> +        '{} {!r} of size {} at 0x{:x} is not in any RELRO range: {}'.format(
> +            kind, name.decode('UTF-8'), size, start,
> +            ', '.join('[0x{:x}, 0x{:x})'.format(*region)
> +                      for region in relro_regions)))
>  
>  def get_parser():
>      """Return an argument parser for this script."""
> @@ -78,7 +83,7 @@ def main(argv):
>      symbols_found = set()
>  
>      # Discover the extent of the RELRO segment.
> -    relro_begin, relro_end = find_relro(opts.object, img)
> +    relro_regions = find_relro(opts.object, img)
>      symbol_table_found = False
>  
>      errors = False
> @@ -109,13 +114,13 @@ def main(argv):
>                              sym.st_name.decode('UTF-8')))
>                          continue
>  
> -                    check_in_relro('symbol', relro_begin, relro_end,
> +                    check_in_relro('symbol', relro_regions,
>                                     sym.st_name, sym.st_value, sym.st_size,
>                                     error)
>              continue # SHT_SYMTAB
>          if shdr.sh_name == b'.data.rel.ro' \
>             or shdr.sh_name.startswith(b'.data.rel.ro.'):
> -            check_in_relro('section', relro_begin, relro_end,
> +            check_in_relro('section', relro_regions,
>                             shdr.sh_name, shdr.sh_addr, shdr.sh_size,
>                             error)
>              continue
> diff --git a/include/link.h b/include/link.h
> index 8f851d2212d..04274b490ea 100644
> --- a/include/link.h
> +++ b/include/link.h
> @@ -340,10 +340,6 @@ struct link_map
>         lock.  See also: CONCURRENCY NOTES in cxa_thread_atexit_impl.c.  */
>      size_t l_tls_dtor_count;
>  
> -    /* Information used to change permission after the relocations are
> -       done.  */
> -    ElfW(Addr) l_relro_addr;
> -    size_t l_relro_size;
>  
>      unsigned long long int l_serial;
>    };
> diff --git a/scripts/tst-elf-edit.py b/scripts/tst-elf-edit.py
> index 07fa7e90f55..1d036e925cc 100644
> --- a/scripts/tst-elf-edit.py
> +++ b/scripts/tst-elf-edit.py
> @@ -47,7 +47,12 @@ ET_EXEC=2
>  ET_DYN=3
>  
>  PT_LOAD=1
> +PT_NOTE=4
>  PT_TLS=7
> +PT_GNU_RELRO=0x6474e552
> +
> +# PT_NOTE constant mark used by --note-to-relro.
> +NN_GLIBC_TST_RELRO=b'GLIBC-TST-RELRO\x00'
>  
>  def elf_types_fmts(e_ident):
>      endian = '<' if e_ident[EI_DATA] == ELFDATA2LSB else '>'
> @@ -156,6 +161,40 @@ def elf_edit_maximize_tls_size(phdr, elfclass):
>      else:
>          phdr.p_memsz = 1 << 63
>  
> +def elf_note_name(f, e_ident, phdr):
> +    """Return the owner name of the first note in a PT_NOTE segment."""
> +    endian, _, _ = elf_types_fmts(e_ident)
> +    fmt = '{}III'.format(endian)
> +    nhdr_len = struct.calcsize(fmt)
> +    if phdr.p_filesz < nhdr_len:
> +        return None
> +    f.seek(phdr.p_offset)
> +    namesz, descsz, n_type = struct.unpack(fmt, f.read(nhdr_len))
> +    if namesz == 0 or namesz > phdr.p_filesz - nhdr_len:
> +        return None
> +    return f.read(namesz)
> +
> +def elf_edit_note_to_relro(f, e_ident, ehdr, expected):
> +    converted = 0
> +    for i in range(0, ehdr.e_phnum):
> +        phdr = Elf_Phdr(e_ident)
> +        f.seek(ehdr.e_phoff + i * phdr.len)
> +        phdr.read(f)
> +        if phdr.p_type != PT_NOTE:
> +            continue
> +        if elf_note_name(f, e_ident, phdr) != NN_GLIBC_TST_RELRO:
> +            continue
> +        phdr.p_type = PT_GNU_RELRO
> +        # Match the alignment the linker uses for PT_GNU_RELRO.
> +        phdr.p_align = 1
> +        f.seek(ehdr.e_phoff + i * phdr.len)
> +        phdr.write(f)
> +        converted += 1
> +
> +    if converted != expected:
> +        error('{}: converted {} PT_NOTE segment(s), expected {}'.format(
> +            f.name, converted, expected))
> +
>  def elf_edit(f, opts):
>      ei_nident_fmt = 'c' * EI_NIDENT
>      ei_nident_len = struct.calcsize(ei_nident_fmt)
> @@ -184,6 +223,10 @@ def elf_edit(f, opts):
>      if ehdr.e_type not in (ET_EXEC, ET_DYN):
>         error('{}: not an executable or shared library'.format(f.name))
>  
> +    if opts.note_to_relro is not None:
> +        elf_edit_note_to_relro(f, e_ident, ehdr, opts.note_to_relro)
> +        return
> +
>      phdr = Elf_Phdr(e_ident)
>      maximize_tls_size_done = False
>      for i in range(0, ehdr.e_phnum):
> @@ -210,6 +253,9 @@ def get_parser():
>                          help='How to set the LOAD alignment')
>      parser.add_argument('--maximize-tls-size', action='store_true',
>                          help='Set maximum PT_TLS size')
> +    parser.add_argument('--note-to-relro', type=int, metavar='COUNT',
> +                        help='Convert COUNT PT_NOTE segments whose first '
> +                        'note has the GLIBC-TST-RELRO name to PT_GNU_RELRO')
>      parser.add_argument('output',
>                          help='ELF file to edit')
>      return parser
> diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
> index 305ca6e0df2..9f17e9d34c5 100644
> --- a/sysdeps/generic/ldsodefs.h
> +++ b/sysdeps/generic/ldsodefs.h
> @@ -32,6 +32,7 @@
>  #include <dlfcn.h>
>  #include <fpu_control.h>
>  #include <sys/mman.h>
> +#include <libc-pointer-arith.h>
>  #include <link.h>
>  #include <dl-lookupcfg.h>
>  #include <dl-sysdep.h>
> @@ -1037,6 +1038,41 @@ void _dl_relocate_object_no_relro (struct link_map *map,
>  /* Protect PT_GNU_RELRO area.  */
>  extern void _dl_protect_relro (struct link_map *map) attribute_hidden;
>  
> +struct dl_relro_range
> +{
> +  ElfW(Addr) start;
> +  ElfW(Addr) end;
> +};
> +
> +/* Compute the address range to protect for the PT_GNU_RELRO segment
> +   PH of map L.  AT_LOAD_START tells whether the segment starts a
> +   PT_LOAD segment.  mprotect requires page-aligned boundaries, but
> +   the static linker only aligns the segment end, possibly only to a
> +   link-time page size smaller than the run-time one.  The end is
> +   rounded down so the writable data after the segment never loses
> +   write access.  A segment that starts a PT_LOAD segment (the
> +   traditional single-RELRO layout) has only unused slack below it in
> +   its first page, so its start is rounded down; rounding up instead
> +   could drop the protection entirely (BFD ld does not align the
> +   start at all).  Any other segment is preceded by live writable
> +   data, so its start is rounded up.  The result may be empty with
> +   START above END; callers must check START < END.  */
> +static inline struct dl_relro_range
> +_dl_relro_range (const struct link_map *l, const ElfW(Phdr) *ph,
> +		 bool at_load_start)
> +{
> +  ElfW(Addr) start = l->l_addr + ph->p_vaddr;
> +  ElfW(Addr) end = start + ph->p_memsz;
> +
> +  return (struct dl_relro_range)
> +    {
> +      .start = at_load_start
> +	       ? ALIGN_DOWN (start, GLRO(dl_pagesize))
> +	       : ALIGN_UP (start, GLRO(dl_pagesize)),
> +      .end = ALIGN_DOWN (end, GLRO(dl_pagesize)),
> +    };
> +}
> +
>  /* Call _dl_signal_error with a message about an unhandled reloc type.
>     TYPE is the result of ELFW(R_TYPE) (r_info), i.e. an R_<CPU>_* value.
>     PLT is nonzero if this was a PLT reloc; it just affects the message.  */
  
Florian Weimer Sept. 9, 2026, 12:26 p.m. UTC | #2
* Adhemerval Zanella:

> When binaries become extremely large, PC-relative references to a
> single GOT can exceed the +/- 2GB limit.

The limit is architecture-specific.  I assume this is talking about
x86-64.  Maybe add this to the commit message.

> To resolve this, we'd like to generate multiple GOTs, which would
> require multiple PT_GNU_RELRO segments.

> This change modifies RELRO protection by removing cached fields
> (l_relro_addr and l_relro_size) and instead iterating over all program
> headers to protect every PT_GNU_RELRO segment discovered. The same
> applies to _dl_readonly_area, where a queried range may now span
> multiple adjacent segments.
>
> Since mprotect operates on whole pages while the static linker only
> aligns the segment end (and possibly only to a link-time page size
> smaller than the run-time one), the protected range is computed by the
> new _dl_relro_range helper. The end is rounded down so following
> writable data never loses write access, and the start is rounded down
> only for a segment that starts a PT_LOAD segment, where the leading
> page slack is unused (BFD ld does not page-align the start, so
> rounding it up could drop the protection entirely).  A RELRO segment
> in the middle of a PT_LOAD segment is preceded by live writable data,
> so its start is rounded up instead.

Should the final case (rounding up) be an error instead?  It could
result in unexpected loss of RELRO protection.

> diff --git a/elf/dl-readonly-area.c b/elf/dl-readonly-area.c
> index 833f4559049..0b7a5c0d319 100644
> --- a/elf/dl-readonly-area.c
> +++ b/elf/dl-readonly-area.c

> +static enum dl_readonly_area_error_type
>  check_relro (const struct link_map *l, uintptr_t start, uintptr_t end)
>  {
> +  /* The range may span multiple PT_GNU_RELRO segments whose ranges are
> +     adjacent, so accumulate the covered bytes.  */
> +  size_t size = end - start;
> +  for (const ElfW(Phdr) *ph = l->l_phdr; ph < &l->l_phdr[l->l_phnum]; ++ph)
> +    if (ph->p_type == PT_GNU_RELRO)
> +      {
> +	bool at_load_start = false;
> +	for (const ElfW(Phdr) *lph = l->l_phdr;
> +	     lph < &l->l_phdr[l->l_phnum]; ++lph)
> +	  if (lph->p_type == PT_LOAD && lph->p_vaddr == ph->p_vaddr)
> +	    {
> +	      at_load_start = true;
> +	      break;
> +	    }

That's quadratic behavior, which isn't great for huge binaries.

Do we need to do any rounding here?  I think we want to make sure that
the address comes from .data.rel.ro or equivalent.  I would expect it up
to the toolchain to make sure that PT_GNU_RELRO covers the
language-defined objects.  I think we can restrict the rounding
operations the actual mapping steps only.

> diff --git a/elf/dl-reloc.c b/elf/dl-reloc.c
> index 191d39cbbd9..5154937897e 100644
> --- a/elf/dl-reloc.c
> +++ b/elf/dl-reloc.c
> @@ -406,23 +406,28 @@ _dl_relocate_object (struct link_map *l, struct r_scope_elem *scope[],
>  void
>  _dl_protect_relro (struct link_map *l)
>  {
> +  for (const ElfW(Phdr) *ph = l->l_phdr; ph < &l->l_phdr[l->l_phnum]; ++ph)
> +    if (ph->p_type == PT_GNU_RELRO)
> +      {
> +	bool at_load_start = false;
> +	for (const ElfW(Phdr) *lph = l->l_phdr;
> +	     lph < &l->l_phdr[l->l_phnum]; ++lph)
> +	  if (lph->p_type == PT_LOAD && lph->p_vaddr == ph->p_vaddr)
> +	    {
> +	      at_load_start = true;
> +	      break;
> +	    }

This happens just once, so the quadratic behavior is more acceptable.

> diff --git a/elf/tst-relro-symbols.py b/elf/tst-relro-symbols.py
> index ffbe9958fed..28d2bbf80e6 100644
> --- a/elf/tst-relro-symbols.py
> +++ b/elf/tst-relro-symbols.py
> @@ -32,25 +32,30 @@ sys.path.append(os.path.join(

> +def find_relro(path: str, img: glibcelf.Image) -> list:
> +    """Discover the address ranges of the PT_GNU_RELRO segments."""
> +    # The computation is not entirely accurate because
> +    # _dl_protect_relro in elf/dl-reloc.c rounds both the
> +    # start end and downwards using the run-time page size.

If _dl_protect_relro is simplified as suggested, then the comment needs
to be updated.

Rest looks okay.

Thanks,
Florian
  
Adhemerval Zanella Netto Sept. 9, 2026, 4:21 p.m. UTC | #3
On 09/09/26 09:26, Florian Weimer wrote:
> * Adhemerval Zanella:
> 
>> When binaries become extremely large, PC-relative references to a
>> single GOT can exceed the +/- 2GB limit.
> 
> The limit is architecture-specific.  I assume this is talking about
> x86-64.  Maybe add this to the commit message.

Ack, I will update the commit message.

> 
>> To resolve this, we'd like to generate multiple GOTs, which would
>> require multiple PT_GNU_RELRO segments.
> 
>> This change modifies RELRO protection by removing cached fields
>> (l_relro_addr and l_relro_size) and instead iterating over all program
>> headers to protect every PT_GNU_RELRO segment discovered. The same
>> applies to _dl_readonly_area, where a queried range may now span
>> multiple adjacent segments.
>>
>> Since mprotect operates on whole pages while the static linker only
>> aligns the segment end (and possibly only to a link-time page size
>> smaller than the run-time one), the protected range is computed by the
>> new _dl_relro_range helper. The end is rounded down so following
>> writable data never loses write access, and the start is rounded down
>> only for a segment that starts a PT_LOAD segment, where the leading
>> page slack is unused (BFD ld does not page-align the start, so
>> rounding it up could drop the protection entirely).  A RELRO segment
>> in the middle of a PT_LOAD segment is preceded by live writable data,
>> so its start is rounded up instead.
> 
> Should the final case (rounding up) be an error instead?  It could
> result in unexpected loss of RELRO protection.

I think we shouldn't for now. The clang/ldd (fa5d8f8c41df) creates one
PT_GNU_RELRO per contiguous run of relro sections, with no per-run alignment 
or padding (.relro_padding is only synthesized for the default layout or 
with DATA_SEGMENT_RELRO_END). So we might already have binaries with
multiple PT_GNU_RELRO that *works* now, but that will start to abort
with this change:

$ cat << EOF > relro-data.c
long relro_a __attribute__ ((section (".relro_a"))) = 0xAAAA;
long gap_d __attribute__ ((section (".mid"))) = 0xDDDD;
long relro_b __attribute__ ((section (".relro_b"))) = 0xBBBB;
EOF
$ cat << EOF > multi-relro.lds
SECTIONS {
  . = ALIGN(CONSTANT(MAXPAGESIZE));
  .data.rel.ro : { *(.relro_a) . = ALIGN(CONSTANT(MAXPAGESIZE)); }
  .mid         : { *(.mid) }
  .bss.rel.ro  : { *(.relro_b) }
}
INSERT AFTER .data;
EOF
$ cat << EOF > main.c
int main () { }
EOF
$ clang -fuse-ld=lld -pie -fPIE -Wl,-z,relro -Wl,-T,multi-relro.lds main.c relro-data.c -o test
$ readelf -Wl test  | grep GNU_RELRO
  GNU_RELRO      0x0007e0 0x00000000000027e0 0x00000000000027e0 0x0001d8 0x000820 R   0x1
  GNU_RELRO      0x0009c8 0x00000000000039c8 0x00000000000039c8 0x000638 0x000638 R   0x1
  GNU_RELRO      0x001008 0x0000000000004008 0x0000000000004008 0x000008 0x000008 R   0x1
$ ./test
$

And we already silently loses protection at the tail via the end ALIGN_DOWN
anyway.

I think it is worth a LD_DEBUG warning for such cases.

> 
>> diff --git a/elf/dl-readonly-area.c b/elf/dl-readonly-area.c
>> index 833f4559049..0b7a5c0d319 100644
>> --- a/elf/dl-readonly-area.c
>> +++ b/elf/dl-readonly-area.c
> 
>> +static enum dl_readonly_area_error_type
>>  check_relro (const struct link_map *l, uintptr_t start, uintptr_t end)
>>  {
>> +  /* The range may span multiple PT_GNU_RELRO segments whose ranges are
>> +     adjacent, so accumulate the covered bytes.  */
>> +  size_t size = end - start;
>> +  for (const ElfW(Phdr) *ph = l->l_phdr; ph < &l->l_phdr[l->l_phnum]; ++ph)
>> +    if (ph->p_type == PT_GNU_RELRO)
>> +      {
>> +	bool at_load_start = false;
>> +	for (const ElfW(Phdr) *lph = l->l_phdr;
>> +	     lph < &l->l_phdr[l->l_phnum]; ++lph)
>> +	  if (lph->p_type == PT_LOAD && lph->p_vaddr == ph->p_vaddr)
>> +	    {
>> +	      at_load_start = true;
>> +	      break;
>> +	    }
> 
> That's quadratic behavior, which isn't great for huge binaries.
> 
> Do we need to do any rounding here?  I think we want to make sure that
> the address comes from .data.rel.ro or equivalent.  I would expect it up
> to the toolchain to make sure that PT_GNU_RELRO covers the
> language-defined objects.  I think we can restrict the rounding
> operations the actual mapping steps only.

It sound's reasonable and it removes the quadratic behavior, I will change it.

> 
>> diff --git a/elf/dl-reloc.c b/elf/dl-reloc.c
>> index 191d39cbbd9..5154937897e 100644
>> --- a/elf/dl-reloc.c
>> +++ b/elf/dl-reloc.c
>> @@ -406,23 +406,28 @@ _dl_relocate_object (struct link_map *l, struct r_scope_elem *scope[],
>>  void
>>  _dl_protect_relro (struct link_map *l)
>>  {
>> +  for (const ElfW(Phdr) *ph = l->l_phdr; ph < &l->l_phdr[l->l_phnum]; ++ph)
>> +    if (ph->p_type == PT_GNU_RELRO)
>> +      {
>> +	bool at_load_start = false;
>> +	for (const ElfW(Phdr) *lph = l->l_phdr;
>> +	     lph < &l->l_phdr[l->l_phnum]; ++lph)
>> +	  if (lph->p_type == PT_LOAD && lph->p_vaddr == ph->p_vaddr)
>> +	    {
>> +	      at_load_start = true;
>> +	      break;
>> +	    }
> 
> This happens just once, so the quadratic behavior is more acceptable.

Ack, I think it has some room for improvements but it will most likely
require it to split the at_load_start setup and move it to a global
variable. 

> 
>> diff --git a/elf/tst-relro-symbols.py b/elf/tst-relro-symbols.py
>> index ffbe9958fed..28d2bbf80e6 100644
>> --- a/elf/tst-relro-symbols.py
>> +++ b/elf/tst-relro-symbols.py
>> @@ -32,25 +32,30 @@ sys.path.append(os.path.join(
> 
>> +def find_relro(path: str, img: glibcelf.Image) -> list:
>> +    """Discover the address ranges of the PT_GNU_RELRO segments."""
>> +    # The computation is not entirely accurate because
>> +    # _dl_protect_relro in elf/dl-reloc.c rounds both the
>> +    # start end and downwards using the run-time page size.
> 
> If _dl_protect_relro is simplified as suggested, then the comment needs
> to be updated.

Ack.

> 
> Rest looks okay.
> 
> Thanks,
> Florian
  

Patch

diff --git a/elf/Makefile b/elf/Makefile
index 8b063e1bbae..a6c4afc6265 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -1530,6 +1530,38 @@  tests-special += $(objpfx)tst-tls-debug-recursive.out
 tests-special += $(objpfx)tst-dl-debug-exclude.out
 endif
 
+test-srcs += tst-relro-multi
+tests-static += tst-relro-multi-static
+extra-test-objs += tst-relro-multi-notes.o
+LDFLAGS-tst-relro-multi = -Wl,-z,now -Wl,-T,$(..)elf/tst-relro-multi.lds
+LDFLAGS-tst-relro-multi-static = -Wl,-T,$(..)elf/tst-relro-multi.lds
+$(objpfx)tst-relro-multi: +nolink-deps += tst-relro-multi.lds
+$(objpfx)tst-relro-multi: $(objpfx)tst-relro-multi-notes.o tst-relro-multi.lds
+$(objpfx)tst-relro-multi-static: +nolink-deps += tst-relro-multi.lds
+$(objpfx)tst-relro-multi-static: $(objpfx)tst-relro-multi-notes.o \
+  tst-relro-multi.lds
+
+$(objpfx)tst-relro-multi-patched $(objpfx)tst-relro-multi-static-patched: \
+  %-patched: % $(..)scripts/tst-elf-edit.py
+	cp $< $@.tmp
+	$(PYTHON) $(..)scripts/tst-elf-edit.py --note-to-relro 2 $@.tmp
+	mv $@.tmp $@
+
+$(objpfx)tst-relro-multi-patched.out: $(objpfx)tst-relro-multi-patched \
+  $(objpfx)ld.so
+	$(run-program-prefix) $< > $@ 2>&1; \
+	$(evaluate-test)
+
+$(objpfx)tst-relro-multi-static-patched.out: \
+  $(objpfx)tst-relro-multi-static-patched
+	$(test-wrapper) $< > $@ 2>&1; \
+	$(evaluate-test)
+
+ifeq ($(run-built-tests),yes)
+tests-special += $(objpfx)tst-relro-multi-patched.out
+tests-special += $(objpfx)tst-relro-multi-static-patched.out
+endif
+
 # The test requires shared _and_ PIE because the executable
 # unit test driver must be able to link with the shared object
 # that is going to eventually go into an installed DSO.
diff --git a/elf/dl-load.c b/elf/dl-load.c
index 95404adae94..e76d149f1f6 100644
--- a/elf/dl-load.c
+++ b/elf/dl-load.c
@@ -1091,11 +1091,6 @@  _dl_map_object_scan_phdrs (struct dl_pt_load_iterator *it,
 	case PT_GNU_STACK:
 	  *stack_flagsp = pf_to_prot (ph->p_flags);
 	  break;
-
-	case PT_GNU_RELRO:
-	  l->l_relro_addr = ph->p_vaddr;
-	  l->l_relro_size = ph->p_memsz;
-	  break;
 	}
     }
 
diff --git a/elf/dl-readonly-area.c b/elf/dl-readonly-area.c
index 833f4559049..0b7a5c0d319 100644
--- a/elf/dl-readonly-area.c
+++ b/elf/dl-readonly-area.c
@@ -17,23 +17,37 @@ 
    <https://www.gnu.org/licenses/>.  */
 
 #include <ldsodefs.h>
+#include <sys/param.h>
 
-static bool
+static enum dl_readonly_area_error_type
 check_relro (const struct link_map *l, uintptr_t start, uintptr_t end)
 {
-  if (l->l_relro_addr != 0)
-    {
-      uintptr_t relro_start = ALIGN_DOWN (l->l_addr + l->l_relro_addr,
-					  GLRO(dl_pagesize));
-      uintptr_t relro_end = ALIGN_DOWN (l->l_addr + l->l_relro_addr
-					+ l->l_relro_size,
-					GLRO(dl_pagesize));
-      /* RELRO is caved out from a RW segment, so the next range is either
-	 RW or nonexistent.  */
-      return relro_start <= start && end <= relro_end
-	? dl_readonly_area_rdonly : dl_readonly_area_writable;
+  /* The range may span multiple PT_GNU_RELRO segments whose ranges are
+     adjacent, so accumulate the covered bytes.  */
+  size_t size = end - start;
+  for (const ElfW(Phdr) *ph = l->l_phdr; ph < &l->l_phdr[l->l_phnum]; ++ph)
+    if (ph->p_type == PT_GNU_RELRO)
+      {
+	bool at_load_start = false;
+	for (const ElfW(Phdr) *lph = l->l_phdr;
+	     lph < &l->l_phdr[l->l_phnum]; ++lph)
+	  if (lph->p_type == PT_LOAD && lph->p_vaddr == ph->p_vaddr)
+	    {
+	      at_load_start = true;
+	      break;
+	    }
 
-    }
+	struct dl_relro_range relro = _dl_relro_range (l, ph, at_load_start);
+	uintptr_t from = MAX (relro.start, start);
+	uintptr_t to = MIN (relro.end, end);
+	if (from < to)
+	  size -= to - from;
+	if (size == 0)
+	  return dl_readonly_area_rdonly;
+      }
+
+  /* RELRO is caved out from a RW segment, so any range outside of
+     a RELRO segment is either RW or nonexistent.  */
   return dl_readonly_area_writable;
 }
 
diff --git a/elf/dl-reloc.c b/elf/dl-reloc.c
index 191d39cbbd9..5154937897e 100644
--- a/elf/dl-reloc.c
+++ b/elf/dl-reloc.c
@@ -406,23 +406,28 @@  _dl_relocate_object (struct link_map *l, struct r_scope_elem *scope[],
 void
 _dl_protect_relro (struct link_map *l)
 {
-  if (l->l_relro_size == 0)
-    return;
+  for (const ElfW(Phdr) *ph = l->l_phdr; ph < &l->l_phdr[l->l_phnum]; ++ph)
+    if (ph->p_type == PT_GNU_RELRO)
+      {
+	bool at_load_start = false;
+	for (const ElfW(Phdr) *lph = l->l_phdr;
+	     lph < &l->l_phdr[l->l_phnum]; ++lph)
+	  if (lph->p_type == PT_LOAD && lph->p_vaddr == ph->p_vaddr)
+	    {
+	      at_load_start = true;
+	      break;
+	    }
 
-  ElfW(Addr) start = ALIGN_DOWN((l->l_addr
-				 + l->l_relro_addr),
-				GLRO(dl_pagesize));
-  ElfW(Addr) end = ALIGN_DOWN((l->l_addr
-			       + l->l_relro_addr
-			       + l->l_relro_size),
-			      GLRO(dl_pagesize));
-  if (start != end
-      && __mprotect ((void *) start, end - start, PROT_READ) < 0)
-    {
-      static const char errstring[] = N_("\
+	struct dl_relro_range range = _dl_relro_range (l, ph, at_load_start);
+	if (range.start < range.end
+	    && __mprotect ((void *) range.start, range.end - range.start,
+			   PROT_READ) < 0)
+	  {
+	    static const char errstring[] = N_("\
 cannot apply additional memory protection after relocation");
-      _dl_signal_error (errno, l->l_name, NULL, errstring);
-    }
+	    _dl_signal_error (errno, l->l_name, NULL, errstring);
+	  }
+      }
 }
 
 void
diff --git a/elf/dl-support.c b/elf/dl-support.c
index b57fd74670b..041b89da797 100644
--- a/elf/dl-support.c
+++ b/elf/dl-support.c
@@ -327,11 +327,6 @@  _dl_non_dynamic_init (void)
       case PT_GNU_STACK:
 	_dl_stack_prot_flags = pf_to_prot (ph->p_flags);
 	break;
-
-      case PT_GNU_RELRO:
-	_dl_main_map.l_relro_addr = ph->p_vaddr;
-	_dl_main_map.l_relro_size = ph->p_memsz;
-	break;
       }
 
   _dl_handle_execstack_tunable ();
diff --git a/elf/rtld.c b/elf/rtld.c
index fc053df8586..b9d0047a686 100644
--- a/elf/rtld.c
+++ b/elf/rtld.c
@@ -1198,11 +1198,6 @@  rtld_setup_main_map (struct link_map *main_map)
       case PT_GNU_STACK:
 	GL(dl_stack_prot_flags) = pf_to_prot (ph->p_flags);
 	break;
-
-      case PT_GNU_RELRO:
-	main_map->l_relro_addr = ph->p_vaddr;
-	main_map->l_relro_size = ph->p_memsz;
-	break;
       }
 
   _dl_executable_postprocess (main_map, phdr, phnum);
@@ -1270,16 +1265,6 @@  rtld_setup_phdr (void)
 				   & ~(GLRO(dl_pagesize) - 1));
 	}
   }
-
-  /* PT_GNU_RELRO is usually the last phdr.  */
-  size_t cnt = rtld_ehdr->e_phnum;
-  while (cnt-- > 0)
-    if (rtld_phdr[cnt].p_type == PT_GNU_RELRO)
-      {
-	_dl_rtld_map.l_relro_addr = rtld_phdr[cnt].p_vaddr;
-	_dl_rtld_map.l_relro_size = rtld_phdr[cnt].p_memsz;
-	break;
-      }
 }
 
 /* Adjusts the contents of the stack and related globals for the user
diff --git a/elf/tst-relro-multi-notes.S b/elf/tst-relro-multi-notes.S
new file mode 100644
index 00000000000..94cd05f28c5
--- /dev/null
+++ b/elf/tst-relro-multi-notes.S
@@ -0,0 +1,58 @@ 
+/* Data definitions for the multiple PT_GNU_RELRO test.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+/* Assembly is used because compilers emit SHT_PROGBITS for
+   __attribute__ ((section)) definitions and gas might warn when its
+   name-based heuristic retypes .note.* sections; and  note sections
+   must not have the SHF_WRITE flag for the same reason.  */
+
+	.section .note.a, "a", %note
+	.balign 4
+	.long 2f-1f		/* n_namesz */
+	.long 4f-3f		/* n_descsz */
+	.long 1			/* n_type */
+1:	.asciz "GLIBC-TST-RELRO"
+2:	.balign 4
+3:	.globl relro_a
+	.type relro_a, %object
+relro_a:
+	.dc.a 0xAAAA
+	.size relro_a, .-relro_a
+4:
+
+	.section .gap, "aw", %progbits
+	.balign 8
+	.globl gap_d
+	.type gap_d, %object
+gap_d:
+	.dc.a 0xDDDD
+	.size gap_d, .-gap_d
+
+	.section .note.b, "a", %note
+	.balign 4
+	.long 2f-1f		/* n_namesz */
+	.long 4f-3f		/* n_descsz */
+	.long 1			/* n_type */
+1:	.asciz "GLIBC-TST-RELRO"
+2:	.balign 4
+3:	.globl relro_b
+	.type relro_b, %object
+relro_b:
+	.dc.a 0xBBBB
+	.size relro_b, .-relro_b
+4:
diff --git a/elf/tst-relro-multi-static.c b/elf/tst-relro-multi-static.c
new file mode 100644
index 00000000000..8f795c84bbc
--- /dev/null
+++ b/elf/tst-relro-multi-static.c
@@ -0,0 +1,19 @@ 
+/* Multiple PT_GNU_RELRO test, static version.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include "tst-relro-multi.c"
diff --git a/elf/tst-relro-multi.c b/elf/tst-relro-multi.c
new file mode 100644
index 00000000000..be8feb3252a
--- /dev/null
+++ b/elf/tst-relro-multi.c
@@ -0,0 +1,43 @@ 
+/* Multiple PT_GNU_RELRO test.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include <support/check.h>
+#include <support/check_mem_access.h>
+
+/* The tst-elf-edit.py --note-to-relro convert relro_a and relro_b
+   to page-padded PT_GNU_RELRO seguments.  */
+extern unsigned long int relro_a;
+extern unsigned long int gap_d;
+extern unsigned long int relro_b;
+
+static int
+do_test (void)
+{
+  /* Both RELRO regions must be readable but not writable.  */
+  TEST_COMPARE (check_mem_access (&relro_a, false), true);
+  TEST_COMPARE (check_mem_access (&relro_a, true), false);
+  TEST_COMPARE (check_mem_access (&relro_b, false), true);
+  TEST_COMPARE (check_mem_access (&relro_b, true), false);
+
+  /* The gap between the two RELRO regions must remain writable.  */
+  TEST_COMPARE (check_mem_access (&gap_d, true), true);
+
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/elf/tst-relro-multi.lds b/elf/tst-relro-multi.lds
new file mode 100644
index 00000000000..eeb6af15d3c
--- /dev/null
+++ b/elf/tst-relro-multi.lds
@@ -0,0 +1,24 @@ 
+/* Multiple PT_GNU_RELRO test.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+SECTIONS {
+  . = ALIGN(CONSTANT(MAXPAGESIZE));
+  .note.a : { *(.note.a) . = ALIGN(CONSTANT(MAXPAGESIZE)); }
+  .gap    : { *(.gap) .    = ALIGN(CONSTANT(MAXPAGESIZE)); }
+  .note.b : { *(.note.b) . = ALIGN(CONSTANT(MAXPAGESIZE)); }
+}
+INSERT AFTER .data;
diff --git a/elf/tst-relro-symbols.py b/elf/tst-relro-symbols.py
index ffbe9958fed..28d2bbf80e6 100644
--- a/elf/tst-relro-symbols.py
+++ b/elf/tst-relro-symbols.py
@@ -32,25 +32,30 @@  sys.path.append(os.path.join(
 
 import glibcelf
 
-def find_relro(path: str, img: glibcelf.Image) -> (int, int):
-    """Discover the address range of the PT_GNU_RELRO segment."""
-    for phdr in img.phdrs():
-        if phdr.p_type == glibcelf.Pt.PT_GNU_RELRO:
-            # The computation is not entirely accurate because
-            # _dl_protect_relro in elf/dl-reloc.c rounds both the
-            # start end and downwards using the run-time page size.
-            return phdr.p_vaddr, phdr.p_vaddr + phdr.p_memsz
-    sys.stdout.write('{}: error: no PT_GNU_RELRO segment\n'.format(path))
-    sys.exit(1)
+def find_relro(path: str, img: glibcelf.Image) -> list:
+    """Discover the address ranges of the PT_GNU_RELRO segments."""
+    # The computation is not entirely accurate because
+    # _dl_protect_relro in elf/dl-reloc.c rounds both the
+    # start end and downwards using the run-time page size.
+    regions = [(phdr.p_vaddr, phdr.p_vaddr + phdr.p_memsz)
+               for phdr in img.phdrs()
+               if phdr.p_type == glibcelf.Pt.PT_GNU_RELRO]
+    if not regions:
+        sys.stdout.write('{}: error: no PT_GNU_RELRO segment\n'.format(path))
+        sys.exit(1)
+    return regions
 
-def check_in_relro(kind, relro_begin, relro_end, name, start, size, error):
-    """Check if a section or symbol falls within in the RELRO segment."""
+def check_in_relro(kind, relro_regions, name, start, size, error):
+    """Check if a section or symbol falls within in any RELRO segment."""
     end = start + size - 1
-    if not (relro_begin <= start < end < relro_end):
-        error(
-            '{} {!r} of size {} at 0x{:x} is not in RELRO range [0x{:x}, 0x{:x})'.format(
-                kind, name.decode('UTF-8'), start, size,
-                relro_begin, relro_end))
+    for relro_begin, relro_end in relro_regions:
+        if relro_begin <= start <= end < relro_end:
+            return
+    error(
+        '{} {!r} of size {} at 0x{:x} is not in any RELRO range: {}'.format(
+            kind, name.decode('UTF-8'), size, start,
+            ', '.join('[0x{:x}, 0x{:x})'.format(*region)
+                      for region in relro_regions)))
 
 def get_parser():
     """Return an argument parser for this script."""
@@ -78,7 +83,7 @@  def main(argv):
     symbols_found = set()
 
     # Discover the extent of the RELRO segment.
-    relro_begin, relro_end = find_relro(opts.object, img)
+    relro_regions = find_relro(opts.object, img)
     symbol_table_found = False
 
     errors = False
@@ -109,13 +114,13 @@  def main(argv):
                             sym.st_name.decode('UTF-8')))
                         continue
 
-                    check_in_relro('symbol', relro_begin, relro_end,
+                    check_in_relro('symbol', relro_regions,
                                    sym.st_name, sym.st_value, sym.st_size,
                                    error)
             continue # SHT_SYMTAB
         if shdr.sh_name == b'.data.rel.ro' \
            or shdr.sh_name.startswith(b'.data.rel.ro.'):
-            check_in_relro('section', relro_begin, relro_end,
+            check_in_relro('section', relro_regions,
                            shdr.sh_name, shdr.sh_addr, shdr.sh_size,
                            error)
             continue
diff --git a/include/link.h b/include/link.h
index 8f851d2212d..04274b490ea 100644
--- a/include/link.h
+++ b/include/link.h
@@ -340,10 +340,6 @@  struct link_map
        lock.  See also: CONCURRENCY NOTES in cxa_thread_atexit_impl.c.  */
     size_t l_tls_dtor_count;
 
-    /* Information used to change permission after the relocations are
-       done.  */
-    ElfW(Addr) l_relro_addr;
-    size_t l_relro_size;
 
     unsigned long long int l_serial;
   };
diff --git a/scripts/tst-elf-edit.py b/scripts/tst-elf-edit.py
index 07fa7e90f55..1d036e925cc 100644
--- a/scripts/tst-elf-edit.py
+++ b/scripts/tst-elf-edit.py
@@ -47,7 +47,12 @@  ET_EXEC=2
 ET_DYN=3
 
 PT_LOAD=1
+PT_NOTE=4
 PT_TLS=7
+PT_GNU_RELRO=0x6474e552
+
+# PT_NOTE constant mark used by --note-to-relro.
+NN_GLIBC_TST_RELRO=b'GLIBC-TST-RELRO\x00'
 
 def elf_types_fmts(e_ident):
     endian = '<' if e_ident[EI_DATA] == ELFDATA2LSB else '>'
@@ -156,6 +161,40 @@  def elf_edit_maximize_tls_size(phdr, elfclass):
     else:
         phdr.p_memsz = 1 << 63
 
+def elf_note_name(f, e_ident, phdr):
+    """Return the owner name of the first note in a PT_NOTE segment."""
+    endian, _, _ = elf_types_fmts(e_ident)
+    fmt = '{}III'.format(endian)
+    nhdr_len = struct.calcsize(fmt)
+    if phdr.p_filesz < nhdr_len:
+        return None
+    f.seek(phdr.p_offset)
+    namesz, descsz, n_type = struct.unpack(fmt, f.read(nhdr_len))
+    if namesz == 0 or namesz > phdr.p_filesz - nhdr_len:
+        return None
+    return f.read(namesz)
+
+def elf_edit_note_to_relro(f, e_ident, ehdr, expected):
+    converted = 0
+    for i in range(0, ehdr.e_phnum):
+        phdr = Elf_Phdr(e_ident)
+        f.seek(ehdr.e_phoff + i * phdr.len)
+        phdr.read(f)
+        if phdr.p_type != PT_NOTE:
+            continue
+        if elf_note_name(f, e_ident, phdr) != NN_GLIBC_TST_RELRO:
+            continue
+        phdr.p_type = PT_GNU_RELRO
+        # Match the alignment the linker uses for PT_GNU_RELRO.
+        phdr.p_align = 1
+        f.seek(ehdr.e_phoff + i * phdr.len)
+        phdr.write(f)
+        converted += 1
+
+    if converted != expected:
+        error('{}: converted {} PT_NOTE segment(s), expected {}'.format(
+            f.name, converted, expected))
+
 def elf_edit(f, opts):
     ei_nident_fmt = 'c' * EI_NIDENT
     ei_nident_len = struct.calcsize(ei_nident_fmt)
@@ -184,6 +223,10 @@  def elf_edit(f, opts):
     if ehdr.e_type not in (ET_EXEC, ET_DYN):
        error('{}: not an executable or shared library'.format(f.name))
 
+    if opts.note_to_relro is not None:
+        elf_edit_note_to_relro(f, e_ident, ehdr, opts.note_to_relro)
+        return
+
     phdr = Elf_Phdr(e_ident)
     maximize_tls_size_done = False
     for i in range(0, ehdr.e_phnum):
@@ -210,6 +253,9 @@  def get_parser():
                         help='How to set the LOAD alignment')
     parser.add_argument('--maximize-tls-size', action='store_true',
                         help='Set maximum PT_TLS size')
+    parser.add_argument('--note-to-relro', type=int, metavar='COUNT',
+                        help='Convert COUNT PT_NOTE segments whose first '
+                        'note has the GLIBC-TST-RELRO name to PT_GNU_RELRO')
     parser.add_argument('output',
                         help='ELF file to edit')
     return parser
diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
index 305ca6e0df2..9f17e9d34c5 100644
--- a/sysdeps/generic/ldsodefs.h
+++ b/sysdeps/generic/ldsodefs.h
@@ -32,6 +32,7 @@ 
 #include <dlfcn.h>
 #include <fpu_control.h>
 #include <sys/mman.h>
+#include <libc-pointer-arith.h>
 #include <link.h>
 #include <dl-lookupcfg.h>
 #include <dl-sysdep.h>
@@ -1037,6 +1038,41 @@  void _dl_relocate_object_no_relro (struct link_map *map,
 /* Protect PT_GNU_RELRO area.  */
 extern void _dl_protect_relro (struct link_map *map) attribute_hidden;
 
+struct dl_relro_range
+{
+  ElfW(Addr) start;
+  ElfW(Addr) end;
+};
+
+/* Compute the address range to protect for the PT_GNU_RELRO segment
+   PH of map L.  AT_LOAD_START tells whether the segment starts a
+   PT_LOAD segment.  mprotect requires page-aligned boundaries, but
+   the static linker only aligns the segment end, possibly only to a
+   link-time page size smaller than the run-time one.  The end is
+   rounded down so the writable data after the segment never loses
+   write access.  A segment that starts a PT_LOAD segment (the
+   traditional single-RELRO layout) has only unused slack below it in
+   its first page, so its start is rounded down; rounding up instead
+   could drop the protection entirely (BFD ld does not align the
+   start at all).  Any other segment is preceded by live writable
+   data, so its start is rounded up.  The result may be empty with
+   START above END; callers must check START < END.  */
+static inline struct dl_relro_range
+_dl_relro_range (const struct link_map *l, const ElfW(Phdr) *ph,
+		 bool at_load_start)
+{
+  ElfW(Addr) start = l->l_addr + ph->p_vaddr;
+  ElfW(Addr) end = start + ph->p_memsz;
+
+  return (struct dl_relro_range)
+    {
+      .start = at_load_start
+	       ? ALIGN_DOWN (start, GLRO(dl_pagesize))
+	       : ALIGN_UP (start, GLRO(dl_pagesize)),
+      .end = ALIGN_DOWN (end, GLRO(dl_pagesize)),
+    };
+}
+
 /* Call _dl_signal_error with a message about an unhandled reloc type.
    TYPE is the result of ELFW(R_TYPE) (r_info), i.e. an R_<CPU>_* value.
    PLT is nonzero if this was a PLT reloc; it just affects the message.  */