I'm trying to understand the difference between partial and full RELRO when compiling ELF files. What I gather from various sources is that only under full RELRO is the entirety of the GOT protected.
Using gcc 13.3.0, I compiled a "Hello, world" program twice:
gcc -Wl,-z,relro,-z,now hello.c -o full
gcc -Wl,-z,relro,-z,lazy hello.c -o partial
checksec
confirms that they have full and partial RELRO, respectively. When I run readelf -a
on full
, I see
...
[21] .init_array INIT_ARRAY 0000000000003db8 002db8 000008 08 WA 0 0 8
[22] .fini_array FINI_ARRAY 0000000000003dc0 002dc0 000008 08 WA 0 0 8
[23] .dynamic DYNAMIC 0000000000003dc8 002dc8 0001f0 10 WA 7 0 8
[24] .got PROGBITS 0000000000003fb8 002fb8 000048 08 WA 0 0 8
[25] .data PROGBITS 0000000000004000 003000 000010 00 WA 0 0 8
[26] .bss NOBITS 0000000000004010 003010 000008 00 WA 0 0 1
...
Program Headers:
...
GNU_RELRO 0x002db8 0x0000000000003db8 0x0000000000003db8 0x000248 0x000248 R 0x1
This makes sense as 0x3db8 + 0x248 = 0x4000
which covers the GOT.
However, when I run readelf -a
on partial
, I see
...
[21] .init_array INIT_ARRAY 0000000000003dd0 002dd0 000008 08 WA 0 0 8
[22] .fini_array FINI_ARRAY 0000000000003dd8 002dd8 000008 08 WA 0 0 8
[23] .dynamic DYNAMIC 0000000000003de0 002de0 0001e0 10 WA 7 0 8
[24] .got PROGBITS 0000000000003fc0 002fc0 000028 08 WA 0 0 8
[25] .got.plt PROGBITS 0000000000003fe8 002fe8 000020 08 WA 0 0 8
[26] .data PROGBITS 0000000000004008 003008 000010 00 WA 0 0 8
[27] .bss NOBITS 0000000000004018 003018 000008 00 WA 0 0 1
...
Program Headers:
...
GNU_RELRO 0x002dd0 0x0000000000003dd0 0x0000000000003dd0 0x000230 0x000230 R 0x1
While the GOT is split into two sections now, the GNU_RELRO
segment still covers both of them.
I guess I'm misunderstanding how RELRO works. What is the difference between partial and full in terms of GOT protection and how can I detect by parsing the ELF?
For context, I have a library I’ve written which parses ELF files and finds GOT PLT entries. I’m trying to add a feature to detect when full RELRO is present.
I'm trying to understand the difference between partial and full RELRO when compiling ELF files. What I gather from various sources is that only under full RELRO is the entirety of the GOT protected.
Using gcc 13.3.0, I compiled a "Hello, world" program twice:
gcc -Wl,-z,relro,-z,now hello.c -o full
gcc -Wl,-z,relro,-z,lazy hello.c -o partial
checksec
confirms that they have full and partial RELRO, respectively. When I run readelf -a
on full
, I see
...
[21] .init_array INIT_ARRAY 0000000000003db8 002db8 000008 08 WA 0 0 8
[22] .fini_array FINI_ARRAY 0000000000003dc0 002dc0 000008 08 WA 0 0 8
[23] .dynamic DYNAMIC 0000000000003dc8 002dc8 0001f0 10 WA 7 0 8
[24] .got PROGBITS 0000000000003fb8 002fb8 000048 08 WA 0 0 8
[25] .data PROGBITS 0000000000004000 003000 000010 00 WA 0 0 8
[26] .bss NOBITS 0000000000004010 003010 000008 00 WA 0 0 1
...
Program Headers:
...
GNU_RELRO 0x002db8 0x0000000000003db8 0x0000000000003db8 0x000248 0x000248 R 0x1
This makes sense as 0x3db8 + 0x248 = 0x4000
which covers the GOT.
However, when I run readelf -a
on partial
, I see
...
[21] .init_array INIT_ARRAY 0000000000003dd0 002dd0 000008 08 WA 0 0 8
[22] .fini_array FINI_ARRAY 0000000000003dd8 002dd8 000008 08 WA 0 0 8
[23] .dynamic DYNAMIC 0000000000003de0 002de0 0001e0 10 WA 7 0 8
[24] .got PROGBITS 0000000000003fc0 002fc0 000028 08 WA 0 0 8
[25] .got.plt PROGBITS 0000000000003fe8 002fe8 000020 08 WA 0 0 8
[26] .data PROGBITS 0000000000004008 003008 000010 00 WA 0 0 8
[27] .bss NOBITS 0000000000004018 003018 000008 00 WA 0 0 1
...
Program Headers:
...
GNU_RELRO 0x002dd0 0x0000000000003dd0 0x0000000000003dd0 0x000230 0x000230 R 0x1
While the GOT is split into two sections now, the GNU_RELRO
segment still covers both of them.
I guess I'm misunderstanding how RELRO works. What is the difference between partial and full in terms of GOT protection and how can I detect by parsing the ELF?
For context, I have a library I’ve written which parses ELF files and finds GOT PLT entries. I’m trying to add a feature to detect when full RELRO is present.
Share Improve this question edited 2 days ago Daniel Walker asked Feb 8 at 5:14 Daniel WalkerDaniel Walker 6,7507 gold badges24 silver badges61 bronze badges2 Answers
Reset to default 0man ld
says about the relro
option:
Create an ELF "PT_GNU_RELRO" segment header in the object. This specifies a memory segment that should be made read-only after relocation, if supported.
The real difference between partial and full RELRO comes from the now
option:
When generating an executable or shared library, mark it to tell the dynamic linker to resolve all symbols when the program is started, or when the shared library is loaded by dlopen, instead of deferring function call resolution to the point when the function is first called.
That is, full RELRO means that .got.plt
entries will be resolved and then marked read-only before control is passed to main
(or before dlopen
returns).
You can detect this in the ELF file by looking at the entries in the .dynamic
section. You'll either see an entry with the DT_BIND_NOW
tag or the DT_FLAGS
tag with the DF_BIND_NOW
flag set in the entry's value.
You've stumbled on an obscure crevice in the GNU linker. You
are almost but not exactly right in saying that even in partial relro
the GNU_RELRO
segment covers the got.plt
section in your "Hello World" linkage.
In fact it doesn't cover it, but it covers the first 24 bytes of it (the first 3 entries).
This outcome is expressly crafted into the default linker script for program linkage, with the intent of negotiating an inconvenient contraint on the demarcation of the relro segment. That constraint is the fact the relro segment must end on a page boundary, because memory access attributes have page grangularity. But page size is an arbitrary number with respect to section size. Hence, when the relro segment is prescribed to protect just the sections S1,...Sn, and Sn, and the final Sn extends some N < page-size bytes into a page, the linker has a choice of either:
- Not protecting those N bytes of Sn
- Protecting some initial bytes of whatever section is Sn + 1 - at most as many as will take the relro segment to the next page boundary
- Pushing all of Sn + 1 beyond the pale, even though it would be cost nothing to protect some of it.
Option #1 is of course unacceptable, but the linker (for x86_64 ELF targets at least), will take option #2 in some linkages and option #3 in others. Your linkage happens to be an option #2 type.
To play it out, I'll redo your "Hello World" linkages:
$ gcc --version | head -n1
gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
$ gcc -g -Wl,-z,relro,-z,now hello.c -o full
$ gcc -g -Wl,-z,relro,-z,lazy hello.c -o partial
Inspecting full
As a baseline, see the program headers of full
:
$ readelf --program-headers --wide full
Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1060
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x0000000000000040 0x0000000000000040 0x0002d8 0x0002d8 R 0x8
INTERP 0x000318 0x0000000000000318 0x0000000000000318 0x00001c 0x00001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x000628 0x000628 R 0x1000
LOAD 0x001000 0x0000000000001000 0x0000000000001000 0x000175 0x000175 R E 0x1000
LOAD 0x002000 0x0000000000002000 0x0000000000002000 0x0000f4 0x0000f4 R 0x1000
LOAD 0x002db8 0x0000000000003db8 0x0000000000003db8 0x000258 0x000260 RW 0x1000
DYNAMIC 0x002dc8 0x0000000000003dc8 0x0000000000003dc8 0x0001f0 0x0001f0 RW 0x8
NOTE 0x000338 0x0000000000000338 0x0000000000000338 0x000030 0x000030 R 0x8
NOTE 0x000368 0x0000000000000368 0x0000000000000368 0x000044 0x000044 R 0x4
GNU_PROPERTY 0x000338 0x0000000000000338 0x0000000000000338 0x000030 0x000030 R 0x8
GNU_EH_FRAME 0x002014 0x0000000000002014 0x0000000000002014 0x000034 0x000034 R 0x4
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x002db8 0x0000000000003db8 0x0000000000003db8 0x000248 0x000248 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .plt.got .plt.sec .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .dynamic .got .data .bss
06 .dynamic
07 .note.gnu.property
08 .note.gnu.build-id .note.ABI-tag
09 .note.gnu.property
10 .eh_frame_hdr
11
12 .init_array .fini_array .dynamic .got
The GNU_RELRO
#12 segment starts at the same address, 0x3db8
as the LOAD
segment #5. It extends 0x248 memory bytes into load segment #5, which is 0x18 bytes
less than the memory size, 0x260, of load segment #5. The relro segment isn't a load
segment, or a distinct segment. It's an mprotect
-ed
initial sub-segment of a load segment.
Per the Section to Segment mappings, the relro segment covers the first 4 sections
of load segment #5, ending with the .got
. It leaves out .data
and .bss
, so the
sum of their sizes by rights should be 0x18, and so it is:
$ readelf --sections --wide full | egrep \(data\|bss\)
[18] .rodata PROGBITS 0000000000002000 002000 000011 00 A 0 0 4
[25] .data PROGBITS 0000000000004000 003000 000010 00 WA 0 0 8
[26] .bss NOBITS 0000000000004010 003010 000008 00 WA 0 0 1
And the relro segment ends on a page boundary: 0x3db8 + 0x248 = 0x4000
Inspecting partial
Next, compare the program headers of partial
:
$ readelf --program-headers --wide partial
Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1060
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x0000000000000040 0x0000000000000040 0x0002d8 0x0002d8 R 0x8
INTERP 0x000318 0x0000000000000318 0x0000000000000318 0x00001c 0x00001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x000628 0x000628 R 0x1000
LOAD 0x001000 0x0000000000001000 0x0000000000001000 0x000175 0x000175 R E 0x1000
LOAD 0x002000 0x0000000000002000 0x0000000000002000 0x0000f4 0x0000f4 R 0x1000
LOAD 0x002dd0 0x0000000000003dd0 0x0000000000003dd0 0x000248 0x000250 RW 0x1000
DYNAMIC 0x002de0 0x0000000000003de0 0x0000000000003de0 0x0001e0 0x0001e0 RW 0x8
NOTE 0x000338 0x0000000000000338 0x0000000000000338 0x000030 0x000030 R 0x8
NOTE 0x000368 0x0000000000000368 0x0000000000000368 0x000044 0x000044 R 0x4
GNU_PROPERTY 0x000338 0x0000000000000338 0x0000000000000338 0x000030 0x000030 R 0x8
GNU_EH_FRAME 0x002014 0x0000000000002014 0x0000000000002014 0x000034 0x000034 R 0x4
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x002dd0 0x0000000000003dd0 0x0000000000003dd0 0x000230 0x000230 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .plt.got .plt.sec .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .dynamic .got .got.plt .data .bss
06 .dynamic
07 .note.gnu.property
08 .note.gnu.build-id .note.ABI-tag
09 .note.gnu.property
10 .eh_frame_hdr
11
12 .init_array .fini_array .dynamic .got
As you noted, we've now got an new .got.plt
section in addition to .got
.
"Officially", the protected sections in load section #5:
12 .init_array .fini_array .dynamic .got
are the same as before, and .got.plt
is added to the unprotected sections:
05 ..................................... .got.plt .data .bss
But the numbers don't quite agree. Now load segment #5 extends 0x250 memory bytes
from address 0x3dd0 and the relro segment extends 0x230 bytes from the same
base address. That makes the relro segment end on the very same page boundary
as in full
: 0x3dd0 + 0x230 = 0x4000. But per the section stats:
$ readelf --sections --wide partial | grep got.plt
[25] .got.plt PROGBITS 0000000000003fe8 002fe8 000020 08 WA 0 0 8
.got.plt
starts at address 0x3fe8 and extends 0x20 memory bytes, making it end
at 0x3fe8 + 0x20 = 0x4008. So in fact the first 0x18 bytes of .got.plt
are within
the relro segment and the last 0x8 bytes are not.
How so?
The source of this outcome is a fragment of the default linker script that drove
the linkage. You can snip the script out from the Wl,--verbose
output of
the linkage. The fragment is:
.got : { *(.got) *(.igot) }
. = DATA_SEGMENT_RELRO_END (SIZEOF (.got.plt) >= 24 ? 24 : 0, .);
.got.plt : { *(.got.plt) *(.igot.plt) }
This is where the end of the relro segment is assigned to the location counter (.
) by the
the builtin-function DATA_SEGMENT_RELRO_END(offset,expression)
.
The documentation of this function
DATA_SEGMENT_RELRO_END(offset, exp)
This defines the end of the PT_GNU_RELRO segment when ‘-z relro’ option is used. When ‘-z relro’ option is not present, DATA_SEGMENT_RELRO_END does nothing, otherwise DATA_SEGMENT_ALIGN is padded so that exp + offset is aligned to the commonpagesize argument given to DATA_SEGMENT_ALIGN. If present in the linker script, it must be placed between DATA_SEGMENT_ALIGN and DATA_SEGMENT_END. Evaluates to the second argument plus any padding needed at the end of the PT_GNU_RELRO segment due to section alignment.
. = DATA_SEGMENT_RELRO_END(24, .);
is as clear as mud. I reverse-engineered the meaning with experimental linker scripts and linkages. Here we need only consider cases of the form:
. = DATA_SEGMENT_RELRO_END(offset, .);
The function resets the current segment base address, pulling it up
to make .
+ offset
be the next page boundary after all the sections in the segment, ensuring that offset
additional bytes following .
can fit into the
relro segment, and returns the fulfilling value of .
So in case offset
== 0,
the fulfilling .
is itself that next page boundary and the end of the relro
segment.
In the variation that we see in the linker script:
. = DATA_SEGMENT_RELRO_END (SIZEOF (.got.plt) >= 24 ? 24 : 0, .);
separating the mapping of .got
from that of .got.plt
, the effect
is to shoe the first three .got.plt
entries, if there are that many, into the relro segment
after the officially protected whole sections as per the program headers,
and if there < 3 then to push the whole section into the unprotected next
segment.
In our "Hello World" linkage, the size of .got.plt
is 0x20 = 32 bytes as
we've already seen. So it qualifies for its first 24 bytes to be unofficially
carved off into the relro segment and the residual 8 pushed out, as we've also seen.
If we redo the partial
linkage requesting the mapfile (e.g. -Wl,-Map=partial.map
),
the relevant snippet is:
.got 0x0000000000003fc0 0x28
*(.got)
.got 0x0000000000003fc0 0x28 /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o
*(.igot)
0x0000000000003fe8 . = DATA_SEGMENT_RELRO_END (., (SIZEOF (.got.plt) >= 0x18)?0x18:0x0)
.got.plt 0x0000000000003fe8 0x20
*(.got.plt)
.got.plt 0x0000000000003fe8 0x20 /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o
0x0000000000003fe8 _GLOBAL_OFFSET_TABLE_
*(.igot.plt)
.data 0x0000000000004008 0x10
Here we have to discount a bug in the mapfile generation which transposes the arguments
of DATA_SEGMENT_RELRO_END
. But we see the 0x20 bytes of .got.plt
mapped starting
at 0x3fe8
within the 0x3dd0-based relro segment, right after .got
, and overshooting
the 0x4000
end of the relro segment by 8 bytes, after which .data
is mapped in.
Without changing hello_world.c
we can simulate a .got.plt
that's too small to
get any of its entries into the relro segment by copying the default linker script
and tweaking it to say:
. = DATA_SEGMENT_RELRO_END (SIZEOF (.got.plt) >= 48 ? 48 : 0, .);
A 48 byte threshold will exclude our 32 byte .got.plt
. Then after:
$ gcc -g -Wl,-z,relro,-z,lazy hello.c -o partial-expt -Wl,-Texpt.ld,-Map=expt.map
the corresponding snippet of expt.map
is:
.got 0x0000000000003fd8 0x28
*(.got)
.got 0x0000000000003fd8 0x28 /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o
*(.igot)
0x0000000000004000 . = DATA_SEGMENT_RELRO_END (., (SIZEOF (.got.plt) >= 0x30)?0x30:0x0)
.got.plt 0x0000000000004000 0x20
with the whole .got.plt
now unprotected at 0x4000
. That's an option #3 linkage.
What is the point of this 24 byte threshold?
I don't know. I've found no explanation and none of my speculations stand scrutiny.
How can you distinguish full from partial relro by parsing an ELF?
The problem case is partial. If .got.plt
< 24 bytes then it will start
where the relro segment ends in partial relro. If it's >= 24 bytes its
first 24 bytes will be at the end of the relro segment in partial relro and the residue at the start of the next segment.
Remember that this rule is good only for x86_64 targets; other ELF targets' default linker scripts will vary. And I wouldn't even bet on it being good for x86_64 targets permanently.