This is an archive of the discontinued LLVM Phabricator instance.

[esan] EfficiencySanitizer shadow memory
ClosedPublic

Authored by bruening on May 4 2016, 7:59 AM.

Details

Summary

Adds shadow memory mapping support common to all tools to the new
Efficiencysanitizer ("esan") family of tools. This includes:

+ Shadow memory layout and mapping support for 64-bit Linux for any

power-of-2 scale-down (1x, 2x, 4x, 8x, 16x, etc.) that ensures that
shadow(shadow(address)) does not overlap shadow or application
memory.

+ Mmap interception to ensure the application does not map on top of

our shadow memory.

+ Init-time sanity checks for shadow regions.

+ A test of the mmap conflict mechanism.

Diff Detail

Repository
rL LLVM

Event Timeline

bruening updated this revision to Diff 56152.May 4 2016, 7:59 AM
bruening retitled this revision from to [esan] EfficiencySanitizer shadow memory.
bruening updated this object.
bruening added a reviewer: aizatsky.
bruening added subscribers: zhaoqin, kcc, eugenis and 2 others.
filcab added a subscriber: filcab.May 4 2016, 10:58 AM

The OS-related stuff like fix_mmap_addr should be in a esan_posix.cc file, which can include external headers (like the ones in asan do).
That way you don't need to hard-code values or anything, you just include the required headers.

Can you explain a bit better why there's so much complexity around the mapping? I have some inline comments about it.

Thank you,

Filipe

