Skip to content

Wipe memory reliably in Memory::Zero#1816

Open
damianrickard wants to merge 1 commit into
veracrypt:masterfrom
damianrickard:harden/memory-zero-wipe
Open

Wipe memory reliably in Memory::Zero#1816
damianrickard wants to merge 1 commit into
veracrypt:masterfrom
damianrickard:harden/memory-zero-wipe

Conversation

@damianrickard

Copy link
Copy Markdown
Contributor

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.

@idrassi

idrassi commented Jul 8, 2026

Copy link
Copy Markdown
Member

Thank you for looking into this.

I can't merge this PR as-is. Memory::Zero is a generic zero initialization primitive, not only a secret wiping primitive. It is used in several contexts where the intent is normal zero-fill, including non secret structures and larger working buffers.

In particular, the statement that this is not on a bulk path is not correct. During non quick volume creation, src/Core/VolumeCreator.cpp calls outputBuffer.Zero on a File::GetOptimalWriteSize buffer before encryption and writing. Changing Memory::Zero globally to burn would put the volatile byte-wise wipe loop in a repeated large buffer path and can have a measurable performance impact.

For me, the right approach is to introduce a dedicated secure erasure API, e.g. Memory::Erase / Memory::SecureErase, and then use it explicitly where the data is sensitive. BufferPtr::Erase should use that secure erasure primitive, while generic Zero should remain normal zero initialization. Other call sites should then be reviewed case by case.

So I agree with the goal, but not with changing Memory::Zero globally.

Comment thread src/Platform/Memory.cpp
// 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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@damianrickard damianrickard force-pushed the harden/memory-zero-wipe branch from ca467fa to 4539200 Compare July 8, 2026 16:18
@damianrickard

Copy link
Copy Markdown
Contributor Author

Thanks — that's a fair objection, and you're right that the "not on a bulk path" claim was wrong: VolumeCreator's outputBuffer.Zero() before each encrypt pass goes through Memory::Zero.

I've reworked this along the lines you suggested. Memory::Zero is back to a plain memset for generic zero-initialization. There's a new dedicated Memory::SecureErase (the same volatile-write burn loop Buffer::Erase already used), and both BufferPtr::Erase and Buffer::Erase now go through it.

Going through the call sites case by case, this turned out to fix a real gap rather than just harden one: every BufferPtr::Erase() caller is wiping secrets — typed passwords (TextUserInterface, VolumePasswordPanel) and security-token keyfile data (TextUserInterface, SecurityTokenKeyfilesDialog) — but BufferPtr::Erase was an alias for Zero()/memset, so those wipes were the ones actually at risk of being elided. They now reach burn in every case. The remaining .Zero() sites are all genuine zero-init (header buffers before serialization, keyfile-pool padding, RNG self-test reset, FAT scratch sectors), so they stay on memset and the bulk paths are unaffected.

Rebased onto current master. Builds clean (C++03 and C++11); --text --test passes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants