This is an archive of the discontinued LLVM Phabricator instance.

[ELF] Fill the empty space in executable segments with instruction padding
ClosedPublic

Authored by phosek on May 28 2017, 1:53 AM.

Details

Summary

We want to avoid having arbitrary data in the executable segment. LLD already fills the space between sections with non-executable instruction padding, but doesn't currently fill the tail of the segment which this patch addresses.

This is especially important when the code segment is the last segment. In that case it should be aligned to the page boundary to avoid loading the non-segment parts of the ELF file at the end of the file.

Diff Detail

Repository
rL LLVM

Event Timeline

phosek created this revision.May 28 2017, 1:53 AM

I would use slightly different logic: if the last segment is PF_X, then pad it.
And can you verify that it fills the gap with instruction fill rather than generic (zero) fill?
I think this means that p_memsz of the PF_X segment will be rounded up when it's last but not when there is next (data) segment (though the next segment's p_vaddr will be rounded up).
So that's a slightly odd difference, though I think both cases are about as right as they can be.
The important thing is that in either case the padding part of the file after the actual code sections and up to the page boundary is instruction fill.

And can you verify that it fills the gap with instruction fill rather than generic (zero) fill?

It doesn't, but that's not that case even when the data segment is the last segment; the executable segment will be aligned because the data segment will be aligned but it won't be padded. Is this something LLD should do in all cases?

Petr and I had some bits of discussion offline.
I have several thoughts here after some more reflection.

We've been talking about p_memsz and that's what's mentioned in the code
here. I don't know how that field is used in the LLD code, but in pure ELF
terms it's probably clearer to talk about p_filesz. p_filesz explicitly is
the amount to be mapped from the file, and that's what we're talking about
here. In all read-only segments, p_filesz and p_memsz are expected to be
equal. So my point is just about what's most clear in how we discuss our
intent.

First, I'm not sure any p_memsz/p_filesz changes like this should be done
at all when there is a linker script SECTIONS clause. There should
probably be a test that the linker script case gets exactly the not-aligned
segment sizes indicated by its SECTIONS (with or without a PHDRS clause).

Now, on to the more arcane issues.

First, some background. In the ELF world, there are two page sizes to
think about:

There is the actual runtime page size, the granularity of mapping in the
address space. The linker doesn't presume to know this ahead of time. The
linker has a guess about what this size is likely to be, which it calls the
common paeg size. (This size is controlled in the linker by the -z
common-page-size switch. In linker script expressions,
CONSTANT(COMMONPAGESIZE) gives this value.)

Then there is the ABI page size, also called the maximum page size. This
is the maximum value the runtime page size might actually take at runtime
that can actually work with ELF files built using this value. It's the
minimum value that can appear in the p_align field of PT_LOAD segments,
specifically for the purpose of the p_vaddr % p_align == p_offset % p_align
rule. (This size is controlled in the linker by the -z max-page-size
switch, with a default theoretically taken from some ABI document, hence
the name. In linker script expressions, CONSTANT(MAXPAGESIZE) gives this
value.)

Essential layout considerations at link time only pay attention to the ABI
page size, since at runtime that might be the smallest actual size. Gold's
sole use of the "common page size" value (aside from linker script support,
of course) is a layout optimization it does when doing a layout where each
segment's start address is *not* aligned to the ABI page size. (AIUI, LLD
always aligns segment start addresses to the ABI page size, so there is no
equivalent situation where LLD's current behavior might want to use the
"common page size" for anything.)

For Fuchsia we have been using 4kb as the ABI page size so far (still might
change in the future, since our ABI is not permanent yet). For Linux
x86-64, the ABI document says 64kb but BFD ld actually uses 2mb and Gold
actually uses 4kb and has been in fairly widespread use for a few years, so
the de facto ABI page size is really 4kb. The subtleties here are moot
when the ABI page size is the smallest plausible runtime page size as it is
today for Fuchsia and for Linux x86-64. But I'll still talk about the
general case, since that's what one should consider for the linker's
generic behavior.

So, with all that background in mind, what we ultimately want to achieve is
the runtime effect that all the bytes of every page mapped for a PF_X
segment is "valid" instructions. We're not working from any rigorous
definition of "valid" for the machine, just one that makes sense for the
linker. What I mean here is that every byte is either a byte that came
from some SHF_EXECINSTR input section (including synthesized sections like
.plt et al), or is the machine-specific fill pattern that the linker uses
for alignment padding between adjacent SHF_EXECINSTR input sections.
Specifically, it should not just be zero if that's not the most sensible
should-not-be-reached-but-is-executable instruction for the machine, and
even more important it should certainly not be the random contents of
non-allocated sections (debugging info, symbol tables), section headers, or
.shstrtab that happen to to follow the loaded segment contents in the file.

