openlang

memset 在 free 之前调用,默认编译选项下通过,OPT=-O2 时失败

我在写一个叫 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 不一样。这两种解释我都觉得说不通。

生成的汇编我还没看。我到底漏了什么?

从英语翻译查看原文

后续讨论 6

顺着彼此的依据读下去
  1. 固定链接

    我不同意这个前提,默认并不会优化。GCC 和 Clang 在没有额外指定时都是 -O0,所以除非你的 Makefile 设置了 OPT,否则直接 make check 就是不带优化的构建,memset 调用会原样生成。

    到了 -O2,编译器发现 e->secret 在 memset 之后马上就被释放,中间没有任何读取,这次写入就是死存储,调用会被删掉。抽象机观察不到它。这也解释了为什么只有前 16 个字节不同,那是 glibc 自己的管理信息。

    对比两个构建的 -S 输出,就会发现那个调用不见了。改用 explicit_bzero 或者 C23 的 memset_explicit。

    从英语翻译查看原文
  2. 固定链接

    好吧,lathe 说得对,是我搞反了。GCC 文档写得很明确,不带 -O 选项就是 -O0。所以我那个“普通” target 从来就没有优化过,这两个 target 确实是同一份源码的两种不同构建。

    在 -O0 下,或者命令行上没有设置 -O 级别时,大多数优化都是完全关闭的

    https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html

    这样那 16 个字节也就不再是谜了。那是 glibc 的空闲链表指针,free() 往那里写本来就没有疑问。反正我在测试里已经跳过它们了。

    kiln,cppreference 那个页面说的并不是你以为的意思。memset 就是一个普通的函数调用,而 5.1.2.3 只要求保持抽象机的可观察行为。对一块已释放的内存做写入,中间又没有读取,这种写入是不可观察的,所以编译器可以把它去掉。“一定会执行”只是民间说法,不是标准的保证。

    两个构建的 -S 输出我还没看,动代码之前我会先把它们 diff 一下。之后就换成 explicit_bzero,等我的工具链支持 C23 了再换 memset_explicit。发帖前真该先查一下默认值的。

    从英语翻译查看原文
  3. 固定链接

    在换成 explicit_bzero 之前我想问一下,通过 volatile 函数指针调用 memset 的那个技巧,标准真的保证有效吗,还是只是编译器碰巧照顾了它?我真不想用一个民间说法换另一个民间说法。

    从英语翻译查看原文

同一个问题的其他角度

  1. -race 报了 DATA RACE,可 ack 明明是无缓冲接收,是 time.After 的锅吗?

    会不会是竞态检测器在这里判断错了,还是我对 ack 提供的保证理解有误?这是 pushgate,一个小服务,给我的移动应用校验推送通知用的设备令牌上的 HMAC 签名。调用方在无缓冲的 channel 上发送一个 Request,worker 填好…

    回复 4最近活动 go
  2. 仅有一个 webhook 的 HMAC 不匹配(名字里带重音符号的那个)

    我写了个小的 Node 检查器(纯 JS,不需要构建步骤),用来重放保存下来的 webhook 投递,并在下游信任它们之前校验 HMAC-SHA256。三个里有两个能过。来自 billing-partner 的 evt_2044 失败了,但同一个合作…

    回复 7最近活动 typescriptjavascript
  3. 同样的溢出检查,-O0 和 -O2 下走了不同的错误分支

    恶意构造的 pack,count=65536,rec_size=32768。-O0 下我得到 "pack too large",-O2 下却变成了 "out of memory"。源码完全一样,我真的想不通。是 -O2 把它编译错了,还是 mallo…

    回复 6最近活动 c