친구 초대가 어떤 사람에게만 안 됐다. 초대 링크를 눌러도 아무 일이 없었다.
Table of contents
Open Table of contents
안 되는 사람만 안 됐다
되는 사람과 안 되는 사람의 차이를 찾으니 안 되는 쪽은 닉네임이 길었다.
char url[128];
sprintf(url, "http://example.com/invite?uid=%d&nick=%s&code=%s", uid, nickname, inviteCode);
버퍼가 128자인데 닉네임이 길면 넘친다. 증상은 초대가 안 되는 것이고 원인은 길이였다. 두 가지가 이어지지 않아서 처음에 초대 로직만 들여다봤다.
넘친 뒤에 어떻게 되는지 봤다
sprintf 는 크기를 안 받는 함수라 넘치면 그 뒤 메모리에 쓴다. 죽지 않은 것이 오히려 이상했는데 그 뒤에 쓸 자리가 남아 있었을 뿐이었다. 나중에 다른 값이 깨졌다.
snprintf 로 바꾸니 이제 넘치면 잘린다. 죽지는 않는다.
잘려도 실패였다
잘려도 안 되기는 마찬가지였다. 뒤에 붙는 초대 코드가 잘려서 링크를 눌러도 코드가 없다.
잘린 것과 안 만들어진 것은 다르게 보이지만 결과가 같다. 그리고 잘린 쪽은 아무 말도 안 하므로 더 안 보인다. snprintf 가 돌려주는 것이 실제로 쓴 길이가 아니라 쓰려던 길이라는 것을 알고 나서 그 값으로 판정하게 했다.
int n = snprintf(url, sizeof(url), "http://...", ...);
if (n < 0 || (size_t)n >= sizeof(url)) {
CCLOG("[invite] URL 이 잘렸다. 필요 %d, 있는 자리 %zu", n, sizeof(url));
return false;
}
strncpy 로 바꾸려다 만 자리도 있었다. 크기를 받기는 하는데 꽉 차면 널을 안 붙여서 더 위험하다. Android 쪽 libc 에는 strlcpy 와 strlcat 이 있어서 이쪽은 항상 널을 붙이고 필요한 길이를 돌려준다.
최악의 길이 계산
쓰던 판의 CCString 에는 포맷 함수가 없어서 이런 조립은 C 문자열로 해야 했다. 버퍼를 크게 잡을 수도 있었는데 얼마면 되는지를 몰랐다. 세어 보니 닉네임 길이가 정해진 적이 없었다.
넣는 쪽에 제한을 걸면서 계산이 두 번 달라졌다. 한글은 UTF-8 로 한 글자가 세 바이트라 12자가 36바이트이고, 주소에 넣으려고 퍼센트 인코딩하면 한 글자가 아홉 바이트까지 늘어 108바이트가 된다. 넉넉히 잡는다는 말로는 안 되고 최악을 계산해야 했다.
같은 모양을 전부 찾으니 23곳이었다. 그중 넷만 실제로 넘칠 수 있는 자리였고 나머지 열아홉은 넣는 값이 짧아서 안 났을 뿐이었다. 안 난 것과 안 나는 것은 다르다.
정리
- 길이 제한에 걸려 잘리면 기능 실패로 나타난다
- 잘렸다는 말은 어디에도 안 나온다
sprintf는 넘쳐도 안 죽을 수 있고 나중에 다른 값이 깨진다snprintf는 잘림과 무관하게 쓰려던 길이를 돌려준다. 그 값으로 판정한다strncpy는 꽉 차면 널을 안 붙인다.strlcpy는 붙인다- 자리를 넉넉히 잡는 대신 들어가는 것을 세어 본다
- UTF-8 한글은 세 바이트이고 퍼센트 인코딩하면 아홉 바이트가 된다
- 안 난 자리는 값이 짧았을 뿐이다