[PATCH v2] elf: Support multiple PT_GNU_RELRO segments

Justin Rivera <[email protected]>
Newsgroups gmane.comp.lib.glibc.alpha
Message-ID <[email protected]>
Hello everyone,

I've managed to adopt the testing strategy of substituting dummy
PT_NOTE segments for PT_GNU_RELRO that was previously discussed
and included it in v2 of this patch.
--- >8 ---
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  |  63 ++++++++++++++++++++++++
 elf/tst-relro-symbols.py |  34 +++++++------
 include/link.h           |   4 --
 scripts/tst-relrofy.py   | 102 +++++++++++++++++++++++++++++++++++++++
 11 files changed, 255 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..111c8a591c 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)
 
+tests += 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
+	$(test-wrapper) $< > $@ 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..0153d4a0f0
--- /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 (".relro.a"), used)) unsigned long int relro_a = 0xAAAA;
+__attribute__ ((section (".gap"),     used)) unsigned long int gap_d   = 0xDDDD;
+__attribute__ ((section (".relro.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..0bd5541fd2
--- /dev/null
+++ b/elf/tst-relro-multi.lds
@@ -0,0 +1,63 @@
+/* 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/>.  */
+PHDRS {
+  phdr    PT_PHDR PHDRS FLAGS(4);
+  interp  PT_INTERP FLAGS(4);
+  ro      PT_LOAD FILEHDR PHDRS FLAGS(4);
+  text    PT_LOAD FLAGS(5);
+  rw      PT_LOAD FLAGS(6);
+  dynamic PT_DYNAMIC FLAGS(6);
+  note_a  PT_NOTE FLAGS(4);   /* placeholder -> PT_GNU_RELRO #1 */
+  note_b  PT_NOTE FLAGS(4);   /* placeholder -> PT_GNU_RELRO #2 */
+}
+SECTIONS {
+  . = SIZEOF_HEADERS;
+  .interp        : { *(.interp) }                :ro :interp
+  .note.gnu.build-id : { *(.note.gnu.build-id) } :ro
+  .dynsym  : { *(.dynsym) }   :ro
+  .gnu.hash : { *(.gnu.hash) } :ro
+  .hash    : { *(.hash) }     :ro
+  .dynstr  : { *(.dynstr) }   :ro
+  .gnu.version   : { *(.gnu.version) }   :ro
+  .gnu.version_r : { *(.gnu.version_r) } :ro
+  .rela.dyn : { *(.rela.dyn) } :ro
+  .rela.plt : { *(.rela.plt) } :ro
+  .rodata  : { *(.rodata .rodata.*) } :ro
+  .eh_frame_hdr : { *(.eh_frame_hdr) } :ro
+  .eh_frame : { *(.eh_frame) } :ro
+
+  . = ALIGN(CONSTANT(MAXPAGESIZE));
+  .init : { *(.init) } :text
+  .plt  : { *(.plt) *(.iplt) } :text
+  .text : { *(.text .text.*) } :text
+  .fini : { *(.fini) } :text
+
+  . = ALIGN(CONSTANT(MAXPAGESIZE));
+  /* relro region #1: one full page, tagged into note_a */
+  .relro.a : { *(.relro.a) . = ALIGN(CONSTANT(MAXPAGESIZE)); } :rw :note_a
+  .dynamic : { *(.dynamic) } :rw :dynamic
+  .got     : { *(.got) *(.igot) } :rw
+  .got.plt : { *(.got.plt) *(.igot.plt) } :rw
+  /* non-relro gap, one full page, stays writable */
+  .gap  : { *(.gap) . = ALIGN(CONSTANT(MAXPAGESIZE)); } :rw
+  .data : { *(.data .data.*) } :rw
+  /* relro region #2: one full page, tagged into note_b */
+  . = ALIGN(CONSTANT(MAXPAGESIZE));
+  .relro.b : { *(.relro.b) . = ALIGN(CONSTANT(MAXPAGESIZE)); } :rw :note_b
+  .bss  : { *(.bss) *(COMMON) } :rw
+}
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.141.g00534a21ce-goog
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.