elf: Support multiple PT_GNU_RELRO segments
Checks
| Context |
Check |
Description |
| redhat-pt-bot/TryBot-apply_patch |
success
|
Patch applied to master at the time it was sent
|
| linaro-tcwg-bot/tcwg_glibc_build--master-aarch64 |
success
|
Build passed
|
| linaro-tcwg-bot/tcwg_glibc_check--master-aarch64 |
success
|
Test passed
|
| linaro-tcwg-bot/tcwg_glibc_build--master-arm |
success
|
Build passed
|
| redhat-pt-bot/TryBot-32bit |
success
|
Build for i686
|
| linaro-tcwg-bot/tcwg_glibc_check--master-arm |
success
|
Test passed
|
Commit Message
Hello,
While this patch appears to be working as intended, I had a question
regarding any additions to the test suite for testing RELRO behavior.
This patch modifies elf/tst-relro-symbols.py, which can statically check
ELF headers. However, there don't seem to be any dynamic checks that would
work for multiple RELRO segments. We were able to do some ad-hoc testing
with some hand-written assembly, but getting something working within the
test suite seemed to be a bit trickier. (Often ran into
`/usr/bin/ld: foo.so: warning: unable to allocate any sections to PT_GNU_RELRO segment`
without some additional patching hack, for example).
Are there any suggestions on how to best test the behavior changed by 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.
Signed-off-by: Justin Rivera <jnrivera@google.com>
---
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-symbols.py | 34 +++++++++++++++++++---------------
include/link.h | 4 ----
7 files changed, 46 insertions(+), 71 deletions(-)
Comments
On Tue, Jul 7, 2026 at 5:11 AM Justin Rivera <jnrivera@google.com> wrote:
>
> Hello,
>
> While this patch appears to be working as intended, I had a question
> regarding any additions to the test suite for testing RELRO behavior.
>
> This patch modifies elf/tst-relro-symbols.py, which can statically check
> ELF headers. However, there don't seem to be any dynamic checks that would
> work for multiple RELRO segments. We were able to do some ad-hoc testing
> with some hand-written assembly, but getting something working within the
> test suite seemed to be a bit trickier. (Often ran into
> `/usr/bin/ld: foo.so: warning: unable to allocate any sections to PT_GNU_RELRO segment`
> without some additional patching hack, for example).
>
> Are there any suggestions on how to best test the behavior changed by this patch?
We should add the linker support first. Linker can have an option
to generate multiple PT_GNU_RELRO segments for small binaries.
H.J.
On Mon, Jul 6, 2026 at 5:15 PM H.J. Lu <hjl.tools@gmail.com> wrote:
>
> On Tue, Jul 7, 2026 at 5:11 AM Justin Rivera <jnrivera@google.com> wrote:
> >
> > Hello,
> >
> > While this patch appears to be working as intended, I had a question
> > regarding any additions to the test suite for testing RELRO behavior.
> >
> > This patch modifies elf/tst-relro-symbols.py, which can statically check
> > ELF headers. However, there don't seem to be any dynamic checks that would
> > work for multiple RELRO segments. We were able to do some ad-hoc testing
> > with some hand-written assembly, but getting something working within the
> > test suite seemed to be a bit trickier. (Often ran into
> > `/usr/bin/ld: foo.so: warning: unable to allocate any sections to PT_GNU_RELRO segment`
> > without some additional patching hack, for example).
> >
> > Are there any suggestions on how to best test the behavior changed by this patch?
>
> We should add the linker support first. Linker can have an option
> to generate multiple PT_GNU_RELRO segments for small binaries.
>
>
> H.J.
Hi,
Could you provide some more details regarding adding linker support?
If you could point me toward
the right place in binutils as well, that'd help tons.
Also, just for additional context, we were able to test this change
locally via a hand-written
assembly ELF binary. We bypassed the GNU linker to emit an ELF file
with two `PT_GNU_RELRO`
segments, and used it to dynamically verify that the loader calls
protects both segments. We proved
this via a shell script that checks for segfaults when the binary
attempts to illegally write to either protected segment.
Is this something that can be useful for the glibc test suite if
cleaned up? Or would you prefer to hold
off on this glibc patch entirely until explicit linker support is landed?
On 07/07/26 15:17, Justin Rivera wrote:
> On Mon, Jul 6, 2026 at 5:15 PM H.J. Lu <hjl.tools@gmail.com> wrote:
>>
>> On Tue, Jul 7, 2026 at 5:11 AM Justin Rivera <jnrivera@google.com> wrote:
>>>
>>> Hello,
>>>
>>> While this patch appears to be working as intended, I had a question
>>> regarding any additions to the test suite for testing RELRO behavior.
>>>
>>> This patch modifies elf/tst-relro-symbols.py, which can statically check
>>> ELF headers. However, there don't seem to be any dynamic checks that would
>>> work for multiple RELRO segments. We were able to do some ad-hoc testing
>>> with some hand-written assembly, but getting something working within the
>>> test suite seemed to be a bit trickier. (Often ran into
>>> `/usr/bin/ld: foo.so: warning: unable to allocate any sections to PT_GNU_RELRO segment`
>>> without some additional patching hack, for example).
>>>
>>> Are there any suggestions on how to best test the behavior changed by this patch?
>>
>> We should add the linker support first. Linker can have an option
>> to generate multiple PT_GNU_RELRO segments for small binaries.
>>
>>
>> H.J.
>
> Hi,
>
> Could you provide some more details regarding adding linker support?
> If you could point me toward
> the right place in binutils as well, that'd help tons.
>
> Also, just for additional context, we were able to test this change
> locally via a hand-written
> assembly ELF binary. We bypassed the GNU linker to emit an ELF file
> with two `PT_GNU_RELRO`
> segments, and used it to dynamically verify that the loader calls
> protects both segments. We proved
> this via a shell script that checks for segfaults when the binary
> attempts to illegally write to either protected segment.
>
> Is this something that can be useful for the glibc test suite if
> cleaned up? Or would you prefer to hold
> off on this glibc patch entirely until explicit linker support is landed?
We already have scripts/tst-elf-edit.py which creates synthetic DSO with
different alignments, so a platform agnostic testing framework could work.
Is these hand-written assembly arch-specific or can you make them generic
(so we can test onany supported ABI)?
One possibility is to create the tests (ET_EXEC/ET_DYN) with some extra dummy
DT_NEEDED tags by linking with -Wl,--as-needed and some libraries, and then
post-processing it and replace it with PT_GNU_RELRO tags.
The mold linker has --spare-dynamic-tags=N, but unfortunately we can not rely
on it for testing.
On Wed, Jul 8, 2026 at 3:52 AM Adhemerval Zanella Netto
<adhemerval.zanella@linaro.org> wrote:
>
>
>
> On 07/07/26 15:17, Justin Rivera wrote:
> > On Mon, Jul 6, 2026 at 5:15 PM H.J. Lu <hjl.tools@gmail.com> wrote:
> >>
> >> On Tue, Jul 7, 2026 at 5:11 AM Justin Rivera <jnrivera@google.com> wrote:
> >>>
> >>> Hello,
> >>>
> >>> While this patch appears to be working as intended, I had a question
> >>> regarding any additions to the test suite for testing RELRO behavior.
> >>>
> >>> This patch modifies elf/tst-relro-symbols.py, which can statically check
> >>> ELF headers. However, there don't seem to be any dynamic checks that would
> >>> work for multiple RELRO segments. We were able to do some ad-hoc testing
> >>> with some hand-written assembly, but getting something working within the
> >>> test suite seemed to be a bit trickier. (Often ran into
> >>> `/usr/bin/ld: foo.so: warning: unable to allocate any sections to PT_GNU_RELRO segment`
> >>> without some additional patching hack, for example).
> >>>
> >>> Are there any suggestions on how to best test the behavior changed by this patch?
> >>
> >> We should add the linker support first. Linker can have an option
> >> to generate multiple PT_GNU_RELRO segments for small binaries.
> >>
> >>
> >> H.J.
> >
> > Hi,
> >
> > Could you provide some more details regarding adding linker support?
> > If you could point me toward
> > the right place in binutils as well, that'd help tons.
> >
> > Also, just for additional context, we were able to test this change
> > locally via a hand-written
> > assembly ELF binary. We bypassed the GNU linker to emit an ELF file
> > with two `PT_GNU_RELRO`
> > segments, and used it to dynamically verify that the loader calls
> > protects both segments. We proved
> > this via a shell script that checks for segfaults when the binary
> > attempts to illegally write to either protected segment.
> >
> > Is this something that can be useful for the glibc test suite if
> > cleaned up? Or would you prefer to hold
> > off on this glibc patch entirely until explicit linker support is landed?
>
> We already have scripts/tst-elf-edit.py which creates synthetic DSO with
> different alignments, so a platform agnostic testing framework could work.
>
> Is these hand-written assembly arch-specific or can you make them generic
> (so we can test onany supported ABI)?
>
> One possibility is to create the tests (ET_EXEC/ET_DYN) with some extra dummy
> DT_NEEDED tags by linking with -Wl,--as-needed and some libraries, and then
> post-processing it and replace it with PT_GNU_RELRO tags.
>
> The mold linker has --spare-dynamic-tags=N, but unfortunately we can not rely
> on it for testing.
>
ld has
‘--spare-dynamic-tags=COUNT’
This option specifies the number of empty slots to leave in the
.dynamic section of ELF shared objects. Empty slots may be needed
by post processing tools, such as the prelinker. The default is 5.
* Adhemerval Zanella Netto:
> One possibility is to create the tests (ET_EXEC/ET_DYN) with some extra dummy
> DT_NEEDED tags by linking with -Wl,--as-needed and some libraries, and then
> post-processing it and replace it with PT_GNU_RELRO tags.
I don't see how this would work. We can try to create extra PT_NOTE
segments (perhaps by varying alignment?), and turn those into
PT_GNU_RELRO via post-processing.
Thanks,
Florian
On 09/07/26 05:43, Florian Weimer wrote:
> * Adhemerval Zanella Netto:
>
>> One possibility is to create the tests (ET_EXEC/ET_DYN) with some extra dummy
>> DT_NEEDED tags by linking with -Wl,--as-needed and some libraries, and then
>> post-processing it and replace it with PT_GNU_RELRO tags.
>
> I don't see how this would work. We can try to create extra PT_NOTE
> segments (perhaps by varying alignment?), and turn those into
> PT_GNU_RELRO via post-processing.
The idea is just to create extra DT_* entries, the shared libraries are just
placeholders. But as H.J has noted, ld.bfd already supports --spare-dynamic-tags=
(not sure which version, but we can add a configure script).
* Adhemerval Zanella Netto:
> On 09/07/26 05:43, Florian Weimer wrote:
>> * Adhemerval Zanella Netto:
>>
>>> One possibility is to create the tests (ET_EXEC/ET_DYN) with some extra dummy
>>> DT_NEEDED tags by linking with -Wl,--as-needed and some libraries, and then
>>> post-processing it and replace it with PT_GNU_RELRO tags.
>>
>> I don't see how this would work. We can try to create extra PT_NOTE
>> segments (perhaps by varying alignment?), and turn those into
>> PT_GNU_RELRO via post-processing.
>
> The idea is just to create extra DT_* entries, the shared libraries are just
> placeholders. But as H.J has noted, ld.bfd already supports --spare-dynamic-tags=
> (not sure which version, but we can add a configure script).
I'm not sure this reserves space in the right area of the ELF object.
Thanks,
Florian
On 09/07/26 08:47, Florian Weimer wrote:
> * Adhemerval Zanella Netto:
>
>> On 09/07/26 05:43, Florian Weimer wrote:
>>> * Adhemerval Zanella Netto:
>>>
>>>> One possibility is to create the tests (ET_EXEC/ET_DYN) with some extra dummy
>>>> DT_NEEDED tags by linking with -Wl,--as-needed and some libraries, and then
>>>> post-processing it and replace it with PT_GNU_RELRO tags.
>>>
>>> I don't see how this would work. We can try to create extra PT_NOTE
>>> segments (perhaps by varying alignment?), and turn those into
>>> PT_GNU_RELRO via post-processing.
>>
>> The idea is just to create extra DT_* entries, the shared libraries are just
>> placeholders. But as H.J has noted, ld.bfd already supports --spare-dynamic-tags=
>> (not sure which version, but we can add a configure script).
>
> I'm not sure this reserves space in the right area of the ELF object.
Scratch that, we need extra ElfXX_Phdr and not a ElfXX_Dyn. The PT_NOTE does seems
a better way to hack this.
With linker scripts' PHDRS, we can create multiple PT_GNU_RELRO segments, working with both GNU ld and ld.lld.
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
}
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
> }
@@ -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;
}
}
@@ -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;
}
@@ -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
@@ -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 ();
@@ -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
@@ -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
@@ -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;
};