[v2] elf: Support multiple PT_GNU_RELRO segments
Commit Message
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
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. */
* 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
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
@@ -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.
@@ -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;
}
}
@@ -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;
}
@@ -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
@@ -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 ();
@@ -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
new file mode 100644
@@ -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:
new file mode 100644
@@ -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"
new file mode 100644
@@ -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>
new file mode 100644
@@ -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;
@@ -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
@@ -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;
};
@@ -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
@@ -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. */