[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