[PATCH v3] elf: Support multiple PT_GNU_RELRO segments
Justin Rivera <[email protected]> Thu, 30 Jul 2026 21:19:41 +0000
| Newsgroups | gmane.comp.lib.glibc.alpha |
|---|---|
| Message-ID | <[email protected]> |
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. elf/tst-relro-symbols.py is also updated to validate symbols against a list of RELRO regions. Tested against elf/tst-relro-symbols.py and the glibc test suite, no regression observed. Added an additional test for coverage of mutli RELRO behavior. Required a script to patch dummy PT_NOTE segments into PT_GNU_RELRO. Signed-off-by: Justin Rivera <[email protected]> --- elf/Makefile | 15 ++++++ elf/dl-load.c | 5 -- elf/dl-readonly-area.c | 23 ++++----- elf/dl-reloc.c | 31 ++++++------ elf/dl-support.c | 5 -- elf/rtld.c | 15 ------ elf/tst-relro-multi.c | 29 +++++++++++ elf/tst-relro-multi.lds | 24 +++++++++ elf/tst-relro-symbols.py | 34 +++++++------ include/link.h | 4 -- scripts/tst-relrofy.py | 102 +++++++++++++++++++++++++++++++++++++++ 11 files changed, 216 insertions(+), 71 deletions(-) create mode 100644 elf/tst-relro-multi.c create mode 100644 elf/tst-relro-multi.lds create mode 100755 scripts/tst-relrofy.py diff --git a/elf/Makefile b/elf/Makefile index 94c5b7e6ed..344f04a787 100644 --- a/elf/Makefile +++ b/elf/Makefile @@ -700,6 +700,21 @@ $(objpfx)tst-relro-libc.out: tst-relro-symbols.py $(..)/scripts/glibcelf.py \ --required=__io_vtables \ > $@ 2>&1; $(evaluate-test) +test-srcs += tst-relro-multi +LDFLAGS-tst-relro-multi = -Wl,-z,now -Wl,-T,$(..)elf/tst-relro-multi.lds + +ifeq ($(run-built-tests),yes) +tests-special += $(objpfx)tst-relro-multi-patched.out + +$(objpfx)tst-relro-multi-patched: $(objpfx)tst-relro-multi $(..)scripts/tst-relrofy.py + cp $< $@ + $(PYTHON) $(..)scripts/tst-relrofy.py $@ 1 + +$(objpfx)tst-relro-multi-patched.out: $(objpfx)tst-relro-multi-patched + $(run-program-prefix) $< > $@ 2>&1; \ + $(evaluate-test) +endif + ifeq ($(run-built-tests),yes) tests-special += $(objpfx)tst-valgrind-smoke.out endif diff --git a/elf/dl-load.c b/elf/dl-load.c index 95404adae9..e76d149f1f 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 833f455904..7883d6c7bf 100644 --- a/elf/dl-readonly-area.c +++ b/elf/dl-readonly-area.c @@ -21,19 +21,20 @@ static bool 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, + for (const ElfW(Phdr) *ph = l->l_phdr; ph < &l->l_phdr[l->l_phnum]; ++ph) + if (ph->p_type == PT_GNU_RELRO) + { + uintptr_t relro_start = ALIGN_DOWN (l->l_addr + ph->p_vaddr, + GLRO(dl_pagesize)); + uintptr_t relro_end = ALIGN_DOWN (l->l_addr + ph->p_vaddr + + ph->p_memsz, 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; + if (relro_start <= start && end <= relro_end) + 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 15a6a4cffe..f4003bfee5 100644 --- a/elf/dl-reloc.c +++ b/elf/dl-reloc.c @@ -348,23 +348,22 @@ _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; - - 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_("\ + const ElfW(Phdr) *ph; + for (ph = l->l_phdr; ph < &l->l_phdr[l->l_phnum]; ++ph) + if (ph->p_type == PT_GNU_RELRO) + { + ElfW(Addr) start = ALIGN_DOWN (l->l_addr + ph->p_vaddr, + GLRO(dl_pagesize)); + ElfW(Addr) end = ALIGN_DOWN (l->l_addr + ph->p_vaddr + ph->p_memsz, + GLRO(dl_pagesize)); + if (start != end + && __mprotect ((void *) start, end - 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 b57fd74670..041b89da79 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 fc053df858..b9d0047a68 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.c b/elf/tst-relro-multi.c new file mode 100644 index 0000000000..f8e1e0ac30 --- /dev/null +++ b/elf/tst-relro-multi.c @@ -0,0 +1,29 @@ +/* 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/>. */ +/* Three page-padded regions in the RW load. A and B become PT_GNU_RELRO + after post-processing; the .data gap between them stays writable, so a + correct loader must protect two *non-contiguous* relro regions. */ +__attribute__ ((section (".note.a"), used)) unsigned long int relro_a = 0xAAAA; +__attribute__ ((section (".gap"), used)) unsigned long int gap_d = 0xDDDD; +__attribute__ ((section (".note.b"), used)) unsigned long int relro_b = 0xBBBB; + +int +main (void) +{ + return 0; +} diff --git a/elf/tst-relro-multi.lds b/elf/tst-relro-multi.lds new file mode 100644 index 0000000000..eeb6af15d3 --- /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 ffbe9958fe..93d9308851 100644 --- a/elf/tst-relro-symbols.py +++ b/elf/tst-relro-symbols.py @@ -32,25 +32,29 @@ 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.""" +def find_relro(path: str, img: glibcelf.Image) -> list: + """Discover the address ranges of the PT_GNU_RELRO segments.""" + regions = [] 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) + regions.append((phdr.p_vaddr, phdr.p_vaddr + phdr.p_memsz)) + 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'), start, size)) def get_parser(): """Return an argument parser for this script.""" @@ -78,7 +82,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 +113,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 8f851d2212..04274b490e 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-relrofy.py b/scripts/tst-relrofy.py new file mode 100755 index 0000000000..0c396fcf4e --- /dev/null +++ b/scripts/tst-relrofy.py @@ -0,0 +1,102 @@ +#! /usr/bin/env python3 +# ELF editor to convert PT_NOTE to PT_GNU_RELRO. +# Copyright (C) 2026 Free Software Foundation, Inc. +# Copyright The GNU Toolchain Authors. +# 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/>. +"""Convert placeholder PT_NOTE segments into PT_GNU_RELRO inplace. + +Targets only PT_NOTE phdrs whose p_vaddr lies inside a writable PT_LOAD, so +genuine notes (build-id, gnu.property) in the read-only load are skipped. +""" + +from __future__ import print_function + +import struct +import sys + +PT_LOAD = 1 +PT_NOTE = 4 +PT_GNU_RELRO = 0x6474E552 +PF_W = 2 + + +def get_field(elf, base, offset, fmt): + """Unpack a specific field from an ELF program header.""" + return struct.unpack_from(fmt, elf, base + offset)[0] + + +def main(path, set_align=None): + """Convert placeholder PT_NOTE segments into PT_GNU_RELRO inplace. + + Args: + path: the filepath of the ELF binary to make PT_NOTE replacements on + set_align: optional p_align value to set on converted PT_GNU_RELRO + segments + """ + with open(path, 'rb') as f: + elf = bytearray(f.read()) + + if elf[:4] != b'\x7fELF': + sys.exit('Error: %s is not a valid ELF file.' % path) + + is64 = elf[4] == 2 + endian = '<' if elf[5] == 1 else '>' + type_fmt = '%sI' % endian + addr_fmt = '%sQ' % endian if is64 else '%sI' % endian + + if is64: + ph_off = struct.unpack_from(endian + 'Q', elf, 0x20)[0] + phentsz, phnum = struct.unpack_from(endian + 'HH', elf, 0x36) + o_type, o_flags, o_vaddr, o_memsz, o_align = 0, 4, 16, 40, 48 + else: + ph_off = struct.unpack_from(endian + 'I', elf, 0x1C)[0] + phentsz, phnum = struct.unpack_from(endian + 'HH', elf, 0x2A) + o_type, o_flags, o_vaddr, o_memsz, o_align = 0, 24, 8, 20, 28 + + wr_loads = [] + for i in range(phnum): + base = ph_off + i * phentsz + if get_field(elf, base, o_type, type_fmt) == PT_LOAD and ( + get_field(elf, base, o_flags, type_fmt) & PF_W + ): + vaddr = get_field(elf, base, o_vaddr, addr_fmt) + memsz = get_field(elf, base, o_memsz, addr_fmt) + wr_loads.append((vaddr, vaddr + memsz)) + + converted = 0 + for i in range(phnum): + base = ph_off + i * phentsz + if get_field(elf, base, o_type, type_fmt) != PT_NOTE: + continue + + vaddr = get_field(elf, base, o_vaddr, addr_fmt) + if not any(lo <= vaddr < hi for lo, hi in wr_loads): + continue # Real note, skip + + struct.pack_into(type_fmt, elf, base + o_type, PT_GNU_RELRO) + if set_align is not None: + struct.pack_into(addr_fmt, elf, base + o_align, set_align) + converted += 1 + + with open(path, 'wb') as f: + f.write(elf) + print('converted %d PT_NOTE -> PT_GNU_RELRO in %s' % (converted, path)) + + +if __name__ == '__main__': + align = int(sys.argv[2], 0) if len(sys.argv) > 2 else 1 + main(sys.argv[1], align) -- 2.55.0.508.g3f0d502094-goog