memset(key, 0, len) followed by free(key) is not a wipe. Nothing reads the buffer again, so the store is dead, and the standard lets the compiler drop it. 5.1.2.3 says so in one sentence. "An actual implementation need not evaluate part of an expression if it can deduce that its value is not used and that no needed side effects are produced".
void drop_key(unsigned char *key, size_t len) {
memset(key, 0, len);
free(key);
}
clang -O2 for x86-64 Linux turns that into a lone jmp free@PLT. The memset is gone. Read the source and you see a wipe. The binary never had one.
C11 had the right words in Annex K. memset_s "shall be evaluated strictly according to the rules of the abstract machine". Annex K is optional and glibc never shipped it. C23 puts memset_explicit in <string.h>. Its normative text says the purpose is "to make sensitive information stored in the object inaccessible". The part about the store never being elided lives in a footnote that calls it "the intention", and footnotes aren't normative.
Until your libc has it, explicit_bzero on glibc and the BSDs, SecureZeroMemory on Windows. None of them reach copies the compiler left in registers or on the stack.