C로 작은 credential holder를 만들고 있어요 (WSL, Neovim, 별거 없어요). vault_release()는 secret을 지운 다음 free하게 되어 있어요:
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는 secret 포인터를 저장해 두고, vault_release()를 호출한 다음, free된 블록을 읽어요. 네, UB인 거 알아요. 한 번 쓰고 버릴 테스트고, 저는 메모리에 뭐가 남아 있는지만 보고 싶어요. 앞의 16바이트는 glibc가 자기 bookkeeping을 거기에 쓰기 때문에 건너뛰어요.
그냥 make check를 하면 ok 줄이 나와요. 그다음 make release-check를 하면 clean하고 OPT=-O2로 다시 빌드해요:
release-check:
$(MAKE) clean
$(MAKE) check OPT=-O2
이러면 실패해요. FAIL 줄은 secret 바이트가 남아 있다고 하고, 직접 보면 앞의 16바이트만 덮어써져 있어요. 그 뒤로는 전부 원래 secret이 그대로 읽히는 상태로 남아 있어요.
소스도 같고 memset도 같아요. 기본 빌드는 제가 알기로는 이미 최적화된 상태예요, 그게 기본값이니까요. 그래서 -O2가 뭘 바꾸는지 모르겠어요. 그럼 어느 쪽일까요? glibc의 free()가 블록에 뭔가를 다시 쓰는 건가요, 아니면 제 -O2 타깃이 기본 빌드와 다른 걸 만드는 건가요? 생성된 어셈블리는 아직 안 봤어요.
"해제된 메모리를 읽지 마라"는 말씀을 하실 분들께: 알고 있어요. 묻는 질문에 답해 주세요.
영어에서 번역·원문 보기I'm building a small credential holder in C (WSL, Neovim, nothing fancy). 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 memset is right there, full length. I've read it three times. It's not subtle.
My test, tests/wipe_check.c, saves the secret pointer, calls vault_release(), then reads the freed block. Yes, that's UB. It's a throwaway test and I really only want to see what's left in memory. I skip the first 16 bytes because glibc puts its own bookkeeping there.
Plain make check prints the ok line. Then make release-check, which cleans and rebuilds with OPT=-O2:
release-check:
$(MAKE) clean
$(MAKE) check OPT=-O2
Now it fails. The FAIL line says secret bytes survived, and when I look, only the first 16 bytes are clobbered. Everything after that is still the original secret, sitting there readable.
Same source, same memset. The plain build was already optimized as far as I know, since that's the default, so I don't see what -O2 changes. So which is it? Is glibc's free() writing stuff back into the block, or is my -O2 target building something different from the plain one? I haven't looked at the generated assembly yet.
Before someone says "don't read freed memory": I know. Answer the actual question.