Wipe memory reliably in Memory::Zero#1816
Conversation
|
Thank you for looking into this. I can't merge this PR as-is. In particular, the statement that this is not on a bulk path is not correct. During non quick volume creation, For me, the right approach is to introduce a dedicated secure erasure API, e.g. So I agree with the goal, but not with changing |
| // buffer that is not read afterwards can be removed as a dead store, | ||
| // leaving secrets in memory. burn() is the same volatile-write wipe | ||
| // already used by Buffer::Erase(). This is not on the bulk I/O path. | ||
| burn (memory, size); |
There was a problem hiding this comment.
I don't think Memory::Zero should call burn globally. This function is a generic zero fill primitive and is used outside secret erasure contexts, including repeated large buffer paths such as non quick volume creation via outputBuffer.Zero in VolumeCreator.cpp. Please introduce a dedicated secure erasure API instead, use it from BufferPtr::Erase, and leave generic Zero as normal zero initialization.
BufferPtr::Erase() was an alias for Zero(), i.e. a plain memset(), which the compiler may remove as a dead store when the buffer is not read afterwards. Every current BufferPtr::Erase() call site wipes secrets -- typed passwords (TextUserInterface, VolumePasswordPanel) and security token keyfile data (TextUserInterface, SecurityTokenKeyfilesDialog) -- so an elided wipe can leave them in memory. Introduce a dedicated secure erasure primitive, Memory::SecureErase(), implemented with the same volatile-write burn() loop that Buffer::Erase() already uses, and route both BufferPtr::Erase() and Buffer::Erase() through it. Memory::Zero() remains a plain memset() for generic zero initialization, so bulk zero-fill paths such as VolumeCreator's outputBuffer.Zero() before encryption are unaffected. A case-by-case review of the remaining call sites found no other changes needed: all .Zero() calls in the tree are genuine zero-initialization (header buffers before serialization, keyfile pool padding, RNG self-test pool reset, FAT formatter scratch sectors, POD struct init), and all .Erase() calls are genuine wipes, which now reach burn() in every case.
ca467fa to
4539200
Compare
|
Thanks — that's a fair objection, and you're right that the "not on a bulk path" claim was wrong: I've reworked this along the lines you suggested. Going through the call sites case by case, this turned out to fix a real gap rather than just harden one: every Rebased onto current master. Builds clean (C++03 and C++11); |
|
Thanks for reworking the PR. First, I initially suggested introducing The affected sensitive call sites were changed to expand Reintroducing Please therefore:
Second, the review of the current callers exposed an exception handling gap in the token keyfile cleanup. In four token keyfile paths, the cleanup guard is installed only after the sensitive vector has been populated:
If either operation throws after allocating or partially filling the vector, the cleanup guard is never constructed. The vector is then released without wiping its contents. Please install the cleanup guard immediately after declaring or allocating the vector. The guard should reference the vector itself instead of capturing a BufferPtr containing its current address. For example: For the import paths, use the same pattern immediately after constructing the sized vector and before calling Referencing the vector ensures that the cleanup uses its current storage if the vector was reallocated. After a successful The cleanup guards for the password arrays are already installed before the sensitive operations and don't require changes. Finally, please update the PR title and description. They still describe changing Memory::Zero, while the revised implementation correctly leaves Memory::Zero unchanged. The commit message should also avoid claiming that every cleanup path is covered until the exception handling issue described above has been addressed. |
Memory::Zero() used a plain memset(), which the compiler may remove as a dead store when the buffer is not read afterwards. Memory::Zero backs BufferPtr::Erase() and BufferPtr::Zero(), which are used to clear key material and other secrets, so an elided memset can leave secrets in memory.
Use burn() instead — the same volatile-write wipe already used by Buffer::Erase(). Memory::Zero is not called on the bulk encryption/I/O path, so this has no throughput impact.