Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Do compilers optimize memset in destructor?

Given a struct like:

struct CryptoKey {
    std::vector<unsigned char> key;
    ~CryptoKey() { memset(key.data(),0,key.size()); }
};

The compiler is entitled to eliminate the call to memset because this will save time, and no program with defined behaviour can tell the difference. (Given that the variable key will cease to exist once the destructor returns.)

Nevertheless, code like this is useful in cryptographic applications, because the less time that a secret is stored in memory, the less chance an attacker has to extract it. (The memset does not provide security, but it does provide "defence in depth".)

My question is, which real compilers actually do eliminate such memset calls (obviously, with optimization turned on)?

like image 642
Martin Bonner supports Monica Avatar asked Jan 05 '23 19:01

Martin Bonner supports Monica


2 Answers

Perhaps it is better to say that a good compiler would attempt to eliminate the memset call and a developer should not rely on differences of compiler implementation to avoid this optimisation. These compilers typically have secure alternatives that will not be optimised.

Secure version of memset

C11 introduces memset_s which one of the characteristics is that is will not be optimised out.

Unlike memset, any call to the memset_s function shall be evaluated strictly according to the rules of the abstract machine as described in (5.1.2.3). That is, any call to the memset_s function shall assume that the memory indicated by s and n may be accessible in the future and thus must contain the values indicated by c.

Windows specific

On windows there are other choices. SecureZeroMemory or using a #pragma optimize pragma to turn off optimisation.

Common sub-expression optimisations

There is a broader issue with cryptographic safety: compilers are within their rights to copy buffers for optimisation reasons. Zeroing may not remove all copies, the compiler may have applied optimisations that copy the heap to the stack to eliminate common sub-expressions. So besides avoiding optimising out the zeroing, care should be taken that the compiler isn't inserting additional copies.

like image 120
andygavin Avatar answered Jan 11 '23 10:01

andygavin


The problem for optimizers here is that your memset isn't writing to a member at all. Yes, key will cease to exist, but not so key.data. That memory will be returned to std::allocator. And std::allocator will very likely read adjacent memory to determine the memory block from which key.data came. Typical implementations store such data in the header of allocated blocks, i.e. at negative offsets. It's not unlikely that the header will be updated to reflect the block is free, or to coalesce the free block with other free blocks.

This may even be inlined, so the optimizer sees one function doing a memset and then the header access. It would be unreasonable to expect that the optimizer can figure out the memset is harmless. For all it knows, the allocator may be keeping a pool of zeroed blocks.

like image 39
MSalters Avatar answered Jan 11 '23 10:01

MSalters