Пишу небольшой хранитель учётных данных на C (WSL, Neovim, ничего особенного). 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);
}
memset стоит прямо там, на всю длину. Я перечитал три раза. Тут нет ничего хитрого.
Мой тест, tests/wipe_check.c, сохраняет указатель на секрет, вызывает vault_release(), а потом читает освобождённый блок. Да, это UB. Тест одноразовый, и мне правда нужно только посмотреть, что осталось в памяти. Первые 16 байт я пропускаю, потому что glibc хранит там свою служебную информацию.
Обычный make check печатает строку ok. Потом make release-check, который делает clean и пересобирает с OPT=-O2:
release-check:
$(MAKE) clean
$(MAKE) check OPT=-O2
И теперь падает. Строка FAIL говорит, что байты секрета уцелели, и когда я смотрю, затёрты только первые 16 байт. Всё остальное так и лежит там исходным секретом, читаемое.
Тот же исходник, тот же memset. Обычная сборка, насколько я знаю, уже была оптимизированной, раз это значение по умолчанию, так что я не понимаю, что меняет -O2. Так что же из двух? free() в glibc что-то записывает обратно в блок, или моя цель с -O2 собирает не то же самое, что обычная? Сгенерированный ассемблер я пока не смотрел.
Прежде чем кто-нибудь скажет «не читайте освобождённую память»: я знаю. Ответьте на настоящий вопрос.