Tuesday, August 14, 2018

About the C++14 sized delete operator

Alright, I am breaking a 3-year-posting-slumber here. Don't get too excited,  I am probably not going to post regularly but I will try and share some security and/or allocator related thoughts here.

One of the novelties introduced by C++14 was sized delete operators. Taking an extra size_t parameter, those are meant for efficiency purposes, allowing to avoid a potentially costly lookup of the size of a chunk, to quote N3536:
Modern memory allocators often allocate in size categories, and, for space efficiency reasons, do not store the size of the object near the object. Deallocation then requires searching for the size category store that contains the object. This search can be expensive, particularly as the search data structures are often not in memory caches.
And this is indeed the case. While someone can directly call the sized delete operator, it's usually up to the compiler to the heavy lifting, specifying the command line flag -fsized-deallocation; but it is usually enabled for -std=c++14 and above (see gcc c++ dialect options).

So what happens on the allocator side when the sized deallocation function is used? The allocator usually has fast path function that will use the size provided to look up where the chunk will end up (see tc_free_sized for tcmalloc, je_sdallocx for jemalloc). That's great, no size to compute for a given pointer, it's faster.  But it implies that the compiler gets it right all the time (or that a programmer doesn't blindly call the sized operator with a wrong size, or that a malicious user doesn't pass a mismatched pointer to a sized deallocation function), otherwise the deleted chunk ends up in the wrong bin/freelist/*, and when it's later returned to fulfill an allocation, something bad is likely to happen.

My catastrophic thinking self expected this was going to go wrong at some point, but as far as I can tell, there was nothing much in the world of exploitable bugs related to this, except for the early implementation hiccups.

ASan's allocator has an optional check for this, and so does Scudo (an allocator I work on): if the size passed to the deallocation doesn't match the one of the chunk being deallocated, kill things as something is terribly wrong somewhere (but do not trust the size passed in any case - so much for efficiency 😕).

But then a few days ago, it was pointed out that the Intel Compiler was totally messing up the sized deallocation (see the compiled code). The consequences of this are entirely dependent on the allocator being used at runtime, and it looks like for most this could just result in some wasted memory (a large chunk ending up in a smaller bin), but that likely requires some additional digging (TODO(cryptoad) I guess). Anyway, if you compiled anything with ICC 18.0.0 in C++14 mode, update your compiler and recompile your binaries!

The reporter found the issue using Scudo, and it makes me somewhat happy that the check found a meaningful justification. Anyway, if you have examples of a sized deallocation gone wrong, feel free to chime in.