我在写一个叫 vault 的小型 C 凭据存储库,在 WSL 里构建。vault_release() 的作用是先擦除密钥再释放,代码如下
void vault_release(vault_entry *e)
{
if (!e)
return;
memset(e->secret, 0, e->secret_len);
free(e->secret);
e->secret = NULL;
free(e);
}
测试文件 tests/wipe_check.c 会先保存密钥指针,调用 vault_release(),然后从第 16 个字节开始读已经释放的内存块。对,这是 UB。这只是个用完就扔的测试,我只想看看内存里还剩什么。别拿这个来教育我。
直接 make check 能通过。make release-check 会先 clean,再执行 make check OPT=-O2,这个就失败了。前 16 个字节之后的内容仍然是原来的密钥,只有前 16 个字节看起来被改写了。反正测试里我已经跳过那 16 个字节了,因为 glibc 会在那里放自己的管理信息。
memset 明明就覆盖了整个长度,这个函数我已经看了三遍。我的普通 target 按理说应该已经在优化了,毕竟那是默认行为,所以我不明白同一份源码怎么会有两种结果。要么是 glibc 的 free() 会往块里写东西,要么是我的 -O2 target 构建出来的东西和普通 target 不一样。这两种解释我都觉得说不通。
生成的汇编我还没看。我到底漏了什么?
从英语翻译·查看原文I'm working on a small C credential holder called vault, built in WSL. vault_release() is supposed to wipe the secret and then free it:
void vault_release(vault_entry *e)
{
if (!e)
return;
memset(e->secret, 0, e->secret_len);
free(e->secret);
e->secret = NULL;
free(e);
}
The test, tests/wipe_check.c, saves the secret pointer, calls vault_release(), then reads the freed block from byte 16 on. Yes, that's UB. It's a throwaway test and I only want to see what's left in memory. Don't lecture me about it.
Plain make check passes. make release-check does a clean and then make check OPT=-O2, and that one fails. The bytes after the first 16 are still the original secret, and only the first 16 look clobbered. I'm skipping those 16 in the test anyway, since glibc puts its own bookkeeping there.
The memset is right there on the whole length, and I've read this function three times. My plain target should already be optimizing, since that's the default, so I don't see how the same source gives two results. Either glibc's free() writes stuff back into the block, or my -O2 target builds something different from the plain one. Neither explanation really makes sense to me.
I haven't looked at the generated assembly yet. What am I missing here?