So, what matters to us for this property is what appears in the file
at [p_offset & -p_align, (p_offset + p_filesz + p_align - 1) & -p_align).
The exact p_filesz value in the header does not affect this property.

Personally, I think it's ideal if p_filesz covers only the actual input
section contents, not any linker-generated fill at the end. It just seems
the tidy way to represent the original intent of the link. But this is a
very minor consideration.

In the general case, there is a potential downside to rounding p_filesz up
to the ABI page size. That is, the case when the runtime page size is
actually smaller than the ABI page size and the true p_filesz before
rounding was not a multiple of the ABI page size. In that case you will
pay the overhead (page table entries, etc.) to map the padding pages, and
you will also make accesses to the padding pages valid at runtime
(reading/executing the fill instructions) rather than faulting at runtime.
The faulting behavior is what seems ideal for accesses to any memory beyond
the true p_filesz before rounding.

So, for these practical reasons as well as my aesthetic preference, what
I'd consider ideal is to fill any "unused" part of the file after a PF_X
segment with instruction fill rather than zero-fill, but not adjust the
p_filesz of the segment. If that's not difficult to implement, that's
great. If it is not so easy to do, then I'm certainly fine with having the
p_filesz of the PF_X segment rounded up to the ABI page size. I don't know
if anybody else has any concerns about making that the default behavior
even for ABIs where the ABI page size is greater than the "common" size.
For me it's not a significant concern, but I'd be entirely happy with
making it a linker switch (that we'd always use for Fuchsia).

When the PF_X segment is the last segment in the file (given the layout we
use, that is to say, when there is no PF_W segment), then rounding its
p_filesz up to the ABI page size seems unavoidable. That's really the only
way to properly express that the file even needs to be that long:

  • a "strip" sort of utility would be entirely reasonable in just truncating the file to the highest p_offset+p_filesz of the PT_LOAD segments, which would mean that in the runtime mapping of the segment from the file the partial page at the end of the segment would get implicit zero-fill from the filesystem.
  • other utilities might entirely reasonably expect they can add some non-allocated stuff to the file and not affect the runtime behavior at all. But adding anything to the region starting at the last segment's p_offset+p_filesz when p_filesz is not a multiple of the runtime page size makes the first partial page of non-allocated stuff visible in the runtime memory image.

Fortunately, the case where the PF_X segment is last is rare and arcane
enough that I really can't imagine that anybody is especially concerned
about the ABI vs runtime page p_filesz question specifically for this case.

So, to sum up, from my perspective the decision tree goes like this:

  1. Figure out if it's feasible to do instruction fill rather than zero fill after the PF_X segment without changing that segment's p_filesz in the general case. If yes, skip to #3. If no, proceed to #2.
  1. Make this change apply to the PF_X segment, whether or not it's last, but only when there's no linker script. Always round the PF_X segment's p_filesz up to the ABI page size. If nobody objects to that change, end of story. This change alone should be enough for Magenta's "RODSO" cases to be work in practice using no linker script (given -z rodynamic). Skip to #4.
  1. Make this change apply only if the PF_X segment is last (and still only when there's no linker script). Round the PF_X segment's p_filesz up to the ABI page size if and only if it's the last segment. This change alone should be enough for Magenta's "RODSO" cases to be work in practice using no linker script (given -z rodynamic).
  1. Make sure that the space between the original p_filesz and the rounded p_filesz in the PF_X segment is getting instruction fill rather than zero fill. If you came here from #2, then you're done! Declare victory.
  1. In the case where PF_X is not last and its p_filesz is not a multiple of the ABI page size, make sure that the space between its p_filesz and the PF_W segment is getting instruction fill rather than zero fill.

If other people have contrary views on any of what I've said, then this
will add more steps and decision points to the procedure. :-)

phosek updated this revision to Diff 102280.Jun 12 2017, 6:50 PM
phosek retitled this revision from [ELF] When the code segment is the last, align it to the page boundary to [ELF] Fill the empty space in executable segments with instruction padding.
phosek edited the summary of this revision. (Show Details)
phosek added reviewers: ruiu, rafael.

This is a working version, although I do expect that code an be cleaned up a bit more if you're fine with the logic itself.

mcgrathr accepted this revision.Jun 15 2017, 11:29 AM

LGTM

ELF/Writer.cpp
1844 ↗(On Diff #102280)

Maybe use sizeof(Target->TrapInstr) ?

This revision is now accepted and ready to land.Jun 15 2017, 11:29 AM
ruiu edited edge metadata.Jun 15 2017, 2:42 PM

Filling the empty space in an executable segment with trap instruction is reasonable, but I think there's room to simplify this patch.

IIUC, the problem is that, if size of a .text OutputSection is not a multiple of a pagesize, it's tail is not filled with trap instructions. If so, can you just adjust size of executable sections so that they don't have any gap after them?

ruiu added a comment.Jun 15 2017, 2:44 PM

By the way, if you can modify your OS's loader, why don't you make a change to your loader so that it fills the gap after executable sections with trap instructions? I believe it should solve the issue that a PF_X segment is at end of a file.

wrt modifying the program loader: code pages are demand-loaded read-only from a filesystem, so anything that requires modifying pages in memory is a non-starter.

ruiu added a comment.Jun 15 2017, 2:50 PM

Ah, got it. Then the only practical option is indeed rounding up the size of the last segment to a page size and filling the empty space with trap instructions.

wrt changing section lengths: as I mentioned in my long comment above, IMHO it best represents the intent of the link if the linker does not change section sizes, so that the shdrs in the output show what portions of the segment were actually intended to be part of the program image, i.e. came from input sections or from sections the linker synthesizes for explicit semantic reasons (such as .plt). By the same token, when there is an alignment gap between two sections in the same segment, the shdrs do not claim that the alignment padding is part of either section. The bytes that aren't part of any section are addresses that the program can never validly use--they exist in the address space at all only because of other constraints outside the programming model (such as p_offset/p_vaddr congruency or page granularity). We care about what those are filled with only to mitigate the invalid behavior, i.e. constrain the possible real-world results of jumps into the address ranges that "do not exist" in the programming model.

ruiu added a comment.Jun 15 2017, 3:01 PM

I think I understand the points you made, and I partially agree with that, but we can see it from a different perspective. We fill the gap after executable segments with trap instructions, with the clear intention that the trap instructions will be loaded to memory. So in that sense it makes sense to round up size of executable sections to a page size. Also, in practice, this code seems too large to do a simple thing we've already done for filling gaps between sections. We already have code to fill gaps in LinkerScript.cpp, and this new code seems almost the same.

In response to ruii's last comment:

I don't think it follows that any *section*'s size should be rounded up. As I just said, that perverts the intent of the link to suggest that the fill bytes are actually part of some section, which they are not.

You may have a case for saying that the *segment*'s size, i.e. p_filesz/p_memsz in the phdr, should be rounded to the page size.
However, given ELF's general statement that up to the whole p_align-aligned area *might* be visible at runtime, I don't really agree with the idea that p_filesz should represent the entire length that we think might be visible.
I think it is better that p_filesz represents the part of the file that *must* be visible at runtime for the program's intent to be correct.
If there were some system or hardware that could choose the accessibility at byte-granularity, then it would be fine and good for it to make only the portion of the file up to p_filesz accessible.

ruiu added a comment.Jun 15 2017, 3:21 PM

In response to ruii's last comment:

I don't think it follows that any *section*'s size should be rounded up. As I just said, that perverts the intent of the link to suggest that the fill bytes are actually part of some section, which they are not.

You may have a case for saying that the *segment*'s size, i.e. p_filesz/p_memsz in the phdr, should be rounded to the page size.
However, given ELF's general statement that up to the whole p_align-aligned area *might* be visible at runtime, I don't really agree with the idea that p_filesz should represent the entire length that we think might be visible.
I think it is better that p_filesz represents the part of the file that *must* be visible at runtime for the program's intent to be correct.
If there were some system or hardware that could choose the accessibility at byte-granularity, then it would be fine and good for it to make only the portion of the file up to p_filesz accessible.

If there's a hardware on which you can map a page at byte-granularity, you still want to fill the gap at end of executable sections with trap instructions, so you would round it up to page size, no? That hypothetical hardware is actually useful as an example of the point I'm trying to make. My point is that, if we write trap instructions at end of a segment with an assumption that that must be loaded to memory at runtime, there should be some segment which covers that area of the file.

No, it's the exact opposite. It's not the case that they *must* be loaded. It's the case that they *might* be loaded.

ruiu added a comment.Jun 15 2017, 3:50 PM

If it is not loaded and end of a segment is filled with zeros in memory, it is not a desired result for you, no?

The hypothetical system/hardware I'm talking about is one where only the exact range of bytes is accessible to the program at runtime. There is no notion of "what the rest of the page was filled with" because the program cannot access "the rest of the page" at all. That's what makes it meaningfully "outside the segment".

ruiu added a comment.Jun 15 2017, 4:09 PM

OK, that makes sense. But can you simplify the implementation? It seems this patch changes too many places.

I don't think that's sufficient. The problem is the case where the executable segment is the PT_LOAD segment. In that case, the symbol and the string tables will immediately follow the last segment and will get loaded. That's why we need to adjust the offset of the section following the last segment and the file size. However, I like the idea of writing the trap instructions before rather than after writing the segments, it simplifies the logic a lot. I'll try to combine it with my change and simplify it as much as possible.

The second sentence should have been: "The problem is the case where the executable segment is the *last* PT_LOAD segment."

phosek updated this revision to Diff 103132.Jun 19 2017, 6:15 PM

I've combined the two patches, PTAL. Currently, the executable segment is only filled with trap instructions when linkerscript doesn't contain SECTIONS statement, there are several linkerscript test cases that check the content of the file which would otherwise fail. LLD's semantics of FILL vs =fill is different from ld and gold (i.e. LLD treats them as same) which appears to be causing issue in that case as well.

ruiu added inline comments.Jun 19 2017, 6:25 PM
ELF/Writer.cpp
69–70 ↗(On Diff #103132)

Reverse the order.

250 ↗(On Diff #103132)

Why do you need this condition?

1666–1672 ↗(On Diff #103132)

I wouldn't do this in an exiting loop as the loop is already too complicated. Can you run another loop after everything is done?

1689 ↗(On Diff #103132)

Why do you need to check this condition?

1694–1695 ↗(On Diff #103132)

Can you just increase p_memsz so that it becomes a multiple of the page size? I know that is "not ideal" and you are trying to keep the original p_memsz, but in practice I don't care about that much. If you do that, I think you can remove the new code from assignFileOffsets.

1883–1884 ↗(On Diff #103132)

Please fix. Otherwise I expect the new code will noticeably slows down the linker.

phosek updated this revision to Diff 103447.Jun 21 2017, 1:10 PM
phosek marked 3 inline comments as done.
phosek added inline comments.
ELF/Writer.cpp
250 ↗(On Diff #103132)

When using linker script with SECTIONS commands, there's no guaranteed section alignment which makes instruction padding difficult and probably not worth it (also when using such linker script, you have a way to do instruction padding yourself with FILL() or =fill).

1689 ↗(On Diff #103132)

The same as above.

1694–1695 ↗(On Diff #103132)

That's different from what we want to achieve though. Increasing p_memsz is just going tell loader to map p_memsz sized chunk of memory (which it'll do anyway because of the page granularity), but that memory will contain !PT_LOAD part of the ELF file like the section and string table in case the executable segment is the last loadable one. What we want is to adjust the size of the file so any !PT_LOAD end up in the following page, and fill that space with traps.

We could avoid changing the p_filesz and just adjust the offset of the following section as done in assignFileOffsets. The problem is that if we use strip on this file, it'll remove the non-loadable segments at the end of the file and then trim the file to the p_filesz of the last loadable segment which will undo the padding, so that's why we're adjusting p_filesz here.

ruiu added inline comments.Jun 21 2017, 2:30 PM
ELF/Writer.cpp
1626–1629 ↗(On Diff #103447)

You can merge these two ifs.

1889–1892 ↗(On Diff #103447)

I'd define a function so that I can write

fillTrapInstr(Buf + alignDown(P.p_offset, Target->PageSize),
              alignTo(P.p_offset, Target->PageSize)
1894–1895 ↗(On Diff #103447)

Can you use alignDown?

250 ↗(On Diff #103132)

But it doesn't sound like doing it is harmful. If so, I'd do this unconditionally.

phosek updated this revision to Diff 103535.Jun 22 2017, 12:14 AM
phosek marked 3 inline comments as done.
phosek added inline comments.
ELF/Writer.cpp
1894–1895 ↗(On Diff #103447)

Yes, but I had to change the order in which we adjust the file size of the last segment so it's done after this.

250 ↗(On Diff #103132)

I don't think it's harmful, but it affects a lot of linker scripts tests because many of them check the content of the file including the space between sections which is now going to be the trap instruction instead of zeroes. Is that fine with you?

ruiu added inline comments.Jun 26 2017, 9:21 AM
ELF/Writer.cpp
250 ↗(On Diff #103132)

I'm fine if we end up have to update a lot of tests for this change.

phosek updated this revision to Diff 108972.Jul 31 2017, 12:26 PM

Rebased. There now seem to be two different versions of this patch, D33630 and D35680, we should decide which one we want to proceed with and abandon the other one.

phosek added inline comments.Jul 31 2017, 12:38 PM
ELF/Writer.cpp
250 ↗(On Diff #103132)

I've played with this a bit, but it's really difficult to make it work with SECTIONS because of the lack of alignment. I'd prefer to do it in a separate change since this might require additional changes.

ruiu accepted this revision.Jul 31 2017, 9:52 PM

LGTM

ELF/Writer.cpp
1605 ↗(On Diff #108972)

nit: remove {}

1839 ↗(On Diff #108972)

Please write a function comment saying that this function fills the first and the last page of executable segments with trap instructions instead of leaving them as zero, because even though it is not required by any standard or anything, it is in general a good thing to do for security reasons.

phosek updated this revision to Diff 109267.Aug 1 2017, 8:27 PM
phosek marked 2 inline comments as done.
This revision was automatically updated to reflect the committed changes.