This is an archive of the discontinued LLVM Phabricator instance.

[sanitizer] Re-introduce kUseSeparateSizeClassForBatch for the 32-bit Primary
ClosedPublic

Authored by cryptoad on Aug 23 2017, 3:30 PM.

Details

Summary

Currently TransferBatch are located within the same memory regions as
"regular" chunks. This is not ideal for security: they make for an interesting
target to overwrite, and are not protected by the frontend (namely, Scudo).

To solve this, we re-introduce kUseSeparateSizeClassForBatch for the 32-bit
Primary allowing for TransferBatch to end up in their own memory region.
Currently only Scudo would use this new feature, the default behavior remains
unchanged. The separate kBatchClassID was used for a brief period of time
previously but removed when the 64-bit ended up using the "free array".

Event Timeline

cryptoad created this revision.Aug 23 2017, 3:30 PM
alekseyshl added inline comments.Aug 24 2017, 11:13 AM
lib/sanitizer_common/sanitizer_allocator_local_cache.h
132

Do we really need all these types and consts to be public?

232

Add {} around in and else parts.

lib/sanitizer_common/sanitizer_allocator_primary32.h
103–104

Shouldn't it be also guarded by kUseSeparateSizeClassForBatch flag?

lib/sanitizer_common/sanitizer_allocator_size_class_map.h
137

So now we have two unused classes at the top? Why kLargestClassID was not adjusted then? Many other places depend on kNumClasses, including asan_allocator, I'm feeling not that easy about this change, how these places are affected?

150

Wouldn't these checks affect all other allocator's performance? From your earlier measurements, I recall that Size, ClassID and MaxCachedHint are pretty hot functions.

206

Why this changed? Everywhere else iterating classes we use kNumClasses.

cryptoad added inline comments.Aug 24 2017, 11:55 AM
lib/sanitizer_common/sanitizer_allocator_local_cache.h
132

That's an interesting point.
You can see below that the private: was commented out. I am not sure when or why, but it's all public.
Do you want me to try and reintroduce it an see what we can put in there?

232

Will do.

lib/sanitizer_common/sanitizer_allocator_primary32.h
103–104

In my opinion, even when not using a separate size class for batches, this should return kBatchSize for consistency.

lib/sanitizer_common/sanitizer_allocator_size_class_map.h
137

kLargestClassID was indeed not adjusted after the removal of kBatchClassID. It should have been kNumClasses - 1, but was left - 2.
It ended up not having any impact as it was only used in a test (SizeClassAllocatorGetBlockBeginStress), which means we didn't stress the last class.
In the end there are 2 "unused" classes when not using separate batches: 0 and kBatchClassID.
I tried to repurpose class 0, but ended up hitting some snags (for example in a ByteMap 0 means not-ours as opposed to being region 0), so I went back to getting an extra class past the end.

Here is a sample output of a SizeClassMap::Print():

c00 => s: 0 diff: +0 00% l 0 cached: 0 0; id 0
c01 => s: 16 diff: +16 00% l 4 cached: 64 1024; id 1
c02 => s: 32 diff: +16 100% l 5 cached: 64 2048; id 2
c03 => s: 48 diff: +16 50% l 5 cached: 64 3072; id 3
...
c50 => s: 98304 diff: +16384 20% l 16 cached: 1 98304; id 50
c51 => s: 114688 diff: +16384 16% l 16 cached: 1 114688; id 51

c52 => s: 131072 diff: +16384 14% l 17 cached: 1 131072; id 52

c53 => s: 512 diff: +0 00% l 0 cached: 16 8192; id 20

I didn't hit any snag anywhere, all tests pass for check-asan, check-sanitizer, etc.

It's still fair game to loop on kNumClasses anywhere, as long as there is no assumption that kNumClasses - 1 is the largest class.
As far as I can tell, no code anywhere is making that assumption.

150

