Skip to content
isdnetworks
Go back

글자가 잘려서 기능이 안 됐다

친구 초대가 어떤 사람에게만 안 됐다. 초대 링크를 눌러도 아무 일이 없었다.

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 에는 strlcpystrlcat 이 있어서 이쪽은 항상 널을 붙이고 필요한 길이를 돌려준다.

최악의 길이 계산

쓰던 판의 CCString 에는 포맷 함수가 없어서 이런 조립은 C 문자열로 해야 했다. 버퍼를 크게 잡을 수도 있었는데 얼마면 되는지를 몰랐다. 세어 보니 닉네임 길이가 정해진 적이 없었다.

넣는 쪽에 제한을 걸면서 계산이 두 번 달라졌다. 한글은 UTF-8 로 한 글자가 세 바이트라 12자가 36바이트이고, 주소에 넣으려고 퍼센트 인코딩하면 한 글자가 아홉 바이트까지 늘어 108바이트가 된다. 넉넉히 잡는다는 말로는 안 되고 최악을 계산해야 했다.

같은 모양을 전부 찾으니 23곳이었다. 그중 넷만 실제로 넘칠 수 있는 자리였고 나머지 열아홉은 넣는 값이 짧아서 안 났을 뿐이었다. 안 난 것과 안 나는 것은 다르다.

정리


Share this post on:

Previous Post
저장 파일이 깨지자 앱이 안 떴다
Next Post
목록이 전부를 불러오고 있었다