5

I'm trying to safely zero a std::array in a class destructor. From safely, I mean I want to be sure that compiler never optimize this zeroing. Here is what I came with:

template<size_t SZ>
struct Buf {
    ~Buf() {
        auto ptr = static_cast<volatile uint8_t*>(buf_.data());
        std::fill(ptr, ptr + buf_.size(), 0);
    }

    std::array<uint8_t, SZ> buf_{};
};

is this code working as expected? Will that volatile keyword prevent optimizing by compiler in any case?

Afshin
  • 8,839
  • 1
  • 18
  • 53
  • 4
    why do you want that – Raildex Jan 30 '22 at 20:07
  • 1
    @Raildex There are a lot of reasons, but most important thing is security. Assume buf is a cryptography key. You want to zero memory when object destructs to make sure no one can steal key from memory. – Afshin Jan 30 '22 at 20:10
  • 2
    To securely erase memory you need to use some platform/compiler-specific function specifically meant for that purpose. Standard C++ cannot guarantee it. Which platform/compiler are you interested in? – user17732522 Jan 30 '22 at 20:10
  • Related: [Should Member Data be Cleared/Zeroed in the Destructor?](https://stackoverflow.com/q/28398666/11082165), [Are passwords stored in memory safe?](https://security.stackexchange.com/q/29019), and [What kinds of optimizations does 'volatile' prevent in C++?](https://stackoverflow.com/q/3604569/11082165) – Brian61354270 Jan 30 '22 at 20:10
  • @user17732522 I was wondering if I can find a standard-compliant cross-platform method in C++. I know some methods in Windows API to do so, but I was wondering if I can do anything with C++ itself. – Afshin Jan 30 '22 at 20:11
  • 2
    Does this answer your question? [How-to write a password-safe class?](https://stackoverflow.com/questions/3785582/how-to-write-a-password-safe-class) – Suma Jan 30 '22 at 20:14
  • Short answer, there isn't. Long answer, if you're responsible for security, you should probably need to take some lectures on it. – Passer By Jan 30 '22 at 20:14
  • 7
    I think the C++ Standard does *not* guarantee this. And practically, if you do make sure to zero out some RAM, it's still possible the OS left a copy in swap storage. – aschepler Jan 30 '22 at 20:15
  • @Afshin then i am just doing a snapshot right before the object gets destroyed :) – Raildex Jan 30 '22 at 20:17
  • @Suma it seems the idea behind that code is that after casting to `volatile`, compiler cannot optimize zeroing memory too. I guess that's the way then. – Afshin Jan 30 '22 at 20:17
  • @Afshin `Will that volatile keyword prevent optimizing by compiler in any case?` Did it work when you tried it out? – eerorika Jan 30 '22 at 20:29
  • 2
    @eerorika in my test it did. but a test does not mean *any case*... – Afshin Jan 30 '22 at 20:33
  • @Afshin You should mention that in the question. – eerorika Jan 30 '22 at 20:33
  • @aschepler I don't think there will be any way to make sure swap is clean too, even with OS specific APIs. I think if there will be a way to make sure an allocated data does not into swap and stays in RAM (and I don't know if it is possible), safely zeroing RAM will be enough. – Afshin Jan 30 '22 at 20:38
  • 4
    The volatile cast makes sure that the machine will actually perform the write operations and zero out memory. Whether or not it will provide any security is anyone's guess because your threat is not explicitly stated, but my guess is firmy on the "no" side. – n. m. could be an AI Jan 30 '22 at 20:39
  • [this is the implementation in crypto++](https://github.com/weidai11/cryptopp/blob/47a6d46db7cbc436d1cc32e64a0f59e613030dec/misc.h#L1373) – Alan Birtles Jan 30 '22 at 20:40
  • @AlanBirtles Thanks, since crypto++ is also using this way, I guess we can say it is somehow secure enough in some cases. – Afshin Jan 30 '22 at 20:44
  • Linux has `mlock` which could help reduce risk. No guarantees, since I'm no expert on the topic and the risk isn't really defined. – aschepler Jan 30 '22 at 20:46
  • @Afshin: As of Linux-5.14 there is the `memfd_secret` syscall to allocate memory that can't be paged and also is inaccessible to the kernel. And for all intents and purposes, memory that has been locked using `mlock` or `VirtualLock` will also not see its contents paged out to swap memory (albeit there's no strict guaranteed for that). Also the x86_64 red zone is pretty much safe from being paged out. – datenwolf Jan 30 '22 at 23:08
  • If it works now, keep it and write a unit test for it (but in a smart way - not thru a pointer, to not spook the optimizer, but by scanning the entire heap for the specific pattern) – rustyx Jan 30 '22 at 23:32
  • Windows has SecureZeroMemory – Michael Chourdakis Jan 30 '22 at 23:57
  • @datenwolf I know about `VirtualLock` and `SecureZeroMemory`, but I didn't know about `mlock` or `memfd_secret` in linux. I guess I write a small small for class for this. Thanks everyone. – Afshin Jan 31 '22 at 05:53

1 Answers1

4

The C++ standard itself doesn't make explicit guarantees. It says:

[dcl.type.cv]

The semantics of an access through a volatile glvalue are implementation-defined. ...

[Note 5: volatile is a hint to the implementation to avoid aggressive optimization involving the object because the value of the object might be changed by means undetectable by an implementation. Furthermore, for some implementations, volatile might indicate that special hardware instructions are required to access the object. See [intro.execution] for detailed semantics. In general, the semantics of volatile are intended to be the same in C++ as they are in C. — end note]

Despite the lack of guarantees by the C++ standard, over-writing the memory through a pointer to volatile is one way that some crypto libraries clear memory - at least as a fallback when system specific function isn't available.

P.S. I recommend using const_cast instead, in order to avoid accidentally casting to a different type rather than differently qualified same type:

auto ptr = const_cast<volatile std::uint8_t*>(buf_.data());

Implicit conversion also works:

volatile std::uint8_t* ptr = buf_.data();

System specific functions for this purpose are SecureZeroMemory in windows and explicit_bzero in some BSDs and glibc.

The C11 standard has an optional function memset_s for this purpose and it may be available for you in C++ too but isn't of course guaranteed to be available.

There is a proposal P1315 to introduce similar function to the C++ standard.


Note that secure erasure is not the only consideration that has to be taken to minimise possibility of leaking sensitive data. For example, operating system may swap the memory onto permanent storage unless instructed to not do so. There's no standard way to make such instruction in C++. There's mlock in POSIX and VirtualLock in windows.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • `memset_s` doesn't appear to guarantee anything related to this. – aschepler Jan 30 '22 at 20:53
  • 2
    @aschepler Why would you not consider `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.` to be a guarantee related to this? – eerorika Jan 30 '22 at 20:58
  • @eerorika it is funny that cppref states that one option is `std::fill` with volatile pointers in notes here: https://en.cppreference.com/w/cpp/string/byte/memset – Afshin Jan 30 '22 at 21:02
  • 1
    Oops, I looked at just the function description in https://en.cppreference.com/w/c/string/byte/memset which doesn't make it clear. The strict evaluation is explained later under Notes. – aschepler Jan 30 '22 at 21:09