This is indeed the case, but we cached the hottest ones in the per-class arrays (in class_size and max_count), which is done on cache initialization (once per thread). The overhead of the other calls shouldn't be a problem.

206

The issue is that kBatchClassID doesn't follow the same logic as the other class: its size is not larger than the one of the previous class, and it is meant to allocate only a single size. Meaning the test checking that size minus 1 still belongs to that class wouldn't pass.
In that sense, the validation code doesn't make sense for that particular class.

alekseyshl edited edge metadata.Aug 24 2017, 2:37 PM

Ok, overall, I have no better idea how to implement it, so let's iron out minor details and get it in.

lib/sanitizer_common/sanitizer_allocator_local_cache.h
132

Yes, if it is not too much of a burden, it would be great to make all non-API stuff private.

lib/sanitizer_common/sanitizer_allocator_primary32.h
103–104

Maybe yes, it does not matter that much. Now I am more concerned with ClassID(ClassIdToSize(kBatchClassID)) != kBatchClassID, although I cannot point out where exactly it might be a problem.

lib/sanitizer_common/sanitizer_allocator_size_class_map.h
150

It would be great if you could run that perf test with these changes, I'm still a bit concerned.

cryptoad marked 2 inline comments as done.Aug 24 2017, 2:52 PM
cryptoad added inline comments.
lib/sanitizer_common/sanitizer_allocator_local_cache.h
132

Working on it.

lib/sanitizer_common/sanitizer_allocator_primary32.h
103–104

It does show in the SizeClassMap::Print:

c53 => s: 512 diff: +0 00% l 0 cached: 16 8192; id 20

53 is the kBatchClassID, and the ClassID(ClassIdToSize(kBatchClassID)) is 20.
I didn't think much of it, it could be solved with a special case in ClassID.

lib/sanitizer_common/sanitizer_allocator_size_class_map.h
150

Will do.

206

Revisiting this: I can loop on kNumClasses and make a special case for kBatchClassID. Validate is only a test/debug functionality anyway.

cryptoad updated this revision to Diff 112617.Aug 24 2017, 3:08 PM

Re-introducing private: in SizeClassAllocator*LocalCache.
Adding {} where requested.

cryptoad updated this revision to Diff 112618.Aug 24 2017, 3:24 PM
cryptoad marked 4 inline comments as done.

Making kNumClasses more consistent between the SizeClassAllocator*LocalCache.

alekseyshl added inline comments.Aug 24 2017, 4:01 PM
lib/sanitizer_common/sanitizer_allocator_primary32.h
103–104

How exactly can it be solved there? It does not know what these 8KB of memory are going to be used for, for the transfer batch or is it a regular user allocation. Anyway, it's more of a hypothetical problem at this point.

lib/sanitizer_common/sanitizer_allocator_size_class_map.h
206

That would be a better way to handle it, please do.

cryptoad updated this revision to Diff 112702.Aug 25 2017, 8:55 AM
cryptoad marked 4 inline comments as done.

Changing SizeClassMap::Validate to loop on kNumClasses.
Performance testing is the last item remaining.

Latest piece of information: this patch doesn't seem to have a meaningful impact on performances, whether it be single or multi threaded for the 64-bit allocator and 32-bit allocator without batch separation.
For the 32-bit allocator with batch separation enabled (eg: Scudo), this translates to a performance loss of about 2% for a single threaded pure allocation benchmark. A bit less on multi-threaded benchmarks.
This is expected as we are now allocating batches instead of re-using a chunk, but is required to keep the 32-bit allocator safe.
This also translate in a 1MB RSS increase due to the new region allocated for the batches. This is not expected to grow much higher as a single 1MB region can hold 2048 batches (for 512bytes batches).

alekseyshl accepted this revision.Aug 25 2017, 2:11 PM
This revision is now accepted and ready to land.Aug 25 2017, 2:11 PM
cryptoad closed this revision.Aug 28 2017, 8:21 AM