The thing I'm wondering about with this is why ld by default creates only two LOAD segments, one r-x and one rw-. .text and .rodata are put into the former while .data is put into the latter. This is the only reason this works. So my question is why there isn't a third LOAD segment with access r-- for the read only data? If anything it would reduce the surface of where to find ROP gadgets slightly so couldn't hurt, no?
Now, you'd expect all to be read-only, but with Position Independent Code (PIC) and Position Independent Executables (PIE), which are prerequisite for Address Space Layout Randomization (ASLR), it's not, because that errors array contains pointers to those literal strings, which means those pointers need to be "relocated": the executable or library can't contain the actual pointer to those, since their addess may vary between executions. So the executable/library only contains offsets, and the dynamic linker adjusts those offsets to make them real pointers. Thus that errors array is actually not read-only.
There is an additional ELF segment for those read-only-but-really-write-because-relocated data, GNU_RELRO, which tells the dynamic linker to remove the write bit on those parts of the read-write section where relocations happened.
LLD (the new linker from the LLVM project) actually does exactly this; by default, it creates a separate read-only segment, and you have to pass --no-rosegment explicitly if you don't want this.
Amusingly enough, I ran into some issues because of this; e.g. valgrind's symbolication was failing on LLD-linked binaries unless I used --no-rosegment. I didn't dig into it too much, but it's probably making some bad assumptions about the text section's load address. (LLD places the read-executable segment after the read-only segment, and I think valgrind was assuming that the text section would be part of the first segment.)
That's architecture-specific. On some machines there is indeed a separate "execute-only" segment in the ELF file. But on many architectures (x86) there has historically been no hardware support for that, and for compatibility reasons the standard linker output still maintains the same scheme.
It's not a lack of execute-only segment but of a read-only, no-execute segment I'm talking about. x86 has had support for that for exactly as long as it has supported no-execute, which the second LOAD segment is marked with (i.e. the one that .data goes in).
Platform support is irrelevant, systems not supporting the access flags simply gives you more access. E.g. running a modern Linux on pre-Athlon 64 CPUs will just cause all readable pages to be executable as well.
The compatibility requirement is the result of the historic lack of x86 platform support, though: x86 used not to support r-- permissions, so it put rodata in an r-x segment, so there are likely programs in the wild which accidentally rely on that, so the linker can't now tighten the rodata permissions without breaking some existing set of programs of unknown size. Whether you think that's a good tradeoff depends on your opinion on the size of that set of programs and how heavily you weight 'avoid breaking code that used to work' against 'tighten permissions for security reasons'.
It would be interesting to know if you can ask the linux linker to put rodata in its own r-- segment -- I scanned the docs but didn't see an option for it.
Depends on what you mean, the original page table entries have a R (Read/Write) flag, if not set the page is read-only. What you couldn't do was mark a page non-executable. But nevertheless the second LOAD segment is marked rw- and not rwx, so it would seem that it wasn't deemed a problem in the past having segments with unsupported permissions.
At the time when we got the NX bit it did happen that some programs broke because they expected executable data, but the security benefits were more important.
> It would be interesting to know if you can ask the linux linker to put rodata in its own r-- segment -- I scanned the docs but didn't see an option for it.
You have to write your own linker script, see e.g. [0].
By the r-- syntax I meant specifically 'readable, not writable, not executable' as distinct from rw- 'readable, writable, not executable' or r-x 'readable, not writable, executable'.
On i386 the descriptor cannot be both writable and executable at same time. But in order to support sane semantics for C, typical Unix OS (which for purposes of this discussion includes 32bit Windows) loads CS, DS and SS with different descriptor selectors that nevertheless alias to same range of linear addresses and thus essentially disable most of the MMU's protection logic and rely only on paging. And traditional 32bit i386 page table entries only have two flags: accessible at all (called "present") and writable.
The old a.out format used by BSD had a third, virtual section for uninitialized data. It was allocated at runtime but of course took no space in the object file.
It is different only in the sense that in ELF it is real segment (that even has sane ELF segment header when loaded), but in MZ, most flavours of a.out and some flavours of COFF it is just a single word quantity somewhere in the image header.
It is not really there in the ELF. It exists as a section. But sections are not used at runtime, they only exists for tools like debuggers etc. You can strip them from the binary with e.g. the sstrip[0] tool and it still works fine.
What is used at runtime is the program headers named LOAD, which specifies the segments. Here is an example:
Idx Name Size VMA LMA File off Algn
25 .bss 00000420 0000000000601040 0000000000601040 00001030 2**5
ALLOC
So note that .bss is placed at the end of the second segment (.bss at 0x601040 + 0x420 = 0x601460,
and the second segments ends in memory at 0x600e10 + 0x650 which is also 0x601460).
But note also that the file size is only 0x220, which places the end of the data mapped from the
file at 0x600e10 + 0x220 = 0x601030 which is slightly before .bss starts. So what happens is
that the information about .bss is actually described by setting the memory size of the LOAD
segment bigger than the file size, the dynamic linker will then fill the rest with zeroes.
I stand corrected about real linker behavior :) In fact merging all RW sections into one big segment with .bss at the end makes perfect sense.
My point was essentially that for ELF this is not some kind of kludgeish special case for .bdd, but general feature that any segment can be zero extended to arbitrary size larger than what is contained in the executable image (although on sane platforms it is not useful for anything but .bss)