[PATCH v3] elf: Support multiple PT_GNU_RELRO segments
Justin Rivera
jnrivera@google.com
Thu Jul 30 21:19:41 GMT 2026
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 <jnrivera@google.com>
---
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
More information about the Libc-alpha
mailing list