[PATCH] elf: Support multiple PT_GNU_RELRO segments

Adhemerval Zanella Netto adhemerval.zanella@linaro.org
Fri Jul 10 14:24:24 GMT 2026



On 10/07/26 00:24, Fangrui Song wrote:
> With linker scripts' PHDRS, we can create multiple PT_GNU_RELRO segments, working with both GNU ld and ld.lld.

Does it really work with ld.bfd? Trying the phdrs.lds with 2.46.50.20260710,
and it throws:

[...]ld-new: a-x86_64: warning: unable to allocate any sections to PT_GNU_RELRO segment
[...]ld-new: a-x86_64: warning: unable to allocate any sections to PT_GNU_RELRO segment

Ideally we should use only one way to create multiple PT_GNU_RELRO, either
through some canonical way (it could be either through SECTIONS or by PHDRS,
as seems supported by recent ld), or by some clever hack with PT_NOTES plus 
pos-process script.

I don't really want to add different ways to handle different linker on testing,
specially if ld.bfd does not add similar support as lld. I do not mind restrict
to a configure check; but having the test enabled independent of linker
support would be better.

The NOTES hacks seems a slight better way to accomplish it, it works on both
gcc and clang, with ld and lld. I believe we could trim down the linker script
further (not sure).

$ cat test.c
/* 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 relro_a = 0xAAAA;
__attribute__((section(".gap"),     used)) unsigned long gap_d   = 0xDDDD;
__attribute__((section(".relro.b"), used)) unsigned long relro_b = 0xBBBB;

int main(void) {
  return 0;
}
# The notes.lds enforce the PT_NOTE expected alignment for PT_GNU_RELRO
# without adding this requirement on the C code (which can be tricky for
# glibc testing).
$ cat notes.lds
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
}
$ gcc -Wall test.c -Wl,-z,now -Wl,-T,notes.lds -o test
$ readelf -Wl test | grep RELRO
$ cat relrofy.py
#! /usr/bin/env python3
"""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"""
import struct, sys

PT_LOAD, PT_NOTE, PT_GNU_RELRO = 1, 4, 0x6474e552
PF_W = 2

def main(path, set_align=None):
    b = bytearray(open(path, 'rb').read())
    assert b[:4] == b'\x7fELF'
    is64 = b[4] == 2
    en = '<' if b[5] == 1 else '>'
    # ELF64
    if is64:
        phoff = struct.unpack_from(en+'Q', b, 0x20)[0]
        phentsize, phnum = struct.unpack_from(en+'HH', b, 0x36)
        T = en+'I'; A = en+'Q'
        off_type, off_flags, off_vaddr, off_memsz, off_align = 0, 4, 16, 40, 48
    # ELF32
    else:
        phoff = struct.unpack_from(en+'I', b, 0x1c)[0]
        phentsize, phnum = struct.unpack_from(en+'HH', b, 0x2a)
        T = en+'I'; A = en+'I'
        off_type, off_flags, off_vaddr, off_memsz, off_align = 0, 24, 8, 20, 28

    def fld(i, o, f):
        return struct.unpack_from(f, b, phoff + i*phentsize + o)[0]

    wr = []
    for i in range(phnum):
        if fld(i, off_type, T) == PT_LOAD and (fld(i, off_flags, T) & PF_W):
            va, msz = fld(i, off_vaddr, A), fld(i, off_memsz, A)
            wr.append((va, va + msz))

    n = 0
    for i in range(phnum):
        base = phoff + i*phentsize
        if fld(i, off_type, T) != PT_NOTE:
            continue
        va = fld(i, off_vaddr, A)
        if not any(lo <= va < hi for lo, hi in wr):
            continue  # a real note, skip
        struct.pack_into(T, b, base + off_type, PT_GNU_RELRO)
        if set_align is not None:
            struct.pack_into(A, b, base + off_align, set_align)
        n += 1

    open(path, 'wb').write(b)
    print(f"converted {n} PT_NOTE -> PT_GNU_RELRO in {path}")

if __name__ == '__main__':
    align = int(sys.argv[2], 0) if len(sys.argv) > 2 else 1
    main(sys.argv[1], align)
$ ./relrofy.py test
converted 2 PT_NOTE -> PT_GNU_RELRO in test
$ readelf -Wl test | grep RELRO
  GNU_RELRO      0x002000 0x0000000000002000 0x0000000000002000 0x001000 0x001000 R   0x1
  GNU_RELRO      0x005000 0x0000000000005000 0x0000000000005000 0x001000 0x001000 R   0x1

> 
> I have recently added support for multiple PT_GNU_RELRO segments without PHDRS
> https://github.com/llvm/llvm-project/pull/203675
> 
> // a.c
> 
> #define _GNU_SOURCE
> #include <link.h>
> #include <stdio.h>
> 
> const char *const greeting = "Hello, world!";
> 
> int counter = 42;
> 
> static char perm_at(unsigned long addr) {
>   FILE *f = fopen("/proc/self/maps", "r");
>   char line[128], w = '?';
>   unsigned long beg, end;
>   char perms[8];
>   while (f && fgets(line, sizeof line, f))
>     if (sscanf(line, "%lx-%lx %7s", &beg, &end, perms) == 3 && beg <= addr &&
>         addr < end) {
>       w = perms[1];
>       break;
>     }
>   if (f)
>     fclose(f);
>   return w;
> }
> 
> static int report(struct dl_phdr_info *info, size_t size, void *data) {
>   if (info->dlpi_name[0] != '\0')
>     return 0;
>   for (unsigned i = 0; i < info->dlpi_phnum; i++) {
>     const ElfW(Phdr) *ph = &info->dlpi_phdr[i];
>     if (ph->p_type != PT_GNU_RELRO)
>       continue;
>     unsigned long beg = info->dlpi_addr + ph->p_vaddr;
>     printf("PT_GNU_RELRO [0x%lx, 0x%lx): %s\n", beg, beg + ph->p_memsz,
>            perm_at(beg) == 'w' ? "still writable (ld.so did not protect it)"
>                                : "read-only");
>   }
>   return 0;
> }
> 
> int main(void) {
>   counter++;
>   printf("%s (counter=%d, greeting is in .data.rel.ro)\n", greeting, counter);
>   dl_iterate_phdr(report, NULL);
>   return 0;
> }
> 
> ## relro.lds
> 
> SECTIONS {
>   . = SIZEOF_HEADERS;
>   .interp : { *(.interp) }
>   .rodata : { *(.rodata .rodata.*) }
>   .eh_frame_hdr : { *(.eh_frame_hdr) }
>   .eh_frame : { *(.eh_frame) }
> 
>   . = ALIGN(CONSTANT(MAXPAGESIZE));
>   .init : { *(.init) }
>   .text : { *(.text .text.*) }
>   .fini : { *(.fini) }
>   .plt : { *(.plt) }
> 
>   /* relro run 1, padded to a full page so that a loader honoring it can
>      mprotect whole pages */
>   . = ALIGN(CONSTANT(MAXPAGESIZE));
>   .init_array : { *(.init_array) }
>   .fini_array : { *(.fini_array) }
>   .dynamic : { *(.dynamic) }
>   .got : { *(.got) }
>   .got.plt : { *(.got.plt) . = ALIGN(CONSTANT(MAXPAGESIZE)); }
> 
>   /* non-relro: splits PT_GNU_RELRO in two */
>   .data : { *(.data) }
> 
>   /* relro run 2, page-aligned and page-padded (see above) */
>   . = ALIGN(CONSTANT(MAXPAGESIZE));
>   .data.rel.ro : { *(.data.rel.ro .data.rel.ro.*) . = ALIGN(CONSTANT(MAXPAGESIZE)); }
> 
>   .bss : { *(.bss) *(COMMON) }
> }
> 
> ## build.sh
> 
> clang -O1 -g -fPIE -c hello.c -o hello.o
> clang -pie --ld-path=/tmp/Rel/bin/ld.lld -Wl,-z,now -Wl,-T,relro.lds hello.o -o hello
> readelf -lW hello | grep -E 'Type|LOAD|GNU_RELRO'
> ./hello
> 
> ## phdrs.lds
> 
> PHDRS {
>   hdrs 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);
>   dyn PT_DYNAMIC FLAGS (6);
>   relro1 PT_GNU_RELRO FLAGS (4);
>   relro2 PT_GNU_RELRO FLAGS (4);
>   eh PT_GNU_EH_FRAME FLAGS (4);
>   stack PT_GNU_STACK FLAGS (6);
> }
> 
> SECTIONS {
>   . = SIZEOF_HEADERS;
>   .interp : { *(.interp) } :ro :interp
>   .note : { *(.note*) } :ro
>   .dynsym : { *(.dynsym) } :ro
>   .gnu.version : { *(.gnu.version) } :ro
>   .gnu.version_r : { *(.gnu.version_r) } :ro
>   .gnu.hash : { *(.gnu.hash) } :ro
>   .dynstr : { *(.dynstr) } :ro
>   .rela.dyn : { *(.rela.dyn) } :ro
>   .rela.plt : { *(.rela.plt) } :ro
>   .rodata : { *(.rodata .rodata.*) } :ro
>   .eh_frame_hdr : { *(.eh_frame_hdr) } :ro :eh
>   .eh_frame : { *(.eh_frame) } :ro
> 
>   . = ALIGN(CONSTANT(MAXPAGESIZE));
>   .init : { *(.init) } :text
>   .text : { *(.text .text.*) } :text
>   .fini : { *(.fini) } :text
>   .plt : { *(.plt) } :text
> 
>   /* relro run 1, page-padded so that a loader honoring it can mprotect
>      whole pages */
>   . = ALIGN(CONSTANT(MAXPAGESIZE));
>   .init_array : { *(.init_array) } :rw :relro1
>   .fini_array : { *(.fini_array) } :rw :relro1
>   .dynamic : { *(.dynamic) } :rw :dyn :relro1
>   .got : { *(.got) } :rw :relro1
>   .got.plt : { *(.got.plt) . = ALIGN(CONSTANT(MAXPAGESIZE)); } :rw :relro1
> 
>   /* non-relro: splits PT_GNU_RELRO in two */
>   .data : { *(.data) } :rw
>   .data.rel.local : { *(.data.rel.local) } :rw
> 
>   /* relro run 2, page-aligned and page-padded (see above) */
>   . = ALIGN(CONSTANT(MAXPAGESIZE));
>   .data.rel.ro : { *(.data.rel.ro .data.rel.ro.*) . = ALIGN(CONSTANT(MAXPAGESIZE)); } :rw :relro2
> 
>   .bss : { *(.bss) *(COMMON) } :rw
> }



More information about the Libc-alpha mailing list