Re: [PATCH] elf: Support multiple PT_GNU_RELRO segments

Adhemerval Zanella Netto <[email protected]>
Newsgroups gmane.comp.lib.glibc.alpha
Organization Linaro
Message-ID <[email protected]>

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
> }
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.