lib/esan/esan.cpp
68 ↗(On Diff #56152)

Don't abbreviate "offset".

72 ↗(On Diff #56152)

Why loop over the regions and mapping them to different shadow regions? Why not do the same as ASan and use the system's virtual memory to your advantage, by simply mapping a huge region which covers everything you want, especially since your shadow is so much smaller?

73 ↗(On Diff #56152)

ShadowStart

74 ↗(On Diff #56152)

Why?
Do we want the byte after the last shadow byte and this may not be the same as the shadow of the byte past the last byte of the region? When does that happen?

75 ↗(On Diff #56152)

[start,end) is a better way to show half-open ranges (like what ASan does when it shows the mapping).

lib/esan/esan_interceptors.cpp
26 ↗(On Diff #56152)

No.
This should, at the very least, be under an #if SANITIZER_LINUX, since it's clearly Linux only.
I think we might end up not needing this, either.

425 ↗(On Diff #56152)

What if addr+sz gets to the next region?

lib/esan/esan_shadow.h
47 ↗(On Diff #56152)

Don't abbreviate "offset" (same for next line).

50 ↗(On Diff #56152)

What is that strategy, and why the tweaks?

54 ↗(On Diff #56152)

Is this better than having a simple offset? I'm unclear on what this complexity is trying to accomplish.

77 ↗(On Diff #56152)

ShadowMapping

86 ↗(On Diff #56152)

An array of regions would be better than a bunch of constants.

90 ↗(On Diff #56152)

Scale
Offset
(We should just mention it's "shadow"-related on the type and be done with it)

91 ↗(On Diff #56152)

init(uptr ShadowScale)

92 ↗(On Diff #56152)

OffsetArray

105 ↗(On Diff #56152)

I would just call it Mapping, unless it could be confused with anything, in which case the current name is ok.

157 ↗(On Diff #56152)

Should we #ifndef NDEBUG, to make sure it's only there in debug versions?

aizatsky added inline comments.May 4 2016, 10:59 AM
lib/esan/esan.cpp
66 ↗(On Diff #56152)

Looks like mapping won't be initialized for other tools.

lib/esan/esan_interceptors.cpp
425 ↗(On Diff #56152)

theoretically addr & addr + sz - 1 could be in app memory, but in different regions. Do we worry about such scenario?

lib/esan/esan_shadow.h
39 ↗(On Diff #56152)

Any reference? Paper/source code/github project.

47 ↗(On Diff #56152)

Mask choice surprises me. Why 3 f's?

51 ↗(On Diff #56152)

Why can't we use the same shadow offset for all scales? You'll need 2 loads now to calculate shadow address. Why not:

(app & mask) >> scale + offs?

bruening marked 14 inline comments as done.May 4 2016, 8:31 PM

The OS-related stuff like fix_mmap_addr should be in a esan_posix.cc file, which can include external headers (like the ones in asan do).
That way you don't need to hard-code values or anything, you just include the required headers.

Making it #if LINUX should be sufficient. This matches what tsan does.

Can you explain a bit better why there's so much complexity around the mapping? I have some inline comments about it.

Please see the commit message and esan_shadow.h comments: this supports multiple scales, as different tools need different shadow configurations. I would argue that it is not much more complex than other mappings: it simply varies the offset for the different scales.

lib/esan/esan.cpp
66 ↗(On Diff #56152)

Other tools need to set their own mapping, but I think your point is that it's too easy to accidentally be uninitialized, so I will add an else case here.

72 ↗(On Diff #56152)

ASan also has multiple shadow regions. Any direct-map shadow approach has to use different shadow regions for the different 64-bit app regions. Our shadow mapping supports both larger and smaller scales than ASan (please see the commit message and the esan_shadow.h comments).

74 ↗(On Diff #56152)

The mapping expression does not work for the excluded endpoint.

75 ↗(On Diff #56152)

Address ranges are pretty much always open-ended. It looks a little cluttered with the brackets there next to the parentheses for the size.

lib/esan/esan_interceptors.cpp
425 ↗(On Diff #56152)

That would be a pretty big mmap. Tsan does not check for it. I suppose it's not hard to check.

lib/esan/esan_shadow.h
47 ↗(On Diff #56152)

To distinguish PIE, lib/stack, and low app addresses, which differ by that 1st nibble of the mask.

50 ↗(On Diff #56152)

That is the entirety of it, to shift the offs by the scale, but it does not work for two of the scales and has to be tweaked to avoid failing the double-shadow test.

51 ↗(On Diff #56152)

It fails the double-shadow test for scales 1 and 2.

There are not two loads at execution time for the fastpath in the compiler instrumentation.

77 ↗(On Diff #56152)

That's the var name.

86 ↗(On Diff #56152)

See the comment above.

90 ↗(On Diff #56152)

The other constants here have App in them, this distinguishes.

157 ↗(On Diff #56152)

Really it's more that it's assumed to not be on a critical path.

bruening updated this revision to Diff 56237.May 4 2016, 8:31 PM
bruening marked 6 inline comments as done.

Addresses reviewer comments + better separates the vsyscall shadow

Addresses the reviewer comments.

Adds better handling of the vsyscall vs library shadow overlap.

filcab added a comment.May 5 2016, 1:31 AM

Making it #if LINUX should be sufficient. This matches what tsan does.

It's still very Linux-specific, and we don't need to over-specialize the sanitizer when stopping at POSIX is more than enough.
TSan does this and look at the mess of #if it has for things that are called only once. Especially bad since many of them are not performance-critical (e.g: SIG* definitions), and we end up making a mess, but gaining next to nothing.

Sometimes we end up with #if SANITIZER_LINUX (or another OS) inside a function because it's performance-sensitive, or extracting all the information needed for the Linux-specific code would be a big refactoring and not worth it. This is not one of those cases.

... it is not much more complex than other mappings: it simply varies the offset...

That is more complex, though. Both me and Mike asked about this, so it seems like a better explanation would be nice.
I am not seeing a reason for the different offsets. I understand it's all a simple transformation to get each offset, but the "why" still eludes me (can't talk for Mike, though).

lib/esan/esan.cpp
72 ↗(On Diff #56237)

Not really. ASan has one special case where there's more than a shadow region.
All the others have one shadow region (divided into low shadow, high shadow, and shadow gap).

You mention that "Any direct-map shadow approach has to use different shadow regions for the different 64-bit app regions", what's missing is the "why?".

I might be missing something, but like I said, I don't see a reason to have all this complexity.
Having a single, big, shadow region would also help with not needing to mask addresses in the memToShadow transformation. This also avoids having problems in the appToShadow(appToShadow(x)) transformation, as that hits the shadow gap, which has no read nor write permissions.

74 ↗(On Diff #56237)

Right, you have white-listed some app memory, and we end up with appToShadow(AppEnd) not falling into shadow memory because there are blanks in the address space that aren't "app" nor "shadow", is that so?

lib/esan/esan_interceptors.cpp
427 ↗(On Diff #56237)

fixMmapAddr

438 ↗(On Diff #56237)

[%p, %p)

lib/esan/esan_shadow.h
18 ↗(On Diff #56237)

`#include <sanitizer_common/sanitizer_platform.h>
#if SANITIZER_WORDSIZE !=64
#error ...
`

24 ↗(On Diff #56237)

#if SANITIZER_LINUX

25 ↗(On Diff #56237)

Linux. We've already seen differences in mapping for Linux and FreeBSD, so I wouldn't lump them together unless an actual port is in the way and the same mapping works.

48 ↗(On Diff #56237)

Nothing in this patch mentions "why" supporting different offsets is a priority. Is it not possible to do just the one offset (with possibly varying scales) on Linux due to virtual address space fragmentation?
Are there performance concerns which we can address when we scale down the shadow, but can't when the shadow is very large?

I really want to avoid this code being more complex than it needs to. Especially since it will likely change among different OS.

78 ↗(On Diff #56237)

Mapping is more ambiguous, and it's a top-level (in ESan) name.
ShadowMapping is explicitly for a shadow memory mapping. We only have the one class and is easily reachable everywhere in ESan. It should be more explicit.

bruening marked 5 inline comments as done.May 5 2016, 8:21 AM

That is more complex, though. Both me and Mike asked about this, so it seems like a better explanation would be nice.

I think you are overlooking the very similar complexities in the other sanitizers. ASan has to use a different offset to handle PIE versus the offset it uses for non-PIE, which is a complexity I do not want. Each of the other sanitizers has its own, different, set of offsets. Our mapping handles PIE and vsyscall and handles multiple scales to support multiple tools, all with one formula with two instances of tweaked offset.

I am not seeing a reason for the different offsets. I understand it's all a simple transformation to get each offset, but the "why" still eludes me (can't talk for Mike, though).

This is explained in esan_shadow.h: for two scale cases, the base offset produces a shadow(shadow) conflict. Tweaking it satisfies the shadow(shadow) property, helping to avoid wild accesses by the app clobbering a tool's own metadata. It makes the tools more robust.

lib/esan/esan.cpp
72 ↗(On Diff #56237)

Low shadow and high shadow are two different regions. There is not a third region for PIE in that same scheme because ASan has to use a different offset when PIE is present, resulting in a completely different mapping. There is as much complexity there as in our mapping here, yet ours handles multiples scales and it handles PIE and the vsyscall page.

Look at the application memory ranges: they are distinct and far apart. Using a linear mapping that preserves their identities places them into distinct shadow ranges.

74 ↗(On Diff #56237)

Anything outside of the listed regions is disallowed.

lib/esan/esan_shadow.h
48 ↗(On Diff #56237)

Please read the full comment. This is explained. Pasting:

56              // For other scales, the
57		​// offset is shifted left by the scale, except for scales of 1 and 2 where
58		​// it must be tweaked in order to pass the double-shadow test
59		​// (see the "shadow(shadow)" comments below):"

No, it is not possible to use one offset.

Again, there is as much complexity in the other sanitizer shadow mappings. They have special offsets as well, and they all have to be tweaked on different platforms.

If you can come up with a linear mapping involving a scale and an add that uses a single offset for every scale and avoids colliding with anything (including shadow(shadow)), please let me know.

bruening updated this revision to Diff 56293.May 5 2016, 8:21 AM

Addresses reviewer comments

bruening added inline comments.May 5 2016, 9:02 AM
lib/esan/esan_shadow.h
78 ↗(On Diff #56237)

I'm seeing requests for s/ShadowMapping/Mapping/ here, but later s/ShadowMapping/Mapping/ for the global variable; similarly, s/ShadowScale/Scale/ for the field, yet later s/Scale/ShadowScale/ for the parameter. The global variable is the one referenced the most externally and it is ShadowMapping. "Mapping" is used throughout the sanitizers: asan_mapping.h, AsanMappingProfile[], ASAN_FIXED_MAPPING, tsan's struct Mapping, enum MappingType.

filcab added a comment.May 6 2016, 5:52 AM

Can you hide some more complexity of the Mapping?
This almost looks like it will have to be fully rewritten for any port (with all the different App spaces, etc, which will very likely not map to other OS).
isAppMem, isShadowMem, and other functions should be more general than they are here.

lib/esan/esan_shadow.h
79 ↗(On Diff #56293)

The rationale was that the type *should* already explicitly state that it was shadow related. Added to that, the struct members, since it's a struct used to describe a shadow mapping, shouldn't have "shadow" in their name, since that struct is "all about shadow memory". A bit like llvm's struct ShadowMapping, in AddressSanitizer.cpp. The ones where I asked to add shadow (other than the class name) were simply a way to avoid clashes in names.
AsanMappingProfile is a recent addition and it ends up being the profile for asan_mapping.h, so it makes sense to me to "keep" the file name, transformed to a proper variable name.

But this is only bike-shedding on names, so if you feel strongly, I won't push back on this any more. Except for the initMapping method in the Mapping class. No sense repeating Mapping all the time.

87 ↗(On Diff #56293)

Has this been measured?
We'll get huge 64-bit mov instructions vs a few loads with high locality.
I would prefer readibility over "potential performance" unless this has been shown to have a good performance impact.

bruening updated this revision to Diff 56447.May 6 2016, 12:26 PM

Rewrite to use an array

aizatsky added inline comments.May 9 2016, 4:13 PM
lib/esan/esan_shadow.h
79 ↗(On Diff #56447)

The protection from this kind of wild access adds significant complexity to the mapping scheme. Is it really important for esan?

I can imagine this being important for asan, even though it seems to ignore this issue atm. Do we really need this complexity?

bruening added inline comments.May 9 2016, 8:25 PM
lib/esan/esan_shadow.h
79 ↗(On Diff #56447)

IMHO this is not adding much complexity: the offset for two of the scale values is tweaked slightly from the formula. It makes the mapping more robust and has no performance cost in the fastpath. This general mapping could potentially be used for other non-efficiency sanitizers in the future. I don't see much value in removing a robustness feature from the mapping.

ASan is not ignoring this issue. ASan's mapping makes any
shadow(shadow(x)) end up in the shadow gap, which is protected against
reading and writing.

Filipe

ASan is not ignoring this issue. ASan's mapping makes any
shadow(shadow(x)) end up in the shadow gap, which is protected against
reading and writing.

Yes, and FTR I was the one who pointed this out and had it added to asan back in its early days, as its original shadow mapping did not have this feature.

If there are no other issues I would like to move forward.

filcab accepted this revision.May 11 2016, 7:31 AM
filcab added a reviewer: filcab.

LGTM.

This revision is now accepted and ready to land.May 11 2016, 7:31 AM
This revision was automatically updated to reflect the committed changes.